@jgengine/core 0.7.0 → 0.9.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 +63 -0
- package/README.md +2 -0
- package/dist/ai/flock.d.ts +36 -0
- package/dist/ai/flock.js +182 -0
- package/dist/ai/groupAssist.d.ts +19 -0
- package/dist/ai/groupAssist.js +59 -0
- package/dist/ai/laneSelect.d.ts +21 -0
- package/dist/ai/laneSelect.js +37 -0
- package/dist/ai/mobBrain.d.ts +68 -0
- package/dist/ai/mobBrain.js +115 -0
- package/dist/ai/spawnDirector.d.ts +5 -0
- package/dist/ai/spawnDirector.js +14 -2
- package/dist/ai/threat.d.ts +2 -0
- package/dist/ai/threat.js +34 -0
- package/dist/anim/easing.d.ts +14 -0
- package/dist/anim/easing.js +55 -0
- package/dist/behaviour/behaviour.d.ts +136 -0
- package/dist/behaviour/behaviour.js +388 -0
- package/dist/cards/cardPile.d.ts +1 -0
- package/dist/cards/cardPile.js +12 -3
- package/dist/cartridge/runtime.d.ts +15 -0
- package/dist/cartridge/runtime.js +556 -0
- package/dist/cartridge/spec.d.ts +188 -0
- package/dist/cartridge/spec.js +19 -0
- package/dist/cartridge/validate.d.ts +2 -0
- package/dist/cartridge/validate.js +78 -0
- package/dist/combat/abilityKit.d.ts +22 -2
- package/dist/combat/abilityKit.js +72 -13
- package/dist/combat/castRunner.d.ts +40 -0
- package/dist/combat/castRunner.js +56 -0
- package/dist/combat/comboPoints.d.ts +14 -0
- package/dist/combat/comboPoints.js +52 -0
- package/dist/combat/defensiveWindow.d.ts +1 -1
- package/dist/combat/defensiveWindow.js +5 -3
- package/dist/combat/effects.d.ts +1 -1
- package/dist/combat/effects.js +9 -6
- package/dist/combat/projectiles.d.ts +64 -6
- package/dist/combat/projectiles.js +232 -74
- package/dist/combat/resistance.d.ts +16 -4
- package/dist/combat/resistance.js +43 -7
- package/dist/combat/resourcePool.d.ts +20 -0
- package/dist/combat/resourcePool.js +55 -0
- package/dist/combat/shotOrigin.d.ts +40 -0
- package/dist/combat/shotOrigin.js +57 -0
- package/dist/combat/telegraph.d.ts +30 -1
- package/dist/combat/telegraph.js +61 -0
- package/dist/commands/commandRegistry.d.ts +1 -1
- package/dist/commands/commandRegistry.js +2 -1
- package/dist/crafting/recipe.js +4 -1
- 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 +205 -0
- package/dist/devtools/devtools.js +832 -0
- package/dist/devtools/transformTunables.d.ts +14 -0
- package/dist/devtools/transformTunables.js +38 -0
- package/dist/devtools/tunableSchema.d.ts +140 -0
- package/dist/devtools/tunableSchema.js +476 -0
- package/dist/faction/factions.d.ts +33 -0
- package/dist/faction/factions.js +76 -0
- package/dist/faction/reputation.d.ts +32 -0
- package/dist/faction/reputation.js +82 -0
- package/dist/game/chat.d.ts +68 -0
- package/dist/game/chat.js +173 -0
- package/dist/game/chatFilter.d.ts +15 -0
- package/dist/game/chatFilter.js +55 -0
- package/dist/game/controlGate.d.ts +4 -0
- package/dist/game/controlGate.js +7 -0
- package/dist/game/defineGame.d.ts +10 -2
- package/dist/game/defineGame.js +2 -1
- package/dist/game/events.d.ts +35 -0
- package/dist/game/gamePhase.d.ts +20 -0
- package/dist/game/gamePhase.js +21 -0
- package/dist/game/levelSequence.d.ts +44 -0
- package/dist/game/levelSequence.js +78 -0
- package/dist/game/lootTable.d.ts +10 -1
- package/dist/game/lootTable.js +27 -7
- package/dist/game/playableGame.d.ts +286 -6
- 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/quest.d.ts +7 -3
- package/dist/game/quest.js +17 -8
- package/dist/game/race.d.ts +44 -1
- package/dist/game/race.js +116 -11
- package/dist/game/recordBook.d.ts +29 -0
- package/dist/game/recordBook.js +86 -0
- package/dist/game/social.d.ts +42 -0
- package/dist/game/social.js +122 -0
- package/dist/game/spawnPoints.d.ts +19 -0
- package/dist/game/spawnPoints.js +23 -0
- package/dist/game/talents.d.ts +47 -0
- package/dist/game/talents.js +140 -0
- package/dist/game/worldItem.d.ts +3 -0
- package/dist/game/worldItem.js +12 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/input/actionBindings.d.ts +10 -0
- package/dist/input/actionBindings.js +14 -0
- package/dist/input/axisInput.d.ts +26 -3
- package/dist/input/axisInput.js +44 -4
- package/dist/input/bindingOverrides.d.ts +25 -0
- package/dist/input/bindingOverrides.js +95 -0
- package/dist/input/gestureSurface.d.ts +31 -0
- package/dist/input/gestureSurface.js +120 -0
- package/dist/input/pointer.d.ts +45 -0
- package/dist/input/pointer.js +48 -0
- package/dist/input/pointerAxis.d.ts +30 -0
- package/dist/input/pointerAxis.js +31 -0
- package/dist/input/touchScheme.d.ts +89 -0
- package/dist/input/touchScheme.js +153 -0
- package/dist/item/affix.d.ts +1 -1
- package/dist/item/affix.js +1 -19
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +59 -1
- package/dist/movement/dash.d.ts +3 -0
- package/dist/movement/dash.js +19 -5
- package/dist/movement/glideModel.d.ts +44 -0
- package/dist/movement/glideModel.js +54 -0
- package/dist/movement/grappleSwing.d.ts +34 -0
- package/dist/movement/grappleSwing.js +56 -0
- package/dist/movement/leaderTrail.d.ts +29 -0
- package/dist/movement/leaderTrail.js +67 -0
- package/dist/movement/movementModel.d.ts +36 -3
- package/dist/movement/movementModel.js +81 -5
- package/dist/movement/steering.d.ts +27 -0
- package/dist/movement/steering.js +40 -0
- 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/feedWriteGate.d.ts +10 -0
- package/dist/multiplayer/feedWriteGate.js +12 -0
- package/dist/multiplayer/identity.d.ts +13 -0
- package/dist/multiplayer/identity.js +20 -0
- package/dist/multiplayer/poseSyncGate.d.ts +1 -0
- package/dist/multiplayer/poseSyncGate.js +13 -1
- package/dist/multiplayer/presenceModel.d.ts +17 -2
- package/dist/multiplayer/presenceModel.js +30 -4
- package/dist/multiplayer/voiceContract.d.ts +44 -0
- package/dist/multiplayer/voiceContract.js +108 -0
- package/dist/nav/corridors.d.ts +30 -0
- package/dist/nav/corridors.js +69 -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 +10 -0
- package/dist/nav/navGrid.js +48 -5
- package/dist/nav/pathFollow.d.ts +4 -2
- package/dist/nav/pathFollow.js +5 -3
- package/dist/nav/railGraph.d.ts +66 -0
- package/dist/nav/railGraph.js +129 -0
- package/dist/nav/timetable.d.ts +41 -0
- package/dist/nav/timetable.js +94 -0
- package/dist/physics/ballisticSweep.d.ts +18 -0
- package/dist/physics/ballisticSweep.js +49 -0
- package/dist/physics/buoyancy.d.ts +6 -0
- package/dist/physics/buoyancy.js +14 -0
- package/dist/physics/flowTube.d.ts +32 -0
- package/dist/physics/flowTube.js +64 -0
- package/dist/physics/forceVolume.d.ts +27 -0
- package/dist/physics/forceVolume.js +43 -5
- package/dist/physics/kinematicVehicle.d.ts +52 -0
- package/dist/physics/kinematicVehicle.js +81 -0
- package/dist/physics/physicsWorld.d.ts +54 -8
- package/dist/physics/physicsWorld.js +400 -79
- package/dist/physics/ragdoll.js +1 -1
- package/dist/physics/spatialGrid.d.ts +1 -1
- package/dist/physics/spatialGrid.js +5 -0
- package/dist/physics/vehicleBody.d.ts +6 -0
- package/dist/physics/vehicleBody.js +18 -18
- 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/random/seedLink.d.ts +5 -0
- package/dist/random/seedLink.js +35 -0
- package/dist/runtime/adapter.d.ts +35 -0
- package/dist/runtime/adapter.js +37 -1
- package/dist/runtime/cameraDirector.d.ts +15 -0
- package/dist/runtime/cameraDirector.js +31 -0
- package/dist/runtime/commandRunner.d.ts +2 -2
- package/dist/runtime/commandRunner.js +2 -2
- package/dist/runtime/gameContext.d.ts +65 -2
- package/dist/runtime/gameContext.js +280 -9
- package/dist/runtime/gameRuntime.d.ts +5 -1
- package/dist/runtime/gameRuntime.js +13 -12
- package/dist/runtime/hostPersistence.d.ts +30 -1
- package/dist/runtime/hostPersistence.js +70 -5
- package/dist/runtime/inputSnapshot.d.ts +12 -0
- package/dist/runtime/inputSnapshot.js +17 -0
- package/dist/runtime/motionIntents.d.ts +33 -0
- package/dist/runtime/motionIntents.js +43 -0
- package/dist/runtime/snapshot.d.ts +1 -0
- package/dist/runtime/snapshot.js +1 -1
- package/dist/runtime/transport.d.ts +32 -0
- package/dist/runtime/visibility.d.ts +74 -0
- package/dist/runtime/visibility.js +127 -0
- package/dist/scene/colliders.d.ts +44 -0
- package/dist/scene/colliders.js +77 -0
- package/dist/scene/entityStore.d.ts +30 -5
- package/dist/scene/entityStore.js +83 -13
- package/dist/scene/movementSpeed.d.ts +35 -0
- package/dist/scene/movementSpeed.js +20 -0
- package/dist/scene/objectQuery.d.ts +29 -0
- package/dist/scene/objectQuery.js +140 -0
- package/dist/scene/objectStore.d.ts +13 -0
- package/dist/scene/objectStore.js +130 -6
- package/dist/scene/paintLayer.d.ts +15 -0
- package/dist/scene/paintLayer.js +53 -0
- package/dist/scene/sceneRaycast.d.ts +80 -0
- package/dist/scene/sceneRaycast.js +284 -0
- package/dist/scene/spatial.d.ts +17 -1
- package/dist/scene/spatial.js +140 -17
- 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/recordingBuffer.d.ts +6 -0
- package/dist/sensor/recordingBuffer.js +7 -0
- package/dist/sensor/replayLoop.d.ts +39 -0
- package/dist/sensor/replayLoop.js +64 -0
- package/dist/sensor/visionCone.d.ts +31 -0
- package/dist/sensor/visionCone.js +72 -0
- package/dist/session/extraction.d.ts +2 -2
- package/dist/session/extraction.js +13 -2
- package/dist/session/roles.d.ts +6 -0
- package/dist/session/roles.js +32 -0
- package/dist/session/roundState.d.ts +41 -17
- package/dist/session/roundState.js +67 -38
- package/dist/settings/settingsModel.d.ts +97 -0
- package/dist/settings/settingsModel.js +100 -0
- package/dist/stats/statModifiers.d.ts +5 -2
- package/dist/stats/statModifiers.js +32 -14
- package/dist/store/observableKeyedStore.js +8 -2
- package/dist/survival/decayMeter.d.ts +1 -1
- package/dist/survival/decayMeter.js +0 -1
- package/dist/tactics/fallingGrid.d.ts +62 -0
- package/dist/tactics/fallingGrid.js +158 -0
- package/dist/tactics/tacticalGrid.d.ts +11 -0
- package/dist/tactics/tacticalGrid.js +17 -0
- package/dist/time/beatClock.d.ts +18 -0
- package/dist/time/beatClock.js +24 -0
- package/dist/time/idleProgress.d.ts +29 -0
- package/dist/time/idleProgress.js +31 -0
- package/dist/time/serverTick.d.ts +7 -1
- package/dist/time/serverTick.js +27 -4
- package/dist/time/simClock.d.ts +25 -1
- package/dist/time/simClock.js +28 -3
- package/dist/time/stateSchedule.d.ts +57 -0
- package/dist/time/stateSchedule.js +121 -0
- package/dist/turn/commit.d.ts +2 -2
- package/dist/turn/commit.js +2 -2
- package/dist/turn/intent.d.ts +14 -0
- package/dist/turn/intent.js +29 -0
- package/dist/turn/turnLoop.d.ts +2 -0
- package/dist/turn/turnLoop.js +8 -1
- package/dist/ui/hudLayout.d.ts +58 -0
- package/dist/ui/hudLayout.js +207 -0
- package/dist/ui/hudScale.d.ts +44 -0
- package/dist/ui/hudScale.js +55 -0
- package/dist/visibility/assetStreaming.d.ts +66 -0
- package/dist/visibility/assetStreaming.js +201 -0
- package/dist/visibility/bounds.d.ts +66 -0
- package/dist/visibility/bounds.js +113 -0
- package/dist/visibility/camera.d.ts +21 -0
- package/dist/visibility/camera.js +3 -0
- package/dist/visibility/config.d.ts +21 -0
- package/dist/visibility/config.js +1 -0
- package/dist/visibility/diagnostics.d.ts +26 -0
- package/dist/visibility/diagnostics.js +36 -0
- package/dist/visibility/distance.d.ts +11 -0
- package/dist/visibility/distance.js +30 -0
- package/dist/visibility/frustum.d.ts +47 -0
- package/dist/visibility/frustum.js +186 -0
- package/dist/visibility/occlusion.d.ts +36 -0
- package/dist/visibility/occlusion.js +45 -0
- package/dist/visibility/settings.d.ts +75 -0
- package/dist/visibility/settings.js +49 -0
- package/dist/visibility/simulationCulling.d.ts +34 -0
- package/dist/visibility/simulationCulling.js +36 -0
- package/dist/visibility/spatialIndex.d.ts +45 -0
- package/dist/visibility/spatialIndex.js +198 -0
- package/dist/visibility/visibilitySystem.d.ts +90 -0
- package/dist/visibility/visibilitySystem.js +257 -0
- package/dist/world/buildings.d.ts +6 -0
- package/dist/world/buildings.js +152 -0
- package/dist/world/carve.d.ts +7 -2
- package/dist/world/carve.js +17 -7
- package/dist/world/cellStates.d.ts +45 -0
- package/dist/world/cellStates.js +103 -0
- package/dist/world/environmentSummary.d.ts +61 -0
- package/dist/world/environmentSummary.js +138 -0
- package/dist/world/features.d.ts +138 -9
- package/dist/world/features.js +73 -2
- package/dist/world/gridInstances.d.ts +12 -0
- package/dist/world/gridInstances.js +160 -0
- package/dist/world/lod.d.ts +17 -0
- package/dist/world/lod.js +66 -0
- package/dist/world/mapLayers.d.ts +60 -0
- package/dist/world/mapLayers.js +44 -0
- package/dist/world/massing.d.ts +45 -0
- package/dist/world/massing.js +581 -0
- package/dist/world/minimap.d.ts +13 -1
- package/dist/world/minimap.js +21 -2
- package/dist/world/placementController.d.ts +22 -1
- package/dist/world/placementController.js +56 -0
- package/dist/world/terrain.d.ts +62 -1
- package/dist/world/terrain.js +253 -9
- package/dist/world/voxelField.d.ts +54 -0
- package/dist/world/voxelField.js +241 -0
- package/dist/world/water.d.ts +7 -0
- package/dist/world/water.js +19 -4
- package/dist/world/weather.d.ts +3 -18
- package/dist/world/weather.js +4 -17
- package/dist/world/windZones.d.ts +47 -0
- package/dist/world/windZones.js +59 -0
- package/llms.txt +4983 -0
- package/package.json +14 -3
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gravity tick interval for a given level: linear speed-up with a floor, generalizing
|
|
3
|
+
* `Games/block-stacker`'s `gravityInterval` (`max(0.05, 0.8 - level * 0.07)` seconds) to milliseconds
|
|
4
|
+
* with configurable base/step/floor.
|
|
5
|
+
*/
|
|
6
|
+
export function gravityIntervalMs(level, config = {}) {
|
|
7
|
+
const baseMs = config.baseMs ?? 800;
|
|
8
|
+
const perLevel = config.perLevel ?? 70;
|
|
9
|
+
const minMs = config.minMs ?? 50;
|
|
10
|
+
return Math.max(minMs, baseMs - level * perLevel);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A game-agnostic cell grid for gravity-driven falling-block mechanics: bounded occupancy,
|
|
14
|
+
* footprint placement/collision, line-clear collapse, column-gravity settle (for match-3/sand
|
|
15
|
+
* style cascades), and a pure lock-delay helper. Extracted from `Games/block-stacker`'s tetris
|
|
16
|
+
* logic so any falling-block game can adopt it instead of reimplementing the grid.
|
|
17
|
+
*/
|
|
18
|
+
export function createFallingGrid(config) {
|
|
19
|
+
const cols = config.cols;
|
|
20
|
+
const rows = config.rows;
|
|
21
|
+
const lockDelayMs = config.lockDelayMs ?? 500;
|
|
22
|
+
if (cols <= 0 || rows <= 0)
|
|
23
|
+
throw new Error("falling grid needs positive dimensions");
|
|
24
|
+
let cells = new Array(cols * rows).fill(null);
|
|
25
|
+
function index(col, row) {
|
|
26
|
+
return row * cols + col;
|
|
27
|
+
}
|
|
28
|
+
function inBounds(col, row) {
|
|
29
|
+
return col >= 0 && col < cols && row >= 0 && row < rows;
|
|
30
|
+
}
|
|
31
|
+
function cellAt(col, row) {
|
|
32
|
+
if (!inBounds(col, row))
|
|
33
|
+
return null;
|
|
34
|
+
return cells[index(col, row)] ?? null;
|
|
35
|
+
}
|
|
36
|
+
function occupied(col, row) {
|
|
37
|
+
return cellAt(col, row) !== null;
|
|
38
|
+
}
|
|
39
|
+
function setCell(col, row, value) {
|
|
40
|
+
if (!inBounds(col, row))
|
|
41
|
+
return;
|
|
42
|
+
cells[index(col, row)] = value;
|
|
43
|
+
}
|
|
44
|
+
function clearCell(col, row) {
|
|
45
|
+
if (!inBounds(col, row))
|
|
46
|
+
return;
|
|
47
|
+
cells[index(col, row)] = null;
|
|
48
|
+
}
|
|
49
|
+
function canPlace(footprint, offset) {
|
|
50
|
+
for (const [dc, dr] of footprint) {
|
|
51
|
+
const col = offset[0] + dc;
|
|
52
|
+
const row = offset[1] + dr;
|
|
53
|
+
if (col < 0 || col >= cols || row >= rows)
|
|
54
|
+
return false;
|
|
55
|
+
if (row >= 0 && occupied(col, row))
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
function place(footprint, offset, value) {
|
|
61
|
+
if (!canPlace(footprint, offset))
|
|
62
|
+
return false;
|
|
63
|
+
for (const [dc, dr] of footprint) {
|
|
64
|
+
const col = offset[0] + dc;
|
|
65
|
+
const row = offset[1] + dr;
|
|
66
|
+
if (row >= 0)
|
|
67
|
+
setCell(col, row, value);
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
function fullRows() {
|
|
72
|
+
const full = [];
|
|
73
|
+
for (let row = 0; row < rows; row += 1) {
|
|
74
|
+
let rowFull = true;
|
|
75
|
+
for (let col = 0; col < cols; col += 1) {
|
|
76
|
+
if (!occupied(col, row)) {
|
|
77
|
+
rowFull = false;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (rowFull)
|
|
82
|
+
full.push(row);
|
|
83
|
+
}
|
|
84
|
+
return full;
|
|
85
|
+
}
|
|
86
|
+
function clearRows(rowsToClear) {
|
|
87
|
+
const toClear = new Set(rowsToClear);
|
|
88
|
+
if (toClear.size === 0)
|
|
89
|
+
return 0;
|
|
90
|
+
const kept = [];
|
|
91
|
+
for (let row = 0; row < rows; row += 1) {
|
|
92
|
+
if (toClear.has(row))
|
|
93
|
+
continue;
|
|
94
|
+
kept.push(cells.slice(index(0, row), index(0, row) + cols));
|
|
95
|
+
}
|
|
96
|
+
const cleared = rows - kept.length;
|
|
97
|
+
const next = [];
|
|
98
|
+
for (let i = 0; i < cleared; i += 1)
|
|
99
|
+
next.push(...new Array(cols).fill(null));
|
|
100
|
+
for (const row of kept)
|
|
101
|
+
next.push(...row);
|
|
102
|
+
cells = next;
|
|
103
|
+
return cleared;
|
|
104
|
+
}
|
|
105
|
+
function settle() {
|
|
106
|
+
let moved = 0;
|
|
107
|
+
for (let col = 0; col < cols; col += 1) {
|
|
108
|
+
const values = [];
|
|
109
|
+
for (let row = 0; row < rows; row += 1) {
|
|
110
|
+
const value = cellAt(col, row);
|
|
111
|
+
if (value !== null)
|
|
112
|
+
values.push({ row, value });
|
|
113
|
+
}
|
|
114
|
+
for (let row = 0; row < rows; row += 1)
|
|
115
|
+
clearCell(col, row);
|
|
116
|
+
const count = values.length;
|
|
117
|
+
for (let i = 0; i < count; i += 1) {
|
|
118
|
+
const newRow = rows - count + i;
|
|
119
|
+
const { row: oldRow, value } = values[i];
|
|
120
|
+
setCell(col, newRow, value);
|
|
121
|
+
if (oldRow !== newRow)
|
|
122
|
+
moved += 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return moved;
|
|
126
|
+
}
|
|
127
|
+
function advanceLock(state, grounded, dtMs) {
|
|
128
|
+
if (!grounded)
|
|
129
|
+
return null;
|
|
130
|
+
return { elapsedMs: (state?.elapsedMs ?? 0) + dtMs };
|
|
131
|
+
}
|
|
132
|
+
function lockExpired(state) {
|
|
133
|
+
return state !== null && state.elapsedMs >= lockDelayMs;
|
|
134
|
+
}
|
|
135
|
+
function snapshot() {
|
|
136
|
+
return { cols, rows, cells: cells.slice() };
|
|
137
|
+
}
|
|
138
|
+
function reset() {
|
|
139
|
+
cells = new Array(cols * rows).fill(null);
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
cols,
|
|
143
|
+
rows,
|
|
144
|
+
occupied,
|
|
145
|
+
cellAt,
|
|
146
|
+
setCell,
|
|
147
|
+
clearCell,
|
|
148
|
+
canPlace,
|
|
149
|
+
place,
|
|
150
|
+
fullRows,
|
|
151
|
+
clearRows,
|
|
152
|
+
settle,
|
|
153
|
+
advanceLock,
|
|
154
|
+
lockExpired,
|
|
155
|
+
snapshot,
|
|
156
|
+
reset,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
export type Tile = readonly [number, number];
|
|
2
|
+
/** Maps abstract `[col, row]` tiles onto world-space `[x, z]`, mirroring `navGrid`'s bounds+cellSize convention. */
|
|
3
|
+
export interface TacticalGridWorldConfig {
|
|
4
|
+
/** World-space `[x, z]` of the min corner of tile `[0, 0]`. */
|
|
5
|
+
origin: readonly [number, number];
|
|
6
|
+
tileSize: number;
|
|
7
|
+
}
|
|
2
8
|
export interface TacticalGridConfig {
|
|
3
9
|
width: number;
|
|
4
10
|
height: number;
|
|
5
11
|
blocked?: readonly Tile[];
|
|
6
12
|
diagonal?: boolean;
|
|
13
|
+
world?: TacticalGridWorldConfig;
|
|
7
14
|
}
|
|
8
15
|
export interface ReachableOptions {
|
|
9
16
|
passThrough?: readonly string[];
|
|
@@ -53,5 +60,9 @@ export interface TacticalGrid {
|
|
|
53
60
|
push(id: string, direction: Tile, options?: PushOptions): PushResult;
|
|
54
61
|
capture(): TacticalGridSnapshot;
|
|
55
62
|
restore(snapshot: TacticalGridSnapshot): void;
|
|
63
|
+
/** World `[x, z]` to the containing tile, or `null` when outside the grid. Requires `config.world`. */
|
|
64
|
+
worldToTile(x: number, z: number): Tile | null;
|
|
65
|
+
/** A tile's world-space center. Requires `config.world`. */
|
|
66
|
+
tileToWorld(tile: Tile): readonly [number, number];
|
|
56
67
|
}
|
|
57
68
|
export declare function createTacticalGrid(config: TacticalGridConfig): TacticalGrid;
|
|
@@ -12,9 +12,24 @@ export function createTacticalGrid(config) {
|
|
|
12
12
|
const blocked = new Set((config.blocked ?? []).map(key));
|
|
13
13
|
const occupants = new Map();
|
|
14
14
|
const tiles = new Map();
|
|
15
|
+
const world = config.world;
|
|
15
16
|
function inBounds(tile) {
|
|
16
17
|
return tile[0] >= 0 && tile[0] < width && tile[1] >= 0 && tile[1] < height;
|
|
17
18
|
}
|
|
19
|
+
function requireWorld() {
|
|
20
|
+
if (world === undefined)
|
|
21
|
+
throw new Error("tacticalGrid: worldToTile/tileToWorld require config.world.");
|
|
22
|
+
return world;
|
|
23
|
+
}
|
|
24
|
+
function worldToTile(x, z) {
|
|
25
|
+
const { origin, tileSize } = requireWorld();
|
|
26
|
+
const tile = [Math.floor((x - origin[0]) / tileSize), Math.floor((z - origin[1]) / tileSize)];
|
|
27
|
+
return inBounds(tile) ? tile : null;
|
|
28
|
+
}
|
|
29
|
+
function tileToWorld(tile) {
|
|
30
|
+
const { origin, tileSize } = requireWorld();
|
|
31
|
+
return [origin[0] + (tile[0] + 0.5) * tileSize, origin[1] + (tile[1] + 0.5) * tileSize];
|
|
32
|
+
}
|
|
18
33
|
function isBlocked(tile) {
|
|
19
34
|
return blocked.has(key(tile));
|
|
20
35
|
}
|
|
@@ -223,6 +238,8 @@ export function createTacticalGrid(config) {
|
|
|
223
238
|
reachable,
|
|
224
239
|
path,
|
|
225
240
|
push,
|
|
241
|
+
worldToTile,
|
|
242
|
+
tileToWorld,
|
|
226
243
|
capture: () => {
|
|
227
244
|
const occ = {};
|
|
228
245
|
for (const [k, unitId] of occupants)
|
package/dist/time/beatClock.d.ts
CHANGED
|
@@ -22,6 +22,24 @@ export interface BeatClock {
|
|
|
22
22
|
bpm(): number;
|
|
23
23
|
}
|
|
24
24
|
export declare function createBeatClock(config: BeatClockConfig, onBeat?: (beatIndex: number) => void): BeatClock;
|
|
25
|
+
/** Signed offset (game-seconds) from `nowSec` to the nearest beat boundary — negative = early, positive = late (#286.12). */
|
|
26
|
+
export declare function nearestBeatDelta(nowSec: number, beatDurationSec: number): number;
|
|
27
|
+
export interface BeatAccuracyTier {
|
|
28
|
+
id: string;
|
|
29
|
+
/** Maximum absolute delta (game-seconds) from the beat that still earns this tier. */
|
|
30
|
+
windowSec: number;
|
|
31
|
+
}
|
|
32
|
+
export interface BeatJudgement {
|
|
33
|
+
/** The matched tier id, or `"miss"` beyond every tier. */
|
|
34
|
+
tier: string;
|
|
35
|
+
/** Signed delta to the nearest beat — negative = early, positive = late. */
|
|
36
|
+
deltaSec: number;
|
|
37
|
+
}
|
|
38
|
+
export declare const DEFAULT_BEAT_TIERS: readonly BeatAccuracyTier[];
|
|
39
|
+
/** Classify a press against the nearest beat into tiered judgements (perfect/good/miss by default). Tiers must be ordered tightest-first. */
|
|
40
|
+
export declare function classifyBeatAccuracy(nowSec: number, beatDurationSec: number, tiers?: readonly BeatAccuracyTier[]): BeatJudgement;
|
|
41
|
+
/** Whether a snapshot sits inside a bar-relative open window `[fromBeat, toBeat)` — e.g. "act on beats 2-4 of every bar" (#286.12). */
|
|
42
|
+
export declare function inBarWindow(snapshot: BeatSnapshot, fromBeat: number, toBeat: number): boolean;
|
|
25
43
|
/** The absolute beat-time (game-seconds) that a press at `nowSec` quantizes to: the same instant if it lands on a beat boundary, else the next one. */
|
|
26
44
|
export declare function nextBeatTime(nowSec: number, beatDurationSec: number, epsilon?: number): number;
|
|
27
45
|
export interface BufferedAction<T> {
|
package/dist/time/beatClock.js
CHANGED
|
@@ -35,6 +35,30 @@ export function createBeatClock(config, onBeat) {
|
|
|
35
35
|
bpm: () => config.bpm,
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
+
/** Signed offset (game-seconds) from `nowSec` to the nearest beat boundary — negative = early, positive = late (#286.12). */
|
|
39
|
+
export function nearestBeatDelta(nowSec, beatDurationSec) {
|
|
40
|
+
const beatIndex = Math.round(nowSec / beatDurationSec);
|
|
41
|
+
return nowSec - beatIndex * beatDurationSec;
|
|
42
|
+
}
|
|
43
|
+
export const DEFAULT_BEAT_TIERS = [
|
|
44
|
+
{ id: "perfect", windowSec: 0.06 },
|
|
45
|
+
{ id: "good", windowSec: 0.14 },
|
|
46
|
+
];
|
|
47
|
+
/** Classify a press against the nearest beat into tiered judgements (perfect/good/miss by default). Tiers must be ordered tightest-first. */
|
|
48
|
+
export function classifyBeatAccuracy(nowSec, beatDurationSec, tiers = DEFAULT_BEAT_TIERS) {
|
|
49
|
+
const deltaSec = nearestBeatDelta(nowSec, beatDurationSec);
|
|
50
|
+
const magnitude = Math.abs(deltaSec);
|
|
51
|
+
for (const tier of tiers) {
|
|
52
|
+
if (magnitude <= tier.windowSec)
|
|
53
|
+
return { tier: tier.id, deltaSec };
|
|
54
|
+
}
|
|
55
|
+
return { tier: "miss", deltaSec };
|
|
56
|
+
}
|
|
57
|
+
/** Whether a snapshot sits inside a bar-relative open window `[fromBeat, toBeat)` — e.g. "act on beats 2-4 of every bar" (#286.12). */
|
|
58
|
+
export function inBarWindow(snapshot, fromBeat, toBeat) {
|
|
59
|
+
const barBeat = snapshot.beatInBar + snapshot.phase;
|
|
60
|
+
return barBeat >= fromBeat && barBeat < toBeat;
|
|
61
|
+
}
|
|
38
62
|
const QUANTIZE_EPSILON = 1e-6;
|
|
39
63
|
/** The absolute beat-time (game-seconds) that a press at `nowSec` quantizes to: the same instant if it lands on a beat boundary, else the next one. */
|
|
40
64
|
export function nextBeatTime(nowSec, beatDurationSec, epsilon = QUANTIZE_EPSILON) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface IdleWindowConfig {
|
|
2
|
+
maxSeconds?: number;
|
|
3
|
+
efficiency?: number;
|
|
4
|
+
}
|
|
5
|
+
export interface IdleWindow {
|
|
6
|
+
elapsedSeconds: number;
|
|
7
|
+
effectiveSeconds: number;
|
|
8
|
+
capped: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function idleWindow(lastSeenMs: number, nowMs: number, config?: IdleWindowConfig): IdleWindow;
|
|
11
|
+
export interface LinearCatchUpInput {
|
|
12
|
+
current: number;
|
|
13
|
+
ratePerSecond: number;
|
|
14
|
+
min?: number;
|
|
15
|
+
max?: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function linearCatchUp(seconds: number, input: LinearCatchUpInput): number;
|
|
18
|
+
export interface ExponentialCatchUpInput {
|
|
19
|
+
current: number;
|
|
20
|
+
factorPerSecond: number;
|
|
21
|
+
min?: number;
|
|
22
|
+
max?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare function exponentialCatchUp(seconds: number, input: ExponentialCatchUpInput): number;
|
|
25
|
+
export interface SteppedCatchUpResult {
|
|
26
|
+
steps: number;
|
|
27
|
+
remainderSeconds: number;
|
|
28
|
+
}
|
|
29
|
+
export declare function steppedCatchUp(seconds: number, stepSeconds: number, step: (index: number) => void, maxSteps?: number): SteppedCatchUpResult;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function idleWindow(lastSeenMs, nowMs, config = {}) {
|
|
2
|
+
const elapsedSeconds = Math.max(0, (nowMs - lastSeenMs) / 1000);
|
|
3
|
+
const capped = config.maxSeconds !== undefined && elapsedSeconds > config.maxSeconds;
|
|
4
|
+
const boundedSeconds = Math.min(elapsedSeconds, config.maxSeconds ?? Number.POSITIVE_INFINITY);
|
|
5
|
+
const efficiency = Math.max(0, config.efficiency ?? 1);
|
|
6
|
+
return {
|
|
7
|
+
elapsedSeconds,
|
|
8
|
+
effectiveSeconds: boundedSeconds * efficiency,
|
|
9
|
+
capped,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function linearCatchUp(seconds, input) {
|
|
13
|
+
const clampedSeconds = Math.max(0, seconds);
|
|
14
|
+
const value = input.current + input.ratePerSecond * clampedSeconds;
|
|
15
|
+
return Math.min(input.max ?? Number.POSITIVE_INFINITY, Math.max(input.min ?? Number.NEGATIVE_INFINITY, value));
|
|
16
|
+
}
|
|
17
|
+
export function exponentialCatchUp(seconds, input) {
|
|
18
|
+
const clampedSeconds = Math.max(0, seconds);
|
|
19
|
+
const factor = Math.max(0, input.factorPerSecond);
|
|
20
|
+
const value = input.current * factor ** clampedSeconds;
|
|
21
|
+
return Math.min(input.max ?? Number.POSITIVE_INFINITY, Math.max(input.min ?? Number.NEGATIVE_INFINITY, value));
|
|
22
|
+
}
|
|
23
|
+
export function steppedCatchUp(seconds, stepSeconds, step, maxSteps) {
|
|
24
|
+
if (stepSeconds <= 0 || seconds <= 0) {
|
|
25
|
+
return { steps: 0, remainderSeconds: Math.max(0, seconds) };
|
|
26
|
+
}
|
|
27
|
+
const steps = Math.min(Math.floor(seconds / stepSeconds), maxSteps ?? Number.POSITIVE_INFINITY);
|
|
28
|
+
for (let index = 0; index < steps; index++)
|
|
29
|
+
step(index);
|
|
30
|
+
return { steps, remainderSeconds: seconds - steps * stepSeconds };
|
|
31
|
+
}
|
|
@@ -9,8 +9,14 @@ export interface ServerTickPlan<TSystemId extends string = string> {
|
|
|
9
9
|
due: TSystemId[];
|
|
10
10
|
anchors: TickAnchors;
|
|
11
11
|
}
|
|
12
|
+
export type PlanServerTickOptions = {
|
|
13
|
+
/** Cap catch-up runs per system per heartbeat to avoid spiral-of-death. Default 3. */
|
|
14
|
+
maxCatchUp?: number;
|
|
15
|
+
};
|
|
12
16
|
/**
|
|
13
17
|
* Decides which systems a heartbeat should run. A system with no anchor runs immediately.
|
|
18
|
+
* When wall time stalls past multiple intervals, the system id is repeated (bounded by
|
|
19
|
+
* maxCatchUp). Excess lag past the bound resyncs the anchor to `now`.
|
|
14
20
|
* Returned anchors carry only systems present in the pipeline, so removed systems clean up.
|
|
15
21
|
*/
|
|
16
|
-
export declare function planServerTick<TSystemId extends string>(systems: readonly TickSystemDefinition<TSystemId>[], anchors: TickAnchors, now: number): ServerTickPlan<TSystemId>;
|
|
22
|
+
export declare function planServerTick<TSystemId extends string>(systems: readonly TickSystemDefinition<TSystemId>[], anchors: TickAnchors, now: number, options?: PlanServerTickOptions): ServerTickPlan<TSystemId>;
|
package/dist/time/serverTick.js
CHANGED
|
@@ -1,18 +1,41 @@
|
|
|
1
|
+
const DEFAULT_MAX_CATCH_UP = 3;
|
|
1
2
|
/**
|
|
2
3
|
* Decides which systems a heartbeat should run. A system with no anchor runs immediately.
|
|
4
|
+
* When wall time stalls past multiple intervals, the system id is repeated (bounded by
|
|
5
|
+
* maxCatchUp). Excess lag past the bound resyncs the anchor to `now`.
|
|
3
6
|
* Returned anchors carry only systems present in the pipeline, so removed systems clean up.
|
|
4
7
|
*/
|
|
5
|
-
export function planServerTick(systems, anchors, now) {
|
|
8
|
+
export function planServerTick(systems, anchors, now, options) {
|
|
9
|
+
const maxCatchUp = options?.maxCatchUp ?? DEFAULT_MAX_CATCH_UP;
|
|
6
10
|
const due = [];
|
|
7
11
|
const nextAnchors = {};
|
|
8
12
|
for (const system of systems) {
|
|
9
13
|
const lastRunAt = anchors[system.id];
|
|
10
|
-
if (lastRunAt
|
|
14
|
+
if (lastRunAt === undefined) {
|
|
15
|
+
due.push(system.id);
|
|
16
|
+
nextAnchors[system.id] = now;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (system.intervalMs <= 0) {
|
|
20
|
+
due.push(system.id);
|
|
21
|
+
nextAnchors[system.id] = now;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const elapsed = now - lastRunAt;
|
|
25
|
+
if (elapsed < system.intervalMs) {
|
|
11
26
|
nextAnchors[system.id] = lastRunAt;
|
|
12
27
|
continue;
|
|
13
28
|
}
|
|
14
|
-
|
|
15
|
-
|
|
29
|
+
const missed = Math.floor(elapsed / system.intervalMs);
|
|
30
|
+
if (missed > maxCatchUp) {
|
|
31
|
+
for (let i = 0; i < maxCatchUp; i += 1)
|
|
32
|
+
due.push(system.id);
|
|
33
|
+
nextAnchors[system.id] = now;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
for (let i = 0; i < missed; i += 1)
|
|
37
|
+
due.push(system.id);
|
|
38
|
+
nextAnchors[system.id] = lastRunAt + missed * system.intervalMs;
|
|
16
39
|
}
|
|
17
40
|
return { due, anchors: nextAnchors };
|
|
18
41
|
}
|
package/dist/time/simClock.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ export interface TimeConfig {
|
|
|
9
9
|
start?: number;
|
|
10
10
|
/** Boot with the clock paused. Default false. */
|
|
11
11
|
startPaused?: boolean;
|
|
12
|
+
/** In-world days per year, driving `calendar().year`/`dayOfYear`/`yearFraction`. Default 365. */
|
|
13
|
+
daysPerYear?: number;
|
|
14
|
+
/** Named season segments spanning the year in equal shares; when set, `calendar().season` names the current one. */
|
|
15
|
+
seasons?: readonly string[];
|
|
12
16
|
}
|
|
13
17
|
export interface CalendarTime {
|
|
14
18
|
/** Total game-seconds since the world started. */
|
|
@@ -20,14 +24,24 @@ export interface CalendarTime {
|
|
|
20
24
|
second: number;
|
|
21
25
|
/** Progress through the current day, 0..1. */
|
|
22
26
|
dayFraction: number;
|
|
27
|
+
/** 0-based year index, using `daysPerYear` (default 365). */
|
|
28
|
+
year: number;
|
|
29
|
+
/** 0-based day index within the current year. */
|
|
30
|
+
dayOfYear: number;
|
|
31
|
+
/** Progress through the current year, 0..1. */
|
|
32
|
+
yearFraction: number;
|
|
33
|
+
/** Current season name, present only when `TimeConfig.seasons` is configured. */
|
|
34
|
+
season?: string;
|
|
23
35
|
}
|
|
24
36
|
export interface ClockSnapshot {
|
|
25
37
|
now: number;
|
|
26
38
|
paused: boolean;
|
|
27
|
-
/**
|
|
39
|
+
/** Speed-control multiplier — 0 while paused. The full game-time rate is `speed × timescale × scale`. */
|
|
28
40
|
speed: number;
|
|
29
41
|
/** The non-zero multiplier `play()` resumes to. */
|
|
30
42
|
playSpeed: number;
|
|
43
|
+
/** Global slow-motion/fast-forward multiplier; 1 unless `setTimescale` was called. */
|
|
44
|
+
timescale: number;
|
|
31
45
|
scale: number;
|
|
32
46
|
speeds: readonly number[];
|
|
33
47
|
calendar: CalendarTime;
|
|
@@ -47,6 +61,16 @@ export interface SimClock {
|
|
|
47
61
|
setSpeed(multiplier: number): void;
|
|
48
62
|
/** Step to the next configured speed (wraps); always unpauses. */
|
|
49
63
|
cycleSpeed(): void;
|
|
64
|
+
/** Current global timescale multiplier; 1 by default. */
|
|
65
|
+
timescale(): number;
|
|
66
|
+
/**
|
|
67
|
+
* Global slow-motion/fast-forward multiplier composed with the speed control —
|
|
68
|
+
* `advance` returns `realDt × scale × speed × timescale`. `0` freezes game time
|
|
69
|
+
* without flipping pause state (`0.08` = bullet time, `2` = fast-forward); negative
|
|
70
|
+
* or non-finite values clamp to 0. Timescale is host-authoritative: in multiplayer
|
|
71
|
+
* it belongs to the host simulation, never to an individual client.
|
|
72
|
+
*/
|
|
73
|
+
setTimescale(multiplier: number): void;
|
|
50
74
|
/** Run `callback` once, `seconds` of game-time from now. Returns a cancel handle. */
|
|
51
75
|
after(seconds: number, callback: () => void): () => void;
|
|
52
76
|
/** Run `callback` every `seconds` of game-time. Returns a cancel handle. */
|
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,57 @@
|
|
|
1
|
+
export interface SchedulePhase<T> {
|
|
2
|
+
state: T;
|
|
3
|
+
durationSeconds: number;
|
|
4
|
+
}
|
|
5
|
+
export interface StateScheduleConfig<T> {
|
|
6
|
+
phases: readonly SchedulePhase<T>[];
|
|
7
|
+
/** Repeat the phase list forever (default); `false` clamps to the last phase after one pass. */
|
|
8
|
+
loop?: boolean;
|
|
9
|
+
/** Shifts the whole timeline so `t = 0` lands `offsetSeconds` into the cycle — staggers identical schedules. */
|
|
10
|
+
offsetSeconds?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface ScheduleSample<T> {
|
|
13
|
+
state: T;
|
|
14
|
+
/** Index into `phases`. */
|
|
15
|
+
index: number;
|
|
16
|
+
elapsedSeconds: number;
|
|
17
|
+
/** Seconds until this phase ends; `Infinity` on the clamped final phase of a non-looping schedule. */
|
|
18
|
+
remainingSeconds: number;
|
|
19
|
+
/** Elapsed fraction of the phase in `[0, 1]`; `1` on the clamped final phase. */
|
|
20
|
+
fraction: number;
|
|
21
|
+
}
|
|
22
|
+
export interface ScheduleWindow {
|
|
23
|
+
start: number;
|
|
24
|
+
end: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A deterministic timeline of states — position-in-schedule is a pure function of absolute time,
|
|
28
|
+
* so a forecast preview and the live state can never disagree. One primitive behind zoned weather
|
|
29
|
+
* shifts, hazard cycles, and timetabled machinery: `stateAt`/`sampleAt` for the live state,
|
|
30
|
+
* `nextTransitionAt` for countdowns, `windowsOf`/`nextWindow` for "when is it safe" forecasts.
|
|
31
|
+
*/
|
|
32
|
+
export interface StateSchedule<T> {
|
|
33
|
+
/** Sum of all phase durations — one full cycle. */
|
|
34
|
+
readonly cycleSeconds: number;
|
|
35
|
+
stateAt(t: number): T;
|
|
36
|
+
sampleAt(t: number): ScheduleSample<T>;
|
|
37
|
+
/** Absolute time of the next phase boundary after `t`; `Infinity` once a non-looping schedule ends. */
|
|
38
|
+
nextTransitionAt(t: number): number;
|
|
39
|
+
/** All windows within `[fromT, fromT + horizonSeconds]` where the state matches. */
|
|
40
|
+
windowsOf(match: (state: T) => boolean, fromT: number, horizonSeconds: number): ScheduleWindow[];
|
|
41
|
+
/** The first matching window at or after `fromT` (looking ahead `horizonSeconds`, default one full cycle). */
|
|
42
|
+
nextWindow(match: (state: T) => boolean, fromT: number, horizonSeconds?: number): ScheduleWindow | null;
|
|
43
|
+
}
|
|
44
|
+
export declare function createStateSchedule<T>(config: StateScheduleConfig<T>): StateSchedule<T>;
|
|
45
|
+
export interface ClearWindowScan {
|
|
46
|
+
fromSeconds: number;
|
|
47
|
+
horizonSeconds: number;
|
|
48
|
+
/** Sampling resolution; smaller catches shorter gaps. */
|
|
49
|
+
stepSeconds: number;
|
|
50
|
+
/** Reject windows shorter than this; default `0`. */
|
|
51
|
+
minDurationSeconds?: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Forward-scan any predicate-of-time — a timetable mover's "is the crossing clear at `t`" — for the
|
|
55
|
+
* next open window. Sampling-based: pick `stepSeconds` at or below half the shortest gap that matters.
|
|
56
|
+
*/
|
|
57
|
+
export declare function nextClearWindow(isClear: (t: number) => boolean, scan: ClearWindowScan): ScheduleWindow | null;
|