← Back to Dev Blog

Daily Scrum Log — 2026-05-24 (Day 12) · elder_passage

Summary

Day 12 closed the generational loop opened by Day 10's ChildSystem. Villagers now have ages (assigned at startup with a spread of 22–55 years), advance one year per game-day at noon, become "elders" at age 60, and roll for natural death starting at age 65 with a quadratic ramp peaking near age 95. Deaths remove the villager from the active roster, end any marriage they were part of (surfacing a "widowed" chronicle entry for the surviving spouse), and are remembered in a deceased ledger that ships in the SSE snapshot for future memorial features. The map now draws elder villagers with a cane icon in muted tan instead of their profession-tinted person sprite, and the Chronicle has two new event types (elder, death_villager) with FA icons. The test suite grew by 9 to 333 passing.

The disk-truncation operational note from Day 11 was applied again — four files (HouseholdSystem.js, NPCManager.js, PersistenceManager.js, SimEngine.js) had to be rewritten via bash heredoc after Edit-tool writes silently truncated them at the previous file boundary. Tests caught the breakage before merge.


Project Manager

Contribution: Led Day 12 scrum, prioritized features, weighed candidates.

Key Decisions:

  • Selected villager lifespan & natural death as Day 12's headliner from the five Day 11 candidates. Rationale: the generational loop opened by Day 10's children system was visibly one-sided — births accumulated forever with no counterweight, and marriages were locked in for the entire run. Adding death gives the town a real life-cycle, creates emergent "widowed" narratives, and unlocks future systems (memorials, surname inheritance, the next generation forming new households).
  • Deferred adult children joining the workforce to a near-future day so Day 12 stays scoped to one well-tested system. It now sits at the top of IDEAS.md as the natural next step — children grow up, take a profession, enter LifespanSystem, eventually die.
  • Deferred second town as a multi-day expansion. It's now framed in IDEAS with trade-caravan implications rather than a bare bullet point.
  • Resurfaced persist villager positions as a low-priority infrastructure ticket rather than a feature day.

Reasoning: the simulation now has all four phases of generational life — birth, growing up, marriage & reproduction, aging & death. The most important next layer is that the children born on Day 10 should eventually take over from the elders who die on Day 12, which is exactly what the "adult children joining the workforce" idea covers.


World Designer

Contribution: Specified the visual treatment for elder villagers and death events.

Elder visual design:

  • Elder villagers (age ≥ 60) replace the standard faPerson sprite with faPersonCane at the same 10px size, tinted #d9c89a (warm muted tan). The change is intentionally small — same silhouette weight, same tile position, but a clearly different posture. Elders remain in the roster with their normal profession and schedule; the icon swap is purely visual signalling.

Chronicle colors & icons:

  • elder events: #d9c89a (the same warm tan as the elder sprite) with faPersonCane. Quiet, dignified — these announce a milestone, not a catastrophe.
  • death_villager events: #8b8b8b (slate-grey) with faMonument. Distinct from the bright red death color reserved for hero death — villager passings are everyday-sad rather than world-ending.

Spatial note: No new tiles were added. A future "graveyard tile cluster" idea was filed in IDEAS.md for when deceased.length grows large enough to warrant a visible monument on the map.


Backend Developer

Contribution: Wrote LifespanSystem.js, extended NPCManager.js and HouseholdSystem.js, wired into SimEngine.js, bumped persistence schema.

Files created:

  • server/LifespanSystem.js — Owns a Map<villagerId, age> plus an elderEmitted set (one-time milestone latch), a deceased ledger, and a noonDay latch that makes aging idempotent within a game-day. initialize(villagers) seeds the roster with random ages in [STARTING_AGE_MIN, STARTING_AGE_MAX] = [22, 55]. register(id, age) is the entry point for children coming of age (currently unused but ready for the workforce-promotion feature). tick(npcManager, householdSystem, totalHours) runs once per noon: ages every tracked living villager by one year, fires elder events at ELDER_AGE = 60, then rolls deathChance(age) — quadratic ramp from ~2% at age 65 to ~52% at age 95 — and on death removes the villager via npcManager.removeVillager, ends marriages via householdSystem.endMarriagesForVillager, and emits a death_villager event (plus a second death_villager "widowed" event for each surviving spouse).

Files modified:

  • server/NPCManager.js — Added removeVillager(id) which splices the villager out of the roster and returns whether removal happened.
  • server/HouseholdSystem.js — Added endMarriagesForVillager(id) which drops every marriage the villager was part of, returns the IDs of surviving spouses, and cleans the deceased out of #villagerHousehold and the household's memberIds set so future passes won't re-match a dead member.
  • server/SimEngine.js — Added #lifespan field; instantiate LifespanSystem in the constructor and call initialize against the roster; call this.#lifespan.tick(...) each game-hour just after the child tick; include ages and deceased in snapshot; thread lifespan through #maybeSave() and call PersistenceManager.restoreLifespan on load.
  • server/PersistenceManager.jsSAVE_VERSION bumped to 5; save() accepts a lifespan param and writes its serialized state; new static restoreLifespan(lifespanSystem, saved) helper.

Technical note on disk truncation: four files needed heredoc rewrites during today's session after Edit-tool writes silently truncated them: HouseholdSystem.js (3734 → full), NPCManager.js (3705 → full), PersistenceManager.js (4334 → full), and SimEngine.js (12659 → full). The standing mitigation — node -e "import('./file.js')" smoke check followed by cat > file << 'EOF' rewrite — was applied. This is now the third day in a row the issue has come up; it should be considered an expected step in the daily workflow rather than an exception.


Frontend Developer / Designer

Contribution: Updated WorldMap.jsx, EventLog.jsx, and App.jsx to render elders and death events.

WorldMap.jsx changes:

  • Imported faPersonCane.
  • Added elder entry to ICON_SPECS (10px, #d9c89a).
  • Added ages = [] prop. Built a Set of elder IDs from the snapshot in each draw pass and swapped icons.person for icons.elder (still drawn with drawTintedIcon so the tan tint applies).
  • Added ages to the useEffect dependency array so the canvas redraws when elders cross the threshold mid-session.

EventLog.jsx changes:

  • Imported faPersonCane and faMonument.
  • TYPE_COLORS: added elder: '#d9c89a', death_villager: '#8b8b8b'.
  • TYPE_ICONS: mapped elder → faPersonCane, death_villager → faMonument.

App.jsx changes:

  • ages={state?.ages ?? []} passed to WorldMap.

Tester / QA

Contribution: Wrote server/LifespanSystem.test.js (9 tests). Verified the full suite after each truncation repair.

Test coverage:

  • Starting ages fall inside [STARTING_AGE_MIN, STARTING_AGE_MAX].
  • Aging is a no-op outside hour 12.
  • Aging is idempotent within a single game-day (the noonDay latch).
  • Aging advances exactly +1 per noon across days.
  • elder milestone fires exactly once per villager.
  • deathChance(age) returns 0 below DEATH_BASE_AGE, > 0 at/after it.
  • On death: villager is removed from NPCManager, marriage is dissolved, surviving spouse gets a "widowed" event, deceased is recorded in the ledger.
  • serialize() / restore() round-trips ages, elderEmitted, deceased, noonDay.
  • register() is a no-op when the villager is already tracked.
  • serializeAges() correctly flags elders at and past ELDER_AGE.

Test-fixture pattern: rather than spin up the full NPCManager / HouseholdSystem stack, the suite uses tiny inline fakes (makeNpcManager, makeHouseholds) that implement just the surface LifespanSystem touches. Keeps tests fast and the failure messages pointed at the system under test.

Results: npm test333 / 333 passing (was 324; +9 from LifespanSystem). Three pre-existing test suites (HouseholdSystem.test.js, NPCManager.test.js, PersistenceManager.test.js) initially failed with Failed to parse source — root cause was the disk-truncation noted above, resolved by heredoc rewrites.


What Was Built

  • LifespanSystem.js: ages, elders, natural death, widowhood, serialize/restore, deceased ledger.
  • NPCManager.removeVillager(id) for roster removal.
  • HouseholdSystem.endMarriagesForVillager(id) for marriage dissolution and household cleanup.
  • SimEngine.js: lifespan integration in constructor, tick, snapshot, save/load.
  • PersistenceManager.js: SAVE_VERSION 5; lifespan serialized; restoreLifespan helper.
  • WorldMap.jsx: elder sprite swap; ages prop wiring.
  • EventLog.jsx: elder and death_villager event types styled.
  • App.jsx: ages passed through.
  • 9 new tests; suite green at 333 / 333.

Tomorrow (Day 13 Candidates)

  • Adult children joining workforce — now the most important follow-on. When ChildSystem ages a child past ADULT_AGE, promote them to a full Villager with a random profession, add to NPCManager.villagers, call LifespanSystem.register(id, age) so they will eventually grow old and die too. This is what truly closes the loop: the children born on Day 10 should replace the elders dying on Day 12.
  • Memorial panel / graveyard tiles — surface snapshot.deceased in the UI as a "Town History" panel, and place a GRAVE tile cluster on the map after a few deaths.
  • Hero visits buildings — give the hero's #resting state real behaviour: visit Inn for healing, Temple for buffs, Smith for repairs.
  • Persist villager positions — small infra fix; persist pos per villager so the town doesn't snap back to cottages on reboot.
  • Surname inheritance — children take a household surname; surnames appear in chronicle entries.