Skip to content

v1.7.0 — Particles & Progress

Choose a tag to compare

@avcode-exe avcode-exe released this 23 Jul 12:17
· 17 commits to main since this release

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 _autoTier when 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

  • SaveRotationManager with 3 auto-save slots (autosave.0autosave.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() in gamePersistence.js with 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 settingsDraft object; Save writes to
    Electron main process, Cancel reloads from disk
  • SETTINGS_FIELD_TYPES configuration in settingsDefaults.js drives the form
    rendering (sliders, toggles, selects, keybinds)
  • All collapsed-panel state persisted and restored

🐛 Bug Fixes

  • Sim tick crash recovery_runSimTick() now sets game._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
    call openFn twice (once from transitionend, once from the fallback
    setTimeout()). Added an idempotent opened guard flag.
  • Popup fallback timer leak — Moved the fallback setTimeout to an instance
    property (this._popupFallbackTimer) so it can be cleared on subsequent calls,
    preventing multiple openFn invocations when rapidly switching popups.
  • PopupManager fallback timer — Module-level _popupFallbackTimer prevents
    duplicate openTarget calls when popups are rapidly opened/closed from the
    bar buttons.
  • Save slot validationGame.loadFromSlot() now validates save data via
    SaveSerializer.isValid() before calling restore(), preventing corrupt or
    malformed saves (NaN, negative values, missing fields) from crashing the game.
  • Monster _hitTroops memory leak — Clear _hitTroops Set when a monster
    dies (.alive = false), freeing stale troop references and preventing
    unbounded Set growth.
  • Input bounding rect stale cacheInput._cachedRect is recalculated on
    every mousedown event instead of being cached indefinitely, ensuring
    accurate coordinates after window resize.
  • Drag-to-place _dragState cleanup_dragState is reset to null in
    restart() and restore() so stale drag state doesn't persist after reset.
  • Shield shop placement index — Fixed a duplicate SHIELD_SHOP_WIDTH in
    config that caused incorrect shield placement bounds.
  • Missing about key in electron-main settingsDEFAULT_SETTINGS.collapsed
    in electron-main.js was missing about: false; synced with settingsDefaults.js.
  • Dead code removal — Removed unused _compactMonsters() method from game.js
    and an unreachable early return in resolveDownloadTag.
  • Redundant if (m.alive) guard — Removed redundant alive check inside the
    if (m.hp <= 0) block in _stepMonsters().

⚡ Performance

  • Input bounding rect cachingInput class now caches
    canvas.getBoundingClientRect() and recalculates only on mousedown instead
    of every mousemove/wheel event, 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 _clearedTileScratch array reuses memory across compact cycles
    • _updateMonsterTileIndex rewritten from full clear-and-rebuild to
      incremental updates (tracks _prevTileIdx per monster)
    • Entity compaction in _cleanupDead() now tracks size changes to know when
      to rebuild the troop tile index
    • Cached DPS/HPS in troop.js via getDps()/getHps() reading from
      _cachedStats, computed once in _recomputeStats()
    • Cached stat lines in shop.js_buildStatLines() caches on the troop
      object, invalidated on upgrade
    • popupManager.js uses module-level _popupFallbackTimer to avoid
      per-call timer allocation
  • 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
  • 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, _meta field, 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, _meta block 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, icons
    • SETTINGS_FIELD_TYPES: all field type configs for audio/graphics/controls/
      accessibility (sliders, toggles, selects, keybinds)
    • PARTICLE_QUALITY_TIERS: pool/spawn/lifetime values, monotonic increase
    • DEFAULT_SETTINGS: structure and default values (Medium quality, 0.5 volume,
      Space/Enter/R/S/F keybinds)
    • COLLAPSED_KEYS and makeCollapsedDefaults()
  • 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 Game class
    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