@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
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { WeaponHandlingFrame } from "./weaponHandling.js";
|
|
2
|
+
/** Camera-space position `[x, y, z]` in metres: `+x` right, `+y` up, `-z` forward. */
|
|
3
|
+
export type ViewmodelOffset = readonly [number, number, number];
|
|
4
|
+
/**
|
|
5
|
+
* How one weapon sits in first person. Offsets are camera-space metres, angles are radians, FOVs are
|
|
6
|
+
* degrees. Every field is optional; two weapons differ by the ones they set.
|
|
7
|
+
*/
|
|
8
|
+
export interface WeaponPresentationTuning {
|
|
9
|
+
/** Viewmodel anchor at the hip (default `[0.34, -0.26, -0.72]`). */
|
|
10
|
+
hip?: ViewmodelOffset;
|
|
11
|
+
/** Viewmodel anchor fully aimed; put the sight on the camera axis (default `[0, -0.07, -0.55]`). */
|
|
12
|
+
ads?: ViewmodelOffset;
|
|
13
|
+
/** Viewmodel FOV in degrees, independent of the world FOV; omit to draw the viewmodel at the world FOV. */
|
|
14
|
+
viewmodelFov?: {
|
|
15
|
+
hip?: number;
|
|
16
|
+
ads?: number;
|
|
17
|
+
};
|
|
18
|
+
/** World FOV multiplier at full ADS, e.g. `0.8` for a light zoom (default `1`). */
|
|
19
|
+
adsZoom?: number;
|
|
20
|
+
/** Viewmodel lag behind look motion. */
|
|
21
|
+
sway?: {
|
|
22
|
+
/** Radians of lag per rad/s of look speed (default `0.02`). */
|
|
23
|
+
perRadPerSec?: number;
|
|
24
|
+
/** Largest lag, rad (default `0.08`). */
|
|
25
|
+
max?: number;
|
|
26
|
+
/** How fast lag follows its target, 1/s (default `12`). */
|
|
27
|
+
response?: number;
|
|
28
|
+
/** Lag multiplier at full ADS (default `0.3`). */
|
|
29
|
+
adsScale?: number;
|
|
30
|
+
};
|
|
31
|
+
/** Walk bob driven by the movement probe's speed and `bobPhase`. */
|
|
32
|
+
bob?: {
|
|
33
|
+
/** Side and vertical amplitude in metres at `fullSpeed` (default `[0.012, 0.01]`). */
|
|
34
|
+
amplitude?: readonly [number, number];
|
|
35
|
+
/** Ground speed that gives the full amplitude, m/s (default `4`). */
|
|
36
|
+
fullSpeed?: number;
|
|
37
|
+
/** Bob multiplier at full ADS (default `0.25`). */
|
|
38
|
+
adsScale?: number;
|
|
39
|
+
};
|
|
40
|
+
/** Viewmodel reaction to the handling's recoil offset. */
|
|
41
|
+
kick?: {
|
|
42
|
+
/** Metres pushed back per radian of recoil (default `1.2`). */
|
|
43
|
+
back?: number;
|
|
44
|
+
/** Viewmodel muzzle rise per radian of recoil (default `1.5`). */
|
|
45
|
+
rise?: number;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** One frame of inputs: look motion, the movement probe, and the weapon's handling readout. */
|
|
49
|
+
export interface WeaponPresentationInput {
|
|
50
|
+
/** Look yaw speed, rad/s, positive turning left (the same sign as a camera yaw that grows left). */
|
|
51
|
+
lookYawRate: number;
|
|
52
|
+
/** Look pitch speed, rad/s, positive looking up. */
|
|
53
|
+
lookPitchRate: number;
|
|
54
|
+
/** Horizontal ground speed, m/s (e.g. `EntityRenderCues.speed`). */
|
|
55
|
+
speed: number;
|
|
56
|
+
/** `0..1` walk-cycle phase (e.g. `EntityRenderCues.bobPhase`). */
|
|
57
|
+
bobPhase: number;
|
|
58
|
+
handling: WeaponHandlingFrame;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Where the viewmodel and camera go this frame. First person applies all of it; third person and
|
|
62
|
+
* over-the-shoulder read `lookPitch`/`lookYaw`, `aimPitch`/`aimYaw` and `adsProgress` from the same frame.
|
|
63
|
+
*/
|
|
64
|
+
export interface WeaponPose {
|
|
65
|
+
/** Viewmodel anchor, camera-space metres. */
|
|
66
|
+
offset: [number, number, number];
|
|
67
|
+
/** Viewmodel rotation about its anchor, rad: `pitch > 0` muzzle up, `yaw > 0` muzzle left, `roll > 0` tilts left. */
|
|
68
|
+
pitch: number;
|
|
69
|
+
yaw: number;
|
|
70
|
+
roll: number;
|
|
71
|
+
/** Viewmodel FOV in degrees, or `null` to use the world FOV. */
|
|
72
|
+
viewmodelFov: number | null;
|
|
73
|
+
/** Multiplier for the world FOV (ADS zoom). */
|
|
74
|
+
fovScale: number;
|
|
75
|
+
/** Offset to add to the camera look, rad: aim kick plus view punch. `lookYaw > 0` pulls right. */
|
|
76
|
+
lookPitch: number;
|
|
77
|
+
lookYaw: number;
|
|
78
|
+
/** Offset shots follow, rad; a third-person rig or upper-body aim reads this. */
|
|
79
|
+
aimPitch: number;
|
|
80
|
+
aimYaw: number;
|
|
81
|
+
/** `0` hip … `1` fully aimed. */
|
|
82
|
+
adsProgress: number;
|
|
83
|
+
}
|
|
84
|
+
/** Serializable presentation state: the current sway lag. */
|
|
85
|
+
export interface WeaponPresentationState {
|
|
86
|
+
swayPitch: number;
|
|
87
|
+
swayYaw: number;
|
|
88
|
+
}
|
|
89
|
+
/** Turns a {@link WeaponHandlingFrame} plus look and movement into a viewmodel and camera pose. */
|
|
90
|
+
export interface WeaponPresentation {
|
|
91
|
+
/** Advance sway by `dt` and return the pose. The returned object is reused between calls. */
|
|
92
|
+
update(dt: number, input: WeaponPresentationInput): WeaponPose;
|
|
93
|
+
pose(): WeaponPose;
|
|
94
|
+
retune(next: WeaponPresentationTuning): void;
|
|
95
|
+
snapshot(): WeaponPresentationState;
|
|
96
|
+
restore(state: WeaponPresentationState): void;
|
|
97
|
+
reset(): void;
|
|
98
|
+
}
|
|
99
|
+
/** Default hip anchor, matching the shell's built-in viewmodel. */
|
|
100
|
+
export declare const DEFAULT_VIEWMODEL_HIP: ViewmodelOffset;
|
|
101
|
+
/** Default ADS anchor: the built-in viewmodel's top rail on the camera axis. */
|
|
102
|
+
export declare const DEFAULT_VIEWMODEL_ADS: ViewmodelOffset;
|
|
103
|
+
/**
|
|
104
|
+
* Creates a {@link WeaponPresentation}: per-weapon viewmodel offset, ADS pose and viewmodel FOV, sway
|
|
105
|
+
* and inertia from look speed, bob from the movement probe, and a recoil kick, all driven by the same
|
|
106
|
+
* handling frame third-person and over-the-shoulder rigs read. Deterministic and allocation-free per update.
|
|
107
|
+
* @capability weapon-presentation per-weapon viewmodel offset, ADS pose, viewmodel FOV, sway, bob and recoil kick from a weapon handling frame
|
|
108
|
+
*/
|
|
109
|
+
export declare function createWeaponPresentation(initial?: WeaponPresentationTuning): WeaponPresentation;
|
|
110
|
+
/**
|
|
111
|
+
* Scale for a viewmodel drawn at `viewmodelFov` through a camera at `worldFov` (both degrees): multiply
|
|
112
|
+
* the viewmodel's camera-space x and y by this and it projects as if rendered with its own FOV, without
|
|
113
|
+
* a second camera or render pass.
|
|
114
|
+
*/
|
|
115
|
+
export declare function viewmodelFovScale(worldFov: number, viewmodelFov: number): number;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Default hip anchor, matching the shell's built-in viewmodel. */
|
|
2
|
+
export const DEFAULT_VIEWMODEL_HIP = [0.34, -0.26, -0.72];
|
|
3
|
+
/** Default ADS anchor: the built-in viewmodel's top rail on the camera axis. */
|
|
4
|
+
export const DEFAULT_VIEWMODEL_ADS = [0, -0.07, -0.55];
|
|
5
|
+
const TAU = Math.PI * 2;
|
|
6
|
+
const DEFAULT_BOB = [0.012, 0.01];
|
|
7
|
+
function lerp(hip, aimed, ads) {
|
|
8
|
+
return hip * (1 - ads) + aimed * ads;
|
|
9
|
+
}
|
|
10
|
+
function clamp(value, limit) {
|
|
11
|
+
return Math.max(-limit, Math.min(limit, value));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Creates a {@link WeaponPresentation}: per-weapon viewmodel offset, ADS pose and viewmodel FOV, sway
|
|
15
|
+
* and inertia from look speed, bob from the movement probe, and a recoil kick, all driven by the same
|
|
16
|
+
* handling frame third-person and over-the-shoulder rigs read. Deterministic and allocation-free per update.
|
|
17
|
+
* @capability weapon-presentation per-weapon viewmodel offset, ADS pose, viewmodel FOV, sway, bob and recoil kick from a weapon handling frame
|
|
18
|
+
*/
|
|
19
|
+
export function createWeaponPresentation(initial = {}) {
|
|
20
|
+
let tuning = initial;
|
|
21
|
+
const state = { swayPitch: 0, swayYaw: 0 };
|
|
22
|
+
const out = {
|
|
23
|
+
offset: [...(tuning.hip ?? DEFAULT_VIEWMODEL_HIP)],
|
|
24
|
+
pitch: 0,
|
|
25
|
+
yaw: 0,
|
|
26
|
+
roll: 0,
|
|
27
|
+
viewmodelFov: tuning.viewmodelFov?.hip ?? null,
|
|
28
|
+
fovScale: 1,
|
|
29
|
+
lookPitch: 0,
|
|
30
|
+
lookYaw: 0,
|
|
31
|
+
aimPitch: 0,
|
|
32
|
+
aimYaw: 0,
|
|
33
|
+
adsProgress: 0,
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
update(dt, input) {
|
|
37
|
+
const step = Math.max(0, dt);
|
|
38
|
+
const handling = input.handling;
|
|
39
|
+
const ads = Math.max(0, Math.min(1, handling.adsProgress));
|
|
40
|
+
const sway = tuning.sway;
|
|
41
|
+
const swayMax = sway?.max ?? 0.08;
|
|
42
|
+
const swayScale = lerp(1, sway?.adsScale ?? 0.3, ads);
|
|
43
|
+
const per = sway?.perRadPerSec ?? 0.02;
|
|
44
|
+
const targetYaw = clamp(-input.lookYawRate * per, swayMax) * swayScale;
|
|
45
|
+
const targetPitch = clamp(-input.lookPitchRate * per, swayMax) * swayScale;
|
|
46
|
+
const follow = 1 - Math.exp(-(sway?.response ?? 12) * step);
|
|
47
|
+
state.swayYaw += (targetYaw - state.swayYaw) * follow;
|
|
48
|
+
state.swayPitch += (targetPitch - state.swayPitch) * follow;
|
|
49
|
+
const bob = tuning.bob;
|
|
50
|
+
const [bobX, bobY] = bob?.amplitude ?? DEFAULT_BOB;
|
|
51
|
+
const bobScale = Math.min(1, Math.max(0, input.speed) / Math.max(1e-6, bob?.fullSpeed ?? 4)) * lerp(1, bob?.adsScale ?? 0.25, ads);
|
|
52
|
+
const phase = input.bobPhase * TAU;
|
|
53
|
+
const recoil = Math.hypot(handling.aimPitch + handling.cameraPitch, handling.aimYaw + handling.cameraYaw);
|
|
54
|
+
const hip = tuning.hip ?? DEFAULT_VIEWMODEL_HIP;
|
|
55
|
+
const aimed = tuning.ads ?? DEFAULT_VIEWMODEL_ADS;
|
|
56
|
+
out.offset[0] = lerp(hip[0], aimed[0], ads) + Math.sin(phase) * bobX * bobScale;
|
|
57
|
+
out.offset[1] = lerp(hip[1], aimed[1], ads) - Math.abs(Math.sin(phase)) * bobY * bobScale;
|
|
58
|
+
out.offset[2] = lerp(hip[2], aimed[2], ads) + recoil * (tuning.kick?.back ?? 1.2);
|
|
59
|
+
out.pitch = state.swayPitch + recoil * (tuning.kick?.rise ?? 1.5);
|
|
60
|
+
out.yaw = state.swayYaw;
|
|
61
|
+
out.roll = state.swayYaw * 0.5;
|
|
62
|
+
const hipFov = tuning.viewmodelFov?.hip ?? tuning.viewmodelFov?.ads;
|
|
63
|
+
const adsFov = tuning.viewmodelFov?.ads ?? hipFov;
|
|
64
|
+
out.viewmodelFov = hipFov === undefined || adsFov === undefined ? null : lerp(hipFov, adsFov, ads);
|
|
65
|
+
out.fovScale = lerp(1, tuning.adsZoom ?? 1, ads);
|
|
66
|
+
out.lookPitch = handling.aimPitch + handling.cameraPitch;
|
|
67
|
+
out.lookYaw = handling.aimYaw + handling.cameraYaw;
|
|
68
|
+
out.aimPitch = handling.aimPitch;
|
|
69
|
+
out.aimYaw = handling.aimYaw;
|
|
70
|
+
out.adsProgress = ads;
|
|
71
|
+
return out;
|
|
72
|
+
},
|
|
73
|
+
pose: () => out,
|
|
74
|
+
retune(next) {
|
|
75
|
+
tuning = next;
|
|
76
|
+
},
|
|
77
|
+
snapshot: () => ({ ...state }),
|
|
78
|
+
restore(next) {
|
|
79
|
+
state.swayPitch = next.swayPitch;
|
|
80
|
+
state.swayYaw = next.swayYaw;
|
|
81
|
+
},
|
|
82
|
+
reset() {
|
|
83
|
+
state.swayPitch = 0;
|
|
84
|
+
state.swayYaw = 0;
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Scale for a viewmodel drawn at `viewmodelFov` through a camera at `worldFov` (both degrees): multiply
|
|
90
|
+
* the viewmodel's camera-space x and y by this and it projects as if rendered with its own FOV, without
|
|
91
|
+
* a second camera or render pass.
|
|
92
|
+
*/
|
|
93
|
+
export function viewmodelFovScale(worldFov, viewmodelFov) {
|
|
94
|
+
const toHalf = (deg) => Math.tan((Math.max(1, Math.min(179, deg)) * Math.PI) / 360);
|
|
95
|
+
return toHalf(worldFov) / toHalf(viewmodelFov);
|
|
96
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logs a dev-mode `console.warn` once per `site`, and never in a production build. Reach for it
|
|
3
|
+
* where a permissive config would otherwise fail silently — a clipped sky dome, an ignored tint.
|
|
4
|
+
* @capability warn-once emit a dev-only warning a single time per call site
|
|
5
|
+
*/
|
|
6
|
+
export declare function warnOnce(site: string, message: string): void;
|
|
7
|
+
/** Clears the once-per-site record so a test can observe the warning again. @internal */
|
|
8
|
+
export declare function resetWarnOnce(): void;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { isProductionEnvironment } from "./saveEndpoint.js";
|
|
2
|
+
const warned = new Set();
|
|
3
|
+
/**
|
|
4
|
+
* Logs a dev-mode `console.warn` once per `site`, and never in a production build. Reach for it
|
|
5
|
+
* where a permissive config would otherwise fail silently — a clipped sky dome, an ignored tint.
|
|
6
|
+
* @capability warn-once emit a dev-only warning a single time per call site
|
|
7
|
+
*/
|
|
8
|
+
export function warnOnce(site, message) {
|
|
9
|
+
if (warned.has(site) || isProductionEnvironment())
|
|
10
|
+
return;
|
|
11
|
+
warned.add(site);
|
|
12
|
+
const host = globalThis;
|
|
13
|
+
host.console?.warn?.(message);
|
|
14
|
+
}
|
|
15
|
+
/** Clears the once-per-site record so a test can observe the warning again. @internal */
|
|
16
|
+
export function resetWarnOnce() {
|
|
17
|
+
warned.clear();
|
|
18
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export interface CurrencyDefinition<TCurrencyId extends string = string> {
|
|
2
2
|
id: TCurrencyId;
|
|
3
3
|
name: string;
|
|
4
|
+
/** Fractional digits in major-unit amounts; defaults to 0. */
|
|
5
|
+
decimals?: number;
|
|
4
6
|
/** Prepended when formatting amounts (e.g. "$"). */
|
|
5
7
|
symbol?: string;
|
|
6
8
|
/** Appended when formatting amounts (e.g. "tokens"). */
|
|
@@ -16,7 +18,19 @@ export type CurrencyAdjustment = {
|
|
|
16
18
|
reason: string;
|
|
17
19
|
};
|
|
18
20
|
/** @internal */
|
|
19
|
-
export declare function sanitizeCurrencyAmount(amount: number): number;
|
|
21
|
+
export declare function sanitizeCurrencyAmount(amount: number, currency?: CurrencyDefinition): number;
|
|
22
|
+
/** Convert major units to integer minor units, rounding once at the write boundary.
|
|
23
|
+
* @capability currency-minor-units round decimal currency into safe integer minor units
|
|
24
|
+
*/
|
|
25
|
+
export declare function toMinorUnits(currency: Pick<CurrencyDefinition, "decimals"> | undefined, value: number): number;
|
|
26
|
+
/** Convert stored integer minor units to major units for display or existing balance records.
|
|
27
|
+
* @capability currency-major-units convert safe minor-unit integers into currency amounts
|
|
28
|
+
*/
|
|
29
|
+
export declare function fromMinorUnits(currency: Pick<CurrencyDefinition, "decimals"> | undefined, value: number): number;
|
|
30
|
+
/** Format a major-unit value using the currency's declared precision.
|
|
31
|
+
* @capability currency-precision-format display a currency using its declared decimal precision
|
|
32
|
+
*/
|
|
33
|
+
export declare function formatCurrency(currency: CurrencyDefinition, value: number): string;
|
|
20
34
|
/**
|
|
21
35
|
* Format a currency amount with its symbol and grouping for HUD display.
|
|
22
36
|
*
|
|
@@ -31,7 +45,7 @@ export declare function applyCurrencyOperation(currency: CurrencyDefinition, cur
|
|
|
31
45
|
/** Deducts clamp to the available balance (a delta can never overdraw); adds apply in full.
|
|
32
46
|
* @internal
|
|
33
47
|
*/
|
|
34
|
-
export declare function resolveCurrencyDelta(current: number, delta: number): {
|
|
48
|
+
export declare function resolveCurrencyDelta(current: number, delta: number, currency?: CurrencyDefinition): {
|
|
35
49
|
operation: CurrencyOperation;
|
|
36
50
|
amount: number;
|
|
37
51
|
};
|
package/dist/economy/currency.js
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
/** @internal */
|
|
2
|
-
export function sanitizeCurrencyAmount(amount) {
|
|
3
|
-
|
|
2
|
+
export function sanitizeCurrencyAmount(amount, currency) {
|
|
3
|
+
if (!Number.isFinite(amount))
|
|
4
|
+
throw new RangeError("Currency amount must be finite");
|
|
5
|
+
return fromMinorUnits(currency, toMinorUnits(currency, Math.max(0, amount)));
|
|
6
|
+
}
|
|
7
|
+
function currencyScale(currency) {
|
|
8
|
+
const decimals = currency?.decimals ?? 0;
|
|
9
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 9) {
|
|
10
|
+
throw new RangeError("Currency decimals must be an integer between 0 and 9");
|
|
11
|
+
}
|
|
12
|
+
return 10 ** decimals;
|
|
13
|
+
}
|
|
14
|
+
/** Convert major units to integer minor units, rounding once at the write boundary.
|
|
15
|
+
* @capability currency-minor-units round decimal currency into safe integer minor units
|
|
16
|
+
*/
|
|
17
|
+
export function toMinorUnits(currency, value) {
|
|
18
|
+
if (!Number.isFinite(value))
|
|
19
|
+
throw new RangeError("Currency amount must be finite");
|
|
20
|
+
const scaled = value * currencyScale(currency);
|
|
21
|
+
const result = Math.sign(scaled) * Math.round(Math.abs(scaled) + Math.min(1e-7, Number.EPSILON * Math.abs(scaled)));
|
|
22
|
+
if (!Number.isSafeInteger(result))
|
|
23
|
+
throw new RangeError("Currency amount exceeds safe integer precision");
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
/** Convert stored integer minor units to major units for display or existing balance records.
|
|
27
|
+
* @capability currency-major-units convert safe minor-unit integers into currency amounts
|
|
28
|
+
*/
|
|
29
|
+
export function fromMinorUnits(currency, value) {
|
|
30
|
+
if (!Number.isSafeInteger(value))
|
|
31
|
+
throw new RangeError("Minor units must be a safe integer");
|
|
32
|
+
return value / currencyScale(currency);
|
|
33
|
+
}
|
|
34
|
+
/** Format a major-unit value using the currency's declared precision.
|
|
35
|
+
* @capability currency-precision-format display a currency using its declared decimal precision
|
|
36
|
+
*/
|
|
37
|
+
export function formatCurrency(currency, value) {
|
|
38
|
+
const decimals = currency.decimals ?? 0;
|
|
39
|
+
const rounded = fromMinorUnits(currency, toMinorUnits(currency, value));
|
|
40
|
+
return `${currency.symbol ?? ""}${rounded.toFixed(decimals)}${currency.unit ? ` ${currency.unit}` : ""}`;
|
|
4
41
|
}
|
|
5
42
|
/**
|
|
6
43
|
* Format a currency amount with its symbol and grouping for HUD display.
|
|
@@ -9,9 +46,7 @@ export function sanitizeCurrencyAmount(amount) {
|
|
|
9
46
|
* @internal
|
|
10
47
|
*/
|
|
11
48
|
export function formatCurrencyAmount(currency, amount) {
|
|
12
|
-
|
|
13
|
-
const suffix = currency.unit ? ` ${currency.unit}` : "";
|
|
14
|
-
return `${prefix}${amount}${suffix}`;
|
|
49
|
+
return formatCurrency(currency, amount);
|
|
15
50
|
}
|
|
16
51
|
/** @internal */
|
|
17
52
|
export function insufficientCurrencyReason(currency, needed, current) {
|
|
@@ -19,20 +54,26 @@ export function insufficientCurrencyReason(currency, needed, current) {
|
|
|
19
54
|
}
|
|
20
55
|
/** @internal */
|
|
21
56
|
export function applyCurrencyOperation(currency, current, operation, amount) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
57
|
+
if (!Number.isFinite(amount) || amount < 0 || !Number.isFinite(current)) {
|
|
58
|
+
return { success: false, reason: "Invalid currency amount" };
|
|
59
|
+
}
|
|
60
|
+
const currentMinor = toMinorUnits(currency, current);
|
|
61
|
+
const amountMinor = toMinorUnits(currency, amount);
|
|
62
|
+
if (operation === "deduct" && currentMinor < amountMinor) {
|
|
63
|
+
return { success: false, reason: insufficientCurrencyReason(currency, fromMinorUnits(currency, amountMinor), current) };
|
|
27
64
|
}
|
|
28
|
-
const
|
|
29
|
-
return {
|
|
65
|
+
const deltaMinor = operation === "add" ? amountMinor : -amountMinor;
|
|
66
|
+
return {
|
|
67
|
+
success: true,
|
|
68
|
+
newBalance: fromMinorUnits(currency, currentMinor + deltaMinor),
|
|
69
|
+
appliedDelta: fromMinorUnits(currency, deltaMinor),
|
|
70
|
+
};
|
|
30
71
|
}
|
|
31
72
|
/** Deducts clamp to the available balance (a delta can never overdraw); adds apply in full.
|
|
32
73
|
* @internal
|
|
33
74
|
*/
|
|
34
|
-
export function resolveCurrencyDelta(current, delta) {
|
|
35
|
-
const safeDelta =
|
|
75
|
+
export function resolveCurrencyDelta(current, delta, currency) {
|
|
76
|
+
const safeDelta = fromMinorUnits(currency, toMinorUnits(currency, delta));
|
|
36
77
|
if (safeDelta > 0)
|
|
37
78
|
return { operation: "add", amount: safeDelta };
|
|
38
79
|
return { operation: "deduct", amount: Math.min(Math.abs(safeDelta), current) };
|
package/dist/economy/wallet.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CurrencyDefinition } from "./currency.js";
|
|
1
2
|
export interface WalletState {
|
|
2
3
|
balances: Readonly<Record<string, number>>;
|
|
3
4
|
}
|
|
@@ -31,16 +32,16 @@ export interface ChargeOptions {
|
|
|
31
32
|
* @capability wallet hold currency balances with charge and affordability checks
|
|
32
33
|
*/
|
|
33
34
|
export declare function createEmptyWallet(): WalletState;
|
|
34
|
-
export declare function balance(state: WalletState, currency: string): number;
|
|
35
|
-
export declare function grant(state: WalletState, currency: string, amount: number): WalletState;
|
|
35
|
+
export declare function balance(state: WalletState, currency: string | CurrencyDefinition): number;
|
|
36
|
+
export declare function grant(state: WalletState, currency: string | CurrencyDefinition, amount: number): WalletState;
|
|
36
37
|
/**
|
|
37
38
|
* Deduct `amount`, rejecting when it would leave the balance negative unless `options.overdraft`
|
|
38
39
|
* opts into carrying debt (`true` unlimited, `{ max }` capped) — the strict same-tick affordability
|
|
39
40
|
* check stays the default with `options` omitted.
|
|
40
41
|
*/
|
|
41
|
-
export declare function charge(state: WalletState, currency: string, amount: number, options?: ChargeOptions): ChargeResult;
|
|
42
|
+
export declare function charge(state: WalletState, currency: string | CurrencyDefinition, amount: number, options?: ChargeOptions): ChargeResult;
|
|
42
43
|
/** True once `balance(state, currency)` has gone negative under an overdraft-enabled charge. */
|
|
43
|
-
export declare function isOverdrawn(state: WalletState, currency: string): boolean;
|
|
44
|
+
export declare function isOverdrawn(state: WalletState, currency: string | CurrencyDefinition): boolean;
|
|
44
45
|
/** True when every currency in `costs` has at least that much balance (a pure, non-mutating check). */
|
|
45
46
|
export declare function canAfford(state: WalletState, costs: Readonly<Record<string, number>>): boolean;
|
|
46
47
|
export declare function chargeAll(state: WalletState, costs: Readonly<Record<string, number>>, options?: ChargeOptions): ChargeResult;
|
package/dist/economy/wallet.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fromMinorUnits, toMinorUnits } from "./currency.js";
|
|
1
2
|
/**
|
|
2
3
|
* Hold per-currency balances with affordability checks and charge/grant operations.
|
|
3
4
|
*
|
|
@@ -12,14 +13,16 @@ function assertValidAmount(amount) {
|
|
|
12
13
|
}
|
|
13
14
|
}
|
|
14
15
|
export function balance(state, currency) {
|
|
15
|
-
return state.balances[currency] ?? 0;
|
|
16
|
+
return state.balances[typeof currency === "string" ? currency : currency.id] ?? 0;
|
|
16
17
|
}
|
|
17
18
|
export function grant(state, currency, amount) {
|
|
18
19
|
assertValidAmount(amount);
|
|
19
20
|
return {
|
|
20
21
|
balances: {
|
|
21
22
|
...state.balances,
|
|
22
|
-
[currency]:
|
|
23
|
+
[typeof currency === "string" ? currency : currency.id]: typeof currency === "string"
|
|
24
|
+
? balance(state, currency) + amount
|
|
25
|
+
: fromMinorUnits(currency, toMinorUnits(currency, balance(state, currency)) + toMinorUnits(currency, amount)),
|
|
23
26
|
},
|
|
24
27
|
};
|
|
25
28
|
}
|
|
@@ -40,7 +43,8 @@ function withinOverdraft(nextBalance, overdraft) {
|
|
|
40
43
|
export function charge(state, currency, amount, options) {
|
|
41
44
|
assertValidAmount(amount);
|
|
42
45
|
const current = balance(state, currency);
|
|
43
|
-
const next = current - amount
|
|
46
|
+
const next = typeof currency === "string" ? current - amount
|
|
47
|
+
: fromMinorUnits(currency, toMinorUnits(currency, current) - toMinorUnits(currency, amount));
|
|
44
48
|
if (!withinOverdraft(next, options?.overdraft)) {
|
|
45
49
|
return { status: "rejected", reason: "insufficient-funds" };
|
|
46
50
|
}
|
|
@@ -49,7 +53,7 @@ export function charge(state, currency, amount, options) {
|
|
|
49
53
|
state: {
|
|
50
54
|
balances: {
|
|
51
55
|
...state.balances,
|
|
52
|
-
[currency]: next,
|
|
56
|
+
[typeof currency === "string" ? currency : currency.id]: next,
|
|
53
57
|
},
|
|
54
58
|
},
|
|
55
59
|
};
|
|
@@ -447,6 +447,7 @@ const mutationHandlers = {
|
|
|
447
447
|
}),
|
|
448
448
|
setTerrain: (state, command) => ({ ...state, document: { ...state.document, terrain: command.terrain } }),
|
|
449
449
|
setMinimapBake: (state, command) => ({ ...state, document: { ...state.document, minimap: command.minimap } }),
|
|
450
|
+
setBake: (state, command) => ({ ...state, document: { ...state.document, bakes: [...(state.document.bakes ?? []).filter((bake) => bake.id !== command.bake.id), command.bake] } }),
|
|
450
451
|
setEnvironment: (state, command) => {
|
|
451
452
|
if (command.environment === undefined) {
|
|
452
453
|
const { environment: _removed, ...rest } = state.document;
|
|
@@ -2,7 +2,7 @@ import { type SurfaceDelta, type TerraformDelta, type TerrainMaterialLayer, type
|
|
|
2
2
|
import type { EditorUiDocument, EditorUiPanelLayout } from "../ui/hudDocument.js";
|
|
3
3
|
import type { EditorGridCellEdit, EditorGridLayer } from "./grid.js";
|
|
4
4
|
import type { ParamSchema } from "../scene/sceneKinds.js";
|
|
5
|
-
import type { EditorCatalogEntry, EditorDocument, EditorEnvironment, EditorMarker, EditorMinimapBake, EditorNote, EditorPath, EditorTerrain, EditorVec3, EditorVolume } from "./types.js";
|
|
5
|
+
import type { EditorCatalogEntry, EditorBake, EditorDocument, EditorEnvironment, EditorMarker, EditorMinimapBake, EditorNote, EditorPath, EditorTerrain, EditorVec3, EditorVolume } from "./types.js";
|
|
6
6
|
/** A single editor mutation — select, move, add, remove, undo/redo — dispatched to a session. */
|
|
7
7
|
export type EditorCommand = {
|
|
8
8
|
type: "select";
|
|
@@ -122,6 +122,9 @@ export type EditorCommand = {
|
|
|
122
122
|
} | {
|
|
123
123
|
type: "setMinimapBake";
|
|
124
124
|
minimap: EditorMinimapBake;
|
|
125
|
+
} | {
|
|
126
|
+
type: "setBake";
|
|
127
|
+
bake: EditorBake;
|
|
125
128
|
}
|
|
126
129
|
/**
|
|
127
130
|
* Replace the scene-document environment/lighting bag. Pass `undefined` to clear authored sky/fog
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type EditorGridLayer } from "./grid.js";
|
|
2
|
-
import type { EditorCatalogData, EditorCatalogDefinition, EditorCatalogEntry, EditorCollection, EditorDirective, EditorDocument, EditorEnvironment, EditorFragmentContent, EditorLayersInput, EditorMarker, EditorMinimapBake, EditorNote, EditorPath, EditorPrefab, EditorTerrain, EditorVolume } from "./types.js";
|
|
2
|
+
import type { EditorCatalogData, EditorBake, EditorCatalogDefinition, EditorCatalogEntry, EditorCollection, EditorDirective, EditorDocument, EditorEnvironment, EditorFragmentContent, EditorLayersInput, EditorMarker, EditorMinimapBake, EditorNote, EditorPath, EditorPrefab, EditorTerrain, EditorVolume } from "./types.js";
|
|
3
3
|
/** Spread-ready non-placeable fields preserved across document rebuilds. @internal */
|
|
4
4
|
export declare function editorDocumentExtras(doc: EditorDocument): {
|
|
5
5
|
prefabs: EditorPrefab[];
|
|
@@ -10,6 +10,7 @@ export declare function editorDocumentExtras(doc: EditorDocument): {
|
|
|
10
10
|
ui?: EditorDocument["ui"];
|
|
11
11
|
directives?: EditorDirective[];
|
|
12
12
|
minimap?: EditorMinimapBake;
|
|
13
|
+
bakes?: EditorBake[];
|
|
13
14
|
environment?: EditorEnvironment;
|
|
14
15
|
};
|
|
15
16
|
/** Builds a fresh, empty editor document to start authoring a scene from scratch.
|
package/dist/editor/document.js
CHANGED
|
@@ -5,18 +5,28 @@ import { cloneGridLayer, migrateGridLayer, } from "./grid.js";
|
|
|
5
5
|
function cloneMinimapBake(minimap) {
|
|
6
6
|
return { background: minimap.background, bounds: { ...minimap.bounds } };
|
|
7
7
|
}
|
|
8
|
+
function cloneBakes(bakes) {
|
|
9
|
+
return bakes === undefined ? undefined : bakes.map((bake) => ({ ...bake, data: structuredClone(bake.data) }));
|
|
10
|
+
}
|
|
8
11
|
/** Deep-copies scene-document environment/lighting so edits never mutate history snapshots. */
|
|
9
12
|
function cloneEnvironment(environment) {
|
|
10
13
|
return {
|
|
14
|
+
...(environment.source === undefined ? {} : { source: cloneEnvironmentSource(environment.source) }),
|
|
15
|
+
...(environment.pointLights === undefined ? {} : { pointLights: environment.pointLights.map((light) => ({ ...light, position: [...light.position] })) }),
|
|
11
16
|
...(environment.preset === undefined ? {} : { preset: environment.preset }),
|
|
12
17
|
...(environment.timeOfDay === undefined ? {} : { timeOfDay: environment.timeOfDay }),
|
|
13
18
|
...(environment.horizonColor === undefined ? {} : { horizonColor: environment.horizonColor }),
|
|
14
19
|
...(environment.zenithColor === undefined ? {} : { zenithColor: environment.zenithColor }),
|
|
15
20
|
...(environment.sunIntensity === undefined ? {} : { sunIntensity: environment.sunIntensity }),
|
|
16
21
|
...(environment.ambientIntensity === undefined ? {} : { ambientIntensity: environment.ambientIntensity }),
|
|
22
|
+
...(environment.sunAzimuth === undefined ? {} : { sunAzimuth: environment.sunAzimuth }),
|
|
23
|
+
...(environment.sunElevation === undefined ? {} : { sunElevation: environment.sunElevation }),
|
|
17
24
|
...(environment.fog === undefined ? {} : { fog: { ...environment.fog } }),
|
|
18
25
|
};
|
|
19
26
|
}
|
|
27
|
+
function cloneEnvironmentSource(source) {
|
|
28
|
+
return source.kind === "cube" ? { ...source, urls: [...source.urls] } : { ...source };
|
|
29
|
+
}
|
|
20
30
|
function cloneDirective(directive) {
|
|
21
31
|
const area = directive.area === undefined
|
|
22
32
|
? undefined
|
|
@@ -66,6 +76,7 @@ export function editorDocumentExtras(doc) {
|
|
|
66
76
|
...(doc.ui === undefined ? {} : { ui: doc.ui }),
|
|
67
77
|
...(doc.directives === undefined ? {} : { directives: doc.directives }),
|
|
68
78
|
...(doc.minimap === undefined ? {} : { minimap: doc.minimap }),
|
|
79
|
+
...(doc.bakes === undefined ? {} : { bakes: cloneBakes(doc.bakes) }),
|
|
69
80
|
...(doc.environment === undefined ? {} : { environment: doc.environment }),
|
|
70
81
|
};
|
|
71
82
|
}
|
|
@@ -147,6 +158,7 @@ export function cloneEditorDocument(doc) {
|
|
|
147
158
|
...(ui === undefined ? {} : { ui }),
|
|
148
159
|
...(directives === undefined ? {} : { directives }),
|
|
149
160
|
...(doc.minimap === undefined ? {} : { minimap: cloneMinimapBake(doc.minimap) }),
|
|
161
|
+
...(doc.bakes === undefined ? {} : { bakes: cloneBakes(doc.bakes) }),
|
|
150
162
|
...(doc.environment === undefined ? {} : { environment: cloneEnvironment(doc.environment) }),
|
|
151
163
|
};
|
|
152
164
|
}
|
|
@@ -204,6 +216,7 @@ export function normalizeEditorLayers(input) {
|
|
|
204
216
|
...(ui === undefined ? {} : { ui }),
|
|
205
217
|
...(directives === undefined ? {} : { directives }),
|
|
206
218
|
...(resolved.minimap === undefined ? {} : { minimap: cloneMinimapBake(resolved.minimap) }),
|
|
219
|
+
...(resolved.bakes === undefined ? {} : { bakes: cloneBakes(resolved.bakes) }),
|
|
207
220
|
...(resolved.environment === undefined ? {} : { environment: cloneEnvironment(resolved.environment) }),
|
|
208
221
|
};
|
|
209
222
|
}
|
|
@@ -949,6 +962,21 @@ function decodeMinimapBake(value, path, errors) {
|
|
|
949
962
|
bounds: { minX: b.minX, minZ: b.minZ, maxX: b.maxX, maxZ: b.maxZ },
|
|
950
963
|
};
|
|
951
964
|
}
|
|
965
|
+
function decodeBakes(value, path, errors) {
|
|
966
|
+
if (!Array.isArray(value)) {
|
|
967
|
+
errors.push({ path, message: "expected an array" });
|
|
968
|
+
return undefined;
|
|
969
|
+
}
|
|
970
|
+
const bakes = [];
|
|
971
|
+
value.forEach((item, index) => {
|
|
972
|
+
if (!isPlainObject(item) || (item.kind !== "nav" && item.kind !== "minimap") || typeof item.id !== "string" || item.data === undefined) {
|
|
973
|
+
errors.push({ path: `${path}[${index}]`, message: 'expected {kind: "nav" | "minimap", id, data}' });
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
bakes.push({ kind: item.kind, id: item.id, data: structuredClone(item.data) });
|
|
977
|
+
});
|
|
978
|
+
return bakes;
|
|
979
|
+
}
|
|
952
980
|
const SKY_PRESETS = new Set(["day", "dusk", "night"]);
|
|
953
981
|
/**
|
|
954
982
|
* Validates scene-document environment/lighting. Unknown presets, non-finite intensities, and
|
|
@@ -962,6 +990,67 @@ function decodeEnvironment(value, path, errors) {
|
|
|
962
990
|
}
|
|
963
991
|
let failed = false;
|
|
964
992
|
const env = {};
|
|
993
|
+
if (value.source !== undefined) {
|
|
994
|
+
const source = value.source;
|
|
995
|
+
if (!isPlainObject(source) || typeof source.kind !== "string") {
|
|
996
|
+
errors.push({ path: `${path}.source`, message: "expected an environment source object" });
|
|
997
|
+
failed = true;
|
|
998
|
+
}
|
|
999
|
+
else if (source.kind === "gradient") {
|
|
1000
|
+
if (typeof source.sky !== "string") {
|
|
1001
|
+
errors.push({ path: `${path}.source.sky`, message: "expected a string" });
|
|
1002
|
+
failed = true;
|
|
1003
|
+
}
|
|
1004
|
+
if (typeof source.ground !== "string") {
|
|
1005
|
+
errors.push({ path: `${path}.source.ground`, message: "expected a string" });
|
|
1006
|
+
failed = true;
|
|
1007
|
+
}
|
|
1008
|
+
if (source.sun !== undefined && typeof source.sun !== "string") {
|
|
1009
|
+
errors.push({ path: `${path}.source.sun`, message: "expected a string" });
|
|
1010
|
+
failed = true;
|
|
1011
|
+
}
|
|
1012
|
+
if (source.intensity !== undefined && (typeof source.intensity !== "number" || !Number.isFinite(source.intensity))) {
|
|
1013
|
+
errors.push({ path: `${path}.source.intensity`, message: "expected a finite number" });
|
|
1014
|
+
failed = true;
|
|
1015
|
+
}
|
|
1016
|
+
if (!failed)
|
|
1017
|
+
env.source = { kind: "gradient", sky: source.sky, ground: source.ground, ...(source.sun === undefined ? {} : { sun: source.sun }), ...(source.intensity === undefined ? {} : { intensity: source.intensity }) };
|
|
1018
|
+
}
|
|
1019
|
+
else if (source.kind === "hdri" && typeof source.url === "string") {
|
|
1020
|
+
env.source = { kind: "hdri", url: source.url, ...(typeof source.rotation === "number" ? { rotation: source.rotation } : {}), ...(typeof source.intensity === "number" ? { intensity: source.intensity } : {}) };
|
|
1021
|
+
}
|
|
1022
|
+
else if (source.kind === "cube" && Array.isArray(source.urls) && source.urls.length === 6 && source.urls.every((url) => typeof url === "string")) {
|
|
1023
|
+
env.source = { kind: "cube", urls: [...source.urls], ...(typeof source.intensity === "number" ? { intensity: source.intensity } : {}) };
|
|
1024
|
+
}
|
|
1025
|
+
else {
|
|
1026
|
+
errors.push({ path: `${path}.source`, message: 'expected kind "gradient", "hdri", or "cube" with valid fields' });
|
|
1027
|
+
failed = true;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
if (value.pointLights !== undefined) {
|
|
1031
|
+
if (!Array.isArray(value.pointLights)) {
|
|
1032
|
+
errors.push({ path: `${path}.pointLights`, message: "expected an array" });
|
|
1033
|
+
failed = true;
|
|
1034
|
+
}
|
|
1035
|
+
else if (value.pointLights.length > 64 || value.pointLights.some((light) => {
|
|
1036
|
+
if (!isPlainObject(light) || !Array.isArray(light.position) || light.position.length !== 3 || light.position.some((n) => typeof n !== "number" || !Number.isFinite(n)))
|
|
1037
|
+
return true;
|
|
1038
|
+
return light.intensity !== undefined && (typeof light.intensity !== "number" || !Number.isFinite(light.intensity));
|
|
1039
|
+
})) {
|
|
1040
|
+
errors.push({ path: `${path}.pointLights`, message: "expected point lights with finite positions and intensities" });
|
|
1041
|
+
failed = true;
|
|
1042
|
+
}
|
|
1043
|
+
else {
|
|
1044
|
+
env.pointLights = value.pointLights.map((light) => ({
|
|
1045
|
+
position: [...light.position],
|
|
1046
|
+
...(typeof light.color === "string" ? { color: light.color } : {}),
|
|
1047
|
+
...(typeof light.intensity === "number" ? { intensity: light.intensity } : {}),
|
|
1048
|
+
...(typeof light.distance === "number" ? { distance: light.distance } : {}),
|
|
1049
|
+
...(typeof light.decay === "number" ? { decay: light.decay } : {}),
|
|
1050
|
+
...(typeof light.castShadow === "boolean" ? { castShadow: light.castShadow } : {}),
|
|
1051
|
+
}));
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
965
1054
|
if (value.preset !== undefined) {
|
|
966
1055
|
if (typeof value.preset === "string" && SKY_PRESETS.has(value.preset)) {
|
|
967
1056
|
env.preset = value.preset;
|
|
@@ -995,7 +1084,7 @@ function decodeEnvironment(value, path, errors) {
|
|
|
995
1084
|
failed = true;
|
|
996
1085
|
}
|
|
997
1086
|
}
|
|
998
|
-
for (const key of ["sunIntensity", "ambientIntensity"]) {
|
|
1087
|
+
for (const key of ["sunIntensity", "ambientIntensity", "sunAzimuth", "sunElevation"]) {
|
|
999
1088
|
if (value[key] === undefined)
|
|
1000
1089
|
continue;
|
|
1001
1090
|
if (typeof value[key] === "number" && Number.isFinite(value[key]))
|
|
@@ -1152,6 +1241,7 @@ export function decodeEditorDocument(raw) {
|
|
|
1152
1241
|
errors.push({ path: "$.terrain", message: "expected an object" });
|
|
1153
1242
|
}
|
|
1154
1243
|
const minimap = raw.minimap === undefined ? undefined : decodeMinimapBake(raw.minimap, "$.minimap", errors);
|
|
1244
|
+
const bakes = raw.bakes === undefined ? undefined : decodeBakes(raw.bakes, "$.bakes", errors);
|
|
1155
1245
|
const environment = raw.environment === undefined ? undefined : decodeEnvironment(raw.environment, "$.environment", errors);
|
|
1156
1246
|
// Placeable object ids form one document-global namespace (selection, parenting, and removal all
|
|
1157
1247
|
// treat them that way), so a document that reuses an id — even across two different collections —
|
|
@@ -1194,6 +1284,7 @@ export function decodeEditorDocument(raw) {
|
|
|
1194
1284
|
...(ui === undefined ? {} : { ui }),
|
|
1195
1285
|
...(directives === undefined ? {} : { directives }),
|
|
1196
1286
|
...(minimap === undefined ? {} : { minimap }),
|
|
1287
|
+
...(bakes === undefined && minimap === undefined ? {} : { bakes: bakes ?? [{ kind: "minimap", id: "minimap", data: minimap }] }),
|
|
1197
1288
|
...(environment === undefined ? {} : { environment }),
|
|
1198
1289
|
},
|
|
1199
1290
|
};
|