@jgengine/core 0.18.0 → 0.19.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 +265 -2
- package/dist/ai/decisionGraph.d.ts +96 -0
- package/dist/ai/decisionGraph.js +212 -0
- package/dist/ai/groupAssist.d.ts +10 -0
- package/dist/ai/groupAssist.js +11 -0
- package/dist/ai/mobBrainGraph.d.ts +3 -0
- package/dist/ai/mobBrainGraph.js +25 -0
- package/dist/ai/pursuit.d.ts +1 -1
- package/dist/ai/pursuit.js +1 -1
- package/dist/ai/tacticalQueries.d.ts +30 -0
- package/dist/ai/tacticalQueries.js +45 -0
- package/dist/ai/threat.d.ts +8 -0
- package/dist/ai/threat.js +10 -0
- package/dist/anim/animGraph.d.ts +155 -0
- package/dist/anim/animGraph.js +414 -0
- package/dist/anim/boneTexture.d.ts +55 -0
- package/dist/anim/boneTexture.js +43 -0
- package/dist/anim/footPlacement.d.ts +57 -0
- package/dist/anim/footPlacement.js +67 -0
- package/dist/anim/ikSolver.d.ts +51 -0
- package/dist/anim/ikSolver.js +124 -0
- package/dist/anim/locomotionGraph.d.ts +35 -0
- package/dist/anim/locomotionGraph.js +62 -0
- package/dist/assets/spriteAtlas.d.ts +21 -0
- package/dist/assets/spriteAtlas.js +103 -0
- package/dist/audio/audioFalloff.d.ts +12 -0
- package/dist/audio/doppler.d.ts +21 -0
- package/dist/audio/doppler.js +32 -0
- package/dist/audio/engineLayers.d.ts +83 -0
- package/dist/audio/engineLayers.js +115 -0
- package/dist/combat/abilityKit.d.ts +1 -0
- package/dist/combat/abilityKit.js +1 -0
- package/dist/combat/death.d.ts +7 -0
- package/dist/combat/shotOrigin.d.ts +6 -0
- package/dist/combat/shotOrigin.js +6 -0
- package/dist/combat/weaponFeedback.d.ts +36 -0
- package/dist/combat/weaponFeedback.js +42 -0
- package/dist/combat/weaponHandling.d.ts +161 -0
- package/dist/combat/weaponHandling.js +202 -0
- package/dist/combat/weaponPresentation.d.ts +115 -0
- package/dist/combat/weaponPresentation.js +96 -0
- package/dist/devtools/warnOnce.d.ts +8 -0
- package/dist/devtools/warnOnce.js +18 -0
- package/dist/economy/currency.d.ts +16 -2
- package/dist/economy/currency.js +55 -14
- package/dist/economy/wallet.d.ts +5 -4
- package/dist/economy/wallet.js +8 -4
- package/dist/editor/commandApply.js +1 -0
- package/dist/editor/commands.d.ts +4 -1
- package/dist/editor/document.d.ts +2 -1
- package/dist/editor/document.js +92 -1
- package/dist/editor/environment.d.ts +3 -0
- package/dist/editor/environment.js +24 -0
- package/dist/editor/index.d.ts +4 -2
- package/dist/editor/index.js +1 -1
- package/dist/editor/types.d.ts +20 -0
- package/dist/faction/reputation.d.ts +6 -0
- package/dist/faction/reputation.js +11 -0
- package/dist/game/cameraConfig.d.ts +59 -1
- package/dist/game/chat.d.ts +22 -1
- package/dist/game/chat.js +48 -19
- package/dist/game/controlGate.d.ts +6 -0
- package/dist/game/controlGate.js +30 -1
- package/dist/game/defineGame.d.ts +10 -1
- package/dist/game/events.d.ts +14 -1
- package/dist/game/lootTable.d.ts +4 -0
- package/dist/game/lootTable.js +16 -0
- package/dist/game/pixelPerfect.d.ts +25 -0
- package/dist/game/pixelPerfect.js +29 -0
- package/dist/game/playableGame.d.ts +167 -6
- package/dist/game/systemRuntime.js +6 -1
- package/dist/game/toasts.d.ts +8 -0
- package/dist/game/toasts.js +7 -0
- package/dist/game/vfxInstance.d.ts +9 -0
- package/dist/game/vfxInstance.js +12 -1
- package/dist/game/viewports.d.ts +43 -0
- package/dist/game/viewports.js +46 -0
- package/dist/game/worldItem.d.ts +5 -1
- package/dist/game/worldItem.js +0 -1
- package/dist/input/actionBindings.d.ts +17 -2
- package/dist/input/actionBindings.js +24 -11
- package/dist/input/actionContexts.d.ts +45 -0
- package/dist/input/actionContexts.js +113 -0
- package/dist/input/axisInput.d.ts +4 -0
- package/dist/input/axisInput.js +4 -0
- package/dist/input/axisShaper.d.ts +73 -0
- package/dist/input/axisShaper.js +105 -0
- package/dist/input/gamepadModel.d.ts +79 -0
- package/dist/input/gamepadModel.js +108 -0
- package/dist/input/haptics.d.ts +50 -0
- package/dist/input/haptics.js +74 -0
- package/dist/input/inputBuffer.d.ts +40 -0
- package/dist/input/inputBuffer.js +59 -0
- package/dist/input/lookChannel.d.ts +10 -0
- package/dist/input/lookChannel.js +6 -0
- package/dist/input/pointer.d.ts +2 -0
- package/dist/input/pointer.js +4 -0
- package/dist/item/affix.d.ts +6 -0
- package/dist/item/affix.js +6 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +196 -1
- package/dist/movement/characterController.d.ts +76 -0
- package/dist/movement/characterController.js +216 -0
- package/dist/movement/freeFlight.d.ts +119 -0
- package/dist/movement/freeFlight.js +294 -0
- package/dist/movement/glideModel.d.ts +3 -0
- package/dist/movement/glideModel.js +6 -0
- package/dist/movement/grappleSwing.d.ts +2 -0
- package/dist/movement/grappleSwing.js +5 -1
- package/dist/movement/movementModel.d.ts +39 -0
- package/dist/movement/movementModel.js +62 -19
- package/dist/movement/movementProbe.d.ts +51 -0
- package/dist/movement/movementProbe.js +119 -0
- package/dist/movement/playerMovement.d.ts +30 -3
- package/dist/movement/playerMovement.js +304 -7
- package/dist/movement/serverStep.d.ts +5 -0
- package/dist/movement/serverStep.js +6 -0
- package/dist/movement/solidObstacles.d.ts +39 -8
- package/dist/movement/solidObstacles.js +71 -6
- package/dist/movement/voxelController.js +7 -7
- package/dist/multiplayer/poseSyncGate.d.ts +3 -1
- package/dist/multiplayer/poseSyncGate.js +9 -1
- package/dist/multiplayer/presenceContract.d.ts +6 -0
- package/dist/multiplayer/presenceModel.d.ts +2 -0
- package/dist/multiplayer/presenceModel.js +11 -0
- package/dist/nav/navFromEnvironment.d.ts +9 -3
- package/dist/nav/navFromEnvironment.js +18 -12
- package/dist/nav/navMesh.d.ts +74 -0
- package/dist/nav/navMesh.js +570 -0
- package/dist/physics/aircraftDynamics.d.ts +390 -0
- package/dist/physics/aircraftDynamics.js +704 -0
- package/dist/physics/boatDynamics.d.ts +116 -0
- package/dist/physics/boatDynamics.js +160 -0
- package/dist/physics/drivableVehicle.d.ts +22 -8
- package/dist/physics/drivableVehicle.js +4 -2
- package/dist/physics/flightDynamics.d.ts +29 -4
- package/dist/physics/flightDynamics.js +28 -5
- package/dist/physics/handlingProbe.d.ts +249 -0
- package/dist/physics/handlingProbe.js +606 -0
- package/dist/physics/kinematicVehicle.d.ts +37 -2
- package/dist/physics/kinematicVehicle.js +15 -0
- package/dist/physics/physicsBackend.d.ts +203 -0
- package/dist/physics/physicsBackend.js +49 -0
- package/dist/physics/physicsBackendConformance.d.ts +27 -0
- package/dist/physics/physicsBackendConformance.js +151 -0
- package/dist/physics/physicsWorldBackend.d.ts +45 -0
- package/dist/physics/physicsWorldBackend.js +518 -0
- package/dist/physics/vehicleBackendLink.d.ts +49 -0
- package/dist/physics/vehicleBackendLink.js +102 -0
- package/dist/physics/vehicleDynamics.d.ts +374 -0
- package/dist/physics/vehicleDynamics.js +737 -0
- package/dist/physics/worldColliders.d.ts +13 -0
- package/dist/physics/worldColliders.js +128 -0
- package/dist/puzzle/cellGrid.d.ts +15 -9
- package/dist/puzzle/cellGrid.js +13 -9
- package/dist/random/rng.d.ts +12 -1
- package/dist/random/rng.js +20 -3
- package/dist/render/environment.d.ts +23 -0
- package/dist/render/environment.js +1 -0
- package/dist/render/lookPreset.d.ts +19 -6
- package/dist/render/lookPreset.js +56 -4
- package/dist/render/postProcessing.d.ts +40 -1
- package/dist/render/postProcessing.js +1 -1
- package/dist/render/sprite2d.d.ts +38 -0
- package/dist/render/sprite2d.js +40 -0
- package/dist/runtime/adapter.d.ts +20 -1
- package/dist/runtime/adapter.js +8 -1
- package/dist/runtime/cameraDirector.d.ts +17 -3
- package/dist/runtime/cameraDirector.js +25 -0
- package/dist/runtime/commandInput.d.ts +52 -0
- package/dist/runtime/commandInput.js +71 -0
- package/dist/runtime/commandRunner.d.ts +39 -7
- package/dist/runtime/commandRunner.js +35 -5
- package/dist/runtime/context/combat.d.ts +1 -0
- package/dist/runtime/context/combat.js +2 -1
- package/dist/runtime/context/deathLoot.d.ts +2 -0
- package/dist/runtime/context/deathLoot.js +1 -0
- package/dist/runtime/gameContext.js +18 -1
- package/dist/runtime/gameContextTypes.d.ts +33 -7
- package/dist/runtime/gameRuntime.d.ts +23 -5
- package/dist/runtime/gameRuntime.js +21 -4
- package/dist/runtime/headlessRunner.d.ts +10 -0
- package/dist/runtime/headlessRunner.js +26 -8
- package/dist/runtime/hostPolicy.d.ts +3 -2
- package/dist/runtime/hostPolicy.js +8 -10
- package/dist/runtime/hostedGameRunner.js +34 -6
- package/dist/runtime/hostedWorldSession.d.ts +12 -16
- package/dist/runtime/hostedWorldSession.js +29 -6
- package/dist/runtime/hostedWorldStore.d.ts +20 -0
- package/dist/runtime/hostedWorldStore.js +1 -0
- package/dist/runtime/inputRecorder.d.ts +30 -0
- package/dist/runtime/inputRecorder.js +59 -0
- package/dist/runtime/inputSnapshot.d.ts +14 -0
- package/dist/runtime/inputSnapshot.js +11 -0
- package/dist/runtime/localPlayers.d.ts +73 -0
- package/dist/runtime/localPlayers.js +119 -0
- package/dist/runtime/poseInterpolation.d.ts +36 -0
- package/dist/runtime/poseInterpolation.js +91 -0
- package/dist/runtime/prediction.d.ts +45 -0
- package/dist/runtime/prediction.js +62 -0
- package/dist/runtime/simContext.d.ts +45 -0
- package/dist/runtime/simContext.js +65 -0
- package/dist/runtime/simLoop.d.ts +52 -0
- package/dist/runtime/simLoop.js +72 -0
- package/dist/runtime/simSnapshot.d.ts +32 -0
- package/dist/runtime/simSnapshot.js +60 -0
- package/dist/runtime/snapshot.d.ts +8 -0
- package/dist/runtime/snapshotBuffer.d.ts +33 -0
- package/dist/runtime/snapshotBuffer.js +58 -0
- package/dist/runtime/transport.d.ts +41 -2
- package/dist/runtime/transport.js +7 -1
- package/dist/runtime/worldChunks.d.ts +2 -0
- package/dist/runtime/worldChunks.js +14 -0
- package/dist/runtime/worldProjection.d.ts +9 -0
- package/dist/runtime/worldProjection.js +17 -1
- package/dist/runtime/worldSnapshot.d.ts +6 -0
- package/dist/runtime/worldSnapshot.js +3 -0
- package/dist/scene/behaviorRuntime.d.ts +29 -1
- package/dist/scene/behaviorRuntime.js +71 -4
- package/dist/scene/behaviors.d.ts +12 -1
- package/dist/scene/builtinSceneKinds.js +10 -0
- package/dist/scene/sceneKinds.d.ts +9 -0
- package/dist/scene/vehicleSeat.d.ts +5 -1
- package/dist/scene/vehicleSeat.js +5 -1
- package/dist/sensor/concealment.d.ts +6 -0
- package/dist/sensor/concealment.js +8 -0
- package/dist/sensor/freezeMonitor.d.ts +6 -0
- package/dist/sensor/freezeMonitor.js +8 -0
- package/dist/sensor/perception.d.ts +70 -0
- package/dist/sensor/perception.js +189 -0
- package/dist/sensor/recordingBuffer.d.ts +3 -0
- package/dist/sensor/recordingBuffer.js +8 -0
- package/dist/session/roundState.d.ts +4 -0
- package/dist/session/roundState.js +11 -0
- package/dist/settings/frameRateLimit.d.ts +16 -0
- package/dist/settings/frameRateLimit.js +37 -0
- package/dist/settings/graphicsProfile.d.ts +21 -0
- package/dist/settings/graphicsProfile.js +48 -0
- package/dist/settings/graphicsSettings.d.ts +40 -0
- package/dist/settings/graphicsSettings.js +81 -0
- package/dist/settings/settingsModel.d.ts +9 -1
- package/dist/settings/settingsModel.js +10 -0
- package/dist/stats/eventMeter.d.ts +1 -1
- package/dist/stats/eventMeter.js +1 -1
- package/dist/time/accrueSince.d.ts +10 -0
- package/dist/time/accrueSince.js +9 -0
- package/dist/time/rateWindow.d.ts +16 -0
- package/dist/time/rateWindow.js +14 -0
- package/dist/time/serverTick.d.ts +21 -3
- package/dist/time/serverTick.js +14 -13
- package/dist/vfx/feedbackMixer.d.ts +61 -0
- package/dist/vfx/feedbackMixer.js +106 -0
- package/dist/vfx/particleDirector.d.ts +68 -0
- package/dist/vfx/particleDirector.js +81 -0
- package/dist/visibility/simulationCulling.d.ts +8 -1
- package/dist/visibility/simulationCulling.js +7 -0
- package/dist/visibility/spatialIndex.d.ts +21 -0
- package/dist/visibility/spatialIndex.js +16 -0
- package/dist/world/authoredSolids.d.ts +46 -0
- package/dist/world/authoredSolids.js +75 -0
- package/dist/world/buildingGenerator.js +2 -2
- package/dist/world/buildingKit.d.ts +3 -1
- package/dist/world/buildings.d.ts +46 -2
- package/dist/world/buildings.js +53 -3
- package/dist/world/cityContent.js +103 -25
- package/dist/world/cityKind.d.ts +139 -0
- package/dist/world/cityKind.js +367 -16
- package/dist/world/features.d.ts +56 -3
- package/dist/world/features.js +13 -2
- package/dist/world/footprintGrid.d.ts +7 -0
- package/dist/world/footprintGrid.js +16 -0
- package/dist/world/interiors.d.ts +1 -0
- package/dist/world/interiors.js +5 -3
- package/dist/world/lod.d.ts +6 -0
- package/dist/world/lod.js +8 -0
- package/dist/world/placement.d.ts +7 -0
- package/dist/world/placement.js +2 -0
- package/dist/world/placementController.d.ts +1 -1
- package/dist/world/placementController.js +7 -0
- package/dist/world/raceCircuit.d.ts +168 -0
- package/dist/world/raceCircuit.js +797 -0
- package/dist/world/terraform.d.ts +8 -0
- package/dist/world/terraform.js +7 -0
- package/dist/world/terrain.d.ts +5 -1
- package/dist/world/terrain.js +5 -0
- package/dist/world/territory.d.ts +97 -0
- package/dist/world/territory.js +182 -0
- package/dist/world/walls.d.ts +7 -0
- package/dist/world/walls.js +7 -0
- package/dist/world/worldSolids.d.ts +84 -0
- package/dist/world/worldSolids.js +240 -0
- package/dist/world.d.ts +5 -3
- package/dist/world.js +4 -2
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +2 -2
- package/skills/game-design/SKILL.md +11 -1
- package/skills/jgengine/SKILL.md +9 -5
- package/skills/jgengine/api.md +205 -101
- package/skills/jgengine/capabilities.md +56 -0
- package/skills/jgengine/recipes/minimal-game.md +3 -0
- package/skills/jgengine-combat/SKILL.md +5 -0
- package/skills/jgengine-combat/api.md +138 -103
- package/skills/jgengine-combat/capabilities.md +29 -1
- package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
- package/skills/jgengine-gameplay/SKILL.md +10 -0
- package/skills/jgengine-gameplay/api.md +501 -453
- package/skills/jgengine-gameplay/capabilities.md +36 -0
- package/skills/jgengine-gameplay/reference-systems.md +4 -0
- package/skills/jgengine-world/SKILL.md +21 -3
- package/skills/jgengine-world/api.md +1172 -625
- package/skills/jgengine-world/capabilities.md +229 -3
- package/skills/jgengine-world/recipes/character-animation.md +39 -0
- package/skills/jgengine-world/recipes/controls.md +44 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +43 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
- package/skills/jgengine-world/reference.md +31 -2
- package/skills/jgengine-world/visibility.md +112 -0
|
@@ -122,9 +122,28 @@ export declare function offline(): MultiplayerAdapterConfig;
|
|
|
122
122
|
export declare function adapterOf(multiplayer: unknown): MultiplayerAdapterConfig | null;
|
|
123
123
|
export declare function multiplayerAdapterKind(multiplayer: unknown): string | null;
|
|
124
124
|
export type ServersPoolConfig = {
|
|
125
|
+
minPlayersToStart?: number;
|
|
126
|
+
adapter: MultiplayerAdapterConfig;
|
|
127
|
+
} & ({
|
|
128
|
+
topology: "shared";
|
|
129
|
+
maxServers?: number;
|
|
130
|
+
slotsPerServer?: number;
|
|
131
|
+
} | {
|
|
132
|
+
topology?: "rooms";
|
|
133
|
+
maxServers: number;
|
|
134
|
+
slotsPerServer: number;
|
|
135
|
+
});
|
|
136
|
+
/** Resolve the shared-world pool to one unbounded world; room pools keep explicit capacities. */
|
|
137
|
+
export declare function servers(config: ServersPoolConfig): {
|
|
138
|
+
minPlayersToStart?: number;
|
|
139
|
+
adapter: MultiplayerAdapterConfig;
|
|
140
|
+
topology: "rooms" | "shared";
|
|
125
141
|
maxServers: number;
|
|
126
142
|
slotsPerServer: number;
|
|
143
|
+
} | {
|
|
127
144
|
minPlayersToStart?: number;
|
|
128
145
|
adapter: MultiplayerAdapterConfig;
|
|
146
|
+
topology: "rooms" | "shared";
|
|
147
|
+
maxServers: number;
|
|
148
|
+
slotsPerServer: number;
|
|
129
149
|
};
|
|
130
|
-
export declare function servers(config: ServersPoolConfig): ServersPoolConfig;
|
package/dist/runtime/adapter.js
CHANGED
|
@@ -105,6 +105,13 @@ export function adapterOf(multiplayer) {
|
|
|
105
105
|
export function multiplayerAdapterKind(multiplayer) {
|
|
106
106
|
return adapterOf(multiplayer)?.kind ?? null;
|
|
107
107
|
}
|
|
108
|
+
/** Resolve the shared-world pool to one unbounded world; room pools keep explicit capacities. */
|
|
108
109
|
export function servers(config) {
|
|
109
|
-
|
|
110
|
+
const shared = config.topology === "shared";
|
|
111
|
+
const maxServers = shared ? 1 : config.maxServers;
|
|
112
|
+
const slotsPerServer = shared ? Number.MAX_SAFE_INTEGER : config.slotsPerServer;
|
|
113
|
+
if (!Number.isSafeInteger(maxServers) || maxServers < 1 || !Number.isSafeInteger(slotsPerServer) || slotsPerServer < 1) {
|
|
114
|
+
throw new RangeError("Server pool capacities must be positive safe integers");
|
|
115
|
+
}
|
|
116
|
+
return { ...config, topology: config.topology ?? "rooms", maxServers, slotsPerServer };
|
|
110
117
|
}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import type { ChaseCameraConfig, CinematicCameraConfig } from "../game/playableGame.js";
|
|
2
|
-
/** Runtime patch over the static `camera.chase` config — distance/height/fov retuning from gameplay events (#286.11),
|
|
3
|
-
export type ChaseCameraTuning = Partial<Pick<ChaseCameraConfig, "distance" | "height" | "lookHeight" | "springDamping" | "fov" | "lead" | "bank" | "shakePerSpeed" | "velocityYaw" | "yawResponse">>;
|
|
1
|
+
import type { ChaseCameraConfig, ChaseView, CinematicCameraConfig } from "../game/playableGame.js";
|
|
2
|
+
/** Runtime patch over the static `camera.chase` config — distance/height/fov retuning from gameplay events (#286.11), a whole driving-feel overlay applied only while a vehicle is piloted (#1299), or a `view` switch between chase and seat cameras. */
|
|
3
|
+
export type ChaseCameraTuning = Partial<Pick<ChaseCameraConfig, "distance" | "height" | "lookHeight" | "springDamping" | "fov" | "lead" | "bank" | "shakePerSpeed" | "velocityYaw" | "yawResponse" | "distanceBySpeed" | "pitchFollow" | "fovKick" | "lookBackAction" | "collision" | "view">>;
|
|
4
|
+
/** Default order {@link nextChaseView} cycles through. */
|
|
5
|
+
export declare const CHASE_VIEWS: readonly ChaseView[];
|
|
6
|
+
/**
|
|
7
|
+
* The view after `current` in `views`, wrapping at the end; a view missing from the list starts the cycle over.
|
|
8
|
+
* Pair with `setChaseTuning` to bind a "change camera" key:
|
|
9
|
+
* `ctx.camera.setChaseTuning({ ...ctx.camera.chaseTuning(), view: nextChaseView(view) })`.
|
|
10
|
+
*
|
|
11
|
+
* @capability chase-view-cycle cycle a vehicle camera between chase, hood and cockpit views at runtime
|
|
12
|
+
*/
|
|
13
|
+
export declare function nextChaseView(current: ChaseView, views?: readonly ChaseView[]): ChaseView;
|
|
4
14
|
export interface CameraDirector {
|
|
5
15
|
follow(entityId: string | null): void;
|
|
6
16
|
/** `undefined` means no runtime override — the shell falls back to the static `playable.camera.followEntityId`. `null` means explicitly follow nothing. */
|
|
@@ -10,6 +20,10 @@ export interface CameraDirector {
|
|
|
10
20
|
/** Overlay a runtime patch on the chase rig's config — a boss-intro pull-back, drift zoom-out; `null` restores the static config. */
|
|
11
21
|
setChaseTuning(tuning: ChaseCameraTuning | null): void;
|
|
12
22
|
chaseTuning(): ChaseCameraTuning | null;
|
|
23
|
+
/** Add a transient FOV impulse in degrees (a landing, a hit, a boost) that the chase rig layers on and decays per `camera.chase.fovKick`. Kicks from one frame sum. */
|
|
24
|
+
kickFov(degrees: number): void;
|
|
25
|
+
/** Drain the FOV impulses queued since the last call; the shell's chase rig calls this once per frame. */
|
|
26
|
+
takeFovKick(): number;
|
|
13
27
|
subscribe(listener: () => void): () => void;
|
|
14
28
|
}
|
|
15
29
|
/** @internal */
|
|
@@ -1,9 +1,25 @@
|
|
|
1
|
+
/** Default order {@link nextChaseView} cycles through. */
|
|
2
|
+
export const CHASE_VIEWS = ["chase", "hood", "cockpit"];
|
|
3
|
+
/**
|
|
4
|
+
* The view after `current` in `views`, wrapping at the end; a view missing from the list starts the cycle over.
|
|
5
|
+
* Pair with `setChaseTuning` to bind a "change camera" key:
|
|
6
|
+
* `ctx.camera.setChaseTuning({ ...ctx.camera.chaseTuning(), view: nextChaseView(view) })`.
|
|
7
|
+
*
|
|
8
|
+
* @capability chase-view-cycle cycle a vehicle camera between chase, hood and cockpit views at runtime
|
|
9
|
+
*/
|
|
10
|
+
export function nextChaseView(current, views = CHASE_VIEWS) {
|
|
11
|
+
if (views.length === 0)
|
|
12
|
+
return current;
|
|
13
|
+
const index = views.indexOf(current);
|
|
14
|
+
return views[(index + 1) % views.length];
|
|
15
|
+
}
|
|
1
16
|
/** @internal */
|
|
2
17
|
export function createCameraDirector() {
|
|
3
18
|
const listeners = new Set();
|
|
4
19
|
let followEntity = undefined;
|
|
5
20
|
let cinematicConfig = null;
|
|
6
21
|
let chaseTuningPatch = null;
|
|
22
|
+
let pendingFovKick = 0;
|
|
7
23
|
function notify() {
|
|
8
24
|
for (const listener of listeners)
|
|
9
25
|
listener();
|
|
@@ -24,6 +40,15 @@ export function createCameraDirector() {
|
|
|
24
40
|
notify();
|
|
25
41
|
},
|
|
26
42
|
chaseTuning: () => chaseTuningPatch,
|
|
43
|
+
kickFov(degrees) {
|
|
44
|
+
if (Number.isFinite(degrees))
|
|
45
|
+
pendingFovKick += degrees;
|
|
46
|
+
},
|
|
47
|
+
takeFovKick() {
|
|
48
|
+
const kick = pendingFovKick;
|
|
49
|
+
pendingFovKick = 0;
|
|
50
|
+
return kick;
|
|
51
|
+
},
|
|
27
52
|
subscribe(listener) {
|
|
28
53
|
listeners.add(listener);
|
|
29
54
|
return () => listeners.delete(listener);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Validated integer quantity or a stable input rejection. */
|
|
2
|
+
export type QuantityResult = {
|
|
3
|
+
ok: true;
|
|
4
|
+
quantity: number;
|
|
5
|
+
} | {
|
|
6
|
+
ok: false;
|
|
7
|
+
reason: "invalid-quantity";
|
|
8
|
+
};
|
|
9
|
+
/** Read an untrusted whole-item quantity without coercion, truncation, or clamping.
|
|
10
|
+
* @capability command-quantity validate bounded whole-item counts at command boundaries
|
|
11
|
+
*/
|
|
12
|
+
export declare function readQuantity(value: unknown, options?: {
|
|
13
|
+
min?: number;
|
|
14
|
+
max?: number;
|
|
15
|
+
}): QuantityResult;
|
|
16
|
+
/** Narrow an untrusted value to a plain object, rejecting arrays and null. */
|
|
17
|
+
export declare function isInputRecord(value: unknown): value is Record<string, unknown>;
|
|
18
|
+
/** Read a non-empty string of at most `maxLength` characters, or `null`. */
|
|
19
|
+
export declare function readInputString(value: unknown, options?: {
|
|
20
|
+
maxLength?: number;
|
|
21
|
+
}): string | null;
|
|
22
|
+
/**
|
|
23
|
+
* Read a finite number inside `[min, max]`, or `null`. `NaN` and `±Infinity` are always rejected, so a
|
|
24
|
+
* coordinate can never drive an unbounded loop; `integer` also rejects fractions instead of flooring them.
|
|
25
|
+
* @capability command-number validate finite, bounded numbers and integers at command boundaries
|
|
26
|
+
*/
|
|
27
|
+
export declare function readInputNumber(value: unknown, options?: {
|
|
28
|
+
min?: number;
|
|
29
|
+
max?: number;
|
|
30
|
+
integer?: boolean;
|
|
31
|
+
}): number | null;
|
|
32
|
+
/** Read one of a fixed set of values, or `null`. */
|
|
33
|
+
export declare function readInputOneOf<const T extends string | number>(value: unknown, allowed: readonly T[]): T | null;
|
|
34
|
+
/** Read an `{ x, y, z }` point whose axes are finite and within `maxAbs`, or `null`. */
|
|
35
|
+
export declare function readInputPoint3(value: unknown, options?: {
|
|
36
|
+
maxAbs?: number;
|
|
37
|
+
}): {
|
|
38
|
+
x: number;
|
|
39
|
+
y: number;
|
|
40
|
+
z: number;
|
|
41
|
+
} | null;
|
|
42
|
+
/** Read an `{ x, z }` ground point whose axes are finite and within `maxAbs`, or `null`. */
|
|
43
|
+
export declare function readInputPoint2(value: unknown, options?: {
|
|
44
|
+
maxAbs?: number;
|
|
45
|
+
}): {
|
|
46
|
+
x: number;
|
|
47
|
+
z: number;
|
|
48
|
+
} | null;
|
|
49
|
+
/** Read an array of at most `maxItems` entries, each accepted by `readItem`; any rejected entry rejects the whole array. */
|
|
50
|
+
export declare function readInputArray<T>(value: unknown, readItem: (item: unknown) => T | null, options?: {
|
|
51
|
+
maxItems?: number;
|
|
52
|
+
}): T[] | null;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Read an untrusted whole-item quantity without coercion, truncation, or clamping.
|
|
2
|
+
* @capability command-quantity validate bounded whole-item counts at command boundaries
|
|
3
|
+
*/
|
|
4
|
+
export function readQuantity(value, options = {}) {
|
|
5
|
+
const min = options.min ?? 1;
|
|
6
|
+
const max = options.max ?? Number.MAX_SAFE_INTEGER;
|
|
7
|
+
if (!Number.isSafeInteger(min) || !Number.isSafeInteger(max) || min < 0 || max < min) {
|
|
8
|
+
throw new RangeError("Quantity bounds must be nonnegative safe integers in order");
|
|
9
|
+
}
|
|
10
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= max
|
|
11
|
+
? { ok: true, quantity: value }
|
|
12
|
+
: { ok: false, reason: "invalid-quantity" };
|
|
13
|
+
}
|
|
14
|
+
/** Narrow an untrusted value to a plain object, rejecting arrays and null. */
|
|
15
|
+
export function isInputRecord(value) {
|
|
16
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
/** Read a non-empty string of at most `maxLength` characters, or `null`. */
|
|
19
|
+
export function readInputString(value, options = {}) {
|
|
20
|
+
return typeof value === "string" && value.length > 0 && value.length <= (options.maxLength ?? 256) ? value : null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Read a finite number inside `[min, max]`, or `null`. `NaN` and `±Infinity` are always rejected, so a
|
|
24
|
+
* coordinate can never drive an unbounded loop; `integer` also rejects fractions instead of flooring them.
|
|
25
|
+
* @capability command-number validate finite, bounded numbers and integers at command boundaries
|
|
26
|
+
*/
|
|
27
|
+
export function readInputNumber(value, options = {}) {
|
|
28
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
29
|
+
return null;
|
|
30
|
+
if (options.integer === true && !Number.isSafeInteger(value))
|
|
31
|
+
return null;
|
|
32
|
+
if (value < (options.min ?? -Number.MAX_VALUE) || value > (options.max ?? Number.MAX_VALUE))
|
|
33
|
+
return null;
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
/** Read one of a fixed set of values, or `null`. */
|
|
37
|
+
export function readInputOneOf(value, allowed) {
|
|
38
|
+
return allowed.includes(value) ? value : null;
|
|
39
|
+
}
|
|
40
|
+
/** Read an `{ x, y, z }` point whose axes are finite and within `maxAbs`, or `null`. */
|
|
41
|
+
export function readInputPoint3(value, options = {}) {
|
|
42
|
+
if (!isInputRecord(value))
|
|
43
|
+
return null;
|
|
44
|
+
const bound = { min: -(options.maxAbs ?? Number.MAX_VALUE), max: options.maxAbs ?? Number.MAX_VALUE };
|
|
45
|
+
const x = readInputNumber(value.x, bound);
|
|
46
|
+
const y = readInputNumber(value.y, bound);
|
|
47
|
+
const z = readInputNumber(value.z, bound);
|
|
48
|
+
return x === null || y === null || z === null ? null : { x, y, z };
|
|
49
|
+
}
|
|
50
|
+
/** Read an `{ x, z }` ground point whose axes are finite and within `maxAbs`, or `null`. */
|
|
51
|
+
export function readInputPoint2(value, options = {}) {
|
|
52
|
+
if (!isInputRecord(value))
|
|
53
|
+
return null;
|
|
54
|
+
const bound = { min: -(options.maxAbs ?? Number.MAX_VALUE), max: options.maxAbs ?? Number.MAX_VALUE };
|
|
55
|
+
const x = readInputNumber(value.x, bound);
|
|
56
|
+
const z = readInputNumber(value.z, bound);
|
|
57
|
+
return x === null || z === null ? null : { x, z };
|
|
58
|
+
}
|
|
59
|
+
/** Read an array of at most `maxItems` entries, each accepted by `readItem`; any rejected entry rejects the whole array. */
|
|
60
|
+
export function readInputArray(value, readItem, options = {}) {
|
|
61
|
+
if (!Array.isArray(value) || value.length > (options.maxItems ?? 256))
|
|
62
|
+
return null;
|
|
63
|
+
const items = [];
|
|
64
|
+
for (const entry of value) {
|
|
65
|
+
const item = readItem(entry);
|
|
66
|
+
if (item === null)
|
|
67
|
+
return null;
|
|
68
|
+
items.push(item);
|
|
69
|
+
}
|
|
70
|
+
return items;
|
|
71
|
+
}
|
|
@@ -12,23 +12,47 @@ export type CommandScope = {
|
|
|
12
12
|
/** Chunk keys to hydrate; omit for every chunk of the server, `[]` for none. */
|
|
13
13
|
chunkKeys?: readonly string[];
|
|
14
14
|
};
|
|
15
|
+
/** Declarative read scope; actor is resolved before host hydration. */
|
|
16
|
+
export type CommandScopeDefinition = Omit<CommandScope, "players"> & {
|
|
17
|
+
players?: readonly string[] | "actor";
|
|
18
|
+
};
|
|
19
|
+
/** Who may send a command: any client through the public transport, or only trusted host code. */
|
|
20
|
+
export type CommandAccess = "client" | "server";
|
|
21
|
+
/** Reason every host returns when a command's `parse` rejects its input. */
|
|
22
|
+
export declare const MALFORMED_COMMAND_INPUT_REASON = "Malformed command input";
|
|
23
|
+
/** Reason a public transport returns for a {@link CommandDef} marked `access: "server"`. */
|
|
24
|
+
export declare const SERVER_ONLY_COMMAND_REASON = "Command is server-only";
|
|
15
25
|
export type CommandDef<TInput = unknown> = {
|
|
26
|
+
/**
|
|
27
|
+
* `"server"` when `input` carries facts only the host may decide (unlock state, prices, multipliers):
|
|
28
|
+
* public transports then refuse it and only trusted host calls such as `helpers.runCommand` run it.
|
|
29
|
+
* Defaults to `"client"`, so every field of `input` must be validated as untrusted.
|
|
30
|
+
*/
|
|
31
|
+
access?: CommandAccess;
|
|
32
|
+
/**
|
|
33
|
+
* Turns the untrusted wire value into the command's input, or `null` to refuse it with
|
|
34
|
+
* {@link MALFORMED_COMMAND_INPUT_REASON} before `scope`, `validate` or `apply` run. Those then only ever see
|
|
35
|
+
* parsed input, so a field checked here can't be skipped in one of them. Build it from the readers in
|
|
36
|
+
* `runtime/commandInput`; declare typed commands with {@link defineCommand}.
|
|
37
|
+
*/
|
|
38
|
+
parse?: (input: unknown) => TInput | null;
|
|
16
39
|
/**
|
|
17
40
|
* What this command reads and writes, derived from its own input. A host that hydrates through a
|
|
18
41
|
* scope loads only this slice instead of the whole world, and refuses the command if `apply` then
|
|
19
42
|
* dirties a player or chunk the scope did not name — writing an unhydrated chunk would overwrite
|
|
20
|
-
* the stored one with a blank. Omit it to
|
|
43
|
+
* the stored one with a blank. Omit it to load only the actor and no chunks; return `{}` to request the whole world.
|
|
21
44
|
*/
|
|
22
|
-
scope?: (input: TInput, actorUserId: string) =>
|
|
23
|
-
|
|
24
|
-
|
|
45
|
+
scope?: (input: TInput, actorUserId: string) => CommandScopeDefinition;
|
|
46
|
+
/** `nowMs` is the host wall clock, in ms — read it rather than a `now` the client put in `input`. */
|
|
47
|
+
validate: (snapshot: import("./snapshot.js").GameRuntimeSnapshot, input: TInput, actorUserId: string, nowMs: number) => CommandValidationError | null;
|
|
48
|
+
apply: (snapshot: import("./snapshot.js").GameRuntimeSnapshot, input: TInput, actorUserId: string, nowMs: number) => import("./snapshot.js").GameRuntimeSnapshot;
|
|
25
49
|
};
|
|
26
50
|
/**
|
|
27
|
-
* The scope
|
|
51
|
+
* The resolved scope, defaulting to the actor and no chunks.
|
|
28
52
|
* Evaluate it before hydration: the actor and the command's own input are both known by then.
|
|
29
53
|
* @internal
|
|
30
54
|
*/
|
|
31
|
-
export declare function resolveCommandScope<TInput>(commands: Record<string, CommandDef<TInput>>, commandName: string, input: TInput, actorUserId: string): CommandScope
|
|
55
|
+
export declare function resolveCommandScope<TInput>(commands: Record<string, CommandDef<TInput>>, commandName: string, input: TInput, actorUserId: string): CommandScope;
|
|
32
56
|
/**
|
|
33
57
|
* Widen a scope so the actor is always hydrated. A command's own player row is dirtied by
|
|
34
58
|
* {@link runCommand} whatever the command did, and an unhydrated player is dropped from the persist.
|
|
@@ -49,4 +73,12 @@ export type RunCommandResult = {
|
|
|
49
73
|
reason: string;
|
|
50
74
|
};
|
|
51
75
|
/** @internal */
|
|
52
|
-
export declare function runCommand<TInput>(snapshot: import("./snapshot.js").GameRuntimeSnapshot, commands: Record<string, CommandDef<TInput>>, commandName: string, input: TInput, actorUserId: string): RunCommandResult;
|
|
76
|
+
export declare function runCommand<TInput>(snapshot: import("./snapshot.js").GameRuntimeSnapshot, commands: Record<string, CommandDef<TInput>>, commandName: string, input: TInput, actorUserId: string, nowMs?: number): RunCommandResult;
|
|
77
|
+
/**
|
|
78
|
+
* Declare a command with a typed input: `parse` narrows the wire value once, and `scope`, `validate` and
|
|
79
|
+
* `apply` receive the parsed type. Returns the untyped {@link CommandDef} a runtime's command table holds.
|
|
80
|
+
* @capability command-parse parse untrusted command input once so every command stage sees typed, bounded values
|
|
81
|
+
*/
|
|
82
|
+
export declare function defineCommand<TInput>(definition: CommandDef<TInput> & {
|
|
83
|
+
parse: (input: unknown) => TInput | null;
|
|
84
|
+
}): CommandDef;
|
|
@@ -1,10 +1,25 @@
|
|
|
1
|
+
/** Reason every host returns when a command's `parse` rejects its input. */
|
|
2
|
+
export const MALFORMED_COMMAND_INPUT_REASON = "Malformed command input";
|
|
3
|
+
/** Reason a public transport returns for a {@link CommandDef} marked `access: "server"`. */
|
|
4
|
+
export const SERVER_ONLY_COMMAND_REASON = "Command is server-only";
|
|
1
5
|
/**
|
|
2
|
-
* The scope
|
|
6
|
+
* The resolved scope, defaulting to the actor and no chunks.
|
|
3
7
|
* Evaluate it before hydration: the actor and the command's own input are both known by then.
|
|
4
8
|
* @internal
|
|
5
9
|
*/
|
|
6
10
|
export function resolveCommandScope(commands, commandName, input, actorUserId) {
|
|
7
|
-
|
|
11
|
+
const command = commands[commandName];
|
|
12
|
+
let declared;
|
|
13
|
+
if (command?.parse === undefined) {
|
|
14
|
+
declared = command?.scope?.(input, actorUserId);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
const parsed = command.parse(input);
|
|
18
|
+
declared = parsed === null ? undefined : command.scope?.(parsed, actorUserId);
|
|
19
|
+
}
|
|
20
|
+
const scope = declared ?? { players: "actor", chunkKeys: [] };
|
|
21
|
+
const { players, ...rest } = scope;
|
|
22
|
+
return players === undefined ? rest : { ...rest, players: players === "actor" ? [actorUserId] : players };
|
|
8
23
|
}
|
|
9
24
|
/**
|
|
10
25
|
* Widen a scope so the actor is always hydrated. A command's own player row is dirtied by
|
|
@@ -35,16 +50,23 @@ export function commandScopeEscape(scope, dirty, commandName) {
|
|
|
35
50
|
return `Command "${commandName}" wrote outside its declared scope: ${parts.join("; ")}`;
|
|
36
51
|
}
|
|
37
52
|
/** @internal */
|
|
38
|
-
export function runCommand(snapshot, commands, commandName, input, actorUserId) {
|
|
53
|
+
export function runCommand(snapshot, commands, commandName, input, actorUserId, nowMs = Date.now()) {
|
|
39
54
|
const command = commands[commandName];
|
|
40
55
|
if (!command) {
|
|
41
56
|
return { ok: false, reason: `Unknown command: ${commandName}` };
|
|
42
57
|
}
|
|
43
|
-
|
|
58
|
+
let parsed = input;
|
|
59
|
+
if (command.parse !== undefined) {
|
|
60
|
+
const result = command.parse(input);
|
|
61
|
+
if (result === null)
|
|
62
|
+
return { ok: false, reason: MALFORMED_COMMAND_INPUT_REASON };
|
|
63
|
+
parsed = result;
|
|
64
|
+
}
|
|
65
|
+
const validationError = command.validate(snapshot, parsed, actorUserId, nowMs);
|
|
44
66
|
if (validationError) {
|
|
45
67
|
return { ok: false, reason: validationError.reason };
|
|
46
68
|
}
|
|
47
|
-
const next = command.apply(snapshot,
|
|
69
|
+
const next = command.apply(snapshot, parsed, actorUserId, nowMs);
|
|
48
70
|
return {
|
|
49
71
|
ok: true,
|
|
50
72
|
snapshot: {
|
|
@@ -60,3 +82,11 @@ export function runCommand(snapshot, commands, commandName, input, actorUserId)
|
|
|
60
82
|
},
|
|
61
83
|
};
|
|
62
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Declare a command with a typed input: `parse` narrows the wire value once, and `scope`, `validate` and
|
|
87
|
+
* `apply` receive the parsed type. Returns the untyped {@link CommandDef} a runtime's command table holds.
|
|
88
|
+
* @capability command-parse parse untrusted command input once so every command stage sees typed, bounded values
|
|
89
|
+
*/
|
|
90
|
+
export function defineCommand(definition) {
|
|
91
|
+
return definition;
|
|
92
|
+
}
|
|
@@ -36,6 +36,7 @@ export interface CombatSubsystemDeps {
|
|
|
36
36
|
despawnEntity: (instanceId: string) => boolean;
|
|
37
37
|
runCommand: (name: string, args: unknown) => void;
|
|
38
38
|
localUserId: string;
|
|
39
|
+
rng: () => number;
|
|
39
40
|
physics?: PhysicsConfig;
|
|
40
41
|
}
|
|
41
42
|
/** @internal Effects, projectiles, death, and combat presentation surface. */
|
|
@@ -8,7 +8,7 @@ import { createCombatFx } from "./combatFx.js";
|
|
|
8
8
|
import { applyLethalLoot, isLocalPlayerKill } from "./deathLoot.js";
|
|
9
9
|
/** @internal */
|
|
10
10
|
export function createCombatSubsystem(d) {
|
|
11
|
-
const { content, signalNotify, now, events, time, entities, objects, combatSpatial, sceneRaycast, entityCollidersOf, objectCollidersOf, catalogEntry, statsByInstance, weapon, loot, lootRegistry, spawnWorldItem, despawnEntity, runCommand, localUserId, } = d;
|
|
11
|
+
const { content, signalNotify, now, events, time, entities, objects, combatSpatial, sceneRaycast, entityCollidersOf, objectCollidersOf, catalogEntry, statsByInstance, weapon, loot, lootRegistry, spawnWorldItem, despawnEntity, runCommand, localUserId, rng, } = d;
|
|
12
12
|
const death = createDeathSystem({
|
|
13
13
|
resolveOnDeath: (instanceId) => catalogEntry(instanceId)?.onDeath,
|
|
14
14
|
resolveIdentity(instanceId) {
|
|
@@ -64,6 +64,7 @@ export function createCombatSubsystem(d) {
|
|
|
64
64
|
spawnWorldItem,
|
|
65
65
|
grantToPlayer: loot.grantToPlayer,
|
|
66
66
|
localUserId,
|
|
67
|
+
rng,
|
|
67
68
|
});
|
|
68
69
|
},
|
|
69
70
|
}), ["applyEffect"], signalNotify);
|
|
@@ -18,6 +18,8 @@ export interface LethalLootInput {
|
|
|
18
18
|
spawnWorldItem: (input: WorldItemSpawnInput) => void;
|
|
19
19
|
grantToPlayer: (userId: string, drops: Drop[], source?: string) => void;
|
|
20
20
|
localUserId: string;
|
|
21
|
+
/** The world's seeded stream — scatter positions must replay with the rest of the simulation. */
|
|
22
|
+
rng: () => number;
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
23
25
|
* Pure death→loot policy: when a kill yields drops for the local player, either scatter them as
|
|
@@ -14,6 +14,7 @@ export function applyLethalLoot(input) {
|
|
|
14
14
|
const resolved = resolveDeathDrops([...input.drops], {
|
|
15
15
|
mode: "world",
|
|
16
16
|
origin: input.position,
|
|
17
|
+
rng: input.rng,
|
|
17
18
|
resolveRarity: (itemId) => input.content.itemById?.(itemId)?.rarity ?? DEFAULT_RARITY,
|
|
18
19
|
resolveBaseType: (itemId) => input.content.itemById?.(itemId)?.baseType ?? itemId,
|
|
19
20
|
scatter: normalizedOnDeath.scatter,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createCommandRegistry } from "../commands/commandRegistry.js";
|
|
2
2
|
import {} from "../game/cosmetics.js";
|
|
3
3
|
import { groundFieldFor } from "../world/terrain.js";
|
|
4
|
+
import { createWorldSolids, structureSolids } from "../world/worldSolids.js";
|
|
4
5
|
import { createGameEvents } from "../game/events.js";
|
|
5
6
|
import { createGameFeed } from "../game/feed.js";
|
|
6
7
|
import { setGamePhase } from "../game/gamePhase.js";
|
|
@@ -19,8 +20,10 @@ import { createRuntimeSave } from "./runtimeSave.js";
|
|
|
19
20
|
import { isOffline } from "./adapter.js";
|
|
20
21
|
import { localSaveBackend, memorySaveBackend } from "../game/saveStore.js";
|
|
21
22
|
import { createSimClock } from "../time/simClock.js";
|
|
23
|
+
import { createSimContext } from "./simContext.js";
|
|
22
24
|
import { seededRng } from "../random/rng.js";
|
|
23
25
|
import { createCameraDirector } from "./cameraDirector.js";
|
|
26
|
+
import { createParticleDirector } from "../vfx/particleDirector.js";
|
|
24
27
|
import { createInputSnapshot } from "./inputSnapshot.js";
|
|
25
28
|
import { baselineDescriptors } from "./descriptors/baseline.js";
|
|
26
29
|
import { featureDescriptors } from "./descriptors/features.js";
|
|
@@ -41,6 +44,10 @@ export function createGameContext(options) {
|
|
|
41
44
|
const activeUserId = () => actingUserId ?? player.userId;
|
|
42
45
|
const time = createSimClock({ config: definition.time, onChange: signal.notify });
|
|
43
46
|
const ground = groundFieldFor(definition.world);
|
|
47
|
+
const solids = createWorldSolids();
|
|
48
|
+
if (definition.world?.kind === "environment") {
|
|
49
|
+
solids.set("environment:structures", structureSolids(definition.world, ground.sampleHeight));
|
|
50
|
+
}
|
|
44
51
|
const rawEvents = createGameEvents();
|
|
45
52
|
const events = {
|
|
46
53
|
on: rawEvents.on,
|
|
@@ -213,6 +220,7 @@ export function createGameContext(options) {
|
|
|
213
220
|
commandRegistry.run(ctxRef, name, args);
|
|
214
221
|
},
|
|
215
222
|
localUserId: player.userId,
|
|
223
|
+
rng,
|
|
216
224
|
...(definition.physics !== undefined ? { physics: definition.physics } : {}),
|
|
217
225
|
});
|
|
218
226
|
scene.setOnAfterSpawn((instanceId) => combat.death.revive(instanceId));
|
|
@@ -220,6 +228,7 @@ export function createGameContext(options) {
|
|
|
220
228
|
const store = notifyAfter(createObservableKeyedStore(), ["set", "delete", "hydrate"], signal.notify);
|
|
221
229
|
const { pile, loop, raceState, cardPiles, turnLoops } = createContextRegistries(signal.notify);
|
|
222
230
|
const camera = notifyAfter(createCameraDirector(), ["follow", "setCinematic", "setChaseTuning"], signal.notify);
|
|
231
|
+
const particles = createParticleDirector();
|
|
223
232
|
const input = createInputSnapshot();
|
|
224
233
|
// --- Descriptor install (single feature enable path) ---
|
|
225
234
|
const featureDeps = {
|
|
@@ -289,7 +298,7 @@ export function createGameContext(options) {
|
|
|
289
298
|
...featureReplicateModules,
|
|
290
299
|
];
|
|
291
300
|
const replication = options.replication;
|
|
292
|
-
const projectsViewers = policyProjectsViewers(replication);
|
|
301
|
+
const projectsViewers = policyProjectsViewers(replication) || snapshotModules.some((module) => module.priority !== undefined);
|
|
293
302
|
const aoiRadius = replication?.aoiRadius;
|
|
294
303
|
const typedProject = (project) => (data, viewer, world) => project(data, viewer, world);
|
|
295
304
|
const projectorFor = (key) => {
|
|
@@ -403,8 +412,10 @@ export function createGameContext(options) {
|
|
|
403
412
|
world: {
|
|
404
413
|
ground,
|
|
405
414
|
groundHeightAt: ground.sampleHeight,
|
|
415
|
+
solids,
|
|
406
416
|
},
|
|
407
417
|
game: {
|
|
418
|
+
territory: options.territory,
|
|
408
419
|
commands: {
|
|
409
420
|
define: commandRegistry.define,
|
|
410
421
|
has: commandRegistry.has,
|
|
@@ -496,7 +507,9 @@ export function createGameContext(options) {
|
|
|
496
507
|
weapon,
|
|
497
508
|
},
|
|
498
509
|
time,
|
|
510
|
+
sim: createSimContext({ config: definition.simulation, entities }),
|
|
499
511
|
camera,
|
|
512
|
+
particles,
|
|
500
513
|
input,
|
|
501
514
|
subscribe: signal.subscribe,
|
|
502
515
|
version: signal.version,
|
|
@@ -510,6 +523,10 @@ export function createGameContext(options) {
|
|
|
510
523
|
replicatesPerViewer: () => projectsViewers,
|
|
511
524
|
};
|
|
512
525
|
ctxRef = ctx;
|
|
526
|
+
const physicsBackend = definition.physics?.backend;
|
|
527
|
+
if (physicsBackend !== undefined) {
|
|
528
|
+
ctx.sim.addStage({ id: "physics", phase: "afterMovement", run: (dt) => physicsBackend.step(dt) });
|
|
529
|
+
}
|
|
513
530
|
const saveOptions = resolveSaveOptions(definition, options);
|
|
514
531
|
if (saveOptions !== undefined) {
|
|
515
532
|
const saveTarget = {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { Territory } from "../world/territory.js";
|
|
2
|
+
import type { CurrencyDefinition } from "../economy/currency.js";
|
|
1
3
|
import type { CardPile, CardPileConfig } from "../cards/cardPile.js";
|
|
2
4
|
import type { EffectInput, EffectResult, EffectVia, ReceiveMap, SingleTargetEffectInput } from "../combat/effects.js";
|
|
3
5
|
import type { ProjectileSystem } from "../combat/projectiles.js";
|
|
@@ -9,6 +11,7 @@ import type { ChargeOptions as WalletChargeOptions } from "../economy/wallet.js"
|
|
|
9
11
|
import type { Cosmetics } from "../game/cosmetics.js";
|
|
10
12
|
import type { GameDefinition } from "../game/defineGame.js";
|
|
11
13
|
import type { TerrainField } from "../world/terrain.js";
|
|
14
|
+
import type { WorldSolids } from "../world/worldSolids.js";
|
|
12
15
|
import type { GameEventMap, GameEvents, VfxKind } from "../game/events.js";
|
|
13
16
|
import type { VfxInstanceStore } from "../game/vfxInstance.js";
|
|
14
17
|
import type { GameFeed } from "../game/feed.js";
|
|
@@ -52,9 +55,11 @@ import type { SnapshotModule, SnapshotViewer, WorldSnapshot } from "./worldSnaps
|
|
|
52
55
|
import type { ReplicationPolicy } from "./worldProjection.js";
|
|
53
56
|
import type { RuntimeSave, RuntimeSaveOptions } from "./runtimeSave.js";
|
|
54
57
|
import type { SimClock } from "../time/simClock.js";
|
|
58
|
+
import type { SimContext } from "./simContext.js";
|
|
55
59
|
import type { TurnLoop, TurnLoopConfig } from "../turn/turnLoop.js";
|
|
56
60
|
import type { RaceState, RaceStateConfig } from "../game/race.js";
|
|
57
61
|
import type { CameraDirector } from "./cameraDirector.js";
|
|
62
|
+
import type { ParticleDirector } from "../vfx/particleDirector.js";
|
|
58
63
|
import type { InputSnapshot } from "./inputSnapshot.js";
|
|
59
64
|
import type { MotionIntents } from "./motionIntents.js";
|
|
60
65
|
import type { OnDeathSpec } from "../combat/death.js";
|
|
@@ -111,6 +116,7 @@ export interface GameContextOptions<TAssetRef extends ModelAssetRef = ModelAsset
|
|
|
111
116
|
userId: string;
|
|
112
117
|
isNew: boolean;
|
|
113
118
|
};
|
|
119
|
+
territory?: Territory;
|
|
114
120
|
now?: () => number;
|
|
115
121
|
occluder?: (from: EntityPosition, to: EntityPosition) => boolean;
|
|
116
122
|
/**
|
|
@@ -339,6 +345,11 @@ export type WorldItemPickupResult = {
|
|
|
339
345
|
status: "rejected";
|
|
340
346
|
reason: string;
|
|
341
347
|
};
|
|
348
|
+
/**
|
|
349
|
+
* `ctx.scene.worldItem`: items lying in the world — spawn, list, find the nearest, and pick up into a player's bag.
|
|
350
|
+
*
|
|
351
|
+
* @capability world-item-pickup pick up dropped items: ctx.scene.worldItem nearestInRadius → pickup into the player's inventory
|
|
352
|
+
*/
|
|
342
353
|
export interface SceneWorldItemContext {
|
|
343
354
|
spawn(input: WorldItemSpawnInput): WorldItemRecord;
|
|
344
355
|
get(instanceId: string): WorldItemRecord | null;
|
|
@@ -367,14 +378,14 @@ export interface GameContextLoot {
|
|
|
367
378
|
grantToPlayer(userId: string, drops: Drop[], source?: string): void;
|
|
368
379
|
}
|
|
369
380
|
export interface GameContextEconomy {
|
|
370
|
-
balance(userId: string, currencyId: string): number;
|
|
371
|
-
grant(userId: string, currencyId: string, amount: number): void;
|
|
381
|
+
balance(userId: string, currencyId: string | CurrencyDefinition): number;
|
|
382
|
+
grant(userId: string, currencyId: string | CurrencyDefinition, amount: number): void;
|
|
372
383
|
/** `options.overdraft` opts this charge into carrying a negative balance (`true` unlimited, `{ max }` capped) — omitted keeps the strict no-debt default. */
|
|
373
|
-
charge(userId: string, currencyId: string, amount: number, options?: WalletChargeOptions): {
|
|
384
|
+
charge(userId: string, currencyId: string | CurrencyDefinition, amount: number, options?: WalletChargeOptions): {
|
|
374
385
|
reason: string;
|
|
375
386
|
} | null;
|
|
376
387
|
/** True once `balance(userId, currencyId)` has gone negative under an overdraft-enabled charge. */
|
|
377
|
-
isOverdrawn(userId: string, currencyId: string): boolean;
|
|
388
|
+
isOverdrawn(userId: string, currencyId: string | CurrencyDefinition): boolean;
|
|
378
389
|
}
|
|
379
390
|
export interface GameContextItemUse {
|
|
380
391
|
register(handlers: Record<string, ItemUseHandler<GameContext>>): void;
|
|
@@ -385,6 +396,11 @@ export interface GameContextItemUse {
|
|
|
385
396
|
export interface GameContextWorld {
|
|
386
397
|
ground: TerrainField;
|
|
387
398
|
groundHeightAt(x: number, z: number): number;
|
|
399
|
+
/**
|
|
400
|
+
* Static collision for world geometry that is not a scene object. `environment({ structures })`
|
|
401
|
+
* fills the `environment:structures` layer; `syncAuthoredSolids` fills one layer per studio object.
|
|
402
|
+
*/
|
|
403
|
+
solids: WorldSolids;
|
|
388
404
|
}
|
|
389
405
|
export interface GameContextCards {
|
|
390
406
|
/** Lazily creates (on first call, `config` required) or returns the existing notify-wrapped pile for `id`. */
|
|
@@ -415,14 +431,19 @@ export interface GameAudio {
|
|
|
415
431
|
}): void;
|
|
416
432
|
/**
|
|
417
433
|
* Live-update retained loop `id` via `audio.loopSet`: `rate` re-pitches it (1 = authored pitch, clamped
|
|
418
|
-
* 0.25–4 by the shell), `gain` rescales volume (0–1), `at` repositions its emitter.
|
|
419
|
-
*
|
|
420
|
-
*
|
|
434
|
+
* 0.25–4 by the shell), `gain` rescales volume (0–1), `at` repositions its emitter. `lowpass`/`highpass`
|
|
435
|
+
* set filter cutoffs in Hz (muffle an engine off-throttle or behind a wall); `velocity` is the emitter's
|
|
436
|
+
* world velocity, which pitches the loop by doppler when its sound declares `doppler`. Cheap to call every
|
|
437
|
+
* tick (~60 Hz) — the shell ramps rate/gain/cutoffs over ~20 ms to avoid zipper noise. A no-op when `id`
|
|
438
|
+
* is not a live loop (an update may race a stop) (#1051).
|
|
421
439
|
*/
|
|
422
440
|
setLoop(id: string, params: {
|
|
423
441
|
rate?: number;
|
|
424
442
|
gain?: number;
|
|
425
443
|
at?: readonly [number, number, number];
|
|
444
|
+
velocity?: readonly [number, number, number];
|
|
445
|
+
lowpass?: number;
|
|
446
|
+
highpass?: number;
|
|
426
447
|
}): void;
|
|
427
448
|
/** Stop and dispose retained loop `id` (emits `audio.loopStop`); an unknown `id` is ignored (#1051). */
|
|
428
449
|
stopLoop(id: string): void;
|
|
@@ -446,6 +467,7 @@ export interface GameContext {
|
|
|
446
467
|
*/
|
|
447
468
|
rng: () => number;
|
|
448
469
|
game: {
|
|
470
|
+
territory?: Territory;
|
|
449
471
|
commands: GameContextCommands;
|
|
450
472
|
events: GameEvents;
|
|
451
473
|
audio: GameAudio;
|
|
@@ -523,8 +545,12 @@ export interface GameContext {
|
|
|
523
545
|
weapon: WeaponStats;
|
|
524
546
|
};
|
|
525
547
|
time: SimClock;
|
|
548
|
+
/** The simulation stepper (fixed or variable) every driver advances through, plus render-side pose interpolation and per-step stages. */
|
|
549
|
+
sim: SimContext;
|
|
526
550
|
/** Runtime camera-follow/cinematic override (#196.2); the shell reads `followedEntityId()` each frame. */
|
|
527
551
|
camera: CameraDirector;
|
|
552
|
+
/** Particle VFX seam (#1659): queue bursts / standing emitters as data; the shell renders them quality-capped. */
|
|
553
|
+
particles: ParticleDirector;
|
|
528
554
|
/** Held-input snapshot published by the shell each frame before `onTick` (#164.1). */
|
|
529
555
|
input: InputSnapshot;
|
|
530
556
|
subscribe(listener: () => void): () => void;
|