sindicate 0.13.0 → 0.15.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 ADDED
@@ -0,0 +1,69 @@
1
+ # Changelog
2
+
3
+ ## 0.15.0 — the world-building release
4
+
5
+ Everything between the last published version (0.13.0) and here. 0.14.0 was bumped in the
6
+ repo but never published, so its contents ship as part of this release.
7
+
8
+ ### The world, split out of the games (0.14.0's work)
9
+
10
+ The terrain and stamper machinery both games carried privately now belongs to the engine, and
11
+ each game keeps only its own content:
12
+
13
+ - `world/geo.js` — the noise/curve toolkit (fbm, ridged, terrace, segDist, smoothstep).
14
+ - `world/railFormation.js` — the rail solver: max-grade Lipschitz envelopes and vertical curves.
15
+ - `world/bridgeSplice.js`, `world/waterLevels.js`, `world/waterMeshes.js` — bridges and water.
16
+ - `world/heightfield.js` — the chunked terrain mesh and its cook (one height lattice, central-
17
+ difference normals so chunk borders cannot seam, cooked to a single self-writing bin).
18
+ - `world/kit.js` — the kit registries, doors and ladders, `setKitContent`, `buildArea`,
19
+ `buildTown`, `dressTownCenter`.
20
+ - `world/placement.js` — the cook, samplers, resident kits and occupancy.
21
+ - `world/worldBase.js` — the world facade's engine half, with eight hooks a game fills in.
22
+ - `world/permanentWay.js` — the track-laying machinery.
23
+
24
+ ### The World Designer
25
+
26
+ - `tools/designer.js` + `tools/devPlugin.js` — a visual editor over the game's real terrain:
27
+ place, move and remove prefabs, draw road polylines, fly the camera, save to JSON.
28
+ - `tools/designer-mcp.mjs` — an MCP server so a Claude session can drive the designer while
29
+ you watch it.
30
+ - `world/design.js` — `buildDesign()`, the layout-JSON → live-scene bridge.
31
+
32
+ ### The road system (new)
33
+
34
+ Junctions are rigid prefab pieces; the roads between them are generated. Both halves, plus the
35
+ earthworks that make the ground agree with them:
36
+
37
+ - `world/roadKit.js` — reads a pack's measured kit. Lanes are stored by which side of the
38
+ centre line they sit on, so **left/right-hand drive is a runtime switch**, not baked data.
39
+ `gradeLevel`/`seatY` sink a multi-level junction until the roads that meet the outside world
40
+ are at ground level.
41
+ - `world/roadLink.js` — the road between two sockets: a cubic Bezier through both arms'
42
+ tangents so seams are tangential, cross-sections matching each socket's measured width and
43
+ paint, and a vertical profile squeezed into the reachable-at-max-grade envelope then relaxed
44
+ into vertical curves. Connecting is non-negotiable: if two sockets cannot be joined within
45
+ the requested grade the road still meets both ends and reports `feasible: false`.
46
+ - `world/roadTerrain.js` — pads under junctions, cut/fill corridors under links, both using a
47
+ batter slope so a road crossing a valley gets an embankment that reaches it. `pickSites` and
48
+ `pickManySites` put junctions on the flats.
49
+ - `world/roadNetwork.js` — sites in, road system out: a spanning tree plus a few short extra
50
+ links so the map has loops, and for each site the junction piece and rotation whose arms best
51
+ face the neighbours it must serve.
52
+
53
+ ### Pack tooling
54
+
55
+ - `tools/unityMeshToGlb.mjs`, `tools/unityPrefabToAssembly.mjs`, `tools/truthToAssemblies.mjs`,
56
+ `tools/fbxToGlb.mjs`, `tools/roadKitAnalyze.mjs`, `tools/buildKiwiRoadsPack.mjs` — recovering
57
+ a Unity road library into an engine pack: meshes, Unity-resolved transforms and materials,
58
+ per-file FBX import scale, measured sockets and cross-sections.
59
+ - `tools/ExportKiwiRoadMaterials.cs`, `tools/ExportKiwiRoadRefs.cs` — the Unity-side exports.
60
+
61
+ ### Notes
62
+
63
+ - `world/roadGraph.js` (A\* routing for the map) and the road piece system are different
64
+ things that share a word.
65
+ - The road modules are new and may still move before 1.0.
66
+
67
+ ## 0.13.0
68
+
69
+ World splits steps 1–3a; the package catalog and one-click install.
@@ -0,0 +1,97 @@
1
+ # Kiwi Road System — Recon & Re-Implementation Brief (for Sindicate/three.js)
2
+
3
+ > Recon of /Users/nick/Desktop/Kiwi (Unity). Kiwi runs TWO parallel road systems:
4
+ > **A) City grid roads** — Manhattan grid of cardinal segments built from prefab junction
5
+ > pieces + ONE repeated 2.5m straight prefab, a procedural "curved road" mesh overlay, and
6
+ > EasyRoads3D-style terrain benching. **B) Inter-city roads** — procedural spline-extruded
7
+ > meshes (motorway/dual carriageway/country) + generated-then-saved interchange prefabs
8
+ > (roundabouts, overpass, dumbbell). For Sindicate, A's prefab+socket+junction logic and
9
+ > the benching are the model; B is math to mine, not to copy.
10
+
11
+ ## 1. Piece taxonomy + sockets
12
+ - Junction prefabs (TerrainGeneratorWindow_CityJunctions.cs:25-31): Junction_4Way/T/LCorner
13
+ (City_Simpligon + Countryside variants). City straight: RoadSegment_Simpligon (2.5m).
14
+ - Synty source pieces referenced (POLYGON City/Town kit): Road_01/02/03, Straight_01/02/03,
15
+ Corner_01/02, Corner_End_03, Road_Crossing_01, Road_Flyover_01(+_Support), Road_Bare_01,
16
+ Road_Lines_01, Road_YellowLines_02; props Barrier_01, HighwayGuardrail, signs.
17
+ - Interchanges are GENERATED then saved as prefabs (InterCityRoadWindow.cs:48-52, 964-1188).
18
+ - **Kiwi has NO true smallest-cut piece set** — one straight + junction prefabs + curved
19
+ overlay. Turn/ramp/grade PIECES are net-new design for Sindicate.
20
+ - Sockets, simple form (RoadConnectorPoints.cs:46-177): child anchors named Connector_*;
21
+ road runs along local +Z; Connector_A at minZ (back), Connector_B at maxZ (front), at
22
+ road-surface height (mesh maxY), x=0. Junctions: Connector_{N,S,E,W} at ±JUNCTION_RADIUS.
23
+ - Rich form — ADOPT THIS (Runtime/Roads/JunctionConnectionPoints.cs:15-50):
24
+ ConnectionPoint { armId "N"/"S"/"E"/"W"/...; local position (road-width centre at arm
25
+ end); outward direction; RoadType {CountryRoad, SideRoad, DualCarriageway, Motorway};
26
+ laneCount; speedLimit }.
27
+
28
+ ## 2. Network data model
29
+ - ONE constant drives everything (ManhattanGridGenerator.cs:48-65): BLOCK_SIZE=50m →
30
+ ROAD_PIECE_LENGTH=2.5m, JUNCTION_SIZE=20m, junction spacing 0.4–1.2×block; gates
31
+ WATER_LEVEL=5m, MAX_SLOPE=22.5°.
32
+ - Serialized graph (CityData.cs): CityData { junctions[], roadSegments[], blocks[],
33
+ rawRoads[], seed }; JunctionData { id, position(incl terrain Y), JunctionType
34
+ {DeadEnd,Straight,TJunction,Crossroads,Complex}, connectedRoadIds, rotationY,
35
+ flattenRadius }; RoadSegmentData { start/endJunctionId, width, roadType, length,
36
+ heightDifference, splineWaypoints }. JSON-exported (_Cities.cs:2554).
37
+ - Runtime graph (RoadNetworkBuilder.cs): RoadNode merged within 1m; type classified purely
38
+ by CONNECTION COUNT (:184-208).
39
+
40
+ ## 3. Placement math
41
+ - Grid gen (ManhattanGridGenerator.Generate :412-667): population flood-fill (cells =
42
+ pop/200), per-cell water/slope reject, per-edge span validity sampled every ~5m,
43
+ dead-end → perpendicular ray → nearest road → T connector (:791-888), merge verticals,
44
+ largest-connected-component BFS filter (:673-778).
45
+ - Turns: the grid is CARDINAL-ONLY (±5°); curvature comes from the procedural curved-mesh
46
+ overlay along a straight→quadratic-bézier→straight "blue spline" (SampleModifiedSpline,
47
+ _Cities.cs:1640-1682) — because Kiwi lacks turn pieces.
48
+ - Piece fitting: numPieces = floor(dist/pieceLength), walk by pieceLength, orient by
49
+ LookRotation(direction), Y from terrain +0.1 (_Cities.cs:1559-1637).
50
+ - SplinePlacer.cs VARIABLE-SIZE mode (:123-253): grow a piece until tangent deviation
51
+ exceeds angleDeviance (3°), then CUT and scale the piece to the chord — the template for
52
+ turn-aware smallest-cut placement.
53
+ - Weld trick TO AVOID: clone each piece mesh, flatten back edge, SnapVerticesToTargets to
54
+ the junction's edge verts (_Cities.cs:1863-2062) — incompatible with InstancedMesh.
55
+ - Grades: cities are BENCHED (junction at avg terrain height in its box, :248-285; steep
56
+ segments REJECTED not ramped). Only inter-city splines carry true 3D elevation.
57
+
58
+ ## 4. Terrain stamping
59
+ - FlattenTerrainUnderCurvedRoads (…_CityTerrainFlattening.cs:35-352): ROAD_BED_OFFSET=1m,
60
+ INDENT_WIDTH=2m, SURROUNDING_WIDTH=3m. Extract road footprint POLYGONS (vert rings →
61
+ left/right edge lists with per-ring heights, :357-457) + junction polygons (72 angular
62
+ sectors +1m, :462-535); 50m spatial grid; per heightmap pixel: inside → per-edge
63
+ INTERPOLATED roadHeight − offset (grade preserved), indent → edge height − offset, skirt
64
+ → smoothstep back to terrain. Cut AND fill; region-partial writes; pixel-error 2.
65
+ - TerrainStamp.cs: R-channel heightmap stamps with blend modes (cliffs/mesas, not roads).
66
+
67
+ ## 5. Junction resolution (_CityJunctions.cs:36-404)
68
+ - Tally cardinal connections (±5°): 4 → 4-way rot 0°; 3 → T rotated so the MISSING arm
69
+ faces north (:257-260); 2 opposite → two back-to-back straight pieces (first keeps the
70
+ junction's load-bearing name); 2 perpendicular → L-corner rotated by present dirs
71
+ (:289-293); 1 → dead-end straight; 0 → marker. Junction mesh bottom seated at avg
72
+ terrain height. Names "City_X_Junction_Y" are parsed by road placement (load-bearing).
73
+
74
+ ## 6. Editor UX
75
+ - TerrainGeneratorWindow (partial class over ~15 files): Preview Cities → Generate per
76
+ city: ExportCityDataToJson → PlaceJunctionsAndFlattenTerrain → ClearGrassInCity →
77
+ PlaceRoadsAndFlattenTerrain → RoadSplineEditor blue-lines → CurvedRoadMeshGenerator →
78
+ FlattenTerrainUnderCurvedRoads → diagnostics. InterCityRoadWindow: tabbed per road type,
79
+ straight/spline gen, per-type + batch prefab saving.
80
+
81
+ ## 7. Copy vs avoid for Sindicate
82
+ COPY: centralized config; cardinal grid pipeline (flood-fill, gating, dead-end→T,
83
+ largest-component); the RICH socket model (armId+pos+dir+roadType+laneCount) with socket
84
+ transforms precomputed per prefab type; floor(dist/len) fitting + SplinePlacer
85
+ angle-deviance cutting; junction resolution by connection count + 90° rotation; EasyRoads
86
+ benching (indent+skirt, cut+fill, per-edge interpolated heights); CityData-style JSON.
87
+ AVOID: per-instance mesh cloning/vert snapping (author pieces so sockets meet EXACTLY on a
88
+ shared grid instead); three overlapping road systems (pick prefab pieces, add REAL
89
+ turn/ramp pieces); bench-only grades (Sindicate wants ramps — and already owns better
90
+ grade math: the rail formation solver); /tmp logging in loops; per-piece MeshColliders;
91
+ RoadPieceData prefab hacks.
92
+
93
+ Key files: ManhattanGridGenerator.cs, CityData.cs, RoadConnectorPoints.cs,
94
+ JunctionConnectionPoints.cs, TerrainGeneratorWindow_{Cities,CityJunctions,
95
+ CityTerrainFlattening,InterJunctionRoads}.cs, CurvedRoadMeshGenerator.cs,
96
+ RoadNetworkBuilder.cs, SplinePlacer.cs, PROCEDURAL_MAP_GENERATOR_PLAN.md (Phase 8 §4729,
97
+ postmortems §8336-8420).
@@ -0,0 +1,142 @@
1
+ # SINDICATE PREFAB ROADS — design for Nick's review (NO CODE UNTIL SIGNED OFF)
2
+
3
+ > Nick: "roads using prefabs too where we cut the smallest bit of them to allow drawing
4
+ > them in in single instances etc and allow turns and uphill downhill with them etc."
5
+ > Foundation: docs/plans/kiwi-road-recon.md (what Kiwi proved + what it never solved).
6
+
7
+ ## The one-system rule (Kiwi's lesson #1)
8
+
9
+ Kiwi grew three overlapping road systems because its grid had no real turn pieces.
10
+ Sindicate builds ONE: a PIECE GRAPH — typed prefab pieces joined socket-to-socket,
11
+ rendered as InstancedMesh batches (one per piece type), never editing geometry per
12
+ instance. Pieces are AUTHORED to mate exactly; welding-by-vertex-snap is banned.
13
+
14
+ ## 1. The piece library (per road kit, registered like setKitContent)
15
+
16
+ A game registers a ROAD KIT — content, not engine:
17
+ ```js
18
+ setRoadKit({
19
+ dir, atlas,
20
+ pieces: {
21
+ straight: [{ file, len }], // several lengths; shortest = the CUT quantum
22
+ corner: [{ file, radius, angle }], // 90° (and 45° where the pack has one)
23
+ tee: { file }, cross: { file },
24
+ ramp: [{ file, len, rise }], // grade variants (see §4)
25
+ endcap: { file },
26
+ },
27
+ width, surfaceY, // measured off the meshes once
28
+ props: { barrier, lamp, sign... }, // optional dressing hooks
29
+ })
30
+ ```
31
+ - GTA game: POLYGON_City kit (Kiwi's own source pieces — Road_01/Straight_01/Corner_01/
32
+ Road_Crossing_01/Flyover_01 family).
33
+ - Fable: no modern roads in Fantasy Kingdom — its "roads" stay the painted/benched
34
+ terrain lanes we have (the survey), optionally dressed with the pack's cobble/path
35
+ floor pieces later. The piece system is for kits that HAVE road prefabs.
36
+ - Western: stays on painted dirt roads (correct for 1880).
37
+
38
+ Sockets are NOT hand-authored: at kit registration the engine measures each piece
39
+ (bounds; road along local +Z; back socket at minZ, front at maxZ, surface at maxY —
40
+ Kiwi's convention) and builds { armId, pos, dir, laneCount } tables per type.
41
+
42
+ ## 2. The network model (designer-facing)
43
+
44
+ The DESIGNER draws what Kiwi called blue lines: polylines + junction nodes, per road
45
+ type. The layout JSON stores INTENT, the solver output stores RESULT:
46
+ ```js
47
+ roadsV2: {
48
+ nodes: [{ id, x, z }], // junction/endpoint nodes
49
+ links: [{ a, b, type, via: [{x,z}...] }], // optional via = curve hints
50
+ }
51
+ // solved (cached in the cook, not hand-edited):
52
+ // pieces: [{ type, file, x, y, z, ry, pitch, sz }...] + junction placements
53
+ ```
54
+
55
+ ## 3. The solver (engine — the new module, world/roadGraph.js)
56
+
57
+ 1. NODE RESOLUTION (Kiwi §5, copy): count links per node → cross/tee/corner/straight-
58
+ through/endcap piece + 90°-stepped rotation (T's missing arm rule verbatim).
59
+ 2. LINK FITTING: between the two junction sockets, walk the (optionally curved) path
60
+ with the SplinePlacer law: grow the current straight until tangent deviation > A°
61
+ (tunable, Kiwi used 3°), cut, place the LONGEST library straight that fits, repeat;
62
+ remainder < shortest straight → stretch the last piece via sz (the permanentWay
63
+ adaptive-panel trick — same batch, z-scale in the matrix). Corners consume their
64
+ authored arc; curve hints (via points) become corner+straight sequences.
65
+ 3. GRADES (Sindicate's advantage — Kiwi never solved this): the link's vertical profile
66
+ comes from solveFormation (rail solver: max-grade Lipschitz envelopes + vertical
67
+ curves), pieces pitch via makeChordMatrix (yaw→pitch→z-scale — sleepers stay level,
68
+ Kiwi had nothing like it). Ramp pieces are selected when |grade| exceeds what a
69
+ pitched straight reads well at; junction pads are LEVEL (yards law).
70
+ 4. OUTPUT: per-type matrix lists → panelBatch (scatterDeco road surface — walk-over law)
71
+ + collision stays the benched terrain; junction pieces placed like castings.
72
+
73
+ ## 4. Terrain: the road benches the ground (Kiwi §4 + our survey)
74
+
75
+ The solved centreline + width feeds the SAME machinery our rails use: applyRail-style
76
+ corridor blend to the piece profile (bed = surfaceY − offset), Kiwi's indent+skirt
77
+ (2m flat, 3m smoothstep) as the cross-section, cut AND fill, into heightAt → the
78
+ heightfield/zones cooks re-fingerprint (roadsV2 hash joins the cook names). The ground
79
+ is the road, the pieces dress it — the lesson the railway already proved.
80
+
81
+ ## 5. Designer UX (v4)
82
+
83
+ - ROAD MODE v2: click nodes/links instead of raw polylines; type picker; live solver
84
+ preview (pieces appear as you draw, junction pieces swap as links connect).
85
+ - MCP: designer_road_node / designer_road_link / designer_road_solve.
86
+ - Layout JSON carries roadsV2; buildDesign stamps solved pieces; graduation to a game
87
+ feeds the centrelines into setRoadNetwork for the benching + cook.
88
+
89
+ ## 6. Build order (after sign-off)
90
+
91
+ 1. roadGraph.js solver core (nodes→junction resolution, link fitting, chord matrices) —
92
+ provable headless (piece counts + junction types on synthetic networks).
93
+ 2. POLYGON_City import + road-kit registration + a test design stamped in a scratch
94
+ scene (this is also the GTA game's first world artifact).
95
+ 3. Benching integration (roadsV2 → survey corridor → cook re-fingerprint).
96
+ 4. Designer road mode v2 + MCP verbs.
97
+
98
+ ## Open questions for Nick
99
+ - OK that fable/western keep terrain-painted roads (piece roads = City pack era)?
100
+ - Corner radii: lock to the pack's authored radius only (simplest), or also synthesize
101
+ gentle curves from short straights at deviance-cut angles (Kiwi's overlay did this —
102
+ more flexible, more math)?
103
+ - Junction sprawl: POLYGON_City has flyovers/supports — v1 scope flat junctions only,
104
+ flyovers later?
105
+
106
+ ## 7. Direct salvage from Kiwi (Nick: "there are bits you could take out to reuse ofc")
107
+
108
+ **Algorithms — port near-verbatim (C# → JS is mechanical):**
109
+ - SplinePlacer.cs variable-size loop (:123-253) → the link-fitting cutter in roadGraph.js.
110
+ - ManhattanGridGenerator flood-fill + water/slope gating + dead-end→ray→T + largest-
111
+ component filter → the AUTO-CITY-GRID generator (a designer "generate district" verb —
112
+ the GTA game's city blocks for free).
113
+ - _CityJunctions junction-count→piece+rotation table (incl. the missing-arm-north rule).
114
+ - _CityTerrainFlattening footprint-polygon bench (rings→edge lists, indent+skirt,
115
+ per-edge interpolated heights) → the survey corridor cross-section.
116
+ - SampleModifiedSpline straight→bézier→straight blend → curve hints between nodes.
117
+ - JunctionConnectionPoints data model (armId/pos/dir/roadType/laneCount/speedLimit).
118
+
119
+ **Assets — the tuned meshes themselves (need ONE Unity export pass):**
120
+ - Junction_{4Way,T,LCorner}_City_Simpligon + RoadSegment_Simpligon are Nick's own
121
+ tuned pieces, stored as Unity .asset meshes (Road/Markings/Sidewalk sub-meshes per
122
+ junction — the sub-mesh split is EXACTLY our flattenToGeometry split convention).
123
+ - The generated interchange prefabs (SimpleRoundabout_01, OverpassRoundabout, Dumbbell
124
+ variants in Prefabs/Roads/) are real saved meshes — a whole interchange library.
125
+ - ASK NICK: open Kiwi in Unity once and export these to FBX (adapt the existing
126
+ Editor/ExportMyAssets.cs, or the FBX Exporter package) → we import them as a
127
+ "KiwiRoads" pack through the pack importer and the road kit is instantly armed with
128
+ proven pieces + interchanges. ~10 minutes of Unity time buys the whole piece library.
129
+
130
+ **Data:** BLOCK_SIZE-derived constant table; RoadType/laneCount/speedLimit enums.
131
+
132
+ ## 8. The rest of AIGenerateMap (Nick: "and the other bits too ofc")
133
+
134
+ Beyond roads, the Kiwi world-builder code to mine (all plain C#, no Unity needed):
135
+ - ManhattanGridGenerator + CityData → designer "generate district" verb (city blocks).
136
+ - TerrainStamp (blend-mode heightmap stamps) + VoronoiNoise + FalloffGenerator → port
137
+ into engine geo.js (Voronoi + stamps are real gaps; fbm family already ours).
138
+ - Terrain/Vegetation/GrassDecorator (+Configs) → config-shape inspiration for our
139
+ placement/flora systems (machinery stays ours).
140
+ - TerrainTextureApplicator + colormaps → heightfield paint reference.
141
+ - InterCityRoadWindow spline/interchange generation → mined per-need for the solver.
142
+ - NewMapConfig/GeneratedTerrainData assets → GTA-game map parameter reference.
@@ -0,0 +1,43 @@
1
+ # THE THREE CITIES RUN — Nick's founding request for the World Designer
2
+
3
+ > "come up with a huge map with 3 cities 6 towns and road networks between them all
4
+ > using the prefabs from the packs"
5
+
6
+ ## The pipeline (all pieces SHIPPED as of 0.14)
7
+
8
+ 1. **Author** — an MCP-driven Claude session drives `/designer.html` on fable (5317)
9
+ through the `aldenvale-designer` tools (place/move/road/camera/save) while the human
10
+ watches; or write the JSON directly (it is the same artifact). Layouts save to
11
+ `fable/designs/<name>.json`.
12
+ 2. **Preview** — boot `?autostart&design=<name>`: engine `buildDesign()` stamps the
13
+ layout live (visual-only, post-BVH).
14
+ 3. **Integrate** — stamp the accepted designs in `world.build()` BEFORE the BVH bake
15
+ (collision + grass occupancy for free), and feed the design `roads` polylines into
16
+ the terrain survey (`setRoadNetwork`) so the ground benches under them → re-cook.
17
+
18
+ ## Authoring facts
19
+
20
+ - Palette: POLYGON_Fantasy_Kingdom, 2,186 models. The `SM_Bld_Preset_*_Optimized`
21
+ composites are WHOLE dressed buildings — cities assemble from these, not wall pieces.
22
+ Atlas: `PolygonFantasyKingdom_Texture_01_A.png` (every entry carries `tex`).
23
+ - Layout entry: `{ file, dir, x, z, ry, y?, s?, tex }`; omit `y` to seat on terrain.
24
+ - World: 4320 m square, Aldenvale relief is real — site cities on the flats
25
+ (designer viewport shows the relief; check `heightAt` spread before committing a site).
26
+ - Existing settlements to respect (never overlap): Oakhollow (0,14 r~90), the castle
27
+ (1120,800), Bracklow, VILLAGES/CAMPS lists, Ironkeep (DWARF), Ravenspire (-1400,-1200).
28
+ - Compass: north = −z. Roads: polylines in `roads`, ≥12 m from any building
29
+ (the farm-rect law), gentle bends (the survey benches them).
30
+
31
+ ## Suggested sites (from the relief + the existing map)
32
+
33
+ - City A: the plain SE of Oakhollow (~500, 400) — river-adjacent, flat.
34
+ - City B: the western meadows (~-900, 300).
35
+ - City C: the north coast road (~300, -1100).
36
+ - Six towns strung on the road net between them + the existing villages as waypoints.
37
+ - Budget: city ≈ 40-70 presets + walls/props; town ≈ 12-20. Total ≈ 350 placements —
38
+ trivial for the stamper (49k placements cook in seconds).
39
+
40
+ ## Definition of done
41
+
42
+ Every city/town saved as its own design file (composable), one master road list,
43
+ preview screenshots of each site, Nick signs the look before integration.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sindicate",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Sindicate — open-world game engine for three.js WebGPU. Streaming worlds, characters, mounts & vehicles, missions, crowds.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -29,7 +29,8 @@
29
29
  "src",
30
30
  "tools",
31
31
  "docs",
32
- "README.md"
32
+ "README.md",
33
+ "CHANGELOG.md"
33
34
  ],
34
35
  "peerDependencies": {
35
36
  "three": "^0.184.0",
@@ -0,0 +1,72 @@
1
+ // THE DESIGN STAMPER (designer v3) — renders a World Designer layout (designs/<name>.json,
2
+ // authored in /designer.html by hand or by an MCP-driven session) into the live world.
3
+ // MACHINE-WRITTEN LAYOUTS ARE JSON: { name, prefabs: [{ file, dir, x, y?, z, ry?, s?, tex? }],
4
+ // roads: [[{x,z}, ...], ...] }.
5
+ //
6
+ // Same shape as the kit stamper's prop path: prefabs batch per (file|tex) into ONE
7
+ // InstancedMesh each, on the shared townMaterial pipeline (one material per atlas,
8
+ // lamp-lit). Placement seats on the game's ground through the heightAt option unless the
9
+ // entry pins its own y.
10
+ //
11
+ // COLLISION IS THE BAKE'S BUSINESS: batches are named `scatter:` (solid — the world BVH
12
+ // bake picks them up by name) or `scatterDeco:` (walk-through) via opts.solid. Stamping
13
+ // AFTER world.build's bake gives no collision until the next bake — fine for a dev-flag
14
+ // preview; the production path stamps designs before the bake, exactly like the towns.
15
+ //
16
+ // ROADS: not rendered here. A designer road is a ROUTE, and routes belong to the game's
17
+ // terrain survey (road networks bench the ground, paint the surface and re-cook) — the
18
+ // designer's road polylines feed that pipeline game-side; a painted-on decal would lie
19
+ // about where the carriageway really is.
20
+ import * as THREE from 'three/webgpu';
21
+ import { loadModel } from '../core/assets.js';
22
+ import { flattenToGeometry } from './scatter.js';
23
+ import { townMaterial } from './kit.js';
24
+
25
+ export async function buildDesign(scene, design, { heightAt = () => 0, solid = true, defaultTex = null } = {}) {
26
+ const byFile = new Map(); // `${dir}/${file}|${tex}` -> { url, tex, mats: [] }
27
+ const _p = new THREE.Vector3(), _q = new THREE.Quaternion(), _s = new THREE.Vector3();
28
+ const _up = new THREE.Vector3(0, 1, 0);
29
+ for (const e of design.prefabs ?? []) {
30
+ const bare = e.file.replace(/\.(fbx|glb)$/i, '');
31
+ const ext = e.ext ?? (/\.glb$/i.test(e.file) ? 'glb' : 'fbx');
32
+ const url = `${e.dir}/${bare}`;
33
+ e._url = `${url}.${ext}`;
34
+ const tex = e.tex ?? defaultTex;
35
+ const key = `${e._url}|${tex}`;
36
+ if (!byFile.has(key)) byFile.set(key, { url: e._url, tex, mats: [] });
37
+ byFile.get(key).mats.push(new THREE.Matrix4().compose(
38
+ _p.set(e.x, e.y ?? heightAt(e.x, e.z), e.z),
39
+ _q.setFromAxisAngle(_up, e.ry ?? 0),
40
+ _s.setScalar(e.s ?? 1),
41
+ ));
42
+ }
43
+ const meshes = [];
44
+ let count = 0, failed = 0;
45
+ for (const { url, tex, mats } of byFile.values()) {
46
+ try {
47
+ const master = await loadModel(url, tex ? { texture: tex } : {});
48
+ const flat = flattenToGeometry(master);
49
+ if (!flat) { failed++; continue; }
50
+ // textured prefabs ride the shared town pipeline (one material per atlas, lamp-lit);
51
+ // an untextured one gets a plain slate rather than crashing the whole batch pass
52
+ const mat = tex ? await townMaterial(tex, url) : new THREE.MeshStandardMaterial({ color: 0x9a938a, roughness: 0.9 });
53
+ const mesh = new THREE.InstancedMesh(flat.geometry, mat, mats.length);
54
+ mats.forEach((m, i) => mesh.setMatrixAt(i, m));
55
+ mesh.instanceMatrix.needsUpdate = true;
56
+ mesh.castShadow = mats.length < 12;
57
+ mesh.receiveShadow = true;
58
+ const file = url.split('/').pop();
59
+ mesh.name = `${solid ? 'scatter' : 'scatterDeco'}:${file}`;
60
+ mesh.matrixAutoUpdate = false;
61
+ mesh.updateMatrix();
62
+ scene.add(mesh);
63
+ meshes.push(mesh);
64
+ count += mats.length;
65
+ } catch (e) {
66
+ failed++;
67
+ console.warn(`[design] prefab batch ${url} failed`, e?.message ?? e);
68
+ }
69
+ }
70
+ console.log(`[design] '${design.name ?? 'unnamed'}': ${count} prefabs in ${meshes.length} batches${failed ? ` (${failed} files failed)` : ''}${design.roads?.length ? ` · ${design.roads.length} road route(s) carried for the survey` : ''}`);
71
+ return { meshes, count, roads: design.roads ?? [] };
72
+ }
@@ -0,0 +1,146 @@
1
+ // THE HEIGHTFIELD BUILDER (world splits, step 4) — the chunked terrain mesh + cook harness
2
+ // both games carried byte-identically around their own paint and material:
3
+ //
4
+ // · ONE height lattice (every vertex computed exactly once), chunk meshes aligned to it,
5
+ // normals from lattice CENTRAL DIFFERENCES so chunk borders cannot seam
6
+ // · THE TERRAIN COOK — the lattice fill is ~1.74M heightAt calls and the paint pass as
7
+ // many again, all deterministic: cooked to ONE self-writing bin
8
+ // [u32 hlen][JSON {ver,N} pad4][H f32 N²][sand u8 N²][paint u8 4/vert rgba-as-rgbs]
9
+ // fetched by NAME (the game bakes its content fingerprints into cookName, so stale
10
+ // bins 404 and re-cook), recorded back through the dev middleware on a cold boot
11
+ // · the SAND LATTICE (u8 N²) — the game's paint writes it, other systems (grass blade
12
+ // fields) query exactly what the ground shows; returned for the game's sandAt()
13
+ //
14
+ // The GAME supplies: heightAt, the cook name + ver, the node material, and makePaint —
15
+ // a factory receiving { hAt, step, N, sandLattice } and returning the per-vertex painter
16
+ // (x, z, h, li, lj, colors, sands, k), the exact signature both counties already use.
17
+ import * as THREE from 'three/webgpu';
18
+
19
+ export async function buildHeightfield({
20
+ worldSize, segments, chunkTarget = 360,
21
+ heightAt, cookName, cookVer,
22
+ material, makePaint,
23
+ saveRoute = '/__save-cookbin', cookRoute = '/assets/cook',
24
+ }) {
25
+ const CHUNKS = Math.max(1, Math.round(worldSize / chunkTarget));
26
+ if (segments % CHUNKS !== 0) throw new Error(`segments ${segments} not divisible by ${CHUNKS} chunks`);
27
+ const CSEG = segments / CHUNKS; // segments per chunk side
28
+ const CSIZE = worldSize / CHUNKS; // metres per chunk side
29
+ const STEP = worldSize / segments; // lattice spacing
30
+ const N = segments + 1; // lattice verts per side
31
+
32
+ let cook = null;
33
+ try {
34
+ const r = await fetch(`${cookRoute}/${cookName}`);
35
+ if (r.ok) {
36
+ const buf = await r.arrayBuffer();
37
+ const hlen = new DataView(buf).getUint32(0, true);
38
+ const head = JSON.parse(new TextDecoder().decode(new Uint8Array(buf, 4, hlen)));
39
+ if (head.N === N) {
40
+ let o = 4 + hlen;
41
+ cook = { H: new Float32Array(buf.slice(o, o + N * N * 4)) };
42
+ o += N * N * 4;
43
+ cook.sand = new Uint8Array(buf.slice(o, o + N * N));
44
+ o += N * N;
45
+ cook.paint = new Uint8Array(buf.slice(o));
46
+ console.log(`[terrain] COOKED lattice+paint from ${cookName} — heightAt/paint compute skipped`);
47
+ }
48
+ }
49
+ } catch (e) { console.warn('[terrain] cook bin missing/unreadable — falling back to the SLOW compute path (first boot after a VER bump is expected; repeated boots are not)', e?.message ?? e); }
50
+
51
+ // ---- height lattice: every terrain vertex height, computed once ----
52
+ const H = cook ? cook.H : new Float32Array(N * N);
53
+ if (!cook) {
54
+ for (let j = 0; j < N; j++) {
55
+ const z = -worldSize / 2 + j * STEP;
56
+ for (let i = 0; i < N; i++) H[j * N + i] = heightAt(-worldSize / 2 + i * STEP, z);
57
+ }
58
+ }
59
+ const hAt = (i, j) => H[Math.min(N - 1, Math.max(0, j)) * N + Math.min(N - 1, Math.max(0, i))];
60
+ const sandLattice = cook ? cook.sand : new Uint8Array(N * N);
61
+ const paintVertex = cook ? null : makePaint({ hAt, step: STEP, N, sandLattice });
62
+
63
+ // ---- build the chunk grid ----
64
+ const group = new THREE.Group();
65
+ group.name = 'terrain';
66
+ let chunkIdx = 0, vertsPerChunk = 0;
67
+ const recChunks = cook ? null : [];
68
+ for (let cj = 0; cj < CHUNKS; cj++) {
69
+ for (let ci = 0; ci < CHUNKS; ci++) {
70
+ const geo = new THREE.PlaneGeometry(CSIZE, CSIZE, CSEG, CSEG);
71
+ geo.rotateX(-Math.PI / 2);
72
+ const cx = -worldSize / 2 + (ci + 0.5) * CSIZE; // chunk centre (mesh position)
73
+ const cz = -worldSize / 2 + (cj + 0.5) * CSIZE;
74
+ const pos = geo.attributes.position;
75
+ const colors = new Float32Array(pos.count * 3);
76
+ const sands = new Float32Array(pos.count);
77
+ const normals = geo.attributes.normal;
78
+ vertsPerChunk = pos.count;
79
+ for (let k = 0; k < pos.count; k++) {
80
+ const x = pos.getX(k) + cx, z = pos.getZ(k) + cz;
81
+ // lattice indices — chunk grids align exactly to the global lattice
82
+ const li = Math.round((x + worldSize / 2) / STEP);
83
+ const lj = Math.round((z + worldSize / 2) / STEP);
84
+ const h = hAt(li, lj);
85
+ pos.setY(k, h);
86
+ // normal from lattice central differences — identical on both sides of
87
+ // a chunk border (computeVertexNormals would seam: edge verts lack
88
+ // neighbour faces from the adjacent chunk). Cheap H arithmetic — always live.
89
+ const nx = -(hAt(li + 1, lj) - hAt(li - 1, lj)) / (2 * STEP);
90
+ const nz = -(hAt(li, lj + 1) - hAt(li, lj - 1)) / (2 * STEP);
91
+ const inv = 1 / Math.hypot(nx, 1, nz);
92
+ normals.setXYZ(k, nx * inv, inv, nz * inv);
93
+ if (cook) {
94
+ // cooked paint: 4 bytes/vert (r,g,b,sand) in chunk build order
95
+ const po = (chunkIdx * pos.count + k) * 4;
96
+ colors[k * 3] = cook.paint[po] / 255;
97
+ colors[k * 3 + 1] = cook.paint[po + 1] / 255;
98
+ colors[k * 3 + 2] = cook.paint[po + 2] / 255;
99
+ sands[k] = cook.paint[po + 3] / 255;
100
+ } else {
101
+ paintVertex(x, z, h, li, lj, colors, sands, k);
102
+ }
103
+ }
104
+ geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
105
+ geo.setAttribute('aSand', new THREE.BufferAttribute(sands, 1));
106
+ recChunks?.push({ colors, sands });
107
+ chunkIdx++;
108
+ const mesh = new THREE.Mesh(geo, material);
109
+ mesh.position.set(cx, 0, cz);
110
+ mesh.receiveShadow = true;
111
+ mesh.name = `terrain:${ci},${cj}`;
112
+ group.add(mesh);
113
+ }
114
+ }
115
+ console.log(`[terrain] ${CHUNKS}x${CHUNKS} chunks (${CSIZE}m, ${CSEG}x${CSEG} segs each) — frustum-culled${cook ? ' · cooked' : ''}`);
116
+
117
+ // record mode: persist lattice + paint for the next boot (dev middleware writes the file)
118
+ if (recChunks) {
119
+ try {
120
+ let head = JSON.stringify({ ver: cookVer, N });
121
+ while ((head.length + 4) % 4) head += ' '; // 4-byte-align the Float32 payload
122
+ const hb = new TextEncoder().encode(head);
123
+ const paint = new Uint8Array(chunkIdx * vertsPerChunk * 4);
124
+ recChunks.forEach(({ colors, sands }, cIdx) => {
125
+ for (let k = 0; k < sands.length; k++) {
126
+ const po = (cIdx * sands.length + k) * 4;
127
+ paint[po] = Math.min(255, Math.round(colors[k * 3] * 255));
128
+ paint[po + 1] = Math.min(255, Math.round(colors[k * 3 + 1] * 255));
129
+ paint[po + 2] = Math.min(255, Math.round(colors[k * 3 + 2] * 255));
130
+ paint[po + 3] = Math.min(255, Math.round(sands[k] * 255));
131
+ }
132
+ });
133
+ const out = new Uint8Array(4 + hb.length + H.byteLength + sandLattice.byteLength + paint.byteLength);
134
+ const dv = new DataView(out.buffer);
135
+ dv.setUint32(0, hb.length, true);
136
+ out.set(hb, 4);
137
+ let o = 4 + hb.length;
138
+ out.set(new Uint8Array(H.buffer), o); o += H.byteLength;
139
+ out.set(sandLattice, o); o += sandLattice.byteLength;
140
+ out.set(paint, o);
141
+ fetch(`${saveRoute}?name=${cookName}`, { method: 'POST', body: out });
142
+ console.log(`[terrain] cooked → ${cookName} (${(out.length / 1048576).toFixed(1)}MB)`);
143
+ } catch (e) { console.warn('[terrain] cook save failed', e); }
144
+ }
145
+ return { group, sandLattice, N, step: STEP, cooked: !!cook };
146
+ }