@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
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export type RoundPhase =
|
|
1
|
+
export type RoundPhase = string;
|
|
2
|
+
/** Default phase-duration shape for the built-in buy/live/end cycle; pass a wider `Record<string, number>` when using a custom `phaseOrder`. */
|
|
2
3
|
export interface RoundPhaseDurations {
|
|
3
4
|
buy: number;
|
|
4
5
|
live: number;
|
|
@@ -9,9 +10,24 @@ export interface LossBonusRule {
|
|
|
9
10
|
step: number;
|
|
10
11
|
max: number;
|
|
11
12
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
/** A team entry with an optional role tag (e.g. "attacker", "defender") retrievable via `RoundState.roleOf`. */
|
|
14
|
+
export interface RoundTeam {
|
|
15
|
+
id: string;
|
|
16
|
+
role?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface RoundConfig<TPhase extends string = RoundPhase> {
|
|
19
|
+
/** Duration in seconds for each phase name in `phaseOrder` (or the default `RoundPhaseDurations` shape). */
|
|
20
|
+
phases: Record<TPhase, number>;
|
|
21
|
+
teams: readonly string[] | readonly RoundTeam[];
|
|
22
|
+
/**
|
|
23
|
+
* Overrides the default `["buy", "live", "end"]` cycle. Phases other than the first and last are
|
|
24
|
+
* "conclude-eligible": `concludeRound`/`evaluate` may settle the round only while the current phase
|
|
25
|
+
* is neither the first nor the last entry, matching the original buy (no conclude) / live (conclude) /
|
|
26
|
+
* end (no conclude, settles the round) roles. The cycle wraps from the last entry back to the first.
|
|
27
|
+
*/
|
|
28
|
+
phaseOrder?: readonly TPhase[];
|
|
29
|
+
/** Evaluated by `evaluate()`; returning a team id concludes the round with that team as winner. */
|
|
30
|
+
winCondition?: (state: RoundSnapshot<TPhase>) => string | null;
|
|
15
31
|
maxRounds?: number;
|
|
16
32
|
winReward?: number;
|
|
17
33
|
lossBonus?: LossBonusRule;
|
|
@@ -23,33 +39,41 @@ export interface RoundEconomy {
|
|
|
23
39
|
won: boolean;
|
|
24
40
|
lossStreak: number;
|
|
25
41
|
}
|
|
26
|
-
export interface RoundEvent {
|
|
42
|
+
export interface RoundEvent<TPhase extends string = RoundPhase> {
|
|
27
43
|
kind: RoundEventKind;
|
|
28
44
|
round: number;
|
|
29
|
-
phase?:
|
|
30
|
-
nextPhase?:
|
|
45
|
+
phase?: TPhase;
|
|
46
|
+
nextPhase?: TPhase;
|
|
31
47
|
winner?: string;
|
|
32
48
|
economy?: RoundEconomy[];
|
|
33
49
|
}
|
|
34
|
-
export type PhaseEndHook = (endingPhase:
|
|
35
|
-
export interface RoundSnapshot {
|
|
50
|
+
export type PhaseEndHook<TPhase extends string = RoundPhase> = (endingPhase: TPhase, nextPhase: TPhase, round: number) => void;
|
|
51
|
+
export interface RoundSnapshot<TPhase extends string = RoundPhase> {
|
|
36
52
|
round: number;
|
|
37
|
-
phase:
|
|
53
|
+
phase: TPhase;
|
|
38
54
|
timeLeft: number;
|
|
39
55
|
scores: Record<string, number>;
|
|
40
56
|
lossStreaks: Record<string, number>;
|
|
57
|
+
roles: Record<string, string | undefined>;
|
|
41
58
|
matchOver: boolean;
|
|
42
59
|
}
|
|
43
|
-
export interface RoundState {
|
|
44
|
-
tick(dt: number): RoundEvent[];
|
|
45
|
-
concludeRound(winner: string): RoundEvent[];
|
|
46
|
-
|
|
47
|
-
|
|
60
|
+
export interface RoundState<TPhase extends string = RoundPhase> {
|
|
61
|
+
tick(dt: number): RoundEvent<TPhase>[];
|
|
62
|
+
concludeRound(winner: string): RoundEvent<TPhase>[];
|
|
63
|
+
/** Calls `RoundConfig.winCondition` (a no-op when absent) and concludes the round if it returns a team id. */
|
|
64
|
+
evaluate(): RoundEvent<TPhase>[];
|
|
65
|
+
onPhaseEnd(hook: PhaseEndHook<TPhase>): () => void;
|
|
66
|
+
phase(): TPhase;
|
|
48
67
|
round(): number;
|
|
49
68
|
timeLeft(): number;
|
|
50
69
|
score(team: string): number;
|
|
51
70
|
economyFor(team: string): RoundEconomy;
|
|
52
|
-
|
|
71
|
+
/** The role tag configured for `team`, if any. */
|
|
72
|
+
roleOf(team: string): string | undefined;
|
|
73
|
+
snapshot(): RoundSnapshot<TPhase>;
|
|
53
74
|
}
|
|
54
75
|
export declare function lossBonusFor(rule: LossBonusRule | undefined, streak: number): number;
|
|
55
|
-
export declare function createRoundState(config: RoundConfig): RoundState
|
|
76
|
+
export declare function createRoundState(config: RoundConfig<RoundPhase>): RoundState<RoundPhase>;
|
|
77
|
+
export declare function createRoundState<TPhase extends string>(config: RoundConfig<TPhase> & {
|
|
78
|
+
phaseOrder: readonly TPhase[];
|
|
79
|
+
}): RoundState<TPhase>;
|
|
@@ -1,31 +1,47 @@
|
|
|
1
|
-
const
|
|
1
|
+
const DEFAULT_PHASE_ORDER = ["buy", "live", "end"];
|
|
2
2
|
export function lossBonusFor(rule, streak) {
|
|
3
3
|
if (rule === undefined)
|
|
4
4
|
return 0;
|
|
5
5
|
return Math.min(rule.max, rule.base + rule.step * Math.max(0, streak));
|
|
6
6
|
}
|
|
7
|
+
function normalizeTeams(teams) {
|
|
8
|
+
if (teams.length === 0)
|
|
9
|
+
return [{ id: "a" }, { id: "b" }];
|
|
10
|
+
const normalized = [];
|
|
11
|
+
for (const team of teams) {
|
|
12
|
+
normalized.push(typeof team === "string" ? { id: team } : team);
|
|
13
|
+
}
|
|
14
|
+
return normalized;
|
|
15
|
+
}
|
|
16
|
+
function isConcludable(order, phase) {
|
|
17
|
+
const idx = order.indexOf(phase);
|
|
18
|
+
return idx > 0 && idx < order.length - 1;
|
|
19
|
+
}
|
|
7
20
|
export function createRoundState(config) {
|
|
21
|
+
const order = config.phaseOrder ?? DEFAULT_PHASE_ORDER;
|
|
8
22
|
const durations = config.phases;
|
|
9
|
-
const
|
|
23
|
+
const teamRecords = normalizeTeams(config.teams);
|
|
24
|
+
const teamIds = teamRecords.map((t) => t.id);
|
|
25
|
+
const roles = {};
|
|
10
26
|
const scores = {};
|
|
11
27
|
const lossStreaks = {};
|
|
12
|
-
for (const team of
|
|
13
|
-
|
|
14
|
-
|
|
28
|
+
for (const team of teamRecords) {
|
|
29
|
+
roles[team.id] = team.role;
|
|
30
|
+
scores[team.id] = 0;
|
|
31
|
+
lossStreaks[team.id] = 0;
|
|
15
32
|
}
|
|
16
33
|
let round = 1;
|
|
17
|
-
let phase =
|
|
18
|
-
let timeLeft = durations
|
|
34
|
+
let phase = order[0];
|
|
35
|
+
let timeLeft = durations[phase] ?? 0;
|
|
19
36
|
let matchOver = false;
|
|
20
37
|
let pendingWinner = null;
|
|
21
38
|
const hooks = new Set();
|
|
22
39
|
function durationOf(next) {
|
|
23
|
-
return durations[next];
|
|
40
|
+
return durations[next] ?? 0;
|
|
24
41
|
}
|
|
25
42
|
function nextPhaseOf(current) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return PHASE_ORDER[PHASE_ORDER.indexOf(current) + 1];
|
|
43
|
+
const idx = order.indexOf(current);
|
|
44
|
+
return idx >= order.length - 1 ? order[0] : order[idx + 1];
|
|
29
45
|
}
|
|
30
46
|
function economyFor(team) {
|
|
31
47
|
const won = pendingWinner === team;
|
|
@@ -38,12 +54,12 @@ export function createRoundState(config) {
|
|
|
38
54
|
}
|
|
39
55
|
function settleEconomy(winner) {
|
|
40
56
|
const economy = [];
|
|
41
|
-
for (const team of
|
|
57
|
+
for (const team of teamIds) {
|
|
42
58
|
const won = team === winner;
|
|
43
59
|
const reward = won ? config.winReward ?? 0 : lossBonusFor(config.lossBonus, lossStreaks[team]);
|
|
44
60
|
economy.push({ team, reward, won, lossStreak: lossStreaks[team] });
|
|
45
61
|
}
|
|
46
|
-
for (const team of
|
|
62
|
+
for (const team of teamIds) {
|
|
47
63
|
if (team === winner)
|
|
48
64
|
lossStreaks[team] = 0;
|
|
49
65
|
else
|
|
@@ -58,24 +74,48 @@ export function createRoundState(config) {
|
|
|
58
74
|
}
|
|
59
75
|
function endPhase(events) {
|
|
60
76
|
const ending = phase;
|
|
61
|
-
if (ending ===
|
|
77
|
+
if (ending === order[order.length - 1]) {
|
|
62
78
|
if (config.maxRounds !== undefined && round >= config.maxRounds) {
|
|
63
79
|
matchOver = true;
|
|
64
|
-
events.push({ kind: "phase.end", round, phase: ending, nextPhase:
|
|
80
|
+
events.push({ kind: "phase.end", round, phase: ending, nextPhase: order[0] });
|
|
65
81
|
events.push({ kind: "match.end", round });
|
|
66
82
|
for (const hook of hooks)
|
|
67
|
-
hook(ending,
|
|
83
|
+
hook(ending, order[0], round);
|
|
68
84
|
return;
|
|
69
85
|
}
|
|
70
86
|
round += 1;
|
|
71
87
|
pendingWinner = null;
|
|
72
88
|
}
|
|
73
89
|
const next = nextPhaseOf(ending);
|
|
74
|
-
|
|
90
|
+
const endedRound = ending === order[order.length - 1] ? round - 1 : round;
|
|
91
|
+
events.push({ kind: "phase.end", round: endedRound, phase: ending, nextPhase: next });
|
|
75
92
|
for (const hook of hooks)
|
|
76
|
-
hook(ending, next,
|
|
93
|
+
hook(ending, next, endedRound);
|
|
77
94
|
enter(next, events);
|
|
78
95
|
}
|
|
96
|
+
function currentSnapshot() {
|
|
97
|
+
return {
|
|
98
|
+
round,
|
|
99
|
+
phase,
|
|
100
|
+
timeLeft: Math.max(0, timeLeft),
|
|
101
|
+
scores: { ...scores },
|
|
102
|
+
lossStreaks: { ...lossStreaks },
|
|
103
|
+
roles: { ...roles },
|
|
104
|
+
matchOver,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function concludeRoundImpl(winner) {
|
|
108
|
+
if (matchOver || !isConcludable(order, phase))
|
|
109
|
+
return [];
|
|
110
|
+
const events = [];
|
|
111
|
+
pendingWinner = winner;
|
|
112
|
+
scores[winner] = (scores[winner] ?? 0) + 1;
|
|
113
|
+
events.push({ kind: "round.win", round, winner });
|
|
114
|
+
const economy = settleEconomy(winner);
|
|
115
|
+
events.push({ kind: "round.economy", round, winner, economy });
|
|
116
|
+
endPhase(events);
|
|
117
|
+
return events;
|
|
118
|
+
}
|
|
79
119
|
return {
|
|
80
120
|
tick(dt) {
|
|
81
121
|
if (dt <= 0 || matchOver)
|
|
@@ -83,9 +123,9 @@ export function createRoundState(config) {
|
|
|
83
123
|
const events = [];
|
|
84
124
|
timeLeft -= dt;
|
|
85
125
|
let guard = 0;
|
|
86
|
-
while (timeLeft <= 0 && !matchOver && guard <
|
|
126
|
+
while (timeLeft <= 0 && !matchOver && guard < order.length + 2) {
|
|
87
127
|
const carry = timeLeft;
|
|
88
|
-
if (phase
|
|
128
|
+
if (isConcludable(order, phase))
|
|
89
129
|
pendingWinner = null;
|
|
90
130
|
endPhase(events);
|
|
91
131
|
if (!matchOver)
|
|
@@ -94,17 +134,12 @@ export function createRoundState(config) {
|
|
|
94
134
|
}
|
|
95
135
|
return events;
|
|
96
136
|
},
|
|
97
|
-
concludeRound
|
|
98
|
-
|
|
137
|
+
concludeRound: concludeRoundImpl,
|
|
138
|
+
evaluate() {
|
|
139
|
+
if (config.winCondition === undefined)
|
|
99
140
|
return [];
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
scores[winner] = (scores[winner] ?? 0) + 1;
|
|
103
|
-
events.push({ kind: "round.win", round, winner });
|
|
104
|
-
const economy = settleEconomy(winner);
|
|
105
|
-
events.push({ kind: "round.economy", round, winner, economy });
|
|
106
|
-
endPhase(events);
|
|
107
|
-
return events;
|
|
141
|
+
const winner = config.winCondition(currentSnapshot());
|
|
142
|
+
return winner === null ? [] : concludeRoundImpl(winner);
|
|
108
143
|
},
|
|
109
144
|
onPhaseEnd(hook) {
|
|
110
145
|
hooks.add(hook);
|
|
@@ -117,13 +152,7 @@ export function createRoundState(config) {
|
|
|
117
152
|
timeLeft: () => Math.max(0, timeLeft),
|
|
118
153
|
score: (team) => scores[team] ?? 0,
|
|
119
154
|
economyFor,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
phase,
|
|
123
|
-
timeLeft: Math.max(0, timeLeft),
|
|
124
|
-
scores: { ...scores },
|
|
125
|
-
lossStreaks: { ...lossStreaks },
|
|
126
|
-
matchOver,
|
|
127
|
-
}),
|
|
155
|
+
roleOf: (team) => roles[team],
|
|
156
|
+
snapshot: currentSnapshot,
|
|
128
157
|
};
|
|
129
158
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { GameContext } from "../runtime/gameContext.js";
|
|
2
|
+
export type SettingValue = number | boolean | string;
|
|
3
|
+
export type SettingKind = "slider" | "toggle" | "select";
|
|
4
|
+
export type BuiltInSettingCategory = "sound" | "graphics" | "gameplay" | "controls";
|
|
5
|
+
/** Built-in category ids keep autocomplete; any other string makes a fresh category. */
|
|
6
|
+
export type SettingCategory = BuiltInSettingCategory | (string & {});
|
|
7
|
+
export declare const BUILT_IN_SETTING_CATEGORIES: readonly BuiltInSettingCategory[];
|
|
8
|
+
export interface SettingOption {
|
|
9
|
+
value: string;
|
|
10
|
+
label: string;
|
|
11
|
+
}
|
|
12
|
+
/** Declares or relabels/reorders a category tab; use it for a custom category or to reshape the built-ins. */
|
|
13
|
+
export interface SettingCategoryDef {
|
|
14
|
+
id: SettingCategory;
|
|
15
|
+
label: string;
|
|
16
|
+
order?: number;
|
|
17
|
+
}
|
|
18
|
+
/** Extra setting a game appends to a built-in category via `defineGame({ settings: { extra } })`. */
|
|
19
|
+
export interface GameSettingDef {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
category: SettingCategory;
|
|
23
|
+
kind: SettingKind;
|
|
24
|
+
default: SettingValue;
|
|
25
|
+
/** Slider bounds — always pass both for a `slider`; omitted defaults (0..1) collapse the thumb. */
|
|
26
|
+
min?: number;
|
|
27
|
+
max?: number;
|
|
28
|
+
step?: number;
|
|
29
|
+
options?: readonly SettingOption[];
|
|
30
|
+
/** Applies the new value to game state; the value is already persisted before this runs. */
|
|
31
|
+
onChange?: (value: SettingValue, ctx: GameContext) => void;
|
|
32
|
+
}
|
|
33
|
+
/** `quick` shows compact on-screen volume/graphics buttons; `false` (default) mounts no engine trigger — open the menu from your own UI with `<SettingsTrigger>` or `useSettings().open()`. */
|
|
34
|
+
export type SettingsSurface = "quick";
|
|
35
|
+
/** The four themed settings layouts, chosen with `defineGame({ settings: { variant } })`. All read the game's `--jg-*` theme tokens. */
|
|
36
|
+
export type SettingsVariant = "panel" | "sheet" | "sidebar" | "fullscreen";
|
|
37
|
+
/** A game-state action (Restart, Quit to menu, …) shown as rows in the first "Game" settings tab — never a floating button or a rebindable key. */
|
|
38
|
+
export interface SettingsActionDef {
|
|
39
|
+
id: string;
|
|
40
|
+
label: string;
|
|
41
|
+
/** `danger` styles destructive actions (restart, quit) apart from neutral ones. */
|
|
42
|
+
kind?: "default" | "danger";
|
|
43
|
+
/** Optional one-line hint under the label. */
|
|
44
|
+
description?: string;
|
|
45
|
+
/** Runs the action; the menu closes right after. */
|
|
46
|
+
run: (ctx: GameContext) => void;
|
|
47
|
+
}
|
|
48
|
+
export interface GameSettingsConfig {
|
|
49
|
+
/** Layout + skin of the menu; all four theme off `--jg-*`. Default `panel`. */
|
|
50
|
+
variant?: SettingsVariant;
|
|
51
|
+
/** `quick` shows compact on-screen volume/graphics buttons; omitted mounts no engine trigger — open the menu yourself with `<SettingsTrigger>` / `useSettings().open()`, or render `useSettings().categories` your own way. */
|
|
52
|
+
surface?: SettingsSurface | false;
|
|
53
|
+
/** Rows appended to any category — built-in or a brand-new one named by `category`. */
|
|
54
|
+
extra?: readonly GameSettingDef[];
|
|
55
|
+
/** Declare custom category tabs, or relabel/reorder the built-ins. */
|
|
56
|
+
categories?: readonly SettingCategoryDef[];
|
|
57
|
+
/** Built-in categories to hide. */
|
|
58
|
+
hide?: readonly SettingCategory[];
|
|
59
|
+
/** Game-state actions (Restart, Quit…) — become the first "Game" tab, shown before anything else. */
|
|
60
|
+
actions?: readonly SettingsActionDef[];
|
|
61
|
+
/** Input actions to drop from the rebindable Controls list — game-state keys like `restart` that belong in `actions`, not the rebind grid. */
|
|
62
|
+
hideBindings?: readonly string[];
|
|
63
|
+
}
|
|
64
|
+
export declare const SETTINGS_STORAGE_PREFIX = "jgengine:setting:";
|
|
65
|
+
export declare const SETTING_IDS: {
|
|
66
|
+
readonly masterVolume: "sound.master";
|
|
67
|
+
readonly graphicsQuality: "graphics.quality";
|
|
68
|
+
readonly graphicsShadows: "graphics.shadows";
|
|
69
|
+
readonly graphicsUiScale: "graphics.uiScale";
|
|
70
|
+
};
|
|
71
|
+
export type GraphicsQuality = "low" | "medium" | "high";
|
|
72
|
+
export declare const GRAPHICS_QUALITY_OPTIONS: readonly SettingOption[];
|
|
73
|
+
export declare const DEFAULT_MASTER_VOLUME = 1;
|
|
74
|
+
export declare const DEFAULT_GRAPHICS_QUALITY: GraphicsQuality;
|
|
75
|
+
export declare const DEFAULT_GRAPHICS_SHADOWS = true;
|
|
76
|
+
/** Player-controlled multiplier on the HUD's computed fit scale — one lever on desktop and mobile alike. */
|
|
77
|
+
export declare const DEFAULT_UI_SCALE = 1;
|
|
78
|
+
export declare const UI_SCALE_MIN = 0.5;
|
|
79
|
+
export declare const UI_SCALE_MAX = 1.5;
|
|
80
|
+
/** Device-pixel-ratio ceiling per quality tier — the shell's `Canvas` dpr cap. */
|
|
81
|
+
export declare const GRAPHICS_QUALITY_DPR: Record<GraphicsQuality, number>;
|
|
82
|
+
export declare function busVolumeSettingId(busId: string): string;
|
|
83
|
+
export declare function settingStorageKey(id: string): string;
|
|
84
|
+
interface WebStorageLike {
|
|
85
|
+
getItem(key: string): string | null;
|
|
86
|
+
setItem(key: string, value: string): void;
|
|
87
|
+
}
|
|
88
|
+
export declare function loadSettingValue<T extends SettingValue>(id: string, fallback: T, storage?: Pick<WebStorageLike, "getItem"> | null | undefined): T;
|
|
89
|
+
export declare function saveSettingValue(id: string, value: SettingValue, storage?: Pick<WebStorageLike, "setItem"> | null | undefined): void;
|
|
90
|
+
export interface SettingsStore {
|
|
91
|
+
get<T extends SettingValue>(id: string, fallback: T): T;
|
|
92
|
+
set(id: string, value: SettingValue): void;
|
|
93
|
+
subscribe(listener: () => void): () => void;
|
|
94
|
+
}
|
|
95
|
+
/** Reactive, localStorage-backed settings store shared by the shell wiring and React hooks. */
|
|
96
|
+
export declare function createSettingsStore(storage?: Pick<WebStorageLike, "getItem" | "setItem"> | null | undefined): SettingsStore;
|
|
97
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createObservableKeyedStore } from "../store/observableKeyedStore.js";
|
|
2
|
+
export const BUILT_IN_SETTING_CATEGORIES = [
|
|
3
|
+
"sound",
|
|
4
|
+
"graphics",
|
|
5
|
+
"gameplay",
|
|
6
|
+
"controls",
|
|
7
|
+
];
|
|
8
|
+
export const SETTINGS_STORAGE_PREFIX = "jgengine:setting:";
|
|
9
|
+
export const SETTING_IDS = {
|
|
10
|
+
masterVolume: "sound.master",
|
|
11
|
+
graphicsQuality: "graphics.quality",
|
|
12
|
+
graphicsShadows: "graphics.shadows",
|
|
13
|
+
graphicsUiScale: "graphics.uiScale",
|
|
14
|
+
};
|
|
15
|
+
export const GRAPHICS_QUALITY_OPTIONS = [
|
|
16
|
+
{ value: "low", label: "Low" },
|
|
17
|
+
{ value: "medium", label: "Medium" },
|
|
18
|
+
{ value: "high", label: "High" },
|
|
19
|
+
];
|
|
20
|
+
export const DEFAULT_MASTER_VOLUME = 1;
|
|
21
|
+
export const DEFAULT_GRAPHICS_QUALITY = "high";
|
|
22
|
+
export const DEFAULT_GRAPHICS_SHADOWS = true;
|
|
23
|
+
/** Player-controlled multiplier on the HUD's computed fit scale — one lever on desktop and mobile alike. */
|
|
24
|
+
export const DEFAULT_UI_SCALE = 1;
|
|
25
|
+
export const UI_SCALE_MIN = 0.5;
|
|
26
|
+
export const UI_SCALE_MAX = 1.5;
|
|
27
|
+
/** Device-pixel-ratio ceiling per quality tier — the shell's `Canvas` dpr cap. */
|
|
28
|
+
export const GRAPHICS_QUALITY_DPR = {
|
|
29
|
+
low: 1,
|
|
30
|
+
medium: 1.5,
|
|
31
|
+
high: 2,
|
|
32
|
+
};
|
|
33
|
+
export function busVolumeSettingId(busId) {
|
|
34
|
+
return `sound.bus.${busId}`;
|
|
35
|
+
}
|
|
36
|
+
export function settingStorageKey(id) {
|
|
37
|
+
return `${SETTINGS_STORAGE_PREFIX}${id}`;
|
|
38
|
+
}
|
|
39
|
+
function defaultStorage() {
|
|
40
|
+
try {
|
|
41
|
+
if (typeof globalThis !== "undefined" && "localStorage" in globalThis) {
|
|
42
|
+
return globalThis.localStorage ?? null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
function coerce(raw, fallback) {
|
|
51
|
+
if (typeof fallback === "number") {
|
|
52
|
+
const n = typeof raw === "number" ? raw : Number(raw);
|
|
53
|
+
return (Number.isFinite(n) ? n : fallback);
|
|
54
|
+
}
|
|
55
|
+
if (typeof fallback === "boolean") {
|
|
56
|
+
if (typeof raw === "boolean")
|
|
57
|
+
return raw;
|
|
58
|
+
return (raw === "true" ? true : raw === "false" ? false : fallback);
|
|
59
|
+
}
|
|
60
|
+
return (typeof raw === "string" ? raw : fallback);
|
|
61
|
+
}
|
|
62
|
+
export function loadSettingValue(id, fallback, storage = defaultStorage()) {
|
|
63
|
+
if (storage == null)
|
|
64
|
+
return fallback;
|
|
65
|
+
try {
|
|
66
|
+
const raw = storage.getItem(settingStorageKey(id));
|
|
67
|
+
if (raw === null)
|
|
68
|
+
return fallback;
|
|
69
|
+
return coerce(JSON.parse(raw), fallback);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return fallback;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function saveSettingValue(id, value, storage = defaultStorage()) {
|
|
76
|
+
if (storage == null)
|
|
77
|
+
return;
|
|
78
|
+
try {
|
|
79
|
+
storage.setItem(settingStorageKey(id), JSON.stringify(value));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
/* ignore quota / private-mode failures */
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Reactive, localStorage-backed settings store shared by the shell wiring and React hooks. */
|
|
86
|
+
export function createSettingsStore(storage = defaultStorage()) {
|
|
87
|
+
const mem = createObservableKeyedStore();
|
|
88
|
+
return {
|
|
89
|
+
get(id, fallback) {
|
|
90
|
+
if (mem.has(id))
|
|
91
|
+
return coerce(mem.get(id), fallback);
|
|
92
|
+
return loadSettingValue(id, fallback, storage ?? undefined);
|
|
93
|
+
},
|
|
94
|
+
set(id, value) {
|
|
95
|
+
saveSettingValue(id, value, storage ?? undefined);
|
|
96
|
+
mem.set(id, value);
|
|
97
|
+
},
|
|
98
|
+
subscribe: mem.subscribe,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -3,6 +3,9 @@ export interface StatModifier {
|
|
|
3
3
|
multiply?: number;
|
|
4
4
|
}
|
|
5
5
|
export type StatModifierSet<TStat extends string> = Partial<Record<TStat, StatModifier>>;
|
|
6
|
+
export interface CreateStatsOptions {
|
|
7
|
+
now?: () => number;
|
|
8
|
+
}
|
|
6
9
|
export interface Stats<TStat extends string> {
|
|
7
10
|
setBase(stat: TStat, value: number): void;
|
|
8
11
|
getBase(stat: TStat): number;
|
|
@@ -10,9 +13,9 @@ export interface Stats<TStat extends string> {
|
|
|
10
13
|
expiresAtMs?: number;
|
|
11
14
|
}): void;
|
|
12
15
|
removeSource(sourceId: string): void;
|
|
13
|
-
hasSource(sourceId: string): boolean;
|
|
16
|
+
hasSource(sourceId: string, nowMs?: number): boolean;
|
|
14
17
|
get(stat: TStat, nowMs?: number): number;
|
|
15
18
|
pruneExpired(nowMs: number): string[];
|
|
16
19
|
sources(): string[];
|
|
17
20
|
}
|
|
18
|
-
export declare function createStats<TStat extends string>(base: Record<TStat, number
|
|
21
|
+
export declare function createStats<TStat extends string>(base: Record<TStat, number>, options?: CreateStatsOptions): Stats<TStat>;
|
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
export function createStats(base) {
|
|
1
|
+
export function createStats(base, options) {
|
|
2
2
|
const baseValues = { ...base };
|
|
3
3
|
const sourceEntries = new Map();
|
|
4
|
+
const clock = options?.now;
|
|
5
|
+
function resolveNow(nowMs) {
|
|
6
|
+
return nowMs ?? clock?.();
|
|
7
|
+
}
|
|
4
8
|
function isExpired(entry, nowMs) {
|
|
5
9
|
return nowMs !== undefined && entry.expiresAtMs !== undefined && entry.expiresAtMs <= nowMs;
|
|
6
10
|
}
|
|
11
|
+
function dropExpired(nowMs) {
|
|
12
|
+
const pruned = [];
|
|
13
|
+
for (const [sourceId, entry] of sourceEntries) {
|
|
14
|
+
if (isExpired(entry, nowMs)) {
|
|
15
|
+
sourceEntries.delete(sourceId);
|
|
16
|
+
pruned.push(sourceId);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return pruned;
|
|
20
|
+
}
|
|
7
21
|
function resolve(stat, nowMs) {
|
|
8
22
|
let total = baseValues[stat];
|
|
9
23
|
let multiplier = 1;
|
|
@@ -27,27 +41,31 @@ export function createStats(base) {
|
|
|
27
41
|
getBase(stat) {
|
|
28
42
|
return baseValues[stat];
|
|
29
43
|
},
|
|
30
|
-
addSource(sourceId, modifiers,
|
|
31
|
-
sourceEntries.set(sourceId, { modifiers, expiresAtMs:
|
|
44
|
+
addSource(sourceId, modifiers, sourceOptions) {
|
|
45
|
+
sourceEntries.set(sourceId, { modifiers, expiresAtMs: sourceOptions?.expiresAtMs });
|
|
32
46
|
},
|
|
33
47
|
removeSource(sourceId) {
|
|
34
48
|
sourceEntries.delete(sourceId);
|
|
35
49
|
},
|
|
36
|
-
hasSource(sourceId) {
|
|
37
|
-
|
|
50
|
+
hasSource(sourceId, nowMs) {
|
|
51
|
+
const entry = sourceEntries.get(sourceId);
|
|
52
|
+
if (entry === undefined)
|
|
53
|
+
return false;
|
|
54
|
+
const time = resolveNow(nowMs);
|
|
55
|
+
if (isExpired(entry, time)) {
|
|
56
|
+
sourceEntries.delete(sourceId);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
38
60
|
},
|
|
39
61
|
get(stat, nowMs) {
|
|
40
|
-
|
|
62
|
+
const time = resolveNow(nowMs);
|
|
63
|
+
if (time !== undefined)
|
|
64
|
+
dropExpired(time);
|
|
65
|
+
return resolve(stat, time);
|
|
41
66
|
},
|
|
42
67
|
pruneExpired(nowMs) {
|
|
43
|
-
|
|
44
|
-
for (const [sourceId, entry] of sourceEntries) {
|
|
45
|
-
if (isExpired(entry, nowMs)) {
|
|
46
|
-
sourceEntries.delete(sourceId);
|
|
47
|
-
pruned.push(sourceId);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return pruned;
|
|
68
|
+
return dropExpired(nowMs);
|
|
51
69
|
},
|
|
52
70
|
sources() {
|
|
53
71
|
return Array.from(sourceEntries.keys());
|
|
@@ -3,8 +3,9 @@ export function createObservableKeyedStore(areEqual) {
|
|
|
3
3
|
const listeners = new Set();
|
|
4
4
|
const EMPTY = [];
|
|
5
5
|
let arrayCache = EMPTY;
|
|
6
|
+
let arrayDirty = false;
|
|
6
7
|
function emit() {
|
|
7
|
-
|
|
8
|
+
arrayDirty = true;
|
|
8
9
|
for (const listener of listeners)
|
|
9
10
|
listener();
|
|
10
11
|
}
|
|
@@ -13,7 +14,8 @@ export function createObservableKeyedStore(areEqual) {
|
|
|
13
14
|
const previous = store.get(key);
|
|
14
15
|
if (previous !== undefined && areEqual?.(previous, value))
|
|
15
16
|
return;
|
|
16
|
-
|
|
17
|
+
if (previous !== value)
|
|
18
|
+
store.set(key, value);
|
|
17
19
|
emit();
|
|
18
20
|
},
|
|
19
21
|
delete(key) {
|
|
@@ -36,6 +38,10 @@ export function createObservableKeyedStore(areEqual) {
|
|
|
36
38
|
return store;
|
|
37
39
|
},
|
|
38
40
|
arraySnapshot() {
|
|
41
|
+
if (arrayDirty) {
|
|
42
|
+
arrayCache = store.size === 0 ? EMPTY : Array.from(store.values());
|
|
43
|
+
arrayDirty = false;
|
|
44
|
+
}
|
|
39
45
|
return arrayCache;
|
|
40
46
|
},
|
|
41
47
|
};
|
|
@@ -41,7 +41,7 @@ export interface DecayMeterSet {
|
|
|
41
41
|
refill(id: string, amount: number): void;
|
|
42
42
|
/** Overwrite the base drain rate (e.g. a game mode with harsher hunger). */
|
|
43
43
|
setRate(id: string, rate: number): void;
|
|
44
|
-
/** Multiply
|
|
44
|
+
/** Multiply drain until cleared via `setRateModifier(id, 1)` — cold biome, sprint, etc. */
|
|
45
45
|
setRateModifier(id: string, multiplier: number): void;
|
|
46
46
|
/** Moodles for every crossed threshold, worst-first per meter. */
|
|
47
47
|
moodles(): Moodle[];
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export type FallingGridCell = readonly [number, number];
|
|
2
|
+
export interface FallingGridConfig {
|
|
3
|
+
cols: number;
|
|
4
|
+
rows: number;
|
|
5
|
+
/** Milliseconds a grounded piece may sit before `lockExpired` fires. Defaults to 500. */
|
|
6
|
+
lockDelayMs?: number;
|
|
7
|
+
}
|
|
8
|
+
/** Elapsed grounded time for the lock-delay state machine. `null` means airborne / not timing. */
|
|
9
|
+
export interface LockState {
|
|
10
|
+
elapsedMs: number;
|
|
11
|
+
}
|
|
12
|
+
export interface FallingGridSnapshot<TCell> {
|
|
13
|
+
cols: number;
|
|
14
|
+
rows: number;
|
|
15
|
+
cells: readonly (TCell | null)[];
|
|
16
|
+
}
|
|
17
|
+
export interface FallingGrid<TCell> {
|
|
18
|
+
readonly cols: number;
|
|
19
|
+
readonly rows: number;
|
|
20
|
+
occupied(col: number, row: number): boolean;
|
|
21
|
+
cellAt(col: number, row: number): TCell | null;
|
|
22
|
+
setCell(col: number, row: number, value: TCell): void;
|
|
23
|
+
clearCell(col: number, row: number): void;
|
|
24
|
+
/** Bounds + collision check for a footprint (`cells`, relative to `offset`). Rows above the grid (`row < 0`) are allowed through, mirroring a piece spawning above the visible board; only columns are bounds-checked there. */
|
|
25
|
+
canPlace(cells: readonly FallingGridCell[], offset: FallingGridCell): boolean;
|
|
26
|
+
/** Writes `value` into every in-bounds cell of the footprint if `canPlace` allows it. Cells above the grid are silently skipped. Returns whether the placement happened. */
|
|
27
|
+
place(cells: readonly FallingGridCell[], offset: FallingGridCell, value: TCell): boolean;
|
|
28
|
+
/** Row indices that are fully occupied, in ascending order. */
|
|
29
|
+
fullRows(): readonly number[];
|
|
30
|
+
/** Removes the given rows and shifts every row above them down, backfilling the top with empty rows. Returns the number of rows cleared. */
|
|
31
|
+
clearRows(rows: readonly number[]): number;
|
|
32
|
+
/** Column-gravity collapse: every occupied cell in each column falls to the lowest free row below it, preserving relative order. Returns the number of cells that moved. */
|
|
33
|
+
settle(): number;
|
|
34
|
+
/** Advances (or starts) a lock-delay timer while `grounded`, and clears it the instant the piece is airborne. */
|
|
35
|
+
advanceLock(state: LockState | null, grounded: boolean, dtMs: number): LockState | null;
|
|
36
|
+
/** Whether a lock-delay timer has run out `lockDelayMs`. */
|
|
37
|
+
lockExpired(state: LockState | null): boolean;
|
|
38
|
+
snapshot(): FallingGridSnapshot<TCell>;
|
|
39
|
+
/** Clears every cell back to empty. */
|
|
40
|
+
reset(): void;
|
|
41
|
+
}
|
|
42
|
+
export interface GravityIntervalConfig {
|
|
43
|
+
/** Interval at level 0, in milliseconds. Defaults to 800. */
|
|
44
|
+
baseMs?: number;
|
|
45
|
+
/** Milliseconds shaved off per level. Defaults to 70. */
|
|
46
|
+
perLevel?: number;
|
|
47
|
+
/** Floor the interval never drops below, in milliseconds. Defaults to 50. */
|
|
48
|
+
minMs?: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Gravity tick interval for a given level: linear speed-up with a floor, generalizing
|
|
52
|
+
* `Games/block-stacker`'s `gravityInterval` (`max(0.05, 0.8 - level * 0.07)` seconds) to milliseconds
|
|
53
|
+
* with configurable base/step/floor.
|
|
54
|
+
*/
|
|
55
|
+
export declare function gravityIntervalMs(level: number, config?: GravityIntervalConfig): number;
|
|
56
|
+
/**
|
|
57
|
+
* A game-agnostic cell grid for gravity-driven falling-block mechanics: bounded occupancy,
|
|
58
|
+
* footprint placement/collision, line-clear collapse, column-gravity settle (for match-3/sand
|
|
59
|
+
* style cascades), and a pure lock-delay helper. Extracted from `Games/block-stacker`'s tetris
|
|
60
|
+
* logic so any falling-block game can adopt it instead of reimplementing the grid.
|
|
61
|
+
*/
|
|
62
|
+
export declare function createFallingGrid<TCell>(config: FallingGridConfig): FallingGrid<TCell>;
|