backup: 2026-06-17 07:36

This commit is contained in:
2026-06-17 07:36:46 +03:00
parent 78bcf45137
commit 4806acf57d
13 changed files with 544 additions and 66 deletions
+90
View File
@@ -0,0 +1,90 @@
:PROPERTIES:
:ID: 423cac95-a80d-4db6-8bef-14297fb38437
:END:
#+title: trmn/people
#+filetags: :project: :knowledge: :people:
** Architecture
Each NPC is a ~PersonRecord~ stored in ~people: Map<personId, PersonRecord>~ returned by ~simulateLife()~.
Family is generated at the top of ~simulateLife()~ via ~generateFamily(masterSeed)~ before the day loop.
Friends/children are created lazily via post-apply hooks when their triggering events fire.
#+begin_src js
PersonRecord {
id, role, // e.g. 'mother', 'sibling_0', 'friend_3', 'child_0'
seed, // uint32, derived from masterSeed + role salt
sex, // 0=female 1=male
birthDay, // days from protagonist day 0 (negative = already born)
deathDay, // days from protagonist day 0
hairType, // 03
hairColor, // 03
eyeColor, // 03
// friends only:
metOnDay?, // day protagonist gained this friend
lostOnDay?, // day the friendship ended
lostCause?, // 'death' | 'drifted' | 'moved'
}
#+end_src
** PRNG Isolation
Protagonist's daily stream (~makeRng(masterSeed, d)~) is NEVER consumed by NPC logic.
NPC logic uses two isolated streams:
- Each NPC's own rng: ~makeNpcRng(deriveNpcSeed(masterSeed, salt))~ — for visual attrs + death age
- Shared per-day NPC stream: ~makeRng(deriveNpcSeed(masterSeed, 0xffff0000), d)~ — for attendance rolls and lose_friend cause/pick rolls
Reserved day slots: ~day = -1~ (protagonist traits, existing), ~day = -2~ (family layout offsets, new).
#+begin_src js
const ROLE_SALTS = {
mother: 0x00010000, father: 0x00020000,
maternal_grandmother: 0x00030000, maternal_grandfather: 0x00040000,
paternal_grandmother: 0x00050000, paternal_grandfather: 0x00060000,
sibling_0..3: 0x000700000x000a0000,
// friends: 0x01000000 + friendLifetimeIndex
// children: 0x02000000 + childLifetimeIndex
// npc-stream: 0xffff0000
}
#+end_src
~deriveNpcSeed(masterSeed, salt) = splitmix32(splitmix32(masterSeed) ^ salt)~
** Death Age Sampling
Algebraic Gompertz inverse-CDF — no per-NPC simulation loop needed:
#+begin_src js
function sampleDeathAge(rng) {
const healthOffset = (rng() - 0.5) * 15; // draw 1
const u = clamp(rng(), 0.001, 0.999); // draw 2
const t = Math.log(1 - (Math.log(1-u) * 0.095 / 0.0003)) / 0.095;
return Math.round(clamp(t + healthOffset, 1, 108));
}
#+end_src
Draw order per NPC rng: sex → hairType → hairColor → eyeColor → healthOffset → u (sampleDeathAge).
** Post-Apply Hooks (inside simulateLife day loop)
After ~Object.assign(s, ev.apply(...))~, five events trigger NPC record creation/mutation:
- ~gain_friend~ → create PersonRecord, push to ~s.friendList~, increment ~s.friendLifetimeIndex~
- ~lose_friend~ → splice from ~s.friendList~, roll cause (death 20% / drifted 50% / moved 30%); if death, upgrade eventLog entry from ~lose_friend~ to ~friend_death~
- ~move_city~ → splice last min(5, friendList.length) friends, mark lostCause='moved'
- ~first_child~ / ~subsequent_child~ → create child PersonRecord, push to ~s.childrenList~
State additions to ~defaultState()~: ~friendList [], friendLifetimeIndex 0, childrenList [], childLifetimeIndex 0~
(These are not packed into the Float32Array buffer; only ~friendCount~ / ~numChildren~ are packed.)
** Family Generation
~generateFamily(masterSeed)~ uses ~makeRng(masterSeed, -2)~ to draw birth offsets:
- Mother: 2035 years before protagonist; Father: 2038 years before
- Each grandparent: 2035 years before their respective parent
- Siblings: 03, each within ±8 years of protagonist (some already born at day 0, some born later)
Grandparents with ~deathDay < 0~ were dead before protagonist was born — valid, just not triggering events.
** Events
- ~sibling_born~ (scheduled) — fires on sibling's birthDay if > 0; no state change, just logged with ~personId~
- ~family_member_death~ (loop, step 4) — fires when any non-friend NPC's ~deathDay === d~; logged with ~personId~ and ~attended~ (70% probability)
- ~friend_death~ (post-apply of lose_friend) — replaces ~lose_friend~ in eventLog when cause roll < 0.20
** Gotchas
- ~stats.mjs~ and any eventLog consumer must use ~e.id~ not ~e~ directly — entries are now ~{id, personId?, attended?}~ objects, not strings. Broke three spots in stats.mjs (all fixed).
- ~friendList~ and ~friendCount~ are kept in sync manually after every mutation. If they diverge, prereq ~friendCount > 0~ fires ~lose_friend~ but ~friendList~ is empty → guard: ~if (ev.id === 'lose_friend' && s.friendList.length > 0)~.
- Friend ~birthDay~ can be positive (friend younger than protagonist). At young protagonist ages (age 2), the ±15yr range means some friends haven't been born yet at meeting time. Cosmetic only — not used in any gameplay logic.
- ~sibling_born~ uses a dynamically built ~mergedSchedule~ (SCHEDULED + sibling events, sorted). The existing static SCHEDULED array is unchanged.