@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
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { createChangeSignal } from "../store/changeSignal.js";
|
|
2
|
+
const FRAME_CAPACITY = 240;
|
|
3
|
+
const RECENT_FRAMES = 60;
|
|
4
|
+
const LONG_FRAME_MS = 33.4;
|
|
5
|
+
const LATENCY_CAPACITY = 120;
|
|
6
|
+
const LOG_CAPACITY = 200;
|
|
7
|
+
const LOG_MESSAGE_MAX = 400;
|
|
8
|
+
const COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
9
|
+
function nowMs() {
|
|
10
|
+
const perf = globalThis.performance;
|
|
11
|
+
return perf !== undefined ? perf.now() : Date.now();
|
|
12
|
+
}
|
|
13
|
+
function inferKind(initial, hasOptions) {
|
|
14
|
+
if (hasOptions)
|
|
15
|
+
return "select";
|
|
16
|
+
if (typeof initial === "number")
|
|
17
|
+
return "slider";
|
|
18
|
+
if (typeof initial === "boolean")
|
|
19
|
+
return "toggle";
|
|
20
|
+
if (typeof initial === "string" && COLOR_PATTERN.test(initial))
|
|
21
|
+
return "color";
|
|
22
|
+
return "text";
|
|
23
|
+
}
|
|
24
|
+
function sliderBounds(initial, options) {
|
|
25
|
+
const min = options?.min ?? (initial < 0 ? initial * 2 : 0);
|
|
26
|
+
const max = options?.max ?? (initial > 0 ? initial * 2 : initial < 0 ? 0 : 1);
|
|
27
|
+
const step = options?.step ?? (max - min) / 100;
|
|
28
|
+
return { min, max, step };
|
|
29
|
+
}
|
|
30
|
+
function formatLogArg(arg) {
|
|
31
|
+
if (typeof arg === "string")
|
|
32
|
+
return arg;
|
|
33
|
+
if (arg instanceof Error)
|
|
34
|
+
return `${arg.name}: ${arg.message}`;
|
|
35
|
+
try {
|
|
36
|
+
return JSON.stringify(arg) ?? String(arg);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return String(arg);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function formatLogMessage(args) {
|
|
43
|
+
const message = args.map(formatLogArg).join(" ");
|
|
44
|
+
return message.length > LOG_MESSAGE_MAX ? `${message.slice(0, LOG_MESSAGE_MAX)}…` : message;
|
|
45
|
+
}
|
|
46
|
+
function percentile(sorted, fraction) {
|
|
47
|
+
if (sorted.length === 0)
|
|
48
|
+
return 0;
|
|
49
|
+
const index = Math.min(sorted.length - 1, Math.floor(sorted.length * fraction));
|
|
50
|
+
return sorted[index];
|
|
51
|
+
}
|
|
52
|
+
const MAX_TABLE_ENTRIES = 64;
|
|
53
|
+
function discoverableKind(value) {
|
|
54
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
55
|
+
return "slider";
|
|
56
|
+
if (typeof value === "boolean")
|
|
57
|
+
return "toggle";
|
|
58
|
+
if (typeof value === "string" && COLOR_PATTERN.test(value))
|
|
59
|
+
return "color";
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function discoverableEntries(candidate) {
|
|
63
|
+
if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate))
|
|
64
|
+
return [];
|
|
65
|
+
const proto = Object.getPrototypeOf(candidate);
|
|
66
|
+
if (proto !== Object.prototype && proto !== null)
|
|
67
|
+
return [];
|
|
68
|
+
const entries = [];
|
|
69
|
+
for (const [key, value] of Object.entries(candidate)) {
|
|
70
|
+
const kind = discoverableKind(value);
|
|
71
|
+
if (kind !== null)
|
|
72
|
+
entries.push([key, kind]);
|
|
73
|
+
if (entries.length > MAX_TABLE_ENTRIES)
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
return entries;
|
|
77
|
+
}
|
|
78
|
+
export function createDevtools() {
|
|
79
|
+
const signal = createChangeSignal();
|
|
80
|
+
const frameMsBuffer = new Float64Array(FRAME_CAPACITY);
|
|
81
|
+
const simMsBuffer = new Float64Array(FRAME_CAPACITY);
|
|
82
|
+
let frameIndex = 0;
|
|
83
|
+
let frameCount = 0;
|
|
84
|
+
let renderSample = null;
|
|
85
|
+
const logEntries = [];
|
|
86
|
+
let consoleCaptured = false;
|
|
87
|
+
const latencySamples = [];
|
|
88
|
+
const controlRecords = new Map();
|
|
89
|
+
const discoveredRecords = new Map();
|
|
90
|
+
const probeReaders = new Map();
|
|
91
|
+
const readProbes = () => {
|
|
92
|
+
const values = {};
|
|
93
|
+
for (const [name, read] of probeReaders) {
|
|
94
|
+
try {
|
|
95
|
+
values[name] = read();
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
values[name] = `probe failed: ${formatLogArg(error)}`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return values;
|
|
102
|
+
};
|
|
103
|
+
const pushLog = (level, message) => {
|
|
104
|
+
logEntries.push({ at: Date.now(), level, message });
|
|
105
|
+
if (logEntries.length > LOG_CAPACITY)
|
|
106
|
+
logEntries.splice(0, logEntries.length - LOG_CAPACITY);
|
|
107
|
+
};
|
|
108
|
+
const frameStats = () => {
|
|
109
|
+
if (frameCount === 0)
|
|
110
|
+
return null;
|
|
111
|
+
const size = Math.min(frameCount, FRAME_CAPACITY);
|
|
112
|
+
const frames = [];
|
|
113
|
+
const sims = [];
|
|
114
|
+
for (let offset = 0; offset < size; offset += 1) {
|
|
115
|
+
const at = (frameIndex - size + offset + FRAME_CAPACITY) % FRAME_CAPACITY;
|
|
116
|
+
frames.push(frameMsBuffer[at]);
|
|
117
|
+
sims.push(simMsBuffer[at]);
|
|
118
|
+
}
|
|
119
|
+
const recent = frames.slice(-RECENT_FRAMES);
|
|
120
|
+
const recentAvg = recent.reduce((sum, ms) => sum + ms, 0) / recent.length;
|
|
121
|
+
const sorted = [...frames].sort((a, b) => a - b);
|
|
122
|
+
return {
|
|
123
|
+
fps: recentAvg > 0 ? 1000 / recentAvg : 0,
|
|
124
|
+
avgFrameMs: frames.reduce((sum, ms) => sum + ms, 0) / frames.length,
|
|
125
|
+
p95FrameMs: percentile(sorted, 0.95),
|
|
126
|
+
maxFrameMs: sorted[sorted.length - 1],
|
|
127
|
+
avgSimMs: sims.reduce((sum, ms) => sum + ms, 0) / sims.length,
|
|
128
|
+
maxSimMs: sims.reduce((max, ms) => Math.max(max, ms), 0),
|
|
129
|
+
longFrames: frames.filter((ms) => ms > LONG_FRAME_MS).length,
|
|
130
|
+
samples: size,
|
|
131
|
+
recentFrameMs: recent,
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
const latencyStats = () => {
|
|
135
|
+
if (latencySamples.length === 0)
|
|
136
|
+
return null;
|
|
137
|
+
let min = Number.POSITIVE_INFINITY;
|
|
138
|
+
let max = 0;
|
|
139
|
+
let sum = 0;
|
|
140
|
+
for (const ms of latencySamples) {
|
|
141
|
+
min = Math.min(min, ms);
|
|
142
|
+
max = Math.max(max, ms);
|
|
143
|
+
sum += ms;
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
lastMs: latencySamples[latencySamples.length - 1],
|
|
147
|
+
avgMs: sum / latencySamples.length,
|
|
148
|
+
minMs: min,
|
|
149
|
+
maxMs: max,
|
|
150
|
+
samples: latencySamples.length,
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
const toControl = (record) => ({
|
|
154
|
+
name: record.name,
|
|
155
|
+
kind: record.kind,
|
|
156
|
+
label: record.label,
|
|
157
|
+
group: record.group,
|
|
158
|
+
initial: record.initial,
|
|
159
|
+
min: record.min,
|
|
160
|
+
max: record.max,
|
|
161
|
+
step: record.step,
|
|
162
|
+
options: record.options,
|
|
163
|
+
read: () => record.value,
|
|
164
|
+
write: (value) => writeControl(record, value),
|
|
165
|
+
reset: () => writeControl(record, record.initial),
|
|
166
|
+
});
|
|
167
|
+
const writeControl = (record, raw) => {
|
|
168
|
+
let next = raw;
|
|
169
|
+
if (record.kind === "slider" && typeof next === "number") {
|
|
170
|
+
if (record.min !== undefined)
|
|
171
|
+
next = Math.max(record.min, next);
|
|
172
|
+
if (record.max !== undefined)
|
|
173
|
+
next = Math.min(record.max, next);
|
|
174
|
+
}
|
|
175
|
+
if (Object.is(record.value, next))
|
|
176
|
+
return;
|
|
177
|
+
record.value = next;
|
|
178
|
+
for (const listener of record.listeners)
|
|
179
|
+
listener(next);
|
|
180
|
+
signal.notify();
|
|
181
|
+
};
|
|
182
|
+
const register = (name, initial, options) => {
|
|
183
|
+
const kind = inferKind(initial, options?.options !== undefined);
|
|
184
|
+
const existing = controlRecords.get(name);
|
|
185
|
+
const slashIndex = name.indexOf("/");
|
|
186
|
+
const derivedGroup = slashIndex > 0 ? name.slice(0, slashIndex) : "general";
|
|
187
|
+
const derivedLabel = slashIndex > 0 ? name.slice(slashIndex + 1) : name;
|
|
188
|
+
const bounds = kind === "slider" ? sliderBounds(initial, options) : undefined;
|
|
189
|
+
const record = existing !== undefined && existing.kind === kind
|
|
190
|
+
? existing
|
|
191
|
+
: {
|
|
192
|
+
name,
|
|
193
|
+
kind,
|
|
194
|
+
label: "",
|
|
195
|
+
group: "",
|
|
196
|
+
initial,
|
|
197
|
+
value: initial,
|
|
198
|
+
listeners: new Set(),
|
|
199
|
+
};
|
|
200
|
+
record.label = options?.label ?? derivedLabel;
|
|
201
|
+
record.group = options?.group ?? derivedGroup;
|
|
202
|
+
record.initial = initial;
|
|
203
|
+
record.min = bounds?.min;
|
|
204
|
+
record.max = bounds?.max;
|
|
205
|
+
record.step = bounds?.step;
|
|
206
|
+
record.options = options?.options;
|
|
207
|
+
if (options?.onChange !== undefined)
|
|
208
|
+
record.listeners.add(options.onChange);
|
|
209
|
+
controlRecords.set(name, record);
|
|
210
|
+
signal.notify();
|
|
211
|
+
return {
|
|
212
|
+
name,
|
|
213
|
+
kind,
|
|
214
|
+
initial,
|
|
215
|
+
get value() {
|
|
216
|
+
return record.value;
|
|
217
|
+
},
|
|
218
|
+
set: (value) => writeControl(record, value),
|
|
219
|
+
reset: () => writeControl(record, record.initial),
|
|
220
|
+
subscribe: (listener) => {
|
|
221
|
+
record.listeners.add(listener);
|
|
222
|
+
return () => record.listeners.delete(listener);
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
const bindDiscovered = (table, key, kind, accessor, source) => {
|
|
227
|
+
const id = `${table}/${key}`;
|
|
228
|
+
const existing = discoveredRecords.get(id);
|
|
229
|
+
if (existing === undefined) {
|
|
230
|
+
discoveredRecords.set(id, {
|
|
231
|
+
id,
|
|
232
|
+
table,
|
|
233
|
+
key,
|
|
234
|
+
kind,
|
|
235
|
+
initial: accessor.initial,
|
|
236
|
+
enabled: false,
|
|
237
|
+
source,
|
|
238
|
+
get: accessor.get,
|
|
239
|
+
set: accessor.set,
|
|
240
|
+
});
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
if (source !== null && existing.source === source)
|
|
244
|
+
return false;
|
|
245
|
+
existing.source = source;
|
|
246
|
+
existing.get = accessor.get;
|
|
247
|
+
existing.set = accessor.set;
|
|
248
|
+
const control = controlRecords.get(id);
|
|
249
|
+
if (existing.enabled && control !== undefined)
|
|
250
|
+
accessor.set(control.value);
|
|
251
|
+
return true;
|
|
252
|
+
};
|
|
253
|
+
const scanTable = (table, candidate) => {
|
|
254
|
+
const entries = discoverableEntries(candidate);
|
|
255
|
+
if (entries.length === 0)
|
|
256
|
+
return 0;
|
|
257
|
+
const target = candidate;
|
|
258
|
+
let changed = false;
|
|
259
|
+
for (const [key, kind] of entries) {
|
|
260
|
+
const accessor = {
|
|
261
|
+
initial: target[key],
|
|
262
|
+
get: () => target[key],
|
|
263
|
+
set: (value) => {
|
|
264
|
+
target[key] = value;
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
if (bindDiscovered(table, key, kind, accessor, target))
|
|
268
|
+
changed = true;
|
|
269
|
+
}
|
|
270
|
+
if (changed)
|
|
271
|
+
signal.notify();
|
|
272
|
+
return entries.length;
|
|
273
|
+
};
|
|
274
|
+
const enableDiscovered = (id) => {
|
|
275
|
+
const record = discoveredRecords.get(id);
|
|
276
|
+
if (record === undefined || record.enabled)
|
|
277
|
+
return;
|
|
278
|
+
record.enabled = true;
|
|
279
|
+
const current = record.get();
|
|
280
|
+
const handle = register(id, record.initial, { group: record.table, label: record.key });
|
|
281
|
+
handle.subscribe((value) => record.set(value));
|
|
282
|
+
if (!Object.is(current, record.initial))
|
|
283
|
+
handle.set(current);
|
|
284
|
+
};
|
|
285
|
+
return {
|
|
286
|
+
frame: {
|
|
287
|
+
record({ frameMs, simMs }) {
|
|
288
|
+
frameMsBuffer[frameIndex] = frameMs;
|
|
289
|
+
simMsBuffer[frameIndex] = simMs;
|
|
290
|
+
frameIndex = (frameIndex + 1) % FRAME_CAPACITY;
|
|
291
|
+
frameCount += 1;
|
|
292
|
+
},
|
|
293
|
+
stats: frameStats,
|
|
294
|
+
},
|
|
295
|
+
render: {
|
|
296
|
+
record(sample) {
|
|
297
|
+
renderSample = sample;
|
|
298
|
+
},
|
|
299
|
+
latest: () => renderSample,
|
|
300
|
+
},
|
|
301
|
+
logs: {
|
|
302
|
+
push: pushLog,
|
|
303
|
+
captureConsole() {
|
|
304
|
+
if (consoleCaptured)
|
|
305
|
+
return;
|
|
306
|
+
const globalConsole = globalThis.console;
|
|
307
|
+
if (globalConsole === undefined)
|
|
308
|
+
return;
|
|
309
|
+
consoleCaptured = true;
|
|
310
|
+
const levels = ["log", "info", "warn", "error"];
|
|
311
|
+
for (const level of levels) {
|
|
312
|
+
const original = globalConsole[level].bind(globalConsole);
|
|
313
|
+
globalConsole[level] = (...args) => {
|
|
314
|
+
pushLog(level, formatLogMessage(args));
|
|
315
|
+
original(...args);
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
list: () => logEntries,
|
|
320
|
+
clear() {
|
|
321
|
+
logEntries.length = 0;
|
|
322
|
+
signal.notify();
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
latency: {
|
|
326
|
+
record(ms) {
|
|
327
|
+
latencySamples.push(ms);
|
|
328
|
+
if (latencySamples.length > LATENCY_CAPACITY)
|
|
329
|
+
latencySamples.splice(0, latencySamples.length - LATENCY_CAPACITY);
|
|
330
|
+
},
|
|
331
|
+
stats: latencyStats,
|
|
332
|
+
},
|
|
333
|
+
controls: {
|
|
334
|
+
register,
|
|
335
|
+
list: () => [...controlRecords.values()].map(toControl),
|
|
336
|
+
get: (name) => {
|
|
337
|
+
const record = controlRecords.get(name);
|
|
338
|
+
return record === undefined ? null : toControl(record);
|
|
339
|
+
},
|
|
340
|
+
remove(name) {
|
|
341
|
+
if (controlRecords.delete(name))
|
|
342
|
+
signal.notify();
|
|
343
|
+
},
|
|
344
|
+
resetAll() {
|
|
345
|
+
for (const record of controlRecords.values())
|
|
346
|
+
writeControl(record, record.initial);
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
discover: {
|
|
350
|
+
bind(table, key, accessor) {
|
|
351
|
+
const kind = discoverableKind(accessor.initial);
|
|
352
|
+
if (kind === null)
|
|
353
|
+
return;
|
|
354
|
+
if (bindDiscovered(table, key, kind, accessor, null))
|
|
355
|
+
signal.notify();
|
|
356
|
+
},
|
|
357
|
+
scanTable,
|
|
358
|
+
scanModule(exports) {
|
|
359
|
+
let total = 0;
|
|
360
|
+
for (const [name, value] of Object.entries(exports)) {
|
|
361
|
+
total += scanTable(name, value);
|
|
362
|
+
}
|
|
363
|
+
return total;
|
|
364
|
+
},
|
|
365
|
+
list: () => [...discoveredRecords.values()].map((record) => ({
|
|
366
|
+
id: record.id,
|
|
367
|
+
table: record.table,
|
|
368
|
+
key: record.key,
|
|
369
|
+
kind: record.kind,
|
|
370
|
+
initial: record.initial,
|
|
371
|
+
enabled: record.enabled,
|
|
372
|
+
read: () => record.get(),
|
|
373
|
+
})),
|
|
374
|
+
enable(id) {
|
|
375
|
+
enableDiscovered(id);
|
|
376
|
+
signal.notify();
|
|
377
|
+
},
|
|
378
|
+
disable(id) {
|
|
379
|
+
const record = discoveredRecords.get(id);
|
|
380
|
+
if (record === undefined || !record.enabled)
|
|
381
|
+
return;
|
|
382
|
+
record.enabled = false;
|
|
383
|
+
record.set(record.initial);
|
|
384
|
+
controlRecords.delete(id);
|
|
385
|
+
signal.notify();
|
|
386
|
+
},
|
|
387
|
+
clear() {
|
|
388
|
+
for (const record of discoveredRecords.values()) {
|
|
389
|
+
if (record.enabled) {
|
|
390
|
+
record.set(record.initial);
|
|
391
|
+
controlRecords.delete(record.id);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
discoveredRecords.clear();
|
|
395
|
+
signal.notify();
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
overrides: {
|
|
399
|
+
export() {
|
|
400
|
+
const enabled = [...discoveredRecords.values()].filter((r) => r.enabled).map((r) => r.id);
|
|
401
|
+
const values = {};
|
|
402
|
+
for (const record of controlRecords.values()) {
|
|
403
|
+
if (!Object.is(record.value, record.initial))
|
|
404
|
+
values[record.name] = record.value;
|
|
405
|
+
}
|
|
406
|
+
return { enabled, values };
|
|
407
|
+
},
|
|
408
|
+
apply(overrides) {
|
|
409
|
+
for (const id of overrides.enabled)
|
|
410
|
+
enableDiscovered(id);
|
|
411
|
+
for (const [name, value] of Object.entries(overrides.values)) {
|
|
412
|
+
const record = controlRecords.get(name);
|
|
413
|
+
if (record !== undefined)
|
|
414
|
+
writeControl(record, value);
|
|
415
|
+
}
|
|
416
|
+
signal.notify();
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
probes: {
|
|
420
|
+
register(name, read) {
|
|
421
|
+
probeReaders.set(name, read);
|
|
422
|
+
signal.notify();
|
|
423
|
+
return () => {
|
|
424
|
+
probeReaders.delete(name);
|
|
425
|
+
signal.notify();
|
|
426
|
+
};
|
|
427
|
+
},
|
|
428
|
+
read: readProbes,
|
|
429
|
+
},
|
|
430
|
+
signal,
|
|
431
|
+
snapshot() {
|
|
432
|
+
return {
|
|
433
|
+
at: Date.now(),
|
|
434
|
+
frame: frameStats(),
|
|
435
|
+
render: renderSample,
|
|
436
|
+
latency: latencyStats(),
|
|
437
|
+
logs: [...logEntries],
|
|
438
|
+
probes: readProbes(),
|
|
439
|
+
controls: [...controlRecords.values()].map((record) => ({
|
|
440
|
+
name: record.name,
|
|
441
|
+
kind: record.kind,
|
|
442
|
+
value: record.value,
|
|
443
|
+
initial: record.initial,
|
|
444
|
+
})),
|
|
445
|
+
discovered: [...discoveredRecords.values()].map((record) => ({
|
|
446
|
+
id: record.id,
|
|
447
|
+
kind: record.kind,
|
|
448
|
+
value: record.get(),
|
|
449
|
+
enabled: record.enabled,
|
|
450
|
+
})),
|
|
451
|
+
};
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
export const devtools = createDevtools();
|
|
456
|
+
export function tunable(name, initial, options) {
|
|
457
|
+
return devtools.controls.register(name, initial, options);
|
|
458
|
+
}
|
|
459
|
+
export function snapshotDevtools() {
|
|
460
|
+
return devtools.snapshot();
|
|
461
|
+
}
|
|
462
|
+
export function instrumentLatency(target, methods, record = devtools.latency.record) {
|
|
463
|
+
const wrapped = { ...target };
|
|
464
|
+
for (const method of methods) {
|
|
465
|
+
const fn = target[method];
|
|
466
|
+
if (typeof fn !== "function")
|
|
467
|
+
continue;
|
|
468
|
+
wrapped[method] = ((...args) => {
|
|
469
|
+
const start = nowMs();
|
|
470
|
+
const result = fn.call(target, ...args);
|
|
471
|
+
if (result instanceof Promise) {
|
|
472
|
+
return result.then((value) => {
|
|
473
|
+
record(nowMs() - start);
|
|
474
|
+
return value;
|
|
475
|
+
}, (error) => {
|
|
476
|
+
record(nowMs() - start);
|
|
477
|
+
throw error;
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
return result;
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
return wrapped;
|
|
484
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface TunableTransformResult {
|
|
2
|
+
code: string;
|
|
3
|
+
bound: string[];
|
|
4
|
+
}
|
|
5
|
+
export declare function transformTunableExports(code: string, table: string): TunableTransformResult;
|
|
6
|
+
export declare function tunableModuleTable(id: string): string | null;
|
|
7
|
+
export declare function tunableDiscoveryPlugin(): {
|
|
8
|
+
name: string;
|
|
9
|
+
enforce: "pre";
|
|
10
|
+
transform(code: string, id: string): {
|
|
11
|
+
code: string;
|
|
12
|
+
map: null;
|
|
13
|
+
} | null;
|
|
14
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const TUNABLE_EXPORT_PATTERN = /^export const ([A-Za-z_$][\w$]*)\s*=\s*(-?\d+(?:\.\d+)?|true|false|"#(?:[0-9a-fA-F]{3,8})"|'#(?:[0-9a-fA-F]{3,8})')\s*;[ \t]*$/gm;
|
|
2
|
+
export function transformTunableExports(code, table) {
|
|
3
|
+
const bound = [];
|
|
4
|
+
const rewritten = code.replace(TUNABLE_EXPORT_PATTERN, (_match, name, literal) => {
|
|
5
|
+
bound.push(name);
|
|
6
|
+
return `export let ${name} = ${literal};`;
|
|
7
|
+
});
|
|
8
|
+
if (bound.length === 0)
|
|
9
|
+
return { code, bound };
|
|
10
|
+
const bindings = bound
|
|
11
|
+
.map((name) => `__jg_devtools.discover.bind(${JSON.stringify(table)}, ${JSON.stringify(name)}, { initial: ${name}, get: () => ${name}, set: (__jg_value) => { ${name} = __jg_value as never; } });`)
|
|
12
|
+
.join("\n");
|
|
13
|
+
return {
|
|
14
|
+
code: `${rewritten}\nimport { devtools as __jg_devtools } from "@jgengine/core/devtools/devtools";\n${bindings}\n`,
|
|
15
|
+
bound,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const GAME_MODULE_PATTERN = /[/\\]Games[/\\][^/\\]+[/\\]src[/\\](.+)\.(ts|tsx)$/;
|
|
19
|
+
const SKIP_MODULE_PATTERN = /(\.test\.|[/\\]main\.tsx$)/;
|
|
20
|
+
export function tunableModuleTable(id) {
|
|
21
|
+
if (SKIP_MODULE_PATTERN.test(id))
|
|
22
|
+
return null;
|
|
23
|
+
const match = GAME_MODULE_PATTERN.exec(id);
|
|
24
|
+
return match === null ? null : match[1].replace(/\\/g, "/");
|
|
25
|
+
}
|
|
26
|
+
export function tunableDiscoveryPlugin() {
|
|
27
|
+
return {
|
|
28
|
+
name: "jgengine-tunable-discovery",
|
|
29
|
+
enforce: "pre",
|
|
30
|
+
transform(code, id) {
|
|
31
|
+
const table = tunableModuleTable(id);
|
|
32
|
+
if (table === null)
|
|
33
|
+
return null;
|
|
34
|
+
const result = transformTunableExports(code, table);
|
|
35
|
+
return result.bound.length === 0 ? null : { code: result.code, map: null };
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type WalletState } from "./wallet.js";
|
|
2
|
+
export type WalletScope = {
|
|
3
|
+
kind: "user";
|
|
4
|
+
userId: string;
|
|
5
|
+
} | {
|
|
6
|
+
kind: "group";
|
|
7
|
+
groupId: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function scopeKey(scope: WalletScope): string;
|
|
10
|
+
export declare function userScope(userId: string): WalletScope;
|
|
11
|
+
export declare function groupScope(groupId: string): WalletScope;
|
|
12
|
+
export interface WalletBook {
|
|
13
|
+
scopes: Readonly<Record<string, WalletState>>;
|
|
14
|
+
contributions: Readonly<Record<string, Readonly<Record<string, Readonly<Record<string, number>>>>>>;
|
|
15
|
+
}
|
|
16
|
+
export type BookChargeResult = {
|
|
17
|
+
status: "ok";
|
|
18
|
+
book: WalletBook;
|
|
19
|
+
} | {
|
|
20
|
+
status: "rejected";
|
|
21
|
+
reason: "insufficient-funds";
|
|
22
|
+
};
|
|
23
|
+
export declare function createWalletBook(): WalletBook;
|
|
24
|
+
export declare function balanceIn(book: WalletBook, scope: WalletScope, currency: string): number;
|
|
25
|
+
export declare function grantTo(book: WalletBook, scope: WalletScope, currency: string, amount: number, by?: string): WalletBook;
|
|
26
|
+
export declare function chargeFrom(book: WalletBook, scope: WalletScope, currency: string, amount: number, by?: string): BookChargeResult;
|
|
27
|
+
export declare function contributionOf(book: WalletBook, groupId: string, userId: string): Readonly<Record<string, number>>;
|
|
28
|
+
export declare function contributorsOf(book: WalletBook, groupId: string): string[];
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { balance, charge, createEmptyWallet, grant } from "./wallet.js";
|
|
2
|
+
export function scopeKey(scope) {
|
|
3
|
+
return scope.kind === "user" ? `user:${scope.userId}` : `group:${scope.groupId}`;
|
|
4
|
+
}
|
|
5
|
+
export function userScope(userId) {
|
|
6
|
+
return { kind: "user", userId };
|
|
7
|
+
}
|
|
8
|
+
export function groupScope(groupId) {
|
|
9
|
+
return { kind: "group", groupId };
|
|
10
|
+
}
|
|
11
|
+
export function createWalletBook() {
|
|
12
|
+
return { scopes: {}, contributions: {} };
|
|
13
|
+
}
|
|
14
|
+
function scopeState(book, key) {
|
|
15
|
+
return book.scopes[key] ?? createEmptyWallet();
|
|
16
|
+
}
|
|
17
|
+
export function balanceIn(book, scope, currency) {
|
|
18
|
+
return balance(scopeState(book, scopeKey(scope)), currency);
|
|
19
|
+
}
|
|
20
|
+
function withScope(book, key, state) {
|
|
21
|
+
return { ...book, scopes: { ...book.scopes, [key]: state } };
|
|
22
|
+
}
|
|
23
|
+
function recordContribution(book, groupKey, userId, currency, delta) {
|
|
24
|
+
const group = book.contributions[groupKey] ?? {};
|
|
25
|
+
const perUser = group[userId] ?? {};
|
|
26
|
+
const next = (perUser[currency] ?? 0) + delta;
|
|
27
|
+
return {
|
|
28
|
+
...book,
|
|
29
|
+
contributions: {
|
|
30
|
+
...book.contributions,
|
|
31
|
+
[groupKey]: {
|
|
32
|
+
...group,
|
|
33
|
+
[userId]: { ...perUser, [currency]: next },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function grantTo(book, scope, currency, amount, by) {
|
|
39
|
+
const key = scopeKey(scope);
|
|
40
|
+
let next = withScope(book, key, grant(scopeState(book, key), currency, amount));
|
|
41
|
+
if (scope.kind === "group" && by !== undefined) {
|
|
42
|
+
next = recordContribution(next, key, by, currency, amount);
|
|
43
|
+
}
|
|
44
|
+
return next;
|
|
45
|
+
}
|
|
46
|
+
export function chargeFrom(book, scope, currency, amount, by) {
|
|
47
|
+
const key = scopeKey(scope);
|
|
48
|
+
const result = charge(scopeState(book, key), currency, amount);
|
|
49
|
+
if (result.status === "rejected")
|
|
50
|
+
return result;
|
|
51
|
+
let next = withScope(book, key, result.state);
|
|
52
|
+
if (scope.kind === "group" && by !== undefined) {
|
|
53
|
+
next = recordContribution(next, key, by, currency, -amount);
|
|
54
|
+
}
|
|
55
|
+
return { status: "ok", book: next };
|
|
56
|
+
}
|
|
57
|
+
export function contributionOf(book, groupId, userId) {
|
|
58
|
+
return book.contributions[scopeKey(groupScope(groupId))]?.[userId] ?? {};
|
|
59
|
+
}
|
|
60
|
+
export function contributorsOf(book, groupId) {
|
|
61
|
+
return Object.keys(book.contributions[scopeKey(groupScope(groupId))] ?? {});
|
|
62
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { UnlockDef, UnlockState } from "../game/unlocks.js";
|
|
2
|
+
export interface TechNodeDef extends UnlockDef {
|
|
3
|
+
requires?: readonly string[];
|
|
4
|
+
cost?: Readonly<Record<string, number>>;
|
|
5
|
+
grants?: readonly string[];
|
|
6
|
+
recipe?: string;
|
|
7
|
+
}
|
|
8
|
+
export type TechState = UnlockState;
|
|
9
|
+
export type TechRejection = {
|
|
10
|
+
reason: "unknown-node";
|
|
11
|
+
} | {
|
|
12
|
+
reason: "already-unlocked";
|
|
13
|
+
} | {
|
|
14
|
+
reason: "missing-prerequisites";
|
|
15
|
+
missing: readonly string[];
|
|
16
|
+
};
|
|
17
|
+
export type TechCheck = {
|
|
18
|
+
ok: true;
|
|
19
|
+
} | ({
|
|
20
|
+
ok: false;
|
|
21
|
+
} & TechRejection);
|
|
22
|
+
export declare function techPrerequisitesMet(state: TechState, node: TechNodeDef): boolean;
|
|
23
|
+
export declare function missingPrerequisites(state: TechState, node: TechNodeDef): string[];
|
|
24
|
+
export declare function canUnlockTech(defs: readonly TechNodeDef[], state: TechState, id: string): TechCheck;
|
|
25
|
+
export declare function grantTech(state: TechState, node: TechNodeDef): string[];
|
|
26
|
+
export declare function availableTech(defs: readonly TechNodeDef[], state: TechState): TechNodeDef[];
|
|
27
|
+
export declare function unlockedRecipes(defs: readonly TechNodeDef[], state: TechState): string[];
|
|
28
|
+
export interface TechTree {
|
|
29
|
+
has(userId: string, id: string): boolean;
|
|
30
|
+
canUnlock(userId: string, id: string): TechCheck;
|
|
31
|
+
unlock(userId: string, id: string): TechCheck;
|
|
32
|
+
available(userId: string): TechNodeDef[];
|
|
33
|
+
recipes(userId: string): string[];
|
|
34
|
+
list(userId: string): string[];
|
|
35
|
+
tree(categoryId: string): UnlockDef[];
|
|
36
|
+
node(id: string): TechNodeDef | null;
|
|
37
|
+
snapshot(userId: string): string[];
|
|
38
|
+
hydrate(userId: string, ids: string[]): void;
|
|
39
|
+
}
|
|
40
|
+
export declare function createTechTree(defs?: readonly TechNodeDef[]): TechTree;
|