v1.7.0 — Particles & Progress
v1.7.0 — Stable Release
This release upgrades Tower Defense from the 1.6.x beta track to a stable 1.7.0
release. It brings major quality-of-life improvements: a dynamic particle system
with auto-throttle, a multi-slot save rotation system, a fully reworked settings
panel, and numerous performance optimizations and bug fixes.
🎉 Features
Dynamic Particle System with Quality Tiers
- 4 quality tiers: Low (pool: 100, spawn: 0.3×, lifetime: 0.5×), Medium
(300, 0.6×, 0.75×), High (1000, 1.0×, 1.0×), Ultra (2000, 1.5×, 1.5×) - Configurable via
PARTICLES.setQuality()and adjustable from the Settings
panel (Graphics → Particle Quality) - Spawn multiplier scales particle count per effect; lifetime multiplier scales
how long particles remain visible - Pool trimming: switching to a lower tier immediately shrinks the particle pool
(_pool.length = newMaxPool) and caps active count to prevent memory spikes - Hardware-aware default pool size:
Math.min(CONFIG.PARTICLE_POOL_SIZE, Math.max(100, navigator.hardwareConcurrency × 50))
Auto-throttle (_checkFrameBudget)
- Runs every frame from the game simulation tick
- Downgrade: 3 consecutive frames >33ms (≤30 FPS) drops one quality tier
toward Low, cascading if performance remains poor - Upgrade: 60 consecutive frames <16ms (>60 FPS) moves one tier back toward
the user's chosen tier, then clears_autoTierwhen reached - Operates independently of
_userTier— never overwrites the user's preference - Normal frames (16–33ms) reset both counters to prevent false triggers
- Useful on low-end hardware or when the browser tab is backgrounded
Multi-Slot Save Rotation System
SaveRotationManagerwith 3 auto-save slots (autosave.0–autosave.2)
using LRU (Least Recently Used) eviction- Manual named save slots — enter any name, saved to disk
- Save/Load popup: accessible from the bar button, shows all save entries
with thumbnail preview, wave/gold/lives summary, timestamps - Overwrite confirmation dialog: warns when saving to an existing named slot;
auto-save slots show an Overwrite button instead - Auto-save debounced to every 5 waves (
AUTO_SAVE_DEBOUNCE_WAVES) to reduce
disk I/O - Save preview thumbnails: 200×150 JPEG captured from the game canvas
captureSavePreview()ingamePersistence.jswith graceful fallback when
the canvas is not available (e.g. Node.js environment)
Settings Panel Rework
- Tab-based layout: Audio | Graphics | Controls | Accessibility
| Update — each tab shows relevant controls - Audio: Master/SFX/Ambient/UI volume sliders with mute toggles
- Graphics: Particle Quality selector (Low/Medium/High/Ultra), Resolution
Scale slider (0.5×–2×), Screen Shake intensity (0%–100%) - Controls: Scroll/Zoom toggle, keybind capture with modifier key support
(e.g.Ctrl+R,Shift+P), visual feedback on capture - Accessibility: Colorblind Mode (high contrast) toggle, Reduced Motion toggle
- Update: Release channel radio buttons (Release / Pre-release), auto-download
checkbox, check interval (minutes), manual "Check Now" button - Draft-based editing: changes populate a
settingsDraftobject; Save writes to
Electron main process, Cancel reloads from disk SETTINGS_FIELD_TYPESconfiguration insettingsDefaults.jsdrives the form
rendering (sliders, toggles, selects, keybinds)- All collapsed-panel state persisted and restored
🐛 Bug Fixes
- Sim tick crash recovery —
_runSimTick()now setsgame._errorState = true,
stops the game loop, and shows "Game Error — Press R to restart" on the canvas
instead of white-screening. Pressing R re-enables the game. - Popup shortcut race condition —
_handlePopupShortcut()in game.js could
callopenFntwice (once fromtransitionend, once from the fallback
setTimeout()). Added an idempotentopenedguard flag. - Popup fallback timer leak — Moved the fallback
setTimeoutto an instance
property (this._popupFallbackTimer) so it can be cleared on subsequent calls,
preventing multipleopenFninvocations when rapidly switching popups. - PopupManager fallback timer — Module-level
_popupFallbackTimerprevents
duplicateopenTargetcalls when popups are rapidly opened/closed from the
bar buttons. - Save slot validation —
Game.loadFromSlot()now validates save data via
SaveSerializer.isValid()before callingrestore(), preventing corrupt or
malformed saves (NaN, negative values, missing fields) from crashing the game. - Monster
_hitTroopsmemory leak — Clear_hitTroopsSet when a monster
dies (.alive = false), freeing stale troop references and preventing
unbounded Set growth. - Input bounding rect stale cache —
Input._cachedRectis recalculated on
everymousedownevent instead of being cached indefinitely, ensuring
accurate coordinates after window resize. - Drag-to-place
_dragStatecleanup —_dragStateis reset tonullin
restart()andrestore()so stale drag state doesn't persist after reset. - Shield shop placement index — Fixed a duplicate
SHIELD_SHOP_WIDTHin
config that caused incorrect shield placement bounds. - Missing
aboutkey in electron-main settings —DEFAULT_SETTINGS.collapsed
inelectron-main.jswas missingabout: false; synced withsettingsDefaults.js. - Dead code removal — Removed unused
_compactMonsters()method from game.js
and an unreachable early return inresolveDownloadTag. - Redundant
if (m.alive)guard — Removed redundant alive check inside the
if (m.hp <= 0)block in_stepMonsters().
⚡ Performance
- Input bounding rect caching —
Inputclass now caches
canvas.getBoundingClientRect()and recalculates only onmousedowninstead
of everymousemove/wheelevent, reducing layout thrashing on high-DPI
displays. - 7 hot-path optimizations across the game loop, rendering pipeline, and
audio system:- Pre-allocated scratch arrays in
_buildTroopTileIndex(replaces per-call
new Set(), saves ~200 allocs/frame) - Pre-allocated
_clearedTileScratcharray reuses memory across compact cycles _updateMonsterTileIndexrewritten from full clear-and-rebuild to
incremental updates (tracks_prevTileIdxper monster)- Entity compaction in
_cleanupDead()now tracks size changes to know when
to rebuild the troop tile index - Cached DPS/HPS in
troop.jsviagetDps()/getHps()reading from
_cachedStats, computed once in_recomputeStats() - Cached stat lines in
shop.js—_buildStatLines()caches on the troop
object, invalidated on upgrade popupManager.jsuses module-level_popupFallbackTimerto avoid
per-call timer allocation
- Pre-allocated scratch arrays in
- Dead code cleanup: removed
_compactMonsters()(~20 lines) and unused
project file references. - Auto-save debounce:
_stepWaveCompletion()calls_autoSave()every 5
waves instead of every wave, reducing disk I/O during long sessions.
🧪 Testing & Quality
- 1,898 tests across 49 test files — all passing
- Dynamic Particle System tests (21 new tests):
- Quality tier system:
setQuality(),_applyTier()with pool/spawn/lifetime
values for all 4 tiers, invalid tier handling, pool trimming on downgrade - Auto-throttle:
_checkFrameBudget()— slow frame downgrade cascade,
fast frame upgrade cycle, boundary conditions (33ms/16ms),_autoTier
lifecycle, counter reset on normal frames _applyCfg()helper: config reuse, color override, gravity preservation_spawnEffect()helper: default color, overridden color, deterministic
spawn counts with tier multipliers
- Quality tier system:
- Save System tests (40+ tests in
persistence.test.js):SaveRotationManager:autoSaveSlots(),selectSlotForWrite()LRU
eviction (7 edge cases),makeMetaData()(wave/gold/lives extraction,
Infinity handling, missing wave defaults),extractMeta()(null input,
non-object input,_metafield, fallback to top-level),summarize()
(formatted output, partial data, timestamp inclusion)SaveMigrator:CURRENT_VERSION,_migrateV0()(defaults for missing
fields, null preservation, existing value preservation),
_migrateV1toV170()(troop field normalization,_metablock upgrade),
compareVersions()(major/minor/patch, pre-release vs release,
different-length segments, non-numeric segments)captureSavePreview(): null in Node.js, DOM canvas fallback
- Settings Panel Rework tests (17 tests, new file
settingsDefaults.test.js):SETTINGS_SECTIONS: 5 sections with correct IDs, labels, iconsSETTINGS_FIELD_TYPES: all field type configs for audio/graphics/controls/
accessibility (sliders, toggles, selects, keybinds)PARTICLE_QUALITY_TIERS: pool/spawn/lifetime values, monotonic increaseDEFAULT_SETTINGS: structure and default values (Medium quality, 0.5 volume,
Space/Enter/R/S/F keybinds)COLLAPSED_KEYSandmakeCollapsedDefaults()
- Code coverage: Statements 98.47%, Branches 92.15%, Lines 99.64%,
game.js lines at 100% - Lint: 0 errors, 0 warnings
- Format: All 80+ source and test files use consistent Prettier formatting
🔧 Configuration
- Version bump: 1.7.0-beta.2 → 1.7.0 (stable release)
- Magic numbers extracted to
config.js:MAX_SPAWNS_PER_FRAME,
HIT_TROOPS_CAP,PARTICLE_POOL_SIZE,DEV_MODE_CLICK_THRESHOLD,
DEV_MODE_CLICK_WINDOW_MS,WAVE_TRANSITION_DURATION,
AUTO_SAVE_DEBOUNCE_MS,AUTO_SAVE_DEBOUNCE_WAVES,
REVIVE_REWARD_HP_RATIO,POPUP_ANIM_MS
💾 Persistence
- Save migration pipeline (
SaveMigrator): versioned migration system
handling legacy saves (v0) with sensible defaults. Integrated into
GameSnapshotRestorer.apply()so all loaded saves run through migration. - Save error resilience: all save/load/delete operations in
Gameclass
are wrapped in try-catch with console warnings instead of silent failures - Save serializer validation:
SaveSerializer.isValid()checks seed type,
gold/lives ranges, wave structure, troop fields (shield bounds, healTargetLevel
integer check, healGoldSpent finite check), dev-mode constraints
📦 Build & Release
- Electron 42 with context isolation
- electron-builder 26: NSIS installer with configurable install directory,
desktop shortcut, start menu entry - electron-updater 6.6.1: automatic update detection from GitHub Releases
- GitHub Actions CI: ubuntu + windows, Node 20 + 22, enforcing lint +
format + coverage thresholds - Download: Tower-Defense-Setup-1.7.0.exe
Full changelog: CHANGELOG.md