@jgengine/core 0.6.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 (222) hide show
  1. package/CHANGELOG.md +167 -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 +2 -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 +281 -4
  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 +35 -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 +83 -0
  116. package/dist/physics/physicsWorld.js +261 -1
  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 +64 -2
  128. package/dist/runtime/gameContext.js +154 -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/autoTarget.d.ts +15 -0
  134. package/dist/scene/autoTarget.js +54 -0
  135. package/dist/scene/behaviors.d.ts +13 -1
  136. package/dist/scene/behaviors.js +3 -0
  137. package/dist/scene/captureCheck.d.ts +7 -0
  138. package/dist/scene/captureCheck.js +11 -0
  139. package/dist/scene/entityStore.d.ts +1 -1
  140. package/dist/scene/form.d.ts +40 -0
  141. package/dist/scene/form.js +56 -0
  142. package/dist/scene/mount.d.ts +62 -0
  143. package/dist/scene/mount.js +107 -0
  144. package/dist/scene/possession.d.ts +30 -0
  145. package/dist/scene/possession.js +51 -0
  146. package/dist/scene/roster.d.ts +25 -0
  147. package/dist/scene/roster.js +57 -0
  148. package/dist/scene/selection.d.ts +30 -0
  149. package/dist/scene/selection.js +38 -0
  150. package/dist/scene/stationClaim.d.ts +43 -0
  151. package/dist/scene/stationClaim.js +109 -0
  152. package/dist/sensor/frustumSensor.d.ts +60 -0
  153. package/dist/sensor/frustumSensor.js +104 -0
  154. package/dist/sensor/hiddenStateProbe.d.ts +31 -0
  155. package/dist/sensor/hiddenStateProbe.js +40 -0
  156. package/dist/sensor/recordingBuffer.d.ts +29 -0
  157. package/dist/sensor/recordingBuffer.js +61 -0
  158. package/dist/sensor/revealQuery.d.ts +22 -0
  159. package/dist/sensor/revealQuery.js +36 -0
  160. package/dist/session/contestedChannel.d.ts +36 -0
  161. package/dist/session/contestedChannel.js +100 -0
  162. package/dist/session/extraction.d.ts +54 -0
  163. package/dist/session/extraction.js +108 -0
  164. package/dist/session/ring.d.ts +36 -0
  165. package/dist/session/ring.js +88 -0
  166. package/dist/session/roundState.d.ts +55 -0
  167. package/dist/session/roundState.js +129 -0
  168. package/dist/stats/accumulatorMeter.d.ts +31 -0
  169. package/dist/stats/accumulatorMeter.js +81 -0
  170. package/dist/stats/eventMeter.d.ts +34 -0
  171. package/dist/stats/eventMeter.js +76 -0
  172. package/dist/stats/rollCheck.d.ts +15 -0
  173. package/dist/stats/rollCheck.js +15 -0
  174. package/dist/survival/decayMeter.d.ts +59 -0
  175. package/dist/survival/decayMeter.js +95 -0
  176. package/dist/survival/moodle.d.ts +48 -0
  177. package/dist/survival/moodle.js +91 -0
  178. package/dist/survival/regionHealth.d.ts +72 -0
  179. package/dist/survival/regionHealth.js +129 -0
  180. package/dist/tactics/predictiveQuery.d.ts +34 -0
  181. package/dist/tactics/predictiveQuery.js +44 -0
  182. package/dist/tactics/snapshot.d.ts +18 -0
  183. package/dist/tactics/snapshot.js +65 -0
  184. package/dist/tactics/surface.d.ts +52 -0
  185. package/dist/tactics/surface.js +135 -0
  186. package/dist/tactics/tacticalGrid.d.ts +57 -0
  187. package/dist/tactics/tacticalGrid.js +244 -0
  188. package/dist/time/beatClock.d.ts +40 -0
  189. package/dist/time/beatClock.js +73 -0
  190. package/dist/turn/commit.d.ts +27 -0
  191. package/dist/turn/commit.js +72 -0
  192. package/dist/turn/turnLoop.d.ts +58 -0
  193. package/dist/turn/turnLoop.js +216 -0
  194. package/dist/world/buildPermissions.d.ts +47 -0
  195. package/dist/world/buildPermissions.js +134 -0
  196. package/dist/world/carve.d.ts +105 -0
  197. package/dist/world/carve.js +175 -0
  198. package/dist/world/connectors.d.ts +40 -0
  199. package/dist/world/connectors.js +67 -0
  200. package/dist/world/envField.d.ts +71 -0
  201. package/dist/world/envField.js +96 -0
  202. package/dist/world/fog.d.ts +40 -0
  203. package/dist/world/fog.js +137 -0
  204. package/dist/world/markers.d.ts +52 -0
  205. package/dist/world/markers.js +96 -0
  206. package/dist/world/minimap.d.ts +42 -0
  207. package/dist/world/minimap.js +72 -0
  208. package/dist/world/placedStructureStore.d.ts +39 -0
  209. package/dist/world/placedStructureStore.js +95 -0
  210. package/dist/world/placementController.d.ts +48 -0
  211. package/dist/world/placementController.js +97 -0
  212. package/dist/world/realm.d.ts +61 -0
  213. package/dist/world/realm.js +66 -0
  214. package/dist/world/support.d.ts +27 -0
  215. package/dist/world/support.js +75 -0
  216. package/dist/world/terraform.d.ts +54 -0
  217. package/dist/world/terraform.js +186 -0
  218. package/dist/world/walls.d.ts +61 -0
  219. package/dist/world/walls.js +221 -0
  220. package/dist/world/weather.d.ts +92 -0
  221. package/dist/world/weather.js +169 -0
  222. package/package.json +1 -1
@@ -0,0 +1,43 @@
1
+ export interface AxisInput {
2
+ throttle: number;
3
+ brake: number;
4
+ steer: number;
5
+ handbrake: number;
6
+ }
7
+ export declare const NEUTRAL_AXIS: AxisInput;
8
+ export type AxisName = keyof AxisInput;
9
+ export interface AxisBinding {
10
+ positive: readonly string[];
11
+ negative?: readonly string[];
12
+ }
13
+ export type AxisBindingMap = Record<AxisName, AxisBinding>;
14
+ export interface AxisChannelConfig {
15
+ bindings: AxisBindingMap;
16
+ smoothing?: number;
17
+ }
18
+ export interface AxisRange {
19
+ min: number;
20
+ max: number;
21
+ }
22
+ export declare const AXIS_RANGE: Record<AxisName, AxisRange>;
23
+ export declare function clampAxis(value: number, range: AxisRange): number;
24
+ export declare function rampToward(current: number, target: number, rate: number, dt: number): number;
25
+ /**
26
+ * Analog control channel — distinct from the digital action bindings. Throttle/brake/steer/handbrake
27
+ * are continuous values ramped from held keys (a keyboard feels like a pedal) or driven directly from
28
+ * a gamepad axis via `setAnalog`. `sample(dt, isDown)` folds the current held-key state into the
29
+ * smoothed value; a `setAnalog` override replaces the digital target for that axis until cleared.
30
+ */
31
+ export declare class AxisChannel {
32
+ private readonly bindings;
33
+ private readonly smoothing;
34
+ private readonly current;
35
+ private readonly overrides;
36
+ constructor(config: AxisChannelConfig);
37
+ setAnalog(axis: AxisName, value: number): void;
38
+ clearAnalog(axis: AxisName): void;
39
+ reset(): void;
40
+ get value(): AxisInput;
41
+ sample(dt: number, isDown: (code: string) => boolean): AxisInput;
42
+ }
43
+ export declare const DRIVE_AXIS_BINDINGS: AxisBindingMap;
@@ -0,0 +1,70 @@
1
+ export const NEUTRAL_AXIS = { throttle: 0, brake: 0, steer: 0, handbrake: 0 };
2
+ export const AXIS_RANGE = {
3
+ throttle: { min: 0, max: 1 },
4
+ brake: { min: 0, max: 1 },
5
+ steer: { min: -1, max: 1 },
6
+ handbrake: { min: 0, max: 1 },
7
+ };
8
+ export function clampAxis(value, range) {
9
+ return value < range.min ? range.min : value > range.max ? range.max : value;
10
+ }
11
+ export function rampToward(current, target, rate, dt) {
12
+ if (!Number.isFinite(rate))
13
+ return target;
14
+ const delta = target - current;
15
+ const step = rate * dt;
16
+ if (Math.abs(delta) <= step)
17
+ return target;
18
+ return current + Math.sign(delta) * step;
19
+ }
20
+ function digitalTarget(binding, isDown) {
21
+ const positive = binding.positive.some(isDown) ? 1 : 0;
22
+ const negative = binding.negative !== undefined && binding.negative.some(isDown) ? 1 : 0;
23
+ return positive - negative;
24
+ }
25
+ /**
26
+ * Analog control channel — distinct from the digital action bindings. Throttle/brake/steer/handbrake
27
+ * are continuous values ramped from held keys (a keyboard feels like a pedal) or driven directly from
28
+ * a gamepad axis via `setAnalog`. `sample(dt, isDown)` folds the current held-key state into the
29
+ * smoothed value; a `setAnalog` override replaces the digital target for that axis until cleared.
30
+ */
31
+ export class AxisChannel {
32
+ bindings;
33
+ smoothing;
34
+ current = { ...NEUTRAL_AXIS };
35
+ overrides = {};
36
+ constructor(config) {
37
+ this.bindings = config.bindings;
38
+ this.smoothing = config.smoothing ?? 6;
39
+ }
40
+ setAnalog(axis, value) {
41
+ this.overrides[axis] = clampAxis(value, AXIS_RANGE[axis]);
42
+ }
43
+ clearAnalog(axis) {
44
+ delete this.overrides[axis];
45
+ }
46
+ reset() {
47
+ this.current.throttle = 0;
48
+ this.current.brake = 0;
49
+ this.current.steer = 0;
50
+ this.current.handbrake = 0;
51
+ }
52
+ get value() {
53
+ return this.current;
54
+ }
55
+ sample(dt, isDown) {
56
+ for (const axis of Object.keys(this.bindings)) {
57
+ const range = AXIS_RANGE[axis];
58
+ const override = this.overrides[axis];
59
+ const target = override !== undefined ? override : clampAxis(digitalTarget(this.bindings[axis], isDown), range);
60
+ this.current[axis] = clampAxis(rampToward(this.current[axis], target, this.smoothing, dt), range);
61
+ }
62
+ return this.current;
63
+ }
64
+ }
65
+ export const DRIVE_AXIS_BINDINGS = {
66
+ throttle: { positive: ["KeyW", "ArrowUp"] },
67
+ brake: { positive: ["KeyS", "ArrowDown"] },
68
+ steer: { positive: ["KeyD", "ArrowRight"], negative: ["KeyA", "ArrowLeft"] },
69
+ handbrake: { positive: ["Space"] },
70
+ };
@@ -0,0 +1,24 @@
1
+ import type { Aim } from "../scene/spatial.js";
2
+ export type PointerVec3 = readonly [number, number, number];
3
+ /**
4
+ * Renderer-free result of a screen→world raycast. The shell's pointer service
5
+ * produces this from the cursor; core-side gameplay (item.use aim, click-to-move,
6
+ * ground-target abilities, pings) consumes it without touching three.js.
7
+ */
8
+ export interface PointerHit {
9
+ /** World-space point under the cursor (surface or ground plane). */
10
+ point: PointerVec3;
11
+ /** World-space surface normal at the hit; `[0, 1, 0]` for the ground plane. */
12
+ normal: PointerVec3;
13
+ /** Topmost scene-entity instance id under the cursor, or null over open ground. */
14
+ entity: string | null;
15
+ /** Topmost scene-object instance id under the cursor, or null. */
16
+ object: string | null;
17
+ }
18
+ export type PointerButton = "primary" | "secondary" | "middle";
19
+ /** Build an `origin → point` aim for `item.use` / projectiles, firing toward the cursor. */
20
+ export declare function aimToPoint(origin: PointerVec3, point: PointerVec3): Aim;
21
+ /** The move-to target of a pointer hit — sugar over `hit.point`. */
22
+ export declare function moveTargetFromHit(hit: PointerHit): PointerVec3;
23
+ /** Project a pointer hit onto the XZ plane for navmesh routing (`findPath` takes `[x, z]`). */
24
+ export declare function groundOf(hit: PointerHit): readonly [number, number];
@@ -0,0 +1,17 @@
1
+ /** Build an `origin → point` aim for `item.use` / projectiles, firing toward the cursor. */
2
+ export function aimToPoint(origin, point) {
3
+ const dx = point[0] - origin[0];
4
+ const dy = point[1] - origin[1];
5
+ const dz = point[2] - origin[2];
6
+ const length = Math.hypot(dx, dy, dz);
7
+ const direction = length < 1e-9 ? [0, 0, 1] : [dx / length, dy / length, dz / length];
8
+ return { origin, direction };
9
+ }
10
+ /** The move-to target of a pointer hit — sugar over `hit.point`. */
11
+ export function moveTargetFromHit(hit) {
12
+ return hit.point;
13
+ }
14
+ /** Project a pointer hit onto the XZ plane for navmesh routing (`findPath` takes `[x, z]`). */
15
+ export function groundOf(hit) {
16
+ return [hit.point[0], hit.point[2]];
17
+ }
@@ -0,0 +1,30 @@
1
+ export type ContextTargetKind = "entity" | "object";
2
+ /** One right-click verb: a label plus the command it dispatches (walk-then-act supported by args). */
3
+ export interface ContextVerb {
4
+ label: string;
5
+ command: string;
6
+ args?: Record<string, unknown>;
7
+ /** Greyed-out and non-dispatchable when true (e.g. requirement unmet). */
8
+ disabled?: boolean;
9
+ }
10
+ export declare function contextVerb(label: string, command: string, args?: Record<string, unknown>): ContextVerb;
11
+ export interface ContextMenu {
12
+ kind: ContextTargetKind;
13
+ targetId: string;
14
+ /** World point the click landed on, forwarded to dispatched commands for walk-then-act. */
15
+ point?: readonly [number, number, number];
16
+ verbs: readonly ContextVerb[];
17
+ }
18
+ export interface BuildContextMenuInput {
19
+ kind: ContextTargetKind;
20
+ targetId: string;
21
+ verbs: readonly ContextVerb[] | undefined;
22
+ point?: readonly [number, number, number];
23
+ }
24
+ /** Assemble a menu from a target's catalog verbs; null when the target lists none. */
25
+ export declare function buildContextMenu(input: BuildContextMenuInput): ContextMenu | null;
26
+ /**
27
+ * Command input a chosen verb dispatches: the verb's own args, plus the target id and
28
+ * the world point, so a single handler can walk the actor to the target then perform it.
29
+ */
30
+ export declare function contextVerbInput(menu: ContextMenu, verb: ContextVerb): Record<string, unknown>;
@@ -0,0 +1,22 @@
1
+ export function contextVerb(label, command, args) {
2
+ return args === undefined ? { label, command } : { label, command, args };
3
+ }
4
+ /** Assemble a menu from a target's catalog verbs; null when the target lists none. */
5
+ export function buildContextMenu(input) {
6
+ if (input.verbs === undefined || input.verbs.length === 0)
7
+ return null;
8
+ const menu = { kind: input.kind, targetId: input.targetId, verbs: input.verbs };
9
+ if (input.point !== undefined)
10
+ menu.point = input.point;
11
+ return menu;
12
+ }
13
+ /**
14
+ * Command input a chosen verb dispatches: the verb's own args, plus the target id and
15
+ * the world point, so a single handler can walk the actor to the target then perform it.
16
+ */
17
+ export function contextVerbInput(menu, verb) {
18
+ const base = { ...verb.args, target: menu.targetId, targetKind: menu.kind };
19
+ if (menu.point !== undefined)
20
+ base.point = menu.point;
21
+ return base;
22
+ }
@@ -0,0 +1,20 @@
1
+ export interface QteStep {
2
+ id: string;
3
+ action: string;
4
+ windowStart: number;
5
+ windowEnd: number;
6
+ }
7
+ export interface QteInputEvent {
8
+ action: string;
9
+ at: number;
10
+ }
11
+ export type QteOutcome = {
12
+ status: "success";
13
+ } | {
14
+ status: "fail";
15
+ atStep: string;
16
+ reason: "missed-window" | "wrong-action" | "too-early";
17
+ };
18
+ export declare function evaluateQteSequence(steps: readonly QteStep[], inputs: readonly QteInputEvent[]): QteOutcome;
19
+ export declare function pendingQteStep(steps: readonly QteStep[], elapsedSeconds: number): QteStep | null;
20
+ export declare function qteProgress(steps: readonly QteStep[], elapsedSeconds: number): number;
@@ -0,0 +1,24 @@
1
+ export function evaluateQteSequence(steps, inputs) {
2
+ for (let index = 0; index < steps.length; index += 1) {
3
+ const step = steps[index];
4
+ const input = inputs[index];
5
+ if (input === undefined)
6
+ return { status: "fail", atStep: step.id, reason: "missed-window" };
7
+ if (input.at < step.windowStart)
8
+ return { status: "fail", atStep: step.id, reason: "too-early" };
9
+ if (input.at > step.windowEnd)
10
+ return { status: "fail", atStep: step.id, reason: "missed-window" };
11
+ if (input.action !== step.action)
12
+ return { status: "fail", atStep: step.id, reason: "wrong-action" };
13
+ }
14
+ return { status: "success" };
15
+ }
16
+ export function pendingQteStep(steps, elapsedSeconds) {
17
+ return (steps.find((step) => elapsedSeconds >= step.windowStart && elapsedSeconds <= step.windowEnd) ?? null);
18
+ }
19
+ export function qteProgress(steps, elapsedSeconds) {
20
+ if (steps.length === 0)
21
+ return 1;
22
+ const passed = steps.filter((step) => elapsedSeconds > step.windowEnd).length;
23
+ return passed / steps.length;
24
+ }
@@ -0,0 +1,20 @@
1
+ export interface SkillCheckZone {
2
+ start: number;
3
+ end: number;
4
+ }
5
+ export interface SkillCheckConfig {
6
+ trackWidth: number;
7
+ zone: SkillCheckZone;
8
+ markerPeriod: number;
9
+ window: number;
10
+ zoneDriftPerSecond?: number;
11
+ }
12
+ export interface SkillCheckResult {
13
+ success: boolean;
14
+ timedOut: boolean;
15
+ markerPosition: number;
16
+ zone: SkillCheckZone;
17
+ }
18
+ export declare function skillCheckMarkerPosition(config: SkillCheckConfig, elapsedSeconds: number): number;
19
+ export declare function skillCheckZoneAt(config: SkillCheckConfig, elapsedSeconds: number): SkillCheckZone;
20
+ export declare function evaluateSkillCheck(config: SkillCheckConfig, elapsedSeconds: number): SkillCheckResult;
@@ -0,0 +1,26 @@
1
+ function bounce(t, period, max) {
2
+ if (period <= 0 || max <= 0)
3
+ return 0;
4
+ const cycle = max * 2;
5
+ const phase = ((t % period) / period) * cycle;
6
+ return phase <= max ? phase : cycle - phase;
7
+ }
8
+ export function skillCheckMarkerPosition(config, elapsedSeconds) {
9
+ return bounce(Math.max(0, elapsedSeconds), config.markerPeriod, config.trackWidth);
10
+ }
11
+ export function skillCheckZoneAt(config, elapsedSeconds) {
12
+ const drift = config.zoneDriftPerSecond ?? 0;
13
+ if (drift === 0)
14
+ return config.zone;
15
+ const width = config.zone.end - config.zone.start;
16
+ const travel = Math.max(0, config.trackWidth - width);
17
+ const start = bounce(Math.max(0, elapsedSeconds) * Math.abs(drift), config.markerPeriod, travel);
18
+ return { start, end: start + width };
19
+ }
20
+ export function evaluateSkillCheck(config, elapsedSeconds) {
21
+ const timedOut = elapsedSeconds > config.window;
22
+ const markerPosition = skillCheckMarkerPosition(config, elapsedSeconds);
23
+ const zone = skillCheckZoneAt(config, elapsedSeconds);
24
+ const inZone = markerPosition >= zone.start && markerPosition <= zone.end;
25
+ return { success: !timedOut && inZone, timedOut, markerPosition, zone };
26
+ }
@@ -0,0 +1,56 @@
1
+ export type Cell = readonly [number, number];
2
+ export type Footprint = readonly Cell[];
3
+ export type Rotation = 0 | 1 | 2 | 3;
4
+ export interface ShapedItem<T> {
5
+ id: string;
6
+ value: T;
7
+ footprint: Footprint;
8
+ }
9
+ export interface Placement<T> {
10
+ id: string;
11
+ value: T;
12
+ footprint: Footprint;
13
+ origin: Cell;
14
+ rotation: Rotation;
15
+ }
16
+ export interface ShapedGrid<T> {
17
+ readonly width: number;
18
+ readonly height: number;
19
+ readonly placements: readonly Placement<T>[];
20
+ }
21
+ export type ShapedRejection = "out-of-bounds" | "overlap" | "duplicate-id" | "unknown-id";
22
+ export interface ShapedPlaceResult<T> {
23
+ status: "ok";
24
+ grid: ShapedGrid<T>;
25
+ }
26
+ export interface ShapedRejected {
27
+ status: "rejected";
28
+ reason: ShapedRejection;
29
+ detail?: string;
30
+ }
31
+ export type ShapedResult<T> = ShapedPlaceResult<T> | ShapedRejected;
32
+ export declare function normalizeFootprint(footprint: Footprint): Footprint;
33
+ export declare function rotateFootprint(footprint: Footprint, rotation: Rotation): Footprint;
34
+ export declare function occupiedCells(footprint: Footprint, origin: Cell, rotation: Rotation): Cell[];
35
+ export declare function createShapedGrid<T>(width: number, height: number): ShapedGrid<T>;
36
+ export declare function canPlace<T>(grid: ShapedGrid<T>, footprint: Footprint, origin: Cell, rotation: Rotation, ignoreId?: string): ShapedRejection | null;
37
+ export declare function placeShaped<T>(grid: ShapedGrid<T>, item: ShapedItem<T>, origin: Cell, rotation?: Rotation): ShapedResult<T>;
38
+ export declare function removeShaped<T>(grid: ShapedGrid<T>, id: string): ShapedResult<T>;
39
+ export declare function moveShaped<T>(grid: ShapedGrid<T>, id: string, origin: Cell, rotation?: Rotation): ShapedResult<T>;
40
+ export declare function cellOccupant<T>(grid: ShapedGrid<T>, cell: Cell): string | null;
41
+ export interface GridAdjacencyQuery {
42
+ cellsOf(id: string): readonly Cell[];
43
+ neighborsOf(id: string): readonly string[];
44
+ touching(idA: string, idB: string): boolean;
45
+ adjacentCells(cells: readonly Cell[]): readonly Cell[];
46
+ }
47
+ export declare function gridAdjacencyQuery<T>(grid: ShapedGrid<T>, options?: {
48
+ diagonal?: boolean;
49
+ }): GridAdjacencyQuery;
50
+ export declare function cellFromPoint(point: {
51
+ x: number;
52
+ y: number;
53
+ }, cellSize: number, gridOrigin?: {
54
+ x: number;
55
+ y: number;
56
+ }): Cell;
@@ -0,0 +1,176 @@
1
+ function cellKey(cell) {
2
+ return `${cell[0]},${cell[1]}`;
3
+ }
4
+ export function normalizeFootprint(footprint) {
5
+ if (footprint.length === 0)
6
+ return [];
7
+ let minC = Infinity;
8
+ let minR = Infinity;
9
+ for (const [c, r] of footprint) {
10
+ if (c < minC)
11
+ minC = c;
12
+ if (r < minR)
13
+ minR = r;
14
+ }
15
+ const shifted = footprint.map(([c, r]) => [c - minC, r - minR]);
16
+ shifted.sort((a, b) => (a[1] === b[1] ? a[0] - b[0] : a[1] - b[1]));
17
+ const seen = new Set();
18
+ const out = [];
19
+ for (const cell of shifted) {
20
+ const key = cellKey(cell);
21
+ if (!seen.has(key)) {
22
+ seen.add(key);
23
+ out.push(cell);
24
+ }
25
+ }
26
+ return out;
27
+ }
28
+ export function rotateFootprint(footprint, rotation) {
29
+ const turns = ((rotation % 4) + 4) % 4;
30
+ let cells = footprint.map(([c, r]) => [c, r]);
31
+ for (let i = 0; i < turns; i++) {
32
+ cells = cells.map(([c, r]) => [-r, c]);
33
+ }
34
+ return normalizeFootprint(cells);
35
+ }
36
+ export function occupiedCells(footprint, origin, rotation) {
37
+ const rotated = rotateFootprint(footprint, rotation);
38
+ return rotated.map(([c, r]) => [c + origin[0], r + origin[1]]);
39
+ }
40
+ export function createShapedGrid(width, height) {
41
+ if (width <= 0 || height <= 0)
42
+ throw new Error("shaped grid needs positive dimensions");
43
+ return { width, height, placements: [] };
44
+ }
45
+ function inBounds(grid, cells) {
46
+ return cells.every(([c, r]) => c >= 0 && r >= 0 && c < grid.width && r < grid.height);
47
+ }
48
+ function occupancyMap(grid, ignoreId) {
49
+ const map = new Map();
50
+ for (const placement of grid.placements) {
51
+ if (placement.id === ignoreId)
52
+ continue;
53
+ for (const cell of occupiedCells(placement.footprint, placement.origin, placement.rotation)) {
54
+ map.set(cellKey(cell), placement.id);
55
+ }
56
+ }
57
+ return map;
58
+ }
59
+ export function canPlace(grid, footprint, origin, rotation, ignoreId) {
60
+ const cells = occupiedCells(footprint, origin, rotation);
61
+ if (!inBounds(grid, cells))
62
+ return "out-of-bounds";
63
+ const occupied = occupancyMap(grid, ignoreId);
64
+ for (const cell of cells) {
65
+ if (occupied.has(cellKey(cell)))
66
+ return "overlap";
67
+ }
68
+ return null;
69
+ }
70
+ export function placeShaped(grid, item, origin, rotation = 0) {
71
+ if (grid.placements.some((p) => p.id === item.id)) {
72
+ return { status: "rejected", reason: "duplicate-id", detail: item.id };
73
+ }
74
+ const rejection = canPlace(grid, item.footprint, origin, rotation);
75
+ if (rejection !== null)
76
+ return { status: "rejected", reason: rejection };
77
+ const placement = {
78
+ id: item.id,
79
+ value: item.value,
80
+ footprint: item.footprint,
81
+ origin,
82
+ rotation,
83
+ };
84
+ return { status: "ok", grid: { ...grid, placements: [...grid.placements, placement] } };
85
+ }
86
+ export function removeShaped(grid, id) {
87
+ if (!grid.placements.some((p) => p.id === id)) {
88
+ return { status: "rejected", reason: "unknown-id", detail: id };
89
+ }
90
+ return { status: "ok", grid: { ...grid, placements: grid.placements.filter((p) => p.id !== id) } };
91
+ }
92
+ export function moveShaped(grid, id, origin, rotation) {
93
+ const existing = grid.placements.find((p) => p.id === id);
94
+ if (existing === undefined)
95
+ return { status: "rejected", reason: "unknown-id", detail: id };
96
+ const nextRotation = rotation ?? existing.rotation;
97
+ const rejection = canPlace(grid, existing.footprint, origin, nextRotation, id);
98
+ if (rejection !== null)
99
+ return { status: "rejected", reason: rejection };
100
+ return {
101
+ status: "ok",
102
+ grid: {
103
+ ...grid,
104
+ placements: grid.placements.map((p) => p.id === id ? { ...p, origin, rotation: nextRotation } : p),
105
+ },
106
+ };
107
+ }
108
+ export function cellOccupant(grid, cell) {
109
+ return occupancyMap(grid).get(cellKey(cell)) ?? null;
110
+ }
111
+ const ORTHOGONAL = [
112
+ [1, 0],
113
+ [-1, 0],
114
+ [0, 1],
115
+ [0, -1],
116
+ ];
117
+ const DIAGONAL = [
118
+ [1, 1],
119
+ [1, -1],
120
+ [-1, 1],
121
+ [-1, -1],
122
+ ];
123
+ export function gridAdjacencyQuery(grid, options = {}) {
124
+ const directions = options.diagonal ? [...ORTHOGONAL, ...DIAGONAL] : ORTHOGONAL;
125
+ const cellsById = new Map();
126
+ const owner = new Map();
127
+ for (const placement of grid.placements) {
128
+ const cells = occupiedCells(placement.footprint, placement.origin, placement.rotation);
129
+ cellsById.set(placement.id, cells);
130
+ for (const cell of cells)
131
+ owner.set(cellKey(cell), placement.id);
132
+ }
133
+ const cellsOf = (id) => cellsById.get(id) ?? [];
134
+ const neighborsOf = (id) => {
135
+ const found = new Set();
136
+ for (const cell of cellsOf(id)) {
137
+ for (const [dc, dr] of directions) {
138
+ const key = cellKey([cell[0] + dc, cell[1] + dr]);
139
+ const other = owner.get(key);
140
+ if (other !== undefined && other !== id)
141
+ found.add(other);
142
+ }
143
+ }
144
+ return [...found];
145
+ };
146
+ return {
147
+ cellsOf,
148
+ neighborsOf,
149
+ touching: (idA, idB) => neighborsOf(idA).includes(idB),
150
+ adjacentCells(cells) {
151
+ const own = new Set(cells.map(cellKey));
152
+ const found = new Set();
153
+ const out = [];
154
+ for (const cell of cells) {
155
+ for (const [dc, dr] of directions) {
156
+ const target = [cell[0] + dc, cell[1] + dr];
157
+ const key = cellKey(target);
158
+ if (own.has(key) || found.has(key))
159
+ continue;
160
+ if (target[0] < 0 || target[1] < 0 || target[0] >= grid.width || target[1] >= grid.height) {
161
+ continue;
162
+ }
163
+ found.add(key);
164
+ out.push(target);
165
+ }
166
+ }
167
+ return out;
168
+ },
169
+ };
170
+ }
171
+ export function cellFromPoint(point, cellSize, gridOrigin = { x: 0, y: 0 }) {
172
+ return [
173
+ Math.floor((point.x - gridOrigin.x) / cellSize),
174
+ Math.floor((point.y - gridOrigin.y) / cellSize),
175
+ ];
176
+ }
@@ -0,0 +1,52 @@
1
+ export type StorageTier = "carried" | "banked";
2
+ export interface ItemStack {
3
+ itemId: string;
4
+ count: number;
5
+ }
6
+ export interface ContainerSnapshot {
7
+ inventoryId: string;
8
+ tier: StorageTier;
9
+ items: readonly ItemStack[];
10
+ }
11
+ export interface DeathPartition {
12
+ kept: readonly ItemStack[];
13
+ lost: readonly ItemStack[];
14
+ }
15
+ export declare function tierOf(tiers: Record<string, StorageTier>, inventoryId: string): StorageTier;
16
+ export declare function partitionOnDeath(containers: readonly ContainerSnapshot[]): DeathPartition;
17
+ export interface DeliveryEntry {
18
+ id: string;
19
+ userId: string;
20
+ inventoryId: string;
21
+ items: readonly ItemStack[];
22
+ deliverAt: number;
23
+ }
24
+ export interface ScheduledDelivery {
25
+ userId: string;
26
+ inventoryId: string;
27
+ items: readonly ItemStack[];
28
+ deliverAt: number;
29
+ }
30
+ export interface DeliveryQueue {
31
+ schedule(entry: ScheduledDelivery & {
32
+ id?: string;
33
+ }): DeliveryEntry;
34
+ due(now: number): readonly DeliveryEntry[];
35
+ claimDue(now: number): readonly DeliveryEntry[];
36
+ pending(userId?: string): readonly DeliveryEntry[];
37
+ cancel(id: string): boolean;
38
+ }
39
+ export declare function createDeliveryQueue(): DeliveryQueue;
40
+ export interface InsurancePolicy {
41
+ isInsured(itemId: string): boolean;
42
+ returnInventoryId: string;
43
+ delaySeconds: number | [number, number];
44
+ }
45
+ export declare function insureLost(lost: readonly ItemStack[], policy: InsurancePolicy, userId: string, now: number, rng?: () => number): ScheduledDelivery | null;
46
+ export interface ConsolationPolicy {
47
+ loadoutId: string;
48
+ when?: "always" | "if-carried-empty";
49
+ }
50
+ export declare function resolveConsolation(policy: ConsolationPolicy, partition: DeathPartition): {
51
+ loadoutId: string;
52
+ } | null;
@@ -0,0 +1,77 @@
1
+ export function tierOf(tiers, inventoryId) {
2
+ return tiers[inventoryId] ?? "carried";
3
+ }
4
+ function mergeStacks(stacks) {
5
+ const totals = new Map();
6
+ for (const stack of stacks)
7
+ totals.set(stack.itemId, (totals.get(stack.itemId) ?? 0) + stack.count);
8
+ const out = [];
9
+ for (const [itemId, count] of totals)
10
+ if (count > 0)
11
+ out.push({ itemId, count });
12
+ return out;
13
+ }
14
+ export function partitionOnDeath(containers) {
15
+ const kept = [];
16
+ const lost = [];
17
+ for (const container of containers) {
18
+ (container.tier === "banked" ? kept : lost).push(...container.items);
19
+ }
20
+ return { kept: mergeStacks(kept), lost: mergeStacks(lost) };
21
+ }
22
+ export function createDeliveryQueue() {
23
+ const entries = new Map();
24
+ let counter = 0;
25
+ return {
26
+ schedule(entry) {
27
+ const id = entry.id ?? `delivery_${(counter += 1)}`;
28
+ const stored = {
29
+ id,
30
+ userId: entry.userId,
31
+ inventoryId: entry.inventoryId,
32
+ items: entry.items,
33
+ deliverAt: entry.deliverAt,
34
+ };
35
+ entries.set(id, stored);
36
+ return stored;
37
+ },
38
+ due(now) {
39
+ return [...entries.values()].filter((e) => e.deliverAt <= now);
40
+ },
41
+ claimDue(now) {
42
+ const ready = [...entries.values()].filter((e) => e.deliverAt <= now);
43
+ for (const entry of ready)
44
+ entries.delete(entry.id);
45
+ return ready;
46
+ },
47
+ pending(userId) {
48
+ const all = [...entries.values()];
49
+ return userId === undefined ? all : all.filter((e) => e.userId === userId);
50
+ },
51
+ cancel(id) {
52
+ return entries.delete(id);
53
+ },
54
+ };
55
+ }
56
+ function resolveDelay(delay, rng) {
57
+ if (typeof delay === "number")
58
+ return delay;
59
+ const [min, max] = delay;
60
+ return min + rng() * (max - min);
61
+ }
62
+ export function insureLost(lost, policy, userId, now, rng = Math.random) {
63
+ const items = lost.filter((stack) => policy.isInsured(stack.itemId));
64
+ if (items.length === 0)
65
+ return null;
66
+ return {
67
+ userId,
68
+ inventoryId: policy.returnInventoryId,
69
+ items,
70
+ deliverAt: now + resolveDelay(policy.delaySeconds, rng),
71
+ };
72
+ }
73
+ export function resolveConsolation(policy, partition) {
74
+ if (policy.when === "if-carried-empty" && partition.lost.length > 0)
75
+ return null;
76
+ return { loadoutId: policy.loadoutId };
77
+ }