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