@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.
- package/CHANGELOG.md +15 -0
- package/README.md +421 -2199
- 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
|
@@ -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.*
|