@zylem/game-lib 0.5.1 → 0.6.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.
@@ -1,108 +1,854 @@
1
- import { G as GameEntity, m as CollisionContext, U as UpdateContext, B as BehaviorCallbackType } from './entity-bQElAdpo.js';
1
+ import { G as GameEntity, q as BehaviorDescriptor, d as ZylemWorld } from './world-C8tQ7Plj.js';
2
+ export { s as BehaviorHandle, r as BehaviorRef, e as BehaviorSystem, f as BehaviorSystemFactory, t as DefineBehaviorConfig, p as defineBehavior } from './world-C8tQ7Plj.js';
3
+ import { RigidBody, World } from '@dimforge/rapier3d-compat';
4
+ import { Vector3, Quaternion } from 'three';
5
+ import { StateMachine } from 'typescript-fsm';
6
+ import { B as BaseEntityInterface } from './entity-types-DAu8sGJH.js';
2
7
  import { M as MoveableEntity } from './moveable-B_vyA6cw.js';
3
- import { Vector } from '@dimforge/rapier3d-compat';
4
- import 'three';
8
+ import './entity-Bq_eNEDI.js';
5
9
  import 'bitecs';
10
+ import 'mitt';
6
11
 
7
12
  /**
8
- * A branded bitmask representing a set of collision types.
9
- * Construct with {@link buildCollisionMask}.
10
- */
11
- type CollisionMask = number & {
12
- readonly __brand: "CollisionMask";
13
- };
14
-
15
- type NameSelector = string | string[] | RegExp;
16
- type CollisionSelector = {
17
- name: NameSelector;
18
- } | {
19
- mask: CollisionMask | RegExp;
20
- } | {
21
- test: (other: GameEntity<any>) => boolean;
22
- };
23
-
24
- /**
25
- * Behavior for ricocheting an entity off other objects in 2D
26
- */
27
- declare function ricochet2DCollision(options?: Partial<Ricochet2DCollisionOptions>, callback?: Ricochet2DCollisionCallback): {
28
- type: 'collision';
29
- handler: (ctx: CollisionContext<MoveableEntity, GameEntity<any>>) => void;
30
- };
31
-
32
- interface RicochetEvent extends Partial<UpdateContext<MoveableEntity>> {
33
- boundary?: 'top' | 'bottom' | 'left' | 'right';
34
- position: Vector;
35
- velocityBefore: Vector;
36
- velocityAfter: Vector;
37
- }
38
- interface RicochetCollisionEvent extends CollisionContext<MoveableEntity, GameEntity<any>> {
39
- position: Vector;
40
- }
41
- interface Ricochet2DInBoundsOptions {
42
- restitution?: number;
43
- minSpeed?: number;
44
- maxSpeed?: number;
45
- boundaries: {
46
- top: number;
47
- bottom: number;
48
- left: number;
49
- right: number;
13
+ * Type-safe helper to apply a behavior to an entity and return the entity cast to the behavior's interface.
14
+ *
15
+ * @param entity The entity to apply the behavior to
16
+ * @param descriptor The behavior descriptor
17
+ * @param options Behavior options
18
+ * @returns The entity, cast to E & I (where I is the behavior's interface)
19
+ */
20
+ declare function useBehavior<E extends GameEntity<any>, O extends Record<string, any>, H extends Record<string, any>, I>(entity: E, descriptor: BehaviorDescriptor<O, H, I>, options?: Partial<O>): E & I;
21
+
22
+ /**
23
+ * Core ECS Components
24
+ *
25
+ * These are pure data interfaces with no logic.
26
+ * They work alongside the existing bitecs components in transformable.system.ts
27
+ */
28
+
29
+ interface TransformComponent {
30
+ position: Vector3;
31
+ rotation: Quaternion;
32
+ }
33
+ declare function createTransformComponent(): TransformComponent;
34
+ interface PhysicsBodyComponent {
35
+ body: RigidBody;
36
+ }
37
+ declare function createPhysicsBodyComponent(body: RigidBody): PhysicsBodyComponent;
38
+
39
+ /**
40
+ * Thruster-specific ECS Components
41
+ *
42
+ * These components are specific to the thruster movement system.
43
+ */
44
+ interface ThrusterMovementComponent {
45
+ /** Linear thrust force in Newtons (or scaled units) */
46
+ linearThrust: number;
47
+ /** Angular thrust torque scalar */
48
+ angularThrust: number;
49
+ /** Optional linear damping override */
50
+ linearDamping?: number;
51
+ /** Optional angular damping override */
52
+ angularDamping?: number;
53
+ }
54
+ declare function createThrusterMovementComponent(linearThrust: number, angularThrust: number, options?: {
55
+ linearDamping?: number;
56
+ angularDamping?: number;
57
+ }): ThrusterMovementComponent;
58
+ interface ThrusterInputComponent {
59
+ /** Forward thrust intent: 0..1 */
60
+ thrust: number;
61
+ /** Rotation intent: -1..1 */
62
+ rotate: number;
63
+ }
64
+ declare function createThrusterInputComponent(): ThrusterInputComponent;
65
+ interface ThrusterStateComponent {
66
+ /** Whether the thruster is enabled */
67
+ enabled: boolean;
68
+ /** Current thrust after FSM/gating */
69
+ currentThrust: number;
70
+ }
71
+ declare function createThrusterStateComponent(): ThrusterStateComponent;
72
+
73
+ /**
74
+ * ThrusterMovementBehavior
75
+ *
76
+ * This is the heart of the thruster movement system - a pure, stateless force generator.
77
+ * Works identically for player, AI, and replay.
78
+ */
79
+
80
+ /**
81
+ * Zylem-style Behavior interface
82
+ */
83
+ interface Behavior {
84
+ update(dt: number): void;
85
+ }
86
+ /**
87
+ * Entity with thruster components
88
+ */
89
+ interface ThrusterEntity {
90
+ physics: PhysicsBodyComponent;
91
+ thruster: ThrusterMovementComponent;
92
+ $thruster: ThrusterInputComponent;
93
+ }
94
+ /**
95
+ * ThrusterMovementBehavior - Force generator for thruster-equipped entities
96
+ *
97
+ * Responsibilities:
98
+ * - Query entities with PhysicsBody, ThrusterMovement, and ThrusterInput components
99
+ * - Apply velocities based on thrust input (2D mode)
100
+ * - Apply angular velocity based on rotation input
101
+ */
102
+ declare class ThrusterMovementBehavior implements Behavior {
103
+ private world;
104
+ constructor(world: ZylemWorld);
105
+ /**
106
+ * Query function - returns entities with required thruster components
107
+ */
108
+ private queryEntities;
109
+ update(_dt: number): void;
110
+ }
111
+
112
+ /**
113
+ * PhysicsStepBehavior
114
+ *
115
+ * Single authoritative place where Rapier advances.
116
+ * Runs after all force-producing behaviors.
117
+ */
118
+
119
+ /**
120
+ * PhysicsStepBehavior - Authoritative physics step
121
+ *
122
+ * This behavior is responsible for advancing the Rapier physics simulation.
123
+ * It should run AFTER all force-producing behaviors (like ThrusterMovementBehavior).
124
+ */
125
+ declare class PhysicsStepBehavior implements Behavior {
126
+ private physicsWorld;
127
+ constructor(physicsWorld: World);
128
+ update(dt: number): void;
129
+ }
130
+
131
+ /**
132
+ * PhysicsSyncBehavior
133
+ *
134
+ * Syncs physics state (position, rotation) from Rapier bodies to ECS TransformComponents.
135
+ * This is what keeps Three.js honest - rendering reads from TransformComponent.
136
+ */
137
+
138
+ /**
139
+ * PhysicsSyncBehavior - Physics → ECS sync
140
+ *
141
+ * Responsibilities:
142
+ * - Query entities with PhysicsBodyComponent and TransformComponent
143
+ * - Copy position from body.translation() to transform.position
144
+ * - Copy rotation from body.rotation() to transform.rotation
145
+ *
146
+ * This runs AFTER PhysicsStepBehavior, before rendering.
147
+ */
148
+ declare class PhysicsSyncBehavior implements Behavior {
149
+ private world;
150
+ constructor(world: ZylemWorld);
151
+ /**
152
+ * Query entities that have both physics body and transform components
153
+ */
154
+ private queryEntities;
155
+ update(_dt: number): void;
156
+ }
157
+
158
+ /**
159
+ * ThrusterFSM
160
+ *
161
+ * State machine controller for thruster behavior.
162
+ * FSM does NOT touch physics or ThrusterMovementBehavior - it only writes ThrusterInputComponent.
163
+ */
164
+
165
+ declare enum ThrusterState {
166
+ Idle = "idle",
167
+ Active = "active",
168
+ Boosting = "boosting",
169
+ Disabled = "disabled",
170
+ Docked = "docked"
171
+ }
172
+ declare enum ThrusterEvent {
173
+ Activate = "activate",
174
+ Deactivate = "deactivate",
175
+ Boost = "boost",
176
+ EndBoost = "endBoost",
177
+ Disable = "disable",
178
+ Enable = "enable",
179
+ Dock = "dock",
180
+ Undock = "undock"
181
+ }
182
+ interface ThrusterFSMContext {
183
+ input: ThrusterInputComponent;
184
+ }
185
+ interface PlayerInput {
186
+ thrust: number;
187
+ rotate: number;
188
+ }
189
+ declare class ThrusterFSM {
190
+ private ctx;
191
+ machine: StateMachine<ThrusterState, ThrusterEvent, never>;
192
+ constructor(ctx: ThrusterFSMContext);
193
+ /**
194
+ * Get current state
195
+ */
196
+ getState(): ThrusterState;
197
+ /**
198
+ * Dispatch an event to transition state
199
+ */
200
+ dispatch(event: ThrusterEvent): void;
201
+ /**
202
+ * Update FSM state based on player input.
203
+ * Auto-transitions between Idle/Active to report current state.
204
+ * Does NOT modify input - just observes and reports.
205
+ */
206
+ update(playerInput: PlayerInput): void;
207
+ }
208
+
209
+ /**
210
+ * Thruster behavior options (typed for entity.use() autocomplete)
211
+ */
212
+ interface ThrusterBehaviorOptions {
213
+ /** Forward thrust force (default: 10) */
214
+ linearThrust: number;
215
+ /** Rotation torque (default: 5) */
216
+ angularThrust: number;
217
+ }
218
+ /**
219
+ * ThrusterBehavior - typed descriptor for thruster movement.
220
+ *
221
+ * Uses the existing ThrusterMovementBehavior under the hood.
222
+ *
223
+ * @example
224
+ * ```typescript
225
+ * import { ThrusterBehavior } from "@zylem/game-lib";
226
+ *
227
+ * const ship = createSprite({ ... });
228
+ * ship.use(ThrusterBehavior, { linearThrust: 15, angularThrust: 8 });
229
+ * ```
230
+ */
231
+ declare const ThrusterBehavior: BehaviorDescriptor<ThrusterBehaviorOptions, Record<string, never>, ThrusterEntity>;
232
+
233
+ /**
234
+ * ScreenWrapBehavior
235
+ *
236
+ * When an entity exits the defined 2D bounds, it wraps around to the opposite edge.
237
+ * Asteroids-style screen wrapping with FSM for edge detection.
238
+ */
239
+ /**
240
+ * Screen wrap options (typed for entity.use() autocomplete)
241
+ */
242
+ interface ScreenWrapOptions {
243
+ /** Width of the wrapping area (default: 20) */
244
+ width: number;
245
+ /** Height of the wrapping area (default: 15) */
246
+ height: number;
247
+ /** Center X position (default: 0) */
248
+ centerX: number;
249
+ /** Center Y position (default: 0) */
250
+ centerY: number;
251
+ /** Distance from edge to trigger NearEdge state (default: 2) */
252
+ edgeThreshold: number;
253
+ }
254
+ /**
255
+ * ScreenWrapBehavior - Wraps entities around 2D bounds
256
+ *
257
+ * @example
258
+ * ```typescript
259
+ * import { ScreenWrapBehavior } from "@zylem/game-lib";
260
+ *
261
+ * const ship = createSprite({ ... });
262
+ * const wrapRef = ship.use(ScreenWrapBehavior, { width: 20, height: 15 });
263
+ *
264
+ * // Access FSM to observe edge state
265
+ * const fsm = wrapRef.getFSM();
266
+ * console.log(fsm?.getState()); // 'center', 'near-edge-left', 'wrapped', etc.
267
+ * ```
268
+ */
269
+ declare const ScreenWrapBehavior: BehaviorDescriptor<ScreenWrapOptions, Record<string, never>, unknown>;
270
+
271
+ /**
272
+ * ScreenWrapFSM
273
+ *
274
+ * State machine for screen wrap behavior.
275
+ * Reports position relative to bounds edges.
276
+ */
277
+
278
+ declare enum ScreenWrapState {
279
+ Center = "center",
280
+ NearEdgeLeft = "near-edge-left",
281
+ NearEdgeRight = "near-edge-right",
282
+ NearEdgeTop = "near-edge-top",
283
+ NearEdgeBottom = "near-edge-bottom",
284
+ Wrapped = "wrapped"
285
+ }
286
+ declare enum ScreenWrapEvent {
287
+ EnterCenter = "enter-center",
288
+ ApproachLeft = "approach-left",
289
+ ApproachRight = "approach-right",
290
+ ApproachTop = "approach-top",
291
+ ApproachBottom = "approach-bottom",
292
+ Wrap = "wrap"
293
+ }
294
+ declare class ScreenWrapFSM {
295
+ machine: StateMachine<ScreenWrapState, ScreenWrapEvent, never>;
296
+ constructor();
297
+ getState(): ScreenWrapState;
298
+ dispatch(event: ScreenWrapEvent): void;
299
+ /**
300
+ * Update FSM based on entity position relative to bounds
301
+ */
302
+ update(position: {
303
+ x: number;
304
+ y: number;
305
+ }, bounds: {
306
+ minX: number;
307
+ maxX: number;
308
+ minY: number;
309
+ maxY: number;
310
+ edgeThreshold: number;
311
+ }, wrapped: boolean): void;
312
+ }
313
+
314
+ /**
315
+ * WorldBoundary2DFSM
316
+ *
317
+ * Minimal FSM + extended state to track which world boundaries were hit.
318
+ *
319
+ * Notes:
320
+ * - "Hit boundaries" is inherently a *set* (can hit left+bottom in one frame),
321
+ * so we store it as extended state (`lastHits`) rather than a single FSM state.
322
+ * - The FSM state is still useful for coarse status like "inside" vs "touching".
323
+ */
324
+
325
+ type WorldBoundary2DHit = 'top' | 'bottom' | 'left' | 'right';
326
+ type WorldBoundary2DHits = Record<WorldBoundary2DHit, boolean>;
327
+ interface WorldBoundary2DPosition {
328
+ x: number;
329
+ y: number;
330
+ }
331
+ interface WorldBoundary2DBounds {
332
+ top: number;
333
+ bottom: number;
334
+ left: number;
335
+ right: number;
336
+ }
337
+ declare enum WorldBoundary2DState {
338
+ Inside = "inside",
339
+ Touching = "touching"
340
+ }
341
+ declare enum WorldBoundary2DEvent {
342
+ EnterInside = "enter-inside",
343
+ TouchBoundary = "touch-boundary"
344
+ }
345
+ /**
346
+ * Compute which boundaries are being hit for a position and bounds.
347
+ * This matches the semantics of the legacy `boundary2d` behavior:
348
+ * - left hit if x <= left
349
+ * - right hit if x >= right
350
+ * - bottom hit if y <= bottom
351
+ * - top hit if y >= top
352
+ */
353
+ declare function computeWorldBoundary2DHits(position: WorldBoundary2DPosition, bounds: WorldBoundary2DBounds): WorldBoundary2DHits;
354
+ declare function hasAnyWorldBoundary2DHit(hits: WorldBoundary2DHits): boolean;
355
+ /**
356
+ * FSM wrapper with "extended state" (lastHits / lastPosition).
357
+ * Systems should call `update(...)` once per frame.
358
+ */
359
+ declare class WorldBoundary2DFSM {
360
+ readonly machine: StateMachine<WorldBoundary2DState, WorldBoundary2DEvent, never>;
361
+ private lastHits;
362
+ private lastPosition;
363
+ private lastUpdatedAtMs;
364
+ constructor();
365
+ getState(): WorldBoundary2DState;
366
+ /**
367
+ * Returns the last computed hits (always available after first update call).
368
+ */
369
+ getLastHits(): WorldBoundary2DHits;
370
+ /**
371
+ * Returns adjusted movement values based on boundary hits.
372
+ * If the entity is touching a boundary and trying to move further into it,
373
+ * that axis component is zeroed out.
374
+ *
375
+ * @param moveX - The desired X movement
376
+ * @param moveY - The desired Y movement
377
+ * @returns Adjusted { moveX, moveY } with boundary-blocked axes zeroed
378
+ */
379
+ getMovement(moveX: number, moveY: number): {
380
+ moveX: number;
381
+ moveY: number;
50
382
  };
51
- separation?: number;
383
+ /**
384
+ * Returns the last position passed to `update`, if any.
385
+ */
386
+ getLastPosition(): WorldBoundary2DPosition | null;
387
+ /**
388
+ * Best-effort timestamp (ms) of the last `update(...)` call.
389
+ * This is optional metadata; systems can ignore it.
390
+ */
391
+ getLastUpdatedAtMs(): number | null;
392
+ /**
393
+ * Update FSM + extended state based on current position and bounds.
394
+ * Returns the computed hits for convenience.
395
+ */
396
+ update(position: WorldBoundary2DPosition, bounds: WorldBoundary2DBounds): WorldBoundary2DHits;
397
+ private dispatch;
398
+ }
399
+
400
+ interface WorldBoundary2DOptions {
401
+ /**
402
+ * World boundaries (in world units).
403
+ * - left hit if x <= left
404
+ * - right hit if x >= right
405
+ * - bottom hit if y <= bottom
406
+ * - top hit if y >= top
407
+ */
408
+ boundaries: WorldBoundary2DBounds;
52
409
  }
53
- interface Ricochet2DCollisionOptions {
54
- restitution?: number;
55
- minSpeed?: number;
56
- maxSpeed?: number;
57
- separation?: number;
58
- collisionWith?: CollisionSelector;
410
+ /**
411
+ * Handle methods provided by WorldBoundary2DBehavior
412
+ */
413
+ interface WorldBoundary2DHandle {
59
414
  /**
60
- * Choose between simple axis inversion or angled (paddle-style) reflection.
61
- * Defaults to 'angled'.
415
+ * Get the last computed boundary hits.
416
+ * Returns null until entity is spawned and FSM is initialized.
62
417
  */
63
- reflectionMode?: 'simple' | 'angled';
418
+ getLastHits(): WorldBoundary2DHits | null;
419
+ /**
420
+ * Get adjusted movement values based on boundary hits.
421
+ * Zeros out movement into boundaries the entity is touching.
422
+ */
423
+ getMovement(moveX: number, moveY: number): {
424
+ moveX: number;
425
+ moveY: number;
426
+ };
64
427
  }
65
- type Ricochet2DCallback = (event: RicochetEvent) => void;
66
- type Ricochet2DCollisionCallback = (event: RicochetCollisionEvent) => void;
428
+ /**
429
+ * WorldBoundary2DBehavior
430
+ *
431
+ * @example
432
+ * ```ts
433
+ * import { WorldBoundary2DBehavior } from "@zylem/game-lib";
434
+ *
435
+ * const ship = createSprite({ ... });
436
+ * const boundary = ship.use(WorldBoundary2DBehavior, {
437
+ * boundaries: { left: -10, right: 10, bottom: -7.5, top: 7.5 },
438
+ * });
439
+ *
440
+ * ship.onUpdate(({ me }) => {
441
+ * let moveX = ..., moveY = ...;
442
+ * const hits = boundary.getLastHits(); // Fully typed!
443
+ * ({ moveX, moveY } = boundary.getMovement(moveX, moveY));
444
+ * me.moveXY(moveX, moveY);
445
+ * });
446
+ * ```
447
+ */
448
+ declare const WorldBoundary2DBehavior: BehaviorDescriptor<WorldBoundary2DOptions, WorldBoundary2DHandle, unknown>;
67
449
 
68
450
  /**
69
- * Behavior for ricocheting an entity within fixed 2D boundaries
451
+ * Ricochet2DFSM
452
+ *
453
+ * FSM + extended state to track ricochet events and results.
454
+ * The FSM state tracks whether a ricochet is currently occurring.
70
455
  */
71
- declare function ricochet2DInBounds(options?: Partial<Ricochet2DInBoundsOptions>, callback?: Ricochet2DCallback): {
72
- type: BehaviorCallbackType;
73
- handler: (ctx: UpdateContext<MoveableEntity>) => void;
74
- };
75
456
 
76
- interface BoundaryEvent {
77
- me: MoveableEntity;
78
- boundary: BoundaryHits;
79
- position: Vector;
80
- updateContext: UpdateContext<MoveableEntity>;
457
+ interface Ricochet2DResult {
458
+ /** The reflected velocity vector */
459
+ velocity: {
460
+ x: number;
461
+ y: number;
462
+ z?: number;
463
+ };
464
+ /** The resulting speed after reflection */
465
+ speed: number;
466
+ /** The collision normal used for reflection */
467
+ normal: {
468
+ x: number;
469
+ y: number;
470
+ z?: number;
471
+ };
81
472
  }
82
- interface BoundaryOptions {
83
- boundaries: {
84
- top: number;
85
- bottom: number;
86
- left: number;
87
- right: number;
473
+ interface Ricochet2DCollisionContext {
474
+ entity?: BaseEntityInterface;
475
+ otherEntity?: BaseEntityInterface;
476
+ /** Current velocity of the entity (optional if entity is provided) */
477
+ selfVelocity?: {
478
+ x: number;
479
+ y: number;
480
+ z?: number;
481
+ };
482
+ /** Contact information from the collision */
483
+ contact: {
484
+ /** The collision normal */
485
+ normal?: {
486
+ x: number;
487
+ y: number;
488
+ z?: number;
489
+ };
490
+ /**
491
+ * Optional position where the collision occurred.
492
+ * If provided, used for precise offset calculation.
493
+ */
494
+ position?: {
495
+ x: number;
496
+ y: number;
497
+ z?: number;
498
+ };
499
+ };
500
+ /**
501
+ * Optional position of the entity that owns this behavior.
502
+ * Used with contact.position for offset calculations.
503
+ */
504
+ selfPosition?: {
505
+ x: number;
506
+ y: number;
507
+ z?: number;
88
508
  };
89
- onBoundary?: (event: BoundaryEvent) => void;
90
- stopMovement?: boolean;
509
+ /**
510
+ * Optional position of the other entity in the collision.
511
+ * Used for paddle-style deflection: offset = (contactY - otherY) / halfHeight.
512
+ */
513
+ otherPosition?: {
514
+ x: number;
515
+ y: number;
516
+ z?: number;
517
+ };
518
+ /**
519
+ * Optional size of the other entity (e.g., paddle size).
520
+ * If provided, used to normalize the offset based on the collision face.
521
+ */
522
+ otherSize?: {
523
+ x: number;
524
+ y: number;
525
+ z?: number;
526
+ };
527
+ }
528
+ declare enum Ricochet2DState {
529
+ Idle = "idle",
530
+ Ricocheting = "ricocheting"
531
+ }
532
+ declare enum Ricochet2DEvent {
533
+ StartRicochet = "start-ricochet",
534
+ EndRicochet = "end-ricochet"
535
+ }
536
+ /**
537
+ * FSM wrapper with extended state (lastResult).
538
+ * Systems or consumers call `computeRicochet(...)` when a collision occurs.
539
+ */
540
+ declare class Ricochet2DFSM {
541
+ readonly machine: StateMachine<Ricochet2DState, Ricochet2DEvent, never>;
542
+ private lastResult;
543
+ private lastUpdatedAtMs;
544
+ constructor();
545
+ getState(): Ricochet2DState;
546
+ /**
547
+ * Returns the last computed ricochet result, or null if none.
548
+ */
549
+ getLastResult(): Ricochet2DResult | null;
550
+ /**
551
+ * Best-effort timestamp (ms) of the last computation.
552
+ */
553
+ getLastUpdatedAtMs(): number | null;
554
+ /**
555
+ * Compute a ricochet result from collision context.
556
+ * Returns the result for the consumer to apply, or null if invalid input.
557
+ */
558
+ computeRicochet(ctx: Ricochet2DCollisionContext, options?: {
559
+ minSpeed?: number;
560
+ maxSpeed?: number;
561
+ speedMultiplier?: number;
562
+ reflectionMode?: 'simple' | 'angled';
563
+ maxAngleDeg?: number;
564
+ }): Ricochet2DResult | null;
565
+ /**
566
+ * Extract velocity, position, and size data from entities or context.
567
+ */
568
+ private extractDataFromEntities;
569
+ /**
570
+ * Compute collision normal from entity positions using AABB heuristic.
571
+ */
572
+ private computeNormalFromPositions;
573
+ /**
574
+ * Compute basic reflection using the formula: v' = v - 2(v·n)n
575
+ */
576
+ private computeBasicReflection;
577
+ /**
578
+ * Compute angled deflection for paddle-style reflections.
579
+ */
580
+ private computeAngledDeflection;
581
+ /**
582
+ * Compute hit offset for angled deflection (-1 to 1).
583
+ */
584
+ private computeHitOffset;
585
+ /**
586
+ * Apply speed constraints to the reflected velocity.
587
+ */
588
+ private applySpeedClamp;
589
+ /**
590
+ * Clear the ricochet state (call after consumer has applied the result).
591
+ */
592
+ clearRicochet(): void;
593
+ private dispatch;
594
+ }
595
+
596
+ interface Ricochet2DOptions {
597
+ /**
598
+ * Minimum speed after reflection.
599
+ * Default: 2
600
+ */
601
+ minSpeed: number;
602
+ /**
603
+ * Maximum speed after reflection.
604
+ * Default: 20
605
+ */
606
+ maxSpeed: number;
607
+ /**
608
+ * Speed multiplier applied during angled reflection.
609
+ * Default: 1.05
610
+ */
611
+ speedMultiplier: number;
612
+ /**
613
+ * Reflection mode:
614
+ * - 'simple': Basic axis inversion
615
+ * - 'angled': Paddle-style deflection based on contact point
616
+ * Default: 'angled'
617
+ */
618
+ reflectionMode: 'simple' | 'angled';
619
+ /**
620
+ * Maximum deflection angle in degrees for angled mode.
621
+ * Default: 60
622
+ */
623
+ maxAngleDeg: number;
91
624
  }
92
625
  /**
93
- * Checks if the entity has hit a boundary and stops its movement if it has
626
+ * Handle methods provided by Ricochet2DBehavior
627
+ */
628
+ interface Ricochet2DHandle {
629
+ /**
630
+ * Compute a ricochet/reflection result from collision context.
631
+ * Returns the result for the consumer to apply, or null if invalid input.
632
+ *
633
+ * @param ctx - Collision context with selfVelocity and contact normal
634
+ * @returns Ricochet result with velocity, speed, and normal, or null
635
+ */
636
+ getRicochet(ctx: Ricochet2DCollisionContext): Ricochet2DResult | null;
637
+ /**
638
+ * Get the last computed ricochet result, or null if none.
639
+ */
640
+ getLastResult(): Ricochet2DResult | null;
641
+ }
642
+ /**
643
+ * Ricochet2DBehavior
644
+ *
645
+ * @example
646
+ * ```ts
647
+ * import { Ricochet2DBehavior } from "@zylem/game-lib";
94
648
  *
95
- * @param options Configuration options for the boundary behavior
96
- * @param options.boundaries The boundaries of the stage
97
- * @param options.onBoundary A callback function that is called when the entity hits a boundary
98
- * @param options.stopMovement Whether to stop the entity's movement when it hits a boundary
99
- * @returns A behavior callback with type 'update' and a handler function
649
+ * const ball = createSphere({ ... });
650
+ * const ricochet = ball.use(Ricochet2DBehavior, {
651
+ * minSpeed: 3,
652
+ * maxSpeed: 15,
653
+ * reflectionMode: 'angled',
654
+ * });
655
+ *
656
+ * ball.onCollision(({ entity, other }) => {
657
+ * const velocity = entity.body.linvel();
658
+ * const result = ricochet.getRicochet({
659
+ * selfVelocity: velocity,
660
+ * contact: { normal: { x: 1, y: 0 } }, // from collision data
661
+ * });
662
+ *
663
+ * if (result) {
664
+ * entity.body.setLinvel(result.velocity, true);
665
+ * }
666
+ * });
667
+ * ```
100
668
  */
101
- declare function boundary2d(options?: Partial<BoundaryOptions>): {
102
- type: BehaviorCallbackType;
103
- handler: (ctx: UpdateContext<MoveableEntity>) => void;
104
- };
105
- type BoundaryHit = 'top' | 'bottom' | 'left' | 'right';
106
- type BoundaryHits = Record<BoundaryHit, boolean>;
669
+ declare const Ricochet2DBehavior: BehaviorDescriptor<Ricochet2DOptions, Ricochet2DHandle, unknown>;
670
+
671
+ /**
672
+ * MovementSequence2DFSM
673
+ *
674
+ * FSM + extended state to manage timed movement sequences.
675
+ * Tracks current step, time remaining, and computes movement for consumer.
676
+ */
677
+
678
+ interface MovementSequence2DStep {
679
+ /** Identifier for this step */
680
+ name: string;
681
+ /** X velocity for this step */
682
+ moveX?: number;
683
+ /** Y velocity for this step */
684
+ moveY?: number;
685
+ /** Duration in seconds */
686
+ timeInSeconds: number;
687
+ }
688
+ interface MovementSequence2DMovement {
689
+ moveX: number;
690
+ moveY: number;
691
+ }
692
+ interface MovementSequence2DProgress {
693
+ stepIndex: number;
694
+ totalSteps: number;
695
+ stepTimeRemaining: number;
696
+ done: boolean;
697
+ }
698
+ interface MovementSequence2DCurrentStep {
699
+ name: string;
700
+ index: number;
701
+ moveX: number;
702
+ moveY: number;
703
+ timeRemaining: number;
704
+ }
705
+ declare enum MovementSequence2DState {
706
+ Idle = "idle",
707
+ Running = "running",
708
+ Paused = "paused",
709
+ Completed = "completed"
710
+ }
711
+ declare enum MovementSequence2DEvent {
712
+ Start = "start",
713
+ Pause = "pause",
714
+ Resume = "resume",
715
+ Complete = "complete",
716
+ Reset = "reset"
717
+ }
718
+ declare class MovementSequence2DFSM {
719
+ readonly machine: StateMachine<MovementSequence2DState, MovementSequence2DEvent, never>;
720
+ private sequence;
721
+ private loop;
722
+ private currentIndex;
723
+ private timeRemaining;
724
+ constructor();
725
+ /**
726
+ * Initialize the sequence. Call this once with options.
727
+ */
728
+ init(sequence: MovementSequence2DStep[], loop: boolean): void;
729
+ getState(): MovementSequence2DState;
730
+ /**
731
+ * Start the sequence (from Idle or Completed).
732
+ */
733
+ start(): void;
734
+ /**
735
+ * Pause the sequence.
736
+ */
737
+ pause(): void;
738
+ /**
739
+ * Resume a paused sequence.
740
+ */
741
+ resume(): void;
742
+ /**
743
+ * Reset to Idle state.
744
+ */
745
+ reset(): void;
746
+ /**
747
+ * Update the sequence with delta time.
748
+ * Returns the current movement to apply.
749
+ * Automatically starts if in Idle state.
750
+ */
751
+ update(delta: number): MovementSequence2DMovement;
752
+ /**
753
+ * Get the current movement without advancing time.
754
+ */
755
+ getMovement(): MovementSequence2DMovement;
756
+ /**
757
+ * Get current step info.
758
+ */
759
+ getCurrentStep(): MovementSequence2DCurrentStep | null;
760
+ /**
761
+ * Get sequence progress.
762
+ */
763
+ getProgress(): MovementSequence2DProgress;
764
+ private dispatch;
765
+ }
766
+
767
+ interface MovementSequence2DOptions {
768
+ /**
769
+ * The sequence of movement steps.
770
+ * Each step has name, moveX, moveY, and timeInSeconds.
771
+ */
772
+ sequence: MovementSequence2DStep[];
773
+ /**
774
+ * Whether to loop when the sequence ends.
775
+ * Default: true
776
+ */
777
+ loop: boolean;
778
+ }
779
+ /**
780
+ * Handle methods provided by MovementSequence2DBehavior
781
+ */
782
+ interface MovementSequence2DHandle {
783
+ /**
784
+ * Get the current movement velocity.
785
+ * Returns { moveX: 0, moveY: 0 } if sequence is empty or completed.
786
+ */
787
+ getMovement(): MovementSequence2DMovement;
788
+ /**
789
+ * Get the current step info.
790
+ * Returns null if sequence is empty.
791
+ */
792
+ getCurrentStep(): MovementSequence2DCurrentStep | null;
793
+ /**
794
+ * Get sequence progress.
795
+ */
796
+ getProgress(): MovementSequence2DProgress;
797
+ /**
798
+ * Pause the sequence. Movement values remain but time doesn't advance.
799
+ */
800
+ pause(): void;
801
+ /**
802
+ * Resume a paused sequence.
803
+ */
804
+ resume(): void;
805
+ /**
806
+ * Reset the sequence to the beginning.
807
+ */
808
+ reset(): void;
809
+ }
810
+ /**
811
+ * MovementSequence2DBehavior
812
+ *
813
+ * @example
814
+ * ```ts
815
+ * import { MovementSequence2DBehavior } from "@zylem/game-lib";
816
+ *
817
+ * const enemy = makeMoveable(createSprite({ ... }));
818
+ * const sequence = enemy.use(MovementSequence2DBehavior, {
819
+ * sequence: [
820
+ * { name: 'right', moveX: 3, moveY: 0, timeInSeconds: 2 },
821
+ * { name: 'left', moveX: -3, moveY: 0, timeInSeconds: 2 },
822
+ * ],
823
+ * loop: true,
824
+ * });
825
+ *
826
+ * enemy.onUpdate(({ me }) => {
827
+ * const { moveX, moveY } = sequence.getMovement();
828
+ * me.moveXY(moveX, moveY);
829
+ * });
830
+ * ```
831
+ */
832
+ declare const MovementSequence2DBehavior: BehaviorDescriptor<MovementSequence2DOptions, MovementSequence2DHandle, unknown>;
833
+
834
+ /**
835
+ * Coordinator that bridges WorldBoundary2DBehavior and Ricochet2DBehavior.
836
+ *
837
+ * Automatically handles:
838
+ * 1. Checking boundary hits
839
+ * 2. Computing collision normals
840
+ * 3. Requesting ricochet result
841
+ * 4. Applying movement
842
+ */
843
+ declare class BoundaryRicochetCoordinator {
844
+ private entity;
845
+ private boundary;
846
+ private ricochet;
847
+ constructor(entity: GameEntity<any> & MoveableEntity, boundary: WorldBoundary2DHandle, ricochet: Ricochet2DHandle);
848
+ /**
849
+ * Update loop - call this every frame
850
+ */
851
+ update(): Ricochet2DResult | null;
852
+ }
107
853
 
108
- export { boundary2d, ricochet2DCollision, ricochet2DInBounds };
854
+ export { type Behavior, BehaviorDescriptor, BoundaryRicochetCoordinator, MovementSequence2DBehavior, type MovementSequence2DCurrentStep, MovementSequence2DEvent, MovementSequence2DFSM, type MovementSequence2DHandle, type MovementSequence2DMovement, type MovementSequence2DOptions, type MovementSequence2DProgress, MovementSequence2DState, type MovementSequence2DStep, type PhysicsBodyComponent, PhysicsStepBehavior, PhysicsSyncBehavior, type PlayerInput, Ricochet2DBehavior, type Ricochet2DCollisionContext, Ricochet2DEvent, Ricochet2DFSM, type Ricochet2DHandle, type Ricochet2DOptions, type Ricochet2DResult, Ricochet2DState, ScreenWrapBehavior, ScreenWrapEvent, ScreenWrapFSM, type ScreenWrapOptions, ScreenWrapState, ThrusterBehavior, type ThrusterBehaviorOptions, type ThrusterEntity, ThrusterEvent, ThrusterFSM, type ThrusterFSMContext, type ThrusterInputComponent, ThrusterMovementBehavior, type ThrusterMovementComponent, ThrusterState, type ThrusterStateComponent, type TransformComponent, WorldBoundary2DBehavior, type WorldBoundary2DBounds, WorldBoundary2DEvent, WorldBoundary2DFSM, type WorldBoundary2DHandle, type WorldBoundary2DHit, type WorldBoundary2DHits, type WorldBoundary2DOptions, type WorldBoundary2DPosition, WorldBoundary2DState, computeWorldBoundary2DHits, createPhysicsBodyComponent, createThrusterInputComponent, createThrusterMovementComponent, createThrusterStateComponent, createTransformComponent, hasAnyWorldBoundary2DHit, useBehavior };