ecspresso 0.13.4 → 0.14.2
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/README.md +3 -6
- package/dist/index.js +2 -2
- package/dist/index.js.map +4 -4
- package/dist/plugins/ai/behavior-tree.d.ts +369 -0
- package/dist/plugins/ai/behavior-tree.js +4 -0
- package/dist/plugins/ai/behavior-tree.js.map +10 -0
- package/dist/plugins/ai/detection.js +2 -2
- package/dist/plugins/ai/detection.js.map +2 -2
- package/dist/plugins/ai/flocking.js +2 -2
- package/dist/plugins/ai/flocking.js.map +2 -2
- package/dist/plugins/ai/pathfinding.d.ts +163 -0
- package/dist/plugins/ai/pathfinding.js +4 -0
- package/dist/plugins/ai/pathfinding.js.map +10 -0
- package/dist/plugins/audio/audio.js +2 -2
- package/dist/plugins/audio/audio.js.map +2 -2
- package/dist/plugins/combat/health.js +2 -2
- package/dist/plugins/combat/health.js.map +2 -2
- package/dist/plugins/combat/projectile.js +2 -2
- package/dist/plugins/combat/projectile.js.map +2 -2
- package/dist/plugins/debug/diagnostics.js +3 -3
- package/dist/plugins/debug/diagnostics.js.map +2 -2
- package/dist/plugins/input/input.d.ts +105 -27
- package/dist/plugins/input/input.js +2 -2
- package/dist/plugins/input/input.js.map +3 -3
- package/dist/plugins/input/selection.js +2 -2
- package/dist/plugins/input/selection.js.map +2 -2
- package/dist/plugins/isometric/depth-sort.js +2 -2
- package/dist/plugins/isometric/depth-sort.js.map +2 -2
- package/dist/plugins/isometric/projection.js +2 -2
- package/dist/plugins/isometric/projection.js.map +2 -2
- package/dist/plugins/physics/collision.js +2 -2
- package/dist/plugins/physics/collision.js.map +2 -2
- package/dist/plugins/physics/collision3D.d.ts +83 -0
- package/dist/plugins/physics/collision3D.js +4 -0
- package/dist/plugins/physics/collision3D.js.map +13 -0
- package/dist/plugins/physics/physics2D.js +2 -2
- package/dist/plugins/physics/physics2D.js.map +2 -2
- package/dist/plugins/physics/physics3D.d.ts +140 -0
- package/dist/plugins/physics/physics3D.js +4 -0
- package/dist/plugins/physics/physics3D.js.map +11 -0
- package/dist/plugins/physics/steering.js +2 -2
- package/dist/plugins/physics/steering.js.map +2 -2
- package/dist/plugins/rendering/particles.js +2 -2
- package/dist/plugins/rendering/particles.js.map +2 -2
- package/dist/plugins/rendering/renderer2D.js +2 -2
- package/dist/plugins/rendering/renderer2D.js.map +3 -3
- package/dist/plugins/rendering/renderer3D.d.ts +247 -0
- package/dist/plugins/rendering/renderer3D.js +4107 -0
- package/dist/plugins/rendering/renderer3D.js.map +12 -0
- package/dist/plugins/rendering/sprite-animation.js +2 -2
- package/dist/plugins/rendering/sprite-animation.js.map +2 -2
- package/dist/plugins/rendering/tilemap.d.ts +230 -0
- package/dist/plugins/rendering/tilemap.js +4 -0
- package/dist/plugins/rendering/tilemap.js.map +11 -0
- package/dist/plugins/scripting/coroutine.js +2 -2
- package/dist/plugins/scripting/coroutine.js.map +2 -2
- package/dist/plugins/scripting/state-machine.js +2 -2
- package/dist/plugins/scripting/state-machine.js.map +2 -2
- package/dist/plugins/scripting/timers.js +2 -2
- package/dist/plugins/scripting/timers.js.map +2 -2
- package/dist/plugins/scripting/tween.js +2 -2
- package/dist/plugins/scripting/tween.js.map +2 -2
- package/dist/plugins/spatial/bounds.js +2 -2
- package/dist/plugins/spatial/bounds.js.map +2 -2
- package/dist/plugins/spatial/camera.js +2 -2
- package/dist/plugins/spatial/camera.js.map +2 -2
- package/dist/plugins/spatial/camera3D.d.ts +112 -0
- package/dist/plugins/spatial/camera3D.js +4 -0
- package/dist/plugins/spatial/camera3D.js.map +10 -0
- package/dist/plugins/spatial/spatial-index.js +2 -2
- package/dist/plugins/spatial/spatial-index.js.map +3 -3
- package/dist/plugins/spatial/spatial-index3D.d.ts +80 -0
- package/dist/plugins/spatial/spatial-index3D.js +4 -0
- package/dist/plugins/spatial/spatial-index3D.js.map +11 -0
- package/dist/plugins/spatial/transform.js +2 -2
- package/dist/plugins/spatial/transform.js.map +2 -2
- package/dist/plugins/spatial/transform3D.d.ts +148 -0
- package/dist/plugins/spatial/transform3D.js +4 -0
- package/dist/plugins/spatial/transform3D.js.map +10 -0
- package/dist/plugins/ui/ui.d.ts +116 -0
- package/dist/plugins/ui/ui.js +4 -0
- package/dist/plugins/ui/ui.js.map +11 -0
- package/dist/system-builder.d.ts +31 -0
- package/dist/utils/math.d.ts +65 -1
- package/dist/utils/narrowphase3D.d.ts +120 -0
- package/dist/utils/spatial-hash3D.d.ts +72 -0
- package/package.json +44 -4
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behavior Tree Plugin for ECSpresso
|
|
3
|
+
*
|
|
4
|
+
* Provides composable, priority-driven AI via behavior trees. Shared immutable
|
|
5
|
+
* tree definitions drive per-entity runtime state. Uses hybrid traversal
|
|
6
|
+
* (Approach C): re-evaluate from root each tick to preserve priority, resume
|
|
7
|
+
* running leaves, and abort diverged running nodes via `onAbort`.
|
|
8
|
+
*
|
|
9
|
+
* Each entity gets a `behaviorTree` component referencing a shared definition
|
|
10
|
+
* plus a typed blackboard for per-entity AI memory. One system processes all
|
|
11
|
+
* behavior-tree entities each tick.
|
|
12
|
+
*
|
|
13
|
+
* Node types:
|
|
14
|
+
* Composites — sequence, selector, parallel
|
|
15
|
+
* Decorators — inverter, repeat, cooldown, guard
|
|
16
|
+
* Leaves — action (tick → NodeStatus, optional onAbort), condition (predicate)
|
|
17
|
+
*/
|
|
18
|
+
import { type BasePluginOptions, type WorldConfigFrom, type BaseWorld } from 'ecspresso';
|
|
19
|
+
/**
|
|
20
|
+
* Return value from behavior tree node ticks.
|
|
21
|
+
*
|
|
22
|
+
* - `Success` (0) — node completed successfully
|
|
23
|
+
* - `Failure` (1) — node failed
|
|
24
|
+
* - `Running` (2) — node still executing, will resume next tick
|
|
25
|
+
*/
|
|
26
|
+
export declare const NodeStatus: {
|
|
27
|
+
readonly Success: 0;
|
|
28
|
+
readonly Failure: 1;
|
|
29
|
+
readonly Running: 2;
|
|
30
|
+
};
|
|
31
|
+
export type NodeStatus = (typeof NodeStatus)[keyof typeof NodeStatus];
|
|
32
|
+
/** BaseWorld narrowed to behavior-tree components for typed access in helpers. */
|
|
33
|
+
type BehaviorTreeWorld = BaseWorld<BehaviorTreeComponentTypes>;
|
|
34
|
+
/**
|
|
35
|
+
* Context passed to all leaf node callbacks (action tick, condition check,
|
|
36
|
+
* onAbort, guard predicates).
|
|
37
|
+
*
|
|
38
|
+
* @template BB - Blackboard type for per-entity AI memory
|
|
39
|
+
* @template W - World interface type (default: BehaviorTreeWorld)
|
|
40
|
+
*/
|
|
41
|
+
export interface BehaviorTreeContext<BB extends object = Record<string, unknown>, W extends BaseWorld<BehaviorTreeComponentTypes> = BehaviorTreeWorld> {
|
|
42
|
+
readonly ecs: W;
|
|
43
|
+
readonly entityId: number;
|
|
44
|
+
readonly dt: number;
|
|
45
|
+
readonly blackboard: BB;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Action leaf — executes behavior each tick.
|
|
49
|
+
* Returns `Running` for multi-frame actions. Optional `onAbort` fires
|
|
50
|
+
* when a higher-priority branch preempts this running action.
|
|
51
|
+
*/
|
|
52
|
+
export interface ActionNode<BB extends object = Record<string, unknown>> {
|
|
53
|
+
readonly type: 'action';
|
|
54
|
+
readonly name: string;
|
|
55
|
+
readonly tick: (ctx: BehaviorTreeContext<BB>) => NodeStatus;
|
|
56
|
+
readonly onAbort?: (ctx: BehaviorTreeContext<BB>) => void;
|
|
57
|
+
nodeIndex: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Condition leaf — checks a predicate. Returns Success or Failure, never Running.
|
|
61
|
+
*/
|
|
62
|
+
export interface ConditionNode<BB extends object = Record<string, unknown>> {
|
|
63
|
+
readonly type: 'condition';
|
|
64
|
+
readonly name: string;
|
|
65
|
+
readonly check: (ctx: BehaviorTreeContext<BB>) => boolean;
|
|
66
|
+
nodeIndex: number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Sequence composite — runs children left-to-right.
|
|
70
|
+
* Fails on first failure, succeeds when all succeed.
|
|
71
|
+
* Resumes from stored child index when a running node exists.
|
|
72
|
+
*/
|
|
73
|
+
export interface SequenceNode<BB extends object = Record<string, unknown>> {
|
|
74
|
+
readonly type: 'sequence';
|
|
75
|
+
readonly children: readonly BehaviorTreeNode<BB>[];
|
|
76
|
+
nodeIndex: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Selector composite — runs children left-to-right.
|
|
80
|
+
* Succeeds on first success, fails when all fail.
|
|
81
|
+
* Always re-evaluates from child 0 to preserve priority ordering.
|
|
82
|
+
*/
|
|
83
|
+
export interface SelectorNode<BB extends object = Record<string, unknown>> {
|
|
84
|
+
readonly type: 'selector';
|
|
85
|
+
readonly children: readonly BehaviorTreeNode<BB>[];
|
|
86
|
+
nodeIndex: number;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Parallel composite — ticks all children each frame.
|
|
90
|
+
* Configurable success/failure thresholds.
|
|
91
|
+
*
|
|
92
|
+
* Limitation (v1): only one running leaf is tracked for abort.
|
|
93
|
+
* Other running children in a parallel stop being ticked if the
|
|
94
|
+
* tree path diverges but do not receive an `onAbort` call.
|
|
95
|
+
*/
|
|
96
|
+
export interface ParallelNode<BB extends object = Record<string, unknown>> {
|
|
97
|
+
readonly type: 'parallel';
|
|
98
|
+
readonly children: readonly BehaviorTreeNode<BB>[];
|
|
99
|
+
readonly successThreshold: number;
|
|
100
|
+
readonly failureThreshold: number;
|
|
101
|
+
nodeIndex: number;
|
|
102
|
+
}
|
|
103
|
+
/** Decorator — inverts child result (Success↔Failure), passes Running through. */
|
|
104
|
+
export interface InverterNode<BB extends object = Record<string, unknown>> {
|
|
105
|
+
readonly type: 'inverter';
|
|
106
|
+
readonly child: BehaviorTreeNode<BB>;
|
|
107
|
+
nodeIndex: number;
|
|
108
|
+
}
|
|
109
|
+
/** Decorator — repeats child `count` times (or forever when count is -1). */
|
|
110
|
+
export interface RepeatNode<BB extends object = Record<string, unknown>> {
|
|
111
|
+
readonly type: 'repeat';
|
|
112
|
+
readonly child: BehaviorTreeNode<BB>;
|
|
113
|
+
readonly count: number;
|
|
114
|
+
nodeIndex: number;
|
|
115
|
+
}
|
|
116
|
+
/** Decorator — prevents child re-entry for `duration` seconds after completion. */
|
|
117
|
+
export interface CooldownNode<BB extends object = Record<string, unknown>> {
|
|
118
|
+
readonly type: 'cooldown';
|
|
119
|
+
readonly child: BehaviorTreeNode<BB>;
|
|
120
|
+
readonly duration: number;
|
|
121
|
+
nodeIndex: number;
|
|
122
|
+
}
|
|
123
|
+
/** Decorator — conditional gate. Ticks child only when condition passes. */
|
|
124
|
+
export interface GuardNode<BB extends object = Record<string, unknown>> {
|
|
125
|
+
readonly type: 'guard';
|
|
126
|
+
readonly child: BehaviorTreeNode<BB>;
|
|
127
|
+
readonly condition: (ctx: BehaviorTreeContext<BB>) => boolean;
|
|
128
|
+
nodeIndex: number;
|
|
129
|
+
}
|
|
130
|
+
/** Union of all behavior tree node types. */
|
|
131
|
+
export type BehaviorTreeNode<BB extends object = Record<string, unknown>> = ActionNode<BB> | ConditionNode<BB> | SequenceNode<BB> | SelectorNode<BB> | ParallelNode<BB> | InverterNode<BB> | RepeatNode<BB> | CooldownNode<BB> | GuardNode<BB>;
|
|
132
|
+
/**
|
|
133
|
+
* Create an action leaf node.
|
|
134
|
+
*
|
|
135
|
+
* @param name - Human-readable name (used in abort events)
|
|
136
|
+
* @param tick - Called each frame while this node is active; return NodeStatus
|
|
137
|
+
* @param options - Optional `onAbort` callback fired when preempted by a higher-priority branch
|
|
138
|
+
*/
|
|
139
|
+
export declare function action<BB extends object>(name: string, tick: (ctx: BehaviorTreeContext<BB>) => NodeStatus, options?: {
|
|
140
|
+
onAbort?: (ctx: BehaviorTreeContext<BB>) => void;
|
|
141
|
+
}): ActionNode<BB>;
|
|
142
|
+
/**
|
|
143
|
+
* Create a condition leaf node.
|
|
144
|
+
*
|
|
145
|
+
* @param name - Human-readable name
|
|
146
|
+
* @param check - Predicate returning true (Success) or false (Failure). Never Running.
|
|
147
|
+
*/
|
|
148
|
+
export declare function condition<BB extends object>(name: string, check: (ctx: BehaviorTreeContext<BB>) => boolean): ConditionNode<BB>;
|
|
149
|
+
/**
|
|
150
|
+
* Create a sequence composite. Runs children L→R, fails on first failure.
|
|
151
|
+
*/
|
|
152
|
+
export declare function sequence<BB extends object>(children: BehaviorTreeNode<BB>[]): SequenceNode<BB>;
|
|
153
|
+
/**
|
|
154
|
+
* Create a selector composite. Runs children L→R, succeeds on first success.
|
|
155
|
+
* Always starts from child 0 to re-evaluate priority.
|
|
156
|
+
*/
|
|
157
|
+
export declare function selector<BB extends object>(children: BehaviorTreeNode<BB>[]): SelectorNode<BB>;
|
|
158
|
+
/**
|
|
159
|
+
* Create a parallel composite. Ticks all children each frame.
|
|
160
|
+
*
|
|
161
|
+
* @param children - Child nodes to tick in parallel
|
|
162
|
+
* @param options.successThreshold - Successes needed for parallel to succeed (default: all)
|
|
163
|
+
* @param options.failureThreshold - Failures needed for parallel to fail (default: all)
|
|
164
|
+
*/
|
|
165
|
+
export declare function parallel<BB extends object>(children: BehaviorTreeNode<BB>[], options?: {
|
|
166
|
+
successThreshold?: number;
|
|
167
|
+
failureThreshold?: number;
|
|
168
|
+
}): ParallelNode<BB>;
|
|
169
|
+
/** Create an inverter decorator. Flips Success↔Failure, passes Running. */
|
|
170
|
+
export declare function inverter<BB extends object>(child: BehaviorTreeNode<BB>): InverterNode<BB>;
|
|
171
|
+
/**
|
|
172
|
+
* Create a repeat decorator.
|
|
173
|
+
*
|
|
174
|
+
* @param child - Node to repeat
|
|
175
|
+
* @param count - Number of repetitions, or -1 for infinite (default: -1)
|
|
176
|
+
*/
|
|
177
|
+
export declare function repeat<BB extends object>(child: BehaviorTreeNode<BB>, count?: number): RepeatNode<BB>;
|
|
178
|
+
/**
|
|
179
|
+
* Create a cooldown decorator. Prevents re-entry for `duration` seconds
|
|
180
|
+
* after child completes (Success or Failure).
|
|
181
|
+
*/
|
|
182
|
+
export declare function cooldown<BB extends object>(child: BehaviorTreeNode<BB>, duration: number): CooldownNode<BB>;
|
|
183
|
+
/**
|
|
184
|
+
* Create a guard decorator. Ticks child only when condition returns true.
|
|
185
|
+
* Returns Failure when condition is false.
|
|
186
|
+
*/
|
|
187
|
+
export declare function guard<BB extends object>(cond: (ctx: BehaviorTreeContext<BB>) => boolean, child: BehaviorTreeNode<BB>): GuardNode<BB>;
|
|
188
|
+
/**
|
|
189
|
+
* Immutable behavior tree definition. Shared across entities.
|
|
190
|
+
*
|
|
191
|
+
* @template BB - Blackboard type for per-entity AI memory
|
|
192
|
+
*/
|
|
193
|
+
export interface BehaviorTreeDefinition<BB extends object = Record<string, unknown>> {
|
|
194
|
+
readonly id: string;
|
|
195
|
+
readonly root: BehaviorTreeNode<BB>;
|
|
196
|
+
readonly nodeCount: number;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Define a behavior tree with a typed blackboard.
|
|
200
|
+
*
|
|
201
|
+
* The `blackboard` value serves as both the type source and the default
|
|
202
|
+
* initial state cloned for each entity via `createBehaviorTree`.
|
|
203
|
+
*
|
|
204
|
+
* @param id - Unique identifier for this tree definition
|
|
205
|
+
* @param config - `{ blackboard, root }` — default blackboard + root node
|
|
206
|
+
* @returns Frozen BehaviorTreeDefinition
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* ```typescript
|
|
210
|
+
* const tree = defineBehaviorTree('patrol', {
|
|
211
|
+
* blackboard: { targetId: null as number | null, timer: 0 },
|
|
212
|
+
* root: selector([
|
|
213
|
+
* guard(ctx => ctx.blackboard.targetId !== null, action('chase', ...)),
|
|
214
|
+
* action('wander', ...),
|
|
215
|
+
* ]),
|
|
216
|
+
* });
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
export declare function defineBehaviorTree<BB extends object>(id: string, config: {
|
|
220
|
+
blackboard: BB;
|
|
221
|
+
root: BehaviorTreeNode<BB>;
|
|
222
|
+
}): BehaviorTreeDefinition<BB>;
|
|
223
|
+
/**
|
|
224
|
+
* Runtime behavior tree state stored on each entity.
|
|
225
|
+
*
|
|
226
|
+
* The `blackboard` is typed as `object` at the component level.
|
|
227
|
+
* Inside tree callbacks, the `BehaviorTreeContext<BB>` provides
|
|
228
|
+
* typed access to the blackboard via the tree definition's generic.
|
|
229
|
+
* Outside the tree, cast the blackboard to the specific BB type.
|
|
230
|
+
*/
|
|
231
|
+
export interface BehaviorTreeComponent {
|
|
232
|
+
readonly definition: BehaviorTreeDefinition<Record<string, unknown>>;
|
|
233
|
+
blackboard: object;
|
|
234
|
+
/** Index of the currently running leaf, or -1 if none. */
|
|
235
|
+
runningNodeIndex: number;
|
|
236
|
+
/**
|
|
237
|
+
* Dense per-node state array (sized to `definition.nodeCount`).
|
|
238
|
+
* Semantics vary by node type:
|
|
239
|
+
* - sequence/selector: child progress index
|
|
240
|
+
* - repeat: completed iteration count
|
|
241
|
+
* - cooldown: expiry timestamp (elapsedTime when cooldown ends)
|
|
242
|
+
*/
|
|
243
|
+
nodeState: Float64Array;
|
|
244
|
+
/** Accumulated time (seconds) for cooldown tracking. */
|
|
245
|
+
elapsedTime: number;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Component types provided by the behavior tree plugin.
|
|
249
|
+
*/
|
|
250
|
+
export interface BehaviorTreeComponentTypes {
|
|
251
|
+
behaviorTree: BehaviorTreeComponent;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Event published when a running action is preempted (aborted) by a
|
|
255
|
+
* higher-priority branch taking over.
|
|
256
|
+
*/
|
|
257
|
+
export interface BehaviorTreeAbortEvent {
|
|
258
|
+
entityId: number;
|
|
259
|
+
/** nodeIndex of the aborted action */
|
|
260
|
+
nodeIndex: number;
|
|
261
|
+
/** Human-readable name of the aborted action */
|
|
262
|
+
nodeName: string;
|
|
263
|
+
/** Definition id of the behavior tree */
|
|
264
|
+
definitionId: string;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Event types provided by the behavior tree plugin.
|
|
268
|
+
*/
|
|
269
|
+
export interface BehaviorTreeEventTypes {
|
|
270
|
+
behaviorTreeAbort: BehaviorTreeAbortEvent;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* WorldConfig representing the behavior tree plugin's provided types.
|
|
274
|
+
*/
|
|
275
|
+
export type BehaviorTreeWorldConfig = WorldConfigFrom<BehaviorTreeComponentTypes, BehaviorTreeEventTypes>;
|
|
276
|
+
/**
|
|
277
|
+
* Create a `behaviorTree` component from a definition.
|
|
278
|
+
*
|
|
279
|
+
* @param definition - Shared tree definition
|
|
280
|
+
* @param blackboard - Optional partial overrides for the default blackboard
|
|
281
|
+
* @returns Component object suitable for spreading into spawn()
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* ```typescript
|
|
285
|
+
* ecs.spawn({
|
|
286
|
+
* ...createBehaviorTree(villagerTree, { hunger: 80 }),
|
|
287
|
+
* ...createLocalTransform(100, 200),
|
|
288
|
+
* });
|
|
289
|
+
* ```
|
|
290
|
+
*/
|
|
291
|
+
export declare function createBehaviorTree<BB extends object>(definition: BehaviorTreeDefinition<BB>, blackboard?: Partial<BB>): Pick<BehaviorTreeComponentTypes, 'behaviorTree'>;
|
|
292
|
+
/**
|
|
293
|
+
* Check whether an entity's behavior tree has a running action.
|
|
294
|
+
*/
|
|
295
|
+
export declare function isBehaviorTreeRunning(ecs: {
|
|
296
|
+
getComponent(entityId: number, name: 'behaviorTree'): BehaviorTreeComponent | undefined;
|
|
297
|
+
}, entityId: number): boolean;
|
|
298
|
+
/**
|
|
299
|
+
* Reset an entity's behavior tree: abort any running action, clear all
|
|
300
|
+
* composite progress, and optionally reset the blackboard.
|
|
301
|
+
*/
|
|
302
|
+
export declare function resetBehaviorTree(ecs: BehaviorTreeWorld, entityId: number, blackboard?: Partial<Record<string, unknown>>): void;
|
|
303
|
+
/**
|
|
304
|
+
* Typed helpers for the behavior tree plugin.
|
|
305
|
+
* Creates helpers that validate callback parameters against the world type W.
|
|
306
|
+
* Call after `.build()` using `typeof ecs`.
|
|
307
|
+
*/
|
|
308
|
+
export interface BehaviorTreeHelpers<W extends BaseWorld<BehaviorTreeComponentTypes>> {
|
|
309
|
+
defineBehaviorTree: <BB extends object>(id: string, config: {
|
|
310
|
+
blackboard: BB;
|
|
311
|
+
root: BehaviorTreeNode<BB>;
|
|
312
|
+
}) => BehaviorTreeDefinition<BB>;
|
|
313
|
+
action: <BB extends object>(name: string, tick: (ctx: BehaviorTreeContext<BB, W>) => NodeStatus, options?: {
|
|
314
|
+
onAbort?: (ctx: BehaviorTreeContext<BB, W>) => void;
|
|
315
|
+
}) => ActionNode<BB>;
|
|
316
|
+
condition: <BB extends object>(name: string, check: (ctx: BehaviorTreeContext<BB, W>) => boolean) => ConditionNode<BB>;
|
|
317
|
+
guard: <BB extends object>(cond: (ctx: BehaviorTreeContext<BB, W>) => boolean, child: BehaviorTreeNode<BB>) => GuardNode<BB>;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Create typed behavior tree helpers bound to a specific world type.
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* ```typescript
|
|
324
|
+
* const ecs = ECSpresso.create()
|
|
325
|
+
* .withPlugin(createBehaviorTreePlugin())
|
|
326
|
+
* .build();
|
|
327
|
+
*
|
|
328
|
+
* const { defineBehaviorTree, action, condition, guard } = ecs.getHelpers(createBehaviorTreeHelpers);
|
|
329
|
+
* ```
|
|
330
|
+
*/
|
|
331
|
+
export declare function createBehaviorTreeHelpers<W extends BaseWorld<BehaviorTreeComponentTypes> = BehaviorTreeWorld>(_world?: W): BehaviorTreeHelpers<W>;
|
|
332
|
+
/**
|
|
333
|
+
* Configuration options for the behavior tree plugin.
|
|
334
|
+
*/
|
|
335
|
+
export interface BehaviorTreePluginOptions<G extends string = 'ai'> extends BasePluginOptions<G> {
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Create a behavior tree plugin for ECSpresso.
|
|
339
|
+
*
|
|
340
|
+
* Provides composable, priority-driven AI via behavior trees with:
|
|
341
|
+
* - Hybrid traversal: re-evaluate from root each tick, resume running leaves
|
|
342
|
+
* - Automatic abort with `onAbort` callback when preempted
|
|
343
|
+
* - Typed blackboard for per-entity AI memory
|
|
344
|
+
* - `behaviorTreeAbort` events on preemption
|
|
345
|
+
*
|
|
346
|
+
* @example
|
|
347
|
+
* ```typescript
|
|
348
|
+
* const ecs = ECSpresso.create()
|
|
349
|
+
* .withPlugin(createBehaviorTreePlugin())
|
|
350
|
+
* .build();
|
|
351
|
+
*
|
|
352
|
+
* const { defineBehaviorTree, action, condition, guard } = ecs.getHelpers(createBehaviorTreeHelpers);
|
|
353
|
+
*
|
|
354
|
+
* const tree = defineBehaviorTree('villager', {
|
|
355
|
+
* blackboard: { hunger: 100, targetId: null as number | null },
|
|
356
|
+
* root: selector([
|
|
357
|
+
* guard(ctx => ctx.blackboard.hunger < 30, action('eat', ...)),
|
|
358
|
+
* action('wander', ...),
|
|
359
|
+
* ]),
|
|
360
|
+
* });
|
|
361
|
+
*
|
|
362
|
+
* ecs.spawn({
|
|
363
|
+
* ...createBehaviorTree(tree),
|
|
364
|
+
* ...createLocalTransform(100, 200),
|
|
365
|
+
* });
|
|
366
|
+
* ```
|
|
367
|
+
*/
|
|
368
|
+
export declare function createBehaviorTreePlugin<G extends string = 'ai'>(options?: BehaviorTreePluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithEvents<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, BehaviorTreeComponentTypes>, BehaviorTreeEventTypes>, import("ecspresso").EmptyConfig, "behavior-tree-update", G, never, never>;
|
|
369
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
var $=Object.defineProperty;var G=(j)=>j;function P(j,z){this[j]=G.bind(null,z)}var T=(j,z)=>{for(var H in z)$(j,H,{get:z[H],enumerable:!0,configurable:!0,set:P.bind(z,H)})};var B=(j,z)=>()=>(j&&(z=j(j=0)),z);var C=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(z,H)=>(typeof require<"u"?require:z)[H]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as q}from"ecspresso";var J={Success:0,Failure:1,Running:2};function A(j,z,H){return{type:"action",name:j,tick:z,onAbort:H?.onAbort,nodeIndex:-1}}function W(j,z){return{type:"condition",name:j,check:z,nodeIndex:-1}}function v(j){return{type:"sequence",children:j,nodeIndex:-1}}function I(j){return{type:"selector",children:j,nodeIndex:-1}}function g(j,z){return{type:"parallel",children:j,successThreshold:z?.successThreshold??j.length,failureThreshold:z?.failureThreshold??j.length,nodeIndex:-1}}function p(j){return{type:"inverter",child:j,nodeIndex:-1}}function y(j,z=-1){return{type:"repeat",child:j,count:z,nodeIndex:-1}}function k(j,z){return{type:"cooldown",child:j,duration:z,nodeIndex:-1}}function D(j,z){return{type:"guard",condition:j,child:z,nodeIndex:-1}}var X=new WeakMap,Z=new WeakMap;function F(j,z){let H=0,E=[];function K(M){if(M.nodeIndex=H,E[H]=M,H++,"children"in M)for(let U of M.children)K(U);if("child"in M)K(M.child)}K(z.root);let L=Object.freeze({id:j,root:z.root,nodeCount:H});return X.set(L,E),Z.set(L,z.blackboard),L}function h(j,z){let E={...Z.get(j),...z};return{behaviorTree:{definition:j,blackboard:E,runningNodeIndex:-1,nodeState:new Float64Array(j.nodeCount),elapsedTime:0}}}function f(j,z){let H=j.getComponent(z,"behaviorTree");return H!==void 0&&H.runningNodeIndex!==-1}function S(j,z,H){let E=j.getComponent(z,"behaviorTree");if(!E)return;if(E.runningNodeIndex!==-1){let L=X.get(E.definition)?.[E.runningNodeIndex];if(L&&L.type==="action"&&L.onAbort)L.onAbort({ecs:j,entityId:z,dt:0,blackboard:E.blackboard});E.runningNodeIndex=-1}if(E.nodeState.fill(0),E.elapsedTime=0,H)Object.assign(E.blackboard,H)}function _(j,z){let E=X.get(j.definition)?.[j.runningNodeIndex];if(E&&E.type==="action")E.onAbort?.(z),z.ecs.eventBus.publish("behaviorTreeAbort",{entityId:z.entityId,nodeIndex:j.runningNodeIndex,nodeName:E.name,definitionId:j.definition.id});j.nodeState.fill(0),j.runningNodeIndex=-1}function Q(j,z,H){switch(j.type){case"condition":return j.check(H)?J.Success:J.Failure;case"action":{let E=j.tick(H);if(E===J.Running){if(z.runningNodeIndex!==-1&&z.runningNodeIndex!==j.nodeIndex)_(z,H);z.runningNodeIndex=j.nodeIndex}else if(z.runningNodeIndex===j.nodeIndex)z.runningNodeIndex=-1;return E}case"sequence":{let E=z.runningNodeIndex!==-1?z.nodeState[j.nodeIndex]??0:0;for(let K=E;K<j.children.length;K++){let L=Q(j.children[K],z,H);if(L===J.Failure)return z.nodeState[j.nodeIndex]=0,J.Failure;if(L===J.Running)return z.nodeState[j.nodeIndex]=K,J.Running}return z.nodeState[j.nodeIndex]=0,J.Success}case"selector":{for(let E=0;E<j.children.length;E++){let K=Q(j.children[E],z,H);if(K===J.Success)return J.Success;if(K===J.Running)return J.Running}return J.Failure}case"parallel":{let E=0,K=0,L=!1;for(let M=0;M<j.children.length;M++){let U=Q(j.children[M],z,H);if(U===J.Success)E++;else if(U===J.Failure)K++;else L=!0}if(E>=j.successThreshold)return J.Success;if(K>=j.failureThreshold)return J.Failure;if(L)return J.Running;return J.Failure}case"inverter":{let E=Q(j.child,z,H);if(E===J.Success)return J.Failure;if(E===J.Failure)return J.Success;return J.Running}case"repeat":{let E=z.nodeState[j.nodeIndex]??0,K=Q(j.child,z,H);if(K===J.Failure)return z.nodeState[j.nodeIndex]=0,J.Failure;if(K===J.Running)return J.Running;let L=E+1;if(j.count!==-1&&L>=j.count)return z.nodeState[j.nodeIndex]=0,J.Success;return z.nodeState[j.nodeIndex]=L,J.Running}case"cooldown":{let E=z.nodeState[j.nodeIndex]??0;if(E>0&&z.elapsedTime<E)return J.Failure;let K=Q(j.child,z,H);if(K!==J.Running)z.nodeState[j.nodeIndex]=z.elapsedTime+j.duration;return K}case"guard":{if(!j.condition(H))return J.Failure;return Q(j.child,z,H)}}}function x(j){return{defineBehaviorTree:F,action:A,condition:W,guard:D}}function N(j){let{systemGroup:z="ai",priority:H=0,phase:E="update"}=j??{};return q("behaviorTree").withComponentTypes().withEventTypes().withLabels().withGroups().install((K)=>{K.registerDispose("behaviorTree",({value:L,entityId:M})=>{if(L.runningNodeIndex!==-1){let O=X.get(L.definition)?.[L.runningNodeIndex];if(O&&O.type==="action"&&O.onAbort)O.onAbort({ecs:K,entityId:M,dt:0,blackboard:L.blackboard})}}),K.addSystem("behavior-tree-update").setPriority(H).inPhase(E).inGroup(z).addQuery("trees",{with:["behaviorTree"]}).setProcess(({queries:L,dt:M,ecs:U})=>{let O={ecs:U,entityId:0,dt:0,blackboard:{}};for(let Y of L.trees){let V=Y.components.behaviorTree;if(O.entityId=Y.id,O.dt=M,O.blackboard=V.blackboard,V.elapsedTime+=M,Q(V.definition.root,V,O)!==J.Running&&V.runningNodeIndex!==-1)_(V,O)}})})}export{v as sequence,I as selector,S as resetBehaviorTree,y as repeat,g as parallel,f as isBehaviorTreeRunning,p as inverter,D as guard,F as defineBehaviorTree,N as createBehaviorTreePlugin,x as createBehaviorTreeHelpers,h as createBehaviorTree,k as cooldown,W as condition,A as action,J as NodeStatus};
|
|
2
|
+
|
|
3
|
+
//# debugId=E741AFC03BF2211264756E2164756E21
|
|
4
|
+
//# sourceMappingURL=behavior-tree.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/plugins/ai/behavior-tree.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Behavior Tree Plugin for ECSpresso\n *\n * Provides composable, priority-driven AI via behavior trees. Shared immutable\n * tree definitions drive per-entity runtime state. Uses hybrid traversal\n * (Approach C): re-evaluate from root each tick to preserve priority, resume\n * running leaves, and abort diverged running nodes via `onAbort`.\n *\n * Each entity gets a `behaviorTree` component referencing a shared definition\n * plus a typed blackboard for per-entity AI memory. One system processes all\n * behavior-tree entities each tick.\n *\n * Node types:\n * Composites — sequence, selector, parallel\n * Decorators — inverter, repeat, cooldown, guard\n * Leaves — action (tick → NodeStatus, optional onAbort), condition (predicate)\n */\n\nimport { definePlugin, type BasePluginOptions, type WorldConfigFrom, type BaseWorld } from 'ecspresso';\n\n// ==================== NodeStatus ====================\n\n/**\n * Return value from behavior tree node ticks.\n *\n * - `Success` (0) — node completed successfully\n * - `Failure` (1) — node failed\n * - `Running` (2) — node still executing, will resume next tick\n */\nexport const NodeStatus = { Success: 0, Failure: 1, Running: 2 } as const;\nexport type NodeStatus = (typeof NodeStatus)[keyof typeof NodeStatus];\n\n// ==================== Callback Context ====================\n\n/** BaseWorld narrowed to behavior-tree components for typed access in helpers. */\ntype BehaviorTreeWorld = BaseWorld<BehaviorTreeComponentTypes>;\n\n/**\n * Context passed to all leaf node callbacks (action tick, condition check,\n * onAbort, guard predicates).\n *\n * @template BB - Blackboard type for per-entity AI memory\n * @template W - World interface type (default: BehaviorTreeWorld)\n */\nexport interface BehaviorTreeContext<\n\tBB extends object = Record<string, unknown>,\n\tW extends BaseWorld<BehaviorTreeComponentTypes> = BehaviorTreeWorld,\n> {\n\treadonly ecs: W;\n\treadonly entityId: number;\n\treadonly dt: number;\n\treadonly blackboard: BB;\n}\n\n// ==================== Node Types ====================\n\n/**\n * Action leaf — executes behavior each tick.\n * Returns `Running` for multi-frame actions. Optional `onAbort` fires\n * when a higher-priority branch preempts this running action.\n */\nexport interface ActionNode<BB extends object = Record<string, unknown>> {\n\treadonly type: 'action';\n\treadonly name: string;\n\treadonly tick: (ctx: BehaviorTreeContext<BB>) => NodeStatus;\n\treadonly onAbort?: (ctx: BehaviorTreeContext<BB>) => void;\n\tnodeIndex: number;\n}\n\n/**\n * Condition leaf — checks a predicate. Returns Success or Failure, never Running.\n */\nexport interface ConditionNode<BB extends object = Record<string, unknown>> {\n\treadonly type: 'condition';\n\treadonly name: string;\n\treadonly check: (ctx: BehaviorTreeContext<BB>) => boolean;\n\tnodeIndex: number;\n}\n\n/**\n * Sequence composite — runs children left-to-right.\n * Fails on first failure, succeeds when all succeed.\n * Resumes from stored child index when a running node exists.\n */\nexport interface SequenceNode<BB extends object = Record<string, unknown>> {\n\treadonly type: 'sequence';\n\treadonly children: readonly BehaviorTreeNode<BB>[];\n\tnodeIndex: number;\n}\n\n/**\n * Selector composite — runs children left-to-right.\n * Succeeds on first success, fails when all fail.\n * Always re-evaluates from child 0 to preserve priority ordering.\n */\nexport interface SelectorNode<BB extends object = Record<string, unknown>> {\n\treadonly type: 'selector';\n\treadonly children: readonly BehaviorTreeNode<BB>[];\n\tnodeIndex: number;\n}\n\n/**\n * Parallel composite — ticks all children each frame.\n * Configurable success/failure thresholds.\n *\n * Limitation (v1): only one running leaf is tracked for abort.\n * Other running children in a parallel stop being ticked if the\n * tree path diverges but do not receive an `onAbort` call.\n */\nexport interface ParallelNode<BB extends object = Record<string, unknown>> {\n\treadonly type: 'parallel';\n\treadonly children: readonly BehaviorTreeNode<BB>[];\n\treadonly successThreshold: number;\n\treadonly failureThreshold: number;\n\tnodeIndex: number;\n}\n\n/** Decorator — inverts child result (Success↔Failure), passes Running through. */\nexport interface InverterNode<BB extends object = Record<string, unknown>> {\n\treadonly type: 'inverter';\n\treadonly child: BehaviorTreeNode<BB>;\n\tnodeIndex: number;\n}\n\n/** Decorator — repeats child `count` times (or forever when count is -1). */\nexport interface RepeatNode<BB extends object = Record<string, unknown>> {\n\treadonly type: 'repeat';\n\treadonly child: BehaviorTreeNode<BB>;\n\treadonly count: number;\n\tnodeIndex: number;\n}\n\n/** Decorator — prevents child re-entry for `duration` seconds after completion. */\nexport interface CooldownNode<BB extends object = Record<string, unknown>> {\n\treadonly type: 'cooldown';\n\treadonly child: BehaviorTreeNode<BB>;\n\treadonly duration: number;\n\tnodeIndex: number;\n}\n\n/** Decorator — conditional gate. Ticks child only when condition passes. */\nexport interface GuardNode<BB extends object = Record<string, unknown>> {\n\treadonly type: 'guard';\n\treadonly child: BehaviorTreeNode<BB>;\n\treadonly condition: (ctx: BehaviorTreeContext<BB>) => boolean;\n\tnodeIndex: number;\n}\n\n/** Union of all behavior tree node types. */\nexport type BehaviorTreeNode<BB extends object = Record<string, unknown>> =\n\t| ActionNode<BB>\n\t| ConditionNode<BB>\n\t| SequenceNode<BB>\n\t| SelectorNode<BB>\n\t| ParallelNode<BB>\n\t| InverterNode<BB>\n\t| RepeatNode<BB>\n\t| CooldownNode<BB>\n\t| GuardNode<BB>;\n\n// ==================== Builder Functions ====================\n\n/**\n * Create an action leaf node.\n *\n * @param name - Human-readable name (used in abort events)\n * @param tick - Called each frame while this node is active; return NodeStatus\n * @param options - Optional `onAbort` callback fired when preempted by a higher-priority branch\n */\nexport function action<BB extends object>(\n\tname: string,\n\ttick: (ctx: BehaviorTreeContext<BB>) => NodeStatus,\n\toptions?: { onAbort?: (ctx: BehaviorTreeContext<BB>) => void },\n): ActionNode<BB> {\n\treturn { type: 'action', name, tick, onAbort: options?.onAbort, nodeIndex: -1 };\n}\n\n/**\n * Create a condition leaf node.\n *\n * @param name - Human-readable name\n * @param check - Predicate returning true (Success) or false (Failure). Never Running.\n */\nexport function condition<BB extends object>(\n\tname: string,\n\tcheck: (ctx: BehaviorTreeContext<BB>) => boolean,\n): ConditionNode<BB> {\n\treturn { type: 'condition', name, check, nodeIndex: -1 };\n}\n\n/**\n * Create a sequence composite. Runs children L→R, fails on first failure.\n */\nexport function sequence<BB extends object>(\n\tchildren: BehaviorTreeNode<BB>[],\n): SequenceNode<BB> {\n\treturn { type: 'sequence', children, nodeIndex: -1 };\n}\n\n/**\n * Create a selector composite. Runs children L→R, succeeds on first success.\n * Always starts from child 0 to re-evaluate priority.\n */\nexport function selector<BB extends object>(\n\tchildren: BehaviorTreeNode<BB>[],\n): SelectorNode<BB> {\n\treturn { type: 'selector', children, nodeIndex: -1 };\n}\n\n/**\n * Create a parallel composite. Ticks all children each frame.\n *\n * @param children - Child nodes to tick in parallel\n * @param options.successThreshold - Successes needed for parallel to succeed (default: all)\n * @param options.failureThreshold - Failures needed for parallel to fail (default: all)\n */\nexport function parallel<BB extends object>(\n\tchildren: BehaviorTreeNode<BB>[],\n\toptions?: { successThreshold?: number; failureThreshold?: number },\n): ParallelNode<BB> {\n\treturn {\n\t\ttype: 'parallel',\n\t\tchildren,\n\t\tsuccessThreshold: options?.successThreshold ?? children.length,\n\t\tfailureThreshold: options?.failureThreshold ?? children.length,\n\t\tnodeIndex: -1,\n\t};\n}\n\n/** Create an inverter decorator. Flips Success↔Failure, passes Running. */\nexport function inverter<BB extends object>(\n\tchild: BehaviorTreeNode<BB>,\n): InverterNode<BB> {\n\treturn { type: 'inverter', child, nodeIndex: -1 };\n}\n\n/**\n * Create a repeat decorator.\n *\n * @param child - Node to repeat\n * @param count - Number of repetitions, or -1 for infinite (default: -1)\n */\nexport function repeat<BB extends object>(\n\tchild: BehaviorTreeNode<BB>,\n\tcount = -1,\n): RepeatNode<BB> {\n\treturn { type: 'repeat', child, count, nodeIndex: -1 };\n}\n\n/**\n * Create a cooldown decorator. Prevents re-entry for `duration` seconds\n * after child completes (Success or Failure).\n */\nexport function cooldown<BB extends object>(\n\tchild: BehaviorTreeNode<BB>,\n\tduration: number,\n): CooldownNode<BB> {\n\treturn { type: 'cooldown', child, duration, nodeIndex: -1 };\n}\n\n/**\n * Create a guard decorator. Ticks child only when condition returns true.\n * Returns Failure when condition is false.\n */\nexport function guard<BB extends object>(\n\tcond: (ctx: BehaviorTreeContext<BB>) => boolean,\n\tchild: BehaviorTreeNode<BB>,\n): GuardNode<BB> {\n\treturn { type: 'guard', condition: cond, child, nodeIndex: -1 };\n}\n\n// ==================== Definition ====================\n\n/**\n * Immutable behavior tree definition. Shared across entities.\n *\n * @template BB - Blackboard type for per-entity AI memory\n */\nexport interface BehaviorTreeDefinition<BB extends object = Record<string, unknown>> {\n\treadonly id: string;\n\treadonly root: BehaviorTreeNode<BB>;\n\treadonly nodeCount: number;\n}\n\n/** Internal storage for definition data not exposed on the public interface. */\nconst defFlatNodes = new WeakMap<BehaviorTreeDefinition<object>, readonly BehaviorTreeNode<object>[]>();\nconst defDefaultBB = new WeakMap<BehaviorTreeDefinition<object>, object>();\n\n/**\n * Define a behavior tree with a typed blackboard.\n *\n * The `blackboard` value serves as both the type source and the default\n * initial state cloned for each entity via `createBehaviorTree`.\n *\n * @param id - Unique identifier for this tree definition\n * @param config - `{ blackboard, root }` — default blackboard + root node\n * @returns Frozen BehaviorTreeDefinition\n *\n * @example\n * ```typescript\n * const tree = defineBehaviorTree('patrol', {\n * blackboard: { targetId: null as number | null, timer: 0 },\n * root: selector([\n * guard(ctx => ctx.blackboard.targetId !== null, action('chase', ...)),\n * action('wander', ...),\n * ]),\n * });\n * ```\n */\nexport function defineBehaviorTree<BB extends object>(\n\tid: string,\n\tconfig: { blackboard: BB; root: BehaviorTreeNode<BB> },\n): BehaviorTreeDefinition<BB> {\n\tlet nextIndex = 0;\n\tconst flatNodes: BehaviorTreeNode<BB>[] = [];\n\n\tfunction indexTree(node: BehaviorTreeNode<BB>): void {\n\t\tnode.nodeIndex = nextIndex;\n\t\tflatNodes[nextIndex] = node;\n\t\tnextIndex++;\n\t\tif ('children' in node) {\n\t\t\tfor (const child of node.children) indexTree(child);\n\t\t}\n\t\tif ('child' in node) {\n\t\t\tindexTree(node.child);\n\t\t}\n\t}\n\tindexTree(config.root);\n\n\tconst def: BehaviorTreeDefinition<BB> = Object.freeze({ id, root: config.root, nodeCount: nextIndex });\n\tdefFlatNodes.set(def as BehaviorTreeDefinition<object>, flatNodes as readonly BehaviorTreeNode<object>[]);\n\tdefDefaultBB.set(def as BehaviorTreeDefinition<object>, config.blackboard);\n\treturn def;\n}\n\n// ==================== Per-Entity Component ====================\n\n/**\n * Runtime behavior tree state stored on each entity.\n *\n * The `blackboard` is typed as `object` at the component level.\n * Inside tree callbacks, the `BehaviorTreeContext<BB>` provides\n * typed access to the blackboard via the tree definition's generic.\n * Outside the tree, cast the blackboard to the specific BB type.\n */\nexport interface BehaviorTreeComponent {\n\treadonly definition: BehaviorTreeDefinition<Record<string, unknown>>;\n\tblackboard: object;\n\t/** Index of the currently running leaf, or -1 if none. */\n\trunningNodeIndex: number;\n\t/**\n\t * Dense per-node state array (sized to `definition.nodeCount`).\n\t * Semantics vary by node type:\n\t * - sequence/selector: child progress index\n\t * - repeat: completed iteration count\n\t * - cooldown: expiry timestamp (elapsedTime when cooldown ends)\n\t */\n\tnodeState: Float64Array;\n\t/** Accumulated time (seconds) for cooldown tracking. */\n\telapsedTime: number;\n}\n\n/**\n * Component types provided by the behavior tree plugin.\n */\nexport interface BehaviorTreeComponentTypes {\n\tbehaviorTree: BehaviorTreeComponent;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event published when a running action is preempted (aborted) by a\n * higher-priority branch taking over.\n */\nexport interface BehaviorTreeAbortEvent {\n\tentityId: number;\n\t/** nodeIndex of the aborted action */\n\tnodeIndex: number;\n\t/** Human-readable name of the aborted action */\n\tnodeName: string;\n\t/** Definition id of the behavior tree */\n\tdefinitionId: string;\n}\n\n/**\n * Event types provided by the behavior tree plugin.\n */\nexport interface BehaviorTreeEventTypes {\n\tbehaviorTreeAbort: BehaviorTreeAbortEvent;\n}\n\n// ==================== WorldConfig ====================\n\n/**\n * WorldConfig representing the behavior tree plugin's provided types.\n */\nexport type BehaviorTreeWorldConfig = WorldConfigFrom<BehaviorTreeComponentTypes, BehaviorTreeEventTypes>;\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a `behaviorTree` component from a definition.\n *\n * @param definition - Shared tree definition\n * @param blackboard - Optional partial overrides for the default blackboard\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createBehaviorTree(villagerTree, { hunger: 80 }),\n * ...createLocalTransform(100, 200),\n * });\n * ```\n */\nexport function createBehaviorTree<BB extends object>(\n\tdefinition: BehaviorTreeDefinition<BB>,\n\tblackboard?: Partial<BB>,\n): Pick<BehaviorTreeComponentTypes, 'behaviorTree'> {\n\tconst defaultBB = defDefaultBB.get(definition as BehaviorTreeDefinition<object>) as BB;\n\tconst bb = { ...defaultBB, ...blackboard };\n\treturn {\n\t\tbehaviorTree: {\n\t\t\tdefinition: definition as BehaviorTreeDefinition<Record<string, unknown>>,\n\t\t\tblackboard: bb,\n\t\t\trunningNodeIndex: -1,\n\t\t\tnodeState: new Float64Array(definition.nodeCount),\n\t\t\telapsedTime: 0,\n\t\t},\n\t};\n}\n\n/**\n * Check whether an entity's behavior tree has a running action.\n */\nexport function isBehaviorTreeRunning(\n\tecs: { getComponent(entityId: number, name: 'behaviorTree'): BehaviorTreeComponent | undefined },\n\tentityId: number,\n): boolean {\n\tconst bt = ecs.getComponent(entityId, 'behaviorTree');\n\treturn bt !== undefined && bt.runningNodeIndex !== -1;\n}\n\n/**\n * Reset an entity's behavior tree: abort any running action, clear all\n * composite progress, and optionally reset the blackboard.\n */\nexport function resetBehaviorTree(\n\tecs: BehaviorTreeWorld,\n\tentityId: number,\n\tblackboard?: Partial<Record<string, unknown>>,\n): void {\n\tconst bt = ecs.getComponent(entityId, 'behaviorTree');\n\tif (!bt) return;\n\n\tif (bt.runningNodeIndex !== -1) {\n\t\tconst flatNodes = defFlatNodes.get(bt.definition as BehaviorTreeDefinition<object>);\n\t\tconst node = flatNodes?.[bt.runningNodeIndex];\n\t\tif (node && node.type === 'action' && node.onAbort) {\n\t\t\tnode.onAbort({ ecs, entityId, dt: 0, blackboard: bt.blackboard as Record<string, unknown> });\n\t\t}\n\t\tbt.runningNodeIndex = -1;\n\t}\n\tbt.nodeState.fill(0);\n\tbt.elapsedTime = 0;\n\n\tif (blackboard) {\n\t\tObject.assign(bt.blackboard, blackboard);\n\t}\n}\n\n// ==================== Internal: Traversal ====================\n\n/** Internal shorthand — all runtime traversal uses the erased base types. */\ntype AnyNode = BehaviorTreeNode<Record<string, unknown>>;\n\n/** Mutable version of context for pre-allocation in the system loop. */\ninterface MutableCtx {\n\tecs: BehaviorTreeWorld;\n\tentityId: number;\n\tdt: number;\n\tblackboard: Record<string, unknown>;\n}\n\nfunction abortRunningNode(bt: BehaviorTreeComponent, ctx: MutableCtx): void {\n\tconst flatNodes = defFlatNodes.get(bt.definition as BehaviorTreeDefinition<object>);\n\tconst node = flatNodes?.[bt.runningNodeIndex] as AnyNode | undefined;\n\tif (node && node.type === 'action') {\n\t\tnode.onAbort?.(ctx);\n\t\tctx.ecs.eventBus.publish('behaviorTreeAbort', {\n\t\t\tentityId: ctx.entityId,\n\t\t\tnodeIndex: bt.runningNodeIndex,\n\t\t\tnodeName: node.name,\n\t\t\tdefinitionId: bt.definition.id,\n\t\t} satisfies BehaviorTreeAbortEvent);\n\t}\n\tbt.nodeState.fill(0);\n\tbt.runningNodeIndex = -1;\n}\n\nfunction tickNode(node: AnyNode, bt: BehaviorTreeComponent, ctx: MutableCtx): NodeStatus {\n\tswitch (node.type) {\n\t\tcase 'condition':\n\t\t\treturn node.check(ctx) ? NodeStatus.Success : NodeStatus.Failure;\n\n\t\tcase 'action': {\n\t\t\tconst result = node.tick(ctx);\n\t\t\tif (result === NodeStatus.Running) {\n\t\t\t\tif (bt.runningNodeIndex !== -1 && bt.runningNodeIndex !== node.nodeIndex) {\n\t\t\t\t\tabortRunningNode(bt, ctx);\n\t\t\t\t}\n\t\t\t\tbt.runningNodeIndex = node.nodeIndex;\n\t\t\t} else if (bt.runningNodeIndex === node.nodeIndex) {\n\t\t\t\tbt.runningNodeIndex = -1;\n\t\t\t}\n\t\t\treturn result;\n\t\t}\n\n\t\tcase 'sequence': {\n\t\t\tconst startChild = (bt.runningNodeIndex !== -1)\n\t\t\t\t? (bt.nodeState[node.nodeIndex] ?? 0)\n\t\t\t\t: 0;\n\t\t\tfor (let i = startChild; i < node.children.length; i++) {\n\t\t\t\tconst status = tickNode(node.children[i]!, bt, ctx);\n\t\t\t\tif (status === NodeStatus.Failure) {\n\t\t\t\t\tbt.nodeState[node.nodeIndex] = 0;\n\t\t\t\t\treturn NodeStatus.Failure;\n\t\t\t\t}\n\t\t\t\tif (status === NodeStatus.Running) {\n\t\t\t\t\tbt.nodeState[node.nodeIndex] = i;\n\t\t\t\t\treturn NodeStatus.Running;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbt.nodeState[node.nodeIndex] = 0;\n\t\t\treturn NodeStatus.Success;\n\t\t}\n\n\t\tcase 'selector': {\n\t\t\tfor (let i = 0; i < node.children.length; i++) {\n\t\t\t\tconst status = tickNode(node.children[i]!, bt, ctx);\n\t\t\t\tif (status === NodeStatus.Success) return NodeStatus.Success;\n\t\t\t\tif (status === NodeStatus.Running) return NodeStatus.Running;\n\t\t\t}\n\t\t\treturn NodeStatus.Failure;\n\t\t}\n\n\t\tcase 'parallel': {\n\t\t\tlet successCount = 0;\n\t\t\tlet failureCount = 0;\n\t\t\tlet anyRunning = false;\n\t\t\tfor (let i = 0; i < node.children.length; i++) {\n\t\t\t\tconst status = tickNode(node.children[i]!, bt, ctx);\n\t\t\t\tif (status === NodeStatus.Success) successCount++;\n\t\t\t\telse if (status === NodeStatus.Failure) failureCount++;\n\t\t\t\telse anyRunning = true;\n\t\t\t}\n\t\t\tif (successCount >= node.successThreshold) return NodeStatus.Success;\n\t\t\tif (failureCount >= node.failureThreshold) return NodeStatus.Failure;\n\t\t\tif (anyRunning) return NodeStatus.Running;\n\t\t\treturn NodeStatus.Failure;\n\t\t}\n\n\t\tcase 'inverter': {\n\t\t\tconst status = tickNode(node.child, bt, ctx);\n\t\t\tif (status === NodeStatus.Success) return NodeStatus.Failure;\n\t\t\tif (status === NodeStatus.Failure) return NodeStatus.Success;\n\t\t\treturn NodeStatus.Running;\n\t\t}\n\n\t\tcase 'repeat': {\n\t\t\tconst iteration = bt.nodeState[node.nodeIndex] ?? 0;\n\t\t\tconst status = tickNode(node.child, bt, ctx);\n\t\t\tif (status === NodeStatus.Failure) {\n\t\t\t\tbt.nodeState[node.nodeIndex] = 0;\n\t\t\t\treturn NodeStatus.Failure;\n\t\t\t}\n\t\t\tif (status === NodeStatus.Running) return NodeStatus.Running;\n\t\t\tconst next = iteration + 1;\n\t\t\tif (node.count !== -1 && next >= node.count) {\n\t\t\t\tbt.nodeState[node.nodeIndex] = 0;\n\t\t\t\treturn NodeStatus.Success;\n\t\t\t}\n\t\t\tbt.nodeState[node.nodeIndex] = next;\n\t\t\treturn NodeStatus.Running;\n\t\t}\n\n\t\tcase 'cooldown': {\n\t\t\tconst expiresAt = bt.nodeState[node.nodeIndex] ?? 0;\n\t\t\tif (expiresAt > 0 && bt.elapsedTime < expiresAt) return NodeStatus.Failure;\n\t\t\tconst status = tickNode(node.child, bt, ctx);\n\t\t\tif (status !== NodeStatus.Running) {\n\t\t\t\tbt.nodeState[node.nodeIndex] = bt.elapsedTime + node.duration;\n\t\t\t}\n\t\t\treturn status;\n\t\t}\n\n\t\tcase 'guard': {\n\t\t\tif (!node.condition(ctx)) return NodeStatus.Failure;\n\t\t\treturn tickNode(node.child, bt, ctx);\n\t\t}\n\t}\n}\n\n// ==================== Typed Helpers ====================\n\n/**\n * Typed helpers for the behavior tree plugin.\n * Creates helpers that validate callback parameters against the world type W.\n * Call after `.build()` using `typeof ecs`.\n */\nexport interface BehaviorTreeHelpers<W extends BaseWorld<BehaviorTreeComponentTypes>> {\n\tdefineBehaviorTree: <BB extends object>(\n\t\tid: string,\n\t\tconfig: { blackboard: BB; root: BehaviorTreeNode<BB> },\n\t) => BehaviorTreeDefinition<BB>;\n\taction: <BB extends object>(\n\t\tname: string,\n\t\ttick: (ctx: BehaviorTreeContext<BB, W>) => NodeStatus,\n\t\toptions?: { onAbort?: (ctx: BehaviorTreeContext<BB, W>) => void },\n\t) => ActionNode<BB>;\n\tcondition: <BB extends object>(\n\t\tname: string,\n\t\tcheck: (ctx: BehaviorTreeContext<BB, W>) => boolean,\n\t) => ConditionNode<BB>;\n\tguard: <BB extends object>(\n\t\tcond: (ctx: BehaviorTreeContext<BB, W>) => boolean,\n\t\tchild: BehaviorTreeNode<BB>,\n\t) => GuardNode<BB>;\n}\n\n/**\n * Create typed behavior tree helpers bound to a specific world type.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createBehaviorTreePlugin())\n * .build();\n *\n * const { defineBehaviorTree, action, condition, guard } = ecs.getHelpers(createBehaviorTreeHelpers);\n * ```\n */\nexport function createBehaviorTreeHelpers<\n\tW extends BaseWorld<BehaviorTreeComponentTypes> = BehaviorTreeWorld,\n>(_world?: W): BehaviorTreeHelpers<W> {\n\treturn {\n\t\tdefineBehaviorTree: defineBehaviorTree as BehaviorTreeHelpers<W>['defineBehaviorTree'],\n\t\taction: action as BehaviorTreeHelpers<W>['action'],\n\t\tcondition: condition as BehaviorTreeHelpers<W>['condition'],\n\t\tguard: guard as BehaviorTreeHelpers<W>['guard'],\n\t};\n}\n\n// ==================== Plugin Options ====================\n\n/**\n * Configuration options for the behavior tree plugin.\n */\nexport interface BehaviorTreePluginOptions<G extends string = 'ai'> extends BasePluginOptions<G> {}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a behavior tree plugin for ECSpresso.\n *\n * Provides composable, priority-driven AI via behavior trees with:\n * - Hybrid traversal: re-evaluate from root each tick, resume running leaves\n * - Automatic abort with `onAbort` callback when preempted\n * - Typed blackboard for per-entity AI memory\n * - `behaviorTreeAbort` events on preemption\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createBehaviorTreePlugin())\n * .build();\n *\n * const { defineBehaviorTree, action, condition, guard } = ecs.getHelpers(createBehaviorTreeHelpers);\n *\n * const tree = defineBehaviorTree('villager', {\n * blackboard: { hunger: 100, targetId: null as number | null },\n * root: selector([\n * guard(ctx => ctx.blackboard.hunger < 30, action('eat', ...)),\n * action('wander', ...),\n * ]),\n * });\n *\n * ecs.spawn({\n * ...createBehaviorTree(tree),\n * ...createLocalTransform(100, 200),\n * });\n * ```\n */\nexport function createBehaviorTreePlugin<G extends string = 'ai'>(\n\toptions?: BehaviorTreePluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'ai',\n\t\tpriority = 0,\n\t\tphase = 'update',\n\t} = options ?? {};\n\n\treturn definePlugin('behaviorTree')\n\t\t.withComponentTypes<BehaviorTreeComponentTypes>()\n\t\t.withEventTypes<BehaviorTreeEventTypes>()\n\t\t.withLabels<'behavior-tree-update'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\t// Dispose: abort running node on entity removal\n\t\t\tworld.registerDispose('behaviorTree', ({ value, entityId }) => {\n\t\t\t\tif (value.runningNodeIndex !== -1) {\n\t\t\t\t\tconst flatNodes = defFlatNodes.get(value.definition as BehaviorTreeDefinition<object>);\n\t\t\t\t\tconst node = flatNodes?.[value.runningNodeIndex] as AnyNode | undefined;\n\t\t\t\t\tif (node && node.type === 'action' && node.onAbort) {\n\t\t\t\t\t\tnode.onAbort({\n\t\t\t\t\t\t\tecs: world as unknown as BehaviorTreeWorld,\n\t\t\t\t\t\t\tentityId,\n\t\t\t\t\t\t\tdt: 0,\n\t\t\t\t\t\t\tblackboard: value.blackboard as Record<string, unknown>,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tworld\n\t\t\t\t.addSystem('behavior-tree-update')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('trees', {\n\t\t\t\t\twith: ['behaviorTree'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, dt, ecs: ecsWorld }) => {\n\t\t\t\t\tconst ctx: MutableCtx = {\n\t\t\t\t\t\tecs: ecsWorld as unknown as BehaviorTreeWorld,\n\t\t\t\t\t\tentityId: 0,\n\t\t\t\t\t\tdt: 0,\n\t\t\t\t\t\tblackboard: {},\n\t\t\t\t\t};\n\n\t\t\t\t\tfor (const entity of queries.trees) {\n\t\t\t\t\t\tconst bt = entity.components.behaviorTree;\n\t\t\t\t\t\tctx.entityId = entity.id;\n\t\t\t\t\t\tctx.dt = dt;\n\t\t\t\t\t\tctx.blackboard = bt.blackboard as Record<string, unknown>;\n\t\t\t\t\t\tbt.elapsedTime += dt;\n\n\t\t\t\t\t\tconst result = tickNode(bt.definition.root as AnyNode, bt, ctx);\n\n\t\t\t\t\t\tif (result !== NodeStatus.Running && bt.runningNodeIndex !== -1) {\n\t\t\t\t\t\t\tabortRunningNode(bt, ctx);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "E741AFC03BF2211264756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var C=((k)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(
|
|
1
|
+
var b=Object.defineProperty;var q=(j)=>j;function C(j,k){this[j]=q.bind(null,k)}var F=(j,k)=>{for(var A in k)b(j,A,{get:k[A],enumerable:!0,configurable:!0,set:C.bind(k,A)})};var g=(j,k)=>()=>(j&&(k=j(j=0)),k);var x=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(k,A)=>(typeof require<"u"?require:k)[A]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as R}from"ecspresso";function w(j,k,A=32){return{detector:{range:j,layerFilter:k,maxResults:A}}}function I(j,k){return(j.getComponent(k,"detectedEntities")?.entities.length??0)>0}function h(j,k){return j.distanceSq-k.distanceSq}function m(j){let{systemGroup:k="ai",priority:A=500,phase:G="update"}=j??{},V=new Map,K=new Set,W=new Set,Z=new WeakMap;return R("detection").withComponentTypes().withEventTypes().withLabels().withGroups().requires().install((_)=>{_.registerDispose("detector",({entityId:X})=>{V.delete(X)}),_.addSystem("detection-scan").setPriority(A).inPhase(G).inGroup(k).addQuery("detectors",{with:["detector","worldTransform"]}).setProcess(({queries:X,ecs:E})=>{let P=E.getResource("spatialIndex");for(let H of X.detectors){let{detector:J,worldTransform:O}=H.components;W.clear(),P.queryRadiusInto(O.x,O.y,J.range,W);let L=[],Q=Z.get(J.layerFilter);if(!Q)Q=new Set(J.layerFilter),Z.set(J.layerFilter,Q);for(let z of W){if(z===H.id)continue;if(!E.getEntity(z))continue;let N=E.getComponent(z,"collisionLayer");if(!N)continue;if(!Q.has(N.layer))continue;let Y=E.getComponent(z,"worldTransform");if(!Y)continue;let B=Y.x-O.x,D=Y.y-O.y;L.push({entityId:z,distanceSq:B*B+D*D})}L.sort(h);let U=L.length>J.maxResults?L.slice(0,J.maxResults):L,$=E.getComponent(H.id,"detectedEntities");if($)$.entities=U,E.markChanged(H.id,"detectedEntities");else E.addComponent(H.id,"detectedEntities",{entities:U});let M=V.get(H.id);K.clear();for(let z of U)K.add(z.entityId);if(M){for(let z of K)if(!M.has(z))E.eventBus.publish("detectionGained",{entityId:H.id,detectedId:z});for(let z of M)if(!K.has(z))E.eventBus.publish("detectionLost",{entityId:H.id,lostId:z});M.clear();for(let z of K)M.add(z)}else{let z=new Set;for(let N of U)z.add(N.entityId),E.eventBus.publish("detectionGained",{entityId:H.id,detectedId:N.entityId});V.set(H.id,z)}}})})}export{I as hasDetectedTargets,w as createDetector,m as createDetectionPlugin};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=A1D4A431370EEBBE64756E2164756E21
|
|
4
4
|
//# sourceMappingURL=detection.js.map
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"/**\n * Detection Plugin for ECSpresso\n *\n * Provides automatic proximity detection for entities. Entities with a\n * `detector` component get their `detectedEntities` populated each frame\n * with nearby entities that match the configured collision layer filter,\n * sorted by distance ascending (nearest first).\n *\n * Uses the spatial-index plugin for efficient range queries.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\nimport type { WorldConfigFrom } from 'ecspresso';\nimport type { TransformWorldConfig } from '../spatial/transform';\nimport type { SpatialIndexResourceTypes } from '../spatial/spatial-index';\nimport type { CollisionComponentTypes } from '../physics/collision';\n\n// ==================== Component Types ====================\n\n/**\n * Configures proximity detection for an entity.\n */\nexport interface Detector {\n\t/** Detection radius in world units */\n\trange: number;\n\t/** Only detect entities on these collision layers */\n\tlayerFilter: readonly string[];\n\t/** Maximum number of results to track (default: 32) */\n\tmaxResults: number;\n}\n\n/**\n * A detected entity with its squared distance from the detector.\n */\nexport interface DetectedEntry {\n\tentityId: number;\n\tdistanceSq: number;\n}\n\n/**\n * Auto-populated list of detected entities, sorted by distance ascending.\n */\nexport interface DetectedEntities {\n\tentities: readonly DetectedEntry[];\n}\n\n/**\n * Component types provided by the detection plugin.\n */\nexport interface DetectionComponentTypes {\n\tdetector: Detector;\n\tdetectedEntities: DetectedEntities;\n}\n\n// ==================== Event Types ====================\n\n/**\n * Event fired when a new entity enters detection range.\n */\nexport interface DetectionGainedEvent {\n\t/** The entity doing the detecting */\n\tentityId: number;\n\t/** The entity that was detected */\n\tdetectedId: number;\n}\n\n/**\n * Event fired when an entity leaves detection range.\n */\nexport interface DetectionLostEvent {\n\t/** The entity doing the detecting */\n\tentityId: number;\n\t/** The entity that was lost */\n\tlostId: number;\n}\n\n/**\n * Event types provided by the detection plugin.\n */\nexport interface DetectionEventTypes {\n\tdetectionGained: DetectionGainedEvent;\n\tdetectionLost: DetectionLostEvent;\n}\n\n// ==================== WorldConfig ====================\n\n/**\n * WorldConfig representing the detection plugin's provided types.\n */\nexport type DetectionWorldConfig = WorldConfigFrom<DetectionComponentTypes, DetectionEventTypes>;\n\n// ==================== Plugin Options ====================\n\nexport interface DetectionPluginOptions<G extends string = 'ai'> extends BasePluginOptions<G> {}\n\n// ==================== Helper Functions ====================\n\n/**\n * Create a detector component.\n *\n * @param range Detection radius in world units\n * @param layerFilter Only detect entities on these collision layers\n * @param maxResults Maximum results to track (default: 32)\n * @returns Component object suitable for spreading into spawn()\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * ...createDetector(300, ['enemy']),\n * ...createLocalTransform(400, 400),\n * });\n * ```\n */\nexport function createDetector(\n\trange: number,\n\tlayerFilter: readonly string[],\n\tmaxResults = 32,\n): Pick<DetectionComponentTypes, 'detector'> {\n\treturn { detector: { range, layerFilter, maxResults } };\n}\n\n/**\n * Check whether an entity has any detected targets.\n *\n * @param ecs ECS world instance\n * @param entityId Entity with a detector component\n * @returns true if detectedEntities contains at least one entry\n *\n * @example\n * ```typescript\n * if (hasDetectedTargets(ecs, guardId)) {\n * // transition to chase\n * }\n * ```\n */\nexport function hasDetectedTargets(\n\tecs: { getComponent(entityId: number, name: 'detectedEntities'): DetectedEntities | undefined },\n\tentityId: number,\n): boolean {\n\tconst detected = ecs.getComponent(entityId, 'detectedEntities');\n\treturn (detected?.entities.length ?? 0) > 0;\n}\n\n// ==================== Plugin Factory ====================\n\nfunction compareByDistance(a: DetectedEntry, b: DetectedEntry): number {\n\treturn a.distanceSq - b.distanceSq;\n}\n\n/**\n * Create a detection plugin for ECSpresso.\n *\n * Populates `detectedEntities` each frame with nearby entities matching\n * the detector's layer filter, sorted by distance (nearest first).\n * Publishes `detectionGained`/`detectionLost` events on transitions.\n *\n * Requires the spatial-index and transform plugins to be installed.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createCollisionPlugin({ layers }))\n * .withPlugin(createSpatialIndexPlugin())\n * .withPlugin(createDetectionPlugin())\n * .build();\n *\n * // Read nearest detected entity:\n * const detected = ecs.getComponent(turretId, 'detectedEntities');\n * const nearest = detected?.entities[0];\n * ```\n */\nexport function createDetectionPlugin<G extends string = 'ai'>(\n\toptions?: DetectionPluginOptions<G>,\n) {\n\tconst {\n\t\tsystemGroup = 'ai',\n\t\tpriority = 500,\n\t\tphase = 'update',\n\t} = options ?? {};\n\n\t// Per-detector tracking of previous frame's detected set for event diffing\n\tconst previousSets = new Map<number, Set<number>>();\n\tconst currentSet = new Set<number>();\n\t// Reusable set for spatial index queries (avoids allocation per frame)\n\tconst candidateSet = new Set<number>();\n\t// Cache: layerFilter array → Set for O(1) lookups\n\tconst layerFilterCache = new WeakMap<readonly string[], Set<string>>();\n\n\treturn definePlugin('detection')\n\t\t.withComponentTypes<DetectionComponentTypes>()\n\t\t.withEventTypes<DetectionEventTypes>()\n\t\t.withLabels<'detection-scan'>()\n\t\t.withGroups<G>()\n\t\t.requires<\n\t\t\tTransformWorldConfig &\n\t\t\tWorldConfigFrom<Pick<CollisionComponentTypes<string>, 'collisionLayer'>> &\n\t\t\tWorldConfigFrom<{}, {}, SpatialIndexResourceTypes>\n\t\t>()\n\t\t.install((world) => {\n\t\t\tworld.registerDispose('detector', ({ entityId }) => {\n\t\t\t\tpreviousSets.delete(entityId);\n\t\t\t});\n\n\t\t\tworld\n\t\t\t\t.addSystem('detection-scan')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('detectors', {\n\t\t\t\t\twith: ['detector', 'worldTransform'],\n\t\t\t\t})\n\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\tconst spatialIndex = ecs.getResource('spatialIndex');\n\n\t\t\t\t\tfor (const entity of queries.detectors) {\n\t\t\t\t\t\tconst { detector, worldTransform } = entity.components;\n\n\t\t\t\t\t\tcandidateSet.clear();\n\t\t\t\t\t\tspatialIndex.queryRadiusInto(worldTransform.x, worldTransform.y, detector.range, candidateSet);\n\n\t\t\t\t\t\t// Build sorted results, filtering by layer and excluding self\n\t\t\t\t\t\tconst entries: DetectedEntry[] = [];\n\n\t\t\t\t\t\tlet filterSet = layerFilterCache.get(detector.layerFilter);\n\t\t\t\t\t\tif (!filterSet) {\n\t\t\t\t\t\t\tfilterSet = new Set(detector.layerFilter);\n\t\t\t\t\t\t\tlayerFilterCache.set(detector.layerFilter, filterSet);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tfor (const candidateId of candidateSet) {\n\t\t\t\t\t\t\tif (candidateId === entity.id) continue;\n\t\t\t\t\t\t\tif (!ecs.getEntity(candidateId)) continue;\n\n\t\t\t\t\t\t\tconst layer = ecs.getComponent(candidateId, 'collisionLayer');\n\t\t\t\t\t\t\tif (!layer) continue;\n\t\t\t\t\t\t\tif (!filterSet.has(layer.layer)) continue;\n\n\t\t\t\t\t\t\tconst candidateTransform = ecs.getComponent(candidateId, 'worldTransform');\n\t\t\t\t\t\t\tif (!candidateTransform) continue;\n\n\t\t\t\t\t\t\tconst dx = candidateTransform.x - worldTransform.x;\n\t\t\t\t\t\t\tconst dy = candidateTransform.y - worldTransform.y;\n\t\t\t\t\t\t\tentries.push({ entityId: candidateId, distanceSq: dx * dx + dy * dy });\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tentries.sort(compareByDistance);\n\t\t\t\t\t\tconst capped = entries.length > detector.maxResults\n\t\t\t\t\t\t\t? entries.slice(0, detector.maxResults)\n\t\t\t\t\t\t\t: entries;\n\n\t\t\t\t\t\t// Update or add the detectedEntities component\n\t\t\t\t\t\tconst existing = ecs.getComponent(entity.id, 'detectedEntities');\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\t(existing as { entities: readonly DetectedEntry[] }).entities = capped;\n\t\t\t\t\t\t\tecs.markChanged(entity.id, 'detectedEntities');\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tecs.addComponent(entity.id, 'detectedEntities', { entities: capped });\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Diff against previous frame for events\n\t\t\t\t\t\tconst prev = previousSets.get(entity.id);\n\t\t\t\t\t\tcurrentSet.clear();\n\t\t\t\t\t\tfor (const entry of capped) {\n\t\t\t\t\t\t\tcurrentSet.add(entry.entityId);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (prev) {\n\t\t\t\t\t\t\t// Detect gained\n\t\t\t\t\t\t\tfor (const id of currentSet) {\n\t\t\t\t\t\t\t\tif (!prev.has(id)) {\n\t\t\t\t\t\t\t\t\tecs.eventBus.publish('detectionGained', {\n\t\t\t\t\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t\t\t\t\t\tdetectedId: id,\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// Detect lost\n\t\t\t\t\t\t\tfor (const id of prev) {\n\t\t\t\t\t\t\t\tif (!currentSet.has(id)) {\n\t\t\t\t\t\t\t\t\tecs.eventBus.publish('detectionLost', {\n\t\t\t\t\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t\t\t\t\t\tlostId: id,\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// Update previous set in place\n\t\t\t\t\t\t\tprev.clear();\n\t\t\t\t\t\t\tfor (const id of currentSet) {\n\t\t\t\t\t\t\t\tprev.add(id);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// First frame — all are gained\n\t\t\t\t\t\t\tconst newSet = new Set<number>();\n\t\t\t\t\t\t\tfor (const entry of capped) {\n\t\t\t\t\t\t\t\tnewSet.add(entry.entityId);\n\t\t\t\t\t\t\t\tecs.eventBus.publish('detectionGained', {\n\t\t\t\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t\t\t\t\tdetectedId: entry.entityId,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tpreviousSets.set(entity.id, newSet);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "A1D4A431370EEBBE64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var o=Object.defineProperty;var zz=(z)=>z;function Jz(z,J){this[z]=zz.bind(null,J)}var Dz=(z,J)=>{for(var Q in J)o(z,Q,{get:J[Q],enumerable:!0,configurable:!0,set:Jz.bind(J,Q)})};var Hz=(z,J)=>()=>(z&&(J=z(z=0)),J);var Gz=((z)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(z,{get:(J,Q)=>(typeof require<"u"?require:J)[Q]}):z)(function(z){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+z+'" is not supported')});import{definePlugin as Oz}from"ecspresso";var S={normalX:0,normalY:0,depth:0},s=0;function c(z,J,Q,U,$,j,Z,K){if(z.entityId=J,z.layer=$,z.collidesWith=j,Z)return z.x=Q+(Z.offsetX??0),z.y=U+(Z.offsetY??0),z.shape=0,z.halfWidth=Z.width/2,z.halfHeight=Z.height/2,z.radius=0,!0;if(K)return z.x=Q+(K.offsetX??0),z.y=U+(K.offsetY??0),z.shape=1,z.halfWidth=0,z.halfHeight=0,z.radius=K.radius,!0;return!1}function Kz(z,J,Q,U,$,j,Z,K,H){let L=$-z,k=j-J,O=Q+Z-Math.abs(L),G=U+K-Math.abs(k);if(O<=0||G<=0)return!1;if(O<G)return H.normalX=L>=0?1:-1,H.normalY=0,H.depth=O,!0;return H.normalX=0,H.normalY=k>=0?1:-1,H.depth=G,!0}function Qz(z,J,Q,U,$,j,Z){let K=U-z,H=$-J,L=K*K+H*H,k=Q+j;if(L>=k*k)return!1;let O=Math.sqrt(L);if(O===0)return Z.normalX=1,Z.normalY=0,Z.depth=k,!0;return Z.normalX=K/O,Z.normalY=H/O,Z.depth=k-O,!0}function r(z,J,Q,U,$,j,Z,K){let H=Math.max(z-Q,Math.min($,z+Q)),L=Math.max(J-U,Math.min(j,J+U)),k=$-H,O=j-L,G=k*k+O*O;if(G>=Z*Z)return!1;if(G===0){let D=$-(z-Q),V=z+Q-$,E=j-(J-U),W=J+U-j,_=Math.min(D,V,E,W);if(_===V)return K.normalX=1,K.normalY=0,K.depth=V+Z,!0;if(_===D)return K.normalX=-1,K.normalY=0,K.depth=D+Z,!0;if(_===W)return K.normalX=0,K.normalY=1,K.depth=W+Z,!0;return K.normalX=0,K.normalY=-1,K.depth=E+Z,!0}let N=Math.sqrt(G);return K.normalX=k/N,K.normalY=O/N,K.depth=Z-N,!0}function n(z,J,Q){if(z.shape===0&&J.shape===0)return Kz(z.x,z.y,z.halfWidth,z.halfHeight,J.x,J.y,J.halfWidth,J.halfHeight,Q);if(z.shape===1&&J.shape===1)return Qz(z.x,z.y,z.radius,J.x,J.y,J.radius,Q);if(z.shape===0&&J.shape===1)return r(z.x,z.y,z.halfWidth,z.halfHeight,J.x,J.y,J.radius,Q);if(!r(J.x,J.y,J.halfWidth,J.halfHeight,z.x,z.y,z.radius,Q))return!1;return Q.normalX=-Q.normalX,Q.normalY=-Q.normalY,!0}var g=new Set,l=!1,Zz=50;function i(z,J,Q,U,$,j){if(U)jz(z,J,Q,U,$,j);else $z(z,J,$,j)}function $z(z,J,Q,U){if(!l&&J>=Zz)l=!0,console.warn(`[ecspresso] Collision detection is using O(n²) brute force with ${J} colliders. For better performance, install createSpatialIndexPlugin() alongside your collision or physics2D plugin.`);for(let $=0;$<J;$++){let j=z[$];if(!j)continue;for(let Z=$+1;Z<J;Z++){let K=z[Z];if(!K)continue;if(!j.collidesWith.includes(K.layer)&&!K.collidesWith.includes(j.layer))continue;if(!n(j,K,S))continue;Q(j,K,S,U)}}}function jz(z,J,Q,U,$,j){Q.clear();for(let Z=0;Z<J;Z++){let K=z[Z];if(!K)continue;Q.set(K.entityId,K)}for(let Z=0;Z<J;Z++){let K=z[Z];if(!K)continue;let H=K.shape===0?K.halfWidth:K.radius,L=K.shape===0?K.halfHeight:K.radius;g.clear(),U.queryRectInto(K.x-H,K.y-L,K.x+H,K.y+L,g);for(let k of g){if(k<=K.entityId)continue;let O=Q.get(k);if(!O)continue;if(!K.collidesWith.includes(O.layer)&&!O.collidesWith.includes(K.layer))continue;if(!n(K,O,S))continue;$(K,O,S,j)}}}function Ez(z,J){return{rigidBody:{type:z,mass:z==="static"?1/0:J?.mass??1,drag:J?.drag??0,restitution:J?.restitution??0,friction:J?.friction??0,gravityScale:J?.gravityScale??1},force:{x:0,y:0}}}function Rz(z,J){return{force:{x:z,y:J}}}function e(z,J,Q,U){let $=z.getComponent(J,"force");if(!$)return;$.x+=Q,$.y+=U}function Pz(z,J,Q,U){let $=z.getComponent(J,"velocity"),j=z.getComponent(J,"rigidBody");if(!$||!j)return;if(j.mass===1/0||j.mass===0)return;$.x+=Q/j.mass,$.y+=U/j.mass}function Tz(z,J,Q,U){let $=z.getComponent(J,"velocity");if(!$)return;$.x=Q,$.y=U}var w={entityA:0,entityB:0,normalX:0,normalY:0,depth:0};function Uz(z,J,Q,U){let $=z.rigidBody.type==="dynamic"&&z.rigidBody.mass>0&&z.rigidBody.mass!==1/0?1/z.rigidBody.mass:0,j=J.rigidBody.type==="dynamic"&&J.rigidBody.mass>0&&J.rigidBody.mass!==1/0?1/J.rigidBody.mass:0,Z=$+j;if(Z>0){let K=Q.depth/Z;if($>0){let O=U.getComponent(z.entityId,"localTransform");if(!O)return;O.x-=K*$*Q.normalX,O.y-=K*$*Q.normalY,z.x=O.x,U.markChanged(z.entityId,"localTransform")}if(j>0){let O=U.getComponent(J.entityId,"localTransform");if(!O)return;O.x+=K*j*Q.normalX,O.y+=K*j*Q.normalY,U.markChanged(J.entityId,"localTransform")}let H=J.velocity.x-z.velocity.x,L=J.velocity.y-z.velocity.y,k=H*Q.normalX+L*Q.normalY;if(k<0){let G=-(1+Math.min(z.rigidBody.restitution,J.rigidBody.restitution))*k/Z;z.velocity.x-=G*$*Q.normalX,z.velocity.y-=G*$*Q.normalY,J.velocity.x+=G*j*Q.normalX,J.velocity.y+=G*j*Q.normalY;let N=H-k*Q.normalX,D=L-k*Q.normalY,V=Math.sqrt(N*N+D*D);if(V>0.000001){let E=N/V,W=D/V,A=Math.sqrt(z.rigidBody.friction*J.rigidBody.friction)*Math.abs(G),T=Math.min(V/Z,A);z.velocity.x+=T*$*E,z.velocity.y+=T*$*W,J.velocity.x-=T*j*E,J.velocity.y-=T*j*W}}U.markChanged(z.entityId,"velocity"),U.markChanged(J.entityId,"velocity")}w.entityA=z.entityId,w.entityB=J.entityId,w.normalX=Q.normalX,w.normalY=Q.normalY,w.depth=Q.depth,U.eventBus.publish("physicsCollision",w)}function qz(z){let{gravity:J={x:0,y:0},systemGroup:Q="physics2D",collisionSystemGroup:U,integrationPriority:$=1000,collisionPriority:j=900,phase:Z="fixedUpdate"}=z??{};return Oz("physics2D").withComponentTypes().withEventTypes().withResourceTypes().withLabels().withGroups().requires().install((K)=>{K.registerRequired("rigidBody","velocity",()=>({x:0,y:0})),K.registerRequired("rigidBody","force",()=>({x:0,y:0})),K.addResource("physicsConfig",{gravity:{x:J.x,y:J.y}}),K.addSystem("physics2D-integration").setPriority($).inPhase(Z).inGroup(Q).addQuery("bodies",{with:["localTransform","velocity","rigidBody","force"]}).setProcess(({queries:N,dt:D,ecs:V})=>{let{gravity:E}=V.getResource("physicsConfig"),W=E.x,_=E.y;for(let A of N.bodies){let{localTransform:T,velocity:R,rigidBody:P,force:q}=A.components;if(P.type==="static")continue;if(P.type==="dynamic"){if(R.x+=W*P.gravityScale*D,R.y+=_*P.gravityScale*D,P.mass>0&&P.mass!==1/0)R.x+=q.x/P.mass*D,R.y+=q.y/P.mass*D;if(P.drag>0){let M=Math.max(0,1-P.drag*D);R.x*=M,R.y*=M}}T.x+=R.x*D,T.y+=R.y*D,q.x=0,q.y=0,V.markChanged(A.id,"localTransform")}});let H=K.addSystem("physics2D-collision").setPriority(j).inPhase(Z).inGroup(Q);if(U)H.inGroup(U);let L=[],k=new Map,O,G=!1;H.addQuery("collidables",{with:["localTransform","rigidBody","velocity","collisionLayer"]}).setProcess(({queries:N,ecs:D})=>{let V=0;for(let E of N.collidables){let{localTransform:W,rigidBody:_,velocity:A,collisionLayer:T}=E.components,R=D.getComponent(E.id,"aabbCollider"),P=R?void 0:D.getComponent(E.id,"circleCollider");if(!R&&!P)continue;let q=L[V];if(!q)q={entityId:E.id,x:W.x,y:W.y,layer:T.layer,collidesWith:T.collidesWith,shape:s,halfWidth:0,halfHeight:0,radius:0,rigidBody:_,velocity:A},L[V]=q;else q.rigidBody=_,q.velocity=A;if(!c(q,E.id,W.x,W.y,T.layer,T.collidesWith,R,P))continue;V++}if(!G)O=D.tryGetResource("spatialIndex"),G=!0;i(L,V,k,O,Uz,D)})})}import{definePlugin as kz}from"ecspresso";function Xz(z){return{flockingAgent:{perceptionRadius:z?.perceptionRadius??100,separationWeight:z?.separationWeight??1.5,alignmentWeight:z?.alignmentWeight??1,cohesionWeight:z?.cohesionWeight??1,maxForce:z?.maxForce??400,maxSpeed:z?.maxSpeed??200,flockGroup:z?.flockGroup??0}}}var h=new Set,a=0.01;function Yz(z){let{systemGroup:J="ai",priority:Q=500,phase:U="update",headingPriority:$=200}=z??{};return kz("flocking").withComponentTypes().withLabels().withGroups().requires().install((j)=>{j.addSystem("flocking-forces").setPriority(Q).inPhase(U).inGroup(J).addQuery("boids",{with:["flockingAgent","worldTransform","velocity","force"]}).setProcess(({queries:Z,ecs:K})=>{let H=K.getResource("spatialIndex");for(let L of Z.boids){let{flockingAgent:k,worldTransform:O,velocity:G}=L.components,{perceptionRadius:N,separationWeight:D,alignmentWeight:V,cohesionWeight:E,maxForce:W,flockGroup:_}=k;h.clear(),H.queryRadiusInto(O.x,O.y,N,h);let A=0,T=0,R=0,P=0,q=0,M=0,y=0,p=0,I=0,b=N*0.5,t=b*b;for(let F of h){if(F===L.id)continue;let B=K.getComponent(F,"flockingAgent");if(!B)continue;if(B.flockGroup!==_)continue;let C=K.getComponent(F,"worldTransform");if(!C)continue;let x=O.x-C.x,m=O.y-C.y,v=x*x+m*m;if(v>0&&v<t){let d=Math.sqrt(v);A+=x/d,T+=m/d,R++}let f=K.getComponent(F,"velocity");if(f)P+=f.x,q+=f.y,M++;y+=C.x,p+=C.y,I++}let X=0,Y=0;if(R>0)X+=A/R*D,Y+=T/R*D;if(M>0){let F=P/M,B=q/M;X+=(F-G.x)*V,Y+=(B-G.y)*V}if(I>0){let F=y/I,B=p/I;X+=(F-O.x-G.x)*E,Y+=(B-O.y-G.y)*E}let u=X*X+Y*Y;if(u>W*W){let F=Math.sqrt(u);X=X/F*W,Y=Y/F*W}e(K,L.id,X,Y)}}),j.addSystem("flocking-heading").setPriority($).inPhase(U).inGroup(J).addQuery("boids",{with:["flockingAgent","velocity","localTransform"]}).setProcess(({queries:Z,ecs:K})=>{for(let H of Z.boids){let{flockingAgent:L,velocity:k,localTransform:O}=H.components,{maxSpeed:G}=L,N=k.x*k.x+k.y*k.y;if(N>G*G){let D=Math.sqrt(N);k.x=k.x/D*G,k.y=k.y/D*G,K.markChanged(H.id,"velocity")}if(N>a*a){let D=Math.atan2(k.y,k.x);if(D!==O.rotation)O.rotation=D,K.markChanged(H.id,"localTransform")}}})})}export{Yz as createFlockingPlugin,Xz as createFlockingAgent};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=2E4E2EA277C75A1C64756E2164756E21
|
|
4
4
|
//# sourceMappingURL=flocking.js.map
|