@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
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
function clamp01(v) {
|
|
2
|
+
return v < 0 ? 0 : v > 1 ? 1 : v;
|
|
3
|
+
}
|
|
4
|
+
/** Eased progress `0..1` for a transient effect's decay curve. */
|
|
5
|
+
function easeProgress(t, easing) {
|
|
6
|
+
const p = clamp01(t);
|
|
7
|
+
switch (easing) {
|
|
8
|
+
case "linear":
|
|
9
|
+
return p;
|
|
10
|
+
case "easeIn":
|
|
11
|
+
return p * p;
|
|
12
|
+
case "easeInOut":
|
|
13
|
+
return p < 0.5 ? 2 * p * p : 1 - (-2 * p + 2) ** 2 / 2;
|
|
14
|
+
case "easeOut":
|
|
15
|
+
default:
|
|
16
|
+
return 1 - (1 - p) ** 2;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const TWO_PI = Math.PI * 2;
|
|
20
|
+
/**
|
|
21
|
+
* A serializable screen-feedback controller: a game triggers transient flashes and
|
|
22
|
+
* edge vignettes (a red damage hit, a green heal flash) or sustained, optionally
|
|
23
|
+
* oscillating tints (a low-health pulse), and reads back a composite of the effects
|
|
24
|
+
* to draw right now with their eased opacities. It is clock-driven — call
|
|
25
|
+
* `advance()` each frame against an injected `now` — and allocation-aware: the
|
|
26
|
+
* composite array and its entries are pooled and reused, so steady-state ticking
|
|
27
|
+
* never allocates. Nothing here is genre-specific: `kind` is a free label the game
|
|
28
|
+
* owns and styles, and vignette / flash / pulse are just parameterizations of the
|
|
29
|
+
* same data (region shape, decay easing, sustained oscillation). A shell overlay
|
|
30
|
+
* subscribes and renders; `snapshot`/`restore` round-trips through a save.
|
|
31
|
+
*
|
|
32
|
+
* @capability screen-effects serializable, pooled screen-feedback controller — transient flash/vignette and sustained low-health-style pulses with eased opacities, free-string kinds the game styles, and snapshot/restore
|
|
33
|
+
*/
|
|
34
|
+
export function createScreenEffects(options = {}) {
|
|
35
|
+
const now = options.now ?? Date.now;
|
|
36
|
+
const effects = [];
|
|
37
|
+
const listeners = new Set();
|
|
38
|
+
// Pooled composite output — reused across composite() calls, never per-tick allocated.
|
|
39
|
+
const viewPool = [];
|
|
40
|
+
const viewOut = [];
|
|
41
|
+
let nextId = 1;
|
|
42
|
+
function notify() {
|
|
43
|
+
for (const listener of listeners)
|
|
44
|
+
listener();
|
|
45
|
+
}
|
|
46
|
+
/** Eased current opacity for one effect at `t` ms clock time. */
|
|
47
|
+
function evaluate(effect, t) {
|
|
48
|
+
const elapsed = t - effect.startAt;
|
|
49
|
+
if (effect.sustained) {
|
|
50
|
+
if (effect.pulseHz <= 0)
|
|
51
|
+
return effect.intensity;
|
|
52
|
+
const phase = (elapsed / 1000) * effect.pulseHz * TWO_PI;
|
|
53
|
+
const osc = (Math.sin(phase) + 1) / 2; // 0..1
|
|
54
|
+
return effect.minIntensity + (effect.intensity - effect.minIntensity) * osc;
|
|
55
|
+
}
|
|
56
|
+
if (effect.durationMs <= 0)
|
|
57
|
+
return 0;
|
|
58
|
+
const progress = elapsed / effect.durationMs;
|
|
59
|
+
if (progress >= 1)
|
|
60
|
+
return 0;
|
|
61
|
+
return effect.intensity * (1 - easeProgress(progress, effect.easing));
|
|
62
|
+
}
|
|
63
|
+
function refresh(t) {
|
|
64
|
+
// Iterate backwards: reap expired transients with swap-remove, no allocation.
|
|
65
|
+
for (let i = effects.length - 1; i >= 0; i--) {
|
|
66
|
+
const effect = effects[i];
|
|
67
|
+
if (!effect.sustained && t - effect.startAt >= effect.durationMs) {
|
|
68
|
+
const last = effects.length - 1;
|
|
69
|
+
if (i !== last)
|
|
70
|
+
effects[i] = effects[last];
|
|
71
|
+
effects.pop();
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
effect.current = evaluate(effect, t);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function add(spec) {
|
|
78
|
+
const id = nextId++;
|
|
79
|
+
const t = now();
|
|
80
|
+
const effect = {
|
|
81
|
+
id,
|
|
82
|
+
kind: spec.kind,
|
|
83
|
+
color: spec.color,
|
|
84
|
+
shape: spec.shape ?? "full",
|
|
85
|
+
easing: spec.easing ?? "easeOut",
|
|
86
|
+
intensity: clamp01(spec.intensity ?? 1),
|
|
87
|
+
durationMs: spec.durationMs ?? 400,
|
|
88
|
+
sustained: spec.sustained ?? false,
|
|
89
|
+
pulseHz: Math.max(0, spec.pulseHz ?? 0),
|
|
90
|
+
minIntensity: clamp01(spec.minIntensity ?? 0),
|
|
91
|
+
startAt: t,
|
|
92
|
+
current: 0,
|
|
93
|
+
};
|
|
94
|
+
effect.current = evaluate(effect, t);
|
|
95
|
+
effects.push(effect);
|
|
96
|
+
notify();
|
|
97
|
+
return id;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
flash(kind, spec) {
|
|
101
|
+
return add({ ...spec, kind, shape: "full", sustained: false });
|
|
102
|
+
},
|
|
103
|
+
vignette(kind, spec) {
|
|
104
|
+
return add({ ...spec, kind, shape: "vignette", sustained: false });
|
|
105
|
+
},
|
|
106
|
+
pulse(kind, spec) {
|
|
107
|
+
return add({ shape: "vignette", ...spec, kind, sustained: true });
|
|
108
|
+
},
|
|
109
|
+
trigger(spec) {
|
|
110
|
+
return add(spec);
|
|
111
|
+
},
|
|
112
|
+
clear(id) {
|
|
113
|
+
const index = effects.findIndex((effect) => effect.id === id);
|
|
114
|
+
if (index === -1)
|
|
115
|
+
return;
|
|
116
|
+
const last = effects.length - 1;
|
|
117
|
+
if (index !== last)
|
|
118
|
+
effects[index] = effects[last];
|
|
119
|
+
effects.pop();
|
|
120
|
+
notify();
|
|
121
|
+
},
|
|
122
|
+
clearAll() {
|
|
123
|
+
if (effects.length === 0)
|
|
124
|
+
return;
|
|
125
|
+
effects.length = 0;
|
|
126
|
+
notify();
|
|
127
|
+
},
|
|
128
|
+
advance(nowMs) {
|
|
129
|
+
refresh(nowMs ?? now());
|
|
130
|
+
notify();
|
|
131
|
+
},
|
|
132
|
+
composite() {
|
|
133
|
+
viewOut.length = 0;
|
|
134
|
+
for (let i = 0; i < effects.length; i++) {
|
|
135
|
+
const effect = effects[i];
|
|
136
|
+
if (effect.current <= 0)
|
|
137
|
+
continue;
|
|
138
|
+
let view = viewPool[viewOut.length];
|
|
139
|
+
if (view === undefined) {
|
|
140
|
+
view = { id: 0, kind: "", color: "", shape: "full", intensity: 0 };
|
|
141
|
+
viewPool[viewOut.length] = view;
|
|
142
|
+
}
|
|
143
|
+
view.id = effect.id;
|
|
144
|
+
view.kind = effect.kind;
|
|
145
|
+
view.color = effect.color;
|
|
146
|
+
view.shape = effect.shape;
|
|
147
|
+
view.intensity = effect.current;
|
|
148
|
+
viewOut.push(view);
|
|
149
|
+
}
|
|
150
|
+
return viewOut;
|
|
151
|
+
},
|
|
152
|
+
active() {
|
|
153
|
+
return effects.length;
|
|
154
|
+
},
|
|
155
|
+
subscribe(listener) {
|
|
156
|
+
listeners.add(listener);
|
|
157
|
+
return () => listeners.delete(listener);
|
|
158
|
+
},
|
|
159
|
+
snapshot() {
|
|
160
|
+
return {
|
|
161
|
+
now: now(),
|
|
162
|
+
nextId,
|
|
163
|
+
effects: effects.map((effect) => ({
|
|
164
|
+
id: effect.id,
|
|
165
|
+
kind: effect.kind,
|
|
166
|
+
color: effect.color,
|
|
167
|
+
shape: effect.shape,
|
|
168
|
+
easing: effect.easing,
|
|
169
|
+
intensity: effect.intensity,
|
|
170
|
+
durationMs: effect.durationMs,
|
|
171
|
+
sustained: effect.sustained,
|
|
172
|
+
pulseHz: effect.pulseHz,
|
|
173
|
+
minIntensity: effect.minIntensity,
|
|
174
|
+
startAt: effect.startAt,
|
|
175
|
+
})),
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
restore(snapshot) {
|
|
179
|
+
effects.length = 0;
|
|
180
|
+
nextId = snapshot.nextId;
|
|
181
|
+
const t = now();
|
|
182
|
+
const drift = t - snapshot.now;
|
|
183
|
+
for (const stored of snapshot.effects) {
|
|
184
|
+
const effect = { ...stored, startAt: stored.startAt + drift, current: 0 };
|
|
185
|
+
effect.current = evaluate(effect, t);
|
|
186
|
+
effects.push(effect);
|
|
187
|
+
}
|
|
188
|
+
notify();
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import type { ModelAnimationConfig } from "../game/playableGame.js";
|
|
1
2
|
import type { SceneMarkerLike } from "./sceneShapes.js";
|
|
3
|
+
/** A placement's authored rig animation override — the stored `marker.meta.animation` contract (#1274). */
|
|
4
|
+
export type AuthoredAnimation = ModelAnimationConfig | "auto" | "none";
|
|
2
5
|
/**
|
|
3
6
|
* One authored catalog prop resolved from an editor marker — grounded at `x`/`z` with yaw, ready
|
|
4
7
|
* for `ctx.scene.object.place` or {@link placeAuthoredObjects}.
|
|
@@ -11,6 +14,12 @@ export interface AuthoredObject {
|
|
|
11
14
|
instanceId: string;
|
|
12
15
|
/** Extra lift in meters from the marker (`catalogId` field / meta), applied on top of terrain height. */
|
|
13
16
|
verticalOffset: number;
|
|
17
|
+
/**
|
|
18
|
+
* Per-placement rig animation override authored at `marker.meta.animation` (#1274), threaded onto the
|
|
19
|
+
* placed object's `ModelConfig.animation` so the override applies in game. `undefined` when the marker
|
|
20
|
+
* carries no override — behavior is exactly the catalog-resolved default.
|
|
21
|
+
*/
|
|
22
|
+
animation?: AuthoredAnimation;
|
|
14
23
|
}
|
|
15
24
|
/** Minimal marker shape {@link resolveAuthoredObjects} reads; any `EditorMarker` satisfies it. */
|
|
16
25
|
export interface AuthoredObjectMarkerLike extends SceneMarkerLike {
|
|
@@ -26,6 +35,7 @@ export interface AuthoredObjectPlaceTarget {
|
|
|
26
35
|
place(catalogId: string, x: number, y: number, z: number, options?: {
|
|
27
36
|
instanceId?: string;
|
|
28
37
|
rotation?: number;
|
|
38
|
+
animation?: AuthoredAnimation;
|
|
29
39
|
onExisting?: "throw" | "replace" | "keep";
|
|
30
40
|
}): string;
|
|
31
41
|
}
|
|
@@ -42,6 +52,14 @@ export interface PlaceAuthoredObjectsOptions {
|
|
|
42
52
|
* @capability authored-objects place catalog mesh props from an editor document
|
|
43
53
|
*/
|
|
44
54
|
export declare function markerCatalogId(marker: AuthoredObjectMarkerLike): string | null;
|
|
55
|
+
/**
|
|
56
|
+
* The authored per-placement rig animation override stored at `marker.meta.animation` (#1274), or
|
|
57
|
+
* `undefined` when absent/malformed. `"auto"` / `"none"` pass through as the string modes; any other
|
|
58
|
+
* object is a `ModelAnimationConfig` the shell applies verbatim (validated at author time in the
|
|
59
|
+
* editor). Runtime trusts the persisted document here exactly like it trusts position and rotation.
|
|
60
|
+
* @capability authored-objects per-placement animation override from marker.meta.animation
|
|
61
|
+
*/
|
|
62
|
+
export declare function markerAnimation(marker: AuthoredObjectMarkerLike): AuthoredAnimation | undefined;
|
|
45
63
|
/**
|
|
46
64
|
* Marker kinds that are spawnable entities (materialized by `authoredEntitySpawns`), not static
|
|
47
65
|
* catalog props. {@link resolveAuthoredObjects} skips them by default so a placed `mob`/`boss` is
|
|
@@ -16,6 +16,24 @@ export function markerCatalogId(marker) {
|
|
|
16
16
|
return marker.catalogId;
|
|
17
17
|
return metaString(marker.meta, "catalogId");
|
|
18
18
|
}
|
|
19
|
+
function isRecord(value) {
|
|
20
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The authored per-placement rig animation override stored at `marker.meta.animation` (#1274), or
|
|
24
|
+
* `undefined` when absent/malformed. `"auto"` / `"none"` pass through as the string modes; any other
|
|
25
|
+
* object is a `ModelAnimationConfig` the shell applies verbatim (validated at author time in the
|
|
26
|
+
* editor). Runtime trusts the persisted document here exactly like it trusts position and rotation.
|
|
27
|
+
* @capability authored-objects per-placement animation override from marker.meta.animation
|
|
28
|
+
*/
|
|
29
|
+
export function markerAnimation(marker) {
|
|
30
|
+
const value = marker.meta?.["animation"];
|
|
31
|
+
if (value === "auto" || value === "none")
|
|
32
|
+
return value;
|
|
33
|
+
if (isRecord(value))
|
|
34
|
+
return value;
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
19
37
|
function markerVerticalOffset(marker) {
|
|
20
38
|
const fromMeta = metaNumber(marker.meta, "verticalOffset") ??
|
|
21
39
|
metaNumber(marker.meta, "offsetY") ??
|
|
@@ -45,6 +63,7 @@ export function resolveAuthoredObjects(document, options = {}) {
|
|
|
45
63
|
const catalogId = markerCatalogId(marker);
|
|
46
64
|
if (catalogId === null)
|
|
47
65
|
continue;
|
|
66
|
+
const animation = markerAnimation(marker);
|
|
48
67
|
out.push({
|
|
49
68
|
catalogId,
|
|
50
69
|
x: marker.position.x,
|
|
@@ -52,6 +71,7 @@ export function resolveAuthoredObjects(document, options = {}) {
|
|
|
52
71
|
rotationY: marker.rotationY ?? 0,
|
|
53
72
|
instanceId: marker.id,
|
|
54
73
|
verticalOffset: markerVerticalOffset(marker),
|
|
74
|
+
...(animation === undefined ? {} : { animation }),
|
|
55
75
|
});
|
|
56
76
|
}
|
|
57
77
|
return out;
|
|
@@ -70,6 +90,7 @@ export function placeAuthoredObjects(store, objects, sampleHeight, options = {})
|
|
|
70
90
|
ids.push(store.place(object.catalogId, object.x, y, object.z, {
|
|
71
91
|
instanceId: object.instanceId,
|
|
72
92
|
rotation: object.rotationY,
|
|
93
|
+
...(object.animation === undefined ? {} : { animation: object.animation }),
|
|
73
94
|
...(onExisting === undefined ? {} : { onExisting }),
|
|
74
95
|
}));
|
|
75
96
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readSpawnOverride } from "./spawnOverride.js";
|
|
1
2
|
/** Marker kind {@link authoredSpawnPosition} resolves by default. */
|
|
2
3
|
export const PLAYER_SPAWN_KIND = "player_spawn";
|
|
3
4
|
/**
|
|
@@ -8,6 +9,14 @@ export const PLAYER_SPAWN_KIND = "player_spawn";
|
|
|
8
9
|
export function markersOfKind(document, kind) {
|
|
9
10
|
return document.markers.filter((marker) => marker.kind === kind);
|
|
10
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* True when a query targets the *default player spawn* (default kind, no explicit id) — the only
|
|
14
|
+
* resolution an installed {@link readSpawnOverride} may replace. Marker queries for other kinds or
|
|
15
|
+
* a named id always read the authored document, so a capture-time override cannot perturb them.
|
|
16
|
+
*/
|
|
17
|
+
function targetsDefaultPlayerSpawn(options) {
|
|
18
|
+
return options.id === undefined && (options.kind === undefined || options.kind === PLAYER_SPAWN_KIND);
|
|
19
|
+
}
|
|
11
20
|
/**
|
|
12
21
|
* Position of the authored spawn marker as a spawn-ready `[x, y, z]` tuple, or null when the
|
|
13
22
|
* document has none. Reads the first `player_spawn` marker by default, so dragging the marker in
|
|
@@ -15,6 +24,11 @@ export function markersOfKind(document, kind) {
|
|
|
15
24
|
* @capability authored-spawn read spawn points and markers from the editor document
|
|
16
25
|
*/
|
|
17
26
|
export function authoredSpawnPosition(document, options = {}) {
|
|
27
|
+
if (targetsDefaultPlayerSpawn(options)) {
|
|
28
|
+
const override = readSpawnOverride();
|
|
29
|
+
if (override !== null)
|
|
30
|
+
return [override.x, override.y, override.z];
|
|
31
|
+
}
|
|
18
32
|
const kind = options.kind ?? PLAYER_SPAWN_KIND;
|
|
19
33
|
const marker = options.id === undefined
|
|
20
34
|
? document.markers.find((entry) => entry.kind === kind)
|
|
@@ -29,6 +43,11 @@ export function authoredSpawnPosition(document, options = {}) {
|
|
|
29
43
|
* @capability authored-spawn read spawn points and markers from the editor document
|
|
30
44
|
*/
|
|
31
45
|
export function authoredSpawnRotation(document, options = {}) {
|
|
46
|
+
if (targetsDefaultPlayerSpawn(options)) {
|
|
47
|
+
const override = readSpawnOverride();
|
|
48
|
+
if (override?.rotationY !== undefined)
|
|
49
|
+
return override.rotationY;
|
|
50
|
+
}
|
|
32
51
|
const kind = options.kind ?? PLAYER_SPAWN_KIND;
|
|
33
52
|
const marker = options.id === undefined
|
|
34
53
|
? document.markers.find((entry) => entry.kind === kind)
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* @capability world-environment lots aligned to and facing road frontage, with a setback sidewalk strip
|
|
13
13
|
*/
|
|
14
|
-
import type
|
|
14
|
+
import { type Vec2 } from "./cityGeometry.js";
|
|
15
15
|
import type { WorldBounds } from "./features.js";
|
|
16
16
|
/** One road the frontage placer lines with buildings: a centerline polyline and its full width. */
|
|
17
17
|
export interface RoadFrontage {
|
|
@@ -27,24 +27,50 @@ export interface LotArea {
|
|
|
27
27
|
/** Half-extents along world x and z. */
|
|
28
28
|
halfExtents: Vec2;
|
|
29
29
|
}
|
|
30
|
+
/** One weighted plot size a frontage can mix in: `w` along the road, `d` into the block. */
|
|
31
|
+
export interface PlotVariant extends WorldBounds {
|
|
32
|
+
/** Relative pick weight among the variants. Default 1. */
|
|
33
|
+
weight?: number;
|
|
34
|
+
}
|
|
30
35
|
/** Options for {@link deriveBuildingLots}. */
|
|
31
36
|
export interface BuildingLotOptions {
|
|
32
37
|
/** Roads whose frontage is lined with lots. */
|
|
33
38
|
roads: readonly RoadFrontage[];
|
|
34
|
-
/**
|
|
35
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Plot size(s): `w` is frontage width (along the road), `d` is depth (into the block). Pass one
|
|
41
|
+
* {@link WorldBounds} for a uniform district, or a weighted {@link PlotVariant} list to mix plot
|
|
42
|
+
* sizes along the same frontage — narrow-deep apartment slices beside wide detached-house
|
|
43
|
+
* parcels beside tight terraces. Variant rolls are seeded per road AND side, so the two sides of
|
|
44
|
+
* a street can read differently. Default `{ w: 12, d: 10 }`.
|
|
45
|
+
*/
|
|
46
|
+
footprint?: WorldBounds | readonly PlotVariant[];
|
|
36
47
|
/** Gap between neighbouring lots along a frontage, world units. Default 2. */
|
|
37
48
|
spacing?: number;
|
|
38
49
|
/** Sidewalk strip between the curb (`width / 2`) and the building front, world units. Default 3. */
|
|
39
50
|
setback?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Manhattan streetwall dial (0..1). Omit (default) keeps the legacy look byte-for-byte. When set,
|
|
53
|
+
* frontage is compacted as it rises: the along-road gap between neighbours scales from `spacing` at
|
|
54
|
+
* `~0.45` down to 0 at 1 (buildings shoulder-to-shoulder), and each lot's frontage width widens by
|
|
55
|
+
* a modest factor above 0.45 so consecutive lots touch instead of leaving gaps. Below ~0.45 the gap
|
|
56
|
+
* grows past `spacing`, so a low dial reads sparser than the default. Depth is unchanged. This is
|
|
57
|
+
* pure frontage compaction; block-interior fill lives in the content pass (`resolveCityLotContent`).
|
|
58
|
+
*/
|
|
59
|
+
blockFill?: number;
|
|
40
60
|
/** Place lots on both sides of each road. Default true. */
|
|
41
61
|
bothSides?: boolean;
|
|
42
62
|
/** Clip region; lots whose center falls outside are dropped. Omit to keep every lot. */
|
|
43
63
|
area?: LotArea;
|
|
44
64
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
|
|
65
|
+
* Extra road corridors lots must keep clear of WITHOUT lining them with frontage — service
|
|
66
|
+
* alleys, lanes, trails. Every lot's full footprint is kept off these (and off `roads`).
|
|
67
|
+
*/
|
|
68
|
+
avoid?: readonly RoadFrontage[];
|
|
69
|
+
/**
|
|
70
|
+
* Seed for the plot-variant rolls when `footprint` is a variant list (identical inputs always
|
|
71
|
+
* yield the identical layout, with or without it). With a single footprint, lot geometry is
|
|
72
|
+
* fully determined by the roads and dials; per-building variation (floors, massing) is seeded
|
|
73
|
+
* downstream where the lots become buildings.
|
|
48
74
|
*/
|
|
49
75
|
seed?: string;
|
|
50
76
|
/** Hard cap on emitted lots so a long network stays bounded. Default 400. */
|
|
@@ -65,6 +91,20 @@ export interface PlacedBuildingLot {
|
|
|
65
91
|
/** Distance from the lot's front face to the road centerline (= `width / 2 + setback`). */
|
|
66
92
|
frontDistance: number;
|
|
67
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Frontage-fill reference point: at this dial value the compaction is a no-op (legacy spacing/width),
|
|
96
|
+
* so callers can leave `blockFill` unset for the classic look and this value reproduces it exactly.
|
|
97
|
+
*/
|
|
98
|
+
export declare const FRONTAGE_FILL_REFERENCE = 0.45;
|
|
99
|
+
/**
|
|
100
|
+
* Map a `blockFill` dial to frontage compaction factors. `undefined` ⇒ identity (legacy path stays
|
|
101
|
+
* byte-identical). At {@link FRONTAGE_FILL_REFERENCE} the factors are exactly `{1, 1}`; toward 1 the
|
|
102
|
+
* gap collapses and lots widen, toward 0 the gap grows (sparser than default). @internal
|
|
103
|
+
*/
|
|
104
|
+
export declare function frontageCompaction(blockFill: number | undefined): {
|
|
105
|
+
spacingScale: number;
|
|
106
|
+
widthScale: number;
|
|
107
|
+
};
|
|
68
108
|
/**
|
|
69
109
|
* Yaw that turns the building FRONT (local +z) to face the road. The lot center sits at
|
|
70
110
|
* `roadPoint + outwardNormal * d`, so the road lies along `-outwardNormal` from the center. In the
|
|
@@ -74,11 +114,14 @@ export interface PlacedBuildingLot {
|
|
|
74
114
|
*/
|
|
75
115
|
export declare function facingRotation(outwardNormal: Vec2): number;
|
|
76
116
|
/**
|
|
77
|
-
* Derive street-aligned building lots from road frontage.
|
|
78
|
-
* `
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
117
|
+
* Derive street-aligned building lots (PLOTS) from road frontage. Each side of each road walks the
|
|
118
|
+
* frontage placing plots at `plot.w + spacing` pitch — rolling the next plot size from the weighted
|
|
119
|
+
* `footprint` variants when several are given — offset outward by `width / 2 + setback + plot.d / 2`
|
|
120
|
+
* and turned to face the road. The PLOT CONTRACT is enforced here, exactly: no two plots ever overlap
|
|
121
|
+
* (true oriented-rect separation with the spacing gap, so corners and curved frontage can't stack
|
|
122
|
+
* buildings), and no plot footprint ever touches any road corridor in `roads` or `avoid` — not
|
|
123
|
+
* just its own frontage road. Deterministic: identical inputs (including seed) always yield the
|
|
124
|
+
* identical list.
|
|
82
125
|
* @capability building-lots derive street-facing building lots from road frontage
|
|
83
126
|
*/
|
|
84
127
|
export declare function deriveBuildingLots(options: BuildingLotOptions): PlacedBuildingLot[];
|
|
@@ -1,4 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Street-aware building placement: derive building LOTS along ROAD FRONTAGE. Given road segments
|
|
3
|
+
* (centerline + width) and a district area, walk each road by arc length and emit lots on both
|
|
4
|
+
* sides at a consistent setback — a curb + sidewalk strip — with each lot carrying the yaw that
|
|
5
|
+
* makes the building FRONT face its road. Unlike `cityBlocks` (which needs a CLOSED street network
|
|
6
|
+
* to extract planar block faces), this works on a sparse set of segments — the common authoring
|
|
7
|
+
* case — so buildings line the streets instead of scattering on plinths around a center point.
|
|
8
|
+
*
|
|
9
|
+
* Pure seeded math over `cityGeometry` `Vec2` primitives: no rendering, deterministic from the
|
|
10
|
+
* inputs, bounded by `maxLots`.
|
|
11
|
+
*
|
|
12
|
+
* @capability world-environment lots aligned to and facing road frontage, with a setback sidewalk strip
|
|
13
|
+
*/
|
|
14
|
+
import { rectClearsPolyline, rectsSeparated } from "./cityGeometry.js";
|
|
15
|
+
import { seededStreams } from "../random/rng.js";
|
|
1
16
|
const DEFAULT_FOOTPRINT = { w: 12, d: 10 };
|
|
17
|
+
/**
|
|
18
|
+
* Frontage-fill reference point: at this dial value the compaction is a no-op (legacy spacing/width),
|
|
19
|
+
* so callers can leave `blockFill` unset for the classic look and this value reproduces it exactly.
|
|
20
|
+
*/
|
|
21
|
+
export const FRONTAGE_FILL_REFERENCE = 0.45;
|
|
22
|
+
/** Modest extra frontage-width growth applied at full fill so consecutive lots touch (edge-to-edge). */
|
|
23
|
+
const FRONTAGE_WIDTH_OVERFILL = 0.1;
|
|
24
|
+
/**
|
|
25
|
+
* Map a `blockFill` dial to frontage compaction factors. `undefined` ⇒ identity (legacy path stays
|
|
26
|
+
* byte-identical). At {@link FRONTAGE_FILL_REFERENCE} the factors are exactly `{1, 1}`; toward 1 the
|
|
27
|
+
* gap collapses and lots widen, toward 0 the gap grows (sparser than default). @internal
|
|
28
|
+
*/
|
|
29
|
+
export function frontageCompaction(blockFill) {
|
|
30
|
+
if (blockFill === undefined)
|
|
31
|
+
return { spacingScale: 1, widthScale: 1 };
|
|
32
|
+
const f = Math.max(0, Math.min(1, blockFill));
|
|
33
|
+
const spacingScale = (1 - f) / (1 - FRONTAGE_FILL_REFERENCE);
|
|
34
|
+
const widthScale = 1 + (Math.max(0, f - FRONTAGE_FILL_REFERENCE) / (1 - FRONTAGE_FILL_REFERENCE)) * FRONTAGE_WIDTH_OVERFILL;
|
|
35
|
+
return { spacingScale, widthScale };
|
|
36
|
+
}
|
|
2
37
|
/** Cumulative arc length of a polyline. */
|
|
3
38
|
function polylineLength(path) {
|
|
4
39
|
let sum = 0;
|
|
@@ -45,27 +80,59 @@ export function facingRotation(outwardNormal) {
|
|
|
45
80
|
return Math.atan2(-outwardNormal[0], -outwardNormal[1]);
|
|
46
81
|
}
|
|
47
82
|
/**
|
|
48
|
-
* Derive street-aligned building lots from road frontage.
|
|
49
|
-
* `
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
83
|
+
* Derive street-aligned building lots (PLOTS) from road frontage. Each side of each road walks the
|
|
84
|
+
* frontage placing plots at `plot.w + spacing` pitch — rolling the next plot size from the weighted
|
|
85
|
+
* `footprint` variants when several are given — offset outward by `width / 2 + setback + plot.d / 2`
|
|
86
|
+
* and turned to face the road. The PLOT CONTRACT is enforced here, exactly: no two plots ever overlap
|
|
87
|
+
* (true oriented-rect separation with the spacing gap, so corners and curved frontage can't stack
|
|
88
|
+
* buildings), and no plot footprint ever touches any road corridor in `roads` or `avoid` — not
|
|
89
|
+
* just its own frontage road. Deterministic: identical inputs (including seed) always yield the
|
|
90
|
+
* identical list.
|
|
53
91
|
* @capability building-lots derive street-facing building lots from road frontage
|
|
54
92
|
*/
|
|
55
93
|
export function deriveBuildingLots(options) {
|
|
56
|
-
const
|
|
57
|
-
|
|
94
|
+
const raw = options.footprint ?? DEFAULT_FOOTPRINT;
|
|
95
|
+
// `blockFill` compaction: widen each lot's frontage and shrink the along-road gap toward a
|
|
96
|
+
// Manhattan streetwall as the dial rises. Undefined ⇒ identity, so the classic path is untouched.
|
|
97
|
+
// Compaction composes with plot variants — every variant's frontage widens by the same factor.
|
|
98
|
+
const { spacingScale, widthScale } = frontageCompaction(options.blockFill);
|
|
99
|
+
const variants = (Array.isArray(raw) ? raw : [raw])
|
|
100
|
+
.filter((v) => v.w > 0.5 && v.d > 0.5)
|
|
101
|
+
.map((v) => ({ w: v.w * widthScale, d: v.d, weight: Math.max(0, v.weight ?? 1) }));
|
|
102
|
+
if (variants.length === 0)
|
|
103
|
+
variants.push({ ...DEFAULT_FOOTPRINT, weight: 1 });
|
|
104
|
+
const totalWeight = variants.reduce((sum, v) => sum + v.weight, 0) || 1;
|
|
105
|
+
const pickVariant = (roll) => {
|
|
106
|
+
let cursor = roll * totalWeight;
|
|
107
|
+
for (const v of variants) {
|
|
108
|
+
cursor -= v.weight;
|
|
109
|
+
if (cursor <= 0)
|
|
110
|
+
return v;
|
|
111
|
+
}
|
|
112
|
+
return variants[0];
|
|
113
|
+
};
|
|
114
|
+
const spacing = Math.max(0, options.spacing ?? 2) * spacingScale;
|
|
58
115
|
const setback = Math.max(0, options.setback ?? 3);
|
|
59
116
|
const bothSides = options.bothSides ?? true;
|
|
60
117
|
const maxLots = Math.max(0, Math.floor(options.maxLots ?? 400));
|
|
61
|
-
const
|
|
118
|
+
const streams = seededStreams(`plots:${options.seed ?? ""}`);
|
|
62
119
|
const sides = bothSides ? [1, -1] : [1];
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
120
|
+
const corridors = [];
|
|
121
|
+
for (const road of options.roads)
|
|
122
|
+
corridors.push({ path: road.path, half: road.width / 2 });
|
|
123
|
+
for (const road of options.avoid ?? [])
|
|
124
|
+
corridors.push({ path: road.path, half: road.width / 2 });
|
|
67
125
|
const lots = [];
|
|
68
126
|
const accepted = [];
|
|
127
|
+
// Gap grown onto the candidate for the plot-vs-plot separation test. Just under half the
|
|
128
|
+
// spacing keeps same-road neighbours (spaced exactly `spacing` apart) accepted while anything
|
|
129
|
+
// closer — corner stacking, inside-of-curve pinches — is rejected. At `spacing` 0 the gap goes
|
|
130
|
+
// slightly NEGATIVE (a shrink), so exactly-touching plots (terraces, rowhouse walls) are legal:
|
|
131
|
+
// touching is a valid look, overlapping never is.
|
|
132
|
+
const gap = spacing * 0.45 - 0.05;
|
|
133
|
+
// Plots may touch the asphalt edge but never intrude into it; the tiny epsilon keeps an exact
|
|
134
|
+
// setback-0 front face (touching the corridor boundary) legal.
|
|
135
|
+
const roadClearanceSlack = 0.02;
|
|
69
136
|
for (let r = 0; r < options.roads.length && lots.length < maxLots; r += 1) {
|
|
70
137
|
const road = options.roads[r];
|
|
71
138
|
if (road.path.length < 2)
|
|
@@ -74,44 +141,63 @@ export function deriveBuildingLots(options) {
|
|
|
74
141
|
if (total < 1e-6)
|
|
75
142
|
continue;
|
|
76
143
|
const front = road.width / 2 + setback;
|
|
77
|
-
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Left normal of the tangent (CCW): (-tz, tx). Right side negates it.
|
|
86
|
-
for (const side of sides) {
|
|
87
|
-
if (lots.length >= maxLots)
|
|
144
|
+
// Each side walks its own seeded cursor so variant runs differ across the street — terraces
|
|
145
|
+
// can face detached houses. With a single variant the walk degenerates to the fixed pitch.
|
|
146
|
+
for (const side of sides) {
|
|
147
|
+
const roll = variants.length > 1 ? streams(`road:${r}:side:${side}`) : null;
|
|
148
|
+
let cursor = spacing / 2 + 0.5;
|
|
149
|
+
while (cursor < total && lots.length < maxLots) {
|
|
150
|
+
const plot = roll === null ? variants[0] : pickVariant(roll());
|
|
151
|
+
if (cursor + plot.w > total - 0.5)
|
|
88
152
|
break;
|
|
153
|
+
const s = cursor + plot.w / 2;
|
|
154
|
+
cursor += plot.w + spacing;
|
|
155
|
+
const station = stationAt(road.path, s);
|
|
156
|
+
const [tx, tz] = station.tangent;
|
|
157
|
+
// Left normal of the tangent (CCW): (-tz, tx). Right side negates it.
|
|
89
158
|
const nx = -tz * side;
|
|
90
159
|
const nz = tx * side;
|
|
160
|
+
const offset = front + plot.d / 2;
|
|
91
161
|
const cx = station.p[0] + nx * offset;
|
|
92
162
|
const cz = station.p[1] + nz * offset;
|
|
93
163
|
if (!inArea(options.area, cx, cz))
|
|
94
164
|
continue;
|
|
165
|
+
const rotationY = facingRotation([nx, nz]);
|
|
166
|
+
const hw = plot.w / 2;
|
|
167
|
+
const hd = plot.d / 2;
|
|
168
|
+
const rect = { x: cx, z: cz, hw, hd, angle: rotationY };
|
|
169
|
+
// Plot-vs-plot: reject any candidate whose rect (grown by the spacing gap) overlaps an
|
|
170
|
+
// accepted plot — regardless of which road placed it.
|
|
171
|
+
const grown = { ...rect, hw: hw + gap, hd: hd + gap };
|
|
95
172
|
let clash = false;
|
|
96
|
-
for (const
|
|
97
|
-
|
|
98
|
-
const dz = az - cz;
|
|
99
|
-
if (dx * dx + dz * dz < minSep2) {
|
|
173
|
+
for (const other of accepted) {
|
|
174
|
+
if (!rectsSeparated(grown, other)) {
|
|
100
175
|
clash = true;
|
|
101
176
|
break;
|
|
102
177
|
}
|
|
103
178
|
}
|
|
104
179
|
if (clash)
|
|
105
180
|
continue;
|
|
181
|
+
// Plot-vs-road: the full footprint stays off every corridor (frontage roads AND avoid
|
|
182
|
+
// corridors), so a plot can never straddle a crossing street.
|
|
183
|
+
let onRoad = false;
|
|
184
|
+
for (const corridor of corridors) {
|
|
185
|
+
if (!rectClearsPolyline(rect, corridor.path, corridor.half - roadClearanceSlack)) {
|
|
186
|
+
onRoad = true;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (onRoad)
|
|
191
|
+
continue;
|
|
106
192
|
lots.push({
|
|
107
193
|
center: [cx, cz],
|
|
108
|
-
rotationY
|
|
109
|
-
footprint: { w:
|
|
194
|
+
rotationY,
|
|
195
|
+
footprint: { w: plot.w, d: plot.d },
|
|
110
196
|
road: r,
|
|
111
197
|
side,
|
|
112
198
|
frontDistance: front,
|
|
113
199
|
});
|
|
114
|
-
accepted.push(
|
|
200
|
+
accepted.push(rect);
|
|
115
201
|
}
|
|
116
202
|
}
|
|
117
203
|
}
|