@zylem/game-lib 0.6.0 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -16
- package/dist/actions.d.ts +30 -21
- package/dist/actions.js +628 -145
- package/dist/actions.js.map +1 -1
- package/dist/behavior/platformer-3d.d.ts +296 -0
- package/dist/behavior/platformer-3d.js +518 -0
- package/dist/behavior/platformer-3d.js.map +1 -0
- package/dist/behavior/ricochet-2d.d.ts +274 -0
- package/dist/behavior/ricochet-2d.js +394 -0
- package/dist/behavior/ricochet-2d.js.map +1 -0
- package/dist/behavior/screen-wrap.d.ts +86 -0
- package/dist/behavior/screen-wrap.js +195 -0
- package/dist/behavior/screen-wrap.js.map +1 -0
- package/dist/behavior/thruster.d.ts +10 -0
- package/dist/behavior/thruster.js +234 -0
- package/dist/behavior/thruster.js.map +1 -0
- package/dist/behavior/world-boundary-2d.d.ts +141 -0
- package/dist/behavior/world-boundary-2d.js +181 -0
- package/dist/behavior/world-boundary-2d.js.map +1 -0
- package/dist/behavior-descriptor-BWNWmIjv.d.ts +142 -0
- package/dist/{blueprints-BOCc3Wve.d.ts → blueprints-BWGz8fII.d.ts} +2 -2
- package/dist/camera-B5e4c78l.d.ts +468 -0
- package/dist/camera.d.ts +3 -2
- package/dist/camera.js +962 -166
- package/dist/camera.js.map +1 -1
- package/dist/composition-DrzFrbqI.d.ts +218 -0
- package/dist/{core-CZhozNRH.d.ts → core-DAkskq6Y.d.ts} +97 -65
- package/dist/core.d.ts +12 -6
- package/dist/core.js +4449 -1052
- package/dist/core.js.map +1 -1
- package/dist/{entities-BAxfJOkk.d.ts → entities-DC9ce_vx.d.ts} +154 -45
- package/dist/entities.d.ts +5 -2
- package/dist/entities.js +2505 -722
- package/dist/entities.js.map +1 -1
- package/dist/entity-BpbZqg19.d.ts +1100 -0
- package/dist/entity-types-DAu8sGJH.d.ts +26 -0
- package/dist/global-change-Dc8uCKi2.d.ts +25 -0
- package/dist/main.d.ts +472 -29
- package/dist/main.js +11877 -6124
- package/dist/main.js.map +1 -1
- package/dist/{stage-types-CD21XoIU.d.ts → stage-types-BFsm3qsZ.d.ts} +255 -26
- package/dist/stage.d.ts +11 -6
- package/dist/stage.js +3462 -491
- package/dist/stage.js.map +1 -1
- package/dist/thruster-DhRaJnoL.d.ts +172 -0
- package/dist/world-Be5m1XC1.d.ts +31 -0
- package/package.json +21 -4
- package/dist/behaviors.d.ts +0 -106
- package/dist/behaviors.js +0 -398
- package/dist/behaviors.js.map +0 -1
- package/dist/camera-CpbDr4-V.d.ts +0 -116
- package/dist/entity-COvRtFNG.d.ts +0 -395
- package/dist/moveable-B_vyA6cw.d.ts +0 -67
- package/dist/transformable-CUhvyuYO.d.ts +0 -67
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { B as BaseEntityInterface } from '../entity-types-DAu8sGJH.js';
|
|
2
|
+
import { StateMachine } from 'typescript-fsm';
|
|
3
|
+
import { b as BehaviorDescriptor } from '../behavior-descriptor-BWNWmIjv.js';
|
|
4
|
+
import 'three';
|
|
5
|
+
import '@dimforge/rapier3d-compat';
|
|
6
|
+
import 'bitecs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Ricochet2DFSM
|
|
10
|
+
*
|
|
11
|
+
* FSM + extended state to track ricochet events and results.
|
|
12
|
+
* The FSM state tracks whether a ricochet is currently occurring.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
interface Ricochet2DResult {
|
|
16
|
+
/** The reflected velocity vector */
|
|
17
|
+
velocity: {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
z?: number;
|
|
21
|
+
};
|
|
22
|
+
/** The resulting speed after reflection */
|
|
23
|
+
speed: number;
|
|
24
|
+
/** The collision normal used for reflection */
|
|
25
|
+
normal: {
|
|
26
|
+
x: number;
|
|
27
|
+
y: number;
|
|
28
|
+
z?: number;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
interface Ricochet2DCollisionContext {
|
|
32
|
+
entity?: BaseEntityInterface;
|
|
33
|
+
otherEntity?: BaseEntityInterface;
|
|
34
|
+
/** Current velocity of the entity (optional if entity is provided) */
|
|
35
|
+
selfVelocity?: {
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
z?: number;
|
|
39
|
+
};
|
|
40
|
+
/** Contact information from the collision */
|
|
41
|
+
contact: {
|
|
42
|
+
/** The collision normal */
|
|
43
|
+
normal?: {
|
|
44
|
+
x: number;
|
|
45
|
+
y: number;
|
|
46
|
+
z?: number;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Optional position where the collision occurred.
|
|
50
|
+
* If provided, used for precise offset calculation.
|
|
51
|
+
*/
|
|
52
|
+
position?: {
|
|
53
|
+
x: number;
|
|
54
|
+
y: number;
|
|
55
|
+
z?: number;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Optional position of the entity that owns this behavior.
|
|
60
|
+
* Used with contact.position for offset calculations.
|
|
61
|
+
*/
|
|
62
|
+
selfPosition?: {
|
|
63
|
+
x: number;
|
|
64
|
+
y: number;
|
|
65
|
+
z?: number;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Optional position of the other entity in the collision.
|
|
69
|
+
* Used for paddle-style deflection: offset = (contactY - otherY) / halfHeight.
|
|
70
|
+
*/
|
|
71
|
+
otherPosition?: {
|
|
72
|
+
x: number;
|
|
73
|
+
y: number;
|
|
74
|
+
z?: number;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Optional size of the other entity (e.g., paddle size).
|
|
78
|
+
* If provided, used to normalize the offset based on the collision face.
|
|
79
|
+
*/
|
|
80
|
+
otherSize?: {
|
|
81
|
+
x: number;
|
|
82
|
+
y: number;
|
|
83
|
+
z?: number;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
declare enum Ricochet2DState {
|
|
87
|
+
Idle = "idle",
|
|
88
|
+
Ricocheting = "ricocheting"
|
|
89
|
+
}
|
|
90
|
+
declare enum Ricochet2DEvent {
|
|
91
|
+
StartRicochet = "start-ricochet",
|
|
92
|
+
EndRicochet = "end-ricochet"
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Callback type for ricochet event listeners.
|
|
96
|
+
*/
|
|
97
|
+
type RicochetCallback = (result: Ricochet2DResult) => void;
|
|
98
|
+
/**
|
|
99
|
+
* FSM wrapper with extended state (lastResult).
|
|
100
|
+
* Systems or consumers call `computeRicochet(...)` when a collision occurs.
|
|
101
|
+
*/
|
|
102
|
+
declare class Ricochet2DFSM {
|
|
103
|
+
readonly machine: StateMachine<Ricochet2DState, Ricochet2DEvent, never>;
|
|
104
|
+
private lastResult;
|
|
105
|
+
private lastUpdatedAtMs;
|
|
106
|
+
private currentTimeMs;
|
|
107
|
+
private listeners;
|
|
108
|
+
constructor();
|
|
109
|
+
/**
|
|
110
|
+
* Add a listener for ricochet events.
|
|
111
|
+
* @returns Unsubscribe function
|
|
112
|
+
*/
|
|
113
|
+
addListener(callback: RicochetCallback): () => void;
|
|
114
|
+
/**
|
|
115
|
+
* Emit result to all listeners.
|
|
116
|
+
*/
|
|
117
|
+
private emitToListeners;
|
|
118
|
+
getState(): Ricochet2DState;
|
|
119
|
+
/**
|
|
120
|
+
* Returns the last computed ricochet result, or null if none.
|
|
121
|
+
*/
|
|
122
|
+
getLastResult(): Ricochet2DResult | null;
|
|
123
|
+
/**
|
|
124
|
+
* Best-effort timestamp (ms) of the last computation.
|
|
125
|
+
*/
|
|
126
|
+
getLastUpdatedAtMs(): number | null;
|
|
127
|
+
/**
|
|
128
|
+
* Set current game time (called by system each frame).
|
|
129
|
+
* Used for cooldown calculations.
|
|
130
|
+
*/
|
|
131
|
+
setCurrentTimeMs(timeMs: number): void;
|
|
132
|
+
/**
|
|
133
|
+
* Check if ricochet is on cooldown (to prevent rapid duplicate applications).
|
|
134
|
+
* @param cooldownMs Cooldown duration in milliseconds (default: 50ms)
|
|
135
|
+
*/
|
|
136
|
+
isOnCooldown(cooldownMs?: number): boolean;
|
|
137
|
+
/**
|
|
138
|
+
* Reset cooldown state (e.g., on entity respawn).
|
|
139
|
+
*/
|
|
140
|
+
resetCooldown(): void;
|
|
141
|
+
/**
|
|
142
|
+
* Compute a ricochet result from collision context.
|
|
143
|
+
* Returns the result for the consumer to apply, or null if invalid input.
|
|
144
|
+
*/
|
|
145
|
+
computeRicochet(ctx: Ricochet2DCollisionContext, options?: {
|
|
146
|
+
minSpeed?: number;
|
|
147
|
+
maxSpeed?: number;
|
|
148
|
+
speedMultiplier?: number;
|
|
149
|
+
reflectionMode?: 'simple' | 'angled';
|
|
150
|
+
maxAngleDeg?: number;
|
|
151
|
+
}): Ricochet2DResult | null;
|
|
152
|
+
/**
|
|
153
|
+
* Extract velocity, position, and size data from entities or context.
|
|
154
|
+
*/
|
|
155
|
+
private extractDataFromEntities;
|
|
156
|
+
/**
|
|
157
|
+
* Compute collision normal from entity positions using AABB heuristic.
|
|
158
|
+
*/
|
|
159
|
+
private computeNormalFromPositions;
|
|
160
|
+
/**
|
|
161
|
+
* Compute basic reflection using the formula: v' = v - 2(v·n)n
|
|
162
|
+
*/
|
|
163
|
+
private computeBasicReflection;
|
|
164
|
+
/**
|
|
165
|
+
* Compute angled deflection for paddle-style reflections.
|
|
166
|
+
*/
|
|
167
|
+
private computeAngledDeflection;
|
|
168
|
+
/**
|
|
169
|
+
* Compute hit offset for angled deflection (-1 to 1).
|
|
170
|
+
*/
|
|
171
|
+
private computeHitOffset;
|
|
172
|
+
/**
|
|
173
|
+
* Apply speed constraints to the reflected velocity.
|
|
174
|
+
*/
|
|
175
|
+
private applySpeedClamp;
|
|
176
|
+
/**
|
|
177
|
+
* Clear the ricochet state (call after consumer has applied the result).
|
|
178
|
+
*/
|
|
179
|
+
clearRicochet(): void;
|
|
180
|
+
private dispatch;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
interface Ricochet2DOptions {
|
|
184
|
+
/**
|
|
185
|
+
* Minimum speed after reflection.
|
|
186
|
+
* Default: 2
|
|
187
|
+
*/
|
|
188
|
+
minSpeed: number;
|
|
189
|
+
/**
|
|
190
|
+
* Maximum speed after reflection.
|
|
191
|
+
* Default: 20
|
|
192
|
+
*/
|
|
193
|
+
maxSpeed: number;
|
|
194
|
+
/**
|
|
195
|
+
* Speed multiplier applied during angled reflection.
|
|
196
|
+
* Default: 1.05
|
|
197
|
+
*/
|
|
198
|
+
speedMultiplier: number;
|
|
199
|
+
/**
|
|
200
|
+
* Reflection mode:
|
|
201
|
+
* - 'simple': Basic axis inversion
|
|
202
|
+
* - 'angled': Paddle-style deflection based on contact point
|
|
203
|
+
* Default: 'angled'
|
|
204
|
+
*/
|
|
205
|
+
reflectionMode: 'simple' | 'angled';
|
|
206
|
+
/**
|
|
207
|
+
* Maximum deflection angle in degrees for angled mode.
|
|
208
|
+
* Default: 60
|
|
209
|
+
*/
|
|
210
|
+
maxAngleDeg: number;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Handle methods provided by Ricochet2DBehavior
|
|
214
|
+
*/
|
|
215
|
+
interface Ricochet2DHandle {
|
|
216
|
+
/**
|
|
217
|
+
* Compute a ricochet/reflection result from collision context.
|
|
218
|
+
* Returns the result for the consumer to apply, or null if invalid input.
|
|
219
|
+
*
|
|
220
|
+
* @param ctx - Collision context with selfVelocity and contact normal
|
|
221
|
+
* @returns Ricochet result with velocity, speed, and normal, or null
|
|
222
|
+
*/
|
|
223
|
+
getRicochet(ctx: Ricochet2DCollisionContext): Ricochet2DResult | null;
|
|
224
|
+
/**
|
|
225
|
+
* Compute ricochet and apply velocity directly via transformStore.
|
|
226
|
+
* Emits to onRicochet listeners if successful.
|
|
227
|
+
*
|
|
228
|
+
* @param ctx - Collision context with entity and contact normal
|
|
229
|
+
* @returns true if ricochet was computed and applied, false otherwise
|
|
230
|
+
*/
|
|
231
|
+
applyRicochet(ctx: Ricochet2DCollisionContext): boolean;
|
|
232
|
+
/**
|
|
233
|
+
* Get the last computed ricochet result, or null if none.
|
|
234
|
+
*/
|
|
235
|
+
getLastResult(): Ricochet2DResult | null;
|
|
236
|
+
/**
|
|
237
|
+
* Register a listener for ricochet events.
|
|
238
|
+
* Called whenever a ricochet is computed (from getRicochet or applyRicochet).
|
|
239
|
+
*
|
|
240
|
+
* @param callback - Function to call with ricochet result
|
|
241
|
+
* @returns Unsubscribe function
|
|
242
|
+
*/
|
|
243
|
+
onRicochet(callback: RicochetCallback): () => void;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Ricochet2DBehavior
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```ts
|
|
250
|
+
* import { Ricochet2DBehavior } from "@zylem/game-lib";
|
|
251
|
+
*
|
|
252
|
+
* const ball = createSphere({ ... });
|
|
253
|
+
* const ricochet = ball.use(Ricochet2DBehavior, {
|
|
254
|
+
* minSpeed: 3,
|
|
255
|
+
* maxSpeed: 15,
|
|
256
|
+
* reflectionMode: 'angled',
|
|
257
|
+
* });
|
|
258
|
+
*
|
|
259
|
+
* ball.onCollision(({ entity, other }) => {
|
|
260
|
+
* const velocity = entity.body.linvel();
|
|
261
|
+
* const result = ricochet.getRicochet({
|
|
262
|
+
* selfVelocity: velocity,
|
|
263
|
+
* contact: { normal: { x: 1, y: 0 } }, // from collision data
|
|
264
|
+
* });
|
|
265
|
+
*
|
|
266
|
+
* if (result) {
|
|
267
|
+
* entity.body.setLinvel(result.velocity, true);
|
|
268
|
+
* }
|
|
269
|
+
* });
|
|
270
|
+
* ```
|
|
271
|
+
*/
|
|
272
|
+
declare const Ricochet2DBehavior: BehaviorDescriptor<Ricochet2DOptions, Ricochet2DHandle, unknown>;
|
|
273
|
+
|
|
274
|
+
export { Ricochet2DBehavior, type Ricochet2DCollisionContext, Ricochet2DEvent, Ricochet2DFSM, type Ricochet2DHandle, type Ricochet2DOptions, type Ricochet2DResult, Ricochet2DState, type RicochetCallback };
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
// src/lib/behaviors/ricochet-2d/ricochet-2d-fsm.ts
|
|
2
|
+
import { StateMachine, t } from "typescript-fsm";
|
|
3
|
+
var Ricochet2DState = /* @__PURE__ */ ((Ricochet2DState2) => {
|
|
4
|
+
Ricochet2DState2["Idle"] = "idle";
|
|
5
|
+
Ricochet2DState2["Ricocheting"] = "ricocheting";
|
|
6
|
+
return Ricochet2DState2;
|
|
7
|
+
})(Ricochet2DState || {});
|
|
8
|
+
var Ricochet2DEvent = /* @__PURE__ */ ((Ricochet2DEvent2) => {
|
|
9
|
+
Ricochet2DEvent2["StartRicochet"] = "start-ricochet";
|
|
10
|
+
Ricochet2DEvent2["EndRicochet"] = "end-ricochet";
|
|
11
|
+
return Ricochet2DEvent2;
|
|
12
|
+
})(Ricochet2DEvent || {});
|
|
13
|
+
function clamp(value, min, max) {
|
|
14
|
+
return Math.max(min, Math.min(max, value));
|
|
15
|
+
}
|
|
16
|
+
var Ricochet2DFSM = class {
|
|
17
|
+
machine;
|
|
18
|
+
lastResult = null;
|
|
19
|
+
lastUpdatedAtMs = null;
|
|
20
|
+
currentTimeMs = 0;
|
|
21
|
+
listeners = /* @__PURE__ */ new Set();
|
|
22
|
+
constructor() {
|
|
23
|
+
this.machine = new StateMachine(
|
|
24
|
+
"idle" /* Idle */,
|
|
25
|
+
[
|
|
26
|
+
t("idle" /* Idle */, "start-ricochet" /* StartRicochet */, "ricocheting" /* Ricocheting */),
|
|
27
|
+
t("ricocheting" /* Ricocheting */, "end-ricochet" /* EndRicochet */, "idle" /* Idle */),
|
|
28
|
+
// Self transitions (no-ops)
|
|
29
|
+
t("idle" /* Idle */, "end-ricochet" /* EndRicochet */, "idle" /* Idle */),
|
|
30
|
+
t("ricocheting" /* Ricocheting */, "start-ricochet" /* StartRicochet */, "ricocheting" /* Ricocheting */)
|
|
31
|
+
]
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Add a listener for ricochet events.
|
|
36
|
+
* @returns Unsubscribe function
|
|
37
|
+
*/
|
|
38
|
+
addListener(callback) {
|
|
39
|
+
this.listeners.add(callback);
|
|
40
|
+
return () => this.listeners.delete(callback);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Emit result to all listeners.
|
|
44
|
+
*/
|
|
45
|
+
emitToListeners(result) {
|
|
46
|
+
for (const callback of this.listeners) {
|
|
47
|
+
try {
|
|
48
|
+
callback(result);
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.error("[Ricochet2DFSM] Listener error:", e);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
getState() {
|
|
55
|
+
return this.machine.getState();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Returns the last computed ricochet result, or null if none.
|
|
59
|
+
*/
|
|
60
|
+
getLastResult() {
|
|
61
|
+
return this.lastResult;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Best-effort timestamp (ms) of the last computation.
|
|
65
|
+
*/
|
|
66
|
+
getLastUpdatedAtMs() {
|
|
67
|
+
return this.lastUpdatedAtMs;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Set current game time (called by system each frame).
|
|
71
|
+
* Used for cooldown calculations.
|
|
72
|
+
*/
|
|
73
|
+
setCurrentTimeMs(timeMs) {
|
|
74
|
+
this.currentTimeMs = timeMs;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Check if ricochet is on cooldown (to prevent rapid duplicate applications).
|
|
78
|
+
* @param cooldownMs Cooldown duration in milliseconds (default: 50ms)
|
|
79
|
+
*/
|
|
80
|
+
isOnCooldown(cooldownMs = 50) {
|
|
81
|
+
if (this.lastUpdatedAtMs === null) return false;
|
|
82
|
+
return this.currentTimeMs - this.lastUpdatedAtMs < cooldownMs;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Reset cooldown state (e.g., on entity respawn).
|
|
86
|
+
*/
|
|
87
|
+
resetCooldown() {
|
|
88
|
+
this.lastUpdatedAtMs = null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Compute a ricochet result from collision context.
|
|
92
|
+
* Returns the result for the consumer to apply, or null if invalid input.
|
|
93
|
+
*/
|
|
94
|
+
computeRicochet(ctx, options = {}) {
|
|
95
|
+
const {
|
|
96
|
+
minSpeed = 2,
|
|
97
|
+
maxSpeed = 20,
|
|
98
|
+
speedMultiplier = 1.05,
|
|
99
|
+
reflectionMode = "angled",
|
|
100
|
+
maxAngleDeg = 60
|
|
101
|
+
} = options;
|
|
102
|
+
const { selfVelocity, selfPosition, otherPosition, otherSize } = this.extractDataFromEntities(ctx);
|
|
103
|
+
if (!selfVelocity) {
|
|
104
|
+
this.dispatch("end-ricochet" /* EndRicochet */);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const speed = Math.hypot(selfVelocity.x, selfVelocity.y);
|
|
108
|
+
if (speed === 0) {
|
|
109
|
+
this.dispatch("end-ricochet" /* EndRicochet */);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const normal = ctx.contact.normal ?? this.computeNormalFromPositions(selfPosition, otherPosition);
|
|
113
|
+
if (!normal) {
|
|
114
|
+
this.dispatch("end-ricochet" /* EndRicochet */);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
let reflected = this.computeBasicReflection(selfVelocity, normal);
|
|
118
|
+
if (reflectionMode === "angled") {
|
|
119
|
+
reflected = this.computeAngledDeflection(
|
|
120
|
+
selfVelocity,
|
|
121
|
+
normal,
|
|
122
|
+
speed,
|
|
123
|
+
maxAngleDeg,
|
|
124
|
+
speedMultiplier,
|
|
125
|
+
selfPosition,
|
|
126
|
+
otherPosition,
|
|
127
|
+
otherSize,
|
|
128
|
+
ctx.contact.position
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
reflected = this.applySpeedClamp(reflected, minSpeed, maxSpeed);
|
|
132
|
+
const result = {
|
|
133
|
+
velocity: { x: reflected.x, y: reflected.y, z: 0 },
|
|
134
|
+
speed: Math.hypot(reflected.x, reflected.y),
|
|
135
|
+
normal: { x: normal.x, y: normal.y, z: 0 }
|
|
136
|
+
};
|
|
137
|
+
this.lastResult = result;
|
|
138
|
+
this.lastUpdatedAtMs = this.currentTimeMs;
|
|
139
|
+
this.dispatch("start-ricochet" /* StartRicochet */);
|
|
140
|
+
this.emitToListeners(result);
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Extract velocity, position, and size data from entities or context.
|
|
145
|
+
*/
|
|
146
|
+
extractDataFromEntities(ctx) {
|
|
147
|
+
let selfVelocity = ctx.selfVelocity;
|
|
148
|
+
let selfPosition = ctx.selfPosition;
|
|
149
|
+
let otherPosition = ctx.otherPosition;
|
|
150
|
+
let otherSize = ctx.otherSize;
|
|
151
|
+
if (ctx.entity?.body) {
|
|
152
|
+
const vel = ctx.entity.body.linvel();
|
|
153
|
+
selfVelocity = selfVelocity ?? { x: vel.x, y: vel.y, z: vel.z };
|
|
154
|
+
const pos = ctx.entity.body.translation();
|
|
155
|
+
selfPosition = selfPosition ?? { x: pos.x, y: pos.y, z: pos.z };
|
|
156
|
+
}
|
|
157
|
+
if (ctx.otherEntity?.body) {
|
|
158
|
+
const pos = ctx.otherEntity.body.translation();
|
|
159
|
+
otherPosition = otherPosition ?? { x: pos.x, y: pos.y, z: pos.z };
|
|
160
|
+
}
|
|
161
|
+
if (ctx.otherEntity && "size" in ctx.otherEntity) {
|
|
162
|
+
const size = ctx.otherEntity.size;
|
|
163
|
+
if (size && typeof size.x === "number") {
|
|
164
|
+
otherSize = otherSize ?? { x: size.x, y: size.y, z: size.z };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { selfVelocity, selfPosition, otherPosition, otherSize };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Compute collision normal from entity positions using AABB heuristic.
|
|
171
|
+
*/
|
|
172
|
+
computeNormalFromPositions(selfPosition, otherPosition) {
|
|
173
|
+
if (!selfPosition || !otherPosition) return null;
|
|
174
|
+
const dx = selfPosition.x - otherPosition.x;
|
|
175
|
+
const dy = selfPosition.y - otherPosition.y;
|
|
176
|
+
if (Math.abs(dx) > Math.abs(dy)) {
|
|
177
|
+
return { x: dx > 0 ? 1 : -1, y: 0, z: 0 };
|
|
178
|
+
} else {
|
|
179
|
+
return { x: 0, y: dy > 0 ? 1 : -1, z: 0 };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Compute basic reflection using the formula: v' = v - 2(v·n)n
|
|
184
|
+
*/
|
|
185
|
+
computeBasicReflection(velocity, normal) {
|
|
186
|
+
const vx = velocity.x;
|
|
187
|
+
const vy = velocity.y;
|
|
188
|
+
const dotProduct = vx * normal.x + vy * normal.y;
|
|
189
|
+
return {
|
|
190
|
+
x: vx - 2 * dotProduct * normal.x,
|
|
191
|
+
y: vy - 2 * dotProduct * normal.y
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Compute angled deflection for paddle-style reflections.
|
|
196
|
+
*/
|
|
197
|
+
computeAngledDeflection(velocity, normal, speed, maxAngleDeg, speedMultiplier, selfPosition, otherPosition, otherSize, contactPosition) {
|
|
198
|
+
const maxAngleRad = maxAngleDeg * Math.PI / 180;
|
|
199
|
+
let tx = -normal.y;
|
|
200
|
+
let ty = normal.x;
|
|
201
|
+
if (Math.abs(normal.x) > Math.abs(normal.y)) {
|
|
202
|
+
if (ty < 0) {
|
|
203
|
+
tx = -tx;
|
|
204
|
+
ty = -ty;
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
if (tx < 0) {
|
|
208
|
+
tx = -tx;
|
|
209
|
+
ty = -ty;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const offset = this.computeHitOffset(
|
|
213
|
+
velocity,
|
|
214
|
+
normal,
|
|
215
|
+
speed,
|
|
216
|
+
tx,
|
|
217
|
+
ty,
|
|
218
|
+
selfPosition,
|
|
219
|
+
otherPosition,
|
|
220
|
+
otherSize,
|
|
221
|
+
contactPosition
|
|
222
|
+
);
|
|
223
|
+
const angle = clamp(offset, -1, 1) * maxAngleRad;
|
|
224
|
+
const cosA = Math.cos(angle);
|
|
225
|
+
const sinA = Math.sin(angle);
|
|
226
|
+
const newSpeed = speed * speedMultiplier;
|
|
227
|
+
return {
|
|
228
|
+
x: newSpeed * (normal.x * cosA + tx * sinA),
|
|
229
|
+
y: newSpeed * (normal.y * cosA + ty * sinA)
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Compute hit offset for angled deflection (-1 to 1).
|
|
234
|
+
*/
|
|
235
|
+
computeHitOffset(velocity, normal, speed, tx, ty, selfPosition, otherPosition, otherSize, contactPosition) {
|
|
236
|
+
if (otherPosition && otherSize) {
|
|
237
|
+
const useY = Math.abs(normal.x) > Math.abs(normal.y);
|
|
238
|
+
const halfExtent = useY ? otherSize.y / 2 : otherSize.x / 2;
|
|
239
|
+
if (useY) {
|
|
240
|
+
const selfY = selfPosition?.y ?? contactPosition?.y ?? 0;
|
|
241
|
+
const paddleY = otherPosition.y;
|
|
242
|
+
return (selfY - paddleY) / halfExtent;
|
|
243
|
+
} else {
|
|
244
|
+
const selfX = selfPosition?.x ?? contactPosition?.x ?? 0;
|
|
245
|
+
const paddleX = otherPosition.x;
|
|
246
|
+
return (selfX - paddleX) / halfExtent;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return (velocity.x * tx + velocity.y * ty) / speed;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Apply speed constraints to the reflected velocity.
|
|
253
|
+
*/
|
|
254
|
+
applySpeedClamp(velocity, minSpeed, maxSpeed) {
|
|
255
|
+
const currentSpeed = Math.hypot(velocity.x, velocity.y);
|
|
256
|
+
if (currentSpeed === 0) return velocity;
|
|
257
|
+
const targetSpeed = clamp(currentSpeed, minSpeed, maxSpeed);
|
|
258
|
+
const scale = targetSpeed / currentSpeed;
|
|
259
|
+
return {
|
|
260
|
+
x: velocity.x * scale,
|
|
261
|
+
y: velocity.y * scale
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Clear the ricochet state (call after consumer has applied the result).
|
|
266
|
+
*/
|
|
267
|
+
clearRicochet() {
|
|
268
|
+
this.dispatch("end-ricochet" /* EndRicochet */);
|
|
269
|
+
}
|
|
270
|
+
dispatch(event) {
|
|
271
|
+
if (this.machine.can(event)) {
|
|
272
|
+
this.machine.dispatch(event);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// src/lib/behaviors/behavior-descriptor.ts
|
|
278
|
+
function defineBehavior(config) {
|
|
279
|
+
return {
|
|
280
|
+
key: /* @__PURE__ */ Symbol.for(`zylem:behavior:${config.name}`),
|
|
281
|
+
defaultOptions: config.defaultOptions,
|
|
282
|
+
systemFactory: config.systemFactory,
|
|
283
|
+
createHandle: config.createHandle
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/lib/behaviors/ricochet-2d/ricochet-2d.descriptor.ts
|
|
288
|
+
var defaultOptions = {
|
|
289
|
+
minSpeed: 2,
|
|
290
|
+
maxSpeed: 20,
|
|
291
|
+
speedMultiplier: 1.05,
|
|
292
|
+
reflectionMode: "angled",
|
|
293
|
+
maxAngleDeg: 60
|
|
294
|
+
};
|
|
295
|
+
function createRicochet2DHandle(ref) {
|
|
296
|
+
return {
|
|
297
|
+
getRicochet: (ctx) => {
|
|
298
|
+
const fsm = ref.fsm;
|
|
299
|
+
if (!fsm) return null;
|
|
300
|
+
return fsm.computeRicochet(ctx, ref.options);
|
|
301
|
+
},
|
|
302
|
+
applyRicochet: (ctx) => {
|
|
303
|
+
const fsm = ref.fsm;
|
|
304
|
+
if (!fsm) return false;
|
|
305
|
+
if (fsm.isOnCooldown()) return false;
|
|
306
|
+
const result = fsm.computeRicochet(ctx, ref.options);
|
|
307
|
+
if (!result) return false;
|
|
308
|
+
const entity = ctx.entity;
|
|
309
|
+
if (entity?.transformStore) {
|
|
310
|
+
entity.transformStore.velocity.x = result.velocity.x;
|
|
311
|
+
entity.transformStore.velocity.y = result.velocity.y;
|
|
312
|
+
entity.transformStore.velocity.z = result.velocity.z ?? 0;
|
|
313
|
+
entity.transformStore.dirty.velocity = true;
|
|
314
|
+
}
|
|
315
|
+
return true;
|
|
316
|
+
},
|
|
317
|
+
getLastResult: () => {
|
|
318
|
+
const fsm = ref.fsm;
|
|
319
|
+
return fsm?.getLastResult() ?? null;
|
|
320
|
+
},
|
|
321
|
+
onRicochet: (callback) => {
|
|
322
|
+
const fsm = ref.fsm;
|
|
323
|
+
if (!fsm) {
|
|
324
|
+
if (!ref.pendingListeners) {
|
|
325
|
+
ref.pendingListeners = [];
|
|
326
|
+
}
|
|
327
|
+
ref.pendingListeners.push(callback);
|
|
328
|
+
return () => {
|
|
329
|
+
const pending = ref.pendingListeners;
|
|
330
|
+
const idx = pending.indexOf(callback);
|
|
331
|
+
if (idx >= 0) pending.splice(idx, 1);
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
return fsm.addListener(callback);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
var Ricochet2DSystem = class {
|
|
339
|
+
constructor(world) {
|
|
340
|
+
this.world = world;
|
|
341
|
+
}
|
|
342
|
+
elapsedMs = 0;
|
|
343
|
+
update(_ecs, delta) {
|
|
344
|
+
this.elapsedMs += delta * 1e3;
|
|
345
|
+
if (!this.world?.collisionMap) return;
|
|
346
|
+
for (const [, entity] of this.world.collisionMap) {
|
|
347
|
+
const gameEntity = entity;
|
|
348
|
+
if (typeof gameEntity.getBehaviorRefs !== "function") continue;
|
|
349
|
+
const refs = gameEntity.getBehaviorRefs();
|
|
350
|
+
const ricochetRef = refs.find(
|
|
351
|
+
(r) => r.descriptor.key === /* @__PURE__ */ Symbol.for("zylem:behavior:ricochet-2d")
|
|
352
|
+
);
|
|
353
|
+
if (!ricochetRef) continue;
|
|
354
|
+
if (!ricochetRef.fsm) {
|
|
355
|
+
ricochetRef.fsm = new Ricochet2DFSM();
|
|
356
|
+
const pending = ricochetRef.pendingListeners;
|
|
357
|
+
if (pending) {
|
|
358
|
+
for (const cb of pending) {
|
|
359
|
+
ricochetRef.fsm.addListener(cb);
|
|
360
|
+
}
|
|
361
|
+
ricochetRef.pendingListeners = [];
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
ricochetRef.fsm.setCurrentTimeMs(this.elapsedMs);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
destroy(_ecs) {
|
|
368
|
+
if (!this.world?.collisionMap) return;
|
|
369
|
+
for (const [, entity] of this.world.collisionMap) {
|
|
370
|
+
const gameEntity = entity;
|
|
371
|
+
if (typeof gameEntity.getBehaviorRefs !== "function") continue;
|
|
372
|
+
const refs = gameEntity.getBehaviorRefs();
|
|
373
|
+
const ricochetRef = refs.find(
|
|
374
|
+
(r) => r.descriptor.key === /* @__PURE__ */ Symbol.for("zylem:behavior:ricochet-2d")
|
|
375
|
+
);
|
|
376
|
+
if (ricochetRef?.fsm) {
|
|
377
|
+
ricochetRef.fsm.resetCooldown();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
var Ricochet2DBehavior = defineBehavior({
|
|
383
|
+
name: "ricochet-2d",
|
|
384
|
+
defaultOptions,
|
|
385
|
+
systemFactory: (ctx) => new Ricochet2DSystem(ctx.world),
|
|
386
|
+
createHandle: createRicochet2DHandle
|
|
387
|
+
});
|
|
388
|
+
export {
|
|
389
|
+
Ricochet2DBehavior,
|
|
390
|
+
Ricochet2DEvent,
|
|
391
|
+
Ricochet2DFSM,
|
|
392
|
+
Ricochet2DState
|
|
393
|
+
};
|
|
394
|
+
//# sourceMappingURL=ricochet-2d.js.map
|