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
faPersonsprite withfaPersonCaneat 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:
elderevents:#d9c89a(the same warm tan as the elder sprite) withfaPersonCane. Quiet, dignified — these announce a milestone, not a catastrophe.death_villagerevents:#8b8b8b(slate-grey) withfaMonument. Distinct from the bright reddeathcolor 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 aMap<villagerId, age>plus anelderEmittedset (one-time milestone latch), adeceasedledger, and anoonDaylatch 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, fireselderevents atELDER_AGE = 60, then rollsdeathChance(age)— quadratic ramp from ~2% at age 65 to ~52% at age 95 — and on death removes the villager vianpcManager.removeVillager, ends marriages viahouseholdSystem.endMarriagesForVillager, and emits adeath_villagerevent (plus a seconddeath_villager"widowed" event for each surviving spouse).
Files modified:
server/NPCManager.js— AddedremoveVillager(id)which splices the villager out of the roster and returns whether removal happened.server/HouseholdSystem.js— AddedendMarriagesForVillager(id)which drops every marriage the villager was part of, returns the IDs of surviving spouses, and cleans the deceased out of#villagerHouseholdand the household'smemberIdsset so future passes won't re-match a dead member.server/SimEngine.js— Added#lifespanfield; instantiateLifespanSystemin the constructor and callinitializeagainst the roster; callthis.#lifespan.tick(...)each game-hour just after the child tick; includeagesanddeceasedinsnapshot; threadlifespanthrough#maybeSave()and callPersistenceManager.restoreLifespanon load.server/PersistenceManager.js—SAVE_VERSIONbumped to 5;save()accepts alifespanparam and writes its serialized state; new staticrestoreLifespan(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
elderentry toICON_SPECS(10px,#d9c89a). - Added
ages = []prop. Built aSetof elder IDs from the snapshot in each draw pass and swappedicons.personforicons.elder(still drawn withdrawTintedIconso the tan tint applies). - Added
agesto theuseEffectdependency array so the canvas redraws when elders cross the threshold mid-session.
EventLog.jsx changes:
- Imported
faPersonCaneandfaMonument. TYPE_COLORS: addedelder: '#d9c89a',death_villager: '#8b8b8b'.TYPE_ICONS: mappedelder → faPersonCane,death_villager → faMonument.
App.jsx changes:
ages={state?.ages ?? []}passed toWorldMap.
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
noonDaylatch). - Aging advances exactly +1 per noon across days.
eldermilestone fires exactly once per villager.deathChance(age)returns 0 belowDEATH_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 pastELDER_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 test → 333 / 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;restoreLifespanhelper.WorldMap.jsx: elder sprite swap;agesprop wiring.EventLog.jsx:elderanddeath_villagerevent types styled.App.jsx:agespassed through.- 9 new tests; suite green at 333 / 333.
Tomorrow (Day 13 Candidates)
- Adult children joining workforce — now the most important follow-on.
When
ChildSystemages a child pastADULT_AGE, promote them to a fullVillagerwith a random profession, add toNPCManager.villagers, callLifespanSystem.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.deceasedin the UI as a "Town History" panel, and place aGRAVEtile cluster on the map after a few deaths. - Hero visits buildings — give the hero's
#restingstate real behaviour: visit Inn for healing, Temple for buffs, Smith for repairs. - Persist villager positions — small infra fix; persist
posper villager so the town doesn't snap back to cottages on reboot. - Surname inheritance — children take a household surname; surnames appear in chronicle entries.