Emberveil — Wishlist

AI-maintained tracker of planned, in-progress, and abandoned features. Checkboxes are read-only — Claude updates them as work lands.

How this works. Items start as todo. When work begins, they become wip. When shipped, they flip to done with the milestone tag. When a direction is abandoned, they flip to dropped with a strikethrough. Claude reviews this list at each milestone and surfaces items that have aged out or drifted.
Todo In progress Done Dropped / Abandoned

Continuous Release Run — M331 → M345 (2026-04)

Autonomous milestone run kicked off 2026-04-27, completed 2026-04-28. All items below shipped under separate milestones; no silent shelving. Generated 2026-04-28.

M331 → M345

Class catalog pagedone — m331/m332
/assets/class-catalog.html — 28 classes, 7-pose sprite strip, lightbox, select+copy, display modes (all/basic/images/attributes), per-class unlock requirements; data-source.js + classes.json (now enriched with starter + unlockRequirement).
Google Analytics + GDPR consent + privacy + UX-audit dev tooldone — m330/m333/m334
consent.js banner, hostname-based GA id (G-XP7B7KVNY8 demos / G-5H26ZXKBCL Emberveil), telemetry_events Supabase mirror, privacy.html, ux-audit.html. Burst-fix in cloudSaves (~184 reqs → 2).
Codex screen mobile menudone — m328
Native <select> replaces horizontal scroll-tab bar below 600px; list/detail stack vertically.
Map + dungeon — FTL grid rewrite, dungeon return-path bugfixdone — m338
_layoutZoneToFTLGrid: cap branching at 3, boss anchored COLS-1, thru-trunk centred, one-offs at parent column ±1 row. Dungeon node give-up resolved via parent-anchor lookup; visible error fallback when DungeonScreen import fails.
Difficulty picker reorg + Hire estimate paritydone — m339
Both new-game and Change Difficulty: 3 icon+name buttons + dynamic paragraph below; Hard shows MF/XP chips. Hire Custom Estimated Stats reuses CharacterBuilder .preview-stat layout.
Magic shield system (Barrier + Barrier Regen)done — m340
Four cloth-tier off-hand bases (warded_focus / rune_aegis / spellguard_orb / arcane_bulwark) with native barrier + regen; new Wardstone / Conduit affixes (magic-shield only). Combat seeds persistent fromMagicShield barrier status; per-round regen up to maxPower.
Stun overhauldone — m341
New Dazed status (-25 hit pts). AoE stuns converted: Tinker Barrage of Debris + Junk Avalanche, Demon Hunter Absolute Vengeance ult. Single-target stuns kept. Two extended-stun talents reworked (Thorn Wall, Bulwark Strike).
XP cap + Riposte parry counter wiringdone — m342
awardXp caps to ~1.5 levels per fight (defensive against the level-7-at-prologue overflow). Riposte parry counter properly consumes parryStance, deals damageMult*weapon dmg back, logs to combat report + meter.
Skill check / dialog / shrine readabilitydone — m343
Skill-check modal previews Pass/Fail before attempting + Auto-Pass when stat ≥ DC. Disabled dialog choices show Requires: line. Shrines show Effect: block stating exactly what they do.
NPC dialog portraitsdone — m344
DialogScreen renders sprite portraits via NAME_TO_SPRITE map + slug fallback + initials placeholder. Hand-curated map for prologue + Act 1 named NPCs.
Tools menu archivedone — m345
Data Catalogs (Legacy), Data Overrides, AI Content Generator removed from the visible Tools list (HTML pages preserved on disk for direct linking). Shared nav-header.js trimmed to match.
Empty Thornwood node investigationverified — m416
Static-map audit (M416): no Thornwood node has 5 exits — goblin_camp has 3 outgoing + 3 incoming (visually 6-edged but not "empty"), forest_enter has 2 exits, all other nodes have ≤3. Branch cap from M338 + edge-uncrossing pass at the bottom of mapData.js are working. If the user's report referred to a runtime-injected dialog ghost-node, that path was retired in M236; no new reports since. Re-open with a save-state if it recurs.
Tab layout consistency (Inventory/Spells/Passives/Attributes)done — m312/m313
Class-name + Auto button top-left landed in M312, checkbox indicator + green-bg variant in M313. Skill-header bar inside PartyPanel hidden via .pps-tab-body .skill-header. Verified M412 — _renderRecommendBar is now orphaned/unused.
Craft menu revert to "Strength Weapon (Random)"partial — m416
M416 shipped two of three: gold-cost glyph (⛀) on the materials line and a "Craft (N)" count badge on the button so the player sees how many of each recipe they can make right now. Still open: consolidate the per-slot recipe rows into stat-grouped randoms ("Magic Strength Weapon (Random)"), which requires defining a slot→stat mapping and an item-pool randomizer for each rarity.
Fallen hero stale-entry syncdone — m412
SaveManager.deleteKey now prunes any fallenHeroes entry whose savedSlot matches the deleted save key. Both Load-Game and the title-screen monument paths flow through deleteKey, keeping the lists in sync. src/engine/SaveManager.js:104.
Milestone-jargon glossarydone — m413
Glossary at public/docs/milestone-jargon.md covers the 10 most opaque labels: slot-mult, Meter, Ev/EV/ev-overlay, Manual mode, Hardcore/RIP, Companion level sync, Auto-equip/build/spend, Tap weapon/utility, Fame, Veil/veilspawn. Linked from Canonical Docs in CLAUDE.md.

Approved Brainstorm — Milestone Plan (M293–M309)

93 items (74 from brainstorm.html + 19 manual additions). Grouped by target milestone. Small UI/polish first, then accessibility, then devtools/sims, then gameplay/content. Approved 2026-04-26.

M293 — Small UI batch 1

Notification bubble disambiguationdone — m293
Inventory tab gets bubble for unequipped/upgrades; map Town button shows level-up points only. Manual #1: after level-up in combat the unspent-skill-points bubble persists until visiting Character screen — clear it as soon as auto-allocation has run, or on first map render after combat ends.
Quick-equip toast on item pickupdone — m293
"Auto-equipped X to Y" or "X is an upgrade for Z — equip?" when loot drops, so the player doesn't have to open inventory after every fight.
Combat preview tooltip on skill upgrade selectiondone — m293
"This upgrade adds N% damage / changes targeting to AoE" — contextual hint when picking talents.
Scroll-position memory on InventoryScreen tab switchdone — m293
Preserve scroll depth when jumping between characters.
Status effect duration tooltipsdone — m293
Show duration remaining (rounds), not just type. Players can't tell when burns / blocks expire without counting rounds.
Party screen role iconsdone — m293
Tank / DPS / Support / Healer indicators. Manual #3: NO emoji — use SVG / Font Awesome / continue the existing class-icon style. User likes the current generated class icons.

M294 — UI batch 2: Party panel + boss splash + zone dropdown

Unified Spells / Passives / Attributes / Inventory tabbed panel ("Party")done — m294
Reduce screen count; quick item swaps during skill-point allocation flow. Manual #5: Replace existing Character screen but mark deprecated rather than deleting — user may want to refer back. Keep file in repo with a `// DEPRECATED M294` header and unwire from the menu.
Rename map "Inventory" button to "Party"done — m294
Opens unified Party + Inventory panel. Clearer intent; collapses the Inventory + Town notification-bubble overlap.
MapScreen zone fast-travel as dropdowndone — m294
Reduce horizontal width on phones once 6+ zones are unlocked. Replace tabs with dropdown.
Boss-intro splash card before boss combatdone — m294
Sells the moment; differentiates from regular fights. Manual #4: long boss names (e.g. Unraveler-of-Mortal-Threads) get cut off on iPhone — splash must wrap or scale-down on narrow viewports. Audit existing combat headers for the same overflow bug.
Dual-comparison mode in Inventory (rings / offhand)done — m294
Compare both rings or offhand at once for better multi-slot planning. Manual #2: desktop only — iPhone won't fit, gracefully fall back to single-compare on narrow viewports.

M295 — Polish: enchanter, potion belt, toasts, plain-language status

Enchanter upgrade preview (before/after stats on hover)done — m295
Half-done per CLAUDE.md; finalize the tooltip.
Potion belt (combat-quick consumables)done — m295
Separate from inventory; QoL for healing flow.
Achievement-unlock toast notificationsdone — m295
Celebration moment; currently silent unlock.
"Patch notes" shortcut in GameMenudone — m295
Link to latest release summary (in-game splash) with a "full history" link to /assets/changelog.html. Discoverable without leaving the game.
Plain-language status descriptionsdone — m295
Cognitive accessibility; avoid jargon. "Bleeding (3 rounds): loses 8 HP per round" not "Bleed: DoT".
Boss-specific dialogue preludes (2-3 lines)done — m295
Sells stakes; cheap content. Could ship in M295 or M307 with cinematic-dialog.

M296 — Accessibility batch

Colorblind palettes (deuteranopia / protanopia / tritanopia)done — m296
Rarity colors today are red/blue/yellow/orange — collide for ~8% of players.
Font-size scaling (Settings → 80%/100%/120%/150%)done — m296
Fixed 1rem today; mobile readability win.
Tap-target validation (≥ 44×44px per WCAG)done — m296
Many tooltip-trigger areas likely undersized on mobile. Audit + fix pass.
Reduce-motion toggle (kill parallax, screen wipes)done — m296
Already partial; expand coverage. Manual #14: ensure surfaced in Settings menu — user's OS has reduce-motion enabled and may not know how to override per-app, so the toggle must be in-game-discoverable.
Subtitle / caption mode for combat logdone — m296
Screen-reader friendly real-time combat narration.

M297 — Keyboard navigation

Keyboard navigation across screens (arrow + enter)done — m297
Full Tier A coverage: TitleScreen, CharacterBuilderScreen, LoadGameScreen, SettingsScreen, TownScreen, MapScreen, PartyPanelScreen, DialogScreen, LevelUpScreen, AchievementsScreen, StatsScreen, StatsDashboardScreen, GameMenuScreen. Gold :focus-visible ring site-wide. Tier B (combat): digit keys 1–N for manual-combat action picker; auto-combat has no action buttons so target selection remains mouse/touch-only. Combat keyboard nav intentionally limited to digit shortcuts — adding full arrow-key target selection would require significant combat refactor and is out of scope per user guidance ("do not sacrifice existing UX").

M298 — Changelog flow + lore + roadmap + monument

Changelog categorizationdone — m298
Bug Fix / Feature / Balance / QoL / Art icons. Flat lists are hard to scan.
In-game "What's New?" splash on first launch after updatedone — m298
Engage players with patch notes; currently external only.
Per-feature "shipped in M###" badgedone — m298
Visible at the feature itself. Celebrate; drive engagement.
Public roadmap pagedone — m298
Upcoming acts + major features. Manage expectations; hype building. Pulls from this wishlist's milestone groups.
In-game lore compendium (Claude-authored)done — m298
Help follow the 5-act narrative. Manual #19: Claude writes the lore — this is Claude's game; user is one of the players.
Hardcore monument on title screendone — m298
Display fallen heroes. Manual #13: only render if a hardcore character has fallen. Manual #20: hardcore saves remain loadable as disabled slots so character stats can be compared across playthroughs (Character A from previous run vs Character B from new run). Option to delete the slot.

M299 — Devtool catalogs + tools landing

Companion catalog page (sortable: name, class, stats, skills)done — m299
companion-catalog.html — pets, hireables, wild companions, dragon companions. Sortable table, search, filter by kind/class.
Dungeon catalog pagedone — m299
dungeon-catalog.html — card grid with location, stages, mini-boss, rewards. Click to expand details.
Status effect catalog pagedone — m299
status-effect-catalog.html — glyph, color, name, plain description for all STATUS_META entries. Grouped by category (DoT / Control / Buff / Defensive / Debuff / Link).
Achievement roadmap pagedone — m299
achievement-roadmap.html — all achievements grouped by tier (bronze/silver/gold), with unlock conditions. Filter by tier and search.
Tools-menu landing page (curated)done — m299
tools.html — curated table of all 30+ tools with tag filters (catalog / devtool / admin / sim / docs), search, and coming-soon rows for M300/M301.
Live-data + client-side override system across all toolsdone — m299
data-source.js ESM module + emit-game-data.cjs prebuild script. data-overrides.html management UI. All M299 catalog pages use live data. Milestone snapshot scaffold added (M309 planned).
wishlist.html: group by milestone targetdone — m292
Done as part of this M292 migration — this section is the result.

M300 — Devtool batch 2: Sims, audits, archives

Skill interaction simulator (skill A + skill B combo)done — m300
skill-sim.html — two skill pickers from getSkills(), side-by-side effect panels, combo heuristic (stacking/consume/DPS/neutral), and "Run 100 Combats" in-browser sim.
Affix interaction matrix pagedone — m300
affix-matrix.html — visual grid from getAffixes(), heuristic interaction types (stack/conflict/multiply/independent), color-coded, click-to-detail modal, pool filter.
Spell catalog: side-by-side skill comparisondone — m300
spell-catalog.html — added Compare mode button; pick up to 4 skills, click "Show Comparison" for a horizontal stats table (cooldown, MP, damage type, talents, upgrades).
Asset-pipeline trace tooldone — m300
asset-pipeline-trace.html — static knowledge map of 13 scripts (release.sh, optimize_audio, emit-game-data, build-manifest, clean-manifest, etc.) with inputs/outputs/trigger. Search + trigger filter.
image-review-manifest deprecation passdone — m300
scripts/clean-image-review-manifest.cjs — removes missing-file entries and duplicates. Ran: 917 → 782 entries, 1655.7 KB → 1622.3 KB (135 stale removed). Deprecation notice added to image-review.html.
character-redesign archivedone — m300
approve.cjs now archives displaced frames to public/assets/character-redesign-archive/<id>/<timestamp>/ before overwriting. redesign-archive.html lists all archived versions with thumbnails + dates. Forward-only (no backfill — noted in wishlist).
Rebalance report diff viewdone — m300
rebalance-diff.html — select two snapshots from rebalance_snapshots/index.json, renders color-coded diff table (green=buff, red=nerf) by hero/companion/tap/utility. Summary KPIs (buffed/nerfed count). Linked from rebalance-report.html.

M301 — Headless simulator + regression infra

Headless combat simulator APIstatus-done — m301
Spawn combatants, run N rounds, return stats. Currently Monte Carlo lives in scripts; expose as a clean Node API for Claude to call.
Batch rebalance reports (parallel + CSV)status-done — m301
Test 50 skill tweaks in parallel, compare win-rates. Currently serial.
Regression alerts on rebalancestatus-done — m301
Alert if Act win-rate drops > threshold. Manual #15: per-act configurable thresholds (e.g. Act 5 25% floor vs Act 1 10%). Config file: scripts/regression-thresholds.json, hand-editable.
Encounter difficulty ratingstatus-done — m301
Predicted win-rate, flag too-hard fights. Uses simulator.
Item-generation validatorstatus-done — m301
Every affix rolls within bounds. Catch balance bugs early.
Affix coverage reportstatus-done — m301
Which affixes never appear in Act 1-2 loot. Identify dead content.
Save-file load-tester (auto-snapshot per milestone)status-done — m301
Load 100 random saves, check for crashes. Manual #17: auto-save a game per act after each milestone build under assets/references/emberveil/saves/regression/M###/ for regression testing.
Loot distribution auditstatus-done — m301
Drop chance per act; flag rare over-drop. Manual #18: include merchant, guild hall, black market shop pools — they're part of the economy too.
Combat-log replayer (export-only)status-done — m301
Save fight trace JSON, replay visually on demand. Manual #13: no repo bloat — keep traces in localStorage with manual export button. Loader takes pasted JSON or file upload.
Status-effect interaction matrixstatus-done — m301
All 2-effect combos, computed outcomes. Identify unintended synergies.
Automated end-to-end playthroughstatus-done — m301
Seeded RNG, full Act 1-5 run, softlock detection. Smoke test after balance changes.

M302 — Gameplay batch 1: Combat additions + new node types

Companion level-sync optiondone — m302
Scale companion to party avg. Manual #5: party average level should also be displayed on save game slots so you can quickly compare playthroughs.
Flee from combat (DEX check; failure costs gold)done — m302
Recovery option early-act instead of auto-wipe.
Steal-buff mechanic (Rogue)done — m302
Pull enemy buffs to self. New effect type; high-skill play.
Bleed-stack burst finisher skilldone — m302
Make bleed-focused builds rewarding beyond passive DoT.
Curse system (negative buff that spreads on death)done — m302
Differentiate curses from poison/burn; Warlock identity.
Skill-check node (single attribute roll)done — m302
Success/fail branches. Rewards stat investment.
Travelling merchant + minigame node-overlay systemdone — m302
Travelling merchant stops on the path; one-time stock. Manual #9: generalize to a node-overlay system — random encounters can overlay an existing node OR appear between nodes. If overlaying, the node-click triggers the overlay first, THEN starts the underlying node encounter (do NOT let the user skip the underlying node — known bug on Node 1 of new maps). Includes positive (merchant), neutral (memory minigame), and hostile (bandit patrol) overlays. Ties into quest system.
DoT stacking modes per-spelldone — m302
Manual #6: Bleed, poison, and other DoTs each declare a stacking mode: per-source (each character independently up to N stacks — e.g. 2 rogues × 5 = 10), per-character (one stack per source), or global (one stack from any source). Tooltips read e.g. "Poison Dagger: applies up to 5 stacks of poison on target." Pick whichever mode best suits each spell.

M303 — Enemy spells + champion modifiers

Enemy spellsdone — m303
Manual #7: add spells to selected enemies — basic monsters (esp. goblins) keep auto-attack. Reusable element-keyed FX assets (fire/ice/shadow/holy/nature) with monster-class tinting; fall back gracefully when art is missing. Bosses get wind-up spells interruptible by sufficient damage or dispel/spell-steal. Some boss buffs are stealable and can flip the tide of combat.
Blue-name champion modifiersdone — m303
Manual #8: Diablo-2-style champion prefixes — health regen, damage aura, fast, extra strong, etc. No new artwork; pure mechanical layer. Defer the "giant fiery mini-boss" idea to challenge tiles in a later milestone.

M304 — Multi-phase bosses + boss loot + hidden bosses + dialogue

Multi-phase boss fightsstatus-done — m304
HP checkpoints unlock new attack patterns. Generalize the Unraveler pattern to 3-5 other final bosses.
Boss-themed loot tablesstatus-done — m304
e.g. Unraveler always drops void-themed items. Memorable rewards for memorable fights.
Hidden boss encountersstatus-done — m304
Gated by rare-loot-finds or stat thresholds. Easter eggs for completionists / speedrunners.
8 new dialogue branches per actstatus-done — m304
Gated on party comp / past choices. Replay value; story is mostly linear today.

M305 — Set items + uniques + 40 affixes

Set items (24 sets total)done — m305
Manual #10 (doubled per user): 12 low-tier (2-piece — Diablo-2 Sigon-style with powerful low-level modifiers), 8 mid-tier (3-piece), 4 endgame (4–5 piece). Sets have legendary effects on top of stat bonuses (e.g. Mage set: Magic Missile fires bonus AoE projectile at second target). Pre-determined effects, semi-random affix values per template (Sigon Shield always +5 STR but other affixes random).
Unique items (~24 across 5 acts)done — m305
Rare items with a legendary effect (rarely multiple). Pre-determined effects; affixes may be pre-determined or random per template.
40 new affixes (elemental combos, conditional triggers, set bonuses)done — m305
Deepen itemization beyond the current ~30 affixes. Overlaps with sets — design together.

M306 — Infinite scaling dungeon

Procedural challenge dungeon (infinite scaling)done — m306
Manual #11: separate from in-game dungeons. Diablo-3/4 / PoE-delve style infinite scaling tiers. Random Act 1-5 enemies + twists. Replayable post-game content for build-testing.

M307 — Crafting + boss preludes + cinematic dialog

Craftable unique items (recipe unlocks on boss kill)done — m307
Bridge merchant/blacksmith/enchanter workflows. 5 per act.
Crafting recipe unlocks via lore-node explorationdone — m307
Make Blacksmith/Enchanter discovery dynamic instead of static stock. Manual #12: all basic/white recipes should unlock easily — gating is for higher tiers.
Boss-defeat cinematic dialogdone — m307
Capture story beat, not just combat. Leverage story; minimal boss dialog today.

M308 — Number rounding + fame-locked unlocks

Number rounding auditdone — m308
Manual #16: floor int stats (HP/MP/STR/DEX/INT/WIS/CON/CHA, armor, damage); display floats with up to 2 decimals (crit chance shows 1.15%, not 13.000000001%). Apply across DPS meter + character stats. Multipliers, dodge %, resistances stay float; consider per-stat formatting helper.
Fame-locked appearance / weapon variety unlocksdone — m308
Long-term carrot; fame is invisible today. Fame counter in CharacterBuilder + TitleScreen badge. Appearances beyond slot 5 gate behind fame thresholds (100/250/500/1000/2000). Locked tiles greyed out with fame target tooltip.

M309 — News flow + asset archive + release pipeline

public/news/ folder + Latest News widgetdone — m313
public/news/*.md with YAML frontmatter; emit-news-index.cjs prebuild script writes index.json; Latest News widget on asset gallery (top 3 cards); news-archive.html with category filter. M302/M305/M306 starter entries added.
Versioned asset archive (snapshot per milestone)done — m313
snapshot-assets.cjs wired into release.sh after game_meta.json update; idempotent (skips existing). data-overrides.html now probes snapshot manifests and presents working Restore from Snapshot M### dropdown with per-type and restore-all modes.
release.sh prompt for changelog category tagsdone — m313
RELEASE_CATEGORY env var (feature/balance/bugfix/qol/art/infrastructure, comma-separated). Interactive prompt if unset and TTY; default to feature in non-interactive mode. Persisted to releases[N].categories in game_meta.json. parse-changelog-categories.cjs merges explicit tags into structured output.
Backward-compat notes on releasedone — m313
BREAKING_CHANGES env var (pipe-delimited) persisted to releases[N].breakingChanges. emit-release-summary.cjs includes field. What's New splash renders Migration Notes section when non-empty. Retroactive notes added for M302/M305/M306/M307. Documented in public/docs/no-silent-shelving.md.

M314 — Critical UI Bugs (post-M309 feedback)

.dg-screen doesn't go away after dungeon completeddone — m314
DungeonScreen had onLeave() but ScreenManager only calls onExit()/destroy(). Added onExit and destroy aliases that call removeEl(this._el).
.combat-screen becomes 0px tall mid-combatdone — m314
Added height:100%; min-height:100% to .combat-screen CSS rule in COMBAT_STYLES.
Victory screen overflows when many rewardsdone — m314
Added max-height:90vh; overflow-y:auto to .cem-box so the victory modal scrolls instead of clipping.
Boss treasure drops 3 items — should be PICK 1 of 3done — m314
Converted MapScreen._showBossChestModal from "Take All" to a pick-one UI: 3 buttons side-by-side, one click selects + closes. Only rarity colors on borders (no class-suggestion green). mm-box gets max-height/overflow-y.
Boss treasure pick-dialog borders should be rarity, not class suggestiondone — m314
Pick-one modal uses RAR_COLOR/RAR_BG maps keyed by rarity. No character-suggestion overlay rendered.
Achievement screen during combat doesn't pause; victory pops up over itdone — m314
Added onPause/onResume to CombatScreen. onPause saves and zeroes _speedMult. onResume restores it and drains _deferredVictory. _showVictoryModal defers if CombatScreen is not the stack top.
Boss intro splash shows old intro text in background; no auto-dismissdone — m314
Removed 2s auto-dismiss timer from _showBossIntroSplash. HUD, potion belt, and log panel are hidden (visibility:hidden) while splash is up and restored on dismiss.
500-fame guildmaster popup appears after quitting to menudone — m314
TownScreen stores _fameEncounterModal reference. onExit/destroy removes it. Prevents modal from persisting over TitleScreen after GameMenuScreen._goToTitle() clears the stack.
Combat report "TOTAL HEAL 0.38999..." — too many digitsdone — m314
Applied formatStat(...,'int') to totalDmg, totalHeal, totalMit, avgDps, per-row totals, source totals, and individual hit amounts in _buildCombatReportHtml.
Replace big E toggle button with Combat Settings pause menudone — m314
Added "Combat Settings" button to pause menu. Sub-panel: show/hide DPS Meter (with Show Enemies sub-option), show/hide Combat Log (with Show Secondary Effects sub-option), Combat Captions toggle. Redirect note added in main Settings.
Combat captions overlap bottom HP bardone — m314
Changed .cbt-captions bottom from 0 to 80px so captions sit above the .cbt-hud bar.

M315 — Map / fog-of-war fixes

Back-arrow popup covers map nodes — replace with floating DOM box below player nodedone — m315
Replaced canvas edge-label with a DOM .czb-back-popup div positioned below (or above/side) the anchor node. Viewport-aware: tries below first, falls back to above, then clamps. OK button triggers _crossZoneJump. MapScreen.js _updateCrossZoneBackPopup().
Back arrow from Map 2 first node puts player at random node, not boss node where Map 1 endeddone — m315
_getCrossZoneLink now calls GameState.getZoneNode(prev.id) to look up the last-visited node in the previous zone, falling back to zone.nodes[last] only if no saved position exists. MapScreen.js line ~114.
Hidden (fogged) map nodes still hoverable to reveal type/positiondone — m315
_onHover now computes the fog revealed-set and skips any node not in it, preventing tooltip + visual hover on fogged tiles. MapScreen.js _onHover().
Fog-of-war should be active by defaultdone — m315
CharacterBuilderScreen._fogOfWar changed from false to true (new game default). gameState.js load migration changed from !!saved.fogOfWar to saved.fogOfWar !== false so old saves without the field get fog-on rather than fog-off.

M315 (label) — Balance Analysis + Rebalance Pass

Deep combat-sim analysis + enemy stat rebalance (item 44)status-done — m315
Full analysis documented at public/docs/balance-analysis-M315.md. Root causes: (1) sim mapped act=N to ng=N-1 (act5 → 410x HP — wrong); (2) classId not lowercased so skills never loaded in sim; (3) Act 5 enemies had 400-800 HP vs party DPS of 500+/round. Fixes: global enemy damage mult 1.0→1.3; Act 5 HP ×2 + dmg ×1.6; Act 4 HP ×1.5 + dmg ×1.4; Act 3 HP ×1.5 + dmg ×1.4; Act 1–2 HP ×1.3. Reality Shard stun 20%→10%. Backstab Assassinate 3.20→2.50 damageMult. Sim act-ng fix + class lowercase fix. FormulaCodexScreen: Combat Balance Targets section added to Advanced Systems tab.

M322 (label) — XP Catch-Up + Achievement Persistence

#42 XP catch-up mechanic for under-leveled charactersdone — m322
awardXp() now computes partyAvgLevel and applies catchUpMultiplier(memberLevel, partyAvgLevel) = min(3, 1 + 0.5 * gap). 1 below = 1.5x, 2 below = 2x, 4+ below = 3x cap. Victory modal shows "(N.Nx catch-up)" label next to level-up messages when multiplier applied. FormulaCodexScreen Advanced Systems tab documents the formula. src/game/xp.js catchUpMultiplier().
#45 Achievement persistence across runs (no re-toast on new game)done — m322
GameState.init() and GameState.load() now call _buildAchievementsSync() which seeds achievements from emberveil_life_achievements_v1 localStorage. checkAchievements() skips already-unlocked ids so no re-toast fires on new game. AchievementsScreen reads the global store directly for account-wide display. SettingsScreen adds "Reset Achievements" button with ConfirmModal. Old saves migrate automatically on first load. src/game/gameState.js _buildAchievementsSync(); src/game/achievements.js buildInitialAchievements(), resetGlobalAchievements().

Future Claude — read this before starting work

Workflow expectations baked into this batch:

Check this section at every session startworkflow
CLAUDE.md routes you here. Find the next pending milestone, flip its items to wip, ship them, flip to done with the actual milestone-tag (use the milestone number that ships, not the planned one — e.g. if M293 slips to M294, update tags accordingly).
Maintain milestone groupings across the wishlistworkflow
All future approved batches go in milestone-grouped sections like this one. Do not revert to flat lists.
No silent shelvingworkflow
If an item can't ship in its target milestone, surface it explicitly in the response and flip status to dropped (with reason) or shift it to a later milestone with a note. Never let it disappear.

Mod System (M66 — in development)

Extensibility layer: JSON schemas + effect DSL that let anyone describe a character/spell/item/event and drop it into the game.

Schemas & Foundation

v1 schemas under /schemas/v1/done — m128
pack, skill, effect-dsl, class, item, event-chain, appearance, character, loot. Downloadable from Custom Content page.
Effect DSL (~14 ops)done — m128
15 ops shipped in src/mods/dsl.js: damage, heal, applyStatus, buff, setFlag, requireFlag, consumeStatus, resourceSwap, echo, ritual, modifyTurnOrder, onHit, onCrit, onKill, onEvent.
Event chain flag ledgerdone — m133
GameState.hasFlag/incrementFlag/consumeFlag/requireFlags helpers + matching DSL ops (incrementFlag, consumeFlag, requireFlags). Unit tests in src/game/__tests__/flag_ledger.test.js.
JSON-Schema + warn-mode normalizationdone — m142
Canonical stat naming (str/dex/int/con). normalizeStatsDeep walks startingStats, primaryAttr, and effect.stat at pack load; logs shim telemetry on rename.
Custom content page /assets/custom-content.htmldone — m128
4 tabs: Upload/Paste, Schemas, Example Packs, Loaded. Inline registry so it works in built site.
Example mods + downloadable schemasdone — m128
starter-pack.json covers 24 skills + 5 event chains + appearance + 2 loot tables. All 9 schemas downloadable.
Upload packs (array of entities or single-object)done — m128
registry.js infers kind from shape when id/version wrapper is absent.

Supporting Mechanics (unlock mod authorship)

Unify status modeldone — m169
All 7 families unified on actor.statuses[]: burn, poison, bleed, stun, barrier (M166), dmgBuff (M168), critBonus temp-buffs (M169 — addCritBonusStatus/getCritBonusTotal; static talent/gear baseline still lives on actor.critBonus, temp combat buffs now stack as status entries). CombatScreen + simulator + tests all migrated; skillVerify stays on legacy snapshot path (static harness, not runtime).
Per-skill cooldown map (M131)done
actor.skillCooldowns[skillId] replaces single skillCooldown slot in CombatScreen + simulator. Enables 4+ active skills on independent timers.
Seeded RNG exposeddone — m128
src/mods/seededRng.js (LCG). Available to DSL via ctx.rng.
Shim-hit telemetrydone — m128
src/mods/telemetry.js exposes window.__emberveilShimTelemetry.snapshot().
Combat sim diff view vs. nearest vanilladone — m150
CombatSimulatorScreen._computeVanillaDiff runs a parallel sim with runSimulation({vanillaOnly:true}); displays ΔDPS, vanilla DPS, and list of mod skill ids in the stats panel when the party has any active mod skills. Simulator gained `vanillaOnly` flag (src/game/simulator.js) honored by heroToCombatant.
Overheal → barrierdone — m128
src/mods/statusModel.js → overhealToBarrier(). Wired in DSL heal-path via status[] model. CombatScreen full migration still wishlist-tracked.
Loot specifierdone — m128
src/mods/loot.js resolves deterministic list OR rolled {types,bases,quality,rarity,count,seed}. Schema loot.json. Example in starter-pack.

Spells (24 — mechanically distinct)

Active reflect (next hit on ally returns 200%)done — m150
arch_active_reflect in public/mods/examples/archetype-spells.json — applyStatus reflective + onEvent('onDamaged') return-damage. Status-type 'reflective' needs CombatScreen wiring to emit onDamaged to fully activate at runtime (tracked under CombatScreen → statusModel migration).
Damage-on-dodge triggerdone — m150
arch_damage_on_dodge — onEvent('onDodge') + damage. Pack JSON ships; runtime emits onDodge when CombatScreen migrates dodge to the trigger bus (follow-up).
Resource swap (HP↔MP)done — m150
arch_resource_swap_exchange — uses existing resourceSwap DSL op (already runtime-supported).
Delayed payload (detonate in N rounds)done — m150
arch_delayed_payload — ritual op with thenEffects. Runtime already processes _rituals in simulator; CombatScreen hook follows statusModel migration.
Chain-link buffs (stack rewards stacking)done — m150
arch_chain_link_buffs — incrementFlag + requireFlags gates additional heal, stacking buffs across casts.
Dispel + steal buffdone — m172
New stealStatus DSL op (src/mods/dsl.js) moves one/many statuses from fromto, preserving duration/stacks/amount. Supports single type, array of types, or prefix match (e.g. "buff:"); optional max cap. Demo skill arch_dispel_steal (Thief's Whisper) strips all buff:* from target onto caster. Tests: src/game/__tests__/steal_status_m172.test.js.
Persistent AoE auradone — m150
arch_persistent_aura — long-duration applyStatus on allAllies + allEnemies. Runtime tick already honors durations via statusModel.tickStatuses.
Passive tree overhaul (mechanical nodes)done — m176
Pre-M176 passive trees were nearly all flat stat bumps (+1 DEX, +5 HP). M176 rewrites src/game/passives.js: HP/MP/regen nodes kept (user-approved); new mechanical nodes add blockChance, thorns, resistAll, dodgePct, lifesteal, burnOnHit, poisonOnCrit, chainOnHit, hpOnKill, manaOnKill. All 19 class trees redesigned with 4–5 role-themed mechanical picks + 1 staple. Wire sites in CombatScreen: _resolveIncomingDamage (resistAll), _basicAttack (burnOnHit, poisonOnCrit, chainOnHit), _applyDamage (thorns, hpOnKill, manaOnKill). Lifesteal piggybacks on actor.lifeSteal. 202/202 tests passing.
Soulbind (split ally damage)done — m175
Runtime damage redirect wired in CombatScreen._applyDamage (soulbind block, recursion-safe via _soulbindProcessing). Demo skill arch_soulbind in archetype-spells-2.json (cleric, 50% split, 4r). 3 vitest cases in soulbind_m175.test.js.
Momentum (scale with unique hits)done — m150
arch_momentum_strike — onHit + incrementFlag('momentum'). Downstream skills can requireFlags/consumeFlag to scale.
Counter-stance (150% back to attackers)done — m173
Runtime: CombatScreen._applyDamage now consumes counterStance status — returns amount% of incoming damage to attacker, decrements stacks per hit, auto-removes when exhausted. Recursion-safe via _counterProcessing flag. applyStatus DSL op extended to carry amount/power for status entries. Demo skill arch_counter_stance (Iron Counter): 2-stack, 150% reflect, 3-round duration. Tests: src/game/__tests__/counter_stance_m173.test.js.
Targeted silencedone — m171
arch_targeted_silence ('Hush') in public/mods/examples/archetype-spells-2.json — applyStatus 'silenced' for 2 rounds. AI skill-picker (CombatScreen._heroAI, simulator.simHeroAI, CombatScreen._enemyAI healer branch) now gates any magic/heal/mp-costing skill behind isSilenced() via src/mods/statusModel.js::isSilenced. Tests: src/game/__tests__/silence_m171.test.js.
Mark-and-chaindone — m150
arch_mark_and_chain — setFlag marker + onHit/requireFlag/echo chain. Uses starter-pack ex_thunderclap as the chained payload.
Soul tether (mutual revive)done — m151
arch_soul_tether in public/mods/examples/archetype-spells-2.json — applyStatus('soul_tether') to both caster+ally, onEvent('onDeath') heal-pulse revive. Runtime must emit onDeath (same class as onDodge exemplar).
Second wind (auto-revive at 1 HP)done — m151
arch_second_wind — self applyStatus + onEvent('onDeath') flat heal 1 + consumeStatus. Pack JSON ships; runtime onDeath trigger pending.
Channel ramp (+25%/round idle)done — m151
arch_channel_ramp — incrementFlag('idleRounds') + onHit consumeFlag + int-scaled echo damage.
Conditional multi (+1 target per status)dropped — m151
DROPPED — DSL resolveTargets() has no dynamic count primitive (cannot read a flag/status count and use it to size the target list). Needs new op (targetEachOf/sizeByFlag). Deferred to avoid op-proliferation before statusModel migration.
Armor-shred stackingdone — m151
arch_armor_shred_stack — applyStatus stacks + buff(armor,-2) stacking across casts via statusModel.addStatus coalescing.
Sacrifice companiondone — m151
arch_sacrifice_companion — 9999 flat damage to target ally + flat heal + int/str buff to self. No removeFromCombat op needed; ally onDeath handles cleanup.
Ritual (multi-round, interruptible)done — m151
arch_long_ritual — 3-round ritual op, applies 'channeling' status to caster (interruptible via stun/silence), aoe damage+doom on completion.
Dash-strike (hit back row)dropped — m151
DROPPED — resolveTargets() has no back_row/front_row primitive and the skill schema targeting.side enum has no row keyword. Needs new runtime concept (formation + row targets). Authoring now would ship broken.
Phase (untargetable, can't act)done — m151
arch_phase — applyStatus('untargetable',1) + setFlag('skipTurn',true). Runtime honors same class as reflective.
Guard link (redirect 1-round)done — m151
arch_guard_link — applyStatus('redirect_to_caster',1) on ally + self armor buff. Runtime redirect hook shares the Soulbind wishlist item's prerequisite.
Overheal → barrier (spell)done — m151
arch_overheal_barrier — large heal (mult 2.5 + 40 flat) triggers statusModel.overhealToBarrier() which converts overflow into a 3-round barrier status.
Counter-magic screendone — m151
arch_counter_magic_screen — applyStatus('magic_reflect') to allAllies + onEvent('onSpellHit') 100% int damage return.

Event Chains (5)

Rescue Bessa → she hunts you down → rare ring → rescue her AGAINdone — m151
ec_rescue_bessa in public/mods/examples/event-chains.json — 5-node chain: start → rescued_1 → ring_reward → second_capture; abandoned branch leads to hostile/forgave.
Spare bandit Grenn → warns of ambush → joins party at L10done — m151
ec_bandit_grenn — spared → grenn_warned_ambush flag → awaiting_join guarded by hero_level_10 flag → grenn_joined.
Cursed Coin → whispers rewards for corruption → boss at corruption ≥ 5done — m151
ec_cursed_coin — incrementFlag('corruption') across loop (coin_check self-references); coin_boss requires corruption flag; persistent str buff reward for obeying.
Spare dragon whelp → circles in act 3 → allied in act 5 bossdone — m151
ec_dragon_whelp — start → freed (requires act_3_reached) → allied_boss (requires act_5_reached) with aoe str buff on allAllies during the finale.
Rival adventurer → 3-way branch: ally / neutral / bossdone — m151
ec_rival_adventurer — incrementFlag('rival_warmth') across start/second_meeting (±2–4 per choice); third_meeting offers ally/neutral/boss branches and consumes rival_warmth on the boss path.

Class & Appearance System

Decouple character class (skills/talents) from appearance (portrait + animation frames). Play a cleric who looks like a lightning mage.

Appearance registry separate from Classdone — m127
Shipped src/game/appearances.js with 20 base appearances (one per existing class) plus 1 extension: Cleric Priestess.
Appearance picker on Create Herodone — m127
Class pick and Appearance pick are now independent on the stats step of CharacterBuilderScreen.
Appearance-per-companion schemadone — m151
Companion sprite schema documented in src/game/companions.js:1-26: optional east/east_attack/east_ko/east_block frames resolved by src/game/spriteUtils.js. Existing south-only companions still work via fallback.
Companions: block + spell viewsdone — m151
Block view: companions DO block (blockChance/blockPower via getCharacterBlockStats in src/ui/screens/CombatScreen.js:218-244); east_block frame now part of the documented schema. Spell view: DROPPED — companions have no class skill list and do not cast, so east_spell is intentionally omitted (see note in src/game/companions.js).
Swap tavern NPC appearances per roster refreshdone — m160
Currently hirables use class default. Enable per-NPC appearance override.

New Characters

Previously referenced in milestone-report.html but never implemented. Require both class-data work (skills, talents) and art gen.

Heroes (7 phantom classes)

Monkdone — m151
4 skills (palm_strike, flurry, inner_focus, monk_shadow_step) at L1/5/10/15. DEX martial, unlocks at hero level 10. See src/game/classes.js + src/game/skills.js. Art reuses warrior appearance.
Shamandone — m151
4 skills (spirit_bolt, healing_totem, chain_lightning_spirit, ancestral_shield) at L1/5/10/15. INT spirit caster, unlocks after Act 1. See src/game/classes.js + src/game/skills.js. Art reuses druid appearance.
Witch Hunterdone — m151
4 skills (silver_bolt, dispel_curse, purge_strike, inquisitor_mark) at L1/5/10/15. DEX anti-magic, unlocks after Act 2. See src/game/classes.js + src/game/skills.js. Art reuses ranger appearance.
Knightdone — m151
4 skills (knight_shield_bash, knight_taunt, knight_holy_strike, rally) at L1/5/10/15. STR tank, unlocks after Act 1. See src/game/classes.js + src/game/skills.js. Art reuses warrior appearance.
Sorcererdone — m151
4 skills (wild_bolt, sorc_arcane_surge, mana_burn, chaos_ward) at L1/5/10/15. INT chaos caster, unlocks at hero level 15. See src/game/classes.js + src/game/skills.js. Art reuses mage appearance.
Runesmithdone — m151
4 skills (rune_hammer, rune_of_warding, rune_of_might, forge_flame) at L1/5/10/15. STR buff-smith, unlocks after finding 5 rare items. See src/game/classes.js + src/game/skills.js. Art reuses warrior appearance.
Shadow Dancerdone — m151
4 skills (sd_shadow_strike, smoke_veil, assassinate, dance_of_blades) at L1/5/10/15. DEX stealth/execute, unlocks after Act 2. See src/game/classes.js + src/game/skills.js. Art reuses rogue appearance.

Appearance Variants (no new class needed)

Cleric Priestess (female, silver hair, white/gold)done — m127
Full 7-sprite set generated via SpriteCook. Appears in Appearance picker.
Additional female appearance variants for under-represented classesdone — stale entry
Closed in M236 after re-audit. The state file public/data/pixellab_redesign_state.json already lists 16 female variants with referenceStatus: approved: warrior_female, fighter_female, paladin_female, mage_female, bard_female, chronomancer_female, demon_hunter_female, necromancer_female, scavenger_female, swashbuckler_female, stormcaller_female, tactician_female, runesmith_female, witch_hunter_female, monk_female, sorcerer_female. This wishlist entry was stale from the M161 era before those sprites were generated.
Fighter male skin tone drift between framesneeds identity-locked regen
Risk: regen must re-import every existing fighter_male pose as a style-reference to prevent identity drift, then generate all 7 poses with a tighter "EXACT skin tone from portrait" clause. That's ~100 credits minimum and loses if the new gen drifts differently. Low-priority polish that needs a dedicated identity-lock workflow, not a one-shot regen. Current sprites are already in-game and functional.
Cleric (female) reference sheet bobblehead proportionsneeds identity-locked regen
Risk: reference sheet only — pose frames are good. A bare reference-sheet regen without anchoring the new gen to the existing portrait loses character identity. ~15 credits for the gen, but the surrounding review flow (mark pending, diff against approved) is heavier than the gen itself. Scoped as "convenient batch with other cleric work" since the drift is cosmetic and the approved frames already ship.
druid_male east_block hood inconsistencysingle-pose regen candidate
Single-pose drift (hood up in east_block vs hood down elsewhere). Route for future batch: (1) import current druid_male portrait as SpriteCook reference_asset_id, (2) generate east_block with "hood down — exact portrait match" prompt, (3) save to images/pixellab/pending-review/druid_male/east_block.png, (4) state.json marks the pose pending_approval. Requires ~15 credits. Kept open (not silently shelved) — runs when other druid work batches.
mage east_block boot palette driftsingle-pose regen candidate
Same workflow as druid hood drift above — single east_block pose regen with portrait reference-lock. ~15 credits. Batch with other mage work.

Companions

Generate east/attack/ko sprites for 10 existing companionsdone — m151
All 10 companions now have east/east_attack/east_ko sprites in public/images/spritecook/ (dire_wolf, forest_owl, ember_drake, shadow_cat, crystal_golem, spirit_wisp, bone_hound, ice_sprite, swamp_frog, void_moth). Generated via SpriteCook gemini-3.1-flash-image-preview.

Bosses & Enemies

Additional named act-bosses for expansion contentdropped — m218
Closed per user review 2026-04-22: campaign will be designed around existing bosses; no expansion planned. Reopen if expansion content is scoped.

Custom Content Upload

Structured schema pipeline that makes content generation easy and low-bug.

Upload custom sprites (portrait + 6 sprite variants)done — m151
Handled by appearances kind. Inference extended in src/mods/registry.js:71-90 + public/assets/custom-content.html:83-92 to also accept raw 7-frame sprite objects (portrait/south/east/east_attack/east_ko) without requiring a classDefault hint.
Upload custom item types (weapons, armor, trinkets)done — m151
Handled by items kind — inferred from dmg/armor/weaponCategory. Example: public/mods/examples/custom-weapons.json.
Upload custom enemiesdone — m151
Handled by characters kind — inferred from classId or hp+tier. Example: public/mods/examples/custom-enemies.json.
Upload custom classes (with skills + passives)done — m151
Handled by classes kind — inferred from primaryAttr/startingStats. Skills packed under skills merge into the class via classOrigin (src/game/skills.js:2024-2037).
Upload custom events / quest chainsdone — m151
Handled by events kind — inferred from nodes+actRange. Example: public/mods/examples/custom-event.json. Event-chain flag persistence via GameState.flags (M133 ledger).
Downloadable schema + example mod for each systemdone — m142
All 9 v1 schemas + 4 example packs (starter-pack, custom-weapons, custom-enemies, custom-event) linked from /assets/custom-content.html.

AI Integration

In-game AI that reads the mod schemas + currently-loaded modules and generates new content on demand.

Content-generation AI — describe a character or spell, AI writes the JSONdone — m178
Shipped at /assets/ai-content-gen.html. Browser-side tool: user pastes an Anthropic API key (localStorage), picks a kind (skill/class/character/item/loot/event-chain/appearance/pack), writes a plain-English description; the tool fetches the relevant /schemas/v1/ files, builds a system prompt with the schema + DSL op list, and calls the Anthropic Messages API directly from the browser using the anthropic-dangerous-direct-browser-access header. Output is JSON-linted for structural basics (id present, skill.effects non-empty, DSL ops whitelist) then copy/download/hand-off to Custom Content Loader for full runtime validation. No server dependency.
Portrait-generation AI — reference photo + directional framesdone — m218
Covered by existing SpriteCook pipeline (reference_asset_id + 7 canonical poses). See memory/pixellab_redesign_pipeline.md. Closed per user review 2026-04-22.
Image review tool — whole-sprite prompt modedone — m156
Each group header in image-review.html now has a "Prompt whole sprite" button. Opens an inline panel with a prompt textarea, "apply to all poses" checkbox, and a Regenerate (stub) button. Stub logs all 7 poses to console + shows an alert. Notice: "Regeneration pending — requires Portrait-generation AI from roadmap."
Image review tool — archive / library / delete per imagedone — m156
Arc / Lib / Del quick-action buttons on every tile. Clicking stores action in localStorage key imageReview.actions as { action, ts }. Tile gets visual badge (archived/library/deleted) + dimmed/colored border. Filter dropdown at top lets you hide/show by action state.
Image review tool — "Unassigned" appearance categorydone — m156
Section at bottom of image-review.html. Populated from manifest entries where category === 'orphan' AND id/filename matches pose suffixes (portrait/south/east/east_attack/east_spell/east_block/east_ko). Empty state message if none.
Image review tool — "Archived / Unattached" categorydone — m156
Section populated from quickActions archive flag + state-level archive + non-pose orphans. Each tile shows Restore (clears archive flag), Assign (prompt() for sprite name, stores in imageReview.assignments), Delete (marks for deletion). Restore removes entry from section.

Prior Feature Backlog

Items surfaced in earlier batches that are still open.

Batch background removal on opaque spritesdone — m174
New scripts/batch-remove-bg.cjs walks sprite dirs, detects PNGs with no transparency (min alpha = 255), runs corner-sampled chroma removal. 503/1262 opaque sprites processed; 759 already-transparent files skipped. Same weighted-Euclidean algorithm as remove-bg.cjs.
Homepage: single cloud layer onlydone — m174
Removed cloud-layer-2 and cloud-layer-3 from index.html hero section. Kept cloud-layer-1 (opacity 0.45, 90s drift).
Skills menu: remove redundant hover tooltip on skill rowsdone — m174
On mobile, long-press tooltip was covering the detail panel that a tap immediately opens. Dropped attachTooltip on .skill-row — click-to-view-detail is now the single source of truth.
Skills menu: auto-select character + tab with pending pointsdone — m174
On onEnter (and on character-tab switch), SkillTreeScreen picks the first character who has unspent points and jumps to the tab containing them (priority: skill → passive → attr). Single-tap from level-up to "where do I spend?"
Hover-preview tooltips — apply broadlydone — m151, fixed m164
Shared helper extracted to src/ui/components/Tooltip.js (attachTooltip + injectTooltipStyles). Wired into Skill Tree skill rows and passive nodes (src/ui/screens/SkillTreeScreen.js:378-422). Inventory already uses its own richer tooltip. Forge tooltip was present but had two bugs fixed in M164: (1) touchstart held a stale event.currentTarget reference (null after handler returns) — fixed by capturing the recipe element in the closure and passing it directly to _showTooltip(anchorEl, recipeId); (2) CSS ::before arrow was on the wrong edge (appeared at tooltip top pointing up, but tooltip was above the anchor) — replaced with ::after at top:100% using border-top-color, plus a .tooltip-below variant for when the tooltip flips. Also added touchmove cancel to abort long-press on scroll. See src/ui/screens/ForgeScreen.js:207-232 (event wiring) and _showTooltip.
Animation frames for walk/attack on 10 companionstodo
Flagged after M64, merged with appearance-per-companion schema.

Asset Pipeline & Image Regen (M129+)

Move off pixellab archive entirely. All assets indexed in image-review. Rigid per-type spec documented in /docs/.

Image Regeneration Queue

Character art exceptions (flagged during review)todo — m204
State model simplified (M204): all characters with an approved reference sheet are flipped to status=approved. Individual-pose quality issues are tracked here instead of as per-frame pending_approval flags. Known exceptions: (1) chronomancer_female — portrait awkwardly holds two hourglasses (low priority). (2) war_dog east and east_ko frames — muzzle/mouth area has slight color drift from the reference (darker/off-hue vs. the tan muzzle established in the reference sheet). Flagged M216; do not regenerate yet. (3) sorcerer_male — south / east / east_attack / east_block should eventually be regenerated to match the generous full-body framing of sorcerer_male east_spell (which is the new canonical sizing reference). Flagged M217. (4) tactician_male east_attack — scale is too large; shrink on next pass. Flagged M217. (5) witch_hunter (male) east_spell — figure is stretched too big; shrink on next pass. Flagged M217. Review batch shipped M204: rogue, rogue_male, warrior, warrior_female (7 poses each, 28 frames) — list any pose regens here.
19 boss east_spell + east_block frames (38 images)wip — m153
26 of 38 done (13 bosses × 2 poses): abyss, architect, border, breach, core, dragon, dragonking, dragon_throne, dust, plateau, rift, thornwood, void. Remaining 6 bosses (abyssal_knight, archfiend_malgrath, cosmic_titan, dragon_knight, genesis_worm, grax_veil_touched, lava_titan, primordial_elemental, reality_shard, veil_warden) need portraits first.
17 enemies full sprite regen (119 images = 17 × 7 frames)todo
Currently portrait-only in spritecook; south/east served from sprites_pixellab_archive. Regenerate full sets via SpriteCook: ash_wraith, bandit, bandit_captain, cinder_hound, corrupted_bear, corrupted_wolf, demon_brute, goblin_scout, goblin_shaman, goblin_warlord, goblin_warrior, hell_knight, imp, molten_golem, veil_cultist, void_shade, veil_sorcerer. Audit (2026-04-18): All 17 have portrait in spritecook. All 17 have south+east (plain idle only) in sprites_pixellab_archive — but NO south/east/east_attack/east_spell/east_block/east_ko in spritecook. 102 poses missing across 17 enemies (6 per enemy), ~1,224 credits needed. Missing per enemy (all 17 identical): south, east, east_attack, east_spell, east_block, east_ko.
All combat backgrounds regenerated at 1536×1024done — m158
All 12 zones have HD combat BGs shipped + wired into backgrounds.json (border_roads, thornwood, dust_roads, ember_plateau, hell_breach, shattered_core, cosmic_rift, eternal_void, abyssal_depths, primordial_nexus, dragons_reach, dragon_throne).
warlock_east_spell background removaldone — m163/m165
Canvas tool at bg-remove.html — multi-sample color picking, per-channel weighted Euclidean distance, tolerance slider (0–120), optional feather-edge pass. Load any sprite via preset or drag-drop; click pixels to add samples; Apply exports a transparent-bg PNG. CLI equivalent: scripts/remove-bg.cjs (CJS, uses sharp at /tmp/img_opt) — same sqrt(2*dr^2+4*dg^2+3*db^2) formula, --auto-corners flag, --feather flag. Applied to warlock_east_spell.png (m165): 2 bg samples auto-detected (rgb(72,108,85) + rgb(72,108,170)), tol=40 — removed 32,358/65,536 px (49.4%); sprite now has 4-channel alpha PNG.

Open 404s / Missing Variants (M187 audit)

26 stale combat_bg variant refsdropped — m218
Closed per user review 2026-04-22: `_hd.png` per-zone is the canonical single variant; old combat_bg_N files will not be restored. Can delete the stale refs entirely.
32 missing boss pose variants (south/east/east_attack/east_ko for 8 bosses: thornwood, dust, plateau, core, rift, void, dragon, dragonking)todo
Currently each boss only has portrait + east_block + east_spell. The full 7-pose spec (image-policy.md) calls for south/east/east_attack/east_ko as well. Combat works because CombatScreen falls back gracefully, but image-review will show them as missing once added to manifest. Decide: extend bosses to full pose spec, or formally exempt bosses from the 7-pose rule in image-policy.md.
Console 404s for war_dog_portrait.png and dragon_hatchling_portrait.pngdone — m412
war_dog_portrait.png was already in spritecook (re-verified). dragon_hatchling_portrait.png stub-copied from images/portraits/dragon_hatchling.png so the spritecook-first fallback chain no longer 404s. Long-term plan to regenerate via SpriteCook still open in art queue.

Image Catalog & Policy

Rigid asset-type definitions in /docs/image-policy.mddone — m151
public/docs/image-policy.md covers the 7-pose character spec, 1536×1024 combat/map backgrounds with empty-midground rule, and tap-weapon icon+effect spec. Asset-types registry in public/docs/asset-types.md.
Every generated image auto-added to image-review manifestdone — m151
Policy documented in public/docs/asset-types.md ("every image added to disk MUST be added to BOTH indexes in the same change"). scripts/check-unregistered-images.cjs walks public/images/ and exits non-zero on any file missing from assets.json / image-review-manifest.json / spritecook-assets.json / src/ references. Wire into a pre-commit hook when ready.
Tap Weapons + Tap Equipment icons in image-reviewdone — m151
scripts/build-image-review-manifest.cjs now scans public/images/tap_fx/, tap_weapons/, tap_effects/ and tags entries as category 'tap-weapons'. public/assets/image-review.html renders a dedicated Tap Weapons section.
Image-review per-section custom details (bug fix)done — m151
public/assets/image-review.html now persists category-scoped details defaults in localStorage key rsg.imageReview.categoryDetails.v1. snap() writes the template entry's category only, and renderEditor() prefills from the matching category default. Hero details no longer leak onto background or tap-weapon cards.
Restore 'Remove Background' action to image-reviewdone — m151
Present in public/assets/image-review.html (radio option alongside Flip/Re-Generate/Library/Archive/Delete/Other). Hidden for background cards where it doesn't apply. Sub-panel exposes optional hex bgColor.

Documentation & Policy

Markdown policy files live in /docs/. Tools > Documentation renders them in-browser.

/docs/ folder scaffoldingdone — m151
All six scaffolds present: public/docs/image-policy.md, prompt-templates.md, schema-reference.md, mod-authoring.md, asset-types.md, no-silent-shelving.md (plus spell-mechanics.md from M142). Committed to repo so they render on GitHub + in-app.
/assets/docs.html viewerdone — m151
public/assets/docs.html — inline markdown renderer (no CDN dep), side nav lists every doc, hash-routed (#image-policy.md), styled with shared tokens. Covers headings, code blocks, lists, tables, blockquotes, inline code/bold/italic/links.
CLAUDE.md references /docs/done — m151
/home/radgh/claude/game13/CLAUDE.md lines 3-13 — "Canonical Docs" block links each doc and states "Docs-out-of-sync is a bug."
New-tool / new-dependency flag ruledone — m151
/home/radgh/claude/game13/CLAUDE.md lines 14-16 — "≤25KB auto-approved, >25KB name+size+reason and wait, log accepted adds in Technical Debt Registry." Referenced from Technical Debt Registry item below.

M316 — Settings/Title/Save/Party UI Unification

Batch of 18 UI fixes shipped in M316. All items below marked done.

#13 Hire Custom Hero — 2nd page missing CSS, class cards cut off, "Chronomancer" wrapsdone — m316
Grid minmax raised from 150px to 160px. Card overflow set to break-word. Skill body gets min-height:0 to scroll correctly. Class name uses clamp font + ellipsis so long names never wrap.
#14 Inventory/Skills consistency update — visual consistency passdone — m316
PPS overrides normalize sub-screen panel-label color, inv-char-header margin, and scroll behavior when mounted inside PartyPanelScreen.
#16 Settings: remove difficulty toggle; fix DifficultyDialog close/cancel/applydone — m316
Difficulty row removed from SettingsScreen. DifficultyDialog: onLeave never fired (ScreenManager calls onExit) — fixed. Added noGameMenuEsc, Escape key handler with cleanup, onExit/destroy/onPause/onResume lifecycle methods.
#17 Title screen: remove slide-down intro, fade in immediatelydone — m316
CLOUDS and LOGO_DROP phases skipped immediately in update(); menu fades in over 0.35s instead of the 4s slide sequence.
#18 Title screen: frontmost cloud layer overlaps menu by ~1pxdone — m316
Mid cloud layer heightFrac reduced from 0.38 to 0.32 so its bottom edge stays above the menu boundary.
#19 Save menu: fixed-height cards, scrollable page, floating Back buttondone — m316
ls-slots max-height removed; ls-slot changed to flex:0 1 auto; ls-panel gets 6rem bottom padding; Back button is position:fixed at screen bottom with safe-area-inset padding.
#20 Statistics menu: pressing Esc opens GameMenu instead of closingdone — m316
StatsDashboardScreen and StatsScreen both get noGameMenuEsc=true so global Escape handler skips them; their own kbMount onEscape (which pops) fires instead.
#21 Statistics lost after page reload — persist statisticsdone — m316
Run stats flushed to localStorage (emberveil_run_stats_cache_v1) via throttled 5s timer on recordDamageDealt(). ensureStats() restores from cache on page reload when gs.stats is null.
#22 input[type=checkbox]:checked::before size off after 44px tap-target ruledone — m316
Checkmark pseudo-element tuned: left:6px, top:2px, width:4px, height:9px, border-width:2.5px — centered within the 18x18 checkbox box.
#27 Auto-skills/auto-attributes: replace browser confirm() with custom modaldone — m316
SkillTreeScreen #recommend-auto handler replaced: browser confirm() → showConfirmModal() with title/message/confirmText/cancelText/onConfirm. Checkbox reset until confirmed.
#31 Tavern: Inventory + Party buttons route to PartyPanelScreendone — m316
TownScreen imports PartyPanelScreen. btn-inventory → PPS inventory tab, btn-skills → PPS spells tab, btn-party → PPS party tab. Party slot dropdown menu also updated.
#32 PartyPanelScreen: gold/brown palette, Inter primary font, town-style tab bardone — m316
Background changed to dark brown gradient. Tab bar mirrors town interface: Inter font (Cinzel only for active tab label). Gold/brown border and hover colors throughout. Char row updated to match.
#33 PartyPanelScreen: clicking hero switches character in current tab; remove redundant selectdone — m316
inv-char-tabs and skill-char-tabs already hidden via CSS in PPS. Char row click already calls _syncCharToSubScreen(). TownScreen party-slot menu (item 31) routes through PPS instead of old screens.
#34 PartyPanelScreen: hide portrait inside .equip-paneldone — m316
CSS added to PPS_STYLES: .pps-tab-inventory .equip-panel .inv-portrait-wrap { display:none !important } — applies across inventory/spells/passives/attributes tabs.
#35 PartyPanelScreen: Auto-Equip Upgrades checkbox — green style matching Auto togglesdone — m316
.pps-tab-inventory .inv-autoequip-toggle styled with green background (rgba(40,160,80,0.12)), green border, green text — matching Spells/Passives/Attributes Auto checkboxes.
#36 PartyPanelScreen: Auto-equip when ON actually equips existing inventory itemsdone — m316
_sweepAutoEquip() added to InventoryScreen: when autoEquip toggled ON, sweeps all inventory items via GameState.tryAutoEquip() for the selected character. Plays purchase SFX on equip.
#37 Add 0.6rem bottom margin to .inv-char-classdone — m316
margin-bottom:0.6rem added to .inv-char-class in InventoryScreen styles. Also added via PPS override for sub-screen context.
#38 Settings Party menu reusable componentdone — m316
src/ui/components/settingsPanel.js created: createSettingsPanel({ title, rows }) — supports toggle/select/slider/html row types with full event wiring and destroy() cleanup. Ready for use in any screen.

M318 — Skills/Spells/Inventory Consistency + Shop Toasts + Codex + News Fix

9 UX and codex items shipped in M318. All items marked done.

#23 Skills/Passives/Attributes: remove "Recommend" + "Auto" bar from all 3 tabsdone — m318
Removed _renderRecommendBar() call from SkillTreeScreen._render(). The per-section Auto toggle button (_renderAutoToggle) retained. "On"/"Off" text removed from auto-toggle; checkmark icon alone suffices.
#24 "Base" checkbox renamed to "Show Base Attributes" in Skills/Attributes and Inventorydone — m318
CharacterStatsPanel.js line 185 and InventoryScreen.js line 402 updated. Both stat panels now use the longer label.
#25 Spells menu: hide .skill-header bar (useless close button when inside PPS)done — m318
PartyPanelScreen.js CSS: .pps-tab-body .skill-header { display:none !important } — hides the redundant character-tab + close-btn bar when SkillTreeScreen is mounted as a PPS sub-screen.
#26 Spells .skill-panel-head .panel-label: remove "Skills - " prefix, show class name onlydone — m318
SkillTreeScreen.js line 223: "Skills — ${className}" changed to "${className}".
#28 Crafting/shop auto-equip: show "Equipped X to Y" toastdone — m318
TownScreen imports showToast from toast.js. _maybeShowAutoEquipToast() helper fires after every addToInventory in merchant, black market, guild hall, _doForgeCraft, and _doUniqueRecipeCraft handlers.
#29 Blacksmith: add bottom margin beneath "Salvage All" buttondone — m318
CSS added: .forge-salvage-all-row { margin-bottom: 0.75rem } in TownScreen styles.
#30 Enchanter tab redesigned as 3-step wizard: item picker → property picker → tier pickerdone — m318
_enchanterAffixBody() replaced with 3-step flow. Step 1: item grid. Step 2: affix property grid (only shows affixes not already on the item). Step 3: tier/material cards with cost preview. Mobile-first grid layout. "?" info buttons replaced with inline hover tooltips on tier cards (title attr). Back navigation at each step. Wizard state on _encStep/_encSelectedItem/_encSelectedAffix.
#41 What's New: fetch path broken + Full History link 404done — m318
GameMenuScreen.js: fetch uses import.meta.env.BASE_URL prefix. Full History link uses same. Fixes both the "release notes unavailable" error and the GitHub Pages 404 for the changelog link.
#43 In-game codex updated with all current formulasdone — m318
FormulaCodexScreen.js: Armor section updated to show actual curve-DR formula (no longer says "coming soon"). New "Advanced Systems" tab added with: critChance/critDamage, dodge, attack/spell damage, status duration scaling, fame thresholds (M308), legendary effect activation (M305), set bonuses (M305), boss phase thresholds (M304 — all 10 bosses listed), champion modifier stat bumps (M303).

Skill System Overhaul

Move skills out of hardcoded class definitions into a swappable, data-driven system. Variable skill count per character. Spell catalog tool mirroring image-review.

/assets/spell-catalog.html tooldone — m142
Filter by class/archetype/search, click card for full JSON + talents + upgrades, "Export as mod pack" downloads filtered skills. 81 skills across 20 classes indexed.
Protected core-mechanics listdone — m142
src/mods/coreMechanics.js enumerates PROTECTED_IDS (14 vanilla classes) + PROTECTED_STATUS_TYPES. registry.js skips overrides with a console warn + telemetry hit.
Skills as standalone data (not hardcoded to classes)done — m149
SKILLS is already flat-keyed-by-id; getClassSkills now merges mod-registered skills for a class on top of vanilla. Classes reference by skillId in classes.js.
Variable skill count per character (not hardcoded to 4)done — m151
Audit confirms no hard 4-skill assumption anywhere. src/game/classes.js already has classes with 4 and 6 skills (e.g. fighter has 6). getClassSkills/getUnlockedSkills in src/game/skills.js:2024-2043 return the full class list unbounded. SkillTreeScreen._render (src/ui/screens/SkillTreeScreen.js:90-103) loops skills.map. Combat uses AI picker (src/ui/screens/CombatScreen.js:1096-1175) which also iterates unbounded. Character Builder seeds skills: [this._class.skills[0]] (src/ui/screens/CharacterBuilderScreen.js:305) — single-element seed, not a cap. Door is already open for hybrid-class builder.
24-spell mechanic catalog docdone — m142
public/docs/spell-mechanics.md — 24 archetypes across damage/support/control/meta with DSL sketches and vanilla examples.

Combat System Migration

Finishing the mod-system-ready combat primitives. Each item below is a separate milestone.

Per-skill cooldown map (M131)done
actor.skillCooldowns[skillId] now replaces the single slot in CombatScreen + simulator. Combatants respect skill.cooldown per-skill, defaulting to 2. Resurrect uses its 12-round cooldown without locking the rest of the kit.
Event chain flag ledgerdone — m133/m151 verify
Ledger fully shipped. GameState.storyFlags persisted via toSaveData; primitives hasFlag/setFlag/incrementFlag/consumeFlag/requireFlags in src/game/gameState.js:154-189. DSL ops incrementFlag/consumeFlag/requireFlags route through ctx.gameState for persistence in src/mods/dsl.js:66-105. Tests: src/game/__tests__/flag_ledger.test.js.
CombatScreen → statusModel migration (burn/poison/bleed/stun/barrier)done — m166
M165 audited burn; M166 audited poison/bleed/stun/barrier — all five families fully on statuses[] with zero legacy-property reads or writes in src/. M166 removed the dead legacyMap in statusModel.hasStatus and the dead actor.barrierHp write in overhealToBarrier. Remaining: dmgBuff (CombatScreen.js:1410, 1558, 1795, 1887, 2085) and critBonus (CombatScreen.js:1262, 1443, 1796, 2090) — still live legacy fields, tracked separately.
Schema warn-mode normalizationdone — m142
Canonicalizes strength→str etc. in registry.normalizeStatsDeep; telemetry hit on every rename.
Combat sim diff view vs nearest vanilladone — m150
Shipped in M150. Compares total-party DPS of mod-enabled run vs. vanilla-only run. Finer per-skill targeting/stat/mult matching is a follow-up.
Combat sim — save/load encounter settings + resultsdone — m151
Export Encounter / Import Encounter buttons added to src/ui/screens/CombatSimulatorScreen.js (bundle: party, gear, enemies, act, seed, lastResult). Import re-runs with the stored seed so the result reproduces deterministically. Existing Save/Load Preset flow (localStorage + file download) untouched.
Combat sim diff + save/load preset modal + benchmark level sweepdone — m158
Three features added to src/ui/screens/CombatSimulatorScreen.js: (1) Load Preset now opens a modal dropdown (replaces prompt()) with hero/enemy counts and timestamp; Save Preset stamps ts field. (2) Diff vs previous run panel — appears after 2nd Monte Carlo run, shows win rate / avg rounds / dmg mean with colorized deltas (green = better, red = worse). (3) Benchmark toggle — sweeps party at levels 1/3/5/7/10 against the current encounter, renders inline CSS bar chart (no chart library). All 190 tests pass.

Technical Debt Registry

Items deliberately shelved. Keep them visible so they're not forgotten — and so we don't add more without naming the tradeoff.

Cloud asset cleanup (M213 / M214 / M215)done — m413
Rejected variants clouds_04.png and clouds_05.png deleted along with their thumbnails. image-review-manifest.json + image-review-v2.json scrubbed. clouds_01-03 retained because OpeningCinematicScreen still references them as parallax layers; clouds_07/08/08b are the active front-page set; clouds_06 retired (TitleScreen comment on src/ui/screens/TitleScreen.js:37).
Telemetry transport — finish Supabase wiring (scaffolded M211)half-done
Client-side queue, opt-in UX, privacy policy, and sanitization all shipped in M211 (see src/game/telemetry.js + public/docs/telemetry.md). Not wired: (1) Supabase table + indexes + RLS, (2) edge function to accept POSTs and bulk-insert with service role, (3) setTelemetryEndpoint(url) call in main.js, (4) per-event recordEvent calls in MapScreen/CombatScreen/LevelUpScreen. Full step-by-step is in public/docs/telemetry.md. Until activated, events queue locally but never leave the device.
AJV full JSON-Schema validation (shelved)shelved
~120KB. Benefits: required-field errors, type/enum violations, oneOf mismatch messages, $ref resolution, free schema normalization via custom keywords. Path back in: lazy-load only when custom-content.html opens (doesn't cost main bundle). Revisit when structural check starts producing confusing errors for authors.
Veilspawn Herald sprite — too good for a prologue mini-bossart reuse
User feedback (M290 review): "this artwork would be better left to a higher tier act boss, but fine to leave for now." Currently used as the prologue mini-boss (10-minute encounter at game start). Visually heavy enough to anchor an act-3 / act-4 boss. Action: rename + re-tier when a stronger candidate art lands for prologue; the existing veilspawn sprite set (portrait + 7 poses) gets promoted to a real act boss with the appropriate name + stat block.
Technical-debt disciplinedone — m151
Rule documented in CLAUDE.md (🛠️ New-tool flag rule section, line 16). Libraries ≤25KB gzipped auto-approved; anything larger requires name + size + reason + approval. Accepted adds logged in this Technical Debt Registry with their tradeoff.

Code Review M271 — For Triage

Findings from the comprehensive code-review pass on 2026-04-24. Full report at memory/code-review-m271.md. Top items flagged here for user decision on priority.

🚨 Balance bug: pyromancer/stormcaller passives double-counteddone (M272)
passives.js:130 — removed the slotCount multiplier. Duplicate-slot entries in pyromancer (igniting×2) and stormcaller (stormcharged×2) passive trees are now UI-aesthetic only; one purchased rank = one effective rank, matching every other class. This is a slight nerf to those two classes — rebalance sweep after playtest will determine if they need compensatory buffs.
Combat Report: enemy heal/mitigation under-reporteddone (M272)
_meterAddHeal / _meterAddMit / _meterAddDodge now all use _meterEnsureEntry like _meterAddDamage. Enemy healer-role NPCs and enemy block/armor absorb now appear in the Combat Report when "Show enemies" is on.
Meter: per-hit innerHTML rewrite (perf)done (M272)
Added _scheduleMeterRender with a dirty flag + requestAnimationFrame coalescing. User-triggered mode toggle still renders synchronously. Estimated 30–160 ms saved per fight on mobile.
XSS vector: hero name unescaped in meter renderdone (M272)
CombatScreen.js:961 — hero/enemy names interpolated into innerHTML without escaping. A hero named <img src=x onerror=...> would render. Added esc() helper applied to r.name, h.target, srcName across the meter + Combat Report drill-down.
SaveManager.importAllSaves — validate before wipedone (M272)
SaveManager.js:126 — previously wiped every emberveil_* key FIRST then iterated the import blob, so a malformed entry mid-iteration left the player with zero saves. Now parse-validates all entries up front and aborts with a clear error if none are readable, then atomic-swap.
Supabase: class unlocks don't sync across devicesdone (M272)
cloudSaves only synced keys prefixed emberveil_save_; the class-unlock storage uses emberveil_unlocks. Added a META_KEYS allow list + isSyncableKey() helper; classUnlocks._write now pushes to cloud on every local unlock write. On next sign-in the existing downloadAllMissing pulls the unlock blob automatically.
CombatScreen.js refactor: extract ~2100 lines into moduleswip — meter shipped m273; rest pending
meter tracker extracted to src/ui/screens/_meterTracker.js in M273 (proof-of-pattern). CombatScreen.js is now ~8,300 lines after additional features. Remaining cleanly-extractable modules: damagePipeline.js (armor/block/barrier/reflect resolution), aiTargeting.js (hero + enemy AI), backgroundRenderer.js (parallax layers). Future milestones planned per-module to keep each PR reviewable.
setTimeout state-mutation safety auditdone — m273/m412
Tracked _setTimeout helper landed M273 with _destroyed guard and _clearAllTimeouts on exit. M412 wrapped the remaining 4 raw setTimeout callsites (845, 983, 1216, 1510). All combat timer callbacks now no-op against a detached screen.
Tests: _applyDamage pipeline, passives, meter2/3 done
passives.test.js (M273) and meter.test.js + damage_pipeline.test.js / damage_pipeline_resolve.test.js all shipped. Remaining gap: an end-to-end _applyDamage integration test that wires actor → roll → mit → barriers → reflect → meter. Pyromancer regression already pinned by passives.test.js.
Kill-counter + boss-HP-at-kill tracker (for warlock / pyromancer unlocks)done — m273
gs.enemyKillCount incremented in _victory (CombatScreen.js:5704); gs.bossKillsLowHp appended on boss death below 20% HP (CombatScreen.js:5127-5133). Both counters consumed by classUnlocks.js for warlock and pyromancer unlock predicates.
Replace maxMp || 80 fallbacks with computeMaxMp(member)done — pre-m412
All maxMp || 80 fallbacks already removed. Combatants build maxMp via the canonical formula at CombatScreen.js:417-425. Sole remaining fallback is a 9999 upper-bound on a Math.min mp-cap (M276), which is intentional, not a bug.
Flee-fail enemy attack bypasses canonical pipelinedone — pre-m412
_attemptFlee at CombatScreen.js:2475 already routes the flee-fail enemy hit through this._basicAttack(enemy, [target], false, true). Hits go through rollToHit, block, crit, magic resist, and the meter — same as any other attack.
Hoist _victory inline zone maps to module topdone — m412
All six zone-keyed tables (ZONE_DROP_CHANCE, ZONE_FAME_MULT, ZONE_UNLOCK_MAP, ZONE_NAMES, ACT_BOSS_ZONES, BOSS_TAP_DROPS) moved out of CombatScreen and exported from src/maps/mapData.js as a single authoritative registry. Adding a zone now touches one file.
Replace JSON.parse(JSON.stringify()) with structuredClonedone — pre-m412
CombatScreen per-cast clone already removed in earlier refactor. Verified M412 — no JSON.parse(JSON.stringify) deep-clones remain in CombatScreen.js. balance-loader.js continues to use structuredClone.
Cache _updateHud element refsdone — m273
_hudRefs Map cached at build time, invalidated whenever _renderHud rebuilds. Verified M412.
Full code review findings — more items in the reportdone — m412–m415
All concrete long-tail items shipped: recursion guards on soulbind/counter/thorns wrapped in try/finally (M415), gameState._state factory breaks DEFAULT_STATE reference sharing (M414), M65 audio alias table dropped after call-site audit (M415), zone metadata unified into mapData.js (M412). SPRITE_MAP intentionally retained as defensive override map; resolveSprite is the canonical path but SPRITE_MAP catches edge cases.

M264–M271 Shipped

Recent milestones. Archived here as a changelog + to confirm nothing got silently shelved.

Sim + combat AI respect member.skillsdone (M264)
Both the simulator and CombatScreen's _heroAI now intersect unlocked class skills with the player's selected list. Previously sim would auto-cast Holy Strike for a knight who only picked Shield Bash. Primary sim-vs-playtest divergence.
Balance pass: +150% enemy HP, +10% damage (M265) → fine-tuned to 2.0× HP (M266)done (M265/M266)
Sim Monte Carlo against 5 real save files × 20 encounters × 150 seeds per cell. User's hypothesis (+500% HP / +250% damage) tested and rejected — crushed boss win rates to 5%. Chosen profile: 2.0× HP / 1.0× damage → bosses land at ~10 rounds avg in sim.
Simulator shield-block paritydone (M265)
Sim previously didn't roll block at all — heavy-shield builds (Ylva: 58% block / 114 block power) were modeled as if taking every hit full. Now rolls block + applies block power on hero defenders before armor mitigation.
Skill damageMult additive-stacking fixdone (M266)
_mergeInto now uses REPLACE semantics for upgrades (was additive). 43/63 damage skills were scaling ≥2× above their tooltip numbers (Shield Bash 4.9× STR instead of 2.0×). Tooltips in Skill Tree render the effective multiplier via mergeSkillForCast. 1 residual outlier (Slow Time, intentional talent stack).
HP persistence on Hard difficultydone (M266)
Post-victory HP sync now iterates both party + companions by ID (was heroes-only by index). A party with a companion had the companion start every fight at full HP on Hard.
Loot chest timing: fixed polling-loop bugdone (M266)
Removed the forever-poll from MapScreen.onResume. Previously after a boss kill, if the player walked into Town first, the chest would pop when they left town. Now one-shot check; if Map isn't top by the 400ms delay, bail and let the next onResume retry.
DPS meter: DMG / HEAL / MIT 3-way + granular Combat Reportdone (M267/M268)
In-combat meter cycles DMG → HEAL → MIT. Tracks lifesteal+barrier as heals; armor/block/barrier/spell-resist/damage-reduction as mitigation; dodge as a count. Post-combat Combat Report: Character → Skill/Source → individual hit (round, target, crit, overkill, overheal, mitigation passthrough). Meter panel is draggable; position persists. Enemy tracking toggle ("E" in meter, "Show enemies" checkbox in report).
Combat Report button on defeat + final-boss victorydone (M268)
Previously only non-final-boss victory had the Combat Report button. Now available on all three end states.
Skill name attribution in meter (Magic Missile, Fireball, etc.)done (M268)
Skill damage in the meter was attributed to "Attack" — spell breakdown didn't carry skillName. Added breakdown.skillName = skill.name before _applyDamage.
Tavern-hire skill + passive points catch-updone (M268)
Hires at level N joined with pendingSkillPoints: 0 and no skills array populated. Now get skill + passive pending points for every level missed, all class skills unlocked at their level, and autoBuild flags on so points auto-spend on hire. Attr points intentionally stay 0 (template.attrs already bakes in the level's stat spread).
Tinker class + Clockwork Turret companiondone (M269/M270)
New INT-primary class: Clockwork Bolt / Alchemical Grenade / Quick-Fix / Overcharge. Unlocks after Act 1. Ysolde Cogwright hireable. Full 7-pose male + female art + 5-pose turret companion via OpenAI gpt-image-1.5 (gpt-image-2 needs org verification, fell back per memory policy). Turret is a talent-unlock on Clockwork Bolt ("Deploy Turret"), matching Raise Skeleton / Forest Wolf / Bound Imp pattern — not a tavern hirable.
Rebalance report page + balance toolsdone (M267)
/assets/rebalance-report.html — pure-SVG charts covering audit before/after (43→1 outliers), balance profile sweep, per-save round counts, meter mockups. Regression tools: scripts/skill-audit.mjs, scripts/balance-sweep.mjs, scripts/balance-report.mjs, scripts/balance-single.mjs.

M256 Batch — In Progress (user request)

Added 2026-04-23. Working through top to bottom. Each item flips to "done" as it lands, or moves to Shelved with a specific reason.

Level-up dialog shows auto-applied detaildone (M258)
"Auto: +1 STR, +1 CON" badges per hero; "New Skill:" prefix for freshly unlocked skills. M261 follow-up: Spend-Now button hidden when auto-build consumed all pending points.
Scorched Hermit — node-skip bug after bossdone (M256)
Cross-zone arrival now calls _navigateToNode on the destination so its encounter fires before neighbors open.
Loot chest delayed by level-up screendone (M256/M266)
M256 added a retry-poll so the chest waits for LevelUpScreen to close. M266 removed the forever-polling loop that made chests pop after a town visit — now one-shot check per Map.onResume, bailing if Map isn't top.
Cross-zone line from Cinderhold to Ember Plateau (wrong origin)done (M256)
_getCrossZoneLink now filters out town inserts before picking first/last nodes, so cross-zone lines always originate at the boss / end of the previous zone.
Hard difficulty disables Back-to-Town + waypoint teleport entirelydone (M256)
Hard hides the Back-to-Town button (_showBackBtn gated); waypoint teleport gated via _isHardTel. Walk-only as spec'd.
Completed node indicator + no-op revisit (no popups)done (M256)
Visited non-repeatable nodes dim to 0.6 alpha and show a small ✓ checkmark. Dialog/Lore/Treasure revisits are silent no-ops.
Quest indicators on map for quest-relevant nodes (Mira the Seer)done (M260)
_DIALOG_QUEST_HINTS maps dialog node IDs to story flags (seer_met, knows_rift_origin, cleared_hidden_path, ritual_site_found). Active quest + unsatisfied flag → gold "!" indicator.
Ember Watch (undiscovered town) visibilitydone (M259)
Town color brightened from #40a860 → #6fd88a for much better contrast in dim-mode.
Salvage All button on Blacksmith (Salvage tab only)done (M256)
"Salvage All (N)" button prepended to the forge salvage body; iterates unequipped inventory IDs and calls _doForgeSalvage on each.
XP gains × 0.2 across the board + verify cheat multiplierdone (M256)
GLOBAL_XP_MULT = 0.20 in xp.js; awardXp applies it on top of cheats.xpMultiplier. DialogScreen XP now routes through awardXp so flat reward.xp picks up the scaler.
Static Field 65 damage vs expected 29 — investigatedone (M258)
CombatScreen spell damage formula aligned with the Skills panel preview — now `statVal × mult × (1+powerBonus) + weaponFlavor` matching the tooltip. Sim-AI divergence (Thunder Ring vs Static Field) fixed in M261 via mergeSkillForCast + member.skills intersection (M264).
Combat log polish: overkill, secondary-effects toggledone (M258/M259)
Overkill row in damage-breakdown tooltip (M258). Secondary-effects checkbox in log header hides status/buff/debuff lines (M259).
DPS / Heal Meter + Combat Reportdone (M261)
In-combat meter panel (top-right) with damage-ranked rows + bars, DMG↔HEAL toggle, close/reopen via floating M button. Post-combat "Combat Report" modal shows per-character damage/heal totals and per-source breakdown. Session-scoped in-memory only.
NPC appearance type (playable: false) + 3-5 NPC art setstodo
New APPEARANCES flag playable: false excludes from character-creation + tavern rolls. Generate reference + 7 poses for Silas Veilward, Kaela Thorne, Marek Greel, Mira the Seer via SpriteCook following art_direction policy. Save to public/images/pixellab/<id>/ and flag for approval on character-redesign page (pending_approval).
Map Structure Expansion — 2× nodes per act ladderdone (M260)
Act 1 Border Roads expanded 6→9 (gravel_bend, wayside_cache, briar_trail), Thornwood 9→11 (mossy_glade, thorn_thicket). Audit confirmed Acts 2–6 already exceed the ladder target (11–13 nodes per zone vs 7/8/9/10 target) from prior milestones.
Game Info Consolidation — content audit + finish migrationdone (M260)
Story + Six Acts sections ported to assets/index.html. Classes/Roster/Equipment/Companions duplicated by live catalogs — dropped as stale. Milestone Timeline redundant with Reports section — dropped.
Combat Sim vs Playtest divergencedone (M261)
CombatScreen._heroAI now merges skills via mergeSkillForCast before evaluating mpCost (previously used base skills; sim was using merged — that's why picks diverged). Simulator also now reads personality from _member, treating 'protective' as healer for parity.
Telemetry (later, after balance is stable)todo — deferred
Explicit user request to defer until balance sorted. Wire recordEvent calls once sim + playtest agree.

Shelved / Revisit

Items deferred because the current system couldn't host them cleanly. Surface when the foundation catches up.

Hire Custom — unify UX with Create Hero (M151)fixed
`.hb-class-grid` already uses `minmax(150px, 1fr)` and the appearance picker was added in an earlier milestone. M151 finishes parity by mirroring the Create Hero card structure (HireBuilderScreen.js:133-153): same role/hook/"Starts with" rows and the same locked-card treatment (class name + 🔒 + unlock label).
Starting attribute points not persisted after level-upfixed
Promoted from "fixed-ish" → "fixed" (M236). The "ish" was historical hedging; there is no remaining issue. Root cause was the Base-mode toggle hiding allocation deltas so the player read the panel as "points not applied" when they were. Fix in place: Base toggle is session-only and defaults off on page load, so stale state cannot confuse later. Allocation itself persists on char.attrs via the SkillTreeScreen +1 button (see src/ui/screens/SkillTreeScreen.js) and re-renders after every spend.
Pet sprite placeholders: Skeletal Warrior + Wolf (M130)fixed
Root cause: pet_* ids weren't registered in CombatScreen SPRITE_MAP. Added 10 pet entries (pet_skeletal_warrior, pet_wolf, pet_bear, pet_imp, pet_demon, pet_war_hound, pet_fire_elemental, pet_lightning_elemental, pet_bone_golem, pet_familiar) so their spritecook assets resolve.
Druid + Necromancer battle sprites desync from image-reviewno-repro
Closed per user direction (M236). Multiple investigations (M151, M236) found no data mismatch: both druid_* and necromancer_* spritecook files exist at public/images/spritecook/, manifest entries are canonical, SPRITE_MAP entries wire correctly. M236 separately ships an image-review live-data rebuild that derives everything from the approved-sprite state, so if there's still any drift the new manifest will surface it — any future repro opens a NEW entry with a specific screenshot, not this old one.
Goblin shaman attack frame flipped (M130)fixed
Added `goblin_shaman` to `FLIP_EAST` in CombatScreen so the east-facing attack frame renders mirror-corrected. Structural image-review rewrite remains queued separately.
Image-review must mirror game dataset (structural)done — m162
scripts/build-image-review-manifest.cjs rewritten to derive all entity lists directly from canonical game data sources: APPEARANCES (appearances.js) for hero sprites, CLASS_PETS (companions.js) + reward.companion scan (randomEvents.js) for companions, ENEMIES + ENEMIES_ACT5 object keys (mapData.js) for enemies, and map node type:'boss' encounter fields resolved to primary boss entity IDs (mapData.js) for bosses. Manifest is now computed from game data — no manual edits needed. Filesystem scan is the secondary layer for orphan/background detection only.
Click handlers scroll to top throughout UI (M151)fixed
Audit: no `<form>` tags in src/ and no `href="#"` in src/. Two root causes found and fixed: (a) focused button being removed by innerHTML reassignment triggers the browser's "scroll focused element into view" heuristic — fixed by blurring `document.activeElement` before re-render in ScrollPreserve.js:24-29 and SkillTreeScreen.js:51-57; (b) `window.scrollTo(0, doc)` was guarded by `if (doc)`, so a mid-render jump from 0 never got restored — now always restores (ScrollPreserve.js:36-38, SkillTreeScreen.js:132). Also added `type="button"` to every `<button>` in src/ui/ defensively (~165 buttons). Affected: SkillTreeScreen (+1 attr / buy talent / buy passive), all preserveScroll consumers (Town/Inventory/Forge/Party/HireBuilder).
Companion sprites: skeleton, bone_golem, wolf, bear (M151 — already fixed in M130, no repro)no-repro
Audit complete. All four class pets are added to the party via SkillTreeScreen.js:446-452 with `templateId = petId` where petId is `pet_wolf` / `pet_bear` / `pet_skeletal_warrior` / `pet_bone_golem`. resolveSprite (appearances.js:83) returns templateId for class==='companion'. All four entries are already in CombatScreen SPRITE_MAP (CombatScreen.js:56-61, added in M130). Spritecook PNGs for all four exist in public/images/spritecook/ (pet_wolf_east.png, pet_bear_east.png, pet_skeletal_warrior_east.png, pet_bone_golem_east.png + south/attack/ko/portrait variants). No bare `wolf`/`bear`/`skeleton` templateIds are used anywhere in src/game. Item was stale after M130 fix.
Necromancer + Druid still render old pixellab sprites? (M151 — investigated, no repro)no-repro
Full trace: for heroes, resolveSprite (appearances.js:84) returns `templateId || classId || cls` → for a necromancer/druid hero with no explicit appearance, this yields 'necromancer' or 'druid'. SPRITE_MAP['necromancer']='necromancer' and ['druid']='druid' (CombatScreen.js:46,49). _loadSprite (CombatScreen.js:132-150) fetches `images/spritecook/{key}_{variant}.png` FIRST and only falls back to `images/sprites/` if the spritecook file 404s. Confirmed present: necromancer_east/south/east_attack/east_spell/east_block/east_ko.png and druid_* same set. No fallback path is taken. Cannot repro without a specific screenshot + browser trace.
Combat reward mismatch (M151 — investigated, no repro)no-repro
Traced CombatScreen._victory (CombatScreen.js:2184-2202): only ONE `generateItem()` call per drop, and both the display text (line 2291, `drops.map(d=>d.name)`) and the inventory grant (line 2201, `GameState.addToInventory(item)`) reference the same `item`/`drops` array. Display and grant cannot diverge. Likely user confusion: tap-weapon boss drops are shown on a separate line ("Tap Weapon obtained: {name}", line 2294) from the item-drop line — an unrelated ring could have dropped on the same combat and been mistaken for the tap weapon. No code fix warranted without a specific repro. If user sees it again, capture the full modal text + inventory screenshot.
Tavern war_dog portrait — wrong path (M129/M130)fixed
Root cause: `resolveSprite` returned the generic marker `'companion'` for companions whose class is `'companion'`. Now branches on that marker and uses `member.id` (war_dog, dire_wolf, etc) so hire cards, combat frames, and post-hire portraits all resolve to the same file.
Combat character placement — stagger + spread verticallydone — m161
Not priority. Stagger columns left/right/left/right and increase vertical separation so more individual characters are visible. Use the bottom ~70% of screen (top stays sky background). Large combat stage currently uses ~20% of vertical real estate. Mobile must still compact to fit. NOTE: positioning changes would also affect tap-weapon targeting (pixel-distance based) but NOT AoE skills (group/column based).
Hire Custom dialog — full redesign to match Create Character template (M242 partial)done — verified m416
All listed open items already landed: gold/brown palette via shared .cb-* classes, 480 max-width centering (HireBuilderScreen.js:272), scrollable appearance grid with max-height:240px (~3 tiles, line 907), estimated-stats panel (line 983), skill-points-remaining label (line 297). Random-name dice was prior. Verified M416 via code audit; flipping closed.
Game info consolidation onto front page (M242 partial)wip — content audit next
M242 landed the scaffolding: "Home" nav link added to shared nav-header, game-info page removed from disk + dropped from the Emberveil nav menu. Still open: the actual content migration — Tap Weapons, Combat System, Acts/Journey, Equipment, Companions, Timeline sections from the old game-info/index.html haven't been pasted into index.html yet. That copy needs a content-audit pass first — existing marketing copy has known drift vs current mechanics (quest content incomplete, dragon act not represented). Plus the promised scrolling .subnav with scroll-spy section indicator. Commit link for recovery of old content: pre-M242f HEAD on main; sections live under public/game-info/index.html in history.
Map structure expansion — longer paths per act (M250 deferred)deferred — balance risk
Ask: lengthen each zone's critical path to the spec ladder (Act 1b +1 node → 6; Act 2 → 7; Act 3 → 8; Act 4 → 9; Act 5+ → 10). Specific risk: adding path nodes adds more combat/dialog encounters between boss fights, which shifts XP + gold totals per act and can invalidate current balance-sim results. Needs a dedicated milestone with (a) sim rerun against every new encounter count, (b) a layout pass so new nodes don't clump visually, (c) explicit encounter ids for each new combat/dialog so the gameplay stays varied. Not silently shelved — queued as its own milestone with a balance-sim step.
Town in every map (M250)done
Six new towns added via _ACT_TOWN_INSERTS: Greenbough (thornwood @ goblin_camp), Emberwatch (ember_plateau @ veil_stronghold), The Last Bastion (shattered_core @ shard_fortress), Void Harbor (eternal_void @ unraveler_ante), Creation Rest (primordial_nexus @ architect_bridge), Scaleholt (dragon_throne @ dragon_fortress). All use discoverOnVisit so the town stays hidden on the map until the player reaches the attached mid-path node. MapScreen honors discoverOnVisit to skip drawing undiscovered towns. Return-to-Town still falls back to nearest visited town when the new town hasn't been reached yet. Plus town services/taverns inherit existing TOWN_CONFIG — follow-up to wire per-town hire rosters for the 6 new towns.
Hard-difficulty Thornwood Forest Warden fast-travel (M250)done
M250 introduced a waypoint: true flag on map nodes. thornwood/forest_enter (Forest Warden) is now a waypoint, so MapScreen draws the blue ring around it once visited and treats it as a valid click-teleport target. Waypoint teleport bypasses the Hard-mode walk-only rule since waypoints are the Hard alternative to the town teleport the mode disables. Future acts can add more waypoints by setting the flag.
Attack Speed runtime — default ON (M231/M236)shipped default-on
M231 shipped feature-flagged off. M236 flipped the default to ON so fast-tier weapons grant a bonus attack for every player without hunting for a toggle. Dagger / rapier / shortbow / dragonfang_dagger are Fast → +1 bonus attack per round. Very Fast → +2 (reserved for passives). Toggle still available at Settings → Debug → Attack Speed if the player wants to disable (sets key to '0'). STILL OPEN (not deferred — low priority polish, not blocking): attack-speed% overflow meter (15% → bonus at 100%), multi-attack animation retiming, Tap Weapons per-action counting. These are polish; the core runtime is live and in use.
Character redesign regen queue — 7 flagged fixesneeds credits
Specific blocker: requires SpriteCook API credits (each regen ≈ 80–120 credits × 7 sprites ≈ 600–900). User-set budget is ≤2000 credits/session, shared with cloud regens + new appearance variants below. Pending regens: chronomancer_female south (cut-off legs), tactician_male east_attack (scale), witch_hunter east_spell (scale), fighter_male skin tone, druid_male east_block (hood), mage east_block (boots), war_hound east_attack (no metal armor). Page infrastructure ready: bg-color toggle, pending-only filter (now default ON), M217 snapshot in version dropdown — all shipped M232/M236.
Attribute Rating + skill damage scaling + roll animation (M236)done
Rating applied two ways. (1) Skill damage via CombatScreen._ratingFromAttr — attributes ≤ 20 scale 1:1, > 20 soften to 0.6× per point. Caps solo-caster late-game one-shots the user flagged as too powerful. Plugs in at _getSkillStat chokepoint so every skill + healing path goes through it. Toggle off via emberveil_rating_scaling=0. (2) Roll animation — DialogScreen flashes a PASS/FAIL banner (green/red, 650ms) on skill-check resolution before the outcome text plays. Not done: a dedicated +N STR Rating affix entry (would add rolled gear that grants raw rating bonus without pumping the underlying attr). Open follow-up, not blocking.
Encounter / random-event seeding (M236)done
Random-event picker in src/maps/randomEvents.js::getRandomEvent now seeds from gs.gameSeed + zone id + seen-count. Falls back to Math.random only if game seed is null (pre-new-game init). MapScreen passes gs.gameSeed ?? null at the single call site. Map node layouts remain fixed per zone (deterministic by design).
Fast-travel legend (M236)done
Legend under the map gained a "Fast travel" entry (blue ring swatch matching the ring drawn around visited town nodes). Non-town waypoint unlocks and cross-zone teleport from a town to any other visited town were NOT shipped this cycle — they add new state (which nodes count as waypoints) and UX (town→town teleport menu). Not blocking; low-risk open polish.
Difficulty: Hard-mode cleric pricing + walk-only travel (M236)done
Cleric services now scale on Hard: Rest costs 25G per injured member; Revive costs max(50G, member.cost × 0.5) or level × 30 × 0.5 for generated hires. Hard disables fast-travel teleport (both the Return-to-Town button and the click-a-visited-town teleport in MapScreen). Hard preserves HP/MP between combats as shipped in M234. HP-bar visibility toggle (hide in town on Normal) was NOT shipped — low-value polish, bars already show the persistent HP and are not misleading.
Questline system + NPC dialog portraitsin progress (stub)
Specific risk: this is a full narrative feature (5-act main arc + 3 recurring NPCs rival/mentor/traitor, 2 side quests per act = ~12 side quests, bounty board, mobile-scrollable legend, dialog-portrait overlay framework with mood-on-mood-off rules). Shipping a partial authored arc would bake half-baked narrative into the save format. M236 lays the data-model groundwork (src/game/quests.js — to be added) and leaves the authoring pass as a dedicated next-milestone. Shipping this mid-session without quest art + writing review would create narrative "filler" that feels worse than empty. Not silently shelved — explicit priority callout for its own cycle.
Loot Chest system (M236)done (text-only)
Treasure nodes now open a Loot Chest modal with 2–3 item candidates (per zone-tier pool). Player picks ONE; others are discarded. Gold awarded regardless. Rarity fanfare via colored border + box-shadow per rarity tier (magic blue, rare gold, legendary orange). _openLootChest(node, {tier}) helper accepts 'standard'/'medium'/'high' so boss drops + skill-check branches can route through the same modal. Risk/open: no graphical chest sprite or beam-of-light open animation shipped this cycle — adding art requires SpriteCook credit burn for closed/open chest states that would need matching the existing pixel-art style. Text-only modal is in use and functional.
Magic-find rebalance + MF affix (M236)done
Act 1 combat drops rebalanced: border_roads rolls 50% chance to downgrade to normal rarity; thornwood 35%. Magic Find shifts both odds back up (every 1% MF = 1% less chance to downgrade). of_discovery affix added to AFFIXES_ACT1 (magicFind stat, 0.10–0.35 range). Party MF summed via CombatScreen._partyMagicFind() and cached on gs._partyMagicFind so loot chests read the same value. Applies to combat + chest drops. Shop stock intentionally unaffected (user spec).
Wishlist quick-select UI (M236)done
Added: floating control bar (top-right, mobile bottom) with three controls. (1) "Hide completed" toggle adds body.wl-hide-done and CSS hides done/dropped items AND entire sections/groups where every item is done. (2) "Quick Select" toggle enables per-item checkboxes + click-to-select. (3) "Copy (N)" writes the selected labels + descriptions to clipboard as a markdown bullet list. (4) "Reset Selection" clears everything. Supporting Mechanics horizontal-layout bug fixed by forcing .wl-group { display: block } + .wl-group .wl-item { width: 100% }. State persisted per-browser in localStorage.
scripts/audit-sprite-404s.cjs + live-data unification (M236)done
Two things shipped. (1) scripts/audit-sprite-404s.cjs — parses src/game/appearances.js + companions.js + mapData.js ENEMIES/ENEMIES_ACT5 and walks every expected sprite file under public/images/{spritecook,sprites,pixellab}. Reports missing by category. Current audit: 0 misses. Supports --json. (2) scripts/redirect-assets-to-spritecook.cjs — one-shot migration that rewrote 164 entries in public/assets/assets.json from ../images/sprites/ and ../images/portraits/ to ../images/spritecook/, using exact-basename match plus a *_portrait.png rename for bare class portraits. The /assets/#main gallery now shows the approved redesign sprites (necromancer drift resolved). Idempotent — safe to re-run after new sprites land in spritecook. image-review.html was already live-data driven via build-image-review-manifest.cjs from M162.
Clouds_07/08 regen via SpriteCook (M236)done
Generated clouds_07.png and clouds_08.png via SpriteCook (12 credits each — much cheaper than estimated). Both are 1024×256 pixel-art tileable red-tinted cloud strips. 08 used 07 as a reference asset for style lock. Wired into TitleScreen.js as a three-layer parallax: 08 back (slowest, 0.35α), 07 mid (0.018 speed, 0.5α), 06 front (drifting cloud-object layer, unchanged). Credits spent: 24. Remaining: ~2596.