@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,52 @@
1
+ import type { Tile } from "./tacticalGrid.js";
2
+ export interface SurfaceKindDef {
3
+ id: string;
4
+ duration?: number;
5
+ }
6
+ export interface SurfaceReaction {
7
+ when: readonly [string, string];
8
+ result: string | null;
9
+ }
10
+ export interface SurfaceLayerConfig {
11
+ kinds: readonly SurfaceKindDef[];
12
+ reactions?: readonly SurfaceReaction[];
13
+ }
14
+ export interface SurfacePatch {
15
+ duration?: number;
16
+ }
17
+ export type SurfaceEvent = {
18
+ kind: "apply";
19
+ tile: Tile;
20
+ surface: string;
21
+ } | {
22
+ kind: "react";
23
+ tile: Tile;
24
+ consumed: readonly [string, string];
25
+ produced: string | null;
26
+ } | {
27
+ kind: "expire";
28
+ tile: Tile;
29
+ surface: string;
30
+ };
31
+ export interface SurfaceCellKind {
32
+ surface: string;
33
+ remaining: number | null;
34
+ }
35
+ export interface SurfaceCell {
36
+ tile: Tile;
37
+ kinds: SurfaceCellKind[];
38
+ }
39
+ export type SurfaceLayerSnapshot = Record<string, Record<string, number | null>>;
40
+ export interface SurfaceLayer {
41
+ apply(tile: Tile, surface: string, patch?: SurfacePatch): SurfaceEvent[];
42
+ has(tile: Tile, surface: string): boolean;
43
+ kindsAt(tile: Tile): string[];
44
+ remove(tile: Tile, surface: string): void;
45
+ clearTile(tile: Tile): void;
46
+ clear(): void;
47
+ tick(dt: number): SurfaceEvent[];
48
+ cells(): SurfaceCell[];
49
+ capture(): SurfaceLayerSnapshot;
50
+ restore(snapshot: SurfaceLayerSnapshot): void;
51
+ }
52
+ export declare function createSurfaceLayer(config: SurfaceLayerConfig): SurfaceLayer;
@@ -0,0 +1,135 @@
1
+ function key(tile) {
2
+ return `${tile[0]},${tile[1]}`;
3
+ }
4
+ function parseKey(k) {
5
+ const [c, r] = k.split(",");
6
+ return [Number(c), Number(r)];
7
+ }
8
+ function pairKey(a, b) {
9
+ return `${a}|${b}`;
10
+ }
11
+ export function createSurfaceLayer(config) {
12
+ const durations = new Map();
13
+ for (const k of config.kinds)
14
+ durations.set(k.id, k.duration);
15
+ const reactions = new Map();
16
+ for (const reaction of config.reactions ?? []) {
17
+ const [a, b] = reaction.when;
18
+ reactions.set(pairKey(a, b), reaction.result);
19
+ reactions.set(pairKey(b, a), reaction.result);
20
+ }
21
+ const cells = new Map();
22
+ function cellOf(tile) {
23
+ const k = key(tile);
24
+ let cell = cells.get(k);
25
+ if (cell === undefined) {
26
+ cell = new Map();
27
+ cells.set(k, cell);
28
+ }
29
+ return cell;
30
+ }
31
+ function initialRemaining(surface, patch) {
32
+ if (patch?.duration !== undefined)
33
+ return patch.duration;
34
+ const configured = durations.get(surface);
35
+ return configured === undefined ? null : configured;
36
+ }
37
+ function resolveReactions(tile, events) {
38
+ const cell = cellOf(tile);
39
+ for (;;) {
40
+ const active = [...cell.keys()];
41
+ let matched = null;
42
+ outer: for (let i = 0; i < active.length; i += 1) {
43
+ for (let j = i + 1; j < active.length; j += 1) {
44
+ const result = reactions.get(pairKey(active[i], active[j]));
45
+ if (result !== undefined) {
46
+ matched = { a: active[i], b: active[j], result };
47
+ break outer;
48
+ }
49
+ }
50
+ }
51
+ if (matched === null)
52
+ return;
53
+ cell.delete(matched.a);
54
+ cell.delete(matched.b);
55
+ if (matched.result !== null)
56
+ cell.set(matched.result, initialRemaining(matched.result));
57
+ events.push({ kind: "react", tile, consumed: [matched.a, matched.b], produced: matched.result });
58
+ }
59
+ }
60
+ return {
61
+ apply: (tile, surface, patch) => {
62
+ const events = [];
63
+ const cell = cellOf(tile);
64
+ cell.set(surface, initialRemaining(surface, patch));
65
+ events.push({ kind: "apply", tile, surface });
66
+ resolveReactions(tile, events);
67
+ if (cells.get(key(tile))?.size === 0)
68
+ cells.delete(key(tile));
69
+ return events;
70
+ },
71
+ has: (tile, surface) => cells.get(key(tile))?.has(surface) ?? false,
72
+ kindsAt: (tile) => [...(cells.get(key(tile))?.keys() ?? [])],
73
+ remove: (tile, surface) => {
74
+ const cell = cells.get(key(tile));
75
+ if (cell === undefined)
76
+ return;
77
+ cell.delete(surface);
78
+ if (cell.size === 0)
79
+ cells.delete(key(tile));
80
+ },
81
+ clearTile: (tile) => {
82
+ cells.delete(key(tile));
83
+ },
84
+ clear: () => cells.clear(),
85
+ tick: (dt) => {
86
+ const events = [];
87
+ for (const [k, cell] of [...cells]) {
88
+ for (const [surface, remaining] of [...cell]) {
89
+ if (remaining === null)
90
+ continue;
91
+ const next = remaining - dt;
92
+ if (next <= 0) {
93
+ cell.delete(surface);
94
+ events.push({ kind: "expire", tile: parseKey(k), surface });
95
+ }
96
+ else {
97
+ cell.set(surface, next);
98
+ }
99
+ }
100
+ if (cell.size === 0)
101
+ cells.delete(k);
102
+ }
103
+ return events;
104
+ },
105
+ cells: () => {
106
+ const out = [];
107
+ for (const [k, cell] of cells) {
108
+ out.push({
109
+ tile: parseKey(k),
110
+ kinds: [...cell].map(([surface, remaining]) => ({ surface, remaining })),
111
+ });
112
+ }
113
+ return out;
114
+ },
115
+ capture: () => {
116
+ const snapshot = {};
117
+ for (const [k, cell] of cells) {
118
+ const entry = {};
119
+ for (const [surface, remaining] of cell)
120
+ entry[surface] = remaining;
121
+ snapshot[k] = entry;
122
+ }
123
+ return snapshot;
124
+ },
125
+ restore: (snapshot) => {
126
+ cells.clear();
127
+ for (const [k, entry] of Object.entries(snapshot)) {
128
+ const cell = new Map();
129
+ for (const [surface, remaining] of Object.entries(entry))
130
+ cell.set(surface, remaining);
131
+ cells.set(k, cell);
132
+ }
133
+ },
134
+ };
135
+ }
@@ -0,0 +1,57 @@
1
+ export type Tile = readonly [number, number];
2
+ export interface TacticalGridConfig {
3
+ width: number;
4
+ height: number;
5
+ blocked?: readonly Tile[];
6
+ diagonal?: boolean;
7
+ }
8
+ export interface ReachableOptions {
9
+ passThrough?: readonly string[];
10
+ diagonal?: boolean;
11
+ }
12
+ export interface ReachableTile {
13
+ tile: Tile;
14
+ cost: number;
15
+ }
16
+ export type PushObstacle = "wall" | "edge";
17
+ export interface PushCollision {
18
+ mover: string;
19
+ into: string | PushObstacle;
20
+ at: Tile;
21
+ }
22
+ export interface PushMove {
23
+ id: string;
24
+ from: Tile;
25
+ to: Tile;
26
+ }
27
+ export interface PushResult {
28
+ moves: PushMove[];
29
+ collisions: PushCollision[];
30
+ }
31
+ export interface PushOptions {
32
+ distance?: number;
33
+ chain?: boolean;
34
+ }
35
+ export interface TacticalGridSnapshot {
36
+ blocked: string[];
37
+ occupants: Record<string, string>;
38
+ }
39
+ export interface TacticalGrid {
40
+ readonly width: number;
41
+ readonly height: number;
42
+ inBounds(tile: Tile): boolean;
43
+ isBlocked(tile: Tile): boolean;
44
+ setBlocked(tile: Tile, blocked: boolean): void;
45
+ occupantAt(tile: Tile): string | null;
46
+ tileOf(id: string): Tile | null;
47
+ place(id: string, tile: Tile): boolean;
48
+ remove(id: string): void;
49
+ move(id: string, tile: Tile): boolean;
50
+ neighbors(tile: Tile, diagonal?: boolean): Tile[];
51
+ reachable(from: Tile, budget: number, options?: ReachableOptions): ReachableTile[];
52
+ path(from: Tile, to: Tile, options?: ReachableOptions): Tile[] | null;
53
+ push(id: string, direction: Tile, options?: PushOptions): PushResult;
54
+ capture(): TacticalGridSnapshot;
55
+ restore(snapshot: TacticalGridSnapshot): void;
56
+ }
57
+ export declare function createTacticalGrid(config: TacticalGridConfig): TacticalGrid;
@@ -0,0 +1,244 @@
1
+ function key(tile) {
2
+ return `${tile[0]},${tile[1]}`;
3
+ }
4
+ function parseKey(k) {
5
+ const [c, r] = k.split(",");
6
+ return [Number(c), Number(r)];
7
+ }
8
+ export function createTacticalGrid(config) {
9
+ const width = config.width;
10
+ const height = config.height;
11
+ const defaultDiagonal = config.diagonal ?? false;
12
+ const blocked = new Set((config.blocked ?? []).map(key));
13
+ const occupants = new Map();
14
+ const tiles = new Map();
15
+ function inBounds(tile) {
16
+ return tile[0] >= 0 && tile[0] < width && tile[1] >= 0 && tile[1] < height;
17
+ }
18
+ function isBlocked(tile) {
19
+ return blocked.has(key(tile));
20
+ }
21
+ function occupantAt(tile) {
22
+ return occupants.get(key(tile)) ?? null;
23
+ }
24
+ function tileOf(id) {
25
+ const t = tiles.get(id);
26
+ return t === undefined ? null : [t[0], t[1]];
27
+ }
28
+ function place(id, tile) {
29
+ if (!inBounds(tile) || isBlocked(tile))
30
+ return false;
31
+ if (occupants.has(key(tile)))
32
+ return false;
33
+ const prev = tiles.get(id);
34
+ if (prev !== undefined)
35
+ occupants.delete(key(prev));
36
+ occupants.set(key(tile), id);
37
+ tiles.set(id, [tile[0], tile[1]]);
38
+ return true;
39
+ }
40
+ function remove(id) {
41
+ const t = tiles.get(id);
42
+ if (t === undefined)
43
+ return;
44
+ occupants.delete(key(t));
45
+ tiles.delete(id);
46
+ }
47
+ function move(id, tile) {
48
+ if (!tiles.has(id))
49
+ return false;
50
+ if (!inBounds(tile) || isBlocked(tile))
51
+ return false;
52
+ const holder = occupants.get(key(tile));
53
+ if (holder !== undefined && holder !== id)
54
+ return false;
55
+ const prev = tiles.get(id);
56
+ occupants.delete(key(prev));
57
+ occupants.set(key(tile), id);
58
+ tiles.set(id, [tile[0], tile[1]]);
59
+ return true;
60
+ }
61
+ function neighbors(tile, diagonal = defaultDiagonal) {
62
+ const orthogonal = [
63
+ [tile[0] + 1, tile[1]],
64
+ [tile[0] - 1, tile[1]],
65
+ [tile[0], tile[1] + 1],
66
+ [tile[0], tile[1] - 1],
67
+ ];
68
+ const diagonals = [
69
+ [tile[0] + 1, tile[1] + 1],
70
+ [tile[0] + 1, tile[1] - 1],
71
+ [tile[0] - 1, tile[1] + 1],
72
+ [tile[0] - 1, tile[1] - 1],
73
+ ];
74
+ return (diagonal ? [...orthogonal, ...diagonals] : orthogonal).filter(inBounds);
75
+ }
76
+ function enterable(tile, passThrough) {
77
+ if (!inBounds(tile) || isBlocked(tile))
78
+ return false;
79
+ const holder = occupants.get(key(tile));
80
+ return holder === undefined || passThrough.has(holder);
81
+ }
82
+ function reachable(from, budget, options) {
83
+ const diagonal = options?.diagonal ?? defaultDiagonal;
84
+ const passThrough = new Set(options?.passThrough ?? []);
85
+ const mover = occupantAt(from);
86
+ if (mover !== null)
87
+ passThrough.add(mover);
88
+ const best = new Map([[key(from), 0]]);
89
+ const frontier = [from];
90
+ while (frontier.length > 0) {
91
+ const current = frontier.shift();
92
+ const cost = best.get(key(current));
93
+ if (cost >= budget)
94
+ continue;
95
+ for (const next of neighbors(current, diagonal)) {
96
+ if (!enterable(next, passThrough))
97
+ continue;
98
+ const nextCost = cost + 1;
99
+ const known = best.get(key(next));
100
+ if (known !== undefined && known <= nextCost)
101
+ continue;
102
+ best.set(key(next), nextCost);
103
+ frontier.push(next);
104
+ }
105
+ }
106
+ const result = [];
107
+ for (const [k, cost] of best) {
108
+ if (k === key(from))
109
+ continue;
110
+ result.push({ tile: parseKey(k), cost });
111
+ }
112
+ return result.sort((a, b) => a.cost - b.cost || a.tile[0] - b.tile[0] || a.tile[1] - b.tile[1]);
113
+ }
114
+ function path(from, to, options) {
115
+ if (!inBounds(from) || !inBounds(to))
116
+ return null;
117
+ const diagonal = options?.diagonal ?? defaultDiagonal;
118
+ const passThrough = new Set(options?.passThrough ?? []);
119
+ const mover = occupantAt(from);
120
+ if (mover !== null)
121
+ passThrough.add(mover);
122
+ if (key(from) === key(to))
123
+ return [[from[0], from[1]]];
124
+ const cameFrom = new Map();
125
+ const visited = new Set([key(from)]);
126
+ const frontier = [from];
127
+ while (frontier.length > 0) {
128
+ const current = frontier.shift();
129
+ for (const next of neighbors(current, diagonal)) {
130
+ if (visited.has(key(next)))
131
+ continue;
132
+ const isGoal = key(next) === key(to);
133
+ if (!isGoal && !enterable(next, passThrough))
134
+ continue;
135
+ if (!isGoal && isBlocked(next))
136
+ continue;
137
+ visited.add(key(next));
138
+ cameFrom.set(key(next), key(current));
139
+ if (isGoal) {
140
+ const chain = [to];
141
+ let step = key(to);
142
+ while (step !== key(from)) {
143
+ step = cameFrom.get(step);
144
+ chain.push(parseKey(step));
145
+ }
146
+ return chain.reverse();
147
+ }
148
+ frontier.push(next);
149
+ }
150
+ }
151
+ return null;
152
+ }
153
+ function push(id, direction, options) {
154
+ const distance = options?.distance ?? 1;
155
+ const chain = options?.chain ?? false;
156
+ const result = { moves: [], collisions: [] };
157
+ const origins = new Map();
158
+ function record(unitId) {
159
+ const from = origins.get(unitId);
160
+ const to = tileOf(unitId);
161
+ if (from === undefined || to === null)
162
+ return;
163
+ if (from[0] === to[0] && from[1] === to[1])
164
+ return;
165
+ const existing = result.moves.find((m) => m.id === unitId);
166
+ if (existing !== undefined)
167
+ existing.to = to;
168
+ else
169
+ result.moves.push({ id: unitId, from, to });
170
+ }
171
+ function pushUnit(unitId, steps, depth) {
172
+ const start = tileOf(unitId);
173
+ if (start === null)
174
+ return;
175
+ if (!origins.has(unitId))
176
+ origins.set(unitId, start);
177
+ let taken = 0;
178
+ while (taken < steps) {
179
+ const cur = tileOf(unitId);
180
+ const next = [cur[0] + direction[0], cur[1] + direction[1]];
181
+ if (!inBounds(next)) {
182
+ result.collisions.push({ mover: unitId, into: "edge", at: cur });
183
+ break;
184
+ }
185
+ if (isBlocked(next)) {
186
+ result.collisions.push({ mover: unitId, into: "wall", at: cur });
187
+ break;
188
+ }
189
+ const occ = occupantAt(next);
190
+ if (occ !== null) {
191
+ result.collisions.push({ mover: unitId, into: occ, at: cur });
192
+ if (!chain || depth >= width * height)
193
+ break;
194
+ pushUnit(occ, steps - taken, depth + 1);
195
+ if (occupantAt(next) !== null)
196
+ break;
197
+ }
198
+ move(unitId, next);
199
+ taken += 1;
200
+ }
201
+ record(unitId);
202
+ }
203
+ pushUnit(id, distance, 0);
204
+ return result;
205
+ }
206
+ return {
207
+ width,
208
+ height,
209
+ inBounds,
210
+ isBlocked,
211
+ setBlocked: (tile, value) => {
212
+ if (value)
213
+ blocked.add(key(tile));
214
+ else
215
+ blocked.delete(key(tile));
216
+ },
217
+ occupantAt,
218
+ tileOf,
219
+ place,
220
+ remove,
221
+ move,
222
+ neighbors,
223
+ reachable,
224
+ path,
225
+ push,
226
+ capture: () => {
227
+ const occ = {};
228
+ for (const [k, unitId] of occupants)
229
+ occ[k] = unitId;
230
+ return { blocked: [...blocked], occupants: occ };
231
+ },
232
+ restore: (snapshot) => {
233
+ blocked.clear();
234
+ for (const k of snapshot.blocked)
235
+ blocked.add(k);
236
+ occupants.clear();
237
+ tiles.clear();
238
+ for (const [k, unitId] of Object.entries(snapshot.occupants)) {
239
+ occupants.set(k, unitId);
240
+ tiles.set(unitId, parseKey(k));
241
+ }
242
+ },
243
+ };
244
+ }
@@ -0,0 +1,40 @@
1
+ export interface BeatClockConfig {
2
+ bpm: number;
3
+ /** Beats per bar/measure, for bar-relative UI (rhythm-game combo windows). Default 4. */
4
+ beatsPerBar?: number;
5
+ }
6
+ export interface BeatSnapshot {
7
+ /** Fractional beat count since the clock started. */
8
+ beat: number;
9
+ beatIndex: number;
10
+ bar: number;
11
+ beatInBar: number;
12
+ /** 0..1 fractional position within the current beat. */
13
+ phase: number;
14
+ }
15
+ export interface BeatClock {
16
+ /** Advance by a game-time delta (already scaled by ctx.time — never wall-clock). Returns the resulting snapshot and fires onBeat once per newly crossed integer beat. */
17
+ advance(gameDt: number): BeatSnapshot;
18
+ snapshot(): BeatSnapshot;
19
+ /** Elapsed game-seconds since the clock started. */
20
+ now(): number;
21
+ beatDurationSec(): number;
22
+ bpm(): number;
23
+ }
24
+ export declare function createBeatClock(config: BeatClockConfig, onBeat?: (beatIndex: number) => void): BeatClock;
25
+ /** The absolute beat-time (game-seconds) that a press at `nowSec` quantizes to: the same instant if it lands on a beat boundary, else the next one. */
26
+ export declare function nextBeatTime(nowSec: number, beatDurationSec: number, epsilon?: number): number;
27
+ export interface BufferedAction<T> {
28
+ action: T;
29
+ bufferedAtSec: number;
30
+ fireAtSec: number;
31
+ }
32
+ export interface BeatInputBuffer<T> {
33
+ /** Buffer an action; it quantizes to fire on the next beat tick (or immediately if `nowSec` is already on a beat). Returns the resolved fire time. */
34
+ buffer(action: T, nowSec: number): number;
35
+ /** Drain and return every action whose fire time has arrived, in the order they were buffered. */
36
+ advance(nowSec: number): T[];
37
+ pendingCount(): number;
38
+ clear(): void;
39
+ }
40
+ export declare function createBeatInputBuffer<T>(beatDurationSec: number): BeatInputBuffer<T>;
@@ -0,0 +1,73 @@
1
+ const MAX_BEAT_FIRES = 10_000;
2
+ export function createBeatClock(config, onBeat) {
3
+ const beatsPerBar = config.beatsPerBar !== undefined && config.beatsPerBar > 0 ? config.beatsPerBar : 4;
4
+ const beatDurationSec = 60 / config.bpm;
5
+ let elapsedSec = 0;
6
+ let lastBeatIndex = -1;
7
+ function toSnapshot() {
8
+ const beat = elapsedSec / beatDurationSec;
9
+ const beatIndex = Math.floor(beat);
10
+ return {
11
+ beat,
12
+ beatIndex,
13
+ bar: Math.floor(beatIndex / beatsPerBar),
14
+ beatInBar: ((beatIndex % beatsPerBar) + beatsPerBar) % beatsPerBar,
15
+ phase: beat - beatIndex,
16
+ };
17
+ }
18
+ function advance(gameDt) {
19
+ if (gameDt > 0)
20
+ elapsedSec += gameDt;
21
+ const snapshot = toSnapshot();
22
+ let fires = 0;
23
+ while (lastBeatIndex < snapshot.beatIndex && fires < MAX_BEAT_FIRES) {
24
+ lastBeatIndex += 1;
25
+ onBeat?.(lastBeatIndex);
26
+ fires += 1;
27
+ }
28
+ return snapshot;
29
+ }
30
+ return {
31
+ advance,
32
+ snapshot: toSnapshot,
33
+ now: () => elapsedSec,
34
+ beatDurationSec: () => beatDurationSec,
35
+ bpm: () => config.bpm,
36
+ };
37
+ }
38
+ const QUANTIZE_EPSILON = 1e-6;
39
+ /** The absolute beat-time (game-seconds) that a press at `nowSec` quantizes to: the same instant if it lands on a beat boundary, else the next one. */
40
+ export function nextBeatTime(nowSec, beatDurationSec, epsilon = QUANTIZE_EPSILON) {
41
+ const beatIndex = nowSec / beatDurationSec;
42
+ const rounded = Math.round(beatIndex);
43
+ if (Math.abs(beatIndex - rounded) <= epsilon)
44
+ return rounded * beatDurationSec;
45
+ return Math.ceil(beatIndex) * beatDurationSec;
46
+ }
47
+ export function createBeatInputBuffer(beatDurationSec) {
48
+ const pending = [];
49
+ return {
50
+ buffer(action, nowSec) {
51
+ const fireAtSec = nextBeatTime(nowSec, beatDurationSec);
52
+ pending.push({ action, bufferedAtSec: nowSec, fireAtSec });
53
+ return fireAtSec;
54
+ },
55
+ advance(nowSec) {
56
+ const due = [];
57
+ const remaining = [];
58
+ for (const entry of pending) {
59
+ if (entry.fireAtSec <= nowSec + QUANTIZE_EPSILON)
60
+ due.push(entry.action);
61
+ else
62
+ remaining.push(entry);
63
+ }
64
+ pending.length = 0;
65
+ pending.push(...remaining);
66
+ return due;
67
+ },
68
+ pendingCount: () => pending.length,
69
+ clear: () => {
70
+ pending.length = 0;
71
+ },
72
+ };
73
+ }
@@ -0,0 +1,62 @@
1
+ export interface TimeConfig {
2
+ /** Game-seconds that elapse per real second at 1× speed. Default 1 (real time). Sims-style compression sets this high (e.g. 60 → one real second is one in-game minute). */
3
+ scale?: number;
4
+ /** Selectable play/fast-forward multipliers for the speed control. Default [1, 2, 3, 4]. Pause (0×) is always available and separate. */
5
+ speeds?: readonly number[];
6
+ /** Game-seconds in one in-world day; drives the 24-hour calendar breakdown. Default 86400 (1 game-second = 1 clock-second). */
7
+ dayLength?: number;
8
+ /** Game-seconds already elapsed at boot — start the world at 08:00 with `8 * 3600`. Default 0. */
9
+ start?: number;
10
+ /** Boot with the clock paused. Default false. */
11
+ startPaused?: boolean;
12
+ }
13
+ export interface CalendarTime {
14
+ /** Total game-seconds since the world started. */
15
+ totalSeconds: number;
16
+ /** 0-based day index. */
17
+ day: number;
18
+ hour: number;
19
+ minute: number;
20
+ second: number;
21
+ /** Progress through the current day, 0..1. */
22
+ dayFraction: number;
23
+ }
24
+ export interface ClockSnapshot {
25
+ now: number;
26
+ paused: boolean;
27
+ /** Effective multiplier applied to real time — 0 while paused. */
28
+ speed: number;
29
+ /** The non-zero multiplier `play()` resumes to. */
30
+ playSpeed: number;
31
+ scale: number;
32
+ speeds: readonly number[];
33
+ calendar: CalendarTime;
34
+ }
35
+ export interface SimClock {
36
+ /** Advance the world by a real-time delta; returns the scaled game-time delta (0 while paused) and fires any due timers. The shell calls this once per frame and passes the result to `loop.onTick`. */
37
+ advance(realDt: number): number;
38
+ now(): number;
39
+ snapshot(): ClockSnapshot;
40
+ calendar(): CalendarTime;
41
+ isPaused(): boolean;
42
+ speed(): number;
43
+ pause(): void;
44
+ play(): void;
45
+ toggle(): void;
46
+ /** Set the play multiplier; `0` (or negative) pauses, any positive value unpauses and becomes the resume speed. */
47
+ setSpeed(multiplier: number): void;
48
+ /** Step to the next configured speed (wraps); always unpauses. */
49
+ cycleSpeed(): void;
50
+ /** Run `callback` once, `seconds` of game-time from now. Returns a cancel handle. */
51
+ after(seconds: number, callback: () => void): () => void;
52
+ /** Run `callback` every `seconds` of game-time. Returns a cancel handle. */
53
+ every(seconds: number, callback: () => void): () => void;
54
+ /** Run `callback` when game-time reaches the absolute `time` (game-seconds). Returns a cancel handle. */
55
+ at(time: number, callback: () => void): () => void;
56
+ }
57
+ export interface SimClockOptions {
58
+ config?: TimeConfig;
59
+ /** Called on control changes and once per displayed in-game minute so UI can rebind; never fires per frame. */
60
+ onChange?: () => void;
61
+ }
62
+ export declare function createSimClock(options?: SimClockOptions): SimClock;