@jgengine/core 0.9.0 → 0.10.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 +21 -1
- package/dist/ai/spawnDirector.d.ts +13 -0
- package/dist/ai/spawnDirector.js +40 -16
- package/dist/ai/spawnPoint.d.ts +5 -0
- package/dist/ai/spawnPoint.js +5 -0
- package/dist/audio/audioFalloff.d.ts +5 -2
- package/dist/audio/music.d.ts +53 -0
- package/dist/audio/music.js +34 -0
- package/dist/audio/synth.d.ts +60 -0
- package/dist/audio/synth.js +8 -0
- package/dist/authoring.d.ts +14 -0
- package/dist/authoring.js +14 -0
- package/dist/cartridge/runtime.js +2 -2
- package/dist/combat/dotField.d.ts +43 -0
- package/dist/combat/dotField.js +48 -0
- package/dist/combat/projectiles.d.ts +2 -2
- package/dist/combat/projectiles.js +18 -5
- package/dist/combat/regenShield.d.ts +35 -0
- package/dist/combat/regenShield.js +45 -0
- package/dist/combat/shotOrigin.d.ts +13 -1
- package/dist/combat/shotOrigin.js +31 -2
- package/dist/editor/commands.d.ts +65 -0
- package/dist/editor/commands.js +193 -0
- package/dist/editor/document.d.ts +40 -0
- package/dist/editor/document.js +153 -0
- package/dist/editor/index.d.ts +30 -0
- package/dist/editor/index.js +6 -0
- package/dist/editor/types.d.ts +69 -0
- package/dist/editor/types.js +29 -0
- package/dist/game/connectedPlayers.d.ts +28 -0
- package/dist/game/connectedPlayers.js +21 -0
- package/dist/game/defineGame.d.ts +42 -1
- package/dist/game/defineGame.js +1 -0
- package/dist/game/events.d.ts +15 -0
- package/dist/game/keyValueStore.d.ts +25 -0
- package/dist/game/keyValueStore.js +61 -0
- package/dist/game/levelSequence.d.ts +24 -2
- package/dist/game/levelSequence.js +51 -2
- package/dist/game/lootTable.d.ts +4 -0
- package/dist/game/lootTable.js +1 -0
- package/dist/game/modelAnimation.d.ts +8 -0
- package/dist/game/modelAnimation.js +18 -0
- package/dist/game/playableGame.d.ts +68 -5
- package/dist/game/recordBook.d.ts +3 -5
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/input/actionBindings.d.ts +2 -0
- package/dist/input/actionBindings.js +1 -0
- package/dist/input/axisInput.d.ts +7 -0
- package/dist/input/axisInput.js +16 -0
- package/dist/interaction/contextMenu.d.ts +1 -0
- package/dist/interaction/contextMenu.js +1 -0
- package/dist/interaction/proximityPrompt.d.ts +3 -1
- package/dist/interaction/proximityPrompt.js +3 -3
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +22 -1
- package/dist/movement/movementModel.d.ts +4 -0
- package/dist/movement/movementModel.js +13 -0
- package/dist/movement/playerMovement.d.ts +41 -0
- package/dist/movement/playerMovement.js +181 -0
- package/dist/random/rng.d.ts +2 -0
- package/dist/random/rng.js +2 -0
- package/dist/render/postProcessing.d.ts +56 -0
- package/dist/render/postProcessing.js +1 -0
- package/dist/runtime/adapter.d.ts +19 -0
- package/dist/runtime/adapter.js +11 -5
- package/dist/runtime/gameContext.d.ts +53 -10
- package/dist/runtime/gameContext.js +192 -54
- package/dist/runtime/hostedGameRunner.d.ts +49 -0
- package/dist/runtime/hostedGameRunner.js +62 -0
- package/dist/runtime/hostedWorldSession.d.ts +65 -0
- package/dist/runtime/hostedWorldSession.js +57 -0
- package/dist/runtime/inputSnapshot.d.ts +13 -0
- package/dist/runtime/inputSnapshot.js +2 -0
- package/dist/runtime/motionIntents.d.ts +2 -0
- package/dist/runtime/motionIntents.js +11 -0
- package/dist/runtime/worldChannel.d.ts +58 -0
- package/dist/runtime/worldChannel.js +69 -0
- package/dist/runtime/worldMirror.d.ts +24 -0
- package/dist/runtime/worldMirror.js +31 -0
- package/dist/runtime/worldReplication.d.ts +46 -0
- package/dist/runtime/worldReplication.js +185 -0
- package/dist/runtime/worldSnapshot.d.ts +17 -0
- package/dist/runtime/worldSnapshot.js +15 -0
- package/dist/scene/assetCatalog.d.ts +1 -0
- package/dist/scene/colliders.d.ts +6 -0
- package/dist/scene/colliders.js +25 -1
- package/dist/scene/entityStats.d.ts +4 -0
- package/dist/scene/entityStats.js +21 -0
- package/dist/scene/entityStore.d.ts +29 -0
- package/dist/scene/entityStore.js +60 -0
- package/dist/store/observableKeyedStore.d.ts +4 -0
- package/dist/store/observableKeyedStore.js +9 -0
- package/dist/tactics/fallingGrid.d.ts +2 -2
- package/dist/tactics/fallingGrid.js +2 -2
- package/dist/ui/gameLayout.d.ts +125 -0
- package/dist/ui/gameLayout.js +129 -0
- package/dist/ui/orientation.d.ts +39 -0
- package/dist/ui/orientation.js +49 -0
- package/dist/ui/swingTimer.d.ts +42 -0
- package/dist/ui/swingTimer.js +38 -0
- package/dist/visibility/spatialIndex.js +34 -20
- package/dist/world/environmentSummary.d.ts +11 -2
- package/dist/world/environmentSummary.js +11 -1
- package/dist/world/features.d.ts +134 -5
- package/dist/world/features.js +40 -3
- package/dist/world/roads.d.ts +47 -0
- package/dist/world/roads.js +146 -0
- package/dist/world/streets.d.ts +80 -0
- package/dist/world/streets.js +144 -0
- package/dist/world/terrain.d.ts +32 -5
- package/dist/world/terrain.js +124 -12
- package/llms.txt +485 -79
- package/package.json +1 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** A road centerline vertex in world XZ. */
|
|
2
|
+
export type RoadPoint = readonly [number, number];
|
|
3
|
+
/** Renderer-ready triangle ribbon: flat position triples plus triangle indices. */
|
|
4
|
+
export interface RoadRibbon {
|
|
5
|
+
/** Interleaved xyz vertex positions. */
|
|
6
|
+
positions: Float32Array;
|
|
7
|
+
/** Triangle indices into `positions`. */
|
|
8
|
+
indices: Uint32Array;
|
|
9
|
+
}
|
|
10
|
+
/** Options for {@link buildRoadRibbon}. */
|
|
11
|
+
export interface RoadRibbonOptions {
|
|
12
|
+
/** Lift above the sampled ground so the ribbon never z-fights the terrain. Default 0.08. */
|
|
13
|
+
elevation?: number;
|
|
14
|
+
/** Longest centerline step before the polyline is subdivided to drape over relief. Default 4. */
|
|
15
|
+
maxSegmentLength?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Triangulate a road centerline into a ground-draped ribbon mesh: the polyline is subdivided,
|
|
19
|
+
* each vertex is offset half a `width` along the local perpendicular, and every vertex sits at
|
|
20
|
+
* `sampleHeight(x, z) + elevation`. Pure geometry — the shell (or any renderer) turns the result
|
|
21
|
+
* into a mesh, and tests can assert on it directly.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildRoadRibbon(path: readonly RoadPoint[], width: number, sampleHeight: (x: number, z: number) => number, options?: RoadRibbonOptions): RoadRibbon;
|
|
24
|
+
/**
|
|
25
|
+
* Split a centerline into dash sub-polylines for lane markings: `dashLength` of painted line,
|
|
26
|
+
* `gapLength` of asphalt, repeated along the path's arc length. Feed each returned sub-path back
|
|
27
|
+
* through {@link buildRoadRibbon} with a thin width to mesh the dashes.
|
|
28
|
+
*/
|
|
29
|
+
export declare function dashSegments(path: readonly RoadPoint[], dashLength?: number, gapLength?: number): readonly (readonly RoadPoint[])[];
|
|
30
|
+
/** Result of {@link nearestOnPath}: closest point on the centerline plus distance and tangent. */
|
|
31
|
+
export interface RoadSample {
|
|
32
|
+
/** Closest point on the centerline. */
|
|
33
|
+
point: RoadPoint;
|
|
34
|
+
/** World distance from the query to `point`. */
|
|
35
|
+
distance: number;
|
|
36
|
+
/** Unit tangent of the centerline at `point`. */
|
|
37
|
+
tangent: readonly [number, number];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Closest-point query against a road centerline — the seam traffic AI, spawn placement, and
|
|
41
|
+
* "am I on the road" checks share. Returns null for a degenerate path.
|
|
42
|
+
*/
|
|
43
|
+
export declare function nearestOnPath(path: readonly RoadPoint[], x: number, z: number): RoadSample | null;
|
|
44
|
+
/** True when the query point lies within half the road `width` of the centerline. */
|
|
45
|
+
export declare function isOnRoad(path: readonly RoadPoint[], width: number, x: number, z: number): boolean;
|
|
46
|
+
/** Total arc length of a centerline in world units. */
|
|
47
|
+
export declare function pathLength(path: readonly RoadPoint[]): number;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
function subdividePath(path, maxLength) {
|
|
2
|
+
const out = [];
|
|
3
|
+
for (let i = 0; i < path.length - 1; i += 1) {
|
|
4
|
+
const a = path[i];
|
|
5
|
+
const b = path[i + 1];
|
|
6
|
+
const length = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
7
|
+
const steps = Math.max(1, Math.ceil(length / maxLength));
|
|
8
|
+
for (let s = 0; s < steps; s += 1) {
|
|
9
|
+
const t = s / steps;
|
|
10
|
+
out.push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const last = path[path.length - 1];
|
|
14
|
+
if (last !== undefined)
|
|
15
|
+
out.push(last);
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
function normalAt(points, index) {
|
|
19
|
+
const prev = points[Math.max(0, index - 1)];
|
|
20
|
+
const next = points[Math.min(points.length - 1, index + 1)];
|
|
21
|
+
const dx = next[0] - prev[0];
|
|
22
|
+
const dz = next[1] - prev[1];
|
|
23
|
+
const length = Math.hypot(dx, dz) || 1;
|
|
24
|
+
return [-dz / length, dx / length];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Triangulate a road centerline into a ground-draped ribbon mesh: the polyline is subdivided,
|
|
28
|
+
* each vertex is offset half a `width` along the local perpendicular, and every vertex sits at
|
|
29
|
+
* `sampleHeight(x, z) + elevation`. Pure geometry — the shell (or any renderer) turns the result
|
|
30
|
+
* into a mesh, and tests can assert on it directly.
|
|
31
|
+
*/
|
|
32
|
+
export function buildRoadRibbon(path, width, sampleHeight, options = {}) {
|
|
33
|
+
const elevation = options.elevation ?? 0.08;
|
|
34
|
+
const maxSegmentLength = options.maxSegmentLength ?? 4;
|
|
35
|
+
if (path.length < 2 || width <= 0) {
|
|
36
|
+
return { positions: new Float32Array(0), indices: new Uint32Array(0) };
|
|
37
|
+
}
|
|
38
|
+
const points = subdividePath(path, maxSegmentLength);
|
|
39
|
+
const half = width / 2;
|
|
40
|
+
const positions = new Float32Array(points.length * 6);
|
|
41
|
+
for (let i = 0; i < points.length; i += 1) {
|
|
42
|
+
const [x, z] = points[i];
|
|
43
|
+
const [nx, nz] = normalAt(points, i);
|
|
44
|
+
const lx = x + nx * half;
|
|
45
|
+
const lz = z + nz * half;
|
|
46
|
+
const rx = x - nx * half;
|
|
47
|
+
const rz = z - nz * half;
|
|
48
|
+
positions[i * 6] = lx;
|
|
49
|
+
positions[i * 6 + 1] = sampleHeight(lx, lz) + elevation;
|
|
50
|
+
positions[i * 6 + 2] = lz;
|
|
51
|
+
positions[i * 6 + 3] = rx;
|
|
52
|
+
positions[i * 6 + 4] = sampleHeight(rx, rz) + elevation;
|
|
53
|
+
positions[i * 6 + 5] = rz;
|
|
54
|
+
}
|
|
55
|
+
const indices = new Uint32Array((points.length - 1) * 6);
|
|
56
|
+
for (let i = 0; i < points.length - 1; i += 1) {
|
|
57
|
+
const a = i * 2;
|
|
58
|
+
indices[i * 6] = a;
|
|
59
|
+
indices[i * 6 + 1] = a + 1;
|
|
60
|
+
indices[i * 6 + 2] = a + 2;
|
|
61
|
+
indices[i * 6 + 3] = a + 1;
|
|
62
|
+
indices[i * 6 + 4] = a + 3;
|
|
63
|
+
indices[i * 6 + 5] = a + 2;
|
|
64
|
+
}
|
|
65
|
+
return { positions, indices };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Split a centerline into dash sub-polylines for lane markings: `dashLength` of painted line,
|
|
69
|
+
* `gapLength` of asphalt, repeated along the path's arc length. Feed each returned sub-path back
|
|
70
|
+
* through {@link buildRoadRibbon} with a thin width to mesh the dashes.
|
|
71
|
+
*/
|
|
72
|
+
export function dashSegments(path, dashLength = 3, gapLength = 3) {
|
|
73
|
+
if (path.length < 2 || dashLength <= 0 || gapLength < 0)
|
|
74
|
+
return [];
|
|
75
|
+
const cumulative = [0];
|
|
76
|
+
for (let i = 0; i < path.length - 1; i += 1) {
|
|
77
|
+
const a = path[i];
|
|
78
|
+
const b = path[i + 1];
|
|
79
|
+
cumulative.push(cumulative[i] + Math.hypot(b[0] - a[0], b[1] - a[1]));
|
|
80
|
+
}
|
|
81
|
+
const total = cumulative[cumulative.length - 1];
|
|
82
|
+
if (total <= 0)
|
|
83
|
+
return [];
|
|
84
|
+
const pointAt = (s) => {
|
|
85
|
+
const clamped = Math.max(0, Math.min(total, s));
|
|
86
|
+
let index = 0;
|
|
87
|
+
while (index < cumulative.length - 2 && cumulative[index + 1] < clamped)
|
|
88
|
+
index += 1;
|
|
89
|
+
const a = path[index];
|
|
90
|
+
const b = path[index + 1];
|
|
91
|
+
const segLength = cumulative[index + 1] - cumulative[index];
|
|
92
|
+
const t = segLength <= 0 ? 0 : (clamped - cumulative[index]) / segLength;
|
|
93
|
+
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
|
|
94
|
+
};
|
|
95
|
+
const period = dashLength + gapLength;
|
|
96
|
+
const dashes = [];
|
|
97
|
+
for (let start = 0; start < total; start += period) {
|
|
98
|
+
const end = Math.min(total, start + dashLength);
|
|
99
|
+
if (end - start < 1e-6)
|
|
100
|
+
break;
|
|
101
|
+
dashes.push([pointAt(start), pointAt(end)]);
|
|
102
|
+
if (period <= 0)
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
return dashes;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Closest-point query against a road centerline — the seam traffic AI, spawn placement, and
|
|
109
|
+
* "am I on the road" checks share. Returns null for a degenerate path.
|
|
110
|
+
*/
|
|
111
|
+
export function nearestOnPath(path, x, z) {
|
|
112
|
+
if (path.length < 2)
|
|
113
|
+
return null;
|
|
114
|
+
let best = null;
|
|
115
|
+
for (let i = 0; i < path.length - 1; i += 1) {
|
|
116
|
+
const a = path[i];
|
|
117
|
+
const b = path[i + 1];
|
|
118
|
+
const abx = b[0] - a[0];
|
|
119
|
+
const abz = b[1] - a[1];
|
|
120
|
+
const lengthSq = abx * abx + abz * abz;
|
|
121
|
+
const t = lengthSq === 0 ? 0 : Math.max(0, Math.min(1, ((x - a[0]) * abx + (z - a[1]) * abz) / lengthSq));
|
|
122
|
+
const px = a[0] + abx * t;
|
|
123
|
+
const pz = a[1] + abz * t;
|
|
124
|
+
const distance = Math.hypot(x - px, z - pz);
|
|
125
|
+
if (best === null || distance < best.distance) {
|
|
126
|
+
const length = Math.sqrt(lengthSq) || 1;
|
|
127
|
+
best = { point: [px, pz], distance, tangent: [abx / length, abz / length] };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return best;
|
|
131
|
+
}
|
|
132
|
+
/** True when the query point lies within half the road `width` of the centerline. */
|
|
133
|
+
export function isOnRoad(path, width, x, z) {
|
|
134
|
+
const sample = nearestOnPath(path, x, z);
|
|
135
|
+
return sample !== null && sample.distance <= width / 2;
|
|
136
|
+
}
|
|
137
|
+
/** Total arc length of a centerline in world units. */
|
|
138
|
+
export function pathLength(path) {
|
|
139
|
+
let total = 0;
|
|
140
|
+
for (let i = 0; i < path.length - 1; i += 1) {
|
|
141
|
+
const a = path[i];
|
|
142
|
+
const b = path[i + 1];
|
|
143
|
+
total += Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
144
|
+
}
|
|
145
|
+
return total;
|
|
146
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { RoadEnvironmentDescriptor } from "./features.js";
|
|
2
|
+
import { type RoadPoint } from "./roads.js";
|
|
3
|
+
/**
|
|
4
|
+
* Offset a centerline sideways by a signed distance along its local perpendicular — the
|
|
5
|
+
* building block for lanes, curb lines, and sidewalk paths. Positive offsets fall on the
|
|
6
|
+
* left of the direction of travel, negative on the right.
|
|
7
|
+
*/
|
|
8
|
+
export declare function offsetPath(path: readonly RoadPoint[], offset: number): readonly RoadPoint[];
|
|
9
|
+
/** A directed lane derived from a road: an offset centerline plus its direction of travel. */
|
|
10
|
+
export interface StreetLane {
|
|
11
|
+
/** Lane centerline, ordered in the direction of travel. */
|
|
12
|
+
path: readonly RoadPoint[];
|
|
13
|
+
/** "forward" runs with the road's declared path order; "reverse" runs against it. */
|
|
14
|
+
direction: "forward" | "reverse";
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Two right-hand-traffic lane centerlines for a road — each offset a quarter of the drivable
|
|
18
|
+
* width from the center and ordered in its direction of travel. Feed a lane's `path` straight
|
|
19
|
+
* into `nav/pathFollow` for traffic AI, or use its endpoints as directed car spawn points.
|
|
20
|
+
*/
|
|
21
|
+
export declare function laneCenters(road: RoadEnvironmentDescriptor): readonly [StreetLane, StreetLane];
|
|
22
|
+
/** Resolved sidewalk band widths for a road; zero when the road declares no sidewalk. */
|
|
23
|
+
export declare function sidewalkWidthOf(road: RoadEnvironmentDescriptor): number;
|
|
24
|
+
/**
|
|
25
|
+
* The two sidewalk walking paths of a road — offset polylines running down the middle of each
|
|
26
|
+
* sidewalk band. Pedestrians spawn and route along these instead of the asphalt.
|
|
27
|
+
*/
|
|
28
|
+
export declare function sidewalkPaths(road: RoadEnvironmentDescriptor): readonly (readonly RoadPoint[])[];
|
|
29
|
+
/** A placement anchor on the curb line: position, outward-facing heading, and which side it sits on. */
|
|
30
|
+
export interface FurnitureSpot {
|
|
31
|
+
position: RoadPoint;
|
|
32
|
+
/** Yaw (radians, engine `rotationY` convention) facing away from the road. */
|
|
33
|
+
heading: number;
|
|
34
|
+
side: "left" | "right";
|
|
35
|
+
/** Arc-length distance along the road at this spot. */
|
|
36
|
+
along: number;
|
|
37
|
+
}
|
|
38
|
+
/** Options for {@link furnitureSpots}. */
|
|
39
|
+
export interface FurnitureSpotOptions {
|
|
40
|
+
/** Distance between consecutive spots along the road. Default 24. */
|
|
41
|
+
spacing?: number;
|
|
42
|
+
/** Which curb lines to populate. Default "both". */
|
|
43
|
+
sides?: "both" | "left" | "right";
|
|
44
|
+
/** Alternate sides spot-by-spot instead of mirroring every spot. Default true. */
|
|
45
|
+
stagger?: boolean;
|
|
46
|
+
/** Extra outward shift past the curb line (e.g. to center furniture in the sidewalk). Default 0. */
|
|
47
|
+
outset?: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Evenly spaced street-furniture anchors along a road's curb lines — streetlights, palms, signs,
|
|
51
|
+
* hydrants, benches. Each spot sits just outside the asphalt (plus `outset`), faces away from
|
|
52
|
+
* the street, and alternates sides by default so lights stagger like a real avenue. This is the
|
|
53
|
+
* answer to "where do I put it": furniture is an asset of the street, never a hand-typed coordinate.
|
|
54
|
+
*/
|
|
55
|
+
export declare function furnitureSpots(road: RoadEnvironmentDescriptor, options?: FurnitureSpotOptions): readonly FurnitureSpot[];
|
|
56
|
+
/** Options for {@link parkingSpots}. */
|
|
57
|
+
export interface ParkingSpotOptions {
|
|
58
|
+
/** Distance between consecutive parked-car anchors. Default 18. */
|
|
59
|
+
spacing?: number;
|
|
60
|
+
/** Which side(s) of the street to line with parking. Default "both". */
|
|
61
|
+
sides?: "both" | "left" | "right";
|
|
62
|
+
}
|
|
63
|
+
/** A curbside parking anchor: position at the road edge and a heading parallel to the street. */
|
|
64
|
+
export interface ParkingSpot {
|
|
65
|
+
position: RoadPoint;
|
|
66
|
+
/** Yaw (radians) aligned with the direction of travel on that side (right-hand traffic). */
|
|
67
|
+
heading: number;
|
|
68
|
+
side: "left" | "right";
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Curbside parking anchors along a road: hugging the edge of the asphalt, headed parallel to the
|
|
72
|
+
* street in that side's direction of travel. Spawn parked vehicles here instead of eyeballing
|
|
73
|
+
* coordinates in the middle of the carriageway.
|
|
74
|
+
*/
|
|
75
|
+
export declare function parkingSpots(road: RoadEnvironmentDescriptor, options?: ParkingSpotOptions): readonly ParkingSpot[];
|
|
76
|
+
/**
|
|
77
|
+
* A deterministic point on one of a road's sidewalks at a normalized position — `side` picks the
|
|
78
|
+
* band, `fraction` (0..1) picks how far along. The canonical pedestrian spawn helper.
|
|
79
|
+
*/
|
|
80
|
+
export declare function sidewalkPoint(road: RoadEnvironmentDescriptor, side: "left" | "right", fraction: number): RoadPoint | null;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { pathLength } from "./roads.js";
|
|
2
|
+
function normalAt(path, index) {
|
|
3
|
+
const prev = path[Math.max(0, index - 1)];
|
|
4
|
+
const next = path[Math.min(path.length - 1, index + 1)];
|
|
5
|
+
const dx = next[0] - prev[0];
|
|
6
|
+
const dz = next[1] - prev[1];
|
|
7
|
+
const length = Math.hypot(dx, dz) || 1;
|
|
8
|
+
return [-dz / length, dx / length];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Offset a centerline sideways by a signed distance along its local perpendicular — the
|
|
12
|
+
* building block for lanes, curb lines, and sidewalk paths. Positive offsets fall on the
|
|
13
|
+
* left of the direction of travel, negative on the right.
|
|
14
|
+
*/
|
|
15
|
+
export function offsetPath(path, offset) {
|
|
16
|
+
return path.map((point, index) => {
|
|
17
|
+
const [nx, nz] = normalAt(path, index);
|
|
18
|
+
return [point[0] + nx * offset, point[1] + nz * offset];
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Two right-hand-traffic lane centerlines for a road — each offset a quarter of the drivable
|
|
23
|
+
* width from the center and ordered in its direction of travel. Feed a lane's `path` straight
|
|
24
|
+
* into `nav/pathFollow` for traffic AI, or use its endpoints as directed car spawn points.
|
|
25
|
+
*/
|
|
26
|
+
export function laneCenters(road) {
|
|
27
|
+
const laneOffset = road.width / 4;
|
|
28
|
+
const forward = offsetPath(road.path, -laneOffset);
|
|
29
|
+
const reverse = offsetPath(road.path, laneOffset);
|
|
30
|
+
return [
|
|
31
|
+
{ path: forward, direction: "forward" },
|
|
32
|
+
{ path: [...reverse].reverse(), direction: "reverse" },
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
/** Resolved sidewalk band widths for a road; zero when the road declares no sidewalk. */
|
|
36
|
+
export function sidewalkWidthOf(road) {
|
|
37
|
+
return road.sidewalk === false ? 0 : road.sidewalk.width;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The two sidewalk walking paths of a road — offset polylines running down the middle of each
|
|
41
|
+
* sidewalk band. Pedestrians spawn and route along these instead of the asphalt.
|
|
42
|
+
*/
|
|
43
|
+
export function sidewalkPaths(road) {
|
|
44
|
+
const width = sidewalkWidthOf(road);
|
|
45
|
+
if (width <= 0)
|
|
46
|
+
return [];
|
|
47
|
+
const offset = road.width / 2 + width / 2;
|
|
48
|
+
return [offsetPath(road.path, offset), offsetPath(road.path, -offset)];
|
|
49
|
+
}
|
|
50
|
+
function pointAlong(path, along) {
|
|
51
|
+
let remaining = along;
|
|
52
|
+
for (let i = 0; i < path.length - 1; i += 1) {
|
|
53
|
+
const a = path[i];
|
|
54
|
+
const b = path[i + 1];
|
|
55
|
+
const segLength = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
56
|
+
if (remaining <= segLength || i === path.length - 2) {
|
|
57
|
+
const t = segLength <= 0 ? 0 : Math.min(1, remaining / segLength);
|
|
58
|
+
const length = segLength || 1;
|
|
59
|
+
return {
|
|
60
|
+
point: [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t],
|
|
61
|
+
tangent: [(b[0] - a[0]) / length, (b[1] - a[1]) / length],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
remaining -= segLength;
|
|
65
|
+
}
|
|
66
|
+
const first = path[0] ?? [0, 0];
|
|
67
|
+
return { point: first, tangent: [0, 1] };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Evenly spaced street-furniture anchors along a road's curb lines — streetlights, palms, signs,
|
|
71
|
+
* hydrants, benches. Each spot sits just outside the asphalt (plus `outset`), faces away from
|
|
72
|
+
* the street, and alternates sides by default so lights stagger like a real avenue. This is the
|
|
73
|
+
* answer to "where do I put it": furniture is an asset of the street, never a hand-typed coordinate.
|
|
74
|
+
*/
|
|
75
|
+
export function furnitureSpots(road, options = {}) {
|
|
76
|
+
const spacing = options.spacing ?? 24;
|
|
77
|
+
const sides = options.sides ?? "both";
|
|
78
|
+
const stagger = options.stagger ?? true;
|
|
79
|
+
const outset = options.outset ?? 0;
|
|
80
|
+
if (spacing <= 0)
|
|
81
|
+
return [];
|
|
82
|
+
const total = pathLength(road.path);
|
|
83
|
+
const curbOffset = road.width / 2 + 0.6 + outset;
|
|
84
|
+
const spots = [];
|
|
85
|
+
let index = 0;
|
|
86
|
+
for (let along = spacing / 2; along < total; along += spacing) {
|
|
87
|
+
const { point, tangent } = pointAlong(road.path, along);
|
|
88
|
+
const normal = [-tangent[1], tangent[0]];
|
|
89
|
+
const wanted = sides === "both" ? (stagger ? [index % 2 === 0 ? "left" : "right"] : ["left", "right"]) : [sides];
|
|
90
|
+
for (const side of wanted) {
|
|
91
|
+
const sign = side === "left" ? 1 : -1;
|
|
92
|
+
const position = [point[0] + normal[0] * curbOffset * sign, point[1] + normal[1] * curbOffset * sign];
|
|
93
|
+
spots.push({
|
|
94
|
+
position,
|
|
95
|
+
heading: Math.atan2(normal[0] * sign, normal[1] * sign),
|
|
96
|
+
side,
|
|
97
|
+
along,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
index += 1;
|
|
101
|
+
}
|
|
102
|
+
return spots;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Curbside parking anchors along a road: hugging the edge of the asphalt, headed parallel to the
|
|
106
|
+
* street in that side's direction of travel. Spawn parked vehicles here instead of eyeballing
|
|
107
|
+
* coordinates in the middle of the carriageway.
|
|
108
|
+
*/
|
|
109
|
+
export function parkingSpots(road, options = {}) {
|
|
110
|
+
const spacing = options.spacing ?? 18;
|
|
111
|
+
const sides = options.sides ?? "both";
|
|
112
|
+
if (spacing <= 0)
|
|
113
|
+
return [];
|
|
114
|
+
const total = pathLength(road.path);
|
|
115
|
+
const edgeOffset = road.width / 2 - 1.2;
|
|
116
|
+
const spots = [];
|
|
117
|
+
for (let along = spacing; along < total - spacing / 2; along += spacing) {
|
|
118
|
+
const { point, tangent } = pointAlong(road.path, along);
|
|
119
|
+
const normal = [-tangent[1], tangent[0]];
|
|
120
|
+
const wanted = sides === "both" ? ["left", "right"] : [sides];
|
|
121
|
+
for (const side of wanted) {
|
|
122
|
+
const sign = side === "left" ? 1 : -1;
|
|
123
|
+
const dir = side === "left" ? 1 : -1;
|
|
124
|
+
spots.push({
|
|
125
|
+
position: [point[0] + normal[0] * edgeOffset * sign, point[1] + normal[1] * edgeOffset * sign],
|
|
126
|
+
heading: Math.atan2(tangent[0] * dir, tangent[1] * dir),
|
|
127
|
+
side,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return spots;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* A deterministic point on one of a road's sidewalks at a normalized position — `side` picks the
|
|
135
|
+
* band, `fraction` (0..1) picks how far along. The canonical pedestrian spawn helper.
|
|
136
|
+
*/
|
|
137
|
+
export function sidewalkPoint(road, side, fraction) {
|
|
138
|
+
const paths = sidewalkPaths(road);
|
|
139
|
+
const path = side === "left" ? paths[0] : paths[1];
|
|
140
|
+
if (path === undefined)
|
|
141
|
+
return null;
|
|
142
|
+
const along = Math.max(0, Math.min(1, fraction)) * pathLength(path);
|
|
143
|
+
return pointAlong(path, along).point;
|
|
144
|
+
}
|
package/dist/world/terrain.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import type { EnvironmentWorldFeature, TerrainEnvironmentConfig, TerrainEnvironmentDescriptor, TerrainIslandDescriptor, TerrainMaterial, WorldBounds, WorldFeature } from "./features.js";
|
|
1
|
+
import type { BiomeBand, EnvironmentWorldFeature, TerrainDetailConfig, TerrainEnvironmentConfig, TerrainEnvironmentDescriptor, TerrainIslandDescriptor, TerrainMaterial, WorldBounds, WorldFeature } from "./features.js";
|
|
2
|
+
/** A surface normal vector at a terrain sample point. */
|
|
2
3
|
export type TerrainNormal = readonly [number, number, number];
|
|
4
|
+
/** A sampleable ground surface: height and normal at any x/z, with optional bounds and water level. */
|
|
3
5
|
export interface TerrainField {
|
|
4
6
|
sampleHeight(x: number, z: number): number;
|
|
5
7
|
sampleNormal(x: number, z: number): TerrainNormal;
|
|
6
8
|
readonly bounds?: WorldBounds;
|
|
7
9
|
readonly waterLevel?: number;
|
|
8
10
|
}
|
|
11
|
+
/** Smoothly interpolated 2D value noise in `[-1, 1]` for the given seed. */
|
|
9
12
|
export declare function valueNoise(x: number, z: number, seed: number): number;
|
|
13
|
+
/** Octave settings for {@link fractalNoise}: frequency, layering, and optional ridged shaping. */
|
|
10
14
|
export interface FractalNoiseConfig {
|
|
11
15
|
seed: number;
|
|
12
16
|
frequency: number;
|
|
@@ -15,11 +19,15 @@ export interface FractalNoiseConfig {
|
|
|
15
19
|
persistence: number;
|
|
16
20
|
ridged: boolean;
|
|
17
21
|
}
|
|
22
|
+
/** Layers `valueNoise` octaves per `config` into a single normalized noise sample. */
|
|
18
23
|
export declare function fractalNoise(x: number, z: number, config: FractalNoiseConfig): number;
|
|
19
24
|
export declare function seedFrom(value: string | number | undefined, fallback: number): number;
|
|
25
|
+
/** Derives a `TerrainField.sampleNormal` from a height sampler via finite-difference gradients. */
|
|
20
26
|
export declare function withNormal(sampleHeight: (x: number, z: number) => number): TerrainField["sampleNormal"];
|
|
21
27
|
export declare const FLAT_FIELD: TerrainField;
|
|
28
|
+
/** A flat, zero-height `TerrainField` for arenas with no elevation. */
|
|
22
29
|
export declare function flatField(): TerrainField;
|
|
30
|
+
/** Configuration for {@link noiseField}: seed, amplitude, and fractal noise shaping. */
|
|
23
31
|
export interface NoiseFieldConfig {
|
|
24
32
|
seed?: string | number;
|
|
25
33
|
amplitude?: number;
|
|
@@ -32,6 +40,7 @@ export interface NoiseFieldConfig {
|
|
|
32
40
|
bounds?: WorldBounds;
|
|
33
41
|
waterLevel?: number;
|
|
34
42
|
}
|
|
43
|
+
/** Builds a `TerrainField` whose height is fractal noise shaped by `config`. */
|
|
35
44
|
export declare function noiseField(config?: NoiseFieldConfig): TerrainField;
|
|
36
45
|
export interface RollingFieldConfig {
|
|
37
46
|
seed?: string | number;
|
|
@@ -45,8 +54,11 @@ export interface ArenaFieldConfig {
|
|
|
45
54
|
bounds?: WorldBounds;
|
|
46
55
|
}
|
|
47
56
|
export declare const ARENA_WATER_LEVEL = -0.9;
|
|
57
|
+
/** Builds a `TerrainField` with a flat spawn plateau, rolling hills, and a basin, for combat arenas. */
|
|
48
58
|
export declare function arenaField(config?: ArenaFieldConfig): TerrainField;
|
|
49
59
|
export declare function groundFieldFor(world?: WorldFeature): TerrainField;
|
|
60
|
+
/** Whether a world declares real terrain (base heightfield or islands) rather than a flat plane — gates terrain-floor sampling in the movement controllers. */
|
|
61
|
+
export declare function hasEnvironmentTerrain(world: WorldFeature | undefined): boolean;
|
|
50
62
|
/** Ground height between islands when no base terrain exists — deep enough to read as a fall into the void, finite so physics stays sane. */
|
|
51
63
|
export declare const ISLAND_VOID_HEIGHT = -256;
|
|
52
64
|
/**
|
|
@@ -77,13 +89,26 @@ export interface TerrainPalette {
|
|
|
77
89
|
}
|
|
78
90
|
export declare const DEFAULT_TERRAIN_MATERIAL: TerrainMaterial;
|
|
79
91
|
export declare const TERRAIN_MATERIAL_PALETTES: Record<TerrainMaterial, TerrainPalette>;
|
|
92
|
+
/** A {@link TerrainDetailConfig} with every field resolved to a concrete value — the shape the shell's detail material consumes. */
|
|
93
|
+
export type ResolvedTerrainDetail = Required<Omit<TerrainDetailConfig, "waterLevel">> & {
|
|
94
|
+
waterLevel: number;
|
|
95
|
+
};
|
|
96
|
+
/** Fill a `TerrainDetailConfig` with defaults; `waterLevel` falls back to the terrain's own water level. */
|
|
97
|
+
export declare function resolveTerrainDetail(config: TerrainDetailConfig, terrainWaterLevel?: number): ResolvedTerrainDetail;
|
|
80
98
|
export declare function resolveTerrainPalette(descriptor?: Pick<TerrainEnvironmentConfig, "material" | "colors">): TerrainPalette;
|
|
81
99
|
/**
|
|
82
|
-
* Per-
|
|
83
|
-
* `
|
|
84
|
-
*
|
|
100
|
+
* Per-z palette sampler over a descriptor's `biomeBands` — ordered zones that cross-fade their
|
|
101
|
+
* ground palette across a `fade`-wide window centered on the midpoint z between adjacent centers.
|
|
102
|
+
* Below the first / above the last band clamps to that band's palette. Returns `fallback` when no
|
|
103
|
+
* bands are declared. Pure math, unit-testable independent of rendering.
|
|
104
|
+
*/
|
|
105
|
+
export declare function createBiomeBandSampler(bands: readonly BiomeBand[], fallback: TerrainPalette): (z: number) => TerrainPalette;
|
|
106
|
+
/**
|
|
107
|
+
* Per-position palette sampler over the descriptor's base `material`/`colors` plus its z-ordered
|
|
108
|
+
* `biomeBands` (painted first) and radial `materialRegions` (painted over) — the multi-biome coloring
|
|
109
|
+
* seam. Regions paint fully inside `radius` and blend back across `falloff`; later regions win overlaps.
|
|
85
110
|
*/
|
|
86
|
-
export declare function createTerrainPaletteSampler(descriptor: Pick<TerrainEnvironmentConfig, "material" | "colors" | "materialRegions">): (x: number, z: number) => TerrainPalette;
|
|
111
|
+
export declare function createTerrainPaletteSampler(descriptor: Pick<TerrainEnvironmentConfig, "material" | "colors" | "materialRegions" | "biomeBands">): (x: number, z: number) => TerrainPalette;
|
|
87
112
|
export interface HeightMapFieldConfig {
|
|
88
113
|
columns: number;
|
|
89
114
|
rows: number;
|
|
@@ -94,6 +119,7 @@ export interface HeightMapFieldConfig {
|
|
|
94
119
|
waterLevel?: number;
|
|
95
120
|
}
|
|
96
121
|
export declare function heightMapField(config: HeightMapFieldConfig): TerrainField;
|
|
122
|
+
/** Resolves a `TerrainEnvironmentDescriptor` into a concrete `TerrainField`, applying flatten masks. */
|
|
97
123
|
export declare function resolveTerrainField(descriptor?: TerrainEnvironmentDescriptor): TerrainField;
|
|
98
124
|
/** Returns `position` with `y` replaced by the field's ground height (plus `offset`) at its `x`/`z`. */
|
|
99
125
|
export declare function snapToGround(field: TerrainField, position: readonly [number, number, number], offset?: number): [number, number, number];
|
|
@@ -109,6 +135,7 @@ export interface GroundSnapEntityStore {
|
|
|
109
135
|
/** Ground-snaps an already-spawned entity in place; returns false when `id` is unknown. */
|
|
110
136
|
export declare function snapEntityToGround(entities: GroundSnapEntityStore, id: string, field: TerrainField, offset?: number): boolean;
|
|
111
137
|
export declare const DEFAULT_MAX_WALK_SLOPE = 0.6;
|
|
138
|
+
/** Zeroes out a movement step's x or z component where it would climb steeper than `maxSlope`. */
|
|
112
139
|
export declare function resolveGroundStep(field: TerrainField, x: number, z: number, stepX: number, stepZ: number, maxSlope?: number): {
|
|
113
140
|
stepX: number;
|
|
114
141
|
stepZ: number;
|