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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,310 @@
1
+ # Ananke — Host Integration Contract
2
+
3
+ *Platform Hardening PH-3 — Minimal Host Integration Contract*
4
+
5
+ > **Scope:** This document covers only **Tier 1 (Stable)** exports. Every symbol listed
6
+ > here will not change in a breaking way without a major semver bump and a migration guide.
7
+ > See [`STABLE_API.md`](../STABLE_API.md) and [`docs/versioning.md`](versioning.md) for
8
+ > the full tier table and stability guarantees.
9
+ >
10
+ > An engineer can embed Ananke in a host process using only this document and the three
11
+ > [quickstart examples](#quickstart-examples) below — no `src/` reading required.
12
+
13
+ ---
14
+
15
+ ## 1 · World creation
16
+
17
+ ```typescript
18
+ import { mkWorld } from "ananke"; // src/sim/testing — Tier 3 helper, quickstart-safe
19
+
20
+ const world = mkWorld(seed, entities);
21
+ ```
22
+
23
+ | Parameter | Type | Description |
24
+ |-----------|------|-------------|
25
+ | `seed` | `number` | World RNG seed. Same seed + same commands → identical output forever |
26
+ | `entities` | `Entity[]` | Initial entity list. IDs must be unique; `mkWorld` throws on duplicates |
27
+
28
+ `mkWorld` returns a `WorldState` with `tick: 0`. Entities are sorted by `id` ascending.
29
+
30
+ **`WorldState` shape (stable fields):**
31
+
32
+ ```typescript
33
+ interface WorldState {
34
+ tick: number; // current tick; incremented by stepWorld
35
+ seed: number; // RNG seed passed at creation
36
+ entities: Entity[]; // all live and dead entities; do not splice manually
37
+ }
38
+ ```
39
+
40
+ > **Note on `createWorld`:** A `createWorld()` function will replace `mkWorld` when the
41
+ > companion ecosystem ships (CE-2). Until then, use `mkWorld(seed, entities)`.
42
+
43
+ ---
44
+
45
+ ## 2 · Command injection (input protocol)
46
+
47
+ Commands tell entities what to do next tick. They are **consumed and cleared** by
48
+ `stepWorld`; you rebuild the map every tick.
49
+
50
+ ```typescript
51
+ import type { CommandMap, Command } from "ananke";
52
+
53
+ // One entity can have multiple commands in priority order.
54
+ const cmds: CommandMap = new Map<number, readonly Command[]>();
55
+
56
+ cmds.set(entityId, [
57
+ { kind: "attack", targetId: 2, weaponSlot: "mainHand" },
58
+ ]);
59
+
60
+ stepWorld(world, cmds, ctx);
61
+ ```
62
+
63
+ **Common command kinds** (all Tier 1):
64
+
65
+ | `kind` | Required fields | Effect |
66
+ |--------|----------------|--------|
67
+ | `"move"` | `direction: Vec3, speed_mps: I32` | Move entity in direction at speed |
68
+ | `"attack"` | `targetId: number, weaponSlot: string` | Initiate a weapon attack |
69
+ | `"defend"` | `style: "block"\|"parry"\|"dodge"` | Adopt a defensive posture |
70
+ | `"grapple"` | `targetId: number, mode: GrappleMode` | Initiate or advance a grapple |
71
+ | `"treat"` | `targetId: number` | Apply first aid to a target |
72
+ | `"set_prone"` | `prone: boolean` | Go prone or stand up |
73
+
74
+ Entities without a command in the map are idle (continue any ongoing action or stand still).
75
+
76
+ ---
77
+
78
+ ## 3 · `stepWorld` — call contract
79
+
80
+ ```typescript
81
+ import { stepWorld } from "ananke";
82
+ import type { KernelContext } from "ananke";
83
+
84
+ const ctx: KernelContext = {
85
+ tractionCoeff: q(0.80), // ground friction, typically q(0.75)–q(1.0)
86
+ };
87
+
88
+ stepWorld(world, cmds, ctx); // mutates world in place, returns void
89
+ ```
90
+
91
+ **Contract:**
92
+
93
+ | Property | Value |
94
+ |----------|-------|
95
+ | Return value | `void` — world is mutated in place |
96
+ | `world.tick` after call | incremented by 1 |
97
+ | Determinism | `stepWorld(clone(world), cmds, ctx)` ≡ `stepWorld(world, cmds, ctx)` for identical inputs |
98
+ | Thread safety | Not thread-safe. Call from one thread; snapshot with `structuredClone` for parallel reads |
99
+ | `cmds` after call | Map entries are not modified; safe to reuse or discard |
100
+ | `ctx` after call | `ctx.tractionCoeff` may be modified if `ctx.weather` applies weather modifiers |
101
+
102
+ **`KernelContext` required fields:**
103
+
104
+ | Field | Type | Notes |
105
+ |-------|------|-------|
106
+ | `tractionCoeff` | `Q` | Ground friction for movement. Use `q(0.80)` as a safe default |
107
+
108
+ **`KernelContext` optional fields (all Tier 2 or Tier 3):**
109
+
110
+ | Field | Effect when provided |
111
+ |-------|---------------------|
112
+ | `tuning` | Override default tuning constants (tactical / campaign / downtime preset) |
113
+ | `sensoryEnv` | Ambient lighting, visibility range — defaults to full daylight |
114
+ | `weather` | Applies rain/snow/wind modifiers to traction and senses |
115
+ | `terrainGrid` | Per-cell traction lookup by entity position |
116
+ | `obstacleGrid` | Impassable and partial-cover cells |
117
+ | `elevationGrid` | Height above ground — affects reach and projectile range |
118
+ | `ambientTemperature_Q` | Drives thermoregulation (heat/cold stress) |
119
+ | `techCtx` | Technology era gate for era-appropriate item validation |
120
+ | `trace` | Attach a `TraceSink` to receive per-tick debug events |
121
+
122
+ ---
123
+
124
+ ## 4 · Replay and serialization
125
+
126
+ Replays work because `stepWorld` is a **pure function** of `(WorldState, CommandMap, KernelContext)`.
127
+ Recording snapshots the initial world and logs commands; replaying re-applies them in order.
128
+
129
+ ### Recording
130
+
131
+ ```typescript
132
+ import { ReplayRecorder } from "ananke/replay"; // src/replay.ts (Tier 2)
133
+
134
+ const recorder = new ReplayRecorder(world); // deep-clones world at tick 0
135
+
136
+ for (let i = 0; i < N; i++) {
137
+ const cmds = buildCommands(world);
138
+ recorder.record(world.tick, cmds); // log before step
139
+ stepWorld(world, cmds, ctx);
140
+ }
141
+
142
+ const replay = recorder.toReplay(); // { initialState, frames }
143
+ ```
144
+
145
+ ### Serialization
146
+
147
+ ```typescript
148
+ import { serializeReplay, deserializeReplay } from "ananke/replay";
149
+
150
+ const json = serializeReplay(replay); // → string (JSON)
151
+ const replay2 = deserializeReplay(json); // → Replay
152
+ ```
153
+
154
+ `serializeReplay` / `deserializeReplay` round-trip is a **Tier 1 contract**: a serialized
155
+ replay from version `0.x.y` must deserialize and replay identically on version `0.x.z` (same
156
+ minor, higher patch).
157
+
158
+ ### Replaying to a target tick
159
+
160
+ ```typescript
161
+ import { replayTo } from "ananke/replay";
162
+
163
+ const worldAtTick50 = replayTo(replay, 50, ctx);
164
+ // Returns a fresh WorldState cloned from the replay; does not mutate replay.
165
+ ```
166
+
167
+ ---
168
+
169
+ ## 5 · Bridge data extraction (3D renderer integration)
170
+
171
+ The bridge layer converts simulation state into renderer-friendly types. Extract each tick
172
+ after `stepWorld` and pass to `BridgeEngine.update()`.
173
+
174
+ ```typescript
175
+ import { extractRigSnapshots, deriveAnimationHints } from "ananke"; // src/model3d.ts (Tier 2)
176
+ import { BridgeEngine } from "ananke"; // src/bridge/index.ts (Tier 2)
177
+
178
+ // --- simulation side ---
179
+ const snapshots = extractRigSnapshots(world); // RigSnapshot[] — one per entity
180
+ stepWorld(world, cmds, ctx);
181
+
182
+ // --- renderer side (may run at higher frame rate) ---
183
+ engine.update(snapshots, motionVectors);
184
+ const state = engine.getInterpolatedState(entityId, renderTime_s);
185
+ // state: { position_m, velocity_mps, facing, animation, poseModifiers, … }
186
+ ```
187
+
188
+ **Key types:**
189
+
190
+ | Type | Source | Description |
191
+ |------|--------|-------------|
192
+ | `RigSnapshot` | `extractRigSnapshots(world)[i]` | Per-entity rig data at one tick |
193
+ | `AnimationHints` | `deriveAnimationHints(entity)` | State flags: `isMoving`, `isGrappling`, `shockQ`, etc. |
194
+ | `PoseModifier[]` | `derivePoseModifiers(entity)` | Per-segment injury weight for bone deformation |
195
+ | `GrapplePoseConstraint` | `deriveGrappleConstraint(entity)` | IK constraint for grappling pairs |
196
+
197
+ See [`docs/bridge-contract.md`](bridge-api.md) for the full double-buffer protocol and
198
+ interpolation/extrapolation semantics.
199
+
200
+ ---
201
+
202
+ ## 6 · Quickstart-safe helpers
203
+
204
+ These Tier 3 functions are **safe to use in quickstarts** despite being officially internal.
205
+ They are small, stable in practice, and documented here to avoid source-diving.
206
+
207
+ | Helper | Signature | Notes |
208
+ |--------|-----------|-------|
209
+ | `mkWorld(seed, entities)` | `(number, Entity[]) → WorldState` | Create a world from entity array |
210
+ | `mkHumanoidEntity(id, attrs?)` | `(number, Partial<IndividualAttributes>?) → Entity` | Build a humanoid entity with defaults |
211
+ | `generateIndividual(seed, archetype)` | `(number, Archetype) → IndividualAttributes` | Stat-rolled entity attributes (Tier 1) |
212
+
213
+ ---
214
+
215
+ ## Quickstart examples
216
+
217
+ ### Minimal 1v1 duel loop
218
+
219
+ ```typescript
220
+ import { mkWorld, mkHumanoidEntity, stepWorld, q } from "ananke";
221
+ import type { CommandMap } from "ananke";
222
+
223
+ const a = mkHumanoidEntity(1);
224
+ const b = mkHumanoidEntity(2);
225
+ const world = mkWorld(42, [a, b]);
226
+
227
+ const ctx = { tractionCoeff: q(0.80) };
228
+
229
+ for (let tick = 0; tick < 200 && !a.dead && !b.dead; tick++) {
230
+ const cmds: CommandMap = new Map([
231
+ [1, [{ kind: "attack", targetId: 2, weaponSlot: "mainHand" }]],
232
+ [2, [{ kind: "attack", targetId: 1, weaponSlot: "mainHand" }]],
233
+ ]);
234
+ stepWorld(world, cmds, ctx);
235
+ }
236
+
237
+ console.log(`a.dead=${a.dead} b.dead=${b.dead} ticks=${world.tick}`);
238
+ ```
239
+
240
+ ### Record and replay
241
+
242
+ ```typescript
243
+ import { mkWorld, mkHumanoidEntity, stepWorld, q } from "ananke";
244
+ import { ReplayRecorder, serializeReplay, deserializeReplay, replayTo } from "ananke/replay";
245
+
246
+ const world = mkWorld(99, [mkHumanoidEntity(1), mkHumanoidEntity(2)]);
247
+ const ctx = { tractionCoeff: q(0.80) };
248
+ const rec = new ReplayRecorder(world);
249
+
250
+ for (let tick = 0; tick < 50; tick++) {
251
+ const cmds = new Map([[1, [{ kind: "attack", targetId: 2, weaponSlot: "mainHand" }]]]);
252
+ rec.record(world.tick, cmds);
253
+ stepWorld(world, cmds, ctx);
254
+ }
255
+
256
+ const json = serializeReplay(rec.toReplay());
257
+ const replay2 = deserializeReplay(json);
258
+ const world50 = replayTo(replay2, 50, ctx);
259
+
260
+ console.log("replay deterministic:", world50.entities[0]!.shock === world.entities[0]!.shock);
261
+ ```
262
+
263
+ ### 3D renderer integration (bridge)
264
+
265
+ ```typescript
266
+ import { mkWorld, mkHumanoidEntity, stepWorld, q } from "ananke";
267
+ import { extractRigSnapshots } from "ananke";
268
+ import { BridgeEngine } from "ananke";
269
+
270
+ const world = mkWorld(1, [mkHumanoidEntity(1), mkHumanoidEntity(2)]);
271
+ const engine = new BridgeEngine({ mappings: [], defaultBoneName: "root" });
272
+ const ctx = { tractionCoeff: q(0.80) };
273
+
274
+ engine.setEntityBodyPlan(1, "humanoid");
275
+ engine.setEntityBodyPlan(2, "humanoid");
276
+
277
+ function gameLoop(renderTime_s: number) {
278
+ const snaps = extractRigSnapshots(world);
279
+ stepWorld(world, new Map(), ctx);
280
+ engine.update(snaps);
281
+
282
+ // Renderer queries at any sub-tick time
283
+ const state = engine.getInterpolatedState(1, renderTime_s);
284
+ if (state) {
285
+ // state.position_m, state.facing, state.animation, state.poseModifiers…
286
+ }
287
+ }
288
+ ```
289
+
290
+ ---
291
+
292
+ ## Error handling
293
+
294
+ `stepWorld` does not throw under normal operation. Errors you may encounter:
295
+
296
+ | Situation | Error | Fix |
297
+ |-----------|-------|-----|
298
+ | Duplicate entity IDs passed to `mkWorld` | `Error: mkWorld: duplicate entity IDs` | Ensure each entity has a unique `id` |
299
+ | `deserializeReplay(json)` called with malformed JSON | `SyntaxError` | Validate JSON before deserializing |
300
+ | `replayTo(replay, tick, ctx)` with `tick > replay.frames.length` | Returns world at final recorded tick | Check `replay.frames.length` before calling |
301
+
302
+ ---
303
+
304
+ ## What this document does NOT cover
305
+
306
+ - **Subsystem APIs** (disease, sleep, aging, mount, hazard) — see [`STABLE_API.md`](../STABLE_API.md) §Tier 2
307
+ - **Bridge internals** — see [`docs/bridge-api.md`](bridge-api.md)
308
+ - **Validation and calibration** — see [`tools/validation.ts`](../tools/validation.ts)
309
+ - **Downtime and campaign simulation** — see [`src/downtime.ts`](../src/downtime.ts)
310
+ - **Quest, settlement, narrative subsystems** — see [`STABLE_API.md`](../STABLE_API.md) §Tier 2
@@ -0,0 +1,315 @@
1
+ # Ananke Integration Primer
2
+
3
+ *Integration & Adoption Milestone 2 — Deep Integration & Technical Onboarding*
4
+
5
+ ---
6
+
7
+ ## Purpose
8
+
9
+ > **New to Ananke?** Start with [`docs/host-contract.md`](host-contract.md) — it covers the
10
+ > complete stable integration surface with working code examples. Return here for
11
+ > architecture diagrams, type glossary, and integration gotchas.
12
+
13
+ This document captures the technical insights, data‑flow diagrams, type glossaries, and gotchas discovered during the 2–4 week evaluation spike described in the ROADMAP’s **Deep Integration & Technical Onboarding** milestone. It is intended as an internal reference for engineers who will be integrating Ananke into a production game or simulation.
14
+
15
+ The spike consisted of three concrete experiments:
16
+
17
+ 1. **Tracing the data flow of a simple melee attack** — from `Command` input through the kernel to injury output (`tools/trace‑attack.ts`).
18
+ 2. **Building a minimal observer** that reads `WorldState` after each `stepWorld` call and prints entity positions, condition, and injury summaries (`tools/observer.ts`).
19
+ 3. **Experimenting with saving and loading a complete `WorldState`** to understand the serialisation format and any Map/BigInt round‑trip concerns (`tools/serialize.ts`).
20
+
21
+ Each experiment is documented below, followed by a glossary of critical types and a list of integration gotchas.
22
+
23
+ ---
24
+
25
+ ## 1. Architecture Overview
26
+
27
+ Ananke is a deterministic, lockstep‑friendly simulation kernel that models entities using **real physical quantities** stored as **fixed‑point integers** (Q‑scaled values). The simulation proceeds in discrete ticks (default 20 Hz). Each tick, the host supplies a `CommandMap` keyed by entity ID; the kernel advances the `WorldState` and returns the updated state.
28
+
29
+ ### Core data structures
30
+
31
+ ```typescript
32
+ interface WorldState {
33
+ tick: number;
34
+ seed: number;
35
+ entities: Entity[];
36
+ activeFieldEffects?: FieldEffect[];
37
+ __sensoryEnv?: any; // internal side‑channel
38
+ }
39
+
40
+ interface Entity {
41
+ id: number;
42
+ teamId: number;
43
+ attributes: IndividualAttributes; // physical capabilities
44
+ energy: { reserveEnergy_J: number; fatigue: Q };
45
+ loadout: { items: EquipmentItem[] };
46
+ position_m: Vec3;
47
+ velocity_mps: Vec3;
48
+ intent: IntentState; // derived from previous tick’s commands
49
+ action: ActionState; // cooldowns, active binds, etc.
50
+ condition: ConditionState; // fear, morale, sensory modifiers
51
+ injury: InjuryState; // per‑region damage, shock, consciousness
52
+ grapple: GrappleState;
53
+ // optional maps (foodInventory, armourState, reputations)
54
+ }
55
+
56
+ type CommandMap = Map<number, Command[]>;
57
+ ```
58
+
59
+ ### Kernel entry point
60
+
61
+ ```typescript
62
+ function stepWorld(
63
+ world: WorldState,
64
+ commands: CommandMap,
65
+ ctx: KernelContext
66
+ ): void;
67
+ ```
68
+
69
+ The kernel **mutates** `world` in place. All randomness is derived from `world.seed` and the current tick, ensuring determinism across runs.
70
+
71
+ ---
72
+
73
+ ## 2. Data Flow of a Melee Attack
74
+
75
+ The file `tools/trace‑attack.ts` instruments a single tick with a `CollectingTrace` sink and prints every event emitted by the kernel. The following pipeline is observed (events appear in this order):
76
+
77
+ 1. **`TickStart`** — kernel clears the internal `ImpactEvent` queue.
78
+ 2. **`Intent`** — entity’s intent state (derived from previous tick’s commands) is captured before movement.
79
+ 3. **`Move`** — movement resolved; position and velocity updated.
80
+ 4. **`AttackAttempt`** — `resolveAttack` performs the hit roll, block/parry check, area selection, and hit‑quality computation.
81
+ 5. **`Attack`** — `resolveHit` delivers energy to the selected region, accounts for armour/shield penetration, and accumulates injury.
82
+ 6. **`Injury`** — `stepConditionsToInjury` updates shock, fluid‑loss, and consciousness from the accumulated damage.
83
+ 7. **`TickEnd`** — all queued `ImpactEvent`s are applied and the tick’s state is finalised.
84
+
85
+ > **Key insight:** Injury accumulation is deferred until the `Injury` event; multiple attacks in the same tick are queued and resolved together, preserving ordering determinism.
86
+
87
+ ### Example trace output (abridged)
88
+
89
+ ```
90
+ [attackAttempt] tick=0 attackerId=1 targetId=2 hit=true blocked=false area="torso"
91
+ [attack] tick=0 attackerId=1 targetId=2 weaponId="wpn_club" region="torso" energy_J=285
92
+ [injury] tick=0 entityId=2 dead=false shockQ=74 consciousnessQ=9977
93
+ ```
94
+
95
+ The trace shows that a club strike delivering 285 J to the torso raised the target’s shock by 0.74 % (74 Q) and lowered consciousness by 0.23 %.
96
+
97
+ ---
98
+
99
+ ## 3. Observing WorldState Each Tick
100
+
101
+ The observer (`tools/observer.ts`) demonstrates how to hook into the `stepWorld` loop, extract per‑tick entity state, and format it for debugging or visualisation. It uses two pure data‑extraction functions from `src/debug.ts`:
102
+
103
+ - `extractMotionVectors(world)` → `{ entityId, position_m, velocity_mps, facing }`
104
+ - `extractConditionSamples(world)` → `{ entityId, shock, consciousness, fearQ, fluidLoss, dead }`
105
+
106
+ ### Observer pattern
107
+
108
+ ```typescript
109
+ for (let tick = 0; tick < maxTicks; tick++) {
110
+ // 1. Build indexes (required for AI decisions, but we hard‑code commands)
111
+ const index = buildWorldIndex(world);
112
+ const spatial = buildSpatialIndex(world, 4 * SCALE.m);
113
+
114
+ // 2. Generate commands (hard‑coded in this example)
115
+ const cmds: CommandMap = new Map();
116
+ cmds.set(1, [makeAttackCommand(2, ...)]);
117
+ cmds.set(2, [defendBlock(...)]);
118
+
119
+ // 3. Extract and print state BEFORE the tick
120
+ const motion = extractMotionVectors(world);
121
+ const condition = extractConditionSamples(world);
122
+ // … format and log …
123
+
124
+ // 4. Execute the tick
125
+ stepWorld(world, cmds, ctx);
126
+
127
+ // 5. Stop early if a termination condition is met
128
+ if (target.injury.dead || target.injury.consciousness <= 0) break;
129
+ }
130
+ ```
131
+
132
+ > **Key insight:** The observer must call `buildWorldIndex` and `buildSpatialIndex` before generating commands, because the AI decision functions (`decideCommandsForEntity`) depend on those indexes. If you hard‑code commands, the indexes are not strictly needed for `stepWorld` itself.
133
+
134
+ ---
135
+
136
+ ## 4. Serialization and Deterministic Replay
137
+
138
+ The serialisation demo (`tools/serialize.ts`) shows how to round‑trip a `WorldState` through JSON while preserving determinism.
139
+
140
+ ### Map fields
141
+
142
+ Optional Map fields on `Entity` (`foodInventory`, `armourState`, `reputations`) must be explicitly converted to an array of entries for JSON serialisation:
143
+
144
+ ```typescript
145
+ function serializeEntity(e: Entity): unknown {
146
+ const obj: any = { ...e };
147
+ if (e.foodInventory instanceof Map) {
148
+ obj.foodInventory = Array.from(e.foodInventory.entries());
149
+ }
150
+ // … similarly for armourState, reputations
151
+ return obj;
152
+ }
153
+ ```
154
+
155
+ On deserialisation, reconstruct the Map from the array:
156
+
157
+ ```typescript
158
+ function deserializeEntity(e: any): Entity {
159
+ const entity = { ...e } as Entity;
160
+ if (Array.isArray(e.foodInventory)) {
161
+ entity.foodInventory = new Map(e.foodInventory);
162
+ }
163
+ // …
164
+ return entity;
165
+ }
166
+ ```
167
+
168
+ ### Deterministic equality
169
+
170
+ After deserialisation, the simulation can be continued from the saved state and will produce **identical results** to the original run, provided the same seed and commands are used. This is a direct consequence of the kernel’s pure‑deterministic design.
171
+
172
+ > **Gotcha:** The `__sensoryEnv` and `activeFieldEffects` side‑channel fields are not required for basic combat simulation; they can be omitted during serialisation if not needed.
173
+
174
+ ---
175
+
176
+ ## 5. Connecting to a Renderer (Bridge API)
177
+
178
+ Milestone 3 delivers a complete bridge module (`src/bridge/`) that handles tick‑rate conversion, segment‑to‑bone mapping, and deterministic interpolation between simulation ticks. The bridge is a double‑buffered engine that ingests simulation snapshots at 20 Hz and provides smooth interpolated state at render frequency (60 Hz or higher).
179
+
180
+ ### Key features
181
+
182
+ - **Mapping system** – connect simulation segment IDs (`"leftArm"`, `"torso"`) to your skeleton’s bone names (`"arm_L"`, `"spine_02"`).
183
+ - **Fixed‑point interpolation** – deterministic linear interpolation of positions, velocities, animation weights, pose modifiers, and condition.
184
+ - **Extrapolation control** – optional velocity‑based prediction when render time runs ahead of simulation.
185
+ - **Full API documentation** – see [`bridge‑api.md`](./bridge‑api.md) for detailed reference and examples.
186
+
187
+ ### Minimal setup example
188
+
189
+ ```typescript
190
+ import { BridgeEngine } from "ananke";
191
+ import { extractRigSnapshots, extractMotionVectors, extractConditionSamples } from "ananke";
192
+
193
+ const config = {
194
+ mappings: [{
195
+ bodyPlanId: "humanoid",
196
+ segments: [
197
+ { segmentId: "head", boneName: "head" },
198
+ { segmentId: "torso", boneName: "spine_02" },
199
+ // … map all segments your skeleton uses
200
+ ],
201
+ }],
202
+ };
203
+ const engine = new BridgeEngine(config);
204
+
205
+ // Simulation thread (20 Hz)
206
+ const snapshots = extractRigSnapshots(world);
207
+ const motion = extractMotionVectors(world);
208
+ const condition = extractConditionSamples(world);
209
+ engine.update(snapshots, motion, condition);
210
+
211
+ // Render thread (60 Hz)
212
+ const state = engine.getInterpolatedState(entityId, renderTime_s);
213
+ if (state) {
214
+ // Apply state.position_m, state.facing, state.poseModifiers, etc.
215
+ }
216
+ ```
217
+
218
+ ### Working demo
219
+
220
+ Run `npm run run:bridge‑demo` to see a complete bridge workflow with humanoid and quadruped body plans, simulation loop, render‑loop simulation, and determinism verification.
221
+
222
+ ### Integration steps
223
+
224
+ 1. Read the [bridge API documentation](./bridge‑api.md) to understand mapping and interpolation details.
225
+ 2. Author mappings for each body plan your game uses (humanoid, quadruped, avian, etc.).
226
+ 3. Integrate the bridge into your simulation and render threads as shown above.
227
+ 4. Use the `poseModifiers` array to drive vertex‑shader weights or morph targets for injury visualisation.
228
+ 5. Use `animation` hints (`idle`, `walk`, `run`, `sprint`) to blend animation clips.
229
+
230
+ ---
231
+
232
+ ## 6. Type Glossary
233
+
234
+ | Type | Purpose | Module |
235
+ |:---|:---|:---|
236
+ | `Q` | Fixed‑point scale factor (default `SCALE.Q = 10 000`). All dimensionless multipliers are stored as integers where `q(1.0) = 10 000`. | `src/units.ts` |
237
+ | `Vec3` | Three‑dimensional vector with components in `SCALE.m` (position) or `SCALE.mps` (velocity). | `src/sim/vec3.ts` |
238
+ | `IndividualAttributes` | Physical and cognitive capabilities of an entity (peak force, power, reaction time, etc.). | `src/generate.ts` |
239
+ | `IntentState` | What the entity intends to do this tick (move direction/pace, defence mode, prone flag). Derived from previous tick’s commands. | `src/sim/intent.ts` |
240
+ | `ActionState` | Cooldowns, weapon‑bind state, swing momentum, etc. | `src/sim/action.ts` |
241
+ | `ConditionState` | Fear, morale, sensory modifiers, fatigue, thermal state, etc. | `src/sim/condition.ts` |
242
+ | `InjuryState` | Per‑region damage (surface, internal, structural, permanent), shock, consciousness, fluid loss, death flag. | `src/sim/injury.ts` |
243
+ | `GrappleState` | Active grapple relationships, grip strength, positional lock. | `src/sim/entity.ts` |
244
+ | `Command` | Instruction issued by the host (attack, defend, move, use item, etc.). | `src/sim/commands.ts` |
245
+ | `KernelContext` | Environmental coefficients (traction, weather, etc.) passed to `stepWorld`. | `src/sim/context.ts` |
246
+ | `CollectingTrace` | Sink that records all kernel events for debugging. | `src/metrics.ts` |
247
+
248
+ ---
249
+
250
+ ## 7. Integration Gotchas
251
+
252
+ ### Exact optional property types
253
+
254
+ TypeScript’s `exactOptionalPropertyTypes` is enabled in the project. This means an optional property set to `undefined` is **not** the same as omitting the property. For example:
255
+
256
+ ```typescript
257
+ // ❌ Wrong – will cause type errors
258
+ entity.cognition = undefined;
259
+
260
+ // ✅ Correct – use conditional spread
261
+ const updated = {
262
+ ...entity,
263
+ ...(entity.cognition ? { cognition: { ... } } : {})
264
+ };
265
+ ```
266
+
267
+ This pattern appears throughout the codebase (e.g., `applyAgingToAttributes`, `applySleepToAttributes`).
268
+
269
+ ### Map fields are optional
270
+
271
+ The `foodInventory`, `armourState`, and `reputations` fields are optional `Map`s. Always check `instanceof Map` before using them, and be prepared for them to be missing.
272
+
273
+ ### Fixed‑point arithmetic
274
+
275
+ All dimensionless multipliers are stored as Q‑scaled integers. Use the helpers in `src/units.ts`:
276
+
277
+ - `q(v: number): Q` — convert a decimal to fixed‑point.
278
+ - `to(v: Q): number` — convert fixed‑point back to decimal.
279
+ - `qMul(a: Q, b: Q): Q` — multiply two Q values (result stays in Q scale).
280
+ - `clampQ(v: Q, min?: Q, max?: Q): Q` — clamp a Q value.
281
+
282
+ Never use floating‑point multiplication on raw Q values; the scaling will be wrong.
283
+
284
+ ### Deterministic RNG
285
+
286
+ Randomness is derived from `eventSeed(worldSeed, tick, idA, idB, salt)`, which returns a 32‑bit integer. The kernel uses `makeRng(seed)` to create a deterministic PRNG for that specific event. **Do not** replace `eventSeed` with `Math.random()`.
287
+
288
+ ### Tick‑rate mismatch
289
+
290
+ The simulation runs at `TICK_HZ` (20 Hz). The host renderer typically runs at 60 Hz or higher. Interpolate entity positions and animation blends between simulation ticks; extrapolation can cause temporal artefacts if the simulation stalls.
291
+
292
+ ### Body‑plan segmentation
293
+
294
+ When mapping injury regions to a 3D skeleton, note that region IDs are **camelCase** (e.g., `"leftArm"`, `"rightLeg"`), not snake_case. The `model3d.ts` module provides canonical offsets for common segment names.
295
+
296
+ ---
297
+
298
+ ## 8. Recommended Integration Steps
299
+
300
+ 1. **Start with the vertical slice** (`npm run run:vertical-slice`) to see a complete 1v1 duel.
301
+ 2. **Trace a single attack** (`npm run run:trace-attack`) to internalise the data flow.
302
+ 3. **Build an observer** that logs the state of your own entities each tick (copy `observer.ts`).
303
+ 4. **Implement save/load** using the serialisation pattern (`serialize.ts`).
304
+ 5. **Connect the 3D rig** using the bridge API (`npm run run:bridge‑demo`). See [Bridge API documentation](./bridge‑api.md).
305
+ 6. **Profile performance** with many entities (100+) to ensure your bridge does not become a bottleneck.
306
+
307
+ ---
308
+
309
+ ## 9. Conclusion
310
+
311
+ The evaluation spike confirms that Ananke’s deterministic, physics‑first simulation is **technically integrable** into a host application. The kernel’s data flow is transparent, state observation is straightforward, and serialisation round‑trips work as expected. The main challenges are the **fixed‑point arithmetic** and **exact optional property types**, which require disciplined coding patterns.
312
+
313
+ With this primer, a team can proceed to **Milestone 3 (Asset Pipeline & Renderer Bridge)** with a solid understanding of the kernel’s internals and the gotchas to avoid.
314
+
315
+ *Generated by Claude Code during Integration Milestone 2, March 2026.*