@jgengine/core 0.5.0 → 0.7.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.
Files changed (244) hide show
  1. package/CHANGELOG.md +191 -2
  2. package/dist/ai/crowd.d.ts +36 -0
  3. package/dist/ai/crowd.js +213 -0
  4. package/dist/ai/jobBoard.d.ts +49 -0
  5. package/dist/ai/jobBoard.js +155 -0
  6. package/dist/ai/spawnDirector.d.ts +56 -0
  7. package/dist/ai/spawnDirector.js +150 -0
  8. package/dist/ai/threat.d.ts +25 -0
  9. package/dist/ai/threat.js +67 -0
  10. package/dist/audio/audioFalloff.d.ts +38 -0
  11. package/dist/audio/audioFalloff.js +32 -0
  12. package/dist/board/laneBoard.d.ts +63 -0
  13. package/dist/board/laneBoard.js +114 -0
  14. package/dist/board/timelineBoard.d.ts +36 -0
  15. package/dist/board/timelineBoard.js +70 -0
  16. package/dist/cards/cardPile.d.ts +63 -0
  17. package/dist/cards/cardPile.js +166 -0
  18. package/dist/cards/modifierPipeline.d.ts +33 -0
  19. package/dist/cards/modifierPipeline.js +48 -0
  20. package/dist/combat/abilityKit.d.ts +42 -0
  21. package/dist/combat/abilityKit.js +129 -0
  22. package/dist/combat/animationState.d.ts +47 -0
  23. package/dist/combat/animationState.js +101 -0
  24. package/dist/combat/attackTags.d.ts +14 -0
  25. package/dist/combat/attackTags.js +26 -0
  26. package/dist/combat/breakMeters.d.ts +32 -0
  27. package/dist/combat/breakMeters.js +51 -0
  28. package/dist/combat/comboString.d.ts +35 -0
  29. package/dist/combat/comboString.js +59 -0
  30. package/dist/combat/death.d.ts +8 -0
  31. package/dist/combat/death.js +5 -2
  32. package/dist/combat/defensiveWindow.d.ts +34 -0
  33. package/dist/combat/defensiveWindow.js +66 -0
  34. package/dist/combat/downed.d.ts +35 -0
  35. package/dist/combat/downed.js +104 -0
  36. package/dist/combat/effects.d.ts +11 -0
  37. package/dist/combat/effects.js +22 -13
  38. package/dist/combat/hitReaction.d.ts +23 -0
  39. package/dist/combat/hitReaction.js +20 -0
  40. package/dist/combat/resistance.d.ts +13 -0
  41. package/dist/combat/resistance.js +46 -0
  42. package/dist/combat/telegraph.d.ts +27 -0
  43. package/dist/combat/telegraph.js +37 -0
  44. package/dist/crafting/crop.d.ts +59 -0
  45. package/dist/crafting/crop.js +181 -0
  46. package/dist/crafting/production.d.ts +64 -0
  47. package/dist/crafting/production.js +130 -0
  48. package/dist/crafting/recipe.d.ts +62 -0
  49. package/dist/crafting/recipe.js +101 -0
  50. package/dist/economy/sharedWallet.d.ts +28 -0
  51. package/dist/economy/sharedWallet.js +62 -0
  52. package/dist/economy/techTree.d.ts +40 -0
  53. package/dist/economy/techTree.js +66 -0
  54. package/dist/game/cosmetics.d.ts +25 -0
  55. package/dist/game/cosmetics.js +51 -0
  56. package/dist/game/defineGame.d.ts +5 -0
  57. package/dist/game/events.d.ts +62 -0
  58. package/dist/game/lootFilter.d.ts +33 -0
  59. package/dist/game/lootFilter.js +48 -0
  60. package/dist/game/ping.d.ts +67 -0
  61. package/dist/game/ping.js +86 -0
  62. package/dist/game/playableGame.d.ts +291 -5
  63. package/dist/game/race.d.ts +95 -0
  64. package/dist/game/race.js +216 -0
  65. package/dist/game/runDraft.d.ts +34 -0
  66. package/dist/game/runDraft.js +97 -0
  67. package/dist/game/social.d.ts +27 -0
  68. package/dist/game/social.js +16 -0
  69. package/dist/game/worldItem.d.ts +92 -0
  70. package/dist/game/worldItem.js +132 -0
  71. package/dist/input/axisInput.d.ts +43 -0
  72. package/dist/input/axisInput.js +70 -0
  73. package/dist/input/pointer.d.ts +24 -0
  74. package/dist/input/pointer.js +17 -0
  75. package/dist/interaction/contextMenu.d.ts +30 -0
  76. package/dist/interaction/contextMenu.js +22 -0
  77. package/dist/interaction/qte.d.ts +20 -0
  78. package/dist/interaction/qte.js +24 -0
  79. package/dist/interaction/skillCheck.d.ts +20 -0
  80. package/dist/interaction/skillCheck.js +26 -0
  81. package/dist/inventory/shapedGrid.d.ts +56 -0
  82. package/dist/inventory/shapedGrid.js +176 -0
  83. package/dist/inventory/storageTier.d.ts +52 -0
  84. package/dist/inventory/storageTier.js +77 -0
  85. package/dist/item/affix.d.ts +54 -0
  86. package/dist/item/affix.js +133 -0
  87. package/dist/item/durability.d.ts +51 -0
  88. package/dist/item/durability.js +78 -0
  89. package/dist/item/modularItem.d.ts +46 -0
  90. package/dist/item/modularItem.js +75 -0
  91. package/dist/meta/changelog.d.ts +1 -1
  92. package/dist/meta/changelog.js +53 -1
  93. package/dist/movement/dash.d.ts +37 -0
  94. package/dist/movement/dash.js +65 -0
  95. package/dist/multiplayer/combatSnapshot.d.ts +45 -0
  96. package/dist/multiplayer/combatSnapshot.js +89 -0
  97. package/dist/multiplayer/lagCompensation.d.ts +43 -0
  98. package/dist/multiplayer/lagCompensation.js +154 -0
  99. package/dist/multiplayer/matchmaking.d.ts +33 -0
  100. package/dist/multiplayer/matchmaking.js +60 -0
  101. package/dist/multiplayer/simultaneousCommit.d.ts +34 -0
  102. package/dist/multiplayer/simultaneousCommit.js +53 -0
  103. package/dist/nav/navGrid.d.ts +46 -0
  104. package/dist/nav/navGrid.js +248 -0
  105. package/dist/nav/pathFollow.d.ts +29 -0
  106. package/dist/nav/pathFollow.js +67 -0
  107. package/dist/physics/buoyancy.d.ts +49 -0
  108. package/dist/physics/buoyancy.js +112 -0
  109. package/dist/physics/carryable.d.ts +46 -0
  110. package/dist/physics/carryable.js +110 -0
  111. package/dist/physics/damageZones.d.ts +56 -0
  112. package/dist/physics/damageZones.js +107 -0
  113. package/dist/physics/forceVolume.d.ts +48 -0
  114. package/dist/physics/forceVolume.js +119 -0
  115. package/dist/physics/physicsWorld.d.ts +219 -0
  116. package/dist/physics/physicsWorld.js +749 -0
  117. package/dist/physics/ragdoll.d.ts +49 -0
  118. package/dist/physics/ragdoll.js +103 -0
  119. package/dist/physics/spatialGrid.d.ts +41 -0
  120. package/dist/physics/spatialGrid.js +138 -0
  121. package/dist/physics/structure.d.ts +83 -0
  122. package/dist/physics/structure.js +0 -0
  123. package/dist/physics/traversal.d.ts +75 -0
  124. package/dist/physics/traversal.js +138 -0
  125. package/dist/physics/vehicleBody.d.ts +93 -0
  126. package/dist/physics/vehicleBody.js +234 -0
  127. package/dist/runtime/gameContext.d.ts +66 -2
  128. package/dist/runtime/gameContext.js +157 -4
  129. package/dist/runtime/hostPersistence.d.ts +18 -0
  130. package/dist/runtime/hostPersistence.js +4 -0
  131. package/dist/runtime/persistenceScope.d.ts +32 -0
  132. package/dist/runtime/persistenceScope.js +64 -0
  133. package/dist/scene/assetCatalog.d.ts +13 -0
  134. package/dist/scene/autoTarget.d.ts +15 -0
  135. package/dist/scene/autoTarget.js +54 -0
  136. package/dist/scene/behaviors.d.ts +13 -1
  137. package/dist/scene/behaviors.js +3 -0
  138. package/dist/scene/captureCheck.d.ts +7 -0
  139. package/dist/scene/captureCheck.js +11 -0
  140. package/dist/scene/entityStore.d.ts +7 -1
  141. package/dist/scene/entityStore.js +15 -1
  142. package/dist/scene/form.d.ts +40 -0
  143. package/dist/scene/form.js +56 -0
  144. package/dist/scene/mount.d.ts +62 -0
  145. package/dist/scene/mount.js +107 -0
  146. package/dist/scene/possession.d.ts +30 -0
  147. package/dist/scene/possession.js +51 -0
  148. package/dist/scene/roster.d.ts +25 -0
  149. package/dist/scene/roster.js +57 -0
  150. package/dist/scene/selection.d.ts +30 -0
  151. package/dist/scene/selection.js +38 -0
  152. package/dist/scene/stationClaim.d.ts +43 -0
  153. package/dist/scene/stationClaim.js +109 -0
  154. package/dist/sensor/frustumSensor.d.ts +60 -0
  155. package/dist/sensor/frustumSensor.js +104 -0
  156. package/dist/sensor/hiddenStateProbe.d.ts +31 -0
  157. package/dist/sensor/hiddenStateProbe.js +40 -0
  158. package/dist/sensor/recordingBuffer.d.ts +29 -0
  159. package/dist/sensor/recordingBuffer.js +61 -0
  160. package/dist/sensor/revealQuery.d.ts +22 -0
  161. package/dist/sensor/revealQuery.js +36 -0
  162. package/dist/session/contestedChannel.d.ts +36 -0
  163. package/dist/session/contestedChannel.js +100 -0
  164. package/dist/session/extraction.d.ts +54 -0
  165. package/dist/session/extraction.js +108 -0
  166. package/dist/session/ring.d.ts +36 -0
  167. package/dist/session/ring.js +88 -0
  168. package/dist/session/roundState.d.ts +55 -0
  169. package/dist/session/roundState.js +129 -0
  170. package/dist/stats/accumulatorMeter.d.ts +31 -0
  171. package/dist/stats/accumulatorMeter.js +81 -0
  172. package/dist/stats/eventMeter.d.ts +34 -0
  173. package/dist/stats/eventMeter.js +76 -0
  174. package/dist/stats/rollCheck.d.ts +15 -0
  175. package/dist/stats/rollCheck.js +15 -0
  176. package/dist/survival/decayMeter.d.ts +59 -0
  177. package/dist/survival/decayMeter.js +95 -0
  178. package/dist/survival/moodle.d.ts +48 -0
  179. package/dist/survival/moodle.js +91 -0
  180. package/dist/survival/regionHealth.d.ts +72 -0
  181. package/dist/survival/regionHealth.js +129 -0
  182. package/dist/tactics/predictiveQuery.d.ts +34 -0
  183. package/dist/tactics/predictiveQuery.js +44 -0
  184. package/dist/tactics/snapshot.d.ts +18 -0
  185. package/dist/tactics/snapshot.js +65 -0
  186. package/dist/tactics/surface.d.ts +52 -0
  187. package/dist/tactics/surface.js +135 -0
  188. package/dist/tactics/tacticalGrid.d.ts +57 -0
  189. package/dist/tactics/tacticalGrid.js +244 -0
  190. package/dist/time/beatClock.d.ts +40 -0
  191. package/dist/time/beatClock.js +73 -0
  192. package/dist/time/simClock.d.ts +62 -0
  193. package/dist/time/simClock.js +132 -0
  194. package/dist/turn/commit.d.ts +27 -0
  195. package/dist/turn/commit.js +72 -0
  196. package/dist/turn/turnLoop.d.ts +58 -0
  197. package/dist/turn/turnLoop.js +216 -0
  198. package/dist/world/buildPermissions.d.ts +47 -0
  199. package/dist/world/buildPermissions.js +134 -0
  200. package/dist/world/buildingIndex.d.ts +16 -0
  201. package/dist/world/buildingIndex.js +63 -0
  202. package/dist/world/buildings.d.ts +98 -0
  203. package/dist/world/buildings.js +320 -0
  204. package/dist/world/carve.d.ts +105 -0
  205. package/dist/world/carve.js +175 -0
  206. package/dist/world/connectors.d.ts +40 -0
  207. package/dist/world/connectors.js +67 -0
  208. package/dist/world/envField.d.ts +71 -0
  209. package/dist/world/envField.js +96 -0
  210. package/dist/world/features.d.ts +105 -1
  211. package/dist/world/features.js +95 -0
  212. package/dist/world/fog.d.ts +40 -0
  213. package/dist/world/fog.js +137 -0
  214. package/dist/world/markers.d.ts +52 -0
  215. package/dist/world/markers.js +96 -0
  216. package/dist/world/minimap.d.ts +42 -0
  217. package/dist/world/minimap.js +72 -0
  218. package/dist/world/placedStructureStore.d.ts +39 -0
  219. package/dist/world/placedStructureStore.js +95 -0
  220. package/dist/world/placementController.d.ts +48 -0
  221. package/dist/world/placementController.js +97 -0
  222. package/dist/world/realm.d.ts +61 -0
  223. package/dist/world/realm.js +66 -0
  224. package/dist/world/regions.d.ts +66 -0
  225. package/dist/world/regions.js +158 -0
  226. package/dist/world/scatter.d.ts +24 -0
  227. package/dist/world/scatter.js +126 -0
  228. package/dist/world/scatterItems.d.ts +38 -0
  229. package/dist/world/scatterItems.js +85 -0
  230. package/dist/world/support.d.ts +27 -0
  231. package/dist/world/support.js +75 -0
  232. package/dist/world/terraform.d.ts +54 -0
  233. package/dist/world/terraform.js +186 -0
  234. package/dist/world/terrain.d.ts +54 -0
  235. package/dist/world/terrain.js +181 -0
  236. package/dist/world/walls.d.ts +61 -0
  237. package/dist/world/walls.js +221 -0
  238. package/dist/world/water.d.ts +27 -0
  239. package/dist/world/water.js +125 -0
  240. package/dist/world/weather.d.ts +92 -0
  241. package/dist/world/weather.js +169 -0
  242. package/dist/world/wind.d.ts +17 -0
  243. package/dist/world/wind.js +40 -0
  244. package/package.json +1 -1
@@ -0,0 +1,129 @@
1
+ const DEFAULT_FLASH_MS = 220;
2
+ function normalizeConfig(config) {
3
+ return {
4
+ id: config.id,
5
+ cooldownMs: Math.max(0, config.cooldownMs),
6
+ chargesMax: Math.max(1, config.chargesMax ?? 1),
7
+ resourceCost: Math.max(0, config.resourceCost ?? 0),
8
+ castType: config.castType ?? "instant",
9
+ flashMs: Math.max(0, config.flashMs ?? DEFAULT_FLASH_MS),
10
+ };
11
+ }
12
+ function deriveState(runtime, resourceAvailable) {
13
+ if (runtime.flashRemainingMs > 0)
14
+ return "just-cast";
15
+ if (runtime.charges <= 0)
16
+ return "cooldown";
17
+ if (resourceAvailable < runtime.config.resourceCost)
18
+ return "no-resource";
19
+ return "ready";
20
+ }
21
+ function snapshotOf(runtime, resourceAvailable) {
22
+ const { config } = runtime;
23
+ const state = deriveState(runtime, resourceAvailable);
24
+ const cooldownFraction = config.cooldownMs <= 0 ? 0 : Math.max(0, Math.min(1, runtime.rechargeRemainingMs / config.cooldownMs));
25
+ return {
26
+ id: config.id,
27
+ state,
28
+ castType: config.castType,
29
+ resourceCost: config.resourceCost,
30
+ charges: runtime.charges,
31
+ chargesMax: config.chargesMax,
32
+ cooldownRemainingMs: runtime.rechargeRemainingMs,
33
+ cooldownFraction,
34
+ justCast: runtime.flashRemainingMs > 0,
35
+ ready: runtime.charges > 0 && resourceAvailable >= config.resourceCost,
36
+ };
37
+ }
38
+ export function createAbilityKit(configs) {
39
+ const runtimes = new Map();
40
+ const order = [];
41
+ for (const config of configs) {
42
+ const normalized = normalizeConfig(config);
43
+ if (runtimes.has(normalized.id))
44
+ throw new Error(`duplicate ability slot: ${normalized.id}`);
45
+ runtimes.set(normalized.id, {
46
+ config: normalized,
47
+ charges: normalized.chargesMax,
48
+ rechargeRemainingMs: 0,
49
+ flashRemainingMs: 0,
50
+ });
51
+ order.push(normalized.id);
52
+ }
53
+ function fail(slotId, reason, resourceAvailable) {
54
+ const runtime = runtimes.get(slotId);
55
+ return { ok: false, reason, slot: runtime ? snapshotOf(runtime, resourceAvailable) : null };
56
+ }
57
+ function evaluate(slotId, resourceAvailable) {
58
+ const runtime = runtimes.get(slotId);
59
+ if (runtime === undefined)
60
+ return fail(slotId, "unknown-slot", resourceAvailable);
61
+ if (runtime.charges <= 0)
62
+ return fail(slotId, "cooldown", resourceAvailable);
63
+ if (resourceAvailable < runtime.config.resourceCost)
64
+ return fail(slotId, "no-resource", resourceAvailable);
65
+ return { ok: true, slot: snapshotOf(runtime, resourceAvailable) };
66
+ }
67
+ return {
68
+ slots() {
69
+ return order.slice();
70
+ },
71
+ config(slotId) {
72
+ return runtimes.get(slotId)?.config ?? null;
73
+ },
74
+ state(slotId, resourceAvailable = Number.POSITIVE_INFINITY) {
75
+ const runtime = runtimes.get(slotId);
76
+ return runtime === undefined ? null : snapshotOf(runtime, resourceAvailable);
77
+ },
78
+ snapshot(resourceAvailable = Number.POSITIVE_INFINITY) {
79
+ return order.map((slotId) => snapshotOf(runtimes.get(slotId), resourceAvailable));
80
+ },
81
+ canCast(slotId, resourceAvailable = Number.POSITIVE_INFINITY) {
82
+ return evaluate(slotId, resourceAvailable);
83
+ },
84
+ cast(slotId, resourceAvailable = Number.POSITIVE_INFINITY) {
85
+ const result = evaluate(slotId, resourceAvailable);
86
+ if (!result.ok)
87
+ return result;
88
+ const runtime = runtimes.get(slotId);
89
+ runtime.charges -= 1;
90
+ if (runtime.rechargeRemainingMs <= 0)
91
+ runtime.rechargeRemainingMs = runtime.config.cooldownMs;
92
+ runtime.flashRemainingMs = runtime.config.flashMs;
93
+ return { ok: true, slot: snapshotOf(runtime, resourceAvailable) };
94
+ },
95
+ tick(dtSeconds) {
96
+ if (dtSeconds <= 0)
97
+ return;
98
+ const dtMs = dtSeconds * 1000;
99
+ for (const runtime of runtimes.values()) {
100
+ if (runtime.flashRemainingMs > 0)
101
+ runtime.flashRemainingMs = Math.max(0, runtime.flashRemainingMs - dtMs);
102
+ if (runtime.charges >= runtime.config.chargesMax) {
103
+ runtime.rechargeRemainingMs = 0;
104
+ continue;
105
+ }
106
+ if (runtime.config.cooldownMs <= 0) {
107
+ runtime.charges = runtime.config.chargesMax;
108
+ runtime.rechargeRemainingMs = 0;
109
+ continue;
110
+ }
111
+ let remaining = runtime.rechargeRemainingMs <= 0 ? runtime.config.cooldownMs : runtime.rechargeRemainingMs;
112
+ remaining -= dtMs;
113
+ while (remaining <= 0 && runtime.charges < runtime.config.chargesMax) {
114
+ runtime.charges += 1;
115
+ remaining += runtime.config.cooldownMs;
116
+ }
117
+ runtime.rechargeRemainingMs = runtime.charges >= runtime.config.chargesMax ? 0 : remaining;
118
+ }
119
+ },
120
+ reset(slotId) {
121
+ const targets = slotId === undefined ? runtimes.values() : [runtimes.get(slotId)].filter((r) => r !== undefined);
122
+ for (const runtime of targets) {
123
+ runtime.charges = runtime.config.chargesMax;
124
+ runtime.rechargeRemainingMs = 0;
125
+ runtime.flashRemainingMs = 0;
126
+ }
127
+ },
128
+ };
129
+ }
@@ -0,0 +1,47 @@
1
+ export type FramePhase = "windup" | "active" | "recovery" | "cancel";
2
+ export interface FrameRange {
3
+ phase: FramePhase;
4
+ from: number;
5
+ to: number;
6
+ }
7
+ export interface AnimationClip {
8
+ id: string;
9
+ frames: number;
10
+ fps: number;
11
+ ranges: readonly FrameRange[];
12
+ loop?: boolean;
13
+ }
14
+ export interface AnimationSnapshot {
15
+ clipId: string;
16
+ frame: number;
17
+ elapsedMs: number;
18
+ }
19
+ export interface AnimationTickResult {
20
+ clipId: string | null;
21
+ frame: number;
22
+ entered: readonly FramePhase[];
23
+ exited: readonly FramePhase[];
24
+ completed: boolean;
25
+ }
26
+ export declare function clipDurationMs(clip: AnimationClip): number;
27
+ export declare function frameAtMs(clip: AnimationClip, elapsedMs: number): number;
28
+ export declare function phasesAtFrame(clip: AnimationClip, frame: number): FramePhase[];
29
+ export declare function activeRangeAtFrame(clip: AnimationClip, frame: number): FrameRange | null;
30
+ export interface AnimationState {
31
+ play(clipId: string): void;
32
+ stop(): void;
33
+ tick(dtSeconds: number): AnimationTickResult;
34
+ current(): AnimationSnapshot | null;
35
+ phases(): readonly FramePhase[];
36
+ inPhase(phase: FramePhase): boolean;
37
+ isActive(): boolean;
38
+ canCancel(): boolean;
39
+ activeWindowMs(): {
40
+ from: number;
41
+ to: number;
42
+ } | null;
43
+ }
44
+ export interface AnimationStateConfig {
45
+ clips: readonly AnimationClip[];
46
+ }
47
+ export declare function createAnimationState(config: AnimationStateConfig): AnimationState;
@@ -0,0 +1,101 @@
1
+ export function clipDurationMs(clip) {
2
+ return (clip.frames / clip.fps) * 1000;
3
+ }
4
+ export function frameAtMs(clip, elapsedMs) {
5
+ const raw = (elapsedMs / 1000) * clip.fps;
6
+ if (clip.loop === true) {
7
+ const wrapped = raw % clip.frames;
8
+ return wrapped < 0 ? wrapped + clip.frames : wrapped;
9
+ }
10
+ return Math.min(raw, clip.frames);
11
+ }
12
+ export function phasesAtFrame(clip, frame) {
13
+ const phases = [];
14
+ for (const range of clip.ranges) {
15
+ if (frame >= range.from && frame < range.to && !phases.includes(range.phase)) {
16
+ phases.push(range.phase);
17
+ }
18
+ }
19
+ return phases;
20
+ }
21
+ export function activeRangeAtFrame(clip, frame) {
22
+ for (const range of clip.ranges) {
23
+ if (range.phase === "active" && frame >= range.from && frame < range.to)
24
+ return range;
25
+ }
26
+ return null;
27
+ }
28
+ export function createAnimationState(config) {
29
+ const clips = new Map(config.clips.map((clip) => [clip.id, clip]));
30
+ let activeClip = null;
31
+ let elapsedMs = 0;
32
+ let lastPhases = [];
33
+ function frame() {
34
+ return activeClip === null ? 0 : frameAtMs(activeClip, elapsedMs);
35
+ }
36
+ return {
37
+ play(clipId) {
38
+ const clip = clips.get(clipId);
39
+ if (clip === undefined)
40
+ return;
41
+ activeClip = clip;
42
+ elapsedMs = 0;
43
+ lastPhases = phasesAtFrame(clip, 0);
44
+ },
45
+ stop() {
46
+ activeClip = null;
47
+ elapsedMs = 0;
48
+ lastPhases = [];
49
+ },
50
+ tick(dtSeconds) {
51
+ if (activeClip === null) {
52
+ return { clipId: null, frame: 0, entered: [], exited: [], completed: false };
53
+ }
54
+ elapsedMs += dtSeconds * 1000;
55
+ const clip = activeClip;
56
+ let completed = false;
57
+ if (clip.loop !== true && elapsedMs >= clipDurationMs(clip)) {
58
+ elapsedMs = clipDurationMs(clip);
59
+ completed = true;
60
+ }
61
+ const currentFrame = frameAtMs(clip, elapsedMs);
62
+ const nowPhases = phasesAtFrame(clip, currentFrame);
63
+ const entered = nowPhases.filter((p) => !lastPhases.includes(p));
64
+ const exited = lastPhases.filter((p) => !nowPhases.includes(p));
65
+ lastPhases = nowPhases;
66
+ const clipId = clip.id;
67
+ if (completed) {
68
+ activeClip = null;
69
+ elapsedMs = 0;
70
+ lastPhases = [];
71
+ }
72
+ return { clipId, frame: currentFrame, entered, exited, completed };
73
+ },
74
+ current() {
75
+ if (activeClip === null)
76
+ return null;
77
+ return { clipId: activeClip.id, frame: frame(), elapsedMs };
78
+ },
79
+ phases() {
80
+ return activeClip === null ? [] : phasesAtFrame(activeClip, frame());
81
+ },
82
+ inPhase(phase) {
83
+ return activeClip !== null && phasesAtFrame(activeClip, frame()).includes(phase);
84
+ },
85
+ isActive() {
86
+ return activeClip !== null && activeRangeAtFrame(activeClip, frame()) !== null;
87
+ },
88
+ canCancel() {
89
+ return activeClip !== null && phasesAtFrame(activeClip, frame()).includes("cancel");
90
+ },
91
+ activeWindowMs() {
92
+ if (activeClip === null)
93
+ return null;
94
+ const range = activeRangeAtFrame(activeClip, frame());
95
+ if (range === null)
96
+ return null;
97
+ const perFrameMs = 1000 / activeClip.fps;
98
+ return { from: range.from * perFrameMs, to: range.to * perFrameMs };
99
+ },
100
+ };
101
+ }
@@ -0,0 +1,14 @@
1
+ export type AttackTag = "unblockable" | "thrust" | "sweep" | "grab" | "overhead" | "ranged" | "aerial";
2
+ export interface AttackMeta {
3
+ tags: readonly AttackTag[];
4
+ effect?: string;
5
+ power?: number;
6
+ }
7
+ export declare function attackMeta(tags: readonly AttackTag[], extra?: Omit<AttackMeta, "tags">): AttackMeta;
8
+ export declare function hasTag(meta: AttackMeta, tag: AttackTag): boolean;
9
+ export declare function hasAnyTag(meta: AttackMeta, tags: readonly AttackTag[]): boolean;
10
+ export declare function isBlockable(meta: AttackMeta): boolean;
11
+ export declare function isParryable(meta: AttackMeta): boolean;
12
+ export declare function isDodgeable(meta: AttackMeta): boolean;
13
+ export type CounterMove = "mikiri" | "deflect" | "sidestep";
14
+ export declare function counters(meta: AttackMeta, move: CounterMove): boolean;
@@ -0,0 +1,26 @@
1
+ export function attackMeta(tags, extra) {
2
+ return { tags, ...extra };
3
+ }
4
+ export function hasTag(meta, tag) {
5
+ return meta.tags.includes(tag);
6
+ }
7
+ export function hasAnyTag(meta, tags) {
8
+ return tags.some((tag) => meta.tags.includes(tag));
9
+ }
10
+ export function isBlockable(meta) {
11
+ return !hasAnyTag(meta, ["unblockable", "grab"]);
12
+ }
13
+ export function isParryable(meta) {
14
+ return !hasTag(meta, "grab");
15
+ }
16
+ export function isDodgeable(meta) {
17
+ return !hasTag(meta, "grab");
18
+ }
19
+ const COUNTER_TAGS = {
20
+ mikiri: "thrust",
21
+ deflect: "overhead",
22
+ sidestep: "sweep",
23
+ };
24
+ export function counters(meta, move) {
25
+ return hasTag(meta, COUNTER_TAGS[move]);
26
+ }
@@ -0,0 +1,32 @@
1
+ export interface StaggerMeterConfig {
2
+ max: number;
3
+ decayPerSecond?: number;
4
+ decayDelayMs?: number;
5
+ }
6
+ export interface StaggerMeter {
7
+ add(amount: number): boolean;
8
+ broke(): boolean;
9
+ value(): number;
10
+ fraction(): number;
11
+ recover(): void;
12
+ tick(dtSeconds: number): void;
13
+ }
14
+ export declare function createStaggerMeter(config: StaggerMeterConfig): StaggerMeter;
15
+ export interface BuildupMeterConfig {
16
+ status: string;
17
+ max: number;
18
+ durationMs: number;
19
+ decayPerSecond?: number;
20
+ decayDelayMs?: number;
21
+ }
22
+ export interface BuildupProc {
23
+ status: string;
24
+ durationMs: number;
25
+ }
26
+ export interface BuildupMeter {
27
+ add(amount: number): BuildupProc | null;
28
+ value(): number;
29
+ fraction(): number;
30
+ tick(dtSeconds: number): void;
31
+ }
32
+ export declare function createBuildupMeter(config: BuildupMeterConfig): BuildupMeter;
@@ -0,0 +1,51 @@
1
+ import { createAccumulatorMeter } from "../stats/accumulatorMeter.js";
2
+ export function createStaggerMeter(config) {
3
+ const meter = createAccumulatorMeter({
4
+ max: config.max,
5
+ mode: "hold",
6
+ decayPerSecond: config.decayPerSecond ?? 0,
7
+ decayDelayMs: config.decayDelayMs ?? 0,
8
+ });
9
+ return {
10
+ add(amount) {
11
+ return meter.add(amount).fired;
12
+ },
13
+ broke() {
14
+ return meter.broke();
15
+ },
16
+ value() {
17
+ return meter.value();
18
+ },
19
+ fraction() {
20
+ return meter.fraction();
21
+ },
22
+ recover() {
23
+ meter.reset();
24
+ },
25
+ tick(dtSeconds) {
26
+ meter.tick(dtSeconds);
27
+ },
28
+ };
29
+ }
30
+ export function createBuildupMeter(config) {
31
+ const meter = createAccumulatorMeter({
32
+ max: config.max,
33
+ mode: "reset",
34
+ decayPerSecond: config.decayPerSecond ?? 0,
35
+ decayDelayMs: config.decayDelayMs ?? 0,
36
+ });
37
+ return {
38
+ add(amount) {
39
+ return meter.add(amount).fired ? { status: config.status, durationMs: config.durationMs } : null;
40
+ },
41
+ value() {
42
+ return meter.value();
43
+ },
44
+ fraction() {
45
+ return meter.fraction();
46
+ },
47
+ tick(dtSeconds) {
48
+ meter.tick(dtSeconds);
49
+ },
50
+ };
51
+ }
@@ -0,0 +1,35 @@
1
+ import { type AnimationState, type FramePhase } from "./animationState.js";
2
+ export interface ComboStep {
3
+ id: string;
4
+ clip: string;
5
+ cancelInto?: readonly string[];
6
+ cancelPhases?: readonly FramePhase[];
7
+ stance?: string;
8
+ }
9
+ export interface ComboString {
10
+ id: string;
11
+ entry: string;
12
+ steps: readonly ComboStep[];
13
+ }
14
+ export declare function stepById(combo: ComboString, stepId: string): ComboStep | null;
15
+ export interface AdvanceComboInput {
16
+ combo: ComboString;
17
+ currentStepId: string | null;
18
+ requestedStepId: string;
19
+ phases: readonly FramePhase[];
20
+ stance?: string;
21
+ }
22
+ export type AdvanceComboResult = {
23
+ accepted: true;
24
+ step: ComboStep;
25
+ } | {
26
+ accepted: false;
27
+ reason: "unknown-step" | "not-chainable" | "wrong-stance" | "window-closed";
28
+ };
29
+ export declare function advanceCombo(input: AdvanceComboInput): AdvanceComboResult;
30
+ export interface ComboRunner {
31
+ currentStep(): string | null;
32
+ request(stepId: string, stance?: string): AdvanceComboResult;
33
+ reset(): void;
34
+ }
35
+ export declare function createComboRunner(combo: ComboString, anim: AnimationState): ComboRunner;
@@ -0,0 +1,59 @@
1
+ import {} from "./animationState.js";
2
+ const DEFAULT_CANCEL_PHASES = ["cancel"];
3
+ export function stepById(combo, stepId) {
4
+ return combo.steps.find((step) => step.id === stepId) ?? null;
5
+ }
6
+ function stanceMatches(step, stance) {
7
+ return step.stance === undefined || step.stance === stance;
8
+ }
9
+ export function advanceCombo(input) {
10
+ const { combo, currentStepId, requestedStepId, phases, stance } = input;
11
+ const requested = stepById(combo, requestedStepId);
12
+ if (requested === null)
13
+ return { accepted: false, reason: "unknown-step" };
14
+ if (currentStepId === null) {
15
+ if (requestedStepId !== combo.entry)
16
+ return { accepted: false, reason: "not-chainable" };
17
+ if (!stanceMatches(requested, stance))
18
+ return { accepted: false, reason: "wrong-stance" };
19
+ return { accepted: true, step: requested };
20
+ }
21
+ const current = stepById(combo, currentStepId);
22
+ if (current === null)
23
+ return { accepted: false, reason: "unknown-step" };
24
+ if (!(current.cancelInto ?? []).includes(requestedStepId)) {
25
+ return { accepted: false, reason: "not-chainable" };
26
+ }
27
+ if (!stanceMatches(requested, stance))
28
+ return { accepted: false, reason: "wrong-stance" };
29
+ const cancelPhases = current.cancelPhases ?? DEFAULT_CANCEL_PHASES;
30
+ if (!cancelPhases.some((phase) => phases.includes(phase))) {
31
+ return { accepted: false, reason: "window-closed" };
32
+ }
33
+ return { accepted: true, step: requested };
34
+ }
35
+ export function createComboRunner(combo, anim) {
36
+ let current = null;
37
+ return {
38
+ currentStep() {
39
+ return current;
40
+ },
41
+ request(stepId, stance) {
42
+ const result = advanceCombo({
43
+ combo,
44
+ currentStepId: current,
45
+ requestedStepId: stepId,
46
+ phases: anim.phases(),
47
+ stance,
48
+ });
49
+ if (result.accepted) {
50
+ current = result.step.id;
51
+ anim.play(result.step.clip);
52
+ }
53
+ return result;
54
+ },
55
+ reset() {
56
+ current = null;
57
+ },
58
+ };
59
+ }
@@ -1,5 +1,6 @@
1
1
  import type { DeathReason, GameEvents } from "../game/events.js";
2
2
  import type { Drop } from "../game/lootTable.js";
3
+ import type { ScatterOptions } from "../game/worldItem.js";
3
4
  import type { EffectVia } from "./effects.js";
4
5
  export type DeathReasonKind = DeathReason["kind"];
5
6
  export interface OnDeathDropRule {
@@ -15,13 +16,20 @@ export interface OnDeathCommandRule {
15
16
  reason: DeathReasonKind;
16
17
  };
17
18
  }
19
+ export type DropMode = "grant" | "world";
18
20
  export interface OnDeathSpec {
19
21
  drops?: string | OnDeathDropRule[];
20
22
  command?: string | OnDeathCommandRule;
23
+ /** "grant" (default) puts drops straight into the killer's inventory; "world" scatters them as ground `worldItem`s instead (#32). */
24
+ dropMode?: DropMode;
25
+ /** Scatter-offset tuning when `dropMode` is `"world"`; defaults to `DEFAULT_SCATTER` from `game/worldItem`. */
26
+ scatter?: ScatterOptions;
21
27
  }
22
28
  export interface NormalizedOnDeath {
23
29
  drops: OnDeathDropRule[];
24
30
  command: OnDeathCommandRule | null;
31
+ dropMode: DropMode;
32
+ scatter?: ScatterOptions;
25
33
  }
26
34
  export declare function normalizeOnDeath(spec: OnDeathSpec | null | undefined): NormalizedOnDeath;
27
35
  export interface DeathIdentity {
@@ -1,9 +1,12 @@
1
1
  export function normalizeOnDeath(spec) {
2
2
  if (!spec)
3
- return { drops: [], command: null };
3
+ return { drops: [], command: null, dropMode: "grant" };
4
4
  const drops = spec.drops === undefined ? [] : typeof spec.drops === "string" ? [{ table: spec.drops }] : spec.drops;
5
5
  const command = spec.command === undefined ? null : typeof spec.command === "string" ? { name: spec.command } : spec.command;
6
- return { drops, command };
6
+ const normalized = { drops, command, dropMode: spec.dropMode ?? "grant" };
7
+ if (spec.scatter !== undefined)
8
+ normalized.scatter = spec.scatter;
9
+ return normalized;
7
10
  }
8
11
  function matchesReason(when, reason) {
9
12
  return when === undefined || when.reason === reason.kind;
@@ -0,0 +1,34 @@
1
+ import { type AttackMeta } from "./attackTags.js";
2
+ export type DefenseKind = "parry" | "block" | "dodge";
3
+ export interface DefensiveWindowConfig {
4
+ kind: DefenseKind;
5
+ startupMs?: number;
6
+ activeMs: number;
7
+ recoveryMs?: number;
8
+ iframes?: {
9
+ fromMs: number;
10
+ toMs: number;
11
+ };
12
+ }
13
+ export type DefenseOutcome = "parry" | "block" | "iframe" | "hit";
14
+ export interface DefenseResolution {
15
+ outcome: DefenseOutcome;
16
+ perfect: boolean;
17
+ }
18
+ export declare function windowActiveAt(config: DefensiveWindowConfig, elapsedMs: number): boolean;
19
+ export declare function iframeActiveAt(config: DefensiveWindowConfig, elapsedMs: number): boolean;
20
+ export declare function totalWindowMs(config: DefensiveWindowConfig): number;
21
+ export interface ResolveDefenseInput {
22
+ config: DefensiveWindowConfig;
23
+ elapsedMs: number;
24
+ attack: AttackMeta;
25
+ }
26
+ export declare function resolveDefense(input: ResolveDefenseInput): DefenseResolution;
27
+ export interface DefensiveWindow {
28
+ open(nowMs: number): void;
29
+ close(): void;
30
+ isOpen(): boolean;
31
+ evaluate(nowMs: number, attack: AttackMeta): DefenseResolution;
32
+ isInvulnerable(nowMs: number): boolean;
33
+ }
34
+ export declare function createDefensiveWindow(config: DefensiveWindowConfig): DefensiveWindow;
@@ -0,0 +1,66 @@
1
+ import { isBlockable, isDodgeable, isParryable } from "./attackTags.js";
2
+ export function windowActiveAt(config, elapsedMs) {
3
+ const start = config.startupMs ?? 0;
4
+ return elapsedMs >= start && elapsedMs < start + config.activeMs;
5
+ }
6
+ export function iframeActiveAt(config, elapsedMs) {
7
+ const frames = config.iframes;
8
+ if (frames === undefined)
9
+ return false;
10
+ return elapsedMs >= frames.fromMs && elapsedMs < frames.toMs;
11
+ }
12
+ export function totalWindowMs(config) {
13
+ return (config.startupMs ?? 0) + config.activeMs + (config.recoveryMs ?? 0);
14
+ }
15
+ export function resolveDefense(input) {
16
+ const { config, elapsedMs, attack } = input;
17
+ const inWindow = windowActiveAt(config, elapsedMs);
18
+ const inIframe = iframeActiveAt(config, elapsedMs);
19
+ if (config.kind === "dodge") {
20
+ if (inIframe && isDodgeable(attack))
21
+ return { outcome: "iframe", perfect: inWindow };
22
+ return { outcome: "hit", perfect: false };
23
+ }
24
+ if (config.kind === "parry") {
25
+ if (inWindow && isParryable(attack))
26
+ return { outcome: "parry", perfect: true };
27
+ if (inIframe && isDodgeable(attack))
28
+ return { outcome: "iframe", perfect: false };
29
+ return { outcome: "hit", perfect: false };
30
+ }
31
+ if (inWindow && isBlockable(attack))
32
+ return { outcome: "block", perfect: true };
33
+ return { outcome: "hit", perfect: false };
34
+ }
35
+ export function createDefensiveWindow(config) {
36
+ let openedAt = null;
37
+ function elapsed(nowMs) {
38
+ if (openedAt === null)
39
+ return null;
40
+ const dt = nowMs - openedAt;
41
+ if (dt < 0 || dt >= totalWindowMs(config))
42
+ return null;
43
+ return dt;
44
+ }
45
+ return {
46
+ open(nowMs) {
47
+ openedAt = nowMs;
48
+ },
49
+ close() {
50
+ openedAt = null;
51
+ },
52
+ isOpen() {
53
+ return openedAt !== null;
54
+ },
55
+ evaluate(nowMs, attack) {
56
+ const dt = elapsed(nowMs);
57
+ if (dt === null)
58
+ return { outcome: "hit", perfect: false };
59
+ return resolveDefense({ config, elapsedMs: dt, attack });
60
+ },
61
+ isInvulnerable(nowMs) {
62
+ const dt = elapsed(nowMs);
63
+ return dt !== null && iframeActiveAt(config, dt);
64
+ },
65
+ };
66
+ }
@@ -0,0 +1,35 @@
1
+ export type DownedPhase = "alive" | "downed" | "dead";
2
+ export interface DownedConfig {
3
+ bleedoutSeconds: number;
4
+ reviveSeconds?: number;
5
+ reviveHealthFraction?: number;
6
+ banner?: {
7
+ expireSeconds: number;
8
+ };
9
+ }
10
+ export interface DownedEntry {
11
+ phase: DownedPhase;
12
+ bleedoutRemaining: number;
13
+ reviveProgress: number;
14
+ bannerRemaining: number | null;
15
+ }
16
+ export type DownedEventKind = "downed" | "revived" | "reviving" | "died" | "banner.created" | "banner.expired" | "respawned";
17
+ export interface DownedEvent {
18
+ kind: DownedEventKind;
19
+ instanceId: string;
20
+ reason?: string;
21
+ reviveHealthFraction?: number;
22
+ }
23
+ export interface DownedState {
24
+ down(instanceId: string): DownedEvent | null;
25
+ revive(instanceId: string, dt: number): DownedEvent | null;
26
+ interruptRevive(instanceId: string): void;
27
+ finish(instanceId: string, reason?: string): DownedEvent | null;
28
+ tick(dt: number): DownedEvent[];
29
+ respawnFromBanner(instanceId: string): DownedEvent | null;
30
+ phase(instanceId: string): DownedPhase;
31
+ get(instanceId: string): DownedEntry | null;
32
+ clear(instanceId: string): void;
33
+ snapshot(): Record<string, DownedEntry>;
34
+ }
35
+ export declare function createDownedState(config: DownedConfig): DownedState;