sindicate 0.12.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 +69 -0
- package/docs/plans/kiwi-road-recon.md +97 -0
- package/docs/plans/road-system.md +142 -0
- package/docs/plans/three-cities.md +43 -0
- package/package.json +11 -3
- package/src/core/assets.js +15 -0
- package/src/systems/missions.js +2 -2
- package/src/systems/shatter.js +2 -1
- package/src/ui/dialogue.js +2 -1
- package/src/ui/pauseMenu.js +18 -11
- package/src/ui/saveLoadScreen.js +13 -9
- package/src/ui/theme.js +34 -0
- package/src/world/bridgeSplice.js +176 -0
- package/src/world/design.js +72 -0
- package/src/world/geo.js +53 -0
- package/src/world/heightfield.js +146 -0
- package/src/world/kit.js +924 -0
- package/src/world/permanentWay.js +161 -0
- package/src/world/placement.js +309 -0
- package/src/world/railFormation.js +185 -0
- package/src/world/roadKit.js +110 -0
- package/src/world/roadLink.js +270 -0
- package/src/world/roadNetwork.js +118 -0
- package/src/world/roadTerrain.js +263 -0
- package/src/world/waterLevels.js +95 -0
- package/src/world/waterMeshes.js +168 -0
- package/src/world/worldBase.js +493 -0
- package/tools/ExportKiwiRoadMaterials.cs +145 -0
- package/tools/ExportKiwiRoadRefs.cs +94 -0
- package/tools/buildKiwiRoadsPack.mjs +36 -0
- package/tools/designer-mcp.mjs +103 -0
- package/tools/designer.js +316 -0
- package/tools/devPlugin.js +324 -0
- package/tools/fbxToGlb.mjs +115 -0
- package/tools/glbWrite.mjs +62 -0
- package/tools/rebuildSimpleRoundabouts.mjs +211 -0
- package/tools/registry.json +45 -0
- package/tools/roadKitAnalyze.mjs +366 -0
- package/tools/sindicate-cli.mjs +71 -0
- package/tools/truthToAssemblies.mjs +160 -0
- package/tools/unityMeshToGlb.mjs +95 -0
- package/tools/unityPrefabToAssembly.mjs +276 -0
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.
|
|
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,11 +29,19 @@
|
|
|
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",
|
|
36
37
|
"three-mesh-bvh": "^0.9.10"
|
|
37
38
|
},
|
|
38
|
-
"license": "UNLICENSED"
|
|
39
|
+
"license": "UNLICENSED",
|
|
40
|
+
"bin": {
|
|
41
|
+
"sindicate": "tools/sindicate-cli.mjs"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"three": "^0.184.0",
|
|
45
|
+
"three-mesh-bvh": "^0.9.10"
|
|
46
|
+
}
|
|
39
47
|
}
|
package/src/core/assets.js
CHANGED
|
@@ -498,6 +498,21 @@ export async function instantiate(url, opts) {
|
|
|
498
498
|
// CONTRACT: these materials are shared game-wide — never mutate one on an instance;
|
|
499
499
|
// clone first (see player.applyMoralityLook).
|
|
500
500
|
const atlasMatCache = new Map();
|
|
501
|
+
|
|
502
|
+
// NodeMaterial.clone() (three r184 WebGPU) silently DROPS the texture and node slots —
|
|
503
|
+
// a private tint clone (player morality, enemy variants, shatter shards) came back
|
|
504
|
+
// bare white. Clone through THIS and the slots ride along.
|
|
505
|
+
export function cloneMat(m) {
|
|
506
|
+
const c = m.clone();
|
|
507
|
+
for (const k of ['map', 'normalMap', 'roughnessMap', 'metalnessMap', 'aoMap', 'emissiveMap', 'alphaMap', 'bumpMap']) {
|
|
508
|
+
if (m[k] && !c[k]) c[k] = m[k];
|
|
509
|
+
}
|
|
510
|
+
for (const k of ['colorNode', 'emissiveNode', 'roughnessNode', 'positionNode', 'opacityNode', 'normalNode']) {
|
|
511
|
+
if (m[k] && !c[k]) c[k] = m[k];
|
|
512
|
+
}
|
|
513
|
+
return c;
|
|
514
|
+
}
|
|
515
|
+
|
|
501
516
|
export function applyAtlas(root, tex, { emissive = null } = {}) {
|
|
502
517
|
root.traverse((o) => {
|
|
503
518
|
if (!o.isMesh) return;
|
package/src/systems/missions.js
CHANGED
|
@@ -37,7 +37,7 @@ async function captureThumb(game) {
|
|
|
37
37
|
try {
|
|
38
38
|
const { renderer, scene, camera } = game;
|
|
39
39
|
if (!renderer?.readRenderTargetPixelsAsync || !scene || !camera) return null;
|
|
40
|
-
const w =
|
|
40
|
+
const w = 640, h = 360; // pane-sized: 256px thumbs blown up to the detail pane read as mush
|
|
41
41
|
const rt = (captureThumb._rt ||= new THREE.RenderTarget(w, h));
|
|
42
42
|
renderer.setRenderTarget(rt);
|
|
43
43
|
renderer.render(scene, camera);
|
|
@@ -51,7 +51,7 @@ async function captureThumb(game) {
|
|
|
51
51
|
const ctx = c.getContext('2d'), img = ctx.createImageData(w, h);
|
|
52
52
|
img.data.set(buf); // top-left origin — no flip (per map.js)
|
|
53
53
|
ctx.putImageData(img, 0, 0);
|
|
54
|
-
return c.toDataURL('image/jpeg', 0.
|
|
54
|
+
return c.toDataURL('image/jpeg', 0.7);
|
|
55
55
|
} catch (e) { console.warn('[save] thumbnail failed', e?.message ?? e); return null; }
|
|
56
56
|
}
|
|
57
57
|
|
package/src/systems/shatter.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// bounce + settle on the floor, then fade them away. Pure CPU integration on a tiny pool — no engine,
|
|
4
4
|
// no per-shard lights, nothing resident (the meshes are added on smash and removed when faded).
|
|
5
5
|
import * as THREE from 'three/webgpu';
|
|
6
|
+
import { cloneMat } from '../core/assets.js';
|
|
6
7
|
|
|
7
8
|
const G = 17; // gravity (m/s²-ish, tuned for snappy low-poly debris)
|
|
8
9
|
const REST = 0.34; // floor bounce restitution
|
|
@@ -26,7 +27,7 @@ export class Shatter {
|
|
|
26
27
|
const tmpl = frags[i % frags.length];
|
|
27
28
|
const m = tmpl.clone(true);
|
|
28
29
|
// independent material instances so each shard fades on its own
|
|
29
|
-
m.traverse((o) => { if (o.material) o.material = Array.isArray(o.material) ? o.material.map((x) => x
|
|
30
|
+
m.traverse((o) => { if (o.material) o.material = Array.isArray(o.material) ? o.material.map((x) => cloneMat(x)) : cloneMat(o.material); });
|
|
30
31
|
m.scale.multiplyScalar(scale);
|
|
31
32
|
const pivot = tmpl.userData?.pivot;
|
|
32
33
|
let vel;
|
package/src/ui/dialogue.js
CHANGED
|
@@ -42,6 +42,7 @@ let nodeHook = null;
|
|
|
42
42
|
export function setDialogueOptionHandler(fn) { optionHandler = fn; }
|
|
43
43
|
export function setDialogueNodeHook(fn) { nodeHook = fn; }
|
|
44
44
|
import { padMode, buttonCanvas } from './menuHints.js';
|
|
45
|
+
import { THEME } from './theme.js';
|
|
45
46
|
|
|
46
47
|
// ── the two-shot, in numbers ────────────────────────────────────────────────────────────────────
|
|
47
48
|
// The lens stands in FRONT of the NPC (he has already turned to face the player, so his front faces
|
|
@@ -499,7 +500,7 @@ export class Dialogue {
|
|
|
499
500
|
#dlg .dlg-byekey { display:inline-flex; align-items:center; vertical-align:middle; margin-right:9px; }
|
|
500
501
|
#dlg .dlg-byekey.kbd { width:18px; height:18px; justify-content:center; line-height:17px; font-size:12px;
|
|
501
502
|
font-weight:700; color:#f4e3b0; border:1px solid #8a6a3a; border-radius:4px; background:rgba(40,28,14,0.92); }
|
|
502
|
-
|
|
503
|
+
` + (THEME.dialogueCss || '');
|
|
503
504
|
document.head.appendChild(css);
|
|
504
505
|
}
|
|
505
506
|
}
|
package/src/ui/pauseMenu.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { getQualityName, setQualityName, PRESETS } from '../core/quality.js';
|
|
7
7
|
import { renderHints, refreshHints, padMode } from './menuHints.js';
|
|
8
8
|
import { showSaveLoad } from './saveLoadScreen.js';
|
|
9
|
+
import { THEME } from './theme.js';
|
|
9
10
|
|
|
10
11
|
// PAUSE CONTENT is GAME data (setPauseContent in engineSetup):
|
|
11
12
|
// extraItems(game, {resume,close}) → [{label, action}] — game pages (CHARACTER/JOURNAL…)
|
|
@@ -17,10 +18,10 @@ import { showSaveLoad } from './saveLoadScreen.js';
|
|
|
17
18
|
let PAUSE = { saves: null, extraItems: null, settingsRows: null, onQualityChange: null };
|
|
18
19
|
export function setPauseContent(c = {}) { PAUSE = { ...PAUSE, ...c }; }
|
|
19
20
|
|
|
20
|
-
const GOLD =
|
|
21
|
-
const TXT =
|
|
22
|
-
const MUT =
|
|
23
|
-
const FONT =
|
|
21
|
+
const GOLD = THEME.gold;
|
|
22
|
+
const TXT = THEME.text;
|
|
23
|
+
const MUT = THEME.mut;
|
|
24
|
+
const FONT = THEME.font;
|
|
24
25
|
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
|
|
25
26
|
const CORNER_SVG = `<svg viewBox="0 0 44 44" width="30" height="30" fill="none" stroke="${GOLD}" stroke-linecap="round"><path d="M3 24 V3 H24" stroke-width="2"/><path d="M9 19 V9 H19" stroke-width="1" opacity="0.6"/><path d="M3 3 h9 l-9 9 z" fill="${GOLD}" stroke="none"/></svg>`;
|
|
26
27
|
const DIVIDER_SVG = `<svg viewBox="0 0 220 14" width="200" height="12" fill="none" stroke="${GOLD}" stroke-width="1.4" stroke-linecap="round"><line x1="8" y1="7" x2="94" y2="7"/><line x1="126" y1="7" x2="212" y2="7"/><circle cx="8" cy="7" r="1.6" fill="${GOLD}"/><circle cx="212" cy="7" r="1.6" fill="${GOLD}"/><path d="M110 1 l7 6 l-7 6 l-7 -6 z" fill="${GOLD}"/></svg>`;
|
|
@@ -32,11 +33,15 @@ function el(tag, css, text) {
|
|
|
32
33
|
return e;
|
|
33
34
|
}
|
|
34
35
|
function framedPanel(w = 420) {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
36
|
+
const ground = THEME.panelCss
|
|
37
|
+
?? `color:${TXT};border:1px solid rgba(232,200,122,0.55);border-radius:3px;`
|
|
38
|
+
+ 'background:linear-gradient(157deg,#241b11 0%,#160f09 58%,#0d0805 100%);box-shadow:0 26px 74px rgba(0,0,0,0.86);';
|
|
39
|
+
const p = el('div', `position:relative;width:${w}px;max-width:90vw;padding:28px 36px 24px;${ground}`);
|
|
40
|
+
if (THEME.filigree) {
|
|
41
|
+
p.appendChild(el('div', 'position:absolute;inset:6px;border:1px solid rgba(232,200,122,0.26);border-radius:2px;pointer-events:none;'));
|
|
42
|
+
const c = (pos, rot) => { const d = el('div', `position:absolute;${pos}width:30px;height:30px;pointer-events:none;transform:rotate(${rot}deg);`); d.innerHTML = CORNER_SVG; p.appendChild(d); };
|
|
43
|
+
c('left:3px;top:3px;', 0); c('right:3px;top:3px;', 90); c('right:3px;bottom:3px;', 180); c('left:3px;bottom:3px;', 270);
|
|
44
|
+
}
|
|
40
45
|
return p;
|
|
41
46
|
}
|
|
42
47
|
|
|
@@ -55,8 +60,10 @@ export function showPauseMenu(game, opts = {}) {
|
|
|
55
60
|
const close = () => { if (closed) return; closed = true; cancelAnimationFrame(raf); window.removeEventListener('keydown', onKey); root.remove(); };
|
|
56
61
|
const resume = () => { close(); opts.onResume?.(); };
|
|
57
62
|
|
|
58
|
-
const
|
|
59
|
-
const
|
|
63
|
+
const ACT = THEME.btnActive ?? { bg: GOLD, color: '#1a1207', border: GOLD };
|
|
64
|
+
const OFF = THEME.btnCss ? { bg: '', color: '', border: '' } : { bg: 'rgba(232,200,122,0.06)', color: TXT, border: 'rgba(232,200,122,0.45)' };
|
|
65
|
+
const hl = (b, on) => { b.style.background = on ? ACT.bg : OFF.bg; b.style.color = on ? ACT.color : OFF.color; b.style.borderColor = on ? ACT.border : OFF.border; };
|
|
66
|
+
const BTN = THEME.btnCss ?? `padding:9px 20px;border:1px solid rgba(232,200,122,0.45);border-radius:5px;cursor:pointer;font-family:inherit;letter-spacing:2px;font-size:15px;background:rgba(232,200,122,0.06);color:${TXT};transition:background .12s,color .12s;`;
|
|
60
67
|
const rowWrap = () => el('div', 'display:flex;align-items:center;gap:14px;margin:12px 0;padding:5px 6px;border-radius:5px;');
|
|
61
68
|
const rowLabel = (t) => el('div', `width:110px;text-align:right;color:${MUT};font-size:14px;letter-spacing:1px;text-transform:uppercase;`, t);
|
|
62
69
|
|
package/src/ui/saveLoadScreen.js
CHANGED
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
// opts: { mode:'load'|'save', slots:[…ids], list:()=>saves, onLoad(slot), onSave(slot,name),
|
|
8
8
|
// onDelete(slot), onClose() }. Returns { close }.
|
|
9
9
|
import { renderHints, refreshHints, padMode } from './menuHints.js';
|
|
10
|
+
import { THEME } from './theme.js';
|
|
10
11
|
|
|
11
12
|
const UI = '/assets/ui/menu';
|
|
12
|
-
const GOLD =
|
|
13
|
-
const TXT =
|
|
14
|
-
const MUT =
|
|
15
|
-
const FONT =
|
|
13
|
+
const GOLD = THEME.gold;
|
|
14
|
+
const TXT = THEME.text;
|
|
15
|
+
const MUT = THEME.mut;
|
|
16
|
+
const FONT = THEME.font;
|
|
16
17
|
|
|
17
18
|
function el(tag, css, text) {
|
|
18
19
|
const e = document.createElement(tag);
|
|
@@ -27,7 +28,7 @@ export function showSaveLoad(opts = {}) {
|
|
|
27
28
|
const save = opts.mode === 'save';
|
|
28
29
|
const slotIds = opts.slots || ['1', '2', '3', '4', '5', '6'];
|
|
29
30
|
|
|
30
|
-
const root = el('div', `position:fixed;inset:0;z-index:130;display:flex;flex-direction:column;font-family:${FONT};background
|
|
31
|
+
const root = el('div', `position:fixed;inset:0;z-index:130;display:flex;flex-direction:column;font-family:${FONT};background:${THEME.screenBg};`);
|
|
31
32
|
root.appendChild(el('div', `position:absolute;inset:0;background:#000 url(${UI}/title_bg.jpg) center/cover;opacity:0.08;`));
|
|
32
33
|
root.appendChild(el('div', 'position:absolute;inset:0;background:linear-gradient(180deg,rgba(10,7,5,0.86),rgba(10,7,5,0.96));'));
|
|
33
34
|
const head = el('div', 'position:relative;padding:44px 60px 16px;border-bottom:1px solid rgba(232,200,122,0.22);flex:none;');
|
|
@@ -49,9 +50,12 @@ export function showSaveLoad(opts = {}) {
|
|
|
49
50
|
|
|
50
51
|
const showDetail = (rec) => {
|
|
51
52
|
if (!rec) { detail.innerHTML = `<div style="flex:1;display:flex;align-items:center;justify-content:center;color:${MUT};letter-spacing:2px">${save ? 'NEW SAVE' : 'EMPTY'}</div>`; return; }
|
|
52
|
-
const img = rec.thumb ? `<img src="${rec.thumb}" style="width:100%;height:100%;object-fit:cover">`
|
|
53
|
-
: `<div style="
|
|
54
|
-
|
|
53
|
+
const img = rec.thumb ? `<img src="${rec.thumb}" style="width:100%;height:100%;object-fit:cover;display:block">`
|
|
54
|
+
: `<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:${MUT};font-size:12px;letter-spacing:3px">NO PREVIEW</div>`;
|
|
55
|
+
// a FRAMED 16:9 plate, not a full-pane stretch — a small thumb blown to the pane reads as mush
|
|
56
|
+
detail.innerHTML = `<div style="padding:22px 24px 0"><div style="aspect-ratio:16/9;max-height:340px;margin:0 auto;`
|
|
57
|
+
+ `background:linear-gradient(135deg,#1a130b,#0d0906);border:1px solid rgba(232,200,122,0.28);border-radius:3px;`
|
|
58
|
+
+ `box-shadow:0 10px 30px rgba(0,0,0,0.5), inset 0 0 40px rgba(0,0,0,0.35);overflow:hidden">${img}</div></div>`
|
|
55
59
|
+ `<div style="padding:20px 24px"><div style="color:${GOLD};font-size:22px;letter-spacing:2px;font-weight:700">${rec.name || rec.title || opts.defaultTitle || 'Saved game'}</div>`
|
|
56
60
|
+ `<div style="color:${MUT};font-size:13px;margin-top:8px;line-height:1.9">${rec.auto ? 'Autosave · ' : ''}${fmtDate(rec.savedAt)}<br>Played ${fmtTime(rec.playtime)} · ${opts.moneyLabel ?? 'Purse $'}${rec.money ?? 0}${rec.objective ? '<br>' + rec.objective : ''}</div></div>`;
|
|
57
61
|
};
|
|
@@ -59,7 +63,7 @@ export function showSaveLoad(opts = {}) {
|
|
|
59
63
|
const rebuild = () => {
|
|
60
64
|
const saves = opts.list ? opts.list() : [];
|
|
61
65
|
const bySlot = {}; saves.forEach((s) => { bySlot[String(s.slot)] = s; });
|
|
62
|
-
const ids = save ? slotIds : ['auto', ...slotIds].filter((id) => bySlot[id]); // load: only filled; save: all manual slots
|
|
66
|
+
const ids = save ? slotIds : [...new Set(['auto', ...slotIds])].filter((id) => bySlot[id]); // load: only filled (auto deduped — a game may list it in its own slots); save: all manual slots
|
|
63
67
|
listEl.innerHTML = ''; items = [];
|
|
64
68
|
if (!ids.length) { listEl.appendChild(el('div', `color:${MUT};opacity:0.8;padding:12px;`, 'No saved games yet.')); showDetail(null); return; }
|
|
65
69
|
ids.forEach((id) => {
|
package/src/ui/theme.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// THE UI THEME — the rule, codified after the fable title-menu incident: the ENGINE owns
|
|
2
|
+
// the FUNCTIONALITY of a screen; the GAME owns its LOOK. Every value here is declarative
|
|
3
|
+
// (a game ships them as src/game/uiTheme.json and registers it in engineSetup with
|
|
4
|
+
// setUiTheme(theme)), so a re-dress never touches engine code.
|
|
5
|
+
//
|
|
6
|
+
// The defaults are the neutral dark leather-and-gold dress the engine screens shipped with
|
|
7
|
+
// (Dustwater's palette — western's own uiTheme.json restates it explicitly). The engine UI
|
|
8
|
+
// modules are lazily imported AFTER engineSetup runs, so module-load reads of THEME see the
|
|
9
|
+
// game's registration.
|
|
10
|
+
//
|
|
11
|
+
// font — the screens' font stack
|
|
12
|
+
// gold/text/mut — accent, body text, muted labels
|
|
13
|
+
// panelCss — the framed panel's ground (popup confirms, the pause panel). Replaces the
|
|
14
|
+
// default gradient+border block; the engine still positions and sizes it.
|
|
15
|
+
// filigree — draw the gold corner flourishes + inner rule on panels (default true; a
|
|
16
|
+
// parchment skin usually wants them off)
|
|
17
|
+
// btnCss — the standard button plate (settings rows, confirms, save-slot actions)
|
|
18
|
+
// btnActive — { bg, color, border } for the highlighted/selected state
|
|
19
|
+
// screenBg — full-screen browser background (save/load screen ground)
|
|
20
|
+
// dialogueCss — appended after the dialogue panel's own CSS; override any #dlg selector
|
|
21
|
+
export const THEME = {
|
|
22
|
+
font: "Copperplate,'Copperplate Gothic Bold','Palatino','Book Antiqua',Georgia,serif",
|
|
23
|
+
gold: '#e8c87a',
|
|
24
|
+
text: '#ecdfc2',
|
|
25
|
+
mut: '#b6a577',
|
|
26
|
+
panelCss: null, // null = the engine's dark leather gradient
|
|
27
|
+
filigree: true,
|
|
28
|
+
btnCss: null, // null = the engine's gold-outline plate
|
|
29
|
+
btnActive: null, // null = { bg: gold, color: '#1a1207', border: gold }
|
|
30
|
+
screenBg: '#0a0705',
|
|
31
|
+
dialogueCss: '',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function setUiTheme(t = {}) { Object.assign(THEME, t); }
|