@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,114 @@
1
+ function emptyLane(sides) {
2
+ const cells = {};
3
+ for (const side of sides)
4
+ cells[side] = [];
5
+ return cells;
6
+ }
7
+ export function createLaneBoardState(config) {
8
+ if (config.laneCount <= 0)
9
+ throw new Error("laneBoard needs at least one lane");
10
+ if (config.sides.length === 0)
11
+ throw new Error("laneBoard needs at least one side");
12
+ const lanes = [];
13
+ for (let i = 0; i < config.laneCount; i++)
14
+ lanes.push(emptyLane(config.sides));
15
+ return { lanes };
16
+ }
17
+ function validLane(state, lane) {
18
+ return Number.isInteger(lane) && lane >= 0 && lane < state.lanes.length;
19
+ }
20
+ export function placeCard(state, config, lane, side, card) {
21
+ if (!validLane(state, lane))
22
+ return { status: "rejected", reason: "invalid-lane" };
23
+ if (!config.sides.includes(side))
24
+ return { status: "rejected", reason: "invalid-side" };
25
+ const lanes = state.lanes.slice();
26
+ const cell = lanes[lane];
27
+ lanes[lane] = { ...cell, [side]: [...cell[side], card] };
28
+ return { status: "ok", state: { lanes } };
29
+ }
30
+ export function removeCard(state, lane, side, predicate) {
31
+ if (!validLane(state, lane))
32
+ return { status: "rejected", reason: "invalid-lane" };
33
+ const cell = state.lanes[lane];
34
+ if (cell[side] === undefined)
35
+ return { status: "rejected", reason: "invalid-side" };
36
+ const lanes = state.lanes.slice();
37
+ lanes[lane] = { ...cell, [side]: cell[side].filter((card, index) => !predicate(card, index)) };
38
+ return { status: "ok", state: { lanes } };
39
+ }
40
+ export function laneAggregate(state, config, lane, side) {
41
+ const cards = state.lanes[lane]?.[side] ?? [];
42
+ let subtotal = 0;
43
+ for (const card of cards)
44
+ subtotal += config.power(card);
45
+ const rule = config.laneRules?.[lane] ?? null;
46
+ const total = rule ? rule.apply({ lane, side, cards, subtotal }) : subtotal;
47
+ return { lane, side, subtotal, total, count: cards.length };
48
+ }
49
+ export function laneOutcome(state, config, lane) {
50
+ const totals = {};
51
+ let winner = null;
52
+ let best = -Infinity;
53
+ let tie = false;
54
+ for (const side of config.sides) {
55
+ const total = laneAggregate(state, config, lane, side).total;
56
+ totals[side] = total;
57
+ if (total > best) {
58
+ best = total;
59
+ winner = side;
60
+ tie = false;
61
+ }
62
+ else if (total === best) {
63
+ tie = true;
64
+ }
65
+ }
66
+ return { lane, totals, winner: tie ? null : winner };
67
+ }
68
+ export function boardTotals(state, config) {
69
+ const totals = {};
70
+ for (const side of config.sides)
71
+ totals[side] = 0;
72
+ for (let lane = 0; lane < state.lanes.length; lane++) {
73
+ for (const side of config.sides) {
74
+ totals[side] += laneAggregate(state, config, lane, side).total;
75
+ }
76
+ }
77
+ return totals;
78
+ }
79
+ export function lanesWon(state, config) {
80
+ const won = {};
81
+ for (const side of config.sides)
82
+ won[side] = 0;
83
+ for (let lane = 0; lane < state.lanes.length; lane++) {
84
+ const outcome = laneOutcome(state, config, lane);
85
+ if (outcome.winner !== null)
86
+ won[outcome.winner] += 1;
87
+ }
88
+ return won;
89
+ }
90
+ export function createLaneBoard(config) {
91
+ let state = createLaneBoardState(config);
92
+ return {
93
+ state: () => state,
94
+ place(lane, side, card) {
95
+ const result = placeCard(state, config, lane, side, card);
96
+ if (result.status === "ok")
97
+ state = result.state;
98
+ return result;
99
+ },
100
+ remove(lane, side, predicate) {
101
+ const result = removeCard(state, lane, side, predicate);
102
+ if (result.status === "ok")
103
+ state = result.state;
104
+ return result;
105
+ },
106
+ aggregate: (lane, side) => laneAggregate(state, config, lane, side),
107
+ outcome: (lane) => laneOutcome(state, config, lane),
108
+ totals: () => boardTotals(state, config),
109
+ lanesWon: () => lanesWon(state, config),
110
+ reset(next) {
111
+ state = next ?? createLaneBoardState(config);
112
+ },
113
+ };
114
+ }
@@ -0,0 +1,36 @@
1
+ export interface TimelineSlotConfig {
2
+ id: string;
3
+ cooldownMs: number;
4
+ enabled?: boolean;
5
+ offsetMs?: number;
6
+ }
7
+ export interface TimelineSlot {
8
+ readonly id: string;
9
+ readonly cooldownMs: number;
10
+ readonly remainingMs: number;
11
+ readonly enabled: boolean;
12
+ }
13
+ export interface TimelineBoardState {
14
+ readonly slots: readonly TimelineSlot[];
15
+ readonly elapsedMs: number;
16
+ }
17
+ export interface TimelineFire {
18
+ slotId: string;
19
+ slotIndex: number;
20
+ atMs: number;
21
+ }
22
+ export interface TimelineTickResult {
23
+ state: TimelineBoardState;
24
+ fires: readonly TimelineFire[];
25
+ }
26
+ export declare function createTimelineBoardState(slots: readonly TimelineSlotConfig[]): TimelineBoardState;
27
+ export declare function tickTimeline(state: TimelineBoardState, dtMs: number): TimelineTickResult;
28
+ export declare function setSlotEnabled(state: TimelineBoardState, slotId: string, enabled: boolean): TimelineBoardState;
29
+ export declare function resetSlot(state: TimelineBoardState, slotId: string, remainingMs?: number): TimelineBoardState;
30
+ export interface TimelineBoard {
31
+ state(): TimelineBoardState;
32
+ tick(dtMs: number): readonly TimelineFire[];
33
+ setEnabled(slotId: string, enabled: boolean): void;
34
+ reset(slotId?: string, remainingMs?: number): void;
35
+ }
36
+ export declare function createTimelineBoard(slots: readonly TimelineSlotConfig[]): TimelineBoard;
@@ -0,0 +1,70 @@
1
+ export function createTimelineBoardState(slots) {
2
+ const seen = new Set();
3
+ const built = [];
4
+ for (const slot of slots) {
5
+ if (slot.cooldownMs <= 0)
6
+ throw new Error(`timeline slot "${slot.id}" needs a positive cooldown`);
7
+ if (seen.has(slot.id))
8
+ throw new Error(`duplicate timeline slot id: ${slot.id}`);
9
+ seen.add(slot.id);
10
+ built.push({
11
+ id: slot.id,
12
+ cooldownMs: slot.cooldownMs,
13
+ remainingMs: slot.offsetMs ?? slot.cooldownMs,
14
+ enabled: slot.enabled ?? true,
15
+ });
16
+ }
17
+ return { slots: built, elapsedMs: 0 };
18
+ }
19
+ export function tickTimeline(state, dtMs) {
20
+ if (dtMs <= 0)
21
+ return { state, fires: [] };
22
+ const fires = [];
23
+ const nextSlots = state.slots.map((slot, slotIndex) => {
24
+ if (!slot.enabled)
25
+ return slot;
26
+ let remaining = slot.remainingMs;
27
+ let t = remaining;
28
+ while (t <= dtMs) {
29
+ fires.push({ slotId: slot.id, slotIndex, atMs: state.elapsedMs + t });
30
+ t += slot.cooldownMs;
31
+ }
32
+ remaining = t - dtMs;
33
+ return { ...slot, remainingMs: remaining };
34
+ });
35
+ fires.sort((a, b) => (a.atMs === b.atMs ? a.slotIndex - b.slotIndex : a.atMs - b.atMs));
36
+ return {
37
+ state: { slots: nextSlots, elapsedMs: state.elapsedMs + dtMs },
38
+ fires,
39
+ };
40
+ }
41
+ export function setSlotEnabled(state, slotId, enabled) {
42
+ return {
43
+ ...state,
44
+ slots: state.slots.map((slot) => (slot.id === slotId ? { ...slot, enabled } : slot)),
45
+ };
46
+ }
47
+ export function resetSlot(state, slotId, remainingMs) {
48
+ return {
49
+ ...state,
50
+ slots: state.slots.map((slot) => slot.id === slotId ? { ...slot, remainingMs: remainingMs ?? slot.cooldownMs } : slot),
51
+ };
52
+ }
53
+ export function createTimelineBoard(slots) {
54
+ const initial = slots;
55
+ let state = createTimelineBoardState(slots);
56
+ return {
57
+ state: () => state,
58
+ tick(dtMs) {
59
+ const result = tickTimeline(state, dtMs);
60
+ state = result.state;
61
+ return result.fires;
62
+ },
63
+ setEnabled(slotId, enabled) {
64
+ state = setSlotEnabled(state, slotId, enabled);
65
+ },
66
+ reset(slotId, remainingMs) {
67
+ state = slotId === undefined ? createTimelineBoardState(initial) : resetSlot(state, slotId, remainingMs);
68
+ },
69
+ };
70
+ }
@@ -0,0 +1,63 @@
1
+ export type ZoneName = string;
2
+ export interface CardPileConfig {
3
+ zones: readonly ZoneName[];
4
+ drawFrom?: ZoneName;
5
+ discardTo?: ZoneName;
6
+ handZone?: ZoneName;
7
+ handLimit?: number;
8
+ reshuffleFrom?: ZoneName;
9
+ }
10
+ export interface CardPileState {
11
+ readonly zones: Readonly<Record<ZoneName, readonly string[]>>;
12
+ }
13
+ export interface DrawResult {
14
+ state: CardPileState;
15
+ drawn: readonly string[];
16
+ reshuffled: boolean;
17
+ }
18
+ export type PileRejection = "unknown-zone" | "card-not-in-zone" | "hand-limit" | "empty-source";
19
+ export interface PileMoveResult {
20
+ status: "ok";
21
+ state: CardPileState;
22
+ }
23
+ export interface PileMoveRejected {
24
+ status: "rejected";
25
+ reason: PileRejection;
26
+ detail?: string;
27
+ }
28
+ export type PileResult = PileMoveResult | PileMoveRejected;
29
+ export declare function pileRng(seed: string | number): () => number;
30
+ export declare function shuffleWithRng<T>(values: readonly T[], rng: () => number): T[];
31
+ export declare function createCardPileState(config: CardPileConfig, initial?: Partial<Record<ZoneName, readonly string[]>>): CardPileState;
32
+ export declare function zoneOf(state: CardPileState, cardId: string): ZoneName | null;
33
+ export declare function countIn(state: CardPileState, zone: ZoneName): number;
34
+ export declare function peek(state: CardPileState, zone: ZoneName, n?: number): readonly string[];
35
+ export declare function shuffleZone(state: CardPileState, zone: ZoneName, seed: string | number): CardPileState;
36
+ export declare function moveCards(state: CardPileState, ids: readonly string[], from: ZoneName, to: ZoneName, position?: "top" | "bottom"): PileResult;
37
+ export declare function draw(state: CardPileState, n: number, options: {
38
+ from: ZoneName;
39
+ to: ZoneName;
40
+ handLimit?: number;
41
+ reshuffleFrom?: ZoneName;
42
+ seed?: string | number;
43
+ }): DrawResult;
44
+ export interface CardPile {
45
+ state(): CardPileState;
46
+ zones(): readonly ZoneName[];
47
+ count(zone: ZoneName): number;
48
+ peek(zone: ZoneName, n?: number): readonly string[];
49
+ zoneOf(cardId: string): ZoneName | null;
50
+ shuffle(zone: ZoneName | undefined, seed: string | number): void;
51
+ draw(n: number, options?: {
52
+ from?: ZoneName;
53
+ to?: ZoneName;
54
+ }): readonly string[];
55
+ discard(ids: readonly string[], options?: {
56
+ from?: ZoneName;
57
+ to?: ZoneName;
58
+ }): PileResult;
59
+ exhaust(ids: readonly string[], exhaustZone: ZoneName, from?: ZoneName): PileResult;
60
+ move(ids: readonly string[], from: ZoneName, to: ZoneName, position?: "top" | "bottom"): PileResult;
61
+ reset(next: CardPileState): void;
62
+ }
63
+ export declare function createCardPile(config: CardPileConfig, initial?: Partial<Record<ZoneName, readonly string[]>>): CardPile;
@@ -0,0 +1,166 @@
1
+ function fnv1a(text) {
2
+ let h = 0x811c9dc5;
3
+ for (let i = 0; i < text.length; i++) {
4
+ h ^= text.charCodeAt(i);
5
+ h = Math.imul(h, 16777619);
6
+ }
7
+ return h >>> 0;
8
+ }
9
+ export function pileRng(seed) {
10
+ let a = (typeof seed === "number" ? seed >>> 0 : fnv1a(seed)) >>> 0;
11
+ return () => {
12
+ a = (a + 0x6d2b79f5) >>> 0;
13
+ let t = a;
14
+ t = Math.imul(t ^ (t >>> 15), t | 1);
15
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
16
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
17
+ };
18
+ }
19
+ export function shuffleWithRng(values, rng) {
20
+ const out = values.slice();
21
+ for (let i = out.length - 1; i > 0; i--) {
22
+ const j = Math.floor(rng() * (i + 1));
23
+ const tmp = out[i];
24
+ out[i] = out[j];
25
+ out[j] = tmp;
26
+ }
27
+ return out;
28
+ }
29
+ function assertZone(state, zone) {
30
+ if (!(zone in state.zones))
31
+ throw new Error(`unknown card-pile zone: ${zone}`);
32
+ }
33
+ export function createCardPileState(config, initial) {
34
+ if (config.zones.length === 0)
35
+ throw new Error("card pile needs at least one zone");
36
+ const zones = {};
37
+ for (const zone of config.zones)
38
+ zones[zone] = (initial?.[zone] ?? []).slice();
39
+ const guard = (zone, label) => {
40
+ if (zone !== undefined && !(zone in zones)) {
41
+ throw new Error(`${label} names an undeclared zone: ${zone}`);
42
+ }
43
+ };
44
+ guard(config.drawFrom, "drawFrom");
45
+ guard(config.discardTo, "discardTo");
46
+ guard(config.handZone, "handZone");
47
+ guard(config.reshuffleFrom, "reshuffleFrom");
48
+ return { zones };
49
+ }
50
+ export function zoneOf(state, cardId) {
51
+ for (const [zone, cards] of Object.entries(state.zones)) {
52
+ if (cards.includes(cardId))
53
+ return zone;
54
+ }
55
+ return null;
56
+ }
57
+ export function countIn(state, zone) {
58
+ assertZone(state, zone);
59
+ return state.zones[zone].length;
60
+ }
61
+ export function peek(state, zone, n = 1) {
62
+ assertZone(state, zone);
63
+ return state.zones[zone].slice(0, Math.max(0, n));
64
+ }
65
+ export function shuffleZone(state, zone, seed) {
66
+ assertZone(state, zone);
67
+ return { zones: { ...state.zones, [zone]: shuffleWithRng(state.zones[zone], pileRng(seed)) } };
68
+ }
69
+ export function moveCards(state, ids, from, to, position = "top") {
70
+ assertZone(state, from);
71
+ assertZone(state, to);
72
+ const source = state.zones[from];
73
+ for (const id of ids) {
74
+ if (!source.includes(id))
75
+ return { status: "rejected", reason: "card-not-in-zone", detail: id };
76
+ }
77
+ const idSet = new Set(ids);
78
+ const nextFrom = source.filter((id) => !idSet.has(id));
79
+ const dest = state.zones[to];
80
+ const nextTo = position === "top" ? [...ids, ...dest] : [...dest, ...ids];
81
+ return { status: "ok", state: { zones: { ...state.zones, [from]: nextFrom, [to]: nextTo } } };
82
+ }
83
+ export function draw(state, n, options) {
84
+ assertZone(state, options.from);
85
+ assertZone(state, options.to);
86
+ if (options.reshuffleFrom !== undefined)
87
+ assertZone(state, options.reshuffleFrom);
88
+ let current = state;
89
+ let reshuffled = false;
90
+ const drawn = [];
91
+ const wanted = Math.max(0, Math.floor(n));
92
+ for (let i = 0; i < wanted; i++) {
93
+ if (options.handLimit !== undefined && current.zones[options.to].length >= options.handLimit) {
94
+ break;
95
+ }
96
+ if (current.zones[options.from].length === 0) {
97
+ if (options.reshuffleFrom === undefined || current.zones[options.reshuffleFrom].length === 0) {
98
+ break;
99
+ }
100
+ const seed = options.seed ?? drawn.length + i;
101
+ const shuffledSource = shuffleWithRng(current.zones[options.reshuffleFrom], pileRng(seed));
102
+ current = {
103
+ zones: { ...current.zones, [options.from]: shuffledSource, [options.reshuffleFrom]: [] },
104
+ };
105
+ reshuffled = true;
106
+ }
107
+ const source = current.zones[options.from];
108
+ const top = source[0];
109
+ drawn.push(top);
110
+ current = {
111
+ zones: {
112
+ ...current.zones,
113
+ [options.from]: source.slice(1),
114
+ [options.to]: [...current.zones[options.to], top],
115
+ },
116
+ };
117
+ }
118
+ return { state: current, drawn, reshuffled };
119
+ }
120
+ export function createCardPile(config, initial) {
121
+ let state = createCardPileState(config, initial);
122
+ const drawFrom = config.drawFrom ?? config.zones[0];
123
+ const handZone = config.handZone ?? config.zones[Math.min(1, config.zones.length - 1)];
124
+ const discardTo = config.discardTo ?? config.zones[config.zones.length - 1];
125
+ return {
126
+ state: () => state,
127
+ zones: () => config.zones,
128
+ count: (zone) => countIn(state, zone),
129
+ peek: (zone, n) => peek(state, zone, n),
130
+ zoneOf: (cardId) => zoneOf(state, cardId),
131
+ shuffle(zone, seed) {
132
+ state = shuffleZone(state, zone ?? drawFrom, seed);
133
+ },
134
+ draw(n, options) {
135
+ const result = draw(state, n, {
136
+ from: options?.from ?? drawFrom,
137
+ to: options?.to ?? handZone,
138
+ handLimit: config.handLimit,
139
+ reshuffleFrom: config.reshuffleFrom,
140
+ });
141
+ state = result.state;
142
+ return result.drawn;
143
+ },
144
+ discard(ids, options) {
145
+ const result = moveCards(state, ids, options?.from ?? handZone, options?.to ?? discardTo, "top");
146
+ if (result.status === "ok")
147
+ state = result.state;
148
+ return result;
149
+ },
150
+ exhaust(ids, exhaustZone, from) {
151
+ const result = moveCards(state, ids, from ?? handZone, exhaustZone, "top");
152
+ if (result.status === "ok")
153
+ state = result.state;
154
+ return result;
155
+ },
156
+ move(ids, from, to, position) {
157
+ const result = moveCards(state, ids, from, to, position);
158
+ if (result.status === "ok")
159
+ state = result.state;
160
+ return result;
161
+ },
162
+ reset(next) {
163
+ state = next;
164
+ },
165
+ };
166
+ }
@@ -0,0 +1,33 @@
1
+ export interface Modifier<V> {
2
+ id: string;
3
+ source?: string;
4
+ apply: (value: V, context: PipelineContext<V>) => V;
5
+ }
6
+ export interface PipelineContext<V> {
7
+ base: V;
8
+ index: number;
9
+ trace: readonly TraceStep<V>[];
10
+ }
11
+ export interface TraceStep<V> {
12
+ id: string;
13
+ source?: string;
14
+ index: number;
15
+ before: V;
16
+ after: V;
17
+ changed: boolean;
18
+ }
19
+ export interface PipelineResult<V> {
20
+ base: V;
21
+ value: V;
22
+ trace: readonly TraceStep<V>[];
23
+ }
24
+ export declare function runPipeline<V>(base: V, modifiers: readonly Modifier<V>[], equals?: (a: V, b: V) => boolean): PipelineResult<V>;
25
+ export interface ModifierPipeline<V> {
26
+ add(modifier: Modifier<V>): void;
27
+ insertAt(index: number, modifier: Modifier<V>): void;
28
+ remove(id: string): boolean;
29
+ clear(): void;
30
+ list(): readonly Modifier<V>[];
31
+ run(base: V): PipelineResult<V>;
32
+ }
33
+ export declare function createModifierPipeline<V>(initial?: readonly Modifier<V>[], equals?: (a: V, b: V) => boolean): ModifierPipeline<V>;
@@ -0,0 +1,48 @@
1
+ export function runPipeline(base, modifiers, equals = Object.is) {
2
+ const trace = [];
3
+ let value = base;
4
+ let index = 0;
5
+ for (const modifier of modifiers) {
6
+ const before = value;
7
+ const after = modifier.apply(before, { base, index, trace });
8
+ trace.push({
9
+ id: modifier.id,
10
+ source: modifier.source,
11
+ index,
12
+ before,
13
+ after,
14
+ changed: !equals(before, after),
15
+ });
16
+ value = after;
17
+ index += 1;
18
+ }
19
+ return { base, value, trace };
20
+ }
21
+ export function createModifierPipeline(initial = [], equals = Object.is) {
22
+ const modifiers = initial.slice();
23
+ return {
24
+ add(modifier) {
25
+ modifiers.push(modifier);
26
+ },
27
+ insertAt(index, modifier) {
28
+ const clamped = Math.max(0, Math.min(modifiers.length, Math.floor(index)));
29
+ modifiers.splice(clamped, 0, modifier);
30
+ },
31
+ remove(id) {
32
+ const at = modifiers.findIndex((m) => m.id === id);
33
+ if (at === -1)
34
+ return false;
35
+ modifiers.splice(at, 1);
36
+ return true;
37
+ },
38
+ clear() {
39
+ modifiers.length = 0;
40
+ },
41
+ list() {
42
+ return modifiers.slice();
43
+ },
44
+ run(base) {
45
+ return runPipeline(base, modifiers, equals);
46
+ },
47
+ };
48
+ }
@@ -0,0 +1,42 @@
1
+ export type AbilityCastType = "instant" | "projectile" | "channel" | "targeted";
2
+ export type AbilitySlotState = "ready" | "cooldown" | "no-resource" | "just-cast";
3
+ export interface AbilitySlotConfig {
4
+ id: string;
5
+ cooldownMs: number;
6
+ chargesMax?: number;
7
+ resourceCost?: number;
8
+ castType?: AbilityCastType;
9
+ flashMs?: number;
10
+ }
11
+ export interface AbilitySlotSnapshot {
12
+ id: string;
13
+ state: AbilitySlotState;
14
+ castType: AbilityCastType;
15
+ resourceCost: number;
16
+ charges: number;
17
+ chargesMax: number;
18
+ cooldownRemainingMs: number;
19
+ cooldownFraction: number;
20
+ justCast: boolean;
21
+ ready: boolean;
22
+ }
23
+ export type AbilityCastReason = "unknown-slot" | "cooldown" | "no-resource";
24
+ export type AbilityCastResult = {
25
+ ok: true;
26
+ slot: AbilitySlotSnapshot;
27
+ } | {
28
+ ok: false;
29
+ reason: AbilityCastReason;
30
+ slot: AbilitySlotSnapshot | null;
31
+ };
32
+ export interface AbilityKit {
33
+ slots(): readonly string[];
34
+ config(slotId: string): AbilitySlotConfig | null;
35
+ state(slotId: string, resourceAvailable?: number): AbilitySlotSnapshot | null;
36
+ snapshot(resourceAvailable?: number): AbilitySlotSnapshot[];
37
+ canCast(slotId: string, resourceAvailable?: number): AbilityCastResult;
38
+ cast(slotId: string, resourceAvailable?: number): AbilityCastResult;
39
+ tick(dtSeconds: number): void;
40
+ reset(slotId?: string): void;
41
+ }
42
+ export declare function createAbilityKit(configs: readonly AbilitySlotConfig[]): AbilityKit;