@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,132 @@
1
+ import { SECONDS_PER_GAME_DAY } from "./gameClock.js";
2
+ const MAX_TIMER_FIRES = 10_000;
3
+ export function createSimClock(options = {}) {
4
+ const config = options.config ?? {};
5
+ const onChange = options.onChange ?? (() => { });
6
+ const scale = config.scale !== undefined && config.scale > 0 ? config.scale : 1;
7
+ const configuredSpeeds = config.speeds === undefined ? [1, 2, 3, 4] : config.speeds.filter((value) => value > 0);
8
+ const speeds = configuredSpeeds.length > 0 ? configuredSpeeds : [1];
9
+ const dayLength = config.dayLength !== undefined && config.dayLength > 0 ? config.dayLength : SECONDS_PER_GAME_DAY;
10
+ let now = config.start !== undefined && config.start > 0 ? config.start : 0;
11
+ let paused = config.startPaused ?? false;
12
+ let playSpeed = speeds[0];
13
+ let nextTimerId = 0;
14
+ const timers = new Map();
15
+ let displayMinute = minuteIndex(now, dayLength);
16
+ function calendar() {
17
+ const day = Math.floor(now / dayLength);
18
+ const dayFraction = (now - day * dayLength) / dayLength;
19
+ const secondOfDay = dayFraction * 24 * 3600;
20
+ return {
21
+ totalSeconds: now,
22
+ day,
23
+ hour: Math.floor(secondOfDay / 3600) % 24,
24
+ minute: Math.floor(secondOfDay / 60) % 60,
25
+ second: Math.floor(secondOfDay) % 60,
26
+ dayFraction,
27
+ };
28
+ }
29
+ function fireDueTimers() {
30
+ let fires = 0;
31
+ for (;;) {
32
+ let soonest = null;
33
+ for (const [id, timer] of timers) {
34
+ if (timer.due > now)
35
+ continue;
36
+ if (soonest === null || timer.due < soonest.timer.due)
37
+ soonest = { id, timer };
38
+ }
39
+ if (soonest === null)
40
+ return;
41
+ const { id, timer } = soonest;
42
+ if (timer.interval === null) {
43
+ timers.delete(id);
44
+ }
45
+ else {
46
+ timer.due += timer.interval;
47
+ }
48
+ timer.callback();
49
+ fires += 1;
50
+ if (fires >= MAX_TIMER_FIRES)
51
+ return;
52
+ }
53
+ }
54
+ function advance(realDt) {
55
+ if (paused || realDt <= 0)
56
+ return 0;
57
+ const gameDt = realDt * scale * playSpeed;
58
+ now += gameDt;
59
+ fireDueTimers();
60
+ const minute = minuteIndex(now, dayLength);
61
+ if (minute !== displayMinute) {
62
+ displayMinute = minute;
63
+ onChange();
64
+ }
65
+ return gameDt;
66
+ }
67
+ function pause() {
68
+ if (paused)
69
+ return;
70
+ paused = true;
71
+ onChange();
72
+ }
73
+ function play() {
74
+ if (!paused)
75
+ return;
76
+ paused = false;
77
+ onChange();
78
+ }
79
+ function setSpeed(multiplier) {
80
+ if (multiplier <= 0) {
81
+ pause();
82
+ return;
83
+ }
84
+ const changed = paused || multiplier !== playSpeed;
85
+ playSpeed = multiplier;
86
+ paused = false;
87
+ if (changed)
88
+ onChange();
89
+ }
90
+ function cycleSpeed() {
91
+ const index = speeds.indexOf(playSpeed);
92
+ setSpeed(speeds[(index + 1) % speeds.length]);
93
+ }
94
+ function register(due, interval, callback) {
95
+ const id = nextTimerId;
96
+ nextTimerId += 1;
97
+ timers.set(id, { due, interval, callback });
98
+ return () => {
99
+ timers.delete(id);
100
+ };
101
+ }
102
+ return {
103
+ advance,
104
+ now: () => now,
105
+ calendar,
106
+ snapshot: () => ({
107
+ now,
108
+ paused,
109
+ speed: paused ? 0 : playSpeed,
110
+ playSpeed,
111
+ scale,
112
+ speeds,
113
+ calendar: calendar(),
114
+ }),
115
+ isPaused: () => paused,
116
+ speed: () => (paused ? 0 : playSpeed),
117
+ pause,
118
+ play,
119
+ toggle: () => (paused ? play() : pause()),
120
+ setSpeed,
121
+ cycleSpeed,
122
+ after: (seconds, callback) => register(now + Math.max(0, seconds), null, callback),
123
+ every: (seconds, callback) => {
124
+ const interval = Math.max(Number.EPSILON, seconds);
125
+ return register(now + interval, interval, callback);
126
+ },
127
+ at: (time, callback) => register(time, null, callback),
128
+ };
129
+ }
130
+ function minuteIndex(seconds, dayLength) {
131
+ return Math.floor((seconds / dayLength) * 24 * 60);
132
+ }
@@ -0,0 +1,27 @@
1
+ export type CommitMode = "immediate" | "simultaneous" | "rewind";
2
+ export interface SubmittedAction<TAction> {
3
+ participant: string;
4
+ action: TAction;
5
+ }
6
+ export interface CommitOutcome<TAction> {
7
+ status: "committed" | "sealed" | "pending" | "rejected";
8
+ committed: SubmittedAction<TAction>[];
9
+ reason?: string;
10
+ }
11
+ export interface CommitControllerConfig {
12
+ mode: CommitMode;
13
+ participants?: readonly string[];
14
+ }
15
+ export interface CommitController<TAction> {
16
+ readonly mode: CommitMode;
17
+ submit(participant: string, action: TAction): CommitOutcome<TAction>;
18
+ expected(participants: readonly string[]): void;
19
+ hasSubmitted(participant: string): boolean;
20
+ allReady(): boolean;
21
+ pending(): SubmittedAction<TAction>[];
22
+ reveal(): SubmittedAction<TAction>[];
23
+ commit(): SubmittedAction<TAction>[];
24
+ rewind(): SubmittedAction<TAction>[];
25
+ clear(): void;
26
+ }
27
+ export declare function createCommitController<TAction>(config: CommitControllerConfig): CommitController<TAction>;
@@ -0,0 +1,72 @@
1
+ export function createCommitController(config) {
2
+ const mode = config.mode;
3
+ let expected = config.participants === undefined ? null : [...config.participants];
4
+ let sealed = new Map();
5
+ let revealed = false;
6
+ function orderedSubmissions() {
7
+ const source = expected ?? [...sealed.keys()];
8
+ const result = [];
9
+ for (const participant of source) {
10
+ if (sealed.has(participant))
11
+ result.push({ participant, action: sealed.get(participant) });
12
+ }
13
+ for (const [participant, action] of sealed) {
14
+ if (expected !== null && !expected.includes(participant))
15
+ result.push({ participant, action });
16
+ }
17
+ return result;
18
+ }
19
+ function allReady() {
20
+ if (expected === null)
21
+ return sealed.size > 0;
22
+ return expected.every((participant) => sealed.has(participant));
23
+ }
24
+ function clear() {
25
+ sealed = new Map();
26
+ revealed = false;
27
+ }
28
+ return {
29
+ mode,
30
+ submit(participant, action) {
31
+ if (mode === "immediate") {
32
+ return { status: "committed", committed: [{ participant, action }] };
33
+ }
34
+ if (sealed.has(participant) && !(mode === "rewind")) {
35
+ return { status: "rejected", committed: [], reason: "already-submitted" };
36
+ }
37
+ sealed.set(participant, action);
38
+ if (mode === "simultaneous") {
39
+ return { status: "sealed", committed: [] };
40
+ }
41
+ return { status: "pending", committed: [] };
42
+ },
43
+ expected(participants) {
44
+ expected = [...participants];
45
+ },
46
+ hasSubmitted: (participant) => sealed.has(participant),
47
+ allReady,
48
+ pending: () => orderedSubmissions(),
49
+ reveal() {
50
+ if (mode !== "simultaneous")
51
+ return orderedSubmissions();
52
+ if (!allReady())
53
+ return [];
54
+ revealed = true;
55
+ const out = orderedSubmissions();
56
+ return out;
57
+ },
58
+ commit() {
59
+ if (mode === "simultaneous" && !revealed && !allReady())
60
+ return [];
61
+ const out = orderedSubmissions();
62
+ clear();
63
+ return out;
64
+ },
65
+ rewind() {
66
+ const discarded = orderedSubmissions();
67
+ clear();
68
+ return discarded;
69
+ },
70
+ clear,
71
+ };
72
+ }
@@ -0,0 +1,58 @@
1
+ import { type CommitController, type CommitMode } from "./commit.js";
2
+ export type { CommitMode, CommitController } from "./commit.js";
3
+ export interface PoolConfig {
4
+ id: string;
5
+ max: number;
6
+ start?: number;
7
+ }
8
+ export interface TurnLoopConfig {
9
+ order: readonly string[];
10
+ phases?: readonly string[];
11
+ pools?: readonly PoolConfig[];
12
+ commit?: {
13
+ mode: CommitMode;
14
+ };
15
+ }
16
+ export interface PoolState {
17
+ id: string;
18
+ current: number;
19
+ max: number;
20
+ }
21
+ export interface TurnState {
22
+ round: number;
23
+ order: readonly string[];
24
+ activeIndex: number;
25
+ active: string | null;
26
+ phaseIndex: number;
27
+ phase: string | null;
28
+ }
29
+ export interface TurnLoopSnapshot {
30
+ round: number;
31
+ order: string[];
32
+ activeIndex: number;
33
+ phaseIndex: number;
34
+ pools: Record<string, Record<string, number>>;
35
+ }
36
+ export interface TurnLoop<TAction = unknown> {
37
+ readonly commit: CommitController<TAction>;
38
+ state(): TurnState;
39
+ order(): readonly string[];
40
+ active(): string | null;
41
+ phase(): string | null;
42
+ round(): number;
43
+ setOrder(order: readonly string[], keepActive?: boolean): TurnState;
44
+ addParticipant(id: string, atIndex?: number): TurnState;
45
+ removeParticipant(id: string): TurnState;
46
+ advancePhase(): TurnState;
47
+ advanceTurn(): TurnState;
48
+ advanceRound(): TurnState;
49
+ pools(participantId: string): PoolState[];
50
+ pool(participantId: string, poolId: string): PoolState | null;
51
+ canSpend(participantId: string, poolId: string, amount?: number): boolean;
52
+ spend(participantId: string, poolId: string, amount?: number): boolean;
53
+ gain(participantId: string, poolId: string, amount: number): boolean;
54
+ refill(participantId: string, poolId?: string): void;
55
+ capture(): TurnLoopSnapshot;
56
+ restore(snapshot: TurnLoopSnapshot): void;
57
+ }
58
+ export declare function createTurnLoop<TAction = unknown>(config: TurnLoopConfig): TurnLoop<TAction>;
@@ -0,0 +1,216 @@
1
+ import { createCommitController } from "./commit.js";
2
+ export function createTurnLoop(config) {
3
+ const poolConfigs = config.pools ?? [];
4
+ const phases = config.phases ?? [];
5
+ const commit = createCommitController({
6
+ mode: config.commit?.mode ?? "immediate",
7
+ participants: config.order,
8
+ });
9
+ let order = [...config.order];
10
+ let round = 1;
11
+ let activeIndex = order.length > 0 ? 0 : -1;
12
+ let phaseIndex = 0;
13
+ const pools = new Map();
14
+ function resetPools(participantId) {
15
+ const set = new Map();
16
+ for (const pc of poolConfigs) {
17
+ set.set(pc.id, { id: pc.id, current: pc.start ?? pc.max, max: pc.max });
18
+ }
19
+ pools.set(participantId, set);
20
+ }
21
+ function ensurePools(participantId) {
22
+ const existing = pools.get(participantId);
23
+ if (existing !== undefined)
24
+ return existing;
25
+ resetPools(participantId);
26
+ return pools.get(participantId);
27
+ }
28
+ for (const id of order)
29
+ resetPools(id);
30
+ function active() {
31
+ return activeIndex >= 0 && activeIndex < order.length ? order[activeIndex] : null;
32
+ }
33
+ function phase() {
34
+ return phases.length > 0 ? phases[phaseIndex] ?? null : null;
35
+ }
36
+ function state() {
37
+ return {
38
+ round,
39
+ order: [...order],
40
+ activeIndex,
41
+ active: active(),
42
+ phaseIndex: phases.length > 0 ? phaseIndex : 0,
43
+ phase: phase(),
44
+ };
45
+ }
46
+ function advanceTurn() {
47
+ if (order.length === 0) {
48
+ activeIndex = -1;
49
+ return state();
50
+ }
51
+ phaseIndex = 0;
52
+ if (activeIndex < 0) {
53
+ activeIndex = 0;
54
+ }
55
+ else if (activeIndex >= order.length - 1) {
56
+ activeIndex = 0;
57
+ round += 1;
58
+ }
59
+ else {
60
+ activeIndex += 1;
61
+ }
62
+ const current = active();
63
+ if (current !== null)
64
+ resetPools(current);
65
+ return state();
66
+ }
67
+ function advancePhase() {
68
+ if (phases.length === 0)
69
+ return advanceTurn();
70
+ if (phaseIndex >= phases.length - 1)
71
+ return advanceTurn();
72
+ phaseIndex += 1;
73
+ return state();
74
+ }
75
+ function advanceRound() {
76
+ if (order.length === 0) {
77
+ round += 1;
78
+ return state();
79
+ }
80
+ round += 1;
81
+ activeIndex = 0;
82
+ phaseIndex = 0;
83
+ for (const id of order)
84
+ resetPools(id);
85
+ return state();
86
+ }
87
+ function setOrder(next, keepActive = false) {
88
+ const previousActive = active();
89
+ order = [...next];
90
+ for (const id of order)
91
+ if (!pools.has(id))
92
+ resetPools(id);
93
+ for (const id of [...pools.keys()])
94
+ if (!order.includes(id))
95
+ pools.delete(id);
96
+ if (order.length === 0) {
97
+ activeIndex = -1;
98
+ }
99
+ else if (keepActive && previousActive !== null && order.includes(previousActive)) {
100
+ activeIndex = order.indexOf(previousActive);
101
+ }
102
+ else {
103
+ activeIndex = Math.min(Math.max(activeIndex, 0), order.length - 1);
104
+ }
105
+ return state();
106
+ }
107
+ function addParticipant(id, atIndex) {
108
+ if (order.includes(id))
109
+ return state();
110
+ const index = atIndex === undefined ? order.length : Math.min(Math.max(atIndex, 0), order.length);
111
+ order.splice(index, 0, id);
112
+ resetPools(id);
113
+ if (activeIndex < 0)
114
+ activeIndex = 0;
115
+ else if (index <= activeIndex)
116
+ activeIndex += 1;
117
+ return state();
118
+ }
119
+ function removeParticipant(id) {
120
+ const index = order.indexOf(id);
121
+ if (index < 0)
122
+ return state();
123
+ order.splice(index, 1);
124
+ pools.delete(id);
125
+ if (order.length === 0) {
126
+ activeIndex = -1;
127
+ }
128
+ else if (index < activeIndex) {
129
+ activeIndex -= 1;
130
+ }
131
+ else if (activeIndex >= order.length) {
132
+ activeIndex = 0;
133
+ }
134
+ return state();
135
+ }
136
+ return {
137
+ commit,
138
+ state,
139
+ order: () => [...order],
140
+ active,
141
+ phase,
142
+ round: () => round,
143
+ setOrder,
144
+ addParticipant,
145
+ removeParticipant,
146
+ advancePhase,
147
+ advanceTurn,
148
+ advanceRound,
149
+ pools: (participantId) => [...ensurePools(participantId).values()].map((p) => ({ ...p })),
150
+ pool: (participantId, poolId) => {
151
+ const p = ensurePools(participantId).get(poolId);
152
+ return p === undefined ? null : { ...p };
153
+ },
154
+ canSpend: (participantId, poolId, amount = 1) => {
155
+ const p = ensurePools(participantId).get(poolId);
156
+ return p !== undefined && p.current >= amount;
157
+ },
158
+ spend: (participantId, poolId, amount = 1) => {
159
+ const p = ensurePools(participantId).get(poolId);
160
+ if (p === undefined || p.current < amount)
161
+ return false;
162
+ p.current -= amount;
163
+ return true;
164
+ },
165
+ gain: (participantId, poolId, amount) => {
166
+ const p = ensurePools(participantId).get(poolId);
167
+ if (p === undefined)
168
+ return false;
169
+ p.current = Math.min(p.max, p.current + amount);
170
+ return true;
171
+ },
172
+ refill: (participantId, poolId) => {
173
+ const set = ensurePools(participantId);
174
+ if (poolId === undefined) {
175
+ for (const p of set.values())
176
+ p.current = p.max;
177
+ return;
178
+ }
179
+ const p = set.get(poolId);
180
+ if (p !== undefined)
181
+ p.current = p.max;
182
+ },
183
+ capture: () => {
184
+ const poolSnapshot = {};
185
+ for (const [participantId, set] of pools) {
186
+ const entry = {};
187
+ for (const [poolId, p] of set)
188
+ entry[poolId] = p.current;
189
+ poolSnapshot[participantId] = entry;
190
+ }
191
+ return { round, order: [...order], activeIndex, phaseIndex, pools: poolSnapshot };
192
+ },
193
+ restore: (snapshot) => {
194
+ round = snapshot.round;
195
+ order = [...snapshot.order];
196
+ activeIndex = snapshot.activeIndex;
197
+ phaseIndex = snapshot.phaseIndex;
198
+ pools.clear();
199
+ for (const id of order)
200
+ resetPools(id);
201
+ for (const [participantId, entry] of Object.entries(snapshot.pools)) {
202
+ const set = pools.get(participantId) ?? new Map();
203
+ for (const [poolId, current] of Object.entries(entry)) {
204
+ const existing = set.get(poolId);
205
+ if (existing !== undefined)
206
+ existing.current = current;
207
+ else {
208
+ const pc = poolConfigs.find((c) => c.id === poolId);
209
+ set.set(poolId, { id: poolId, current, max: pc?.max ?? current });
210
+ }
211
+ }
212
+ pools.set(participantId, set);
213
+ }
214
+ },
215
+ };
216
+ }
@@ -0,0 +1,47 @@
1
+ export type BuildRole = "owner" | "editor" | "viewer";
2
+ export interface PlotPermissionConfig {
3
+ plotId: string;
4
+ ownerId: string;
5
+ guildId?: string;
6
+ roles?: Readonly<Record<string, BuildRole>>;
7
+ guildRole?: BuildRole;
8
+ }
9
+ export interface PlotPermissionSnapshot {
10
+ plotId: string;
11
+ ownerId: string;
12
+ guildId?: string;
13
+ roles: Readonly<Record<string, BuildRole>>;
14
+ guildRole?: BuildRole;
15
+ }
16
+ export interface BuildActor {
17
+ userId: string;
18
+ guildId?: string;
19
+ }
20
+ export interface PlotPermissions {
21
+ roleOf(actor: BuildActor): BuildRole | null;
22
+ canEdit(actor: BuildActor): boolean;
23
+ canView(actor: BuildActor): boolean;
24
+ grant(userId: string, role: BuildRole): void;
25
+ revoke(userId: string): void;
26
+ setGuild(guildId: string | null, role?: BuildRole): void;
27
+ transferOwner(userId: string): void;
28
+ snapshot(): PlotPermissionSnapshot;
29
+ }
30
+ export declare function createPlotPermissions(config: PlotPermissionConfig): PlotPermissions;
31
+ export type ContributionGoal = Readonly<Record<string, number>>;
32
+ export interface ContributionResult {
33
+ accepted: number;
34
+ overflow: number;
35
+ complete: boolean;
36
+ }
37
+ export interface ContributionPool {
38
+ contribute(userId: string, resource: string, amount: number): ContributionResult;
39
+ totals(): Readonly<Record<string, number>>;
40
+ remaining(): Readonly<Record<string, number>>;
41
+ byContributor(userId: string): Readonly<Record<string, number>>;
42
+ progress(): number;
43
+ isComplete(): boolean;
44
+ snapshot(): Record<string, Record<string, number>>;
45
+ restore(snapshot: Record<string, Record<string, number>>): void;
46
+ }
47
+ export declare function createContributionPool(goal: ContributionGoal): ContributionPool;
@@ -0,0 +1,134 @@
1
+ const ROLE_RANK = { viewer: 0, editor: 1, owner: 2 };
2
+ export function createPlotPermissions(config) {
3
+ let ownerId = config.ownerId;
4
+ let guildId = config.guildId;
5
+ let guildRole = config.guildRole;
6
+ const roles = new Map(Object.entries(config.roles ?? {}));
7
+ function roleOf(actor) {
8
+ if (actor.userId === ownerId)
9
+ return "owner";
10
+ const explicit = roles.get(actor.userId);
11
+ if (explicit !== undefined)
12
+ return explicit;
13
+ if (guildId !== undefined &&
14
+ guildRole !== undefined &&
15
+ actor.guildId !== undefined &&
16
+ actor.guildId === guildId) {
17
+ return guildRole;
18
+ }
19
+ return null;
20
+ }
21
+ return {
22
+ roleOf,
23
+ canEdit(actor) {
24
+ const role = roleOf(actor);
25
+ return role !== null && ROLE_RANK[role] >= ROLE_RANK.editor;
26
+ },
27
+ canView(actor) {
28
+ return roleOf(actor) !== null;
29
+ },
30
+ grant(userId, role) {
31
+ if (userId === ownerId)
32
+ return;
33
+ roles.set(userId, role);
34
+ },
35
+ revoke(userId) {
36
+ roles.delete(userId);
37
+ },
38
+ setGuild(nextGuildId, role) {
39
+ guildId = nextGuildId ?? undefined;
40
+ guildRole = nextGuildId === null ? undefined : role ?? guildRole ?? "editor";
41
+ },
42
+ transferOwner(userId) {
43
+ roles.delete(userId);
44
+ ownerId = userId;
45
+ },
46
+ snapshot() {
47
+ return {
48
+ plotId: config.plotId,
49
+ ownerId,
50
+ ...(guildId === undefined ? {} : { guildId }),
51
+ roles: Object.fromEntries(roles),
52
+ ...(guildRole === undefined ? {} : { guildRole }),
53
+ };
54
+ },
55
+ };
56
+ }
57
+ export function createContributionPool(goal) {
58
+ const perResource = new Map();
59
+ function totalFor(resource) {
60
+ let sum = 0;
61
+ for (const amount of perResource.get(resource)?.values() ?? [])
62
+ sum += amount;
63
+ return sum;
64
+ }
65
+ function totals() {
66
+ const out = {};
67
+ for (const resource of Object.keys(goal))
68
+ out[resource] = totalFor(resource);
69
+ return out;
70
+ }
71
+ function isComplete() {
72
+ for (const resource of Object.keys(goal)) {
73
+ if (totalFor(resource) < goal[resource])
74
+ return false;
75
+ }
76
+ return true;
77
+ }
78
+ return {
79
+ contribute(userId, resource, amount) {
80
+ if (amount <= 0 || goal[resource] === undefined) {
81
+ return { accepted: 0, overflow: amount > 0 ? amount : 0, complete: isComplete() };
82
+ }
83
+ const need = Math.max(0, goal[resource] - totalFor(resource));
84
+ const accepted = Math.min(need, amount);
85
+ const overflow = amount - accepted;
86
+ if (accepted > 0) {
87
+ const contributors = perResource.get(resource) ?? new Map();
88
+ contributors.set(userId, (contributors.get(userId) ?? 0) + accepted);
89
+ perResource.set(resource, contributors);
90
+ }
91
+ return { accepted, overflow, complete: isComplete() };
92
+ },
93
+ totals,
94
+ remaining() {
95
+ const out = {};
96
+ for (const resource of Object.keys(goal))
97
+ out[resource] = Math.max(0, goal[resource] - totalFor(resource));
98
+ return out;
99
+ },
100
+ byContributor(userId) {
101
+ const out = {};
102
+ for (const [resource, contributors] of perResource) {
103
+ const amount = contributors.get(userId);
104
+ if (amount !== undefined)
105
+ out[resource] = amount;
106
+ }
107
+ return out;
108
+ },
109
+ progress() {
110
+ const resources = Object.keys(goal);
111
+ if (resources.length === 0)
112
+ return 1;
113
+ let sum = 0;
114
+ for (const resource of resources) {
115
+ const target = goal[resource];
116
+ sum += target <= 0 ? 1 : Math.min(1, totalFor(resource) / target);
117
+ }
118
+ return sum / resources.length;
119
+ },
120
+ isComplete,
121
+ snapshot() {
122
+ const out = {};
123
+ for (const [resource, contributors] of perResource)
124
+ out[resource] = Object.fromEntries(contributors);
125
+ return out;
126
+ },
127
+ restore(snapshot) {
128
+ perResource.clear();
129
+ for (const [resource, contributors] of Object.entries(snapshot)) {
130
+ perResource.set(resource, new Map(Object.entries(contributors)));
131
+ }
132
+ },
133
+ };
134
+ }