Files
roam/trmnpeople.org
2026-06-17 07:36:46 +03:00

91 lines
4.9 KiB
Org Mode
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
: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.