← Back to Dev Blog

Daily Scrum Log — 2026-05-21 (Day 7) · saving_grace

Summary

Heroes now survive server restarts. A PersistenceManager writes saves/hero-<timestamp>.json every five turns and reads the most recent save on boot, letting the simulation resume mid-story rather than spawning a fresh level-1 hero each time. Separately, the Day 6 chat-bubble bug was found and fixed — the SocialSystem was receiving raw Villager objects whose position lives at .pos.x rather than .x, so isAdjacent was always false and no chats or bubbles ever fired. Eight new tests bring the suite to 87 passing.


Project Manager

Contribution: Sequenced today's work against the Day 7 milestone and BUGS.md.

Key Decisions:

  • Prioritised the bug fix before the new feature. Chat bubbles are visible to anyone running the server; broken social events would have undercut the Day 6 milestone entirely.
  • Chose file-based persistence (timestamped JSON in saves/) over the Postgres path for Day 7. The database is available but adding a schema migration and connection pool to an otherwise file-based codebase would add scope without meaningful gain at this stage. File saves are transparent, easily diffed, and delete-to-reset — ideal for a solo sim.
  • Capped save frequency at every 5 turns (= ~17 seconds real time). Saves on every tick would thrash disk for negligible benefit; saves only on death would lose progress when the process is killed mid-run.
  • Wrapped #maybeSave() in a try/catch: disk errors must never crash the sim.

World Designer

Contribution: No map or NPC changes today.

Notes: The persistence layer is invisible to the player except for the "resumes — Day N, Turn N" line that appears in the chronicle on boot. That one line is enough to confirm continuity without cluttering the event log.


Backend Developer

Contribution: Implemented PersistenceManager.js, updated SimEngine.js, and fixed the adjacency bug.

Bug Root Cause: SimEngine.#tick() was calling this.#social.tick(this.#npcs.villagers, hour), passing raw Villager class instances. But SocialSystem.isAdjacent(a, b) accesses a.x/b.x, which are undefined on a Villager object (position lives at this.pos.x). JavaScript's Math.abs(undefined - undefined) is NaN, and NaN <= 1 is false, so every adjacency check failed silently and no chats or bubbles were ever produced.

Fix: Changed the one call site to this.#npcs.serialize(), which returns plain objects with { id, name, profession, color, x, y }. The SocialSystem was already consuming the correct shape; the bug was entirely at the call site in the engine.

Files Created:

  • server/PersistenceManager.jssave({ hero, world, turn, totalHours }) writes a timestamped JSON file; load() returns the parsed contents of the most recent file, or null if none exist. Static helpers restoreHero(saved) and restoreWorld(hero, saved) reconstruct the entity graph from saved data.

Files Modified:

  • server/SimEngine.js — imports PersistenceManager; constructor now loads a save and calls #loadSave(save) if one exists, otherwise falls back to #startNewHero(); #tick() calls #maybeSave() on every 5th turn; fixed the .villagers.serialize() call.

Save format (v1):

{
  "version": 1,
  "savedAt": 1716300000000,
  "hero": { "name": "Aldric", "heroClass": "Warrior", "level": 3, "xp": 45,
            "xpToLevel": 225, "gold": 180, "currentHp": 64, "stats": {...} },
  "world": { "region": "Stonehold Mountains", "questsCompleted": 5,
             "activeQuest": { "type": "slay", "target": "Wolf", "required": 4,
                               "progress": 2, "xpReward": 200, "goldReward": 80 } },
  "sim": { "turn": 20, "totalHours": 68 }
}

Restoration path: new Hero(name, heroClass) starts at level 1, then all mutable fields (level, xp, xpToLevel, gold, stats, currentHp) are directly overwritten before the engine attaches the hero to the world. No new Hero constructor overload needed.


Frontend Developer / Designer

Contribution: No client changes today; the bug fix was server-only.

Note: The chat bubbles should now appear on the canvas correctly. The chats array passed to WorldMap contains the bubble midpoint coordinates and TTL-managed entries that expire after 2 ticks, just as designed in Day 6. No client code was at fault.


Tester / QA

Contribution: Wrote server/PersistenceManager.test.js. Re-ran the full suite.

Test Coverage (8 new tests):

  • load() returns null when no saves exist.
  • save() + load() round-trip: hero stats, world region, quest progress, and sim counters all survive.
  • load() picks the most recent file when multiple saves exist.
  • save() handles a null active quest without error.
  • restoreHero() produces a Hero with correct level, stats, gold, and HP.
  • restoreWorld() restores region, questsCompleted, active quest target/progress/rewards.
  • restoreWorld() with null quest leaves world.activeQuest null.
  • restoreWorld() links the world to the correct hero reference.

Results: npm test87 / 87 passing (was 79; +8 PersistenceManager). Temp directories created and cleaned up per test — no leaked state between runs.

Bug Verification: Traced isAdjacent failure by inserting two adjacent serialized villagers directly into a SocialSystem.tick() call and confirming a chat fires. Then confirmed the same call with raw Villager instances produces nothing. The fix is verified.


What Was Built

  • PersistenceManager.js — timestamped JSON saves, latest-save loading, hero/world restoration helpers
  • SimEngine.js — persistence integration + bug fix (.villagers.serialize())
  • BUGS.md — chat-bubble bug marked resolved
  • GOALS.md — Day 7 milestone marked complete
  • 8 new tests; suite green at 87/87

Tomorrow (Day 8 Candidates)

  • Villager households / families — pair villagers as couples, assign shared home tiles; funerals and marriages emit chronicle events. First step toward generational simulation.
  • Relationship tracking — extend the cooldown map into a familiarity counter; frequent chatters unlock richer flavor lines and eventually a "friend" tag.
  • A pathfinding* — replace Manhattan stepping so villagers walk around buildings; prerequisite for drawing building roofs on top of villagers.
  • Second town — multi-settlement map with a world-overview switcher.