@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.
- package/dist/behaviors.d.ts +834 -88
- package/dist/behaviors.js +1166 -355
- package/dist/behaviors.js.map +1 -1
- package/dist/blueprints-Cq3Ko6_G.d.ts +26 -0
- package/dist/{camera-Dk-fOVZE.d.ts → camera-CeJPAgGg.d.ts} +20 -46
- package/dist/camera.d.ts +2 -2
- package/dist/camera.js +340 -129
- package/dist/camera.js.map +1 -1
- package/dist/{core-C2mjetAd.d.ts → core-bO8TzV7u.d.ts} +113 -44
- package/dist/core.d.ts +8 -5
- package/dist/core.js +6180 -3678
- package/dist/core.js.map +1 -1
- package/dist/entities-DvByhMGU.d.ts +306 -0
- package/dist/entities.d.ts +5 -267
- package/dist/entities.js +3239 -1893
- package/dist/entities.js.map +1 -1
- package/dist/entity-Bq_eNEDI.d.ts +28 -0
- package/dist/entity-types-DAu8sGJH.d.ts +26 -0
- package/dist/main.d.ts +147 -31
- package/dist/main.js +9364 -5479
- package/dist/main.js.map +1 -1
- package/dist/{stage-CrmY7V0i.d.ts → stage-types-Bd-KtcYT.d.ts} +149 -61
- package/dist/stage.d.ts +42 -20
- package/dist/stage.js +4103 -2027
- package/dist/stage.js.map +1 -1
- package/dist/world-C8tQ7Plj.d.ts +774 -0
- package/package.json +2 -1
- package/dist/entity-bQElAdpo.d.ts +0 -347
- package/dist/entity-spawner-DNnLYnZq.d.ts +0 -11
package/dist/behaviors.js
CHANGED
|
@@ -1,398 +1,1209 @@
|
|
|
1
|
-
// src/lib/
|
|
2
|
-
|
|
3
|
-
boundaries: {
|
|
4
|
-
top: 0,
|
|
5
|
-
bottom: 0,
|
|
6
|
-
left: 0,
|
|
7
|
-
right: 0
|
|
8
|
-
},
|
|
9
|
-
stopMovement: true
|
|
10
|
-
};
|
|
11
|
-
function boundary2d(options = {}) {
|
|
1
|
+
// src/lib/behaviors/behavior-descriptor.ts
|
|
2
|
+
function defineBehavior(config) {
|
|
12
3
|
return {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
4
|
+
key: /* @__PURE__ */ Symbol.for(`zylem:behavior:${config.name}`),
|
|
5
|
+
defaultOptions: config.defaultOptions,
|
|
6
|
+
systemFactory: config.systemFactory,
|
|
7
|
+
createHandle: config.createHandle
|
|
17
8
|
};
|
|
18
9
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
};
|
|
25
|
-
const position = entity.getPosition();
|
|
26
|
-
if (!position) return;
|
|
27
|
-
let boundariesHit = { top: false, bottom: false, left: false, right: false };
|
|
28
|
-
if (position.x <= boundaries.left) {
|
|
29
|
-
boundariesHit.left = true;
|
|
30
|
-
} else if (position.x >= boundaries.right) {
|
|
31
|
-
boundariesHit.right = true;
|
|
32
|
-
}
|
|
33
|
-
if (position.y <= boundaries.bottom) {
|
|
34
|
-
boundariesHit.bottom = true;
|
|
35
|
-
} else if (position.y >= boundaries.top) {
|
|
36
|
-
boundariesHit.top = true;
|
|
37
|
-
}
|
|
38
|
-
const stopMovement = options.stopMovement ?? true;
|
|
39
|
-
if (stopMovement && boundariesHit) {
|
|
40
|
-
const velocity = entity.getVelocity() ?? { x: 0, y: 0, z: 0 };
|
|
41
|
-
let { x: newX, y: newY } = velocity;
|
|
42
|
-
if (velocity?.y < 0 && boundariesHit.bottom) {
|
|
43
|
-
newY = 0;
|
|
44
|
-
} else if (velocity?.y > 0 && boundariesHit.top) {
|
|
45
|
-
newY = 0;
|
|
46
|
-
}
|
|
47
|
-
if (velocity?.x < 0 && boundariesHit.left) {
|
|
48
|
-
newX = 0;
|
|
49
|
-
} else if (velocity?.x > 0 && boundariesHit.right) {
|
|
50
|
-
newX = 0;
|
|
51
|
-
}
|
|
52
|
-
entity.moveXY(newX, newY);
|
|
53
|
-
}
|
|
54
|
-
if (onBoundary && boundariesHit) {
|
|
55
|
-
onBoundary({
|
|
56
|
-
me: entity,
|
|
57
|
-
boundary: boundariesHit,
|
|
58
|
-
position: { x: position.x, y: position.y, z: position.z },
|
|
59
|
-
updateContext
|
|
60
|
-
});
|
|
61
|
-
}
|
|
10
|
+
|
|
11
|
+
// src/lib/behaviors/use-behavior.ts
|
|
12
|
+
function useBehavior(entity, descriptor, options) {
|
|
13
|
+
entity.use(descriptor, options);
|
|
14
|
+
return entity;
|
|
62
15
|
}
|
|
63
16
|
|
|
64
|
-
// src/lib/
|
|
65
|
-
import {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
return groupId;
|
|
17
|
+
// src/lib/behaviors/components.ts
|
|
18
|
+
import { Vector3, Quaternion } from "three";
|
|
19
|
+
function createTransformComponent() {
|
|
20
|
+
return {
|
|
21
|
+
position: new Vector3(),
|
|
22
|
+
rotation: new Quaternion()
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function createPhysicsBodyComponent(body) {
|
|
26
|
+
return { body };
|
|
75
27
|
}
|
|
76
28
|
|
|
77
|
-
// src/lib/
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
29
|
+
// src/lib/behaviors/physics-step.behavior.ts
|
|
30
|
+
var PhysicsStepBehavior = class {
|
|
31
|
+
constructor(physicsWorld) {
|
|
32
|
+
this.physicsWorld = physicsWorld;
|
|
33
|
+
}
|
|
34
|
+
update(dt) {
|
|
35
|
+
this.physicsWorld.timestep = dt;
|
|
36
|
+
this.physicsWorld.step();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/lib/behaviors/physics-sync.behavior.ts
|
|
41
|
+
var PhysicsSyncBehavior = class {
|
|
42
|
+
constructor(world) {
|
|
43
|
+
this.world = world;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Query entities that have both physics body and transform components
|
|
47
|
+
*/
|
|
48
|
+
queryEntities() {
|
|
49
|
+
const entities = [];
|
|
50
|
+
for (const [, entity] of this.world.collisionMap) {
|
|
51
|
+
const gameEntity = entity;
|
|
52
|
+
if (gameEntity.physics?.body && gameEntity.transform) {
|
|
53
|
+
entities.push({
|
|
54
|
+
physics: gameEntity.physics,
|
|
55
|
+
transform: gameEntity.transform
|
|
56
|
+
});
|
|
57
|
+
}
|
|
89
58
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
|
|
59
|
+
return entities;
|
|
60
|
+
}
|
|
61
|
+
update(_dt) {
|
|
62
|
+
const entities = this.queryEntities();
|
|
63
|
+
for (const e of entities) {
|
|
64
|
+
const body = e.physics.body;
|
|
65
|
+
const transform = e.transform;
|
|
66
|
+
const p = body.translation();
|
|
67
|
+
transform.position.set(p.x, p.y, p.z);
|
|
68
|
+
const r = body.rotation();
|
|
69
|
+
transform.rotation.set(r.x, r.y, r.z, r.w);
|
|
99
70
|
}
|
|
100
|
-
} else if ("test" in selector) {
|
|
101
|
-
return !!selector.test(other);
|
|
102
71
|
}
|
|
103
|
-
|
|
104
|
-
}
|
|
72
|
+
};
|
|
105
73
|
|
|
106
|
-
// src/lib/
|
|
107
|
-
function
|
|
74
|
+
// src/lib/behaviors/thruster/components.ts
|
|
75
|
+
function createThrusterMovementComponent(linearThrust, angularThrust, options) {
|
|
108
76
|
return {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
77
|
+
linearThrust,
|
|
78
|
+
angularThrust,
|
|
79
|
+
linearDamping: options?.linearDamping,
|
|
80
|
+
angularDamping: options?.angularDamping
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function createThrusterInputComponent() {
|
|
84
|
+
return {
|
|
85
|
+
thrust: 0,
|
|
86
|
+
rotate: 0
|
|
113
87
|
};
|
|
114
88
|
}
|
|
115
|
-
function
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const {
|
|
120
|
-
minSpeed = 2,
|
|
121
|
-
maxSpeed = 20,
|
|
122
|
-
separation = 0,
|
|
123
|
-
collisionWith = void 0
|
|
124
|
-
} = {
|
|
125
|
-
...options
|
|
89
|
+
function createThrusterStateComponent() {
|
|
90
|
+
return {
|
|
91
|
+
enabled: true,
|
|
92
|
+
currentThrust: 0
|
|
126
93
|
};
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const maxAngleRad = maxAngleDeg * Math.PI / 180;
|
|
222
|
-
const deadzone = Math.max(0, Math.min(1, minOffsetForAngle));
|
|
223
|
-
const clampedOffsetX = clamp(relX, -1, 1);
|
|
224
|
-
const absOff = Math.abs(clampedOffsetX);
|
|
225
|
-
const baseSpeed = Math.sqrt(vel.x * vel.x + vel.y * vel.y);
|
|
226
|
-
const speed = clamp(baseSpeed * speedUpFactor, minSpeed, maxSpeed);
|
|
227
|
-
if (absOff > deadzone) {
|
|
228
|
-
const t = (absOff - deadzone) / (1 - deadzone);
|
|
229
|
-
const angle = Math.sign(clampedOffsetX) * (t * maxAngleRad);
|
|
230
|
-
const cosA = Math.cos(angle);
|
|
231
|
-
const sinA = Math.sin(angle);
|
|
232
|
-
const vy = Math.abs(speed * cosA);
|
|
233
|
-
const vx = speed * sinA;
|
|
234
|
-
newVelY = dy > 0 ? vy : -vy;
|
|
235
|
-
newVelX = vx;
|
|
236
|
-
} else {
|
|
237
|
-
const vx = vel.x * centerRetentionFactor;
|
|
238
|
-
const vyMagSquared = Math.max(0, speed * speed - vx * vx);
|
|
239
|
-
const vy = Math.sqrt(vyMagSquared);
|
|
240
|
-
newVelY = dy > 0 ? vy : -vy;
|
|
241
|
-
newVelX = vx;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/lib/behaviors/thruster/thruster-fsm.ts
|
|
97
|
+
import { StateMachine, t } from "typescript-fsm";
|
|
98
|
+
var ThrusterState = /* @__PURE__ */ ((ThrusterState2) => {
|
|
99
|
+
ThrusterState2["Idle"] = "idle";
|
|
100
|
+
ThrusterState2["Active"] = "active";
|
|
101
|
+
ThrusterState2["Boosting"] = "boosting";
|
|
102
|
+
ThrusterState2["Disabled"] = "disabled";
|
|
103
|
+
ThrusterState2["Docked"] = "docked";
|
|
104
|
+
return ThrusterState2;
|
|
105
|
+
})(ThrusterState || {});
|
|
106
|
+
var ThrusterEvent = /* @__PURE__ */ ((ThrusterEvent2) => {
|
|
107
|
+
ThrusterEvent2["Activate"] = "activate";
|
|
108
|
+
ThrusterEvent2["Deactivate"] = "deactivate";
|
|
109
|
+
ThrusterEvent2["Boost"] = "boost";
|
|
110
|
+
ThrusterEvent2["EndBoost"] = "endBoost";
|
|
111
|
+
ThrusterEvent2["Disable"] = "disable";
|
|
112
|
+
ThrusterEvent2["Enable"] = "enable";
|
|
113
|
+
ThrusterEvent2["Dock"] = "dock";
|
|
114
|
+
ThrusterEvent2["Undock"] = "undock";
|
|
115
|
+
return ThrusterEvent2;
|
|
116
|
+
})(ThrusterEvent || {});
|
|
117
|
+
var ThrusterFSM = class {
|
|
118
|
+
constructor(ctx) {
|
|
119
|
+
this.ctx = ctx;
|
|
120
|
+
this.machine = new StateMachine(
|
|
121
|
+
"idle" /* Idle */,
|
|
122
|
+
[
|
|
123
|
+
// Core transitions
|
|
124
|
+
t("idle" /* Idle */, "activate" /* Activate */, "active" /* Active */),
|
|
125
|
+
t("active" /* Active */, "deactivate" /* Deactivate */, "idle" /* Idle */),
|
|
126
|
+
t("active" /* Active */, "boost" /* Boost */, "boosting" /* Boosting */),
|
|
127
|
+
t("active" /* Active */, "disable" /* Disable */, "disabled" /* Disabled */),
|
|
128
|
+
t("active" /* Active */, "dock" /* Dock */, "docked" /* Docked */),
|
|
129
|
+
t("boosting" /* Boosting */, "endBoost" /* EndBoost */, "active" /* Active */),
|
|
130
|
+
t("boosting" /* Boosting */, "disable" /* Disable */, "disabled" /* Disabled */),
|
|
131
|
+
t("disabled" /* Disabled */, "enable" /* Enable */, "idle" /* Idle */),
|
|
132
|
+
t("docked" /* Docked */, "undock" /* Undock */, "idle" /* Idle */),
|
|
133
|
+
// Self-transitions (no-ops for redundant events)
|
|
134
|
+
t("idle" /* Idle */, "deactivate" /* Deactivate */, "idle" /* Idle */),
|
|
135
|
+
t("active" /* Active */, "activate" /* Activate */, "active" /* Active */)
|
|
136
|
+
]
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
machine;
|
|
140
|
+
/**
|
|
141
|
+
* Get current state
|
|
142
|
+
*/
|
|
143
|
+
getState() {
|
|
144
|
+
return this.machine.getState();
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Dispatch an event to transition state
|
|
148
|
+
*/
|
|
149
|
+
dispatch(event) {
|
|
150
|
+
if (this.machine.can(event)) {
|
|
151
|
+
this.machine.dispatch(event);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Update FSM state based on player input.
|
|
156
|
+
* Auto-transitions between Idle/Active to report current state.
|
|
157
|
+
* Does NOT modify input - just observes and reports.
|
|
158
|
+
*/
|
|
159
|
+
update(playerInput) {
|
|
160
|
+
const state = this.machine.getState();
|
|
161
|
+
const hasInput = Math.abs(playerInput.thrust) > 0.01 || Math.abs(playerInput.rotate) > 0.01;
|
|
162
|
+
if (hasInput && state === "idle" /* Idle */) {
|
|
163
|
+
this.dispatch("activate" /* Activate */);
|
|
164
|
+
} else if (!hasInput && state === "active" /* Active */) {
|
|
165
|
+
this.dispatch("deactivate" /* Deactivate */);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// src/lib/behaviors/thruster/thruster-movement.behavior.ts
|
|
171
|
+
var ThrusterMovementBehavior = class {
|
|
172
|
+
constructor(world) {
|
|
173
|
+
this.world = world;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Query function - returns entities with required thruster components
|
|
177
|
+
*/
|
|
178
|
+
queryEntities() {
|
|
179
|
+
const entities = [];
|
|
180
|
+
for (const [, entity] of this.world.collisionMap) {
|
|
181
|
+
const gameEntity = entity;
|
|
182
|
+
if (gameEntity.physics?.body && gameEntity.thruster && gameEntity.$thruster) {
|
|
183
|
+
entities.push({
|
|
184
|
+
physics: gameEntity.physics,
|
|
185
|
+
thruster: gameEntity.thruster,
|
|
186
|
+
$thruster: gameEntity.$thruster
|
|
187
|
+
});
|
|
242
188
|
}
|
|
243
|
-
usedAngleDeflection = true;
|
|
244
|
-
} else {
|
|
245
|
-
newVelY = dy > 0 ? Math.abs(vel.y) : -Math.abs(vel.y);
|
|
246
|
-
if (reflectionMode === "simple") usedAngleDeflection = true;
|
|
247
189
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
const
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
190
|
+
return entities;
|
|
191
|
+
}
|
|
192
|
+
update(_dt) {
|
|
193
|
+
const entities = this.queryEntities();
|
|
194
|
+
for (const e of entities) {
|
|
195
|
+
const body = e.physics.body;
|
|
196
|
+
const thruster = e.thruster;
|
|
197
|
+
const input = e.$thruster;
|
|
198
|
+
const q = body.rotation();
|
|
199
|
+
const rotationZ = Math.atan2(2 * (q.w * q.z + q.x * q.y), 1 - 2 * (q.y * q.y + q.z * q.z));
|
|
200
|
+
if (input.thrust !== 0) {
|
|
201
|
+
const currentVel = body.linvel();
|
|
202
|
+
if (input.thrust > 0) {
|
|
203
|
+
const forwardX = Math.sin(-rotationZ);
|
|
204
|
+
const forwardY = Math.cos(-rotationZ);
|
|
205
|
+
const thrustAmount = thruster.linearThrust * input.thrust * 0.1;
|
|
206
|
+
body.setLinvel({
|
|
207
|
+
x: currentVel.x + forwardX * thrustAmount,
|
|
208
|
+
y: currentVel.y + forwardY * thrustAmount,
|
|
209
|
+
z: currentVel.z
|
|
210
|
+
}, true);
|
|
211
|
+
} else {
|
|
212
|
+
const brakeAmount = 0.9;
|
|
213
|
+
body.setLinvel({
|
|
214
|
+
x: currentVel.x * brakeAmount,
|
|
215
|
+
y: currentVel.y * brakeAmount,
|
|
216
|
+
z: currentVel.z
|
|
217
|
+
}, true);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (input.rotate !== 0) {
|
|
221
|
+
body.setAngvel({ x: 0, y: 0, z: -thruster.angularThrust * input.rotate }, true);
|
|
268
222
|
} else {
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
const vx = Math.sqrt(vxMagSquared);
|
|
272
|
-
newVelX = dx > 0 ? vx : -vx;
|
|
273
|
-
newVelY = vy;
|
|
223
|
+
const angVel = body.angvel();
|
|
224
|
+
body.setAngvel({ x: angVel.x, y: angVel.y, z: 0 }, true);
|
|
274
225
|
}
|
|
275
|
-
usedAngleDeflection = true;
|
|
276
|
-
} else {
|
|
277
|
-
newVelX = dx > 0 ? Math.abs(vel.x) : -Math.abs(vel.x);
|
|
278
|
-
newVelY = vel.y;
|
|
279
|
-
usedAngleDeflection = true;
|
|
280
226
|
}
|
|
281
227
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// src/lib/behaviors/thruster/thruster.descriptor.ts
|
|
231
|
+
var defaultOptions = {
|
|
232
|
+
linearThrust: 10,
|
|
233
|
+
angularThrust: 5
|
|
234
|
+
};
|
|
235
|
+
var ThrusterBehaviorSystem = class {
|
|
236
|
+
constructor(world) {
|
|
237
|
+
this.world = world;
|
|
238
|
+
this.movementBehavior = new ThrusterMovementBehavior(world);
|
|
289
239
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
240
|
+
movementBehavior;
|
|
241
|
+
update(ecs, delta) {
|
|
242
|
+
if (!this.world?.collisionMap) return;
|
|
243
|
+
for (const [, entity] of this.world.collisionMap) {
|
|
244
|
+
const gameEntity = entity;
|
|
245
|
+
if (typeof gameEntity.getBehaviorRefs !== "function") continue;
|
|
246
|
+
const refs = gameEntity.getBehaviorRefs();
|
|
247
|
+
const thrusterRef = refs.find(
|
|
248
|
+
(r) => r.descriptor.key === /* @__PURE__ */ Symbol.for("zylem:behavior:thruster")
|
|
249
|
+
);
|
|
250
|
+
if (!thrusterRef || !gameEntity.body) continue;
|
|
251
|
+
const options = thrusterRef.options;
|
|
252
|
+
if (!gameEntity.thruster) {
|
|
253
|
+
gameEntity.thruster = {
|
|
254
|
+
linearThrust: options.linearThrust,
|
|
255
|
+
angularThrust: options.angularThrust
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
if (!gameEntity.$thruster) {
|
|
259
|
+
gameEntity.$thruster = {
|
|
260
|
+
thrust: 0,
|
|
261
|
+
rotate: 0
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (!gameEntity.physics) {
|
|
265
|
+
gameEntity.physics = { body: gameEntity.body };
|
|
266
|
+
}
|
|
267
|
+
if (!thrusterRef.fsm && gameEntity.$thruster) {
|
|
268
|
+
thrusterRef.fsm = new ThrusterFSM({ input: gameEntity.$thruster });
|
|
269
|
+
}
|
|
270
|
+
if (thrusterRef.fsm && gameEntity.$thruster) {
|
|
271
|
+
thrusterRef.fsm.update({
|
|
272
|
+
thrust: gameEntity.$thruster.thrust,
|
|
273
|
+
rotate: gameEntity.$thruster.rotate
|
|
308
274
|
});
|
|
309
275
|
}
|
|
310
276
|
}
|
|
277
|
+
this.movementBehavior.update(delta);
|
|
278
|
+
}
|
|
279
|
+
destroy(_ecs) {
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
var ThrusterBehavior = defineBehavior({
|
|
283
|
+
name: "thruster",
|
|
284
|
+
defaultOptions,
|
|
285
|
+
systemFactory: (ctx) => new ThrusterBehaviorSystem(ctx.world)
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// src/lib/behaviors/screen-wrap/screen-wrap-fsm.ts
|
|
289
|
+
import { StateMachine as StateMachine2, t as t2 } from "typescript-fsm";
|
|
290
|
+
var ScreenWrapState = /* @__PURE__ */ ((ScreenWrapState2) => {
|
|
291
|
+
ScreenWrapState2["Center"] = "center";
|
|
292
|
+
ScreenWrapState2["NearEdgeLeft"] = "near-edge-left";
|
|
293
|
+
ScreenWrapState2["NearEdgeRight"] = "near-edge-right";
|
|
294
|
+
ScreenWrapState2["NearEdgeTop"] = "near-edge-top";
|
|
295
|
+
ScreenWrapState2["NearEdgeBottom"] = "near-edge-bottom";
|
|
296
|
+
ScreenWrapState2["Wrapped"] = "wrapped";
|
|
297
|
+
return ScreenWrapState2;
|
|
298
|
+
})(ScreenWrapState || {});
|
|
299
|
+
var ScreenWrapEvent = /* @__PURE__ */ ((ScreenWrapEvent2) => {
|
|
300
|
+
ScreenWrapEvent2["EnterCenter"] = "enter-center";
|
|
301
|
+
ScreenWrapEvent2["ApproachLeft"] = "approach-left";
|
|
302
|
+
ScreenWrapEvent2["ApproachRight"] = "approach-right";
|
|
303
|
+
ScreenWrapEvent2["ApproachTop"] = "approach-top";
|
|
304
|
+
ScreenWrapEvent2["ApproachBottom"] = "approach-bottom";
|
|
305
|
+
ScreenWrapEvent2["Wrap"] = "wrap";
|
|
306
|
+
return ScreenWrapEvent2;
|
|
307
|
+
})(ScreenWrapEvent || {});
|
|
308
|
+
var ScreenWrapFSM = class {
|
|
309
|
+
machine;
|
|
310
|
+
constructor() {
|
|
311
|
+
this.machine = new StateMachine2(
|
|
312
|
+
"center" /* Center */,
|
|
313
|
+
[
|
|
314
|
+
// From Center
|
|
315
|
+
t2("center" /* Center */, "approach-left" /* ApproachLeft */, "near-edge-left" /* NearEdgeLeft */),
|
|
316
|
+
t2("center" /* Center */, "approach-right" /* ApproachRight */, "near-edge-right" /* NearEdgeRight */),
|
|
317
|
+
t2("center" /* Center */, "approach-top" /* ApproachTop */, "near-edge-top" /* NearEdgeTop */),
|
|
318
|
+
t2("center" /* Center */, "approach-bottom" /* ApproachBottom */, "near-edge-bottom" /* NearEdgeBottom */),
|
|
319
|
+
// From NearEdge to Wrapped
|
|
320
|
+
t2("near-edge-left" /* NearEdgeLeft */, "wrap" /* Wrap */, "wrapped" /* Wrapped */),
|
|
321
|
+
t2("near-edge-right" /* NearEdgeRight */, "wrap" /* Wrap */, "wrapped" /* Wrapped */),
|
|
322
|
+
t2("near-edge-top" /* NearEdgeTop */, "wrap" /* Wrap */, "wrapped" /* Wrapped */),
|
|
323
|
+
t2("near-edge-bottom" /* NearEdgeBottom */, "wrap" /* Wrap */, "wrapped" /* Wrapped */),
|
|
324
|
+
// From NearEdge back to Center
|
|
325
|
+
t2("near-edge-left" /* NearEdgeLeft */, "enter-center" /* EnterCenter */, "center" /* Center */),
|
|
326
|
+
t2("near-edge-right" /* NearEdgeRight */, "enter-center" /* EnterCenter */, "center" /* Center */),
|
|
327
|
+
t2("near-edge-top" /* NearEdgeTop */, "enter-center" /* EnterCenter */, "center" /* Center */),
|
|
328
|
+
t2("near-edge-bottom" /* NearEdgeBottom */, "enter-center" /* EnterCenter */, "center" /* Center */),
|
|
329
|
+
// From Wrapped back to Center
|
|
330
|
+
t2("wrapped" /* Wrapped */, "enter-center" /* EnterCenter */, "center" /* Center */),
|
|
331
|
+
// From Wrapped to NearEdge (landed near opposite edge)
|
|
332
|
+
t2("wrapped" /* Wrapped */, "approach-left" /* ApproachLeft */, "near-edge-left" /* NearEdgeLeft */),
|
|
333
|
+
t2("wrapped" /* Wrapped */, "approach-right" /* ApproachRight */, "near-edge-right" /* NearEdgeRight */),
|
|
334
|
+
t2("wrapped" /* Wrapped */, "approach-top" /* ApproachTop */, "near-edge-top" /* NearEdgeTop */),
|
|
335
|
+
t2("wrapped" /* Wrapped */, "approach-bottom" /* ApproachBottom */, "near-edge-bottom" /* NearEdgeBottom */),
|
|
336
|
+
// Self-transitions (no-ops for redundant events)
|
|
337
|
+
t2("center" /* Center */, "enter-center" /* EnterCenter */, "center" /* Center */),
|
|
338
|
+
t2("near-edge-left" /* NearEdgeLeft */, "approach-left" /* ApproachLeft */, "near-edge-left" /* NearEdgeLeft */),
|
|
339
|
+
t2("near-edge-right" /* NearEdgeRight */, "approach-right" /* ApproachRight */, "near-edge-right" /* NearEdgeRight */),
|
|
340
|
+
t2("near-edge-top" /* NearEdgeTop */, "approach-top" /* ApproachTop */, "near-edge-top" /* NearEdgeTop */),
|
|
341
|
+
t2("near-edge-bottom" /* NearEdgeBottom */, "approach-bottom" /* ApproachBottom */, "near-edge-bottom" /* NearEdgeBottom */)
|
|
342
|
+
]
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
getState() {
|
|
346
|
+
return this.machine.getState();
|
|
347
|
+
}
|
|
348
|
+
dispatch(event) {
|
|
349
|
+
if (this.machine.can(event)) {
|
|
350
|
+
this.machine.dispatch(event);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Update FSM based on entity position relative to bounds
|
|
355
|
+
*/
|
|
356
|
+
update(position, bounds, wrapped) {
|
|
357
|
+
const { x, y } = position;
|
|
358
|
+
const { minX, maxX, minY, maxY, edgeThreshold } = bounds;
|
|
359
|
+
if (wrapped) {
|
|
360
|
+
this.dispatch("wrap" /* Wrap */);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const nearLeft = x < minX + edgeThreshold;
|
|
364
|
+
const nearRight = x > maxX - edgeThreshold;
|
|
365
|
+
const nearBottom = y < minY + edgeThreshold;
|
|
366
|
+
const nearTop = y > maxY - edgeThreshold;
|
|
367
|
+
if (nearLeft) {
|
|
368
|
+
this.dispatch("approach-left" /* ApproachLeft */);
|
|
369
|
+
} else if (nearRight) {
|
|
370
|
+
this.dispatch("approach-right" /* ApproachRight */);
|
|
371
|
+
} else if (nearTop) {
|
|
372
|
+
this.dispatch("approach-top" /* ApproachTop */);
|
|
373
|
+
} else if (nearBottom) {
|
|
374
|
+
this.dispatch("approach-bottom" /* ApproachBottom */);
|
|
375
|
+
} else {
|
|
376
|
+
this.dispatch("enter-center" /* EnterCenter */);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// src/lib/behaviors/screen-wrap/screen-wrap.descriptor.ts
|
|
382
|
+
var defaultOptions2 = {
|
|
383
|
+
width: 20,
|
|
384
|
+
height: 15,
|
|
385
|
+
centerX: 0,
|
|
386
|
+
centerY: 0,
|
|
387
|
+
edgeThreshold: 2
|
|
388
|
+
};
|
|
389
|
+
var ScreenWrapSystem = class {
|
|
390
|
+
constructor(world) {
|
|
391
|
+
this.world = world;
|
|
392
|
+
}
|
|
393
|
+
update(ecs, delta) {
|
|
394
|
+
if (!this.world?.collisionMap) return;
|
|
395
|
+
for (const [, entity] of this.world.collisionMap) {
|
|
396
|
+
const gameEntity = entity;
|
|
397
|
+
if (typeof gameEntity.getBehaviorRefs !== "function") continue;
|
|
398
|
+
const refs = gameEntity.getBehaviorRefs();
|
|
399
|
+
const wrapRef = refs.find(
|
|
400
|
+
(r) => r.descriptor.key === /* @__PURE__ */ Symbol.for("zylem:behavior:screen-wrap")
|
|
401
|
+
);
|
|
402
|
+
if (!wrapRef || !gameEntity.body) continue;
|
|
403
|
+
const options = wrapRef.options;
|
|
404
|
+
if (!wrapRef.fsm) {
|
|
405
|
+
wrapRef.fsm = new ScreenWrapFSM();
|
|
406
|
+
}
|
|
407
|
+
const wrapped = this.wrapEntity(gameEntity, options);
|
|
408
|
+
const pos = gameEntity.body.translation();
|
|
409
|
+
const { width, height, centerX, centerY, edgeThreshold } = options;
|
|
410
|
+
const halfWidth = width / 2;
|
|
411
|
+
const halfHeight = height / 2;
|
|
412
|
+
wrapRef.fsm.update(
|
|
413
|
+
{ x: pos.x, y: pos.y },
|
|
414
|
+
{
|
|
415
|
+
minX: centerX - halfWidth,
|
|
416
|
+
maxX: centerX + halfWidth,
|
|
417
|
+
minY: centerY - halfHeight,
|
|
418
|
+
maxY: centerY + halfHeight,
|
|
419
|
+
edgeThreshold
|
|
420
|
+
},
|
|
421
|
+
wrapped
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
wrapEntity(entity, options) {
|
|
426
|
+
const body = entity.body;
|
|
427
|
+
if (!body) return false;
|
|
428
|
+
const { width, height, centerX, centerY } = options;
|
|
429
|
+
const halfWidth = width / 2;
|
|
430
|
+
const halfHeight = height / 2;
|
|
431
|
+
const minX = centerX - halfWidth;
|
|
432
|
+
const maxX = centerX + halfWidth;
|
|
433
|
+
const minY = centerY - halfHeight;
|
|
434
|
+
const maxY = centerY + halfHeight;
|
|
435
|
+
const pos = body.translation();
|
|
436
|
+
let newX = pos.x;
|
|
437
|
+
let newY = pos.y;
|
|
438
|
+
let wrapped = false;
|
|
439
|
+
if (pos.x < minX) {
|
|
440
|
+
newX = maxX - (minX - pos.x);
|
|
441
|
+
wrapped = true;
|
|
442
|
+
} else if (pos.x > maxX) {
|
|
443
|
+
newX = minX + (pos.x - maxX);
|
|
444
|
+
wrapped = true;
|
|
445
|
+
}
|
|
446
|
+
if (pos.y < minY) {
|
|
447
|
+
newY = maxY - (minY - pos.y);
|
|
448
|
+
wrapped = true;
|
|
449
|
+
} else if (pos.y > maxY) {
|
|
450
|
+
newY = minY + (pos.y - maxY);
|
|
451
|
+
wrapped = true;
|
|
452
|
+
}
|
|
453
|
+
if (wrapped) {
|
|
454
|
+
body.setTranslation({ x: newX, y: newY, z: pos.z }, true);
|
|
455
|
+
}
|
|
456
|
+
return wrapped;
|
|
457
|
+
}
|
|
458
|
+
destroy(_ecs) {
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
var ScreenWrapBehavior = defineBehavior({
|
|
462
|
+
name: "screen-wrap",
|
|
463
|
+
defaultOptions: defaultOptions2,
|
|
464
|
+
systemFactory: (ctx) => new ScreenWrapSystem(ctx.world)
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// src/lib/behaviors/world-boundary-2d/world-boundary-2d-fsm.ts
|
|
468
|
+
import { StateMachine as StateMachine3, t as t3 } from "typescript-fsm";
|
|
469
|
+
var WorldBoundary2DState = /* @__PURE__ */ ((WorldBoundary2DState2) => {
|
|
470
|
+
WorldBoundary2DState2["Inside"] = "inside";
|
|
471
|
+
WorldBoundary2DState2["Touching"] = "touching";
|
|
472
|
+
return WorldBoundary2DState2;
|
|
473
|
+
})(WorldBoundary2DState || {});
|
|
474
|
+
var WorldBoundary2DEvent = /* @__PURE__ */ ((WorldBoundary2DEvent2) => {
|
|
475
|
+
WorldBoundary2DEvent2["EnterInside"] = "enter-inside";
|
|
476
|
+
WorldBoundary2DEvent2["TouchBoundary"] = "touch-boundary";
|
|
477
|
+
return WorldBoundary2DEvent2;
|
|
478
|
+
})(WorldBoundary2DEvent || {});
|
|
479
|
+
function computeWorldBoundary2DHits(position, bounds) {
|
|
480
|
+
const hits = {
|
|
481
|
+
top: false,
|
|
482
|
+
bottom: false,
|
|
483
|
+
left: false,
|
|
484
|
+
right: false
|
|
485
|
+
};
|
|
486
|
+
if (position.x <= bounds.left) hits.left = true;
|
|
487
|
+
else if (position.x >= bounds.right) hits.right = true;
|
|
488
|
+
if (position.y <= bounds.bottom) hits.bottom = true;
|
|
489
|
+
else if (position.y >= bounds.top) hits.top = true;
|
|
490
|
+
return hits;
|
|
491
|
+
}
|
|
492
|
+
function hasAnyWorldBoundary2DHit(hits) {
|
|
493
|
+
return !!(hits.left || hits.right || hits.top || hits.bottom);
|
|
494
|
+
}
|
|
495
|
+
var WorldBoundary2DFSM = class {
|
|
496
|
+
machine;
|
|
497
|
+
lastHits = { top: false, bottom: false, left: false, right: false };
|
|
498
|
+
lastPosition = null;
|
|
499
|
+
lastUpdatedAtMs = null;
|
|
500
|
+
constructor() {
|
|
501
|
+
this.machine = new StateMachine3(
|
|
502
|
+
"inside" /* Inside */,
|
|
503
|
+
[
|
|
504
|
+
t3("inside" /* Inside */, "touch-boundary" /* TouchBoundary */, "touching" /* Touching */),
|
|
505
|
+
t3("touching" /* Touching */, "enter-inside" /* EnterInside */, "inside" /* Inside */),
|
|
506
|
+
// Self transitions (no-ops)
|
|
507
|
+
t3("inside" /* Inside */, "enter-inside" /* EnterInside */, "inside" /* Inside */),
|
|
508
|
+
t3("touching" /* Touching */, "touch-boundary" /* TouchBoundary */, "touching" /* Touching */)
|
|
509
|
+
]
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
getState() {
|
|
513
|
+
return this.machine.getState();
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Returns the last computed hits (always available after first update call).
|
|
517
|
+
*/
|
|
518
|
+
getLastHits() {
|
|
519
|
+
return this.lastHits;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Returns adjusted movement values based on boundary hits.
|
|
523
|
+
* If the entity is touching a boundary and trying to move further into it,
|
|
524
|
+
* that axis component is zeroed out.
|
|
525
|
+
*
|
|
526
|
+
* @param moveX - The desired X movement
|
|
527
|
+
* @param moveY - The desired Y movement
|
|
528
|
+
* @returns Adjusted { moveX, moveY } with boundary-blocked axes zeroed
|
|
529
|
+
*/
|
|
530
|
+
getMovement(moveX, moveY) {
|
|
531
|
+
const hits = this.lastHits;
|
|
532
|
+
let adjustedX = moveX;
|
|
533
|
+
let adjustedY = moveY;
|
|
534
|
+
if (hits.left && moveX < 0 || hits.right && moveX > 0) {
|
|
535
|
+
adjustedX = 0;
|
|
536
|
+
}
|
|
537
|
+
if (hits.bottom && moveY < 0 || hits.top && moveY > 0) {
|
|
538
|
+
adjustedY = 0;
|
|
539
|
+
}
|
|
540
|
+
return { moveX: adjustedX, moveY: adjustedY };
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Returns the last position passed to `update`, if any.
|
|
544
|
+
*/
|
|
545
|
+
getLastPosition() {
|
|
546
|
+
return this.lastPosition;
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Best-effort timestamp (ms) of the last `update(...)` call.
|
|
550
|
+
* This is optional metadata; systems can ignore it.
|
|
551
|
+
*/
|
|
552
|
+
getLastUpdatedAtMs() {
|
|
553
|
+
return this.lastUpdatedAtMs;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Update FSM + extended state based on current position and bounds.
|
|
557
|
+
* Returns the computed hits for convenience.
|
|
558
|
+
*/
|
|
559
|
+
update(position, bounds) {
|
|
560
|
+
const hits = computeWorldBoundary2DHits(position, bounds);
|
|
561
|
+
this.lastHits = hits;
|
|
562
|
+
this.lastPosition = { x: position.x, y: position.y };
|
|
563
|
+
this.lastUpdatedAtMs = Date.now();
|
|
564
|
+
if (hasAnyWorldBoundary2DHit(hits)) {
|
|
565
|
+
this.dispatch("touch-boundary" /* TouchBoundary */);
|
|
566
|
+
} else {
|
|
567
|
+
this.dispatch("enter-inside" /* EnterInside */);
|
|
568
|
+
}
|
|
569
|
+
return hits;
|
|
570
|
+
}
|
|
571
|
+
dispatch(event) {
|
|
572
|
+
if (this.machine.can(event)) {
|
|
573
|
+
this.machine.dispatch(event);
|
|
574
|
+
}
|
|
311
575
|
}
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
// src/lib/behaviors/world-boundary-2d/world-boundary-2d.descriptor.ts
|
|
579
|
+
var defaultOptions3 = {
|
|
580
|
+
boundaries: { top: 0, bottom: 0, left: 0, right: 0 }
|
|
581
|
+
};
|
|
582
|
+
function createWorldBoundary2DHandle(ref) {
|
|
583
|
+
return {
|
|
584
|
+
getLastHits: () => {
|
|
585
|
+
const fsm = ref.fsm;
|
|
586
|
+
return fsm?.getLastHits() ?? null;
|
|
587
|
+
},
|
|
588
|
+
getMovement: (moveX, moveY) => {
|
|
589
|
+
const fsm = ref.fsm;
|
|
590
|
+
return fsm?.getMovement(moveX, moveY) ?? { moveX, moveY };
|
|
591
|
+
}
|
|
592
|
+
};
|
|
312
593
|
}
|
|
594
|
+
var WorldBoundary2DSystem = class {
|
|
595
|
+
constructor(world) {
|
|
596
|
+
this.world = world;
|
|
597
|
+
}
|
|
598
|
+
update(_ecs, _delta) {
|
|
599
|
+
if (!this.world?.collisionMap) return;
|
|
600
|
+
for (const [, entity] of this.world.collisionMap) {
|
|
601
|
+
const gameEntity = entity;
|
|
602
|
+
if (typeof gameEntity.getBehaviorRefs !== "function") continue;
|
|
603
|
+
const refs = gameEntity.getBehaviorRefs();
|
|
604
|
+
const boundaryRef = refs.find(
|
|
605
|
+
(r) => r.descriptor.key === /* @__PURE__ */ Symbol.for("zylem:behavior:world-boundary-2d")
|
|
606
|
+
);
|
|
607
|
+
if (!boundaryRef || !gameEntity.body) continue;
|
|
608
|
+
const options = boundaryRef.options;
|
|
609
|
+
if (!boundaryRef.fsm) {
|
|
610
|
+
boundaryRef.fsm = new WorldBoundary2DFSM();
|
|
611
|
+
}
|
|
612
|
+
const body = gameEntity.body;
|
|
613
|
+
const pos = body.translation();
|
|
614
|
+
boundaryRef.fsm.update(
|
|
615
|
+
{ x: pos.x, y: pos.y },
|
|
616
|
+
options.boundaries
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
destroy(_ecs) {
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
var WorldBoundary2DBehavior = defineBehavior({
|
|
624
|
+
name: "world-boundary-2d",
|
|
625
|
+
defaultOptions: defaultOptions3,
|
|
626
|
+
systemFactory: (ctx) => new WorldBoundary2DSystem(ctx.world),
|
|
627
|
+
createHandle: createWorldBoundary2DHandle
|
|
628
|
+
});
|
|
313
629
|
|
|
314
|
-
// src/lib/
|
|
630
|
+
// src/lib/behaviors/ricochet-2d/ricochet-2d-fsm.ts
|
|
631
|
+
import { StateMachine as StateMachine4, t as t4 } from "typescript-fsm";
|
|
632
|
+
var Ricochet2DState = /* @__PURE__ */ ((Ricochet2DState2) => {
|
|
633
|
+
Ricochet2DState2["Idle"] = "idle";
|
|
634
|
+
Ricochet2DState2["Ricocheting"] = "ricocheting";
|
|
635
|
+
return Ricochet2DState2;
|
|
636
|
+
})(Ricochet2DState || {});
|
|
637
|
+
var Ricochet2DEvent = /* @__PURE__ */ ((Ricochet2DEvent2) => {
|
|
638
|
+
Ricochet2DEvent2["StartRicochet"] = "start-ricochet";
|
|
639
|
+
Ricochet2DEvent2["EndRicochet"] = "end-ricochet";
|
|
640
|
+
return Ricochet2DEvent2;
|
|
641
|
+
})(Ricochet2DEvent || {});
|
|
315
642
|
function clamp(value, min, max) {
|
|
316
643
|
return Math.max(min, Math.min(max, value));
|
|
317
644
|
}
|
|
645
|
+
var Ricochet2DFSM = class {
|
|
646
|
+
machine;
|
|
647
|
+
lastResult = null;
|
|
648
|
+
lastUpdatedAtMs = null;
|
|
649
|
+
constructor() {
|
|
650
|
+
this.machine = new StateMachine4(
|
|
651
|
+
"idle" /* Idle */,
|
|
652
|
+
[
|
|
653
|
+
t4("idle" /* Idle */, "start-ricochet" /* StartRicochet */, "ricocheting" /* Ricocheting */),
|
|
654
|
+
t4("ricocheting" /* Ricocheting */, "end-ricochet" /* EndRicochet */, "idle" /* Idle */),
|
|
655
|
+
// Self transitions (no-ops)
|
|
656
|
+
t4("idle" /* Idle */, "end-ricochet" /* EndRicochet */, "idle" /* Idle */),
|
|
657
|
+
t4("ricocheting" /* Ricocheting */, "start-ricochet" /* StartRicochet */, "ricocheting" /* Ricocheting */)
|
|
658
|
+
]
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
getState() {
|
|
662
|
+
return this.machine.getState();
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Returns the last computed ricochet result, or null if none.
|
|
666
|
+
*/
|
|
667
|
+
getLastResult() {
|
|
668
|
+
return this.lastResult;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Best-effort timestamp (ms) of the last computation.
|
|
672
|
+
*/
|
|
673
|
+
getLastUpdatedAtMs() {
|
|
674
|
+
return this.lastUpdatedAtMs;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Compute a ricochet result from collision context.
|
|
678
|
+
* Returns the result for the consumer to apply, or null if invalid input.
|
|
679
|
+
*/
|
|
680
|
+
computeRicochet(ctx, options = {}) {
|
|
681
|
+
const {
|
|
682
|
+
minSpeed = 2,
|
|
683
|
+
maxSpeed = 20,
|
|
684
|
+
speedMultiplier = 1.05,
|
|
685
|
+
reflectionMode = "angled",
|
|
686
|
+
maxAngleDeg = 60
|
|
687
|
+
} = options;
|
|
688
|
+
const { selfVelocity, selfPosition, otherPosition, otherSize } = this.extractDataFromEntities(ctx);
|
|
689
|
+
if (!selfVelocity) {
|
|
690
|
+
this.dispatch("end-ricochet" /* EndRicochet */);
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
const speed = Math.hypot(selfVelocity.x, selfVelocity.y);
|
|
694
|
+
if (speed === 0) {
|
|
695
|
+
this.dispatch("end-ricochet" /* EndRicochet */);
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
const normal = ctx.contact.normal ?? this.computeNormalFromPositions(selfPosition, otherPosition);
|
|
699
|
+
if (!normal) {
|
|
700
|
+
this.dispatch("end-ricochet" /* EndRicochet */);
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
let reflected = this.computeBasicReflection(selfVelocity, normal);
|
|
704
|
+
if (reflectionMode === "angled") {
|
|
705
|
+
reflected = this.computeAngledDeflection(
|
|
706
|
+
selfVelocity,
|
|
707
|
+
normal,
|
|
708
|
+
speed,
|
|
709
|
+
maxAngleDeg,
|
|
710
|
+
speedMultiplier,
|
|
711
|
+
selfPosition,
|
|
712
|
+
otherPosition,
|
|
713
|
+
otherSize,
|
|
714
|
+
ctx.contact.position
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
reflected = this.applySpeedClamp(reflected, minSpeed, maxSpeed);
|
|
718
|
+
const result = {
|
|
719
|
+
velocity: { x: reflected.x, y: reflected.y, z: 0 },
|
|
720
|
+
speed: Math.hypot(reflected.x, reflected.y),
|
|
721
|
+
normal: { x: normal.x, y: normal.y, z: 0 }
|
|
722
|
+
};
|
|
723
|
+
this.lastResult = result;
|
|
724
|
+
this.lastUpdatedAtMs = Date.now();
|
|
725
|
+
this.dispatch("start-ricochet" /* StartRicochet */);
|
|
726
|
+
return result;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Extract velocity, position, and size data from entities or context.
|
|
730
|
+
*/
|
|
731
|
+
extractDataFromEntities(ctx) {
|
|
732
|
+
let selfVelocity = ctx.selfVelocity;
|
|
733
|
+
let selfPosition = ctx.selfPosition;
|
|
734
|
+
let otherPosition = ctx.otherPosition;
|
|
735
|
+
let otherSize = ctx.otherSize;
|
|
736
|
+
if (ctx.entity?.body) {
|
|
737
|
+
const vel = ctx.entity.body.linvel();
|
|
738
|
+
selfVelocity = selfVelocity ?? { x: vel.x, y: vel.y, z: vel.z };
|
|
739
|
+
const pos = ctx.entity.body.translation();
|
|
740
|
+
selfPosition = selfPosition ?? { x: pos.x, y: pos.y, z: pos.z };
|
|
741
|
+
}
|
|
742
|
+
if (ctx.otherEntity?.body) {
|
|
743
|
+
const pos = ctx.otherEntity.body.translation();
|
|
744
|
+
otherPosition = otherPosition ?? { x: pos.x, y: pos.y, z: pos.z };
|
|
745
|
+
}
|
|
746
|
+
if (ctx.otherEntity && "size" in ctx.otherEntity) {
|
|
747
|
+
const size = ctx.otherEntity.size;
|
|
748
|
+
if (size && typeof size.x === "number") {
|
|
749
|
+
otherSize = otherSize ?? { x: size.x, y: size.y, z: size.z };
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return { selfVelocity, selfPosition, otherPosition, otherSize };
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Compute collision normal from entity positions using AABB heuristic.
|
|
756
|
+
*/
|
|
757
|
+
computeNormalFromPositions(selfPosition, otherPosition) {
|
|
758
|
+
if (!selfPosition || !otherPosition) return null;
|
|
759
|
+
const dx = selfPosition.x - otherPosition.x;
|
|
760
|
+
const dy = selfPosition.y - otherPosition.y;
|
|
761
|
+
if (Math.abs(dx) > Math.abs(dy)) {
|
|
762
|
+
return { x: dx > 0 ? 1 : -1, y: 0, z: 0 };
|
|
763
|
+
} else {
|
|
764
|
+
return { x: 0, y: dy > 0 ? 1 : -1, z: 0 };
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Compute basic reflection using the formula: v' = v - 2(v·n)n
|
|
769
|
+
*/
|
|
770
|
+
computeBasicReflection(velocity, normal) {
|
|
771
|
+
const vx = velocity.x;
|
|
772
|
+
const vy = velocity.y;
|
|
773
|
+
const dotProduct = vx * normal.x + vy * normal.y;
|
|
774
|
+
return {
|
|
775
|
+
x: vx - 2 * dotProduct * normal.x,
|
|
776
|
+
y: vy - 2 * dotProduct * normal.y
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Compute angled deflection for paddle-style reflections.
|
|
781
|
+
*/
|
|
782
|
+
computeAngledDeflection(velocity, normal, speed, maxAngleDeg, speedMultiplier, selfPosition, otherPosition, otherSize, contactPosition) {
|
|
783
|
+
const maxAngleRad = maxAngleDeg * Math.PI / 180;
|
|
784
|
+
let tx = -normal.y;
|
|
785
|
+
let ty = normal.x;
|
|
786
|
+
if (Math.abs(normal.x) > Math.abs(normal.y)) {
|
|
787
|
+
if (ty < 0) {
|
|
788
|
+
tx = -tx;
|
|
789
|
+
ty = -ty;
|
|
790
|
+
}
|
|
791
|
+
} else {
|
|
792
|
+
if (tx < 0) {
|
|
793
|
+
tx = -tx;
|
|
794
|
+
ty = -ty;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
const offset = this.computeHitOffset(
|
|
798
|
+
velocity,
|
|
799
|
+
normal,
|
|
800
|
+
speed,
|
|
801
|
+
tx,
|
|
802
|
+
ty,
|
|
803
|
+
selfPosition,
|
|
804
|
+
otherPosition,
|
|
805
|
+
otherSize,
|
|
806
|
+
contactPosition
|
|
807
|
+
);
|
|
808
|
+
const angle = clamp(offset, -1, 1) * maxAngleRad;
|
|
809
|
+
const cosA = Math.cos(angle);
|
|
810
|
+
const sinA = Math.sin(angle);
|
|
811
|
+
const newSpeed = speed * speedMultiplier;
|
|
812
|
+
return {
|
|
813
|
+
x: newSpeed * (normal.x * cosA + tx * sinA),
|
|
814
|
+
y: newSpeed * (normal.y * cosA + ty * sinA)
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Compute hit offset for angled deflection (-1 to 1).
|
|
819
|
+
*/
|
|
820
|
+
computeHitOffset(velocity, normal, speed, tx, ty, selfPosition, otherPosition, otherSize, contactPosition) {
|
|
821
|
+
if (otherPosition && otherSize) {
|
|
822
|
+
const useY = Math.abs(normal.x) > Math.abs(normal.y);
|
|
823
|
+
const halfExtent = useY ? otherSize.y / 2 : otherSize.x / 2;
|
|
824
|
+
if (useY) {
|
|
825
|
+
const selfY = selfPosition?.y ?? contactPosition?.y ?? 0;
|
|
826
|
+
const paddleY = otherPosition.y;
|
|
827
|
+
return (selfY - paddleY) / halfExtent;
|
|
828
|
+
} else {
|
|
829
|
+
const selfX = selfPosition?.x ?? contactPosition?.x ?? 0;
|
|
830
|
+
const paddleX = otherPosition.x;
|
|
831
|
+
return (selfX - paddleX) / halfExtent;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return (velocity.x * tx + velocity.y * ty) / speed;
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Apply speed constraints to the reflected velocity.
|
|
838
|
+
*/
|
|
839
|
+
applySpeedClamp(velocity, minSpeed, maxSpeed) {
|
|
840
|
+
const currentSpeed = Math.hypot(velocity.x, velocity.y);
|
|
841
|
+
if (currentSpeed === 0) return velocity;
|
|
842
|
+
const targetSpeed = clamp(currentSpeed, minSpeed, maxSpeed);
|
|
843
|
+
const scale = targetSpeed / currentSpeed;
|
|
844
|
+
return {
|
|
845
|
+
x: velocity.x * scale,
|
|
846
|
+
y: velocity.y * scale
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Clear the ricochet state (call after consumer has applied the result).
|
|
851
|
+
*/
|
|
852
|
+
clearRicochet() {
|
|
853
|
+
this.dispatch("end-ricochet" /* EndRicochet */);
|
|
854
|
+
}
|
|
855
|
+
dispatch(event) {
|
|
856
|
+
if (this.machine.can(event)) {
|
|
857
|
+
this.machine.dispatch(event);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
};
|
|
318
861
|
|
|
319
|
-
// src/lib/
|
|
320
|
-
|
|
862
|
+
// src/lib/behaviors/ricochet-2d/ricochet-2d.descriptor.ts
|
|
863
|
+
var defaultOptions4 = {
|
|
864
|
+
minSpeed: 2,
|
|
865
|
+
maxSpeed: 20,
|
|
866
|
+
speedMultiplier: 1.05,
|
|
867
|
+
reflectionMode: "angled",
|
|
868
|
+
maxAngleDeg: 60
|
|
869
|
+
};
|
|
870
|
+
function createRicochet2DHandle(ref) {
|
|
321
871
|
return {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
872
|
+
getRicochet: (ctx) => {
|
|
873
|
+
const fsm = ref.fsm;
|
|
874
|
+
if (!fsm) return null;
|
|
875
|
+
return fsm.computeRicochet(ctx, ref.options);
|
|
876
|
+
},
|
|
877
|
+
getLastResult: () => {
|
|
878
|
+
const fsm = ref.fsm;
|
|
879
|
+
return fsm?.getLastResult() ?? null;
|
|
325
880
|
}
|
|
326
881
|
};
|
|
327
882
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
883
|
+
var Ricochet2DSystem = class {
|
|
884
|
+
constructor(world) {
|
|
885
|
+
this.world = world;
|
|
886
|
+
}
|
|
887
|
+
update(_ecs, _delta) {
|
|
888
|
+
if (!this.world?.collisionMap) return;
|
|
889
|
+
for (const [, entity] of this.world.collisionMap) {
|
|
890
|
+
const gameEntity = entity;
|
|
891
|
+
if (typeof gameEntity.getBehaviorRefs !== "function") continue;
|
|
892
|
+
const refs = gameEntity.getBehaviorRefs();
|
|
893
|
+
const ricochetRef = refs.find(
|
|
894
|
+
(r) => r.descriptor.key === /* @__PURE__ */ Symbol.for("zylem:behavior:ricochet-2d")
|
|
895
|
+
);
|
|
896
|
+
if (!ricochetRef) continue;
|
|
897
|
+
if (!ricochetRef.fsm) {
|
|
898
|
+
ricochetRef.fsm = new Ricochet2DFSM();
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
destroy(_ecs) {
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
var Ricochet2DBehavior = defineBehavior({
|
|
906
|
+
name: "ricochet-2d",
|
|
907
|
+
defaultOptions: defaultOptions4,
|
|
908
|
+
systemFactory: (ctx) => new Ricochet2DSystem(ctx.world),
|
|
909
|
+
createHandle: createRicochet2DHandle
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
// src/lib/behaviors/movement-sequence-2d/movement-sequence-2d-fsm.ts
|
|
913
|
+
import { StateMachine as StateMachine5, t as t5 } from "typescript-fsm";
|
|
914
|
+
var MovementSequence2DState = /* @__PURE__ */ ((MovementSequence2DState2) => {
|
|
915
|
+
MovementSequence2DState2["Idle"] = "idle";
|
|
916
|
+
MovementSequence2DState2["Running"] = "running";
|
|
917
|
+
MovementSequence2DState2["Paused"] = "paused";
|
|
918
|
+
MovementSequence2DState2["Completed"] = "completed";
|
|
919
|
+
return MovementSequence2DState2;
|
|
920
|
+
})(MovementSequence2DState || {});
|
|
921
|
+
var MovementSequence2DEvent = /* @__PURE__ */ ((MovementSequence2DEvent2) => {
|
|
922
|
+
MovementSequence2DEvent2["Start"] = "start";
|
|
923
|
+
MovementSequence2DEvent2["Pause"] = "pause";
|
|
924
|
+
MovementSequence2DEvent2["Resume"] = "resume";
|
|
925
|
+
MovementSequence2DEvent2["Complete"] = "complete";
|
|
926
|
+
MovementSequence2DEvent2["Reset"] = "reset";
|
|
927
|
+
return MovementSequence2DEvent2;
|
|
928
|
+
})(MovementSequence2DEvent || {});
|
|
929
|
+
var MovementSequence2DFSM = class {
|
|
930
|
+
machine;
|
|
931
|
+
sequence = [];
|
|
932
|
+
loop = true;
|
|
933
|
+
currentIndex = 0;
|
|
934
|
+
timeRemaining = 0;
|
|
935
|
+
constructor() {
|
|
936
|
+
this.machine = new StateMachine5(
|
|
937
|
+
"idle" /* Idle */,
|
|
938
|
+
[
|
|
939
|
+
// From Idle
|
|
940
|
+
t5("idle" /* Idle */, "start" /* Start */, "running" /* Running */),
|
|
941
|
+
// From Running
|
|
942
|
+
t5("running" /* Running */, "pause" /* Pause */, "paused" /* Paused */),
|
|
943
|
+
t5("running" /* Running */, "complete" /* Complete */, "completed" /* Completed */),
|
|
944
|
+
t5("running" /* Running */, "reset" /* Reset */, "idle" /* Idle */),
|
|
945
|
+
// From Paused
|
|
946
|
+
t5("paused" /* Paused */, "resume" /* Resume */, "running" /* Running */),
|
|
947
|
+
t5("paused" /* Paused */, "reset" /* Reset */, "idle" /* Idle */),
|
|
948
|
+
// From Completed
|
|
949
|
+
t5("completed" /* Completed */, "reset" /* Reset */, "idle" /* Idle */),
|
|
950
|
+
t5("completed" /* Completed */, "start" /* Start */, "running" /* Running */),
|
|
951
|
+
// Self-transitions (no-ops)
|
|
952
|
+
t5("idle" /* Idle */, "pause" /* Pause */, "idle" /* Idle */),
|
|
953
|
+
t5("idle" /* Idle */, "resume" /* Resume */, "idle" /* Idle */),
|
|
954
|
+
t5("running" /* Running */, "start" /* Start */, "running" /* Running */),
|
|
955
|
+
t5("running" /* Running */, "resume" /* Resume */, "running" /* Running */),
|
|
956
|
+
t5("paused" /* Paused */, "pause" /* Pause */, "paused" /* Paused */),
|
|
957
|
+
t5("completed" /* Completed */, "complete" /* Complete */, "completed" /* Completed */)
|
|
958
|
+
]
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Initialize the sequence. Call this once with options.
|
|
963
|
+
*/
|
|
964
|
+
init(sequence, loop) {
|
|
965
|
+
this.sequence = sequence;
|
|
966
|
+
this.loop = loop;
|
|
967
|
+
this.currentIndex = 0;
|
|
968
|
+
this.timeRemaining = sequence.length > 0 ? sequence[0].timeInSeconds : 0;
|
|
969
|
+
}
|
|
970
|
+
getState() {
|
|
971
|
+
return this.machine.getState();
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Start the sequence (from Idle or Completed).
|
|
975
|
+
*/
|
|
976
|
+
start() {
|
|
977
|
+
if (this.machine.getState() === "idle" /* Idle */ || this.machine.getState() === "completed" /* Completed */) {
|
|
978
|
+
this.currentIndex = 0;
|
|
979
|
+
this.timeRemaining = this.sequence.length > 0 ? this.sequence[0].timeInSeconds : 0;
|
|
980
|
+
}
|
|
981
|
+
this.dispatch("start" /* Start */);
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Pause the sequence.
|
|
985
|
+
*/
|
|
986
|
+
pause() {
|
|
987
|
+
this.dispatch("pause" /* Pause */);
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Resume a paused sequence.
|
|
991
|
+
*/
|
|
992
|
+
resume() {
|
|
993
|
+
this.dispatch("resume" /* Resume */);
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Reset to Idle state.
|
|
997
|
+
*/
|
|
998
|
+
reset() {
|
|
999
|
+
this.dispatch("reset" /* Reset */);
|
|
1000
|
+
this.currentIndex = 0;
|
|
1001
|
+
this.timeRemaining = this.sequence.length > 0 ? this.sequence[0].timeInSeconds : 0;
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Update the sequence with delta time.
|
|
1005
|
+
* Returns the current movement to apply.
|
|
1006
|
+
* Automatically starts if in Idle state.
|
|
1007
|
+
*/
|
|
1008
|
+
update(delta) {
|
|
1009
|
+
if (this.sequence.length === 0) {
|
|
1010
|
+
return { moveX: 0, moveY: 0 };
|
|
1011
|
+
}
|
|
1012
|
+
if (this.machine.getState() === "idle" /* Idle */) {
|
|
1013
|
+
this.start();
|
|
1014
|
+
}
|
|
1015
|
+
if (this.machine.getState() !== "running" /* Running */) {
|
|
1016
|
+
if (this.machine.getState() === "completed" /* Completed */) {
|
|
1017
|
+
return { moveX: 0, moveY: 0 };
|
|
1018
|
+
}
|
|
1019
|
+
const step2 = this.sequence[this.currentIndex];
|
|
1020
|
+
return { moveX: step2?.moveX ?? 0, moveY: step2?.moveY ?? 0 };
|
|
1021
|
+
}
|
|
1022
|
+
let timeLeft = this.timeRemaining - delta;
|
|
1023
|
+
while (timeLeft <= 0) {
|
|
1024
|
+
const overflow = -timeLeft;
|
|
1025
|
+
this.currentIndex += 1;
|
|
1026
|
+
if (this.currentIndex >= this.sequence.length) {
|
|
1027
|
+
if (!this.loop) {
|
|
1028
|
+
this.dispatch("complete" /* Complete */);
|
|
1029
|
+
return { moveX: 0, moveY: 0 };
|
|
1030
|
+
}
|
|
1031
|
+
this.currentIndex = 0;
|
|
389
1032
|
}
|
|
1033
|
+
timeLeft = this.sequence[this.currentIndex].timeInSeconds - overflow;
|
|
390
1034
|
}
|
|
1035
|
+
this.timeRemaining = timeLeft;
|
|
1036
|
+
const step = this.sequence[this.currentIndex];
|
|
1037
|
+
return { moveX: step?.moveX ?? 0, moveY: step?.moveY ?? 0 };
|
|
391
1038
|
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Get the current movement without advancing time.
|
|
1041
|
+
*/
|
|
1042
|
+
getMovement() {
|
|
1043
|
+
if (this.sequence.length === 0 || this.machine.getState() === "completed" /* Completed */) {
|
|
1044
|
+
return { moveX: 0, moveY: 0 };
|
|
1045
|
+
}
|
|
1046
|
+
const step = this.sequence[this.currentIndex];
|
|
1047
|
+
return { moveX: step?.moveX ?? 0, moveY: step?.moveY ?? 0 };
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Get current step info.
|
|
1051
|
+
*/
|
|
1052
|
+
getCurrentStep() {
|
|
1053
|
+
if (this.sequence.length === 0) return null;
|
|
1054
|
+
const step = this.sequence[this.currentIndex];
|
|
1055
|
+
if (!step) return null;
|
|
1056
|
+
return {
|
|
1057
|
+
name: step.name,
|
|
1058
|
+
index: this.currentIndex,
|
|
1059
|
+
moveX: step.moveX ?? 0,
|
|
1060
|
+
moveY: step.moveY ?? 0,
|
|
1061
|
+
timeRemaining: this.timeRemaining
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Get sequence progress.
|
|
1066
|
+
*/
|
|
1067
|
+
getProgress() {
|
|
1068
|
+
return {
|
|
1069
|
+
stepIndex: this.currentIndex,
|
|
1070
|
+
totalSteps: this.sequence.length,
|
|
1071
|
+
stepTimeRemaining: this.timeRemaining,
|
|
1072
|
+
done: this.machine.getState() === "completed" /* Completed */
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
dispatch(event) {
|
|
1076
|
+
if (this.machine.can(event)) {
|
|
1077
|
+
this.machine.dispatch(event);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
|
|
1082
|
+
// src/lib/behaviors/movement-sequence-2d/movement-sequence-2d.descriptor.ts
|
|
1083
|
+
var defaultOptions5 = {
|
|
1084
|
+
sequence: [],
|
|
1085
|
+
loop: true
|
|
1086
|
+
};
|
|
1087
|
+
function createMovementSequence2DHandle(ref) {
|
|
1088
|
+
return {
|
|
1089
|
+
getMovement: () => {
|
|
1090
|
+
const fsm = ref.fsm;
|
|
1091
|
+
return fsm?.getMovement() ?? { moveX: 0, moveY: 0 };
|
|
1092
|
+
},
|
|
1093
|
+
getCurrentStep: () => {
|
|
1094
|
+
const fsm = ref.fsm;
|
|
1095
|
+
return fsm?.getCurrentStep() ?? null;
|
|
1096
|
+
},
|
|
1097
|
+
getProgress: () => {
|
|
1098
|
+
const fsm = ref.fsm;
|
|
1099
|
+
return fsm?.getProgress() ?? { stepIndex: 0, totalSteps: 0, stepTimeRemaining: 0, done: true };
|
|
1100
|
+
},
|
|
1101
|
+
pause: () => {
|
|
1102
|
+
const fsm = ref.fsm;
|
|
1103
|
+
fsm?.pause();
|
|
1104
|
+
},
|
|
1105
|
+
resume: () => {
|
|
1106
|
+
const fsm = ref.fsm;
|
|
1107
|
+
fsm?.resume();
|
|
1108
|
+
},
|
|
1109
|
+
reset: () => {
|
|
1110
|
+
const fsm = ref.fsm;
|
|
1111
|
+
fsm?.reset();
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
392
1114
|
}
|
|
1115
|
+
var MovementSequence2DSystem = class {
|
|
1116
|
+
constructor(world) {
|
|
1117
|
+
this.world = world;
|
|
1118
|
+
}
|
|
1119
|
+
update(_ecs, delta) {
|
|
1120
|
+
if (!this.world?.collisionMap) return;
|
|
1121
|
+
for (const [, entity] of this.world.collisionMap) {
|
|
1122
|
+
const gameEntity = entity;
|
|
1123
|
+
if (typeof gameEntity.getBehaviorRefs !== "function") continue;
|
|
1124
|
+
const refs = gameEntity.getBehaviorRefs();
|
|
1125
|
+
const sequenceRef = refs.find(
|
|
1126
|
+
(r) => r.descriptor.key === /* @__PURE__ */ Symbol.for("zylem:behavior:movement-sequence-2d")
|
|
1127
|
+
);
|
|
1128
|
+
if (!sequenceRef) continue;
|
|
1129
|
+
const options = sequenceRef.options;
|
|
1130
|
+
if (!sequenceRef.fsm) {
|
|
1131
|
+
sequenceRef.fsm = new MovementSequence2DFSM();
|
|
1132
|
+
sequenceRef.fsm.init(options.sequence, options.loop);
|
|
1133
|
+
}
|
|
1134
|
+
sequenceRef.fsm.update(delta);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
destroy(_ecs) {
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
var MovementSequence2DBehavior = defineBehavior({
|
|
1141
|
+
name: "movement-sequence-2d",
|
|
1142
|
+
defaultOptions: defaultOptions5,
|
|
1143
|
+
systemFactory: (ctx) => new MovementSequence2DSystem(ctx.world),
|
|
1144
|
+
createHandle: createMovementSequence2DHandle
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
// src/lib/coordinators/boundary-ricochet.coordinator.ts
|
|
1148
|
+
var BoundaryRicochetCoordinator = class {
|
|
1149
|
+
constructor(entity, boundary, ricochet) {
|
|
1150
|
+
this.entity = entity;
|
|
1151
|
+
this.boundary = boundary;
|
|
1152
|
+
this.ricochet = ricochet;
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Update loop - call this every frame
|
|
1156
|
+
*/
|
|
1157
|
+
update() {
|
|
1158
|
+
const hits = this.boundary.getLastHits();
|
|
1159
|
+
if (!hits) return null;
|
|
1160
|
+
const anyHit = hits.left || hits.right || hits.top || hits.bottom;
|
|
1161
|
+
if (!anyHit) return null;
|
|
1162
|
+
let normalX = 0;
|
|
1163
|
+
let normalY = 0;
|
|
1164
|
+
if (hits.left) normalX = 1;
|
|
1165
|
+
if (hits.right) normalX = -1;
|
|
1166
|
+
if (hits.bottom) normalY = 1;
|
|
1167
|
+
if (hits.top) normalY = -1;
|
|
1168
|
+
return this.ricochet.getRicochet({
|
|
1169
|
+
entity: this.entity,
|
|
1170
|
+
contact: { normal: { x: normalX, y: normalY } }
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
393
1174
|
export {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
1175
|
+
BoundaryRicochetCoordinator,
|
|
1176
|
+
MovementSequence2DBehavior,
|
|
1177
|
+
MovementSequence2DEvent,
|
|
1178
|
+
MovementSequence2DFSM,
|
|
1179
|
+
MovementSequence2DState,
|
|
1180
|
+
PhysicsStepBehavior,
|
|
1181
|
+
PhysicsSyncBehavior,
|
|
1182
|
+
Ricochet2DBehavior,
|
|
1183
|
+
Ricochet2DEvent,
|
|
1184
|
+
Ricochet2DFSM,
|
|
1185
|
+
Ricochet2DState,
|
|
1186
|
+
ScreenWrapBehavior,
|
|
1187
|
+
ScreenWrapEvent,
|
|
1188
|
+
ScreenWrapFSM,
|
|
1189
|
+
ScreenWrapState,
|
|
1190
|
+
ThrusterBehavior,
|
|
1191
|
+
ThrusterEvent,
|
|
1192
|
+
ThrusterFSM,
|
|
1193
|
+
ThrusterMovementBehavior,
|
|
1194
|
+
ThrusterState,
|
|
1195
|
+
WorldBoundary2DBehavior,
|
|
1196
|
+
WorldBoundary2DEvent,
|
|
1197
|
+
WorldBoundary2DFSM,
|
|
1198
|
+
WorldBoundary2DState,
|
|
1199
|
+
computeWorldBoundary2DHits,
|
|
1200
|
+
createPhysicsBodyComponent,
|
|
1201
|
+
createThrusterInputComponent,
|
|
1202
|
+
createThrusterMovementComponent,
|
|
1203
|
+
createThrusterStateComponent,
|
|
1204
|
+
createTransformComponent,
|
|
1205
|
+
defineBehavior,
|
|
1206
|
+
hasAnyWorldBoundary2DHit,
|
|
1207
|
+
useBehavior
|
|
397
1208
|
};
|
|
398
1209
|
//# sourceMappingURL=behaviors.js.map
|