@zylem/game-lib 0.6.3 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/actions.d.ts +5 -5
  2. package/dist/actions.js +196 -32
  3. package/dist/actions.js.map +1 -1
  4. package/dist/behavior/jumper-2d.d.ts +114 -0
  5. package/dist/behavior/jumper-2d.js +711 -0
  6. package/dist/behavior/jumper-2d.js.map +1 -0
  7. package/dist/behavior/platformer-3d.d.ts +14 -14
  8. package/dist/behavior/platformer-3d.js +347 -104
  9. package/dist/behavior/platformer-3d.js.map +1 -1
  10. package/dist/behavior/ricochet-2d.d.ts +4 -3
  11. package/dist/behavior/ricochet-2d.js +53 -22
  12. package/dist/behavior/ricochet-2d.js.map +1 -1
  13. package/dist/behavior/ricochet-3d.d.ts +117 -0
  14. package/dist/behavior/ricochet-3d.js +443 -0
  15. package/dist/behavior/ricochet-3d.js.map +1 -0
  16. package/dist/behavior/screen-visibility.d.ts +79 -0
  17. package/dist/behavior/screen-visibility.js +358 -0
  18. package/dist/behavior/screen-visibility.js.map +1 -0
  19. package/dist/behavior/screen-wrap.d.ts +4 -3
  20. package/dist/behavior/screen-wrap.js +100 -49
  21. package/dist/behavior/screen-wrap.js.map +1 -1
  22. package/dist/behavior/shooter-2d.d.ts +79 -0
  23. package/dist/behavior/shooter-2d.js +180 -0
  24. package/dist/behavior/shooter-2d.js.map +1 -0
  25. package/dist/behavior/thruster.d.ts +5 -4
  26. package/dist/behavior/thruster.js +133 -75
  27. package/dist/behavior/thruster.js.map +1 -1
  28. package/dist/behavior/top-down-movement.d.ts +56 -0
  29. package/dist/behavior/top-down-movement.js +125 -0
  30. package/dist/behavior/top-down-movement.js.map +1 -0
  31. package/dist/behavior/world-boundary-2d.d.ts +4 -3
  32. package/dist/behavior/world-boundary-2d.js +90 -36
  33. package/dist/behavior/world-boundary-2d.js.map +1 -1
  34. package/dist/behavior/world-boundary-3d.d.ts +76 -0
  35. package/dist/behavior/world-boundary-3d.js +274 -0
  36. package/dist/behavior/world-boundary-3d.js.map +1 -0
  37. package/dist/{behavior-descriptor-BWNWmIjv.d.ts → behavior-descriptor-BXnVR8Ki.d.ts} +22 -5
  38. package/dist/{blueprints-BWGz8fII.d.ts → blueprints-DmbK2dki.d.ts} +2 -2
  39. package/dist/camera-4XO5gbQH.d.ts +905 -0
  40. package/dist/camera.d.ts +1 -1
  41. package/dist/camera.js +876 -289
  42. package/dist/camera.js.map +1 -1
  43. package/dist/{composition-DrzFrbqI.d.ts → composition-BASvMKrW.d.ts} +1 -1
  44. package/dist/{core-DAkskq6Y.d.ts → core-CARRaS55.d.ts} +57 -14
  45. package/dist/core.d.ts +9 -8
  46. package/dist/core.js +4519 -1255
  47. package/dist/core.js.map +1 -1
  48. package/dist/{entities-DC9ce_vx.d.ts → entities-ChFirVL9.d.ts} +22 -28
  49. package/dist/entities.d.ts +4 -4
  50. package/dist/entities.js +1231 -314
  51. package/dist/entities.js.map +1 -1
  52. package/dist/{entity-BpbZqg19.d.ts → entity-vj-HTjzU.d.ts} +80 -11
  53. package/dist/{global-change-Dc8uCKi2.d.ts → global-change-2JvMaz44.d.ts} +1 -1
  54. package/dist/main.d.ts +718 -19
  55. package/dist/main.js +12129 -5959
  56. package/dist/main.js.map +1 -1
  57. package/dist/physics-pose-DCc4oE44.d.ts +25 -0
  58. package/dist/physics-protocol-BDD3P5W2.d.ts +200 -0
  59. package/dist/physics-worker.d.ts +21 -0
  60. package/dist/physics-worker.js +306 -0
  61. package/dist/physics-worker.js.map +1 -0
  62. package/dist/physics.d.ts +205 -0
  63. package/dist/physics.js +577 -0
  64. package/dist/physics.js.map +1 -0
  65. package/dist/{stage-types-BFsm3qsZ.d.ts → stage-types-C19IhuzA.d.ts} +253 -89
  66. package/dist/stage.d.ts +9 -8
  67. package/dist/stage.js +3782 -1041
  68. package/dist/stage.js.map +1 -1
  69. package/dist/sync-state-machine-CZyspBpj.d.ts +16 -0
  70. package/dist/{thruster-DhRaJnoL.d.ts → thruster-23lzoPZd.d.ts} +16 -8
  71. package/dist/world-DfgxoNMt.d.ts +105 -0
  72. package/package.json +25 -1
  73. package/dist/camera-B5e4c78l.d.ts +0 -468
  74. package/dist/world-Be5m1XC1.d.ts +0 -31
@@ -1,6 +1,7 @@
1
- import { b as BehaviorDescriptor } from '../behavior-descriptor-BWNWmIjv.js';
2
- import { StateMachine } from 'typescript-fsm';
1
+ import { c as BehaviorDescriptor } from '../behavior-descriptor-BXnVR8Ki.js';
2
+ import { S as SyncStateMachine } from '../sync-state-machine-CZyspBpj.js';
3
3
  import 'bitecs';
4
+ import 'typescript-fsm';
4
5
 
5
6
  /**
6
7
  * WorldBoundary2DFSM
@@ -48,7 +49,7 @@ declare function hasAnyWorldBoundary2DHit(hits: WorldBoundary2DHits): boolean;
48
49
  * Systems should call `update(...)` once per frame.
49
50
  */
50
51
  declare class WorldBoundary2DFSM {
51
- readonly machine: StateMachine<WorldBoundary2DState, WorldBoundary2DEvent, never>;
52
+ readonly machine: SyncStateMachine<WorldBoundary2DState, WorldBoundary2DEvent, never>;
52
53
  private lastHits;
53
54
  private lastPosition;
54
55
  private lastUpdatedAtMs;
@@ -8,8 +8,75 @@ function defineBehavior(config) {
8
8
  };
9
9
  }
10
10
 
11
+ // src/lib/core/utility/sync-state-machine.ts
12
+ import {
13
+ StateMachine as BaseStateMachine
14
+ } from "typescript-fsm";
15
+ import { t } from "typescript-fsm";
16
+ var SyncStateMachine = class extends BaseStateMachine {
17
+ constructor(init, transitions = [], logger = console) {
18
+ super(init, transitions, logger);
19
+ }
20
+ dispatch(_event, ..._args) {
21
+ throw new Error("SyncStateMachine does not support async dispatch.");
22
+ }
23
+ syncDispatch(event, ...args) {
24
+ const found = this.transitions.some((transition) => {
25
+ if (transition.fromState !== this._current || transition.event !== event) {
26
+ return false;
27
+ }
28
+ const current = this._current;
29
+ this._current = transition.toState;
30
+ if (!transition.cb) {
31
+ return true;
32
+ }
33
+ try {
34
+ transition.cb(...args);
35
+ return true;
36
+ } catch (error) {
37
+ this._current = current;
38
+ this.logger.error("Exception in callback", error);
39
+ throw error;
40
+ }
41
+ });
42
+ if (!found) {
43
+ const errorMessage = this.formatErr(this._current, event);
44
+ this.logger.error(errorMessage);
45
+ }
46
+ return found;
47
+ }
48
+ };
49
+
50
+ // src/lib/behaviors/shared/bounds-2d.ts
51
+ function computeBounds2DHits(position, bounds) {
52
+ const hits = {
53
+ top: false,
54
+ bottom: false,
55
+ left: false,
56
+ right: false
57
+ };
58
+ if (position.x <= bounds.minX) hits.left = true;
59
+ else if (position.x >= bounds.maxX) hits.right = true;
60
+ if (position.y <= bounds.minY) hits.bottom = true;
61
+ else if (position.y >= bounds.maxY) hits.top = true;
62
+ return hits;
63
+ }
64
+ function hasAnyBounds2DHit(hits) {
65
+ return hits.left || hits.right || hits.top || hits.bottom;
66
+ }
67
+ function constrainMovementToBounds2D(hits, moveX, moveY) {
68
+ let adjustedX = moveX;
69
+ let adjustedY = moveY;
70
+ if (hits.left && moveX < 0 || hits.right && moveX > 0) {
71
+ adjustedX = 0;
72
+ }
73
+ if (hits.bottom && moveY < 0 || hits.top && moveY > 0) {
74
+ adjustedY = 0;
75
+ }
76
+ return { moveX: adjustedX, moveY: adjustedY };
77
+ }
78
+
11
79
  // src/lib/behaviors/world-boundary-2d/world-boundary-2d-fsm.ts
12
- import { StateMachine, t } from "typescript-fsm";
13
80
  var WorldBoundary2DState = /* @__PURE__ */ ((WorldBoundary2DState2) => {
14
81
  WorldBoundary2DState2["Inside"] = "inside";
15
82
  WorldBoundary2DState2["Touching"] = "touching";
@@ -21,20 +88,15 @@ var WorldBoundary2DEvent = /* @__PURE__ */ ((WorldBoundary2DEvent2) => {
21
88
  return WorldBoundary2DEvent2;
22
89
  })(WorldBoundary2DEvent || {});
23
90
  function computeWorldBoundary2DHits(position, bounds) {
24
- const hits = {
25
- top: false,
26
- bottom: false,
27
- left: false,
28
- right: false
29
- };
30
- if (position.x <= bounds.left) hits.left = true;
31
- else if (position.x >= bounds.right) hits.right = true;
32
- if (position.y <= bounds.bottom) hits.bottom = true;
33
- else if (position.y >= bounds.top) hits.top = true;
34
- return hits;
91
+ return computeBounds2DHits(position, {
92
+ minX: bounds.left,
93
+ maxX: bounds.right,
94
+ minY: bounds.bottom,
95
+ maxY: bounds.top
96
+ });
35
97
  }
36
98
  function hasAnyWorldBoundary2DHit(hits) {
37
- return !!(hits.left || hits.right || hits.top || hits.bottom);
99
+ return hasAnyBounds2DHit(hits);
38
100
  }
39
101
  var WorldBoundary2DFSM = class {
40
102
  machine;
@@ -42,7 +104,7 @@ var WorldBoundary2DFSM = class {
42
104
  lastPosition = null;
43
105
  lastUpdatedAtMs = null;
44
106
  constructor() {
45
- this.machine = new StateMachine(
107
+ this.machine = new SyncStateMachine(
46
108
  "inside" /* Inside */,
47
109
  [
48
110
  t("inside" /* Inside */, "touch-boundary" /* TouchBoundary */, "touching" /* Touching */),
@@ -72,16 +134,7 @@ var WorldBoundary2DFSM = class {
72
134
  * @returns Adjusted { moveX, moveY } with boundary-blocked axes zeroed
73
135
  */
74
136
  getMovement(moveX, moveY) {
75
- const hits = this.lastHits;
76
- let adjustedX = moveX;
77
- let adjustedY = moveY;
78
- if (hits.left && moveX < 0 || hits.right && moveX > 0) {
79
- adjustedX = 0;
80
- }
81
- if (hits.bottom && moveY < 0 || hits.top && moveY > 0) {
82
- adjustedY = 0;
83
- }
84
- return { moveX: adjustedX, moveY: adjustedY };
137
+ return constrainMovementToBounds2D(this.lastHits, moveX, moveY);
85
138
  }
86
139
  /**
87
140
  * Returns the last position passed to `update`, if any.
@@ -114,7 +167,7 @@ var WorldBoundary2DFSM = class {
114
167
  }
115
168
  dispatch(event) {
116
169
  if (this.machine.can(event)) {
117
- this.machine.dispatch(event);
170
+ this.machine.syncDispatch(event);
118
171
  }
119
172
  }
120
173
  };
@@ -123,6 +176,9 @@ var WorldBoundary2DFSM = class {
123
176
  var defaultOptions = {
124
177
  boundaries: { top: 0, bottom: 0, left: 0, right: 0 }
125
178
  };
179
+ var WORLD_BOUNDARY_BEHAVIOR_KEY = /* @__PURE__ */ Symbol.for(
180
+ "zylem:behavior:world-boundary-2d"
181
+ );
126
182
  function createWorldBoundary2DHandle(ref) {
127
183
  return {
128
184
  getLastHits: () => {
@@ -136,19 +192,17 @@ function createWorldBoundary2DHandle(ref) {
136
192
  };
137
193
  }
138
194
  var WorldBoundary2DSystem = class {
139
- constructor(world) {
195
+ constructor(world, getBehaviorLinks) {
140
196
  this.world = world;
197
+ this.getBehaviorLinks = getBehaviorLinks;
141
198
  }
142
199
  update(_ecs, _delta) {
143
- if (!this.world?.collisionMap) return;
144
- for (const [, entity] of this.world.collisionMap) {
145
- const gameEntity = entity;
146
- if (typeof gameEntity.getBehaviorRefs !== "function") continue;
147
- const refs = gameEntity.getBehaviorRefs();
148
- const boundaryRef = refs.find(
149
- (r) => r.descriptor.key === /* @__PURE__ */ Symbol.for("zylem:behavior:world-boundary-2d")
150
- );
151
- if (!boundaryRef || !gameEntity.body) continue;
200
+ const links = this.getBehaviorLinks?.(WORLD_BOUNDARY_BEHAVIOR_KEY);
201
+ if (!links) return;
202
+ for (const link of links) {
203
+ const gameEntity = link.entity;
204
+ const boundaryRef = link.ref;
205
+ if (!gameEntity.body) continue;
152
206
  const options = boundaryRef.options;
153
207
  if (!boundaryRef.fsm) {
154
208
  boundaryRef.fsm = new WorldBoundary2DFSM();
@@ -167,7 +221,7 @@ var WorldBoundary2DSystem = class {
167
221
  var WorldBoundary2DBehavior = defineBehavior({
168
222
  name: "world-boundary-2d",
169
223
  defaultOptions,
170
- systemFactory: (ctx) => new WorldBoundary2DSystem(ctx.world),
224
+ systemFactory: (ctx) => new WorldBoundary2DSystem(ctx.world, ctx.getBehaviorLinks),
171
225
  createHandle: createWorldBoundary2DHandle
172
226
  });
173
227
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/behaviors/behavior-descriptor.ts","../../src/lib/behaviors/world-boundary-2d/world-boundary-2d-fsm.ts","../../src/lib/behaviors/world-boundary-2d/world-boundary-2d.descriptor.ts"],"sourcesContent":["/**\n * BehaviorDescriptor\n *\n * Type-safe behavior descriptors that provide options inference.\n * Used with entity.use() to declaratively attach behaviors to entities.\n *\n * Each behavior can define its own handle type via `createHandle`,\n * providing behavior-specific methods with full type safety.\n */\n\nimport type { BehaviorSystemFactory } from './behavior-system';\n\n/**\n * Base handle returned by entity.use() for lazy access to behavior runtime.\n * FSM is null until entity is spawned and components are initialized.\n */\nexport interface BaseBehaviorHandle<\n O extends Record<string, any> = Record<string, any>,\n> {\n /** Get the FSM instance (null until entity is spawned) */\n getFSM(): any | null;\n /** Get the current options */\n getOptions(): O;\n /** Access the underlying behavior ref */\n readonly ref: BehaviorRef<O>;\n}\n\n/**\n * Reference to a behavior stored on an entity\n */\nexport interface BehaviorRef<\n O extends Record<string, any> = Record<string, any>,\n> {\n /** The behavior descriptor */\n descriptor: BehaviorDescriptor<O, any>;\n /** Merged options (defaults + overrides) */\n options: O;\n /** Optional FSM instance - set lazily when entity is spawned */\n fsm?: any;\n}\n\n/**\n * A typed behavior descriptor that associates a symbol key with:\n * - Default options (providing type inference)\n * - A system factory to create the behavior system\n * - An optional handle factory for behavior-specific methods\n */\nexport interface BehaviorDescriptor<\n O extends Record<string, any> = Record<string, any>,\n H extends Record<string, any> = Record<string, never>,\n I = unknown,\n> {\n /** Unique symbol identifying this behavior */\n readonly key: symbol;\n /** Default options (used for type inference) */\n readonly defaultOptions: O;\n /** Factory to create the behavior system */\n readonly systemFactory: BehaviorSystemFactory;\n /**\n * Optional factory to create behavior-specific handle methods.\n * These methods are merged into the handle returned by entity.use().\n */\n readonly createHandle?: (ref: BehaviorRef<O>) => H;\n}\n\n/**\n * The full handle type returned by entity.use().\n * Combines base handle with behavior-specific methods.\n */\nexport type BehaviorHandle<\n O extends Record<string, any> = Record<string, any>,\n H extends Record<string, any> = Record<string, never>,\n> = BaseBehaviorHandle<O> & H;\n\n/**\n * Configuration for defining a new behavior\n */\nexport interface DefineBehaviorConfig<\n O extends Record<string, any>,\n H extends Record<string, any> = Record<string, never>,\n I = unknown,\n> {\n /** Human-readable name for debugging */\n name: string;\n /** Default options - these define the type */\n defaultOptions: O;\n /** Factory function to create the system */\n systemFactory: BehaviorSystemFactory;\n /**\n * Optional factory to create behavior-specific handle methods.\n * The returned object is merged into the handle returned by entity.use().\n *\n * @example\n * ```typescript\n * createHandle: (ref) => ({\n * getLastHits: () => ref.fsm?.getLastHits() ?? null,\n * getMovement: (moveX, moveY) => ref.fsm?.getMovement(moveX, moveY) ?? { moveX, moveY },\n * }),\n * ```\n */\n createHandle?: (ref: BehaviorRef<O>) => H;\n}\n\n/**\n * Define a typed behavior descriptor.\n *\n * @example\n * ```typescript\n * export const WorldBoundary2DBehavior = defineBehavior({\n * name: 'world-boundary-2d',\n * defaultOptions: { boundaries: { top: 0, bottom: 0, left: 0, right: 0 } },\n * systemFactory: (ctx) => new WorldBoundary2DSystem(ctx.world),\n * createHandle: (ref) => ({\n * getLastHits: () => ref.fsm?.getLastHits() ?? null,\n * getMovement: (moveX: number, moveY: number) =>\n * ref.fsm?.getMovement(moveX, moveY) ?? { moveX, moveY },\n * }),\n * });\n *\n * // Usage - handle has getLastHits and getMovement with full types\n * const boundary = ship.use(WorldBoundary2DBehavior, { ... });\n * const hits = boundary.getLastHits(); // Fully typed!\n * ```\n */\nexport function defineBehavior<\n O extends Record<string, any>,\n H extends Record<string, any> = Record<string, never>,\n I = unknown,\n>(\n config: DefineBehaviorConfig<O, H, I>,\n): BehaviorDescriptor<O, H, I> {\n return {\n key: Symbol.for(`zylem:behavior:${config.name}`),\n defaultOptions: config.defaultOptions,\n systemFactory: config.systemFactory,\n createHandle: config.createHandle,\n };\n}\n","/**\n * WorldBoundary2DFSM\n *\n * Minimal FSM + extended state to track which world boundaries were hit.\n *\n * Notes:\n * - \"Hit boundaries\" is inherently a *set* (can hit left+bottom in one frame),\n * so we store it as extended state (`lastHits`) rather than a single FSM state.\n * - The FSM state is still useful for coarse status like \"inside\" vs \"touching\".\n */\n\nimport { StateMachine, t } from 'typescript-fsm';\n\nexport type WorldBoundary2DHit = 'top' | 'bottom' | 'left' | 'right';\nexport type WorldBoundary2DHits = Record<WorldBoundary2DHit, boolean>;\n\nexport interface WorldBoundary2DPosition {\n\tx: number;\n\ty: number;\n}\n\nexport interface WorldBoundary2DBounds {\n\ttop: number;\n\tbottom: number;\n\tleft: number;\n\tright: number;\n}\n\nexport enum WorldBoundary2DState {\n\tInside = 'inside',\n\tTouching = 'touching',\n}\n\nexport enum WorldBoundary2DEvent {\n\tEnterInside = 'enter-inside',\n\tTouchBoundary = 'touch-boundary',\n}\n\n/**\n * Compute which boundaries are being hit for a position and bounds.\n * This matches the semantics of the legacy `boundary2d` behavior:\n * - left hit if x <= left\n * - right hit if x >= right\n * - bottom hit if y <= bottom\n * - top hit if y >= top\n */\nexport function computeWorldBoundary2DHits(\n\tposition: WorldBoundary2DPosition,\n\tbounds: WorldBoundary2DBounds\n): WorldBoundary2DHits {\n\tconst hits: WorldBoundary2DHits = {\n\t\ttop: false,\n\t\tbottom: false,\n\t\tleft: false,\n\t\tright: false,\n\t};\n\n\tif (position.x <= bounds.left) hits.left = true;\n\telse if (position.x >= bounds.right) hits.right = true;\n\n\tif (position.y <= bounds.bottom) hits.bottom = true;\n\telse if (position.y >= bounds.top) hits.top = true;\n\n\treturn hits;\n}\n\nexport function hasAnyWorldBoundary2DHit(hits: WorldBoundary2DHits): boolean {\n\treturn !!(hits.left || hits.right || hits.top || hits.bottom);\n}\n\n/**\n * FSM wrapper with \"extended state\" (lastHits / lastPosition).\n * Systems should call `update(...)` once per frame.\n */\nexport class WorldBoundary2DFSM {\n\tpublic readonly machine: StateMachine<WorldBoundary2DState, WorldBoundary2DEvent, never>;\n\n\tprivate lastHits: WorldBoundary2DHits = { top: false, bottom: false, left: false, right: false };\n\tprivate lastPosition: WorldBoundary2DPosition | null = null;\n\tprivate lastUpdatedAtMs: number | null = null;\n\n\tconstructor() {\n\t\tthis.machine = new StateMachine<WorldBoundary2DState, WorldBoundary2DEvent, never>(\n\t\t\tWorldBoundary2DState.Inside,\n\t\t\t[\n\t\t\t\tt(WorldBoundary2DState.Inside, WorldBoundary2DEvent.TouchBoundary, WorldBoundary2DState.Touching),\n\t\t\t\tt(WorldBoundary2DState.Touching, WorldBoundary2DEvent.EnterInside, WorldBoundary2DState.Inside),\n\n\t\t\t\t// Self transitions (no-ops)\n\t\t\t\tt(WorldBoundary2DState.Inside, WorldBoundary2DEvent.EnterInside, WorldBoundary2DState.Inside),\n\t\t\t\tt(WorldBoundary2DState.Touching, WorldBoundary2DEvent.TouchBoundary, WorldBoundary2DState.Touching),\n\t\t\t]\n\t\t);\n\t}\n\n\tgetState(): WorldBoundary2DState {\n\t\treturn this.machine.getState();\n\t}\n\n\t/**\n\t * Returns the last computed hits (always available after first update call).\n\t */\n\tgetLastHits(): WorldBoundary2DHits {\n\t\treturn this.lastHits;\n\t}\n\n\t/**\n\t * Returns adjusted movement values based on boundary hits.\n\t * If the entity is touching a boundary and trying to move further into it,\n\t * that axis component is zeroed out.\n\t *\n\t * @param moveX - The desired X movement\n\t * @param moveY - The desired Y movement\n\t * @returns Adjusted { moveX, moveY } with boundary-blocked axes zeroed\n\t */\n\tgetMovement(moveX: number, moveY: number): { moveX: number; moveY: number } {\n\t\tconst hits = this.lastHits;\n\n\t\tlet adjustedX = moveX;\n\t\tlet adjustedY = moveY;\n\n\t\t// If moving left and hitting left, or moving right and hitting right, zero X\n\t\tif ((hits.left && moveX < 0) || (hits.right && moveX > 0)) {\n\t\t\tadjustedX = 0;\n\t\t}\n\n\t\t// If moving down and hitting bottom, or moving up and hitting top, zero Y\n\t\tif ((hits.bottom && moveY < 0) || (hits.top && moveY > 0)) {\n\t\t\tadjustedY = 0;\n\t\t}\n\n\t\treturn { moveX: adjustedX, moveY: adjustedY };\n\t}\n\n\t/**\n\t * Returns the last position passed to `update`, if any.\n\t */\n\tgetLastPosition(): WorldBoundary2DPosition | null {\n\t\treturn this.lastPosition;\n\t}\n\n\t/**\n\t * Best-effort timestamp (ms) of the last `update(...)` call.\n\t * This is optional metadata; systems can ignore it.\n\t */\n\tgetLastUpdatedAtMs(): number | null {\n\t\treturn this.lastUpdatedAtMs;\n\t}\n\n\t/**\n\t * Update FSM + extended state based on current position and bounds.\n\t * Returns the computed hits for convenience.\n\t */\n\tupdate(position: WorldBoundary2DPosition, bounds: WorldBoundary2DBounds): WorldBoundary2DHits {\n\t\tconst hits = computeWorldBoundary2DHits(position, bounds);\n\n\t\tthis.lastHits = hits;\n\t\tthis.lastPosition = { x: position.x, y: position.y };\n\t\tthis.lastUpdatedAtMs = Date.now();\n\n\t\tif (hasAnyWorldBoundary2DHit(hits)) {\n\t\t\tthis.dispatch(WorldBoundary2DEvent.TouchBoundary);\n\t\t} else {\n\t\t\tthis.dispatch(WorldBoundary2DEvent.EnterInside);\n\t\t}\n\n\t\treturn hits;\n\t}\n\n\tprivate dispatch(event: WorldBoundary2DEvent): void {\n\t\tif (this.machine.can(event)) {\n\t\t\tthis.machine.dispatch(event);\n\t\t}\n\t}\n}\n","/**\n * WorldBoundary2DBehavior\n *\n * Tracks which 2D world boundaries an entity is touching.\n * Use `getMovement()` on the behavior handle to adjust movement based on hits.\n *\n * Source of truth: `entity.body` (Rapier rigid body), consistent with other new behaviors.\n */\n\nimport type { IWorld } from 'bitecs';\nimport { defineBehavior, type BehaviorRef } from '../behavior-descriptor';\nimport type { BehaviorSystem } from '../behavior-system';\nimport {\n\tWorldBoundary2DFSM,\n\ttype WorldBoundary2DBounds,\n\ttype WorldBoundary2DHits,\n} from './world-boundary-2d-fsm';\n\nexport interface WorldBoundary2DOptions {\n\t/**\n\t * World boundaries (in world units).\n\t * - left hit if x <= left\n\t * - right hit if x >= right\n\t * - bottom hit if y <= bottom\n\t * - top hit if y >= top\n\t */\n\tboundaries: WorldBoundary2DBounds;\n}\n\n/**\n * Handle methods provided by WorldBoundary2DBehavior\n */\nexport interface WorldBoundary2DHandle {\n\t/**\n\t * Get the last computed boundary hits.\n\t * Returns null until entity is spawned and FSM is initialized.\n\t */\n\tgetLastHits(): WorldBoundary2DHits | null;\n\n\t/**\n\t * Get adjusted movement values based on boundary hits.\n\t * Zeros out movement into boundaries the entity is touching.\n\t */\n\tgetMovement(moveX: number, moveY: number): { moveX: number; moveY: number };\n}\n\nconst defaultOptions: WorldBoundary2DOptions = {\n\tboundaries: { top: 0, bottom: 0, left: 0, right: 0 },\n};\n\n/**\n * Creates behavior-specific handle methods for WorldBoundary2DBehavior.\n */\nfunction createWorldBoundary2DHandle(\n\tref: BehaviorRef<WorldBoundary2DOptions>\n): WorldBoundary2DHandle {\n\treturn {\n\t\tgetLastHits: () => {\n\t\t\tconst fsm = ref.fsm as WorldBoundary2DFSM | undefined;\n\t\t\treturn fsm?.getLastHits() ?? null;\n\t\t},\n\t\tgetMovement: (moveX: number, moveY: number) => {\n\t\t\tconst fsm = ref.fsm as WorldBoundary2DFSM | undefined;\n\t\t\treturn fsm?.getMovement(moveX, moveY) ?? { moveX, moveY };\n\t\t},\n\t};\n}\n\n/**\n * WorldBoundary2DSystem\n *\n * Stage-level system that:\n * - finds entities with this behavior attached\n * - computes and tracks boundary hits using the FSM\n */\nclass WorldBoundary2DSystem implements BehaviorSystem {\n\tconstructor(private world: any) {}\n\n\tupdate(_ecs: IWorld, _delta: number): void {\n\t\tif (!this.world?.collisionMap) return;\n\n\t\tfor (const [, entity] of this.world.collisionMap) {\n\t\t\tconst gameEntity = entity as any;\n\n\t\t\tif (typeof gameEntity.getBehaviorRefs !== 'function') continue;\n\n\t\t\tconst refs = gameEntity.getBehaviorRefs();\n\t\t\tconst boundaryRef = refs.find(\n\t\t\t\t(r: any) => r.descriptor.key === Symbol.for('zylem:behavior:world-boundary-2d')\n\t\t\t);\n\n\t\t\tif (!boundaryRef || !gameEntity.body) continue;\n\n\t\t\tconst options = boundaryRef.options as WorldBoundary2DOptions;\n\n\t\t\t// Create FSM lazily on first update after spawn\n\t\t\tif (!boundaryRef.fsm) {\n\t\t\t\tboundaryRef.fsm = new WorldBoundary2DFSM();\n\t\t\t}\n\n\t\t\tconst body = gameEntity.body;\n\t\t\tconst pos = body.translation();\n\n\t\t\t// Update FSM with current position - consumers use getMovement() to act on hits\n\t\t\tboundaryRef.fsm.update(\n\t\t\t\t{ x: pos.x, y: pos.y },\n\t\t\t\toptions.boundaries\n\t\t\t);\n\t\t}\n\t}\n\n\tdestroy(_ecs: IWorld): void {\n\t\t// No explicit cleanup required (FSMs are stored on behavior refs)\n\t}\n}\n\n/**\n * WorldBoundary2DBehavior\n *\n * @example\n * ```ts\n * import { WorldBoundary2DBehavior } from \"@zylem/game-lib\";\n *\n * const ship = createSprite({ ... });\n * const boundary = ship.use(WorldBoundary2DBehavior, {\n * boundaries: { left: -10, right: 10, bottom: -7.5, top: 7.5 },\n * });\n *\n * ship.onUpdate(({ me }) => {\n * let moveX = ..., moveY = ...;\n * const hits = boundary.getLastHits(); // Fully typed!\n * ({ moveX, moveY } = boundary.getMovement(moveX, moveY));\n * me.moveXY(moveX, moveY);\n * });\n * ```\n */\nexport const WorldBoundary2DBehavior = defineBehavior({\n\tname: 'world-boundary-2d',\n\tdefaultOptions,\n\tsystemFactory: (ctx) => new WorldBoundary2DSystem(ctx.world),\n\tcreateHandle: createWorldBoundary2DHandle,\n});\n"],"mappings":"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","names":["WorldBoundary2DState","WorldBoundary2DEvent"]}
1
+ {"version":3,"sources":["../../src/lib/behaviors/behavior-descriptor.ts","../../src/lib/core/utility/sync-state-machine.ts","../../src/lib/behaviors/shared/bounds-2d.ts","../../src/lib/behaviors/world-boundary-2d/world-boundary-2d-fsm.ts","../../src/lib/behaviors/world-boundary-2d/world-boundary-2d.descriptor.ts"],"sourcesContent":["/**\n * BehaviorDescriptor\n *\n * Type-safe behavior descriptors that provide options inference.\n * Used with entity.use() to declaratively attach behaviors to entities.\n *\n * Each behavior can define its own handle type via `createHandle`,\n * providing behavior-specific methods with full type safety.\n */\n\nimport type { BehaviorSystemFactory } from './behavior-system';\n\n/**\n * Base handle returned by entity.use() for lazy access to behavior runtime.\n * FSM is null until entity is spawned and components are initialized.\n */\nexport interface BaseBehaviorHandle<\n O extends Record<string, any> = Record<string, any>,\n> {\n /** Get the FSM instance (null until entity is spawned) */\n getFSM(): any | null;\n /** Get the current options */\n getOptions(): O;\n /** Access the underlying behavior ref */\n readonly ref: BehaviorRef<O>;\n}\n\n/**\n * Reference to a behavior stored on an entity\n */\nexport interface BehaviorRef<\n O extends Record<string, any> = Record<string, any>,\n> {\n /** The behavior descriptor */\n descriptor: BehaviorDescriptor<O, any>;\n /** Merged options (defaults + overrides) */\n options: O;\n /** Optional FSM instance - set lazily when entity is spawned */\n fsm?: any;\n}\n\n/**\n * A typed behavior descriptor that associates a symbol key with:\n * - Default options (providing type inference)\n * - A system factory to create the behavior system\n * - An optional handle factory for behavior-specific methods\n */\nexport interface BehaviorDescriptor<\n O extends Record<string, any> = Record<string, any>,\n H extends Record<string, any> = Record<string, never>,\n I = unknown,\n> {\n /** Unique symbol identifying this behavior */\n readonly key: symbol;\n /** Default options (used for type inference) */\n readonly defaultOptions: O;\n /** Factory to create the behavior system */\n readonly systemFactory: BehaviorSystemFactory;\n /**\n * Optional factory to create behavior-specific handle methods.\n * These methods are merged into the handle returned by entity.use().\n */\n readonly createHandle?: (ref: BehaviorRef<O>) => H;\n}\n\n/**\n * The full handle type returned by entity.use().\n * Combines base handle with behavior-specific methods.\n */\nexport type BehaviorHandle<\n O extends Record<string, any> = Record<string, any>,\n H extends Record<string, any> = Record<string, never>,\n> = BaseBehaviorHandle<O> & H;\n\n/**\n * Configuration for defining a new behavior\n */\nexport interface DefineBehaviorConfig<\n O extends Record<string, any>,\n H extends Record<string, any> = Record<string, never>,\n I = unknown,\n> {\n /** Human-readable name for debugging */\n name: string;\n /** Default options - these define the type */\n defaultOptions: O;\n /** Factory function to create the system */\n systemFactory: BehaviorSystemFactory;\n /**\n * Optional factory to create behavior-specific handle methods.\n * The returned object is merged into the handle returned by entity.use().\n *\n * @example\n * ```typescript\n * createHandle: (ref) => ({\n * getLastHits: () => ref.fsm?.getLastHits() ?? null,\n * getMovement: (moveX, moveY) => ref.fsm?.getMovement(moveX, moveY) ?? { moveX, moveY },\n * }),\n * ```\n */\n createHandle?: (ref: BehaviorRef<O>) => H;\n}\n\n/**\n * Define a typed behavior descriptor.\n *\n * @example\n * ```typescript\n * export const WorldBoundary2DBehavior = defineBehavior({\n * name: 'world-boundary-2d',\n * defaultOptions: { boundaries: { top: 0, bottom: 0, left: 0, right: 0 } },\n * systemFactory: (ctx) => new WorldBoundary2DSystem(ctx.world),\n * createHandle: (ref) => ({\n * getLastHits: () => ref.fsm?.getLastHits() ?? null,\n * getMovement: (moveX: number, moveY: number) =>\n * ref.fsm?.getMovement(moveX, moveY) ?? { moveX, moveY },\n * }),\n * });\n *\n * // Usage - handle has getLastHits and getMovement with full types\n * const boundary = ship.use(WorldBoundary2DBehavior, { ... });\n * const hits = boundary.getLastHits(); // Fully typed!\n * ```\n */\nexport function defineBehavior<\n O extends Record<string, any>,\n H extends Record<string, any> = Record<string, never>,\n I = unknown,\n>(\n config: DefineBehaviorConfig<O, H, I>,\n): BehaviorDescriptor<O, H, I> {\n return {\n key: Symbol.for(`zylem:behavior:${config.name}`),\n defaultOptions: config.defaultOptions,\n systemFactory: config.systemFactory,\n createHandle: config.createHandle,\n };\n}\n","import {\n\tStateMachine as BaseStateMachine,\n\ttype ILogger,\n\ttype ITransition,\n\ttype SyncCallback,\n} from 'typescript-fsm';\n\nexport { t } from 'typescript-fsm';\nexport type { ILogger, ITransition, SyncCallback };\n\n/**\n * Local wrapper around typescript-fsm's SyncStateMachine.\n *\n * typescript-fsm@1.6.0 incorrectly reports callback-less transitions as\n * unhandled even after moving to the next state, which causes noisy\n * `No transition...` console errors for valid FSM dispatches.\n */\nexport class SyncStateMachine<\n\tSTATE extends string | number | symbol,\n\tEVENT extends string | number | symbol,\n\tCALLBACK extends Record<EVENT, SyncCallback> = Record<EVENT, SyncCallback>,\n> extends BaseStateMachine<STATE, EVENT, CALLBACK> {\n\tconstructor(\n\t\tinit: STATE,\n\t\ttransitions: ITransition<STATE, EVENT, CALLBACK[EVENT]>[] = [],\n\t\tlogger: ILogger = console,\n\t) {\n\t\tsuper(init, transitions, logger);\n\t}\n\n\tdispatch<E extends EVENT>(_event: E, ..._args: unknown[]): Promise<void> {\n\t\tthrow new Error('SyncStateMachine does not support async dispatch.');\n\t}\n\n\tsyncDispatch<E extends EVENT>(event: E, ...args: unknown[]): boolean {\n\t\tconst found = this.transitions.some((transition) => {\n\t\t\tif (transition.fromState !== this._current || transition.event !== event) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconst current = this._current;\n\t\t\tthis._current = transition.toState;\n\n\t\t\tif (!transition.cb) {\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\ttransition.cb(...args);\n\t\t\t\treturn true;\n\t\t\t} catch (error) {\n\t\t\t\tthis._current = current;\n\t\t\t\tthis.logger.error('Exception in callback', error);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t});\n\n\t\tif (!found) {\n\t\t\tconst errorMessage = this.formatErr(this._current, event);\n\t\t\tthis.logger.error(errorMessage);\n\t\t}\n\n\t\treturn found;\n\t}\n}\n","export interface Bounds2DPoint {\n\tx: number;\n\ty: number;\n}\n\nexport interface Bounds2DRect {\n\tminX: number;\n\tmaxX: number;\n\tminY: number;\n\tmaxY: number;\n}\n\nexport interface Bounds2DSize {\n\twidth: number;\n\theight: number;\n\tcenterX?: number;\n\tcenterY?: number;\n}\n\nexport interface Bounds2DHits {\n\tleft: boolean;\n\tright: boolean;\n\ttop: boolean;\n\tbottom: boolean;\n}\n\nexport interface Bounds2DWrapResult {\n\tx: number;\n\ty: number;\n\twrapped: boolean;\n}\n\nexport function createBounds2DRect({\n\twidth,\n\theight,\n\tcenterX = 0,\n\tcenterY = 0,\n}: Bounds2DSize): Bounds2DRect {\n\tconst halfWidth = width / 2;\n\tconst halfHeight = height / 2;\n\treturn {\n\t\tminX: centerX - halfWidth,\n\t\tmaxX: centerX + halfWidth,\n\t\tminY: centerY - halfHeight,\n\t\tmaxY: centerY + halfHeight,\n\t};\n}\n\nexport function computeBounds2DHits(\n\tposition: Bounds2DPoint,\n\tbounds: Bounds2DRect,\n): Bounds2DHits {\n\tconst hits: Bounds2DHits = {\n\t\ttop: false,\n\t\tbottom: false,\n\t\tleft: false,\n\t\tright: false,\n\t};\n\n\tif (position.x <= bounds.minX) hits.left = true;\n\telse if (position.x >= bounds.maxX) hits.right = true;\n\n\tif (position.y <= bounds.minY) hits.bottom = true;\n\telse if (position.y >= bounds.maxY) hits.top = true;\n\n\treturn hits;\n}\n\nexport function hasAnyBounds2DHit(hits: Bounds2DHits): boolean {\n\treturn hits.left || hits.right || hits.top || hits.bottom;\n}\n\nexport function constrainMovementToBounds2D(\n\thits: Bounds2DHits,\n\tmoveX: number,\n\tmoveY: number,\n): { moveX: number; moveY: number } {\n\tlet adjustedX = moveX;\n\tlet adjustedY = moveY;\n\n\tif ((hits.left && moveX < 0) || (hits.right && moveX > 0)) {\n\t\tadjustedX = 0;\n\t}\n\n\tif ((hits.bottom && moveY < 0) || (hits.top && moveY > 0)) {\n\t\tadjustedY = 0;\n\t}\n\n\treturn { moveX: adjustedX, moveY: adjustedY };\n}\n\nexport function wrapPoint2D(\n\tposition: Bounds2DPoint,\n\tbounds: Bounds2DRect,\n): Bounds2DWrapResult {\n\tlet x = position.x;\n\tlet y = position.y;\n\tlet wrapped = false;\n\n\tif (position.x < bounds.minX) {\n\t\tx = bounds.maxX - (bounds.minX - position.x);\n\t\twrapped = true;\n\t} else if (position.x > bounds.maxX) {\n\t\tx = bounds.minX + (position.x - bounds.maxX);\n\t\twrapped = true;\n\t}\n\n\tif (position.y < bounds.minY) {\n\t\ty = bounds.maxY - (bounds.minY - position.y);\n\t\twrapped = true;\n\t} else if (position.y > bounds.maxY) {\n\t\ty = bounds.minY + (position.y - bounds.maxY);\n\t\twrapped = true;\n\t}\n\n\treturn { x, y, wrapped };\n}\n\nexport function getBounds2DNormalFromHits(\n\thits: Bounds2DHits,\n): { x: number; y: number } {\n\tlet x = 0;\n\tlet y = 0;\n\n\tif (hits.left) x = 1;\n\tif (hits.right) x = -1;\n\tif (hits.bottom) y = 1;\n\tif (hits.top) y = -1;\n\n\treturn { x, y };\n}\n","/**\n * WorldBoundary2DFSM\n *\n * Minimal FSM + extended state to track which world boundaries were hit.\n *\n * Notes:\n * - \"Hit boundaries\" is inherently a *set* (can hit left+bottom in one frame),\n * so we store it as extended state (`lastHits`) rather than a single FSM state.\n * - The FSM state is still useful for coarse status like \"inside\" vs \"touching\".\n */\n\nimport { SyncStateMachine, t } from '../../core/utility/sync-state-machine';\nimport {\n\tcomputeBounds2DHits,\n\tconstrainMovementToBounds2D,\n\thasAnyBounds2DHit,\n} from '../shared/bounds-2d';\n\nexport type WorldBoundary2DHit = 'top' | 'bottom' | 'left' | 'right';\nexport type WorldBoundary2DHits = Record<WorldBoundary2DHit, boolean>;\n\nexport interface WorldBoundary2DPosition {\n\tx: number;\n\ty: number;\n}\n\nexport interface WorldBoundary2DBounds {\n\ttop: number;\n\tbottom: number;\n\tleft: number;\n\tright: number;\n}\n\nexport enum WorldBoundary2DState {\n\tInside = 'inside',\n\tTouching = 'touching',\n}\n\nexport enum WorldBoundary2DEvent {\n\tEnterInside = 'enter-inside',\n\tTouchBoundary = 'touch-boundary',\n}\n\n/**\n * Compute which boundaries are being hit for a position and bounds.\n * This matches the semantics of the legacy `boundary2d` behavior:\n * - left hit if x <= left\n * - right hit if x >= right\n * - bottom hit if y <= bottom\n * - top hit if y >= top\n */\nexport function computeWorldBoundary2DHits(\n\tposition: WorldBoundary2DPosition,\n\tbounds: WorldBoundary2DBounds\n): WorldBoundary2DHits {\n\treturn computeBounds2DHits(position, {\n\t\tminX: bounds.left,\n\t\tmaxX: bounds.right,\n\t\tminY: bounds.bottom,\n\t\tmaxY: bounds.top,\n\t});\n}\n\nexport function hasAnyWorldBoundary2DHit(hits: WorldBoundary2DHits): boolean {\n\treturn hasAnyBounds2DHit(hits);\n}\n\n/**\n * FSM wrapper with \"extended state\" (lastHits / lastPosition).\n * Systems should call `update(...)` once per frame.\n */\nexport class WorldBoundary2DFSM {\n\tpublic readonly machine: SyncStateMachine<WorldBoundary2DState, WorldBoundary2DEvent, never>;\n\n\tprivate lastHits: WorldBoundary2DHits = { top: false, bottom: false, left: false, right: false };\n\tprivate lastPosition: WorldBoundary2DPosition | null = null;\n\tprivate lastUpdatedAtMs: number | null = null;\n\n\tconstructor() {\n\t\tthis.machine = new SyncStateMachine<WorldBoundary2DState, WorldBoundary2DEvent, never>(\n\t\t\tWorldBoundary2DState.Inside,\n\t\t\t[\n\t\t\t\tt(WorldBoundary2DState.Inside, WorldBoundary2DEvent.TouchBoundary, WorldBoundary2DState.Touching),\n\t\t\t\tt(WorldBoundary2DState.Touching, WorldBoundary2DEvent.EnterInside, WorldBoundary2DState.Inside),\n\n\t\t\t\t// Self transitions (no-ops)\n\t\t\t\tt(WorldBoundary2DState.Inside, WorldBoundary2DEvent.EnterInside, WorldBoundary2DState.Inside),\n\t\t\t\tt(WorldBoundary2DState.Touching, WorldBoundary2DEvent.TouchBoundary, WorldBoundary2DState.Touching),\n\t\t\t]\n\t\t);\n\t}\n\n\tgetState(): WorldBoundary2DState {\n\t\treturn this.machine.getState();\n\t}\n\n\t/**\n\t * Returns the last computed hits (always available after first update call).\n\t */\n\tgetLastHits(): WorldBoundary2DHits {\n\t\treturn this.lastHits;\n\t}\n\n\t/**\n\t * Returns adjusted movement values based on boundary hits.\n\t * If the entity is touching a boundary and trying to move further into it,\n\t * that axis component is zeroed out.\n\t *\n\t * @param moveX - The desired X movement\n\t * @param moveY - The desired Y movement\n\t * @returns Adjusted { moveX, moveY } with boundary-blocked axes zeroed\n\t */\n\tgetMovement(moveX: number, moveY: number): { moveX: number; moveY: number } {\n\t\treturn constrainMovementToBounds2D(this.lastHits, moveX, moveY);\n\t}\n\n\t/**\n\t * Returns the last position passed to `update`, if any.\n\t */\n\tgetLastPosition(): WorldBoundary2DPosition | null {\n\t\treturn this.lastPosition;\n\t}\n\n\t/**\n\t * Best-effort timestamp (ms) of the last `update(...)` call.\n\t * This is optional metadata; systems can ignore it.\n\t */\n\tgetLastUpdatedAtMs(): number | null {\n\t\treturn this.lastUpdatedAtMs;\n\t}\n\n\t/**\n\t * Update FSM + extended state based on current position and bounds.\n\t * Returns the computed hits for convenience.\n\t */\n\tupdate(position: WorldBoundary2DPosition, bounds: WorldBoundary2DBounds): WorldBoundary2DHits {\n\t\tconst hits = computeWorldBoundary2DHits(position, bounds);\n\n\t\tthis.lastHits = hits;\n\t\tthis.lastPosition = { x: position.x, y: position.y };\n\t\tthis.lastUpdatedAtMs = Date.now();\n\n\t\tif (hasAnyWorldBoundary2DHit(hits)) {\n\t\t\tthis.dispatch(WorldBoundary2DEvent.TouchBoundary);\n\t\t} else {\n\t\t\tthis.dispatch(WorldBoundary2DEvent.EnterInside);\n\t\t}\n\n\t\treturn hits;\n\t}\n\n\tprivate dispatch(event: WorldBoundary2DEvent): void {\n\t\tif (this.machine.can(event)) {\n\t\t\tthis.machine.syncDispatch(event);\n\t\t}\n\t}\n}\n","/**\n * WorldBoundary2DBehavior\n *\n * Tracks which 2D world boundaries an entity is touching.\n * Use `getMovement()` on the behavior handle to adjust movement based on hits.\n *\n * Source of truth: `entity.body` (Rapier rigid body), consistent with other new behaviors.\n */\n\nimport type { IWorld } from 'bitecs';\nimport { defineBehavior, type BehaviorRef } from '../behavior-descriptor';\nimport type { BehaviorEntityLink, BehaviorSystem } from '../behavior-system';\nimport {\n\tWorldBoundary2DFSM,\n\ttype WorldBoundary2DBounds,\n\ttype WorldBoundary2DHits,\n} from './world-boundary-2d-fsm';\n\nexport interface WorldBoundary2DOptions {\n\t/**\n\t * World boundaries (in world units).\n\t * - left hit if x <= left\n\t * - right hit if x >= right\n\t * - bottom hit if y <= bottom\n\t * - top hit if y >= top\n\t */\n\tboundaries: WorldBoundary2DBounds;\n}\n\n/**\n * Handle methods provided by WorldBoundary2DBehavior\n */\nexport interface WorldBoundary2DHandle {\n\t/**\n\t * Get the last computed boundary hits.\n\t * Returns null until entity is spawned and FSM is initialized.\n\t */\n\tgetLastHits(): WorldBoundary2DHits | null;\n\n\t/**\n\t * Get adjusted movement values based on boundary hits.\n\t * Zeros out movement into boundaries the entity is touching.\n\t */\n\tgetMovement(moveX: number, moveY: number): { moveX: number; moveY: number };\n}\n\nconst defaultOptions: WorldBoundary2DOptions = {\n\tboundaries: { top: 0, bottom: 0, left: 0, right: 0 },\n};\n\nconst WORLD_BOUNDARY_BEHAVIOR_KEY = Symbol.for(\n\t'zylem:behavior:world-boundary-2d',\n);\n\n/**\n * Creates behavior-specific handle methods for WorldBoundary2DBehavior.\n */\nfunction createWorldBoundary2DHandle(\n\tref: BehaviorRef<WorldBoundary2DOptions>\n): WorldBoundary2DHandle {\n\treturn {\n\t\tgetLastHits: () => {\n\t\t\tconst fsm = ref.fsm as WorldBoundary2DFSM | undefined;\n\t\t\treturn fsm?.getLastHits() ?? null;\n\t\t},\n\t\tgetMovement: (moveX: number, moveY: number) => {\n\t\t\tconst fsm = ref.fsm as WorldBoundary2DFSM | undefined;\n\t\t\treturn fsm?.getMovement(moveX, moveY) ?? { moveX, moveY };\n\t\t},\n\t};\n}\n\n/**\n * WorldBoundary2DSystem\n *\n * Stage-level system that:\n * - finds entities with this behavior attached\n * - computes and tracks boundary hits using the FSM\n */\nclass WorldBoundary2DSystem implements BehaviorSystem {\n\tconstructor(\n\t\tprivate world: any,\n\t\tprivate getBehaviorLinks?: (key: symbol) => Iterable<BehaviorEntityLink>,\n\t) {}\n\n\tupdate(_ecs: IWorld, _delta: number): void {\n\t\tconst links = this.getBehaviorLinks?.(WORLD_BOUNDARY_BEHAVIOR_KEY);\n\t\tif (!links) return;\n\n\t\tfor (const link of links) {\n\t\t\tconst gameEntity = link.entity as any;\n\t\t\tconst boundaryRef = link.ref as any;\n\t\t\tif (!gameEntity.body) continue;\n\n\t\t\tconst options = boundaryRef.options as WorldBoundary2DOptions;\n\n\t\t\t// Create FSM lazily on first update after spawn\n\t\t\tif (!boundaryRef.fsm) {\n\t\t\t\tboundaryRef.fsm = new WorldBoundary2DFSM();\n\t\t\t}\n\n\t\t\tconst body = gameEntity.body;\n\t\t\tconst pos = body.translation();\n\n\t\t\t// Update FSM with current position - consumers use getMovement() to act on hits\n\t\t\tboundaryRef.fsm.update(\n\t\t\t\t{ x: pos.x, y: pos.y },\n\t\t\t\toptions.boundaries\n\t\t\t);\n\t\t}\n\t}\n\n\tdestroy(_ecs: IWorld): void {\n\t\t// No explicit cleanup required (FSMs are stored on behavior refs)\n\t}\n}\n\n/**\n * WorldBoundary2DBehavior\n *\n * @example\n * ```ts\n * import { WorldBoundary2DBehavior } from \"@zylem/game-lib\";\n *\n * const ship = createSprite({ ... });\n * const boundary = ship.use(WorldBoundary2DBehavior, {\n * boundaries: { left: -10, right: 10, bottom: -7.5, top: 7.5 },\n * });\n *\n * ship.onUpdate(({ me }) => {\n * let moveX = ..., moveY = ...;\n * const hits = boundary.getLastHits(); // Fully typed!\n * ({ moveX, moveY } = boundary.getMovement(moveX, moveY));\n * me.moveXY(moveX, moveY);\n * });\n * ```\n */\nexport const WorldBoundary2DBehavior = defineBehavior({\n\tname: 'world-boundary-2d',\n\tdefaultOptions,\n\tsystemFactory: (ctx) =>\n\t\tnew WorldBoundary2DSystem(ctx.world, ctx.getBehaviorLinks),\n\tcreateHandle: createWorldBoundary2DHandle,\n});\n"],"mappings":"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","names":["WorldBoundary2DState","WorldBoundary2DEvent"]}
@@ -0,0 +1,76 @@
1
+ import { c as BehaviorDescriptor } from '../behavior-descriptor-BXnVR8Ki.js';
2
+ import { S as SyncStateMachine } from '../sync-state-machine-CZyspBpj.js';
3
+ import 'bitecs';
4
+ import 'typescript-fsm';
5
+
6
+ interface Bounds3DPoint {
7
+ x: number;
8
+ y: number;
9
+ z: number;
10
+ }
11
+ interface Bounds3DPadding {
12
+ x: number;
13
+ y: number;
14
+ z: number;
15
+ }
16
+ type Bounds3DPaddingInput = number | Partial<Bounds3DPadding> | undefined;
17
+
18
+ type WorldBoundary3DHit = 'top' | 'bottom' | 'left' | 'right' | 'back' | 'front';
19
+ type WorldBoundary3DHits = Record<WorldBoundary3DHit, boolean>;
20
+ interface WorldBoundary3DPosition extends Bounds3DPoint {
21
+ }
22
+ interface WorldBoundary3DBounds {
23
+ top: number;
24
+ bottom: number;
25
+ left: number;
26
+ right: number;
27
+ back: number;
28
+ front: number;
29
+ }
30
+ declare enum WorldBoundary3DState {
31
+ Inside = "inside",
32
+ Touching = "touching"
33
+ }
34
+ declare enum WorldBoundary3DEvent {
35
+ EnterInside = "enter-inside",
36
+ TouchBoundary = "touch-boundary"
37
+ }
38
+ declare function computeWorldBoundary3DHits(position: WorldBoundary3DPosition, bounds: WorldBoundary3DBounds, padding?: Bounds3DPaddingInput): WorldBoundary3DHits;
39
+ declare function hasAnyWorldBoundary3DHit(hits: WorldBoundary3DHits): boolean;
40
+ declare class WorldBoundary3DFSM {
41
+ readonly machine: SyncStateMachine<WorldBoundary3DState, WorldBoundary3DEvent, never>;
42
+ private lastHits;
43
+ private lastPosition;
44
+ private lastClampedPosition;
45
+ private lastUpdatedAtMs;
46
+ constructor();
47
+ getState(): WorldBoundary3DState;
48
+ getLastHits(): WorldBoundary3DHits;
49
+ getMovement(moveX: number, moveY: number, moveZ: number): {
50
+ moveX: number;
51
+ moveY: number;
52
+ moveZ: number;
53
+ };
54
+ getLastPosition(): WorldBoundary3DPosition | null;
55
+ getLastClampedPosition(): WorldBoundary3DPosition | null;
56
+ getLastUpdatedAtMs(): number | null;
57
+ update(position: WorldBoundary3DPosition, bounds: WorldBoundary3DBounds, padding?: Bounds3DPaddingInput): WorldBoundary3DHits;
58
+ private dispatch;
59
+ }
60
+
61
+ interface WorldBoundary3DOptions {
62
+ boundaries: WorldBoundary3DBounds;
63
+ padding?: Bounds3DPaddingInput;
64
+ }
65
+ interface WorldBoundary3DHandle {
66
+ getLastHits(): WorldBoundary3DHits | null;
67
+ getMovement(moveX: number, moveY: number, moveZ: number): {
68
+ moveX: number;
69
+ moveY: number;
70
+ moveZ: number;
71
+ };
72
+ getLastClampedPosition(): WorldBoundary3DPosition | null;
73
+ }
74
+ declare const WorldBoundary3DBehavior: BehaviorDescriptor<WorldBoundary3DOptions, WorldBoundary3DHandle, unknown>;
75
+
76
+ export { WorldBoundary3DBehavior, type WorldBoundary3DBounds, WorldBoundary3DEvent, WorldBoundary3DFSM, type WorldBoundary3DHandle, type WorldBoundary3DHit, type WorldBoundary3DHits, type WorldBoundary3DOptions, type WorldBoundary3DPosition, WorldBoundary3DState, computeWorldBoundary3DHits, hasAnyWorldBoundary3DHit };
@@ -0,0 +1,274 @@
1
+ // src/lib/behaviors/behavior-descriptor.ts
2
+ function defineBehavior(config) {
3
+ return {
4
+ key: /* @__PURE__ */ Symbol.for(`zylem:behavior:${config.name}`),
5
+ defaultOptions: config.defaultOptions,
6
+ systemFactory: config.systemFactory,
7
+ createHandle: config.createHandle
8
+ };
9
+ }
10
+
11
+ // src/lib/core/utility/sync-state-machine.ts
12
+ import {
13
+ StateMachine as BaseStateMachine
14
+ } from "typescript-fsm";
15
+ import { t } from "typescript-fsm";
16
+ var SyncStateMachine = class extends BaseStateMachine {
17
+ constructor(init, transitions = [], logger = console) {
18
+ super(init, transitions, logger);
19
+ }
20
+ dispatch(_event, ..._args) {
21
+ throw new Error("SyncStateMachine does not support async dispatch.");
22
+ }
23
+ syncDispatch(event, ...args) {
24
+ const found = this.transitions.some((transition) => {
25
+ if (transition.fromState !== this._current || transition.event !== event) {
26
+ return false;
27
+ }
28
+ const current = this._current;
29
+ this._current = transition.toState;
30
+ if (!transition.cb) {
31
+ return true;
32
+ }
33
+ try {
34
+ transition.cb(...args);
35
+ return true;
36
+ } catch (error) {
37
+ this._current = current;
38
+ this.logger.error("Exception in callback", error);
39
+ throw error;
40
+ }
41
+ });
42
+ if (!found) {
43
+ const errorMessage = this.formatErr(this._current, event);
44
+ this.logger.error(errorMessage);
45
+ }
46
+ return found;
47
+ }
48
+ };
49
+
50
+ // src/lib/behaviors/shared/bounds-3d.ts
51
+ function normalizeBounds3DPadding(padding) {
52
+ if (typeof padding === "number") {
53
+ return { x: padding, y: padding, z: padding };
54
+ }
55
+ return {
56
+ x: padding?.x ?? 0,
57
+ y: padding?.y ?? 0,
58
+ z: padding?.z ?? 0
59
+ };
60
+ }
61
+ function insetBounds3D(bounds, padding) {
62
+ const resolved = normalizeBounds3DPadding(padding);
63
+ return {
64
+ minX: bounds.minX + resolved.x,
65
+ maxX: bounds.maxX - resolved.x,
66
+ minY: bounds.minY + resolved.y,
67
+ maxY: bounds.maxY - resolved.y,
68
+ minZ: bounds.minZ + resolved.z,
69
+ maxZ: bounds.maxZ - resolved.z
70
+ };
71
+ }
72
+ function computeBounds3DHits(position, bounds) {
73
+ const hits = {
74
+ left: false,
75
+ right: false,
76
+ top: false,
77
+ bottom: false,
78
+ back: false,
79
+ front: false
80
+ };
81
+ if (position.x <= bounds.minX) hits.left = true;
82
+ else if (position.x >= bounds.maxX) hits.right = true;
83
+ if (position.y <= bounds.minY) hits.bottom = true;
84
+ else if (position.y >= bounds.maxY) hits.top = true;
85
+ if (position.z <= bounds.minZ) hits.back = true;
86
+ else if (position.z >= bounds.maxZ) hits.front = true;
87
+ return hits;
88
+ }
89
+ function hasAnyBounds3DHit(hits) {
90
+ return hits.left || hits.right || hits.top || hits.bottom || hits.back || hits.front;
91
+ }
92
+ function constrainMovementToBounds3D(hits, moveX, moveY, moveZ) {
93
+ let adjustedX = moveX;
94
+ let adjustedY = moveY;
95
+ let adjustedZ = moveZ;
96
+ if (hits.left && moveX < 0 || hits.right && moveX > 0) {
97
+ adjustedX = 0;
98
+ }
99
+ if (hits.bottom && moveY < 0 || hits.top && moveY > 0) {
100
+ adjustedY = 0;
101
+ }
102
+ if (hits.back && moveZ < 0 || hits.front && moveZ > 0) {
103
+ adjustedZ = 0;
104
+ }
105
+ return { moveX: adjustedX, moveY: adjustedY, moveZ: adjustedZ };
106
+ }
107
+ function clampPointToBounds3D(position, bounds) {
108
+ return {
109
+ x: Math.min(bounds.maxX, Math.max(bounds.minX, position.x)),
110
+ y: Math.min(bounds.maxY, Math.max(bounds.minY, position.y)),
111
+ z: Math.min(bounds.maxZ, Math.max(bounds.minZ, position.z))
112
+ };
113
+ }
114
+
115
+ // src/lib/behaviors/world-boundary-3d/world-boundary-3d-fsm.ts
116
+ var WorldBoundary3DState = /* @__PURE__ */ ((WorldBoundary3DState2) => {
117
+ WorldBoundary3DState2["Inside"] = "inside";
118
+ WorldBoundary3DState2["Touching"] = "touching";
119
+ return WorldBoundary3DState2;
120
+ })(WorldBoundary3DState || {});
121
+ var WorldBoundary3DEvent = /* @__PURE__ */ ((WorldBoundary3DEvent2) => {
122
+ WorldBoundary3DEvent2["EnterInside"] = "enter-inside";
123
+ WorldBoundary3DEvent2["TouchBoundary"] = "touch-boundary";
124
+ return WorldBoundary3DEvent2;
125
+ })(WorldBoundary3DEvent || {});
126
+ function computeWorldBoundary3DHits(position, bounds, padding) {
127
+ return computeBounds3DHits(position, insetBounds3D({
128
+ minX: bounds.left,
129
+ maxX: bounds.right,
130
+ minY: bounds.bottom,
131
+ maxY: bounds.top,
132
+ minZ: bounds.back,
133
+ maxZ: bounds.front
134
+ }, padding));
135
+ }
136
+ function hasAnyWorldBoundary3DHit(hits) {
137
+ return hasAnyBounds3DHit(hits);
138
+ }
139
+ var WorldBoundary3DFSM = class {
140
+ machine;
141
+ lastHits = {
142
+ top: false,
143
+ bottom: false,
144
+ left: false,
145
+ right: false,
146
+ back: false,
147
+ front: false
148
+ };
149
+ lastPosition = null;
150
+ lastClampedPosition = null;
151
+ lastUpdatedAtMs = null;
152
+ constructor() {
153
+ this.machine = new SyncStateMachine(
154
+ "inside" /* Inside */,
155
+ [
156
+ t("inside" /* Inside */, "touch-boundary" /* TouchBoundary */, "touching" /* Touching */),
157
+ t("touching" /* Touching */, "enter-inside" /* EnterInside */, "inside" /* Inside */),
158
+ t("inside" /* Inside */, "enter-inside" /* EnterInside */, "inside" /* Inside */),
159
+ t("touching" /* Touching */, "touch-boundary" /* TouchBoundary */, "touching" /* Touching */)
160
+ ]
161
+ );
162
+ }
163
+ getState() {
164
+ return this.machine.getState();
165
+ }
166
+ getLastHits() {
167
+ return this.lastHits;
168
+ }
169
+ getMovement(moveX, moveY, moveZ) {
170
+ return constrainMovementToBounds3D(this.lastHits, moveX, moveY, moveZ);
171
+ }
172
+ getLastPosition() {
173
+ return this.lastPosition;
174
+ }
175
+ getLastClampedPosition() {
176
+ return this.lastClampedPosition;
177
+ }
178
+ getLastUpdatedAtMs() {
179
+ return this.lastUpdatedAtMs;
180
+ }
181
+ update(position, bounds, padding) {
182
+ const effectiveBounds = insetBounds3D({
183
+ minX: bounds.left,
184
+ maxX: bounds.right,
185
+ minY: bounds.bottom,
186
+ maxY: bounds.top,
187
+ minZ: bounds.back,
188
+ maxZ: bounds.front
189
+ }, padding);
190
+ const hits = computeBounds3DHits(position, effectiveBounds);
191
+ this.lastHits = hits;
192
+ this.lastPosition = { x: position.x, y: position.y, z: position.z };
193
+ this.lastClampedPosition = clampPointToBounds3D(position, effectiveBounds);
194
+ this.lastUpdatedAtMs = Date.now();
195
+ if (hasAnyWorldBoundary3DHit(hits)) {
196
+ this.dispatch("touch-boundary" /* TouchBoundary */);
197
+ } else {
198
+ this.dispatch("enter-inside" /* EnterInside */);
199
+ }
200
+ return hits;
201
+ }
202
+ dispatch(event) {
203
+ if (this.machine.can(event)) {
204
+ this.machine.syncDispatch(event);
205
+ }
206
+ }
207
+ };
208
+
209
+ // src/lib/behaviors/world-boundary-3d/world-boundary-3d.descriptor.ts
210
+ var defaultOptions = {
211
+ boundaries: { top: 0, bottom: 0, left: 0, right: 0, back: 0, front: 0 },
212
+ padding: 0
213
+ };
214
+ var WORLD_BOUNDARY_3D_BEHAVIOR_KEY = /* @__PURE__ */ Symbol.for(
215
+ "zylem:behavior:world-boundary-3d"
216
+ );
217
+ function createWorldBoundary3DHandle(ref) {
218
+ return {
219
+ getLastHits: () => {
220
+ const fsm = ref.fsm;
221
+ return fsm?.getLastHits() ?? null;
222
+ },
223
+ getMovement: (moveX, moveY, moveZ) => {
224
+ const fsm = ref.fsm;
225
+ return fsm?.getMovement(moveX, moveY, moveZ) ?? { moveX, moveY, moveZ };
226
+ },
227
+ getLastClampedPosition: () => {
228
+ const fsm = ref.fsm;
229
+ return fsm?.getLastClampedPosition() ?? null;
230
+ }
231
+ };
232
+ }
233
+ var WorldBoundary3DSystem = class {
234
+ constructor(world, getBehaviorLinks) {
235
+ this.world = world;
236
+ this.getBehaviorLinks = getBehaviorLinks;
237
+ }
238
+ update(_ecs, _delta) {
239
+ const links = this.getBehaviorLinks?.(WORLD_BOUNDARY_3D_BEHAVIOR_KEY);
240
+ if (!links) return;
241
+ for (const link of links) {
242
+ const gameEntity = link.entity;
243
+ const boundaryRef = link.ref;
244
+ if (!gameEntity.body) continue;
245
+ const options = boundaryRef.options;
246
+ if (!boundaryRef.fsm) {
247
+ boundaryRef.fsm = new WorldBoundary3DFSM();
248
+ }
249
+ const pos = gameEntity.body.translation();
250
+ boundaryRef.fsm.update(
251
+ { x: pos.x, y: pos.y, z: pos.z },
252
+ options.boundaries,
253
+ options.padding
254
+ );
255
+ }
256
+ }
257
+ destroy(_ecs) {
258
+ }
259
+ };
260
+ var WorldBoundary3DBehavior = defineBehavior({
261
+ name: "world-boundary-3d",
262
+ defaultOptions,
263
+ systemFactory: (ctx) => new WorldBoundary3DSystem(ctx.world, ctx.getBehaviorLinks),
264
+ createHandle: createWorldBoundary3DHandle
265
+ });
266
+ export {
267
+ WorldBoundary3DBehavior,
268
+ WorldBoundary3DEvent,
269
+ WorldBoundary3DFSM,
270
+ WorldBoundary3DState,
271
+ computeWorldBoundary3DHits,
272
+ hasAnyWorldBoundary3DHit
273
+ };
274
+ //# sourceMappingURL=world-boundary-3d.js.map