backup: 2026-06-17 07:36
This commit is contained in:
@@ -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, // 0–3
|
||||
hairColor, // 0–3
|
||||
eyeColor, // 0–3
|
||||
// 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: 0x00070000–0x000a0000,
|
||||
// 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: 20–35 years before protagonist; Father: 20–38 years before
|
||||
- Each grandparent: 20–35 years before their respective parent
|
||||
- Siblings: 0–3, 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.
|
||||
Reference in New Issue
Block a user