@jgengine/core 0.6.0 → 0.8.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.
- package/CHANGELOG.md +214 -2
- package/README.md +2 -0
- package/dist/ai/crowd.d.ts +36 -0
- package/dist/ai/crowd.js +213 -0
- package/dist/ai/jobBoard.d.ts +49 -0
- package/dist/ai/jobBoard.js +155 -0
- package/dist/ai/spawnDirector.d.ts +61 -0
- package/dist/ai/spawnDirector.js +162 -0
- package/dist/ai/threat.d.ts +25 -0
- package/dist/ai/threat.js +67 -0
- package/dist/anim/easing.d.ts +14 -0
- package/dist/anim/easing.js +55 -0
- package/dist/audio/audioFalloff.d.ts +38 -0
- package/dist/audio/audioFalloff.js +32 -0
- package/dist/behaviour/behaviour.d.ts +136 -0
- package/dist/behaviour/behaviour.js +388 -0
- package/dist/board/laneBoard.d.ts +63 -0
- package/dist/board/laneBoard.js +114 -0
- package/dist/board/timelineBoard.d.ts +36 -0
- package/dist/board/timelineBoard.js +70 -0
- package/dist/cards/cardPile.d.ts +64 -0
- package/dist/cards/cardPile.js +175 -0
- package/dist/cards/modifierPipeline.d.ts +33 -0
- package/dist/cards/modifierPipeline.js +48 -0
- package/dist/combat/abilityKit.d.ts +47 -0
- package/dist/combat/abilityKit.js +139 -0
- package/dist/combat/animationState.d.ts +47 -0
- package/dist/combat/animationState.js +101 -0
- package/dist/combat/attackTags.d.ts +14 -0
- package/dist/combat/attackTags.js +26 -0
- package/dist/combat/breakMeters.d.ts +32 -0
- package/dist/combat/breakMeters.js +51 -0
- package/dist/combat/comboString.d.ts +35 -0
- package/dist/combat/comboString.js +59 -0
- package/dist/combat/death.d.ts +8 -0
- package/dist/combat/death.js +5 -2
- package/dist/combat/defensiveWindow.d.ts +34 -0
- package/dist/combat/defensiveWindow.js +66 -0
- package/dist/combat/downed.d.ts +35 -0
- package/dist/combat/downed.js +104 -0
- package/dist/combat/effects.d.ts +12 -1
- package/dist/combat/effects.js +31 -19
- package/dist/combat/hitReaction.d.ts +23 -0
- package/dist/combat/hitReaction.js +20 -0
- package/dist/combat/projectiles.d.ts +42 -5
- package/dist/combat/projectiles.js +98 -12
- package/dist/combat/resistance.d.ts +13 -0
- package/dist/combat/resistance.js +46 -0
- package/dist/combat/telegraph.d.ts +31 -0
- package/dist/combat/telegraph.js +51 -0
- package/dist/commands/commandRegistry.d.ts +1 -1
- package/dist/commands/commandRegistry.js +2 -1
- package/dist/crafting/crop.d.ts +59 -0
- package/dist/crafting/crop.js +181 -0
- package/dist/crafting/production.d.ts +64 -0
- package/dist/crafting/production.js +130 -0
- package/dist/crafting/recipe.d.ts +62 -0
- package/dist/crafting/recipe.js +101 -0
- package/dist/data/dataSource.d.ts +26 -0
- package/dist/data/dataSource.js +93 -0
- package/dist/data/devProxy.d.ts +11 -0
- package/dist/data/devProxy.js +38 -0
- package/dist/data/fetchJson.d.ts +20 -0
- package/dist/data/fetchJson.js +43 -0
- package/dist/data/jsonDataSource.d.ts +4 -0
- package/dist/data/jsonDataSource.js +6 -0
- package/dist/devtools/devtools.d.ts +157 -0
- package/dist/devtools/devtools.js +484 -0
- package/dist/devtools/transformTunables.d.ts +14 -0
- package/dist/devtools/transformTunables.js +38 -0
- package/dist/economy/sharedWallet.d.ts +28 -0
- package/dist/economy/sharedWallet.js +62 -0
- package/dist/economy/techTree.d.ts +40 -0
- package/dist/economy/techTree.js +66 -0
- package/dist/game/chat.d.ts +66 -0
- package/dist/game/chat.js +165 -0
- package/dist/game/cosmetics.d.ts +25 -0
- package/dist/game/cosmetics.js +51 -0
- package/dist/game/defineGame.d.ts +10 -2
- package/dist/game/defineGame.js +2 -1
- package/dist/game/events.d.ts +87 -0
- package/dist/game/levelSequence.d.ts +44 -0
- package/dist/game/levelSequence.js +78 -0
- package/dist/game/lootFilter.d.ts +33 -0
- package/dist/game/lootFilter.js +48 -0
- package/dist/game/ping.d.ts +67 -0
- package/dist/game/ping.js +86 -0
- package/dist/game/playableGame.d.ts +516 -8
- package/dist/game/playableGame.js +6 -1
- package/dist/game/progression.d.ts +1 -0
- package/dist/game/progression.js +23 -2
- package/dist/game/race.d.ts +100 -0
- package/dist/game/race.js +236 -0
- package/dist/game/runDraft.d.ts +34 -0
- package/dist/game/runDraft.js +97 -0
- package/dist/game/social.d.ts +69 -0
- package/dist/game/social.js +138 -0
- package/dist/game/spawnPoints.d.ts +19 -0
- package/dist/game/spawnPoints.js +23 -0
- package/dist/game/worldItem.d.ts +95 -0
- package/dist/game/worldItem.js +138 -0
- package/dist/input/actionBindings.d.ts +10 -0
- package/dist/input/actionBindings.js +14 -0
- package/dist/input/axisInput.d.ts +43 -0
- package/dist/input/axisInput.js +70 -0
- package/dist/input/gestureSurface.d.ts +31 -0
- package/dist/input/gestureSurface.js +120 -0
- package/dist/input/pointer.d.ts +69 -0
- package/dist/input/pointer.js +65 -0
- package/dist/input/touchScheme.d.ts +83 -0
- package/dist/input/touchScheme.js +115 -0
- package/dist/interaction/contextMenu.d.ts +30 -0
- package/dist/interaction/contextMenu.js +22 -0
- package/dist/interaction/qte.d.ts +20 -0
- package/dist/interaction/qte.js +24 -0
- package/dist/interaction/skillCheck.d.ts +20 -0
- package/dist/interaction/skillCheck.js +26 -0
- package/dist/inventory/shapedGrid.d.ts +56 -0
- package/dist/inventory/shapedGrid.js +176 -0
- package/dist/inventory/storageTier.d.ts +52 -0
- package/dist/inventory/storageTier.js +77 -0
- package/dist/item/affix.d.ts +54 -0
- package/dist/item/affix.js +115 -0
- package/dist/item/durability.d.ts +51 -0
- package/dist/item/durability.js +78 -0
- package/dist/item/modularItem.d.ts +46 -0
- package/dist/item/modularItem.js +75 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +71 -1
- package/dist/movement/dash.d.ts +37 -0
- package/dist/movement/dash.js +65 -0
- package/dist/movement/movementModel.d.ts +36 -3
- package/dist/movement/movementModel.js +81 -5
- package/dist/movement/voxelController.d.ts +61 -0
- package/dist/movement/voxelController.js +229 -0
- package/dist/multiplayer/chatContract.d.ts +42 -0
- package/dist/multiplayer/chatContract.js +57 -0
- package/dist/multiplayer/combatSnapshot.d.ts +45 -0
- package/dist/multiplayer/combatSnapshot.js +89 -0
- package/dist/multiplayer/identity.d.ts +13 -0
- package/dist/multiplayer/identity.js +20 -0
- package/dist/multiplayer/lagCompensation.d.ts +43 -0
- package/dist/multiplayer/lagCompensation.js +154 -0
- package/dist/multiplayer/matchmaking.d.ts +33 -0
- package/dist/multiplayer/matchmaking.js +60 -0
- package/dist/multiplayer/poseSyncGate.d.ts +1 -0
- package/dist/multiplayer/poseSyncGate.js +13 -1
- package/dist/multiplayer/presenceModel.d.ts +3 -0
- package/dist/multiplayer/presenceModel.js +15 -1
- package/dist/multiplayer/simultaneousCommit.d.ts +34 -0
- package/dist/multiplayer/simultaneousCommit.js +53 -0
- package/dist/multiplayer/voiceContract.d.ts +44 -0
- package/dist/multiplayer/voiceContract.js +108 -0
- package/dist/nav/navConstrain.d.ts +14 -0
- package/dist/nav/navConstrain.js +26 -0
- package/dist/nav/navFromEnvironment.d.ts +11 -0
- package/dist/nav/navFromEnvironment.js +16 -0
- package/dist/nav/navGrid.d.ts +56 -0
- package/dist/nav/navGrid.js +265 -0
- package/dist/nav/pathFollow.d.ts +31 -0
- package/dist/nav/pathFollow.js +69 -0
- package/dist/physics/ballisticSweep.d.ts +18 -0
- package/dist/physics/ballisticSweep.js +49 -0
- package/dist/physics/buoyancy.d.ts +49 -0
- package/dist/physics/buoyancy.js +112 -0
- package/dist/physics/carryable.d.ts +46 -0
- package/dist/physics/carryable.js +110 -0
- package/dist/physics/damageZones.d.ts +56 -0
- package/dist/physics/damageZones.js +107 -0
- package/dist/physics/forceVolume.d.ts +48 -0
- package/dist/physics/forceVolume.js +119 -0
- package/dist/physics/physicsWorld.d.ts +124 -2
- package/dist/physics/physicsWorld.js +493 -41
- package/dist/physics/ragdoll.d.ts +49 -0
- package/dist/physics/ragdoll.js +103 -0
- package/dist/physics/spatialGrid.d.ts +41 -0
- package/dist/physics/spatialGrid.js +143 -0
- package/dist/physics/structure.d.ts +83 -0
- package/dist/physics/structure.js +0 -0
- package/dist/physics/traversal.d.ts +75 -0
- package/dist/physics/traversal.js +138 -0
- package/dist/physics/vehicleBody.d.ts +99 -0
- package/dist/physics/vehicleBody.js +233 -0
- package/dist/puzzle/cellGrid.d.ts +26 -0
- package/dist/puzzle/cellGrid.js +110 -0
- package/dist/puzzle/fallingPiece.d.ts +24 -0
- package/dist/puzzle/fallingPiece.js +51 -0
- package/dist/random/nameGen.d.ts +18 -0
- package/dist/random/nameGen.js +35 -0
- package/dist/random/rng.d.ts +2 -0
- package/dist/random/rng.js +23 -0
- package/dist/runtime/adapter.d.ts +35 -0
- package/dist/runtime/adapter.js +37 -1
- package/dist/runtime/cameraDirector.d.ts +10 -0
- package/dist/runtime/cameraDirector.js +25 -0
- package/dist/runtime/gameContext.d.ts +107 -3
- package/dist/runtime/gameContext.js +253 -7
- package/dist/runtime/hostPersistence.d.ts +39 -0
- package/dist/runtime/hostPersistence.js +54 -0
- package/dist/runtime/inputSnapshot.d.ts +7 -0
- package/dist/runtime/inputSnapshot.js +12 -0
- package/dist/runtime/motionIntents.d.ts +25 -0
- package/dist/runtime/motionIntents.js +25 -0
- package/dist/runtime/persistenceScope.d.ts +32 -0
- package/dist/runtime/persistenceScope.js +64 -0
- package/dist/runtime/transport.d.ts +32 -0
- package/dist/scene/autoTarget.d.ts +15 -0
- package/dist/scene/autoTarget.js +54 -0
- package/dist/scene/behaviors.d.ts +13 -1
- package/dist/scene/behaviors.js +3 -0
- package/dist/scene/captureCheck.d.ts +7 -0
- package/dist/scene/captureCheck.js +11 -0
- package/dist/scene/entityStore.d.ts +9 -1
- package/dist/scene/entityStore.js +25 -2
- package/dist/scene/form.d.ts +40 -0
- package/dist/scene/form.js +56 -0
- package/dist/scene/mount.d.ts +62 -0
- package/dist/scene/mount.js +107 -0
- package/dist/scene/movementSpeed.d.ts +35 -0
- package/dist/scene/movementSpeed.js +20 -0
- package/dist/scene/objectQuery.d.ts +18 -0
- package/dist/scene/objectQuery.js +108 -0
- package/dist/scene/objectStore.d.ts +11 -0
- package/dist/scene/objectStore.js +109 -4
- package/dist/scene/paintLayer.d.ts +15 -0
- package/dist/scene/paintLayer.js +53 -0
- package/dist/scene/possession.d.ts +30 -0
- package/dist/scene/possession.js +51 -0
- package/dist/scene/roster.d.ts +25 -0
- package/dist/scene/roster.js +57 -0
- package/dist/scene/selection.d.ts +30 -0
- package/dist/scene/selection.js +38 -0
- package/dist/scene/spatial.d.ts +21 -0
- package/dist/scene/spatial.js +63 -1
- package/dist/scene/stationClaim.d.ts +43 -0
- package/dist/scene/stationClaim.js +109 -0
- package/dist/sensor/concealment.d.ts +21 -0
- package/dist/sensor/concealment.js +63 -0
- package/dist/sensor/freezeMonitor.d.ts +17 -0
- package/dist/sensor/freezeMonitor.js +35 -0
- package/dist/sensor/frustumSensor.d.ts +60 -0
- package/dist/sensor/frustumSensor.js +104 -0
- package/dist/sensor/hiddenStateProbe.d.ts +31 -0
- package/dist/sensor/hiddenStateProbe.js +40 -0
- package/dist/sensor/recordingBuffer.d.ts +29 -0
- package/dist/sensor/recordingBuffer.js +61 -0
- package/dist/sensor/revealQuery.d.ts +22 -0
- package/dist/sensor/revealQuery.js +36 -0
- package/dist/session/contestedChannel.d.ts +36 -0
- package/dist/session/contestedChannel.js +100 -0
- package/dist/session/extraction.d.ts +54 -0
- package/dist/session/extraction.js +108 -0
- package/dist/session/ring.d.ts +36 -0
- package/dist/session/ring.js +88 -0
- package/dist/session/roles.d.ts +6 -0
- package/dist/session/roles.js +32 -0
- package/dist/session/roundState.d.ts +79 -0
- package/dist/session/roundState.js +158 -0
- package/dist/stats/accumulatorMeter.d.ts +31 -0
- package/dist/stats/accumulatorMeter.js +81 -0
- package/dist/stats/eventMeter.d.ts +34 -0
- package/dist/stats/eventMeter.js +76 -0
- package/dist/stats/rollCheck.d.ts +15 -0
- package/dist/stats/rollCheck.js +15 -0
- package/dist/survival/decayMeter.d.ts +59 -0
- package/dist/survival/decayMeter.js +95 -0
- package/dist/survival/moodle.d.ts +48 -0
- package/dist/survival/moodle.js +91 -0
- package/dist/survival/regionHealth.d.ts +72 -0
- package/dist/survival/regionHealth.js +129 -0
- package/dist/tactics/fallingGrid.d.ts +62 -0
- package/dist/tactics/fallingGrid.js +158 -0
- package/dist/tactics/predictiveQuery.d.ts +34 -0
- package/dist/tactics/predictiveQuery.js +44 -0
- package/dist/tactics/snapshot.d.ts +18 -0
- package/dist/tactics/snapshot.js +65 -0
- package/dist/tactics/surface.d.ts +52 -0
- package/dist/tactics/surface.js +135 -0
- package/dist/tactics/tacticalGrid.d.ts +68 -0
- package/dist/tactics/tacticalGrid.js +261 -0
- package/dist/time/beatClock.d.ts +40 -0
- package/dist/time/beatClock.js +73 -0
- package/dist/time/simClock.d.ts +25 -1
- package/dist/time/simClock.js +28 -3
- package/dist/turn/commit.d.ts +27 -0
- package/dist/turn/commit.js +72 -0
- package/dist/turn/intent.d.ts +14 -0
- package/dist/turn/intent.js +29 -0
- package/dist/turn/turnLoop.d.ts +60 -0
- package/dist/turn/turnLoop.js +223 -0
- package/dist/world/buildPermissions.d.ts +47 -0
- package/dist/world/buildPermissions.js +134 -0
- package/dist/world/carve.d.ts +105 -0
- package/dist/world/carve.js +175 -0
- package/dist/world/connectors.d.ts +40 -0
- package/dist/world/connectors.js +67 -0
- package/dist/world/envField.d.ts +71 -0
- package/dist/world/envField.js +96 -0
- package/dist/world/environmentSummary.d.ts +53 -0
- package/dist/world/environmentSummary.js +129 -0
- package/dist/world/features.d.ts +87 -6
- package/dist/world/features.js +52 -2
- package/dist/world/fog.d.ts +40 -0
- package/dist/world/fog.js +137 -0
- package/dist/world/gridInstances.d.ts +13 -0
- package/dist/world/gridInstances.js +22 -0
- package/dist/world/markers.d.ts +52 -0
- package/dist/world/markers.js +96 -0
- package/dist/world/minimap.d.ts +42 -0
- package/dist/world/minimap.js +72 -0
- package/dist/world/placedStructureStore.d.ts +39 -0
- package/dist/world/placedStructureStore.js +95 -0
- package/dist/world/placementController.d.ts +69 -0
- package/dist/world/placementController.js +153 -0
- package/dist/world/realm.d.ts +61 -0
- package/dist/world/realm.js +66 -0
- package/dist/world/support.d.ts +27 -0
- package/dist/world/support.js +75 -0
- package/dist/world/terraform.d.ts +54 -0
- package/dist/world/terraform.js +186 -0
- package/dist/world/terrain.d.ts +23 -1
- package/dist/world/terrain.js +64 -1
- package/dist/world/voxelField.d.ts +54 -0
- package/dist/world/voxelField.js +241 -0
- package/dist/world/walls.d.ts +61 -0
- package/dist/world/walls.js +221 -0
- package/dist/world/weather.d.ts +92 -0
- package/dist/world/weather.js +169 -0
- package/llms.txt +4102 -0
- package/package.json +3 -2
package/dist/time/simClock.js
CHANGED
|
@@ -7,9 +7,12 @@ export function createSimClock(options = {}) {
|
|
|
7
7
|
const configuredSpeeds = config.speeds === undefined ? [1, 2, 3, 4] : config.speeds.filter((value) => value > 0);
|
|
8
8
|
const speeds = configuredSpeeds.length > 0 ? configuredSpeeds : [1];
|
|
9
9
|
const dayLength = config.dayLength !== undefined && config.dayLength > 0 ? config.dayLength : SECONDS_PER_GAME_DAY;
|
|
10
|
+
const daysPerYear = config.daysPerYear !== undefined && config.daysPerYear > 0 ? config.daysPerYear : 365;
|
|
11
|
+
const seasons = config.seasons;
|
|
10
12
|
let now = config.start !== undefined && config.start > 0 ? config.start : 0;
|
|
11
13
|
let paused = config.startPaused ?? false;
|
|
12
14
|
let playSpeed = speeds[0];
|
|
15
|
+
let timescale = 1;
|
|
13
16
|
let nextTimerId = 0;
|
|
14
17
|
const timers = new Map();
|
|
15
18
|
let displayMinute = minuteIndex(now, dayLength);
|
|
@@ -17,14 +20,26 @@ export function createSimClock(options = {}) {
|
|
|
17
20
|
const day = Math.floor(now / dayLength);
|
|
18
21
|
const dayFraction = (now - day * dayLength) / dayLength;
|
|
19
22
|
const secondOfDay = dayFraction * 24 * 3600;
|
|
20
|
-
|
|
23
|
+
const year = Math.floor(day / daysPerYear);
|
|
24
|
+
const dayOfYear = day - year * daysPerYear;
|
|
25
|
+
const yearFraction = (dayOfYear + dayFraction) / daysPerYear;
|
|
26
|
+
const result = {
|
|
21
27
|
totalSeconds: now,
|
|
22
28
|
day,
|
|
23
29
|
hour: Math.floor(secondOfDay / 3600) % 24,
|
|
24
30
|
minute: Math.floor(secondOfDay / 60) % 60,
|
|
25
31
|
second: Math.floor(secondOfDay) % 60,
|
|
26
32
|
dayFraction,
|
|
33
|
+
year,
|
|
34
|
+
dayOfYear,
|
|
35
|
+
yearFraction,
|
|
27
36
|
};
|
|
37
|
+
if (seasons !== undefined && seasons.length > 0) {
|
|
38
|
+
const segmentLength = daysPerYear / seasons.length;
|
|
39
|
+
const index = Math.min(seasons.length - 1, Math.floor(dayOfYear / segmentLength));
|
|
40
|
+
result.season = seasons[index];
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
28
43
|
}
|
|
29
44
|
function fireDueTimers() {
|
|
30
45
|
let fires = 0;
|
|
@@ -52,9 +67,9 @@ export function createSimClock(options = {}) {
|
|
|
52
67
|
}
|
|
53
68
|
}
|
|
54
69
|
function advance(realDt) {
|
|
55
|
-
if (paused || realDt <= 0)
|
|
70
|
+
if (paused || realDt <= 0 || timescale === 0)
|
|
56
71
|
return 0;
|
|
57
|
-
const gameDt = realDt * scale * playSpeed;
|
|
72
|
+
const gameDt = realDt * scale * playSpeed * timescale;
|
|
58
73
|
now += gameDt;
|
|
59
74
|
fireDueTimers();
|
|
60
75
|
const minute = minuteIndex(now, dayLength);
|
|
@@ -91,6 +106,13 @@ export function createSimClock(options = {}) {
|
|
|
91
106
|
const index = speeds.indexOf(playSpeed);
|
|
92
107
|
setSpeed(speeds[(index + 1) % speeds.length]);
|
|
93
108
|
}
|
|
109
|
+
function setTimescale(multiplier) {
|
|
110
|
+
const next = Number.isFinite(multiplier) && multiplier > 0 ? multiplier : 0;
|
|
111
|
+
if (next === timescale)
|
|
112
|
+
return;
|
|
113
|
+
timescale = next;
|
|
114
|
+
onChange();
|
|
115
|
+
}
|
|
94
116
|
function register(due, interval, callback) {
|
|
95
117
|
const id = nextTimerId;
|
|
96
118
|
nextTimerId += 1;
|
|
@@ -108,6 +130,7 @@ export function createSimClock(options = {}) {
|
|
|
108
130
|
paused,
|
|
109
131
|
speed: paused ? 0 : playSpeed,
|
|
110
132
|
playSpeed,
|
|
133
|
+
timescale,
|
|
111
134
|
scale,
|
|
112
135
|
speeds,
|
|
113
136
|
calendar: calendar(),
|
|
@@ -119,6 +142,8 @@ export function createSimClock(options = {}) {
|
|
|
119
142
|
toggle: () => (paused ? play() : pause()),
|
|
120
143
|
setSpeed,
|
|
121
144
|
cycleSpeed,
|
|
145
|
+
timescale: () => timescale,
|
|
146
|
+
setTimescale,
|
|
122
147
|
after: (seconds, callback) => register(now + Math.max(0, seconds), null, callback),
|
|
123
148
|
every: (seconds, callback) => {
|
|
124
149
|
const interval = Math.max(Number.EPSILON, seconds);
|
|
@@ -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,14 @@
|
|
|
1
|
+
export interface DeclaredIntent<TKind extends string = string> {
|
|
2
|
+
kind: TKind;
|
|
3
|
+
magnitude?: number;
|
|
4
|
+
targetId?: string;
|
|
5
|
+
note?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface IntentBoard<TKind extends string = string> {
|
|
8
|
+
declare(participantId: string, intent: DeclaredIntent<TKind>): void;
|
|
9
|
+
peek(participantId: string): DeclaredIntent<TKind> | null;
|
|
10
|
+
all(): readonly [string, DeclaredIntent<TKind>][];
|
|
11
|
+
consume(participantId: string): DeclaredIntent<TKind> | null;
|
|
12
|
+
clear(participantId?: string): void;
|
|
13
|
+
}
|
|
14
|
+
export declare function createIntentBoard<TKind extends string = string>(): IntentBoard<TKind>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function createIntentBoard() {
|
|
2
|
+
const intents = new Map();
|
|
3
|
+
return {
|
|
4
|
+
declare(participantId, intent) {
|
|
5
|
+
intents.set(participantId, { ...intent });
|
|
6
|
+
},
|
|
7
|
+
peek(participantId) {
|
|
8
|
+
const intent = intents.get(participantId);
|
|
9
|
+
return intent === undefined ? null : { ...intent };
|
|
10
|
+
},
|
|
11
|
+
all() {
|
|
12
|
+
return [...intents.entries()].map(([participantId, intent]) => [participantId, { ...intent }]);
|
|
13
|
+
},
|
|
14
|
+
consume(participantId) {
|
|
15
|
+
const intent = intents.get(participantId);
|
|
16
|
+
if (intent === undefined)
|
|
17
|
+
return null;
|
|
18
|
+
intents.delete(participantId);
|
|
19
|
+
return intent;
|
|
20
|
+
},
|
|
21
|
+
clear(participantId) {
|
|
22
|
+
if (participantId === undefined) {
|
|
23
|
+
intents.clear();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
intents.delete(participantId);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
onTurnStart?(participantId: string): void;
|
|
16
|
+
onTurnEnd?(participantId: string): void;
|
|
17
|
+
}
|
|
18
|
+
export interface PoolState {
|
|
19
|
+
id: string;
|
|
20
|
+
current: number;
|
|
21
|
+
max: number;
|
|
22
|
+
}
|
|
23
|
+
export interface TurnState {
|
|
24
|
+
round: number;
|
|
25
|
+
order: readonly string[];
|
|
26
|
+
activeIndex: number;
|
|
27
|
+
active: string | null;
|
|
28
|
+
phaseIndex: number;
|
|
29
|
+
phase: string | null;
|
|
30
|
+
}
|
|
31
|
+
export interface TurnLoopSnapshot {
|
|
32
|
+
round: number;
|
|
33
|
+
order: string[];
|
|
34
|
+
activeIndex: number;
|
|
35
|
+
phaseIndex: number;
|
|
36
|
+
pools: Record<string, Record<string, number>>;
|
|
37
|
+
}
|
|
38
|
+
export interface TurnLoop<TAction = unknown> {
|
|
39
|
+
readonly commit: CommitController<TAction>;
|
|
40
|
+
state(): TurnState;
|
|
41
|
+
order(): readonly string[];
|
|
42
|
+
active(): string | null;
|
|
43
|
+
phase(): string | null;
|
|
44
|
+
round(): number;
|
|
45
|
+
setOrder(order: readonly string[], keepActive?: boolean): TurnState;
|
|
46
|
+
addParticipant(id: string, atIndex?: number): TurnState;
|
|
47
|
+
removeParticipant(id: string): TurnState;
|
|
48
|
+
advancePhase(): TurnState;
|
|
49
|
+
advanceTurn(): TurnState;
|
|
50
|
+
advanceRound(): TurnState;
|
|
51
|
+
pools(participantId: string): PoolState[];
|
|
52
|
+
pool(participantId: string, poolId: string): PoolState | null;
|
|
53
|
+
canSpend(participantId: string, poolId: string, amount?: number): boolean;
|
|
54
|
+
spend(participantId: string, poolId: string, amount?: number): boolean;
|
|
55
|
+
gain(participantId: string, poolId: string, amount: number): boolean;
|
|
56
|
+
refill(participantId: string, poolId?: string): void;
|
|
57
|
+
capture(): TurnLoopSnapshot;
|
|
58
|
+
restore(snapshot: TurnLoopSnapshot): void;
|
|
59
|
+
}
|
|
60
|
+
export declare function createTurnLoop<TAction = unknown>(config: TurnLoopConfig): TurnLoop<TAction>;
|
|
@@ -0,0 +1,223 @@
|
|
|
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
|
+
if (activeIndex >= 0)
|
|
31
|
+
config.onTurnStart?.(order[activeIndex]);
|
|
32
|
+
function active() {
|
|
33
|
+
return activeIndex >= 0 && activeIndex < order.length ? order[activeIndex] : null;
|
|
34
|
+
}
|
|
35
|
+
function phase() {
|
|
36
|
+
return phases.length > 0 ? phases[phaseIndex] ?? null : null;
|
|
37
|
+
}
|
|
38
|
+
function state() {
|
|
39
|
+
return {
|
|
40
|
+
round,
|
|
41
|
+
order: [...order],
|
|
42
|
+
activeIndex,
|
|
43
|
+
active: active(),
|
|
44
|
+
phaseIndex: phases.length > 0 ? phaseIndex : 0,
|
|
45
|
+
phase: phase(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function advanceTurn() {
|
|
49
|
+
if (order.length === 0) {
|
|
50
|
+
activeIndex = -1;
|
|
51
|
+
return state();
|
|
52
|
+
}
|
|
53
|
+
const previous = active();
|
|
54
|
+
phaseIndex = 0;
|
|
55
|
+
if (activeIndex < 0) {
|
|
56
|
+
activeIndex = 0;
|
|
57
|
+
}
|
|
58
|
+
else if (activeIndex >= order.length - 1) {
|
|
59
|
+
activeIndex = 0;
|
|
60
|
+
round += 1;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
activeIndex += 1;
|
|
64
|
+
}
|
|
65
|
+
if (previous !== null)
|
|
66
|
+
config.onTurnEnd?.(previous);
|
|
67
|
+
const current = active();
|
|
68
|
+
if (current !== null) {
|
|
69
|
+
resetPools(current);
|
|
70
|
+
config.onTurnStart?.(current);
|
|
71
|
+
}
|
|
72
|
+
return state();
|
|
73
|
+
}
|
|
74
|
+
function advancePhase() {
|
|
75
|
+
if (phases.length === 0)
|
|
76
|
+
return advanceTurn();
|
|
77
|
+
if (phaseIndex >= phases.length - 1)
|
|
78
|
+
return advanceTurn();
|
|
79
|
+
phaseIndex += 1;
|
|
80
|
+
return state();
|
|
81
|
+
}
|
|
82
|
+
function advanceRound() {
|
|
83
|
+
if (order.length === 0) {
|
|
84
|
+
round += 1;
|
|
85
|
+
return state();
|
|
86
|
+
}
|
|
87
|
+
round += 1;
|
|
88
|
+
activeIndex = 0;
|
|
89
|
+
phaseIndex = 0;
|
|
90
|
+
for (const id of order)
|
|
91
|
+
resetPools(id);
|
|
92
|
+
return state();
|
|
93
|
+
}
|
|
94
|
+
function setOrder(next, keepActive = false) {
|
|
95
|
+
const previousActive = active();
|
|
96
|
+
order = [...next];
|
|
97
|
+
for (const id of order)
|
|
98
|
+
if (!pools.has(id))
|
|
99
|
+
resetPools(id);
|
|
100
|
+
for (const id of [...pools.keys()])
|
|
101
|
+
if (!order.includes(id))
|
|
102
|
+
pools.delete(id);
|
|
103
|
+
if (order.length === 0) {
|
|
104
|
+
activeIndex = -1;
|
|
105
|
+
}
|
|
106
|
+
else if (keepActive && previousActive !== null && order.includes(previousActive)) {
|
|
107
|
+
activeIndex = order.indexOf(previousActive);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
activeIndex = Math.min(Math.max(activeIndex, 0), order.length - 1);
|
|
111
|
+
}
|
|
112
|
+
return state();
|
|
113
|
+
}
|
|
114
|
+
function addParticipant(id, atIndex) {
|
|
115
|
+
if (order.includes(id))
|
|
116
|
+
return state();
|
|
117
|
+
const index = atIndex === undefined ? order.length : Math.min(Math.max(atIndex, 0), order.length);
|
|
118
|
+
order.splice(index, 0, id);
|
|
119
|
+
resetPools(id);
|
|
120
|
+
if (activeIndex < 0)
|
|
121
|
+
activeIndex = 0;
|
|
122
|
+
else if (index <= activeIndex)
|
|
123
|
+
activeIndex += 1;
|
|
124
|
+
return state();
|
|
125
|
+
}
|
|
126
|
+
function removeParticipant(id) {
|
|
127
|
+
const index = order.indexOf(id);
|
|
128
|
+
if (index < 0)
|
|
129
|
+
return state();
|
|
130
|
+
order.splice(index, 1);
|
|
131
|
+
pools.delete(id);
|
|
132
|
+
if (order.length === 0) {
|
|
133
|
+
activeIndex = -1;
|
|
134
|
+
}
|
|
135
|
+
else if (index < activeIndex) {
|
|
136
|
+
activeIndex -= 1;
|
|
137
|
+
}
|
|
138
|
+
else if (activeIndex >= order.length) {
|
|
139
|
+
activeIndex = 0;
|
|
140
|
+
}
|
|
141
|
+
return state();
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
commit,
|
|
145
|
+
state,
|
|
146
|
+
order: () => [...order],
|
|
147
|
+
active,
|
|
148
|
+
phase,
|
|
149
|
+
round: () => round,
|
|
150
|
+
setOrder,
|
|
151
|
+
addParticipant,
|
|
152
|
+
removeParticipant,
|
|
153
|
+
advancePhase,
|
|
154
|
+
advanceTurn,
|
|
155
|
+
advanceRound,
|
|
156
|
+
pools: (participantId) => [...ensurePools(participantId).values()].map((p) => ({ ...p })),
|
|
157
|
+
pool: (participantId, poolId) => {
|
|
158
|
+
const p = ensurePools(participantId).get(poolId);
|
|
159
|
+
return p === undefined ? null : { ...p };
|
|
160
|
+
},
|
|
161
|
+
canSpend: (participantId, poolId, amount = 1) => {
|
|
162
|
+
const p = ensurePools(participantId).get(poolId);
|
|
163
|
+
return p !== undefined && p.current >= amount;
|
|
164
|
+
},
|
|
165
|
+
spend: (participantId, poolId, amount = 1) => {
|
|
166
|
+
const p = ensurePools(participantId).get(poolId);
|
|
167
|
+
if (p === undefined || p.current < amount)
|
|
168
|
+
return false;
|
|
169
|
+
p.current -= amount;
|
|
170
|
+
return true;
|
|
171
|
+
},
|
|
172
|
+
gain: (participantId, poolId, amount) => {
|
|
173
|
+
const p = ensurePools(participantId).get(poolId);
|
|
174
|
+
if (p === undefined)
|
|
175
|
+
return false;
|
|
176
|
+
p.current = Math.min(p.max, p.current + amount);
|
|
177
|
+
return true;
|
|
178
|
+
},
|
|
179
|
+
refill: (participantId, poolId) => {
|
|
180
|
+
const set = ensurePools(participantId);
|
|
181
|
+
if (poolId === undefined) {
|
|
182
|
+
for (const p of set.values())
|
|
183
|
+
p.current = p.max;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const p = set.get(poolId);
|
|
187
|
+
if (p !== undefined)
|
|
188
|
+
p.current = p.max;
|
|
189
|
+
},
|
|
190
|
+
capture: () => {
|
|
191
|
+
const poolSnapshot = {};
|
|
192
|
+
for (const [participantId, set] of pools) {
|
|
193
|
+
const entry = {};
|
|
194
|
+
for (const [poolId, p] of set)
|
|
195
|
+
entry[poolId] = p.current;
|
|
196
|
+
poolSnapshot[participantId] = entry;
|
|
197
|
+
}
|
|
198
|
+
return { round, order: [...order], activeIndex, phaseIndex, pools: poolSnapshot };
|
|
199
|
+
},
|
|
200
|
+
restore: (snapshot) => {
|
|
201
|
+
round = snapshot.round;
|
|
202
|
+
order = [...snapshot.order];
|
|
203
|
+
activeIndex = snapshot.activeIndex;
|
|
204
|
+
phaseIndex = snapshot.phaseIndex;
|
|
205
|
+
pools.clear();
|
|
206
|
+
for (const id of order)
|
|
207
|
+
resetPools(id);
|
|
208
|
+
for (const [participantId, entry] of Object.entries(snapshot.pools)) {
|
|
209
|
+
const set = pools.get(participantId) ?? new Map();
|
|
210
|
+
for (const [poolId, current] of Object.entries(entry)) {
|
|
211
|
+
const existing = set.get(poolId);
|
|
212
|
+
if (existing !== undefined)
|
|
213
|
+
existing.current = current;
|
|
214
|
+
else {
|
|
215
|
+
const pc = poolConfigs.find((c) => c.id === poolId);
|
|
216
|
+
set.set(poolId, { id: poolId, current, max: pc?.max ?? current });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
pools.set(participantId, set);
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
@@ -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
|
+
}
|