@its-not-rocket-science/ananke 0.1.69 → 0.5.4

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.
Files changed (165) hide show
  1. package/CHANGELOG.md +288 -0
  2. package/README.md +150 -432
  3. package/STABLE_API.md +113 -490
  4. package/conformance/README.md +46 -44
  5. package/conformance/lockstep-sequence.json +395 -395
  6. package/conformance/phase-order.json +21 -0
  7. package/conformance/replay-parity.json +65 -65
  8. package/conformance/state-hash-regression.json +39 -0
  9. package/conformance/state-hash.json +23 -22
  10. package/dist/src/conformance.d.ts +8 -2
  11. package/dist/src/content/index.d.ts +3 -0
  12. package/dist/src/content/index.js +2 -0
  13. package/dist/src/content/injector.d.ts +19 -0
  14. package/dist/src/content/injector.js +61 -0
  15. package/dist/src/content/loader.d.ts +3 -0
  16. package/dist/src/content/loader.js +170 -0
  17. package/dist/src/content/types.d.ts +56 -0
  18. package/dist/src/content/types.js +1 -0
  19. package/dist/src/content/validator.d.ts +6 -0
  20. package/dist/src/content/validator.js +14 -0
  21. package/dist/src/content-pack.d.ts +3 -2
  22. package/dist/src/content-pack.js +205 -10
  23. package/dist/src/data-governance.d.ts +73 -0
  24. package/dist/src/data-governance.js +188 -0
  25. package/dist/src/debug.d.ts +6 -0
  26. package/dist/src/debug.js +9 -0
  27. package/dist/src/determinism.d.ts +12 -0
  28. package/dist/src/determinism.js +93 -0
  29. package/dist/src/dialogue.d.ts +41 -0
  30. package/dist/src/dialogue.js +113 -2
  31. package/dist/src/economy.d.ts +97 -3
  32. package/dist/src/economy.js +191 -37
  33. package/dist/src/history/autosave.d.ts +19 -0
  34. package/dist/src/history/autosave.js +98 -0
  35. package/dist/src/history/timetravel.d.ts +28 -0
  36. package/dist/src/history/timetravel.js +61 -0
  37. package/dist/src/index.d.ts +19 -31
  38. package/dist/src/index.js +20 -38
  39. package/dist/src/model3d.js +63 -2
  40. package/dist/src/narrative/combat-logger.d.ts +25 -0
  41. package/dist/src/narrative/combat-logger.js +97 -0
  42. package/dist/src/narrative/plausibility.d.ts +30 -0
  43. package/dist/src/narrative/plausibility.js +70 -0
  44. package/dist/src/narrative-layer.d.ts +2 -0
  45. package/dist/src/narrative-layer.js +2 -0
  46. package/dist/src/navigation/causal-chain.d.ts +26 -0
  47. package/dist/src/navigation/causal-chain.js +74 -0
  48. package/dist/src/netcode.d.ts +1 -1
  49. package/dist/src/netcode.js +1 -1
  50. package/dist/src/performance/adaptive-tick.d.ts +23 -0
  51. package/dist/src/performance/adaptive-tick.js +50 -0
  52. package/dist/src/plugins/loader.d.ts +23 -0
  53. package/dist/src/plugins/loader.js +86 -0
  54. package/dist/src/plugins/registry.d.ts +11 -0
  55. package/dist/src/plugins/registry.js +28 -0
  56. package/dist/src/plugins/types.d.ts +31 -0
  57. package/dist/src/plugins/types.js +7 -0
  58. package/dist/src/replay.js +5 -2
  59. package/dist/src/scenario.js +2 -1
  60. package/dist/src/serialization/binary.d.ts +5 -0
  61. package/dist/src/serialization/binary.js +193 -0
  62. package/dist/src/session.d.ts +115 -0
  63. package/dist/src/session.js +228 -0
  64. package/dist/src/sim/ai/decide.js +1 -1
  65. package/dist/src/sim/ai/personality.js +2 -2
  66. package/dist/src/sim/bodyplan.d.ts +30 -0
  67. package/dist/src/sim/context.d.ts +2 -0
  68. package/dist/src/sim/kernel.js +80 -1294
  69. package/dist/src/sim/normalization.d.ts +12 -0
  70. package/dist/src/sim/normalization.js +92 -0
  71. package/dist/src/sim/resolvers/attack-resolver.d.ts +30 -0
  72. package/dist/src/sim/resolvers/attack-resolver.js +310 -0
  73. package/dist/src/sim/resolvers/capability-resolver.d.ts +15 -0
  74. package/dist/src/sim/resolvers/capability-resolver.js +270 -0
  75. package/dist/src/sim/resolvers/grapple-resolver.d.ts +28 -0
  76. package/dist/src/sim/resolvers/grapple-resolver.js +70 -0
  77. package/dist/src/sim/resolvers/shoot-resolver.d.ts +27 -0
  78. package/dist/src/sim/resolvers/shoot-resolver.js +212 -0
  79. package/dist/src/sim/resolvers/treat-resolver.d.ts +16 -0
  80. package/dist/src/sim/resolvers/treat-resolver.js +77 -0
  81. package/dist/src/sim/step/apply/intents.d.ts +6 -0
  82. package/dist/src/sim/step/apply/intents.js +83 -0
  83. package/dist/src/sim/step/kernel-pipeline.d.ts +9 -0
  84. package/dist/src/sim/step/kernel-pipeline.js +15 -0
  85. package/dist/src/sim/step/phases/capability-phase.d.ts +8 -0
  86. package/dist/src/sim/step/phases/capability-phase.js +65 -0
  87. package/dist/src/sim/step/phases/cooldowns-phase.d.ts +3 -0
  88. package/dist/src/sim/step/phases/cooldowns-phase.js +37 -0
  89. package/dist/src/sim/step/phases/prepare-phase.d.ts +10 -0
  90. package/dist/src/sim/step/phases/prepare-phase.js +33 -0
  91. package/dist/src/sim/step/pipeline.d.ts +2 -0
  92. package/dist/src/sim/step/pipeline.js +15 -0
  93. package/dist/src/sim/step/resolvers/impact-resolver.d.ts +9 -0
  94. package/dist/src/sim/step/resolvers/impact-resolver.js +55 -0
  95. package/dist/src/sim/step/world-phases.d.ts +7 -0
  96. package/dist/src/sim/step/world-phases.js +15 -0
  97. package/dist/src/sim/step/world-step-context.d.ts +23 -0
  98. package/dist/src/sim/step/world-step-context.js +1 -0
  99. package/dist/src/sim/systemic-toxicology.js +1 -1
  100. package/dist/src/sim/team.js +2 -2
  101. package/dist/src/sim/testing.js +4 -3
  102. package/dist/src/sim/toxicology.d.ts +1 -1
  103. package/dist/src/sim/toxicology.js +1 -1
  104. package/dist/src/sim/world.d.ts +17 -24
  105. package/dist/src/snapshot.js +3 -6
  106. package/dist/src/tier2.d.ts +19 -0
  107. package/dist/src/tier2.js +21 -0
  108. package/dist/src/tier3.d.ts +10 -0
  109. package/dist/src/tier3.js +12 -0
  110. package/dist/src/version.d.ts +6 -0
  111. package/dist/src/version.js +8 -0
  112. package/dist/src/wasm/bridge.d.ts +14 -0
  113. package/dist/src/wasm/bridge.js +84 -0
  114. package/dist/src/wasm-kernel.d.ts +2 -0
  115. package/dist/src/wasm-kernel.js +1 -0
  116. package/dist/src/world-evolution-backend/engine.d.ts +4 -0
  117. package/dist/src/world-evolution-backend/engine.js +347 -0
  118. package/dist/src/world-evolution-backend/host-schema.d.ts +145 -0
  119. package/dist/src/world-evolution-backend/host-schema.js +375 -0
  120. package/dist/src/world-evolution-backend/index.d.ts +1 -0
  121. package/dist/src/world-evolution-backend/index.js +1 -0
  122. package/dist/src/world-evolution-backend/open-world-host-adapter.d.ts +96 -0
  123. package/dist/src/world-evolution-backend/open-world-host-adapter.js +208 -0
  124. package/dist/src/world-evolution-backend/profiles.d.ts +4 -0
  125. package/dist/src/world-evolution-backend/profiles.js +136 -0
  126. package/dist/src/world-evolution-backend/public.d.ts +11 -0
  127. package/dist/src/world-evolution-backend/public.js +7 -0
  128. package/dist/src/world-evolution-backend/reproducibility.d.ts +8 -0
  129. package/dist/src/world-evolution-backend/reproducibility.js +35 -0
  130. package/dist/src/world-evolution-backend/timeline.d.ts +6 -0
  131. package/dist/src/world-evolution-backend/timeline.js +337 -0
  132. package/dist/src/world-evolution-backend/types.d.ts +138 -0
  133. package/dist/src/world-evolution-backend/types.js +1 -0
  134. package/dist/src/world-evolution-host-backend.d.ts +51 -0
  135. package/dist/src/world-evolution-host-backend.js +97 -0
  136. package/dist/src/world-evolution.d.ts +201 -0
  137. package/dist/src/world-evolution.js +543 -0
  138. package/dist/src/world-factory.js +2 -1
  139. package/dist/tools/pack-cli.js +55 -2
  140. package/docs/bridge-contract.md +140 -332
  141. package/docs/completion-evidence-dashboard.md +63 -0
  142. package/docs/host-contract.md +86 -310
  143. package/docs/host-world-evolution-schema.md +155 -0
  144. package/docs/integration-primer.md +29 -22
  145. package/docs/migration-monolith-to-modular.md +102 -120
  146. package/docs/package-architecture.md +116 -323
  147. package/docs/plugins/README.md +42 -0
  148. package/docs/plugins/hooks.md +22 -0
  149. package/docs/plugins/publishing.md +29 -0
  150. package/docs/plugins/security-model.md +273 -0
  151. package/docs/project-overview.md +30 -30
  152. package/docs/release-dashboard.md +47 -0
  153. package/docs/session-api.md +127 -0
  154. package/docs/stable-api-manifest.json +52 -0
  155. package/docs/trust-dashboard.md +37 -0
  156. package/docs/versioning.md +256 -245
  157. package/docs/wire-protocol.md +140 -209
  158. package/docs/world-evolution-backend.md +200 -0
  159. package/docs/world-evolution-engine.md +150 -0
  160. package/docs/world-evolution-host-backend.md +70 -0
  161. package/docs/world-evolution-orchestration.md +248 -0
  162. package/package.json +125 -6
  163. package/schema/content-pack.schema.json +85 -0
  164. package/schema/plugin.schema.json +57 -0
  165. package/schema/world-evolution-input.schema.json +196 -0
@@ -17,6 +17,10 @@ export const WEAR_FUMBLE_THRESHOLD = q(0.70);
17
17
  const FUMBLE_CHANCE_Q = q(0.20); // 20%
18
18
  const DEFAULT_SELL_FRACTION = 0.40; // weapons/armour
19
19
  const MEDICAL_SELL_FRACTION = 0.50;
20
+ const DEFAULT_DURABILITY_VALUE_POLICY = {
21
+ salvageFloorFraction: 0.05,
22
+ conditionExponent: 1.35,
23
+ };
20
24
  // ── computeItemValue ──────────────────────────────────────────────────────────
21
25
  /**
22
26
  * Derive economic metadata for any item or medical resource.
@@ -63,19 +67,42 @@ export function computeItemValue(item, wear_Q = q(0)) {
63
67
  return { itemId: it.id, baseValue: Math.floor(it.mass_kg / SCALE.kg * 8), condition_Q: q(1.0), sellFraction: DEFAULT_SELL_FRACTION };
64
68
  }
65
69
  /**
66
- * Convert armour degradation state to a wear_Q fraction.
70
+ * Convert armour state to condition fraction.
67
71
  *
68
72
  * ```
69
- * wear = q(1.0) − resistRemaining_J / resist_J
73
+ * condition = resistRemaining_J / resist_J
70
74
  * ```
71
- * Used to derive `condition_Q` for ablative armour via `computeItemValue`.
75
+ *
76
+ * The result can be converted to wear via `conditionToWearQ`.
72
77
  */
73
78
  export function armourConditionQ(resist_J, resistRemaining_J) {
74
79
  if (resist_J <= 0)
75
- return q(1.0);
80
+ return q(0);
76
81
  const fraction = Math.trunc((resistRemaining_J * SCALE.Q) / resist_J);
77
82
  return clampQ(fraction, 0, SCALE.Q);
78
83
  }
84
+ /** Convert condition to wear (`wear = 1 - condition`) in fixed-point space. */
85
+ export function conditionToWearQ(condition_Q) {
86
+ return clampQ(SCALE.Q - condition_Q, 0, SCALE.Q);
87
+ }
88
+ /**
89
+ * Convert an ItemValue into market-usable unit values.
90
+ *
91
+ * This keeps `computeItemValue` stable and lets hosts pick stricter/looser
92
+ * durability discounting.
93
+ */
94
+ export function toMarketValue(itemValue, policy = DEFAULT_DURABILITY_VALUE_POLICY) {
95
+ const configured = {
96
+ salvageFloorFraction: policy.salvageFloorFraction ?? DEFAULT_DURABILITY_VALUE_POLICY.salvageFloorFraction,
97
+ conditionExponent: policy.conditionExponent ?? DEFAULT_DURABILITY_VALUE_POLICY.conditionExponent,
98
+ };
99
+ const condition = Math.max(0, Math.min(1, itemValue.condition_Q / SCALE.Q));
100
+ const curved = Math.pow(condition, Math.max(0.01, configured.conditionExponent));
101
+ const multiplier = configured.salvageFloorFraction + (1 - configured.salvageFloorFraction) * curved;
102
+ const buyValue = Math.max(0, Math.round(itemValue.baseValue * multiplier));
103
+ const sellValue = Math.max(0, Math.round(buyValue * itemValue.sellFraction));
104
+ return { buyValue, sellValue, conditionMultiplier: multiplier };
105
+ }
79
106
  // ── applyWear ─────────────────────────────────────────────────────────────────
80
107
  /**
81
108
  * Apply one strike's worth of use-wear to a melee weapon.
@@ -104,42 +131,65 @@ export function applyWear(weapon, actionIntensity_Q, seed) {
104
131
  return { wear_Q: newWear, broke, fumble, penaltyActive };
105
132
  }
106
133
  // ── resolveDrops ──────────────────────────────────────────────────────────────
134
+ function shouldResolveDrops(entity, config) {
135
+ if (entity.injury.dead)
136
+ return { shouldDrop: true, source: "dead" };
137
+ if (config?.dropOnIncapacitated ?? false)
138
+ return { shouldDrop: true, source: "incapacitated" };
139
+ return { shouldDrop: false, source: "none" };
140
+ }
107
141
  /**
108
- * Compute the list of item IDs dropped by an entity on death or incapacitation.
109
- *
110
- * Default behaviour:
111
- * - Dead entity → all equipped weapons, ranged weapons, and armour drop (guaranteed).
112
- * - Incapacitated but living → nothing drops (use `config.dropOnIncapacitated = true` to override).
113
- *
114
- * Additional items from `extra.guaranteed` always drop (when applicable).
115
- * `extra.probabilistic` items are rolled deterministically from `seed`.
142
+ * Structured drop resolution for host-facing UI and telemetry.
116
143
  */
117
- export function resolveDrops(entity, seed, extra, config) {
118
- const shouldDrop = entity.injury.dead || (config?.dropOnIncapacitated ?? false);
119
- if (!shouldDrop)
120
- return [];
121
- const result = [];
122
- // Guaranteed: all equipped weapons and armour
144
+ export function resolveDropsDetailed(entity, seed, extra, config) {
145
+ const dropState = shouldResolveDrops(entity, config);
146
+ if (!dropState.shouldDrop) {
147
+ return {
148
+ dropped: [],
149
+ guaranteed: [],
150
+ probabilistic: [],
151
+ preventedByState: true,
152
+ source: dropState.source,
153
+ };
154
+ }
155
+ const guaranteed = [];
156
+ const probabilistic = [];
123
157
  for (const item of entity.loadout.items) {
124
158
  if (item.kind === "weapon" || item.kind === "armour" || item.kind === "ranged") {
125
- result.push(item.id);
159
+ guaranteed.push(item.id);
126
160
  }
127
161
  }
128
162
  if (extra) {
129
- for (const id of extra.guaranteed) {
130
- result.push(id);
131
- }
132
- // Probabilistic roll — deterministic per item index
163
+ guaranteed.push(...extra.guaranteed);
133
164
  for (let i = 0; i < extra.probabilistic.length; i++) {
134
165
  const entry = extra.probabilistic[i];
135
166
  const rollSeed = eventSeed(seed, i, 0, 0, 0xD50A5);
136
167
  const rng = makeRng(rollSeed, SCALE.Q);
137
168
  if (rng.q01() < entry.chance_Q) {
138
- result.push(entry.itemId);
169
+ probabilistic.push(entry.itemId);
139
170
  }
140
171
  }
141
172
  }
142
- return result;
173
+ return {
174
+ dropped: [...guaranteed, ...probabilistic],
175
+ guaranteed,
176
+ probabilistic,
177
+ preventedByState: false,
178
+ source: dropState.source,
179
+ };
180
+ }
181
+ /**
182
+ * Compute the list of item IDs dropped by an entity on death or incapacitation.
183
+ *
184
+ * Default behaviour:
185
+ * - Dead entity → all equipped weapons, ranged weapons, and armour drop (guaranteed).
186
+ * - Incapacitated but living → nothing drops (use `config.dropOnIncapacitated = true` to override).
187
+ *
188
+ * Additional items from `extra.guaranteed` always drop (when applicable).
189
+ * `extra.probabilistic` items are rolled deterministically from `seed`.
190
+ */
191
+ export function resolveDrops(entity, seed, extra, config) {
192
+ return resolveDropsDetailed(entity, seed, extra, config).dropped;
143
193
  }
144
194
  // ── evaluateTradeOffer ────────────────────────────────────────────────────────
145
195
  /**
@@ -153,22 +203,119 @@ export function resolveDrops(entity, seed, extra, config) {
153
203
  * `feasible` — the accepting party has all `want` items in sufficient quantities.
154
204
  */
155
205
  export function evaluateTradeOffer(offer, inventory) {
156
- // Feasibility: does the accepting party have all wanted items?
157
- let feasible = true;
206
+ const detailed = evaluateTradeOfferDetailed(offer, inventory);
207
+ return { netValue: detailed.netValue, feasible: detailed.feasible };
208
+ }
209
+ /**
210
+ * Inventory-aware trade evaluation that surfaces shortages and value breakdown.
211
+ */
212
+ export function evaluateTradeOfferDetailed(offer, inventory) {
213
+ const snapshots = new Map();
214
+ const giveValue = offer.give.reduce((sum, g) => sum + g.count * g.unitValue, 0);
215
+ const wantValue = offer.want.reduce((sum, w) => sum + w.count * w.unitValue, 0);
216
+ const shortages = [];
158
217
  for (const w of offer.want) {
159
218
  const entry = inventory.get(w.itemId);
160
- if (!entry || entry.count < w.count) {
161
- feasible = false;
162
- break;
219
+ const owned = entry?.count ?? 0;
220
+ const shortage = Math.max(0, w.count - owned);
221
+ if (shortage > 0)
222
+ shortages.push({ itemId: w.itemId, missingCount: shortage });
223
+ snapshots.set(w.itemId, {
224
+ ownedCount: owned,
225
+ shortageCount: shortage,
226
+ entryUnitValue: entry?.unitValue ?? 0,
227
+ offerUnitValue: w.unitValue,
228
+ });
229
+ }
230
+ for (const g of offer.give) {
231
+ if (!snapshots.has(g.itemId)) {
232
+ snapshots.set(g.itemId, {
233
+ ownedCount: inventory.get(g.itemId)?.count ?? 0,
234
+ shortageCount: 0,
235
+ entryUnitValue: inventory.get(g.itemId)?.unitValue ?? 0,
236
+ offerUnitValue: g.unitValue,
237
+ });
238
+ }
239
+ }
240
+ const feasible = shortages.length === 0;
241
+ const netValue = giveValue - wantValue;
242
+ return { netValue, feasible, giveValue, wantValue, shortages, snapshots };
243
+ }
244
+ // ── host reporting helpers ────────────────────────────────────────────────────
245
+ /** Merge simple itemized counts into an ItemInventory map. */
246
+ export function mergeIntoInventory(inventory, items, resolver) {
247
+ for (const item of items) {
248
+ if (item.count <= 0)
249
+ continue;
250
+ const existing = inventory.get(item.itemId);
251
+ if (existing) {
252
+ existing.count += item.count;
253
+ continue;
163
254
  }
255
+ inventory.set(item.itemId, { count: item.count, unitValue: resolver(item.itemId) });
164
256
  }
165
- // Net value for accepting party: receive give items, pay want items
166
- let netValue = 0;
167
- for (const g of offer.give)
168
- netValue += g.count * g.unitValue;
169
- for (const w of offer.want)
170
- netValue -= w.count * w.unitValue;
171
- return { netValue, feasible };
257
+ return inventory;
258
+ }
259
+ /** Build a host-facing value report for any itemized list. */
260
+ export function createEconomyReport(label, entries, resolver, tags = []) {
261
+ const lines = entries
262
+ .filter(e => e.count > 0)
263
+ .map((entry) => {
264
+ const unitValue = Math.max(0, resolver(entry.itemId));
265
+ return {
266
+ itemId: entry.itemId,
267
+ count: entry.count,
268
+ unitValue,
269
+ totalValue: unitValue * entry.count,
270
+ };
271
+ });
272
+ const grossValue = lines.reduce((sum, line) => sum + line.totalValue, 0);
273
+ return {
274
+ label,
275
+ totals: {
276
+ grossValue,
277
+ netValue: grossValue,
278
+ lineCount: lines.length,
279
+ },
280
+ lines,
281
+ tags,
282
+ };
283
+ }
284
+ /**
285
+ * Example flow: post-battle loot screen.
286
+ */
287
+ export function examplePostBattleLootFlow(entity, seed, resolver) {
288
+ const drops = resolveDropsDetailed(entity, seed);
289
+ const counts = countItems(drops.dropped);
290
+ return createEconomyReport("post_battle_loot", counts, resolver, ["loot", drops.source]);
291
+ }
292
+ /**
293
+ * Example flow: repair/reuse loop that compares pre-repair vs post-repair resale.
294
+ */
295
+ export function exampleRepairReuseLoop(item, startingWear_Q, repairedWear_Q) {
296
+ const before = toMarketValue(computeItemValue(item, startingWear_Q));
297
+ const after = toMarketValue(computeItemValue(item, repairedWear_Q));
298
+ return {
299
+ label: "repair_reuse_loop",
300
+ totals: {
301
+ grossValue: after.sellValue,
302
+ netValue: after.sellValue - before.sellValue,
303
+ lineCount: 1,
304
+ },
305
+ lines: [{
306
+ itemId: item.id,
307
+ count: 1,
308
+ unitValue: after.sellValue,
309
+ totalValue: after.sellValue,
310
+ }],
311
+ tags: ["repair", "reuse", after.sellValue >= before.sellValue ? "improved" : "degraded"],
312
+ };
313
+ }
314
+ /**
315
+ * Example flow: settlement/shop interaction, including affordability and shortages.
316
+ */
317
+ export function exampleSettlementTradeFlow(offer, inventory) {
318
+ return evaluateTradeOfferDetailed(offer, inventory);
172
319
  }
173
320
  // ── totalInventoryValue ───────────────────────────────────────────────────────
174
321
  /**
@@ -180,3 +327,10 @@ export function totalInventoryValue(inventory) {
180
327
  total += entry.count * entry.unitValue;
181
328
  return total;
182
329
  }
330
+ function countItems(ids) {
331
+ const counts = new Map();
332
+ for (const id of ids) {
333
+ counts.set(id, (counts.get(id) ?? 0) + 1);
334
+ }
335
+ return [...counts.entries()].map(([itemId, count]) => ({ itemId, count }));
336
+ }
@@ -0,0 +1,19 @@
1
+ import type { WorldState } from "../sim/world.js";
2
+ export interface AutosaveConfig {
3
+ everyNTicks?: number;
4
+ onEntityDeath?: boolean;
5
+ autosaveKey?: string;
6
+ }
7
+ export interface AutosaveStorage {
8
+ save(key: string, bytes: Uint8Array): Promise<void>;
9
+ load(key: string): Promise<Uint8Array | undefined>;
10
+ }
11
+ export declare class AutosaveManager {
12
+ private readonly cfg;
13
+ private readonly storage;
14
+ constructor(storage?: AutosaveStorage, cfg?: AutosaveConfig);
15
+ maybeAutosave(world: WorldState, previous?: WorldState): Promise<void>;
16
+ saveNow(world: WorldState): Promise<void>;
17
+ recover(): Promise<WorldState | undefined>;
18
+ }
19
+ export declare function defaultAutosaveStorage(): AutosaveStorage;
@@ -0,0 +1,98 @@
1
+ import { exportWorldState, importWorldState } from "../serialization/binary.js";
2
+ export class AutosaveManager {
3
+ cfg;
4
+ storage;
5
+ constructor(storage, cfg = {}) {
6
+ this.storage = storage ?? defaultAutosaveStorage();
7
+ this.cfg = {
8
+ everyNTicks: cfg.everyNTicks ?? 100,
9
+ onEntityDeath: cfg.onEntityDeath ?? true,
10
+ autosaveKey: cfg.autosaveKey ?? "ananke:last-autosave",
11
+ };
12
+ }
13
+ async maybeAutosave(world, previous) {
14
+ const tickPolicy = this.cfg.everyNTicks > 0 && world.tick % this.cfg.everyNTicks === 0;
15
+ const deathPolicy = this.cfg.onEntityDeath && previous ? hasDeath(previous, world) : false;
16
+ if (tickPolicy || deathPolicy) {
17
+ await this.saveNow(world);
18
+ }
19
+ }
20
+ async saveNow(world) {
21
+ await this.storage.save(this.cfg.autosaveKey, exportWorldState(world));
22
+ }
23
+ async recover() {
24
+ const bytes = await this.storage.load(this.cfg.autosaveKey);
25
+ if (!bytes)
26
+ return undefined;
27
+ return importWorldState(bytes);
28
+ }
29
+ }
30
+ export function defaultAutosaveStorage() {
31
+ if (typeof window === "undefined")
32
+ return new NodeAutosaveStorage();
33
+ return new IndexedDbAutosaveStorage();
34
+ }
35
+ class NodeAutosaveStorage {
36
+ async save(key, bytes) {
37
+ const fs = await import("node:fs/promises");
38
+ const path = await import("node:path");
39
+ const os = await import("node:os");
40
+ const dir = path.join(os.homedir(), ".ananke", "autosave");
41
+ await fs.mkdir(dir, { recursive: true });
42
+ await fs.writeFile(path.join(dir, sanitizeKey(key) + ".bin"), bytes);
43
+ }
44
+ async load(key) {
45
+ const fs = await import("node:fs/promises");
46
+ const path = await import("node:path");
47
+ const os = await import("node:os");
48
+ try {
49
+ const bytes = await fs.readFile(path.join(os.homedir(), ".ananke", "autosave", sanitizeKey(key) + ".bin"));
50
+ return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
51
+ }
52
+ catch {
53
+ return undefined;
54
+ }
55
+ }
56
+ }
57
+ class IndexedDbAutosaveStorage {
58
+ dbName = "ananke-autosave";
59
+ storeName = "snapshots";
60
+ async save(key, bytes) {
61
+ const db = await this.open();
62
+ await new Promise((resolve, reject) => {
63
+ const tx = db.transaction(this.storeName, "readwrite");
64
+ tx.objectStore(this.storeName).put(bytes, key);
65
+ tx.oncomplete = () => resolve();
66
+ tx.onerror = () => reject(tx.error);
67
+ });
68
+ }
69
+ async load(key) {
70
+ const db = await this.open();
71
+ return new Promise((resolve, reject) => {
72
+ const tx = db.transaction(this.storeName, "readonly");
73
+ const req = tx.objectStore(this.storeName).get(key);
74
+ req.onsuccess = () => resolve(req.result ? new Uint8Array(req.result) : undefined);
75
+ req.onerror = () => reject(req.error);
76
+ });
77
+ }
78
+ async open() {
79
+ return new Promise((resolve, reject) => {
80
+ const req = indexedDB.open(this.dbName, 1);
81
+ req.onupgradeneeded = () => {
82
+ const db = req.result;
83
+ if (!db.objectStoreNames.contains(this.storeName))
84
+ db.createObjectStore(this.storeName);
85
+ };
86
+ req.onsuccess = () => resolve(req.result);
87
+ req.onerror = () => reject(req.error);
88
+ });
89
+ }
90
+ }
91
+ function hasDeath(previous, next) {
92
+ const prevAlive = previous.entities.filter(e => !e.injury.dead).length;
93
+ const nextAlive = next.entities.filter(e => !e.injury.dead).length;
94
+ return nextAlive < prevAlive;
95
+ }
96
+ function sanitizeKey(input) {
97
+ return input.replace(/[^a-z0-9_.-]/gi, "_");
98
+ }
@@ -0,0 +1,28 @@
1
+ import type { WorldState } from "../sim/world.js";
2
+ import type { Command, CommandMap } from "../sim/commands.js";
3
+ import type { KernelContext } from "../sim/context.js";
4
+ export interface TimeTravelOptions {
5
+ bufferSizeTicks?: number;
6
+ }
7
+ export interface TickRecord {
8
+ tick: number;
9
+ state: WorldState;
10
+ commands: Array<readonly [entityId: number, commands: readonly Command[]]>;
11
+ }
12
+ export interface TimeTravelWorld extends WorldState {
13
+ rewind(ticks: number): void;
14
+ fork(): WorldState;
15
+ }
16
+ export declare class TimeTravelController {
17
+ private readonly world;
18
+ private readonly ctx;
19
+ private readonly records;
20
+ private readonly bufferSizeTicks;
21
+ constructor(world: WorldState, ctx: KernelContext, opts?: TimeTravelOptions);
22
+ recordTick(commands: CommandMap): void;
23
+ step(commands: CommandMap): void;
24
+ rewind(ticks: number): void;
25
+ fork(): WorldState;
26
+ getHistory(): readonly TickRecord[];
27
+ }
28
+ export declare function enableTimeTravel(world: WorldState, ctx: KernelContext, opts?: TimeTravelOptions): TimeTravelController;
@@ -0,0 +1,61 @@
1
+ import { stepWorld } from "../sim/kernel.js";
2
+ export class TimeTravelController {
3
+ world;
4
+ ctx;
5
+ records = [];
6
+ bufferSizeTicks;
7
+ constructor(world, ctx, opts = {}) {
8
+ this.world = world;
9
+ this.ctx = ctx;
10
+ const configured = opts.bufferSizeTicks ?? 600;
11
+ this.bufferSizeTicks = Math.max(60, Math.min(3600, configured));
12
+ this.records.push({ tick: world.tick, state: structuredClone(world), commands: [] });
13
+ attachMethods(world, this);
14
+ }
15
+ recordTick(commands) {
16
+ const record = {
17
+ tick: this.world.tick,
18
+ state: structuredClone(this.world),
19
+ commands: [...commands.entries()].map(([id, cmds]) => [id, [...cmds]]),
20
+ };
21
+ this.records.push(record);
22
+ while (this.records.length > this.bufferSizeTicks)
23
+ this.records.shift();
24
+ }
25
+ step(commands) {
26
+ this.recordTick(commands);
27
+ stepWorld(this.world, commands, this.ctx);
28
+ }
29
+ rewind(ticks) {
30
+ if (ticks <= 0)
31
+ return;
32
+ const targetTick = this.world.tick - ticks;
33
+ const candidate = [...this.records].reverse().find(r => r.tick <= targetTick);
34
+ if (!candidate) {
35
+ throw new Error(`timetravel: cannot rewind ${ticks} ticks with buffer size ${this.bufferSizeTicks}`);
36
+ }
37
+ mutateWorld(this.world, structuredClone(candidate.state));
38
+ }
39
+ fork() {
40
+ return structuredClone(this.world);
41
+ }
42
+ getHistory() {
43
+ return this.records;
44
+ }
45
+ }
46
+ export function enableTimeTravel(world, ctx, opts = {}) {
47
+ return new TimeTravelController(world, ctx, opts);
48
+ }
49
+ function attachMethods(world, controller) {
50
+ const mutable = world;
51
+ mutable.rewind = (ticks) => controller.rewind(ticks);
52
+ mutable.fork = () => controller.fork();
53
+ }
54
+ function mutateWorld(target, source) {
55
+ for (const key of Object.keys(target)) {
56
+ if (!(key in source)) {
57
+ delete target[key];
58
+ }
59
+ }
60
+ Object.assign(target, source);
61
+ }
@@ -1,31 +1,19 @@
1
- export * from "./units.js";
2
- export * from "./types.js";
3
- export * from "./archetypes.js";
4
- export * from "./generate.js";
5
- export * from "./equipment.js";
6
- export * from "./weapons.js";
7
- export * from "./presets.js";
8
- export * from "./channels.js";
9
- export * from "./traits.js";
10
- export * from "./describe.js";
11
- export * from "./sim/vec3.js";
12
- export * from "./sim/condition.js";
13
- export * from "./sim/injury.js";
14
- export * from "./sim/entity.js";
15
- export * from "./sim/commands.js";
16
- export * from "./sim/kinds.js";
17
- export * from "./sim/kernel.js";
18
- export * from "./sim/body.js";
19
- export * from "./sim/world.js";
20
- export * from "./model3d.js";
21
- export * from "./replay.js";
22
- export * from "./bridge/index.js";
23
- export * from "./world-factory.js";
24
- export * from "./scenario.js";
25
- export * from "./catalog.js";
26
- export * from "./sim/formation-combat.js";
27
- export * from "./sim/cover.js";
28
- export * from "./sim/ai/behavior-trees.js";
29
- export * from "./snapshot.js";
30
- export * from "./parallel.js";
31
- export * from "./modding.js";
1
+ export { SCALE, G_mps2, q, clampQ, qMul, qDiv, mulDiv, to, from, sqrtQ, } from "./units.js";
2
+ export type { I32, Q } from "./units.js";
3
+ export type { IndividualAttributes } from "./types.js";
4
+ export type { Entity } from "./sim/entity.js";
5
+ export type { WorldState } from "./sim/world.js";
6
+ export type { KernelContext } from "./sim/context.js";
7
+ export type { Command, CommandMap } from "./sim/commands.js";
8
+ export { createWorld } from "./world-factory.js";
9
+ export type { EntitySpec } from "./world-factory.js";
10
+ export { loadScenario, validateScenario } from "./scenario.js";
11
+ export type { AnankeScenario, AnankeScenarioEntity } from "./scenario.js";
12
+ export { stepWorld } from "./sim/kernel.js";
13
+ export { ReplayRecorder, replayTo, serializeReplay, deserializeReplay } from "./replay.js";
14
+ export type { Replay, ReplayFrame } from "./replay.js";
15
+ export { extractRigSnapshots, deriveAnimationHints } from "./model3d.js";
16
+ export type { RigSnapshot, AnimationHints } from "./model3d.js";
17
+ export { loadPlugin } from "./plugins/loader.js";
18
+ export { installPluginFromRegistry } from "./plugins/registry.js";
19
+ export type { PluginManifest, PluginPermission, PluginHookContext, PluginHooks, PluginRuntimeApi, PluginModule, LoadedPlugin, } from "./plugins/types.js";
package/dist/src/index.js CHANGED
@@ -1,40 +1,22 @@
1
- // ── Tier 1 — Stable host API ─────────────────────────────────────────────────
2
- // This is the only import path companion projects and hosts should use:
3
- // import { stepWorld, createWorld, q, SCALE } from "@its-not-rocket-science/ananke"
1
+ // ── Tier 1 — Stable host API (minimal contract) ─────────────────────────────
2
+ // This is the only import path hosts should treat as semver-stable:
3
+ // import { createWorld, stepWorld, q, SCALE } from "@its-not-rocket-science/ananke"
4
4
  //
5
- // Breaking changes to any export here require a major semver bump (x.0.0) and
6
- // a migration guide in CHANGELOG.md. See STABLE_API.md for the full contract.
5
+ // Everything not exported here is intentionally Tier 2/3 and must be imported
6
+ // via explicit subpaths (e.g. "/character", "/combat", "/tier2", "/tier3").
7
7
  //
8
- // Tier 2 (experimental) and Tier 3 (internal) exports are accessible via direct
9
- // module imports, e.g. import { stepAging } from ".../dist/src/sim/aging.js"
10
- export * from "./units.js"; // q(), SCALE, qMul, qDiv, clampQ, mulDiv, to, from, sqrtQ
11
- export * from "./types.js"; // IndividualAttributes, core scalar types
12
- export * from "./archetypes.js"; // Archetype, BodyPlan, built-in species presets
13
- export * from "./generate.js"; // generateIndividual()
14
- export * from "./equipment.js"; // Weapon, Armour, Gear, WeaponDamageProfile, EquipmentCatalogue
15
- export * from "./weapons.js"; // ALL_HISTORICAL_MELEE, ALL_HISTORICAL_RANGED, PREHISTORIC_MELEE … CONTEMPORARY_RANGED
16
- export * from "./presets.js"; // mkKnight(), mkBoxer(), mkWrestler(), mkOctopus(), mkScubaDiver()
17
- export * from "./channels.js"; // DamageChannel, ChannelMask, channelMask()
18
- export * from "./traits.js"; // TraitId, TRAITS, buildTraitProfile(), applyTraitsToAttributes()
19
- export * from "./describe.js"; // describeCharacter(), formatCharacterSheet(), formatOneLine()
20
- export * from "./sim/vec3.js"; // Vec3, lerpVec3, addVec3
21
- export * from "./sim/condition.js"; // ConditionSnapshot, condition constants
22
- export * from "./sim/injury.js"; // InjuryRegion, BodyRegion, injury constants
23
- export * from "./sim/entity.js"; // Entity (stable fields: id, pos, mass_kg, attributes…)
24
- export * from "./sim/commands.js"; // CommandMap, Command, noMove()
25
- export * from "./sim/kinds.js"; // CommandKinds, MoveModes, DefenceModes, EngageModes, HitArea
26
- export * from "./sim/kernel.js"; // stepWorld(), applyImpactToInjury(), applyExplosion()
27
- export * from "./sim/body.js"; // BodyPlan, BodySegment, humanoid / quadruped plans
28
- export * from "./sim/world.js"; // WorldState, KernelContext
29
- export * from "./model3d.js"; // extractRigSnapshots(), deriveAnimationHints(), RigSnapshot, AnimationHints
30
- export * from "./replay.js"; // ReplayRecorder, replayTo(), serializeReplay(), deserializeReplay()
31
- export * from "./bridge/index.js"; // BridgeEngine, InterpolatedState, BridgeConfig
32
- export * from "./world-factory.js"; // createWorld(), EntitySpec, ARCHETYPE_MAP, ITEM_MAP
33
- export * from "./scenario.js"; // loadScenario(), validateScenario(), AnankeScenario
34
- export * from "./catalog.js"; // CE-12: registerArchetype(), registerWeapon(), registerArmour(), getCatalogEntry()
35
- export * from "./sim/formation-combat.js"; // Phase 69: FormationUnit, TacticalEngagement, resolveTacticalEngagement()
36
- export * from "./sim/cover.js"; // CE-15: CoverSegment, computeCoverProtection(), isLineOfSightBlocked(), applyExplosionToTerrain()
37
- export * from "./sim/ai/behavior-trees.js"; // CE-10: BehaviorNode, FlankTarget, RetreatTo, ProtectAlly, GuardPosition, HealTarget, Sequence, Fallback
38
- export * from "./snapshot.js"; // CE-9: diffWorldState(), applyDiff(), packDiff(), unpackDiff(), WorldStateDiff
39
- export * from "./parallel.js"; // CE-7: partitionWorld(), mergePartitions(), detectBoundaryPairs(), assignEntitiesToPartitions()
40
- export * from "./modding.js"; // CE-16: hashMod(), registerPostTickHook(), runPostTickHooks(), registerBehaviorNode(), computeModManifest()
8
+ // Source of truth: docs/stable-api-manifest.json
9
+ // Fixed-point utilities
10
+ export { SCALE, G_mps2, q, clampQ, qMul, qDiv, mulDiv, to, from, sqrtQ, } from "./units.js";
11
+ // World creation and scenario loading
12
+ export { createWorld } from "./world-factory.js";
13
+ export { loadScenario, validateScenario } from "./scenario.js";
14
+ // Stepping
15
+ export { stepWorld } from "./sim/kernel.js";
16
+ // Replay / serialization
17
+ export { ReplayRecorder, replayTo, serializeReplay, deserializeReplay } from "./replay.js";
18
+ // Bridge extraction
19
+ export { extractRigSnapshots, deriveAnimationHints } from "./model3d.js";
20
+ // Plugin SDK
21
+ export { loadPlugin } from "./plugins/loader.js";
22
+ export { installPluginFromRegistry } from "./plugins/registry.js";