@zylem/behaviors 0.2.0
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 +26 -0
- package/dist/behavior-system-DYbx-x_-.d.ts +307 -0
- package/dist/chunk-2C3Z5GE7.js +176 -0
- package/dist/chunk-2C3Z5GE7.js.map +1 -0
- package/dist/chunk-2CLT36VB.js +241 -0
- package/dist/chunk-2CLT36VB.js.map +1 -0
- package/dist/chunk-3PYGRHAL.js +35 -0
- package/dist/chunk-3PYGRHAL.js.map +1 -0
- package/dist/chunk-4I6E6DGV.js +212 -0
- package/dist/chunk-4I6E6DGV.js.map +1 -0
- package/dist/chunk-4URF65BR.js +44 -0
- package/dist/chunk-4URF65BR.js.map +1 -0
- package/dist/chunk-6C6NXNEW.js +38 -0
- package/dist/chunk-6C6NXNEW.js.map +1 -0
- package/dist/chunk-ARJWUNRE.js +431 -0
- package/dist/chunk-ARJWUNRE.js.map +1 -0
- package/dist/chunk-BXP4K7RF.js +1205 -0
- package/dist/chunk-BXP4K7RF.js.map +1 -0
- package/dist/chunk-DYLH6K7B.js +44 -0
- package/dist/chunk-DYLH6K7B.js.map +1 -0
- package/dist/chunk-EIWQM6U3.js +27 -0
- package/dist/chunk-EIWQM6U3.js.map +1 -0
- package/dist/chunk-EZNCCO4L.js +14 -0
- package/dist/chunk-EZNCCO4L.js.map +1 -0
- package/dist/chunk-F3KRX2V4.js +457 -0
- package/dist/chunk-F3KRX2V4.js.map +1 -0
- package/dist/chunk-FPTWVCPN.js +53 -0
- package/dist/chunk-FPTWVCPN.js.map +1 -0
- package/dist/chunk-GCNRXAKH.js +37 -0
- package/dist/chunk-GCNRXAKH.js.map +1 -0
- package/dist/chunk-GYURWCIQ.js +1985 -0
- package/dist/chunk-GYURWCIQ.js.map +1 -0
- package/dist/chunk-HZMY3TT4.js +244 -0
- package/dist/chunk-HZMY3TT4.js.map +1 -0
- package/dist/chunk-ID5QJYF5.js +50 -0
- package/dist/chunk-ID5QJYF5.js.map +1 -0
- package/dist/chunk-JAN34G6S.js +552 -0
- package/dist/chunk-JAN34G6S.js.map +1 -0
- package/dist/chunk-K3UNLQ7W.js +118 -0
- package/dist/chunk-K3UNLQ7W.js.map +1 -0
- package/dist/chunk-KDY5N6J4.js +82 -0
- package/dist/chunk-KDY5N6J4.js.map +1 -0
- package/dist/chunk-KOZUNIIE.js +413 -0
- package/dist/chunk-KOZUNIIE.js.map +1 -0
- package/dist/chunk-KS72RZHX.js +30 -0
- package/dist/chunk-KS72RZHX.js.map +1 -0
- package/dist/chunk-LD5SMIB3.js +32 -0
- package/dist/chunk-LD5SMIB3.js.map +1 -0
- package/dist/chunk-OHCPV2NT.js +86 -0
- package/dist/chunk-OHCPV2NT.js.map +1 -0
- package/dist/chunk-OYWFPIHF.js +115 -0
- package/dist/chunk-OYWFPIHF.js.map +1 -0
- package/dist/chunk-POZER3MQ.js +407 -0
- package/dist/chunk-POZER3MQ.js.map +1 -0
- package/dist/chunk-RTRVBEIB.js +56 -0
- package/dist/chunk-RTRVBEIB.js.map +1 -0
- package/dist/chunk-RTVRUNYR.js +29 -0
- package/dist/chunk-RTVRUNYR.js.map +1 -0
- package/dist/chunk-SS7ELMJ7.js +406 -0
- package/dist/chunk-SS7ELMJ7.js.map +1 -0
- package/dist/chunk-STCCNLPC.js +160 -0
- package/dist/chunk-STCCNLPC.js.map +1 -0
- package/dist/chunk-SVLNF434.js +261 -0
- package/dist/chunk-SVLNF434.js.map +1 -0
- package/dist/chunk-UYFOLI2X.js +24 -0
- package/dist/chunk-UYFOLI2X.js.map +1 -0
- package/dist/chunk-VAN4WW3F.js +67 -0
- package/dist/chunk-VAN4WW3F.js.map +1 -0
- package/dist/chunk-WOMHL23C.js +369 -0
- package/dist/chunk-WOMHL23C.js.map +1 -0
- package/dist/chunk-WWVD5DR5.js +120 -0
- package/dist/chunk-WWVD5DR5.js.map +1 -0
- package/dist/chunk-XG2YMCJW.js +201 -0
- package/dist/chunk-XG2YMCJW.js.map +1 -0
- package/dist/chunk-XIJNE67Y.js +210 -0
- package/dist/chunk-XIJNE67Y.js.map +1 -0
- package/dist/chunk-Y6UJOUE5.js +44 -0
- package/dist/chunk-Y6UJOUE5.js.map +1 -0
- package/dist/chunk-YUO6SKJD.js +348 -0
- package/dist/chunk-YUO6SKJD.js.map +1 -0
- package/dist/components--hjBMTnz.d.ts +120 -0
- package/dist/components-B29WHGQN.d.ts +39 -0
- package/dist/components-BWG1GkZA.d.ts +20 -0
- package/dist/components-XCpL6IYt.d.ts +18 -0
- package/dist/components-qWwOsvwh.d.ts +43 -0
- package/dist/cooldown.d.ts +116 -0
- package/dist/cooldown.js +20 -0
- package/dist/cooldown.js.map +1 -0
- package/dist/coordinators/boundary-ricochet-3d.d.ts +19 -0
- package/dist/coordinators/boundary-ricochet-3d.js +8 -0
- package/dist/coordinators/boundary-ricochet-3d.js.map +1 -0
- package/dist/coordinators/boundary-ricochet.d.ts +34 -0
- package/dist/coordinators/boundary-ricochet.js +8 -0
- package/dist/coordinators/boundary-ricochet.js.map +1 -0
- package/dist/coordinators/first-person-shooter.d.ts +35 -0
- package/dist/coordinators/first-person-shooter.js +9 -0
- package/dist/coordinators/first-person-shooter.js.map +1 -0
- package/dist/coordinators/multidirectional-space-shooter.d.ts +27 -0
- package/dist/coordinators/multidirectional-space-shooter.js +10 -0
- package/dist/coordinators/multidirectional-space-shooter.js.map +1 -0
- package/dist/coordinators/top-down-shooter.d.ts +27 -0
- package/dist/coordinators/top-down-shooter.js +10 -0
- package/dist/coordinators/top-down-shooter.js.map +1 -0
- package/dist/core.d.ts +25 -0
- package/dist/core.js +327 -0
- package/dist/core.js.map +1 -0
- package/dist/destructible-3d.d.ts +42 -0
- package/dist/destructible-3d.js +13 -0
- package/dist/destructible-3d.js.map +1 -0
- package/dist/destructible-prebake.d.ts +28 -0
- package/dist/destructible-prebake.js +23 -0
- package/dist/destructible-prebake.js.map +1 -0
- package/dist/entity-types-CmtQ-IBp.d.ts +15 -0
- package/dist/first-person-perspective-C9Mptl0-.d.ts +23 -0
- package/dist/first-person.d.ts +204 -0
- package/dist/first-person.js +27 -0
- package/dist/first-person.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +263 -0
- package/dist/index.js.map +1 -0
- package/dist/jumper-2d.d.ts +113 -0
- package/dist/jumper-2d.js +27 -0
- package/dist/jumper-2d.js.map +1 -0
- package/dist/jumper-3d.d.ts +164 -0
- package/dist/jumper-3d.js +29 -0
- package/dist/jumper-3d.js.map +1 -0
- package/dist/moveable-BCRpjAQb.d.ts +176 -0
- package/dist/particle-emitter/presets.d.ts +46 -0
- package/dist/particle-emitter/presets.js +15 -0
- package/dist/particle-emitter/presets.js.map +1 -0
- package/dist/particle-emitter.d.ts +78 -0
- package/dist/particle-emitter.js +15 -0
- package/dist/particle-emitter.js.map +1 -0
- package/dist/payload-BQzu-oY_.d.ts +89 -0
- package/dist/platformer-3d.d.ts +347 -0
- package/dist/platformer-3d.js +23 -0
- package/dist/platformer-3d.js.map +1 -0
- package/dist/ricochet-2d.d.ts +274 -0
- package/dist/ricochet-2d.js +16 -0
- package/dist/ricochet-2d.js.map +1 -0
- package/dist/ricochet-3d.d.ts +116 -0
- package/dist/ricochet-3d.js +16 -0
- package/dist/ricochet-3d.js.map +1 -0
- package/dist/runtime-2d.d.ts +2 -0
- package/dist/runtime-2d.js +32 -0
- package/dist/runtime-2d.js.map +1 -0
- package/dist/runtime-pong.d.ts +92 -0
- package/dist/runtime-pong.js +38 -0
- package/dist/runtime-pong.js.map +1 -0
- package/dist/screen-visibility.d.ts +74 -0
- package/dist/screen-visibility.js +10 -0
- package/dist/screen-visibility.js.map +1 -0
- package/dist/screen-wrap.d.ts +86 -0
- package/dist/screen-wrap.js +16 -0
- package/dist/screen-wrap.js.map +1 -0
- package/dist/shooter-2d.d.ts +77 -0
- package/dist/shooter-2d.js +12 -0
- package/dist/shooter-2d.js.map +1 -0
- package/dist/spark-DDlIg1nM.d.ts +148 -0
- package/dist/sync-state-machine-CZyspBpj.d.ts +16 -0
- package/dist/thruster.d.ts +132 -0
- package/dist/thruster.js +26 -0
- package/dist/thruster.js.map +1 -0
- package/dist/top-down-movement.d.ts +40 -0
- package/dist/top-down-movement.js +20 -0
- package/dist/top-down-movement.js.map +1 -0
- package/dist/use-behavior-D_82jAL3.d.ts +13 -0
- package/dist/vector-Bm65-FOG.d.ts +45 -0
- package/dist/world-boundary-2d.d.ts +141 -0
- package/dist/world-boundary-2d.js +21 -0
- package/dist/world-boundary-2d.js.map +1 -0
- package/dist/world-boundary-3d.d.ts +75 -0
- package/dist/world-boundary-3d.js +21 -0
- package/dist/world-boundary-3d.js.map +1 -0
- package/package.json +161 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineBehavior
|
|
3
|
+
} from "./chunk-EZNCCO4L.js";
|
|
4
|
+
|
|
5
|
+
// src/lib/behaviors/cooldown/cooldown-store.ts
|
|
6
|
+
import { proxy } from "valtio";
|
|
7
|
+
var cooldownStore = proxy({});
|
|
8
|
+
function registerCooldown(name, duration, immediate = true) {
|
|
9
|
+
cooldownStore[name] = {
|
|
10
|
+
name,
|
|
11
|
+
duration,
|
|
12
|
+
elapsed: immediate ? duration : 0,
|
|
13
|
+
ready: immediate,
|
|
14
|
+
progress: immediate ? 1 : 0,
|
|
15
|
+
active: !immediate
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function getCooldown(name) {
|
|
19
|
+
return cooldownStore[name];
|
|
20
|
+
}
|
|
21
|
+
function fireCooldown(name) {
|
|
22
|
+
const entry = cooldownStore[name];
|
|
23
|
+
if (!entry) return;
|
|
24
|
+
entry.elapsed = 0;
|
|
25
|
+
entry.ready = false;
|
|
26
|
+
entry.progress = 0;
|
|
27
|
+
entry.active = true;
|
|
28
|
+
}
|
|
29
|
+
function resetCooldown(name) {
|
|
30
|
+
const entry = cooldownStore[name];
|
|
31
|
+
if (!entry) return;
|
|
32
|
+
entry.elapsed = entry.duration;
|
|
33
|
+
entry.ready = true;
|
|
34
|
+
entry.progress = 1;
|
|
35
|
+
entry.active = false;
|
|
36
|
+
}
|
|
37
|
+
function tickCooldowns(delta) {
|
|
38
|
+
for (const name in cooldownStore) {
|
|
39
|
+
const entry = cooldownStore[name];
|
|
40
|
+
if (!entry.active) continue;
|
|
41
|
+
entry.elapsed += delta;
|
|
42
|
+
if (entry.elapsed >= entry.duration) {
|
|
43
|
+
entry.elapsed = entry.duration;
|
|
44
|
+
entry.ready = true;
|
|
45
|
+
entry.progress = 1;
|
|
46
|
+
entry.active = false;
|
|
47
|
+
} else {
|
|
48
|
+
entry.progress = entry.duration > 0 ? entry.elapsed / entry.duration : 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function getCooldownStore() {
|
|
53
|
+
return cooldownStore;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/lib/behaviors/cooldown/cooldown-behavior.ts
|
|
57
|
+
var COOLDOWN_BEHAVIOR_KEY = /* @__PURE__ */ Symbol.for("zylem:behavior:cooldown");
|
|
58
|
+
var CooldownSystem = class {
|
|
59
|
+
constructor(getBehaviorLinks) {
|
|
60
|
+
this.getBehaviorLinks = getBehaviorLinks;
|
|
61
|
+
}
|
|
62
|
+
getBehaviorLinks;
|
|
63
|
+
update(_ecs, delta) {
|
|
64
|
+
const links = this.getBehaviorLinks?.(COOLDOWN_BEHAVIOR_KEY);
|
|
65
|
+
if (links) {
|
|
66
|
+
for (const link of links) {
|
|
67
|
+
ensureCooldownEntriesRegistered(link.ref);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
tickCooldowns(delta);
|
|
71
|
+
}
|
|
72
|
+
destroy(_ecs) {
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
function createCooldownHandle(ref) {
|
|
76
|
+
ensureCooldownEntriesRegistered(ref);
|
|
77
|
+
return {
|
|
78
|
+
isReady: (name) => getCooldown(name)?.ready ?? false,
|
|
79
|
+
fire: (name) => fireCooldown(name),
|
|
80
|
+
reset: (name) => resetCooldown(name),
|
|
81
|
+
getProgress: (name) => getCooldown(name)?.progress ?? 1
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function getCompiledCooldownEntries(ref) {
|
|
85
|
+
const compiled = ref.__compiledCooldownEntries;
|
|
86
|
+
if (compiled) return compiled;
|
|
87
|
+
const entries = Object.entries(ref.options.cooldowns).map(([name, config]) => ({
|
|
88
|
+
name,
|
|
89
|
+
duration: config.duration,
|
|
90
|
+
immediate: config.immediate ?? true
|
|
91
|
+
}));
|
|
92
|
+
ref.__compiledCooldownEntries = entries;
|
|
93
|
+
return entries;
|
|
94
|
+
}
|
|
95
|
+
function ensureCooldownEntriesRegistered(ref) {
|
|
96
|
+
if (ref.__cooldownsRegistered) return;
|
|
97
|
+
for (const entry of getCompiledCooldownEntries(ref)) {
|
|
98
|
+
registerCooldown(entry.name, entry.duration, entry.immediate);
|
|
99
|
+
}
|
|
100
|
+
ref.__cooldownsRegistered = true;
|
|
101
|
+
}
|
|
102
|
+
var CooldownBehavior = defineBehavior({
|
|
103
|
+
name: "cooldown",
|
|
104
|
+
defaultOptions: { cooldowns: {} },
|
|
105
|
+
systemFactory: (ctx) => new CooldownSystem(ctx.getBehaviorLinks),
|
|
106
|
+
createHandle: createCooldownHandle
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
export {
|
|
110
|
+
registerCooldown,
|
|
111
|
+
getCooldown,
|
|
112
|
+
fireCooldown,
|
|
113
|
+
resetCooldown,
|
|
114
|
+
tickCooldowns,
|
|
115
|
+
getCooldownStore,
|
|
116
|
+
CooldownBehavior
|
|
117
|
+
};
|
|
118
|
+
//# sourceMappingURL=chunk-K3UNLQ7W.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/behaviors/cooldown/cooldown-store.ts","../src/lib/behaviors/cooldown/cooldown-behavior.ts"],"sourcesContent":["import { proxy } from 'valtio';\n\n/**\n * A single cooldown entry in the global store.\n */\nexport interface CooldownEntry {\n\t/** Identifier for this cooldown */\n\tname: string;\n\t/** Total cooldown time in seconds */\n\tduration: number;\n\t/** Time accumulated since last fire */\n\telapsed: number;\n\t/** Whether the cooldown is ready (elapsed >= duration) */\n\tready: boolean;\n\t/** Normalized progress 0..1 (0 = just fired, 1 = ready) */\n\tprogress: number;\n\t/** Whether this cooldown is currently counting down */\n\tactive: boolean;\n}\n\n/**\n * Global reactive cooldown registry (valtio proxy).\n * Accessible from entities, behaviors, and UI components.\n */\nconst cooldownStore = proxy<Record<string, CooldownEntry>>({});\n\n/**\n * Register a named cooldown in the global store.\n * If a cooldown with the same name already exists, it is overwritten.\n *\n * @param name Unique cooldown identifier\n * @param duration Cooldown duration in seconds\n * @param immediate If true, the cooldown starts ready (default: true)\n */\nexport function registerCooldown(\n\tname: string,\n\tduration: number,\n\timmediate: boolean = true,\n): void {\n\tcooldownStore[name] = {\n\t\tname,\n\t\tduration,\n\t\telapsed: immediate ? duration : 0,\n\t\tready: immediate,\n\t\tprogress: immediate ? 1 : 0,\n\t\tactive: !immediate,\n\t};\n}\n\n/**\n * Get a cooldown entry by name.\n * Returns undefined if the cooldown has not been registered.\n */\nexport function getCooldown(name: string): CooldownEntry | undefined {\n\treturn cooldownStore[name];\n}\n\n/**\n * Fire (trigger) a cooldown, resetting its timer.\n * Sets the cooldown to not-ready and starts the countdown.\n */\nexport function fireCooldown(name: string): void {\n\tconst entry = cooldownStore[name];\n\tif (!entry) return;\n\tentry.elapsed = 0;\n\tentry.ready = false;\n\tentry.progress = 0;\n\tentry.active = true;\n}\n\n/**\n * Force a cooldown to ready immediately.\n */\nexport function resetCooldown(name: string): void {\n\tconst entry = cooldownStore[name];\n\tif (!entry) return;\n\tentry.elapsed = entry.duration;\n\tentry.ready = true;\n\tentry.progress = 1;\n\tentry.active = false;\n}\n\n/**\n * Advance all active cooldowns by delta seconds.\n * Called once per frame by the CooldownBehavior system.\n */\nexport function tickCooldowns(delta: number): void {\n\tfor (const name in cooldownStore) {\n\t\tconst entry = cooldownStore[name];\n\t\tif (!entry.active) continue;\n\n\t\tentry.elapsed += delta;\n\t\tif (entry.elapsed >= entry.duration) {\n\t\t\tentry.elapsed = entry.duration;\n\t\t\tentry.ready = true;\n\t\t\tentry.progress = 1;\n\t\t\tentry.active = false;\n\t\t} else {\n\t\t\tentry.progress = entry.duration > 0 ? entry.elapsed / entry.duration : 1;\n\t\t}\n\t}\n}\n\n/**\n * Get the raw cooldown store (for advanced/debugging use).\n */\nexport function getCooldownStore(): Record<string, CooldownEntry> {\n\treturn cooldownStore;\n}\n","/**\n * CooldownBehavior\n *\n * A stage-scoped behavior that manages named cooldowns via the global CooldownStore.\n * Entities use this to register, check, and fire cooldowns.\n * UI components (CooldownIcon) can read from the same store.\n */\n\nimport { defineBehavior, type BehaviorRef } from '../behavior-descriptor';\nimport type { BehaviorEntityLink, BehaviorSystem } from '../behavior-system';\nimport {\n\tregisterCooldown,\n\tgetCooldown,\n\tfireCooldown,\n\tresetCooldown,\n\ttickCooldowns,\n} from './cooldown-store';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Options\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface CooldownConfig {\n\t/** Duration in seconds */\n\tduration: number;\n\t/** Whether the cooldown starts ready (default: true) */\n\timmediate?: boolean;\n}\n\nexport interface CooldownOptions {\n\t/**\n\t * Named cooldowns to register.\n\t * Keys are the cooldown names, values are their config.\n\t */\n\tcooldowns: Record<string, CooldownConfig>;\n}\n\ninterface CompiledCooldownEntry {\n\tname: string;\n\tduration: number;\n\timmediate: boolean;\n}\n\nconst COOLDOWN_BEHAVIOR_KEY = Symbol.for('zylem:behavior:cooldown');\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Handle\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Handle methods returned by entity.use(CooldownBehavior, ...).\n */\nexport interface CooldownHandle {\n\t/** Check if a named cooldown is ready */\n\tisReady(name: string): boolean;\n\t/** Fire (trigger) a named cooldown, resetting its timer */\n\tfire(name: string): void;\n\t/** Force a cooldown to ready immediately */\n\treset(name: string): void;\n\t/** Get cooldown progress 0..1 (0 = just fired, 1 = ready) */\n\tgetProgress(name: string): number;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// System\n// ─────────────────────────────────────────────────────────────────────────────\n\nclass CooldownSystem implements BehaviorSystem {\n\tconstructor(\n\t\tprivate getBehaviorLinks?: (key: symbol) => Iterable<BehaviorEntityLink>,\n\t) { }\n\n\tupdate(_ecs: unknown, delta: number): void {\n\t\tconst links = this.getBehaviorLinks?.(COOLDOWN_BEHAVIOR_KEY);\n\t\tif (links) {\n\t\t\tfor (const link of links) {\n\t\t\t\tensureCooldownEntriesRegistered(link.ref as BehaviorRef<CooldownOptions>);\n\t\t\t}\n\t\t}\n\n\t\ttickCooldowns(delta);\n\t}\n\n\tdestroy(_ecs: unknown): void { }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Handle factory\n// ─────────────────────────────────────────────────────────────────────────────\n\nfunction createCooldownHandle(ref: BehaviorRef<CooldownOptions>): CooldownHandle {\n\tensureCooldownEntriesRegistered(ref);\n\n\treturn {\n\t\tisReady: (name: string) => getCooldown(name)?.ready ?? false,\n\t\tfire: (name: string) => fireCooldown(name),\n\t\treset: (name: string) => resetCooldown(name),\n\t\tgetProgress: (name: string) => getCooldown(name)?.progress ?? 1,\n\t};\n}\n\nfunction getCompiledCooldownEntries(\n\tref: BehaviorRef<CooldownOptions>,\n): CompiledCooldownEntry[] {\n\tconst compiled = (ref as any).__compiledCooldownEntries as CompiledCooldownEntry[] | undefined;\n\tif (compiled) return compiled;\n\n\tconst entries = Object.entries(ref.options.cooldowns).map(([name, config]) => ({\n\t\tname,\n\t\tduration: config.duration,\n\t\timmediate: config.immediate ?? true,\n\t}));\n\t(ref as any).__compiledCooldownEntries = entries;\n\treturn entries;\n}\n\nfunction ensureCooldownEntriesRegistered(ref: BehaviorRef<CooldownOptions>): void {\n\tif ((ref as any).__cooldownsRegistered) return;\n\n\tfor (const entry of getCompiledCooldownEntries(ref)) {\n\t\tregisterCooldown(entry.name, entry.duration, entry.immediate);\n\t}\n\n\t(ref as any).__cooldownsRegistered = true;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Behavior Definition\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * CooldownBehavior\n *\n * @example\n * ```ts\n * import { CooldownBehavior } from '@zylem/game-lib/behavior';\n * import { moveBy } from '@zylem/game-lib/actions';\n *\n * const hero = createBox({ ... });\n * const cd = hero.use(CooldownBehavior, {\n * cooldowns: {\n * attack: { duration: 5 },\n * dash: { duration: 2, immediate: false },\n * },\n * });\n *\n * hero.onUpdate(({ me, inputs }) => {\n * if (cd.isReady('attack') && inputs.p1.buttons.A.pressed) {\n * cd.fire('attack');\n * // perform attack\n * }\n * if (cd.isReady('dash') && inputs.p1.buttons.B.pressed) {\n * cd.fire('dash');\n * me.runAction(moveBy({ x: 10, duration: 0.3 }));\n * }\n * });\n * ```\n */\nexport const CooldownBehavior = defineBehavior<CooldownOptions, CooldownHandle>({\n\tname: 'cooldown',\n\tdefaultOptions: { cooldowns: {} },\n\tsystemFactory: (ctx) =>\n\t\tnew CooldownSystem(ctx.getBehaviorLinks),\n\tcreateHandle: createCooldownHandle,\n});\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// src/lib/behaviors/shared/bounds-2d.ts
|
|
2
|
+
function createBounds2DRect({
|
|
3
|
+
width,
|
|
4
|
+
height,
|
|
5
|
+
centerX = 0,
|
|
6
|
+
centerY = 0
|
|
7
|
+
}) {
|
|
8
|
+
const halfWidth = width / 2;
|
|
9
|
+
const halfHeight = height / 2;
|
|
10
|
+
return {
|
|
11
|
+
minX: centerX - halfWidth,
|
|
12
|
+
maxX: centerX + halfWidth,
|
|
13
|
+
minY: centerY - halfHeight,
|
|
14
|
+
maxY: centerY + halfHeight
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function computeBounds2DHits(position, bounds) {
|
|
18
|
+
const hits = {
|
|
19
|
+
top: false,
|
|
20
|
+
bottom: false,
|
|
21
|
+
left: false,
|
|
22
|
+
right: false
|
|
23
|
+
};
|
|
24
|
+
if (position.x <= bounds.minX) hits.left = true;
|
|
25
|
+
else if (position.x >= bounds.maxX) hits.right = true;
|
|
26
|
+
if (position.y <= bounds.minY) hits.bottom = true;
|
|
27
|
+
else if (position.y >= bounds.maxY) hits.top = true;
|
|
28
|
+
return hits;
|
|
29
|
+
}
|
|
30
|
+
function hasAnyBounds2DHit(hits) {
|
|
31
|
+
return hits.left || hits.right || hits.top || hits.bottom;
|
|
32
|
+
}
|
|
33
|
+
function constrainMovementToBounds2D(hits, moveX, moveY) {
|
|
34
|
+
let adjustedX = moveX;
|
|
35
|
+
let adjustedY = moveY;
|
|
36
|
+
if (hits.left && moveX < 0 || hits.right && moveX > 0) {
|
|
37
|
+
adjustedX = 0;
|
|
38
|
+
}
|
|
39
|
+
if (hits.bottom && moveY < 0 || hits.top && moveY > 0) {
|
|
40
|
+
adjustedY = 0;
|
|
41
|
+
}
|
|
42
|
+
return { moveX: adjustedX, moveY: adjustedY };
|
|
43
|
+
}
|
|
44
|
+
function wrapPoint2D(position, bounds) {
|
|
45
|
+
let x = position.x;
|
|
46
|
+
let y = position.y;
|
|
47
|
+
let wrapped = false;
|
|
48
|
+
if (position.x < bounds.minX) {
|
|
49
|
+
x = bounds.maxX - (bounds.minX - position.x);
|
|
50
|
+
wrapped = true;
|
|
51
|
+
} else if (position.x > bounds.maxX) {
|
|
52
|
+
x = bounds.minX + (position.x - bounds.maxX);
|
|
53
|
+
wrapped = true;
|
|
54
|
+
}
|
|
55
|
+
if (position.y < bounds.minY) {
|
|
56
|
+
y = bounds.maxY - (bounds.minY - position.y);
|
|
57
|
+
wrapped = true;
|
|
58
|
+
} else if (position.y > bounds.maxY) {
|
|
59
|
+
y = bounds.minY + (position.y - bounds.maxY);
|
|
60
|
+
wrapped = true;
|
|
61
|
+
}
|
|
62
|
+
return { x, y, wrapped };
|
|
63
|
+
}
|
|
64
|
+
function getBounds2DNormalFromHits(hits) {
|
|
65
|
+
let x = 0;
|
|
66
|
+
let y = 0;
|
|
67
|
+
if (hits.left) x = 1;
|
|
68
|
+
if (hits.right) x = -1;
|
|
69
|
+
if (hits.bottom) y = 1;
|
|
70
|
+
if (hits.top) y = -1;
|
|
71
|
+
return { x, y };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
createBounds2DRect,
|
|
76
|
+
computeBounds2DHits,
|
|
77
|
+
hasAnyBounds2DHit,
|
|
78
|
+
constrainMovementToBounds2D,
|
|
79
|
+
wrapPoint2D,
|
|
80
|
+
getBounds2DNormalFromHits
|
|
81
|
+
};
|
|
82
|
+
//# sourceMappingURL=chunk-KDY5N6J4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/behaviors/shared/bounds-2d.ts"],"sourcesContent":["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"],"mappings":";AAgCO,SAAS,mBAAmB;AAAA,EAClC;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,UAAU;AACX,GAA+B;AAC9B,QAAM,YAAY,QAAQ;AAC1B,QAAM,aAAa,SAAS;AAC5B,SAAO;AAAA,IACN,MAAM,UAAU;AAAA,IAChB,MAAM,UAAU;AAAA,IAChB,MAAM,UAAU;AAAA,IAChB,MAAM,UAAU;AAAA,EACjB;AACD;AAEO,SAAS,oBACf,UACA,QACe;AACf,QAAM,OAAqB;AAAA,IAC1B,KAAK;AAAA,IACL,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,EACR;AAEA,MAAI,SAAS,KAAK,OAAO,KAAM,MAAK,OAAO;AAAA,WAClC,SAAS,KAAK,OAAO,KAAM,MAAK,QAAQ;AAEjD,MAAI,SAAS,KAAK,OAAO,KAAM,MAAK,SAAS;AAAA,WACpC,SAAS,KAAK,OAAO,KAAM,MAAK,MAAM;AAE/C,SAAO;AACR;AAEO,SAAS,kBAAkB,MAA6B;AAC9D,SAAO,KAAK,QAAQ,KAAK,SAAS,KAAK,OAAO,KAAK;AACpD;AAEO,SAAS,4BACf,MACA,OACA,OACmC;AACnC,MAAI,YAAY;AAChB,MAAI,YAAY;AAEhB,MAAK,KAAK,QAAQ,QAAQ,KAAO,KAAK,SAAS,QAAQ,GAAI;AAC1D,gBAAY;AAAA,EACb;AAEA,MAAK,KAAK,UAAU,QAAQ,KAAO,KAAK,OAAO,QAAQ,GAAI;AAC1D,gBAAY;AAAA,EACb;AAEA,SAAO,EAAE,OAAO,WAAW,OAAO,UAAU;AAC7C;AAEO,SAAS,YACf,UACA,QACqB;AACrB,MAAI,IAAI,SAAS;AACjB,MAAI,IAAI,SAAS;AACjB,MAAI,UAAU;AAEd,MAAI,SAAS,IAAI,OAAO,MAAM;AAC7B,QAAI,OAAO,QAAQ,OAAO,OAAO,SAAS;AAC1C,cAAU;AAAA,EACX,WAAW,SAAS,IAAI,OAAO,MAAM;AACpC,QAAI,OAAO,QAAQ,SAAS,IAAI,OAAO;AACvC,cAAU;AAAA,EACX;AAEA,MAAI,SAAS,IAAI,OAAO,MAAM;AAC7B,QAAI,OAAO,QAAQ,OAAO,OAAO,SAAS;AAC1C,cAAU;AAAA,EACX,WAAW,SAAS,IAAI,OAAO,MAAM;AACpC,QAAI,OAAO,QAAQ,SAAS,IAAI,OAAO;AACvC,cAAU;AAAA,EACX;AAEA,SAAO,EAAE,GAAG,GAAG,QAAQ;AACxB;AAEO,SAAS,0BACf,MAC2B;AAC3B,MAAI,IAAI;AACR,MAAI,IAAI;AAER,MAAI,KAAK,KAAM,KAAI;AACnB,MAAI,KAAK,MAAO,KAAI;AACpB,MAAI,KAAK,OAAQ,KAAI;AACrB,MAAI,KAAK,IAAK,KAAI;AAElB,SAAO,EAAE,GAAG,EAAE;AACf;","names":[]}
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import {
|
|
2
|
+
StageRicochetDim,
|
|
3
|
+
StageRicochetReflection
|
|
4
|
+
} from "./chunk-Y6UJOUE5.js";
|
|
5
|
+
import {
|
|
6
|
+
SyncStateMachine,
|
|
7
|
+
t
|
|
8
|
+
} from "./chunk-4URF65BR.js";
|
|
9
|
+
import {
|
|
10
|
+
defineBehavior
|
|
11
|
+
} from "./chunk-EZNCCO4L.js";
|
|
12
|
+
|
|
13
|
+
// src/lib/behaviors/ricochet-2d/ricochet-2d-fsm.ts
|
|
14
|
+
var Ricochet2DState = /* @__PURE__ */ ((Ricochet2DState2) => {
|
|
15
|
+
Ricochet2DState2["Idle"] = "idle";
|
|
16
|
+
Ricochet2DState2["Ricocheting"] = "ricocheting";
|
|
17
|
+
return Ricochet2DState2;
|
|
18
|
+
})(Ricochet2DState || {});
|
|
19
|
+
var Ricochet2DEvent = /* @__PURE__ */ ((Ricochet2DEvent2) => {
|
|
20
|
+
Ricochet2DEvent2["StartRicochet"] = "start-ricochet";
|
|
21
|
+
Ricochet2DEvent2["EndRicochet"] = "end-ricochet";
|
|
22
|
+
return Ricochet2DEvent2;
|
|
23
|
+
})(Ricochet2DEvent || {});
|
|
24
|
+
function clamp(value, min, max) {
|
|
25
|
+
return Math.max(min, Math.min(max, value));
|
|
26
|
+
}
|
|
27
|
+
var Ricochet2DFSM = class {
|
|
28
|
+
machine;
|
|
29
|
+
lastResult = null;
|
|
30
|
+
lastUpdatedAtMs = null;
|
|
31
|
+
currentTimeMs = 0;
|
|
32
|
+
listeners = /* @__PURE__ */ new Set();
|
|
33
|
+
constructor() {
|
|
34
|
+
this.machine = new SyncStateMachine(
|
|
35
|
+
"idle" /* Idle */,
|
|
36
|
+
[
|
|
37
|
+
t("idle" /* Idle */, "start-ricochet" /* StartRicochet */, "ricocheting" /* Ricocheting */),
|
|
38
|
+
t("ricocheting" /* Ricocheting */, "end-ricochet" /* EndRicochet */, "idle" /* Idle */),
|
|
39
|
+
// Self transitions (no-ops)
|
|
40
|
+
t("idle" /* Idle */, "end-ricochet" /* EndRicochet */, "idle" /* Idle */),
|
|
41
|
+
t("ricocheting" /* Ricocheting */, "start-ricochet" /* StartRicochet */, "ricocheting" /* Ricocheting */)
|
|
42
|
+
]
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Add a listener for ricochet events.
|
|
47
|
+
* @returns Unsubscribe function
|
|
48
|
+
*/
|
|
49
|
+
addListener(callback) {
|
|
50
|
+
this.listeners.add(callback);
|
|
51
|
+
return () => this.listeners.delete(callback);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Emit result to all listeners.
|
|
55
|
+
*/
|
|
56
|
+
emitToListeners(result) {
|
|
57
|
+
for (const callback of this.listeners) {
|
|
58
|
+
try {
|
|
59
|
+
callback(result);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.error("[Ricochet2DFSM] Listener error:", e);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
getState() {
|
|
66
|
+
return this.machine.getState();
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Returns the last computed ricochet result, or null if none.
|
|
70
|
+
*/
|
|
71
|
+
getLastResult() {
|
|
72
|
+
return this.lastResult;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Best-effort timestamp (ms) of the last computation.
|
|
76
|
+
*/
|
|
77
|
+
getLastUpdatedAtMs() {
|
|
78
|
+
return this.lastUpdatedAtMs;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Set current game time (called by system each frame).
|
|
82
|
+
* Used for cooldown calculations.
|
|
83
|
+
*/
|
|
84
|
+
setCurrentTimeMs(timeMs) {
|
|
85
|
+
this.currentTimeMs = timeMs;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Check if ricochet is on cooldown (to prevent rapid duplicate applications).
|
|
89
|
+
* @param cooldownMs Cooldown duration in milliseconds (default: 50ms)
|
|
90
|
+
*/
|
|
91
|
+
isOnCooldown(cooldownMs = 50) {
|
|
92
|
+
if (this.lastUpdatedAtMs === null) return false;
|
|
93
|
+
return this.currentTimeMs - this.lastUpdatedAtMs < cooldownMs;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Reset cooldown state (e.g., on entity respawn).
|
|
97
|
+
*/
|
|
98
|
+
resetCooldown() {
|
|
99
|
+
this.lastUpdatedAtMs = null;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Compute a ricochet result from collision context.
|
|
103
|
+
* Returns the result for the consumer to apply, or null if invalid input.
|
|
104
|
+
*/
|
|
105
|
+
computeRicochet(ctx, options = {}) {
|
|
106
|
+
const {
|
|
107
|
+
minSpeed = 2,
|
|
108
|
+
maxSpeed = 20,
|
|
109
|
+
speedMultiplier = 1.05,
|
|
110
|
+
reflectionMode = "angled",
|
|
111
|
+
maxAngleDeg = 60
|
|
112
|
+
} = options;
|
|
113
|
+
const { selfVelocity, selfPosition, otherPosition, otherSize } = this.extractDataFromEntities(ctx);
|
|
114
|
+
if (!selfVelocity) {
|
|
115
|
+
this.dispatch("end-ricochet" /* EndRicochet */);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const speed = Math.hypot(selfVelocity.x, selfVelocity.y);
|
|
119
|
+
if (speed === 0) {
|
|
120
|
+
this.dispatch("end-ricochet" /* EndRicochet */);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
const normal = ctx.contact.normal ?? this.computeNormalFromPositions(selfPosition, otherPosition);
|
|
124
|
+
if (!normal) {
|
|
125
|
+
this.dispatch("end-ricochet" /* EndRicochet */);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
let reflected = this.computeBasicReflection(selfVelocity, normal);
|
|
129
|
+
if (reflectionMode === "angled") {
|
|
130
|
+
reflected = this.computeAngledDeflection(
|
|
131
|
+
selfVelocity,
|
|
132
|
+
normal,
|
|
133
|
+
speed,
|
|
134
|
+
maxAngleDeg,
|
|
135
|
+
speedMultiplier,
|
|
136
|
+
selfPosition,
|
|
137
|
+
otherPosition,
|
|
138
|
+
otherSize,
|
|
139
|
+
ctx.contact.position
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
reflected = this.applySpeedClamp(reflected, minSpeed, maxSpeed);
|
|
143
|
+
const result = {
|
|
144
|
+
velocity: { x: reflected.x, y: reflected.y, z: 0 },
|
|
145
|
+
speed: Math.hypot(reflected.x, reflected.y),
|
|
146
|
+
normal: { x: normal.x, y: normal.y, z: 0 }
|
|
147
|
+
};
|
|
148
|
+
this.lastResult = result;
|
|
149
|
+
this.lastUpdatedAtMs = this.currentTimeMs;
|
|
150
|
+
this.dispatch("start-ricochet" /* StartRicochet */);
|
|
151
|
+
this.emitToListeners(result);
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Extract velocity, position, and size data from entities or context.
|
|
156
|
+
*/
|
|
157
|
+
extractDataFromEntities(ctx) {
|
|
158
|
+
let selfVelocity = ctx.selfVelocity;
|
|
159
|
+
let selfPosition = ctx.selfPosition;
|
|
160
|
+
let otherPosition = ctx.otherPosition;
|
|
161
|
+
let otherSize = ctx.otherSize;
|
|
162
|
+
if (ctx.entity?.body) {
|
|
163
|
+
const vel = ctx.entity.body.linvel();
|
|
164
|
+
selfVelocity = selfVelocity ?? { x: vel.x, y: vel.y, z: vel.z };
|
|
165
|
+
const pos = ctx.entity.body.translation();
|
|
166
|
+
selfPosition = selfPosition ?? { x: pos.x, y: pos.y, z: pos.z };
|
|
167
|
+
}
|
|
168
|
+
if (ctx.otherEntity?.body) {
|
|
169
|
+
const pos = ctx.otherEntity.body.translation();
|
|
170
|
+
otherPosition = otherPosition ?? { x: pos.x, y: pos.y, z: pos.z };
|
|
171
|
+
}
|
|
172
|
+
if (ctx.otherEntity && "size" in ctx.otherEntity) {
|
|
173
|
+
const size = ctx.otherEntity.size;
|
|
174
|
+
if (size && typeof size.x === "number") {
|
|
175
|
+
otherSize = otherSize ?? { x: size.x, y: size.y, z: size.z };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { selfVelocity, selfPosition, otherPosition, otherSize };
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Compute collision normal from entity positions using AABB heuristic.
|
|
182
|
+
*/
|
|
183
|
+
computeNormalFromPositions(selfPosition, otherPosition) {
|
|
184
|
+
if (!selfPosition || !otherPosition) return null;
|
|
185
|
+
const dx = selfPosition.x - otherPosition.x;
|
|
186
|
+
const dy = selfPosition.y - otherPosition.y;
|
|
187
|
+
if (Math.abs(dx) > Math.abs(dy)) {
|
|
188
|
+
return { x: dx > 0 ? 1 : -1, y: 0, z: 0 };
|
|
189
|
+
} else {
|
|
190
|
+
return { x: 0, y: dy > 0 ? 1 : -1, z: 0 };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Compute basic reflection using the formula: v' = v - 2(v·n)n
|
|
195
|
+
*/
|
|
196
|
+
computeBasicReflection(velocity, normal) {
|
|
197
|
+
const vx = velocity.x;
|
|
198
|
+
const vy = velocity.y;
|
|
199
|
+
const dotProduct = vx * normal.x + vy * normal.y;
|
|
200
|
+
return {
|
|
201
|
+
x: vx - 2 * dotProduct * normal.x,
|
|
202
|
+
y: vy - 2 * dotProduct * normal.y
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Compute angled deflection for paddle-style reflections.
|
|
207
|
+
*/
|
|
208
|
+
computeAngledDeflection(velocity, normal, speed, maxAngleDeg, speedMultiplier, selfPosition, otherPosition, otherSize, contactPosition) {
|
|
209
|
+
const maxAngleRad = maxAngleDeg * Math.PI / 180;
|
|
210
|
+
let tx = -normal.y;
|
|
211
|
+
let ty = normal.x;
|
|
212
|
+
if (Math.abs(normal.x) > Math.abs(normal.y)) {
|
|
213
|
+
if (ty < 0) {
|
|
214
|
+
tx = -tx;
|
|
215
|
+
ty = -ty;
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
if (tx < 0) {
|
|
219
|
+
tx = -tx;
|
|
220
|
+
ty = -ty;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const offset = this.computeHitOffset(
|
|
224
|
+
velocity,
|
|
225
|
+
normal,
|
|
226
|
+
speed,
|
|
227
|
+
tx,
|
|
228
|
+
ty,
|
|
229
|
+
selfPosition,
|
|
230
|
+
otherPosition,
|
|
231
|
+
otherSize,
|
|
232
|
+
contactPosition
|
|
233
|
+
);
|
|
234
|
+
const angle = clamp(offset, -1, 1) * maxAngleRad;
|
|
235
|
+
const cosA = Math.cos(angle);
|
|
236
|
+
const sinA = Math.sin(angle);
|
|
237
|
+
const newSpeed = speed * speedMultiplier;
|
|
238
|
+
return {
|
|
239
|
+
x: newSpeed * (normal.x * cosA + tx * sinA),
|
|
240
|
+
y: newSpeed * (normal.y * cosA + ty * sinA)
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Compute hit offset for angled deflection (-1 to 1).
|
|
245
|
+
*/
|
|
246
|
+
computeHitOffset(velocity, normal, speed, tx, ty, selfPosition, otherPosition, otherSize, contactPosition) {
|
|
247
|
+
if (otherPosition && otherSize) {
|
|
248
|
+
const useY = Math.abs(normal.x) > Math.abs(normal.y);
|
|
249
|
+
const halfExtent = useY ? otherSize.y / 2 : otherSize.x / 2;
|
|
250
|
+
if (useY) {
|
|
251
|
+
const selfY = selfPosition?.y ?? contactPosition?.y ?? 0;
|
|
252
|
+
const paddleY = otherPosition.y;
|
|
253
|
+
return (selfY - paddleY) / halfExtent;
|
|
254
|
+
} else {
|
|
255
|
+
const selfX = selfPosition?.x ?? contactPosition?.x ?? 0;
|
|
256
|
+
const paddleX = otherPosition.x;
|
|
257
|
+
return (selfX - paddleX) / halfExtent;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return (velocity.x * tx + velocity.y * ty) / speed;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Apply speed constraints to the reflected velocity.
|
|
264
|
+
*/
|
|
265
|
+
applySpeedClamp(velocity, minSpeed, maxSpeed) {
|
|
266
|
+
const currentSpeed = Math.hypot(velocity.x, velocity.y);
|
|
267
|
+
if (currentSpeed === 0) return velocity;
|
|
268
|
+
const targetSpeed = clamp(currentSpeed, minSpeed, maxSpeed);
|
|
269
|
+
const scale = targetSpeed / currentSpeed;
|
|
270
|
+
return {
|
|
271
|
+
x: velocity.x * scale,
|
|
272
|
+
y: velocity.y * scale
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Clear the ricochet state (call after consumer has applied the result).
|
|
277
|
+
*/
|
|
278
|
+
clearRicochet() {
|
|
279
|
+
this.dispatch("end-ricochet" /* EndRicochet */);
|
|
280
|
+
}
|
|
281
|
+
dispatch(event) {
|
|
282
|
+
if (this.machine.can(event)) {
|
|
283
|
+
this.machine.syncDispatch(event);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// src/lib/behaviors/ricochet-2d/ricochet-2d.descriptor.ts
|
|
289
|
+
var defaultOptions = {
|
|
290
|
+
minSpeed: 2,
|
|
291
|
+
maxSpeed: 20,
|
|
292
|
+
speedMultiplier: 1.05,
|
|
293
|
+
reflectionMode: "angled",
|
|
294
|
+
maxAngleDeg: 60
|
|
295
|
+
};
|
|
296
|
+
var RICOCHET_BEHAVIOR_KEY = /* @__PURE__ */ Symbol.for("zylem:behavior:ricochet-2d");
|
|
297
|
+
function createRicochet2DHandle(ref) {
|
|
298
|
+
return {
|
|
299
|
+
getRicochet: (ctx) => {
|
|
300
|
+
const fsm = ref.fsm;
|
|
301
|
+
if (!fsm) return null;
|
|
302
|
+
return fsm.computeRicochet(ctx, ref.options);
|
|
303
|
+
},
|
|
304
|
+
applyRicochet: (ctx) => {
|
|
305
|
+
const fsm = ref.fsm;
|
|
306
|
+
if (!fsm) return false;
|
|
307
|
+
if (fsm.isOnCooldown()) return false;
|
|
308
|
+
const result = fsm.computeRicochet(ctx, ref.options);
|
|
309
|
+
if (!result) return false;
|
|
310
|
+
const entity = ctx.entity;
|
|
311
|
+
if (entity?.transformStore) {
|
|
312
|
+
entity.transformStore.velocity.x = result.velocity.x;
|
|
313
|
+
entity.transformStore.velocity.y = result.velocity.y;
|
|
314
|
+
entity.transformStore.velocity.z = result.velocity.z ?? 0;
|
|
315
|
+
entity.transformStore.dirty.velocity = true;
|
|
316
|
+
}
|
|
317
|
+
return true;
|
|
318
|
+
},
|
|
319
|
+
getLastResult: () => {
|
|
320
|
+
const fsm = ref.fsm;
|
|
321
|
+
return fsm?.getLastResult() ?? null;
|
|
322
|
+
},
|
|
323
|
+
onRicochet: (callback) => {
|
|
324
|
+
const fsm = ref.fsm;
|
|
325
|
+
if (!fsm) {
|
|
326
|
+
if (!ref.pendingListeners) {
|
|
327
|
+
ref.pendingListeners = [];
|
|
328
|
+
}
|
|
329
|
+
ref.pendingListeners.push(callback);
|
|
330
|
+
return () => {
|
|
331
|
+
const pending = ref.pendingListeners;
|
|
332
|
+
const idx = pending.indexOf(callback);
|
|
333
|
+
if (idx >= 0) pending.splice(idx, 1);
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
return fsm.addListener(callback);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
var Ricochet2DSystem = class {
|
|
341
|
+
constructor(world, wasmStage, getBehaviorLinks) {
|
|
342
|
+
this.world = world;
|
|
343
|
+
this.wasmStage = wasmStage;
|
|
344
|
+
this.getBehaviorLinks = getBehaviorLinks;
|
|
345
|
+
}
|
|
346
|
+
world;
|
|
347
|
+
wasmStage;
|
|
348
|
+
getBehaviorLinks;
|
|
349
|
+
elapsedMs = 0;
|
|
350
|
+
attachedRuntimeSlots = /* @__PURE__ */ new Set();
|
|
351
|
+
update(_ecs, delta) {
|
|
352
|
+
this.elapsedMs += delta * 1e3;
|
|
353
|
+
const links = this.getBehaviorLinks?.(RICOCHET_BEHAVIOR_KEY);
|
|
354
|
+
if (!links) return;
|
|
355
|
+
for (const link of links) {
|
|
356
|
+
const ricochetRef = link.ref;
|
|
357
|
+
const gameEntity = link.entity;
|
|
358
|
+
if (!ricochetRef.fsm) {
|
|
359
|
+
ricochetRef.fsm = new Ricochet2DFSM();
|
|
360
|
+
const pending = ricochetRef.pendingListeners;
|
|
361
|
+
if (pending) {
|
|
362
|
+
for (const cb of pending) {
|
|
363
|
+
ricochetRef.fsm.addListener(cb);
|
|
364
|
+
}
|
|
365
|
+
ricochetRef.pendingListeners = [];
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
ricochetRef.fsm.setCurrentTimeMs(this.elapsedMs);
|
|
369
|
+
const handle = gameEntity?.runtimeHandle ?? -1;
|
|
370
|
+
if (this.wasmStage && handle >= 0) {
|
|
371
|
+
this.ensureWasmAttached(handle, ricochetRef.options);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
ensureWasmAttached(handle, options) {
|
|
376
|
+
if (!this.wasmStage || this.attachedRuntimeSlots.has(handle)) return;
|
|
377
|
+
const reflectionMode = options.reflectionMode === "simple" ? StageRicochetReflection.Mirror : StageRicochetReflection.Angled;
|
|
378
|
+
this.wasmStage.attachRicochet(handle, StageRicochetDim.Two, {
|
|
379
|
+
minSpeed: options.minSpeed,
|
|
380
|
+
maxSpeed: options.maxSpeed,
|
|
381
|
+
speedMultiplier: options.speedMultiplier,
|
|
382
|
+
maxAngleDeg: options.maxAngleDeg,
|
|
383
|
+
reflectionMode
|
|
384
|
+
});
|
|
385
|
+
this.attachedRuntimeSlots.add(handle);
|
|
386
|
+
}
|
|
387
|
+
destroy(_ecs) {
|
|
388
|
+
const links = this.getBehaviorLinks?.(RICOCHET_BEHAVIOR_KEY);
|
|
389
|
+
if (links) {
|
|
390
|
+
for (const link of links) {
|
|
391
|
+
const ricochetRef = link.ref;
|
|
392
|
+
if (ricochetRef?.fsm) {
|
|
393
|
+
ricochetRef.fsm.resetCooldown();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
this.attachedRuntimeSlots.clear();
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
var Ricochet2DBehavior = defineBehavior({
|
|
401
|
+
name: "ricochet-2d",
|
|
402
|
+
defaultOptions,
|
|
403
|
+
systemFactory: (ctx) => new Ricochet2DSystem(ctx.world, ctx.wasmStage ?? null, ctx.getBehaviorLinks),
|
|
404
|
+
createHandle: createRicochet2DHandle
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
export {
|
|
408
|
+
Ricochet2DState,
|
|
409
|
+
Ricochet2DEvent,
|
|
410
|
+
Ricochet2DFSM,
|
|
411
|
+
Ricochet2DBehavior
|
|
412
|
+
};
|
|
413
|
+
//# sourceMappingURL=chunk-KOZUNIIE.js.map
|