@jgengine/core 0.17.0 → 0.18.1
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 +130 -2
- package/dist/ai/decisionGraph.d.ts +52 -0
- package/dist/ai/decisionGraph.js +82 -0
- package/dist/ai/mobBrainGraph.d.ts +3 -0
- package/dist/ai/mobBrainGraph.js +25 -0
- package/dist/ai/tacticalQueries.d.ts +30 -0
- package/dist/ai/tacticalQueries.js +45 -0
- package/dist/anim/animGraph.d.ts +144 -0
- package/dist/anim/animGraph.js +286 -0
- package/dist/anim/ikSolver.d.ts +51 -0
- package/dist/anim/ikSolver.js +124 -0
- package/dist/anim/locomotionGraph.d.ts +26 -0
- package/dist/anim/locomotionGraph.js +33 -0
- package/dist/assets/spriteAtlas.d.ts +21 -0
- package/dist/assets/spriteAtlas.js +103 -0
- package/dist/audio/audioFalloff.d.ts +10 -0
- package/dist/economy/currency.d.ts +16 -2
- package/dist/economy/currency.js +55 -14
- package/dist/economy/wallet.d.ts +5 -4
- package/dist/economy/wallet.js +8 -4
- package/dist/editor/commandApply.js +1 -0
- package/dist/editor/commands.d.ts +4 -1
- package/dist/editor/document.d.ts +2 -1
- package/dist/editor/document.js +89 -0
- package/dist/editor/environment.d.ts +3 -0
- package/dist/editor/environment.js +16 -0
- package/dist/editor/index.d.ts +4 -2
- package/dist/editor/index.js +1 -1
- package/dist/editor/types.d.ts +16 -0
- package/dist/game/cameraConfig.d.ts +8 -0
- package/dist/game/chat.d.ts +22 -1
- package/dist/game/chat.js +48 -19
- package/dist/game/controlGate.d.ts +6 -0
- package/dist/game/controlGate.js +30 -1
- package/dist/game/defineGame.d.ts +10 -1
- package/dist/game/events.d.ts +8 -1
- package/dist/game/pixelPerfect.d.ts +25 -0
- package/dist/game/pixelPerfect.js +29 -0
- package/dist/game/playableGame.d.ts +97 -3
- package/dist/game/systemRuntime.js +6 -1
- package/dist/input/actionBindings.d.ts +2 -1
- package/dist/input/actionBindings.js +10 -8
- package/dist/input/actionContexts.d.ts +21 -0
- package/dist/input/actionContexts.js +56 -0
- package/dist/input/gamepadModel.d.ts +45 -0
- package/dist/input/gamepadModel.js +69 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +103 -1
- package/dist/movement/characterController.d.ts +76 -0
- package/dist/movement/characterController.js +216 -0
- package/dist/movement/freeFlight.d.ts +119 -0
- package/dist/movement/freeFlight.js +294 -0
- package/dist/movement/playerMovement.d.ts +10 -2
- package/dist/movement/playerMovement.js +218 -4
- package/dist/movement/serverStep.d.ts +5 -0
- package/dist/movement/serverStep.js +6 -0
- package/dist/movement/solidObstacles.d.ts +30 -7
- package/dist/movement/solidObstacles.js +27 -4
- package/dist/multiplayer/poseSyncGate.d.ts +3 -1
- package/dist/multiplayer/poseSyncGate.js +9 -1
- package/dist/multiplayer/presenceContract.d.ts +6 -0
- package/dist/multiplayer/presenceModel.d.ts +2 -0
- package/dist/multiplayer/presenceModel.js +11 -0
- package/dist/nav/navMesh.d.ts +33 -0
- package/dist/nav/navMesh.js +164 -0
- package/dist/physics/flightDynamics.d.ts +8 -0
- package/dist/physics/kinematicVehicle.d.ts +6 -1
- package/dist/physics/physicsBackend.d.ts +191 -0
- package/dist/physics/physicsBackend.js +49 -0
- package/dist/physics/physicsBackendConformance.d.ts +27 -0
- package/dist/physics/physicsBackendConformance.js +117 -0
- package/dist/physics/physicsWorldBackend.d.ts +44 -0
- package/dist/physics/physicsWorldBackend.js +485 -0
- package/dist/physics/worldColliders.d.ts +13 -0
- package/dist/physics/worldColliders.js +96 -0
- package/dist/random/rng.d.ts +12 -1
- package/dist/random/rng.js +20 -3
- package/dist/render/environment.d.ts +23 -0
- package/dist/render/environment.js +1 -0
- package/dist/render/lookPreset.d.ts +11 -5
- package/dist/render/lookPreset.js +29 -4
- package/dist/render/postProcessing.d.ts +10 -1
- package/dist/render/sprite2d.d.ts +38 -0
- package/dist/render/sprite2d.js +40 -0
- package/dist/runtime/adapter.d.ts +20 -1
- package/dist/runtime/adapter.js +8 -1
- package/dist/runtime/commandInput.d.ts +15 -0
- package/dist/runtime/commandInput.js +13 -0
- package/dist/runtime/commandRunner.d.ts +44 -3
- package/dist/runtime/commandRunner.js +41 -3
- package/dist/runtime/gameContext.js +11 -1
- package/dist/runtime/gameContextTypes.d.ts +14 -4
- package/dist/runtime/gameRuntime.d.ts +33 -4
- package/dist/runtime/gameRuntime.js +26 -5
- package/dist/runtime/headlessRunner.d.ts +10 -0
- package/dist/runtime/headlessRunner.js +26 -8
- package/dist/runtime/hostPolicy.d.ts +3 -2
- package/dist/runtime/hostPolicy.js +8 -10
- package/dist/runtime/hostedGameRunner.js +34 -6
- package/dist/runtime/hostedWorldSession.d.ts +12 -16
- package/dist/runtime/hostedWorldSession.js +29 -6
- package/dist/runtime/hostedWorldStore.d.ts +20 -0
- package/dist/runtime/hostedWorldStore.js +1 -0
- package/dist/runtime/inputRecorder.d.ts +30 -0
- package/dist/runtime/inputRecorder.js +59 -0
- package/dist/runtime/inputSnapshot.d.ts +8 -0
- package/dist/runtime/inputSnapshot.js +1 -0
- package/dist/runtime/poseInterpolation.d.ts +36 -0
- package/dist/runtime/poseInterpolation.js +91 -0
- package/dist/runtime/prediction.d.ts +45 -0
- package/dist/runtime/prediction.js +62 -0
- package/dist/runtime/simContext.d.ts +45 -0
- package/dist/runtime/simContext.js +65 -0
- package/dist/runtime/simLoop.d.ts +52 -0
- package/dist/runtime/simLoop.js +72 -0
- package/dist/runtime/simSnapshot.d.ts +32 -0
- package/dist/runtime/simSnapshot.js +60 -0
- package/dist/runtime/snapshot.d.ts +8 -0
- package/dist/runtime/snapshotBuffer.d.ts +33 -0
- package/dist/runtime/snapshotBuffer.js +58 -0
- package/dist/runtime/transport.d.ts +38 -2
- package/dist/runtime/worldChunks.d.ts +2 -0
- package/dist/runtime/worldChunks.js +14 -0
- package/dist/runtime/worldProjection.d.ts +9 -0
- package/dist/runtime/worldProjection.js +17 -1
- package/dist/runtime/worldSnapshot.d.ts +6 -0
- package/dist/runtime/worldSnapshot.js +3 -0
- package/dist/scene/behaviorRuntime.d.ts +19 -1
- package/dist/scene/behaviorRuntime.js +55 -4
- package/dist/scene/behaviors.d.ts +8 -1
- package/dist/scene/builtinSceneKinds.js +10 -0
- package/dist/sensor/perception.d.ts +60 -0
- package/dist/sensor/perception.js +142 -0
- package/dist/settings/graphicsProfile.d.ts +19 -0
- package/dist/settings/graphicsProfile.js +36 -0
- package/dist/time/accrueSince.d.ts +10 -0
- package/dist/time/accrueSince.js +9 -0
- package/dist/time/rateWindow.d.ts +16 -0
- package/dist/time/rateWindow.js +14 -0
- package/dist/time/serverTick.d.ts +21 -3
- package/dist/time/serverTick.js +14 -13
- package/dist/vfx/particleDirector.d.ts +68 -0
- package/dist/vfx/particleDirector.js +81 -0
- package/dist/world/buildings.d.ts +12 -0
- package/dist/world/buildings.js +43 -3
- package/dist/world/cityContent.js +103 -25
- package/dist/world/cityKind.d.ts +132 -0
- package/dist/world/cityKind.js +315 -15
- package/dist/world/features.d.ts +27 -1
- package/dist/world/features.js +7 -2
- package/dist/world/placement.d.ts +7 -0
- package/dist/world/placement.js +2 -0
- package/dist/world/placementController.d.ts +1 -1
- package/dist/world/placementController.js +7 -0
- package/dist/world/raceCircuit.d.ts +168 -0
- package/dist/world/raceCircuit.js +797 -0
- package/dist/world/terrain.d.ts +5 -1
- package/dist/world/terrain.js +5 -0
- package/dist/world/territory.d.ts +97 -0
- package/dist/world/territory.js +182 -0
- package/dist/world.d.ts +5 -3
- package/dist/world.js +4 -2
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/game-design/SKILL.md +11 -1
- package/skills/jgengine/SKILL.md +2 -0
- package/skills/jgengine/api.md +98 -22
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine-gameplay/SKILL.md +10 -0
- package/skills/jgengine-gameplay/api.md +40 -18
- package/skills/jgengine-gameplay/capabilities.md +12 -0
- package/skills/jgengine-world/SKILL.md +6 -0
- package/skills/jgengine-world/api.md +312 -12
- package/skills/jgengine-world/capabilities.md +102 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Maximum accepted operations in a sliding duration. */
|
|
2
|
+
export interface RateWindowPolicy {
|
|
3
|
+
windowMs: number;
|
|
4
|
+
max: number;
|
|
5
|
+
}
|
|
6
|
+
/** Admission result, next stored timestamps, and wait before retrying. */
|
|
7
|
+
export interface RateWindowDecision {
|
|
8
|
+
allowed: boolean;
|
|
9
|
+
timestamps: number[];
|
|
10
|
+
retryAfterMs: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A sliding rate window; persist timestamps only alongside the accepted operation.
|
|
14
|
+
* @capability rate-window-policy Evaluate deterministic request windows with retry timing.
|
|
15
|
+
*/
|
|
16
|
+
export declare function decideRateWindow(timestamps: readonly number[], nowMs: number, policy: RateWindowPolicy): RateWindowDecision;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A sliding rate window; persist timestamps only alongside the accepted operation.
|
|
3
|
+
* @capability rate-window-policy Evaluate deterministic request windows with retry timing.
|
|
4
|
+
*/
|
|
5
|
+
export function decideRateWindow(timestamps, nowMs, policy) {
|
|
6
|
+
if (!Number.isFinite(nowMs) || !Number.isFinite(policy.windowMs) || policy.windowMs <= 0 ||
|
|
7
|
+
!Number.isSafeInteger(policy.max) || policy.max < 1)
|
|
8
|
+
throw new RangeError("Invalid rate window policy");
|
|
9
|
+
const active = timestamps.filter((at) => Number.isFinite(at) && at > nowMs - policy.windowMs).sort((a, b) => a - b);
|
|
10
|
+
if (active.length >= policy.max) {
|
|
11
|
+
return { allowed: false, timestamps: active, retryAfterMs: Math.max(0, active[active.length - policy.max] + policy.windowMs - nowMs) };
|
|
12
|
+
}
|
|
13
|
+
return { allowed: true, timestamps: [...active, nowMs], retryAfterMs: 0 };
|
|
14
|
+
}
|
|
@@ -2,11 +2,26 @@ export interface TickSystemDefinition<TSystemId extends string = string> {
|
|
|
2
2
|
id: TSystemId;
|
|
3
3
|
/** Minimum real time between runs; the heartbeat fires the system on the first tick at or past it. */
|
|
4
4
|
intervalMs: number;
|
|
5
|
+
scope?: "onlinePlayers" | "allServers";
|
|
6
|
+
batchSize?: number;
|
|
5
7
|
}
|
|
6
8
|
/** Last-run timestamp per system id. */
|
|
7
9
|
export type TickAnchors = Record<string, number>;
|
|
10
|
+
/** One due system and how many times this heartbeat owes it. */
|
|
11
|
+
export interface TickSystemRun<TSystemId extends string = string> {
|
|
12
|
+
id: TSystemId;
|
|
13
|
+
/** Times to run the system now; above 1 when wall time stalled past several intervals. */
|
|
14
|
+
runs: number;
|
|
15
|
+
scope?: "onlinePlayers" | "allServers";
|
|
16
|
+
batchSize?: number;
|
|
17
|
+
}
|
|
8
18
|
export interface ServerTickPlan<TSystemId extends string = string> {
|
|
9
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Due systems, at most one entry per id. `runs` carries catch-up multiplicity — the anchors were
|
|
21
|
+
* advanced by that many intervals, so a host that runs the handler once per entry silently drops
|
|
22
|
+
* the rest.
|
|
23
|
+
*/
|
|
24
|
+
due: TickSystemRun<TSystemId>[];
|
|
10
25
|
anchors: TickAnchors;
|
|
11
26
|
}
|
|
12
27
|
export type PlanServerTickOptions = {
|
|
@@ -15,8 +30,11 @@ export type PlanServerTickOptions = {
|
|
|
15
30
|
};
|
|
16
31
|
/**
|
|
17
32
|
* Decides which systems a heartbeat should run. A system with no anchor runs immediately.
|
|
18
|
-
* When wall time stalls past multiple intervals
|
|
19
|
-
* maxCatchUp)
|
|
33
|
+
* When wall time stalls past multiple intervals the system's `runs` climbs (bounded by
|
|
34
|
+
* maxCatchUp), and the returned anchor moves by that many intervals — so a host that ignores
|
|
35
|
+
* `runs` loses the catch-up work. Excess lag past the bound resyncs the anchor to `now`.
|
|
20
36
|
* Returned anchors carry only systems present in the pipeline, so removed systems clean up.
|
|
21
37
|
*/
|
|
22
38
|
export declare function planServerTick<TSystemId extends string>(systems: readonly TickSystemDefinition<TSystemId>[], anchors: TickAnchors, now: number, options?: PlanServerTickOptions): ServerTickPlan<TSystemId>;
|
|
39
|
+
/** Times this plan owes `id`, or 0 when it is not due. Saves a host a lookup over {@link ServerTickPlan.due}. */
|
|
40
|
+
export declare function tickRunCount<TSystemId extends string>(plan: ServerTickPlan<TSystemId>, id: TSystemId): number;
|
package/dist/time/serverTick.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
const DEFAULT_MAX_CATCH_UP = 3;
|
|
2
2
|
/**
|
|
3
3
|
* Decides which systems a heartbeat should run. A system with no anchor runs immediately.
|
|
4
|
-
* When wall time stalls past multiple intervals
|
|
5
|
-
* maxCatchUp)
|
|
4
|
+
* When wall time stalls past multiple intervals the system's `runs` climbs (bounded by
|
|
5
|
+
* maxCatchUp), and the returned anchor moves by that many intervals — so a host that ignores
|
|
6
|
+
* `runs` loses the catch-up work. Excess lag past the bound resyncs the anchor to `now`.
|
|
6
7
|
* Returned anchors carry only systems present in the pipeline, so removed systems clean up.
|
|
7
8
|
*/
|
|
8
9
|
export function planServerTick(systems, anchors, now, options) {
|
|
@@ -10,14 +11,12 @@ export function planServerTick(systems, anchors, now, options) {
|
|
|
10
11
|
const due = [];
|
|
11
12
|
const nextAnchors = {};
|
|
12
13
|
for (const system of systems) {
|
|
14
|
+
if (system.batchSize !== undefined && (!Number.isSafeInteger(system.batchSize) || system.batchSize < 1 || system.batchSize > 1000))
|
|
15
|
+
throw new RangeError("batchSize must be an integer from 1 to 1000");
|
|
16
|
+
const dispatch = { ...(system.scope === undefined ? {} : { scope: system.scope }), ...(system.batchSize === undefined ? {} : { batchSize: system.batchSize }) };
|
|
13
17
|
const lastRunAt = anchors[system.id];
|
|
14
|
-
if (lastRunAt === undefined) {
|
|
15
|
-
due.push(system.id);
|
|
16
|
-
nextAnchors[system.id] = now;
|
|
17
|
-
continue;
|
|
18
|
-
}
|
|
19
|
-
if (system.intervalMs <= 0) {
|
|
20
|
-
due.push(system.id);
|
|
18
|
+
if (lastRunAt === undefined || system.intervalMs <= 0) {
|
|
19
|
+
due.push({ id: system.id, runs: 1, ...dispatch });
|
|
21
20
|
nextAnchors[system.id] = now;
|
|
22
21
|
continue;
|
|
23
22
|
}
|
|
@@ -28,14 +27,16 @@ export function planServerTick(systems, anchors, now, options) {
|
|
|
28
27
|
}
|
|
29
28
|
const missed = Math.floor(elapsed / system.intervalMs);
|
|
30
29
|
if (missed > maxCatchUp) {
|
|
31
|
-
|
|
32
|
-
due.push(system.id);
|
|
30
|
+
due.push({ id: system.id, runs: maxCatchUp, ...dispatch });
|
|
33
31
|
nextAnchors[system.id] = now;
|
|
34
32
|
continue;
|
|
35
33
|
}
|
|
36
|
-
|
|
37
|
-
due.push(system.id);
|
|
34
|
+
due.push({ id: system.id, runs: missed, ...dispatch });
|
|
38
35
|
nextAnchors[system.id] = lastRunAt + missed * system.intervalMs;
|
|
39
36
|
}
|
|
40
37
|
return { due, anchors: nextAnchors };
|
|
41
38
|
}
|
|
39
|
+
/** Times this plan owes `id`, or 0 when it is not due. Saves a host a lookup over {@link ServerTickPlan.due}. */
|
|
40
|
+
export function tickRunCount(plan, id) {
|
|
41
|
+
return plan.due.find((entry) => entry.id === id)?.runs ?? 0;
|
|
42
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { EmitterConfig, Vec3 } from "./particles.js";
|
|
2
|
+
/** How a particle effect composites on screen: `additive` for sparks/fire/glow, `normal` for smoke/dust. */
|
|
3
|
+
export type ParticleBlendHint = "additive" | "normal";
|
|
4
|
+
/** A one-shot burst request — consumed once by the renderer, never serialized. */
|
|
5
|
+
export interface ParticleBurst {
|
|
6
|
+
/** Monotonic sequence id so a renderer can consume each burst exactly once. */
|
|
7
|
+
seq: number;
|
|
8
|
+
config: EmitterConfig;
|
|
9
|
+
/** Particles to emit immediately. */
|
|
10
|
+
count: number;
|
|
11
|
+
blending?: ParticleBlendHint;
|
|
12
|
+
}
|
|
13
|
+
/** Options for {@link ParticleDirector.attach}. */
|
|
14
|
+
export interface ParticleAttachOptions {
|
|
15
|
+
/** Scene entity id the emitter origin tracks each frame (plus `offset`). */
|
|
16
|
+
follow?: string;
|
|
17
|
+
/** World offset from the followed entity (or from the config position when unfollowed). */
|
|
18
|
+
offset?: Vec3;
|
|
19
|
+
blending?: ParticleBlendHint;
|
|
20
|
+
}
|
|
21
|
+
/** A keyed continuous emitter owned by the director until detached. */
|
|
22
|
+
export interface ParticleEmitterSpec {
|
|
23
|
+
id: string;
|
|
24
|
+
config: EmitterConfig;
|
|
25
|
+
follow?: string;
|
|
26
|
+
offset?: Vec3;
|
|
27
|
+
blending?: ParticleBlendHint;
|
|
28
|
+
}
|
|
29
|
+
/** Serializable director state: the standing emitters (bursts are transient by nature). */
|
|
30
|
+
export interface ParticleDirectorState {
|
|
31
|
+
nextSeq: number;
|
|
32
|
+
emitters: ParticleEmitterSpec[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The game-side seam for particle effects. Game logic requests one-shot bursts and
|
|
36
|
+
* standing emitters as plain data; the shell renders them through
|
|
37
|
+
* `createParticleSystem`/`ParticleField`, applies the graphics-quality particle cap,
|
|
38
|
+
* and tracks `follow` entities. Nothing here touches a renderer, so commands and
|
|
39
|
+
* `onTick` systems can drive VFX headlessly and tests can assert the requested effects.
|
|
40
|
+
*/
|
|
41
|
+
export interface ParticleDirector {
|
|
42
|
+
/** Queue a one-shot burst (impact puff, debris, muzzle smoke). */
|
|
43
|
+
burst(config: EmitterConfig, count: number, blending?: ParticleBlendHint): void;
|
|
44
|
+
/** Create or replace a standing emitter under `id` (exhaust, dust plume, torch smoke). */
|
|
45
|
+
attach(id: string, config: EmitterConfig, options?: ParticleAttachOptions): void;
|
|
46
|
+
/** Patch a standing emitter's config in place (e.g. scale exhaust rate with speed). */
|
|
47
|
+
retune(id: string, patch: Partial<EmitterConfig>): void;
|
|
48
|
+
/** Remove a standing emitter. Unknown ids are ignored. */
|
|
49
|
+
detach(id: string): void;
|
|
50
|
+
/** Drop every standing emitter and any unconsumed bursts. */
|
|
51
|
+
clear(): void;
|
|
52
|
+
/** The standing emitters, for renderers and assertions. */
|
|
53
|
+
emitters(): readonly ParticleEmitterSpec[];
|
|
54
|
+
/** Return and forget the queued bursts — the renderer's consume-once read. */
|
|
55
|
+
drainBursts(): ParticleBurst[];
|
|
56
|
+
subscribe(listener: () => void): () => void;
|
|
57
|
+
snapshot(): ParticleDirectorState;
|
|
58
|
+
restore(state: ParticleDirectorState): void;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Create the particle intent registry a `GameContext` exposes as `ctx.particles`.
|
|
62
|
+
* State is data-only and serializable: standing emitters survive `snapshot`/`restore`,
|
|
63
|
+
* queued bursts are transient. The renderer subscribes, drains bursts, and mirrors
|
|
64
|
+
* the emitter list — the director never allocates particle pools itself.
|
|
65
|
+
*
|
|
66
|
+
* @capability particle-director game-reachable VFX seam: queue one-shot particle bursts and keyed follow-capable standing emitters as serializable data for the shell to render
|
|
67
|
+
*/
|
|
68
|
+
export declare function createParticleDirector(): ParticleDirector;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create the particle intent registry a `GameContext` exposes as `ctx.particles`.
|
|
3
|
+
* State is data-only and serializable: standing emitters survive `snapshot`/`restore`,
|
|
4
|
+
* queued bursts are transient. The renderer subscribes, drains bursts, and mirrors
|
|
5
|
+
* the emitter list — the director never allocates particle pools itself.
|
|
6
|
+
*
|
|
7
|
+
* @capability particle-director game-reachable VFX seam: queue one-shot particle bursts and keyed follow-capable standing emitters as serializable data for the shell to render
|
|
8
|
+
*/
|
|
9
|
+
export function createParticleDirector() {
|
|
10
|
+
let nextSeq = 1;
|
|
11
|
+
let bursts = [];
|
|
12
|
+
const emitters = new Map();
|
|
13
|
+
const listeners = new Set();
|
|
14
|
+
function notify() {
|
|
15
|
+
for (const listener of listeners)
|
|
16
|
+
listener();
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
burst(config, count, blending) {
|
|
20
|
+
const wanted = Math.floor(count);
|
|
21
|
+
if (wanted <= 0)
|
|
22
|
+
return;
|
|
23
|
+
bursts.push({ seq: nextSeq++, config, count: wanted, ...(blending === undefined ? {} : { blending }) });
|
|
24
|
+
notify();
|
|
25
|
+
},
|
|
26
|
+
attach(id, config, options) {
|
|
27
|
+
emitters.set(id, {
|
|
28
|
+
id,
|
|
29
|
+
config,
|
|
30
|
+
...(options?.follow === undefined ? {} : { follow: options.follow }),
|
|
31
|
+
...(options?.offset === undefined ? {} : { offset: options.offset }),
|
|
32
|
+
...(options?.blending === undefined ? {} : { blending: options.blending }),
|
|
33
|
+
});
|
|
34
|
+
notify();
|
|
35
|
+
},
|
|
36
|
+
retune(id, patch) {
|
|
37
|
+
const current = emitters.get(id);
|
|
38
|
+
if (current === undefined)
|
|
39
|
+
return;
|
|
40
|
+
emitters.set(id, { ...current, config: { ...current.config, ...patch } });
|
|
41
|
+
notify();
|
|
42
|
+
},
|
|
43
|
+
detach(id) {
|
|
44
|
+
if (!emitters.delete(id))
|
|
45
|
+
return;
|
|
46
|
+
notify();
|
|
47
|
+
},
|
|
48
|
+
clear() {
|
|
49
|
+
if (emitters.size === 0 && bursts.length === 0)
|
|
50
|
+
return;
|
|
51
|
+
emitters.clear();
|
|
52
|
+
bursts = [];
|
|
53
|
+
notify();
|
|
54
|
+
},
|
|
55
|
+
emitters() {
|
|
56
|
+
return Array.from(emitters.values());
|
|
57
|
+
},
|
|
58
|
+
drainBursts() {
|
|
59
|
+
if (bursts.length === 0)
|
|
60
|
+
return [];
|
|
61
|
+
const drained = bursts;
|
|
62
|
+
bursts = [];
|
|
63
|
+
return drained;
|
|
64
|
+
},
|
|
65
|
+
subscribe(listener) {
|
|
66
|
+
listeners.add(listener);
|
|
67
|
+
return () => listeners.delete(listener);
|
|
68
|
+
},
|
|
69
|
+
snapshot() {
|
|
70
|
+
return { nextSeq, emitters: Array.from(emitters.values()) };
|
|
71
|
+
},
|
|
72
|
+
restore(state) {
|
|
73
|
+
nextSeq = state.nextSeq;
|
|
74
|
+
bursts = [];
|
|
75
|
+
emitters.clear();
|
|
76
|
+
for (const spec of state.emitters)
|
|
77
|
+
emitters.set(spec.id, spec);
|
|
78
|
+
notify();
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -108,6 +108,18 @@ export type BuildingPaletteOverrides = Partial<BuildingPalette>;
|
|
|
108
108
|
export type BuildingStyle = "generic" | "capital" | "village" | "desert" | "industrial" | "coastal" | "neon" | "ruin" | "frontier" | "aerial";
|
|
109
109
|
export declare const DEFAULT_BUILDING_STYLE: BuildingStyle;
|
|
110
110
|
export declare const BUILDING_STYLE_PALETTES: Record<BuildingStyle, BuildingPalette>;
|
|
111
|
+
/**
|
|
112
|
+
* Per-style facade-tone family: the wall colours a district spreads across its buildings so
|
|
113
|
+
* neighbours differ in hue and value while the block stays one palette. Index 0 is the palette's
|
|
114
|
+
* own `wall`; the rest fan warmer/cooler and lighter/darker around it.
|
|
115
|
+
*/
|
|
116
|
+
export declare const BUILDING_STYLE_WALL_TONES: Record<BuildingStyle, readonly string[]>;
|
|
117
|
+
/**
|
|
118
|
+
* The facade-tone family for a style — the ramp {@link BUILDING_STYLE_WALL_TONES} keeps, falling
|
|
119
|
+
* back to the style's single palette wall when a style has no ramp.
|
|
120
|
+
* @internal
|
|
121
|
+
*/
|
|
122
|
+
export declare function resolveBuildingWallTones(style?: BuildingStyle): readonly string[];
|
|
111
123
|
/** @internal */
|
|
112
124
|
export declare function resolveBuildingPalette(style?: BuildingStyle, overrides?: BuildingPaletteOverrides): BuildingPalette;
|
|
113
125
|
/**
|
package/dist/world/buildings.js
CHANGED
|
@@ -181,6 +181,34 @@ export const BUILDING_STYLE_PALETTES = {
|
|
|
181
181
|
corner: "#c3d0d7",
|
|
182
182
|
},
|
|
183
183
|
};
|
|
184
|
+
/**
|
|
185
|
+
* Per-style facade-tone family: the wall colours a district spreads across its buildings so
|
|
186
|
+
* neighbours differ in hue and value while the block stays one palette. Index 0 is the palette's
|
|
187
|
+
* own `wall`; the rest fan warmer/cooler and lighter/darker around it.
|
|
188
|
+
*/
|
|
189
|
+
export const BUILDING_STYLE_WALL_TONES = {
|
|
190
|
+
generic: ["#9c8f80", "#736a60", "#b7a894", "#8b8a80", "#c3ac90", "#635c54", "#a1978c"],
|
|
191
|
+
capital: ["#cfc5ad", "#ac9878", "#e6e0cf", "#a8a698", "#c8b48d", "#847b68", "#ded2b3"],
|
|
192
|
+
village: ["#b59e7a", "#9c8360", "#c9b895", "#8d7a5f", "#c2a173", "#a8977f", "#d6c3a1"],
|
|
193
|
+
desert: ["#c9a877", "#b08f61", "#ddc396", "#a4855c", "#d3b98d", "#93764f", "#c6b189"],
|
|
194
|
+
industrial: ["#7c838f", "#565d69", "#96a0ac", "#68737a", "#8a8173", "#4a5058", "#a0a6ac"],
|
|
195
|
+
coastal: ["#e8e2d2", "#cfd6d3", "#f1ece0", "#c9c0ac", "#dbe0dc", "#b7b3a3", "#e5d9c4"],
|
|
196
|
+
neon: ["#1b1b26", "#262233", "#141a24", "#2c2030", "#181f2b", "#221d1f", "#101018"],
|
|
197
|
+
ruin: ["#6b6156", "#57503f", "#7d7266", "#4f4a44", "#736450", "#8a8072", "#5d5b53"],
|
|
198
|
+
frontier: ["#7f5539", "#6a462d", "#96684a", "#5c4331", "#8a6540", "#a07a55", "#6f523c"],
|
|
199
|
+
aerial: ["#dfe7ec", "#c6d2da", "#eef3f6", "#b6c3cb", "#d3dde2", "#a7b7c0", "#e7eef1"],
|
|
200
|
+
};
|
|
201
|
+
/**
|
|
202
|
+
* The facade-tone family for a style — the ramp {@link BUILDING_STYLE_WALL_TONES} keeps, falling
|
|
203
|
+
* back to the style's single palette wall when a style has no ramp.
|
|
204
|
+
* @internal
|
|
205
|
+
*/
|
|
206
|
+
export function resolveBuildingWallTones(style = DEFAULT_BUILDING_STYLE) {
|
|
207
|
+
const tones = BUILDING_STYLE_WALL_TONES[style];
|
|
208
|
+
if (tones !== undefined && tones.length > 0)
|
|
209
|
+
return tones;
|
|
210
|
+
return [resolveBuildingPalette(style).wall];
|
|
211
|
+
}
|
|
184
212
|
/** @internal */
|
|
185
213
|
export function resolveBuildingPalette(style = DEFAULT_BUILDING_STYLE, overrides) {
|
|
186
214
|
const preset = BUILDING_STYLE_PALETTES[style];
|
|
@@ -413,9 +441,21 @@ function pushRoof(parts, config, width, depth) {
|
|
|
413
441
|
for (let index = 0; index < roofCells; index += 1) {
|
|
414
442
|
if (!chance(config.seed, `roof:prop:${index}`, config.probabilities.roofProp))
|
|
415
443
|
continue;
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
444
|
+
// Plant sized and nudged off its cell centre: props locked to the bay grid at one size read as
|
|
445
|
+
// a checkerboard of cubes rather than a roof of mechanical units.
|
|
446
|
+
const wide = hash01(config.seed, `roof:prop:${index}:w`);
|
|
447
|
+
const tall = hash01(config.seed, `roof:prop:${index}:h`);
|
|
448
|
+
const propW = config.bayWidth * (0.2 + wide * 0.4);
|
|
449
|
+
const propD = config.bayWidth * (0.18 + (1 - wide) * 0.36);
|
|
450
|
+
const propH = config.floorHeight * (0.14 + tall * 0.52);
|
|
451
|
+
const x = config.center[0] -
|
|
452
|
+
width / 2 +
|
|
453
|
+
config.bayWidth * ((index % config.baysWide) + 0.5 + (hash01(config.seed, `roof:prop:${index}:x`) - 0.5) * 0.5);
|
|
454
|
+
const z = config.center[1] -
|
|
455
|
+
depth / 2 +
|
|
456
|
+
config.bayWidth *
|
|
457
|
+
(Math.floor(index / config.baysWide) + 0.5 + (hash01(config.seed, `roof:prop:${index}:z`) - 0.5) * 0.5);
|
|
458
|
+
parts.push(part(config, "roofProp", "roof", [x, height + config.facadeDepth + propH / 2, z], [propW, propH, propD], `roof:prop:${index}`, { facade: "roof", level: config.floors, bay: index }, ["decor"]));
|
|
419
459
|
}
|
|
420
460
|
}
|
|
421
461
|
function pushCorners(parts, config, width, depth) {
|
|
@@ -13,18 +13,18 @@ export const CITY_LOT_CLASSES = [
|
|
|
13
13
|
/** All filler classes, for validation and interior-pass allow-lists. */
|
|
14
14
|
export const CITY_FILLER_CLASSES = ["garage", "depot"];
|
|
15
15
|
const CLASS_PROFILES = {
|
|
16
|
-
tower: { width: [
|
|
17
|
-
slab: { width: [
|
|
18
|
-
shop: { width: [
|
|
19
|
-
rowhouse: { width: [
|
|
16
|
+
tower: { width: [14, 22], depth: [12, 20], setbackFactor: 0.15, spacingFactor: 0.35, floors: [12, 34], backRow: true },
|
|
17
|
+
slab: { width: [12, 20], depth: [10, 16], setbackFactor: 0.2, spacingFactor: 0.4, floors: [4, 9], backRow: true },
|
|
18
|
+
shop: { width: [8, 14], depth: [8, 12], setbackFactor: 0.05, spacingFactor: 0.15, floors: [1, 2], backRow: true },
|
|
19
|
+
rowhouse: { width: [5.5, 8], depth: [9, 13], setbackFactor: 0.2, spacingFactor: 0.05, floors: [2, 3], backRow: true },
|
|
20
20
|
house: { width: [9, 12], depth: [8, 11], setbackFactor: 1, spacingFactor: 1, floors: [1, 2], backRow: true },
|
|
21
21
|
mansion: { width: [18, 26], depth: [14, 20], setbackFactor: 2, spacingFactor: 1.6, floors: [2, 3], backRow: false },
|
|
22
22
|
farmhouse: { width: [11, 14], depth: [9, 12], setbackFactor: 2.8, spacingFactor: 4, floors: [2, 2], backRow: false },
|
|
23
23
|
barn: { width: [12, 16], depth: [9, 13], setbackFactor: 3.6, spacingFactor: 5, floors: [1, 1], backRow: false },
|
|
24
24
|
silo: { width: [5, 6], depth: [5, 6], setbackFactor: 4, spacingFactor: 3, floors: [1, 1], backRow: false },
|
|
25
25
|
// Interior-fill fillers: broad, plain footprints that pack the backs of dense blocks.
|
|
26
|
-
garage: { width: [
|
|
27
|
-
depot: { width: [
|
|
26
|
+
garage: { width: [16, 24], depth: [14, 20], setbackFactor: 0.2, spacingFactor: 0.3, floors: [2, 6], backRow: true },
|
|
27
|
+
depot: { width: [18, 28], depth: [16, 24], setbackFactor: 0.25, spacingFactor: 0.35, floors: [1, 2], backRow: true },
|
|
28
28
|
};
|
|
29
29
|
/**
|
|
30
30
|
* Natural frontage-width range (meters) of a class's massing — what the plot-size class bias reads
|
|
@@ -131,6 +131,59 @@ function box(offset, size, role, options = {}) {
|
|
|
131
131
|
function gable(offset, size, rotationY = 0) {
|
|
132
132
|
return { shape: "gable", offset, size, rotationY, role: "roof", grounded: false, banded: false };
|
|
133
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Finish a flat roof at height `top`: a recessed dark deck, a parapet upstand on all four edges,
|
|
136
|
+
* and seeded rooftop plant (mechanical units, a stair bulkhead, a tank). A city skyline is mostly
|
|
137
|
+
* roofs seen from above, so every flat-capped class runs through here instead of a bare slab.
|
|
138
|
+
*/
|
|
139
|
+
function flatRoofTop(pieces, width, depth, top, rng) {
|
|
140
|
+
const rail = Math.min(1.15, 0.55 + rng() * 0.55);
|
|
141
|
+
const wall = Math.min(0.55, Math.max(0.18, Math.min(width, depth) * 0.035));
|
|
142
|
+
const innerW = Math.max(0.4, width - wall * 2);
|
|
143
|
+
const innerD = Math.max(0.4, depth - wall * 2);
|
|
144
|
+
pieces.push(box([0, top, 0], [innerW, 0.14, innerD], "roof"));
|
|
145
|
+
pieces.push(box([0, top, (depth - wall) / 2], [width, rail, wall], "trim"));
|
|
146
|
+
pieces.push(box([0, top, -(depth - wall) / 2], [width, rail, wall], "trim"));
|
|
147
|
+
pieces.push(box([(width - wall) / 2, top, 0], [wall, rail, innerD], "trim"));
|
|
148
|
+
pieces.push(box([-(width - wall) / 2, top, 0], [wall, rail, innerD], "trim"));
|
|
149
|
+
const deck = top + 0.14;
|
|
150
|
+
const units = innerW * innerD > 110 ? 1 + Math.floor(rng() * 3) : rng() < 0.7 ? 1 : 0;
|
|
151
|
+
for (let i = 0; i < units; i += 1) {
|
|
152
|
+
const uw = Math.min(innerW * 0.34, 1.6 + rng() * 2.4);
|
|
153
|
+
const ud = Math.min(innerD * 0.34, 1.4 + rng() * 2);
|
|
154
|
+
pieces.push(box([(rng() - 0.5) * Math.max(0, innerW - uw), deck, (rng() - 0.5) * Math.max(0, innerD - ud)], [uw, 0.6 + rng() * 0.9, ud], "roof"));
|
|
155
|
+
}
|
|
156
|
+
if (rng() < 0.55) {
|
|
157
|
+
const bw = Math.min(innerW * 0.34, 2.2 + rng() * 1.6);
|
|
158
|
+
const bd = Math.min(innerD * 0.36, 2 + rng() * 1.4);
|
|
159
|
+
pieces.push(box([(rng() - 0.5) * Math.max(0, innerW - bw), deck, (rng() - 0.5) * Math.max(0, innerD - bd)], [bw, 1.9 + rng() * 1.1, bd], "trim"));
|
|
160
|
+
}
|
|
161
|
+
if (rng() < 0.4) {
|
|
162
|
+
const r = Math.min(2.2, Math.min(innerW, innerD) * 0.26);
|
|
163
|
+
pieces.push({
|
|
164
|
+
shape: "cylinder",
|
|
165
|
+
offset: [(rng() - 0.5) * Math.max(0, innerW - r), deck, (rng() - 0.5) * Math.max(0, innerD - r)],
|
|
166
|
+
size: [r, 1.4 + rng() * 1.2, r],
|
|
167
|
+
rotationY: 0,
|
|
168
|
+
role: "trim",
|
|
169
|
+
grounded: false,
|
|
170
|
+
banded: false,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Street-level treatment for a commercial frontage: a plinth course wrapping the base, a recessed
|
|
176
|
+
* dark entrance on the street face, and a canopy over the sidewalk — so floor 1 never reads like
|
|
177
|
+
* floor 30. `overhang` is the plinth's outward step (0 keeps it flush for terraced classes).
|
|
178
|
+
*/
|
|
179
|
+
function streetLevel(pieces, width, depth, bandHeight, overhang, rng) {
|
|
180
|
+
pieces.push(box([0, 0, 0], [width + overhang, bandHeight, depth + overhang], "trim", { grounded: true }));
|
|
181
|
+
const doorW = Math.min(width * 0.32, 3.4);
|
|
182
|
+
const doorX = (rng() - 0.5) * Math.max(0, width - doorW * 1.6) * 0.5;
|
|
183
|
+
const face = depth / 2 + overhang / 2;
|
|
184
|
+
pieces.push(box([doorX, 0, face], [doorW, bandHeight * 0.76, 0.5], "roof", { grounded: true }));
|
|
185
|
+
pieces.push(box([doorX, bandHeight * 0.8, face + 0.6], [doorW * 1.5, 0.22, 1.3], "accent"));
|
|
186
|
+
}
|
|
134
187
|
/**
|
|
135
188
|
* Compose the massing pieces for one lot. Deterministic per `rng` stream: the same class, size,
|
|
136
189
|
* floors, and stream always yield the identical silhouette. Sizes are in meters, lot-local.
|
|
@@ -147,17 +200,22 @@ export function buildLotPieces(cls, width, depth, floors, floorHeight, rng) {
|
|
|
147
200
|
const h2 = H * (0.24 + rng() * 0.08);
|
|
148
201
|
const h3 = H - h1 - h2;
|
|
149
202
|
pieces.push(box([0, 0, 0], [width, h1, depth], "wall", { grounded: true, banded: true }));
|
|
203
|
+
// Occasional attached wing breaks the identical-rect streetwall without replacing the shaft.
|
|
204
|
+
if (width >= 16 && depth >= 14 && rng() < 0.42) {
|
|
205
|
+
const wingW = width * (0.34 + rng() * 0.1);
|
|
206
|
+
const side = rng() < 0.5 ? 1 : -1;
|
|
207
|
+
pieces.push(box([side * (width / 2 - wingW / 2), 0, depth * 0.28], [wingW, h1 * (0.55 + rng() * 0.2), depth * 0.42], "wall", { grounded: true, banded: true }));
|
|
208
|
+
}
|
|
150
209
|
const w2 = width * (0.78 + rng() * 0.08);
|
|
151
210
|
const d2 = depth * (0.78 + rng() * 0.08);
|
|
152
211
|
pieces.push(box([0, h1, 0], [w2, h2, d2], "wall", { banded: true }));
|
|
153
212
|
const w3 = width * (0.55 + rng() * 0.1);
|
|
154
213
|
const d3 = depth * (0.55 + rng() * 0.1);
|
|
155
214
|
pieces.push(box([0, h1 + h2, 0], [w3, h3, d3], "wall", { banded: true }));
|
|
156
|
-
pieces
|
|
215
|
+
flatRoofTop(pieces, w3 * 1.04, d3 * 1.04, H, rng);
|
|
157
216
|
if (rng() < 0.35)
|
|
158
217
|
pieces.push(box([0, H + 0.5, 0], [1.2, 3 + rng() * 5, 1.2], "trim"));
|
|
159
|
-
|
|
160
|
-
pieces.push(box([0, H + 0.5, 0], [w3 * 0.4, 1.6, d3 * 0.4], "roof"));
|
|
218
|
+
streetLevel(pieces, width, depth, Math.min(floorHeight * 1.1, H * 0.5), 0.35, rng);
|
|
161
219
|
}
|
|
162
220
|
else {
|
|
163
221
|
const h1 = H * (0.66 + rng() * 0.1);
|
|
@@ -165,32 +223,58 @@ export function buildLotPieces(cls, width, depth, floors, floorHeight, rng) {
|
|
|
165
223
|
const w2 = width * (0.72 + rng() * 0.1);
|
|
166
224
|
const d2 = depth * (0.72 + rng() * 0.1);
|
|
167
225
|
pieces.push(box([0, h1, 0], [w2, H - h1, d2], "wall", { banded: true }));
|
|
168
|
-
pieces
|
|
226
|
+
flatRoofTop(pieces, w2 * 1.05, d2 * 1.05, H, rng);
|
|
227
|
+
streetLevel(pieces, width, depth, Math.min(floorHeight * 1.1, H * 0.5), 0.35, rng);
|
|
169
228
|
}
|
|
170
229
|
return pieces;
|
|
171
230
|
}
|
|
172
231
|
case "slab": {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
232
|
+
// Full streetwall depth; irregular plans break the identical-cell silhouette.
|
|
233
|
+
if (width >= 14 && depth >= 12 && rng() < 0.55) {
|
|
234
|
+
const wing = width * (0.38 + rng() * 0.14);
|
|
235
|
+
const side = rng() < 0.5 ? 1 : -1;
|
|
236
|
+
const frontH = H * (0.92 + rng() * 0.12);
|
|
237
|
+
const wingH = H * (0.7 + rng() * 0.25);
|
|
238
|
+
pieces.push(box([0, 0, depth * 0.08], [width, frontH, depth * 0.62], "wall", { grounded: true, banded: true }));
|
|
239
|
+
pieces.push(box([side * (width / 2 - wing / 2), 0, -depth * 0.08], [wing, wingH, depth * 0.82], "wall", { grounded: true, banded: true }));
|
|
240
|
+
flatRoofTop(pieces, width * 0.98, depth * 0.62, frontH, rng);
|
|
241
|
+
if (rng() < 0.45)
|
|
242
|
+
flatRoofTop(pieces, wing * 1.02, depth * 0.78, wingH, rng);
|
|
243
|
+
streetLevel(pieces, width, depth * 0.62, Math.min(floorHeight * 1.05, frontH * 0.5), 0.25, rng);
|
|
244
|
+
}
|
|
245
|
+
else if (width >= 16 && depth >= 14 && rng() < 0.35) {
|
|
246
|
+
// Open mid-block court: street wall + two side wings, rear open as a service yard.
|
|
247
|
+
const wallT = Math.min(width, depth) * 0.28;
|
|
248
|
+
pieces.push(box([0, 0, (depth - wallT) / 2], [width, H, wallT], "wall", { grounded: true, banded: true }));
|
|
249
|
+
pieces.push(box([(width - wallT) / 2, 0, -wallT * 0.15], [wallT, H * (0.75 + rng() * 0.2), depth - wallT], "wall", { grounded: true, banded: true }));
|
|
250
|
+
pieces.push(box([-(width - wallT) / 2, 0, -wallT * 0.15], [wallT, H * (0.7 + rng() * 0.2), depth - wallT], "wall", { grounded: true, banded: true }));
|
|
251
|
+
flatRoofTop(pieces, width * 1.01, wallT * 1.02, H, rng);
|
|
252
|
+
streetLevel(pieces, width, wallT, Math.min(floorHeight * 1.05, H * 0.5), 0.25, rng);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
pieces.push(box([0, 0, 0], [width, H, depth], "wall", { grounded: true, banded: true }));
|
|
256
|
+
flatRoofTop(pieces, width * 1.02, depth * 1.02, H, rng);
|
|
257
|
+
streetLevel(pieces, width, depth, Math.min(floorHeight * 1.05, H * 0.5), 0.25, rng);
|
|
258
|
+
}
|
|
177
259
|
return pieces;
|
|
178
260
|
}
|
|
179
261
|
case "shop": {
|
|
180
262
|
pieces.push(box([0, 0, 0], [width, H, depth], "wall", { grounded: true }));
|
|
181
|
-
pieces
|
|
263
|
+
flatRoofTop(pieces, width * 1.03, depth * 1.03, H, rng);
|
|
182
264
|
// Front awning strip over the sidewalk face.
|
|
183
265
|
pieces.push(box([0, Math.min(H - 0.4, 2.6), depth / 2 + 0.35], [width * 0.9, 0.35, 0.9], "accent"));
|
|
266
|
+
pieces.push(box([0, 0, depth / 2 - 0.05], [width * 0.82, Math.min(H * 0.62, 2.5), 0.4], "roof", { grounded: true }));
|
|
184
267
|
return pieces;
|
|
185
268
|
}
|
|
186
269
|
case "rowhouse": {
|
|
187
|
-
pieces.push(box([0, 0, 0], [width * 0.
|
|
270
|
+
pieces.push(box([0, 0, 0], [width * 0.98, H, depth * 0.92], "wall", { grounded: true, banded: true }));
|
|
188
271
|
if (rng() < 0.55) {
|
|
189
|
-
pieces.push(gable([0, H, 0], [width * 0.
|
|
272
|
+
pieces.push(gable([0, H, 0], [width * 0.98, 1.6 + rng() * 0.8, depth * 0.96]));
|
|
190
273
|
}
|
|
191
274
|
else {
|
|
192
|
-
pieces.push(box([0, H, 0], [width * 1.0, 0.45, depth * 0.
|
|
275
|
+
pieces.push(box([0, H, 0], [width * 1.0, 0.45, depth * 0.94], "trim"));
|
|
193
276
|
}
|
|
277
|
+
streetLevel(pieces, width * 0.98, depth * 0.92, Math.min(floorHeight * 0.95, H * 0.5), 0.04, rng);
|
|
194
278
|
return pieces;
|
|
195
279
|
}
|
|
196
280
|
case "house": {
|
|
@@ -273,13 +357,7 @@ export function buildLotPieces(cls, width, depth, floors, floorHeight, rng) {
|
|
|
273
357
|
case "garage": {
|
|
274
358
|
// Parking structure: a plain banded box (the window bands read as deck slots) with a flat cap.
|
|
275
359
|
pieces.push(box([0, 0, 0], [width, H, depth], "wall", { grounded: true, banded: true }));
|
|
276
|
-
pieces
|
|
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
|
-
}
|
|
360
|
+
flatRoofTop(pieces, width * 1.02, depth * 1.02, H, rng);
|
|
283
361
|
return pieces;
|
|
284
362
|
}
|
|
285
363
|
case "depot": {
|