@its-not-rocket-science/ananke 0.1.6 → 0.1.8

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 CHANGED
@@ -10,6 +10,37 @@ Versioning follows [Semantic Versioning](https://semver.org/).
10
10
 
11
11
  ---
12
12
 
13
+ ## [0.1.8] — 2026-03-24
14
+
15
+ ### Added
16
+
17
+ - **CE-7 · Spatial Partitioning API for WebWorker Support** (`src/parallel.ts`)
18
+ - Add partitionWorld / mergePartitions / detectBoundaryPairs /
19
+ assignEntitiesToPartitions / canonicaliseBoundaryPairs. Boundary pairs
20
+ are sorted in canonical (min-id first) order to preserve determinism
21
+ across partitions.
22
+ - Export via src/index.ts
23
+
24
+ ---
25
+
26
+ ## [0.1.7] — 2026-03-23
27
+
28
+ ### Added
29
+
30
+ - **CE-9 · World-state Diffing and Incremental Snapshots** (`src/sim/cover.ts`)
31
+ - diffWorldState(prev, next): top-level-field diff per entity; world
32
+ scalar/subsystem diffs; added/removed entity tracking
33
+ - applyDiff(base, diff): reconstruct next state (non-mutating, copy-on-write)
34
+ - packDiff(diff): custom binary encoding — magic "ANKD", tagged-value
35
+ format (null/bool/uint8/int32/float64/string/array/object); zero
36
+ external dependencies, implemented with DataView/Uint8Array
37
+ - unpackDiff(bytes): full round-trip with magic and version validation
38
+ - isDiffEmpty(), diffStats() — helpers for logging and network budgeting
39
+ - 30 tests; verified binary size < full JSON for single-entity changes
40
+ - Export via src/index.ts
41
+
42
+ ---
43
+
13
44
  ## [0.1.6] — 2026-03-23
14
45
 
15
46
  ### Added
@@ -20,3 +20,6 @@ export * from "./scenario.js";
20
20
  export * from "./catalog.js";
21
21
  export * from "./sim/formation-combat.js";
22
22
  export * from "./sim/cover.js";
23
+ export * from "./sim/ai/behavior-trees.js";
24
+ export * from "./snapshot.js";
25
+ export * from "./parallel.js";
package/dist/src/index.js CHANGED
@@ -29,3 +29,6 @@ export * from "./scenario.js"; // loadScenario(), validateScenario(), AnankeScen
29
29
  export * from "./catalog.js"; // CE-12: registerArchetype(), registerWeapon(), registerArmour(), getCatalogEntry()
30
30
  export * from "./sim/formation-combat.js"; // Phase 69: FormationUnit, TacticalEngagement, resolveTacticalEngagement()
31
31
  export * from "./sim/cover.js"; // CE-15: CoverSegment, computeCoverProtection(), isLineOfSightBlocked(), applyExplosionToTerrain()
32
+ export * from "./sim/ai/behavior-trees.js"; // CE-10: BehaviorNode, FlankTarget, RetreatTo, ProtectAlly, GuardPosition, HealTarget, Sequence, Fallback
33
+ export * from "./snapshot.js"; // CE-9: diffWorldState(), applyDiff(), packDiff(), unpackDiff(), WorldStateDiff
34
+ export * from "./parallel.js"; // CE-7: partitionWorld(), mergePartitions(), detectBoundaryPairs(), assignEntitiesToPartitions()
@@ -0,0 +1,161 @@
1
+ /**
2
+ * CE-7 — Multi-threading / WebWorker Support
3
+ *
4
+ * Spatial partitioning utilities for running `stepWorld` in parallel Workers.
5
+ *
6
+ * ## Threading Model
7
+ *
8
+ * ```
9
+ * Host thread
10
+ * 1. partitionWorld(world, specs) → N WorldState slices
11
+ * 2. postMessage(slice_i, commandSubset_i) to Worker i
12
+ *
13
+ * Worker i
14
+ * 3. stepWorld(slice_i, commands_i, ctx) → stepped WorldState
15
+ * 4. postMessage(steppedSlice_i) back to host
16
+ *
17
+ * Host thread
18
+ * 5. mergePartitions([steppedSlice_0, …, steppedSlice_N-1], boundaryPairs)
19
+ * → merged WorldState
20
+ * 6. (optional) run a cross-partition boundary-pair resolution pass using
21
+ * the sorted boundary pairs returned by canonicaliseBoundaryPairs()
22
+ * ```
23
+ *
24
+ * ## Determinism guarantee
25
+ *
26
+ * Each partition is fully deterministic in isolation: same seed + same commands
27
+ * always produces the same output. Cross-partition boundary pairs **must** be
28
+ * resolved in canonical order (lower entity id first) after merging to avoid
29
+ * seed divergence. Use `canonicaliseBoundaryPairs()` before any boundary
30
+ * resolution step.
31
+ *
32
+ * ## Partition sizing guidelines
33
+ *
34
+ * | Entity count | Suggested partitions |
35
+ * |-------------|---------------------|
36
+ * | < 200 | 1 (no benefit) |
37
+ * | 200–500 | 2 |
38
+ * | 500–2 000 | 4 |
39
+ * | > 2 000 | 8 |
40
+ *
41
+ * Keep each partition roughly equal in entity count for best load balance.
42
+ * Avoid partitions with < 25 entities (thread-overhead exceeds compute savings).
43
+ */
44
+ import type { WorldState } from "./sim/world.js";
45
+ /**
46
+ * Specification for a single spatial partition.
47
+ *
48
+ * `regionIds` are arbitrary string labels used to identify the geographic
49
+ * region(s) this partition covers (e.g. `["north-west", "north-centre"]`).
50
+ * They are metadata only — they do not affect computation.
51
+ *
52
+ * `entities` lists the entity ids that belong to this partition.
53
+ * An entity id must appear in **at most one** partition; duplicate ids across
54
+ * partitions cause undefined behaviour in `mergePartitions`.
55
+ */
56
+ export interface PartitionSpec {
57
+ /** Human-readable region labels (metadata only). */
58
+ regionIds: string[];
59
+ /** Entity ids assigned to this partition. */
60
+ entities: number[];
61
+ }
62
+ /**
63
+ * Result summary returned by `mergePartitions`.
64
+ * The merged `WorldState` is the primary payload; `unresolvedBoundaryPairs` are
65
+ * the boundary pairs sorted in canonical order and ready for an optional
66
+ * post-merge resolution pass (e.g. cross-partition push/repulsion).
67
+ */
68
+ export interface MergeResult {
69
+ /** Merged world state with all entities from all partitions. */
70
+ world: WorldState;
71
+ /**
72
+ * Boundary pairs sorted in canonical order (a < b) for post-merge
73
+ * cross-partition resolution. Pass these to your boundary-resolution step
74
+ * to preserve determinism.
75
+ */
76
+ sortedBoundaryPairs: [number, number][];
77
+ }
78
+ /**
79
+ * Split a `WorldState` into N independent partition slices.
80
+ *
81
+ * Each partition slice contains:
82
+ * - All world-level scalar / subsystem fields (shared, copy-on-write).
83
+ * - Only the entities whose ids appear in `spec.entities`.
84
+ *
85
+ * Entity ids not referenced in any spec are silently dropped from all slices.
86
+ * If you need unassigned entities preserved, include them in one of the specs.
87
+ *
88
+ * @param world The current world state to partition.
89
+ * @param specs One spec per desired partition. Must have at least one entry.
90
+ * @returns Array of `WorldState` slices, one per spec, in the same order.
91
+ */
92
+ export declare function partitionWorld(world: WorldState, specs: PartitionSpec[]): WorldState[];
93
+ /**
94
+ * Merge N independently-stepped partition slices back into a single
95
+ * `WorldState`, and return the boundary pairs sorted in canonical order.
96
+ *
97
+ * **Entity ownership rule:** If an entity id appears in multiple partition
98
+ * results (which can happen if a host intentionally duplicated a boundary
99
+ * entity as a read-only context ghost), the entity state from the
100
+ * **last partition** in the array wins. To avoid ambiguity, ensure each
101
+ * entity id appears in at most one partition before stepping.
102
+ *
103
+ * World-level fields (`tick`, `seed`, subsystems) are taken from the first
104
+ * non-empty partition. All partitions should have been stepped with the same
105
+ * seed and world-level state; mixing partitions stepped at different ticks
106
+ * produces undefined behaviour.
107
+ *
108
+ * @param partitions Stepped WorldState slices, one per worker.
109
+ * @param boundaryPairs Pairs of entity ids that span partition boundaries.
110
+ * Order within each pair does not matter — they are
111
+ * normalised to `[min, max]` internally.
112
+ * @returns Merged world state + canonically sorted boundary pairs.
113
+ */
114
+ export declare function mergePartitions(partitions: WorldState[], boundaryPairs: [number, number][]): MergeResult;
115
+ /**
116
+ * Normalise and sort an array of entity-id pairs into canonical form.
117
+ *
118
+ * Each pair is normalised to `[min(a, b), max(a, b)]` and the array is sorted
119
+ * lexicographically by `[a, b]`. Duplicate pairs are preserved (callers may
120
+ * de-duplicate if needed).
121
+ *
122
+ * Use this before any cross-partition resolution step to guarantee the same
123
+ * pair-processing order regardless of how the host partitioned the world.
124
+ *
125
+ * @param pairs Boundary pairs in any order.
126
+ * @returns New sorted array with each pair normalised.
127
+ */
128
+ export declare function canonicaliseBoundaryPairs(pairs: [number, number][]): [number, number][];
129
+ /**
130
+ * Automatically detect cross-partition entity pairs within a given range.
131
+ *
132
+ * Scans all entity pairs `(e_i from partition_A, e_j from partition_B)` where
133
+ * `A ≠ B`, and returns any pair whose entities are within `range_m` of each
134
+ * other (using 2-D Euclidean distance). Results are canonically sorted.
135
+ *
136
+ * Useful for building the `boundaryPairs` argument to `mergePartitions` without
137
+ * manually specifying which pairs cross boundaries.
138
+ *
139
+ * Complexity: O(E²) — only suitable for boundary detection (not the hot path).
140
+ * For large entity counts, build boundary pairs from the spatial index instead.
141
+ *
142
+ * @param partitions Post-step (or pre-step) partition slices.
143
+ * @param range_m Maximum inter-entity distance to consider a boundary pair
144
+ * (in fixed-point metres, same scale as `entity.position_m`).
145
+ * @returns Canonically sorted cross-partition boundary pairs.
146
+ */
147
+ export declare function detectBoundaryPairs(partitions: WorldState[], range_m: number): [number, number][];
148
+ /**
149
+ * Simple spatial partitioning helper — assigns entities to N partitions based
150
+ * on their X position (vertical strip partitioning).
151
+ *
152
+ * Entities are sorted by ascending `position_m.x` and divided into `n` roughly
153
+ * equal buckets. This is suitable for combat scenarios where combatants are
154
+ * spread along the X axis. For 2-D grid layouts, call this function twice
155
+ * (once per axis) and intersect the results.
156
+ *
157
+ * @param world World state whose entities should be partitioned.
158
+ * @param n Number of partitions (≥ 1).
159
+ * @returns `PartitionSpec` array ready for `partitionWorld`.
160
+ */
161
+ export declare function assignEntitiesToPartitions(world: WorldState, n: number): PartitionSpec[];
@@ -0,0 +1,235 @@
1
+ /**
2
+ * CE-7 — Multi-threading / WebWorker Support
3
+ *
4
+ * Spatial partitioning utilities for running `stepWorld` in parallel Workers.
5
+ *
6
+ * ## Threading Model
7
+ *
8
+ * ```
9
+ * Host thread
10
+ * 1. partitionWorld(world, specs) → N WorldState slices
11
+ * 2. postMessage(slice_i, commandSubset_i) to Worker i
12
+ *
13
+ * Worker i
14
+ * 3. stepWorld(slice_i, commands_i, ctx) → stepped WorldState
15
+ * 4. postMessage(steppedSlice_i) back to host
16
+ *
17
+ * Host thread
18
+ * 5. mergePartitions([steppedSlice_0, …, steppedSlice_N-1], boundaryPairs)
19
+ * → merged WorldState
20
+ * 6. (optional) run a cross-partition boundary-pair resolution pass using
21
+ * the sorted boundary pairs returned by canonicaliseBoundaryPairs()
22
+ * ```
23
+ *
24
+ * ## Determinism guarantee
25
+ *
26
+ * Each partition is fully deterministic in isolation: same seed + same commands
27
+ * always produces the same output. Cross-partition boundary pairs **must** be
28
+ * resolved in canonical order (lower entity id first) after merging to avoid
29
+ * seed divergence. Use `canonicaliseBoundaryPairs()` before any boundary
30
+ * resolution step.
31
+ *
32
+ * ## Partition sizing guidelines
33
+ *
34
+ * | Entity count | Suggested partitions |
35
+ * |-------------|---------------------|
36
+ * | < 200 | 1 (no benefit) |
37
+ * | 200–500 | 2 |
38
+ * | 500–2 000 | 4 |
39
+ * | > 2 000 | 8 |
40
+ *
41
+ * Keep each partition roughly equal in entity count for best load balance.
42
+ * Avoid partitions with < 25 entities (thread-overhead exceeds compute savings).
43
+ */
44
+ // ── partitionWorld ─────────────────────────────────────────────────────────────
45
+ /**
46
+ * Split a `WorldState` into N independent partition slices.
47
+ *
48
+ * Each partition slice contains:
49
+ * - All world-level scalar / subsystem fields (shared, copy-on-write).
50
+ * - Only the entities whose ids appear in `spec.entities`.
51
+ *
52
+ * Entity ids not referenced in any spec are silently dropped from all slices.
53
+ * If you need unassigned entities preserved, include them in one of the specs.
54
+ *
55
+ * @param world The current world state to partition.
56
+ * @param specs One spec per desired partition. Must have at least one entry.
57
+ * @returns Array of `WorldState` slices, one per spec, in the same order.
58
+ */
59
+ export function partitionWorld(world, specs) {
60
+ if (specs.length === 0) {
61
+ throw new RangeError("parallel: partitionWorld requires at least one PartitionSpec");
62
+ }
63
+ // Build O(1) entity lookup
64
+ const entityMap = new Map(world.entities.map(e => [e.id, e]));
65
+ return specs.map(spec => {
66
+ // Collect entities for this partition; skip unknown ids
67
+ const entities = [];
68
+ for (const id of spec.entities) {
69
+ const e = entityMap.get(id);
70
+ if (e !== undefined)
71
+ entities.push(e);
72
+ }
73
+ // Maintain canonical ascending-id sort within partition
74
+ entities.sort((a, b) => a.id - b.id);
75
+ return {
76
+ ...world,
77
+ entities,
78
+ };
79
+ });
80
+ }
81
+ // ── mergePartitions ────────────────────────────────────────────────────────────
82
+ /**
83
+ * Merge N independently-stepped partition slices back into a single
84
+ * `WorldState`, and return the boundary pairs sorted in canonical order.
85
+ *
86
+ * **Entity ownership rule:** If an entity id appears in multiple partition
87
+ * results (which can happen if a host intentionally duplicated a boundary
88
+ * entity as a read-only context ghost), the entity state from the
89
+ * **last partition** in the array wins. To avoid ambiguity, ensure each
90
+ * entity id appears in at most one partition before stepping.
91
+ *
92
+ * World-level fields (`tick`, `seed`, subsystems) are taken from the first
93
+ * non-empty partition. All partitions should have been stepped with the same
94
+ * seed and world-level state; mixing partitions stepped at different ticks
95
+ * produces undefined behaviour.
96
+ *
97
+ * @param partitions Stepped WorldState slices, one per worker.
98
+ * @param boundaryPairs Pairs of entity ids that span partition boundaries.
99
+ * Order within each pair does not matter — they are
100
+ * normalised to `[min, max]` internally.
101
+ * @returns Merged world state + canonically sorted boundary pairs.
102
+ */
103
+ export function mergePartitions(partitions, boundaryPairs) {
104
+ if (partitions.length === 0) {
105
+ throw new RangeError("parallel: mergePartitions requires at least one partition");
106
+ }
107
+ // Merge entities — last-partition-wins for duplicates
108
+ const entityMap = new Map();
109
+ for (const partition of partitions) {
110
+ for (const e of partition.entities) {
111
+ entityMap.set(e.id, e);
112
+ }
113
+ }
114
+ // Restore canonical ascending-id sort
115
+ const entities = [...entityMap.values()].sort((a, b) => a.id - b.id);
116
+ // World-level fields from first partition (tick, seed, subsystems)
117
+ const base = partitions[0];
118
+ const world = {
119
+ ...base,
120
+ entities,
121
+ };
122
+ // Canonical boundary pairs: normalise order + sort
123
+ const sortedBoundaryPairs = canonicaliseBoundaryPairs(boundaryPairs);
124
+ return { world, sortedBoundaryPairs };
125
+ }
126
+ // ── canonicaliseBoundaryPairs ─────────────────────────────────────────────────
127
+ /**
128
+ * Normalise and sort an array of entity-id pairs into canonical form.
129
+ *
130
+ * Each pair is normalised to `[min(a, b), max(a, b)]` and the array is sorted
131
+ * lexicographically by `[a, b]`. Duplicate pairs are preserved (callers may
132
+ * de-duplicate if needed).
133
+ *
134
+ * Use this before any cross-partition resolution step to guarantee the same
135
+ * pair-processing order regardless of how the host partitioned the world.
136
+ *
137
+ * @param pairs Boundary pairs in any order.
138
+ * @returns New sorted array with each pair normalised.
139
+ */
140
+ export function canonicaliseBoundaryPairs(pairs) {
141
+ return pairs
142
+ .map(([a, b]) => a <= b ? [a, b] : [b, a])
143
+ .sort(([a1, b1], [a2, b2]) => (a1 - a2) || (b1 - b2));
144
+ }
145
+ // ── detectBoundaryPairs ────────────────────────────────────────────────────────
146
+ /**
147
+ * Automatically detect cross-partition entity pairs within a given range.
148
+ *
149
+ * Scans all entity pairs `(e_i from partition_A, e_j from partition_B)` where
150
+ * `A ≠ B`, and returns any pair whose entities are within `range_m` of each
151
+ * other (using 2-D Euclidean distance). Results are canonically sorted.
152
+ *
153
+ * Useful for building the `boundaryPairs` argument to `mergePartitions` without
154
+ * manually specifying which pairs cross boundaries.
155
+ *
156
+ * Complexity: O(E²) — only suitable for boundary detection (not the hot path).
157
+ * For large entity counts, build boundary pairs from the spatial index instead.
158
+ *
159
+ * @param partitions Post-step (or pre-step) partition slices.
160
+ * @param range_m Maximum inter-entity distance to consider a boundary pair
161
+ * (in fixed-point metres, same scale as `entity.position_m`).
162
+ * @returns Canonically sorted cross-partition boundary pairs.
163
+ */
164
+ export function detectBoundaryPairs(partitions, range_m) {
165
+ const range2 = range_m * range_m;
166
+ // Build (entityId → partitionIndex) map
167
+ const ownerMap = new Map();
168
+ for (let pi = 0; pi < partitions.length; pi++) {
169
+ for (const e of partitions[pi].entities) {
170
+ ownerMap.set(e.id, pi);
171
+ }
172
+ }
173
+ // Flat entity list for pair scanning
174
+ const all = partitions.flatMap(p => p.entities);
175
+ const pairs = [];
176
+ for (let i = 0; i < all.length; i++) {
177
+ const ei = all[i];
178
+ if (ei.injury.dead)
179
+ continue;
180
+ const piOwner = ownerMap.get(ei.id);
181
+ for (let j = i + 1; j < all.length; j++) {
182
+ const ej = all[j];
183
+ if (ej.injury.dead)
184
+ continue;
185
+ if (ownerMap.get(ej.id) === piOwner)
186
+ continue; // same partition — skip
187
+ const dx = ei.position_m.x - ej.position_m.x;
188
+ const dy = ei.position_m.y - ej.position_m.y;
189
+ const dist2 = dx * dx + dy * dy;
190
+ if (dist2 <= range2) {
191
+ const a = Math.min(ei.id, ej.id);
192
+ const b = Math.max(ei.id, ej.id);
193
+ pairs.push([a, b]);
194
+ }
195
+ }
196
+ }
197
+ return canonicaliseBoundaryPairs(pairs);
198
+ }
199
+ // ── assignEntitiesToPartitions ────────────────────────────────────────────────
200
+ /**
201
+ * Simple spatial partitioning helper — assigns entities to N partitions based
202
+ * on their X position (vertical strip partitioning).
203
+ *
204
+ * Entities are sorted by ascending `position_m.x` and divided into `n` roughly
205
+ * equal buckets. This is suitable for combat scenarios where combatants are
206
+ * spread along the X axis. For 2-D grid layouts, call this function twice
207
+ * (once per axis) and intersect the results.
208
+ *
209
+ * @param world World state whose entities should be partitioned.
210
+ * @param n Number of partitions (≥ 1).
211
+ * @returns `PartitionSpec` array ready for `partitionWorld`.
212
+ */
213
+ export function assignEntitiesToPartitions(world, n) {
214
+ if (n < 1)
215
+ throw new RangeError("parallel: partition count must be ≥ 1");
216
+ const live = world.entities
217
+ .filter(e => !e.injury.dead)
218
+ .sort((a, b) => a.position_m.x - b.position_m.x);
219
+ const specs = Array.from({ length: n }, (_, i) => ({
220
+ regionIds: [`strip-${i}`],
221
+ entities: [],
222
+ }));
223
+ // Assign dead entities to first partition so they're not dropped
224
+ const dead = world.entities.filter(e => e.injury.dead);
225
+ for (const e of dead) {
226
+ specs[0].entities.push(e.id);
227
+ }
228
+ // Round-robin striped assignment for live entities
229
+ const bucketSize = Math.ceil(live.length / n);
230
+ for (let i = 0; i < live.length; i++) {
231
+ const bucket = Math.min(Math.trunc(i / bucketSize), n - 1);
232
+ specs[bucket].entities.push(live[i].id);
233
+ }
234
+ return specs;
235
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * CE-10 — Pre-built AI Behavior Tree Library
3
+ *
4
+ * A thin, composable layer over the existing AI decision system. Each
5
+ * `BehaviorNode` receives the ticking entity, the current world state, and
6
+ * kernel context, and either returns a `Command` (success) or `null` (failure /
7
+ * not applicable).
8
+ *
9
+ * ## Semantics
10
+ * - `null` — node failed / condition not met
11
+ * - Command — node succeeded; caller should use this command
12
+ *
13
+ * ## Composite nodes
14
+ * - `Sequence` — run children left-to-right; return first **non-null** result
15
+ * (priority / fallback selector pattern)
16
+ * - `Fallback` — identical semantics to Sequence for this usage:
17
+ * try children in order, first non-null wins
18
+ *
19
+ * Note: The ROADMAP spec uses "first success wins" for both Sequence and
20
+ * Fallback (i.e. both are priority-selectors). A traditional BT Sequence
21
+ * would return the *last* child result; here all composites return the first
22
+ * non-null command for practical game-AI use.
23
+ *
24
+ * ## Determinism constraint
25
+ * All nodes are deterministic. Any tie-breaking or random sampling uses
26
+ * `eventSeed(world.seed, world.tick, entity.id, salt)` — never `Math.random()`.
27
+ */
28
+ import type { Entity } from "../entity.js";
29
+ import type { WorldState } from "../world.js";
30
+ import type { KernelContext } from "../context.js";
31
+ import type { Command } from "../commands.js";
32
+ import { type Q } from "../../units.js";
33
+ /**
34
+ * A single node in a behavior tree.
35
+ *
36
+ * `tick` is called once per AI frame (typically once per `stepWorld` tick).
37
+ * Returns a `Command` if this node produces an action, or `null` if the node's
38
+ * condition is not satisfied (pass control to the next node in a composite).
39
+ */
40
+ export interface BehaviorNode {
41
+ tick(entity: Entity, world: WorldState, ctx: KernelContext): Command | null;
42
+ }
43
+ /**
44
+ * Move toward a target entity and attack when in range.
45
+ *
46
+ * If the target is dead or missing, returns null.
47
+ * Flanks by approaching from the target's perpendicular when `flankOffset > 0`.
48
+ *
49
+ * @param targetId Id of the entity to flank/attack.
50
+ * @param flankOffset Lateral offset in SCALE.m units (0 = direct approach).
51
+ */
52
+ export declare function FlankTarget(targetId: number, flankOffset?: number): BehaviorNode;
53
+ /**
54
+ * Move to a fixed world position and stop when within `arrivalRadius_m` metres.
55
+ *
56
+ * Returns null when already at the destination (no command needed).
57
+ *
58
+ * @param x_m Destination x [SCALE.m].
59
+ * @param y_m Destination y [SCALE.m].
60
+ * @param arrivalRadius_m Stop radius [SCALE.m]. Default 5 000 (0.5 m).
61
+ */
62
+ export declare function RetreatTo(x_m: number, y_m: number, arrivalRadius_m?: number): BehaviorNode;
63
+ /**
64
+ * Move toward an allied entity and interpose between it and the nearest threat.
65
+ *
66
+ * - If ally is missing or dead, returns null.
67
+ * - If no threats (other entities) are present, returns null.
68
+ * - Otherwise moves toward the ally.
69
+ *
70
+ * @param allyId Id of the entity to protect.
71
+ */
72
+ export declare function ProtectAlly(allyId: number): BehaviorNode;
73
+ /**
74
+ * Hold a position: move toward the guard point if outside `radius_m`, defend
75
+ * if inside. Returns null when the entity is already inside the radius and
76
+ * there are no threats to defend against.
77
+ *
78
+ * @param x_m Guard point x [SCALE.m].
79
+ * @param y_m Guard point y [SCALE.m].
80
+ * @param radius_m Patrol radius [SCALE.m].
81
+ */
82
+ export declare function GuardPosition(x_m: number, y_m: number, radius_m: number): BehaviorNode;
83
+ /**
84
+ * Move toward a target entity and issue a Treat command when in range.
85
+ *
86
+ * - Returns null if target is missing, already dead, or has no injuries.
87
+ * - Healing uses the first injured region found (deterministic iteration order).
88
+ *
89
+ * @param targetId Id of the entity to heal.
90
+ */
91
+ export declare function HealTarget(targetId: number): BehaviorNode;
92
+ /**
93
+ * Priority selector: tries each child node in order and returns the first
94
+ * non-null command. If all children return null, returns null.
95
+ *
96
+ * This is the most commonly used composite. Place higher-priority behaviours
97
+ * earlier in the list.
98
+ *
99
+ * @param nodes Child nodes to evaluate in order.
100
+ */
101
+ export declare function Sequence(...nodes: BehaviorNode[]): BehaviorNode;
102
+ /**
103
+ * Fallback: identical to `Sequence` — tries children in order, returns first
104
+ * non-null result. Provided as a semantic alias for callers who prefer the
105
+ * standard BT naming convention (Sequence = AND-chain; Fallback = OR-chain).
106
+ *
107
+ * @param nodes Child nodes to evaluate in order.
108
+ */
109
+ export declare function Fallback(...nodes: BehaviorNode[]): BehaviorNode;
110
+ /**
111
+ * Gate: return `inner.tick()` only if the entity's shock is below `maxShockQ`.
112
+ * When the entity is in severe shock it cannot act — returns null instead.
113
+ *
114
+ * Useful for wrapping aggressive nodes: "attack only if not badly shocked."
115
+ *
116
+ * @param maxShockQ Q threshold; entity.injury.shock must be below this.
117
+ * @param inner Wrapped node.
118
+ */
119
+ export declare function IfNotShocked(maxShockQ: Q, inner: BehaviorNode): BehaviorNode;
120
+ /**
121
+ * Gate: return `inner.tick()` only when `entity.energy.fatigue` is below
122
+ * `maxFatigueQ`. Prevents exhausted entities from taking high-intensity actions.
123
+ *
124
+ * @param maxFatigueQ Q threshold; fatigue must be below this.
125
+ * @param inner Wrapped node.
126
+ */
127
+ export declare function IfNotExhausted(maxFatigueQ: Q, inner: BehaviorNode): BehaviorNode;
128
+ /**
129
+ * Probabilistic gate: run `inner` only when
130
+ * `eventSeed(world.seed, world.tick, entity.id, salt) % SCALE.Q < probability_Q`.
131
+ *
132
+ * Allows stochastic variation without `Math.random()`.
133
+ *
134
+ * @param probability_Q Q probability [0..SCALE.Q]; q(1.0) = always, q(0) = never.
135
+ * @param salt Arbitrary integer to distinguish independent rolls on the
136
+ * same entity+tick (default 0).
137
+ * @param inner Wrapped node.
138
+ */
139
+ export declare function WithProbability(probability_Q: Q, inner: BehaviorNode, salt?: number): BehaviorNode;
140
+ /**
141
+ * Standard aggressive attacker: attack `targetId` at full intensity.
142
+ * Falls back to retreat if badly shocked (shock ≥ q(0.70)).
143
+ *
144
+ * @param targetId Primary attack target.
145
+ * @param retreatX Fallback retreat x [SCALE.m].
146
+ * @param retreatY Fallback retreat y [SCALE.m].
147
+ */
148
+ export declare function aggressorTree(targetId: number, retreatX: number, retreatY: number): BehaviorNode;
149
+ /**
150
+ * Standard defender: hold position, heal allies when in range, defend otherwise.
151
+ *
152
+ * @param guardX Guard point x [SCALE.m].
153
+ * @param guardY Guard point y [SCALE.m].
154
+ * @param radius_m Guard radius [SCALE.m].
155
+ * @param allyIds Ally ids to heal (checked in order; first injured ally wins).
156
+ */
157
+ export declare function defenderTree(guardX: number, guardY: number, radius_m: number, allyIds: number[]): BehaviorNode;
158
+ /**
159
+ * Medic tree: prioritise healing each ally in the given list (first injured wins),
160
+ * then retreat to a safe point if badly shocked.
161
+ *
162
+ * @param allyIds Allies to heal in priority order.
163
+ * @param safeX Retreat x [SCALE.m].
164
+ * @param safeY Retreat y [SCALE.m].
165
+ */
166
+ export declare function medicTree(allyIds: number[], safeX: number, safeY: number): BehaviorNode;