Skip to content

Latest commit

 

History

History
207 lines (104 loc) · 94.4 KB

File metadata and controls

207 lines (104 loc) · 94.4 KB

Dev Log

2026-07-20

Three threads today: the skill-check system (#83) shipped its first two phases, a playtest drove a sweep through the half-inert item economy, and a content decision about Heroic Fantasy tier 3 was made and reverted the same evening. The headline is #83, engine-rolled skill checks, which extends the #76 "engine referees, LLM narrates" contract to social play. Phase 1: the model proposes a check by emitting a [CHECK: skill, tier] marker and stops; the engine rolls instantly and locally using the same modifier stack as combat (stat mod, gear, level bonus, Lead/Support), posts a visible d20 breakdown as a SYSTEM line, and injects the result into the next prompt for the model to narrate as fact, so one generation covers the consequence, not two. Skill words and synonyms canonicalize to real skills (an unrecognizable one is rejected rather than mis-rolled), and a raw-number difficulty is clamped to the nearest tier so the model cannot hand itself an easy DC. Phase 2 made failure stick: a failed check records a lock in a save-persisted ledger keyed on (location, target, skill), so a reload is not a free reroll and rephrasing cannot farm retries. Two defenses layer up: spent approaches are injected into every prompt so the model steers away (soft), and a re-proposed locked check is refused outright with no fresh die (hard). Locks clear on a long rest or on entering a different town or site; stepping out to the world map and straight back in resets nothing, success never locks, and milestone progression never routes through a check, so the golden path cannot dead-end. A same-day hotfix fixed a mount crash the suite missed: the Phase 2 lock-clear effect referenced mapHook above its declaration (temporal dead zone), and nothing caught it because Game.js has no unit render and the Express e2e stalls before Game mounts.

The playtest sweep found the item economy quietly broken in several places. Two consumables were pure dead loot: Healing Herbs lacked the effect:'heal' field every other healing item carries, and Minstrel's Blessing was an unhandled buff type with value 0; both are now real heal consumables. Labels got honest too: weapon bonuses read a bare "+1" with no stat (now "+1 attack", matching armour's wording), and shop wares now show a short effect line beside the price so buyers can see what they are paying for. The big one: weapon attack bonuses now scale with rarity (common +1, uncommon +1, rare +2, very rare +3, legendary +4), so a green and a blue weapon are no longer mechanically identical. The "Minimal" curve was chosen deliberately after a Gentle curve (uncommon +2) was tried and reverted for buffing the mid-gear baseline enough to trivialize the four tier-1 bosses; Minimal keeps the sim's mid loadout at +1, so every progressionLint band stays identical and no boss retune was needed. Site combat also became click-to-attack (a fled, de-aggroed mob could previously never be finished off; clicking now engages or walks the party adjacent), and the obsolete express-smoke e2e was dropped in favour of the guest-save-reload-resume spec that superseded it.

The evening detour: Heroic Fantasy tier 3 was removed, then restored. The removal (capping the arc at tier 2) was aimed at a blocked "The Shattered Throne" card on the new-game page, but it deleted both the comingSoon built-in and the shop-window teaser stub, so any session without server delivery (guests, fresh browsers) lost the t3 row entirely, and a vanished row is worse UX than the blocked card. The revert restored the greyed "sign in to play" teaser, and diagnosis moved to the real cause: the "Members" lock a signed-in free user sees is stale production data, a premium_templates row deployed 2026-07-06 as minTier: 'member' and never redeployed after the source flipped to 'free'. The fix is a one-row redeploy via deploy-campaigns.mjs, not a code change.

Decisions & notes: #83 deliberately duplicates the roll math instead of sharing it with encounterResolver, to converge later without risking combat; Phases 3 (fail-forward and faction consequences, crit-fail escalation, authored dcBias) and 4 (BG3-style animated d20) are deferred, and the lock ledger's structured persistence lays the RAG-sync groundwork without wiring it. The Minimal weapon curve leaves common equal to uncommon at +1; splitting those needs the baseline-buff plus boss-retune session, deferred as a real balance pass. The hf-t3 detour hardened a working rule: a locked, blocked, or missing card is a gate-to-delivery-to-cache problem (usually stale data), so diagnose delivery before deleting authored content.

2026-07-19

Two substantial features landed, one closing a long-running architectural sore point and one fixing a world-feel problem straight out of the previous day's playtest. The headline is #76 Phase 1: engine referees, LLM narrates. Milestone and campaign completion was the last surface where the LLM adjudicated game state, via free-text [COMPLETE_MILESTONE]/[COMPLETE_CAMPAIGN] markers parsed by the client, and the 2026-07-15 model trial showed every candidate model (incumbent included) misfiring premature markers, with no prompt guard able to fix the class. So the judgment itself was removed: DM_PROTOCOL lost its milestone-tracking and campaign-completion blocks and gained an explicit "outcomes are the engine's, narrate, never adjudicate" rule; the client marker-parsing path in useGameInteraction was deleted (milestones now complete only on engine events, and campaign completion rides the engine's final-milestone check, which was already wired and made the marker redundant). Any marker a model still leaks is stripped as defense in depth in three places (client response cleaning plus both sanitize passes, worker and dev-server side), while the [CHECK/ROLL] skill-check trigger was deliberately kept as #83's bounded surface. The last producer of narrative milestones, NewGame's "Speak with the {role}" slot, became a talk milestone, and a load-time migration converts any surviving narrative milestone in old saves to its engine type off its spawn, so no legacy save's objective or campaign is stranded.

The second feature is living sites: cleared caves stayed empty forever because a site is populated once, its slot mobs cleared for good, with no respawn, which read as a dead world in the 2026-07-19 playtest. An ambient-creature layer now sits on top of the frozen layout without violating the generate-once-never-regenerate map rule: populateSite seeds two ambient roamers at generation (with the rng consumed last so existing per-seed layouts stay byte-identical), and repopulateSiteRoamers tops a site back up to target on every entry, so cleared and old cached sites re-colonise retroactively while fresh sites no-op. Roamers are ordinary wandering mobs that start idle and only wake inside aggro radius, drawn from a gentle pool (capped at MEDIUM difficulty, then level-gated); authored bosses and objectives never respawn. The wandering cap and per-step chance were nudged up to sit on top of the seeded baseline, and cave_kobolds was retuned (dc12/hp24 to dc15/hp32) after progression lint flagged it as a ~95% walkover outside the wilderness win band.

Decisions & notes: #76's fix is removal, not mitigation: since no prompt guard could stop premature markers across every model tested, the model simply no longer holds the authority, and mechanical milestones were already engine-run so this unifies the whole system under one referee. Phase 2 (few-shot exemplar bank, code-appended "What do you do?", scripted beats for emotional peaks, dead-helper cleanup) is deferred pending maintainer taste. Living sites follows the established additive-injector contract (same as harvest resources and site objectives), which is what lets it heal old cached saves with no migration.

2026-07-18

A long live-playtest session drove almost everything today: sixteen commits of fixes, balance work, and cleanup, most traceable to something that actually went wrong at the table. The nastiest class was the site-mob infinite loop: single-round hazards (sudden storm, then the bat swarm) entered the explorable-site combat pool as chasing mobs, but a mob is only cleared on a combat "victory", which a one-round skill-check encounter never reports, so the party was trapped re-fighting the same storm forever. The fix generalized into a rule with two layers: the wandering roll routes ALL single-round encounters to a one-off event modal instead of spawning a mob, and any single-round mob that does slip through clears on any non-flee resolution. Around it came a wider combat-resolution audit sweep in the same "strand the player" class: milestone combats are now forced multi-round so a boss can't silently fail to complete its milestone (new MS-08 audit rule), a stalemate/defeat/failed-flee repositions the party and de-aggros the mob so "you broke off" is true, and a full party wipe now revives everyone at half HP back in the nearest town instead of dead-ending the save.

Towns and caves each got a habitability pass. Towns were nearly impassable: the town path opened every encounter as a blocking fight modal regardless of tier (the Bustling Market forced a skill roll plus a 70% flee to escape, every few steps), and the "every 3rd tile" throttle reset on each click so short hops always rolled. Narrative town encounters are now non-blocking flavor notes, the throttle is an honest persistent per-visit counter, and the tavern brawl (the one real town combat) moved off random street walking entirely: it now fires only when you visit a tavern or inn, on its own cooldown. Town walls became solid (the hero could previously stroll through city perimeters), and side-quest turn-in buildings pulse the same gold border as milestone buildings so you can see where to hand a quest in. Caves went from monotonous bat spam to a varied, level-safe crawl: the per-step wandering roll got its own throttled rate (bats dropped from ~7.5% per step to ~1.8%), three new balance-simmed easy/medium spawns landed (kobolds, lurker, a rockfall hazard) plus a revived giant-rats table entry, the wandering pool is level-capped so a low party never randomly meets the deadly guardian, and the difficulty band now always spans more than one tier, ending the "three spider nests in a row" wall a mid-level party hit in playtest.

The playtest also surfaced quest and content-plumbing work. Side quests now carry a real origin town: resolveQuestOrigin picks the giver's town (preferring one already mapped so its actual generated building name is usable) and stamps it immutably at offer time, so hints read "Return to The Ironbond Chapterhouse in Millhaven" instead of inventing a guild, and turn-ins only fire at the right town. Partial gather progress ("collect 3 glowing fungi") is now persisted on every change rather than only on step completion, which had been silently discarding progress every pickup. A spawn-system audit fixed the authored encounterTier field being dead (recomputed three inconsistent ways at roll time; it is now the source of truth in all three roll paths) and revived the orphaned mountain_hermit_cave template, with a new ENC-07 audit rule guarding the unreachable-template class going forward. Alongside all this: #79 Thread B combat-UI cleanup continued (flee gated to encounters that can hurt you, the redundant "Claim Victory" interstitial removed, the item picker converted from a modal-in-a-modal to an inline tray), the four settlement-arrival images were regenerated as unified 1024x1024 painterly-dusk webp to match the square-aware modal sizing, DeepSeek V3.2 was added to the premium registry as an inert US-pinned candidate (a new providerOnly field prevents routing to Chinese-hosted endpoints; not the default, reachable only by explicit modelId), and the half-working Freeform (AI-generate) campaign tab was removed until #77 rebuilds AI generation with proper engine wiring.

Decisions & notes: "single-round encounters never become chasing mobs" is now a structural rule, not a per-encounter patch, with a belt-and-braces clear path as the second layer. Selection variety and threat assessment were deliberately separated: the wider spawn band lives in difficultyBandForLevel while threat badges keep their own narrower threatBandForLevel, so an easy foe still reads "Trivial" even though it can now spawn for a mid party. Freeform's removal is a scope call, not a loss: its untyped milestones could never complete through the engine, so it goes away until #77 does it properly. Several of the fixes (town walls, encounter tiers) are view-layer or roll-time predicates, so they heal cached saves retroactively without data migration.

2026-07-17

The combat-UX rework (#79) landed its keystone plus the immediate follow-on polish. The headline is a behavior-preserving extraction of the fight flow into a headless useEncounterFight hook: the entire encounter phase machine (formation → initiative → action → resolving → round result → final), all ~15 state hooks, and every handler moved verbatim out of the 1439-line EncounterActionModal, leaving the modal a thin ~919-line view over the hook. The engine files (encounterResolver, multiRoundEncounter) were untouched, and the resolveAndClose exit contract (every fight exit flushes through onResolve, with the heroIndex attach rules and the load-bearing 50ms defer preserved) is now pinned by a new 26-test useEncounterFight.test.js covering the behavior checklist — init-identity guard, the preserved same-hero initiative-message quirk, single/multi-round orchestration, item round-spend and cap overflow, flee formulas, and the exit contract. Suite went 1934 → 1960, all green. The COMBAT_UX_PLAN "Thread B" cleanups (spinner, interstitials, item-picker) were deliberately left out so this PR was the move only.

With the hook in place, the modal itself got fitted to the viewport and its art shown properly. The encounter modal is the core gameplay surface but was cramped — a scrollbar during fights and a wide-but-short crop of the square 640×640 encounter art. Header chips were regrouped onto sized-to-match rows (Threat + Round; Enemy Morale + Your Advantage collapsed to one line; Use Item + Flee sharing a row), paddings tightened so no fight phase scrolls across 900–1300px viewports, and the modal may now stretch to calc(100vh - 8px) and hand the reclaimed height to the art. ClickableImage gained a centered maxWidth and a maxHeight that defaults to height (dropping the old 240px cap), so viewport-elastic clamp() heights let the square art fill whatever each screen and phase leave free; non-combat encounters (no HP/morale below) hide the player HP bar and give that height to the image. Two Thread B cleanups rode along here: the "Resolving…" spinner was removed as latency theater (combat resolves locally, so it only ever flashed a wait that does not exist; isResolving stays to disable buttons), and the dead EncounterActionModalSimple.js was deleted. A companion debug fix repaired three encounter harnesses (EncounterModalDebug, EncounterTest, EncounterVisualDebug) that predated the ModalContext migration and still passed isOpen/onClose/encounter as props the modal ignores — each flipped its own local state and logged success while nothing rendered; they now open through useModal('encounterAction') with Game.js's payload shape, and a non-combat Wandering Minstrel sample was added to exercise the HP-bar-hidden path. This surfaced while runtime-verifying the extraction.

The day also added a design doc: MAP_LAYOUT_PLAN.md (#84, HIGH PRIORITY) proposing the map modal become a two-pane workspace (docked map plus a persistent Adventure Log pane, tabbed on mobile) so the narration log the map feeds is no longer hidden and the reopen-around-encounters churn dissolves, converging with #79's eventual open-play stage. Backlog and doc-map entries were added; the mockups fold into the #82 landing/design pass.

Decisions & notes: the useEncounterFight extraction was strictly a move — no mechanics changed, the render tree is untouched except two inline handlers now calling named hook handlers with identical logic, and the 72 fight tests plus the new hook tests gate it. Thread B was split intentionally so the risky extraction shipped alone before the cosmetic spinner/dead-code cleanups. #84 is a plan only, staged behind the #82 design pass.

2026-07-16

A planning day rather than a shipping one: the backlog was audited, resequenced into a single working priority queue, and six new design docs went in for a cluster of ideas the maintainer wants tackled next. OUTSTANDING_ISSUES.md gained a priority queue (P0-P5, superseding the 2026-07-03 parallelization map for ordering) — P0 clears small decks-clearing items, P1 is the two strategic maintainer-flagged rows (landing page, admin dashboard), P2 is "the engine-referee program" (narration contract → skill checks → AI quest generation, deliberately scheduled together since they share the same DM_PROTOCOL prompt surgery), P3 is revenue completion, P4 is product feel, P5 is deliberate backburner. Two backlog rows were then struck outright: #1 (OTP email) was verified resolved — the magic-link email carries no OTP code and the row predated the Octonion auth centralization — and #30 (admin read views) is covered by the existing internal admin tooling.

The bulk of the day was six new docs capturing 2026-07-15/16 brainstorms referenced by rows #76-#81, each written as a design doc rather than code so the ideas have a landing spot before scheduling: AI_NARRATION_CONTRACT.md (#76, decided 2026-07-15 — "engine referees, LLM narrates" for milestone completion, killing the premature-marker-misfire class the model trial surfaced), AI_QUEST_GENERATION_PLAN.md (#77 — lean on AI for campaign prose while the engine keeps deterministic structure; Freeform's "Generate with AI" button shipped gated to Members+ same day), COMBAT_UX_PLAN.md (#79 — extract a headless useEncounterFight hook out of the ~1440-line EncounterActionModal as the keystone unlocking animation, modal refine, and an eventual open-play rework), GAME_FEEL_PLAN.md (#78 — audio/visual juice phased from free CSS button glow through a premium music system), MOOD_THEMING_PLAN.md (#80 — grimdark/eldritch as a mood layer over any biome, retroactive via the existing grimness/darkness dials), and dungeongpt-tts-notes.md (#81 — Workers AI Aura + R2 hash-cache for Phase 1, split between cheap cached authored lines and pricier live LLM narration). SKILL_CHECK_PLAN.md (#83, flagged HIGH PRIORITY) got the most design weight: engine-rolled d20 checks for narrative actions like persuade/intimidate/sneak, replacing the current dead-end where the LLM alone decides success — the core rule is that failure must change world state (fail-forward costs, complications, an NPC-topic lockout ledger) rather than just blocking, and a failed check can never dead-end the golden path. The day closed with a deploy-state close-out audit confirming the entire premium/save-sync stack (#39, #40, #54, #62, #72, #73, redemption codes) is merged and migrations 002-006 are applied in production, emptying P0 of the new priority queue.

Decisions & notes: #76/#83/#77 are deliberately sequenced together because they share one prompt-surgery pass (the DM_PROTOCOL rewrite) and one few-shot exemplar bank. #82 (landing page) was independently confirmed as the top P1 item — it's flagged as a known competitor-analysis gap where a weak site specifically undercuts the credibility-based differentiation pitch, not just a cosmetic lag. All six 07-16 docs are captured as "idea/design done, not yet scheduled" — none of this is implementation, it's making the next quarter's work legible before starting it.

2026-07-15

A billing-authority day: the Octonion hub (octonion.io) started becoming the single source of truth for who has paid for what, followed by a small run of Profile-page fixes that surfaced hub grants honestly. The headline is hub payments Phase 1: the Worker gained a hub-entitlements helper (GET /api/me/entitlements forwarding the user's JWT, with a 60s per-user cache mirroring the JWKS pattern, a 3s timeout, and a fail-closed-to-free posture where failures stay uncached) plus a new GET /api/entitlements route that reports MAX(local tier, hub tier), so existing manual grants and redemption codes never regress while billing moves hubward. The hub's members rung is normalized to the game ladder's member in exactly one place (hubTierToGameTier) for later enforcement to reuse, and the client now fetches the new route while keeping the raw hub snapshot purely as display metadata: every gate still keys on the game-ladder tier. New config is a HUB_URL var in wrangler.toml.

The rest was Profile polish around lifetime membership. A lifetime hub grant (Founder unlock or grandfathered tester) has no end date, but the membership row had been showing the local grant's shorter expiry ("active until 06/08/2026"), a date sooner than what the player actually owns; a new display-only getHubLifetimeGameTier() maps a hub lifetime grant onto the game ladder and Profile now prefers "lifetime" when that is the rung being shown, while a genuinely higher time-boxed local grant (e.g. Premium-until-August stacked on lifetime Member) still shows its honest end date. Then a styling fix: the tier-badge/tier-note classes had never existed in any stylesheet, so "lifetime" rendered as tiny plain text; new profile.css styles the membership row and renders Lifetime as a gold pill (Cinzel small-caps, tinted on --primary) to mirror the LIFETIME badge on the hub account page. The home page also got a new tagline: "handcrafted campaigns, brought to life by AI."

Decisions & notes: The hub is now the intended single billing authority, but the migration is deliberately additive and non-regressing — the server reports the max of local and hub tiers, so redemption codes and manual grants keep working through the transition, and hub values never drive a gate (they are display-only on the client). The membersmember rename lives in one function precisely so Phase 3 enforcement reuses it. Fail-closed (to free, uncached on error) is the chosen posture for the hub fetch, matching the money-surface caution used elsewhere.

2026-07-12

A combat-feel and consumables day, mostly playtest-response polish layered on the previous day's movement and mob work. The headline is that round-capped fights now end in a stalemate rather than a defeat. The multi-round outcome checks were reordered so a fight that simply runs out of rounds resolves as the softer STALEMATE before the advantage-floor DEFEAT can fire: HP/morale still win outright, and a genuine mid-fight rout (advantage collapsing on any round before the cap) still defeats you, but hitting the cap on the same round your advantage bottomed out no longer scores the harsher loss. STALEMATE was then fully wired: generateEncounterSummary treats it like a chosen disengage (no loot, keep all gear, no defeat penalty) but grants a reduced 0.6x XP, slightly below fleeing's 0.7x since you traded blows to the end rather than running. A follow-up folded the "No spoils, but you keep your gear" note into the existing loss-reason line so a stalemate reads as one coherent message instead of two duplicated paragraphs, and the final card now shows the enemy's remaining HP on any defeat or stalemate so a loss is legible. Relatedly, some authored failure-tier narration was rewritten because it described the player winning: a losing round could render under a red "Failure" badge while the prose said "You defeat the spiders." Three cave/ruins strings were fixed, and a new guard test walks every consequences block across the encounter templates, quest enemies, and story templates, failing if a failure/criticalFailure string casts "you" as the subject of a victory verb.

The other big thread was consumables becoming real combat tools. The previously inert Fire Scroll now deals 3d6 damage to an enemy (combatOnly), and the inert antidote (nothing to cure, there is no status system) was repurposed as a small 1d4+1 heal through the existing heal path. The consume path in inventorySystem.js was generalized: isConsumable (heal OR spell), a consumeSpellItem that rolls damage and decrements the scroll off its owner without ever touching hero HP, and a consumeConsumable dispatch by effect, with applyItemDamageRound in multiRoundEncounter.js mirroring the enemy-side round logic (subtract damage, nudge morale, spend the round, resolve victory or timeout) so combat math stays out of the modal. The in-combat item picker now offers spell scrolls too, gated to a live multi-round fight with an enemy present, branching by effect (heal keeps pick-a-hero, spell skips it and fires at the enemy). Item detail modals gained "Deals: 3d6 (3 to 18)" and "Restores: 2d4+2 HP (4 to 10)" rows via shared describeSpellDamage/describeHealAmount helpers, and scroll_fireball was added to the alchemist's stock. On the site side, an immediate-tier wandering roll now spawns a visible, already-aggro'd mob a few tiles from the party instead of popping a combat modal out of nowhere: it renders with its threat ring, chases via the existing mob-stepping path, and engages on contact so you can see it coming and flee. A new pure spawnWanderingMob picks a walkable, unoccupied, in-band, reachable tile, capped at two live wandering mobs (over cap consumes the roll silently) and falling back to opening the fight if no spawn tile is found, so a monster is never lost. This is sites-only, since the mob layer only exists there; world and town rolls still use the modal. Rounding it out, inventory UI polish removed two more nested double-scrollbars (item shelf and loadout view now let the Adventure Book body provide the single scroll) and raised the Adventure Book height to min(95vh, 1200px).

Decisions & notes: Only the check order changed in the stalemate fix; no advantage math, damage tables, round-cap computation, or thresholds moved, and the party-wipe terminal stays a defeat regardless of round. Stalemate XP (0.6x) is deliberately set just below fleeing (0.7x). Spell scrolls never touch a hero's HP and are only offered in a live multi-round fight, never in single-round encounters or the out-of-combat party picker. The wandering-mob spawn is confined to sites on purpose because the entity/mob layer does not exist on the world map or in towns.

2026-07-11

An exploration-and-combat feel day: the way you move through towns and sites was rebuilt, foes inside those sites were turned into things that hunt you, fleeing was made to actually work, and the whole encounter cadence was retuned around the new movement so fights land where they should. The headline is free tile-to-tile movement. Town and POI-site movement no longer caps at five tiles; the party now walks to any reachable tile, stepping along the BFS shortest path at a tuned ~400ms/tile via a new pure tileWalk.js (computeWalkPath + a timer-injectable runTileWalk), halting on the exact tile where a random or fixed encounter fires (world-map movement untouched). Loot/objective/location pickups are non-blocking; only combat stops the walk, and a fresh click cancels an in-progress walk and restarts from the current position. The switch from teleport-hop to a connected BFS path exposed stale cached town data: the old 5-tile hop skipped over water and masked wrong walkable:false flags, so a path-checked walk turned mis-flagged bridges and shores into impassable walls. Because town maps are generated once and cached forever, the fix derives walkability from tile type (only water and buildings are ever non-walkable) instead of the possibly-stale boolean, healing old saves retroactively without rewriting the stored grid; the town per-step mover was pointed at that same isTownTileWalkable, plus a TDZ fix from ordering the walk-cancel effect below the map hook.

On top of that movement, site foes became moving entities that force the fight. Previously a site's combat was static tile content the party could route around; now fightable foes live in site.mobs (a pure, headless-testable mobMovement.js) with idle/alerted/aggro states, waking within an aggro radius and taking BFS steps toward the party, leashing home when they give up. Hard foes become speed-2 "hunters" that are unavoidable once alerted, while a milestone boss becomes a stationary speed-0 guard holding the objective tile. Game.js's site step advances the mobs first and enforces at most one halt per step (mob contact, else fixed content, else the wandering-monster roll). That made fleeing matter, and fleeing was broken three ways: no disengage (the party stayed on the threat tile), single-round encounters had no flee button, and "Tactical Retreat" crashed because it wasn't in suggestedActions. All three were fixed: the pre-encounter tile is captured at move time and restored on a flee, the flee button now renders in the single-round phase, resolveEncounter recognizes a shared contextual-actions map instead of throwing, and a distinct fled/escaped outcome skips the defeat path. Even then a fled mob re-closed and re-contacted immediately (a speed-2 hunter the very next step), so a successful flee now briefly de-aggros that specific mob (a fleeCooldown forcing it idle and stepping it back home) so the reposition actually breaks contact.

The new per-step movement broke the old encounter math, which was tuned for one roll per multi-tile hop, so encounter rates were retuned. Town rolls fired far too often: the pity/streak bonus is an absolute add that dwarfed the tiny town base, and rolling once per tile compounded a 6-tile crossing to ~59-87% chance of a fight. Towns are now exempted from the pity bonus and roll only every third entered tile (the party still animates every tile), dropping a 6-tile crossing to ~13%; site and world rolls are untouched. Outdoor/weather encounters were also stopped from firing inside caves. Two visibility features rounded out combat: a relative-threat indicator (a shared threat.js comparing a mob's difficulty against the party-level band) draws a colored ring on site mobs (green trivial → gold fair → orange tough → red deadly, with a distinct pulsing ring for hunters) and a matching "Threat" chip in the combat modal across all encounter types, purely display-only and backwards-safe. And consumable use was fixed and extended: the Party inventory "Use" button had silently rendered its hero-picker below the fold, so it became a fixed centered overlay, a new "Use Item" action was added inside combat (consuming the round in a multi-round fight, since items are finite), the roll/heal/decrement/pooled-owner logic was lifted into a shared consumeHealingItem helper, and the in-combat heal picker was given hero names. Also: the Quest Board now offers a side quest on success through the shared rumour flow, settlement arrival images (city/town/village/hamlet) plus dedicated quest-item, pine-resin and armor icons landed (unpinning the earlier generic placeholders), and completed milestone POIs stopped glowing.

Decisions & notes: The mob "hybrid AI" was deliberate: random slot foes aggro-chase, but a milestone boss is a stationary guard rather than a hard gate (guardian-via-impassable-tile was intentionally not wired, since the guard already forces the fight in the deep room). The flee-cooldown and the reposition compose on purpose (reposition buys distance, the cooldown protects it). The walkability heal reads tile type instead of the stored walkable flag because the generator invariant guarantees only water and buildings are non-walkable, so it is safe to override stale cached saves without a data migration. Town encounter rolling was gated to every third tile rather than lowering the base further so the base rate stays legible for the untouched site/world paths.

2026-07-10

A dense playtest-response and hardening day that touched nearly every game surface, with the headline being a full root-cause and fix of the single-device save-fork data loss. The client fork/reconcile/resume path could strand a player's newest turns with no second device ever involved, from four linked defects: the reconcile pass read a stale local snapshot and an un-queued cloud rev outside the save lock, so in the window after a live save committed R→R+1 but before the local base_rev caught up it saw R+1 > R and false-forked; a lagged or failed markSynced looked like a divergence; and on any fork the live session kept pointing at the abandoned ancestor, so a reload rehydrated the row without the newest turns. The fix routes the per-row divergence check and any fork through the same per-session save queue as save() (re-reading cloud rev and local row fresh inside the lock), compares against the greater of the local base_rev and this device's committed sessionBaseRev, and on a fork repoints activeGameSessionId (in memory and localStorage) at the parked copy holding the newest turns, carrying the user-set save name onto it. Genuine cross-device advances still fork as designed. Supporting worker fixes scoped the conversation upsert and its guard-miss lookup by owner, made the conversations list query lightweight, and set Cache-Control: no-store on all /api/db responses. Complementing the data fix on the visibility side, a new always-present SaveSyncIndicator next to the Save button surfaces the existing save ack (Saving / Saved / Saved locally / Save issue) for the previously-silent 30s autosave and every setTimeout(performSave) caller, via a pure deriveSaveIndicatorState in useGamePersistence; a guest's local-only result reads as a plain "Saved" since local is their store, only a signed-in player whose cloud push fell back to local sees the degraded state. Also fixed here: the save fingerprint now includes map state, so a map-only change (e.g. exploring) is no longer skipped as a no-op write.

The rest of the day was playtest-driven feel and correctness work. Combat got a computed, varying starting advantage (lead's stat + attack + level + a share of party support vs the encounter DC, clamped to [-2,+2] and centered so a Lv1 mid-gear hero vs a DC15 fight scores ~0), surfaced from the start of the fight instead of only after round 1 had already been lost; non-quest wilderness/site combats are now level-matched to party level (the immediate pool had been dominated by hard/deadly foes, so a Lv1 solo won the deadly Treasure Guardian ~3.4%), with deadly reserved for authored bosses and level-appropriate filler foes added; and player failures no longer raise enemy morale. Milestones/quests replaced the silent location-arrival auto-complete with a deliberate "Search this location" action on the POI modal (re-opening with fresh state so a co-located boss still unlocks on the same visit), made the talk-milestone button read as a real quest action, gave rumor/side-quest offers real context (title, objective, reward), and re-supply gather-quest harvest resources on site entry. Inventory/rewards gained per-item flavor descriptions and a click-to-open item-detail modal (description moved out of the list into the modal), hero name + class on each loadout card, and reward-log text that resolves item ids to display names. Campaign continuation fixed a completion-modal freeze (the "Continue your legend" picker auto-opened over the world map whose bare FocusTrap swallowed every outside click, deadening the picker until a hard refresh; fixed with allowOutsideClick), made the modal show only next-tier + fresh-start cards, and made the continuation prologue reflect healed heroes and drop stale status tags.

Two more correctness threads and a batch of polish rounded it out. A silent campaign-completion bug: every tier-1 campaign's final combat milestone carried minLevel: 2, but a main-quest party reaches the boss at level 1 (~150-175 XP vs the 300 needed), so checkMilestoneCompletion returned level_blocked and the campaign could never be finished; the gate was set to null on all six tier-1 finales (defeating the boss is the qualification), a system message now surfaces on level_blocked instead of a dead click, and an MS-07 audit was added so an unreachable gate can't ship again. Hero names got a safe-character allowlist enforced both client-side and, as defense in depth, server-side in the worker (400 invalid_hero_name), stopping injection-shaped names from ever reaching storage or a prompt. Polish: new-game launch settings are now persisted so a reload can't lose the campaign, hero-selection refetches on mount, auth-loading is treated as not-yet-guest at adventure start, town quest-building pickups land in inventory, visible milestone POIs got a glowing findability border, mountain interiors inherit the authored world-tile name, the definite-article doubling in tile/POI modals was fixed, Continue Journey keeps the world map open, and the art queue registered the Bitter Cold hazard image plus flagged borrowed quest-item icons as tracked placeholders.

Decisions & notes: The worker's rev-guarded upsert was correct throughout and deliberately left untouched; every save-fork defect was client-side (stale reads outside the lock, a fork that stranded the live session on the ancestor). "Saved locally" is shown only to signed-in players whose cloud push degraded, since local is a guest's store. The tier-1 minLevel gate was a genuine bug (unreachable), but tier-2+ gates are left in place because carried-over XP makes them reachable. The location-milestone search action reuses the exact location_visited event, so rewards/codex/ledger/idempotency and the completability harness are unchanged.

2026-07-09

A focused art-completion day: milestone POIs, which had all rendered as one shared red pennant on the world map, got 17 distinctive per-POI sprites (ice cave, dark keep, standing-stone circle, lighthouse, foundry, barrow, a bolder trilithon-doorway for the famine barrow, and the rest), wired into worldTileArt's poiSprite keyed on tile.poi and placed ahead of the generic milestone() flag so any unlisted or absent id still falls through to the pennant. Because tile art is a pure view layer, this re-skins every existing save with no data migration. The content audit reconciled to match: MAP-02 now passes on real coverage (the POI_SPRITE_TYPES mirror was extended and CONTENT_AUDIT.md updated), moving the suite from 31 passed / 3 warn to 32 passed / 2 warn, with worldTileArt.test.js gaining per-id distinctness and fallback assertions.

2026-07-08

A dense day with four intertwined threads: the new-game opening was rebuilt to stop hallucinating, a headless quest-testing harness was stood up to evaluate the premium pool end to end, the milestone content got a hardening and completeness pass, and a nasty save-fork bug was root-caused. The opening rebuild ran through several iterations before landing on the right shape. The AI opening had been a single under-grounded prompt that dumped every active milestone's NPC and venue into one scene, so a start in Icemoor could describe Frosthollow's Lodge and Warden Sigrun; the first fix split it into two grounded stages (scene from the current town only, objective from the current milestone framed as a place to travel to). But the LLM kept inventing towns and chaseable NPCs even then, so the final move (fcf06ea) removed its authority to compose the scene at all: composeIntro became the single authored opening builder used by everyone (scene from start town + biome + shortDescription, only real placed NPCs; objective naming the destination settlement via the #69 logic). Signed-in players get a tightly bounded LLM polish pass that may only reword the authored text, validated by isPolishSafe (must still name both towns, stay near the original length) or it falls back to the authored version verbatim; guests get authored verbatim. Only the opening is authored, every later turn stays fully AI-narrated. The quest-testing harness (scripts/quest-harness.mjs) grew from a guarded headless OpenRouter path into a full evaluation rig: deterministic guest/member quest-completion simulation, a --premium-dir flag to safely simulate the private premium templates, and a --playthrough mode that composes the opening plus one scripted player turn per milestone (in requires order), sends each through the same guarded OpenRouter path (dry-run unless --live --yes), and records every prompt, response, and usage to gitignored transcripts, with light automated checks for refusals, prompt-echo, and marker presence. A heroic-fantasy-t1 dry run projected 6 requests at trivial cost.

The milestone content got both a completeness and a hardening pass. Talk milestones gained dual completion (the Talk button or the AI [COMPLETE_MILESTONE] marker), Warden Sigrun and two arcane narrative beats were converted to talk so guests can actually finish them without a paid model, and frozen-frontier-t2 was resequenced with two talk beats buttoned. On the hardening side, the authoring invariants that fail silently at runtime were addressed at the source and then written into CLAUDE.md: every quest item now carries a unique id so a stray loot drop cannot complete a milestone in the wrong town, hidden milestone POIs stopped leaking their id in the hover tooltip, and a content-integrity audit suite plus a generated CAMPAIGN_MILESTONES.md (with a drift guard) now enforce coverage. Two debt queues were cleared: name generators for 12 more building types (clearing the building-name audit) and arrival images for all 16 milestone POIs (clearing the arrival-image audit). Rounding out the day: a save-fork bug was root-caused (a1fa2cf) where unserialized same-device saves raced the optimistic-concurrency rev counter and forked a "diverged on this device" copy recursively, with no second device ever involved; saves are now serialized per session via a promise queue so a surviving 409 means only a genuine other-device advance. Saved-games UI switched to showing the campaign arc name (dropping AI provider/model) and now persists renames across the next autosave, environmental encounters became climate-aware (a snow campaign can no longer roll "Scorching Heat", and a new "Bitter Cold" encounter was added), the new-game flow gained a begin-campaign button that advances to hero selection, and the AI-settings panel dropped its now-redundant active-model readout.

Decisions & notes: The opening's governing principle is now that the model cannot introduce an entity the in-game grounding does not know about, achieved by making the opening authored and demoting the LLM to a validated reword. The quest harness is deliberately a copy of the in-game handleSubmit turn (a drift note flags this) so premium-pool behavior can be evaluated offline without touching the live hook. The quest-item-uniqueness and POI-art rules were promoted from lessons learned to enforced invariants (CLAUDE.md + audit suite) precisely because they fail silently. The save serialization keeps the server guard untouched, so genuine cross-device divergence still forks as designed.

2026-07-07

The day the storefront and its plumbing came together: campaign discovery got a real shape, the money rails got their first slice, the premium AI pool got a playtest-driven polish pass, and a security scrub closed a leak the maintainer had spotted. Discovery moved in three steps. Shop-window stubs established the governing rule that the public bundle carries the face of every campaign (id, names, tier, level band, blurb, teaser flag, art) while the playable content stays private in premium_templates and arrives by delivery at sign-in, replacing its stub by id: guests and free accounts now see the full ladder as locked cards with zero network calls (the bundle is everything you may see, the database everything you may play). On top of that, arc cards (#73 Phase 1) collapsed nineteen individual campaign cards into seven theme-derived arc cards on the Ready-Made tab, each a chapter ladder with truthful chips and a detail modal that doubles as the chapter picker (absorbing the old Seasoned/Legendary sections, whose disappearance had dropped coming-soon finales from view entirely); the modal is stub-tolerant by construction, which fixed the maintainer's guest crash (Cannot read properties of undefined (reading 'shortDescription')) and let signed-out teaser rows say "sign in to play" rather than an upsell, with teaser rows that self-heal by retrying the delivery on click. The billing MVP opened (#6) with redemption codes: migration 006 adds redemption_codes (mandatory expiry, max-uses, disable, no immortal codes), time-boxed source-tagged tier_grants, and one-redeem-per-account tracking, with effective tier becoming max(base, active grants) so every existing gate (premium templates, the AI pool, allowances) honours grants unchanged; the redeem endpoint is atomic (conditional-UPDATE inside a transaction so a race on the last use cannot oversubscribe), returns one generic failure for every brute-force case, and is fail-closed rate-limited at 10/day (unlike the fail-open AI limiter, because this one guards money). The Profile page gained the Redeem-a-code flow, refreshing tier and delivered content exactly like sign-in with no re-login.

The premium AI pool got a playtest response. First a hard bug: maxTokens was set to 1600 against the worker's 1500 cap, so every premium generate returned a 400 (a dead turn until the free-pool fallback caught it). Then a quality pass: the output cap rose 800→1500 for parity with the free pool (premium reading shorter than free was backwards), the default model flipped gpt-5-mini→Claude Haiku 4.5 for better name-grounding and richer prose (5-mini stays next in the fallback chain as the cheap workhorse), and a NAMES rule went into DM_PROTOCOL plus an explicit "this is a PLACE" tag in the location prompt, after gpt-5-mini personified a town name ("Hearthmere crouches low..."). Gaining member+ now auto-selects the premium pool, the dev-only AI-settings engine line was made to tell the truth about which pool is actually active, and the Profile page grew AI-pool pills via a shared component. A security scrub removed operational connection details (an ssh alias, psql specifics) the maintainer had caught in PR #56's body and the sweep then found sitting in migrations 002-005 runbook comments, the ops runbook, and the deploy guard message; public files now say <games-box> pointing at private ops notes. That scrub also surfaced a .gitignore ordering bug: the per-file migration negation list had silently dropped 006_redemption_codes.sql, so PR #56 merged without its migration and the CI guard (which only sees tracked files) never fired: numbered migrations are now tracked by default. Rounding out the day, quest buildings got a findability pass (glow, glyph, name-with-type) plus a fix for them vanishing when a campaign launched over a stale preview map, and the snow towns got a readability art pass.

Decisions & notes: The shop-window governing model is now explicit: the public bundle is the storefront (faces only), the database is the vault (playable content, server-delivered at sign-in). Redemption codes are the deliberate first slice of billing (#6) to seed players who will eventually pay; grants are passive-expiry and never deleted, and the redeem limiter fails closed precisely because it is a money surface. Premium output cost per call roughly doubled with the 1500-token parity but stays fractions of a cent, bounded by the monthly allowance. The connection-alias leak was judged not worth a history rewrite (the alias exposes no host/IP/port and role names carry no secrets); the fix is going-forward redaction plus the migration-tracking correction so the CI guard can never again miss an untracked migration. Test suites grew steadily through the day (worker 130, frontend past 1600), green throughout.

2026-07-06

A production-readiness day: the morning built the operational floor under the newly self-hosted stack, midday shipped the premium AI pool, and the evening opened up campaign discovery while auditing every path premium content could leak through. The ops hardening pack (#11, #19-21) finally gave production Postgres a backup story (it had none since the Supabase cutover): a nightly pg_dump script with TOC verification before rename, 14-daily/8-weekly retention via hardlink promotion, and optional offsite copy to R2 using copy-not-sync so a wiped box cannot cascade-wipe its own offsite copies; a restore script defaults to a scratch-database drill mode with typed confirmation required for production; an ops runbook records the post-cutover architecture, incident quick-checks, and migration rollback notes; and a deploy-worker CI workflow (typecheck, deploy, then a prod smoke test) refuses to deploy any push that adds a migration file, encoding the migrate-before-deploy rule with manual dispatch as the post-migration button. Alongside it, the maintainer's standing "are we sure these limits are working?" question got a permanent answer (#71): a vitest-pool-workers suite runs 78 tests inside workerd itself, fully offline, with a fake Postgres seam, stubbed AI binding, and real ES256 JWT verification against a stubbed JWKS, plus 38 branch-gated tests that would activate automatically when the premium branch merged.

They activated the same day. The premium AI pool (#7, #12) landed as one worker lane: fixed-window rate limiting (atomic upsert-increment per request, failing open on DB errors with loud logging, 429 + Retry-After) covers AI generation, embeddings, and DB writes, and a pool: premium request from a member+ account routes to a curated OpenRouter registry (verified against the live catalog after the first draft named a model that no longer exists there) through the same sanitization as the free pool. Any premium failure falls back to a free-pool generation, so a paid request can never produce a dead turn, and a DB error never opens the paid pool. The economics are revenue-aligned by construction: prompt and output caps plus monthly allowances sized so worst-case cost stays well under each tier's price, with daily caps purely as burst protection. Player model choice was removed entirely; the premium pool always runs the server's default chain. Three follow-up passes made the local dev AI settings honest about this (an active-engine line that warns when a non-worker provider bypasses the pool, dimmed pickers under Premium since the server ignores them, and finally the whole line gated to dev builds since production has no dropdown to disagree with). The premium tier page graduated from /debug/premium to a real but deliberately unlinked /premium route with copy audited against what actually shipped, the never-designed housing claims removed, and a tier-aware "Active on your account" state; the profile page gained a click-to-copy Account ID, since game records are keyed by that ID and quoting it is how a player gets matched to an account for support or a membership grant.

The New Game screen opened up (#72): higher-tier campaigns had been invisible until a save completed one, leaving the premium flagship undiscoverable, so the Ready-Made tab gained Seasoned Parties and Legendary Campaigns sections with premium cards segregated into gold sub-sections within every tier, a shared level-fit notice replacing two duplicated ad-hoc warnings, and Start never disabled (honesty over blocking, since engine minLevel gates already protect the deep milestones). The custom builder gained Tier 3 with requirement-based greying, a canonical tier-to-level-band mapping shared with the catalog so the two pages cannot drift apart again, and a floor of two complete milestone slots after the maintainer judged single-milestone campaigns uninteresting. The hardening side of that discovery push was an entitlement audit of everything custom quests can pull through: the item slot was a real leak (the searchable pool exposed the flagship reward and the whole legendary shelf to every account, and slot-1 items spawn as acquirable objectives, so a free user could hand themselves a +3 legendary artifact), fixed at the source with an exclusion list, a per-tier rarity ceiling matching the drop system, and an invariant test so a future shelf item cannot ship unexcluded. Related fixes closed the preview path that baked a literal "Town B" into generated worlds, made all placeholder-location fallbacks draw real themed names, and stopped the template preselect path from applying premium themes to free accounts. A polish batch closed three backlog rows (seeded narrated reward lines instead of flat "+50 XP" strings, RAG vector purge when a save is deleted, and embedding model-version stamps so mismatched vectors re-embed via the existing backfill), and worldgen playtest reports shrank lakes to a coast-aware budget (exactly one modest lake per default world now, the twin-companion lake mathematically extinct) and stopped forest POIs spawning in the sea, with a retroactive art heal for existing saves. Art batches cleared the entire generation queue twice over: the t2 template cards and finale boss portraits (KNOWN_MISSING_CARDS empty for the first time since the guard was built), six bespoke site-arrival images, and the tidewater campaign cards plus a bespoke bell icon. Two of those batches fixed the same production wound: images generated on disk but never committed, so local rendered them while the live site 404'd; the arrival-image pointers joined the art-integrity scan in response. The day closed with Eldritch Horror moving from premium to the member tier, informed by production data showing zero eldritch saves across 12 players, giving the ladder a uniform shape (free three realms, member adds three, premium the tidewater flagship).

Decisions & notes: Rate limiting fails open on DB errors, choosing availability over strictness, but a DB error never opens the paid pool. Premium AI allowances are monthly as the revenue bound with daily caps only for burst; the fallback is stated honestly on the premium page (play never stops, the free pool answers, the game says which pool did). The /premium route is intentionally unlinked from nav until billing (#6) ships. The art-integrity test cannot catch generated-but-untracked files (git never sees them); this bit production twice today. Frontend suite grew from 1440 to 1548 tests, plus the new 78-test worker suite, green throughout.

2026-07-05

Three arcs ran through the day: water towns went from design doc to playable in real games, the premium infrastructure chain finally became buildable after a database cutover, and the save-sync plan reached its final phase. The water-towns program (#65) opened with the design itself (canal city as the Premium flagship, river city for Members, both riding a single additive waterway tile flag with renderer-tolerant defaults) and shipped five of its six phases by evening. The riverfork archetype carves a windy channel edge to edge before roads exist and forks around an island district with at least two guaranteed bridges; the canal city seats a 2x2 harbor basin off the town square, runs windy canal spokes under a unified water budget, hugs the banks with quay lanes, and introduces a boathouse building staffed by a new Boatwright NPC. Phase 3 put both into real games: two options-gated world-gen shims route the first river to the sea and settle a city at the estuary, and campaign launch stamps the world once per account tier (premium worlds get exactly one canal city, member worlds roll riverforks on about half of eligible river settlements), with saves grandfathering their stamp so a lapsed tier never rewrites a world. Phase 4 dressed the water as channels, autotiling all sixteen canal shapes from a 4-bit waterway-neighbor mask (stone banks, junctions, open mouths, bridge decks oriented across the channel, quay lips with bollards), and Phase 6 added six dockside side quests whose givers are all water venues, so landlocked worlds never see them. Playtesting along the way killed mid-channel "phantom bollards" (a bank nib now requires a dry diagonal behind it), retuned canal cities denser (Venice should feel veined, not moated), and root-caused a structural bug where every padded river city's channel stopped one ring short of the map edge. A late river-doctrine wave (#67-69) then made placement realistic for everyone: settlements now bias toward water scaled by size, towns adjacent to a river render a real one-bank waterway edge with a quay lane, coastal riverforks keep their island by merging into the sea flood, and river mouths at the map border stopped rendering a bank wall damming the flow.

The premium chain unlocked in sequence. First the data Postgres migrated off Supabase to the self-hosted Hetzner box via Cloudflare Hyperdrive (worker DB routes rewritten from supabase-js to postgres.js, JWTs still verified against the hub), which unblocked the two long-parked issues gated on it. The entitlements service (#39) put account tiers in Postgres (a free/member/premium/elite ladder, manual grants for now) behind a worker endpoint and a fail-closed client cache, with isPremium() reinterpreted as member-or-above so every existing gate flowed unchanged; a Membership badge on the profile page doubles as the end-to-end sanity check. The content delivery channel (#40) closed the loop: premium story templates now live server-side as JSONB rows and are fetched once per session by entitled accounts, registered idempotently alongside built-ins, failing closed to the free experience for guests and outages. The Drowned Bells flagship's finale reward joined the item catalog with a new per-template minTier gate (#70), since the canal city it is set in only exists in premium worlds. Save-sync concluded with the rev protocol (#54 Phase 3): conversations carry a revision counter, a stale push is now a detected fork rather than a silent clobber, the local timeline parks Dropbox-style as a diverged save, and hero ledgers union across the fork with exact-event dedupe, so a fork loses narrative position but never earned progress. Around the edges: towns gained themed building palettes (#64, desert adobe and snow timber, later jetty-plank docks, chimney smoke, and icicles) with three tuning rounds before desert buildings read against sand, and the world map got a viewport with three fixed zoom steps and tile culling for the coming larger worlds (#60 step 2), shipped dark so 10x10 worlds render byte-identically through the legacy branch.

Decisions & notes: The premium boundary for water settlements is the archetype, not the river: placement realism is free for everyone, riverfork cities are member-tier, canal cities premium. The public/private split was recorded: generators, tile art, and gates are public code (clients must render cached canal towns regardless), authored premium campaigns stay private and server-delivered, and client gates are UX while enforcement is worker-side. Save renames deliberately do not bump the rev so metadata edits cannot fork another device. Legacy world fixtures were re-captured for the settlement bias, allowed because the chunk scheme is still explicitly experimental. Test suite grew from 1161 to 1440 tests, green throughout.

2026-07-04

A heavy playtest-response day with two main threads: making saves unable to lose progress, and a world-generation quality push that ended with a working larger-worlds prototype. The save thread opened with the smoking gun behind "hero equipment vanished after reload": the save-dedup fingerprint tracked inventory by count and ignored equipment entirely, so a pure equip never triggered a write. From there the new save storage and sync design (#54) shipped its first two phases in a single day. Phase 1 makes the IndexedDB auth-expiry fallback honest ("Saved on this device" copy instead of silence, reconcile on auth restoration, and a no-clobber guard that parks diverged copies as separate saves); Phase 2 goes local-first outright, writing every save to IndexedDB before the cloud push, presenting a merged badge-aware save list, and retrying pending pushes automatically. On top of that, heroes gained a mechanics layer (#58): an on-load invariant checker heals stats upward (level from XP, maxHP from class/level, equipped items must exist in inventory) and an append-only grant ledger records every XP, gold, and item grant (gold spends included, as negative events), so a stale snapshot can no longer erase what the party earned. A follow-up made the invariant maxHP raise preserve damage taken rather than the raw HP number, since fully healed heroes were reloading as wounded. Parallel playtest fixes (#55/#56) delivered party-wide milestone XP, balanced NPC gender dealing, quest venues that stop converting houses, and a large site-content package: every site event now shows visible in-map feedback (working mechanics had read as broken because all feedback went to a chat log hidden behind the map modal), harvest nodes were generalized across caves, mountains, and ruins, all active quest objectives now inject into a site instead of first-quest-wins, and active side quests are grounded in the AI prompt so the DM stops inventing sources for quest items.

The world-gen thread began with the twin-lakes wart (#59): 10x10 maps could roll two giant near-identical lakes, fixed with a 12% total water budget, a one-large-one-small rule, and per-lake shape personality. The larger-worlds design (#60) was agreed and immediately prototyped (#61) behind /debug/large-world: k x k chunk worlds assembled around a byte-identical legacy heart chunk, with an ocean-side coast model and deterministic gate-point road crossings on every land seam. Two review passes then hardened it: lake allocation moved to the world level (later tightened to a hard cap of three lakes per world, after independent per-chunk rolls compounded into five-lake worlds), a seam-matched coast depth profile guarantees depth steps never land on a chunk boundary, and coast corners now reuse the lakes' diagonal-shore code path instead of forming right-angle stairs. Town interiors got their own overhaul (#62): a hub-and-spoke path network anchored on the town square, with windy gate roads, shared spoke trunks, and street-front houses, surveyed to zero disconnected path components across 580 towns (from 191 of 200 broken before), plus a manor-placement starvation fix so no city seed silently ships a reduced building roster. Desert mountains stopped rendering as snow-capped alps (now sandstone mesas, retroactive since tile art is a pure view), and four design notes were logged for the next wave: a lake quota, themed town tilesets, Venice-style water cities, and fields-based realistic worldgen (#63-66).

Decisions & notes: Save conflicts will fork Dropbox-style rather than merge; the rev protocol is Phase 3, gated on the Hetzner cutover. The chunked large-world scheme is explicitly experimental and not frozen: the fields-based realism idea (#66, world-level elevation and moisture fields sampled by chunks, which would make seams free and feed hydrology) is to be evaluated before freezing it. Guests stay on 10x10 worlds; larger worlds are a paid-tier feature stored flat with mapVersion 2 plus worldOrigin so a subscriber's save can grow additively later. Test suite grew from 851 to 1161 tests, green throughout.

2026-07-03 (supplementary: afternoon and evening sessions)

Second entry for this date: the entry below was written mid-morning and covers only the after-midnight session; this one covers the roughly forty commits that landed afterwards, from late morning to midnight.

The bulk of the day was the T3-campaigns and combat-depth program (issues #43-#50), opened by a code-verified progression audit with startling findings: leveling moved combat win rates by exactly 0.0 percentage points (no level term in the d20, stats frozen at creation), level-up could actually LOWER max HP, nine of ten template bosses dealt zero damage, and four items were wholly unobtainable. The fixes rolled out in waves. Level-ups can no longer reduce max HP: healthSystem now owns one canonical formula, monotonic by construction, and bug-damaged saves self-repair at their next level-up (#48). Forest, hills, and mountain sites got their themed loot pools back (populateSite had been coercing every site to 'cave' for loot as well as combat, which also falsified the journal's derived source hints) (#49). A Monte-Carlo sim harness now drives the real combat resolver across level, loadout, and party-size scenarios, backed by a six-guard progression lint over live game data with pinned known gaps so both regressions and silent fixes surface (#46). Levels grant +1 to checks per 2 levels, capped at +3, retroactive since it is computed rather than stored (#47). The centerpiece was party boss fights (#43): bosses have real HP that matters (flat per-outcome damage replaces percent-of-max), they hit back via explicit damage profiles, and a Lead + Support system has one hero roll while the living others add support bonuses; measured by the sim, the deadly t2 bosses went from roughly 1% retry lotteries to 42-56% wins for a three-hero mid-gear party. Around it: the gear ladder gained an obtainable +2 rung and a t3 legendary shelf (#44), the side-quest pool grew from 30 to 42 with the Lv3+ band tripled and every quest enemy carrying the new damage schema (#45/#50), the t3 enemy registry was retuned into healthy win bands (#53), a /debug/boss-fight harness lets a configurable party fight any authored boss through the real modal flow, and the grown quest pool now backfills into in-progress saves deterministically on load. Narrative encounter hooks also got real affordances: action chips on the narration bubble, three-move persistence with a single reminder, and inline hook art (#35-#37).

Quest chaining shipped twice in one evening. It first landed as designed: linked "Chapter 2" saves with a fresh world and the carried party, plus a campaignLauncher extraction so NewGame and continuation share one start pipeline. Hours later a maintainer design correction replaced it: continuation happens INSIDE the same save, same world, same journal, same RAG memories, with the next campaign spawned additively into the live world and retro-injected into already-cached towns; the same-world conflicts this creates (stale quest-building stamps, historical POI visibility, an NPC inheriting a building from his predecessor) were each fixed and tested, and the desert and frozen t2 sequels landed on their t1 geographies so no genre dead-ends. Premium groundwork continued in parallel: a gitignored local content slot lets private campaigns (the t3 flagship "The Shattered Throne" lives in a separate private repo) be playtested locally while a predeploy guard hard-fails if the slot would ever reach the public bundle; the accounts and pricing plan moved into the new gitignored docs/private/ convention; a draft five-tier benefits page sits at /debug/premium, not publicly routed. An art push fixed nine of ten boss fights showing a ritual-dagger item icon instead of a portrait, added an art-integrity CI guard (every declared asset path must exist on disk), and then worked the generation queue to empty, so every item, boss, and template card in the game now has its own art. The day closed with the Adventure Book (#51/#52): one fixed-frame tabbed hub (Campaign, Side Quests, Codex, Party, AI) replacing the separate Journal, Inventory, and Settings modals, with the map deliberately kept standalone; its new Codex tab is a discovered-only bestiary and item compendium generated from live game data, spoiler-light by construction. Finally the production AI tab was simplified twice: provider choice hidden and pinned to cf-workers (other providers route to the local dev server and silently broke for players), then all model dropdowns removed outright, so production shows only the Free and Premium pool chips.

Decisions & notes: The same-save continuation explicitly supersedes the linked-save Phase 1 shipped hours earlier (recorded in QUEST_CHAINING_PLAN); the linked-save machinery was deleted, not kept as an option. Sim-measured balance numbers are pinned as code comments and enforced by cheap static tests rather than re-simulating in CI. Two new repo conventions: business-sensitive planning docs live in gitignored docs/private/, and premium content must be delivered from DB/R2 since the worker source is public. The journal side-quest tab (#41/#42) that opened the day derives its "how and where" hints live from reward tables, loot pools, and shop stock, so they can never go stale. Test suite grew from 323 to 851 tests across the day, green throughout.

2026-07-03

A short after-midnight session that carried the 2026-07-02 playtest push over the line, then shifted to design planning. Two gameplay fixes landed. First, the decorative crystals scattered through cave and mountain site maps (which players kept trying to pick up, issue #38) became real: populateSite now converts 1-3 of them per site into genuine walk-on loot deposits (caves yield raw gems, mountains yield mountain or storm crystals) and demotes the leftover decorations to bare floor, so nothing on screen teases a pickup that cannot happen. Second, the blighted-village campaign was unblocked: its "gather healing herbs from the Grey Moors" milestone was an item milestone authored at a wilderness location with no building, and no code path could ever grant the item (the third instance of the authored-but-never-consumed family), stalling the whole campaign. A new getMilestoneItemForTile now powers a "Gather" action in the POI arrival modal, buying the quest item from a shop finally counts as acquiring it, the Journal shows the what and where, and a template lint test makes any fourth gap of this family fail CI. The rest of the session was documentation: a quest-chaining design plan plus its decisions, a side-quest UX design covering objective clarity and a Journal tab (issues #41/#42), and a help-center pass adding a ScrollToTop component (route transitions used to land the reader mid-page) alongside a claim-by-claim audit of the help content against the code, dropping the stale "9 Playable Races" line and correcting the model list and guest-vs-account copy.

Decisions & notes: The herb fix is deliberately retroactive (the gather action is derived at runtime from milestone state, so already-stuck saves self-repair), while the crystal conversion is going-forward-only (cached sites are never retro-mutated, covered by tests). Quest chaining was decided as linked-save handoff first, with same-world sequels as the end goal.

2026-07-02

The busiest day on record, roughly two dozen commits driven by working through the June playtest feedback and then live-playtesting the goblin campaign end to end. The feedback batch came first: equipped items stop double-counting in the pooled inventory, equipment bonuses now say which stat they modify, a Town/World tab lets you consult the world map from inside a town, Archives and Tavern got distinct tile-art silhouettes, and the local narrator gained larger biome pools plus an anti-repetition window. Its big follow-up delivered pooled equipment (any hero equips from the shared party stock via tested move-one-instance helpers, surfaced in a new Loadout tab), a guardrail preventing worn items being sold, guest reload-resume (a GameResumeGate rehydrates the active game from IndexedDB on hard reload instead of starting blank), and editable save names with honest saved/no-change/error feedback. Smaller quality fixes staffed the barracks and workshop (the only unstaffed building types, now guarded by a coverage test), made food actually heal, and had the save confirmation say where the save went. The new-game screen was reworked: the five tone dropdowns became segmented chips with theme-provided editable defaults, the previously dead Narrative Style setting was wired into generation, world gen now honors declared settlement sizes (so "the village of Ashford" no longer generates as a city), and the quest-item picker gained rarity colors and sorting. Premium groundwork ran in parallel: a data-driven rarity gate keeps very_rare/legendary items out of Tier 1 random drops, WoW-style rarity coloring spread across equipment, shops, and inventory through the shared getRarityColor, a client-side entitlements seam (isPremium() placeholder) gates the desert and snow themes at every reachable path, a new premium snow campaign "Frozen Frontier" landed along with the snow world-gen wiring it exposed (snow maps had been rendering green), and the licensing question was settled: stay Apache-2.0 and defend premium with engineering, not licensing (issues #39 server-side entitlements and #40 a server-delivered premium content channel). The milestone NPC-grounding plan shipped both phases: authored campaign NPCs are now actually placed in their towns and named to the AI so it stops inventing "Jorik" and nonexistent mayors (Option B), then a fourth mechanical milestone type 'talk' with an in-building Talk button made NPC-meeting completion deterministic rather than AI-judged (Option C), with the Briarwood captain renamed to Ulric along the way. Live-playtesting then surfaced and fixed a string of campaign-breakers: a raw "GOBLIN_HIDEOUT" POI name, the authored boss fight being completely unreachable (a new Confront action launches it), guests dead-ending on narrative-tier encounters (they now get the interactive modal), players stranded on unenterable tiles after an encounter auto-closed the location modal (clicking your own tile now reopens it), and a completed quest-item search that kept re-offering itself. The day closed on save integrity, chasing a report of lost quest progress: a stale-spread setSettings that silently reverted concurrent changes, snapshot races where two completions overwrote each other, quest fields missing from the save fingerprint (so quest-only changes were skipped as "no change"), and milestone completions never triggering an immediate save, all fixed with functional updates, by-ID application, fingerprint coverage, and explicit performSave calls.

Decisions & notes: The 'talk' milestone type is going-forward-only; existing saves keep their narrative milestone and the AI-marker path, and a new marker guard means a stray [COMPLETE_MILESTONE] can never complete a mechanical milestone. The entitlements seam keys off isPremium() everywhere so the real auth-tier check swaps in with no call-site changes; hand-authored milestone and boss rewards intentionally bypass the rarity tier gate as explicit design. Issue #34 was closed with the accordion approach rejected in favor of the tone chips. 323 unit tests green, plus a new Playwright e2e driving guest save, reload, resume, and rename end to end.

2026-07-01

A quiet documentation-sync day. CF_WORKER_GUIDE.md was brought back in line with the code: the model registry is now 5 models across 3 tiers (the doc claimed 7 across 5), the default is gpt-oss-120b rather than the 8B, fallback is derived from registry order via getFallbackCandidates() instead of the obsolete FALLBACK_MAP, and the retired Llama 3.3 70B and 3.2 3B entries were removed. The docs/README.md document map also picked up three feature docs that existed on disk but were never indexed (hero recovery, quest givers, side-quest backfill).

2026-06-29

A correctness, balance, and player-feedback day after the big build-out. The headline fix was in encounters: generateLoot now pays XP/gold/items only on success tiers (failure tiers may still grant healer-mercy healing but never loot), killing a "rewarded on a critical failure" bug where a crit-fail healer visit handed out gold; and a new clampPenaltyGold caps the displayed gold loss to what the hero actually has, rewriting the message for both single-round and multi-round summaries so a player with 0 gold no longer reads "lost 44 gold" (the real deduction was already clamped). The world-map "can only move to an adjacent tile" message was also moved from the sticky error banner into the conversation as a positioned entry, so it stops masquerading as the latest narrative line. On content, the apothecary became a real shop type with a herbal/first-aid stock (healing herbs, antidote, medicine kit, rare herb) distinct from the alchemist's manufactured potions, gaining the Visit|Wares tabs. The Race selector was hidden in the hero creator: with only human portraits available, every new hero is now created Human and class templates default to Human, though heroRace stays on the data model so legacy saves keep and display their race (the change is purely cosmetic, race has no mechanical effect). The day closed with planning and housekeeping: three new feature plans (hero-recovery restore-from-roster, side-quest backfill for in-progress saves, widening quest-givers beyond inns/taverns), an issue logged to consolidate the drop-down-heavy custom-game setup into accordions, and a gitignore for local playtester feedback notes.

Decisions & notes: Hiding the race selector is reversible by restoring the <select>; the field is deliberately retained rather than dropped so existing heroes are unaffected. The side-quest-backfill and hero-recovery plans both lean on the roster surviving campaign-snapshot corruption (restore comes back at template state) and mirror the normalizeParty de-dupe approach. 301 tests pass across the day's commits.

2026-06-28

The single biggest content day so far: three large feature streams (wilderness sites, side quests, and the equipment/shops/explorable-POI trio) all landed, plus a town-generation overhaul and a nasty save-corruption fix. Wilderness sites were built end-to-end across phases: a generic rooms-and-corridors siteMapGenerator (caves carved as organic blobs from rock, ruins placed as broken walls on open ground) with its own programmatic-SVG siteTileArt and a shared MapLegend built from the same tile-art functions so the key can't drift from the map; then live entry/exploration as a new "site" map level in useGameMap mirroring the town spine (seed-deterministic, cached, persisted in the save fingerprint); then populateSite filling content slots with seeded encounters and loot (deepest room a guaranteed hoard, corridors rolling wandering-monster chances); then injectSiteObjective giving campaign milestones (item/combat/location) the ability to live inside a site, all routed through the existing checkMilestoneEvent path. Side quests arrived as a full optional system parallel to the main campaign: a questEngine with multi-step item/combat/location/gather/bounty/courier chains, giver-building pickup and return/courier turn-ins, and crucially map-validated selection (a quest is offered only if it can be both started and completed given the actual buildings and sites on the map). That grew the same day from 9 quests to a ~30-quest pool across 12 giver types, with caves/ruins now hidden on the world map until a quest reveals them, and a second discovery path where a quiet world move may offer a level-appropriate quest. The equipment/shops/explorable-POI trio merged from parallel worktrees: forest/hills/mountain tiles became enterable sub-maps (extending the site framework, always explorable not quest-gated); buy/sell shops via a pure shopController/shopStock surfaced as a Wares section; and equippable weapon/armour/accessory slots whose bonuses flow through encounterResolver in combat, followed by a full items pass (a leather→plate armour ladder, +1 to-hit weapons, an artificer's workshop tile). Town generation got a major overhaul too: lakefront/coast water carved first, road-aware gates that follow world-map roads from any side, 10 new building types with bespoke icons and titled NPC occupants, townhalls planted on the town square, and a balance bump to 3-6 towns per map with bigger, fuller settlements. Finally, a reproduced hero-duplication bug was root-caused and fixed: party-update code matched on the legacy characterId (undefined on every real hero, which carries heroId), so undefined === undefined matched the whole party and the per-round combat update overwrote every hero with the active combatant; a new tested partyUtils (heroUid/replaceHeroInParty/normalizeParty) fixes it and repairs already-corrupted saves on load.

Decisions & notes: Everything stayed backwards-compatible per the procedural-map rules: new site/quest fields default cleanly on old saves, siteVersion rides each cached site, cached towns keep their layout despite the bigger footprints, and quest-gating is skipped entirely when a save has no side quests. Quest "find" items reuse existing catalog icons via QUEST_ITEM_ICON_FROM (a guard test enforces every find-item has an icon source) so no new art was needed; dragonscale_plate is defined but intentionally sourceless. The features were specced as file-owned scopes specifically so they could be built in parallel worktrees with minimal overlap. The hero-dupe fix prompted OUTSTANDING_ISSUES #30 (an admin DB-inspection dashboard) to confirm/recover the affected player's hero. A large docs reorg also happened: a new docs/README.md document map, 13 shipped/stale plans archived to the gitignored docs/archive/, and six new forward-looking feature plans (crafting, spells, factions, companions, inventory/gear, fast travel). Test count climbed steadily through the day (247 → 282 → 287 → 296), all green.

2026-06-27

Two threads ran in parallel: making movement narration cheaper-by-default, and polishing world-map water. On narration, the day implemented the first real slices of the tiered-narration plan. B3a added localNarrator.js, a pure seeded movement narrator (xfnv1a + mulberry32 keyed on world seed plus tile x/y) with biome-aware template pools, first-visit-vs-revisit variants, and neighbour-landmark/party-wounded clauses, so logged-out guests get templated prose instead of the silence they used to get behind the sign-in-gated AI path. B3b then went further and flipped signed-in play to the same model: moving on the world map no longer auto-calls the AI at all. Every move now appends a short deterministic local line, and full AI narration became on-demand only via a new "Look around" button (AI for signed-in, a local ambient line for guests/master-off). On the water side, world-map lakes were reworked from single isLake ponds into contiguous multi-tile blobs: a seeded flood-fill carves a ~4-9 tile body ringed with proper beach shore tiles (reusing the placeCoast beachDirection convention) so the renderer draws a sandy shoreline instead of a hard water/grass edge. A follow-up pass added concave and convex corner beach tiles, a 2-tile lake-separation rule, and a blob-smoothing step so shorelines only ever produce cleanly-renderable corner types. Town decorations got their Phase 2c theming (desert places cactus/rock/dead-bush, snow places pine/rock/snowdrift, plus a snow ground tile), the map modal was resized to fit the now-larger tiles, and a docs file of building-interior image prompts for the 9 missing buildings was added.

Decisions & notes: B3b deliberately supersedes the plan's routing scorer + 3-way narration setting with a simpler "Smart-by-pull" model — local lines by default, AI only when the user asks for it — keeping aiNarrativeEnabled as a master on/off; encounter detection, RAG sync, summarization, autosave, and milestone checks were left untouched, and a narrative-tier encounter landing on a move is parked and woven into the next Look-around. The local narrator is strictly pure (no Math.random/Date.now, markdown uses *italics*/**bold** only) so reloads reproduce identical text. All map/town generation changes stay going-forward-only: default grassland towns are byte-identical to before, the renderer's old single-tile isLake branch is untouched so legacy ponds still render, and cached town maps are never regenerated. A new /debug/lake-test page with every beach tile labelled by beachDirection was added for precise corner debugging.

2026-06-26

The big push was extending yesterday's town-tileset treatment to the world map, so the whole game now renders through one programmatic SVG art layer instead of colored divs and emoji. The day opened on towns, though: generation was reworked to build each town at its proven native size and then frame it in a uniform 20x20 canvas (padTownToUniform) that copies the core verbatim, runs the entry road all the way to the map edge, and dresses the surrounding ring with farmland and trees (hamlets getting the thickest countryside). Walls now enclose both town and city; the tileset also gained real building variety — distinct shapes for manor/keep/barracks (previously identical), a two-storey inn, an anvil on the blacksmith, a twin-chimney foundry with furnace glow, a columned library, and doored/recolored shops — so buildings read at a glance. From there the world map got the full art-engine arc, mirroring how towns were done: new biome tiles (desert, swamp, snow, woodland) and POI sprites (ruins, hills, caves, a walled-city icon with a keep) built and iterated behind a /debug/world-map-art preview first, then the live WorldMapDisplay migrated onto the SVG layer, with a late polish pass for bigger tiles, biome-aware POIs, peak outlines, and a softer beach. Underneath the art, world generation grew content in phases: Phase 2a sprinkles hills and ruins onto open ground, caves were wired and hardened, and Phase 2b introduced themed regions — an optional biome theme threaded through map/town/narration generation, shipped end-to-end via a new "Desert Expedition" template that produces sand-based desert maps with oasis lakes. Two smaller items rounded out the day: a higher-intent guest sign-in prompt placed right at the gated AI chat, and a CLAUDE.md section codifying the procedural-map persistence model and its backwards-compatibility rules.

Decisions & notes: All generation changes are strictly going-forward-only — the desert theme defaults to grassland, which reproduces today's output byte-for-byte, and existing saves keep their stored grids; the art changes, being a pure view layer, re-skin every old save for free. Per the new CLAUDE.md rule the world map is a bare 2D array, so mapVersion: 2 and theme are stamped on game_settings, not inside the map. Legacy saves carrying old forest/mountains biome tiles are migrated to POIs on load in useGameMap so they render correctly under the new art. The preview-first-then-live-swap pattern was reused (validate in /debug/world-map-art, then migrate the live renderer). The tiered-narration plan was revisited with notes favoring biome-aware local templates and a B3a-first sequencing, but that remains a plan, not implementation.

2026-06-25

The day's main thread was giving towns a real visual identity by replacing the old colored-div-and-emoji renderer with a programmatic SVG tileset. It landed in two steps. First, townTileArt.js was built as the art engine: seamless SVG tiles emitted as CSS data-URIs with no raster assets, covering terrain (grass/dirt/water/farm/stone/bridge with deterministic per-coordinate variants), walls and a keep that autotile from a 4-bit neighbour mask (towers appearing on corners and endpoints), and distinct silhouettes for each of 18 building types (gable house, hall, barn, smithy, temple, bank, dome, market awning, guild banner, and so on). A /debug/tileset preview page (TilesetTest.js) gives a swatch gallery plus a live generateTownMap() rendered through the tileset with autotiling, size/zoom/reseed controls, and tests guard SVG well-formedness, determinism, wall autotiling, and that building shapes stay visually distinct. That first commit was preview-only. The second commit then migrated the live TownMapDisplay onto the tileset: every tile is now a backgroundImage from tileBackground(...), walls/keep autotile, terrain varies per coordinate, and the grid is seamless (gap 0) with a framed border, while all existing interaction (click/discovery, building modal, player portrait, entry highlight, Leave Town, tooltip) was preserved. Only POI markers (fountain, tree, legacy well) remain as emoji overlays. Alongside the art work, the town generator was polished: a fountain now sits at the centre of every town size (dropping the small-town well), and the approach road is single-width for all sizes. The day also included a documentation refresh: the long-stale ROADMAP was brought current (moving narrative milestones, in-game RAG, guest play, onboarding, combat-condition narration, and CLI hardening to Shipped), and two new design plans were added.

Decisions & notes: tileBackground is memoised by (type, wall-mask, buildingType, coord-variant) because the town re-renders on every player move and would otherwise rebuild every tile's SVG each step. The tileset migration was deliberately split into preview-first then live-swap so the new renderer could be validated in isolation before touching the playable map. The tiered-narration plan (docs/TIERED_NARRATION_PLAN.md) generalizes the old Guest Mode B3 idea into an app-wide cost lever: local templated prose for routine moves, AI for notable moments, with a 3-way Full AI / Smart / Local-only setting and a routing scorer; Guest Mode B3 now cross-links to it. The graph-RAG plan (docs/RAG_GRAPH_ENHANCEMENT_PLAN.md) is a design sketch extending the already-shipped local RAG, not yet implementation.

2026-06-24

Guest Mode reached its final planned phase (B2) and got a conversion-focused polish pass. The headline was LocalGameSync, a mirror of the existing LocalHeroSync: when a guest signs in, their IndexedDB games are uploaded to the cloud via conversationsApi.save, preserving each session_id so the live game keeps autosaving to the same row (an upsert rather than a duplicate). It maps snake_case rows back to camelCase save payloads, removes each row as it syncs, and leaves any failures behind for retry, toasting how many games were saved. App.js mounts it right after LocalHeroSync so heroes import first. A second theme tackled a subtler trust problem: a returning user browsing while logged out used to see empty "nothing here" states that implied their data was gone. A new PII-free hadAccount flag (set in AuthContext when a session resolves) now lets AllHeroes and SavedConversations say "your heroes/adventures are in your account, sign in" to returning users, while genuine first-timers still get the create-your-first onboarding. Later in the evening came the guest-conversion nudge itself: a slim, session-dismissible GuestBanner ("Playing as a guest, sign in to unlock the AI DM and save across devices") shown app-wide except on landing/auth and the full-height game view. Hero Selection was also recolored from the blue --state-info to the theme --primary so the selection cues match the active theme, and the tour's select-party step was re-anchored to the Start Game button so it gets the same amber pulsing ring as the other coachmarks.

Decisions & notes: Guest game sync uses upsert keyed on session_id specifically so a guest's in-progress game keeps writing to one row after sign-in rather than forking. The unused "Download Hero JSON" / "Download" buttons were hidden behind comments (not deleted) on HeroSummary and AllHeroes, re-enable on request. A backend-free round-trip test (LocalGameSync.test.js) proves no persisted field is dropped in the row/payload mapping. This completes the Guest Mode plan in docs/GUEST_MODE_PLAN.md.

2026-06-21

A big, focused push on the new-player experience and on letting people try the game without an account. The day opened with a guided onboarding tour (#23): a GuidedTourContext plus TourOverlay/OnboardingSteps that walk a first-time visitor through the home page, hero roster, and saved-games flow. From there the character creator was reworked end-to-end — proper validation, a point-buy stat system (new heroValidation module + heroData), and a portrait picker modal — and the New Game page was stripped down to the essentials so it reads cleanly to someone who has never played. Hero selection was also made more obvious (#24) so players don't get stuck not knowing they need to pick a character. The other half of the day went to Guest Mode: a local-first roster (Phase A, localHeroStore/LocalHeroSync) and then AI-free guest play (Phase B1, localGameStore + introComposer) that lets guests create heroes and play locally, with games syncing up once they sign in. The day closed with map discoverability fixes (#25) — the world map now stays open while exploring, the Map button is more prominent, and in-game coachmarks point players to it — and a small but meaningful AI change: the player's combat condition is now surfaced to the DM prompt (#33) so narration reflects how the fight is actually going.

Decisions & notes: Guest Mode is deliberately phased and local-first — guests play with no AI/account, and their local heroes and games are designed to sync to the server on sign-in (Phase B2 still planned, see docs/GUEST_MODE_PLAN.md). A combat-depth backlog was also logged in docs/OUTSTANDING_ISSUES.md alongside the combat-condition change. New tests landed for hero validation and for the combat-condition prompt composition.

2026-06-19

A busy day centered on turning Henry's playtest feedback into concrete fixes, plus a security hardening pass on the local dev tooling. The feedback was first triaged into a list of outstanding issues, then several items were worked through. The headline fix addressed the "missing workshop on some maps" bug (#22): the world map was placing a random 2–4 towns with no awareness of how many named towns a campaign actually needs, so campaigns like Arcane Renaissance (which require Tinker-Row, Brasswick, Gear-End) could silently drop milestone towns and their quest buildings/items — leaving the quest uncompletable. Map generation now guarantees at least as many towns as the campaign's customNames list, relaxing town spacing on crowded maps rather than skipping required towns, and the spawner now warns loudly when a quest building targets a town that isn't on the map. Two smaller UX fixes from the same feedback: hero selection cards no longer stretch to fill a row when only a few heroes are present (grid track capped at 360px and centered), and the Arcane Renaissance setting text now points at the Tinker-Row workshop instead of misdirecting players to Cogsworth. Separately, the local-dev CLI task runner (src/llm/runner), which spawns agentic CLI tools, got a defense-in-depth hardening pass.

Decisions & notes: The runner was already safe from shell injection (spawn with shell:false), but several gaps were closed anyway: bind the dev server to 127.0.0.1 by default (LAN exposure now requires explicitly setting HOST), stop trusting a client-supplied cwd (always use process.cwd()), pass an env allowlist to children instead of the full process.env, add per-task timeouts and a max-concurrent cap, cap prompt length, and add a -- terminator before the codex positional prompt. New tests cover the map-placement guarantee across seeds, the runner behavior, and request validation; the promptComposer milestone assertion was also updated to the current format.

2026-05-31

Added ROADMAP.md in the canonical ProjectShelf format to track planned work.