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

|
|
4
|
+
|
|
5
|
+
> **Package:** `@its-not-rocket-science/ananke`
|
|
6
|
+
> **Full project overview:** [`docs/project-overview.md`](docs/project-overview.md)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## What is Ananke?
|
|
11
|
+
|
|
12
|
+
Ananke is a **deterministic, physics-grounded simulation engine** for characters, combat,
|
|
13
|
+
and survivability. It models entities using real physical quantities — mass in kg, force
|
|
14
|
+
in newtons, energy in joules — rather than abstract hit points or dice rolls.
|
|
15
|
+
|
|
16
|
+
Same seed + same inputs → identical results, every time. No floating-point drift.
|
|
17
|
+
Suitable for lockstep multiplayer, reproducible research, and offline AI training.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @its-not-rocket-science/ananke
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Requires Node ≥ 18. ESM-only. TypeScript declarations included — no `@types/` package
|
|
28
|
+
needed. Zero runtime dependencies.
|
|
29
|
+
|
|
30
|
+
> **Versioning:** pin to a specific version in production. The `0.x` series may include
|
|
31
|
+
> minor-version breaking changes to Tier 2 (experimental) APIs; Tier 1 (Stable) APIs follow
|
|
32
|
+
> full semver. See [`STABLE_API.md`](STABLE_API.md) for the tier breakdown and
|
|
33
|
+
> [`docs/versioning.md`](docs/versioning.md) for the upgrade policy.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Core concepts
|
|
38
|
+
|
|
39
|
+
### Fixed-point arithmetic
|
|
40
|
+
|
|
41
|
+
All simulation values use `Q` — a fixed-point integer where `SCALE.Q = 16384` represents
|
|
42
|
+
`1.0`. Never use raw `number` for simulation values; always use `q()` to construct them.
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { q, SCALE } from "@its-not-rocket-science/ananke";
|
|
46
|
+
|
|
47
|
+
const half = q(0.50); // 8192 — 50%
|
|
48
|
+
const full = q(1.00); // 16384 — 100%
|
|
49
|
+
const eighty = q(0.80); // 13107 — 80%
|
|
50
|
+
|
|
51
|
+
// SI unit scales
|
|
52
|
+
SCALE.m; // 1000 — 1 metre in fixed-point units
|
|
53
|
+
SCALE.kg; // 1000 — 1 kilogram
|
|
54
|
+
SCALE.mps; // 1000 — 1 m/s
|
|
55
|
+
SCALE.J; // 1 — 1 joule (energy is stored at 1:1)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
You will see values like `position_m: { x: 3000, y: 0, z: 0 }` — that is 3 metres on the
|
|
59
|
+
x-axis (`3000 / SCALE.m = 3`). The `_m`, `_kg`, `_J`, `_s` suffixes on field names tell
|
|
60
|
+
you the unit.
|
|
61
|
+
|
|
62
|
+
### The Entity
|
|
63
|
+
|
|
64
|
+
An `Entity` is any simulated object. Required fields at creation:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import type { Entity } from "@its-not-rocket-science/ananke";
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
| Field | Type | Meaning |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `id` | `number` | Unique integer; used as RNG salt |
|
|
73
|
+
| `teamId` | `number` | Entities attack those on different teams |
|
|
74
|
+
| `position_m` | `Vec3` | World-space position in fixed-point metres |
|
|
75
|
+
| `attributes` | `IndividualAttributes` | Physical stats (force, power, mass…) |
|
|
76
|
+
| `energy` | `{ current_J, max_J }` | Stamina pool in joules |
|
|
77
|
+
| `injury` | `InjuryState` | Per-region damage accumulation |
|
|
78
|
+
| `condition` | `ConditionSnapshot` | Shock, fear, fatigue |
|
|
79
|
+
| `loadout` | `{ items: Item[] }` | Equipped weapons and armour |
|
|
80
|
+
|
|
81
|
+
Use a factory instead of constructing these manually — see **Quick starts** below.
|
|
82
|
+
|
|
83
|
+
### The simulation loop
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { mkWorld, stepWorld } from "@its-not-rocket-science/ananke";
|
|
87
|
+
|
|
88
|
+
const world = mkWorld(seed, entities); // create world with deterministic seed
|
|
89
|
+
|
|
90
|
+
for (let tick = 0; tick < 2000; tick++) {
|
|
91
|
+
const commands = buildCommands(world); // your AI / player input
|
|
92
|
+
stepWorld(world, commands, ctx); // mutates world in-place
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`stepWorld` is the only function that mutates state. Everything else is pure computation.
|
|
97
|
+
Call it at 20 Hz for real-time simulation; 1 Hz or lower for campaign-scale time.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Quick start A — Melee combat
|
|
102
|
+
|
|
103
|
+
Two fighters, one fight, three seeds:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import {
|
|
107
|
+
mkWorld, stepWorld, generateIndividual, q,
|
|
108
|
+
SCALE, STARTER_WEAPONS, STARTER_ARMOUR,
|
|
109
|
+
buildAICommands, buildWorldIndex, buildSpatialIndex,
|
|
110
|
+
AI_PRESETS,
|
|
111
|
+
} from "@its-not-rocket-science/ananke";
|
|
112
|
+
import type { Q } from "@its-not-rocket-science/ananke";
|
|
113
|
+
|
|
114
|
+
const LONGSWORD = STARTER_WEAPONS[2]!;
|
|
115
|
+
const LEATHER = STARTER_ARMOUR[0]!;
|
|
116
|
+
|
|
117
|
+
function makeEntity(id: number, teamId: number, x_m: number) {
|
|
118
|
+
const e = generateIndividual("KNIGHT_INFANTRY", id, teamId);
|
|
119
|
+
e.position_m = { x: x_m * SCALE.m, y: 0, z: 0 };
|
|
120
|
+
e.loadout = { items: [LONGSWORD, LEATHER] };
|
|
121
|
+
return e;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const policy = AI_PRESETS["lineInfantry"]!;
|
|
125
|
+
|
|
126
|
+
for (const seed of [1, 42, 99]) {
|
|
127
|
+
const a = makeEntity(1, 1, -2);
|
|
128
|
+
const b = makeEntity(2, 2, +2);
|
|
129
|
+
const world = mkWorld(seed, [a, b]);
|
|
130
|
+
const ctx = { tractionCoeff: q(0.85) as Q };
|
|
131
|
+
|
|
132
|
+
let tick = 0;
|
|
133
|
+
while (tick < 2000 && !a.injury.dead && !b.injury.dead) {
|
|
134
|
+
tick++;
|
|
135
|
+
const idx = buildWorldIndex(world);
|
|
136
|
+
const spat = buildSpatialIndex(world, 40_000);
|
|
137
|
+
const cmds = buildAICommands(world, idx, spat, () => policy);
|
|
138
|
+
stepWorld(world, cmds, ctx);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const winner = a.injury.dead ? "B" : b.injury.dead ? "A" : "draw";
|
|
142
|
+
console.log(`seed=${seed} winner=${winner} ticks=${tick}`);
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Reading injury state
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
for (const [region, inj] of Object.entries(entity.injury.regions)) {
|
|
150
|
+
const pct = (inj.surfaceDamage / SCALE.Q * 100).toFixed(0);
|
|
151
|
+
if (inj.surfaceDamage > 0)
|
|
152
|
+
console.log(` ${region}: ${pct}% surface damage${inj.infected ? " [infected]" : ""}`);
|
|
153
|
+
}
|
|
154
|
+
console.log(` dead: ${entity.injury.dead}`);
|
|
155
|
+
console.log(` shock: ${(entity.condition.shockQ / SCALE.Q * 100).toFixed(0)}%`);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Using the narrative layer
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import {
|
|
162
|
+
CollectingTrace, renderChronicle,
|
|
163
|
+
} from "@its-not-rocket-science/ananke";
|
|
164
|
+
|
|
165
|
+
const trace = new CollectingTrace();
|
|
166
|
+
stepWorld(world, commands, { ...ctx, trace });
|
|
167
|
+
|
|
168
|
+
const log = renderChronicle(trace.events, world.entities, { verbosity: "normal" });
|
|
169
|
+
console.log(log);
|
|
170
|
+
// → "Knight strikes Brawler in the torso for 340 J. Brawler staggers."
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Quick start B — Campaign and world simulation
|
|
176
|
+
|
|
177
|
+
Advance two polities through 90 days with tech diffusion and emotional contagion:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import {
|
|
181
|
+
createPolityRegistry, stepPolityDay,
|
|
182
|
+
applyEmotionalContagion, stepTechDiffusion,
|
|
183
|
+
createEmotionalWave, FEAR_WAVE, q, SCALE,
|
|
184
|
+
} from "@its-not-rocket-science/ananke";
|
|
185
|
+
|
|
186
|
+
const WORLD_SEED = 1;
|
|
187
|
+
|
|
188
|
+
const registry = createPolityRegistry([
|
|
189
|
+
{ id: 1, name: "Ironhold", population: 50_000, techEra: 2, moraleQ: q(0.70) /* ... */ },
|
|
190
|
+
{ id: 2, name: "Ashfeld", population: 30_000, techEra: 1, moraleQ: q(0.55) /* ... */ },
|
|
191
|
+
]);
|
|
192
|
+
|
|
193
|
+
const pairs = [{ polityA: 1, polityB: 2, routeQuality_Q: q(0.60), atWar: false /* ... */ }];
|
|
194
|
+
|
|
195
|
+
for (let day = 1; day <= 90; day++) {
|
|
196
|
+
stepPolityDay(registry, WORLD_SEED, day);
|
|
197
|
+
stepTechDiffusion(registry, pairs, WORLD_SEED, day);
|
|
198
|
+
applyEmotionalContagion(registry, [createEmotionalWave(FEAR_WAVE, 1)], pairs);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (const p of registry.polities) {
|
|
202
|
+
console.log(`${p.name}: pop=${p.population} era=${p.techEra} morale=${(p.moraleQ / SCALE.Q).toFixed(2)}`);
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Quick start C — Species and character generation
|
|
209
|
+
|
|
210
|
+
Generate individuals from a body-plan archetype, apply aging, and describe them:
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
import {
|
|
214
|
+
generateIndividual, applyAgingToAttributes,
|
|
215
|
+
describeCharacter, formatCharacterSheet,
|
|
216
|
+
} from "@its-not-rocket-science/ananke";
|
|
217
|
+
|
|
218
|
+
// Generate a 45-year-old knight
|
|
219
|
+
const base = generateIndividual("KNIGHT_INFANTRY", 1, 1);
|
|
220
|
+
const aged = applyAgingToAttributes(base.attributes, 45);
|
|
221
|
+
|
|
222
|
+
console.log(formatCharacterSheet({ ...base, attributes: aged }));
|
|
223
|
+
// → Strength: 1840 N [above average]
|
|
224
|
+
// Reaction: 0.21 s [average]
|
|
225
|
+
// ...
|
|
226
|
+
|
|
227
|
+
// Fantasy species
|
|
228
|
+
const elf = generateIndividual("ELF_ARCHER", 2, 2);
|
|
229
|
+
console.log(describeCharacter(elf));
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Available built-in archetypes: `KNIGHT_INFANTRY`, `PRO_BOXER`, `GRECO_WRESTLER`,
|
|
233
|
+
`AMATEUR_BOXER`, `LARGE_PACIFIC_OCTOPUS`, and all species defined in
|
|
234
|
+
[`src/species.ts`](src/species.ts) — humans, elves, dwarves, orcs, dragons,
|
|
235
|
+
Vulcans, Klingons, and more.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## The command system
|
|
240
|
+
|
|
241
|
+
`stepWorld` takes a `CommandMap` — a `Map<entityId, EntityCommand>`. You build it
|
|
242
|
+
manually, from your AI layer, or from the built-in AI system:
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
import type { EntityCommand } from "@its-not-rocket-science/ananke";
|
|
246
|
+
|
|
247
|
+
// Attack
|
|
248
|
+
const commands = new Map<number, EntityCommand>([
|
|
249
|
+
[entityId, { kind: "attack", targetId: opponentId, weapon: LONGSWORD }],
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
// Move to a position
|
|
253
|
+
commands.set(entityId, {
|
|
254
|
+
kind: "move",
|
|
255
|
+
destination: { x: 5 * SCALE.m, y: 0, z: 0 },
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Treat a wounded ally
|
|
259
|
+
commands.set(medicId, {
|
|
260
|
+
kind: "treat",
|
|
261
|
+
targetId: woundedId,
|
|
262
|
+
schedule: { care: "field_surgery", equipmentTier: 2 },
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Valid `kind` values: `"attack"`, `"move"`, `"grapple"`, `"treat"`, `"use_capability"`,
|
|
267
|
+
`"signal"`, `"idle"`.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Determinism
|
|
272
|
+
|
|
273
|
+
Ananke guarantees that `mkWorld(seed, entities)` followed by identical commands produces
|
|
274
|
+
identical `WorldState` at every tick, regardless of platform, JS engine, or execution time.
|
|
275
|
+
|
|
276
|
+
**Rules to preserve determinism in your host:**
|
|
277
|
+
|
|
278
|
+
1. Never use `Math.random()` — use `makeRng(eventSeed(...))` from the package instead
|
|
279
|
+
2. Iterate `world.entities` in insertion order (it is a stable array, not a Map)
|
|
280
|
+
3. Keep entity `id` values stable across ticks — IDs are used as RNG salts
|
|
281
|
+
4. Do not rely on wall-clock time inside the simulation loop
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
import { makeRng, eventSeed } from "@its-not-rocket-science/ananke";
|
|
285
|
+
|
|
286
|
+
// Deterministic RNG inside your AI or event code:
|
|
287
|
+
const rng = makeRng(eventSeed(world.seed, world.tick, entityId, 0, 42));
|
|
288
|
+
const roll = rng(); // float in [0, 1) — deterministic from inputs
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Replay and serialisation
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
import {
|
|
297
|
+
ReplayRecorder, serializeReplay, deserializeReplay, replayTo,
|
|
298
|
+
} from "@its-not-rocket-science/ananke";
|
|
299
|
+
|
|
300
|
+
// Record
|
|
301
|
+
const recorder = new ReplayRecorder();
|
|
302
|
+
for (let tick = 0; tick < N; tick++) {
|
|
303
|
+
const cmds = buildCommands(world);
|
|
304
|
+
recorder.record(tick, cmds);
|
|
305
|
+
stepWorld(world, cmds, ctx);
|
|
306
|
+
}
|
|
307
|
+
const json = serializeReplay(recorder.replay); // stable JSON string
|
|
308
|
+
|
|
309
|
+
// Replay to any tick
|
|
310
|
+
const replay = deserializeReplay(json);
|
|
311
|
+
const state = replayTo(replay, initialWorld, targetTick, ctx);
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## 3D renderer bridge
|
|
317
|
+
|
|
318
|
+
Extract per-segment pose data for driving a humanoid rig at renderer frame rate:
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
import {
|
|
322
|
+
extractRigSnapshots, deriveAnimationHints, BridgeEngine,
|
|
323
|
+
} from "@its-not-rocket-science/ananke";
|
|
324
|
+
|
|
325
|
+
// Per-tick: get bone transforms
|
|
326
|
+
const snapshots = extractRigSnapshots(world.entities, bodyPlan);
|
|
327
|
+
// snapshots[entityId] → RigSnapshot { segments: Map<segmentId, { position_m, rotation }> }
|
|
328
|
+
|
|
329
|
+
// Per-tick: get animation state machine hints
|
|
330
|
+
const hints = deriveAnimationHints(entity);
|
|
331
|
+
// hints → { idle, walk, run, attacking, prone, unconscious, dead, shockQ, fearQ, ... }
|
|
332
|
+
|
|
333
|
+
// Or use BridgeEngine for double-buffered interpolation at renderer frame rate:
|
|
334
|
+
const bridge = new BridgeEngine(config);
|
|
335
|
+
bridge.writeSimFrame(world.tick, world.entities);
|
|
336
|
+
const interp = bridge.readInterpolated(rendererTimestamp);
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
See [`docs/bridge-contract.md`](docs/bridge-contract.md) for the full double-buffer
|
|
340
|
+
protocol and `AnimationHints` field-by-field contract.
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## API stability tiers
|
|
345
|
+
|
|
346
|
+
| Tier | Guarantee | Examples |
|
|
347
|
+
|------|-----------|---------|
|
|
348
|
+
| **Tier 1 — Stable** | Breaking changes require major semver bump + migration guide | `stepWorld`, `generateIndividual`, `Entity`, `q`, `SCALE`, bridge module |
|
|
349
|
+
| **Tier 2 — Experimental** | May change in minor versions; CHANGELOG will note it | Campaign, polity, dialogue, faction, quest subsystems |
|
|
350
|
+
| **Tier 3 — Internal** | No stability guarantee; may change at any time | `makeRng`, `eventSeed`, kernel tuning constants, `mkHumanoidEntity` |
|
|
351
|
+
|
|
352
|
+
Full tier table: [`STABLE_API.md`](STABLE_API.md)
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## TypeScript
|
|
357
|
+
|
|
358
|
+
The package ships full `.d.ts` declarations. Key types to know:
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
import type {
|
|
362
|
+
Entity, // the simulated object
|
|
363
|
+
WorldState, // world.entities + world.tick + world.seed
|
|
364
|
+
KernelContext, // tractionCoeff, weather, etc. — passed to stepWorld
|
|
365
|
+
EntityCommand, // what an entity does this tick
|
|
366
|
+
IndividualAttributes, // physical stats (SI units)
|
|
367
|
+
InjuryState, // per-region damage
|
|
368
|
+
ConditionSnapshot, // shock, fear, fatigue
|
|
369
|
+
Q, // fixed-point number alias (just `number` at runtime)
|
|
370
|
+
Vec3, // { x, y, z } in fixed-point metres
|
|
371
|
+
} from "@its-not-rocket-science/ananke";
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
`Q` is a nominal alias for `number` — it carries no runtime overhead, but the `q()`
|
|
375
|
+
constructor and `SCALE` constants make the intent clear in every formula.
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## Performance guidance
|
|
380
|
+
|
|
381
|
+
| Scenario | Recommended tick rate | Practical entity cap |
|
|
382
|
+
|---|---|---|
|
|
383
|
+
| Duel / 1v1 | 20 Hz | Unlimited |
|
|
384
|
+
| Skirmish (squads) | 20 Hz | ~300 |
|
|
385
|
+
| Battle (formations) | 10 Hz | ~500 |
|
|
386
|
+
| Siege / campaign | 1 Hz | ~1 000 |
|
|
387
|
+
| World simulation | 0.01 Hz (once/day) | ~10 000 |
|
|
388
|
+
|
|
389
|
+
Enable `buildSpatialIndex` when entities exceed ~50 and distances matter. Disable
|
|
390
|
+
expensive subsystems (disease O(n²) spread, thermoregulation) at high entity counts
|
|
391
|
+
unless required.
|
|
392
|
+
|
|
393
|
+
Full benchmark methodology and operational guide: [`docs/performance.md`](docs/performance.md)
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## Validation and trust
|
|
398
|
+
|
|
399
|
+
Ananke's outputs are validated against historical and experimental sources:
|
|
400
|
+
|
|
401
|
+
- **Isolated sub-system validation** — compares physical constants against sport-science
|
|
402
|
+
and biomechanics datasets: `npm run run:validation`
|
|
403
|
+
- **Emergent validation** — four historical combat scenarios (du Picq, Keegan, Lanchester,
|
|
404
|
+
Raudzens) across 100 seeds each: `npm run run:emergent-validation`
|
|
405
|
+
- **Pinned baseline** — committed result summaries that CI guards against regression:
|
|
406
|
+
[`docs/emergent-validation-report.md`](docs/emergent-validation-report.md)
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## Further reading
|
|
411
|
+
|
|
412
|
+
| Document | What's in it |
|
|
413
|
+
|---|---|
|
|
414
|
+
| [`docs/host-contract.md`](docs/host-contract.md) | Stable integration surface — everything needed to embed Ananke without reading `src/` |
|
|
415
|
+
| [`docs/integration-primer.md`](docs/integration-primer.md) | Data-flow diagrams, type glossary, gotchas |
|
|
416
|
+
| [`docs/bridge-contract.md`](docs/bridge-contract.md) | 3D renderer bridge protocol (AnimationHints, GrapplePoseConstraint) |
|
|
417
|
+
| [`STABLE_API.md`](STABLE_API.md) | Full tier table for every export |
|
|
418
|
+
| [`docs/versioning.md`](docs/versioning.md) | Semver policy, breaking-change tiers, upgrade cadence |
|
|
419
|
+
| [`docs/performance.md`](docs/performance.md) | Benchmark results, operational guide, entity caps |
|
|
420
|
+
| [`docs/emergent-validation-report.md`](docs/emergent-validation-report.md) | Historical scenario validation report |
|
|
421
|
+
| [`docs/project-overview.md`](docs/project-overview.md) | Full project overview — implementation status, entity model reference, design principles, architecture |
|