@jgengine/core 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/dist/ai/populationDirector.d.ts +103 -0
- package/dist/ai/populationDirector.js +190 -0
- package/dist/ai/pursuit.d.ts +56 -0
- package/dist/ai/pursuit.js +53 -0
- package/dist/authoring.d.ts +1 -1
- package/dist/authoring.js +1 -1
- package/dist/combat.d.ts +1 -0
- package/dist/combat.js +1 -0
- package/dist/devtools/controls.d.ts +38 -0
- package/dist/devtools/controls.js +181 -0
- package/dist/devtools/devtools.d.ts +5 -197
- package/dist/devtools/devtools.js +38 -774
- package/dist/devtools/discover.d.ts +32 -0
- package/dist/devtools/discover.js +240 -0
- package/dist/devtools/fallbackSeams.d.ts +50 -0
- package/dist/devtools/fallbackSeams.js +111 -0
- package/dist/devtools/frame.d.ts +15 -0
- package/dist/devtools/frame.js +247 -0
- package/dist/devtools/logs.d.ts +15 -0
- package/dist/devtools/logs.js +86 -0
- package/dist/devtools/overrides.d.ts +16 -0
- package/dist/devtools/overrides.js +66 -0
- package/dist/devtools/types.d.ts +212 -0
- package/dist/devtools/types.js +1 -0
- package/dist/economy/auctionBook.d.ts +122 -0
- package/dist/economy/auctionBook.js +213 -0
- package/dist/economy/priceHistory.d.ts +44 -0
- package/dist/economy/priceHistory.js +89 -0
- package/dist/economy/unlockPoints.d.ts +92 -0
- package/dist/economy/unlockPoints.js +103 -0
- package/dist/editor/commandApply.d.ts +5 -0
- package/dist/editor/commandApply.js +755 -0
- package/dist/editor/commands.d.ts +48 -3
- package/dist/editor/commands.js +40 -754
- package/dist/editor/directives.d.ts +94 -0
- package/dist/editor/directives.js +206 -0
- package/dist/editor/document.d.ts +37 -2
- package/dist/editor/document.js +422 -6
- package/dist/editor/entityCatalog.d.ts +42 -0
- package/dist/editor/entityCatalog.js +101 -0
- package/dist/editor/environment.d.ts +74 -0
- package/dist/editor/environment.js +169 -0
- package/dist/editor/index.d.ts +26 -1
- package/dist/editor/index.js +8 -1
- package/dist/editor/minimap.d.ts +31 -0
- package/dist/editor/minimap.js +73 -0
- package/dist/editor/streamer.d.ts +66 -0
- package/dist/editor/streamer.js +68 -0
- package/dist/editor/types.d.ts +143 -0
- package/dist/editor/world.d.ts +136 -0
- package/dist/editor/world.js +303 -0
- package/dist/game/breeding.d.ts +161 -0
- package/dist/game/breeding.js +181 -0
- package/dist/game/cameraConfig.d.ts +451 -0
- package/dist/game/cameraConfig.js +1 -0
- package/dist/game/defineGame.d.ts +16 -9
- package/dist/game/defineGame.js +14 -6
- package/dist/game/defineSystem.d.ts +2 -0
- package/dist/game/defineSystem.js +2 -0
- package/dist/game/events.d.ts +28 -0
- package/dist/game/playableGame.d.ts +34 -435
- package/dist/game/playableGame.js +1 -1
- package/dist/game/progression.d.ts +35 -0
- package/dist/game/progression.js +19 -0
- package/dist/game/quest.js +1 -3
- package/dist/game/tribe.d.ts +184 -0
- package/dist/game/tribe.js +432 -0
- package/dist/gameplay.d.ts +3 -1
- package/dist/gameplay.js +3 -1
- package/dist/inventory/encumbrance.d.ts +69 -0
- package/dist/inventory/encumbrance.js +68 -0
- package/dist/item/durability.js +1 -3
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +24 -1
- package/dist/movement/movementModel.d.ts +15 -1
- package/dist/movement/movementModel.js +33 -15
- package/dist/movement/playerMovement.js +102 -2
- package/dist/multiplayer/combatSnapshot.js +13 -8
- package/dist/multiplayer/presenceModel.d.ts +2 -0
- package/dist/multiplayer/presenceModel.js +8 -0
- package/dist/physics/drivableVehicle.d.ts +2 -0
- package/dist/physics/drivableVehicle.js +1 -1
- package/dist/physics/flightDynamics.d.ts +82 -0
- package/dist/physics/flightDynamics.js +165 -0
- package/dist/physics/gravityField.d.ts +25 -0
- package/dist/physics/gravityField.js +41 -0
- package/dist/physics/kinematicVehicle.d.ts +87 -0
- package/dist/physics/kinematicVehicle.js +193 -11
- package/dist/physics/physicsWorld.d.ts +26 -0
- package/dist/physics/physicsWorld.js +109 -117
- package/dist/physics/vehicleObstacles.d.ts +44 -0
- package/dist/physics/vehicleObstacles.js +115 -0
- package/dist/random/rng.d.ts +4 -1
- package/dist/random/rng.js +4 -1
- package/dist/runtime/adapter.d.ts +13 -0
- package/dist/runtime/adapter.js +13 -0
- package/dist/runtime/context/combat.d.ts +51 -0
- package/dist/runtime/context/combat.js +130 -0
- package/dist/runtime/context/deathLoot.d.ts +30 -0
- package/dist/runtime/context/deathLoot.js +39 -0
- package/dist/runtime/context/player.d.ts +48 -0
- package/dist/runtime/context/player.js +125 -0
- package/dist/runtime/context/scene.d.ts +59 -0
- package/dist/runtime/context/scene.js +291 -0
- package/dist/runtime/context/snapshotCodecs.d.ts +12 -0
- package/dist/runtime/context/snapshotCodecs.js +26 -0
- package/dist/runtime/descriptors/baseline.js +26 -3
- package/dist/runtime/descriptors/features.d.ts +5 -0
- package/dist/runtime/descriptors/features.js +20 -2
- package/dist/runtime/gameContext.d.ts +2 -428
- package/dist/runtime/gameContext.js +88 -501
- package/dist/runtime/gameContextTypes.d.ts +467 -0
- package/dist/runtime/gameContextTypes.js +1 -0
- package/dist/runtime/headlessRunner.d.ts +4 -2
- package/dist/runtime/headlessRunner.js +1 -0
- package/dist/runtime/hostPolicy.d.ts +51 -0
- package/dist/runtime/hostPolicy.js +68 -0
- package/dist/runtime/hostedGameRunner.d.ts +3 -1
- package/dist/runtime/hostedGameRunner.js +1 -0
- package/dist/runtime/hostedWorldSession.d.ts +3 -1
- package/dist/runtime/hostedWorldSession.js +1 -0
- package/dist/runtime/worldSnapshot.d.ts +13 -1
- package/dist/runtime/worldSnapshot.js +15 -3
- package/dist/scene/assetCatalog.d.ts +5 -0
- package/dist/scene/assetSpace.js +1 -3
- package/dist/scene/authoredTriggers.d.ts +50 -0
- package/dist/scene/authoredTriggers.js +114 -0
- package/dist/scene/builtinSceneKinds.js +8 -3
- package/dist/scene/colliders.d.ts +58 -0
- package/dist/scene/colliders.js +108 -0
- package/dist/scene/collisionMesh.d.ts +94 -0
- package/dist/scene/collisionMesh.js +699 -0
- package/dist/scene/companion.d.ts +147 -0
- package/dist/scene/companion.js +193 -0
- package/dist/scene/entityStats.d.ts +7 -9
- package/dist/scene/entityStats.js +10 -14
- package/dist/scene/sceneKinds.d.ts +16 -0
- package/dist/scene/sceneKinds.js +4 -0
- package/dist/scene/sceneRaycast.js +15 -0
- package/dist/stats/spawnLevelStats.d.ts +110 -0
- package/dist/stats/spawnLevelStats.js +140 -0
- package/dist/stats/statPool.d.ts +70 -0
- package/dist/stats/statPool.js +59 -0
- package/dist/store/changeSignal.d.ts +3 -0
- package/dist/store/changeSignal.js +2 -0
- package/dist/store/observableKeyedStore.d.ts +2 -0
- package/dist/store/observableKeyedStore.js +1 -0
- package/dist/survival/decayMeter.js +1 -3
- package/dist/survival/regionHealth.js +1 -3
- package/dist/ui/hudLayout.d.ts +5 -0
- package/dist/ui/hudLayout.js +17 -1
- package/dist/world/authoredEntities.d.ts +29 -0
- package/dist/world/authoredEntities.js +29 -0
- package/dist/world/authoredObjects.d.ts +15 -1
- package/dist/world/authoredObjects.js +13 -1
- package/dist/world/authoredSpawn.d.ts +39 -0
- package/dist/world/authoredSpawn.js +37 -0
- package/dist/world/buildSockets.d.ts +152 -0
- package/dist/world/buildSockets.js +188 -0
- package/dist/world/cityBlocks.d.ts +152 -0
- package/dist/world/cityBlocks.js +670 -0
- package/dist/world/cityContent.d.ts +77 -0
- package/dist/world/cityContent.js +261 -0
- package/dist/world/cityGeometry.d.ts +114 -0
- package/dist/world/cityGeometry.js +573 -0
- package/dist/world/cityKind.d.ts +314 -0
- package/dist/world/cityKind.js +1578 -0
- package/dist/world/features.d.ts +18 -3
- package/dist/world/features.js +10 -1
- package/dist/world/grassKind.js +2 -2
- package/dist/world/markers.d.ts +50 -4
- package/dist/world/markers.js +49 -0
- package/dist/world/massing.js +1 -1
- package/dist/world/minimapBake.d.ts +78 -0
- package/dist/world/minimapBake.js +208 -0
- package/dist/world/minimapTrack.d.ts +17 -0
- package/dist/world/minimapTrack.js +22 -0
- package/dist/world/pathNetwork.d.ts +159 -0
- package/dist/world/pathNetwork.js +684 -0
- package/dist/world/place.d.ts +166 -0
- package/dist/world/place.js +96 -0
- package/dist/world/placeAsset.js +3 -1
- package/dist/world/poleLineKind.d.ts +40 -0
- package/dist/world/poleLineKind.js +97 -0
- package/dist/world/polyline.js +9 -2
- package/dist/world/resourceNode.d.ts +109 -0
- package/dist/world/resourceNode.js +140 -0
- package/dist/world/scatter.js +2 -20
- package/dist/world/streets.d.ts +29 -0
- package/dist/world/streets.js +55 -0
- package/dist/world/terraform.js +58 -88
- package/dist/world/terrain.d.ts +45 -0
- package/dist/world/terrain.js +78 -0
- package/dist/world.d.ts +14 -5
- package/dist/world.js +12 -3
- package/package.json +1 -1
- package/skills/game-design/SKILL.md +28 -0
- package/skills/game-design/agents/openai.yaml +4 -0
- package/skills/game-design/references/application-playbook.md +170 -0
- package/skills/game-design/references/field-guide.md +288 -0
- package/skills/jgengine/SKILL.md +7 -4
- package/skills/jgengine/api.md +172 -79
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-combat/SKILL.md +5 -1
- package/skills/jgengine-combat/api.md +36 -0
- package/skills/jgengine-combat/capabilities.md +44 -0
- package/skills/jgengine-combat/recipes/portable-stat-pools.md +122 -0
- package/skills/jgengine-combat/reference.md +12 -2
- package/skills/jgengine-gameplay/api.md +152 -26
- package/skills/jgengine-gameplay/capabilities.md +78 -2
- package/skills/jgengine-gameplay/reference-systems.md +1 -5
- package/skills/jgengine-world/SKILL.md +13 -0
- package/skills/jgengine-world/api.md +419 -15
- package/skills/jgengine-world/capabilities.md +108 -2
- package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
- package/skills/jgengine-world/reference.md +43 -4
- package/skills/jgengine/reference-cartridge.md +0 -72
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* City content synthesis: the zone/class/massing layer of the `city` studio. Where `cityKind`
|
|
3
|
+
* synthesizes the street NETWORK, this module decides what stands on it — which zone band a lot
|
|
4
|
+
* falls in (core/mid/edge, invertible), which building class the band's weighted mix picks
|
|
5
|
+
* (tower, slab, shop, rowhouse, house, mansion, farmhouse, barn, silo), and the deterministic
|
|
6
|
+
* massing PIECES each class composes (boxes, gable prisms, cylinders, domes with palette roles).
|
|
7
|
+
* Pure data + seeded math — the shell renderer just instances the piece shapes, so every visual
|
|
8
|
+
* variant is core-owned, testable, and identical per seed.
|
|
9
|
+
*
|
|
10
|
+
* @capability city-district zone bands, weighted building-class mixes, and per-class massing pieces
|
|
11
|
+
*/
|
|
12
|
+
import type { WeightedParamEntry } from "../scene/sceneKinds.js";
|
|
13
|
+
/** A building class a zone mix can weight — drives lot size, floors, setback, and massing. */
|
|
14
|
+
export type CityLotClass = "tower" | "slab" | "shop" | "rowhouse" | "house" | "mansion" | "farmhouse" | "barn" | "silo";
|
|
15
|
+
/** All classes, for schema hints and validation. */
|
|
16
|
+
export declare const CITY_LOT_CLASSES: readonly CityLotClass[];
|
|
17
|
+
/** Zone band a lot falls in: dense core, middle ring, or the district edge. */
|
|
18
|
+
export type CityZoneBand = "core" | "mid" | "edge";
|
|
19
|
+
/** How the radial zone metric maps to bands. */
|
|
20
|
+
export type CityZoneProfile = "core-out" | "inverted" | "uniform";
|
|
21
|
+
/** Primitive shapes massing pieces instance — the renderer keeps one InstancedMesh per shape. */
|
|
22
|
+
export type CityPieceShape = "box" | "gable" | "cylinder" | "dome";
|
|
23
|
+
/** Palette role a piece colors from (wall/roof/trim/accent map onto the district's style palette). */
|
|
24
|
+
export type CityPieceRole = "wall" | "roof" | "trim" | "accent";
|
|
25
|
+
/** One massing piece in LOT-LOCAL space: x along frontage width, z into the block, y up from grade. */
|
|
26
|
+
export interface CityLotPiece {
|
|
27
|
+
shape: CityPieceShape;
|
|
28
|
+
/** Piece-center offset from the lot center; y is the piece BASE height above grade. */
|
|
29
|
+
offset: readonly [number, number, number];
|
|
30
|
+
/** Full extents: width (x), height (y), depth (z). */
|
|
31
|
+
size: readonly [number, number, number];
|
|
32
|
+
/** Extra local yaw (crossing gables); 0 keeps the ridge along the lot width. */
|
|
33
|
+
rotationY: number;
|
|
34
|
+
role: CityPieceRole;
|
|
35
|
+
/** Grounded pieces grow a foundation down to the lowest terrain corner under the lot. */
|
|
36
|
+
grounded: boolean;
|
|
37
|
+
/** Banded walls receive the window-strip treatment in the renderer. */
|
|
38
|
+
banded: boolean;
|
|
39
|
+
}
|
|
40
|
+
/** Placement numbers a resolved lot carries out of the class profile. @internal */
|
|
41
|
+
export interface ClassPlacement {
|
|
42
|
+
width: number;
|
|
43
|
+
depth: number;
|
|
44
|
+
setback: number;
|
|
45
|
+
gap: number;
|
|
46
|
+
floors: number;
|
|
47
|
+
backRow: boolean;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Roll a class's lot dimensions/setback/floors from the district's placement dials. Setback keeps
|
|
51
|
+
* only a whisper of jitter around `setbackBase × class factor`, so a street's frontage reads as an
|
|
52
|
+
* aligned building wall; spacing likewise, so neighbors form rhythmic rows instead of scatter.
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
55
|
+
export declare function rollClassPlacement(cls: CityLotClass, rng: () => number, lotScale: number, floorsMin: number, floorsMax: number, setbackBase: number, spacingBase: number): ClassPlacement;
|
|
56
|
+
/**
|
|
57
|
+
* Normalized zone metric for a point in district-local space: 0 at the center line, 1 at the rim,
|
|
58
|
+
* shaped to the district's rectangle (Chebyshev) so bands follow the volume's footprint.
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
61
|
+
export declare function zoneMetric(x: number, z: number, hx: number, hz: number): number;
|
|
62
|
+
/** Map a zone metric to a band under a profile; `uniform` rolls a band from the extents instead. @internal */
|
|
63
|
+
export declare function zoneBand(metric: number, profile: CityZoneProfile, coreExtent: number, midExtent: number, roll: number): CityZoneBand;
|
|
64
|
+
/** Pick a class from a weighted mix; unknown items are ignored, an empty mix falls back to `house`. @internal */
|
|
65
|
+
export declare function pickClass(mix: readonly WeightedParamEntry[], roll: number): CityLotClass;
|
|
66
|
+
/**
|
|
67
|
+
* Compose the massing pieces for one lot. Deterministic per `rng` stream: the same class, size,
|
|
68
|
+
* floors, and stream always yield the identical silhouette. Sizes are in meters, lot-local.
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
export declare function buildLotPieces(cls: CityLotClass, width: number, depth: number, floors: number, floorHeight: number, rng: () => number): CityLotPiece[];
|
|
72
|
+
/** Tree species a district's tree mix can weight — the renderer keeps one canopy mesh per species. */
|
|
73
|
+
export type CityTreeSpecies = "broadleaf" | "conifer" | "palm" | "cypress";
|
|
74
|
+
/** All species, for schema hints and validation. */
|
|
75
|
+
export declare const CITY_TREE_SPECIES: readonly CityTreeSpecies[];
|
|
76
|
+
/** Pick a species from a weighted mix; unknown items are ignored, an empty mix falls back to broadleaf. @internal */
|
|
77
|
+
export declare function pickSpecies(mix: readonly WeightedParamEntry[], roll: number): CityTreeSpecies;
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/** All classes, for schema hints and validation. */
|
|
2
|
+
export const CITY_LOT_CLASSES = [
|
|
3
|
+
"tower",
|
|
4
|
+
"slab",
|
|
5
|
+
"shop",
|
|
6
|
+
"rowhouse",
|
|
7
|
+
"house",
|
|
8
|
+
"mansion",
|
|
9
|
+
"farmhouse",
|
|
10
|
+
"barn",
|
|
11
|
+
"silo",
|
|
12
|
+
];
|
|
13
|
+
const CLASS_PROFILES = {
|
|
14
|
+
tower: { width: [16, 24], depth: [14, 20], setbackFactor: 0.3, spacingFactor: 1, floors: [12, 34], backRow: true },
|
|
15
|
+
slab: { width: [14, 20], depth: [11, 15], setbackFactor: 0.5, spacingFactor: 1, floors: [4, 9], backRow: true },
|
|
16
|
+
shop: { width: [10, 15], depth: [8, 12], setbackFactor: 0.2, spacingFactor: 0.6, floors: [1, 2], backRow: true },
|
|
17
|
+
rowhouse: { width: [6, 8.5], depth: [9, 12], setbackFactor: 0.5, spacingFactor: 0.12, floors: [2, 3], backRow: true },
|
|
18
|
+
house: { width: [9, 12], depth: [8, 11], setbackFactor: 1, spacingFactor: 1, floors: [1, 2], backRow: true },
|
|
19
|
+
mansion: { width: [18, 26], depth: [14, 20], setbackFactor: 2, spacingFactor: 1.6, floors: [2, 3], backRow: false },
|
|
20
|
+
farmhouse: { width: [11, 14], depth: [9, 12], setbackFactor: 2.8, spacingFactor: 4, floors: [2, 2], backRow: false },
|
|
21
|
+
barn: { width: [12, 16], depth: [9, 13], setbackFactor: 3.6, spacingFactor: 5, floors: [1, 1], backRow: false },
|
|
22
|
+
silo: { width: [5, 6], depth: [5, 6], setbackFactor: 4, spacingFactor: 3, floors: [1, 1], backRow: false },
|
|
23
|
+
};
|
|
24
|
+
function range(rng, [lo, hi]) {
|
|
25
|
+
return lo + rng() * (hi - lo);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Roll a class's lot dimensions/setback/floors from the district's placement dials. Setback keeps
|
|
29
|
+
* only a whisper of jitter around `setbackBase × class factor`, so a street's frontage reads as an
|
|
30
|
+
* aligned building wall; spacing likewise, so neighbors form rhythmic rows instead of scatter.
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
export function rollClassPlacement(cls, rng, lotScale, floorsMin, floorsMax, setbackBase, spacingBase) {
|
|
34
|
+
const profile = CLASS_PROFILES[cls];
|
|
35
|
+
const floors = Math.round(range(rng, profile.floors));
|
|
36
|
+
return {
|
|
37
|
+
width: range(rng, profile.width) * lotScale,
|
|
38
|
+
depth: range(rng, profile.depth) * lotScale,
|
|
39
|
+
setback: setbackBase * profile.setbackFactor * (0.9 + rng() * 0.2),
|
|
40
|
+
gap: Math.max(0.2, spacingBase * profile.spacingFactor * (0.7 + rng() * 0.6)),
|
|
41
|
+
floors: Math.max(floorsMin, Math.min(floorsMax, floors)),
|
|
42
|
+
backRow: profile.backRow,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Normalized zone metric for a point in district-local space: 0 at the center line, 1 at the rim,
|
|
47
|
+
* shaped to the district's rectangle (Chebyshev) so bands follow the volume's footprint.
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
export function zoneMetric(x, z, hx, hz) {
|
|
51
|
+
return Math.min(1, Math.max(Math.abs(x) / Math.max(1e-6, hx), Math.abs(z) / Math.max(1e-6, hz)));
|
|
52
|
+
}
|
|
53
|
+
/** Map a zone metric to a band under a profile; `uniform` rolls a band from the extents instead. @internal */
|
|
54
|
+
export function zoneBand(metric, profile, coreExtent, midExtent, roll) {
|
|
55
|
+
const mid = Math.max(coreExtent, midExtent);
|
|
56
|
+
if (profile === "uniform") {
|
|
57
|
+
const coreW = Math.max(0.001, coreExtent);
|
|
58
|
+
const midW = Math.max(0.001, mid - coreExtent);
|
|
59
|
+
const edgeW = Math.max(0.001, 1 - mid);
|
|
60
|
+
const total = coreW + midW + edgeW;
|
|
61
|
+
const pick = roll * total;
|
|
62
|
+
return pick < coreW ? "core" : pick < coreW + midW ? "mid" : "edge";
|
|
63
|
+
}
|
|
64
|
+
const t = profile === "inverted" ? 1 - metric : metric;
|
|
65
|
+
return t < coreExtent ? "core" : t < mid ? "mid" : "edge";
|
|
66
|
+
}
|
|
67
|
+
const CLASS_SET = new Set(CITY_LOT_CLASSES);
|
|
68
|
+
/** Pick a class from a weighted mix; unknown items are ignored, an empty mix falls back to `house`. @internal */
|
|
69
|
+
export function pickClass(mix, roll) {
|
|
70
|
+
let total = 0;
|
|
71
|
+
for (const entry of mix)
|
|
72
|
+
if (CLASS_SET.has(entry.item) && entry.weight > 0)
|
|
73
|
+
total += entry.weight;
|
|
74
|
+
if (total <= 0)
|
|
75
|
+
return "house";
|
|
76
|
+
let cursor = roll * total;
|
|
77
|
+
for (const entry of mix) {
|
|
78
|
+
if (!CLASS_SET.has(entry.item) || entry.weight <= 0)
|
|
79
|
+
continue;
|
|
80
|
+
cursor -= entry.weight;
|
|
81
|
+
if (cursor <= 0)
|
|
82
|
+
return entry.item;
|
|
83
|
+
}
|
|
84
|
+
return "house";
|
|
85
|
+
}
|
|
86
|
+
function box(offset, size, role, options = {}) {
|
|
87
|
+
return {
|
|
88
|
+
shape: "box",
|
|
89
|
+
offset,
|
|
90
|
+
size,
|
|
91
|
+
rotationY: options.rotationY ?? 0,
|
|
92
|
+
role,
|
|
93
|
+
grounded: options.grounded ?? false,
|
|
94
|
+
banded: options.banded ?? false,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function gable(offset, size, rotationY = 0) {
|
|
98
|
+
return { shape: "gable", offset, size, rotationY, role: "roof", grounded: false, banded: false };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Compose the massing pieces for one lot. Deterministic per `rng` stream: the same class, size,
|
|
102
|
+
* floors, and stream always yield the identical silhouette. Sizes are in meters, lot-local.
|
|
103
|
+
* @internal
|
|
104
|
+
*/
|
|
105
|
+
export function buildLotPieces(cls, width, depth, floors, floorHeight, rng) {
|
|
106
|
+
const H = Math.max(1, floors) * floorHeight;
|
|
107
|
+
const pieces = [];
|
|
108
|
+
switch (cls) {
|
|
109
|
+
case "tower": {
|
|
110
|
+
if (floors >= 14) {
|
|
111
|
+
// Wedding-cake tiers: full-footprint shaft, then two setback tiers, parapet, maybe a spire.
|
|
112
|
+
const h1 = H * (0.5 + rng() * 0.12);
|
|
113
|
+
const h2 = H * (0.24 + rng() * 0.08);
|
|
114
|
+
const h3 = H - h1 - h2;
|
|
115
|
+
pieces.push(box([0, 0, 0], [width, h1, depth], "wall", { grounded: true, banded: true }));
|
|
116
|
+
const w2 = width * (0.78 + rng() * 0.08);
|
|
117
|
+
const d2 = depth * (0.78 + rng() * 0.08);
|
|
118
|
+
pieces.push(box([0, h1, 0], [w2, h2, d2], "wall", { banded: true }));
|
|
119
|
+
const w3 = width * (0.55 + rng() * 0.1);
|
|
120
|
+
const d3 = depth * (0.55 + rng() * 0.1);
|
|
121
|
+
pieces.push(box([0, h1 + h2, 0], [w3, h3, d3], "wall", { banded: true }));
|
|
122
|
+
pieces.push(box([0, H, 0], [w3 * 1.04, 0.5, d3 * 1.04], "trim"));
|
|
123
|
+
if (rng() < 0.35)
|
|
124
|
+
pieces.push(box([0, H + 0.5, 0], [1.2, 3 + rng() * 5, 1.2], "trim"));
|
|
125
|
+
else if (rng() < 0.5)
|
|
126
|
+
pieces.push(box([0, H + 0.5, 0], [w3 * 0.4, 1.6, d3 * 0.4], "roof"));
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
const h1 = H * (0.66 + rng() * 0.1);
|
|
130
|
+
pieces.push(box([0, 0, 0], [width, h1, depth], "wall", { grounded: true, banded: true }));
|
|
131
|
+
const w2 = width * (0.72 + rng() * 0.1);
|
|
132
|
+
const d2 = depth * (0.72 + rng() * 0.1);
|
|
133
|
+
pieces.push(box([0, h1, 0], [w2, H - h1, d2], "wall", { banded: true }));
|
|
134
|
+
pieces.push(box([0, H, 0], [w2 * 1.05, 0.45, d2 * 1.05], "trim"));
|
|
135
|
+
}
|
|
136
|
+
return pieces;
|
|
137
|
+
}
|
|
138
|
+
case "slab": {
|
|
139
|
+
pieces.push(box([0, 0, 0], [width, H, depth * 0.95], "wall", { grounded: true, banded: true }));
|
|
140
|
+
pieces.push(box([0, H, 0], [width * 1.02, 0.35, depth * 0.97], "trim"));
|
|
141
|
+
if (rng() < 0.6)
|
|
142
|
+
pieces.push(box([(rng() - 0.5) * width * 0.4, H + 0.35, 0], [width * 0.22, 2, depth * 0.3], "roof"));
|
|
143
|
+
return pieces;
|
|
144
|
+
}
|
|
145
|
+
case "shop": {
|
|
146
|
+
pieces.push(box([0, 0, 0], [width, H, depth], "wall", { grounded: true }));
|
|
147
|
+
pieces.push(box([0, H, 0], [width * 1.03, 0.5, depth * 1.03], "trim"));
|
|
148
|
+
// Front awning strip over the sidewalk face.
|
|
149
|
+
pieces.push(box([0, Math.min(H - 0.4, 2.6), depth / 2 + 0.35], [width * 0.9, 0.35, 0.9], "accent"));
|
|
150
|
+
return pieces;
|
|
151
|
+
}
|
|
152
|
+
case "rowhouse": {
|
|
153
|
+
pieces.push(box([0, 0, 0], [width * 0.96, H, depth * 0.85], "wall", { grounded: true, banded: true }));
|
|
154
|
+
if (rng() < 0.55) {
|
|
155
|
+
pieces.push(gable([0, H, 0], [width * 0.96, 1.6 + rng() * 0.8, depth * 0.9]));
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
pieces.push(box([0, H, 0], [width * 1.0, 0.45, depth * 0.9], "trim"));
|
|
159
|
+
}
|
|
160
|
+
return pieces;
|
|
161
|
+
}
|
|
162
|
+
case "house": {
|
|
163
|
+
const bw = width * (0.68 + rng() * 0.1);
|
|
164
|
+
const bd = depth * (0.58 + rng() * 0.1);
|
|
165
|
+
pieces.push(box([0, 0, 0], [bw, H, bd], "wall", { grounded: true }));
|
|
166
|
+
const along = rng() < 0.5;
|
|
167
|
+
pieces.push(gable([0, H, 0], along ? [bw * 1.08, 1.9 + rng() * 1.1, bd * 1.12] : [bd * 1.12, 1.9 + rng() * 1.1, bw * 1.08], along ? 0 : Math.PI / 2));
|
|
168
|
+
if (rng() < 0.45)
|
|
169
|
+
pieces.push(box([bw * (0.2 + rng() * 0.12), H + 1.2, bd * 0.1], [0.7, 1.6, 0.7], "trim"));
|
|
170
|
+
if (rng() < 0.55)
|
|
171
|
+
pieces.push(box([0, 0, bd / 2 + 0.8], [bw * 0.55, 0.25, 1.6], "trim", { grounded: true }));
|
|
172
|
+
if (rng() < 0.4) {
|
|
173
|
+
// Attached garage with its own low gable.
|
|
174
|
+
const gw = bw * 0.5;
|
|
175
|
+
const gx = (bw / 2 + gw / 2) * (rng() < 0.5 ? 1 : -1);
|
|
176
|
+
pieces.push(box([gx, 0, bd * 0.05], [gw, H * 0.6, bd * 0.7], "wall", { grounded: true }));
|
|
177
|
+
pieces.push(gable([gx, H * 0.6, bd * 0.05], [gw * 1.06, 1.1, bd * 0.75]));
|
|
178
|
+
}
|
|
179
|
+
return pieces;
|
|
180
|
+
}
|
|
181
|
+
case "mansion": {
|
|
182
|
+
const cw = width * (0.42 + rng() * 0.08);
|
|
183
|
+
const cd = depth * (0.5 + rng() * 0.08);
|
|
184
|
+
pieces.push(box([0, 0, 0], [cw, H, cd], "wall", { grounded: true }));
|
|
185
|
+
pieces.push(gable([0, H, 0], [cw * 1.1, 2.4 + rng() * 1, cd * 1.12]));
|
|
186
|
+
for (const side of [-1, 1]) {
|
|
187
|
+
const ww = width * (0.24 + rng() * 0.05);
|
|
188
|
+
const wh = H * (0.72 + rng() * 0.15);
|
|
189
|
+
const wd = cd * (0.75 + rng() * 0.15);
|
|
190
|
+
const wx = side * (cw / 2 + ww / 2 - 0.3);
|
|
191
|
+
pieces.push(box([wx, 0, 0], [ww, wh, wd], "wall", { grounded: true }));
|
|
192
|
+
pieces.push(gable([wx, wh, 0], [wd * 1.1, 1.6 + rng() * 0.7, ww * 1.08], Math.PI / 2));
|
|
193
|
+
}
|
|
194
|
+
// Portico: a slab-and-posts entrance strip on the street face.
|
|
195
|
+
pieces.push(box([0, 0, cd / 2 + 0.9], [cw * 0.4, 0.3, 1.8], "trim", { grounded: true }));
|
|
196
|
+
pieces.push(box([0, H * 0.55, cd / 2 + 0.9], [cw * 0.42, 0.35, 1.9], "trim"));
|
|
197
|
+
return pieces;
|
|
198
|
+
}
|
|
199
|
+
case "farmhouse": {
|
|
200
|
+
const bw = width * 0.8;
|
|
201
|
+
const bd = depth * 0.62;
|
|
202
|
+
pieces.push(box([0, 0, 0], [bw, H, bd], "wall", { grounded: true }));
|
|
203
|
+
pieces.push(gable([0, H, 0], [bw * 1.08, 2.2 + rng() * 0.8, bd * 1.12]));
|
|
204
|
+
// Wrap-around porch slab + roof lip on the street face.
|
|
205
|
+
pieces.push(box([0, 0, bd / 2 + 1], [bw * 0.9, 0.25, 2], "trim", { grounded: true }));
|
|
206
|
+
pieces.push(box([0, H * 0.45, bd / 2 + 1], [bw * 0.92, 0.28, 2.1], "roof"));
|
|
207
|
+
if (rng() < 0.5)
|
|
208
|
+
pieces.push(box([bw * 0.25, H + 1.4, 0], [0.7, 1.7, 0.7], "trim"));
|
|
209
|
+
return pieces;
|
|
210
|
+
}
|
|
211
|
+
case "barn": {
|
|
212
|
+
const bw = width * 0.85;
|
|
213
|
+
const bd = depth * 0.78;
|
|
214
|
+
const wallH = 3 + rng() * 0.8;
|
|
215
|
+
// Gambrel silhouette: squat walls, a steep lower gable, then a shallower upper gable.
|
|
216
|
+
pieces.push(box([0, 0, 0], [bw, wallH, bd], "accent", { grounded: true }));
|
|
217
|
+
pieces.push(gable([0, wallH, 0], [bw * 1.04, 1.7, bd * 1.06]));
|
|
218
|
+
pieces.push(gable([0, wallH + 1.55, 0], [bw * 0.62, 1.3, bd * 1.06]));
|
|
219
|
+
if (rng() < 0.6) {
|
|
220
|
+
// Adjacent silo on a corner of the same parcel.
|
|
221
|
+
const sx = (bw / 2 + 2.2) * (rng() < 0.5 ? 1 : -1);
|
|
222
|
+
const sh = 7 + rng() * 3;
|
|
223
|
+
pieces.push({ shape: "cylinder", offset: [sx, 0, -bd * 0.2], size: [4, sh, 4], rotationY: 0, role: "trim", grounded: true, banded: false });
|
|
224
|
+
pieces.push({ shape: "dome", offset: [sx, sh, -bd * 0.2], size: [4, 1.6, 4], rotationY: 0, role: "roof", grounded: false, banded: false });
|
|
225
|
+
}
|
|
226
|
+
return pieces;
|
|
227
|
+
}
|
|
228
|
+
case "silo": {
|
|
229
|
+
const sh = 8 + rng() * 4;
|
|
230
|
+
pieces.push({ shape: "cylinder", offset: [0, 0, 0], size: [width * 0.8, sh, width * 0.8], rotationY: 0, role: "trim", grounded: true, banded: false });
|
|
231
|
+
pieces.push({ shape: "dome", offset: [0, sh, 0], size: [width * 0.8, 1.8, width * 0.8], rotationY: 0, role: "roof", grounded: false, banded: false });
|
|
232
|
+
if (rng() < 0.5) {
|
|
233
|
+
const sw = width * 0.55;
|
|
234
|
+
pieces.push({ shape: "cylinder", offset: [width * 0.75, 0, 0], size: [sw, sh * 0.75, sw], rotationY: 0, role: "trim", grounded: true, banded: false });
|
|
235
|
+
pieces.push({ shape: "dome", offset: [width * 0.75, sh * 0.75, 0], size: [sw, 1.3, sw], rotationY: 0, role: "roof", grounded: false, banded: false });
|
|
236
|
+
}
|
|
237
|
+
return pieces;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** All species, for schema hints and validation. */
|
|
242
|
+
export const CITY_TREE_SPECIES = ["broadleaf", "conifer", "palm", "cypress"];
|
|
243
|
+
const SPECIES_SET = new Set(CITY_TREE_SPECIES);
|
|
244
|
+
/** Pick a species from a weighted mix; unknown items are ignored, an empty mix falls back to broadleaf. @internal */
|
|
245
|
+
export function pickSpecies(mix, roll) {
|
|
246
|
+
let total = 0;
|
|
247
|
+
for (const entry of mix)
|
|
248
|
+
if (SPECIES_SET.has(entry.item) && entry.weight > 0)
|
|
249
|
+
total += entry.weight;
|
|
250
|
+
if (total <= 0)
|
|
251
|
+
return "broadleaf";
|
|
252
|
+
let cursor = roll * total;
|
|
253
|
+
for (const entry of mix) {
|
|
254
|
+
if (!SPECIES_SET.has(entry.item) || entry.weight <= 0)
|
|
255
|
+
continue;
|
|
256
|
+
cursor -= entry.weight;
|
|
257
|
+
if (cursor <= 0)
|
|
258
|
+
return entry.item;
|
|
259
|
+
}
|
|
260
|
+
return "broadleaf";
|
|
261
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic 2D polygon math for the `city` studio's block/parcel pipeline: signed areas,
|
|
3
|
+
* point-in-polygon, half-plane clipping, per-edge inward insets (the curb → sidewalk → land and
|
|
4
|
+
* parcel → buildable transforms), simple-loop recovery after aggressive insets, and rotated-rect
|
|
5
|
+
* fitting inside arbitrary polygons. Pure functions over `[x, z]` tuples — no allocation-heavy
|
|
6
|
+
* classes, no rendering, no randomness — so every consumer from the resolver to the tests shares
|
|
7
|
+
* one geometric truth.
|
|
8
|
+
*
|
|
9
|
+
* @capability city-district polygon math for road-derived blocks, parcels, and buildable footprints
|
|
10
|
+
*/
|
|
11
|
+
/** A 2D point in district-local or world XZ space. */
|
|
12
|
+
export type Vec2 = readonly [number, number];
|
|
13
|
+
/** Signed area of a polygon ring (positive = counter-clockwise in XZ). */
|
|
14
|
+
export declare function polygonSignedArea(ring: readonly Vec2[]): number;
|
|
15
|
+
/** Absolute polygon area. */
|
|
16
|
+
export declare function polygonArea(ring: readonly Vec2[]): number;
|
|
17
|
+
/** Ring perimeter length. */
|
|
18
|
+
export declare function polygonPerimeter(ring: readonly Vec2[]): number;
|
|
19
|
+
/** Area centroid of a simple polygon (falls back to the vertex mean for degenerate rings). */
|
|
20
|
+
export declare function polygonCentroid(ring: readonly Vec2[]): Vec2;
|
|
21
|
+
/** Ensure counter-clockwise winding (positive signed area) without mutating the input. */
|
|
22
|
+
export declare function ensureCcw(ring: readonly Vec2[]): Vec2[];
|
|
23
|
+
/** Even-odd point-in-polygon test. */
|
|
24
|
+
export declare function pointInPolygon(ring: readonly Vec2[], x: number, z: number): boolean;
|
|
25
|
+
/** Distance from a point to the nearest boundary segment of a ring. */
|
|
26
|
+
export declare function distanceToRing(ring: readonly Vec2[], x: number, z: number): number;
|
|
27
|
+
/**
|
|
28
|
+
* Clip a polygon to the half-plane `dot(p, normal) <= limit` (Sutherland–Hodgman step). Returns
|
|
29
|
+
* the surviving ring, possibly empty. The subject may be concave; the output of a single
|
|
30
|
+
* half-plane clip of a simple ring is always a valid (possibly pinched) ring.
|
|
31
|
+
*/
|
|
32
|
+
export declare function clipHalfPlane(ring: readonly Vec2[], normal: Vec2, limit: number): Vec2[];
|
|
33
|
+
/** Split a polygon by the line `dot(p, normal) = limit` into the `<=` and `>=` sides. */
|
|
34
|
+
export declare function splitByLine(ring: readonly Vec2[], normal: Vec2, limit: number): {
|
|
35
|
+
below: Vec2[];
|
|
36
|
+
above: Vec2[];
|
|
37
|
+
};
|
|
38
|
+
/** Remove consecutive (near-)duplicate vertices; returns [] when fewer than 3 survive. */
|
|
39
|
+
export declare function dedupeRing(ring: readonly Vec2[], epsilon?: number): Vec2[];
|
|
40
|
+
/** True when any two non-adjacent edges of the ring properly cross. O(n²), fine for block-scale n. */
|
|
41
|
+
export declare function ringSelfIntersects(ring: readonly Vec2[]): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Recover the dominant simple loop from a lightly self-intersecting ring: whenever two edges
|
|
44
|
+
* cross, the smaller of the two loops the crossing pinches off is dropped. Bounded passes; rings
|
|
45
|
+
* that stay tangled after that return []. This is the cleanup net under aggressive insets — a
|
|
46
|
+
* concave block inset past a neck pinches into a bow-tie, and the bigger lobe is the block.
|
|
47
|
+
*/
|
|
48
|
+
export declare function extractSimpleLoop(ring: readonly Vec2[], maxPasses?: number): Vec2[];
|
|
49
|
+
/**
|
|
50
|
+
* Inset a CCW ring inward with a PER-EDGE distance (edge i runs vertex i → i+1). True corners take
|
|
51
|
+
* the intersection of the two adjacent offset lines (so a street corner lands exactly where the
|
|
52
|
+
* two curb lines meet, even with different street widths); near-collinear vertices — polyline
|
|
53
|
+
* samples along a curve — fall back to the averaged-normal offset, which is exact in the limit.
|
|
54
|
+
* Returns [] when the inset collapses (sliver) or stays tangled after loop recovery.
|
|
55
|
+
*/
|
|
56
|
+
export declare function insetRing(ring: readonly Vec2[], edgeDistances: readonly number[]): Vec2[];
|
|
57
|
+
/** Uniform-distance convenience over {@link insetRing}. */
|
|
58
|
+
export declare function insetRingUniform(ring: readonly Vec2[], distance: number): Vec2[];
|
|
59
|
+
/**
|
|
60
|
+
* Mean "width" proxy for sliver detection: the diameter of the largest inscribed strip is
|
|
61
|
+
* approximated by 2·area / perimeter (exact for long rectangles, conservative elsewhere).
|
|
62
|
+
*/
|
|
63
|
+
export declare function polygonMeanWidth(ring: readonly Vec2[]): number;
|
|
64
|
+
/** All four corners of a rotated rect (center, half-extents, yaw in engine rotationY convention). */
|
|
65
|
+
export declare function rectCorners(cx: number, cz: number, hw: number, hd: number, rotationY: number): Vec2[];
|
|
66
|
+
/** True when the whole rotated rect (corners + edge midpoints) sits inside the ring. */
|
|
67
|
+
export declare function rectInsidePolygon(ring: readonly Vec2[], cx: number, cz: number, hw: number, hd: number, rotationY: number): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Fit the largest rect (up to `maxW`×`maxD`, oriented to `rotationY`, centered near `[cx, cz]`)
|
|
70
|
+
* inside a polygon by bounded bisection on a single scale factor. Deterministic; returns null
|
|
71
|
+
* when even `minScale` of the request does not fit after nudging the center inward.
|
|
72
|
+
*/
|
|
73
|
+
export declare function fitRectInPolygon(ring: readonly Vec2[], cx: number, cz: number, maxW: number, maxD: number, rotationY: number, minScale?: number): {
|
|
74
|
+
w: number;
|
|
75
|
+
d: number;
|
|
76
|
+
cx: number;
|
|
77
|
+
cz: number;
|
|
78
|
+
} | null;
|
|
79
|
+
/**
|
|
80
|
+
* Distance from `origin` along `dir` to the first ring-boundary crossing (ignoring hits closer
|
|
81
|
+
* than `minT`, e.g. the edge the origin sits on). Infinity when the ray never leaves — degenerate.
|
|
82
|
+
*/
|
|
83
|
+
export declare function rayDistanceToRing(ring: readonly Vec2[], origin: Vec2, dir: Vec2, minT?: number): number;
|
|
84
|
+
/** Axis-aligned bounding box of a ring. */
|
|
85
|
+
export declare function ringBounds(ring: readonly Vec2[]): {
|
|
86
|
+
minX: number;
|
|
87
|
+
minZ: number;
|
|
88
|
+
maxX: number;
|
|
89
|
+
maxZ: number;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Ear-clip a simple polygon into triangles. Returns the cleaned/CCW-oriented vertex ring plus flat
|
|
93
|
+
* index triples into it (so a draping consumer can sample a height per returned vertex). O(n²) —
|
|
94
|
+
* fine for block/park rings. A polygon that stays un-clippable (self-touching after dedupe) yields
|
|
95
|
+
* whatever ears were found before the guard trips; degenerate input returns no triangles.
|
|
96
|
+
*/
|
|
97
|
+
export declare function triangulatePolygon(ring: readonly Vec2[]): {
|
|
98
|
+
positions: Vec2[];
|
|
99
|
+
indices: number[];
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Triangulate the band between two nested rings (an outer ring with an inner hole, e.g. a curb ring
|
|
103
|
+
* around a land ring — the sidewalk). Pairs the loops by cumulative arc-length fraction, aligning
|
|
104
|
+
* the inner start to the outer's first vertex, so it is robust to differing vertex counts and to a
|
|
105
|
+
* rotational offset between the rings. Returns the merged vertex list (outer, then the rotated
|
|
106
|
+
* inner) and index triples into it. Zero-area triangles — where the two rings coincide (a street
|
|
107
|
+
* edge with no sidewalk) — are dropped so `computeVertexNormals` never sees a degenerate face.
|
|
108
|
+
*/
|
|
109
|
+
export declare function triangulateRingBand(outer: readonly Vec2[], inner: readonly Vec2[]): {
|
|
110
|
+
positions: Vec2[];
|
|
111
|
+
indices: number[];
|
|
112
|
+
};
|
|
113
|
+
/** Conservative polygon-overlap test: any vertex of one strictly inside the other, or any edges crossing. */
|
|
114
|
+
export declare function polygonsOverlap(a: readonly Vec2[], b: readonly Vec2[], shrink?: number): boolean;
|