@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,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data-driven modular build-piece snap system: a catalog of piece types, each declaring named
|
|
3
|
+
* sockets (local position + outward facing + a generic "kind"), plus a connection-rule table saying
|
|
4
|
+
* which socket kinds may mate. Given a placed piece and a candidate type it resolves every valid
|
|
5
|
+
* snap — the world transform that lands the candidate's socket exactly on the placed piece's socket,
|
|
6
|
+
* facing back into it. Genre-agnostic: foundation/wall/ceiling/pillar/edge snapping falls out of the
|
|
7
|
+
* catalog data, with no game-specific piece names baked in. Pure and serializable; composes with
|
|
8
|
+
* `world/footprintGrid` occupancy (via an injected `isFree` predicate — never imported) and with
|
|
9
|
+
* `scene/modelSockets` named attach points (via {@link pieceSocketsFromModel}).
|
|
10
|
+
*/
|
|
11
|
+
/** A local- or world-space position/offset, matching the `[x, y, z]` triple used across core. */
|
|
12
|
+
export type SocketVec3 = readonly [number, number, number];
|
|
13
|
+
/** One named socket on a piece: where it sits and which horizontal direction it faces (yaw about Y). */
|
|
14
|
+
export interface SocketDef {
|
|
15
|
+
/** Stable socket name, unique within its piece. */
|
|
16
|
+
name: string;
|
|
17
|
+
/** Generic connection kind — e.g. "top", "bottom", "edge", "side". Matched by the rule table. */
|
|
18
|
+
kind: string;
|
|
19
|
+
/** Local offset from the piece origin. */
|
|
20
|
+
position: SocketVec3;
|
|
21
|
+
/** Outward-facing yaw in radians (rotation about +Y), local to the piece. Default `0`. */
|
|
22
|
+
yaw?: number;
|
|
23
|
+
}
|
|
24
|
+
/** A rectangular footprint (world units) a piece occupies on the build grid — the occupancy bridge. */
|
|
25
|
+
export interface BuildFootprint {
|
|
26
|
+
w: number;
|
|
27
|
+
d: number;
|
|
28
|
+
}
|
|
29
|
+
/** A registered piece type: its named sockets and, optionally, the footprint it claims when placed. */
|
|
30
|
+
export interface BuildPieceDef {
|
|
31
|
+
/** Stable piece-type id, unique within the catalog. */
|
|
32
|
+
type: string;
|
|
33
|
+
sockets: readonly SocketDef[];
|
|
34
|
+
/** Grid footprint for occupancy checks; omit for pieces that don't reserve cells. */
|
|
35
|
+
footprint?: BuildFootprint;
|
|
36
|
+
}
|
|
37
|
+
/** Declares that two socket kinds may snap together; `mutual` (default `true`) also allows `b`→`a`. */
|
|
38
|
+
export interface ConnectionRule {
|
|
39
|
+
a: string;
|
|
40
|
+
b: string;
|
|
41
|
+
/** When `false` the rule is directional: an `a`-kind candidate socket may mate onto a `b`-kind placed socket, but not the reverse. */
|
|
42
|
+
mutual?: boolean;
|
|
43
|
+
}
|
|
44
|
+
/** World transform of a placed piece: position plus a yaw about +Y. */
|
|
45
|
+
export interface PieceTransform {
|
|
46
|
+
position: SocketVec3;
|
|
47
|
+
/** Rotation about +Y in radians. Default `0`. */
|
|
48
|
+
yaw?: number;
|
|
49
|
+
}
|
|
50
|
+
/** An already-placed piece the resolver snaps candidates against. */
|
|
51
|
+
export interface PlacedPiece {
|
|
52
|
+
type: string;
|
|
53
|
+
transform: PieceTransform;
|
|
54
|
+
}
|
|
55
|
+
/** A piece's socket resolved into world space. */
|
|
56
|
+
export interface PlacedSocket {
|
|
57
|
+
name: string;
|
|
58
|
+
kind: string;
|
|
59
|
+
/** World-space socket position. */
|
|
60
|
+
position: SocketVec3;
|
|
61
|
+
/** World-space outward facing yaw (radians). */
|
|
62
|
+
yaw: number;
|
|
63
|
+
}
|
|
64
|
+
/** One integer grid cell — structurally the `GridCell` of `world/footprintGrid` (no import needed). */
|
|
65
|
+
export interface BuildCell {
|
|
66
|
+
col: number;
|
|
67
|
+
row: number;
|
|
68
|
+
}
|
|
69
|
+
/** A resolved snap: which sockets mate and the world transform that lands the candidate onto the target. */
|
|
70
|
+
export interface SnapCandidate {
|
|
71
|
+
candidateType: string;
|
|
72
|
+
/** Name of the socket on the already-placed piece. */
|
|
73
|
+
targetSocket: string;
|
|
74
|
+
/** Name of the candidate's socket that mates onto it. */
|
|
75
|
+
candidateSocket: string;
|
|
76
|
+
/** Where to place the candidate so its socket aligns onto the target socket. */
|
|
77
|
+
transform: {
|
|
78
|
+
position: SocketVec3;
|
|
79
|
+
yaw: number;
|
|
80
|
+
};
|
|
81
|
+
/** Shared world position of the mated sockets. */
|
|
82
|
+
point: SocketVec3;
|
|
83
|
+
/** Distance from the resolve `cursor` to `point`, or `0` when no cursor was given. */
|
|
84
|
+
distance: number;
|
|
85
|
+
}
|
|
86
|
+
/** Options for {@link BuildSocketCatalog.resolveSnaps}. */
|
|
87
|
+
export interface ResolveSnapsOptions {
|
|
88
|
+
/** Cursor point; when given, candidates are ordered nearest-first by mate-point distance. */
|
|
89
|
+
cursor?: SocketVec3;
|
|
90
|
+
/**
|
|
91
|
+
* Occupancy predicate — return `false` to reject a snap whose target cells are taken. Only applied
|
|
92
|
+
* to candidate pieces that declare a `footprint`. Compose with `world/footprintGrid` via
|
|
93
|
+
* `(cell) => grid.isFree([cell])`; the catalog never imports the grid.
|
|
94
|
+
*/
|
|
95
|
+
isFree?: (cell: BuildCell) => boolean;
|
|
96
|
+
}
|
|
97
|
+
/** Serializable catalog config; also the shape returned by {@link BuildSocketCatalog.toJSON}. */
|
|
98
|
+
export interface BuildSocketCatalogConfig {
|
|
99
|
+
pieces: readonly BuildPieceDef[];
|
|
100
|
+
rules: readonly ConnectionRule[];
|
|
101
|
+
/** World units per grid cell for footprint occupancy. Default `1`. */
|
|
102
|
+
cellSize?: number;
|
|
103
|
+
}
|
|
104
|
+
/** Handle returned by {@link createBuildSocketCatalog}. */
|
|
105
|
+
export interface BuildSocketCatalog {
|
|
106
|
+
readonly cellSize: number;
|
|
107
|
+
has(type: string): boolean;
|
|
108
|
+
piece(type: string): BuildPieceDef | null;
|
|
109
|
+
/** Registered piece-type ids, in declaration order. */
|
|
110
|
+
types(): readonly string[];
|
|
111
|
+
/** True when a `candidateKind` socket may mate onto a `placedKind` socket. */
|
|
112
|
+
canConnect(candidateKind: string, placedKind: string): boolean;
|
|
113
|
+
/** Every socket of `placed` resolved into world space. */
|
|
114
|
+
worldSockets(placed: PlacedPiece): PlacedSocket[];
|
|
115
|
+
/** Every valid snap of `candidateType` onto `placed`, ordered deterministically. */
|
|
116
|
+
resolveSnaps(placed: PlacedPiece, candidateType: string, options?: ResolveSnapsOptions): SnapCandidate[];
|
|
117
|
+
/** Plain, serializable snapshot — feed straight back into {@link createBuildSocketCatalog}. */
|
|
118
|
+
toJSON(): BuildSocketCatalogConfig;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The grid cells a piece `footprint` centered on `position` (world units) covers, honoring `yaw`
|
|
122
|
+
* (snapped to the nearest quarter turn). Mirrors `world/footprintGrid`'s `cellsFor` so a game can
|
|
123
|
+
* feed the result straight into `grid.isFree`/`grid.reserve` after committing a resolved snap —
|
|
124
|
+
* without this module importing the grid.
|
|
125
|
+
*
|
|
126
|
+
* @capability build-footprint-cells grid cells a snapped build piece footprint covers (footprintGrid occupancy bridge)
|
|
127
|
+
*/
|
|
128
|
+
export declare function footprintCells(position: SocketVec3, footprint: BuildFootprint, yaw?: number, cellSize?: number): BuildCell[];
|
|
129
|
+
/** A `{ name, offset }` attach point — structurally the `ModelSocket` of `scene/modelSockets`. */
|
|
130
|
+
export interface ModelSocketLike {
|
|
131
|
+
name: string;
|
|
132
|
+
offset: SocketVec3;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Turn a model's named attach points (from `scene/modelSockets`' `readNamedSockets`) into
|
|
136
|
+
* {@link SocketDef}s, classifying each into a connection `kind` via `kindOf` and, optionally, an
|
|
137
|
+
* outward `yaw` via `yawOf`. Lets an artist author snap sockets as empties in a GLB instead of by
|
|
138
|
+
* hand in the catalog. Pure and structural — no three.js or scene import.
|
|
139
|
+
*
|
|
140
|
+
* @capability piece-sockets-from-model derive build-piece snap sockets from a model's named attach points
|
|
141
|
+
*/
|
|
142
|
+
export declare function pieceSocketsFromModel(sockets: readonly ModelSocketLike[], kindOf: (name: string) => string, yawOf?: (name: string) => number): SocketDef[];
|
|
143
|
+
/**
|
|
144
|
+
* Build a modular snap catalog from piece definitions and a connection-rule table. Resolves the set
|
|
145
|
+
* of valid snap transforms when hovering a candidate piece near a placed one: for every compatible
|
|
146
|
+
* socket pair it computes the world transform that seats the candidate's socket onto the placed
|
|
147
|
+
* socket, oriented to face back into it. Deterministic ordering (nearest a cursor when given, else
|
|
148
|
+
* declaration order) and an optional occupancy predicate keep it drop-in for a build-mode controller.
|
|
149
|
+
*
|
|
150
|
+
* @capability build-socket-catalog data-driven named-socket snap catalog for modular base building
|
|
151
|
+
*/
|
|
152
|
+
export declare function createBuildSocketCatalog(config: BuildSocketCatalogConfig): BuildSocketCatalog;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data-driven modular build-piece snap system: a catalog of piece types, each declaring named
|
|
3
|
+
* sockets (local position + outward facing + a generic "kind"), plus a connection-rule table saying
|
|
4
|
+
* which socket kinds may mate. Given a placed piece and a candidate type it resolves every valid
|
|
5
|
+
* snap — the world transform that lands the candidate's socket exactly on the placed piece's socket,
|
|
6
|
+
* facing back into it. Genre-agnostic: foundation/wall/ceiling/pillar/edge snapping falls out of the
|
|
7
|
+
* catalog data, with no game-specific piece names baked in. Pure and serializable; composes with
|
|
8
|
+
* `world/footprintGrid` occupancy (via an injected `isFree` predicate — never imported) and with
|
|
9
|
+
* `scene/modelSockets` named attach points (via {@link pieceSocketsFromModel}).
|
|
10
|
+
*/
|
|
11
|
+
/** Length-prefixed composite key (avoids delimiter collisions without any control characters). */
|
|
12
|
+
function pairKey(a, b) {
|
|
13
|
+
return `${a.length}:${a}:${b}`;
|
|
14
|
+
}
|
|
15
|
+
/** Rotate a local offset about +Y by `yaw` radians (affects X/Z, leaves Y). */
|
|
16
|
+
function rotateY(v, yaw) {
|
|
17
|
+
if (yaw === 0)
|
|
18
|
+
return [v[0], v[1], v[2]];
|
|
19
|
+
const cos = Math.cos(yaw);
|
|
20
|
+
const sin = Math.sin(yaw);
|
|
21
|
+
return [v[0] * cos - v[2] * sin, v[1], v[0] * sin + v[2] * cos];
|
|
22
|
+
}
|
|
23
|
+
function cloneSocket(socket) {
|
|
24
|
+
return {
|
|
25
|
+
name: socket.name,
|
|
26
|
+
kind: socket.kind,
|
|
27
|
+
position: [socket.position[0], socket.position[1], socket.position[2]],
|
|
28
|
+
...(socket.yaw === undefined ? {} : { yaw: socket.yaw }),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function clonePiece(piece) {
|
|
32
|
+
return {
|
|
33
|
+
type: piece.type,
|
|
34
|
+
sockets: piece.sockets.map(cloneSocket),
|
|
35
|
+
...(piece.footprint === undefined ? {} : { footprint: { w: piece.footprint.w, d: piece.footprint.d } }),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The grid cells a piece `footprint` centered on `position` (world units) covers, honoring `yaw`
|
|
40
|
+
* (snapped to the nearest quarter turn). Mirrors `world/footprintGrid`'s `cellsFor` so a game can
|
|
41
|
+
* feed the result straight into `grid.isFree`/`grid.reserve` after committing a resolved snap —
|
|
42
|
+
* without this module importing the grid.
|
|
43
|
+
*
|
|
44
|
+
* @capability build-footprint-cells grid cells a snapped build piece footprint covers (footprintGrid occupancy bridge)
|
|
45
|
+
*/
|
|
46
|
+
export function footprintCells(position, footprint, yaw = 0, cellSize = 1) {
|
|
47
|
+
const quarterTurns = Math.round(yaw / (Math.PI / 2));
|
|
48
|
+
const turned = (((quarterTurns % 2) + 2) % 2) === 1;
|
|
49
|
+
const cols = Math.max(1, Math.round((turned ? footprint.d : footprint.w) / cellSize));
|
|
50
|
+
const rows = Math.max(1, Math.round((turned ? footprint.w : footprint.d) / cellSize));
|
|
51
|
+
const originCol = Math.round(position[0] / cellSize - cols / 2);
|
|
52
|
+
const originRow = Math.round(position[2] / cellSize - rows / 2);
|
|
53
|
+
const cells = [];
|
|
54
|
+
for (let row = 0; row < rows; row += 1) {
|
|
55
|
+
for (let col = 0; col < cols; col += 1)
|
|
56
|
+
cells.push({ col: originCol + col, row: originRow + row });
|
|
57
|
+
}
|
|
58
|
+
return cells;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Turn a model's named attach points (from `scene/modelSockets`' `readNamedSockets`) into
|
|
62
|
+
* {@link SocketDef}s, classifying each into a connection `kind` via `kindOf` and, optionally, an
|
|
63
|
+
* outward `yaw` via `yawOf`. Lets an artist author snap sockets as empties in a GLB instead of by
|
|
64
|
+
* hand in the catalog. Pure and structural — no three.js or scene import.
|
|
65
|
+
*
|
|
66
|
+
* @capability piece-sockets-from-model derive build-piece snap sockets from a model's named attach points
|
|
67
|
+
*/
|
|
68
|
+
export function pieceSocketsFromModel(sockets, kindOf, yawOf) {
|
|
69
|
+
return sockets.map((socket) => ({
|
|
70
|
+
name: socket.name,
|
|
71
|
+
kind: kindOf(socket.name),
|
|
72
|
+
position: [socket.offset[0], socket.offset[1], socket.offset[2]],
|
|
73
|
+
...(yawOf === undefined ? {} : { yaw: yawOf(socket.name) }),
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Build a modular snap catalog from piece definitions and a connection-rule table. Resolves the set
|
|
78
|
+
* of valid snap transforms when hovering a candidate piece near a placed one: for every compatible
|
|
79
|
+
* socket pair it computes the world transform that seats the candidate's socket onto the placed
|
|
80
|
+
* socket, oriented to face back into it. Deterministic ordering (nearest a cursor when given, else
|
|
81
|
+
* declaration order) and an optional occupancy predicate keep it drop-in for a build-mode controller.
|
|
82
|
+
*
|
|
83
|
+
* @capability build-socket-catalog data-driven named-socket snap catalog for modular base building
|
|
84
|
+
*/
|
|
85
|
+
export function createBuildSocketCatalog(config) {
|
|
86
|
+
const cellSize = config.cellSize ?? 1;
|
|
87
|
+
const pieces = new Map();
|
|
88
|
+
const order = [];
|
|
89
|
+
for (const piece of config.pieces) {
|
|
90
|
+
const clone = clonePiece(piece);
|
|
91
|
+
pieces.set(clone.type, clone);
|
|
92
|
+
order.push(clone.type);
|
|
93
|
+
}
|
|
94
|
+
const compatible = new Set();
|
|
95
|
+
const rules = [];
|
|
96
|
+
for (const rule of config.rules) {
|
|
97
|
+
compatible.add(pairKey(rule.a, rule.b));
|
|
98
|
+
if (rule.mutual !== false)
|
|
99
|
+
compatible.add(pairKey(rule.b, rule.a));
|
|
100
|
+
rules.push({ a: rule.a, b: rule.b, ...(rule.mutual === undefined ? {} : { mutual: rule.mutual }) });
|
|
101
|
+
}
|
|
102
|
+
const canConnect = (candidateKind, placedKind) => compatible.has(pairKey(candidateKind, placedKind));
|
|
103
|
+
const worldSockets = (placed) => {
|
|
104
|
+
const def = pieces.get(placed.type);
|
|
105
|
+
if (def === undefined)
|
|
106
|
+
return [];
|
|
107
|
+
const baseYaw = placed.transform.yaw ?? 0;
|
|
108
|
+
const origin = placed.transform.position;
|
|
109
|
+
return def.sockets.map((socket) => {
|
|
110
|
+
const rotated = rotateY(socket.position, baseYaw);
|
|
111
|
+
return {
|
|
112
|
+
name: socket.name,
|
|
113
|
+
kind: socket.kind,
|
|
114
|
+
position: [origin[0] + rotated[0], origin[1] + rotated[1], origin[2] + rotated[2]],
|
|
115
|
+
yaw: baseYaw + (socket.yaw ?? 0),
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
cellSize,
|
|
121
|
+
has(type) {
|
|
122
|
+
return pieces.has(type);
|
|
123
|
+
},
|
|
124
|
+
piece(type) {
|
|
125
|
+
const def = pieces.get(type);
|
|
126
|
+
return def === undefined ? null : clonePiece(def);
|
|
127
|
+
},
|
|
128
|
+
types() {
|
|
129
|
+
return order.slice();
|
|
130
|
+
},
|
|
131
|
+
canConnect,
|
|
132
|
+
worldSockets,
|
|
133
|
+
resolveSnaps(placed, candidateType, options = {}) {
|
|
134
|
+
const candidateDef = pieces.get(candidateType);
|
|
135
|
+
if (candidateDef === undefined)
|
|
136
|
+
return [];
|
|
137
|
+
const targets = worldSockets(placed);
|
|
138
|
+
const cursor = options.cursor;
|
|
139
|
+
const isFree = options.isFree;
|
|
140
|
+
const out = [];
|
|
141
|
+
for (const target of targets) {
|
|
142
|
+
for (const socket of candidateDef.sockets) {
|
|
143
|
+
if (!canConnect(socket.kind, target.kind))
|
|
144
|
+
continue;
|
|
145
|
+
// Seat the candidate socket onto the target: same world point, facing opposite.
|
|
146
|
+
const candidateYaw = target.yaw + Math.PI - (socket.yaw ?? 0);
|
|
147
|
+
const rotated = rotateY(socket.position, candidateYaw);
|
|
148
|
+
const positionX = target.position[0] - rotated[0];
|
|
149
|
+
const positionY = target.position[1] - rotated[1];
|
|
150
|
+
const positionZ = target.position[2] - rotated[2];
|
|
151
|
+
const position = [positionX, positionY, positionZ];
|
|
152
|
+
if (isFree !== undefined && candidateDef.footprint !== undefined) {
|
|
153
|
+
const cells = footprintCells(position, candidateDef.footprint, candidateYaw, cellSize);
|
|
154
|
+
if (!cells.every((cell) => isFree(cell)))
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const distance = cursor === undefined
|
|
158
|
+
? 0
|
|
159
|
+
: Math.hypot(target.position[0] - cursor[0], target.position[1] - cursor[1], target.position[2] - cursor[2]);
|
|
160
|
+
out.push({
|
|
161
|
+
candidateType,
|
|
162
|
+
targetSocket: target.name,
|
|
163
|
+
candidateSocket: socket.name,
|
|
164
|
+
transform: { position, yaw: candidateYaw },
|
|
165
|
+
point: [target.position[0], target.position[1], target.position[2]],
|
|
166
|
+
distance,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Nearest-first when a cursor is given; Array.sort is stable so equal distances keep
|
|
171
|
+
// declaration order. Without a cursor the array is already in declaration order.
|
|
172
|
+
if (cursor !== undefined)
|
|
173
|
+
out.sort((x, y) => x.distance - y.distance);
|
|
174
|
+
return out;
|
|
175
|
+
},
|
|
176
|
+
toJSON() {
|
|
177
|
+
return {
|
|
178
|
+
cellSize,
|
|
179
|
+
pieces: order.map((type) => clonePiece(pieces.get(type))),
|
|
180
|
+
rules: rules.map((rule) => ({
|
|
181
|
+
a: rule.a,
|
|
182
|
+
b: rule.b,
|
|
183
|
+
...(rule.mutual === undefined ? {} : { mutual: rule.mutual }),
|
|
184
|
+
})),
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block-first city fabric for the `city` studio: turns the synthesized street network into a
|
|
3
|
+
* planar graph, extracts its faces as CLOSED BLOCK POLYGONS (a curved street yields a curved
|
|
4
|
+
* block edge, a diagonal one a diagonal edge), insets each block by its bordering roads'
|
|
5
|
+
* half-widths into curb and land rings (the sidewalk band lives between them), classifies every
|
|
6
|
+
* block (buildable, park, plaza, field, buffer sliver), subdivides buildable block frontage into
|
|
7
|
+
* POLYGONAL PARCELS with street references and corner status, and computes per-parcel BUILDABLE
|
|
8
|
+
* polygons after setbacks. Buildings then derive from parcel geometry instead of nearest-road
|
|
9
|
+
* guesses. Pure seeded math over `cityGeometry` primitives — no rendering, bounded work.
|
|
10
|
+
*
|
|
11
|
+
* @capability city-district road-derived block polygons, parcel subdivision, buildable footprints
|
|
12
|
+
*/
|
|
13
|
+
import { type Vec2 } from "./cityGeometry.js";
|
|
14
|
+
/** How a block (a face of the road graph) is used. */
|
|
15
|
+
export type CityBlockKind = "buildable" | "park" | "plaza" | "field" | "buffer";
|
|
16
|
+
/** One closed block: a face of the road network, inset to its curb and land boundaries. */
|
|
17
|
+
export interface CityBlockLocal {
|
|
18
|
+
id: string;
|
|
19
|
+
/** Face polygon on road CENTERLINES (CCW) — the raw graph face. */
|
|
20
|
+
face: Vec2[];
|
|
21
|
+
/** Street index bordering each face edge (edge i = face[i]→face[i+1]); -1 = district boundary. */
|
|
22
|
+
faceStreets: number[];
|
|
23
|
+
/** Pavement edge: face inset by each bordering road's half width (+ curb margin). */
|
|
24
|
+
curb: Vec2[];
|
|
25
|
+
/** Land edge: curb inset further by the sidewalk band where the bordering street has one. */
|
|
26
|
+
polygon: Vec2[];
|
|
27
|
+
kind: CityBlockKind;
|
|
28
|
+
area: number;
|
|
29
|
+
}
|
|
30
|
+
/** One street-frontage reference of a parcel. */
|
|
31
|
+
export interface CityParcelFrontage {
|
|
32
|
+
/** Index into the resolved city's street list. */
|
|
33
|
+
street: number;
|
|
34
|
+
/** Frontage chord endpoints on the parcel boundary. */
|
|
35
|
+
edgeStart: Vec2;
|
|
36
|
+
edgeEnd: Vec2;
|
|
37
|
+
/** Unit tangent along the frontage chord. */
|
|
38
|
+
tangent: Vec2;
|
|
39
|
+
}
|
|
40
|
+
/** How a parcel is used after occupancy/fitting rolls. */
|
|
41
|
+
export type CityParcelKind = "built" | "vacant" | "yard";
|
|
42
|
+
/** One polygonal parcel subdivided out of a block's street frontage. */
|
|
43
|
+
export interface CityParcelLocal {
|
|
44
|
+
id: string;
|
|
45
|
+
block: number;
|
|
46
|
+
polygon: Vec2[];
|
|
47
|
+
/** Buildable polygon after front/side/rear setbacks; [] when setbacks consume the parcel. */
|
|
48
|
+
buildable: Vec2[];
|
|
49
|
+
frontage: CityParcelFrontage[];
|
|
50
|
+
area: number;
|
|
51
|
+
/** Usable depth from the frontage chord to the parcel rear. */
|
|
52
|
+
depth: number;
|
|
53
|
+
isCorner: boolean;
|
|
54
|
+
kind: CityParcelKind;
|
|
55
|
+
}
|
|
56
|
+
/** Minimal street shape the fabric needs (matches cityKind's LocalStreet). */
|
|
57
|
+
export interface FabricStreet {
|
|
58
|
+
points: readonly (readonly [number, number])[];
|
|
59
|
+
width: number;
|
|
60
|
+
level: "boulevard" | "avenue" | "street" | "lane";
|
|
61
|
+
sidewalk: boolean;
|
|
62
|
+
}
|
|
63
|
+
/** Sidewalk band width by street hierarchy (base = the district's street-level width). */
|
|
64
|
+
export declare function sidewalkWidthFor(level: FabricStreet["level"], base: number): number;
|
|
65
|
+
/** One extracted face: closed centerline polygon + the street index along each polygon edge. */
|
|
66
|
+
export interface GraphFace {
|
|
67
|
+
polygon: Vec2[];
|
|
68
|
+
streets: number[];
|
|
69
|
+
}
|
|
70
|
+
/** Everything block extraction needs from the district rules. */
|
|
71
|
+
export interface FabricParams {
|
|
72
|
+
streetWidthBase: number;
|
|
73
|
+
/** Base sidewalk band width in meters at "street" hierarchy level. */
|
|
74
|
+
sidewalkBase: number;
|
|
75
|
+
/** Curb strip between pavement edge and sidewalk, meters. */
|
|
76
|
+
curbMargin: number;
|
|
77
|
+
}
|
|
78
|
+
/** A block's derived rings plus the sidewalk band (outer = curb line, inner = land line). */
|
|
79
|
+
export interface BlockRings {
|
|
80
|
+
face: Vec2[];
|
|
81
|
+
faceStreets: number[];
|
|
82
|
+
curb: Vec2[];
|
|
83
|
+
land: Vec2[];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Extract closed blocks from the street network: planar faces of the graph, each inset to its
|
|
87
|
+
* curb ring (pavement edge) and land ring (behind the sidewalk band). Faces that collapse under
|
|
88
|
+
* the inset come back with an empty `land` and are the caller's slivers/buffers.
|
|
89
|
+
*/
|
|
90
|
+
export declare function extractBlocks(streets: readonly FabricStreet[], hx: number, hz: number, params: FabricParams): {
|
|
91
|
+
blocks: BlockRings[];
|
|
92
|
+
deadEnds: {
|
|
93
|
+
pts: Vec2[];
|
|
94
|
+
width: number;
|
|
95
|
+
}[];
|
|
96
|
+
};
|
|
97
|
+
/** Arc-length station lookup along a closed ring. */
|
|
98
|
+
export declare class RingWalker {
|
|
99
|
+
readonly ring: readonly Vec2[];
|
|
100
|
+
readonly cum: number[];
|
|
101
|
+
readonly total: number;
|
|
102
|
+
constructor(ring: readonly Vec2[]);
|
|
103
|
+
/** Point, unit tangent, and inward normal (CCW ring: interior on the left) at arc length `s`. */
|
|
104
|
+
at(s: number): {
|
|
105
|
+
p: Vec2;
|
|
106
|
+
tangent: Vec2;
|
|
107
|
+
normal: Vec2;
|
|
108
|
+
edge: number;
|
|
109
|
+
};
|
|
110
|
+
/** Ring vertex indices whose arc position lies strictly inside (s0, s1) (wrapping). */
|
|
111
|
+
verticesBetween(s0: number, s1: number): number[];
|
|
112
|
+
/** Interior angle (radians) at ring vertex i — π on straight runs, < π at convex corners. */
|
|
113
|
+
interiorAngle(i: number): number;
|
|
114
|
+
}
|
|
115
|
+
/** Inputs for one parcel strip cut from a block ring. */
|
|
116
|
+
export interface ParcelCut {
|
|
117
|
+
/** Arc-length start/end stations along the block's land ring. */
|
|
118
|
+
s0: number;
|
|
119
|
+
s1: number;
|
|
120
|
+
/** Strip depth from the frontage inward. */
|
|
121
|
+
depth: number;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Cut one polygonal parcel out of a block land ring: the frontage arc between two stations,
|
|
125
|
+
* extruded inward along the station normals, clipped back to the block. Returns null when the
|
|
126
|
+
* strip collapses (over-tight corner, sliver arc).
|
|
127
|
+
*/
|
|
128
|
+
export declare function cutParcel(walker: RingWalker, blockRing: readonly Vec2[], cut: ParcelCut): Vec2[] | null;
|
|
129
|
+
/**
|
|
130
|
+
* Conform a polygon to a containing ring: sample every edge (a straight edge can bulge across a
|
|
131
|
+
* concave notch even when its endpoints hug the boundary), then pull each escaping point to its
|
|
132
|
+
* nearest boundary point. Callers re-run this after any operation that adds mid-edge vertices.
|
|
133
|
+
*/
|
|
134
|
+
export declare function conformPolygonToRing(poly: readonly Vec2[], ring: readonly Vec2[]): Vec2[];
|
|
135
|
+
/**
|
|
136
|
+
* Buildable polygon for a parcel: the parcel clipped by the front setback (a half-plane pushed in
|
|
137
|
+
* from the frontage chord), side gaps along both cut lines, and a rear margin. Works on any
|
|
138
|
+
* parcel polygon — no vertex correspondence needed.
|
|
139
|
+
*/
|
|
140
|
+
export declare function buildablePolygon(parcel: readonly Vec2[], frontA: Vec2, frontB: Vec2, frontSetback: number, sideGap: number, rearMargin: number, depth: number): Vec2[];
|
|
141
|
+
/**
|
|
142
|
+
* Carve dead-end road corridors (cul-de-sac lanes pruned from the face graph) out of a polygon:
|
|
143
|
+
* for every corridor segment crossing it, keep the polygon piece on the side where `keep` lies.
|
|
144
|
+
*/
|
|
145
|
+
export declare function carveCorridors(poly: Vec2[], keep: Vec2, corridors: readonly {
|
|
146
|
+
pts: readonly Vec2[];
|
|
147
|
+
width: number;
|
|
148
|
+
}[], margin: number): Vec2[];
|
|
149
|
+
/** Deterministic sliver verdict shared by classification and tests. */
|
|
150
|
+
export declare function isSliverBlock(land: readonly Vec2[], minArea: number, minWidth: number): boolean;
|
|
151
|
+
/** Sample deterministic points inside a polygon by seeded rejection over its bounds. */
|
|
152
|
+
export declare function pointsInPolygon(ring: readonly Vec2[], count: number, rng: () => number, margin?: number): Vec2[];
|