@jgengine/core 0.11.0 → 0.12.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 +26 -0
- package/dist/ai/populationDirector.d.ts +103 -0
- package/dist/ai/populationDirector.js +190 -0
- package/dist/ai/pursuit.d.ts +56 -0
- package/dist/ai/pursuit.js +53 -0
- package/dist/authoring.d.ts +1 -1
- package/dist/authoring.js +1 -1
- package/dist/combat.d.ts +1 -0
- package/dist/combat.js +1 -0
- package/dist/devtools/controls.d.ts +38 -0
- package/dist/devtools/controls.js +181 -0
- package/dist/devtools/devtools.d.ts +5 -197
- package/dist/devtools/devtools.js +38 -774
- package/dist/devtools/discover.d.ts +32 -0
- package/dist/devtools/discover.js +240 -0
- package/dist/devtools/fallbackSeams.d.ts +50 -0
- package/dist/devtools/fallbackSeams.js +111 -0
- package/dist/devtools/frame.d.ts +15 -0
- package/dist/devtools/frame.js +247 -0
- package/dist/devtools/logs.d.ts +15 -0
- package/dist/devtools/logs.js +86 -0
- package/dist/devtools/overrides.d.ts +16 -0
- package/dist/devtools/overrides.js +66 -0
- package/dist/devtools/types.d.ts +212 -0
- package/dist/devtools/types.js +1 -0
- package/dist/economy/auctionBook.d.ts +122 -0
- package/dist/economy/auctionBook.js +213 -0
- package/dist/economy/priceHistory.d.ts +44 -0
- package/dist/economy/priceHistory.js +89 -0
- package/dist/economy/unlockPoints.d.ts +92 -0
- package/dist/economy/unlockPoints.js +103 -0
- package/dist/editor/commandApply.d.ts +5 -0
- package/dist/editor/commandApply.js +755 -0
- package/dist/editor/commands.d.ts +48 -3
- package/dist/editor/commands.js +40 -754
- package/dist/editor/directives.d.ts +94 -0
- package/dist/editor/directives.js +206 -0
- package/dist/editor/document.d.ts +37 -2
- package/dist/editor/document.js +422 -6
- package/dist/editor/entityCatalog.d.ts +42 -0
- package/dist/editor/entityCatalog.js +101 -0
- package/dist/editor/environment.d.ts +74 -0
- package/dist/editor/environment.js +169 -0
- package/dist/editor/index.d.ts +26 -1
- package/dist/editor/index.js +8 -1
- package/dist/editor/minimap.d.ts +31 -0
- package/dist/editor/minimap.js +73 -0
- package/dist/editor/streamer.d.ts +66 -0
- package/dist/editor/streamer.js +68 -0
- package/dist/editor/types.d.ts +143 -0
- package/dist/editor/world.d.ts +136 -0
- package/dist/editor/world.js +303 -0
- package/dist/game/breeding.d.ts +161 -0
- package/dist/game/breeding.js +181 -0
- package/dist/game/cameraConfig.d.ts +451 -0
- package/dist/game/cameraConfig.js +1 -0
- package/dist/game/defineGame.d.ts +16 -9
- package/dist/game/defineGame.js +14 -6
- package/dist/game/defineSystem.d.ts +2 -0
- package/dist/game/defineSystem.js +2 -0
- package/dist/game/events.d.ts +28 -0
- package/dist/game/playableGame.d.ts +34 -435
- package/dist/game/playableGame.js +1 -1
- package/dist/game/progression.d.ts +35 -0
- package/dist/game/progression.js +19 -0
- package/dist/game/quest.js +1 -3
- package/dist/game/tribe.d.ts +184 -0
- package/dist/game/tribe.js +432 -0
- package/dist/gameplay.d.ts +3 -1
- package/dist/gameplay.js +3 -1
- package/dist/inventory/encumbrance.d.ts +69 -0
- package/dist/inventory/encumbrance.js +68 -0
- package/dist/item/durability.js +1 -3
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +24 -1
- package/dist/movement/movementModel.d.ts +15 -1
- package/dist/movement/movementModel.js +33 -15
- package/dist/movement/playerMovement.js +102 -2
- package/dist/multiplayer/combatSnapshot.js +13 -8
- package/dist/multiplayer/presenceModel.d.ts +2 -0
- package/dist/multiplayer/presenceModel.js +8 -0
- package/dist/physics/drivableVehicle.d.ts +2 -0
- package/dist/physics/drivableVehicle.js +1 -1
- package/dist/physics/flightDynamics.d.ts +82 -0
- package/dist/physics/flightDynamics.js +165 -0
- package/dist/physics/gravityField.d.ts +25 -0
- package/dist/physics/gravityField.js +41 -0
- package/dist/physics/kinematicVehicle.d.ts +87 -0
- package/dist/physics/kinematicVehicle.js +193 -11
- package/dist/physics/physicsWorld.d.ts +26 -0
- package/dist/physics/physicsWorld.js +109 -117
- package/dist/physics/vehicleObstacles.d.ts +44 -0
- package/dist/physics/vehicleObstacles.js +115 -0
- package/dist/random/rng.d.ts +4 -1
- package/dist/random/rng.js +4 -1
- package/dist/runtime/adapter.d.ts +13 -0
- package/dist/runtime/adapter.js +13 -0
- package/dist/runtime/context/combat.d.ts +51 -0
- package/dist/runtime/context/combat.js +130 -0
- package/dist/runtime/context/deathLoot.d.ts +30 -0
- package/dist/runtime/context/deathLoot.js +39 -0
- package/dist/runtime/context/player.d.ts +48 -0
- package/dist/runtime/context/player.js +125 -0
- package/dist/runtime/context/scene.d.ts +59 -0
- package/dist/runtime/context/scene.js +291 -0
- package/dist/runtime/context/snapshotCodecs.d.ts +12 -0
- package/dist/runtime/context/snapshotCodecs.js +26 -0
- package/dist/runtime/descriptors/baseline.js +26 -3
- package/dist/runtime/descriptors/features.d.ts +5 -0
- package/dist/runtime/descriptors/features.js +20 -2
- package/dist/runtime/gameContext.d.ts +2 -428
- package/dist/runtime/gameContext.js +88 -501
- package/dist/runtime/gameContextTypes.d.ts +467 -0
- package/dist/runtime/gameContextTypes.js +1 -0
- package/dist/runtime/headlessRunner.d.ts +4 -2
- package/dist/runtime/headlessRunner.js +1 -0
- package/dist/runtime/hostPolicy.d.ts +51 -0
- package/dist/runtime/hostPolicy.js +68 -0
- package/dist/runtime/hostedGameRunner.d.ts +3 -1
- package/dist/runtime/hostedGameRunner.js +1 -0
- package/dist/runtime/hostedWorldSession.d.ts +3 -1
- package/dist/runtime/hostedWorldSession.js +1 -0
- package/dist/runtime/worldSnapshot.d.ts +13 -1
- package/dist/runtime/worldSnapshot.js +15 -3
- package/dist/scene/assetCatalog.d.ts +5 -0
- package/dist/scene/assetSpace.js +1 -3
- package/dist/scene/authoredTriggers.d.ts +50 -0
- package/dist/scene/authoredTriggers.js +114 -0
- package/dist/scene/builtinSceneKinds.js +8 -3
- package/dist/scene/colliders.d.ts +58 -0
- package/dist/scene/colliders.js +108 -0
- package/dist/scene/collisionMesh.d.ts +94 -0
- package/dist/scene/collisionMesh.js +699 -0
- package/dist/scene/companion.d.ts +147 -0
- package/dist/scene/companion.js +193 -0
- package/dist/scene/entityStats.d.ts +7 -9
- package/dist/scene/entityStats.js +10 -14
- package/dist/scene/sceneKinds.d.ts +16 -0
- package/dist/scene/sceneKinds.js +4 -0
- package/dist/scene/sceneRaycast.js +15 -0
- package/dist/stats/spawnLevelStats.d.ts +110 -0
- package/dist/stats/spawnLevelStats.js +140 -0
- package/dist/stats/statPool.d.ts +70 -0
- package/dist/stats/statPool.js +59 -0
- package/dist/store/changeSignal.d.ts +3 -0
- package/dist/store/changeSignal.js +2 -0
- package/dist/store/observableKeyedStore.d.ts +2 -0
- package/dist/store/observableKeyedStore.js +1 -0
- package/dist/survival/decayMeter.js +1 -3
- package/dist/survival/regionHealth.js +1 -3
- package/dist/ui/hudLayout.d.ts +5 -0
- package/dist/ui/hudLayout.js +17 -1
- package/dist/world/authoredEntities.d.ts +29 -0
- package/dist/world/authoredEntities.js +29 -0
- package/dist/world/authoredObjects.d.ts +15 -1
- package/dist/world/authoredObjects.js +13 -1
- package/dist/world/authoredSpawn.d.ts +39 -0
- package/dist/world/authoredSpawn.js +37 -0
- package/dist/world/buildSockets.d.ts +152 -0
- package/dist/world/buildSockets.js +188 -0
- package/dist/world/cityBlocks.d.ts +152 -0
- package/dist/world/cityBlocks.js +670 -0
- package/dist/world/cityContent.d.ts +77 -0
- package/dist/world/cityContent.js +261 -0
- package/dist/world/cityGeometry.d.ts +114 -0
- package/dist/world/cityGeometry.js +573 -0
- package/dist/world/cityKind.d.ts +314 -0
- package/dist/world/cityKind.js +1578 -0
- package/dist/world/features.d.ts +18 -3
- package/dist/world/features.js +10 -1
- package/dist/world/grassKind.js +2 -2
- package/dist/world/markers.d.ts +50 -4
- package/dist/world/markers.js +49 -0
- package/dist/world/massing.js +1 -1
- package/dist/world/minimapBake.d.ts +78 -0
- package/dist/world/minimapBake.js +208 -0
- package/dist/world/minimapTrack.d.ts +17 -0
- package/dist/world/minimapTrack.js +22 -0
- package/dist/world/pathNetwork.d.ts +159 -0
- package/dist/world/pathNetwork.js +684 -0
- package/dist/world/place.d.ts +166 -0
- package/dist/world/place.js +96 -0
- package/dist/world/placeAsset.js +3 -1
- package/dist/world/poleLineKind.d.ts +40 -0
- package/dist/world/poleLineKind.js +97 -0
- package/dist/world/polyline.js +9 -2
- package/dist/world/resourceNode.d.ts +109 -0
- package/dist/world/resourceNode.js +140 -0
- package/dist/world/scatter.js +2 -20
- package/dist/world/streets.d.ts +29 -0
- package/dist/world/streets.js +55 -0
- package/dist/world/terraform.js +58 -88
- package/dist/world/terrain.d.ts +45 -0
- package/dist/world/terrain.js +78 -0
- package/dist/world.d.ts +14 -5
- package/dist/world.js +12 -3
- package/package.json +1 -1
- package/skills/game-design/SKILL.md +28 -0
- package/skills/game-design/agents/openai.yaml +4 -0
- package/skills/game-design/references/application-playbook.md +170 -0
- package/skills/game-design/references/field-guide.md +288 -0
- package/skills/jgengine/SKILL.md +7 -4
- package/skills/jgengine/api.md +172 -79
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-combat/SKILL.md +5 -1
- package/skills/jgengine-combat/api.md +36 -0
- package/skills/jgengine-combat/capabilities.md +44 -0
- package/skills/jgengine-combat/recipes/portable-stat-pools.md +122 -0
- package/skills/jgengine-combat/reference.md +12 -2
- package/skills/jgengine-gameplay/api.md +152 -26
- package/skills/jgengine-gameplay/capabilities.md +78 -2
- package/skills/jgengine-gameplay/reference-systems.md +1 -5
- package/skills/jgengine-world/SKILL.md +13 -0
- package/skills/jgengine-world/api.md +419 -15
- package/skills/jgengine-world/capabilities.md +108 -2
- package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
- package/skills/jgengine-world/reference.md +43 -4
- package/skills/jgengine/reference-cartridge.md +0 -72
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { clamp } from "../math/scalar.js";
|
|
2
|
+
function resolveQuantity(quantity, rng) {
|
|
3
|
+
if (typeof quantity === "number")
|
|
4
|
+
return quantity;
|
|
5
|
+
const [lo, hi] = quantity;
|
|
6
|
+
return lo + rng() * (hi - lo);
|
|
7
|
+
}
|
|
8
|
+
function toState(node) {
|
|
9
|
+
return {
|
|
10
|
+
id: node.def.id,
|
|
11
|
+
budget: node.budget,
|
|
12
|
+
maxBudget: node.maxBudget,
|
|
13
|
+
depleted: node.depleted,
|
|
14
|
+
respawnTimer: node.respawnTimer,
|
|
15
|
+
respawnBlocked: node.respawnBlocked,
|
|
16
|
+
fraction: node.maxBudget > 0 ? clamp(node.budget / node.maxBudget, 0, 1) : 0,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A field of depletable, respawning harvest nodes with tool-dependent yields — the generic rock / tree /
|
|
21
|
+
* bush / vein / corpse. Each node holds a finite budget across one or more resource kinds. A harvest applies
|
|
22
|
+
* a {@link ToolProfile} whose per-resource biases decide *which* resources and *how much* the same node
|
|
23
|
+
* returns (pick favors ore, hatchet favors wood), scaled by a creature/harvester multiplier and optional
|
|
24
|
+
* deterministic rng variance; the budget drops by the tool's power. When a node empties it starts a respawn
|
|
25
|
+
* timer that `tick(dt)` counts down and refills — unless respawn is suppressed by the per-node flag or the
|
|
26
|
+
* injected predicate (a structure claimed the spot). All state is plain and serializable for save-load and
|
|
27
|
+
* multiplayer sync via {@link snapshot}/{@link hydrate}.
|
|
28
|
+
*
|
|
29
|
+
* @capability resource-node depletable, respawning harvest nodes with tool-dependent yields
|
|
30
|
+
*/
|
|
31
|
+
export function createResourceNodeField(config) {
|
|
32
|
+
const rng = config.rng ?? Math.random;
|
|
33
|
+
const nodes = new Map();
|
|
34
|
+
const order = [];
|
|
35
|
+
for (const def of config.nodes) {
|
|
36
|
+
if (nodes.has(def.id))
|
|
37
|
+
throw new Error(`duplicate resource node "${def.id}"`);
|
|
38
|
+
const respawnBudget = def.respawnBudget ?? def.budget;
|
|
39
|
+
nodes.set(def.id, {
|
|
40
|
+
def,
|
|
41
|
+
budget: def.budget,
|
|
42
|
+
maxBudget: def.budget,
|
|
43
|
+
respawnBudget,
|
|
44
|
+
depleted: def.budget <= 0,
|
|
45
|
+
respawnTimer: 0,
|
|
46
|
+
respawnBlocked: false,
|
|
47
|
+
});
|
|
48
|
+
order.push(def.id);
|
|
49
|
+
}
|
|
50
|
+
const getNode = (id) => {
|
|
51
|
+
const node = nodes.get(id);
|
|
52
|
+
if (node === undefined)
|
|
53
|
+
throw new Error(`unknown resource node "${id}"`);
|
|
54
|
+
return node;
|
|
55
|
+
};
|
|
56
|
+
const isSuppressed = (node) => {
|
|
57
|
+
if (node.respawnBlocked)
|
|
58
|
+
return true;
|
|
59
|
+
if (config.respawnSuppressed === undefined)
|
|
60
|
+
return false;
|
|
61
|
+
return config.respawnSuppressed(toState(node));
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
harvest(nodeId, tool, options = {}) {
|
|
65
|
+
const node = getNode(nodeId);
|
|
66
|
+
if (node.depleted || node.budget <= 0) {
|
|
67
|
+
return { nodeId, granted: [], spent: 0, harvested: false, depleted: node.depleted };
|
|
68
|
+
}
|
|
69
|
+
const power = tool.power !== undefined && tool.power > 0 ? tool.power : 1;
|
|
70
|
+
const spent = Math.min(power, node.budget);
|
|
71
|
+
const multiplier = options.multiplier ?? 1;
|
|
72
|
+
const defaultBias = tool.defaultBias ?? 1;
|
|
73
|
+
const granted = [];
|
|
74
|
+
for (const resource of node.def.resources) {
|
|
75
|
+
// Resolve variance for every resource so the rng stream stays independent of tool choice.
|
|
76
|
+
const base = resolveQuantity(resource.amount, rng);
|
|
77
|
+
const bias = tool.biases?.[resource.kind] ?? defaultBias;
|
|
78
|
+
const amount = base * bias * multiplier;
|
|
79
|
+
if (amount > 0)
|
|
80
|
+
granted.push({ kind: resource.kind, amount });
|
|
81
|
+
}
|
|
82
|
+
node.budget -= spent;
|
|
83
|
+
let depleted = false;
|
|
84
|
+
if (node.budget <= 0) {
|
|
85
|
+
node.budget = 0;
|
|
86
|
+
node.depleted = true;
|
|
87
|
+
depleted = true;
|
|
88
|
+
node.respawnTimer = node.def.respawn ?? 0;
|
|
89
|
+
}
|
|
90
|
+
return { nodeId, granted, spent, harvested: true, depleted };
|
|
91
|
+
},
|
|
92
|
+
tick(dt) {
|
|
93
|
+
if (dt <= 0)
|
|
94
|
+
return;
|
|
95
|
+
for (const id of order) {
|
|
96
|
+
const node = nodes.get(id);
|
|
97
|
+
if (!node.depleted || node.def.respawn === undefined)
|
|
98
|
+
continue;
|
|
99
|
+
if (isSuppressed(node))
|
|
100
|
+
continue;
|
|
101
|
+
node.respawnTimer -= dt;
|
|
102
|
+
if (node.respawnTimer <= 0) {
|
|
103
|
+
node.budget = node.respawnBudget;
|
|
104
|
+
node.depleted = false;
|
|
105
|
+
node.respawnTimer = 0;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
state(nodeId) {
|
|
110
|
+
return toState(getNode(nodeId));
|
|
111
|
+
},
|
|
112
|
+
isDepleted(nodeId) {
|
|
113
|
+
return getNode(nodeId).depleted;
|
|
114
|
+
},
|
|
115
|
+
setRespawnBlocked(nodeId, blocked) {
|
|
116
|
+
getNode(nodeId).respawnBlocked = blocked;
|
|
117
|
+
},
|
|
118
|
+
ids() {
|
|
119
|
+
return order;
|
|
120
|
+
},
|
|
121
|
+
snapshot() {
|
|
122
|
+
const out = {};
|
|
123
|
+
for (const id of order)
|
|
124
|
+
out[id] = toState(nodes.get(id));
|
|
125
|
+
return out;
|
|
126
|
+
},
|
|
127
|
+
hydrate(snapshot) {
|
|
128
|
+
for (const id of order) {
|
|
129
|
+
const saved = snapshot[id];
|
|
130
|
+
if (saved === undefined)
|
|
131
|
+
continue;
|
|
132
|
+
const node = nodes.get(id);
|
|
133
|
+
node.budget = saved.budget;
|
|
134
|
+
node.depleted = saved.depleted;
|
|
135
|
+
node.respawnTimer = saved.respawnTimer;
|
|
136
|
+
node.respawnBlocked = saved.respawnBlocked;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
package/dist/world/scatter.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { expandAabb, pointInAabb } from "./geometry.js";
|
|
2
|
+
import { seededRng } from "../random/rng.js";
|
|
2
3
|
function isAabb(area) {
|
|
3
4
|
return (typeof area.minX === "number" &&
|
|
4
5
|
typeof area.minZ === "number" &&
|
|
@@ -15,25 +16,6 @@ export function scatterAabb(area) {
|
|
|
15
16
|
const halfD = area.d / 2;
|
|
16
17
|
return { minX: cx - halfW, minZ: cz - halfD, maxX: cx + halfW, maxZ: cz + halfD };
|
|
17
18
|
}
|
|
18
|
-
function hashSeed(seed) {
|
|
19
|
-
const text = typeof seed === "number" ? seed.toString() : seed;
|
|
20
|
-
let h = 2166136261 >>> 0;
|
|
21
|
-
for (let i = 0; i < text.length; i++) {
|
|
22
|
-
h ^= text.charCodeAt(i);
|
|
23
|
-
h = Math.imul(h, 16777619);
|
|
24
|
-
}
|
|
25
|
-
return h >>> 0;
|
|
26
|
-
}
|
|
27
|
-
function mulberry32(state) {
|
|
28
|
-
let a = state >>> 0;
|
|
29
|
-
return () => {
|
|
30
|
-
a = (a + 0x6d2b79f5) >>> 0;
|
|
31
|
-
let t = a;
|
|
32
|
-
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
33
|
-
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
34
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
19
|
export function scatter(config) {
|
|
38
20
|
const aabb = scatterAabb(config.area);
|
|
39
21
|
const width = aabb.maxX - aabb.minX;
|
|
@@ -47,7 +29,7 @@ export function scatter(config) {
|
|
|
47
29
|
const jitter = config.jitter ?? 1;
|
|
48
30
|
const maxAttempts = config.maxAttempts ?? target * 30;
|
|
49
31
|
const avoid = (config.avoid ?? []).map((a) => (avoidMargin !== 0 ? expandAabb(a, avoidMargin) : a));
|
|
50
|
-
const rng =
|
|
32
|
+
const rng = seededRng(config.seed ?? "scatter");
|
|
51
33
|
const useDistance = minDistance > 0;
|
|
52
34
|
const buckets = new Map();
|
|
53
35
|
const bucketKey = (x, z) => `${Math.floor(x / minDistance)}:${Math.floor(z / minDistance)}`;
|
package/dist/world/streets.d.ts
CHANGED
|
@@ -73,6 +73,35 @@ export interface ParkingSpot {
|
|
|
73
73
|
* coordinates in the middle of the carriageway.
|
|
74
74
|
*/
|
|
75
75
|
export declare function parkingSpots(road: RoadEnvironmentDescriptor, options?: ParkingSpotOptions): readonly ParkingSpot[];
|
|
76
|
+
/**
|
|
77
|
+
* Signed planar distance from `(x, z)` to the nearest road *surface edge* across every segment of
|
|
78
|
+
* every road (#1051): negative inside the asphalt (magnitude = how far in from the closest edge, so
|
|
79
|
+
* `-width/2` on the centerline), `~0` right at the curb, and positive off the road (the gap to the
|
|
80
|
+
* nearest edge). Point-to-segment distance minus the road's half width, minimised over all segments —
|
|
81
|
+
* the "am I on the road, and by how much" seam driving grip, tyre audio, and off-road penalties.
|
|
82
|
+
*
|
|
83
|
+
* Allocation-free per call (only scalars — no arrays or objects) so a per-tick car sim can sample it
|
|
84
|
+
* on the hot path; degenerate roads (fewer than two path points) contribute nothing, and with no road
|
|
85
|
+
* surface at all it returns {@link Number.POSITIVE_INFINITY} (everywhere is off-road).
|
|
86
|
+
*/
|
|
87
|
+
export declare function distanceToRoadEdge(roads: readonly RoadEnvironmentDescriptor[], x: number, z: number): number;
|
|
88
|
+
/** Grip levels and blend width for {@link roadSurfaceSampler}; each field is optional and defaulted. */
|
|
89
|
+
export interface RoadSurfaceOptions {
|
|
90
|
+
/** Grip multiplier on the asphalt. Default 1. */
|
|
91
|
+
onRoad?: number;
|
|
92
|
+
/** Grip multiplier once fully off the road past the shoulder. Default 0.72. */
|
|
93
|
+
offRoad?: number;
|
|
94
|
+
/** Width (world units) of the blend band outside the curb from full to off-road grip. Default 3. */
|
|
95
|
+
shoulder?: number;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A `surfaceFriction`-compatible sampler (the `(x, z) => grip` hook a kinematic car reads each tick)
|
|
99
|
+
* that turns road geometry into grip (#1051): full `onRoad` grip on the asphalt, a monotonic linear ramp
|
|
100
|
+
* down across a `shoulder`-wide band at the curb, and flat `offRoad` grip beyond it — so a car that
|
|
101
|
+
* clips onto the grass loses bite progressively instead of snapping. Closes over {@link
|
|
102
|
+
* distanceToRoadEdge}, so the returned sampler is itself allocation-free per call.
|
|
103
|
+
*/
|
|
104
|
+
export declare function roadSurfaceSampler(roads: readonly RoadEnvironmentDescriptor[], options?: RoadSurfaceOptions): (x: number, z: number) => number;
|
|
76
105
|
/**
|
|
77
106
|
* A deterministic point on one of a road's sidewalks at a normalized position — `side` picks the
|
|
78
107
|
* band, `fraction` (0..1) picks how far along. The canonical pedestrian spawn helper.
|
package/dist/world/streets.js
CHANGED
|
@@ -130,6 +130,61 @@ export function parkingSpots(road, options = {}) {
|
|
|
130
130
|
}
|
|
131
131
|
return spots;
|
|
132
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Signed planar distance from `(x, z)` to the nearest road *surface edge* across every segment of
|
|
135
|
+
* every road (#1051): negative inside the asphalt (magnitude = how far in from the closest edge, so
|
|
136
|
+
* `-width/2` on the centerline), `~0` right at the curb, and positive off the road (the gap to the
|
|
137
|
+
* nearest edge). Point-to-segment distance minus the road's half width, minimised over all segments —
|
|
138
|
+
* the "am I on the road, and by how much" seam driving grip, tyre audio, and off-road penalties.
|
|
139
|
+
*
|
|
140
|
+
* Allocation-free per call (only scalars — no arrays or objects) so a per-tick car sim can sample it
|
|
141
|
+
* on the hot path; degenerate roads (fewer than two path points) contribute nothing, and with no road
|
|
142
|
+
* surface at all it returns {@link Number.POSITIVE_INFINITY} (everywhere is off-road).
|
|
143
|
+
*/
|
|
144
|
+
export function distanceToRoadEdge(roads, x, z) {
|
|
145
|
+
let best = Number.POSITIVE_INFINITY;
|
|
146
|
+
for (let r = 0; r < roads.length; r += 1) {
|
|
147
|
+
const road = roads[r];
|
|
148
|
+
const path = road.path;
|
|
149
|
+
if (path.length < 2)
|
|
150
|
+
continue;
|
|
151
|
+
const half = road.width / 2;
|
|
152
|
+
for (let i = 0; i < path.length - 1; i += 1) {
|
|
153
|
+
const a = path[i];
|
|
154
|
+
const b = path[i + 1];
|
|
155
|
+
const abx = b[0] - a[0];
|
|
156
|
+
const abz = b[1] - a[1];
|
|
157
|
+
const lengthSq = abx * abx + abz * abz;
|
|
158
|
+
const t = lengthSq === 0 ? 0 : Math.max(0, Math.min(1, ((x - a[0]) * abx + (z - a[1]) * abz) / lengthSq));
|
|
159
|
+
const px = a[0] + abx * t;
|
|
160
|
+
const pz = a[1] + abz * t;
|
|
161
|
+
const edge = Math.hypot(x - px, z - pz) - half;
|
|
162
|
+
if (edge < best)
|
|
163
|
+
best = edge;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return best;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* A `surfaceFriction`-compatible sampler (the `(x, z) => grip` hook a kinematic car reads each tick)
|
|
170
|
+
* that turns road geometry into grip (#1051): full `onRoad` grip on the asphalt, a monotonic linear ramp
|
|
171
|
+
* down across a `shoulder`-wide band at the curb, and flat `offRoad` grip beyond it — so a car that
|
|
172
|
+
* clips onto the grass loses bite progressively instead of snapping. Closes over {@link
|
|
173
|
+
* distanceToRoadEdge}, so the returned sampler is itself allocation-free per call.
|
|
174
|
+
*/
|
|
175
|
+
export function roadSurfaceSampler(roads, options = {}) {
|
|
176
|
+
const onRoad = options.onRoad ?? 1;
|
|
177
|
+
const offRoad = options.offRoad ?? 0.72;
|
|
178
|
+
const shoulder = options.shoulder ?? 3;
|
|
179
|
+
return (x, z) => {
|
|
180
|
+
const edge = distanceToRoadEdge(roads, x, z);
|
|
181
|
+
if (edge <= 0)
|
|
182
|
+
return onRoad;
|
|
183
|
+
if (shoulder <= 0 || edge >= shoulder)
|
|
184
|
+
return offRoad;
|
|
185
|
+
return onRoad + (offRoad - onRoad) * (edge / shoulder);
|
|
186
|
+
};
|
|
187
|
+
}
|
|
133
188
|
/**
|
|
134
189
|
* A deterministic point on one of a road's sidewalks at a normalized position — `side` picks the
|
|
135
190
|
* band, `fraction` (0..1) picks how far along. The canonical pedestrian spawn helper.
|
package/dist/world/terraform.js
CHANGED
|
@@ -218,13 +218,18 @@ export function createEditableTerrain(config) {
|
|
|
218
218
|
record?.(index, before, surface);
|
|
219
219
|
return true;
|
|
220
220
|
}
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
/**
|
|
222
|
+
* Visits every cell in a brush footprint on the CELL grid, yielding each cell's flat `index`, its
|
|
223
|
+
* {@link brushWeight} `weight`, world center (`wx`,`wz`), and raw `dist` from the brush center.
|
|
224
|
+
* Shared by the cell-grid brushes ({@link paintSurface}, {@link blendPaint}); each applies its own
|
|
225
|
+
* inclusion test on the yielded values (paint uses `dist <= radius`, blend uses `weight > 0`).
|
|
226
|
+
*/
|
|
227
|
+
function forEachBrushCell(edit, visit) {
|
|
228
|
+
const falloff = edit.falloff ?? "smooth";
|
|
223
229
|
const shape = edit.shape ?? "circle";
|
|
224
230
|
const rCells = Math.ceil(edit.radius / cellSize) + 1;
|
|
225
231
|
const cx = gridX(edit.center[0]);
|
|
226
232
|
const cz = gridZ(edit.center[1]);
|
|
227
|
-
let painted = 0;
|
|
228
233
|
for (let iz = -rCells; iz <= rCells; iz += 1) {
|
|
229
234
|
const gz = Math.floor(cz) + iz;
|
|
230
235
|
if (gz < 0 || gz >= rows)
|
|
@@ -233,15 +238,25 @@ export function createEditableTerrain(config) {
|
|
|
233
238
|
const gx = Math.floor(cx) + ix;
|
|
234
239
|
if (gx < 0 || gx >= cols)
|
|
235
240
|
continue;
|
|
236
|
-
const
|
|
237
|
-
const
|
|
241
|
+
const wx = cellWorldX(gx);
|
|
242
|
+
const wz = cellWorldZ(gz);
|
|
243
|
+
const dx = wx - edit.center[0];
|
|
244
|
+
const dz = wz - edit.center[1];
|
|
238
245
|
const dist = shape === "square" ? Math.max(Math.abs(dx), Math.abs(dz)) : Math.hypot(dx, dz);
|
|
239
|
-
|
|
240
|
-
continue;
|
|
241
|
-
if (writeSurface(gz * cols + gx, surface, record))
|
|
242
|
-
painted += 1;
|
|
246
|
+
visit(gz * cols + gx, brushWeight(dist, edit.radius, falloff), wx, wz, dist);
|
|
243
247
|
}
|
|
244
248
|
}
|
|
249
|
+
}
|
|
250
|
+
function paintSurface(edit, record) {
|
|
251
|
+
const surface = edit.surface ?? null;
|
|
252
|
+
let painted = 0;
|
|
253
|
+
// Binary inclusion by raw distance (includes dist == radius), independent of falloff shaping.
|
|
254
|
+
forEachBrushCell(edit, (index, _weight, _wx, _wz, dist) => {
|
|
255
|
+
if (dist > edit.radius)
|
|
256
|
+
return;
|
|
257
|
+
if (writeSurface(index, surface, record))
|
|
258
|
+
painted += 1;
|
|
259
|
+
});
|
|
245
260
|
return painted;
|
|
246
261
|
}
|
|
247
262
|
function localNormal(wx, wz) {
|
|
@@ -371,31 +386,14 @@ export function createEditableTerrain(config) {
|
|
|
371
386
|
const target = layers.findIndex((entry) => entry.surface === surface);
|
|
372
387
|
if (target < 0)
|
|
373
388
|
return 0;
|
|
374
|
-
const falloff = edit.falloff ?? "smooth";
|
|
375
|
-
const shape = edit.shape ?? "circle";
|
|
376
389
|
const strength = edit.strength ?? 1;
|
|
377
|
-
const rCells = Math.ceil(edit.radius / cellSize) + 1;
|
|
378
|
-
const cx = gridX(edit.center[0]);
|
|
379
|
-
const cz = gridZ(edit.center[1]);
|
|
380
390
|
let painted = 0;
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (gx < 0 || gx >= cols)
|
|
388
|
-
continue;
|
|
389
|
-
const dx = cellWorldX(gx) - edit.center[0];
|
|
390
|
-
const dz = cellWorldZ(gz) - edit.center[1];
|
|
391
|
-
const dist = shape === "square" ? Math.max(Math.abs(dx), Math.abs(dz)) : Math.hypot(dx, dz);
|
|
392
|
-
const weight = brushWeight(dist, edit.radius, falloff);
|
|
393
|
-
if (weight <= 0)
|
|
394
|
-
continue;
|
|
395
|
-
if (blendCell(gz * cols + gx, target, weight * strength, record))
|
|
396
|
-
painted += 1;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
391
|
+
forEachBrushCell(edit, (index, weight) => {
|
|
392
|
+
if (weight <= 0)
|
|
393
|
+
return;
|
|
394
|
+
if (blendCell(index, target, weight * strength, record))
|
|
395
|
+
painted += 1;
|
|
396
|
+
});
|
|
399
397
|
return painted;
|
|
400
398
|
}
|
|
401
399
|
function runHeightEdit(edit, record) {
|
|
@@ -545,23 +543,29 @@ export function createEditableTerrain(config) {
|
|
|
545
543
|
};
|
|
546
544
|
return field;
|
|
547
545
|
}
|
|
548
|
-
/**
|
|
549
|
-
|
|
546
|
+
/**
|
|
547
|
+
* The shared first/latest Map accumulator behind every `begin*Stroke`. Records each key's first
|
|
548
|
+
* `before` and latest `after` across many stamps, then assembles the net changed entries (skipping
|
|
549
|
+
* keys whose latest equals their first). Each stroke supplies its own recording call via `recordStamp`
|
|
550
|
+
* and wraps {@link StrokeChanges} into its concrete delta shape.
|
|
551
|
+
*/
|
|
552
|
+
function createStroke(recordStamp) {
|
|
550
553
|
const first = new Map();
|
|
551
554
|
const latest = new Map();
|
|
552
555
|
return {
|
|
553
556
|
stamp(edit) {
|
|
554
|
-
return
|
|
557
|
+
return recordStamp(edit, (index, before, after) => {
|
|
555
558
|
if (!first.has(index))
|
|
556
559
|
first.set(index, before);
|
|
557
560
|
latest.set(index, after);
|
|
558
561
|
});
|
|
559
562
|
},
|
|
560
|
-
|
|
563
|
+
changes() {
|
|
561
564
|
const indices = [];
|
|
562
565
|
const before = [];
|
|
563
566
|
const after = [];
|
|
564
567
|
for (const [index, next] of latest) {
|
|
568
|
+
// Every latest key was set in `first` on the same callback, so this is always present.
|
|
565
569
|
const prev = first.get(index);
|
|
566
570
|
if (next === prev)
|
|
567
571
|
continue;
|
|
@@ -576,35 +580,22 @@ export function beginTerraformStroke(terrain) {
|
|
|
576
580
|
},
|
|
577
581
|
};
|
|
578
582
|
}
|
|
583
|
+
/** Opens a stroke recorder over `terrain`; stamp edits into it, then read one net delta. */
|
|
584
|
+
export function beginTerraformStroke(terrain) {
|
|
585
|
+
const stroke = createStroke((edit, record) => terrain.applyRecording(edit, record));
|
|
586
|
+
return {
|
|
587
|
+
stamp: stroke.stamp,
|
|
588
|
+
delta: () => stroke.changes(),
|
|
589
|
+
isEmpty: stroke.isEmpty,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
579
592
|
/** Opens a paint-stroke recorder over `terrain`; stamp paint edits into it, then read one net delta. */
|
|
580
593
|
export function beginSurfaceStroke(terrain) {
|
|
581
|
-
const
|
|
582
|
-
const latest = new Map();
|
|
594
|
+
const stroke = createStroke((edit, record) => terrain.paintRecording(edit, record));
|
|
583
595
|
return {
|
|
584
|
-
stamp
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
first.set(index, before);
|
|
588
|
-
latest.set(index, after);
|
|
589
|
-
});
|
|
590
|
-
},
|
|
591
|
-
delta() {
|
|
592
|
-
const indices = [];
|
|
593
|
-
const before = [];
|
|
594
|
-
const after = [];
|
|
595
|
-
for (const [index, next] of latest) {
|
|
596
|
-
const prev = first.get(index) ?? null;
|
|
597
|
-
if (next === prev)
|
|
598
|
-
continue;
|
|
599
|
-
indices.push(index);
|
|
600
|
-
before.push(prev);
|
|
601
|
-
after.push(next);
|
|
602
|
-
}
|
|
603
|
-
return { indices, before, after };
|
|
604
|
-
},
|
|
605
|
-
isEmpty() {
|
|
606
|
-
return latest.size === 0;
|
|
607
|
-
},
|
|
596
|
+
stamp: stroke.stamp,
|
|
597
|
+
delta: () => stroke.changes(),
|
|
598
|
+
isEmpty: stroke.isEmpty,
|
|
608
599
|
};
|
|
609
600
|
}
|
|
610
601
|
/** Returns a new snapshot with a surface delta's `after` ids applied (copy-on-write). */
|
|
@@ -700,33 +691,12 @@ export function revertWeightDeltaFromSnapshot(snapshot, delta) {
|
|
|
700
691
|
* @internal — the stroke recorder behind the blend brush and the `blendTerrain` command.
|
|
701
692
|
*/
|
|
702
693
|
export function beginBlendStroke(terrain) {
|
|
703
|
-
const
|
|
704
|
-
const latest = new Map();
|
|
694
|
+
const stroke = createStroke((edit, record) => terrain.blendRecording(edit, record));
|
|
705
695
|
return {
|
|
706
|
-
stamp
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
latest.set(index, after);
|
|
711
|
-
});
|
|
712
|
-
},
|
|
713
|
-
delta() {
|
|
714
|
-
const indices = [];
|
|
715
|
-
const before = [];
|
|
716
|
-
const after = [];
|
|
717
|
-
for (const [index, next] of latest) {
|
|
718
|
-
const prev = first.get(index);
|
|
719
|
-
if (next === prev)
|
|
720
|
-
continue;
|
|
721
|
-
indices.push(index);
|
|
722
|
-
before.push(prev);
|
|
723
|
-
after.push(next);
|
|
724
|
-
}
|
|
725
|
-
return { layerCount: terrain.layers.length, indices, before, after };
|
|
726
|
-
},
|
|
727
|
-
isEmpty() {
|
|
728
|
-
return latest.size === 0;
|
|
729
|
-
},
|
|
696
|
+
stamp: stroke.stamp,
|
|
697
|
+
// layerCount is read at delta() time, guarding replay after the layer stack changed shape.
|
|
698
|
+
delta: () => ({ layerCount: terrain.layers.length, ...stroke.changes() }),
|
|
699
|
+
isEmpty: stroke.isEmpty,
|
|
730
700
|
};
|
|
731
701
|
}
|
|
732
702
|
/**
|
package/dist/world/terrain.d.ts
CHANGED
|
@@ -124,6 +124,51 @@ export declare function sampleSlope(field: TerrainField, x: number, z: number):
|
|
|
124
124
|
* @internal
|
|
125
125
|
*/
|
|
126
126
|
export declare function slopeForce(field: TerrainField, x: number, z: number, scale?: number): readonly [number, number];
|
|
127
|
+
/** XZ region a {@link raycastHeightField} march is clipped to. */
|
|
128
|
+
export interface HeightFieldRayBounds {
|
|
129
|
+
minX: number;
|
|
130
|
+
minZ: number;
|
|
131
|
+
maxX: number;
|
|
132
|
+
maxZ: number;
|
|
133
|
+
}
|
|
134
|
+
/** Tuning for {@link raycastHeightField}: march resolution and refinement depth. */
|
|
135
|
+
export interface HeightFieldRaycastOptions {
|
|
136
|
+
/** XZ region to march within; the ray is clipped to it before any sampling. */
|
|
137
|
+
bounds: HeightFieldRayBounds;
|
|
138
|
+
/**
|
|
139
|
+
* March step in world units. Use about half the field's cell size — features narrower than one
|
|
140
|
+
* step can be skipped. Defaults to 1/256 of the larger bounds span, clamped to `[0.25, 4]`.
|
|
141
|
+
*/
|
|
142
|
+
step?: number;
|
|
143
|
+
/** Bisection iterations refining the crossing once found (default 8 ≈ step/256 precision). */
|
|
144
|
+
refine?: number;
|
|
145
|
+
/** Stop marching past this ray distance (default: the full bounds crossing). */
|
|
146
|
+
maxDistance?: number;
|
|
147
|
+
/**
|
|
148
|
+
* Hard cap on march samples (default 4096). Bounds the work for rays the XZ slab cannot clip —
|
|
149
|
+
* a near-vertical ray inside bounds has an enormous slab crossing, and an upward ray never
|
|
150
|
+
* meets the surface at all; the cap turns both into a prompt miss instead of a stall.
|
|
151
|
+
*/
|
|
152
|
+
maxSteps?: number;
|
|
153
|
+
}
|
|
154
|
+
/** Where a {@link raycastHeightField} march crossed the surface. */
|
|
155
|
+
export interface HeightFieldRayHit {
|
|
156
|
+
x: number;
|
|
157
|
+
y: number;
|
|
158
|
+
z: number;
|
|
159
|
+
/** Ray-parameter distance from the origin to the hit (direction is normalized internally). */
|
|
160
|
+
distance: number;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Intersects a ray with a `sampleHeight` field by fixed-step raymarching plus bisection — the
|
|
164
|
+
* O(steps) editor/gameplay picking seam that replaces brute-force triangle raycasts against a
|
|
165
|
+
* tessellated ground mesh (tens of thousands of triangle tests per pick on a sculpt-sized grid).
|
|
166
|
+
* The ray is clipped to `bounds` on XZ first, marched at `step`, and the first above→below
|
|
167
|
+
* crossing is bisected. An origin already under the surface hits immediately at entry. Returns
|
|
168
|
+
* null when the clipped ray never crosses the surface.
|
|
169
|
+
* @capability terrain-raycast pick a point on a heightfield along a camera/pointer ray
|
|
170
|
+
*/
|
|
171
|
+
export declare function raycastHeightField(sampleHeight: (x: number, z: number) => number, origin: readonly [number, number, number], direction: readonly [number, number, number], options: HeightFieldRaycastOptions): HeightFieldRayHit | null;
|
|
127
172
|
export interface TerrainPalette {
|
|
128
173
|
low: string;
|
|
129
174
|
high: string;
|
package/dist/world/terrain.js
CHANGED
|
@@ -174,6 +174,10 @@ export function arenaField(config = {}) {
|
|
|
174
174
|
export function groundFieldFor(world) {
|
|
175
175
|
if (world !== undefined && world.kind === "environment")
|
|
176
176
|
return resolveEnvironmentField(world);
|
|
177
|
+
// Place worlds (`world()` from world/place) stand on a flat baseline field: flat/board grounds are
|
|
178
|
+
// exactly y=0, while round/voxel grounds keep their sampled shape in the game's own generation
|
|
179
|
+
// systems (the substrate declaration never bakes a heightfield). Editor-authored sculpt still
|
|
180
|
+
// layers through the environment path above.
|
|
177
181
|
return flatField();
|
|
178
182
|
}
|
|
179
183
|
/** Whether a world declares real terrain (base heightfield or islands) rather than a flat plane — gates terrain-floor sampling in the movement controllers.
|
|
@@ -338,6 +342,80 @@ export function slopeForce(field, x, z, scale = 9.8) {
|
|
|
338
342
|
return [0, 0];
|
|
339
343
|
return [nx * scale, nz * scale];
|
|
340
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* Intersects a ray with a `sampleHeight` field by fixed-step raymarching plus bisection — the
|
|
347
|
+
* O(steps) editor/gameplay picking seam that replaces brute-force triangle raycasts against a
|
|
348
|
+
* tessellated ground mesh (tens of thousands of triangle tests per pick on a sculpt-sized grid).
|
|
349
|
+
* The ray is clipped to `bounds` on XZ first, marched at `step`, and the first above→below
|
|
350
|
+
* crossing is bisected. An origin already under the surface hits immediately at entry. Returns
|
|
351
|
+
* null when the clipped ray never crosses the surface.
|
|
352
|
+
* @capability terrain-raycast pick a point on a heightfield along a camera/pointer ray
|
|
353
|
+
*/
|
|
354
|
+
export function raycastHeightField(sampleHeight, origin, direction, options) {
|
|
355
|
+
const length = Math.hypot(direction[0], direction[1], direction[2]);
|
|
356
|
+
if (length < 1e-12)
|
|
357
|
+
return null;
|
|
358
|
+
const dx = direction[0] / length;
|
|
359
|
+
const dy = direction[1] / length;
|
|
360
|
+
const dz = direction[2] / length;
|
|
361
|
+
const { bounds } = options;
|
|
362
|
+
// Clip [t0, t1] to the XZ slab of bounds.
|
|
363
|
+
let t0 = 0;
|
|
364
|
+
let t1 = options.maxDistance ?? Infinity;
|
|
365
|
+
for (const [o, d, min, max] of [
|
|
366
|
+
[origin[0], dx, bounds.minX, bounds.maxX],
|
|
367
|
+
[origin[2], dz, bounds.minZ, bounds.maxZ],
|
|
368
|
+
]) {
|
|
369
|
+
if (Math.abs(d) < 1e-12) {
|
|
370
|
+
if (o < min || o > max)
|
|
371
|
+
return null;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const ta = (min - o) / d;
|
|
375
|
+
const tb = (max - o) / d;
|
|
376
|
+
const near = Math.min(ta, tb);
|
|
377
|
+
const far = Math.max(ta, tb);
|
|
378
|
+
if (near > t0)
|
|
379
|
+
t0 = near;
|
|
380
|
+
if (far < t1)
|
|
381
|
+
t1 = far;
|
|
382
|
+
}
|
|
383
|
+
if (t1 <= t0)
|
|
384
|
+
return null;
|
|
385
|
+
const spanX = bounds.maxX - bounds.minX;
|
|
386
|
+
const spanZ = bounds.maxZ - bounds.minZ;
|
|
387
|
+
const step = options.step ?? Math.min(4, Math.max(0.25, Math.max(spanX, spanZ) / 256));
|
|
388
|
+
const refine = options.refine ?? 8;
|
|
389
|
+
const maxSteps = options.maxSteps ?? 4096;
|
|
390
|
+
const above = (t) => origin[1] + dy * t - sampleHeight(origin[0] + dx * t, origin[2] + dz * t);
|
|
391
|
+
let prevT = t0;
|
|
392
|
+
if (above(t0) <= 0) {
|
|
393
|
+
return { x: origin[0] + dx * t0, y: origin[1] + dy * t0, z: origin[2] + dz * t0, distance: t0 };
|
|
394
|
+
}
|
|
395
|
+
let steps = 0;
|
|
396
|
+
while (prevT < t1 && steps < maxSteps) {
|
|
397
|
+
steps += 1;
|
|
398
|
+
const t = Math.min(prevT + step, t1);
|
|
399
|
+
if (above(t) <= 0) {
|
|
400
|
+
let lo = prevT;
|
|
401
|
+
let hi = t;
|
|
402
|
+
for (let i = 0; i < refine; i += 1) {
|
|
403
|
+
const mid = (lo + hi) / 2;
|
|
404
|
+
if (above(mid) <= 0)
|
|
405
|
+
hi = mid;
|
|
406
|
+
else
|
|
407
|
+
lo = mid;
|
|
408
|
+
}
|
|
409
|
+
const x = origin[0] + dx * hi;
|
|
410
|
+
const z = origin[2] + dz * hi;
|
|
411
|
+
return { x, y: sampleHeight(x, z), z, distance: hi };
|
|
412
|
+
}
|
|
413
|
+
if (t >= t1)
|
|
414
|
+
break;
|
|
415
|
+
prevT = t;
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
341
419
|
export const DEFAULT_TERRAIN_MATERIAL = "grass";
|
|
342
420
|
export const TERRAIN_MATERIAL_PALETTES = {
|
|
343
421
|
grass: { low: "#30402c", high: "#7f8b50", waterline: "#1d4c6e" },
|