@its-not-rocket-science/ananke 0.1.5 → 0.1.7
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 +37 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +4 -0
- package/dist/src/sim/ai/behavior-trees.d.ts +166 -0
- package/dist/src/sim/ai/behavior-trees.js +340 -0
- package/dist/src/sim/cover.d.ts +186 -0
- package/dist/src/sim/cover.js +290 -0
- package/dist/src/sim/formation-combat.d.ts +170 -0
- package/dist/src/sim/formation-combat.js +255 -0
- package/dist/src/snapshot.d.ts +117 -0
- package/dist/src/snapshot.js +379 -0
- package/package.json +1 -1
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 69 — Macro-Scale Formation Combat
|
|
3
|
+
*
|
|
4
|
+
* A tactical abstraction layer between individual entity simulation (20 Hz) and
|
|
5
|
+
* polity-level conflict (1 tick/day). Squads and companies resolve combat as
|
|
6
|
+
* cohesive units via Lanchester's square law, adjusted for terrain and morale.
|
|
7
|
+
*
|
|
8
|
+
* When a named entity (id < NAMED_ENTITY_THRESHOLD, or in a caller-supplied set)
|
|
9
|
+
* participates in the engagement, the resolver marks them in `namedEntityIds` so the
|
|
10
|
+
* host can run a full per-entity micro-simulation frame at the decisive tick.
|
|
11
|
+
*
|
|
12
|
+
* Lanchester's Square Law:
|
|
13
|
+
* Attrition per tick ∝ opponent_strength² / own_strength
|
|
14
|
+
* δA = k × B² δB = k × A²
|
|
15
|
+
*
|
|
16
|
+
* where k is derived from aggregated combat effectiveness (force_N × endurance × morale).
|
|
17
|
+
*/
|
|
18
|
+
import { q, SCALE, clampQ, qMul, mulDiv } from "../units.js";
|
|
19
|
+
import { HUMAN_BASE } from "../archetypes.js";
|
|
20
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
21
|
+
/**
|
|
22
|
+
* Entity ids strictly below this value are treated as "named" and trigger
|
|
23
|
+
* micro-simulation delegation. Hosts may override via `FormationUnit.namedEntityIds`.
|
|
24
|
+
*/
|
|
25
|
+
export const NAMED_ENTITY_THRESHOLD = 1000;
|
|
26
|
+
/**
|
|
27
|
+
* Morale Q threshold below which a unit routs.
|
|
28
|
+
* Matches Phase 32D formation morale `BASE_DECAY` model.
|
|
29
|
+
*/
|
|
30
|
+
export const ROUT_THRESHOLD = q(0.20);
|
|
31
|
+
/**
|
|
32
|
+
* Base morale decay per tactical tick due to casualties (Q per tick per 1% casualty rate).
|
|
33
|
+
* A unit sustaining 10% casualties/tick loses ~q(0.10) morale/tick.
|
|
34
|
+
*/
|
|
35
|
+
export const MORALE_CASUALTY_DECAY_PER_PCT = q(0.010);
|
|
36
|
+
/**
|
|
37
|
+
* Lanchester attrition rate (fraction of effective opponent strength killed per tick).
|
|
38
|
+
*
|
|
39
|
+
* Lanchester's Square Law differential form:
|
|
40
|
+
* dA/dt = -rate × B_eff (attacker casualties ∝ effective defender count)
|
|
41
|
+
* dB/dt = -rate × A_eff (defender casualties ∝ effective attacker count)
|
|
42
|
+
*
|
|
43
|
+
* The "square law" refers to the conservation integral (A²-B²=const), not squared
|
|
44
|
+
* differentials. rate=0.01 gives ~100-tick engagements for equal 100-person units.
|
|
45
|
+
*/
|
|
46
|
+
export const LANCHESTER_RATE = 0.10;
|
|
47
|
+
/**
|
|
48
|
+
* Reference combat power of a single standard human soldier at q(1.0) morale.
|
|
49
|
+
* Used to convert aggregated sidePower() to "effective fighter count" for attrition.
|
|
50
|
+
* Units: same as aggregatedForce_N × conversionEfficiency / SCALE.Q (SCALE.N units).
|
|
51
|
+
*/
|
|
52
|
+
export const REFERENCE_POWER_PER_SOLDIER = Math.round((HUMAN_BASE.peakForce_N * HUMAN_BASE.conversionEfficiency) / SCALE.Q);
|
|
53
|
+
// ── Terrain multipliers ───────────────────────────────────────────────────────
|
|
54
|
+
/**
|
|
55
|
+
* Defender effectiveness multiplier per terrain type.
|
|
56
|
+
* Applied to the defender's combat power (force × endurance × morale).
|
|
57
|
+
* Attackers always use multiplier q(1.0).
|
|
58
|
+
*/
|
|
59
|
+
export const TERRAIN_DEFENDER_MUL = {
|
|
60
|
+
open: q(1.00), // no terrain advantage
|
|
61
|
+
difficult: q(1.30), // broken ground, forest, river — 30% defender bonus
|
|
62
|
+
fortified: q(2.00), // walls, prepared positions — 2× defender effectiveness
|
|
63
|
+
};
|
|
64
|
+
// ── Internal helpers ──────────────────────────────────────────────────────────
|
|
65
|
+
/**
|
|
66
|
+
* Aggregate combat power for a side (Q-scaled, relative units).
|
|
67
|
+
* power = sum(force_N × endurance × morale / SCALE.Q²)
|
|
68
|
+
* Returns an integer in SCALE.N × Q units (before the /SCALE.Q² normalisation).
|
|
69
|
+
*/
|
|
70
|
+
function sidePower(units, terrainMul) {
|
|
71
|
+
let total = 0;
|
|
72
|
+
for (const u of units) {
|
|
73
|
+
if (u.strength <= 0 || u.moraleQ <= 0)
|
|
74
|
+
continue;
|
|
75
|
+
// effective = force_N × (endurance / SCALE.Q) × (morale / SCALE.Q) × (terrain / SCALE.Q)
|
|
76
|
+
const effForce = mulDiv(u.aggregatedForce_N, u.aggregatedEndurance, SCALE.Q);
|
|
77
|
+
const withMorale = mulDiv(effForce, u.moraleQ, SCALE.Q);
|
|
78
|
+
total += mulDiv(withMorale, terrainMul, SCALE.Q);
|
|
79
|
+
}
|
|
80
|
+
return Math.max(0, total);
|
|
81
|
+
}
|
|
82
|
+
/** Total headcount across all units on a side. */
|
|
83
|
+
function sideStrength(units) {
|
|
84
|
+
return units.reduce((s, u) => s + Math.max(0, u.strength), 0);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Distribute `casualties` proportionally across units by current strength.
|
|
88
|
+
* Mutates unit.strength in-place. Returns actual casualties applied.
|
|
89
|
+
*/
|
|
90
|
+
function distributeCasualties(units, casualties) {
|
|
91
|
+
const total = sideStrength(units);
|
|
92
|
+
if (total <= 0 || casualties <= 0)
|
|
93
|
+
return 0;
|
|
94
|
+
let applied = 0;
|
|
95
|
+
for (const u of units) {
|
|
96
|
+
if (u.strength <= 0)
|
|
97
|
+
continue;
|
|
98
|
+
const share = Math.round((casualties * u.strength) / total);
|
|
99
|
+
const actual = Math.min(share, u.strength);
|
|
100
|
+
u.strength -= actual;
|
|
101
|
+
applied += actual;
|
|
102
|
+
}
|
|
103
|
+
return applied;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Apply morale pressure to all units on a side.
|
|
107
|
+
* Pressure = casualty rate × MORALE_CASUALTY_DECAY_PER_PCT.
|
|
108
|
+
*/
|
|
109
|
+
function applyMoralePressure(units, casualtiesThisTick) {
|
|
110
|
+
const total = sideStrength(units) + casualtiesThisTick;
|
|
111
|
+
if (total <= 0)
|
|
112
|
+
return;
|
|
113
|
+
const pct = Math.round((casualtiesThisTick * SCALE.Q) / total);
|
|
114
|
+
const decay = qMul(MORALE_CASUALTY_DECAY_PER_PCT, pct);
|
|
115
|
+
for (const u of units) {
|
|
116
|
+
u.moraleQ = clampQ((u.moraleQ - decay), 0, SCALE.Q);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** Collect named entity ids from all units across both sides. */
|
|
120
|
+
function collectNamedIds(attackers, defenders) {
|
|
121
|
+
const ids = new Set();
|
|
122
|
+
for (const u of [...attackers, ...defenders]) {
|
|
123
|
+
// Explicit named ids
|
|
124
|
+
if (u.namedEntityIds) {
|
|
125
|
+
for (const id of u.namedEntityIds)
|
|
126
|
+
ids.add(id);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return [...ids];
|
|
130
|
+
}
|
|
131
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
132
|
+
/**
|
|
133
|
+
* Build a `FormationUnit` from headcount and archetype.
|
|
134
|
+
*
|
|
135
|
+
* @param id Unique unit identifier.
|
|
136
|
+
* @param factionId Faction / polity identifier.
|
|
137
|
+
* @param strength Number of combatants.
|
|
138
|
+
* @param archetype Representative archetype (used for force and endurance derivation).
|
|
139
|
+
* @param moraleQ Initial morale (defaults to q(0.70)).
|
|
140
|
+
*/
|
|
141
|
+
export function createFormationUnit(id, factionId, strength, archetype, moraleQ = q(0.70)) {
|
|
142
|
+
return {
|
|
143
|
+
id,
|
|
144
|
+
factionId,
|
|
145
|
+
strength: Math.max(0, strength),
|
|
146
|
+
aggregatedForce_N: Math.round(archetype.peakForce_N * strength),
|
|
147
|
+
aggregatedEndurance: archetype.conversionEfficiency,
|
|
148
|
+
moraleQ,
|
|
149
|
+
archetype,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Resolve a tactical engagement over `durationTicks` using Lanchester's square law.
|
|
154
|
+
*
|
|
155
|
+
* The engagement proceeds tick-by-tick:
|
|
156
|
+
* 1. Compute combat power for each side (aggregated force × endurance × morale × terrain).
|
|
157
|
+
* 2. Apply Lanchester attrition: δA = k × B²/A, δB = k × A²/B.
|
|
158
|
+
* 3. Apply morale pressure proportional to casualty rate.
|
|
159
|
+
* 4. Check rout conditions — any unit below ROUT_THRESHOLD is considered routed.
|
|
160
|
+
* 5. Stop early if all units on one side are routed or wiped out.
|
|
161
|
+
*
|
|
162
|
+
* **Note:** This mutates `strength` and `moraleQ` on the supplied `FormationUnit` objects.
|
|
163
|
+
* Clone them before calling if you need to preserve original state.
|
|
164
|
+
*
|
|
165
|
+
* @param engagement - The engagement parameters.
|
|
166
|
+
* @returns `TacticalResult` with per-side outcomes, routed factions, and named entity ids.
|
|
167
|
+
*/
|
|
168
|
+
export function resolveTacticalEngagement(engagement) {
|
|
169
|
+
const { attackers, defenders, terrain, durationTicks } = engagement;
|
|
170
|
+
const terrainMul = TERRAIN_DEFENDER_MUL[terrain];
|
|
171
|
+
const namedEntityIds = collectNamedIds(attackers, defenders);
|
|
172
|
+
let totalAttackerCasualties = 0;
|
|
173
|
+
let totalDefenderCasualties = 0;
|
|
174
|
+
const routedFactions = new Set();
|
|
175
|
+
let decisiveTick = durationTicks;
|
|
176
|
+
for (let tick = 0; tick < durationTicks; tick++) {
|
|
177
|
+
const aStrength = sideStrength(attackers);
|
|
178
|
+
const dStrength = sideStrength(defenders);
|
|
179
|
+
// Stop if one side is wiped out
|
|
180
|
+
if (aStrength <= 0 || dStrength <= 0) {
|
|
181
|
+
decisiveTick = tick;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
// Combat power for each side
|
|
185
|
+
const aPower = sidePower(attackers, SCALE.Q); // attackers: no terrain bonus
|
|
186
|
+
const dPower = sidePower(defenders, terrainMul); // defenders: terrain multiplier
|
|
187
|
+
// Lanchester's Square Law — linear differential form:
|
|
188
|
+
// δA = rate × dEff (attacker casualties ∝ effective defender count)
|
|
189
|
+
// δB = rate × aEff (defender casualties ∝ effective attacker count)
|
|
190
|
+
//
|
|
191
|
+
// Convert aggregated power to "effective fighter count" by dividing by the
|
|
192
|
+
// reference combat power of one standard soldier.
|
|
193
|
+
const ref = Math.max(1, REFERENCE_POWER_PER_SOLDIER);
|
|
194
|
+
const aEff = Math.max(1, Math.round(aPower / ref));
|
|
195
|
+
const dEff = Math.max(1, Math.round(dPower / ref));
|
|
196
|
+
const aCas = Math.max(0, Math.round(LANCHESTER_RATE * dEff));
|
|
197
|
+
const dCas = Math.max(0, Math.round(LANCHESTER_RATE * aEff));
|
|
198
|
+
// Distribute casualties
|
|
199
|
+
totalAttackerCasualties += distributeCasualties(attackers, aCas);
|
|
200
|
+
totalDefenderCasualties += distributeCasualties(defenders, dCas);
|
|
201
|
+
// Morale pressure
|
|
202
|
+
applyMoralePressure(attackers, aCas);
|
|
203
|
+
applyMoralePressure(defenders, dCas);
|
|
204
|
+
// Rout check
|
|
205
|
+
for (const u of attackers) {
|
|
206
|
+
if (u.moraleQ < ROUT_THRESHOLD || u.strength <= 0)
|
|
207
|
+
routedFactions.add(u.factionId);
|
|
208
|
+
}
|
|
209
|
+
for (const u of defenders) {
|
|
210
|
+
if (u.moraleQ < ROUT_THRESHOLD || u.strength <= 0)
|
|
211
|
+
routedFactions.add(u.factionId);
|
|
212
|
+
}
|
|
213
|
+
// Early stop: all attacker or all defender factions routed
|
|
214
|
+
const aAllRouted = attackers.every(u => routedFactions.has(u.factionId) || u.strength <= 0);
|
|
215
|
+
const dAllRouted = defenders.every(u => routedFactions.has(u.factionId) || u.strength <= 0);
|
|
216
|
+
if (aAllRouted || dAllRouted) {
|
|
217
|
+
decisiveTick = tick + 1;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const attackerResult = {
|
|
222
|
+
casualties: totalAttackerCasualties,
|
|
223
|
+
survivingStrength: sideStrength(attackers),
|
|
224
|
+
finalMoraleQ: Math.round(attackers.reduce((s, u) => s + u.moraleQ, 0) / Math.max(1, attackers.length)),
|
|
225
|
+
routed: attackers.every(u => routedFactions.has(u.factionId) || u.strength <= 0),
|
|
226
|
+
};
|
|
227
|
+
const defenderResult = {
|
|
228
|
+
casualties: totalDefenderCasualties,
|
|
229
|
+
survivingStrength: sideStrength(defenders),
|
|
230
|
+
finalMoraleQ: Math.round(defenders.reduce((s, u) => s + u.moraleQ, 0) / Math.max(1, defenders.length)),
|
|
231
|
+
routed: defenders.every(u => routedFactions.has(u.factionId) || u.strength <= 0),
|
|
232
|
+
};
|
|
233
|
+
return {
|
|
234
|
+
attackerResult,
|
|
235
|
+
defenderResult,
|
|
236
|
+
routedFactions: [...routedFactions],
|
|
237
|
+
namedEntityIds,
|
|
238
|
+
decisiveTick,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Apply the tactical result back to polity military strength (Q).
|
|
243
|
+
*
|
|
244
|
+
* @param currentStrength_Q - Current `polity.militaryStrength_Q`.
|
|
245
|
+
* @param initialStrength - Headcount at engagement start.
|
|
246
|
+
* @param result - Side result from `resolveTacticalEngagement`.
|
|
247
|
+
* @returns Updated `militaryStrength_Q`.
|
|
248
|
+
*/
|
|
249
|
+
export function applyTacticalResultToPolity(currentStrength_Q, initialStrength, result) {
|
|
250
|
+
if (initialStrength <= 0)
|
|
251
|
+
return currentStrength_Q;
|
|
252
|
+
const survivorFrac = Math.round((result.survivingStrength * SCALE.Q) / initialStrength);
|
|
253
|
+
const moraleAdj = qMul(survivorFrac, result.finalMoraleQ);
|
|
254
|
+
return clampQ(Math.round((currentStrength_Q * moraleAdj) / SCALE.Q), 0, SCALE.Q);
|
|
255
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CE-9 — World-State Diffing + Incremental Snapshots
|
|
3
|
+
*
|
|
4
|
+
* Reduces long-run storage from O(ticks × fullState) to O(initialState + Σ deltas).
|
|
5
|
+
*
|
|
6
|
+
* ## Diff model
|
|
7
|
+
* `WorldStateDiff` captures:
|
|
8
|
+
* - World-level scalar changes (`tick`, `seed`, optional subsystem fields).
|
|
9
|
+
* - Entity-level changes at **top-level-field granularity**: each field that
|
|
10
|
+
* differs from the previous snapshot is stored in full; unchanged fields are
|
|
11
|
+
* omitted. This avoids deep-diffing complex nested types.
|
|
12
|
+
* - Newly added entities (stored in full).
|
|
13
|
+
* - Removed entity ids.
|
|
14
|
+
*
|
|
15
|
+
* ## Binary wire format (`packDiff` / `unpackDiff`)
|
|
16
|
+
* A compact, dependency-free binary encoding using a simple tag-value scheme.
|
|
17
|
+
* Zero external dependencies — implemented entirely with `DataView` / `Uint8Array`.
|
|
18
|
+
*
|
|
19
|
+
* Layout:
|
|
20
|
+
* [4 bytes magic "ANKD"] [1 byte version=1] [tag-value payload...]
|
|
21
|
+
*
|
|
22
|
+
* Tag bytes:
|
|
23
|
+
* 0x01 null | 0x02 true | 0x03 false
|
|
24
|
+
* 0x04 uint8 (1 byte) | 0x05 int32 LE (4 bytes) | 0x06 float64 LE (8 bytes)
|
|
25
|
+
* 0x07 string (uint16 LE length + UTF-8 bytes)
|
|
26
|
+
* 0x08 array (uint32 LE count + items)
|
|
27
|
+
* 0x09 object (uint32 LE count + key-value pairs, keys as 0x07 strings)
|
|
28
|
+
* 0x0A undefined/absent (skipped in round-trip)
|
|
29
|
+
*/
|
|
30
|
+
import type { WorldState } from "./sim/world.js";
|
|
31
|
+
import type { Entity } from "./sim/entity.js";
|
|
32
|
+
/** A patch for a single entity — only changed top-level fields are included. */
|
|
33
|
+
export interface EntityPatch {
|
|
34
|
+
/** Entity id. */
|
|
35
|
+
id: number;
|
|
36
|
+
/** Changed top-level fields (field name → new value). */
|
|
37
|
+
changes: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Delta between two consecutive `WorldState` snapshots.
|
|
41
|
+
*
|
|
42
|
+
* Applying a `WorldStateDiff` to the `prev` snapshot must yield a state
|
|
43
|
+
* indistinguishable (JSON-round-trip-equal) from `next`.
|
|
44
|
+
*/
|
|
45
|
+
export interface WorldStateDiff {
|
|
46
|
+
/** `next.tick` — always present for sequencing. */
|
|
47
|
+
tick: number;
|
|
48
|
+
/** Changed world-level scalar/subsystem fields (excluding `entities`). */
|
|
49
|
+
worldChanges: Record<string, unknown>;
|
|
50
|
+
/** Entities present in `next` but not in `prev` — stored in full. */
|
|
51
|
+
added: Entity[];
|
|
52
|
+
/** Entity ids present in `prev` but not in `next`. */
|
|
53
|
+
removed: number[];
|
|
54
|
+
/** Top-level-field patches for entities present in both states. */
|
|
55
|
+
modified: EntityPatch[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Compute the diff between two `WorldState` snapshots.
|
|
59
|
+
*
|
|
60
|
+
* The diff is guaranteed to be **idempotent**: `applyDiff(prev, diffWorldState(prev, next))`
|
|
61
|
+
* produces a state that is JSON-round-trip-equal to `next`.
|
|
62
|
+
*
|
|
63
|
+
* Complexity: O(E × F) where E = entity count and F = top-level field count per entity.
|
|
64
|
+
*
|
|
65
|
+
* @param prev Base snapshot.
|
|
66
|
+
* @param next New snapshot (must have the same `seed`; `tick` may differ).
|
|
67
|
+
* @returns `WorldStateDiff` — empty diff if states are identical.
|
|
68
|
+
*/
|
|
69
|
+
export declare function diffWorldState(prev: WorldState, next: WorldState): WorldStateDiff;
|
|
70
|
+
/**
|
|
71
|
+
* Apply a `WorldStateDiff` to a base `WorldState`, producing the `next` state.
|
|
72
|
+
*
|
|
73
|
+
* **Does not mutate `base`** — returns a new `WorldState` object.
|
|
74
|
+
* The returned state may share sub-object references with `base` for unchanged
|
|
75
|
+
* entities (copy-on-write semantics).
|
|
76
|
+
*
|
|
77
|
+
* @param base The `prev` snapshot that was passed to `diffWorldState`.
|
|
78
|
+
* @param diff The diff produced by `diffWorldState`.
|
|
79
|
+
* @returns Reconstructed `next` state.
|
|
80
|
+
*/
|
|
81
|
+
export declare function applyDiff(base: WorldState, diff: WorldStateDiff): WorldState;
|
|
82
|
+
/**
|
|
83
|
+
* Returns `true` when the diff contains no changes — states were identical.
|
|
84
|
+
*/
|
|
85
|
+
export declare function isDiffEmpty(diff: WorldStateDiff): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Summary statistics for a diff (useful for logging / network budget monitoring).
|
|
88
|
+
*/
|
|
89
|
+
export interface DiffStats {
|
|
90
|
+
/** Number of world-level changed fields. */
|
|
91
|
+
worldChangedFields: number;
|
|
92
|
+
addedEntities: number;
|
|
93
|
+
removedEntities: number;
|
|
94
|
+
modifiedEntities: number;
|
|
95
|
+
/** Total changed fields across all modified entities. */
|
|
96
|
+
totalEntityChanges: number;
|
|
97
|
+
}
|
|
98
|
+
export declare function diffStats(diff: WorldStateDiff): DiffStats;
|
|
99
|
+
/**
|
|
100
|
+
* Encode a `WorldStateDiff` as a compact binary `Uint8Array`.
|
|
101
|
+
*
|
|
102
|
+
* The binary format is self-describing (no schema required for decoding).
|
|
103
|
+
* `unpackDiff(packDiff(diff))` is guaranteed to produce a diff that when
|
|
104
|
+
* applied gives the same result as the original.
|
|
105
|
+
*
|
|
106
|
+
* @param diff Diff to encode.
|
|
107
|
+
* @returns Binary representation.
|
|
108
|
+
*/
|
|
109
|
+
export declare function packDiff(diff: WorldStateDiff): Uint8Array;
|
|
110
|
+
/**
|
|
111
|
+
* Decode a `WorldStateDiff` previously encoded by `packDiff`.
|
|
112
|
+
*
|
|
113
|
+
* @param bytes Binary data produced by `packDiff`.
|
|
114
|
+
* @returns Decoded `WorldStateDiff`.
|
|
115
|
+
* @throws If the magic bytes or version do not match.
|
|
116
|
+
*/
|
|
117
|
+
export declare function unpackDiff(bytes: Uint8Array): WorldStateDiff;
|