@jgengine/core 0.13.0 → 0.15.0-next.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 +529 -1
- package/dist/ai/difficulty.d.ts +154 -0
- package/dist/ai/difficulty.js +194 -0
- package/dist/ai/driver.d.ts +107 -0
- package/dist/ai/driver.js +175 -0
- package/dist/ai/flock.js +13 -23
- package/dist/ai/laneSelect.js +2 -1
- package/dist/ai/mobBrain.js +2 -1
- package/dist/ai/waveRunner.d.ts +81 -0
- package/dist/ai/waveRunner.js +104 -0
- package/dist/authoring.d.ts +6 -4
- package/dist/authoring.js +6 -4
- package/dist/combat/castRunner.d.ts +13 -1
- package/dist/combat/castRunner.js +5 -1
- package/dist/combat/comboMeter.d.ts +140 -0
- package/dist/combat/comboMeter.js +215 -0
- package/dist/combat/comboPoints.d.ts +11 -0
- package/dist/combat/comboPoints.js +11 -2
- package/dist/combat/damageResolution.d.ts +1 -1
- package/dist/combat/defensiveWindow.d.ts +5 -0
- package/dist/combat/effects.d.ts +24 -0
- package/dist/combat/effects.js +6 -1
- package/dist/combat/projectiles.d.ts +6 -0
- package/dist/combat/projectiles.js +2 -2
- package/dist/combat/regenShield.d.ts +40 -2
- package/dist/combat/regenShield.js +66 -18
- package/dist/combat/resourcePool.d.ts +19 -0
- package/dist/combat/resourcePool.js +20 -2
- package/dist/combat/shotOrigin.d.ts +5 -0
- package/dist/combat/statusApplication.d.ts +1 -1
- package/dist/combat/statusApplication.js +2 -1
- package/dist/combat/statusEffectView.d.ts +56 -0
- package/dist/combat/statusEffectView.js +52 -0
- package/dist/combat.d.ts +4 -2
- package/dist/combat.js +2 -0
- package/dist/devtools/fallbackSeams.d.ts +3 -2
- package/dist/devtools/fallbackSeams.js +1 -1
- package/dist/devtools/textureErrors.d.ts +40 -0
- package/dist/devtools/textureErrors.js +68 -0
- package/dist/economy/shopStock.d.ts +110 -0
- package/dist/economy/shopStock.js +157 -0
- package/dist/economy/wallet.d.ts +6 -0
- package/dist/economy/wallet.js +1 -0
- package/dist/game/achievements.d.ts +80 -0
- package/dist/game/achievements.js +124 -0
- package/dist/game/cameraConfig.d.ts +7 -0
- package/dist/game/codex.d.ts +60 -0
- package/dist/game/codex.js +89 -0
- package/dist/game/credits.d.ts +36 -0
- package/dist/game/credits.js +0 -0
- package/dist/game/defineGame.d.ts +28 -3
- package/dist/game/defineGame.js +5 -0
- package/dist/game/dialogueGraph.d.ts +116 -0
- package/dist/game/dialogueGraph.js +103 -0
- package/dist/game/eventTicker.d.ts +95 -0
- package/dist/game/eventTicker.js +91 -0
- package/dist/game/events.d.ts +24 -5
- package/dist/game/events.js +18 -0
- package/dist/game/gamePhase.d.ts +1 -1
- package/dist/game/gamePhase.js +1 -1
- package/dist/game/leaderboardRank.d.ts +71 -0
- package/dist/game/leaderboardRank.js +76 -0
- package/dist/game/lootTable.d.ts +10 -1
- package/dist/game/lootTable.js +4 -2
- package/dist/game/notifications.d.ts +57 -0
- package/dist/game/notifications.js +107 -0
- package/dist/game/playableGame.d.ts +39 -2
- package/dist/game/quest.d.ts +26 -0
- package/dist/game/quest.js +31 -0
- package/dist/game/runDraft.js +2 -1
- package/dist/game/saveSlots.d.ts +102 -0
- package/dist/game/saveSlots.js +136 -0
- package/dist/game/talentTreeView.d.ts +106 -0
- package/dist/game/talentTreeView.js +114 -0
- package/dist/game/worldItem.js +2 -1
- package/dist/gameplay.d.ts +15 -4
- package/dist/gameplay.js +13 -2
- package/dist/i18n/i18n.d.ts +51 -0
- package/dist/i18n/i18n.js +82 -0
- package/dist/input/axisInput.d.ts +1 -1
- package/dist/input/axisInput.js +18 -2
- package/dist/input/rebindSession.d.ts +141 -0
- package/dist/input/rebindSession.js +235 -0
- package/dist/input/touchControlsMode.d.ts +16 -0
- package/dist/input/touchControlsMode.js +23 -0
- package/dist/input/touchScheme.d.ts +30 -1
- package/dist/input/touchScheme.js +31 -1
- package/dist/interaction/promptRegistry.d.ts +78 -0
- package/dist/interaction/promptRegistry.js +130 -0
- package/dist/inventory/storageTier.js +3 -2
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +21 -1
- package/dist/movement/movementModel.d.ts +26 -3
- package/dist/movement/movementModel.js +121 -15
- package/dist/movement/playerMovement.js +161 -25
- package/dist/physics/drivableVehicle.js +3 -1
- package/dist/physics/kinematicVehicle.d.ts +56 -1
- package/dist/physics/kinematicVehicle.js +99 -10
- package/dist/physics/vehicleObstacles.js +19 -0
- package/dist/random/resolveRng.d.ts +14 -0
- package/dist/random/resolveRng.js +31 -0
- package/dist/render/postProcessing.d.ts +12 -1
- package/dist/runtime/cameraDirector.d.ts +2 -2
- package/dist/runtime/context/combat.js +8 -0
- package/dist/runtime/context/scene.d.ts +2 -0
- package/dist/runtime/context/scene.js +48 -1
- package/dist/runtime/gameContext.js +8 -2
- package/dist/runtime/gameContextTypes.d.ts +39 -0
- package/dist/runtime/inputSnapshot.d.ts +10 -1
- package/dist/runtime/inputSnapshot.js +13 -1
- package/dist/runtime/runtimeSave.d.ts +3 -3
- package/dist/runtime/runtimeSave.js +11 -2
- package/dist/scene/assetDiagnostics.js +2 -2
- package/dist/scene/autoTarget.js +2 -1
- package/dist/scene/builtinSceneKinds.d.ts +6 -1
- package/dist/scene/builtinSceneKinds.js +10 -3
- package/dist/scene/captureCheck.js +3 -2
- package/dist/scene/colliders.d.ts +20 -0
- package/dist/scene/colliders.js +78 -0
- package/dist/scene/collisionMesh.d.ts +8 -0
- package/dist/scene/collisionMesh.js +48 -0
- package/dist/scene/entityStore.d.ts +4 -1
- package/dist/scene/entityStore.js +3 -0
- package/dist/scene/objectStore.d.ts +10 -0
- package/dist/scene/objectStore.js +29 -6
- package/dist/scene/sceneKinds.d.ts +37 -0
- package/dist/scene/sceneKinds.js +19 -0
- package/dist/scene/sequenceDirector.d.ts +110 -0
- package/dist/scene/sequenceDirector.js +146 -0
- package/dist/session/roles.js +3 -2
- package/dist/settings/settingsModel.d.ts +1 -0
- package/dist/settings/settingsModel.js +1 -0
- package/dist/stats/rollCheck.js +4 -2
- package/dist/time/dayNightCycle.d.ts +114 -0
- package/dist/time/dayNightCycle.js +208 -0
- package/dist/time/timerSet.d.ts +132 -0
- package/dist/time/timerSet.js +243 -0
- package/dist/ui/accessibility.d.ts +51 -0
- package/dist/ui/accessibility.js +83 -0
- package/dist/ui/coachMarks.d.ts +99 -0
- package/dist/ui/coachMarks.js +126 -0
- package/dist/ui/floatingText.d.ts +114 -0
- package/dist/ui/floatingText.js +203 -0
- package/dist/ui/hudScale.d.ts +16 -1
- package/dist/ui/hudScale.js +22 -1
- package/dist/ui/modalStack.d.ts +143 -0
- package/dist/ui/modalStack.js +160 -0
- package/dist/ui/objectiveBanner.d.ts +123 -0
- package/dist/ui/objectiveBanner.js +162 -0
- package/dist/ui/photoMode.d.ts +32 -0
- package/dist/ui/photoMode.js +51 -0
- package/dist/ui/radialMenu.d.ts +47 -0
- package/dist/ui/radialMenu.js +74 -0
- package/dist/ui/screenMarkers.d.ts +128 -0
- package/dist/ui/screenMarkers.js +118 -0
- package/dist/ui.d.ts +8 -0
- package/dist/ui.js +8 -0
- package/dist/vfx/cameraShake.d.ts +97 -0
- package/dist/vfx/cameraShake.js +136 -0
- package/dist/vfx/damageDirection.d.ts +102 -0
- package/dist/vfx/damageDirection.js +188 -0
- package/dist/vfx/particles.d.ts +103 -0
- package/dist/vfx/particles.js +256 -0
- package/dist/vfx/screenEffects.d.ts +149 -0
- package/dist/vfx/screenEffects.js +191 -0
- package/dist/world/authoredObjects.d.ts +18 -0
- package/dist/world/authoredObjects.js +21 -0
- package/dist/world/authoredSpawn.js +19 -0
- package/dist/world/buildingLots.d.ts +54 -11
- package/dist/world/buildingLots.js +116 -30
- package/dist/world/cityBlocks.d.ts +45 -0
- package/dist/world/cityBlocks.js +159 -0
- package/dist/world/cityContent.d.ts +46 -2
- package/dist/world/cityContent.js +160 -2
- package/dist/world/cityGenerator.d.ts +261 -23
- package/dist/world/cityGenerator.js +643 -33
- package/dist/world/cityGeometry.d.ts +20 -0
- package/dist/world/cityGeometry.js +91 -0
- package/dist/world/cityKind.js +9 -46
- package/dist/world/fastTravel.d.ts +64 -0
- package/dist/world/fastTravel.js +108 -0
- package/dist/world/features.d.ts +16 -0
- package/dist/world/features.js +2 -0
- package/dist/world/grassKind.js +4 -1
- package/dist/world/grounded.d.ts +11 -0
- package/dist/world/grounded.js +12 -0
- package/dist/world/mapAnnotations.d.ts +75 -0
- package/dist/world/mapAnnotations.js +139 -0
- package/dist/world/markers.js +1 -0
- package/dist/world/resourceNode.d.ts +1 -1
- package/dist/world/resourceNode.js +2 -1
- package/dist/world/roads.d.ts +294 -5
- package/dist/world/roads.js +1262 -21
- package/dist/world/scatterCoverage.d.ts +98 -0
- package/dist/world/scatterCoverage.js +101 -0
- package/dist/world/scatterRegion.js +6 -0
- package/dist/world/sharedAnnotations.d.ts +84 -0
- package/dist/world/sharedAnnotations.js +76 -0
- package/dist/world/spawnOverride.d.ts +37 -0
- package/dist/world/spawnOverride.js +50 -0
- package/dist/world/streetGenerator.d.ts +60 -11
- package/dist/world/streetGenerator.js +1786 -129
- package/dist/world/terrain.d.ts +1 -0
- package/dist/world/terrain.js +2 -0
- package/dist/world/waypoints.d.ts +63 -0
- package/dist/world/waypoints.js +137 -0
- package/dist/world.d.ts +18 -2
- package/dist/world.js +18 -2
- package/package.json +2 -2
- package/skills/ce-handoff/SKILL.md +149 -0
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +7 -3
- package/skills/jgengine/api.md +17 -7
- package/skills/jgengine/capabilities.md +12 -0
- package/skills/jgengine/recipes/minimal-game.md +7 -4
- package/skills/jgengine-combat/api.md +40 -4
- package/skills/jgengine-combat/capabilities.md +12 -0
- package/skills/jgengine-combat/reference.md +2 -0
- package/skills/jgengine-gameplay/api.md +212 -11
- package/skills/jgengine-gameplay/capabilities.md +66 -1
- package/skills/jgengine-world/api.md +408 -12
- package/skills/jgengine-world/capabilities.md +127 -2
- package/skills/jgengine-world/reference.md +15 -1
|
@@ -94,6 +94,51 @@ export declare function extractBlocks(streets: readonly FabricStreet[], hx: numb
|
|
|
94
94
|
width: number;
|
|
95
95
|
}[];
|
|
96
96
|
};
|
|
97
|
+
/** Minimal node/edge shapes {@link extractGraphBlocks} needs (matches `StreetNetwork.nodes`/`edges`). */
|
|
98
|
+
export interface GraphBlockNode {
|
|
99
|
+
x: number;
|
|
100
|
+
z: number;
|
|
101
|
+
}
|
|
102
|
+
/** One street-graph edge {@link extractGraphBlocks} traces block faces through. */
|
|
103
|
+
export interface GraphBlockEdge {
|
|
104
|
+
/** Endpoint node indices. `points[0]` sits exactly on node `a`, the last on node `b`. */
|
|
105
|
+
a: number;
|
|
106
|
+
b: number;
|
|
107
|
+
points: readonly (readonly [number, number])[];
|
|
108
|
+
width: number;
|
|
109
|
+
level: FabricStreet["level"];
|
|
110
|
+
/** Owning street index, carried through to pruned dead-end corridors so callers can line them. */
|
|
111
|
+
street?: number;
|
|
112
|
+
}
|
|
113
|
+
/** One block from {@link extractGraphBlocks}: the face polygon and its inset land ring. */
|
|
114
|
+
export interface GraphBlockRings {
|
|
115
|
+
/** Face polygon on road CENTERLINES (as traced, interior faces CCW). */
|
|
116
|
+
face: Vec2[];
|
|
117
|
+
/** Land ring: face inset by each bordering edge's half width + curb margin + sidewalk band. */
|
|
118
|
+
land: Vec2[];
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Extract closed blocks from an EXACT street graph — the `nodes`/`edges` a street network already
|
|
122
|
+
* carries — instead of re-deriving topology from welded polylines. Half-edges are ordered by true
|
|
123
|
+
* departure angle at each node, dangling chains are pruned first (returned as `deadEnds` for
|
|
124
|
+
* corridor carving), interior faces are traced through the rotation system, and each face is inset
|
|
125
|
+
* to its land ring by the bordering edges' half-widths (+ curb + sidewalk). Robust against wandered
|
|
126
|
+
* and arc-filleted centerlines, which defeat proximity welding. Pure bounded geometry.
|
|
127
|
+
*
|
|
128
|
+
* @capability city-district extract block polygons directly from a street network's exact graph
|
|
129
|
+
*/
|
|
130
|
+
export declare function extractGraphBlocks(nodes: readonly GraphBlockNode[], edges: readonly GraphBlockEdge[], params: {
|
|
131
|
+
sidewalkBase: number;
|
|
132
|
+
curbMargin: number;
|
|
133
|
+
}): {
|
|
134
|
+
blocks: GraphBlockRings[];
|
|
135
|
+
deadEnds: {
|
|
136
|
+
pts: Vec2[];
|
|
137
|
+
width: number;
|
|
138
|
+
level: FabricStreet["level"];
|
|
139
|
+
street?: number;
|
|
140
|
+
}[];
|
|
141
|
+
};
|
|
97
142
|
/** Arc-length station lookup along a closed ring. */
|
|
98
143
|
export declare class RingWalker {
|
|
99
144
|
readonly ring: readonly Vec2[];
|
package/dist/world/cityBlocks.js
CHANGED
|
@@ -11,8 +11,20 @@
|
|
|
11
11
|
* @capability city-district road-derived block polygons, parcel subdivision, buildable footprints
|
|
12
12
|
*/
|
|
13
13
|
import { clipHalfPlane, dedupeRing, distanceToRing, ensureCcw, extractSimpleLoop, insetRing, pointInPolygon, polygonArea, polygonMeanWidth, polygonSignedArea, } from "./cityGeometry.js";
|
|
14
|
+
import { isProductionEnvironment } from "../devtools/saveEndpoint.js";
|
|
14
15
|
const MAX_BLOCKS = 420;
|
|
15
16
|
const WELD_RADIUS = 1.4;
|
|
17
|
+
// Proximity welding (WELD_RADIUS) assumes near-collinear / aligned centerlines. Wandered and
|
|
18
|
+
// arc-filleted streets weld into a degenerate face set: a real case went 38 streets → 2 faces
|
|
19
|
+
// with no signal. These constants gate a dev-mode collapse warning. A connected road grid has
|
|
20
|
+
// bounded faces on the order of its street count (Euler: bounded faces = edges − nodes + 1), so
|
|
21
|
+
// faces far below the street count means welding ate the topology, not that the district is sparse.
|
|
22
|
+
// COLLAPSE_MIN_STREETS avoids false positives on genuinely tiny inputs (a handful of crossings
|
|
23
|
+
// legitimately yields few faces); COLLAPSE_RATIO_DIVISOR flags only a gross collapse — with 8
|
|
24
|
+
// streets it fires under 2 faces, with 38 streets under 9 (the 38 → 2 case triggers, a healthy
|
|
25
|
+
// grid where faces ≥ street count never does).
|
|
26
|
+
const COLLAPSE_MIN_STREETS = 8;
|
|
27
|
+
const COLLAPSE_RATIO_DIVISOR = 4;
|
|
16
28
|
const RIM_SNAP = 6;
|
|
17
29
|
/** Sidewalk band width by street hierarchy (base = the district's street-level width). */
|
|
18
30
|
export function sidewalkWidthFor(level, base) {
|
|
@@ -346,6 +358,7 @@ function extractFaces(nodes, halves) {
|
|
|
346
358
|
export function extractBlocks(streets, hx, hz, params) {
|
|
347
359
|
const { nodes, halves, deadEnds } = buildGraph(streets, hx, hz);
|
|
348
360
|
const faces = extractFaces(nodes, halves);
|
|
361
|
+
warnOnImplausibleCollapse(streets.length, faces.length);
|
|
349
362
|
faces.sort((a, b) => polygonArea(b.polygon) - polygonArea(a.polygon));
|
|
350
363
|
const blocks = [];
|
|
351
364
|
for (const face of faces) {
|
|
@@ -391,6 +404,26 @@ export function extractBlocks(streets, hx, hz, params) {
|
|
|
391
404
|
}
|
|
392
405
|
return { blocks, deadEnds };
|
|
393
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* Dev-mode guard: warn when {@link extractBlocks} extracts implausibly few faces for the street
|
|
409
|
+
* count — the silent-collapse failure mode where proximity welding folds wandered / arc-filleted
|
|
410
|
+
* centerlines into a degenerate face set (see issue #1502: 38 streets → 2 faces). No-ops in a
|
|
411
|
+
* production build. Points at {@link extractGraphBlocks}, which traces faces from the exact street
|
|
412
|
+
* graph and is robust against the centerlines that defeat welding.
|
|
413
|
+
*/
|
|
414
|
+
function warnOnImplausibleCollapse(streetCount, faceCount) {
|
|
415
|
+
if (isProductionEnvironment() || typeof console === "undefined")
|
|
416
|
+
return;
|
|
417
|
+
if (streetCount < COLLAPSE_MIN_STREETS)
|
|
418
|
+
return;
|
|
419
|
+
if (faceCount >= streetCount / COLLAPSE_RATIO_DIVISOR)
|
|
420
|
+
return;
|
|
421
|
+
console.warn(`[jgengine:extractBlocks] ${streetCount} streets extracted only ${faceCount} block face(s) — ` +
|
|
422
|
+
"implausibly few, likely a silent proximity-welding collapse on wandered / arc-filleted " +
|
|
423
|
+
"centerlines (the WELD_RADIUS heuristic assumes near-aligned streets). The fabric will look " +
|
|
424
|
+
"empty rather than error. Trace blocks from the exact street graph with extractGraphBlocks() " +
|
|
425
|
+
"instead, which is robust against centerlines that defeat welding.");
|
|
426
|
+
}
|
|
394
427
|
/** Reverse per-edge street labels when a ring got rewound to CCW. */
|
|
395
428
|
function remapReversedEdgeStreets(streetsPerEdge) {
|
|
396
429
|
const n = streetsPerEdge.length;
|
|
@@ -400,6 +433,132 @@ function remapReversedEdgeStreets(streetsPerEdge) {
|
|
|
400
433
|
out[i] = streetsPerEdge[(2 * n - 2 - i) % n];
|
|
401
434
|
return out;
|
|
402
435
|
}
|
|
436
|
+
/**
|
|
437
|
+
* Extract closed blocks from an EXACT street graph — the `nodes`/`edges` a street network already
|
|
438
|
+
* carries — instead of re-deriving topology from welded polylines. Half-edges are ordered by true
|
|
439
|
+
* departure angle at each node, dangling chains are pruned first (returned as `deadEnds` for
|
|
440
|
+
* corridor carving), interior faces are traced through the rotation system, and each face is inset
|
|
441
|
+
* to its land ring by the bordering edges' half-widths (+ curb + sidewalk). Robust against wandered
|
|
442
|
+
* and arc-filleted centerlines, which defeat proximity welding. Pure bounded geometry.
|
|
443
|
+
*
|
|
444
|
+
* @capability city-district extract block polygons directly from a street network's exact graph
|
|
445
|
+
*/
|
|
446
|
+
export function extractGraphBlocks(nodes, edges, params) {
|
|
447
|
+
// --- prune dangling chains: an edge touching a degree-1 node can't border a closed face ---
|
|
448
|
+
const degree = new Array(nodes.length).fill(0);
|
|
449
|
+
const alive = edges.map((e) => e.a !== e.b && e.points.length >= 2);
|
|
450
|
+
edges.forEach((e, i) => {
|
|
451
|
+
if (!alive[i])
|
|
452
|
+
return;
|
|
453
|
+
degree[e.a] += 1;
|
|
454
|
+
degree[e.b] += 1;
|
|
455
|
+
});
|
|
456
|
+
const deadEnds = [];
|
|
457
|
+
let pruned = true;
|
|
458
|
+
while (pruned) {
|
|
459
|
+
pruned = false;
|
|
460
|
+
for (let i = 0; i < edges.length; i += 1) {
|
|
461
|
+
if (!alive[i])
|
|
462
|
+
continue;
|
|
463
|
+
const e = edges[i];
|
|
464
|
+
if (degree[e.a] === 1 || degree[e.b] === 1) {
|
|
465
|
+
alive[i] = false;
|
|
466
|
+
deadEnds.push({ pts: e.points.map((p) => [p[0], p[1]]), width: e.width, level: e.level, street: e.street });
|
|
467
|
+
degree[e.a] -= 1;
|
|
468
|
+
degree[e.b] -= 1;
|
|
469
|
+
pruned = true;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const halves = [];
|
|
474
|
+
const out = nodes.map(() => []);
|
|
475
|
+
const departAngle = (pts) => {
|
|
476
|
+
// First non-degenerate segment sets the true departure direction.
|
|
477
|
+
for (let i = 1; i < pts.length; i += 1) {
|
|
478
|
+
const dx = pts[i][0] - pts[0][0];
|
|
479
|
+
const dz = pts[i][1] - pts[0][1];
|
|
480
|
+
if (dx * dx + dz * dz > 1e-12)
|
|
481
|
+
return Math.atan2(dz, dx);
|
|
482
|
+
}
|
|
483
|
+
return 0;
|
|
484
|
+
};
|
|
485
|
+
const angles = [];
|
|
486
|
+
edges.forEach((e, ei) => {
|
|
487
|
+
if (!alive[ei])
|
|
488
|
+
return;
|
|
489
|
+
const fwd = e.points.map((p) => [p[0], p[1]]);
|
|
490
|
+
const rev = [...fwd].reverse();
|
|
491
|
+
const h0 = halves.length;
|
|
492
|
+
halves.push({ to: e.b, pts: fwd, twin: h0 + 1, edge: ei, visited: false });
|
|
493
|
+
halves.push({ to: e.a, pts: rev, twin: h0, edge: ei, visited: false });
|
|
494
|
+
angles.push(departAngle(fwd), departAngle(rev));
|
|
495
|
+
out[e.a].push(h0);
|
|
496
|
+
out[e.b].push(h0 + 1);
|
|
497
|
+
});
|
|
498
|
+
for (const list of out)
|
|
499
|
+
list.sort((a, b) => angles[a] - angles[b]);
|
|
500
|
+
// --- trace interior faces: at each destination, rotate clockwise from the twin ---
|
|
501
|
+
const blocks = [];
|
|
502
|
+
for (let start = 0; start < halves.length && blocks.length < MAX_BLOCKS; start += 1) {
|
|
503
|
+
if (halves[start].visited)
|
|
504
|
+
continue;
|
|
505
|
+
const polygon = [];
|
|
506
|
+
const segEdge = [];
|
|
507
|
+
let current = start;
|
|
508
|
+
let guard = 0;
|
|
509
|
+
let closed = false;
|
|
510
|
+
while (guard < halves.length + 4) {
|
|
511
|
+
guard += 1;
|
|
512
|
+
const half = halves[current];
|
|
513
|
+
if (half.visited)
|
|
514
|
+
break;
|
|
515
|
+
half.visited = true;
|
|
516
|
+
for (let i = 0; i + 1 < half.pts.length; i += 1) {
|
|
517
|
+
polygon.push(half.pts[i]);
|
|
518
|
+
segEdge.push(half.edge);
|
|
519
|
+
}
|
|
520
|
+
const list = out[half.to];
|
|
521
|
+
const idx = list.indexOf(half.twin);
|
|
522
|
+
if (idx < 0)
|
|
523
|
+
break;
|
|
524
|
+
current = list[(idx - 1 + list.length) % list.length];
|
|
525
|
+
if (current === start) {
|
|
526
|
+
closed = true;
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (!closed || polygon.length < 3)
|
|
531
|
+
continue;
|
|
532
|
+
if (polygonSignedArea(polygon) <= 1)
|
|
533
|
+
continue; // outer face (CW) or degenerate sliver
|
|
534
|
+
// Dedupe near-coincident vertices while keeping each surviving segment's source edge.
|
|
535
|
+
const ring = [];
|
|
536
|
+
const ringEdge = [];
|
|
537
|
+
for (let i = 0; i < polygon.length; i += 1) {
|
|
538
|
+
const p = polygon[i];
|
|
539
|
+
const prev = ring[ring.length - 1];
|
|
540
|
+
if (prev !== undefined && Math.hypot(prev[0] - p[0], prev[1] - p[1]) < 0.02)
|
|
541
|
+
continue;
|
|
542
|
+
ring.push(p);
|
|
543
|
+
ringEdge.push(segEdge[i]);
|
|
544
|
+
}
|
|
545
|
+
while (ring.length > 1 && Math.hypot(ring[0][0] - ring[ring.length - 1][0], ring[0][1] - ring[ring.length - 1][1]) < 0.02) {
|
|
546
|
+
ring.pop();
|
|
547
|
+
ringEdge.pop();
|
|
548
|
+
}
|
|
549
|
+
if (ring.length < 3)
|
|
550
|
+
continue;
|
|
551
|
+
const dist = ring.map((_, i) => {
|
|
552
|
+
const e = edges[ringEdge[i]];
|
|
553
|
+
const walk = e.level === "lane" ? 0 : sidewalkWidthFor(e.level, params.sidewalkBase);
|
|
554
|
+
return e.width / 2 + params.curbMargin + walk;
|
|
555
|
+
});
|
|
556
|
+
const land = insetRing(ring, dist);
|
|
557
|
+
blocks.push({ face: ring, land });
|
|
558
|
+
}
|
|
559
|
+
blocks.sort((a, b) => polygonArea(b.land) - polygonArea(a.land));
|
|
560
|
+
return { blocks, deadEnds };
|
|
561
|
+
}
|
|
403
562
|
/** Arc-length station lookup along a closed ring. */
|
|
404
563
|
export class RingWalker {
|
|
405
564
|
ring;
|
|
@@ -14,6 +14,15 @@ import type { WeightedParamEntry } from "../scene/sceneKinds.js";
|
|
|
14
14
|
export type CityLotClass = "tower" | "slab" | "shop" | "rowhouse" | "house" | "mansion" | "farmhouse" | "barn" | "silo";
|
|
15
15
|
/** All classes, for schema hints and validation. */
|
|
16
16
|
export declare const CITY_LOT_CLASSES: readonly CityLotClass[];
|
|
17
|
+
/**
|
|
18
|
+
* Filler classes used ONLY by the block-interior fill pass (`resolveCityLotContent` at a high
|
|
19
|
+
* `blockFill`). They are deliberately kept OUT of {@link CITY_LOT_CLASSES} so a zone mix can never
|
|
20
|
+
* weight them onto street frontage — they exist to pack the backs of Manhattan-style blocks with
|
|
21
|
+
* parking structures and warehouses/depots, mixed in with ordinary zone classes.
|
|
22
|
+
*/
|
|
23
|
+
export type CityFillerClass = "garage" | "depot";
|
|
24
|
+
/** All filler classes, for validation and interior-pass allow-lists. */
|
|
25
|
+
export declare const CITY_FILLER_CLASSES: readonly CityFillerClass[];
|
|
17
26
|
/** Zone band a lot falls in: dense core, middle ring, or the district edge. */
|
|
18
27
|
export type CityZoneBand = "core" | "mid" | "edge";
|
|
19
28
|
/** How the radial zone metric maps to bands. */
|
|
@@ -37,6 +46,11 @@ export interface CityLotPiece {
|
|
|
37
46
|
/** Banded walls receive the window-strip treatment in the renderer. */
|
|
38
47
|
banded: boolean;
|
|
39
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Natural frontage-width range (meters) of a class's massing — what the plot-size class bias reads
|
|
51
|
+
* so towers land on wide plots and rowhouses on narrow ones. @internal
|
|
52
|
+
*/
|
|
53
|
+
export declare function classWidthRange(cls: string): readonly [number, number] | undefined;
|
|
40
54
|
/** Placement numbers a resolved lot carries out of the class profile. @internal */
|
|
41
55
|
export interface ClassPlacement {
|
|
42
56
|
width: number;
|
|
@@ -52,7 +66,16 @@ export interface ClassPlacement {
|
|
|
52
66
|
* aligned building wall; spacing likewise, so neighbors form rhythmic rows instead of scatter.
|
|
53
67
|
* @internal
|
|
54
68
|
*/
|
|
55
|
-
export declare function rollClassPlacement(cls: CityLotClass, rng: () => number, lotScale: number, floorsMin: number, floorsMax: number, setbackBase: number, spacingBase: number): ClassPlacement;
|
|
69
|
+
export declare function rollClassPlacement(cls: CityLotClass | CityFillerClass, rng: () => number, lotScale: number, floorsMin: number, floorsMax: number, setbackBase: number, spacingBase: number, widthStretch?: number): ClassPlacement;
|
|
70
|
+
/**
|
|
71
|
+
* Weight multiplier favouring classes whose natural frontage fits a plot of width `plotW`: a class
|
|
72
|
+
* far wider than the plot (a tower profile on a 6 m slice) is heavily downweighted, a class that
|
|
73
|
+
* would rattle around a much wider plot is mildly downweighted, and anything whose width range
|
|
74
|
+
* genuinely fits keeps weight 1. Pure data over {@link CLASS_PROFILES} — this is what turns a
|
|
75
|
+
* mixed-plot frontage into terraces on the slices and detached houses on the wide parcels.
|
|
76
|
+
* @internal
|
|
77
|
+
*/
|
|
78
|
+
export declare function classPlotFit(cls: CityLotClass, plotW: number): number;
|
|
56
79
|
/**
|
|
57
80
|
* Normalized zone metric for a point in district-local space: 0 at the center line, 1 at the rim,
|
|
58
81
|
* shaped to the district's rectangle (Chebyshev) so bands follow the volume's footprint.
|
|
@@ -68,7 +91,28 @@ export declare function pickClass(mix: readonly WeightedParamEntry[], roll: numb
|
|
|
68
91
|
* floors, and stream always yield the identical silhouette. Sizes are in meters, lot-local.
|
|
69
92
|
* @internal
|
|
70
93
|
*/
|
|
71
|
-
export declare function buildLotPieces(cls: CityLotClass, width: number, depth: number, floors: number, floorHeight: number, rng: () => number): CityLotPiece[];
|
|
94
|
+
export declare function buildLotPieces(cls: CityLotClass | CityFillerClass, width: number, depth: number, floors: number, floorHeight: number, rng: () => number): CityLotPiece[];
|
|
95
|
+
/**
|
|
96
|
+
* A block-scale landmark class. Unlike {@link CityLotClass} these are never weighted into a zone
|
|
97
|
+
* mix — the landmark pass in `resolveCityLotContent` picks clusters of adjacent lots and stamps one
|
|
98
|
+
* of these over them. Each expresses with the same four piece shapes (box/gable/cylinder/dome) at a
|
|
99
|
+
* much larger footprint (~40–90 m).
|
|
100
|
+
*/
|
|
101
|
+
export type CityLandmarkClass = "hall" | "arena" | "market" | "campus";
|
|
102
|
+
/** All landmark classes, for schema hints, validation, and default allow-lists. */
|
|
103
|
+
export declare const CITY_LANDMARK_CLASSES: readonly CityLandmarkClass[];
|
|
104
|
+
/** Minimum depth-into-block factor for a landmark class (the pass grows shallow clusters to this). @internal */
|
|
105
|
+
export declare function landmarkMinDepthFactor(cls: CityLandmarkClass): number;
|
|
106
|
+
/** Roll a landmark's floor count from its profile range. @internal */
|
|
107
|
+
export declare function landmarkFloors(cls: CityLandmarkClass, rng: () => number): number;
|
|
108
|
+
/**
|
|
109
|
+
* Compose the massing pieces for one landmark parcel, filling the given `width`×`depth` footprint
|
|
110
|
+
* (lot-local, x along frontage, z into the block). Deterministic per `rng`. Every piece is kept
|
|
111
|
+
* inside the `[-width/2, width/2] × [-depth/2, depth/2]` box so the reported footprint matches the
|
|
112
|
+
* merged cluster's AABB. Sizes are in meters.
|
|
113
|
+
* @internal
|
|
114
|
+
*/
|
|
115
|
+
export declare function buildLandmarkPieces(cls: CityLandmarkClass, width: number, depth: number, floors: number, floorHeight: number, rng: () => number): CityLotPiece[];
|
|
72
116
|
/** Tree species a district's tree mix can weight — the renderer keeps one canopy mesh per species. */
|
|
73
117
|
export type CityTreeSpecies = "broadleaf" | "conifer" | "palm" | "cypress";
|
|
74
118
|
/** All species, for schema hints and validation. */
|
|
@@ -10,6 +10,8 @@ export const CITY_LOT_CLASSES = [
|
|
|
10
10
|
"barn",
|
|
11
11
|
"silo",
|
|
12
12
|
];
|
|
13
|
+
/** All filler classes, for validation and interior-pass allow-lists. */
|
|
14
|
+
export const CITY_FILLER_CLASSES = ["garage", "depot"];
|
|
13
15
|
const CLASS_PROFILES = {
|
|
14
16
|
tower: { width: [16, 24], depth: [14, 20], setbackFactor: 0.3, spacingFactor: 1, floors: [12, 34], backRow: true },
|
|
15
17
|
slab: { width: [14, 20], depth: [11, 15], setbackFactor: 0.5, spacingFactor: 1, floors: [4, 9], backRow: true },
|
|
@@ -20,7 +22,17 @@ const CLASS_PROFILES = {
|
|
|
20
22
|
farmhouse: { width: [11, 14], depth: [9, 12], setbackFactor: 2.8, spacingFactor: 4, floors: [2, 2], backRow: false },
|
|
21
23
|
barn: { width: [12, 16], depth: [9, 13], setbackFactor: 3.6, spacingFactor: 5, floors: [1, 1], backRow: false },
|
|
22
24
|
silo: { width: [5, 6], depth: [5, 6], setbackFactor: 4, spacingFactor: 3, floors: [1, 1], backRow: false },
|
|
25
|
+
// Interior-fill fillers: broad, plain footprints that pack the backs of dense blocks.
|
|
26
|
+
garage: { width: [18, 26], depth: [16, 22], setbackFactor: 0.3, spacingFactor: 0.4, floors: [2, 6], backRow: true },
|
|
27
|
+
depot: { width: [20, 30], depth: [18, 26], setbackFactor: 0.4, spacingFactor: 0.5, floors: [1, 2], backRow: true },
|
|
23
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* Natural frontage-width range (meters) of a class's massing — what the plot-size class bias reads
|
|
31
|
+
* so towers land on wide plots and rowhouses on narrow ones. @internal
|
|
32
|
+
*/
|
|
33
|
+
export function classWidthRange(cls) {
|
|
34
|
+
return CLASS_PROFILES[cls]?.width;
|
|
35
|
+
}
|
|
24
36
|
function range(rng, [lo, hi]) {
|
|
25
37
|
return lo + rng() * (hi - lo);
|
|
26
38
|
}
|
|
@@ -30,11 +42,13 @@ function range(rng, [lo, hi]) {
|
|
|
30
42
|
* aligned building wall; spacing likewise, so neighbors form rhythmic rows instead of scatter.
|
|
31
43
|
* @internal
|
|
32
44
|
*/
|
|
33
|
-
export function rollClassPlacement(cls, rng, lotScale, floorsMin, floorsMax, setbackBase, spacingBase) {
|
|
45
|
+
export function rollClassPlacement(cls, rng, lotScale, floorsMin, floorsMax, setbackBase, spacingBase, widthStretch = 1) {
|
|
34
46
|
const profile = CLASS_PROFILES[cls];
|
|
35
47
|
const floors = Math.round(range(rng, profile.floors));
|
|
36
48
|
return {
|
|
37
|
-
|
|
49
|
+
// `widthStretch` (≥1, from the block-fill dial) is a post-multiply on the rolled width — it
|
|
50
|
+
// never changes the rng draw order, so the default (1) stays byte-identical.
|
|
51
|
+
width: range(rng, profile.width) * lotScale * widthStretch,
|
|
38
52
|
depth: range(rng, profile.depth) * lotScale,
|
|
39
53
|
setback: setbackBase * profile.setbackFactor * (0.9 + rng() * 0.2),
|
|
40
54
|
gap: Math.max(0.2, spacingBase * profile.spacingFactor * (0.7 + rng() * 0.6)),
|
|
@@ -42,6 +56,26 @@ export function rollClassPlacement(cls, rng, lotScale, floorsMin, floorsMax, set
|
|
|
42
56
|
backRow: profile.backRow,
|
|
43
57
|
};
|
|
44
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Weight multiplier favouring classes whose natural frontage fits a plot of width `plotW`: a class
|
|
61
|
+
* far wider than the plot (a tower profile on a 6 m slice) is heavily downweighted, a class that
|
|
62
|
+
* would rattle around a much wider plot is mildly downweighted, and anything whose width range
|
|
63
|
+
* genuinely fits keeps weight 1. Pure data over {@link CLASS_PROFILES} — this is what turns a
|
|
64
|
+
* mixed-plot frontage into terraces on the slices and detached houses on the wide parcels.
|
|
65
|
+
* @internal
|
|
66
|
+
*/
|
|
67
|
+
export function classPlotFit(cls, plotW) {
|
|
68
|
+
const [lo, hi] = CLASS_PROFILES[cls].width;
|
|
69
|
+
if (plotW < lo * 0.6)
|
|
70
|
+
return 0; // physically cannot fit — a tower never squeezes onto a slice
|
|
71
|
+
if (plotW < lo) {
|
|
72
|
+
const r = plotW / lo;
|
|
73
|
+
return Math.max(0.1, r * r);
|
|
74
|
+
}
|
|
75
|
+
if (plotW > hi * 1.7)
|
|
76
|
+
return Math.max(0.35, (hi * 1.7) / plotW);
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
45
79
|
/**
|
|
46
80
|
* Normalized zone metric for a point in district-local space: 0 at the center line, 1 at the rim,
|
|
47
81
|
* shaped to the district's rectangle (Chebyshev) so bands follow the volume's footprint.
|
|
@@ -236,6 +270,130 @@ export function buildLotPieces(cls, width, depth, floors, floorHeight, rng) {
|
|
|
236
270
|
}
|
|
237
271
|
return pieces;
|
|
238
272
|
}
|
|
273
|
+
case "garage": {
|
|
274
|
+
// Parking structure: a plain banded box (the window bands read as deck slots) with a flat cap.
|
|
275
|
+
pieces.push(box([0, 0, 0], [width, H, depth], "wall", { grounded: true, banded: true }));
|
|
276
|
+
pieces.push(box([0, H, 0], [width * 1.02, 0.4, depth * 1.02], "trim"));
|
|
277
|
+
if (rng() < 0.6) {
|
|
278
|
+
// Stair/lift core poking above the roof deck.
|
|
279
|
+
const cx = (rng() - 0.5) * width * 0.5;
|
|
280
|
+
const cz = (rng() - 0.5) * depth * 0.4;
|
|
281
|
+
pieces.push(box([cx, H + 0.4, cz], [width * 0.16, 1.6 + rng() * 1.2, depth * 0.16], "accent"));
|
|
282
|
+
}
|
|
283
|
+
return pieces;
|
|
284
|
+
}
|
|
285
|
+
case "depot": {
|
|
286
|
+
// Warehouse/depot: a low broad box under a shallow gable, with an optional loading canopy.
|
|
287
|
+
const bw = width * 0.98;
|
|
288
|
+
const bd = depth * 0.96;
|
|
289
|
+
pieces.push(box([0, 0, 0], [bw, H, bd], "wall", { grounded: true }));
|
|
290
|
+
const roofH = Math.min(2.4 + rng() * 1.6, bd * 0.28);
|
|
291
|
+
pieces.push(gable([0, H, 0], [bw, roofH, bd]));
|
|
292
|
+
if (rng() < 0.5) {
|
|
293
|
+
pieces.push(box([0, Math.min(H - 0.4, 2.6), bd / 2 - 0.4], [bw * 0.55, 0.4, 0.8], "accent"));
|
|
294
|
+
}
|
|
295
|
+
return pieces;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/** All landmark classes, for schema hints, validation, and default allow-lists. */
|
|
300
|
+
export const CITY_LANDMARK_CLASSES = ["hall", "arena", "market", "campus"];
|
|
301
|
+
const LANDMARK_PROFILES = {
|
|
302
|
+
hall: { floors: [2, 3], minDepthFactor: 0.55 },
|
|
303
|
+
arena: { floors: [3, 5], minDepthFactor: 0.85 },
|
|
304
|
+
market: { floors: [1, 2], minDepthFactor: 0.6 },
|
|
305
|
+
campus: { floors: [3, 6], minDepthFactor: 0.7 },
|
|
306
|
+
};
|
|
307
|
+
/** Minimum depth-into-block factor for a landmark class (the pass grows shallow clusters to this). @internal */
|
|
308
|
+
export function landmarkMinDepthFactor(cls) {
|
|
309
|
+
return LANDMARK_PROFILES[cls].minDepthFactor;
|
|
310
|
+
}
|
|
311
|
+
/** Roll a landmark's floor count from its profile range. @internal */
|
|
312
|
+
export function landmarkFloors(cls, rng) {
|
|
313
|
+
return Math.round(range(rng, LANDMARK_PROFILES[cls].floors));
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Compose the massing pieces for one landmark parcel, filling the given `width`×`depth` footprint
|
|
317
|
+
* (lot-local, x along frontage, z into the block). Deterministic per `rng`. Every piece is kept
|
|
318
|
+
* inside the `[-width/2, width/2] × [-depth/2, depth/2]` box so the reported footprint matches the
|
|
319
|
+
* merged cluster's AABB. Sizes are in meters.
|
|
320
|
+
* @internal
|
|
321
|
+
*/
|
|
322
|
+
export function buildLandmarkPieces(cls, width, depth, floors, floorHeight, rng) {
|
|
323
|
+
const H = Math.max(1, floors) * floorHeight;
|
|
324
|
+
const pieces = [];
|
|
325
|
+
switch (cls) {
|
|
326
|
+
case "hall": {
|
|
327
|
+
// Civic block: broad walled base, an entablature, a fronting colonnade, and a central dome.
|
|
328
|
+
const bw = width * 0.96;
|
|
329
|
+
const bd = depth * 0.96;
|
|
330
|
+
pieces.push(box([0, 0, 0], [bw, H, bd], "wall", { grounded: true }));
|
|
331
|
+
pieces.push(box([0, H, 0], [bw, 0.6, bd], "trim"));
|
|
332
|
+
const cols = Math.max(4, Math.round(bw / 6));
|
|
333
|
+
const colH = H * 0.92;
|
|
334
|
+
const zFront = bd / 2 - 0.6;
|
|
335
|
+
for (let i = 0; i < cols; i += 1) {
|
|
336
|
+
const t = cols === 1 ? 0.5 : i / (cols - 1);
|
|
337
|
+
const x = (t - 0.5) * (bw - 1.4);
|
|
338
|
+
pieces.push(box([x, 0, zFront], [0.7, colH, 0.7], "trim", { grounded: true }));
|
|
339
|
+
}
|
|
340
|
+
// Portico roof over the colonnade.
|
|
341
|
+
pieces.push(box([0, colH, zFront], [bw - 1.0, 0.5, 1.6], "roof"));
|
|
342
|
+
const dd = Math.min(bw, bd) * (0.4 + rng() * 0.06);
|
|
343
|
+
pieces.push({ shape: "dome", offset: [0, H, 0], size: [dd, dd * 0.55, dd], rotationY: 0, role: "roof", grounded: false, banded: false });
|
|
344
|
+
return pieces;
|
|
345
|
+
}
|
|
346
|
+
case "arena": {
|
|
347
|
+
// Bowl: an elliptical outer wall, a rim ring, and a sunken inner field ring.
|
|
348
|
+
const ow = width * 0.94;
|
|
349
|
+
const od = depth * 0.94;
|
|
350
|
+
pieces.push({ shape: "cylinder", offset: [0, 0, 0], size: [ow, H, od], rotationY: 0, role: "wall", grounded: true, banded: true });
|
|
351
|
+
pieces.push({ shape: "cylinder", offset: [0, H, 0], size: [ow * 0.99, 0.9, od * 0.99], rotationY: 0, role: "trim", grounded: false, banded: false });
|
|
352
|
+
pieces.push({ shape: "cylinder", offset: [0, 0, 0], size: [ow * 0.66, H * 0.5, od * 0.66], rotationY: 0, role: "accent", grounded: true, banded: false });
|
|
353
|
+
if (rng() < 0.6) {
|
|
354
|
+
// A short mast/scoreboard on the rim.
|
|
355
|
+
pieces.push(box([0, H, 0], [1.4, 3 + rng() * 3, 1.4], "trim"));
|
|
356
|
+
}
|
|
357
|
+
return pieces;
|
|
358
|
+
}
|
|
359
|
+
case "market": {
|
|
360
|
+
// Broad low gabled hall with an open stall face on the street side.
|
|
361
|
+
const bw = width * 0.97;
|
|
362
|
+
const bd = depth * 0.95;
|
|
363
|
+
pieces.push(box([0, 0, 0], [bw, H, bd], "wall", { grounded: true }));
|
|
364
|
+
const roofH = Math.min(4 + rng() * 2, bd * 0.4);
|
|
365
|
+
pieces.push(gable([0, H, 0], [bw, roofH, bd]));
|
|
366
|
+
// Clerestory monitor along the ridge.
|
|
367
|
+
pieces.push(box([0, H, 0], [bw * 0.5, roofH * 0.5, bd * 0.25], "accent"));
|
|
368
|
+
// Awning/stall lip on the front face.
|
|
369
|
+
pieces.push(box([0, Math.min(H - 0.5, 3), bd / 2 - 0.4], [bw * 0.94, 0.4, 1.0], "accent"));
|
|
370
|
+
return pieces;
|
|
371
|
+
}
|
|
372
|
+
case "campus": {
|
|
373
|
+
// Courtyard: four connected slab wings around an open center, varied heights.
|
|
374
|
+
const bw = width * 0.96;
|
|
375
|
+
const bd = depth * 0.96;
|
|
376
|
+
const wingT = Math.min(bw, bd) * 0.22;
|
|
377
|
+
const innerD = Math.max(1, bd - 2 * wingT);
|
|
378
|
+
const frontH = H * (0.9 + rng() * 0.2);
|
|
379
|
+
const backH = H * (0.85 + rng() * 0.2);
|
|
380
|
+
const leftH = H * (0.9 + rng() * 0.25);
|
|
381
|
+
const rightH = H * (1.0 + rng() * 0.2);
|
|
382
|
+
pieces.push(box([0, 0, (bd - wingT) / 2], [bw, frontH, wingT], "wall", { grounded: true, banded: true }));
|
|
383
|
+
pieces.push(box([0, 0, -(bd - wingT) / 2], [bw, backH, wingT], "wall", { grounded: true, banded: true }));
|
|
384
|
+
pieces.push(box([(bw - wingT) / 2, 0, 0], [wingT, rightH, innerD], "wall", { grounded: true, banded: true }));
|
|
385
|
+
pieces.push(box([-(bw - wingT) / 2, 0, 0], [wingT, leftH, innerD], "wall", { grounded: true, banded: true }));
|
|
386
|
+
// Parapet caps.
|
|
387
|
+
for (const [wx, wz, ww, wd, wh] of [
|
|
388
|
+
[0, (bd - wingT) / 2, bw, wingT, frontH],
|
|
389
|
+
[0, -(bd - wingT) / 2, bw, wingT, backH],
|
|
390
|
+
[(bw - wingT) / 2, 0, wingT, innerD, rightH],
|
|
391
|
+
[-(bw - wingT) / 2, 0, wingT, innerD, leftH],
|
|
392
|
+
]) {
|
|
393
|
+
pieces.push(box([wx, wh, wz], [ww, 0.4, wd], "trim"));
|
|
394
|
+
}
|
|
395
|
+
return pieces;
|
|
396
|
+
}
|
|
239
397
|
}
|
|
240
398
|
}
|
|
241
399
|
/** All species, for schema hints and validation. */
|