@jgengine/core 0.18.1 → 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 +158 -7
- package/dist/ai/decisionGraph.d.ts +48 -4
- package/dist/ai/decisionGraph.js +153 -23
- package/dist/ai/groupAssist.d.ts +10 -0
- package/dist/ai/groupAssist.js +11 -0
- package/dist/ai/pursuit.d.ts +1 -1
- package/dist/ai/pursuit.js +1 -1
- package/dist/ai/threat.d.ts +8 -0
- package/dist/ai/threat.js +10 -0
- package/dist/anim/animGraph.d.ts +11 -0
- package/dist/anim/animGraph.js +128 -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/locomotionGraph.d.ts +9 -0
- package/dist/anim/locomotionGraph.js +29 -0
- package/dist/audio/audioFalloff.d.ts +2 -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/editor/document.js +3 -1
- package/dist/editor/environment.js +8 -0
- package/dist/editor/types.d.ts +4 -0
- package/dist/faction/reputation.d.ts +6 -0
- package/dist/faction/reputation.js +11 -0
- package/dist/game/cameraConfig.d.ts +51 -1
- package/dist/game/events.d.ts +6 -0
- package/dist/game/lootTable.d.ts +4 -0
- package/dist/game/lootTable.js +16 -0
- package/dist/game/playableGame.d.ts +84 -17
- 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 +15 -1
- package/dist/input/actionBindings.js +14 -3
- package/dist/input/actionContexts.d.ts +25 -1
- package/dist/input/actionContexts.js +82 -25
- 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 +36 -2
- package/dist/input/gamepadModel.js +69 -30
- 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 +112 -1
- 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 +21 -2
- package/dist/movement/playerMovement.js +89 -6
- package/dist/movement/solidObstacles.d.ts +9 -1
- package/dist/movement/solidObstacles.js +44 -2
- package/dist/movement/voxelController.js +7 -7
- package/dist/nav/navFromEnvironment.d.ts +9 -3
- package/dist/nav/navFromEnvironment.js +18 -12
- package/dist/nav/navMesh.d.ts +43 -2
- package/dist/nav/navMesh.js +534 -128
- 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 +21 -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 +32 -2
- package/dist/physics/kinematicVehicle.js +15 -0
- package/dist/physics/physicsBackend.d.ts +12 -0
- package/dist/physics/physicsBackendConformance.js +34 -0
- package/dist/physics/physicsWorldBackend.d.ts +1 -0
- package/dist/physics/physicsWorldBackend.js +33 -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 +1 -1
- package/dist/physics/worldColliders.js +35 -3
- package/dist/puzzle/cellGrid.d.ts +15 -9
- package/dist/puzzle/cellGrid.js +13 -9
- package/dist/render/lookPreset.d.ts +9 -2
- package/dist/render/lookPreset.js +29 -2
- package/dist/render/postProcessing.d.ts +30 -0
- package/dist/render/postProcessing.js +1 -1
- package/dist/runtime/cameraDirector.d.ts +17 -3
- package/dist/runtime/cameraDirector.js +25 -0
- package/dist/runtime/commandInput.d.ts +37 -0
- package/dist/runtime/commandInput.js +58 -0
- package/dist/runtime/commandRunner.d.ts +27 -0
- package/dist/runtime/commandRunner.js +31 -3
- 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 +7 -0
- package/dist/runtime/gameContextTypes.d.ts +19 -3
- package/dist/runtime/gameRuntime.d.ts +3 -1
- package/dist/runtime/gameRuntime.js +4 -0
- package/dist/runtime/inputSnapshot.d.ts +6 -0
- package/dist/runtime/inputSnapshot.js +10 -0
- package/dist/runtime/localPlayers.d.ts +73 -0
- package/dist/runtime/localPlayers.js +119 -0
- package/dist/runtime/transport.d.ts +9 -0
- package/dist/runtime/transport.js +7 -1
- package/dist/scene/behaviorRuntime.d.ts +12 -2
- package/dist/scene/behaviorRuntime.js +30 -14
- package/dist/scene/behaviors.d.ts +5 -1
- 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 +11 -1
- package/dist/sensor/perception.js +57 -10
- 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 +2 -0
- package/dist/settings/graphicsProfile.js +12 -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/vfx/feedbackMixer.d.ts +61 -0
- package/dist/vfx/feedbackMixer.js +106 -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 +34 -2
- package/dist/world/buildings.js +11 -1
- package/dist/world/cityKind.d.ts +7 -0
- package/dist/world/cityKind.js +52 -1
- package/dist/world/features.d.ts +29 -2
- package/dist/world/features.js +6 -0
- 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/terraform.d.ts +8 -0
- package/dist/world/terraform.js +7 -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 +2 -2
- package/dist/world.js +1 -1
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/game-design/SKILL.md +2 -2
- package/skills/jgengine/SKILL.md +7 -5
- package/skills/jgengine/api.md +118 -89
- package/skills/jgengine/capabilities.md +28 -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/api.md +462 -436
- package/skills/jgengine-gameplay/capabilities.md +24 -0
- package/skills/jgengine-gameplay/reference-systems.md +4 -0
- package/skills/jgengine-world/SKILL.md +15 -3
- package/skills/jgengine-world/api.md +873 -626
- package/skills/jgengine-world/capabilities.md +130 -6
- 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
|
@@ -7,7 +7,7 @@ export interface WorldColliderSync {
|
|
|
7
7
|
/** Remove all bodies owned by this synchronizer and stop reacting to scene changes. */
|
|
8
8
|
dispose(): void;
|
|
9
9
|
}
|
|
10
|
-
/** Mirrors static scene-object physical colliders and the context's ground field into a physics backend.
|
|
10
|
+
/** Mirrors static scene-object physical colliders, `ctx.world.solids` and the context's ground field into a physics backend.
|
|
11
11
|
* @capability world-physics-colliders Synchronize terrain and authored static colliders with a physics backend.
|
|
12
12
|
*/
|
|
13
13
|
export declare function syncWorldColliders(backend: PhysicsBackend, ctx: GameContext): WorldColliderSync;
|
|
@@ -10,11 +10,13 @@ function bodyShape(shape) {
|
|
|
10
10
|
function bodyPosition(shape, position, rotationY) {
|
|
11
11
|
return colliderWorldCenter({ name: "body", purpose: "physical", shape, damageEligible: false, blocks: true }, position, rotationY);
|
|
12
12
|
}
|
|
13
|
-
/** Mirrors static scene-object physical colliders and the context's ground field into a physics backend.
|
|
13
|
+
/** Mirrors static scene-object physical colliders, `ctx.world.solids` and the context's ground field into a physics backend.
|
|
14
14
|
* @capability world-physics-colliders Synchronize terrain and authored static colliders with a physics backend.
|
|
15
15
|
*/
|
|
16
16
|
export function syncWorldColliders(backend, ctx) {
|
|
17
17
|
const objectBodies = new Map();
|
|
18
|
+
const solidBodies = [];
|
|
19
|
+
let solidsVersion = -1;
|
|
18
20
|
let terrainBody = null;
|
|
19
21
|
let disposed = false;
|
|
20
22
|
function rebuildTerrain() {
|
|
@@ -62,10 +64,32 @@ export function syncWorldColliders(backend, ctx) {
|
|
|
62
64
|
});
|
|
63
65
|
objectBodies.set(objectId, handle);
|
|
64
66
|
}
|
|
67
|
+
function syncSolids() {
|
|
68
|
+
const solids = ctx.world.solids;
|
|
69
|
+
if (solids.version() === solidsVersion)
|
|
70
|
+
return;
|
|
71
|
+
solidsVersion = solids.version();
|
|
72
|
+
for (const handle of solidBodies)
|
|
73
|
+
backend.removeBody(handle);
|
|
74
|
+
solidBodies.length = 0;
|
|
75
|
+
for (const layer of solids.layers()) {
|
|
76
|
+
for (const solid of solids.layer(layer)) {
|
|
77
|
+
const yaw = solid.rotationY ?? 0;
|
|
78
|
+
solidBodies.push(backend.addBody({
|
|
79
|
+
shape: { kind: "box", halfExtents: [solid.halfExtents[0], solid.halfExtents[1], solid.halfExtents[2]] },
|
|
80
|
+
position: [solid.center[0], solid.center[1], solid.center[2]],
|
|
81
|
+
rotation: [0, Math.sin(yaw / 2), 0, Math.cos(yaw / 2)],
|
|
82
|
+
kind: "static",
|
|
83
|
+
userData: { kind: "solid", layer },
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
65
88
|
function sync() {
|
|
66
89
|
if (disposed)
|
|
67
90
|
return;
|
|
68
91
|
rebuildTerrain();
|
|
92
|
+
syncSolids();
|
|
69
93
|
const ids = new Set(ctx.scene.object.ids());
|
|
70
94
|
for (const [objectId, handle] of objectBodies) {
|
|
71
95
|
if (!ids.has(objectId)) {
|
|
@@ -76,7 +100,11 @@ export function syncWorldColliders(backend, ctx) {
|
|
|
76
100
|
for (const objectId of ids)
|
|
77
101
|
syncObject(objectId);
|
|
78
102
|
}
|
|
79
|
-
const
|
|
103
|
+
const unsubscribeObjects = ctx.scene.object.subscribe(sync);
|
|
104
|
+
const unsubscribeSolids = ctx.world.solids.subscribe(() => {
|
|
105
|
+
if (!disposed)
|
|
106
|
+
syncSolids();
|
|
107
|
+
});
|
|
80
108
|
sync();
|
|
81
109
|
return {
|
|
82
110
|
sync,
|
|
@@ -84,7 +112,11 @@ export function syncWorldColliders(backend, ctx) {
|
|
|
84
112
|
if (disposed)
|
|
85
113
|
return;
|
|
86
114
|
disposed = true;
|
|
87
|
-
|
|
115
|
+
unsubscribeObjects();
|
|
116
|
+
unsubscribeSolids();
|
|
117
|
+
for (const handle of solidBodies)
|
|
118
|
+
backend.removeBody(handle);
|
|
119
|
+
solidBodies.length = 0;
|
|
88
120
|
if (terrainBody !== null)
|
|
89
121
|
backend.removeBody(terrainBody);
|
|
90
122
|
terrainBody = null;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
/** A fixed-size 2D board of cells, row-major from the top-left; `null` is an empty cell. Plain data, so it serializes and restores as-is. */
|
|
1
2
|
export interface CellGrid<T> {
|
|
2
3
|
readonly width: number;
|
|
3
4
|
readonly height: number;
|
|
4
5
|
readonly cells: readonly (T | null)[];
|
|
5
6
|
}
|
|
7
|
+
/** A straight line of matching cells found by {@link findRuns}. */
|
|
6
8
|
export interface CellRun<T> {
|
|
7
9
|
readonly value: T;
|
|
8
10
|
readonly cells: readonly {
|
|
@@ -11,25 +13,29 @@ export interface CellRun<T> {
|
|
|
11
13
|
}[];
|
|
12
14
|
readonly direction: "row" | "column";
|
|
13
15
|
}
|
|
14
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* An empty `width` × `height` board. Every helper here returns a new grid, so a store slot can hold it
|
|
18
|
+
* and undo, replay, and save/restore work without copying.
|
|
19
|
+
* @capability cell-grid immutable 2D board of cells with row clears, gravity collapse, and match runs
|
|
20
|
+
*/
|
|
15
21
|
export declare function createCellGrid<T>(width: number, height: number): CellGrid<T>;
|
|
16
|
-
/**
|
|
22
|
+
/** Whether `(x, y)` lies on the board. */
|
|
17
23
|
export declare function inGridBounds<T>(grid: CellGrid<T>, x: number, y: number): boolean;
|
|
18
|
-
/**
|
|
24
|
+
/** The value at `(x, y)`, or `null` when empty or off the board. */
|
|
19
25
|
export declare function cellAt<T>(grid: CellGrid<T>, x: number, y: number): T | null;
|
|
20
|
-
/**
|
|
26
|
+
/** A copy with `(x, y)` set to `value`; off-board writes return the grid unchanged. */
|
|
21
27
|
export declare function withCell<T>(grid: CellGrid<T>, x: number, y: number, value: T | null): CellGrid<T>;
|
|
22
|
-
/**
|
|
28
|
+
/** A copy with every in-bounds entry written; one allocation for a whole piece or stamp. */
|
|
23
29
|
export declare function withCells<T>(grid: CellGrid<T>, entries: readonly {
|
|
24
30
|
readonly x: number;
|
|
25
31
|
readonly y: number;
|
|
26
32
|
readonly value: T | null;
|
|
27
33
|
}[]): CellGrid<T>;
|
|
28
|
-
/**
|
|
34
|
+
/** Indices of rows with no empty cell, top to bottom. */
|
|
29
35
|
export declare function fullRows<T>(grid: CellGrid<T>): number[];
|
|
30
|
-
/**
|
|
36
|
+
/** A copy with `rows` removed and the rows above shifted down, refilling empty rows at the top. */
|
|
31
37
|
export declare function clearRows<T>(grid: CellGrid<T>, rows: readonly number[]): CellGrid<T>;
|
|
32
|
-
/**
|
|
38
|
+
/** A copy where every column's cells fall to the bottom, keeping their order. */
|
|
33
39
|
export declare function collapseColumns<T>(grid: CellGrid<T>): CellGrid<T>;
|
|
34
|
-
/**
|
|
40
|
+
/** Horizontal and vertical runs of at least `minLength` cells whose values `matches` (default `===`). */
|
|
35
41
|
export declare function findRuns<T>(grid: CellGrid<T>, minLength: number, matches?: (a: T, b: T) => boolean): CellRun<T>[];
|
package/dist/puzzle/cellGrid.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* An empty `width` × `height` board. Every helper here returns a new grid, so a store slot can hold it
|
|
3
|
+
* and undo, replay, and save/restore work without copying.
|
|
4
|
+
* @capability cell-grid immutable 2D board of cells with row clears, gravity collapse, and match runs
|
|
5
|
+
*/
|
|
2
6
|
export function createCellGrid(width, height) {
|
|
3
7
|
return { width, height, cells: new Array(width * height).fill(null) };
|
|
4
8
|
}
|
|
5
|
-
/**
|
|
9
|
+
/** Whether `(x, y)` lies on the board. */
|
|
6
10
|
export function inGridBounds(grid, x, y) {
|
|
7
11
|
return x >= 0 && x < grid.width && y >= 0 && y < grid.height;
|
|
8
12
|
}
|
|
9
|
-
/**
|
|
13
|
+
/** The value at `(x, y)`, or `null` when empty or off the board. */
|
|
10
14
|
export function cellAt(grid, x, y) {
|
|
11
15
|
if (!inGridBounds(grid, x, y))
|
|
12
16
|
return null;
|
|
13
17
|
return grid.cells[y * grid.width + x] ?? null;
|
|
14
18
|
}
|
|
15
|
-
/**
|
|
19
|
+
/** A copy with `(x, y)` set to `value`; off-board writes return the grid unchanged. */
|
|
16
20
|
export function withCell(grid, x, y, value) {
|
|
17
21
|
if (!inGridBounds(grid, x, y))
|
|
18
22
|
return grid;
|
|
@@ -20,7 +24,7 @@ export function withCell(grid, x, y, value) {
|
|
|
20
24
|
cells[y * grid.width + x] = value;
|
|
21
25
|
return { width: grid.width, height: grid.height, cells };
|
|
22
26
|
}
|
|
23
|
-
/**
|
|
27
|
+
/** A copy with every in-bounds entry written; one allocation for a whole piece or stamp. */
|
|
24
28
|
export function withCells(grid, entries) {
|
|
25
29
|
const cells = grid.cells.slice();
|
|
26
30
|
for (const entry of entries) {
|
|
@@ -29,7 +33,7 @@ export function withCells(grid, entries) {
|
|
|
29
33
|
}
|
|
30
34
|
return { width: grid.width, height: grid.height, cells };
|
|
31
35
|
}
|
|
32
|
-
/**
|
|
36
|
+
/** Indices of rows with no empty cell, top to bottom. */
|
|
33
37
|
export function fullRows(grid) {
|
|
34
38
|
const rows = [];
|
|
35
39
|
for (let y = 0; y < grid.height; y += 1) {
|
|
@@ -45,7 +49,7 @@ export function fullRows(grid) {
|
|
|
45
49
|
}
|
|
46
50
|
return rows;
|
|
47
51
|
}
|
|
48
|
-
/**
|
|
52
|
+
/** A copy with `rows` removed and the rows above shifted down, refilling empty rows at the top. */
|
|
49
53
|
export function clearRows(grid, rows) {
|
|
50
54
|
const removed = new Set(rows);
|
|
51
55
|
const kept = [];
|
|
@@ -62,7 +66,7 @@ export function clearRows(grid, rows) {
|
|
|
62
66
|
cells.push(...row);
|
|
63
67
|
return { width: grid.width, height: grid.height, cells };
|
|
64
68
|
}
|
|
65
|
-
/**
|
|
69
|
+
/** A copy where every column's cells fall to the bottom, keeping their order. */
|
|
66
70
|
export function collapseColumns(grid) {
|
|
67
71
|
const cells = grid.cells.slice();
|
|
68
72
|
for (let x = 0; x < grid.width; x += 1) {
|
|
@@ -96,7 +100,7 @@ function scanRuns(length, cellAtIndex, minLength, matches) {
|
|
|
96
100
|
}
|
|
97
101
|
return runs;
|
|
98
102
|
}
|
|
99
|
-
/**
|
|
103
|
+
/** Horizontal and vertical runs of at least `minLength` cells whose values `matches` (default `===`). */
|
|
100
104
|
export function findRuns(grid, minLength, matches = (a, b) => a === b) {
|
|
101
105
|
const runs = [];
|
|
102
106
|
for (let y = 0; y < grid.height; y += 1) {
|
|
@@ -6,12 +6,15 @@ import type { PostProcessingConfig } from "./postProcessing.js";
|
|
|
6
6
|
* `"neutral"` (the default when unset) keeps presentation unstyled; `"cinematic"` draws a scene lit like a shipped game — a real day sky
|
|
7
7
|
* with a view-following shadow-casting sun + hemisphere fill, a network-free image-based-lighting
|
|
8
8
|
* environment so PBR surfaces catch soft reflections, and a tuned tone-map/bloom/AO/vignette post
|
|
9
|
-
* stack. `"
|
|
9
|
+
* stack. `"comic"` adds ink outlines and cel bands over a saturated grade (Borderlands-style);
|
|
10
|
+
* `"retro"` pixelates and posterizes the frame. `"flat"` opts out of the sky/IBL/post rig to the bare ambient+directional default (pre-#773).
|
|
10
11
|
* The upgraded default primitive materials — tuned roughness/metalness plus subtle procedural surface
|
|
11
12
|
* detail so un-modeled boxes/capsules stop reading as flat plastic — apply under both presets.
|
|
12
13
|
* @capability default-look one field that lights a scene like a shipped game (opt out with "flat")
|
|
13
14
|
*/
|
|
14
|
-
export type LookPreset = "neutral" | "photoreal" | "toon" | "cinematic" | "flat";
|
|
15
|
+
export type LookPreset = "neutral" | "photoreal" | "toon" | "comic" | "retro" | "cinematic" | "flat";
|
|
16
|
+
/** Every look preset name, for pickers and validating a preset read from a URL or file. */
|
|
17
|
+
export declare const LOOK_PRESETS: readonly LookPreset[];
|
|
15
18
|
/** Explicit graphics knobs a game may set; the preset only fills the ones left undefined. */
|
|
16
19
|
export interface GameLookInput {
|
|
17
20
|
look?: LookPreset;
|
|
@@ -37,6 +40,10 @@ export declare const NEUTRAL_POST_PROCESSING: PostProcessingConfig;
|
|
|
37
40
|
export declare const PHOTOREAL_POST_PROCESSING: PostProcessingConfig;
|
|
38
41
|
/** Clean graphic presentation; toon materials remain game-owned while this sets the grade. */
|
|
39
42
|
export declare const TOON_POST_PROCESSING: PostProcessingConfig;
|
|
43
|
+
/** Ink outlines, four cel bands and a punchy grade — a hand-inked comic look on any model. */
|
|
44
|
+
export declare const COMIC_POST_PROCESSING: PostProcessingConfig;
|
|
45
|
+
/** Chunky pixels and posterized colour with no smoothing — a low-resolution retro look. */
|
|
46
|
+
export declare const RETRO_POST_PROCESSING: PostProcessingConfig;
|
|
40
47
|
/**
|
|
41
48
|
* Expand a game's `look` into concrete lighting/backdrop/post. The default is `"neutral"`;
|
|
42
49
|
* `"flat"` passes the explicit knobs through untouched.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** Every look preset name, for pickers and validating a preset read from a URL or file. */
|
|
2
|
+
export const LOOK_PRESETS = ["neutral", "photoreal", "cinematic", "toon", "comic", "retro", "flat"];
|
|
1
3
|
/** A static day sky: a real dome plus the shell's view-following shadow-casting sun and hemisphere fill. */
|
|
2
4
|
export const CINEMATIC_SKY = { preset: "day" };
|
|
3
5
|
/** Tuned tone-map + bloom + gentle SSAO + vignette/grade stack — the shipped-game post look. */
|
|
@@ -28,6 +30,32 @@ export const TOON_POST_PROCESSING = {
|
|
|
28
30
|
ao: { intensity: 0.8, radius: 1.2 },
|
|
29
31
|
grade: { saturation: 1.18, gamma: 1, vignette: 0, grain: 0 },
|
|
30
32
|
};
|
|
33
|
+
/** Ink outlines, four cel bands and a punchy grade — a hand-inked comic look on any model. */
|
|
34
|
+
export const COMIC_POST_PROCESSING = {
|
|
35
|
+
toneMapping: "aces",
|
|
36
|
+
exposure: 1.05,
|
|
37
|
+
aa: "msaa",
|
|
38
|
+
bloom: { strength: 0.12, radius: 0.4, threshold: 1 },
|
|
39
|
+
ao: { intensity: 1, radius: 1.2 },
|
|
40
|
+
stylize: { outline: { thickness: 1.5 }, bands: 4 },
|
|
41
|
+
grade: { saturation: 1.3, gamma: 0.94, vignette: 0.15, grain: 0 },
|
|
42
|
+
};
|
|
43
|
+
/** Chunky pixels and posterized colour with no smoothing — a low-resolution retro look. */
|
|
44
|
+
export const RETRO_POST_PROCESSING = {
|
|
45
|
+
toneMapping: "aces",
|
|
46
|
+
aa: false,
|
|
47
|
+
bloom: false,
|
|
48
|
+
ao: false,
|
|
49
|
+
stylize: { pixelSize: 3, bands: 6 },
|
|
50
|
+
grade: { saturation: 1.15, gamma: 1, vignette: 0, grain: 0 },
|
|
51
|
+
};
|
|
52
|
+
const PRESET_POST = {
|
|
53
|
+
photoreal: PHOTOREAL_POST_PROCESSING,
|
|
54
|
+
toon: TOON_POST_PROCESSING,
|
|
55
|
+
comic: COMIC_POST_PROCESSING,
|
|
56
|
+
retro: RETRO_POST_PROCESSING,
|
|
57
|
+
cinematic: CINEMATIC_POST_PROCESSING,
|
|
58
|
+
};
|
|
31
59
|
/**
|
|
32
60
|
* Expand a game's `look` into concrete lighting/backdrop/post. The default is `"neutral"`;
|
|
33
61
|
* `"flat"` passes the explicit knobs through untouched.
|
|
@@ -48,7 +76,6 @@ export function resolveGameLook(input) {
|
|
|
48
76
|
return {
|
|
49
77
|
lighting: input.lighting,
|
|
50
78
|
backdrop,
|
|
51
|
-
postProcessing: input.postProcessing ??
|
|
52
|
-
(look === "toon" ? TOON_POST_PROCESSING : look === "photoreal" ? PHOTOREAL_POST_PROCESSING : CINEMATIC_POST_PROCESSING),
|
|
79
|
+
postProcessing: input.postProcessing ?? PRESET_POST[look],
|
|
53
80
|
};
|
|
54
81
|
}
|
|
@@ -55,6 +55,34 @@ export interface DofConfig {
|
|
|
55
55
|
/** Maximum blur radius, 0..1. Default 0.01. */
|
|
56
56
|
maxBlur?: number;
|
|
57
57
|
}
|
|
58
|
+
/** Ink outlines traced from depth and surface-normal edges — silhouettes plus interior creases. */
|
|
59
|
+
export interface OutlineConfig {
|
|
60
|
+
/** Line colour (CSS). Default near-black "#101014". */
|
|
61
|
+
color?: string;
|
|
62
|
+
/** Line thickness in render pixels. Default 1. */
|
|
63
|
+
thickness?: number;
|
|
64
|
+
/** Line opacity, 0..1. Default 1. */
|
|
65
|
+
opacity?: number;
|
|
66
|
+
/** Relative depth jump that counts as a silhouette edge. Lower draws more lines. Default 0.08. */
|
|
67
|
+
depthThreshold?: number;
|
|
68
|
+
/** Normal difference (1 − cos angle) that counts as a crease. Lower draws more lines. Default 0.35. */
|
|
69
|
+
normalThreshold?: number;
|
|
70
|
+
/** Distance in world units at which lines have faded out, so far scenery stays clean. Default 160. */
|
|
71
|
+
fadeDistance?: number;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Art-style stage that turns the same models and lighting into a different look without
|
|
75
|
+
* touching materials: ink outlines (comic / Borderlands), cel bands (cartoon), pixelation (retro).
|
|
76
|
+
* Runs after tone mapping, before the grade.
|
|
77
|
+
*/
|
|
78
|
+
export interface StylizeConfig {
|
|
79
|
+
/** Ink outlines. Omitted or `false` draws none. */
|
|
80
|
+
outline?: OutlineConfig | false;
|
|
81
|
+
/** Quantize brightness into this many cel-shading bands, keeping hue. 0 or omitted is off; 3–6 reads as cartoon. */
|
|
82
|
+
bands?: number;
|
|
83
|
+
/** Snap the frame to blocks of this many render pixels for a retro look. 0, 1 or omitted is off. */
|
|
84
|
+
pixelSize?: number;
|
|
85
|
+
}
|
|
58
86
|
/**
|
|
59
87
|
* Edge antialiasing for the post chain. SMAA runs in linear-sRGB before the output
|
|
60
88
|
* tone-map and cleans alpha-tested foliage/particles that MSAA samples alone leave crawling.
|
|
@@ -86,6 +114,8 @@ export interface PostProcessingConfig {
|
|
|
86
114
|
/** Depth-of-field / bokeh. Heavier stage — omit or `false` to skip. Default off (omitted). */
|
|
87
115
|
dof?: DofConfig | false;
|
|
88
116
|
grade?: GradeConfig | false;
|
|
117
|
+
/** Art-style stage: outlines, cel bands, pixelation. Default off (omitted). */
|
|
118
|
+
stylize?: StylizeConfig | false;
|
|
89
119
|
}
|
|
90
120
|
/**
|
|
91
121
|
* A cinematic "product shot" post preset — the full chain on (contact-AO, soft bloom, a warm film
|
|
@@ -8,7 +8,7 @@ export const STUDIO_STAGE_POST = {
|
|
|
8
8
|
toneMapping: "aces",
|
|
9
9
|
exposure: 1.05,
|
|
10
10
|
bloom: { strength: 0.28, radius: 0.6, threshold: 0.8 },
|
|
11
|
-
ao: { radius: 1.
|
|
11
|
+
ao: { radius: 1.1, intensity: 1.5, distanceFalloff: 3, blend: 1 },
|
|
12
12
|
grade: {
|
|
13
13
|
lift: [0.008, 0.008, 0.014],
|
|
14
14
|
gain: [1.05, 1.02, 0.98],
|
|
@@ -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);
|
|
@@ -13,3 +13,40 @@ export declare function readQuantity(value: unknown, options?: {
|
|
|
13
13
|
min?: number;
|
|
14
14
|
max?: number;
|
|
15
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;
|
|
@@ -11,3 +11,61 @@ export function readQuantity(value, options = {}) {
|
|
|
11
11
|
? { ok: true, quantity: value }
|
|
12
12
|
: { ok: false, reason: "invalid-quantity" };
|
|
13
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
|
+
}
|
|
@@ -16,7 +16,26 @@ export type CommandScope = {
|
|
|
16
16
|
export type CommandScopeDefinition = Omit<CommandScope, "players"> & {
|
|
17
17
|
players?: readonly string[] | "actor";
|
|
18
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";
|
|
19
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;
|
|
20
39
|
/**
|
|
21
40
|
* What this command reads and writes, derived from its own input. A host that hydrates through a
|
|
22
41
|
* scope loads only this slice instead of the whole world, and refuses the command if `apply` then
|
|
@@ -55,3 +74,11 @@ export type RunCommandResult = {
|
|
|
55
74
|
};
|
|
56
75
|
/** @internal */
|
|
57
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,23 @@
|
|
|
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
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
|
-
const
|
|
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: [] };
|
|
8
21
|
const { players, ...rest } = scope;
|
|
9
22
|
return players === undefined ? rest : { ...rest, players: players === "actor" ? [actorUserId] : players };
|
|
10
23
|
}
|
|
@@ -42,11 +55,18 @@ export function runCommand(snapshot, commands, commandName, input, actorUserId,
|
|
|
42
55
|
if (!command) {
|
|
43
56
|
return { ok: false, reason: `Unknown command: ${commandName}` };
|
|
44
57
|
}
|
|
45
|
-
|
|
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);
|
|
46
66
|
if (validationError) {
|
|
47
67
|
return { ok: false, reason: validationError.reason };
|
|
48
68
|
}
|
|
49
|
-
const next = command.apply(snapshot,
|
|
69
|
+
const next = command.apply(snapshot, parsed, actorUserId, nowMs);
|
|
50
70
|
return {
|
|
51
71
|
ok: true,
|
|
52
72
|
snapshot: {
|
|
@@ -62,3 +82,11 @@ export function runCommand(snapshot, commands, commandName, input, actorUserId,
|
|
|
62
82
|
},
|
|
63
83
|
};
|
|
64
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
|