@its-not-rocket-science/ananke 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2227 @@
1
+ # Ananke
2
+ ![CI](../../actions/workflows/ci.yml/badge.svg)
3
+
4
+ **Ananke** is a deterministic, physics-grounded simulation engine for characters, combat,
5
+ and survivability — from a single duel to a campaign-scale world.
6
+
7
+ It models entities using **real physical quantities** rather than abstract hit points,
8
+ using **SI units stored as fixed-point integers** for full determinism.
9
+ Same seed + same inputs → identical results, at every scale.
10
+
11
+ The core promise is narrow and hard: deterministic, physics-grounded simulation for
12
+ characters, combat, and survivability across tactical-to-campaign scales.
13
+ Everything else — world simulation, mythology, emotional contagion, tech diffusion — is
14
+ built on top of that foundation and can be adopted independently.
15
+
16
+ ---
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @its-not-rocket-science/ananke
22
+ ```
23
+
24
+ ```typescript
25
+ import { stepWorld, mkWorld, q, SCALE } from "@its-not-rocket-science/ananke";
26
+ ```
27
+
28
+ Requires **Node ≥ 18**. TypeScript declarations are included — no `@types/` package needed.
29
+
30
+ The package is ESM-only and ships compiled JS + `.d.ts` files. There are no runtime
31
+ dependencies.
32
+
33
+ > **New to Ananke?** Start with the **[Programmer's Guide](docs/programmers-guide.md)** —
34
+ > quick starts for all three adoption paths, core concepts, API reference, and worked code
35
+ > examples.
36
+
37
+ > **Versioning:** pin to a specific version in production. The `0.x` series may include
38
+ > minor-version breaking changes to Tier 2 (experimental) APIs; Tier 1 (Stable) APIs follow
39
+ > full semver. See [`STABLE_API.md`](STABLE_API.md) for the tier breakdown and
40
+ > [`docs/versioning.md`](docs/versioning.md) for the upgrade policy.
41
+
42
+ ---
43
+
44
+ ## Start here: three adoption paths
45
+
46
+ Choose the path that matches your immediate need. Ignore the rest until you need it.
47
+
48
+ ### Path A — "I want a deterministic combat kernel"
49
+
50
+ **Install:** `npm install @its-not-rocket-science/ananke`
51
+
52
+ **Minimal imports:** `stepWorld`, `mkWorld`, `q`, `SCALE`, `generateIndividual`
53
+
54
+ **30-minute quickstart:** Run `tools/vertical-slice.ts` (`npm run run:vertical-slice`).
55
+ A Knight fights a Brawler across three seeds, producing a physics-grounded combat log.
56
+ Read `docs/host-contract.md` for the stable integration surface, then
57
+ `docs/integration-primer.md` for data-flow diagrams and type glossary.
58
+
59
+ **Key entry points:** `stepWorld()`, `resolveHit()`, `generateIndividual()`, `mkKnight()`
60
+
61
+ **What to ignore for now:** Phases 56–67 (campaign-scale systems), all `tools/` except `vertical-slice.ts`
62
+
63
+ ---
64
+
65
+ ### Path B — "I want a campaign / world simulation"
66
+
67
+ **Minimal modules:** Path A + `src/campaign.ts`, `src/polity.ts`, `src/tech-diffusion.ts`
68
+
69
+ **30-minute quickstart:** Run `npm run run:what-if` to see alternate-history polity scenarios
70
+ (plague, charismatic leader, sudden war) with probability-weighted outcome distributions.
71
+
72
+ **Key entry points:** `stepPolityDay()`, `stepTechDiffusion()`, `applyEmotionalContagion()`,
73
+ `compressMythsFromHistory()`
74
+
75
+ **What to ignore for now:** `src/sim/model3d.ts`, `src/bridge/`, `docs/editors/`
76
+
77
+ ---
78
+
79
+ ### Path C — "I want physiology / species modelling"
80
+
81
+ **Minimal modules:** `src/units.ts`, `src/sim/bodyplan.ts`, `src/archetypes.ts`,
82
+ `src/describe.ts`
83
+
84
+ **30-minute quickstart:** Open `docs/editors/species-forge.html` in a browser.
85
+ Design a body plan, set archetype sliders, and copy the generated TypeScript trio
86
+ (`BodyPlan` + `Archetype` + `NarrativeBias`) into your project.
87
+
88
+ **Key entry points:** `generateIndividual()`, `applyAgingToAttributes()`,
89
+ `applySleepToAttributes()`, `extractRigSnapshots()`
90
+
91
+ **What to ignore for now:** `src/polity.ts`, `src/tech-diffusion.ts`, all `tools/`
92
+
93
+ ---
94
+
95
+ ### Where Ananke sits in a larger system
96
+
97
+ Ananke is the physics and biology layer — Layer 2 in the stack below. Layers above it
98
+ are partially or fully implemented; Layer 7 is long-term vision.
99
+
100
+ | Layer | Purpose | Status |
101
+ |-------|---------|--------|
102
+ | **7 — The Universe** | Cosmological scale: planets, interstellar travel, multiple worlds | Long-term vision |
103
+ | **6 — The World** | Geopolitical scale: nations, empires, diplomacy, trade, war, tech diffusion | **Complete** (Phases 61, 67) |
104
+ | **5 — The Society** | Cultural scale: cities, factions, myths, legends, mass psychology | **Complete** (Phases 36, 45, 50, 66) |
105
+ | **4 — The Group** | Social scale: parties, armies, organisations, emotional contagion | **Complete** (Phases 22, 48, 51, 65) |
106
+ | **3 — The Individual** | Character scale: generation, narrative shaping, aging, sleep, skill progression | **Complete** (Phases 21, 33–39, 57–58, 62) |
107
+ | **2 — The Simulation** | Physics and biology kernel | **Complete** (Phases 1–60) |
108
+ | **1 — The Interface** | Visual editors, Species Forge, Culture Forge, Simulation Zoo, Generative Cartography, Persistent World Server, "What If?" engine, validation dashboard, Narrative Stress Test | **Complete** (ROADMAP items 7–11, Long-Term Vision all four; companion renderer READMEs in `docs/companion-projects/`) |
109
+
110
+ The kernel's deterministic, physics-first foundation makes it suited to the full stack. A
111
+ scenario that would require hand-waving in a narrative RPG — "does the hero survive this
112
+ battle?" — becomes a distribution of outcomes across thousands of seeded runs, each
113
+ physically grounded.
114
+
115
+ ---
116
+
117
+ ## Core design principles
118
+
119
+ ### Deterministic by default
120
+
121
+ - Lockstep-safe simulation
122
+ - No floating-point drift
123
+ - Stable ordering and RNG consumption
124
+ - Same seed + inputs → identical results
125
+ - Entity iteration and pair resolution are stable and ordered
126
+
127
+ ### Physics-first
128
+
129
+ - Impact energy → injury (not dice roll → damage)
130
+ - Mass, velocity, leverage, penetration physics
131
+ - Encumbrance affects movement and fatigue
132
+
133
+ ### Fixed-point arithmetic
134
+
135
+ - All simulation values use Q fixed-point integers
136
+ - No runtime floats in simulation path
137
+ - Units centralised in `src/units.ts`
138
+
139
+ ### No arbitrary scales
140
+
141
+ Individual variation is expressed in absolute physical terms, not 1–20 ranges.
142
+
143
+ | What varies | Engine representation | Unit |
144
+ |---|---|---|
145
+ | Strength | `peakForce_N` | N (newtons) |
146
+ | Explosive power | `peakPower_W` | W (watts) |
147
+ | Stamina | `continuousPower_W` | W (watts) |
148
+ | Energy reserves | `reserveEnergy_J` | J (joules) |
149
+ | Reaction speed | `reactionTime_s` | s (seconds) |
150
+ | Motor coordination | `controlQuality`, `fineControl` | dimensionless 0–1 |
151
+ | Body size | `stature_m`, `mass_kg` | m, kg |
152
+
153
+ An "average human" has `peakForce_N = 1840`, `peakPower_W = 1200`, `continuousPower_W = 200`,
154
+ `reserveEnergy_J = 20000`, `reactionTime_s = 0.20`. These are real sport-science values.
155
+ Variation between individuals is generated by `generateIndividual()` using per-archetype
156
+ variance distributions, producing a unique entity with realistic physical spread.
157
+
158
+ ### Biology-agnostic
159
+
160
+ - Supports humans, robots, aliens, and abstract entities
161
+ - Genericised actuation, structure, and control systems
162
+ - Region-based anatomy adaptable to any morphology (Phase 8)
163
+
164
+ ### Scalable
165
+
166
+ - Single entity to squad to formation to army
167
+ - Spatial partitioning and density modelling
168
+ - Deterministic large-battle simulation
169
+
170
+ ---
171
+
172
+ ## Current implementation status
173
+
174
+ **Phases 1–60 complete** (including Phase 6 Formation System, 2ext, 3ext, 8B, 8C, 10B, 10C, 11C, 12B, 31–60). Melee combat,
175
+ grappling, stamina and exhaustion, weapon dynamics (including swing momentum carry), ranged
176
+ and projectile combat (including aiming time, moving target penalty, suppression→AI behaviour,
177
+ and ammo type overrides), injury, entity environmental hazards, movement physics, formation
178
+ basics, deterministic AI scaffolding,
179
+ perception/cognition (sensory model, decision latency, surprise mechanics), morale and
180
+ psychological state (fear accumulation, routing, panic variety, leader/banner auras, rally
181
+ mechanic), terrain systems (surface friction, obstacle/cover grids, elevation, slope direction,
182
+ dynamic hazard cells, AI cover-seeking, cover morale bonus, elevation melee advantage), a
183
+ physics-grounded skill system, a universal data-driven body plan system (humanoid, quadruped,
184
+ theropod, sauropod, avian, vermiform, centaur, octopoid — adding a new species requires only a
185
+ BodyPlan data file and an Archetype baseline, no kernel changes), a full injury and medical
186
+ simulation layer (fractures, infection, permanent damage, natural clotting, fatal fluid loss,
187
+ and a `TreatCommand` system with tiered medical equipment and skill-scaled treatment rates),
188
+ environmental physics including blast and fragmentation explosions, fall damage,
189
+ pharmacokinetics with substance interactions (stimulant/haemostatic/anaesthetic/poison
190
+ cross-effects, temperature-dependent metabolism), flash blindness from explosions, ambient
191
+ temperature stress, a technology spectrum system (`TechContext`, `TechEra`,
192
+ `TechCapability`, `validateLoadout`) with powered exoskeleton items, energy weapons,
193
+ reflective and ablative armour, sensor items, and era-gated starter items, a **capability
194
+ sources and effects** system (Clarke's Third Law — magic and advanced technology are the
195
+ same abstraction; only the tags differ), a **deterministic replay and analytics** system
196
+ (`ReplayRecorder`, `replayTo`, `serializeReplay`/`deserializeReplay`, `CollectingTrace`,
197
+ `collectMetrics`, `survivalRate`, `meanTimeToIncapacitation`), a **3D model integration
198
+ layer** (`deriveMassDistribution`, `deriveInertiaTensor`, `deriveAnimationHints`,
199
+ `derivePoseModifiers`, `deriveGrappleConstraint`, `extractRigSnapshots`) for driving
200
+ host renderer rigs from simulation state, a **named archetype and scenario library**
201
+ (`AMATEUR_BOXER`, `PRO_BOXER`, `GRECO_WRESTLER`, `KNIGHT_INFANTRY`, `LARGE_PACIFIC_OCTOPUS`
202
+ with corresponding `mkBoxer`/`mkWrestler`/`mkKnight`/`mkOctopus`/`mkScubaDiver` factory
203
+ functions in `src/presets.ts`) validated against real-world biomechanics data by a
204
+ statistical scenario test suite, a **character description layer** (`describeCharacter`,
205
+ `formatCharacterSheet`, `formatOneLine` in `src/describe.ts`) that translates SI fixed-point
206
+ attributes into human-readable summaries with tier ratings, labelled comparisons, and plain
207
+ English descriptions grounded in real-world benchmarks, a **historical weapons database**
208
+ (`src/weapons.ts`) covering ~70 weapons across six eras (Prehistoric through Contemporary)
209
+ with two combat extensions: flexible/chain weapon **shield bypass** (`shieldBypassQ` reduces
210
+ effective blocking coverage for flails and morning stars) and **magazine tracking**
211
+ (`magCapacity` + `shotInterval_s` give magazine firearms per-shot and reload cooldowns), a
212
+ **combat narrative layer** (`src/narrative.ts`) that converts trace event streams into
213
+ human-readable combat logs with configurable verbosity, physics-grounded verb selection, and
214
+ injury/outcome summaries, a **downtime and recovery simulation** (`src/downtime.ts`) that
215
+ bridges combat-resolution time (20 Hz) and campaign time (hours to weeks) via a compressed
216
+ 1 Hz loop with tiered care levels, resource tracking, and recovery projection, an **arena
217
+ simulation framework** (`src/arena.ts`) providing a declarative scenario DSL, batch trial
218
+ runner, expectation system, and six physics-calibrated built-in scenarios for validating
219
+ simulation realism, and a **character progression system** (`src/progression.ts`) that adds
220
+ the temporal axis to entity attributes: XP/milestone-driven skill advancement using geometric
221
+ thresholds, physical training drift bounded by genetic ceiling, physiologically-grounded
222
+ ageing curves, and permanent injury sequelae, and a **campaign and world state layer**
223
+ (`src/campaign.ts`) that persists entity state, location, and inventory between sessions:
224
+ world clock advancement with integrated downtime healing, a location registry with
225
+ travelCost routing, campaign-level item stockpiles, and Map-aware JSON serialisation so
226
+ full campaign state round-trips cleanly, and a **dialogue and negotiation layer**
227
+ (`src/dialogue.ts`) that resolves non-combat social actions — intimidation, persuasion,
228
+ deception, surrender, and trade negotiation — using the same physical and psychological
229
+ attributes as the combat engine: intimidation strength derives from `peakForce_N`, deception
230
+ is defeated by `attentionDepth`, and escalation triggers when a fearless target interprets
231
+ an intimidation attempt as an insult, and a **faction and reputation system**
232
+ (`src/faction.ts`) that tracks faction membership, inter-faction standing, entity reputation,
233
+ and a witness system that propagates reputation deltas from combat events: kills reduce the
234
+ actor's standing with the victim's faction, aid increases it, and the AI decision layer
235
+ suppresses attacks on entities with standing above `STANDING_FRIENDLY_THRESHOLD`, with a
236
+ self-defence override when the attacker has already taken damage, and a **loot and economy
237
+ layer** (`src/economy.ts`) that adds item valuation, equipment degradation, drop resolution,
238
+ and trade mechanics: `computeItemValue` derives cost-unit prices from physical properties
239
+ (mass, reach, armour resist), `applyWear` tracks cumulative use-wear on melee weapons
240
+ (penalty at q(0.30), fumble at q(0.70), breaks at q(1.0)), `resolveDrops` produces
241
+ deterministic loot from dead or incapacitated entities, and `evaluateTradeOffer` evaluates
242
+ trades from the accepting party's perspective, a **momentum transfer and knockback**
243
+ layer (`src/sim/knockback.ts`) that converts impulse-momentum physics into stagger and
244
+ prone transitions, a **hydrostatic shock and cavitation** layer (`src/sim/hydrostatic.ts`)
245
+ that amplifies internal damage for high-velocity projectiles based on per-tissue compliance
246
+ (temporary cavity ×1.0–×3.0 above 600 m/s; cavitation bleed boost in fluid-saturated organs
247
+ above 900 m/s), and a **cone AoE** system (`src/sim/cone.ts`) that adds directional
248
+ geometry to the capability system for breath weapons, flamethrowers, gas dispersal, and
249
+ sonic blasts: `entityInCone` geometry with configurable half-angle and range, `"facing"` and
250
+ `"fixed"` direction modes, sustained emission with per-tick cost deduction and shock interrupt,
251
+ a `weaponImpact` payload variant for custom per-effect damage profiles, and a full
252
+ dragon-vs-knight scenario demonstration, a **staged thermoregulation model**
253
+ (`src/sim/thermoregulation.ts`) that tracks core body temperature using a heat-balance
254
+ equation (metabolic heat vs. conductive loss through armour and skin), deriving performance
255
+ modifiers across seven stages from mild hyperthermia to cardiac-arrest hypothermia — each
256
+ with calibrated powerMul, fineControlPen, and latencyMul penalties, and a **nutrition and
257
+ starvation model** (`src/sim/nutrition.ts`) that adds the longest survivability axis:
258
+ caloric balance tracked via Kleiber's-law BMR, four hunger states (sated → hungry →
259
+ starving → critical) with staged fat catabolism (300 g/day), muscle catabolism (0.5 N/hour
260
+ reduction in peakForce_N), and a six-item food catalogue from ration bars to water flasks,
261
+ a **species and race system** (`src/species.ts`) providing a data-driven species registry
262
+ where `SpeciesDefinition` bundles archetype, body plan, innate traits, capabilities, natural
263
+ weapons, and physiological overrides into one record: 14 species across fantasy humanoids
264
+ (elf, dwarf, halfling, orc, ogre, goblin, troll), sci-fi humanoids (Vulcan, Klingon, Romulan),
265
+ mythological (dragon, centaur, satyr), and fictional (Heechee); `SpeciesPhysiology` adds
266
+ `coldBlooded` (skips thermoregulation), `bmrMultiplier` (Vulcan q(0.72) starves slower;
267
+ orc q(1.15) starves faster), and `naturalInsulation_m2KW` (scales heat loss), and a
268
+ **multiple intelligences layer** (`src/types.ts`, wired through targeting, morale, decide,
269
+ and dialogue) implementing Howard Gardner's nine cognitive domains plus inter-species empathy
270
+ as Q-coded attributes on every archetype and species: `spatial` scales the threat-horizon
271
+ perception radius (human baseline q(0.60) → ×1.0; octopus q(0.90) → ×1.30),
272
+ `logicalMathematical` reduces decision latency (Vulcan q(0.95) → ×0.82; ogre q(0.25) →
273
+ ×1.10), `interpersonal` scales the effective leader-aura reception radius, `intrapersonal`
274
+ boosts effective distress tolerance in morale computations, `linguistic` sets the per-entity
275
+ persuasion base probability in dialogue (elf q(0.80) → q(0.44); ogre q(0.25) → q(0.275)),
276
+ and `bodilyKinesthetic` sets a floor on fine-motor precision (`fineControl ≥ bk × q(0.80)`).
277
+
278
+ **Phase 34** extends `bodilyKinesthetic` and `spatial` into non-combat competence resolvers
279
+ (`src/competence/`): `resolveCrafting(entity, spec, seed)` computes craft quality
280
+ (materialQ × BK × fineControl-bonus × toolBonus ± RNG) and time (baseTime_s × q(0.50) / BK);
281
+ `computeSurgicalPrecision(entity)` returns lerp(q(0.70), q(1.30), BK) for use as
282
+ `TreatmentSchedule.surgicalPrecisionMul` in downtime surgery; `resolveNavigation(entity, spec, seed)`
283
+ gives routeEfficiency = clamp(spatial × mapBonus × terrainQ × visibilityQ, q(0.50), q(1.0)) and
284
+ the resulting timeLost_s.
285
+
286
+ **Phase 35** adds the `naturalist` intelligence competence resolver (`src/competence/naturalist.ts`):
287
+ `resolveTracking(entity, spec, seed)` computes track confidence from naturalist skill, track age,
288
+ terrain preservation (ideal/rain/urban/deep_water/forest), and species-specific difficulty;
289
+ `resolveForaging(entity, spec, seed)` yields itemsFound per hour by biome (forest/plains/desert/
290
+ swamp/mountain) and season with misidentification risk (P_misidentify = max(0, 0.30 − naturalist × 0.40));
291
+ `resolveTaming(entity, spec, seed)` computes trust_Q = naturalist × interSpecies × effort − fear × 0.50,
292
+ with attack probability when fear exceeds trust.
293
+
294
+ **Phase 36** adds inter-species intelligence (`src/competence/interspecies.ts`): the `signal` dialogue
295
+ action for cross-species communication (`{ kind: "signal"; targetSpecies; intent: "calm" | "submit" | "ally" | "territory" }`)
296
+ with success probability `empathy_Q × vocab × (1 − fear × 0.60)` and potential aggravation when low
297
+ empathy meets fearful targets; `computeUnfamiliarSpeciesLatencyPenalty(entity, species)` returns
298
+ up to +80 ms decision latency when facing unfamiliar species (those not in `speciesAffinity`).
299
+
300
+ **Phase 37** extends linguistic and interpersonal intelligence into practical applications:
301
+ `LanguageCapacity` (languageId + fluency_Q) added to `Entity.languages`; `resolveCommandTransmission`
302
+ computes formation reception rate (`linguistic × formationBonus`) and transmission delay;
303
+ `resolveTeaching` computes XP transfer (`hours × BASE_XP_RATE × interpersonalTeacher × interpersonalLearner`)
304
+ with teacher fatigue; `computeDeceptionDetectionProbability` factors interpersonal intuition into
305
+ detecting lies (`attentionDepth × 0.50 + interpersonal × 0.50 − plausibility`).
306
+
307
+ **Phase 38** implements logical-mathematical and intrapersonal intelligence: `WillpowerState`
308
+ (`current_J/max_J`) added to `Entity.willpower`; `computeMaxWillpower(entity)` calculates capacity
309
+ from intrapersonal intelligence (max 50kJ); `deductWillpower()`, `replenishWillpower()`, and
310
+ `stepConcentrationWillpower()` manage cognitive stamina for sustained concentration (Phase 12B);
311
+ `resolveEngineering(entity, spec, seed)` computes project quality (`qualityMul = logicalMath ×
312
+ complexityMul × timeFactor`) with latent flaw probability (`P_flaw = max(0, complexity − logicalMath) × 0.40`);
313
+ descriptor bands: exceptional/good/adequate/poor/failure.
314
+
315
+ **Phase 39** adds musical intelligence and acoustic systems: `deriveAcousticSignature(entity)`
316
+ computes noise level from movement velocity, equipment (metal armour +15, heavy weapons +10),
317
+ and stealth skill; `detectAcousticSignature(listener, source, dist_m)` returns detection confidence
318
+ (`sourceNoise / dist_m × listener.musical × SCALE_ACOUSTIC`); `resolveFormationSignal(signaller,
319
+ signal, listener, dist_m)` computes command clarity (`musical(signaller) × musical(listener) ×
320
+ rangeFactor`) for military coordination (drums, horns); `resolvePerformance(performer, spec)`
321
+ generates morale auras (`fearDecayBonus = musical × q(0.020)`) draining willpower per second;
322
+ performance types: march/rally/dirge/celebration/lament.
323
+
324
+ **Phase 47** adds individual AI personalities beyond policy presets (`src/sim/ai/personality.ts`):
325
+ `PersonalityTraits` — four Q-coded axes: `aggression` (shifts retreat range, overrides hesitation
326
+ above q(0.70)), `caution` (±q(0.20) defence intensity boost), `loyalty` (switches focus target to
327
+ protect a distressed ally), `opportunism` (switches to the most-wounded enemy). Five named presets:
328
+ `berserker`, `coward`, `guardian`, `schemer`, `soldier`. `derivePersonalityFromCognition()` maps
329
+ distressTolerance → aggression, intrapersonal → caution, interpersonal → loyalty,
330
+ logicalMathematical → opportunism. `entity.personality?: PersonalityTraits` is optional and
331
+ backward-compatible (absent = neutral = q(0.50) on all axes, no behaviour change).
332
+
333
+ **Phase 48** adds multi-party dynamics (`src/party.ts`): `Party` and `PartyMember` types with
334
+ per-member `loyaltyQ`, `stepPartyLoyalty()` (leader interpersonal aura + resource/injury/hunger
335
+ modifiers), `checkDefection()` (probabilistic when loyalty < q(0.20)), `resolvePartyConflict()`
336
+ (combat-power ratio determines outcome), `recordMissionSuccess/Failure()`.
337
+
338
+ **Phase 49** adds legacy and inheritance (`src/inheritance.ts`): character death does not end the
339
+ campaign — `transferEquipment()` moves loadout items to the heir, `transferRelationships()` partially
340
+ copies the relationship graph (trust/affinity scaled by `relationshipTransferRate_Q`, default q(0.50)),
341
+ `transferInventory()` merges campaign inventories, and `applyInheritance()` orchestrates all three
342
+ steps while updating the campaign registry and location tracking.
343
+
344
+ **Phase 50** adds mythology and legend (`src/legend.ts`): high-significance chronicle entries crystallise
345
+ into `Legend` objects with a `LegendReputation` (`heroic`, `notorious`, `legendary`, `forgotten`),
346
+ a `fame_Q` (how widely known), and thematic `tags` derived from source events and story arcs.
347
+ `createLegendFromChronicle(chronicle, subjectId, subjectName)` promotes qualifying entries (default
348
+ significance ≥ 60) into a legend; `fame_Q` scales with total significance (7 × 100-pt entries →
349
+ q(1.0)). `getLegendEffect(legend)` returns `LegendEffect` — persuasion, intimidation, fear, and
350
+ morale bonuses that scale with fame. `npcKnowsLegend(legend, npcId, worldSeed, tick)` is a
351
+ deterministic fame roll (same inputs = same result). `applyLegendToDialogueContext(initiatorId,
352
+ targetId, registry, worldSeed, tick)` aggregates effects for NPCs who know the legend. `stepLegendFame`
353
+ decays fame over time (legendary legends have a q(0.50) floor). Full serialisation support.
354
+
355
+ **Phase 51** adds weather and atmospheric environment (`src/sim/weather.ts`): `WeatherState` bundles
356
+ `WindField`, `PrecipitationType` (`none` / `rain` / `heavy_rain` / `snow` / `blizzard` / `hail`),
357
+ and `fogDensity_Q`. `deriveWeatherModifiers(weather)` returns `WeatherModifiers` — traction,
358
+ vision, and thermal deltas. Per-tick kernel integration: rain/snow reduce `ctx.tractionCoeff`
359
+ (blizzard → q(0.40)), fog and precipitation reduce `SensoryEnvironment.lightMul` and `.smokeMul`,
360
+ precipitation cools `ctx.thermalAmbient_Q` (snow −5 °C, blizzard −12 °C). `computeWindAimError`
361
+ adds crosswind drift to ranged-combat grouping radius (drift = v_wind_perp × range / v_proj).
362
+ `adjustConeRange` modulates breath-weapon range ±20 % for head/tailwind. All effects are additive
363
+ with existing sensory and thermal modifiers; `weather` absent = backward-compatible.
364
+
365
+ **Phase 52** adds extended sensory modalities (`src/sim/sensory-extended.ts`): `ExtendedSenses` on
366
+ `Entity` enables echolocation (range-checked, darkness-independent, degraded by noiseMul — dead
367
+ targets still detected via physical echo), electroreception (bioelectric field detection, dead
368
+ targets not detected), and olfaction (wind-direction-aware, precipitation-dispersed scent tracking).
369
+ `computeDaylightMul(hourOfDay)` returns a Q multiplier (q(0.10) at midnight → q(1.0) at noon) for
370
+ integration with `SensoryEnvironment.lightMul`. `canDetectExtended` wraps Phase 4 `canDetect` and
371
+ additionally checks each extended modality in quality order: electroreception q(0.80),
372
+ echolocation q(0.70), olfaction q(0.20–0.40).
373
+
374
+ **Phase 53** adds systemic toxicology for ingested substances (`src/sim/systemic-toxicology.ts`):
375
+ five toxin profiles — `alcohol` (15 min onset, motor impairment + disinhibition, addictive),
376
+ `sedative_plant` (30 min onset, consciousness erosion, addictive), `alkaloid_poison` (20 min,
377
+ internal damage + fear), `heavy_lead` and `radiation_dose` (cumulative irreversible dose
378
+ accumulation via `CumulativeExposureRecord`). Exponential concentration decay via metabolic
379
+ half-life (fixed-point minimum-1/s guarantee ensures clearance). Withdrawal states
380
+ (`WithdrawalState`) are triggered after sustained addictive use, applying fatigue and fear
381
+ penalties for the withdrawal duration. `deriveCumulativeToxicity(entity)` → Q for chronic
382
+ exposure queries. Stepped at 1 Hz alongside nutrition and Phase 32C venom. Backward-compatible:
383
+ absent fields = no change.
384
+
385
+ **Phase 54** adds wound aging and long-term sequelae (`src/sim/wound-aging.ts`):
386
+ `stepWoundAging(entity, elapsedSeconds)` models four time-scale processes — uninfected-region
387
+ healing (surface 1%/day, internal 0.5%/day, clamped to permanentDamage floor), infection
388
+ worsening (1.5%/day internalDamage, sepsis threshold at q(0.85)), chronic fatigue from
389
+ sustained permanent damage, and phantom pain shock from fractured+permanent regions.
390
+ `recordTraumaEvent` / `deriveFearThresholdMul` implement PTSD-like trauma accumulation
391
+ with natural q(0.002)/day decay; max trauma halves the effective fear threshold (q(0.50)).
392
+ `deriveSepsisRisk(entity)` → Q aggregates infected-region severity for medical triage AI.
393
+ `TraumaState` added to `Entity`. All healing and worsening rates are deterministic from
394
+ elapsed seconds — no RNG required. Backward-compatible: absent `traumaState` = no effect.
395
+
396
+ **Phase 55** adds collective non-combat activities (`src/collective-activities.ts`):
397
+ three self-contained group systems for downtime and logistics. **Siege engineering**:
398
+ `createCollectiveProject` / `contributeToCollectiveProject` maintain a shared `progress_Q`
399
+ pool; each entity's contribution = `deriveEngineeringCompetence(entity) × hoursWorked /
400
+ requiredWorkHours`, where competence is the average of `logicalMathematical + bodilyKinesthetic`;
401
+ `isProjectComplete` and `completedAtTick` track completion. **Ritual & ceremony**:
402
+ `stepRitual(participants, elapsedSeconds)` produces a `moraleBonus_Q` (cap q(0.30)) and
403
+ `fearReduction_Q` (60 % of morale bonus) by pooling each participant's average `(intrapersonal
404
+ + musical) / 2` with sqrt(N) group scaling and a linear time ramp over one hour. **Trade
405
+ caravan logistics**: `planCaravanRoute(waypoints, participants, inventory)` derives
406
+ `routeQuality_Q` from the best navigator's `logicalMathematical`, a `negotiationBonus_Q` from
407
+ the best `interpersonal` score, shortens travel time via a speed factor ∈ [q(0.80), q(1.00)],
408
+ and computes `supplySufficiency_Q` from available rations vs. travel-day demand.
409
+ No kernel integration — all three systems are host-application-driven downtime APIs.
410
+
411
+ **Phase 56** adds entity-to-entity disease transmission (`src/sim/disease.ts`):
412
+ six disease profiles (common_fever, wound_fever, plague_pneumonic, dysentery, marsh_fever,
413
+ wasting_sickness) each with `transmissionRoute`, daily fatigue drain, incubation/symptomatic
414
+ phase timers, a mortality roll via deterministic `eventSeed`, and graduated immunity.
415
+ `exposeToDisease(entity, id)` adds an incubating state (skips immune/already-infected
416
+ entities); `stepDiseaseForEntity(entity, delta_s, worldSeed, tick)` advances phases, drains
417
+ fatigue, rolls mortality on recovery, grants immunity; `computeTransmissionRisk` returns Q
418
+ risk — airborne decays linearly to zero at `airborneRange_Sm`, contact/vector/waterborne are
419
+ flat within 2 m; `spreadDisease(entityMap, pairs, worldSeed, tick)` performs a deterministic
420
+ batch exposure from host-supplied spatial pairs. `DiseaseState` and `ImmunityRecord` added
421
+ to `Entity`. Backward-compatible: absent `activeDiseases` = no effect.
422
+
423
+ **Phase 57** adds aging and lifespan simulation (`src/sim/aging.ts`):
424
+ species-agnostic attribute curves parameterized by normalized age fraction
425
+ (`ageFrac = ageYears / lifespanYears`). Seven piecewise-linear Q dimensions —
426
+ muscular strength (peaks at ageFrac ≈ 0.28), reaction time (faster at peak,
427
+ slowest in infancy and antiquity), motor control, stature (stable adult, slight
428
+ compression in ancient), fluid cognition (logical/spatial/musical — peaks young),
429
+ crystallized cognition (linguistic/interpersonal/intrapersonal — peaks mid-life,
430
+ wisdom outlasts speed), and distress tolerance (peaks middle age). A 25-year-old
431
+ human and a 187-year-old elf (lifespan 600) share the same ageFrac and therefore
432
+ the same developmental multipliers. `applyAgingToAttributes(base, ageYears)` returns
433
+ a new `IndividualAttributes` without mutating the peak baseline; `stepAging(entity,
434
+ elapsedSeconds)` accumulates `entity.age.ageSeconds` for campaign-scale time tracking.
435
+
436
+ **Phase 58** adds sleep and circadian rhythm (`src/sim/sleep.ts`): a two-factor
437
+ deprivation model tracking `awakeSeconds` (continuous wake duration) and `sleepDebt_s`
438
+ (cumulative nightly shortfall, capped at 72 h). `circadianAlertness(hourOfDay)` → Q
439
+ piecewise-linear alertness curve (peak 17:00, nadir 03:00). Impairment begins after 17 h
440
+ awake and degrades four attributes linearly to max at 72 h: fluid cognition −45%, reaction
441
+ time +45% slower, stability −25%, distress tolerance −35%. Sleep-phase cycling follows the
442
+ 90-minute NREM/REM cycle (light 45 min → deep 25 min → rem 20 min → repeat). Prior-night
443
+ debt persists even after short sleep: effective driver = max(awakeSeconds, sleepDebt_s).
444
+ `applySleepToAttributes(base, state)` returns a new `IndividualAttributes` (immutable,
445
+ same pattern as Phase 57); `stepSleep(entity, elapsedSeconds, isSleeping)` mutates
446
+ `entity.sleep`; `entitySleepDebt_h(entity)` convenience helper.
447
+
448
+ **Phase 59** adds mounted combat (`src/sim/mount.ts`): five mount profiles (pony, horse,
449
+ warhorse, camel, war_elephant) with physics-based charge energy (`bonusEnergy_J = ½mv²`),
450
+ rider height advantage (aim bonus proportional to seat elevation, q(0.12)/m, max q(0.30)),
451
+ mount-to-rider fear contagion (40% of excess shock beyond fearThreshold), and
452
+ forced-dismount detection (rider over-shock, mount death, mount bolt). `checkMountStep`
453
+ returns a pure `MountStepResult` with no entity mutation. `MountState { mountId, riderId,
454
+ gait }` added to `Entity`.
455
+
456
+ **Phase 60** adds environmental hazard zones (`src/sim/hazard.ts`): persistent 2-D circular areas
457
+ with linear exposure falloff that inflict per-second effects (fatigue, thermal shift, radiation dose,
458
+ surface damage, disease exposure). Five types: fire, radiation, toxic_gas, acid, extreme_cold.
459
+ `computeHazardExposure(dist_Sm, hazard)` → Q; `deriveHazardEffect(hazard, exposureQ)` → `HazardEffect`;
460
+ `stepHazardZone` ticks duration (permanent zones are untouched); `isHazardExpired` signals removal.
461
+ No entity field needed — hazards are world-level objects the host iterates each tick.
462
+
463
+ **Phase 63** adds a **narrative stress test** framework (`src/narrative-stress.ts`): given a
464
+ scene described as a sequence of `NarrativeBeat` predicates — each with a tick window in which
465
+ it must become true — `runNarrativeStressTest` runs the simulation across hundreds of seeds
466
+ and measures what fraction of runs spontaneously produce that sequence. The complement is the
467
+ **narrative push**: `0.00` = plausible (no authorial pressure needed); `1.00` = "extreme — plot
468
+ armour" (the story never happens without explicit intervention). Beat predicate helpers
469
+ (`beatEntityDefeated`, `beatEntitySurvives`, `beatTeamDefeated`, `beatEntityShockExceeds`,
470
+ `beatEntityFatigued`) cover the most common story requirements. `formatStressTestReport`
471
+ produces a human-readable breakdown labelled "none — plausible" / "light" / "moderate" /
472
+ "heavy" / "extreme — plot armour". Demo: `npm run run:narrative-stress-test`.
473
+
474
+ **Artificial Life Validation ("Blade Runner" Test)** (`tools/blade-runner.ts`, `npm run run:blade-runner`) —
475
+ the ultimate integration test: 198 named NPCs, 3 polities, and 365 simulated days wiring every
476
+ major phase simultaneously (disease, polity economics, war, sleep debt, skill progression).
477
+ Validates 4 emergent-behaviour claims: social hierarchy, disease mortality spikes, morale–economy
478
+ correlation, and skill accumulation hierarchy. All 4/4 claims pass on seed 1.
479
+
480
+ **Species Forge** (`docs/editors/species-forge.html`) — four-tab standalone HTML/JS species designer
481
+ extending the Body Plan Editor. Tab 1: segment body plan with live validation. Tab 2: 24 archetype
482
+ sliders (stature, mass, peak force/power, reaction time, control quality, resilience, perception).
483
+ Tab 3: five Narrative Bias sliders (strength/speed/resilience/agility/size, −1 to +1) with six
484
+ preset profiles (Warrior/Scholar/Rogue/Tank/Feral Beast/Clear). Tab 4: live-generated TypeScript
485
+ trio (`BodyPlan` + `Archetype` + `NarrativeBias`). Four templates: Humanoid, Large Beast, War
486
+ Machine, Mind Swarm. Linked from `docs/editors/index.html`.
487
+
488
+ **Phase 67 — Technology Diffusion at Polity Scale** (`src/tech-diffusion.ts`) — tech eras
489
+ spread from advanced polities to lagging neighbours via trade routes and cultural contact.
490
+ `computeDiffusionPressure(source, target, pair, warActive)` computes per-day advance probability
491
+ scaling with era gap (×1–3×), route quality, shared locations, and stability threshold.
492
+ `stepTechDiffusion(registry, pairs, worldSeed, tick)` rolls for advances each tick, mutates
493
+ `techEra`, and refreshes military strength; one advance per polity per tick maximum.
494
+ `totalInboundPressure` provides a signed-Q AI query for "how likely is this polity to advance?"
495
+ Long-run convergence test: era-1 polity reaches era-3 within 2 000 daily ticks (~5.5 years).
496
+ 34 tests; 100% coverage.
497
+
498
+ **Phase 66 — Generative Mythology** (`src/mythology.ts`) — narrative compression of the Legend/
499
+ Chronicle log into in-world cultural beliefs. `compressMythsFromHistory(legendRegistry, entries,
500
+ factionIds)` detects six archetypal patterns (hero, monster, great_plague, divine_wrath, golden_age,
501
+ trickster) from accumulated legends and chronicle events; each myth carries a `MythEffect` (fear
502
+ threshold, diplomacy, morale, tech modifiers) scaled by its `belief_Q`. `stepMythologyYear`
503
+ decays belief 12%/year to a floor of q(0.10). `aggregateFactionMythEffect` gives the net cultural
504
+ modifier for polity-day AI use. 39 tests; 100% statement/branch/function/line coverage.
505
+
506
+ **Phase 65 — Emotional Contagion at Polity Scale** (`src/emotional-contagion.ts`) — fear and hope
507
+ propagate between polities using the Phase 56 disease transmission model with `fear_Q`/`hope_Q` as
508
+ the "pathogen". Four built-in profiles: `military_rout` (fast-spreading panic), `plague_panic`
509
+ (slow-decaying dread), `victory_rally` (hope wave from a battle win), `charismatic_address`
510
+ (leader-amplified; Phase 39 `leaderPerformance_Q` scales initial intensity).
511
+ `applyEmotionalContagion(registry, pairs, waves, profiles, worldSeed, tick)` drives spread +
512
+ moraleQ updates each polity day-tick. `netEmotionalPressure` gives a signed Q for AI queries.
513
+ 46 tests; 100% statement/function/line coverage.
514
+
515
+ **"What If?" / Alternate History Engine** (`tools/what-if.ts`, `npm run run:what-if`, Phase 64) —
516
+ polity-scale alternate-history simulator that runs a `WhatIfScenario` across N seeds and reports
517
+ probability-weighted outcome distributions. Three built-in scenarios: plague devastating a capital
518
+ (−92.5% population; density-floor mechanics), a charismatic leader's morale surge (+22% military
519
+ at day 90), and a sudden war between equal polities (−100% aggressor stability; −40% treasury for
520
+ both; war persists all 180 days). Demonstrates geopolitical consequence modelling built on Phase 61.
521
+
522
+ **Emergent Behaviour Validation Suite** (`tools/emergent-validation.ts`, `npm run run:emergent-validation`) —
523
+ four historical combat scenarios validated across 100 seeds each: 10v10 open-ground skirmish
524
+ (Ardant du Picq), environmental friction in rain + fog (Keegan), Lanchester's numerical
525
+ superiority law (5 vs. 10), and siege attrition via disease (Raudzens). All 4/4 scenarios pass.
526
+
527
+ **Performance & Scalability Benchmarks** (`tools/benchmark.ts`, `npm run run:benchmark`) —
528
+ reproducible throughput figures across four entity-count scenarios (10 / 100 / 500 / 1 000).
529
+ Includes AI-decision-budget breakdown, spatial-index comparison vs. naïve O(n²), and a tuning
530
+ guide. Full report in `docs/performance.md`.
531
+
532
+ **Dataset Contribution Pipeline** (`docs/dataset-contribution.md`) — step-by-step guide for
533
+ adding empirical datasets and `DirectValidationScenario` entries to the validation runner.
534
+ Includes CSV format spec, four code templates, tolerance-selection table, and a live example
535
+ (`datasets/example-sprint-speed.csv` — human peak anaerobic power, ✓ PASS at 9.5% error).
536
+
537
+ **Public Validation Dashboard** (`docs/dashboard/`, `npm run run:validation-dashboard`) —
538
+ self-contained HTML dashboard showing all 43 validation scenarios with pass/fail status,
539
+ simulated vs. empirical bars with ±tolerance bands, and filter controls. JSON data is
540
+ regenerated automatically on push via `.github/workflows/validation-dashboard.yml`.
541
+ Serve locally with `python -m http.server` inside `docs/dashboard/`.
542
+
543
+ **Visual Editors for Non-Developers** (`docs/editors/`) — two standalone HTML/JS tools
544
+ requiring no build step or TypeScript knowledge. **Body Plan Editor**: define species segments
545
+ with mass-share sliders, locomotion/manipulation/CNS roles, and live validation; generates a
546
+ `BodyPlan` TypeScript literal. **Validation Scenario Builder**: configure entities, simulation
547
+ parameters, weather, and empirical reference data; generates a `DirectValidationScenario`
548
+ block. Both tools serve via GitHub Pages or `python -m http.server`.
549
+
550
+ **2904 tests.** All coverage thresholds met (statements 93.75%+, branches 84.69%+, functions 92%+, lines 93.75%+).
551
+
552
+ See `ROADMAP.md` for the full development plan.
553
+
554
+ ---
555
+
556
+ ## Validation against real-world data
557
+
558
+ > **Emergent validation report:** [`docs/emergent-validation-report.md`](docs/emergent-validation-report.md)
559
+ > — four historical combat scenarios, 100 seeds each, all passing. This is the flagship trust
560
+ > artifact: it validates *distributions of outcomes* across multi-system interactions, not just
561
+ > individual formula outputs. CI runs a 20-seed fast subset on every push.
562
+
563
+ Ananke's physics-based approach is systematically validated against external real-world datasets and literature sources. The validation framework (`tools/validation.ts`) compares simulation outputs with empirical measurements across multiple sub‑systems, ensuring the simulation's predictions remain grounded in reality.
564
+
565
+ **Key validated sub‑systems:**
566
+ - Movement energy cost (AddBiomechanics walking metabolic dataset)
567
+ - Projectile drag (BVR Air Combat dataset)
568
+ - Jump height and sprint speed (sports‑science literature)
569
+ - Fracture thresholds (Yamada 1970, McElhaney 1970)
570
+ - Thoracic and pelvic impact tolerance (AFRL Biodynamics Data Bank)
571
+ - Damage energy constants (NATO STANAG 4526)
572
+ - Sleep deprivation cognitive impairment (Van Dongen et al. 2003 meta‑analysis)
573
+ - Disease mortality rates (historical epidemiology)
574
+ - Calibration scenarios: armed vs. unarmed, untreated knife wound, first‑aid saves lives, fracture recovery, untreated infection, plate armour effectiveness
575
+
576
+ Each validation scenario runs a deterministic simulation that replicates the experimental conditions of the external dataset, then compares the simulated mean against the empirical mean with a ±20 % tolerance. A constant‑suggestion system provides actionable recommendations for tuning constants when deviations exceed tolerance.
577
+
578
+ **Validation inventory:** A comprehensive catalogue of all validated and potential future datasets is maintained in [`docs/external-dataset-validation-inventory.md`](docs/external-dataset-validation-inventory.md). The inventory includes 19 currently validated sources and identifies 11 high‑value external datasets for future validation work across muscle mechanics, ground‑reaction forces, blast physics, cognitive state, and melee combat.
579
+
580
+ **Run validation:** `npm run run:validation <subsystem>` generates a validation report for a specific sub‑system.
581
+
582
+ ## Physics realism summary (post-Phase 30)
583
+
584
+ Ananke now models the complete survivability stack. Five independent threat vectors can kill
585
+ an entity, each operating on a different time scale and requiring different intervention:
586
+
587
+ | Threat | Time scale | Mechanism | Primary phases |
588
+ |--------|-----------|-----------|----------------|
589
+ | Injury / blood loss | Seconds | Fluid loss → shock → death | 1, 9 |
590
+ | Infection | Hours–days | Internal damage accumulation | 9 |
591
+ | Thermal stress | Minutes–hours | Core temperature extremes | 29 |
592
+ | Dehydration | Hours–days | Hydration balance collapse | 30 |
593
+ | Starvation | Days–weeks | Caloric deficit → catabolism | 30 |
594
+
595
+ ### Physical phenomena modelled
596
+
597
+ **Newtonian mechanics**
598
+ - Kinetic energy → injury via impact physics, penetration, and leverage (Phases 1–3)
599
+ - Impulse-momentum → knockback velocity and stagger/prone transitions (Phase 26)
600
+ - Angular momentum → miss recovery time and weapon bind probability (Phase 2)
601
+
602
+ **Wound physiology**
603
+ - Per-region surface / internal / structural damage; penetration bias by tissue type
604
+ - Bleeding rate proportional to internal damage; natural clotting proportional to tissue integrity
605
+ - Fluid loss → haemodynamic shock → consciousness erosion → death (`fluidLoss ≥ 0.80` fatal)
606
+ - Fracture (structural ≥ 0.70), permanent damage (≥ 0.90), infection onset after 100 ticks of bleeding
607
+
608
+ **High-velocity ballistics** (Phase 27)
609
+ - Temporary cavity multiplier ×1.0–×3.0 above 600 m/s; scales by tissue compliance
610
+ - Cavitation bleed boost in fluid-saturated organs (torso, liver, spleen, lung, legs) above 900 m/s
611
+
612
+ **Thermodynamics** (Phase 29)
613
+ - Core temperature balance: metabolic heat (1.06–5.50 W/kg by activity) vs. conductive loss through skin + armour insulation
614
+ - Thermal resistance: `R = 0.09 + Σ(armour.insulation_m2KW)` °C/W; thermal mass = `3500 × mass_real` J/°C
615
+ - Seven-stage temperature model: critical hyperthermia → heat stroke → heat exhaustion → normal → mild → moderate → severe hypothermia → cardiac arrest
616
+
617
+ **Metabolism** (Phase 30)
618
+ - Basal metabolic rate: `80 × (mass_kg / 75)^0.75` W (Kleiber's law)
619
+ - Active metabolic rate: `BMR + peakPower_W × activityFrac × 0.15`
620
+ - Four hunger states: sated (< 12 h deficit), hungry (12–24 h), starving (24–72 h), critical (≥ 72 h)
621
+ - Fat catabolism: 300 g/day during starvation; muscle catabolism: 0.5 N/hour reduction in `peakForce_N` during critical state
622
+
623
+ **Pharmacokinetics** (Phase 10)
624
+ - One-compartment absorption/elimination model per active substance
625
+ - Cross-substance interactions: stimulant, haemostatic, anaesthetic, poison
626
+
627
+ **Neuromuscular and cognitive**
628
+ - Reaction time → attack/defence timing; decision latency → AI replanning interval
629
+ - Fine motor control impairment from arm damage; manipulation penalty from fractures
630
+ - Fatigue accumulation (joules) → exhaustion threshold → prone collapse
631
+
632
+ **Environmental**
633
+ - Fire, corrosive, electrical, radiation, suffocation — tick-based accumulation with channel-specific armour
634
+ - Blast wave (quadratic falloff) and fragmentation (stochastic count, random region)
635
+ - Ambient temperature stress (Phase 10); terrain friction, elevation, cover, hazard cells (Phase 6)
636
+
637
+ **Psychology**
638
+ - Fear accumulation from near-miss fire, ally deaths, calibre size (Phase 5)
639
+ - Routing, panic varieties (surrender / freeze / flee), leader auras, rally mechanics
640
+ - Suppression from near-miss ranged fire → AI cover-seeking
641
+
642
+ ### Time scales covered
643
+
644
+ | Scale | System |
645
+ |-------|--------|
646
+ | 50 ms / tick (20 Hz) | Combat resolution, movement, stamina |
647
+ | Seconds | Bleeding, pharmacokinetics, thermal tick |
648
+ | Minutes | Clotting, hypothermia onset |
649
+ | Hours | Infection onset, hunger onset, thermal equilibrium |
650
+ | Days | Hunger state transitions, mass loss from starvation |
651
+ | Weeks | Wound healing, infection mortality |
652
+ | Months / years | Training drift, ageing decline, injury sequelae (Phase 21) |
653
+
654
+ ---
655
+
656
+ ## Quick start
657
+
658
+ ```typescript
659
+ import { stepWorld, TICK_HZ } from "./src/sim/kernel.js";
660
+ import { STARTER_WEAPONS } from "./src/equipment.js";
661
+ import { HUMAN_BASE } from "./src/archetypes.js";
662
+ import { generateIndividual } from "./src/generate.js";
663
+ import { q, SCALE } from "./src/units.js";
664
+ import { v3 } from "./src/sim/vec3.js";
665
+ import { defaultIntent } from "./src/sim/intent.js";
666
+ import { defaultAction } from "./src/sim/action.js";
667
+ import { defaultCondition } from "./src/sim/condition.js";
668
+ import { defaultInjury } from "./src/sim/injury.js";
669
+
670
+ // Two fighters, procedurally generated from the human archetype.
671
+ // Each will have unique physical attributes drawn from realistic distributions.
672
+ const attrsA = generateIndividual(1, HUMAN_BASE);
673
+ const attrsB = generateIndividual(2, HUMAN_BASE);
674
+
675
+ const world = {
676
+ tick: 0,
677
+ seed: 12345,
678
+ entities: [
679
+ {
680
+ id: 1, teamId: 1,
681
+ attributes: attrsA,
682
+ energy: { reserveEnergy_J: attrsA.performance.reserveEnergy_J, fatigue: q(0) },
683
+ loadout: { items: [STARTER_WEAPONS[0]] },
684
+ traits: [],
685
+ position_m: v3(0, 0, 0),
686
+ velocity_mps: v3(0, 0, 0),
687
+ intent: defaultIntent(), action: defaultAction(),
688
+ condition: defaultCondition(), injury: defaultInjury(),
689
+ grapple: { holdingTargetId: 0, heldByIds: [], gripQ: q(0) },
690
+ },
691
+ {
692
+ id: 2, teamId: 2,
693
+ attributes: attrsB,
694
+ energy: { reserveEnergy_J: attrsB.performance.reserveEnergy_J, fatigue: q(0) },
695
+ loadout: { items: [] },
696
+ traits: [],
697
+ position_m: v3(Math.trunc(0.8 * SCALE.m), 0, 0),
698
+ velocity_mps: v3(0, 0, 0),
699
+ intent: defaultIntent(), action: defaultAction(),
700
+ condition: defaultCondition(), injury: defaultInjury(),
701
+ grapple: { holdingTargetId: 0, heldByIds: [], gripQ: q(0) },
702
+ },
703
+ ],
704
+ };
705
+
706
+ const cmds = new Map([
707
+ [1, [{ kind: "attack", targetId: 2, weaponId: "wpn_club", intensity: q(1.0), mode: "strike" }]],
708
+ ]);
709
+
710
+ for (let i = 0; i < 5 * TICK_HZ; i++) {
711
+ stepWorld(world, cmds, { tractionCoeff: q(0.9) });
712
+ }
713
+
714
+ const target = world.entities.find(e => e.id === 2)!;
715
+ console.log("Torso surface damage:", target.injury.byRegion.torso.surfaceDamage / SCALE.Q);
716
+ console.log("Consciousness:", target.injury.consciousness / SCALE.Q);
717
+ console.log("Dead:", target.injury.dead);
718
+ ```
719
+
720
+ ---
721
+
722
+ ## Fixed-point unit system
723
+
724
+ All simulation values are scaled integers. Never use `Math.random()` or floating-point
725
+ arithmetic in simulation code.
726
+
727
+ | Unit | SCALE constant | Meaning |
728
+ |---|---|---|
729
+ | metres | `SCALE.m = 10000` | 1 m = 10 000 units |
730
+ | seconds | `SCALE.s = 10000` | 1 s = 10 000 units |
731
+ | kilograms | `SCALE.kg = 1000` | 1 kg = 1 000 units |
732
+ | newtons | `SCALE.N = 1000` | 1 N = 1 000 units |
733
+ | joules | `SCALE.J = 1000` | 1 J = 1 000 units |
734
+ | watts | `SCALE.W = 1000` | 1 W = 1 000 units |
735
+ | dimensionless | `SCALE.Q = 10000` | 1.0 = 10 000 units |
736
+
737
+ Convert to fixed-point: `q(0.5)` → `5000`, `to.m(1.8)` → `18000`.
738
+ Read back: `value / SCALE.m` → metres, `value / SCALE.Q` → dimensionless fraction.
739
+
740
+ ---
741
+
742
+ ## Entity model
743
+
744
+ Every entity is built from four attribute groups, all in SI units.
745
+
746
+ ### Morphology
747
+
748
+ | Field | Unit | Meaning |
749
+ |---|---|---|
750
+ | `stature_m` | m | Height |
751
+ | `mass_kg` | kg | Total mass |
752
+ | `actuatorMass_kg` | kg | Muscle or actuator mass |
753
+ | `actuatorScale` | Q | Actuator strength multiplier |
754
+ | `structureScale` | Q | Structural toughness multiplier |
755
+ | `reachScale` | Q | Limb reach multiplier |
756
+
757
+ ### Performance
758
+
759
+ | Field | Unit | Meaning |
760
+ |---|---|---|
761
+ | `peakForce_N` | N | Maximum output force |
762
+ | `peakPower_W` | W | Maximum instantaneous power |
763
+ | `continuousPower_W` | W | Sustainable aerobic power |
764
+ | `reserveEnergy_J` | J | Total metabolic reserve (stamina pool) |
765
+ | `conversionEfficiency` | Q | Fraction of metabolic energy delivered as mechanical work |
766
+
767
+ ### Control
768
+
769
+ | Field | Unit | Meaning |
770
+ |---|---|---|
771
+ | `controlQuality` | Q | Overall motor coordination (0–1) |
772
+ | `reactionTime_s` | s | Neuromuscular response latency |
773
+ | `stability` | Q | Balance and postural stability (0–1) |
774
+ | `fineControl` | Q | Precision of fine movements (0–1) |
775
+
776
+ ### Resilience
777
+
778
+ | Field | Unit | Meaning |
779
+ |---|---|---|
780
+ | `surfaceIntegrity` | Q | Resistance to surface-layer damage |
781
+ | `bulkIntegrity` | Q | Resistance to bulk soft-tissue damage |
782
+ | `structureIntegrity` | Q | Resistance to skeletal or structural damage |
783
+ | `distressTolerance` | Q | Pain and distress tolerance (0–1) |
784
+ | `shockTolerance` | Q | Resistance to haemodynamic shock (0–1) |
785
+ | `concussionTolerance` | Q | CNS impact tolerance (0–1) |
786
+ | `heatTolerance` | Q | Thermal hazard resistance (0–1) |
787
+ | `coldTolerance` | Q | Cold hazard resistance (0–1) |
788
+ | `fatigueRate` | Q | Fatigue accumulation rate multiplier |
789
+ | `recoveryRate` | Q | Recovery rate multiplier |
790
+ | `magicResist` | Q | Resistance to capability effects (magic/technology); q(1.0) = fully immune (optional) |
791
+
792
+ ### Perception (Phase 4)
793
+
794
+ | Field | Unit | Meaning |
795
+ |---|---|---|
796
+ | `visionRange_m` | m | Maximum reliable visual detection range |
797
+ | `visionArcDeg` | degrees | Horizontal field of view (120° human, 360° robot) |
798
+ | `halfArcCosQ` | Q | cos(visionArcDeg/2) pre-computed for fast sim-path arc checks |
799
+ | `hearingRange_m` | m | Omnidirectional acoustic detection range |
800
+ | `decisionLatency_s` | s | Minimum time between plan revisions (500ms human, 50ms robot) |
801
+ | `attentionDepth` | integer | Maximum simultaneously tracked threats |
802
+ | `threatHorizon_m` | m | Range at which threats are integrated into decisions |
803
+
804
+ ### Individual variation
805
+
806
+ `generateIndividual(seed, archetype)` produces a unique entity by applying triangular
807
+ distributions to each attribute, parameterised by the archetype's variance fields.
808
+
809
+ A human with seed 1 might have `peakForce_N = 1640` (below average) but
810
+ `reserveEnergy_J = 26000` (high stamina). These values are physically meaningful and feed
811
+ directly into combat, movement, and fatigue calculations with no intermediate conversion.
812
+
813
+ Archetypes in `src/archetypes.ts`: `HUMAN_BASE`, `SERVICE_ROBOT`, `AMATEUR_BOXER`,
814
+ `PRO_BOXER`, `GRECO_WRESTLER`, `KNIGHT_INFANTRY`, `LARGE_PACIFIC_OCTOPUS`. Additional
815
+ archetypes (quadruped, alien, etc.) are data additions, not code changes.
816
+
817
+ ---
818
+
819
+ ## Anatomy and injury
820
+
821
+ Per-region injury tracking — fully data-driven via `BodyPlan` (Phase 8). Default is humanoid
822
+ (head, torso, left arm, right arm, left leg, right leg). Other plans: quadruped, theropod,
823
+ sauropod, avian, vermiform, centaur, octopoid — see `src/sim/bodyplan.ts`.
824
+
825
+ Each region tracks:
826
+
827
+ - `surfaceDamage` — skin, scales, outer covering
828
+ - `internalDamage` — organ, soft-tissue, deep injury
829
+ - `structuralDamage` — bone, frame, load-bearing structure
830
+ - `bleedingRate` — active blood loss rate
831
+ - `fractured` — set when `structuralDamage ≥ 0.70`; persistent impairment until surgically repaired (Phase 9)
832
+ - `permanentDamage` — floor below which surgery cannot reduce `structuralDamage` (set at ≥ 0.90) (Phase 9)
833
+ - `bleedDuration_ticks` — ticks with active bleeding; infection onsets after 100 ticks if `internalDamage > 0.10` (Phase 9)
834
+ - `infectedTick` — tick at which infection began (`-1` = none); infected regions gain `+q(0.0003)` internal damage per tick (Phase 9)
835
+
836
+ Global injury state:
837
+
838
+ - `shock` — haemodynamic and neurological shock (0–1)
839
+ - `fluidLoss` — cumulative blood or fluid loss (0–1); fatal at 0.80 (Phase 9)
840
+ - `consciousness` — 1.0 = fully conscious, 0 = unconscious
841
+ - `dead` — irreversible cessation
842
+
843
+ ---
844
+
845
+ ## Functional impairment
846
+
847
+ Damage produces functional penalties that feed automatically into movement, combat, and
848
+ stamina calculations:
849
+
850
+ | Damage location | Effect |
851
+ |---|---|
852
+ | Leg structural | Sprint speed and acceleration reduction |
853
+ | Leg fracture | Up to −30% mobility (Phase 9) |
854
+ | Arm damage | Manipulation quality and parry effectiveness reduction |
855
+ | Arm fracture | Up to −25% manipulation (Phase 9) |
856
+ | Head damage | Coordination and consciousness degradation |
857
+ | Torso damage | Breathing impairment and shock vulnerability |
858
+ | Shock (global) | All performance multipliers degraded |
859
+
860
+ ---
861
+
862
+ ## Combat resolution
863
+
864
+ ### Hit resolution
865
+
866
+ 1. Attacker rolls against `controlQuality` and `reactionTime_s`
867
+ 2. Defender rolls against their defence mode (block, parry, dodge) and intensity
868
+ 3. Geometry influence (facing angle) modifies contest
869
+ 4. On hit: location selected by weighted region roll, hit quality computed
870
+ 5. Shield interposition checked against region coverage
871
+ 6. Armour checked per region and channel
872
+ 7. Residual energy applied to injury
873
+
874
+ ### Impact physics
875
+
876
+ Impact energy derived from:
877
+
878
+ - Weapon effective mass (kg)
879
+ - Relative velocity (m/s) from attacker's `peakPower_W` and `continuousPower_W`
880
+ - Leverage: parry leverage from weapon moment arm (N·m)
881
+ - Two-handed grip: 1.12× energy bonus when both arms are free
882
+
883
+ Converted to surface, internal, and structural injury proportional to penetration profile.
884
+ Bleeding rate increases proportionally to internal damage severity.
885
+
886
+ ### Weapon dynamics
887
+
888
+ - **Reach dominance**: shorter weapon is penalised in both attack and parry against a longer one
889
+ - **Miss recovery**: missed strikes add extra cooldown ticks proportional to weapon angular momentum (mass × reach)
890
+ - **Weapon bind**: successful parry with heavy weapons can lock both weapons; requires a strength contest (`breakBind`) to escape
891
+ - **Grappling**: strength+mass+technique contest; positions (standing/pinned/prone), throws, chokes, joint locks
892
+ - **Swing momentum carry**: `swingMomentumQ` decays 5%/tick; a clean hit sets it to `intensity × q(0.80)`; adds up to +12% energy on the next strike; reset to 0 on miss, block, or parry
893
+
894
+ ### Stamina
895
+
896
+ Actions cost energy (joules). When reserve falls below 15% of baseline, functional penalties
897
+ ramp in. At zero reserve the entity collapses prone and loses all active defence.
898
+
899
+ ### Medical treatment (Phase 9)
900
+
901
+ Treatment is issued via `TreatCommand` and resolved by `resolveTreat()` in the kernel.
902
+ The treater must be within 2 m of the target. Outcome scales with equipment tier and the
903
+ treater's `medical` skill (`treatmentRateMul`).
904
+
905
+ **Equipment tiers** (ascending capability): `bandage`, `surgicalKit`, `autodoc`, `nanomedicine`.
906
+
907
+ | Action | Min tier | Effect |
908
+ |---|---|---|
909
+ | `tourniquet` | bandage | Zeroes `bleedingRate` for one region immediately |
910
+ | `bandage` | bandage | Reduces `bleedingRate` by `q(0.005) × effectMul` per tick |
911
+ | `surgery` | surgicalKit | Reduces `structuralDamage`; clears fracture; clears infection (≥ surgicalKit) |
912
+ | `fluidReplacement` | autodoc | Reduces `fluidLoss`; slightly reduces shock |
913
+
914
+ **Natural clotting**: `bleedingRate` decays automatically each tick at a rate proportional
915
+ to `(1 − structuralDamage) × q(0.0002)`. Severe structural damage slows natural clotting.
916
+
917
+ **Injury progression** each tick:
918
+ - Bleeding accumulates `fluidLoss` at `bleedingRate / TICK_HZ`
919
+ - Fluid loss drives shock; shock erodes consciousness
920
+ - `fluidLoss ≥ 0.80` → immediate death
921
+ - Infection (if present) adds `+q(0.0003)` internal damage per tick
922
+
923
+ ### Ranged combat
924
+
925
+ Physics-based projectile system parallel to melee. Issued via `shoot` command.
926
+
927
+ **Energy at range** — linear drag approximation: `energy_J = launchEnergy_J × max(0, 1 − range_m × dragCoeff_perM)`. No energy means no hit.
928
+
929
+ **Accuracy** — angular dispersion converts to grouping radius at range (`dispersionQ × range_m`). Compared against body half-width (~20% of stature). Miss if error exceeds half-width; near-miss if within 3× half-width.
930
+
931
+ **Dispersion modifiers**: `controlQuality`, `fineControl`, fatigue, and aiming intensity all scale the base weapon dispersion.
932
+
933
+ **Aiming time** — issuing consecutive `shoot` commands against the same target while stationary accumulates `aimTicks` (max 20, capped at 50% dispersion reduction). Resets on target switch, movement above 0.5 m/s, or after firing.
934
+
935
+ **Moving target penalty** — target velocity adds a lead error (`velocity × 0.2 s reaction time`) to the grouping radius before the hit roll. A sprinting target at 30 m is dramatically harder to hit than a stationary one.
936
+
937
+ **Suppression** — near-misses set `suppressedTicks` on the target, applying a −10% `coordinationMul` penalty until the counter drains. Entities with `suppressedTicks ≥ 3` and low `distressTolerance` will go prone via AI; all suppressed entities seek cover at a higher threshold (q(0.50) vs q(0.30)).
938
+
939
+ **Ammo types** — a `RangedWeapon` may carry an `ammo: AmmoType[]` array. A `shoot` command with `ammoId` selects a round that overrides mass, drag, damage profile, and/or launch energy multiplier for that shot. `STARTER_AMMO` provides `ammo_ap` (penetrating), `ammo_hv` (×1.20 launch energy), and `ammo_hollow` (high bleed factor).
940
+
941
+ **Projectile categories:**
942
+
943
+ | Category | Launch energy | Examples |
944
+ |---|---|---|
945
+ | Thrown | Derived from thrower `peakPower_W` (÷10) | Sling |
946
+ | Bow | Fixed weapon property (J) | Short bow, long bow, crossbow |
947
+ | Firearm | Fixed weapon property (J) | Pistol, musket |
948
+
949
+ Hits reuse the existing injury pipeline (`applyImpactToInjury`) via a weapon proxy. Armour and shields interpose by the same rules as melee.
950
+
951
+ ---
952
+
953
+ ## Armour
954
+
955
+ Per-region coverage with per-channel protection:
956
+
957
+ | Channel | Protects against |
958
+ |---|---|
959
+ | Kinetic | Blunt and penetrating impacts |
960
+ | Thermal | Fire, extreme heat |
961
+ | Chemical | Corrosive aerosols |
962
+ | Electrical | Electrical discharge |
963
+ | Radiation | Ionising radiation |
964
+ | Suffocation | Airborne hazard (sealed suits) |
965
+ | ControlDisruption | EMP, neural disruption |
966
+
967
+ Armour properties: `resist_J` (kinetic threshold), `coverageByRegion` (probability of
968
+ interposition), `protectedDamageMul` (residual damage fraction), `channelResistMul`
969
+ (per-channel resistance modifier), `mobilityMul`, `fatigueMul`.
970
+
971
+ ---
972
+
973
+ ## Environmental hazards
974
+
975
+ Conditions accumulate on entities per tick and drive injury via the same per-channel,
976
+ per-region pipeline as combat:
977
+
978
+ | Condition | Effect |
979
+ |---|---|
980
+ | `onFire` | Surface damage + shock accumulation |
981
+ | `corrosiveExposure` | Surface and internal damage |
982
+ | `electricalOverload` | Internal damage + stun |
983
+ | `radiation` | Internal damage + shock |
984
+ | `suffocation` | Shock and consciousness erosion |
985
+
986
+ Armour provides protection against all channels. Traits (`radiationHardened`, `sealed`,
987
+ `nonConductive`, etc.) provide immunity or resistance.
988
+
989
+ ### Blast and fragmentation (Phase 10)
990
+
991
+ `applyExplosion(world, origin, spec, tick, trace)` applies a `BlastSpec` point-source explosion
992
+ to all living entities within the blast radius. Uses quadratic falloff (`1 − dist²/radius²`).
993
+
994
+ - **Blast wave**: delivered to torso as `BLAST_WEAPON` (high internal damage)
995
+ - **Fragments**: stochastic count per entity; each fragment hits a random region as `FRAG_WEAPON`
996
+ (high penetration bias, high bleed factor)
997
+ - Emits `BlastHit` trace event per affected entity
998
+
999
+ ### Fall damage (Phase 10)
1000
+
1001
+ `applyFallDamage(world, entityId, height_m, tick, trace)` applies fall physics.
1002
+ KE = mass × g × height; 85% absorbed by muscles (15% transmitted). Any fall ≥ 1 m forces prone.
1003
+ Damage distributed: locomotion-primary regions 70%, others 30% (body-plan-aware).
1004
+ Humanoid fallback: legs 35% each, arms 10% each, torso/head 5% each.
1005
+
1006
+ ### Pharmacokinetics (Phase 10)
1007
+
1008
+ One-compartment model per active substance (`entity.substances`). Each tick:
1009
+ absorption rate × pendingDose absorbed into concentration; elimination rate × concentration removed.
1010
+ Effects activate when concentration exceeds `effectThreshold`:
1011
+
1012
+ | Effect type | Per-tick effect |
1013
+ |---|---|
1014
+ | `stimulant` | Reduces `fearQ` and slows fatigue accumulation |
1015
+ | `anaesthetic` | Erodes `consciousness` |
1016
+ | `poison` | Internal damage to torso + mild shock |
1017
+ | `haemostatic` | Reduces `bleedingRate` across all regions |
1018
+
1019
+ `STARTER_SUBSTANCES` provides four ready-made entries: `stimulant`, `anaesthetic`, `poison`, `haemostatic`.
1020
+
1021
+ ### Ambient temperature (Phase 10)
1022
+
1023
+ `KernelContext.ambientTemperature_Q` — comfort range `[q(0.35), q(0.65)]`.
1024
+ - Heat (above q(0.65)): shock + torso surface damage; scaled by `heatTolerance`
1025
+ - Cold (below q(0.35)): shock + fatigue accumulation; scaled by `coldTolerance`
1026
+
1027
+ ### Technology spectrum (Phase 11)
1028
+
1029
+ `TechContext` gates which items are usable in a scenario without locking them to a specific era.
1030
+
1031
+ ```typescript
1032
+ import { TechEra, defaultTechContext, isCapabilityAvailable } from "./src/sim/tech.js";
1033
+ import { validateLoadout, STARTER_EXOSKELETONS } from "./src/equipment.js";
1034
+
1035
+ const ctx = defaultTechContext(TechEra.NearFuture);
1036
+ // ctx.available includes PoweredExoskeleton but not EnergyWeapons
1037
+
1038
+ const exo = STARTER_EXOSKELETONS.find(e => e.id === "exo_combat")!;
1039
+ const loadout = { items: [exo] };
1040
+ const errors = validateLoadout(loadout, ctx); // [] — valid
1041
+ ```
1042
+
1043
+ **`Exoskeleton` item kind** — integrated with kernel:
1044
+ - `speedMultiplier: Q` — factored into `stepMovement` baseMul
1045
+ - `forceMultiplier: Q` — applied to strike `energy_J` in `resolveAttack`
1046
+ - `powerDrain_W: number` — added to metabolic demand in `stepEnergy`
1047
+
1048
+ **Starter items**: `exo_combat` (+25% speed, +40% force, 200 W drain),
1049
+ `exo_heavy` (+10% speed, +80% force, 400 W drain), `arm_plate` (800 J resist, requires `MetallicArmour`),
1050
+ `rng_plasma_rifle` (2000 J, requires `EnergyWeapons`).
1051
+
1052
+ Era capabilities are cumulative: Prehistoric has none; DeepSpace has all eight.
1053
+
1054
+ **Magic and para-science types** (Phase 12B) — four additional `TechCapability` values gate
1055
+ Clarke's Third Law capability effects: `ArcaneMagic`, `DivineMagic`, `Psionics`, `Nanotech`.
1056
+ These are not assigned to any era by `ERA_DEFAULTS`; host applications opt in explicitly.
1057
+
1058
+ **Medical technology gate**: `TIER_TECH_REQ` in `medical.ts` maps medical tiers to required
1059
+ capabilities. When `ctx.techCtx` is set, `resolveTreat()` blocks treatment if the treater's
1060
+ tier requires a capability absent from the scenario. Currently: `nanomedicine` tier requires
1061
+ `NanomedicalRepair`. Tiers without a listed requirement work in any era.
1062
+
1063
+ ---
1064
+
1065
+ ## Capability Sources and Effects (Phase 12)
1066
+
1067
+ **Clarke's Third Law**: a fireball and a plasma grenade, a healing spell and a nanobot swarm,
1068
+ a mana pool and a fusion reactor — all resolve through identical engine primitives. The engine
1069
+ cannot distinguish magic from technology. Only the `tags` field differs.
1070
+
1071
+ ### CapabilitySource
1072
+
1073
+ Attached to `entity.capabilitySources?: CapabilitySource[]`. Each source is an energy reservoir
1074
+ (always in joules) with one of five regen models:
1075
+
1076
+ | RegenModel type | Behaviour |
1077
+ |---|---|
1078
+ | `rest` | Regens only when entity is stationary and not in combat |
1079
+ | `constant` | Regens every tick regardless of activity |
1080
+ | `ambient` | Regens proportional to `ambientGrid` cell value at entity's position |
1081
+ | `event` | Fires on tick intervals or kill triggers |
1082
+ | `boundless` | Never depletes; cost deduction skipped entirely |
1083
+
1084
+ ### ActivateCommand
1085
+
1086
+ ```typescript
1087
+ { kind: "activate", sourceId: string, effectId: string, targetId?: number, targetPos?: Vec3 }
1088
+ ```
1089
+
1090
+ ### EffectPayload variants
1091
+
1092
+ | Payload kind | Effect | Engine primitive |
1093
+ |---|---|---|
1094
+ | `impact` | Kinetic, thermal, internal, or penetrating damage | `applyImpactToInjury` |
1095
+ | `treatment` | Healing at specified tier and rate multiplier | `resolveTreat` |
1096
+ | `armourLayer` | Temporary per-channel armour overlay | `condition.temporaryArmour` |
1097
+ | `velocity` | Direct velocity delta (telekinesis, jump jet) | velocity integration |
1098
+ | `substance` | Pharmacokinetic substance injection | `stepSubstances` |
1099
+ | `structuralRepair` | Structural damage write-back (respects `permanentDamage`) | injury state |
1100
+ | `fieldEffect` | Places suppression zone in `world.activeFieldEffects` | `stepFieldEffects` |
1101
+
1102
+ ### Phase 12B extensions
1103
+
1104
+ - **Per-capability cooldowns**: `cooldown_ticks?: number` on `CapabilityEffect`; tracked in
1105
+ `action.capabilityCooldowns` (Map, key = `"sourceId:effectId"`); decremented at tick start.
1106
+ - **TechCapability gating**: `requiredCapability?: TechCapability` on `CapabilityEffect`;
1107
+ checked against `ctx.techCtx` when set. Includes magic gates: `ArcaneMagic`, `DivineMagic`,
1108
+ `Psionics`, `Nanotech`.
1109
+ - **Magic resistance**: `magicResist?: Q` on `Resilience`; seeded roll per non-self target in
1110
+ `applyCapabilityEffect`; q(1.0) = always resist; self-cast bypasses entirely.
1111
+ - **Kill-triggered regen**: `{ on: "kill", amount_J }` in `EventRegen.triggers`; dispatched at
1112
+ entity death; all living non-dead observers receive the reward (including the killer).
1113
+ - **Terrain-entry triggers**: `{ on: "terrain", tag, amount_J }` fires exactly once per
1114
+ cell-boundary crossing; `action.lastCellKey` tracks the previous cell; supply
1115
+ `KernelContext.terrainTagGrid` (Map of cell key → tag array) and optional `cellSize_m`.
1116
+ - **Concentration auras**: `castTime_ticks = -1` marks an ongoing per-tick effect;
1117
+ `entity.activeConcentration` holds the active aura; `cost_J` is deducted every tick;
1118
+ concentration breaks when reserve falls below `cost_J` or shock reaches q(0.30), emitting
1119
+ `CastInterrupted`.
1120
+ - **Linked sources**: `CapabilitySource.linkedFallbackId` names a secondary source to draw
1121
+ from when the primary is depleted; fallback can be `boundless` for unlimited overflow.
1122
+ - **Effect chains**: `FieldEffectSpec.chainPayload?: EffectPayload | EffectPayload[]` — payload
1123
+ applied to every living entity within the field's radius each tick while the field is active;
1124
+ fires before expiry so the final tick still delivers the payload.
1125
+
1126
+ ---
1127
+
1128
+ ## 3D model integration (Phase 14)
1129
+
1130
+ `src/model3d.ts` provides pure data-extraction functions for driving 3D character rigs from
1131
+ simulation state. No kernel changes, no state mutations. Call once per tick after `stepWorld`.
1132
+
1133
+ ```typescript
1134
+ import { extractRigSnapshots } from "./src/model3d.js";
1135
+
1136
+ const snapshots = extractRigSnapshots(world);
1137
+ for (const snap of snapshots) {
1138
+ // snap.mass — MassDistribution: per-segment mass and estimated CoG in real metres
1139
+ // snap.inertia — InertiaTensor: yaw/pitch/roll moment of inertia (kg·m²)
1140
+ // snap.animation — AnimationHints: locomotion blend weights, guarding, attacking, prone, dead
1141
+ // snap.pose — PoseModifier[]: per-region injury deformation blend weights
1142
+ // snap.grapple — GrapplePoseConstraint: relative pose lock for grappling pairs
1143
+ hostRenderer.updateRig(snap.entityId, snap);
1144
+ }
1145
+ ```
1146
+
1147
+ **Mass and inertia** are derived from `entity.bodyPlan` segment masses via canonical keyword
1148
+ matching (`head`, `torso`, `forearm`, `thigh`, `shin`, `wing`, etc.); fallback to a solid-sphere
1149
+ approximation when no body plan is present.
1150
+
1151
+ **Animation hints** — locomotion weights (`idle`/`walk`/`run`/`sprint`/`crawl`) are mutually
1152
+ exclusive; exactly one is `SCALE.Q` when mobile. Overlays include `guardingQ`, `attackingQ`,
1153
+ `shockQ`, `fearQ`, and boolean flags `prone`, `unconscious`, `dead`.
1154
+
1155
+ **Pose modifiers** — one entry per `injury.byRegion` key. `impairmentQ = max(structuralQ, surfaceQ)`.
1156
+ Map each `segmentId` to a skeleton bone and drive blend-shape or constraint weights.
1157
+
1158
+ **Grapple constraints** — `isHolder`/`isHeld` flags, `holdingEntityId`, `heldByIds`, `position`
1159
+ (`standing`/`prone`/`pinned`), and `gripQ`. Use to lock relative pose between grappling entities.
1160
+
1161
+ **Integration note:** These functions provide data snapshots only. Mapping Ananke's abstract
1162
+ segment IDs to a specific engine's skeleton, handling tick-rate mismatch (20 Hz → 60+ Hz), and
1163
+ wiring animation hints into a state machine are the host's responsibility. See
1164
+ `docs/bridge-api.md` for the full API reference and `docs/ecosystem.md` for Unity/Godot adapter
1165
+ sketches. A minimal runnable reference plugin (ROADMAP item 6) is the next priority for
1166
+ lowering this integration barrier.
1167
+
1168
+ ---
1169
+
1170
+ ## API stability contract
1171
+
1172
+ > Full reference: [`STABLE_API.md`](./STABLE_API.md) — Versioning policy: [`CHANGELOG.md`](./CHANGELOG.md)
1173
+
1174
+ Ananke distinguishes three tiers of API stability so adopters know what to pin and what to
1175
+ expect to change.
1176
+
1177
+ | Tier | What | Promise |
1178
+ |------|------|---------|
1179
+ | **Stable host API** | `stepWorld()`, `generateIndividual()`, `resolveHit()`, `Entity` shape (core fields), `q()` / `qMul()` / `clampQ()`, `ReplayRecorder` / `replayTo()`, `serializeReplay()` / `deserializeReplay()`, `extractRigSnapshots()` | No breaking changes without a major version bump and migration guide |
1180
+ | **Experimental extension API** | `stepPolityDay()`, `stepTechDiffusion()`, `applyEmotionalContagion()`, `compressMythsFromHistory()`, `stepNarrativeStress()`, `arena.ts` scenario DSL | Good-faith stability; may shift between minor versions with changelog |
1181
+ | **Internal kernel structures** | `src/sim/push.ts`, `src/sim/kernel.ts` internals, `src/rng.ts`, `eventSeed()` | No stability promise; do not import directly |
1182
+
1183
+ All replay, serialization, and campaign round-trip guarantees apply only to the Stable tier.
1184
+ Experimental APIs are safe to use but may require migration on minor version updates.
1185
+
1186
+ ---
1187
+
1188
+ ## Determinism rules
1189
+
1190
+ To maintain lockstep safety:
1191
+
1192
+ - Never use `Math.random()`
1193
+ - Avoid floating point in simulation path
1194
+ - Iterate in stable, deterministic order
1195
+ - Consume RNG in fixed sequence per event type
1196
+ - Use deterministic event batching
1197
+ - Avoid unordered map iteration for gameplay logic
1198
+ - All seeds derived from `(worldSeed, tick, entityId, eventType)` — no global state
1199
+
1200
+ ---
1201
+
1202
+ ## AI and Perception
1203
+
1204
+ Deterministic AI modules (Phase 4):
1205
+
1206
+ - `sensory.ts` — `canDetect()`: vision arc, hearing range, environmental multipliers
1207
+ - `perception.ts` — `perceiveLocal()`: sensory-filtered enemy and ally detection
1208
+ - `targeting.ts` — `pickTarget()`, `updateFocus()`: horizon-limited, focus stickiness
1209
+ - `decide.ts` — `decideCommandsForEntity()`: behaviour presets with decision latency
1210
+ - `presets.ts` — `AI_PRESETS`: lineInfantry, skirmisher
1211
+ - `system.ts` — `buildAICommands()`: full AI pass over world
1212
+
1213
+ **Decision latency**: entities re-plan at most once every `decisionLatency_s` seconds
1214
+ (10 ticks for humans, 1 tick for robots). Between plans, previous intent persists.
1215
+
1216
+ **Surprise mechanics**: `canDetect(defender, attacker, env)` returns detection quality Q.
1217
+ If attacker is outside defender's FoV (< q(0.8)), defensive intensity is scaled
1218
+ proportionally. Full surprise (q(0)) eliminates defensive reaction entirely.
1219
+
1220
+ ---
1221
+
1222
+ ## Skills (Phase 7)
1223
+
1224
+ Skills represent learned technique — adjustments to physical outcomes, not abstract point totals.
1225
+ They are provided by the host application and consumed by the engine. The engine never writes
1226
+ back to skill values; progression is the host's responsibility.
1227
+
1228
+ ### Skill map
1229
+
1230
+ Each entity carries an optional `skills?: SkillMap` (`Map<SkillId, SkillLevel>`).
1231
+ When a skill is absent, `getSkill()` returns neutral defaults (no effect on simulation output),
1232
+ making all existing entities fully backward-compatible.
1233
+
1234
+ ```typescript
1235
+ import { buildSkillMap } from "./src/sim/skills.js";
1236
+
1237
+ entity.skills = buildSkillMap({
1238
+ meleeCombat: { hitTimingOffset_s: -to.s(0.2), energyTransferMul: q(1.35) },
1239
+ meleeDefence: { energyTransferMul: q(1.5) },
1240
+ athleticism: { fatigueRateMul: q(0.75) },
1241
+ });
1242
+ ```
1243
+
1244
+ ### Skill domains
1245
+
1246
+ | SkillId | Physical effect |
1247
+ |---|---|
1248
+ | `meleeCombat` | `hitTimingOffset_s` reduces attack cooldown; `energyTransferMul` scales strike energy |
1249
+ | `meleeDefence` | `energyTransferMul` scales effective parry/block quality |
1250
+ | `grappling` | `energyTransferMul` scales grapple contest score |
1251
+ | `rangedCombat` | `dispersionMul` tightens grouping radius (more accurate fire) |
1252
+ | `throwingWeapons` | `energyTransferMul` scales thrown weapon launch energy |
1253
+ | `shieldCraft` | `energyTransferMul` boosts defence skill when blocking with a shield |
1254
+ | `medical` | `treatmentRateMul` reduces fluid loss from bleeding each tick |
1255
+ | `athleticism` | `fatigueRateMul` reduces fatigue accumulation per energy tick |
1256
+ | `tactics` | `hitTimingOffset_s` reduces AI decision latency |
1257
+ | `stealth` | `dispersionMul` reduces the subject's acoustic signature (harder to hear) |
1258
+
1259
+ ### Composing skill levels
1260
+
1261
+ `combineSkillLevels(a, b)` multiplies Q fields and adds time offsets, letting the host express
1262
+ synergy bonuses or composite experience modifiers before building the SkillMap:
1263
+
1264
+ ```typescript
1265
+ import { combineSkillLevels, defaultSkillLevel } from "./src/sim/skills.js";
1266
+
1267
+ // Melee expert with an athleticism timing synergy (−0.25 s total)
1268
+ const combined = combineSkillLevels(
1269
+ { ...defaultSkillLevel(), hitTimingOffset_s: -to.s(0.20), energyTransferMul: q(1.40) },
1270
+ { ...defaultSkillLevel(), hitTimingOffset_s: -to.s(0.05) },
1271
+ );
1272
+ entity.skills = buildSkillMap({ meleeCombat: combined });
1273
+ ```
1274
+
1275
+ ---
1276
+
1277
+ ## Replay and analytics (Phase 13)
1278
+
1279
+ ### Deterministic replay
1280
+
1281
+ Every simulation is reproducible from initial state + command log. `ReplayRecorder` snapshots
1282
+ the world before the first tick and records command maps per tick. `replayTo` reconstructs
1283
+ any past tick by restoring the snapshot and re-applying frames.
1284
+
1285
+ ```typescript
1286
+ import { ReplayRecorder, replayTo, serializeReplay, deserializeReplay } from "./src/replay.js";
1287
+
1288
+ const recorder = new ReplayRecorder(world);
1289
+ for (let i = 0; i < 100; i++) {
1290
+ recorder.record(world.tick, cmds);
1291
+ stepWorld(world, cmds, ctx);
1292
+ }
1293
+
1294
+ // Seek to any past tick
1295
+ const worldAt50 = replayTo(recorder.toReplay(), 50, ctx);
1296
+
1297
+ // Persist and restore across sessions
1298
+ const json = serializeReplay(recorder.toReplay());
1299
+ const restored = deserializeReplay(json);
1300
+ const worldAt50Again = replayTo(restored, 50, ctx);
1301
+ ```
1302
+
1303
+ `serializeReplay`/`deserializeReplay` handle `Map` fields (`entity.armourState`,
1304
+ `action.capabilityCooldowns`) that `JSON.stringify` would otherwise drop.
1305
+
1306
+ ### Combat analytics
1307
+
1308
+ `CollectingTrace` implements `TraceSink` and accumulates all trace events for offline
1309
+ analysis. Pass it to `stepWorld` via `ctx.trace`, then extract metrics.
1310
+
1311
+ ```typescript
1312
+ import { CollectingTrace, collectMetrics, survivalRate, meanTimeToIncapacitation }
1313
+ from "./src/metrics.js";
1314
+
1315
+ const tracer = new CollectingTrace();
1316
+ for (let i = 0; i < 200; i++) stepWorld(world, cmds, { ...ctx, trace: tracer });
1317
+
1318
+ const m = collectMetrics(tracer.events);
1319
+ console.log("Damage dealt by entity 1:", m.damageDealt.get(1)); // joules
1320
+ console.log("Hits landed by entity 1:", m.hitsLanded.get(1));
1321
+ console.log("Survival rate:", survivalRate(tracer.events, [1, 2, 3, 4]));
1322
+ console.log("Mean TTI (ticks):", meanTimeToIncapacitation(tracer.events, [1, 2, 3, 4], 200));
1323
+ ```
1324
+
1325
+ `collectMetrics` covers melee `Attack` events and ranged `ProjectileHit` events in a single
1326
+ pass over any flat `TraceEvent[]` — ordering and tick mixing are fine.
1327
+
1328
+ ### Visual debug layer (Phase 13)
1329
+
1330
+ `extractMotionVectors`, `extractHitTraces`, and `extractConditionSamples` in `src/debug.ts`
1331
+ transform world state and trace events into renderer-friendly snapshots:
1332
+
1333
+ ```typescript
1334
+ import { extractMotionVectors, extractHitTraces, extractConditionSamples } from "./src/debug.js";
1335
+ import { CollectingTrace } from "./src/metrics.js";
1336
+
1337
+ const tracer = new CollectingTrace();
1338
+ stepWorld(world, commands, { ...ctx, trace: tracer });
1339
+
1340
+ // Motion overlay — one entry per entity with position, velocity, and facing
1341
+ const arrows = extractMotionVectors(world);
1342
+
1343
+ // Hit display — melee and projectile hits with region and energy
1344
+ const { meleeHits, projectileHits } = extractHitTraces(tracer.events);
1345
+
1346
+ // Condition heatmap — fear, shock, consciousness, fluid loss per entity
1347
+ const heatmap = extractConditionSamples(world);
1348
+
1349
+ // Sample any past tick via replay
1350
+ const past = replayTo(recorder.toReplay(), 42, ctx);
1351
+ const pastHeatmap = extractConditionSamples(past);
1352
+ ```
1353
+
1354
+ ---
1355
+
1356
+ ## Character description layer (Phase 16)
1357
+
1358
+ `src/describe.ts` translates raw SI fixed-point attributes into human-readable summaries.
1359
+ No simulation dependencies — safe to import from any host application.
1360
+
1361
+ ```typescript
1362
+ import { describeCharacter, formatCharacterSheet, formatOneLine } from "./src/describe.js";
1363
+ import { generateIndividual } from "./src/generate.js";
1364
+ import { PRO_BOXER } from "./src/archetypes.js";
1365
+
1366
+ const attrs = generateIndividual(7, PRO_BOXER);
1367
+ const desc = describeCharacter(attrs);
1368
+
1369
+ console.log(formatOneLine(desc));
1370
+ // → "Tall (1.83 m), 86.4 kg; strength excellent (4982 N), reaction quick (180 ms), resilience tough."
1371
+
1372
+ console.log(formatCharacterSheet(desc));
1373
+ // Body
1374
+ // Stature: 1.83 m — tall
1375
+ // Mass: 86.4 kg — average build
1376
+ //
1377
+ // Performance
1378
+ // Strength: 4982 N [excellent] elite level — professional fighter strength
1379
+ // Power: 2214 W [excellent] elite explosive output
1380
+ // Endurance: 398 W [strong] strong sustained performance
1381
+ // Stamina: 40 kJ [strong] good combat energy reserves
1382
+ // ...
1383
+ ```
1384
+
1385
+ ### Tier system
1386
+
1387
+ Every attribute is rated 1–6 using breakpoints grounded in sports-science literature:
1388
+
1389
+ | Tier | Label | Meaning |
1390
+ |------|-------|---------|
1391
+ | 1 | feeble / sluggish / fragile | Well below human baseline |
1392
+ | 2 | weak / slow / low | Below-average adult |
1393
+ | 3 | average | Baseline healthy adult (HUMAN_BASE anchor) |
1394
+ | 4 | strong / precise / resilient | Trained athlete or competitive fighter |
1395
+ | 5 | excellent / fast / tough | Elite / professional level |
1396
+ | 6 | exceptional / instant / ironclad | Superhuman, mechanical, or distributed biology |
1397
+
1398
+ Reaction time and decision latency use an **inverted** scale (lower value = higher tier).
1399
+ `SERVICE_ROBOT` (80 ms reaction, 50 ms decision) → tier 6 "instant" / "machine-like" in both.
1400
+ `LARGE_PACIFIC_OCTOPUS` (no enclosed skull) → tier 6 "ironclad" concussion resistance.
1401
+
1402
+ ### API
1403
+
1404
+ ```typescript
1405
+ describeCharacter(attrs: IndividualAttributes): CharacterDescription
1406
+ // Returns a structured object with tier, label, comparison string, and formatted value
1407
+ // for every attribute. Vision and hearing are formatted strings (e.g. "200 m, 120° arc").
1408
+
1409
+ formatCharacterSheet(desc: CharacterDescription): string
1410
+ // Multi-line columnar output suitable for a character screen or debug log.
1411
+
1412
+ formatOneLine(desc: CharacterDescription): string
1413
+ // Single sentence, no newlines — suitable for tooltips or list views.
1414
+ ```
1415
+
1416
+ ---
1417
+
1418
+ ## Combat narrative layer (Phase 18)
1419
+
1420
+ `src/narrative.ts` converts raw `TraceEvent` streams into human-readable text. Like
1421
+ `src/describe.ts`, it has no simulation dependencies — safe to import from UI code or CLI tools.
1422
+
1423
+ ```typescript
1424
+ import { narrateEvent, buildCombatLog, describeInjuries, describeCombatOutcome }
1425
+ from "./src/narrative.js";
1426
+ import { CollectingTrace } from "./src/metrics.js";
1427
+ import { ALL_HISTORICAL_MELEE } from "./src/weapons.js";
1428
+
1429
+ const tracer = new CollectingTrace();
1430
+ for (let i = 0; i < 10 * TICK_HZ; i++) stepWorld(world, cmds, { ...ctx, trace: tracer });
1431
+
1432
+ // Build weapon profile lookup for verb selection
1433
+ const profiles = new Map(ALL_HISTORICAL_MELEE.map(w => [w.id, w.damage]));
1434
+
1435
+ const cfg = {
1436
+ verbosity: "normal" as const,
1437
+ nameMap: new Map([[1, "Sir Roland"], [2, "the orc"]]),
1438
+ weaponProfiles: profiles,
1439
+ };
1440
+
1441
+ // Per-event narration
1442
+ for (const ev of tracer.events) {
1443
+ const line = narrateEvent(ev, cfg);
1444
+ if (line) console.log(line);
1445
+ }
1446
+ // → "Sir Roland stabs the orc in the torso"
1447
+ // → "the orc attacks Sir Roland — parried"
1448
+ // → "Sir Roland powerfully stabs the orc in the head"
1449
+ // → "the orc is knocked unconscious"
1450
+
1451
+ // Batch log
1452
+ const log = buildCombatLog(tracer.events, cfg);
1453
+
1454
+ // Injury summary
1455
+ const orc = world.entities.find(e => e.id === 2)!;
1456
+ console.log(describeInjuries(orc.injury));
1457
+ // → "Unconscious; Significant blood loss; head fractured"
1458
+
1459
+ // Outcome
1460
+ const summary = describeCombatOutcome(
1461
+ world.entities.map(e => ({ id: e.id, teamId: e.teamId, injury: e.injury })),
1462
+ 200,
1463
+ );
1464
+ console.log(summary);
1465
+ // → "Team 1 wins — Team 2 defeated (200 ticks)"
1466
+ ```
1467
+
1468
+ ### Verbosity levels
1469
+
1470
+ | Level | What is included |
1471
+ |-------|-----------------|
1472
+ | `terse` | Landed hits, KO, death, morale route/rally, fractures, blasts — nothing else |
1473
+ | `normal` | Adds blocked/parried/shield notes, misses, grapple start/break, weapon bind, treatment |
1474
+ | `verbose` | Adds grapple maintenance ticks, capability events |
1475
+
1476
+ ### Verb selection
1477
+
1478
+ Verb is chosen from the weapon's `WeaponDamageProfile` (supplied via `weaponProfiles` config):
1479
+
1480
+ | Profile dominant field | Verb |
1481
+ |------------------------|------|
1482
+ | `penetrationBias ≥ q(0.65)` | stab(s) |
1483
+ | `structuralFrac ≥ q(0.50)` | bludgeon(s) |
1484
+ | `surfaceFrac ≥ q(0.50)` | slash(es) |
1485
+ | Default | strike(s) |
1486
+
1487
+ Ranged: `penetrationBias ≥ q(0.80)` → snipe(s); `surfaceFrac ≥ q(0.55)` → blast(s); default → shoot(s).
1488
+
1489
+ Energy qualifiers: `< 10J` → "barely grazes"; `≥ 200J` → "powerfully"; `≥ 500J` → "devastatingly".
1490
+
1491
+ Set an entity's name to `"you"` in `nameMap` for second-person verb conjugation
1492
+ ("you strike" rather than "you strikes").
1493
+
1494
+ ---
1495
+
1496
+ ## Downtime & Recovery simulation (Phase 19)
1497
+
1498
+ `src/downtime.ts` bridges the gap between 20 Hz combat and hours-to-weeks campaign time.
1499
+ It runs a compressed 1 Hz loop applying the same healing physics as the kernel, suitable
1500
+ for computing wound outcomes, resource consumption, and recovery timelines between sessions.
1501
+
1502
+ ```typescript
1503
+ import { stepDowntime, MEDICAL_RESOURCES } from "./src/downtime.js";
1504
+
1505
+ const reports = stepDowntime(world, 3600, {
1506
+ treatments: new Map([
1507
+ [1, { careLevel: "first_aid" }],
1508
+ [2, { careLevel: "field_medicine", onsetDelay_s: 120 }],
1509
+ ]),
1510
+ rest: true,
1511
+ });
1512
+
1513
+ for (const r of reports) {
1514
+ console.log(`Entity ${r.entityId}: died=${r.died}, bleedingStopped=${r.bleedingStopped}`);
1515
+ console.log(` Fluid loss: ${r.injuryAtStart.fluidLoss} → ${r.injuryAtEnd.fluidLoss}`);
1516
+ console.log(` Resources used: ${r.totalCostUnits} cost units`);
1517
+ console.log(` Combat ready in: ${r.combatReadyAt_s}s`);
1518
+ }
1519
+ ```
1520
+
1521
+ **Care levels:** `"none"` | `"first_aid"` | `"field_medicine"` | `"hospital"` | `"autodoc"` | `"nanomedicine"`
1522
+
1523
+ **Resource catalogue** (`MEDICAL_RESOURCES`): 7 items from field bandage (1 unit) to nanomed dose (2000 units). All items have `costUnits` and `massGrams` for encumbrance and economy integration.
1524
+
1525
+ **Calibration:** `q(0.06)` bleedingRate → fatal in ~267 simulated seconds without treatment. First aid stops bleeding in under 60 seconds. Untreated infection fatal within 21 simulated days.
1526
+
1527
+ ---
1528
+
1529
+ ## Arena simulation framework (Phase 20)
1530
+
1531
+ `src/arena.ts` provides a declarative scenario DSL for batch-running combat trials with statistical expectations. Use it to validate simulation realism, balance archetypes, and author calibration tests.
1532
+
1533
+ ```typescript
1534
+ import { runArena, expectWinRate, expectSurvivalRate, formatArenaReport }
1535
+ from "./src/arena.js";
1536
+ import { mkKnight, mkBoxer } from "./src/presets.js";
1537
+
1538
+ const scenario = {
1539
+ name: "Knight vs Boxer",
1540
+ combatants: [
1541
+ { id: 1, teamId: 1, factory: () => mkKnight(1, 1, 0, 0) },
1542
+ { id: 2, teamId: 2, factory: () => mkBoxer(2, 2, 1, 0) },
1543
+ ],
1544
+ maxTicks: 400,
1545
+ expectations: [
1546
+ expectWinRate(1, 0.70), // knight wins ≥ 70% of trials
1547
+ expectSurvivalRate(1, 1, 0.90), // knight survives ≥ 90%
1548
+ ],
1549
+ };
1550
+
1551
+ const result = runArena(scenario, 50);
1552
+ console.log(formatArenaReport(result));
1553
+ ```
1554
+
1555
+ **Six built-in calibration scenarios** validate core physics:
1556
+ `CALIBRATION_ARMED_VS_UNARMED`, `CALIBRATION_UNTREATED_KNIFE_WOUND`,
1557
+ `CALIBRATION_FIRST_AID_SAVES_LIVES`, `CALIBRATION_FRACTURE_RECOVERY`,
1558
+ `CALIBRATION_INFECTION_UNTREATED`, `CALIBRATION_PLATE_ARMOUR`.
1559
+
1560
+ **Recovery integration:** supply `scenario.recovery` to run `stepDowntime` post-combat and get `recoveryStats` (mean days to combat-ready, p90 resource cost, etc.).
1561
+
1562
+ ---
1563
+
1564
+ ## Character progression (Phase 21)
1565
+
1566
+ `src/progression.ts` adds the temporal axis: how entities improve through training and experience, decline through ageing, and carry permanent marks from injury. No simulation dependencies — safe to use in save-game serialisation and UI layers.
1567
+
1568
+ ```typescript
1569
+ import {
1570
+ createProgressionState, awardXP, advanceSkill,
1571
+ applyTrainingSession, stepAgeing, applyAgeingDelta,
1572
+ deriveSequelae, serialiseProgression,
1573
+ } from "./src/progression.js";
1574
+ import { buildSkillMap } from "./src/sim/skills.js";
1575
+ import { to, q } from "./src/units.js";
1576
+
1577
+ // XP and milestones
1578
+ const prog = createProgressionState();
1579
+ const milestones = awardXP(prog, "meleeCombat", 1, world.tick); // +1 XP per combat
1580
+ for (const m of milestones) {
1581
+ entity.skills = advanceSkill(entity.skills ?? buildSkillMap({}), m.domain, m.delta);
1582
+ }
1583
+
1584
+ // Physical training (peakForce_N ceiling 3500 N)
1585
+ const plan = { sessions: [], frequency_d: 3/7, ceiling: to.N(3500) };
1586
+ const sess = { attribute: "peakForce_N", intensity_Q: q(0.50), duration_s: 3600 };
1587
+ entity.attributes.performance.peakForce_N =
1588
+ applyTrainingSession(entity.attributes.performance.peakForce_N, plan, sess, 3);
1589
+
1590
+ // Annual ageing
1591
+ const delta = stepAgeing(entity.attributes, entity.ageYears ?? 30);
1592
+ applyAgeingDelta(entity.attributes, delta);
1593
+
1594
+ // Injury sequelae after a bad fracture
1595
+ const seqs = deriveSequelae(entity.injury.byRegion["leftLeg"]!, entity.bodyPlan!);
1596
+ for (const s of seqs) prog.sequelae.push({ region: "leftLeg", ...s });
1597
+
1598
+ // Persist
1599
+ const json = serialiseProgression(prog); // Map-aware JSON
1600
+ ```
1601
+
1602
+ **Milestone thresholds** grow geometrically (`BASE_XP=20`, `GROWTH_FACTOR=1.80`): milestone 0 at 20 XP, milestone 1 at 36, milestone 2 at 65, milestone 3 at 117… Calibrated so 100 combats (1 XP each) reduce `meleeCombat` reaction time by ~80 ms.
1603
+
1604
+ **Training calibration:** 12-week strength programme (3×/week, moderate intensity) raises `peakForce_N` by 150–300 N. Overtraining penalty applies above 5 sessions/week.
1605
+
1606
+ **Ageing:** 1 %/year performance decline after 35; +2 ms decision latency per year after 45. Integrating from age 20 to 70 always stays above zero (physically plausible minimum).
1607
+
1608
+ **Sequelae types:** `fracture_malunion` (−15 % peak force), `nerve_damage` (−10 % fine control), `scar_tissue` (surface bleed threshold −5 %).
1609
+
1610
+ ---
1611
+
1612
+ ## Campaign & World State (Phase 22)
1613
+
1614
+ `src/campaign.ts` is the persistence layer for multi-session campaigns. It tracks world time,
1615
+ entity state, location, and item stockpiles between encounters, and delegates wound recovery
1616
+ to `stepDowntime`.
1617
+
1618
+ ```typescript
1619
+ import {
1620
+ createCampaign, addLocation, travel,
1621
+ creditInventory, debitInventory, getInventoryCount,
1622
+ stepCampaignTime, mergeEntityState,
1623
+ serialiseCampaign, deserialiseCampaign,
1624
+ } from "./src/campaign.js";
1625
+
1626
+ // Create a campaign with starting entities
1627
+ const campaign = createCampaign("my-campaign", [fighter1, fighter2], "2025-01-01T00:00:00Z");
1628
+
1629
+ // Register locations with travel costs (seconds)
1630
+ addLocation(campaign, { id: "town", name: "Town", elevation_m: 50, travelCost: new Map() });
1631
+ addLocation(campaign, { id: "dungeon", name: "Dungeon", elevation_m: 20,
1632
+ travelCost: new Map([["town", 1800]]) }); // 30 min from dungeon to town
1633
+
1634
+ // Move an entity — advances worldTime_s by travel cost
1635
+ const travelTime = travel(campaign, fighter1.id, "dungeon"); // 1800
1636
+
1637
+ // After an encounter, merge updated entity states back into the registry
1638
+ mergeEntityState(campaign, [fighter1, fighter2]);
1639
+
1640
+ // Advance time with wound recovery (delegates to stepDowntime)
1641
+ const reports = stepCampaignTime(campaign, 3600, {
1642
+ downtimeConfig: {
1643
+ treatments: new Map([[fighter1.id, { careLevel: "first_aid" }]]),
1644
+ rest: true,
1645
+ },
1646
+ });
1647
+
1648
+ // Campaign inventory (arrows, bandages, rations, etc.)
1649
+ creditInventory(campaign, fighter1.id, "arrow", 30);
1650
+ debitInventory(campaign, fighter1.id, "arrow", 5); // returns false if insufficient
1651
+ getInventoryCount(campaign, fighter1.id, "arrow"); // 25
1652
+
1653
+ // Persist across sessions (Map-aware JSON)
1654
+ const json = serialiseCampaign(campaign);
1655
+ const restored = deserialiseCampaign(json);
1656
+ ```
1657
+
1658
+ **`CampaignState` fields:**
1659
+ - `id`, `epoch` — campaign identity and ISO start timestamp
1660
+ - `worldTime_s` — absolute simulated seconds since epoch (monotonically increasing)
1661
+ - `entities: Map<number, Entity>` — master registry; deep-cloned on write
1662
+ - `locations: Map<string, Location>` — registered locations with travel routing
1663
+ - `entityLocations: Map<number, string>` — current locationId per entity
1664
+ - `entityInventories: Map<number, Map<string, number>>` — campaign item stockpiles (separate from `entity.loadout`)
1665
+ - `log: Array<{ worldTime_s, text }>` — timestamped event log
1666
+
1667
+ **Healing integration:** `stepCampaignTime` builds a minimal `WorldState`, calls `stepDowntime`,
1668
+ then writes the healed `InjuryState` back into the entity registry. The optional
1669
+ `downtimeConfig` matches `stepDowntime`'s `DowntimeConfig` exactly; if omitted, all entities
1670
+ rest with `careLevel: "none"` (natural clotting only).
1671
+
1672
+ **Serialisation:** `serialiseCampaign`/`deserialiseCampaign` handle all nested `Map` fields
1673
+ using the `__ananke_map__` marker pattern — entities, locations, entityLocations,
1674
+ entityInventories, entity skills, armourState, and location travelCost all survive round-trip.
1675
+
1676
+ ---
1677
+
1678
+ ## Dialogue & Negotiation (Phase 23)
1679
+
1680
+ `src/dialogue.ts` resolves non-combat social encounters. No Charisma stat — intimidation
1681
+ strength comes from `peakForce_N`, persuasion from cognitive depth, deception is beaten by
1682
+ sharp minds. Fully deterministic when given a seed.
1683
+
1684
+ ```typescript
1685
+ import {
1686
+ resolveDialogue, applyDialogueOutcome, narrateDialogue, dialogueProbability,
1687
+ type DialogueContext,
1688
+ } from "./src/dialogue.js";
1689
+
1690
+ const ctx: DialogueContext = {
1691
+ initiator: knight,
1692
+ target: bandit,
1693
+ worldSeed: world.seed,
1694
+ tick: world.tick,
1695
+ };
1696
+
1697
+ // Check probability before rolling
1698
+ const P = dialogueProbability({ kind: "intimidate", intensity_Q: q(0.80) }, ctx);
1699
+ // → q(0.72) for a 2800N knight vs. a mid-fear bandit
1700
+
1701
+ // Roll the outcome
1702
+ const outcome = resolveDialogue({ kind: "intimidate", intensity_Q: q(0.80) }, ctx);
1703
+ // → { result: "success", fearDelta: q(0.15) } or { result: "failure", cooldown_s: 30 }
1704
+ // → { result: "escalate" } if target.fearQ < q(0.20) and failed
1705
+
1706
+ // Write deltas back to entities
1707
+ applyDialogueOutcome(outcome, bandit);
1708
+
1709
+ // Natural language description
1710
+ const line = narrateDialogue(
1711
+ { kind: "intimidate", intensity_Q: q(0.80) }, outcome,
1712
+ { verbosity: "verbose", nameMap: new Map([[1, "Sir Roland"], [2, "the bandit"]]) },
1713
+ { initiatorId: 1, targetId: 2 },
1714
+ );
1715
+ // → "Sir Roland attempted intimidation on the bandit — the target was cowed by the show of force."
1716
+ ```
1717
+
1718
+ ### Action types
1719
+
1720
+ | Action | Resolution | Escalates? |
1721
+ |--------|-----------|------------|
1722
+ | `intimidate` | `q(peakForce_N/4000) + fearQ − distressTolerance`; leader trait −q(0.15) | Yes, if fearQ < q(0.20) |
1723
+ | `persuade` | base q(0.40) + learningBonus(attentionDepth) + factionBonus − failurePenalty | No |
1724
+ | `deceive` | `plausibility_Q × (1 − attentionDepth/10)` | No |
1725
+ | `surrender` | Deterministic: accepted if `target.fearQ > q(0.40)` | No |
1726
+ | `negotiate` | Deterministic: accepted if trade utility is positive for target | No |
1727
+
1728
+ **`dialogueProbability(action, ctx)`** — exported helper that returns the success probability
1729
+ without rolling RNG. Useful for UI previews, AI decision-making, and testing.
1730
+
1731
+ **Verbosity levels** match the narrative layer: `terse` (label: result), `normal` (one sentence),
1732
+ `verbose` (entity names + full outcome description). Entity names resolved from `cfg.nameMap`.
1733
+
1734
+ ---
1735
+
1736
+ ## Faction & Reputation (Phase 24)
1737
+
1738
+ `src/faction.ts` tracks political standing, witnesses hostile events, and modulates AI
1739
+ behaviour based on inter-faction relationships. Fully deterministic — no RNG, standing
1740
+ changes are pure arithmetic.
1741
+
1742
+ ```typescript
1743
+ import {
1744
+ createFactionState, adjustStanding, getStanding,
1745
+ extractWitnessEvents, applyReputationDelta,
1746
+ type FactionState,
1747
+ } from "./src/faction.js";
1748
+ ```
1749
+
1750
+ - **`adjustStanding(state, factionId, delta)`** — clamp standing to [−SCALE.Q, SCALE.Q]
1751
+ - **`getStanding(state, factionId)`** — 0 for unknown factions
1752
+ - **`extractWitnessEvents(world, env)`** — returns assault events seen by bystanders
1753
+ - **`applyReputationDelta`** — propagate witness events into faction standing
1754
+
1755
+ AI target-selection uses `STANDING_HOSTILE_THRESHOLD = q(−0.40)` to activate attack
1756
+ mode; the `factionGuard` preset additionally suppresses attack below standing `q(0.60)`.
1757
+
1758
+ ---
1759
+
1760
+ ## Loot & Economy (Phase 25)
1761
+
1762
+ `src/economy.ts` provides item valuation, equipment wear, loot drop resolution, and
1763
+ trade evaluation. No kernel dependency — pure data management.
1764
+
1765
+ ```typescript
1766
+ import {
1767
+ computeItemValue, armourConditionQ, applyWear,
1768
+ resolveDrops, evaluateTradeOffer, totalInventoryValue,
1769
+ type ItemInventory, type TradeOffer, type DropTable,
1770
+ } from "./src/economy.js";
1771
+ ```
1772
+
1773
+ - **`computeItemValue(item, wear_Q?)`** — derives `baseValue`, `condition_Q`, `sellFraction`
1774
+ for any weapon, armour, or medical resource
1775
+ - **`applyWear(weapon, intensity_Q, seed?)`** — accumulates `WEAR_BASE = q(0.001)` per strike;
1776
+ penalty at `q(0.30)`, fumble at `q(0.70)`, breaks at `q(1.0)`
1777
+ - **`resolveDrops(entity, seed, extra?, config?)`** — dead → all equipped items drop;
1778
+ probabilistic extras rolled deterministically from seed
1779
+ - **`evaluateTradeOffer(offer, inventory)`** — `netValue` + `feasible` from accepting party's view
1780
+
1781
+ ---
1782
+
1783
+ ## Momentum Transfer & Knockback (Phase 26)
1784
+
1785
+ `src/sim/knockback.ts` adds the impulse-momentum half of Newtonian mechanics: every
1786
+ impact now imparts a velocity delta to the target. Calibrated to real physics — a
1787
+ 5.56 mm rifle round produces negligible knockback (≈ 0.05 m/s on 75 kg), while a
1788
+ large-creature kick can knock humans prone.
1789
+
1790
+ ```typescript
1791
+ import {
1792
+ computeKnockback, applyKnockback,
1793
+ STAGGER_THRESHOLD_mps, PRONE_THRESHOLD_mps, STAGGER_TICKS,
1794
+ } from "./src/sim/knockback.js";
1795
+
1796
+ const result = computeKnockback(energy_J, massEff_kg, target);
1797
+ // → { impulse_Ns, knockback_v, staggered, prone }
1798
+
1799
+ applyKnockback(target, result, { dx, dy });
1800
+ // → mutates velocity_mps, condition.prone, action.staggerTicks
1801
+ ```
1802
+
1803
+ **Physics**: `impulse = sqrt(2 × E × m_eff)` [N·s]; `Δv = impulse / m_target` [m/s].
1804
+ Stability coefficient reduces effective knockback before threshold checks:
1805
+ `effective_v = Δv × (1 − stabilityQ)`.
1806
+
1807
+ | Threshold | Value | Effect |
1808
+ |-----------|-------|--------|
1809
+ | `STAGGER_THRESHOLD_mps` | 0.5 m/s | 3-tick stagger window |
1810
+ | `PRONE_THRESHOLD_mps` | 2.0 m/s | `condition.prone = true` |
1811
+
1812
+ The kernel integrates knockback automatically for every melee and ranged hit — no
1813
+ changes to call sites required.
1814
+
1815
+ ---
1816
+
1817
+ ## Hydrostatic Shock & Cavitation (Phase 27)
1818
+
1819
+ `src/sim/hydrostatic.ts` models the wound-amplification physics of high-velocity
1820
+ projectiles. Above 600 m/s a temporary stretch wave radiates outward through
1821
+ inelastic tissue; above 900 m/s momentary vacuum cavitation further boosts
1822
+ haemorrhage in fluid-saturated organs.
1823
+
1824
+ ```typescript
1825
+ import {
1826
+ computeTemporaryCavityMul, computeCavitationBleed,
1827
+ HYDROSTATIC_THRESHOLD_mps, CAVITATION_THRESHOLD_mps,
1828
+ } from "./src/sim/hydrostatic.js";
1829
+
1830
+ // Multiplier applied to internalDamage (q(1.0) = no effect, q(3.0) = max)
1831
+ const cavMul = computeTemporaryCavityMul(v_impact_mps, "liver"); // → q(3.0) at 960 m/s
1832
+
1833
+ // Cavitation bleed boost for fluid-saturated tissue (torso, liver, lung, spleen, legs)
1834
+ const newBleed = computeCavitationBleed(v_impact_mps, currentBleed, "torso");
1835
+ ```
1836
+
1837
+ **Tissue compliance** governs how much the stretch wave amplifies damage:
1838
+
1839
+ | Region | Compliance | Behaviour |
1840
+ |--------|-----------|-----------|
1841
+ | bone / skull | q(0.05) | Brittle; maximum cavity damage |
1842
+ | brain / liver / spleen | q(0.10) | Very inelastic; high amplification |
1843
+ | lung | q(0.30) | Partially air-filled; intermediate |
1844
+ | torso | q(0.40) | Mixed; moderate amplification |
1845
+ | muscle / limbs | q(0.60) | Elastic; minimum amplification |
1846
+
1847
+ The kernel computes `v_impact_mps` from pre-armour projectile energy and mass in
1848
+ `resolveShoot`, passes it through `ImpactEvent`, and applies both functions
1849
+ automatically in the finalImpacts loop — no changes to call sites required.
1850
+
1851
+ ---
1852
+
1853
+ ## Cone AoE: Breath Weapons, Fire, Gas (Phase 28)
1854
+
1855
+ `src/sim/cone.ts` adds directional cone geometry to the capability system, enabling
1856
+ breath weapons, flamethrowers, gas dispensers, and sonic disorientation blasts.
1857
+
1858
+ ```typescript
1859
+ import { entityInCone, buildEntityFacingCone } from "./src/sim/cone.js";
1860
+ import type { CapabilityEffect, CapabilitySource } from "./src/sim/capability.js";
1861
+ import { q, SCALE } from "./src/units.js";
1862
+ import { DamageChannel } from "./src/channels.js";
1863
+
1864
+ // Dragon fire breath — 20 ticks sustained, 30° half-angle cone, 10m range
1865
+ const DRAGON_FIRE: CapabilityEffect = {
1866
+ id: "fire_breath",
1867
+ cost_J: 800, // deducted each sustained tick
1868
+ castTime_ticks: 5,
1869
+ sustainedTicks: 20, // fires 20 consecutive ticks
1870
+ coneHalfAngle_rad: Math.PI / 6, // 30° half-angle = 60° total cone
1871
+ coneDir: "facing", // follows entity's facingDirQ
1872
+ range_m: 10 * SCALE.m,
1873
+ payload: {
1874
+ kind: "weaponImpact",
1875
+ energy_J: 800,
1876
+ profile: {
1877
+ surfaceFrac: q(0.60), // fire burns surface heavily
1878
+ internalFrac: q(0.30), // convective heat reaches internal tissue
1879
+ structuralFrac: q(0.10),
1880
+ bleedFactor: q(0.05),
1881
+ penetrationBias: q(0.05),
1882
+ },
1883
+ },
1884
+ };
1885
+ // Total reserve cost for one breath: 800J × 20 ticks = 16 000 J
1886
+ ```
1887
+
1888
+ **Cone direction modes**:
1889
+ - `coneDir: "facing"` — cone follows the actor's `facingDirQ` (updated by movement commands)
1890
+ - `coneDir: "fixed"` with `coneDirFixed: { dx, dy }` — fixed world-space direction (gas cloud, mounted turret)
1891
+
1892
+ **Sustained emission** respects the same concentration-break rules as `castTime_ticks = -1`
1893
+ auras: shock ≥ q(0.30) cancels emission immediately. Each tick deducts `cost_J`; if the
1894
+ source reserve falls below `cost_J` the emission stops early.
1895
+
1896
+ **`weaponImpact` payload** allows any damage profile without being constrained to a
1897
+ `DamageChannel`. Unlike `impact` (which maps to a synthetic weapon via `DamageChannel`),
1898
+ `weaponImpact` takes a `WeaponDamageProfile` directly — enabling fire's
1899
+ surface-heavy/internal-pass-through split or acid's structural bias.
1900
+
1901
+ ---
1902
+
1903
+ ## Environmental Stress: Thermoregulation (Phase 29)
1904
+
1905
+ `src/sim/thermoregulation.ts` models core body temperature as a continuous heat-balance
1906
+ system. Unlike abstract "cold resistance" stats, temperature is tracked in real °C (encoded
1907
+ as Q) and driven by genuine thermophysics.
1908
+
1909
+ ```typescript
1910
+ import { stepCoreTemp, deriveTempModifiers, cToQ, CORE_TEMP_NORMAL_Q } from "./src/sim/thermoregulation.js";
1911
+ import type { KernelContext } from "./src/sim/context.js";
1912
+
1913
+ // Ambient temperature −10°C arctic environment
1914
+ const ctx: KernelContext = {
1915
+ thermalAmbient_Q: cToQ(-10), // cToQ converts Celsius to engine Q encoding
1916
+ tractionCoeff: q(0.9),
1917
+ };
1918
+
1919
+ // stepWorld automatically calls stepCoreTemp for every living entity each tick.
1920
+ // After simulation, query an entity's thermal state:
1921
+ const mods = deriveTempModifiers((entity.condition as any).coreTemp_Q ?? CORE_TEMP_NORMAL_Q);
1922
+ // → { powerMul: q(0.80), fineControlPen: q(0.15), latencyMul: q(1.2), dead: false }
1923
+ // → moderate hypothermia: −20% power, +20% reaction time
1924
+ ```
1925
+
1926
+ **Temperature stage thresholds** (Q encoding; `q(0.5)` = 37 °C; full range 10–64 °C):
1927
+
1928
+ | Stage | Core temp | `powerMul` | `fineControlPen` | `latencyMul` |
1929
+ |-------|-----------|-----------|-----------------|-------------|
1930
+ | Critical hyperthermia | > 40.1 °C | q(0.60) | q(0.30) | q(3.0) — dead |
1931
+ | Heat stroke | 39.4–40.1 °C | q(0.60) | q(0.20) | q(2.0) |
1932
+ | Heat exhaustion | 38.6–39.4 °C | q(0.85) | q(0.10) | q(1.0) |
1933
+ | Mild hyperthermia | 37.8–38.6 °C | q(0.95) | q(0) | q(1.0) |
1934
+ | Normal | 37.0–37.8 °C | q(1.0) | q(0) | q(1.0) |
1935
+ | Mild hypothermia | 36.2–37.0 °C | q(0.95) | q(0.05) | q(1.0) |
1936
+ | Moderate hypothermia | 34.6–36.2 °C | q(0.80) | q(0.15) | q(1.2) |
1937
+ | Severe hypothermia | 33.0–34.6 °C | q(0.50) | q(0.20) | q(3.0) |
1938
+ | Critical hypothermia | < 33.0 °C | q(0.50) | q(0.30) | q(4.0) — dead |
1939
+
1940
+ **Armour insulation** is modelled via `insulation_m2KW?: number` on `Armour` items.
1941
+ Higher insulation slows both heating and cooling. Typical values: plate armour 0.02, wool 0.15, fur 0.25.
1942
+
1943
+ **Downtime integration**: `DowntimeConfig.thermalAmbient_Q` enables temperature tracking
1944
+ through multi-hour recovery simulations. `EntityRecoveryReport.finalCoreTemp_Q` reports the
1945
+ final state.
1946
+
1947
+ ---
1948
+
1949
+ ## Nutrition & Starvation (Phase 30)
1950
+
1951
+ `src/sim/nutrition.ts` adds the longest survivability axis: caloric balance from hours to
1952
+ weeks. Hunger states impose escalating combat penalties and eventually cause mass loss —
1953
+ fat catabolism first, then muscle tissue.
1954
+
1955
+ ```typescript
1956
+ import {
1957
+ computeBMR, stepNutrition, consumeFood, deriveHungerModifiers,
1958
+ FOOD_ITEMS, type HungerState,
1959
+ } from "./src/sim/nutrition.js";
1960
+
1961
+ // BMR for a 75 kg entity: 80 W (Kleiber's law)
1962
+ const bmr = computeBMR(entity.attributes.morphology.mass_kg); // → 80
1963
+
1964
+ // stepWorld calls stepNutrition automatically at 1 Hz for every living entity.
1965
+ // Feed an entity from their food inventory:
1966
+ (entity as any).foodInventory = new Map([["ration_bar", 3], ["water_flask", 2]]);
1967
+ const ate = consumeFood(entity, "ration_bar", world.tick); // → true; caloricBalance += 2 000 000 J
1968
+
1969
+ // Query current state:
1970
+ const hunger: HungerState = (entity.condition as any).hungerState ?? "sated";
1971
+ const mods = deriveHungerModifiers(hunger);
1972
+ // → { staminaMul: q(1.0), forceMul: q(1.0), latencyMul: q(1.0), moraleDecay: q(0) } // sated
1973
+ // → { staminaMul: q(0.50), forceMul: q(0.80), latencyMul: q(1.50), moraleDecay: q(0.030) } // critical
1974
+ ```
1975
+
1976
+ **Hunger state thresholds** (deficit relative to BMR):
1977
+
1978
+ | State | Onset | `staminaMul` | `forceMul` | `latencyMul` | `moraleDecay` |
1979
+ |-------|-------|-------------|-----------|-------------|--------------|
1980
+ | `sated` | deficit < 12 h × BMR | q(1.0) | q(1.0) | q(1.0) | q(0) |
1981
+ | `hungry` | 12–24 h × BMR | q(0.90) | q(1.0) | q(1.0) | q(0) |
1982
+ | `starving` | 24–72 h × BMR | q(0.75) | q(0.90) | q(1.0) | q(0.030)/tick |
1983
+ | `critical` | ≥ 72 h × BMR | q(0.50) | q(0.80) | q(1.50) | q(0.030)/tick |
1984
+
1985
+ **Food catalogue** (`FOOD_ITEMS`):
1986
+
1987
+ | Item | Energy | Mass | Hydration |
1988
+ |------|--------|------|-----------|
1989
+ | `ration_bar` | 2 000 000 J | 500 g | — |
1990
+ | `dried_meat` | 1 500 000 J | 300 g | — |
1991
+ | `hardtack` | 800 000 J | 200 g | — |
1992
+ | `fresh_bread` | 700 000 J | 250 g | — |
1993
+ | `berry_handful` | 150 000 J | 50 g | — |
1994
+ | `water_flask` | 0 J | 500 g | 500 000 hydJ |
1995
+
1996
+ **Mass loss** (accumulated as float; applies only during starvation/critical):
1997
+ - Fat catabolism: 300 g/day (`mass_kg -= 300/86400 × delta_s`)
1998
+ - Muscle catabolism: 0.5 N/hour (`peakForce_N -= 0.5 × SCALE.N / 3600 × delta_s`, critical only)
1999
+
2000
+ **Entity food inventory**: attach `(entity as any).foodInventory = new Map<string, number>()`
2001
+ before calling `consumeFood`. Absent inventory = unlimited supply.
2002
+
2003
+ ---
2004
+
2005
+ ## Project layout
2006
+
2007
+ ```
2008
+ src/
2009
+ units.ts Fixed-point SI unit system and arithmetic helpers
2010
+ types.ts Core attribute interfaces (Morphology, Performance, Control, Resilience, Perception)
2011
+ channels.ts DamageChannel enum and bitmask helpers
2012
+ traits.ts Entity trait definitions and attribute multiplier application
2013
+ archetypes.ts Reference archetype baselines (HUMAN_BASE, SERVICE_ROBOT, AMATEUR_BOXER, PRO_BOXER, GRECO_WRESTLER, KNIGHT_INFANTRY, LARGE_PACIFIC_OCTOPUS)
2014
+ equipment.ts Weapon, Armour, Shield, RangedWeapon, Loadout types and starter item catalogue (includes wpn_boxing_gloves)
2015
+ generate.ts Procedural individual generation from archetype with variance distributions; NarrativeBias parameter (Phase 62)
2016
+ polity.ts Phase 61: Polity & World-State System — createPolity/Registry, trade, war, diplomacy, tech advancement, population-scale disease spread; integrates with Faction/Economy/Tech/Disease/Campaign
2017
+ presets.ts Entity factory functions for named real-world archetypes (mkBoxer, mkWrestler, mkKnight, mkOctopus, mkScubaDiver)
2018
+ derive.ts Movement caps and energy/fatigue derived from attributes and loadout
2019
+ replay.ts ReplayRecorder, replayTo, serializeReplay/deserializeReplay — deterministic replay
2020
+ metrics.ts CollectingTrace, collectMetrics, survivalRate, meanTimeToIncapacitation — analytics
2021
+ debug.ts extractMotionVectors, extractHitTraces, extractConditionSamples — visual debug layer
2022
+ model3d.ts deriveMassDistribution, deriveInertiaTensor, deriveAnimationHints, derivePoseModifiers, deriveGrappleConstraint, extractRigSnapshots — 3D rig integration
2023
+ describe.ts describeCharacter, formatCharacterSheet, formatOneLine — SI→human-readable translation layer (no sim dependencies)
2024
+ weapons.ts Historical weapons database — ~70 weapons across 6 eras (Prehistoric → Contemporary); shieldBypassQ for flexible weapons; magCapacity + shotInterval_s for magazine firearms
2025
+ narrative.ts narrateEvent, buildCombatLog, describeInjuries, describeCombatOutcome — combat narrative layer (no sim dependencies)
2026
+ downtime.ts stepDowntime(), MEDICAL_RESOURCES — 1 Hz wound recovery bridge (hours-to-weeks scale)
2027
+ arena.ts runArena(), expectWinRate/SurvivalRate/MeanDuration/Recovery/ResourceCost, formatArenaReport, 6 calibration scenarios
2028
+ progression.ts createProgressionState(), awardXP(), advanceSkill(), applyTrainingSession(), stepAgeing(), applyAgeingDelta(), deriveSequelae()
2029
+ campaign.ts createCampaign(), addLocation(), travel(), mergeEntityState(), stepCampaignTime(), debitInventory/creditInventory/getInventoryCount(), serialiseCampaign/deserialiseCampaign()
2030
+ dialogue.ts resolveDialogue(), applyDialogueOutcome(), narrateDialogue(), dialogueProbability() — social encounter resolution (intimidate/persuade/deceive/surrender/negotiate)
2031
+ faction.ts createFactionState(), adjustStanding(), extractWitnessEvents(), applyReputationDelta() — faction tracking and reputation system
2032
+ economy.ts computeItemValue(), applyWear(), resolveDrops(), evaluateTradeOffer() — item valuation, wear, loot drops, trade evaluation
2033
+ narrative-stress.ts runNarrativeStressTest(), formatStressTestReport(), beatEntityDefeated/Survives/TeamDefeated/ShockExceeds/Fatigued() — narrative push analyser (Phase 63)
2034
+
2035
+ sim/
2036
+ kernel.ts stepWorld(), applyFallDamage(), applyExplosion() — main simulation entry points
2037
+ entity.ts Entity type (all mutable simulation state)
2038
+ world.ts WorldState type
2039
+ kinds.ts CommandKind, TraceKind, MoveMode, DefenceMode enums (includes MoraleRally)
2040
+ body.ts BodyRegion type, region weights, hit-to-region mapping
2041
+ injury.ts InjuryState, per-region damage, bleeding rate helpers
2042
+ medical.ts MedicalTier, MedicalAction, tier rank/multiplier tables
2043
+ condition.ts ConditionState (fire, radiation, suffocation, stun, prone, etc.)
2044
+ impairment.ts deriveFunctionalState() — damage to mobility and manipulation penalties
2045
+ combat.ts resolveHit(), parryLeverageQ(), shield helpers
2046
+ grapple.ts Grapple resolution: score, positions, throw/choke/joint-lock
2047
+ weapon_dynamics.ts Reach dominance, two-handed bonus, miss recovery, weapon bind/break
2048
+ ranged.ts Ranged physics: energy at range, dispersion, grouping radius, costs
2049
+ explosion.ts BlastSpec, blastEnergyFracQ, fragmentsExpected, fragmentKineticEnergy
2050
+ substance.ts Substance, ActiveSubstance, STARTER_SUBSTANCES — pharmacokinetics model
2051
+ tech.ts TechEra, TechCapability, TechContext, defaultTechContext, isCapabilityAvailable
2052
+ capability.ts CapabilitySource, CapabilityEffect, RegenModel, EffectPayload, FieldEffect — Clarke's Third Law abstraction
2053
+ knockback.ts computeKnockback(), applyKnockback() — impulse-momentum transfer; stagger/prone checks
2054
+ hydrostatic.ts computeTemporaryCavityMul(), computeCavitationBleed() — high-velocity wound physics
2055
+ cone.ts entityInCone(), buildEntityFacingCone(), ConeSpec — directional cone AoE geometry (breath weapons, flamethrowers, gas)
2056
+ thermoregulation.ts stepCoreTemp(), deriveTempModifiers(), cToQ() — 9-stage core-temp model (mild hyperthermia → cardiac-arrest hypothermia)
2057
+ nutrition.ts computeBMR(), stepNutrition(), consumeFood(), deriveHungerModifiers(), FOOD_ITEMS — caloric balance, hunger states, fat/muscle catabolism
2058
+ events.ts ImpactEvent type, deterministic sort
2059
+ seeds.ts Deterministic per-event seed derivation
2060
+ formation.ts pickNearestEnemyInReach()
2061
+ frontage.ts applyFrontageCap() — limits engagers per target
2062
+ occlusion.ts isMeleeLaneOccludedByFriendly()
2063
+ density.ts computeDensityField() — crowd slowdown
2064
+ push.ts stepPushAndRepulsion() — entity separation
2065
+ spatial.ts Grid spatial index and neighbour queries
2066
+ indexing.ts buildWorldIndex() — O(1) id-to-entity lookup
2067
+ vec3.ts Fixed-point 3D vector maths
2068
+ team.ts isEnemy() helper
2069
+ intent.ts IntentState defaults
2070
+ action.ts ActionState defaults
2071
+ trace.ts TraceSink interface and nullTrace
2072
+ tuning.ts SimulationTuning presets (arcade, tactical, sim)
2073
+ testing.ts mkHumanoidEntity(), mkWorld() test helpers
2074
+
2075
+ bodyplan.ts BodyPlan, BodySegment, 8 body plan constants, resolveHitSegment, getExposureWeight
2076
+ sensory.ts canDetect(), SensoryEnvironment — vision arc + hearing + env modifiers
2077
+ skills.ts SkillId, SkillLevel, SkillMap, buildSkillMap, getSkill, combineSkillLevels
2078
+
2079
+ ai/
2080
+ types.ts AIPolicy interface
2081
+ presets.ts AI_PRESETS (lineInfantry, skirmisher)
2082
+ perception.ts perceiveLocal() — sensory-filtered enemy and ally detection
2083
+ targeting.ts pickTarget(), updateFocus() — horizon-limited target selection
2084
+ decide.ts decideCommandsForEntity() — with decision latency cooldown
2085
+ system.ts buildAICommands() — full AI pass over world
2086
+
2087
+ formation-unit.ts computeShieldWallCoverage, deriveRankSplit, stepFormationCasualtyFill, computeFormationMomentum, deriveFormationCohesion, deriveFormationAllyFearDecay — Phase 6 formation system
2088
+
2089
+ test/ Vitest test suite (one file per feature area)
2090
+ tools/ Developer utilities and runnable demos
2091
+ blade-runner.ts Artificial Life Validation — 365-day city-scale emergent-behaviour test
2092
+ emergent-validation.ts Emergent Behaviour Validation Suite — 4 historical combat scenarios × 100 seeds
2093
+ benchmark.ts Performance & Scalability Benchmarks — 10/100/500/1 000 entity throughput
2094
+ what-if.ts "What If?" Alternate History Engine — polity divergence × 100 seeds (Phase 64)
2095
+ generate-zoo.ts Simulation Zoo generator — 5 pre-computed scenarios embedded in docs/zoo/index.html
2096
+ generate-map.ts Generative Cartography — 180-day polity simulation → docs/map/index.html SVG viewer
2097
+ world-server.ts Persistent World Server — HTTP server with live polling client at docs/world-client/
2098
+ docs/
2099
+ onboarding.md New-engineer two-week onboarding guide
2100
+ contributing.md Contribution guide: conventions, PR checklist, module skeleton
2101
+ versioning.md Versioning contract: commit-hash pinning, breaking-change tiers, upgrade cadence
2102
+ ecosystem.md Ecosystem index: body-plan templates, renderer bridge boilerplate, companion repo suggestions
2103
+ integration-primer.md Deep technical onboarding (architecture, data-flow, gotchas)
2104
+ bridge-api.md Renderer bridge API reference
2105
+ use-case-validation.md Integration Milestone 1 — use-case fit validation
2106
+ narrative-stress-test.md Narrative Stress Test guide: API, Deus Ex score, cinematic benchmark table
2107
+ editors/
2108
+ index.html Editor hub — links to all visual design tools
2109
+ species-forge.html Species Forge — body plan + archetype + narrative bias editor
2110
+ culture-forge.html Culture Forge — cultural values, taboos, myth predispositions, diplomacy modifiers editor
2111
+ zoo/
2112
+ index.html Simulation Zoo — 5 pre-computed combat/disease scenarios with health visualiser
2113
+ map/
2114
+ index.html Generative Cartography — 180-day polity map with timeline slider
2115
+ world-client/
2116
+ index.html Persistent World Client — live-polling browser UI for world-server.ts
2117
+ companion-projects/
2118
+ ananke-godot-reference/README.md Godot 4 physics-driven character plugin starter
2119
+ ananke-unity-reference/README.md Unity 6 physics-driven character plugin starter
2120
+ ananke-threejs-bridge/README.md In-browser Three.js renderer bridge starter
2121
+ ananke-language-forge/README.md LLM-backed dynamic dialogue generation starter
2122
+ ananke-world-ui/README.md SvelteKit world management UI starter
2123
+ ananke-fantasy-species/README.md Fantasy / sci-fi species pack starter
2124
+ ananke-historical-battles/README.md Historical battle validation suite starter
2125
+ ananke-archive/README.md Searchable public simulation database starter
2126
+ ```
2127
+
2128
+ ---
2129
+
2130
+ ## Companion ecosystem
2131
+
2132
+ Seven companion projects are designed to build on top of Ananke. Each has a starter README
2133
+ in `docs/companion-projects/` describing the architecture, key Ananke entry points, and a
2134
+ suggested first milestone.
2135
+
2136
+ | Repository | Purpose |
2137
+ |---|---|
2138
+ | `ananke-godot-reference` | Godot 4 plugin driving a humanoid rig from `extractRigSnapshots` over WebSocket |
2139
+ | `ananke-unity-reference` | Unity 6 plugin using the HTTP polling sidecar; HumanBodyBones mapping |
2140
+ | `ananke-threejs-bridge` | In-browser Three.js renderer — no sidecar, WebAssembly kernel long-term target |
2141
+ | `ananke-language-forge` | LLM-backed dynamic dialogue generation reading `linguisticIntelligence_Q` and Phase 66 myth events |
2142
+ | `ananke-world-ui` | SvelteKit world management UI — supersedes `docs/editors/` when feature-complete |
2143
+ | `ananke-fantasy-species` | Fantasy / sci-fi species pack with allometric-scaled body plans and archetype templates |
2144
+ | `ananke-historical-battles` | Historical battle validation suite comparing outcomes against primary sources |
2145
+ | `ananke-archive` | Searchable public database of simulation runs, parameter spaces, and raw trace data |
2146
+
2147
+ ### Companion ecosystem infrastructure (ROADMAP CE-1 to CE-4)
2148
+
2149
+ The following Ananke-side changes are the highest-leverage items for enabling companion projects:
2150
+
2151
+ **CE-1 — npm publish + subpath exports map.** Remove `"private": true` from `package.json`,
2152
+ add a `"exports"` map so consumers can import `ananke/units`, `ananke/sim/kernel`, etc.
2153
+ without bundling the whole library. See ROADMAP for proposed exports JSON.
2154
+
2155
+ **CE-2 — `createWorld()` convenience factory.** A single call that builds a `World` with
2156
+ sensible defaults (5 humans, no terrain, tactical tuning). Eliminates 20 lines of setup
2157
+ boilerplate for companion projects that need a quick simulation harness.
2158
+
2159
+ **CE-3 — JSON scenario schema + `loadScenario()`.** Lets companion projects (especially
2160
+ `ananke-historical-battles` and `ananke-fantasy-species`) ship scenario definitions as JSON
2161
+ files rather than TypeScript, reducing the barrier to non-developer contribution.
2162
+
2163
+ **CE-4 — `src/index.ts` stable-API barrel.** A single import surface (`import { ... } from "ananke"`)
2164
+ covering everything in `STABLE_API.md`. Companion projects should depend on this barrel, not
2165
+ on internal module paths that may change.
2166
+
2167
+ ---
2168
+
2169
+ ## Performance baselines
2170
+
2171
+ Performance is a published contract, not just a tool that exists.
2172
+
2173
+ | Scenario | Entities | Median tick | Throughput | 20 Hz budget |
2174
+ |----------|----------|-------------|------------|--------------|
2175
+ | Melee skirmish | 10 | 0.19 ms | 5 300 ticks/s | 4% |
2176
+ | Mixed ranged/melee | 100 | 4.68 ms | 214 ticks/s | 9% |
2177
+ | Formation combat | 500 | 31 ms | 32 ticks/s | 62% |
2178
+ | Weather + disease | 1 000 | 64 ms | 16 ticks/s | 129% (exceeds real-time) |
2179
+
2180
+ Measured on Node 22, Apple M-series reference hardware. Full methodology, AI-budget
2181
+ breakdown, spatial-index comparison, memory footprint, and tuning guidance are in
2182
+ `docs/performance.md`. Run with `npm run run:benchmark`.
2183
+
2184
+ **Key constraint:** at 20 Hz real-time rate, 500 entities fits comfortably within budget;
2185
+ 1 000 entities requires a tick-rate reduction or spatial partitioning. `stepWorld` (kernel
2186
+ physics) consumes ≥ 95% of tick budget at all entity counts; AI is negligible (< 1%).
2187
+
2188
+ Benchmark regression is enforced in CI via `npm run benchmark-check` — throughput regressions
2189
+ are caught before release (ROADMAP item 15, complete).
2190
+
2191
+ ---
2192
+
2193
+ ## Validation
2194
+
2195
+ Ananke's validation strategy has two layers:
2196
+
2197
+ **Isolated subsystem validation** — 19+ subsystems (sprint speed, bleeding rate, sleep
2198
+ deprivation, thermoregulation, etc.) tested individually against empirical datasets with
2199
+ ±20 % tolerance on the simulated mean. Run with `npm run run:validation`.
2200
+
2201
+ **Emergent behaviour validation** — Four historical combat scenarios (English longbowmen at
2202
+ Agincourt, Roman testudo vs. Gaul charge, small-unit skirmish attrition, epidemic spread)
2203
+ each run across 100 seeds. The distribution of outcomes is compared against historical
2204
+ casualty data. Run with `npm run run:emergent-validation`.
2205
+
2206
+ Results from both suites are committed to `docs/` and linked from releases. See ROADMAP
2207
+ item PH-8 for the plan to make these first-class trust artifacts.
2208
+
2209
+ ---
2210
+
2211
+ ## Next steps — Platform Hardening
2212
+
2213
+ The simulation is architecturally complete. The highest-leverage remaining work is making
2214
+ the existing depth trustworthy and legible to adopters:
2215
+
2216
+ | Item | What | Status |
2217
+ |------|------|--------|
2218
+ | PH-1 | API tiering — Stable / Advanced / Internal tiers in exports and docs | Planned |
2219
+ | PH-2 | Versioning policy unification — one unambiguous adopter contract | Planned |
2220
+ | PH-3 | Minimal host integration contract document | Planned |
2221
+ | PH-4 | Save / replay / bridge contract tests — golden compatibility fixtures | Planned |
2222
+ | PH-5 | Bridge as first-class supported surface — `docs/bridge-contract.md` | Planned |
2223
+ | PH-6 | Entity / WorldState core vs. extensions split — JSDoc annotations | Planned |
2224
+ | PH-7 | Benchmark operational guide — tick-rate and entity-cap recommendations | Planned |
2225
+ | PH-8 | Emergent validation as flagship trust artifact — versioned, CI-enforced | Planned |
2226
+
2227
+ See ROADMAP `## Platform Hardening` for full scope of each item.