@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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** Sample a {@link FeedbackCurve}; an empty curve is the identity. */
|
|
2
|
+
export function sampleFeedbackCurve(curve, x) {
|
|
3
|
+
if (curve === undefined || curve.length === 0)
|
|
4
|
+
return x;
|
|
5
|
+
const first = curve[0];
|
|
6
|
+
if (x <= first[0])
|
|
7
|
+
return first[1];
|
|
8
|
+
for (let i = 1; i < curve.length; i += 1) {
|
|
9
|
+
const [x1, y1] = curve[i];
|
|
10
|
+
if (x <= x1) {
|
|
11
|
+
const [x0, y0] = curve[i - 1];
|
|
12
|
+
const span = x1 - x0;
|
|
13
|
+
return span <= 0 ? y1 : y0 + ((y1 - y0) * (x - x0)) / span;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return curve[curve.length - 1][1];
|
|
17
|
+
}
|
|
18
|
+
function approach(current, target, attack, release, dt) {
|
|
19
|
+
const rate = target > current ? attack : release;
|
|
20
|
+
if (!Number.isFinite(rate))
|
|
21
|
+
return target;
|
|
22
|
+
const step = Math.max(0, rate) * dt;
|
|
23
|
+
return Math.abs(target - current) <= step ? target : current + Math.sign(target - current) * step;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Creates a {@link FeedbackMixer}: the one place a game declares how sim telemetry drives presentation —
|
|
27
|
+
* rpm to engine pitch, tire saturation to squeal and rumble, speed to FOV, landing speed to a thud — instead
|
|
28
|
+
* of hand-writing that glue per vehicle, aircraft or weapon. Deterministic, allocation-free per update.
|
|
29
|
+
* @capability feedback-mixer map sim telemetry to camera, audio and haptic parameters through curves, smoothing and threshold events
|
|
30
|
+
*/
|
|
31
|
+
export function createFeedbackMixer(initial) {
|
|
32
|
+
let config = initial;
|
|
33
|
+
let routeValues = [];
|
|
34
|
+
let eventSignals = [];
|
|
35
|
+
let cooldowns = [];
|
|
36
|
+
let firedNow = new Set();
|
|
37
|
+
const output = {};
|
|
38
|
+
const touched = new Set();
|
|
39
|
+
function shapeState() {
|
|
40
|
+
routeValues = config.routes.map((_, i) => routeValues[i] ?? 0);
|
|
41
|
+
eventSignals = (config.events ?? []).map((_, i) => eventSignals[i] ?? 0);
|
|
42
|
+
cooldowns = (config.events ?? []).map((_, i) => cooldowns[i] ?? 0);
|
|
43
|
+
for (const route of config.routes)
|
|
44
|
+
output[route.target] ??= 0;
|
|
45
|
+
for (const key of Object.keys(config.base ?? {}))
|
|
46
|
+
output[key] ??= config.base?.[key] ?? 0;
|
|
47
|
+
}
|
|
48
|
+
shapeState();
|
|
49
|
+
return {
|
|
50
|
+
update(dt, signals) {
|
|
51
|
+
const step = Math.max(0, dt);
|
|
52
|
+
for (const key of Object.keys(output))
|
|
53
|
+
output[key] = config.base?.[key] ?? 0;
|
|
54
|
+
touched.clear();
|
|
55
|
+
for (let i = 0; i < config.routes.length; i += 1) {
|
|
56
|
+
const route = config.routes[i];
|
|
57
|
+
const target = sampleFeedbackCurve(route.curve, signals[route.signal] ?? 0);
|
|
58
|
+
const attack = route.attack ?? Number.POSITIVE_INFINITY;
|
|
59
|
+
const value = approach(routeValues[i], target, attack, route.release ?? attack, step);
|
|
60
|
+
routeValues[i] = value;
|
|
61
|
+
const mode = config.combine?.[route.target] ?? "sum";
|
|
62
|
+
if (mode === "max") {
|
|
63
|
+
const current = touched.has(route.target) ? output[route.target] : Number.NEGATIVE_INFINITY;
|
|
64
|
+
output[route.target] = Math.max(current, value);
|
|
65
|
+
}
|
|
66
|
+
else
|
|
67
|
+
output[route.target] += value;
|
|
68
|
+
touched.add(route.target);
|
|
69
|
+
}
|
|
70
|
+
firedNow.clear();
|
|
71
|
+
const events = config.events ?? [];
|
|
72
|
+
for (let i = 0; i < events.length; i += 1) {
|
|
73
|
+
const event = events[i];
|
|
74
|
+
const value = signals[event.signal] ?? 0;
|
|
75
|
+
cooldowns[i] = Math.max(0, cooldowns[i] - step);
|
|
76
|
+
if (value >= event.threshold && eventSignals[i] < event.threshold && cooldowns[i] === 0) {
|
|
77
|
+
firedNow.add(event.id);
|
|
78
|
+
cooldowns[i] = event.cooldown ?? 0;
|
|
79
|
+
}
|
|
80
|
+
eventSignals[i] = value;
|
|
81
|
+
}
|
|
82
|
+
return output;
|
|
83
|
+
},
|
|
84
|
+
fired: (id) => firedNow.has(id),
|
|
85
|
+
value: () => output,
|
|
86
|
+
retune(next) {
|
|
87
|
+
config = next;
|
|
88
|
+
shapeState();
|
|
89
|
+
},
|
|
90
|
+
snapshot: () => ({ routes: [...routeValues], eventSignals: [...eventSignals], cooldowns: [...cooldowns] }),
|
|
91
|
+
restore(state) {
|
|
92
|
+
routeValues = [...state.routes];
|
|
93
|
+
eventSignals = [...state.eventSignals];
|
|
94
|
+
cooldowns = [...state.cooldowns];
|
|
95
|
+
shapeState();
|
|
96
|
+
},
|
|
97
|
+
reset() {
|
|
98
|
+
routeValues = routeValues.map(() => 0);
|
|
99
|
+
eventSignals = eventSignals.map(() => 0);
|
|
100
|
+
cooldowns = cooldowns.map(() => 0);
|
|
101
|
+
firedNow = new Set();
|
|
102
|
+
for (const key of Object.keys(output))
|
|
103
|
+
output[key] = config.base?.[key] ?? 0;
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LodBand } from "../world/lod.js";
|
|
1
|
+
import type { LodBand, LodSchedulerState } from "../world/lod.js";
|
|
2
2
|
/**
|
|
3
3
|
* Simulation culling is a SEPARATE, opt-in system from render culling. Render culling only
|
|
4
4
|
* decides what is drawn; this decides whether a low-priority off-screen entity updates this
|
|
@@ -23,6 +23,11 @@ export interface SimulationDecision {
|
|
|
23
23
|
/** Seconds of accumulated time delivered when `update` is true (for catch-up integration). */
|
|
24
24
|
elapsed: number;
|
|
25
25
|
}
|
|
26
|
+
/** Plain JSON state of a {@link SimulationCuller}: the enabled flag and each id's throttle accumulator. */
|
|
27
|
+
export interface SimulationCullerState {
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
scheduler: LodSchedulerState;
|
|
30
|
+
}
|
|
26
31
|
export interface SimulationCuller {
|
|
27
32
|
enabled(): boolean;
|
|
28
33
|
setEnabled(value: boolean): void;
|
|
@@ -30,5 +35,7 @@ export interface SimulationCuller {
|
|
|
30
35
|
step(id: string, distance: number, dt: number): SimulationDecision;
|
|
31
36
|
forget(id: string): void;
|
|
32
37
|
clear(): void;
|
|
38
|
+
snapshot(): SimulationCullerState;
|
|
39
|
+
restore(next: SimulationCullerState): void;
|
|
33
40
|
}
|
|
34
41
|
export declare function createSimulationCuller(options?: SimulationCullingOptions): SimulationCuller;
|
|
@@ -32,5 +32,12 @@ export function createSimulationCuller(options = {}) {
|
|
|
32
32
|
clear() {
|
|
33
33
|
scheduler.clear();
|
|
34
34
|
},
|
|
35
|
+
snapshot() {
|
|
36
|
+
return { enabled, scheduler: scheduler.snapshot() };
|
|
37
|
+
},
|
|
38
|
+
restore(next) {
|
|
39
|
+
enabled = next.enabled;
|
|
40
|
+
scheduler.restore(next.scheduler);
|
|
41
|
+
},
|
|
35
42
|
};
|
|
36
43
|
}
|
|
@@ -10,6 +10,25 @@ export interface SpatialIndexOptions {
|
|
|
10
10
|
*/
|
|
11
11
|
readonly maxCellSpan?: number;
|
|
12
12
|
}
|
|
13
|
+
/** Integer cell bounds an object covers, inclusive on both ends. */
|
|
14
|
+
export interface SpatialCellRange {
|
|
15
|
+
x0: number;
|
|
16
|
+
y0: number;
|
|
17
|
+
z0: number;
|
|
18
|
+
x1: number;
|
|
19
|
+
y1: number;
|
|
20
|
+
z1: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Plain JSON state of a {@link SpatialIndex}. Cells and their ids keep insertion order, so a restored
|
|
24
|
+
* index returns query results in the same order.
|
|
25
|
+
*/
|
|
26
|
+
export interface SpatialIndexState {
|
|
27
|
+
/** Per object: its cell range (`null` when oversized) and whether it was inserted as dynamic. */
|
|
28
|
+
entries: [id: string, range: SpatialCellRange | null, dynamic: boolean][];
|
|
29
|
+
cells: [x: number, y: number, z: number, ids: string[]][];
|
|
30
|
+
oversized: string[];
|
|
31
|
+
}
|
|
13
32
|
/**
|
|
14
33
|
* A uniform 3D spatial hash the renderer and streaming system query for potentially-visible
|
|
15
34
|
* objects instead of scanning the whole scene. Objects are keyed by their world AABB into
|
|
@@ -39,6 +58,8 @@ export interface SpatialIndex {
|
|
|
39
58
|
}[];
|
|
40
59
|
cellSize(): number;
|
|
41
60
|
clear(): void;
|
|
61
|
+
snapshot(): SpatialIndexState;
|
|
62
|
+
restore(next: SpatialIndexState): void;
|
|
42
63
|
}
|
|
43
64
|
export declare function createSpatialIndex(options?: SpatialIndexOptions): SpatialIndex;
|
|
44
65
|
/** Precise AABB-vs-AABB test re-exported for callers refining a broad-phase result. */
|
|
@@ -206,6 +206,22 @@ export function createSpatialIndex(options = {}) {
|
|
|
206
206
|
entries.clear();
|
|
207
207
|
stamps.clear();
|
|
208
208
|
},
|
|
209
|
+
snapshot() {
|
|
210
|
+
return {
|
|
211
|
+
entries: [...entries].map(([id, entry]) => [id, entry.range === null ? null : { ...entry.range }, entry.dynamic]),
|
|
212
|
+
cells: [...cells.values()].map((bucket) => [bucket.x, bucket.y, bucket.z, [...bucket.ids]]),
|
|
213
|
+
oversized: [...oversized],
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
restore(next) {
|
|
217
|
+
this.clear();
|
|
218
|
+
for (const [id, range, dynamic] of next.entries)
|
|
219
|
+
entries.set(id, { range: range === null ? null : { ...range }, dynamic });
|
|
220
|
+
for (const [x, y, z, ids] of next.cells)
|
|
221
|
+
cells.set(packCell(x, y, z), { x, y, z, ids: new Set(ids) });
|
|
222
|
+
for (const id of next.oversized)
|
|
223
|
+
oversized.add(id);
|
|
224
|
+
},
|
|
209
225
|
};
|
|
210
226
|
}
|
|
211
227
|
/** Precise AABB-vs-AABB test re-exported for callers refining a broad-phase result. */
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { WorldSolid, WorldSolids } from "./worldSolids.js";
|
|
2
|
+
type Vec3Like = {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
z: number;
|
|
6
|
+
};
|
|
7
|
+
/** Minimal editor document shape {@link resolveAuthoredSolids} walks; any `EditorDocument` satisfies it. */
|
|
8
|
+
export interface AuthoredSolidsDocumentLike {
|
|
9
|
+
markers: readonly {
|
|
10
|
+
id: string;
|
|
11
|
+
kind: string;
|
|
12
|
+
position: Vec3Like;
|
|
13
|
+
rotationY?: number;
|
|
14
|
+
meta?: Record<string, unknown>;
|
|
15
|
+
}[];
|
|
16
|
+
volumes: readonly {
|
|
17
|
+
id: string;
|
|
18
|
+
kind: string;
|
|
19
|
+
center: Vec3Like;
|
|
20
|
+
halfExtents?: Vec3Like;
|
|
21
|
+
radius?: number;
|
|
22
|
+
meta?: Record<string, unknown>;
|
|
23
|
+
}[];
|
|
24
|
+
paths: readonly {
|
|
25
|
+
id: string;
|
|
26
|
+
kind: string;
|
|
27
|
+
points: readonly Vec3Like[];
|
|
28
|
+
meta?: Record<string, unknown>;
|
|
29
|
+
}[];
|
|
30
|
+
}
|
|
31
|
+
/** Layer prefix `syncAuthoredSolids` owns in `ctx.world.solids`; one layer per document object. */
|
|
32
|
+
export declare const AUTHORED_SOLID_LAYER_PREFIX = "authored:";
|
|
33
|
+
/**
|
|
34
|
+
* Solids for every document object whose scene kind declares `solids`, keyed by object id. Objects
|
|
35
|
+
* whose kind has no `solids` hook, or whose hook returns none, are left out.
|
|
36
|
+
* @capability authored-solids collision for studio-authored world content such as city volumes
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveAuthoredSolids(document: AuthoredSolidsDocumentLike, sampleHeight?: (x: number, z: number) => number): Map<string, readonly WorldSolid[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Write a document's studio solids into `solids` (usually `ctx.world.solids`), one
|
|
41
|
+
* `authored:<objectId>` layer per object, dropping authored layers the document no longer has.
|
|
42
|
+
* Call again whenever the document changes.
|
|
43
|
+
* @capability authored-solids collision for studio-authored world content such as city volumes
|
|
44
|
+
*/
|
|
45
|
+
export declare function syncAuthoredSolids(solids: WorldSolids, document: AuthoredSolidsDocumentLike, sampleHeight?: (x: number, z: number) => number): void;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { registerBuiltinSceneKinds } from "../scene/builtinSceneKinds.js";
|
|
2
|
+
import { getSceneKind, parseParams } from "../scene/sceneKinds.js";
|
|
3
|
+
/** Layer prefix `syncAuthoredSolids` owns in `ctx.world.solids`; one layer per document object. */
|
|
4
|
+
export const AUTHORED_SOLID_LAYER_PREFIX = "authored:";
|
|
5
|
+
function kindObjects(document) {
|
|
6
|
+
const objects = [];
|
|
7
|
+
for (const marker of document.markers) {
|
|
8
|
+
objects.push({
|
|
9
|
+
id: marker.id,
|
|
10
|
+
kind: marker.kind,
|
|
11
|
+
position: marker.position,
|
|
12
|
+
...(marker.rotationY === undefined ? {} : { rotationY: marker.rotationY }),
|
|
13
|
+
...(marker.meta === undefined ? {} : { meta: marker.meta }),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
for (const volume of document.volumes) {
|
|
17
|
+
objects.push({
|
|
18
|
+
id: volume.id,
|
|
19
|
+
kind: volume.kind,
|
|
20
|
+
center: volume.center,
|
|
21
|
+
...(volume.halfExtents === undefined ? {} : { halfExtents: volume.halfExtents }),
|
|
22
|
+
...(volume.radius === undefined ? {} : { radius: volume.radius }),
|
|
23
|
+
...(volume.meta === undefined ? {} : { meta: volume.meta }),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
for (const path of document.paths) {
|
|
27
|
+
objects.push({
|
|
28
|
+
id: path.id,
|
|
29
|
+
kind: path.kind,
|
|
30
|
+
points: path.points,
|
|
31
|
+
...(path.meta === undefined ? {} : { meta: path.meta }),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return objects;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Solids for every document object whose scene kind declares `solids`, keyed by object id. Objects
|
|
38
|
+
* whose kind has no `solids` hook, or whose hook returns none, are left out.
|
|
39
|
+
* @capability authored-solids collision for studio-authored world content such as city volumes
|
|
40
|
+
*/
|
|
41
|
+
export function resolveAuthoredSolids(document, sampleHeight) {
|
|
42
|
+
registerBuiltinSceneKinds();
|
|
43
|
+
const out = new Map();
|
|
44
|
+
const objects = kindObjects(document);
|
|
45
|
+
const context = { ...(sampleHeight === undefined ? {} : { sampleHeight }), objects };
|
|
46
|
+
for (const object of objects) {
|
|
47
|
+
const definition = getSceneKind(object.kind);
|
|
48
|
+
if (definition?.solids === undefined || definition.resolve === undefined)
|
|
49
|
+
continue;
|
|
50
|
+
const params = parseParams(definition.schema, object.meta);
|
|
51
|
+
const solids = definition.solids(definition.resolve(object, params, context), object, params, context);
|
|
52
|
+
if (solids.length > 0)
|
|
53
|
+
out.set(object.id, solids);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Write a document's studio solids into `solids` (usually `ctx.world.solids`), one
|
|
59
|
+
* `authored:<objectId>` layer per object, dropping authored layers the document no longer has.
|
|
60
|
+
* Call again whenever the document changes.
|
|
61
|
+
* @capability authored-solids collision for studio-authored world content such as city volumes
|
|
62
|
+
*/
|
|
63
|
+
export function syncAuthoredSolids(solids, document, sampleHeight) {
|
|
64
|
+
const resolved = resolveAuthoredSolids(document, sampleHeight);
|
|
65
|
+
solids.batch(() => {
|
|
66
|
+
for (const layer of solids.layers()) {
|
|
67
|
+
if (!layer.startsWith(AUTHORED_SOLID_LAYER_PREFIX))
|
|
68
|
+
continue;
|
|
69
|
+
if (!resolved.has(layer.slice(AUTHORED_SOLID_LAYER_PREFIX.length)))
|
|
70
|
+
solids.remove(layer);
|
|
71
|
+
}
|
|
72
|
+
for (const [id, set] of resolved)
|
|
73
|
+
solids.set(`${AUTHORED_SOLID_LAYER_PREFIX}${id}`, set);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @capability building-generator seeded building as a placeable parametric asset
|
|
8
8
|
*/
|
|
9
9
|
import { registerAssetGenerator, partsBounds } from "../scene/assetGenerator.js";
|
|
10
|
-
import { BUILDING_STYLE_PALETTES, DEFAULT_BUILDING_CONFIG, generateBuilding, resolveBuildingPalette } from "./buildings.js";
|
|
10
|
+
import { BUILDING_STYLE_PALETTES, DEFAULT_BUILDING_CONFIG, buildingSurfaceColor, generateBuilding, resolveBuildingPalette } from "./buildings.js";
|
|
11
11
|
/** The generator id a catalog entry / placed marker references. */
|
|
12
12
|
export const BUILDING_GENERATOR_ID = "building";
|
|
13
13
|
/** The building generator's slider schema — drives the inspector via #809. */
|
|
@@ -47,7 +47,7 @@ export function generateBuildingAsset(params, seed) {
|
|
|
47
47
|
position: part.position,
|
|
48
48
|
size: part.scale,
|
|
49
49
|
rotationY: part.rotationY,
|
|
50
|
-
color: palette[part.kind],
|
|
50
|
+
color: buildingSurfaceColor(palette[part.kind]),
|
|
51
51
|
}));
|
|
52
52
|
return { parts, bounds: partsBounds(parts) };
|
|
53
53
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* as an instanced asset kit instead of untextured blocks. Pure serializable data — core resolves
|
|
4
4
|
* bindings, the renderer loads them.
|
|
5
5
|
*/
|
|
6
|
-
import type { BuildingKitSlot, BuildingPartKind, BuildingVariantCounts, Vec3 } from "./buildings.js";
|
|
6
|
+
import type { BuildingKitSlot, BuildingPartKind, BuildingSurfaceMaterial, BuildingVariantCounts, Vec3 } from "./buildings.js";
|
|
7
7
|
/**
|
|
8
8
|
* How a kit model's native bounds are mapped onto the slot box `BuildingPartPlacement.scale`
|
|
9
9
|
* describes.
|
|
@@ -32,6 +32,8 @@ export interface BuildingKitPart {
|
|
|
32
32
|
/** Multiplier applied after `fit`. */
|
|
33
33
|
scale?: Vec3;
|
|
34
34
|
tint?: string;
|
|
35
|
+
/** Tiled PBR maps applied over the model's own materials; `color` tints, `repeat` sets the tiling. */
|
|
36
|
+
material?: BuildingSurfaceMaterial;
|
|
35
37
|
/**
|
|
36
38
|
* Quarter-turn the model when its long horizontal axis disagrees with the slot's, so a panel
|
|
37
39
|
* authored running along Z still tiles a bay that runs along X. Default true — modular kits do not
|
|
@@ -103,11 +103,43 @@ export interface BuildingGridConfig {
|
|
|
103
103
|
floorRange?: readonly [number, number];
|
|
104
104
|
}
|
|
105
105
|
export declare const DEFAULT_BUILDING_CONFIG: BuildingConfig;
|
|
106
|
-
|
|
106
|
+
/** URLs of the PBR maps a building surface tiles; `buildMaterialCatalog(...).resolve(id)!.maps` fits directly. */
|
|
107
|
+
export interface BuildingSurfaceMaps {
|
|
108
|
+
color?: string;
|
|
109
|
+
normal?: string;
|
|
110
|
+
roughness?: string;
|
|
111
|
+
ao?: string;
|
|
112
|
+
metalness?: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* A textured building surface: a base colour plus optional tiled PBR maps. With `maps`, `color`
|
|
116
|
+
* multiplies the albedo (leave it light to show the map as authored); without, it is the flat
|
|
117
|
+
* colour the part has always had.
|
|
118
|
+
*/
|
|
119
|
+
export interface BuildingSurfaceMaterial {
|
|
120
|
+
/** Base colour; the style's colour for that part kind when unset. */
|
|
121
|
+
color?: string;
|
|
122
|
+
maps?: BuildingSurfaceMaps;
|
|
123
|
+
/**
|
|
124
|
+
* Texture tiles per part slot along [x, y]; a number tiles both. Slots within one building share
|
|
125
|
+
* a size, so one repeat per kind is uniform there; it drifts across buildings with a different
|
|
126
|
+
* `bayWidth`. Default 1.
|
|
127
|
+
*/
|
|
128
|
+
repeat?: number | readonly [number, number];
|
|
129
|
+
roughness?: number;
|
|
130
|
+
metalness?: number;
|
|
131
|
+
}
|
|
132
|
+
/** What a building part kind is painted with: a hex colour, or a colour plus tiled PBR maps. */
|
|
133
|
+
export type BuildingSurface = string | BuildingSurfaceMaterial;
|
|
134
|
+
export type BuildingPalette = Record<BuildingPartKind, BuildingSurface>;
|
|
107
135
|
export type BuildingPaletteOverrides = Partial<BuildingPalette>;
|
|
136
|
+
/** The flat colour of a surface: the hex itself, or a material's `color`, else `fallback`. */
|
|
137
|
+
export declare function buildingSurfaceColor(surface: BuildingSurface | undefined, fallback?: string): string;
|
|
138
|
+
/** Normalises a surface to its material form so a renderer reads one shape. */
|
|
139
|
+
export declare function resolveBuildingSurface(surface: BuildingSurface): BuildingSurfaceMaterial;
|
|
108
140
|
export type BuildingStyle = "generic" | "capital" | "village" | "desert" | "industrial" | "coastal" | "neon" | "ruin" | "frontier" | "aerial";
|
|
109
141
|
export declare const DEFAULT_BUILDING_STYLE: BuildingStyle;
|
|
110
|
-
export declare const BUILDING_STYLE_PALETTES: Record<BuildingStyle,
|
|
142
|
+
export declare const BUILDING_STYLE_PALETTES: Record<BuildingStyle, Record<BuildingPartKind, string>>;
|
|
111
143
|
/**
|
|
112
144
|
* Per-style facade-tone family: the wall colours a district spreads across its buildings so
|
|
113
145
|
* neighbours differ in hue and value while the block stays one palette. Index 0 is the palette's
|
package/dist/world/buildings.js
CHANGED
|
@@ -38,6 +38,16 @@ export const DEFAULT_BUILDING_CONFIG = {
|
|
|
38
38
|
probabilities: DEFAULT_PROBABILITIES,
|
|
39
39
|
variants: DEFAULT_VARIANTS,
|
|
40
40
|
};
|
|
41
|
+
/** The flat colour of a surface: the hex itself, or a material's `color`, else `fallback`. */
|
|
42
|
+
export function buildingSurfaceColor(surface, fallback = "#808080") {
|
|
43
|
+
if (surface === undefined)
|
|
44
|
+
return fallback;
|
|
45
|
+
return typeof surface === "string" ? surface : (surface.color ?? fallback);
|
|
46
|
+
}
|
|
47
|
+
/** Normalises a surface to its material form so a renderer reads one shape. */
|
|
48
|
+
export function resolveBuildingSurface(surface) {
|
|
49
|
+
return typeof surface === "string" ? { color: surface } : surface;
|
|
50
|
+
}
|
|
41
51
|
export const DEFAULT_BUILDING_STYLE = "generic";
|
|
42
52
|
export const BUILDING_STYLE_PALETTES = {
|
|
43
53
|
generic: {
|
|
@@ -207,7 +217,7 @@ export function resolveBuildingWallTones(style = DEFAULT_BUILDING_STYLE) {
|
|
|
207
217
|
const tones = BUILDING_STYLE_WALL_TONES[style];
|
|
208
218
|
if (tones !== undefined && tones.length > 0)
|
|
209
219
|
return tones;
|
|
210
|
-
return [resolveBuildingPalette(style).wall];
|
|
220
|
+
return [buildingSurfaceColor(resolveBuildingPalette(style).wall)];
|
|
211
221
|
}
|
|
212
222
|
/** @internal */
|
|
213
223
|
export function resolveBuildingPalette(style = DEFAULT_BUILDING_STYLE, overrides) {
|
package/dist/world/cityKind.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type ParamSchema, type SceneKindObject, type SceneKindResolveContext, t
|
|
|
2
2
|
import { type BuildingStyle } from "./buildings.js";
|
|
3
3
|
import { type CityLotClass, type CityLotPiece, type CityTreeSpecies, type CityZoneBand, type CityZoneProfile } from "./cityContent.js";
|
|
4
4
|
import { type CircuitCorner, type CircuitGate, type CircuitSealKind } from "./raceCircuit.js";
|
|
5
|
+
import type { WorldSolid } from "./worldSolids.js";
|
|
5
6
|
import type { RoadPoint } from "./roads.js";
|
|
6
7
|
import { type StreetFeatureKind } from "./streetGenerator.js";
|
|
7
8
|
import { type CityBlockKind } from "./cityBlocks.js";
|
|
@@ -432,6 +433,12 @@ export interface ResolvedCity {
|
|
|
432
433
|
/** The instanced street race through these streets, when `race` is on and a lap could be found. */
|
|
433
434
|
race?: ResolvedRace;
|
|
434
435
|
}
|
|
436
|
+
/**
|
|
437
|
+
* One solid per wall and roof piece of every lot, grounded the way the city renderer grounds it:
|
|
438
|
+
* grade at the highest lot corner, foundations down to half a metre under the lowest.
|
|
439
|
+
* @capability city-district editor-authorable procedural city district (streets, zoning, buildings, furniture)
|
|
440
|
+
*/
|
|
441
|
+
export declare function citySolids(city: ResolvedCity, sampleHeight?: (x: number, z: number) => number): WorldSolid[];
|
|
435
442
|
/** Extended resolve context: sibling `cityzone` volumes that override the band/mix locally. */
|
|
436
443
|
export interface CityResolveContext extends SceneKindResolveContext {
|
|
437
444
|
zoneOverrides?: readonly SceneKindObject[];
|
package/dist/world/cityKind.js
CHANGED
|
@@ -164,6 +164,7 @@ export const CITY_SCHEMA = {
|
|
|
164
164
|
options: [{ value: "auto" }, { value: "gravel" }],
|
|
165
165
|
},
|
|
166
166
|
{ type: "bool", key: "fabric", label: "buildings & parcels", group: "layout", default: CITY_DEFAULTS.fabric },
|
|
167
|
+
{ type: "bool", key: "solid", label: "solid buildings", group: "layout", default: true },
|
|
167
168
|
{ type: "bool", key: "bridges", label: "bridges over water", group: "layout", default: CITY_DEFAULTS.bridges },
|
|
168
169
|
{ type: "bool", key: "tunnels", label: "tunnels through ridges", group: "layout", default: CITY_DEFAULTS.tunnels },
|
|
169
170
|
{
|
|
@@ -638,6 +639,55 @@ export function readCityRules(meta) {
|
|
|
638
639
|
seed: params["seed"],
|
|
639
640
|
};
|
|
640
641
|
}
|
|
642
|
+
/** Lot pieces that block: walls and roofs. Trim and accents (cornices, awnings) stay passable. */
|
|
643
|
+
const SOLID_PIECE_ROLES = new Set(["wall", "roof"]);
|
|
644
|
+
/**
|
|
645
|
+
* One solid per wall and roof piece of every lot, grounded the way the city renderer grounds it:
|
|
646
|
+
* grade at the highest lot corner, foundations down to half a metre under the lowest.
|
|
647
|
+
* @capability city-district editor-authorable procedural city district (streets, zoning, buildings, furniture)
|
|
648
|
+
*/
|
|
649
|
+
export function citySolids(city, sampleHeight = () => 0) {
|
|
650
|
+
const solids = [];
|
|
651
|
+
for (const lot of city.lots) {
|
|
652
|
+
const c = Math.cos(lot.rotationY);
|
|
653
|
+
const s = Math.sin(lot.rotationY);
|
|
654
|
+
const hw = lot.size[0] / 2;
|
|
655
|
+
const hd = lot.size[1] / 2;
|
|
656
|
+
let minY = Infinity;
|
|
657
|
+
let maxY = -Infinity;
|
|
658
|
+
for (const [dx, dz] of [
|
|
659
|
+
[hw, hd],
|
|
660
|
+
[hw, -hd],
|
|
661
|
+
[-hw, hd],
|
|
662
|
+
[-hw, -hd],
|
|
663
|
+
]) {
|
|
664
|
+
const y = sampleHeight(lot.center[0] + dx * c + dz * s, lot.center[1] - dx * s + dz * c);
|
|
665
|
+
minY = Math.min(minY, y);
|
|
666
|
+
maxY = Math.max(maxY, y);
|
|
667
|
+
}
|
|
668
|
+
const grade = maxY;
|
|
669
|
+
const foundationBase = minY - 0.5;
|
|
670
|
+
for (const piece of lot.pieces) {
|
|
671
|
+
if (!SOLID_PIECE_ROLES.has(piece.role))
|
|
672
|
+
continue;
|
|
673
|
+
const bottom = piece.grounded ? foundationBase : grade + piece.offset[1];
|
|
674
|
+
const top = piece.grounded ? grade + piece.size[1] : grade + piece.offset[1] + piece.size[1];
|
|
675
|
+
if (top - bottom <= 0 || piece.size[0] <= 0 || piece.size[2] <= 0)
|
|
676
|
+
continue;
|
|
677
|
+
const yaw = lot.rotationY + piece.rotationY;
|
|
678
|
+
solids.push({
|
|
679
|
+
center: [
|
|
680
|
+
lot.center[0] + piece.offset[0] * c + piece.offset[2] * s,
|
|
681
|
+
(bottom + top) / 2,
|
|
682
|
+
lot.center[1] - piece.offset[0] * s + piece.offset[2] * c,
|
|
683
|
+
],
|
|
684
|
+
halfExtents: [piece.size[0] / 2, (top - bottom) / 2, piece.size[2] / 2],
|
|
685
|
+
...(yaw === 0 ? {} : { rotationY: yaw }),
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return solids;
|
|
690
|
+
}
|
|
641
691
|
/** Bounded-work caps so a huge volume can never generate unbounded content. */
|
|
642
692
|
const MAX_STREETS = 320;
|
|
643
693
|
/** Building budget — the shared scatterable cap so the inspector's "capped at N (budget)" note is truthful. */
|
|
@@ -1302,7 +1352,7 @@ export function resolveCityObject(object, context) {
|
|
|
1302
1352
|
return Math.hypot(dx, dz) / (2 * d);
|
|
1303
1353
|
};
|
|
1304
1354
|
// Child `cityzone` volumes override the band (and optionally the class mix) for lots inside them.
|
|
1305
|
-
const overrides = (context?.zoneOverrides ?? []).filter((volume) => volume.kind === CITY_ZONE_KIND);
|
|
1355
|
+
const overrides = (context?.zoneOverrides ?? context?.objects ?? []).filter((volume) => volume.kind === CITY_ZONE_KIND);
|
|
1306
1356
|
const overrideAt = overrides.length === 0
|
|
1307
1357
|
? null
|
|
1308
1358
|
: (x, z) => {
|
|
@@ -1789,6 +1839,7 @@ export function registerCityKind() {
|
|
|
1789
1839
|
schema: CITY_SCHEMA,
|
|
1790
1840
|
coverage: { spec: "city", densityKey: "roadsideOccupancy" },
|
|
1791
1841
|
resolve: (object, _params, context) => resolveCityObject(object, context),
|
|
1842
|
+
solids: (resolved, _object, params, context) => resolved === null || params["solid"] === false ? [] : citySolids(resolved, context.sampleHeight),
|
|
1792
1843
|
note: (object) => {
|
|
1793
1844
|
const resolved = resolveCityObject(object);
|
|
1794
1845
|
if (resolved === null)
|
package/dist/world/features.d.ts
CHANGED
|
@@ -296,16 +296,38 @@ export interface OceanEnvironmentConfig {
|
|
|
296
296
|
waveSpeed?: number;
|
|
297
297
|
color?: string;
|
|
298
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Where the sun sits, as a compass bearing and a height above the horizon, both in degrees.
|
|
301
|
+
* `azimuth` 0 points toward -Z (north), 90 toward +X (east); `elevation` 90 is straight overhead.
|
|
302
|
+
*/
|
|
303
|
+
export interface SkySunConfig {
|
|
304
|
+
/** Compass bearing of the sun in degrees; 0 = -Z, 90 = +X. Default 0. */
|
|
305
|
+
azimuth?: number;
|
|
306
|
+
/** Height above the horizon in degrees; 0 sits on the horizon, 90 is overhead. Default 68. */
|
|
307
|
+
elevation?: number;
|
|
308
|
+
}
|
|
299
309
|
export interface SkyEnvironmentConfig {
|
|
300
310
|
/** Fixed look used when `timeOfDay` is off (or no clock is available); default "day". */
|
|
301
311
|
preset?: "day" | "dusk" | "night";
|
|
302
312
|
/** Drive sun/sky from the world clock's `calendar().dayFraction` instead of the fixed `preset`. */
|
|
303
313
|
timeOfDay?: boolean;
|
|
314
|
+
/**
|
|
315
|
+
* Horizon tint at noon. Under `timeOfDay` this only sets the noon keyframe; dawn, dusk, and night
|
|
316
|
+
* still crossfade through the engine presets, so an authored tint is visible for part of the day.
|
|
317
|
+
*/
|
|
304
318
|
horizonColor?: string;
|
|
319
|
+
/** Zenith tint at noon; same `timeOfDay` crossfade caveat as `horizonColor`. */
|
|
305
320
|
zenithColor?: string;
|
|
306
321
|
sunIntensity?: number;
|
|
307
322
|
ambientIntensity?: number;
|
|
308
|
-
/**
|
|
323
|
+
/**
|
|
324
|
+
* Sun placement for the dome glow and the sky-owned sun light. With a fixed `preset` this is the
|
|
325
|
+
* sun's exact direction; under `timeOfDay` it is where the sun stands at noon and the day arc
|
|
326
|
+
* swings around that bearing. Unset with an authored `lighting.directional` light, the shell takes
|
|
327
|
+
* the first directional light's direction so the dome sun agrees with the shadows.
|
|
328
|
+
*/
|
|
329
|
+
sun?: SkySunConfig;
|
|
330
|
+
/** Sky-dome sphere radius in world units. Enlarge for a playfield sited far from the origin so the viewer never exits the dome. Must stay below the camera far plane or the dome depth-clips to black. Default 260. */
|
|
309
331
|
radius?: number;
|
|
310
332
|
/** Horizon haze-band strength: 0 removes the dusty band, ~1 makes it heavy. Default 0.5. */
|
|
311
333
|
hazeStrength?: number;
|
|
@@ -377,6 +399,11 @@ export interface BuildingEnvironmentConfig {
|
|
|
377
399
|
* untextured block; without a kit the whole facade stays blocks, as before.
|
|
378
400
|
*/
|
|
379
401
|
kit?: BuildingKit;
|
|
402
|
+
/**
|
|
403
|
+
* Whether the buildings block movement and physics through `ctx.world.solids` (default `true`).
|
|
404
|
+
* `false` keeps them render-only, e.g. a distant skyline nobody can reach.
|
|
405
|
+
*/
|
|
406
|
+
solid?: boolean;
|
|
380
407
|
}
|
|
381
408
|
export type PadSize = readonly [number, number] | {
|
|
382
409
|
radius: number;
|
|
@@ -418,7 +445,7 @@ export type OceanEnvironmentDescriptor = {
|
|
|
418
445
|
} & Required<Pick<OceanEnvironmentConfig, "bounds" | "level" | "waveHeight" | "waveScale" | "waveSpeed" | "color">> & Pick<OceanEnvironmentConfig, "position" | "levelAt">;
|
|
419
446
|
export type BuildingEnvironmentDescriptor = {
|
|
420
447
|
kind: "building";
|
|
421
|
-
} & Required<Pick<BuildingEnvironmentConfig, "count" | "footprint" | "stories" | "storyHeight" | "spacing" | "style">> & Pick<BuildingEnvironmentConfig, "seed" | "position" | "palette" | "along" | "kit">;
|
|
448
|
+
} & Required<Pick<BuildingEnvironmentConfig, "count" | "footprint" | "stories" | "storyHeight" | "spacing" | "style">> & Pick<BuildingEnvironmentConfig, "seed" | "position" | "palette" | "along" | "kit" | "solid">;
|
|
422
449
|
export type PadEnvironmentDescriptor = {
|
|
423
450
|
kind: "pad";
|
|
424
451
|
} & Required<Pick<PadEnvironmentConfig, "center" | "size" | "height" | "color">> & Pick<PadEnvironmentConfig, "rotationY" | "elevation">;
|
package/dist/world/features.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { warnOnce } from "../devtools/warnOnce.js";
|
|
1
2
|
function list(value) {
|
|
2
3
|
if (value === undefined) {
|
|
3
4
|
return undefined;
|
|
@@ -110,6 +111,9 @@ export function terrain(config = {}) {
|
|
|
110
111
|
});
|
|
111
112
|
}
|
|
112
113
|
export function sky(config = {}) {
|
|
114
|
+
if (config.timeOfDay === true && (config.zenithColor !== undefined || config.horizonColor !== undefined)) {
|
|
115
|
+
warnOnce("sky-timeofday-tint", "[jgengine] sky(): zenithColor/horizonColor only set the noon keyframe under timeOfDay; dawn, dusk, and night crossfade through the engine presets. Drop timeOfDay for a fixed tint.");
|
|
116
|
+
}
|
|
113
117
|
return withOptional({
|
|
114
118
|
kind: "sky",
|
|
115
119
|
preset: config.preset ?? "day",
|
|
@@ -119,6 +123,7 @@ export function sky(config = {}) {
|
|
|
119
123
|
...(config.zenithColor === undefined ? {} : { zenithColor: config.zenithColor }),
|
|
120
124
|
...(config.sunIntensity === undefined ? {} : { sunIntensity: config.sunIntensity }),
|
|
121
125
|
...(config.ambientIntensity === undefined ? {} : { ambientIntensity: config.ambientIntensity }),
|
|
126
|
+
...(config.sun === undefined ? {} : { sun: { ...config.sun } }),
|
|
122
127
|
...(config.radius === undefined ? {} : { radius: config.radius }),
|
|
123
128
|
...(config.hazeStrength === undefined ? {} : { hazeStrength: config.hazeStrength }),
|
|
124
129
|
...(config.sunGlowStrength === undefined ? {} : { sunGlowStrength: config.sunGlowStrength }),
|
|
@@ -253,6 +258,7 @@ export function building(config = {}) {
|
|
|
253
258
|
...(config.palette === undefined ? {} : { palette: config.palette }),
|
|
254
259
|
...(config.along === undefined ? {} : { along: config.along }),
|
|
255
260
|
...(config.kit === undefined ? {} : { kit: config.kit }),
|
|
261
|
+
...(config.solid === undefined ? {} : { solid: config.solid }),
|
|
256
262
|
});
|
|
257
263
|
}
|
|
258
264
|
export function pad(config) {
|
|
@@ -16,6 +16,10 @@ export interface FootprintReservation {
|
|
|
16
16
|
kind: string;
|
|
17
17
|
cells: readonly GridCell[];
|
|
18
18
|
}
|
|
19
|
+
/** Plain JSON state of a {@link FootprintGrid}: live reservations in claim order. */
|
|
20
|
+
export interface FootprintGridState {
|
|
21
|
+
reservations: FootprintReservation[];
|
|
22
|
+
}
|
|
19
23
|
/** Handle returned by {@link createFootprintGrid}. */
|
|
20
24
|
export interface FootprintGrid {
|
|
21
25
|
readonly cellSize: number;
|
|
@@ -30,6 +34,9 @@ export interface FootprintGrid {
|
|
|
30
34
|
reservationOf(id: string): FootprintReservation | null;
|
|
31
35
|
list(): readonly FootprintReservation[];
|
|
32
36
|
clear(): void;
|
|
37
|
+
snapshot(): FootprintGridState;
|
|
38
|
+
/** Replaces every reservation with `next`'s and rebuilds cell occupancy from them. */
|
|
39
|
+
restore(next: FootprintGridState): void;
|
|
33
40
|
}
|
|
34
41
|
/**
|
|
35
42
|
* Multi-cell footprint occupancy/reservation on a shared build grid — `world/placementController`
|