sindicate 0.10.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/docs/plans/world-splits-recon.json +387 -0
- package/package.json +1 -1
- package/src/core/assets.js +18 -0
- package/src/core/engine.js +9 -0
- package/src/core/retarget.js +4 -1
- package/src/core/tune.js +50 -0
- package/src/systems/nav.js +4 -1
- package/src/ui/cartograph.js +6 -0
- package/src/ui/deathScreen.js +3 -3
- package/src/ui/dialogue.js +85 -14
- package/src/ui/intro.js +1 -1
- package/src/ui/pauseMenu.js +32 -3
- package/src/ui/saveLoadScreen.js +3 -3
- package/src/ui/titleScreen.js +28 -34
- package/src/ui/weaponWheel.js +138 -0
- package/src/ui/worldmap.js +119 -161
- package/tools/devPlugin.js +69 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
{
|
|
2
|
+
"summary": "Map both games' world layers for the final engine extraction",
|
|
3
|
+
"agentCount": 2,
|
|
4
|
+
"logs": [],
|
|
5
|
+
"result": {
|
|
6
|
+
"western": {
|
|
7
|
+
"summary": "WESTERN's world layer is ~6,100 lines of builder code over ~6,000 lines of data (src/world/data/). The engine (Sindicate/src/world/) already holds the leaf systems — scatter, tileManager, grass, water materials, sky/weather, collision BVH, flora, walkHumps, lampField/Glow, footprints — and western already feeds them through the established setX(content)/self-register seams (setFloraTerrain, registerHumps, setRoadNetwork, setChartWorld, setHerdTerrain...). What remains game-side is the world-BUILDING machinery: the terrain math kit + solvers (terrainData.js), the chunked heightfield + cook harness (terrain.js), the Synty kit-stamper with doors/datums (town.js), the placement/cook/occupancy machinery (zones.js), the permanent-way builder (railway.js), the chord-deck bridge stamper (bridges.js), and the World facade (world.js) whose capsule-controller half is duplicated nearly line-for-line in fable. Two files (regions.js, road.js) are dead fable-fork leftovers with zero importers in western. The extraction is factoring each file into pure machinery (engine) + county tables and policy regexes (game), guarded by the cook-fingerprint scheme (TERRAIN/ZONES_COOK_VER + FIELDS/PAINT/WATER/CROP_SIG in bin filenames) which makes behavioral drift loudly visible.",
|
|
8
|
+
"modules": [
|
|
9
|
+
{
|
|
10
|
+
"file": "/Users/nick/Sites/western/src/world/terrainData.js",
|
|
11
|
+
"lines": 1611,
|
|
12
|
+
"what": "Headless terrain math + the county's survey. The single heightAt() source of truth: base noise, mesa relief, valley cuts, lake bowls, road benches, rail formation, bridge deck holds — composed at module scope over the data tables.",
|
|
13
|
+
"mechanism": "The richest extraction target: fbm/ridged/terrace/smoothstep noise kit; chunked-AABB polyline distance indices (roadDistance, riverInfo with per-course enclosing boxes, railInfo with _railBox — all the same pattern); the bridge SPLICER (road x river segment intersection -> chord anchors -> road re-splice -> BRIDGE_SPOTS, multi-pass to fixed point); valleyCut (asymmetric banks keyed to baked curvature, point-bar shelves, single-gate core damp, road approach cone mode); fallBench (terrace stamp aimed at a bed step); the RAIL FORMATION SOLVER (survey ground -> level yards -> Lipschitz ceil/floor envelopes at MAX_GRADE -> midpoint -> diffusion vertical curves -> merge-weld spurs -> trestle gate past FILL_MAX); applyRail ownership-weighted corridor blend (1/(d/w)^8); crossing discovery + deck lift; the 5-pass river wy bake (solved PASS 0 + unsolved 1-4 machinery); waterSurfaceAt; WATER_SIG generic FNV table hasher.",
|
|
14
|
+
"content": "ROADS (16 hand-annotated polylines), TOWN/PADS anchors, RIVERS/LAKES resolved from data/waterSolved.js, baseHeight + outerRelief noise recipes (the county's bones), SEA_LEVEL/COAST_Z parked, Albion-era anchors (GUILD/MOOR/CRYPT... — fable fork residue kept for straggler imports), biome factor stubs returning 0, VILLAGE_CASTLE_ROAD back-compat alias (only dead road.js wanted it). At module end it self-registers: setRoadNetwork({roads, roadDistance}) into sindicate/systems/roadGraph.js — the pattern the whole extraction follows.",
|
|
15
|
+
"deps": "three/addons ImprovedNoise (only non-data import — stays headless), ./scale.js, data/outposts.js, data/railLine.js, data/waterSolved.js; registers into sindicate/systems/roadGraph.js.",
|
|
16
|
+
"extractability": "needs-seams"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"file": "/Users/nick/Sites/western/src/world/terrain.js",
|
|
20
|
+
"lines": 607,
|
|
21
|
+
"what": "The rendered ground: chunked vertex-painted heightfield mesh + the terrain cook, re-exporting terrainData.",
|
|
22
|
+
"mechanism": "Chunk-grid mesh builder over ONE shared height lattice with central-difference normals (seam-free borders by construction); the self-writing TERRAIN COOK (bin fetch -> replay, record -> POST /__save-cookbin, fingerprints in the FILENAME so stale bins 404); the TSL ground-material stack — world-space dual-scale detail texture, bump relief, and the whole rain response (puddle mask from flatness x hollows x ruts, sky-mirror roughnessNode, splash cells, snow cover) reading engine weatherUniforms; sandAt ground-code lattice; PAINT_SIG hash-from-the-table pattern.",
|
|
23
|
+
"content": "PAINT palette (the county's colours), paintVertex biome layering order (sage under grass under fields under town under rock), analytic wheel ruts + plough furrows in the fragment shader, TERRAIN_COOK_VER changelog, building aprons off dustwaterLayout.",
|
|
24
|
+
"deps": "sindicate/core/assets, sindicate/world/weatherUniforms, three/tsl, ./terrainData.js, ./scale.js, data/biomes.js, data/dustwaterLayout.js.",
|
|
25
|
+
"extractability": "needs-seams"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"file": "/Users/nick/Sites/western/src/world/town.js",
|
|
29
|
+
"lines": 1172,
|
|
30
|
+
"what": "THE KIT-STAMPER: merges Synty kit pieces into per-building geometries, stamps area-local layouts anywhere (buildArea), splits doors onto hinges, seats props by datum rules.",
|
|
31
|
+
"mechanism": "buildingGeometry merge + _geoCache (tiled plots share geometry); det3/flipWinding/bake negative-determinant mirror handling; the DOOR pipeline (DOOR_SPLIT leaf extraction, pane re-weld, clusterDoorways union-find, hinge solving from measured hardware, one BatchedMesh per material with per-instance culling, 8m door-collider grid, updateDoors/toggleDoor with hx=0 as the open switch); buildLadders collinear-merge; buildArea area-local transform stamper with the whole seating law (GROUND_OFF/SNAP/BED datum, floorUnder/interiorUnder oriented-box floor claims, area.datum='ground', landform/overLandform support tests, drop rule, floorSeat); per-file InstancedMesh prop batching with mirrored-instance second batch; fence-panel capsules; townMaterial one-pipeline-per-(atlas,flip) cache.",
|
|
32
|
+
"content": "Dustwater EXTRAS (fountain, hitching rails, derived lamp posts), PROP_DELETE/PROP_GROUND click ledgers, BUILDING_ATLASES recolour list, HINGE_HW measured hardware table, SHELL_RE/SHELL_BODY/NO_COLLIDE/AIRBORNE_OK/GROUND_ONLY policy regexes (Synty-kit-specific, belong in a kit contract config), LAYOUT/PIECES imports, dressTownCenter map-editor path. Exports the LIVE MUTABLE REGISTRIES everything shares: colliders, buildingObjects, doors, ladders, doorColliders, PROPS, BUILDINGS.",
|
|
33
|
+
"deps": "sindicate core/assets + world/scatter (flattenToGeometry) + world/lampField, ./terrain.js, data/townMap.js, data/dustwaterLayout.js; consumed by world.js, zones.js, railway.js, bridges.js.",
|
|
34
|
+
"extractability": "needs-seams"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"file": "/Users/nick/Sites/western/src/world/zones.js",
|
|
38
|
+
"lines": 1230,
|
|
39
|
+
"what": "Wilderness scatter manifest + placement cook + grass occupancy + water meshes — the county's dressing, streamed through the engine's TileField/scatterResident/GrassField.",
|
|
40
|
+
"mechanism": "placementField rejection sampler (box/accept/sPow/fitY) + listField grid generator, both behind the PLACEMENT COOK (record/replay Float32 slices, sigs in filename, slice-count mismatch guard, determinism FNV probe); residentSpec/res/resFlat/resHump wrappers encoding the collision-by-name contract (scatter:/scatterGround:) and walkHumps registration identical on cooked+computed paths; the grass OCCUPANCY GRID (circle mark, oriented-rect mark, and per-triangle building-footprint rasterizer); buildRiverMesh ribbon chunker (200m chunks, per-vertex depth/flow/drop bake from solved hydraulics, seam-duplicated boundary rows); lake sheet builder with aDepth/aRClip bakes on ONE shared material; buildSea; refreshRiverDepths re-bake; map-editor overrides applier addressed slice:index.",
|
|
41
|
+
"content": "The manifest itself (~60 Synty species rows with counts/colliders/salts), SOW crop table + CROP_SIG, all the biome accepts (openDesert, cactusPatch, sage, riparian, badland, townGround...), field builders (cropRows, cutStraw windrows, scarecrows, fieldFences + dedupe + capsule pass), TOWN_BOX/VERGE_BOX, ZONES_COOK_VER changelog, grassAccept gates, straw recolour of engine grass.",
|
|
42
|
+
"deps": "sindicate world/scatter + tileManager + grass + water + flora + walkHumps, core/utils + quality, ./terrain.js, ./scale.js, ./town.js (colliders/BUILDINGS/PROPS/buildingObjects), data/biomes + railLine + railway + outposts + townMap; registers setFloraTerrain at import.",
|
|
43
|
+
"extractability": "needs-seams"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"file": "/Users/nick/Sites/western/src/world/world.js",
|
|
47
|
+
"lines": 747,
|
|
48
|
+
"what": "World facade: build orchestration + ALL spatial queries — the capsule controller, 2D collider walk, BVH oracles, LOS, region streamer, per-frame world update.",
|
|
49
|
+
"mechanism": "The World class body is engine-grade and duplicated near-verbatim in fable: collide() (oriented boxes, capsules, circles, height bands, door grid), moveCapsule/_capsuleStep substepped capsule-vs-BVH with up-resolve on walkable normals + snap-to-ground, solidPush vehicle wall push-out, walkSurfaceAt/navFloorAt straight-down BVH floor oracles, losBlocked/segmentBlocked, probe() dev capsule drop, _updateRegionStreamer (approach + idle-trickle deferred builds spliced into worker-built side BVHs), the update() plumbing (LOD rebucket with stationary skip, grass/tile streaming, lamp-field nightly resort), BVH assembly from the scatter:/scatterCol: name contract.",
|
|
50
|
+
"content": "build()'s county recipe (terrain -> town -> coach stops -> outposts -> railway -> doors -> bridges BEFORE zones -> BVH -> refreshRiverDepths), the EXCLUDE regex with the quarry_ exception, saloon interior lamp positions, zoneName(), deferred-asset prewarm paths, and the boot-time engine registrations: setHerdTerrain, setTumbleweedTerrain, setFootstepTerrain, setLawBench, setCoachContent, setTrainContent, plus importing ./chartData.js for its side effect.",
|
|
51
|
+
"deps": "Everything: sindicate world/* + systems/* content hooks, ./terrain, ./town, ./zones, ./railway, ./bridges, all data modules, game/data/drops.",
|
|
52
|
+
"extractability": "needs-seams"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"file": "/Users/nick/Sites/western/src/world/railway.js",
|
|
56
|
+
"lines": 378,
|
|
57
|
+
"what": "Lays the permanent way as geometry: track/ballast panel batches, trestles, junction castings, tunnel-ridge portals, station areas.",
|
|
58
|
+
"mechanism": "matrixFor YXZ chord placement (yaw then pitch then z-scale — adaptive panel lengths in one batch); flattenToGeometry split track/ballast into two batches so ballast runs through junctions; trestle builder (deck-only casting under flying panels + one bent per joint, walkables returned for a side BVH); mirrorX proper geometry reflection (positions+normals+winding); single() one-off casting placer; seeded-rng ridge dresser.",
|
|
59
|
+
"content": "Synty track/mine asset paths and their measured 2:1 gauge mating, tunnel portal art direction (rock rows, bore quad, core plugs — heavily Nick-iterated), Dustwater station/loading-stage stamping via RAILWAY_AREAS, scatterDeco walk-over contract.",
|
|
60
|
+
"deps": "sindicate core/assets + world/scatter, ./terrainData (heightAt), ./town (buildArea, colliders, buildingObjects), data/railway.js.",
|
|
61
|
+
"extractability": "needs-seams"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"file": "/Users/nick/Sites/western/src/world/bridges.js",
|
|
65
|
+
"lines": 159,
|
|
66
|
+
"what": "Seats a timber span asset on each BRIDGE_SPOTS chord; header states it is ported from fable's bridges.js (177 lines).",
|
|
67
|
+
"mechanism": "Measure the GLB deck plane (triangle-area histogram gave DECK_Y=0.80), centre XZ-only, tile 2 pieces along the chord at near-native plank pitch, size the across-cover from the ACTUAL imported coach route polylines (coachLegsFor/laneCover walking riverInfo), push mesh into buildingObjects for the BVH.",
|
|
68
|
+
"content": "SM_Env_Bridge_01 + frontier atlas, DECK_Y/LIP constants, COACH_LANE/TRAV_LANE imports from game/coachRoute.js and traveller lane law.",
|
|
69
|
+
"deps": "./terrain.js (heightAt, riverInfo, BRIDGE_SPOTS), sindicate core/assets (instantiate), ./town.js (buildingObjects), ../game/coachRoute.js.",
|
|
70
|
+
"extractability": "needs-seams"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"file": "/Users/nick/Sites/western/src/world/scale.js",
|
|
74
|
+
"lines": 84,
|
|
75
|
+
"what": "The county's size law: WORLD_SCALE and the PIN / SITE-LOCAL / BEHAVIOUR three-bucket doctrine.",
|
|
76
|
+
"mechanism": "The authoring-time scale constructors S/P/off, dens polyline re-densifier, poly compose — plus SEGMENTS derived from CHUNKS so divisibility holds by construction. Deliberately import-free leaf.",
|
|
77
|
+
"content": "WORLD_SCALE=2, BASE_SIZE 2160, BASE_INNER, LATTICE 3.2727, CHUNK_TARGET 360 — all county numbers.",
|
|
78
|
+
"deps": "None (load-bearing: data/railLine.js depends on it staying a leaf).",
|
|
79
|
+
"extractability": "needs-seams"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"file": "/Users/nick/Sites/western/src/world/chartData.js",
|
|
83
|
+
"lines": 24,
|
|
84
|
+
"what": "Pure registration module: hands the county's survey to the engine cartograph.",
|
|
85
|
+
"mechanism": "None — it IS the target pattern: data self-registers via setChartWorld/setChartGazetteer/setChartSettlements before the boot chart bake (imported for side effect by world.js).",
|
|
86
|
+
"content": "All of it: wiring of heightAt/roads/rivers/rail/outposts/buildings/settlements/shops into the engine chart hooks.",
|
|
87
|
+
"deps": "sindicate/ui/cartograph.js, ./terrainData, data/*, ../game/data/county + shops.",
|
|
88
|
+
"extractability": "game-specific"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"file": "/Users/nick/Sites/western/src/world/regions.js",
|
|
92
|
+
"lines": 53,
|
|
93
|
+
"what": "DEAD CODE in western: fable's Aldenvale region table, byte-identical to /Users/nick/Sites/fable/src/world/regions.js. Zero importers here.",
|
|
94
|
+
"mechanism": "regionAt point-in-blob lookup (trivial). The REGIONS blob shape is live in fable's cartograph/discovery — worth an engine setChartRegions hook when fable ports.",
|
|
95
|
+
"content": "Frostreach/Gravemoor/King's Vale... — fable content, meaningless in Dustwater.",
|
|
96
|
+
"deps": "None.",
|
|
97
|
+
"extractability": "game-specific"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"file": "/Users/nick/Sites/western/src/world/road.js",
|
|
101
|
+
"lines": 66,
|
|
102
|
+
"what": "DEAD CODE in western: fable's Oakhollow->Kingsreach cobble paver (castle kit piece, fantasy atlas). Zero importers; terrainData exports VILLAGE_CASTLE_ROAD solely to keep it compiling.",
|
|
103
|
+
"mechanism": "A tidy generic polyline paver (two jittered lanes of an instanced cluster piece, river-bed skip) — live in fable, worth engine adoption THERE, deletable here along with the alias.",
|
|
104
|
+
"content": "Castle-kit asset paths, fantasy road polyline import.",
|
|
105
|
+
"deps": "sindicate/core/assets, ./terrainData (heightAt, riverInfo, VILLAGE_CASTLE_ROAD).",
|
|
106
|
+
"extractability": "game-specific"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"file": "/Users/nick/Sites/western/src/world/data/ (14 files)",
|
|
110
|
+
"lines": 6059,
|
|
111
|
+
"what": "The county itself: biomes.js (383 — biome field fns + FIELDS/FIELDS_SIG), railLine.js (491 — survey plan + measured kit constants + formation params, import-free leaf), railway.js (600 — solved panels/areas/trestleSpans/inTunnelRidge), waterSolved.js (899 — GENERATED by scripts/solve-water.mjs), settlements.js (774), dustwaterLayout.js + frontierLayout.js (2536 — GENERATED by parse_demo_scene.py / parse_frontier_scene.py), outposts.js, coachStop.js, townMap.js, campfires.js, lodItems.js.",
|
|
112
|
+
"mechanism": "Mostly none — this is the content that STAYS. Exceptions worth engine notes: biomes.js's '(x,z) -> 0..1 field read by both paint and scatter' is the engine's biome contract shape; the offline generators (water solver, demo-scene parsers, verify-* scripts in western/scripts/) are engine-grade tooling candidates for a later pass.",
|
|
113
|
+
"content": "Everything — this directory is the definition of 'the county stays game data'.",
|
|
114
|
+
"deps": "scale.js only (and each other); deliberately renderer-free so verify scripts run headless.",
|
|
115
|
+
"extractability": "game-specific"
|
|
116
|
+
}
|
|
117
|
+
],
|
|
118
|
+
"counterpart": "Fable's src/world/ is the same organism one fork back — the shared shapes are explicit. (1) Identical file skeleton: terrain.js / terrainData.js (976 lines: same fbm/heightAt/ROADS/RIVERS/BRIDGE_SPOTS/PADS architecture — western's is the evolved twin that added the rail formation, solved water, and cook sigs; western still exports fable's GUILD/MOOR/CRYPT anchors as parked 'Albion-era' names). (2) world.js contains the SAME World class — groundAt, collide, moveCapsule/_capsuleStep, losBlocked, probe at nearly the same line numbers — plus fable-only dungeon/castle floor branches; this is the single highest-value union (engine WorldBase, game overrides). (3) bridges.js: western's header says outright 'Ported from fable's bridges.js (177 lines)' — the chord-seating + infos {x,z,y,r,mesh} return shape is already a shared contract. (4) western's regions.js and road.js ARE fable's files, dead here — when fable ports, regionAt/REGIONS wants a cartograph setChartRegions hook and road.js's polyline paver is a clean engine candidate. (5) zones.js: same placement-cook/scatter architecture (987 lines). (6) Where western unified all settlement building onto ONE buildArea stamper, fable still has seven bespoke stampers (villages, castleTown, ironkeep, goblinCamp, camps, kingsreachMarket, bracklow) — the union should crown western's kit-stamper as the engine's and port fable's places onto area layouts. Fable already consumes sindicate (12 engine imports in its world.js alone), so both games are positioned to swap onto engine world modules file by file.",
|
|
119
|
+
"plan": "Registry patterns this project already uses and each step must follow: (a) engine module exports setXContent(obj) merging into a module-local record (setFloraTerrain, setRoadNetwork, setChartWorld); (b) game DATA modules self-register at module end, before consumers import (chartData.js, terrainData's setRoadNetwork — 'data registers itself; consumers follow'); (c) engine modules may own live-mutable registries games push into (walkHumps.registerHumps; town.js's colliders/buildingObjects arrays become this); (d) cook fingerprints live in bin FILENAMES — every extraction must either keep outputs bit-identical (same seeds/constants passed in, verified by the zones determinism hash and cook names) or bump the VER knowingly.\n\nOrdered so western boots after every step:\n\nSTEP 0 — Delete dead fable residue: western regions.js, road.js, and terrainData's VILLAGE_CASTLE_ROAD alias. Pure removal, boot test. (Optionally park them in fable's tree where they are live.)\n\nSTEP 1 — engine world/terrainKit.js: the pure math kit — makeFbm(noise) (ImprovedNoise injected so determinism is the game's), clamp/lerp/smoothstep, ridged, terrace, segDist, buildPolylineIndex(pts, near) returning {distanceAt, nearestAt} (unifies the road/river/rail/swamp chunked-AABB pattern), and fnvSig(table) (unifies WATER_SIG/FIELDS_SIG/PAINT_SIG/CROP_SIG). terrainData/zones/biomes switch imports; cook names must not change — that is the regression test.\n\nSTEP 2 — engine world/railFormation.js: solveFormation({nodes,s,w,flat}, {maxGrade, smoothPasses, mergeIn/Out, fillMax}, groundFn, mergedSolve) -> {y,g,e}; makeRailCorridor(lines, {formHalf, batter, batterMin/Max, railhead}) -> {railInfo, railDistance, applyRail}; findCrossings(roads, railLines) + crossingBlend factory. Game keeps railLine.js untouched and composes in terrainData. Verify RAIL_FORMATION arrays bit-identical (terrain cook name unchanged = proof).\n\nSTEP 3 — engine world/waterSurvey.js: densifyCourse/confluence/estuary passes, bakeReachDepths, bakeValleyCurvature, applyPlungePools, makeRiverIndex -> riverInfo, valleyCut factory, fallBench factory, applyLakes, bakeWaterLevels (passes 0-4), makeWaterSurfaceAt, and spliceBridges(roads, rivers, opts) -> BRIDGE_SPOTS (the splicer + final bank push). The game's terrainData keeps its exact call ORDER (the TDZ/ordering comments are the spec). Regression: scripts/verify-water.mjs + WATER_SIG/cook names unchanged.\n\nSTEP 4 — engine world/heightfield.js + world/groundMaterial.js: buildHeightfield({worldSize, chunks, segments, heightAt, paint(vertexCtx), cookName, cookVer, saveEndpoint}) doing lattice fill, central-diff normals, chunk grid, cook read/record; makeGroundMaterial({detailTex, baseColor(vcol,ctx), extraNodes}) wrapping the rain/puddle/splash/snow/roughness/bump stack that already reads engine weatherUniforms (western injects ruts/furrows as extraNodes, keeps PAINT + paintVertex + VER). First boot may re-cook; behavior otherwise identical.\n\nSTEP 5 — engine world/kitStamper.js (the centerpiece): move buildingGeometry/_geoCache, bake/flipWinding/det3, the whole door pipeline (clusterDoorways, doorway, makeDoors, buildDoorBatches, updateDoors, toggleDoor, indexDoors/doorsAt + doorColliders), buildLadders, townMaterial cache, buildArea + prop batching + fence capsules + all datum/seating law. Engine owns the live registries (colliders, buildingObjects, doors, ladders) — world.js/zones.js/railway.js/bridges.js re-import them from the engine path. Game supplies a KIT CONTRACT via setKitContent({resolveDir/resolveTex, NO_COLLIDE, HINGE_HW, SHELL_RE, AIRBORNE_OK, GROUND_ONLY, GROUND_OFF/SNAP/BED, propDelete/propGround}) — western town.js shrinks to that contract + EXTRAS + a buildTown that is buildArea(LAYOUT at origin). Regression: doors/leaves/slabs boot log counts + scripts/verify_dustwater.mjs.\n\nSTEP 6 — engine world/placement.js + world/waterMeshes.js: placementField/listField + the cook record/replay harness (sig parts and save endpoint injected), residentSpec/res/resFlat/resHump (already wrap engine scatterResident; resHump already calls engine registerHumps), the occupancy grid + building-triangle rasterizer + editor-overrides applier; buildRiverMesh, lake sheet builder, buildSea, refreshRiverDepths (parameterized by RIVERS/LAKES shapes + engine water materials). western zones.js becomes accepts + SOW + manifest + wiring. Regression: determinism hash and zones cook name unchanged.\n\nSTEP 7 — engine world/worldBase.js: WorldBase carrying collide/moveCapsule/_capsuleStep/solidPush/walkSurfaceAt/navFloorAt/losBlocked/segmentBlocked/probe/colliderInfo/showCollider/_updateRegionStreamer and the update() plumbing, reading {groundAt parts, doorsAt, colliders, tiles/grass/lampGlow handles} it owns or is handed. Western's World extends it keeping build() recipe, zoneName, saloon lamps, quarry EXCLUDE extension, and all setX registrations. Last because it touches everything — and it is the step that unblocks fable, whose identical class body then ports onto the same base with dungeon/castle overrides.\n\nSTEP 8 (parallel/optional) — engine world/permanentWay.js: matrixFor chord placement, track/ballast split batching, trestle deck+bents+walkables, mirrorX, single() — game keeps data/railway.js, station stamping and the tunnel-ridge dressing. Bridges' chord-deck stamper can follow as a thin engine helper (spanOnChord) or stay game-side; it is 159 lines and both games already agree on its contract.\n\nThroughout: never move a data/ file; every moved mechanism takes its numbers as parameters so FIELDS_SIG/WATER_SIG/CROP_SIG/PAINT_SIG and the cook names prove equivalence for free."
|
|
120
|
+
},
|
|
121
|
+
"fable": {
|
|
122
|
+
"summary": "Fable's world layer (22 modules + data/, ~6,850 lines) is the starter code western forked, already half-ported onto the Sindicate engine (sky/weather/scatter/tileManager/grass/water/flora/collision/footprints all consumed from 'sindicate/world/*', with setX content registries in use). It splits into three strata: (1) shared machinery that western also carries near-verbatim and belongs in the engine — the World capsule/BVH/streamer core, the chunked terrain-lattice+cook builder, the placement-manifest cook + occupancy grid, the polyline solvers (roads/rivers/bridge-splice), and the building placer; regions.js and road.js are byte-identical between the two games; (2) fable-only set-piece streamers (castleTown/ironkeep/bloodfang/goblinCamp) that share one unextracted streamer pattern among themselves; (3) pure fable content (anchors, LAKES/RIVERS/ROADS/PADS, settlement/camp/POI layouts, biome manifest). One latent bug found: floraFields.js forestness() references FOREST and sstep without importing them — crashes on any non-cooked boot when placement accepts run.",
|
|
123
|
+
"modules": [
|
|
124
|
+
{
|
|
125
|
+
"file": "/Users/nick/Sites/fable/src/world/world.js",
|
|
126
|
+
"lines": 722,
|
|
127
|
+
"what": "World assembly + spatial queries: build() orchestrates every builder, plus the capsule character controller and update loop",
|
|
128
|
+
"mechanism": "Capsule controller (moveCapsule/_capsuleStep: substepped BVH shapecast depenetration, up-normal resolve, snap-to-ground, heightfield floor), 2D collide() (circle/capsule/oriented-box with height bands + jumpable 'low' fences), LOS/segment ray tests over a BVH list (_shotBVHs/_rayBlocked), region streamer (_updateRegionStreamer: deferred builds, nearest-first + idle trickle, worker side-BVHs), deferred-asset pre-warm pen, fixed lamp-pool reposition (never toggle lights — WebGPU recompile law), LOD rebucket cadence with stationary skip, probe()/colliderInfo() dev tools",
|
|
129
|
+
"content": "Build order for every fable region (town→villages→camps→bracklow→carriages→POIs→bridges→zones→dungeon→castle→goblinCamp→ironkeep→bloodfang→road), zoneName() names, dungeon/Ironkeep fog+ambience overrides, underIronkeepRoof(), bloodfangFloorAt(), engine registrations at module scope (setFloraTerrain/setFloraFields/setHerdTerrain/setHorseWorld/setFootstepTerrain)",
|
|
130
|
+
"deps": "sindicate: sky, weather, footprints, scatter (LODs/impostor), collision (buildBVH/Async), assets, utils, quality, flora, herd, horse, footsteps; fable: terrain(Data), ironkeepMask, all sibling builders",
|
|
131
|
+
"extractability": "needs-seams"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"file": "/Users/nick/Sites/fable/src/world/terrain.js",
|
|
135
|
+
"lines": 304,
|
|
136
|
+
"what": "Chunked heightfield mesh + vertex-paint + binary terrain cook",
|
|
137
|
+
"mechanism": "Shared height lattice (each vert computed once), central-difference normals (seam-proof chunk borders), TERRAIN_COOK_VER binary cache (heights+sand+paint, self-writing via /__save-cookbin), sandAt() baked lattice query, TSL node material (grass tex × vertex colour, aSand drop, uWet/uCover weather response)",
|
|
138
|
+
"content": "Fable palette (grass/moor/snow/blight/swamp/jungle/coast colours), paintVertex biome rules, plaza/road tinting, Ironkeep-mask Discard in colorNode, coast/riverbed paint logic, CHUNK_TARGET 360",
|
|
139
|
+
"deps": "./terrainData (re-exports it all), ./ironkeepMask, sindicate assets + weatherUniforms, three/tsl",
|
|
140
|
+
"extractability": "needs-seams"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"file": "/Users/nick/Sites/fable/src/world/terrainData.js",
|
|
144
|
+
"lines": 976,
|
|
145
|
+
"what": "Headless terrain math + ALL geographic data; the single heightAt source of truth",
|
|
146
|
+
"mechanism": "fbm/ridged/terrace noise, catmull densify, chunked-AABB polyline distance accelerators (roadDistance/riverInfo/swampFactor), 4-pass river water-level bake (cross-section min → terminus monotone → riffle relief → estuary descent), fixed-point bridge-crossing splice producing BRIDGE_SPOTS (segment intersection, perpendicular deck chords, rejoin walking, bank push), pad stamping with river-carve precedence, outer-ring relief mask, road bench grading, waterSurfaceAt() single source of truth",
|
|
147
|
+
"content": "WORLD_SIZE 4320/SEGMENTS/SEA_LEVEL/COAST_Z, every landmark anchor (TOWN/GUILD/MOOR/DWARF/CASTLE/CITY…), 25 LAKES with adaptive bowls, 9 RIVERS with confluence widths, ~30 ROADS, ~45 PADS, biome factor fields (snow/blight/swamp/jungle/moor), Ironkeep sculpt loading + sculptKey; registers setRoadNetwork(ROADS, roadDistance) at module end",
|
|
148
|
+
"deps": "three/addons ImprovedNoise, sindicate roadGraph (registration only) — deliberately headless, no three/webgpu",
|
|
149
|
+
"extractability": "needs-seams"
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"file": "/Users/nick/Sites/fable/src/world/zones.js",
|
|
153
|
+
"lines": 987,
|
|
154
|
+
"what": "The placement manifest: all wilderness/biome scatter, water meshes, streaming grass, tiles",
|
|
155
|
+
"mechanism": "placementField (deterministic mulberry32(1337) + space-hashed accepts + guard loop + fitY cliff seating), ZONES_COOK_VER binary placement cook (record/replay, slice refs for editor overrides, FNV determinism hash), residentSpec contract feeding TileField (ensure/write/free, per-tile collider registration), occupancy-grid grass gating (mark circle/markRect/rasterTri stamping real building-floor triangles), buildRiverMesh ribbon (per-vert depth/flow/drop attrs), buildSea flood-mask plane, refreshRiverDepths re-derive, palm bark+leaf albedo compositor, editor overrides.json application",
|
|
156
|
+
"content": "~150 manifest entries: all species per biome (meadow/forest/moor/coast/beach/alpine/blight/swamp/jungle/meadowlands/highland mountains), boxes, atlases, accept predicates, quality multipliers, riverside dressing, crypt/gallows set props",
|
|
157
|
+
"deps": "sindicate: scatter (5 variants), tileManager, grass, water materials, flora, utils, quality; fable: terrain, town, villages, camps, bracklow, data/townMap",
|
|
158
|
+
"extractability": "needs-seams"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"file": "/Users/nick/Sites/fable/src/world/town.js",
|
|
162
|
+
"lines": 294,
|
|
163
|
+
"what": "Oakhollow: buildings/props/fences data + builder; owns the shared colliders and buildingObjects registries",
|
|
164
|
+
"mechanism": "Building placer: road-distance-gradient nudge, lowest-footprint-sample seating (0.8m cap), LOD0/LOD3-GLB/impostor cross-fade registration, fence-run instancing with jumpable capsule colliders, per-file prop batching, dressTownCenter (editor TOWN_MAP matrices, dual FBX/GLB atlas flips, scatterCol:/scatterDeco: naming for BVH pickup)",
|
|
165
|
+
"content": "BUILDINGS/FENCES/PROPS/SPOTS/WANDER_POINTS for Oakhollow + moor/mill outliers",
|
|
166
|
+
"deps": "fable terrain, data/townMap; sindicate assets, quality, scatter, utils",
|
|
167
|
+
"extractability": "needs-seams"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"file": "/Users/nick/Sites/fable/src/world/villages.js",
|
|
171
|
+
"lines": 345,
|
|
172
|
+
"what": "~35 hamlets/towns: hand layouts + procedural preset() generator + builder",
|
|
173
|
+
"mechanism": "preset() deterministic ring generator (hashed id → rng), snowifyBuilding TSL node material (normalWorld-gated snow caps at LOD0+LOD3), bubble/deferred split (near.r) returning streamer closures, buildOneVillage = same building placer as town.js verbatim (nudge/seat/LOD/impostor) + per-frame yield",
|
|
174
|
+
"content": "VILLAGES data (layouts, styles, clearR/musicR, torchAt, props), SPOTS registration for every village at module end, drumfell/glendour bible anchors",
|
|
175
|
+
"deps": "fable terrain, town.js (SPOTS/colliders/buildingObjects mutation); sindicate assets, quality, scatter",
|
|
176
|
+
"extractability": "needs-seams"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"file": "/Users/nick/Sites/fable/src/world/camps.js",
|
|
180
|
+
"lines": 234,
|
|
181
|
+
"what": "War camps + goblin-camp integration surfaces + garrison spawner data",
|
|
182
|
+
"mechanism": "Grouped per-file instanced prop stamping with bubble/deferred split (buildOneCamp); circle colliders register at build time so no side-BVH needed",
|
|
183
|
+
"content": "CAMPS (layouts, clearR/baldR scatter integration radii), CAMP_SPAWNERS + RAVENSPIRE_COURT (combat spawn data living in a world file), atlas routing (dungeon/corrupt flags)",
|
|
184
|
+
"deps": "fable terrain; sindicate scatter",
|
|
185
|
+
"extractability": "game-specific"
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"file": "/Users/nick/Sites/fable/src/world/bracklow.js",
|
|
189
|
+
"lines": 139,
|
|
190
|
+
"what": "Bracklow loom-city quarter: fixed layout + builder",
|
|
191
|
+
"mechanism": "Same building placer pattern (LOD chain, buildingObjects) + generated wall runs from corner-pivot pieces",
|
|
192
|
+
"content": "B layout (gate, walkable quarter, skyline facade), PROPS/lamps, BRACKLOW anchor",
|
|
193
|
+
"deps": "fable terrain, town.js registries; sindicate assets/quality/scatter",
|
|
194
|
+
"extractability": "game-specific"
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"file": "/Users/nick/Sites/fable/src/world/carriages.js",
|
|
198
|
+
"lines": 90,
|
|
199
|
+
"what": "Fast-travel carriage network: stops, fares, NPC dialogue defs, stand dressing",
|
|
200
|
+
"mechanism": "Distance-priced fare + fade-to-black game-time teleport (carriageTravel); generated Carriage Master npcDefs",
|
|
201
|
+
"content": "CARRIAGE_STOPS + SPOTS registration, dialogue lines",
|
|
202
|
+
"deps": "fable terrain, town SPOTS; sindicate scatter; game object (g.teleport/ui/after)",
|
|
203
|
+
"extractability": "game-specific"
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"file": "/Users/nick/Sites/fable/src/world/pois.js",
|
|
207
|
+
"lines": 135,
|
|
208
|
+
"what": "Roadside POIs: waystones/shrines/loot camps/named landmarks/vantages",
|
|
209
|
+
"mechanism": "Plain scatter seating + chest base/lid split for openChest contract; NAMED_POIS auto-pin via mapIcon spread",
|
|
210
|
+
"content": "All POI coordinates, lore strings, POI_BUILD preset lists",
|
|
211
|
+
"deps": "fable terrain; sindicate assets/scatter",
|
|
212
|
+
"extractability": "game-specific"
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"file": "/Users/nick/Sites/fable/src/world/goblinCamp.js",
|
|
216
|
+
"lines": 274,
|
|
217
|
+
"what": "Goblin raider camps: procedural palisade layouts + one streamer for all camps",
|
|
218
|
+
"mechanism": "Mathematical palisade ring from measured wall-segment width (tangent-aligned, gate arc skip, towers), heightAt−pieceMinY ground seating, LOAD_R streamer + shared BVH, goblinCampLamps() pre-export for the night pool",
|
|
219
|
+
"content": "GOBLIN_CAMPS sites/sizes, interior layouts per size, KitForge texmap paths",
|
|
220
|
+
"deps": "fable terrain; sindicate utils/collision; /assets/goblincamp manifest",
|
|
221
|
+
"extractability": "game-specific"
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"file": "/Users/nick/Sites/fable/src/world/castleTown.js",
|
|
225
|
+
"lines": 308,
|
|
226
|
+
"what": "Kingsreach: KitForge town.json streamer (walled town + castle)",
|
|
227
|
+
"mechanism": "Manifest set-piece streamer: piece-GLB cache, per-primitive texmap routing, seat modes (floor/base/asis), 3rd-percentile baseY, instancing grouped by piece-mesh×texture, COLLIDE/NO_COLLIDE regexes, async BVH, _compiled reveal gate, hinged gate doors + portcullis with day/night animation and toggleable oriented-box gate colliders, ladder gathering for Climbing, road-tree culling",
|
|
228
|
+
"content": "KINGSREACH anchor, LOAD_R, regex filters, buried-foundation cull, market hookup",
|
|
229
|
+
"deps": "fable terrain/town colliders/kingsreachMarket; sindicate assets/utils/collision",
|
|
230
|
+
"extractability": "needs-seams"
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
"file": "/Users/nick/Sites/fable/src/world/ironkeep.js",
|
|
234
|
+
"lines": 580,
|
|
235
|
+
"what": "Ironkeep: POLYGON Dwarven Dungeon Map demo-scene streamer at the NE massif",
|
|
236
|
+
"mechanism": "Unity negX matrix conjugation, mirrored-instance (det<0) BackSide meshes, localStorage+committed overrides/additions editor merge, tree/grass swap to alpine biome foliage via scatterTree, boot-created brazier light pool, ironkeepFireLamps export, ironkeepMask bake trigger, INTERIOR_R visibility ring, merge-by-texture draws",
|
|
237
|
+
"content": "ANCHOR/LOAD_R/CULL/COLLIDE regexes, ALP_PINE_SPECS swap table, dwarfmap asset paths",
|
|
238
|
+
"deps": "fable terrainData (DWARF), ironkeepMask, town colliders, game/torchfire; sindicate assets/utils/collision/scatter/quality",
|
|
239
|
+
"extractability": "game-specific"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"file": "/Users/nick/Sites/fable/src/world/ironkeepMask.js",
|
|
243
|
+
"lines": 70,
|
|
244
|
+
"what": "Runtime-baked 2D 'under the hold's roof' mask",
|
|
245
|
+
"mechanism": "Chunked async bake: per 2m cell, up-ray (roof within 60m) AND down-ray (floor within 40m) against the set-piece BVH → DataTexture consumed by terrain colorNode Discard + ironkeepMaskAt() physics query in _capsuleStep",
|
|
246
|
+
"content": "IKM grid box around DWARF",
|
|
247
|
+
"deps": "fable terrainData; three",
|
|
248
|
+
"extractability": "needs-seams"
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"file": "/Users/nick/Sites/fable/src/world/bloodfang.js",
|
|
252
|
+
"lines": 268,
|
|
253
|
+
"what": "Ravenspire: POLYGON Dark Fantasy cathedral+graveyard streamer",
|
|
254
|
+
"mechanism": "Same negX/merge-by-texture×winding streamer family as ironkeep; canonical per-piece MATMAP (Godot prefab-derived) material routing, glass skip, hinged great doors kept out of the merge, crag baseY seating",
|
|
255
|
+
"content": "BLOODFANG/ANCHOR/GRAVE_OFFSET, COLLIDE/NO_COLLIDE regexes, DF texture conventions",
|
|
256
|
+
"deps": "fable town colliders; sindicate assets/collision; BufferGeometryUtils",
|
|
257
|
+
"extractability": "game-specific"
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"file": "/Users/nick/Sites/fable/src/world/dungeon.js",
|
|
261
|
+
"lines": 312,
|
|
262
|
+
"what": "The Barrow Depths: seeded procedural dungeon beneath the overworld",
|
|
263
|
+
"mechanism": "generateLayout(): seeded 5m-grid rooms (entry/boss/side crypts) + L-corridors, wall/door edge rules; in-browser pivot calibration (bbox-measured), instanced floors/walls/ceilings/doorframes, dual atlas-flip materials, torch FX + flame pool, dungeonColliders registry",
|
|
264
|
+
"content": "DUNGEON_ORIGIN/SPAWN/BOSS_POS, SEED, Synty dungeon kit dressing",
|
|
265
|
+
"deps": "sindicate assets/scatter(flatten)/utils; game/torchfire",
|
|
266
|
+
"extractability": "needs-seams"
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
"file": "/Users/nick/Sites/fable/src/world/bridges.js",
|
|
270
|
+
"lines": 177,
|
|
271
|
+
"what": "Auto-bridges at every BRIDGE_SPOTS crossing",
|
|
272
|
+
"mechanism": "Composed wooden span from kit pieces (module rhythm tiling, lanes, rails/posts, bed-stretched piers, deck-end stone stairs with live STEP_TUNE), stone-arch scaling for becks, bbox-centred piece instancing, decks join buildingObjects for walkable BVH; returns autopilot zone infos for road travellers",
|
|
273
|
+
"content": "FK Walkway_Wood/step piece paths, STEP_TUNE bake, w>7 wooden-vs-stone rule",
|
|
274
|
+
"deps": "fable terrain (BRIDGE_SPOTS), town buildingObjects; sindicate assets",
|
|
275
|
+
"extractability": "needs-seams"
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
"file": "/Users/nick/Sites/fable/src/world/road.js",
|
|
279
|
+
"lines": 66,
|
|
280
|
+
"what": "Oakhollow→Kingsreach cobble highway instancer",
|
|
281
|
+
"mechanism": "Two-lane overlapping cluster instancing along a densified polyline with yaw/scale/height jitter, river-bed skip — BYTE-IDENTICAL to western/src/world/road.js",
|
|
282
|
+
"content": "Piece path, LANES offsets, VILLAGE_CASTLE_ROAD consumer",
|
|
283
|
+
"deps": "fable terrainData; sindicate assets",
|
|
284
|
+
"extractability": "clean"
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
"file": "/Users/nick/Sites/fable/src/world/regions.js",
|
|
288
|
+
"lines": 53,
|
|
289
|
+
"what": "Named map regions + regionAt() lookup — BYTE-IDENTICAL to western's",
|
|
290
|
+
"mechanism": "Blob-circle membership with closest-relative tie-break",
|
|
291
|
+
"content": "7 REGIONS (labels, anchors, blobs, ranks) — this data is fable's despite the identical file (western never re-authored it)",
|
|
292
|
+
"deps": "none — leaf module",
|
|
293
|
+
"extractability": "clean"
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
"file": "/Users/nick/Sites/fable/src/world/floraFields.js",
|
|
297
|
+
"lines": 26,
|
|
298
|
+
"what": "Fable's moisture/forestness registered into engine flora via setFloraFields",
|
|
299
|
+
"mechanism": "Already the extraction seam (engine flora.js live-binding exports). BUG: forestness() uses FOREST and sstep, neither imported — ReferenceError on any cook-miss boot when placement accepts actually run (cooked boots never call accepts, masking it)",
|
|
300
|
+
"content": "Greenshade forest field + river/lake moisture model",
|
|
301
|
+
"deps": "fable terrainData; sindicate utils",
|
|
302
|
+
"extractability": "clean"
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
"file": "/Users/nick/Sites/fable/src/world/footstepSurfaces.js",
|
|
306
|
+
"lines": 24,
|
|
307
|
+
"what": "Footstep surface classifier registered via engine setFootstepTerrain",
|
|
308
|
+
"mechanism": "Already the seam: priority classifier (water>snow>mud>stone>earth>grass) closed over world",
|
|
309
|
+
"content": "PAVED plaza circles",
|
|
310
|
+
"deps": "fable terrain; consumed by sindicate footsteps",
|
|
311
|
+
"extractability": "clean"
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
"file": "/Users/nick/Sites/fable/src/world/kingsreachMarket.js",
|
|
315
|
+
"lines": 70,
|
|
316
|
+
"what": "Kingsreach plaza market dressing + throne",
|
|
317
|
+
"mechanism": "Editor-layout-driven placement with z-fight ground clearance; bbox-normalized throne seating",
|
|
318
|
+
"content": "Reads data/kingsreachLayout.js; streamed by castleTown",
|
|
319
|
+
"deps": "fable terrain, data/kingsreachLayout; sindicate assets",
|
|
320
|
+
"extractability": "game-specific"
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
"file": "/Users/nick/Sites/fable/src/world/data/",
|
|
324
|
+
"lines": 395,
|
|
325
|
+
"what": "Pure content: townMap.js (editor matrices), kingsreachLayout.js, oakhollowLayout.js, lodItems.js + lodConfig.json (impostor tier manifest for /lodconfig), ironkeepFloor/Ground.json",
|
|
326
|
+
"mechanism": "None — data only; lodItems/lodConfig is the same shape as western's data/ pair",
|
|
327
|
+
"content": "All of it",
|
|
328
|
+
"deps": "none (leaf data modules)",
|
|
329
|
+
"extractability": "game-specific"
|
|
330
|
+
}
|
|
331
|
+
],
|
|
332
|
+
"counterpart": "Western's src/world/ is the same skeleton fable forked, re-themed to a desert and further along the engine port. Byte-identical: regions.js and road.js. Same-role, same-machinery files: terrainData.js (1611 ln — same fbm/roadDistance/riverInfo chunked accelerators, BRIDGE_SPOTS, setRoadNetwork-at-module-end; adds scale.js WORLD_SCALE three-bucket law and moves solved rivers/lakes into data/waterSolved.js, rail into data/railLine.js), terrain.js (same lattice/cook/chunk builder, richer desert TSL shader), zones.js (same placementField/cook/TileField residentSpec + GrassField + water/river materials, but simple scatterResident only — no impostor chains; registers setFloraTerrain from zones instead of world), world.js (same World class: moveCapsule/_capsuleStep/collide/region streamer/sideBVHs — 631 diff lines but the machinery is line-for-line the same shape), town.js (1172 ln — same building placer grown into buildArea for settlements/outposts, plus doors machinery fable lacks). Western consumes MORE engine: walkHumps, lampField/lampGlow (replacing fable's hand-rolled world.js lamp pool), cartograph via self-registering chartData.js, setCoachContent/setTrainContent/setLawBench/setTumbleweedTerrain. Western-only: railway.js + scale.js. Fable-only: the four set-piece streamers (castleTown/ironkeep/bloodfang/goblinCamp), dungeon.js, ironkeepMask.js, villages/camps/carriages/pois. Shared shapes worth unioning into the engine: World capsule/BVH/streamer core, terrain lattice+cook builder, placement manifest cook + occupancy grid, river ribbon/sea/refreshRiverDepths, polyline solver toolkit (catmull/chunk-index/river-wy bake/bridge splice), building placer (nudge/seat/LOD/impostor), regionAt, laned-path instancer.",
|
|
333
|
+
"plan": "Registry patterns this project already uses and extraction should follow: (a) setX content hooks — engine holds machinery, game injects data/queries (setFloraTerrain/setFloraFields/setFootstepTerrain/setHerdTerrain/setRoadNetwork/setCoachContent); (b) data registers itself at module end (terrainData.js calls setRoadNetwork after building ROADS; western's chartData.js is a pure side-effect import); (c) live-binding exports replaced by registration (flora.js `export let moistureAt` + setFloraFields). Ordered steps:\n\n1. Fix fable floraFields.js first (import FOREST + smoothstep-as-sstep from terrainData) — latent ReferenceError on any cook-miss boot; cheap and de-risks everything after.\n2. Extract the World core from WESTERN into sindicate/world/worldCore.js (or a World base class): moveCapsule/_capsuleStep/collide/_rayBlocked/probe/colliderInfo/showCollider + the region streamer + a registered-BVH list (setWorldBVHs or world.registerBVH) so fable's castle/goblinCamp/ironkeep/bloodfang bvhs and western's sideBVHs both become list entries instead of hardcoded fields. Fable adopts by subclassing: keeps build() order, zoneName, fog overrides, underIronkeepRoof.\n3. Extract the terrain mesh builder into sindicate/world/terrainMesh.js: lattice fill + central-difference normals + chunk grid + cook bin read/record + sandAt, parameterized by { heightAt, paintVertex, makeMaterial, cookVer, cookKey }. Fable keeps its palette/paintVertex + ironkeep-mask Discard; western keeps its desert shader.\n4. Extract the polyline/terrain solver toolkit into sindicate/world/geo.js: catmull, segDist, chunked-AABB polyline index (generalizes roadDistance/riverInfo/swampFactor/roadBenchAt), the 4-pass river wy bake, the fixed-point bridge-crossing splice (emits BRIDGE_SPOTS), pad stamping, ridged/terrace/outer-ring helpers. Each game's terrainData.js shrinks toward data + composition and keeps registering setRoadNetwork at module end. Optionally adopt western's scale.js pattern engine-side as the canonical scaling law.\n5. Extract the placement layer into sindicate/world/manifest.js: placementField, cook record/replay + determinism hash + slice refs, residentSpec, editor-overrides application, occupancy grid (mark/markRect/rasterTri); and move buildRiverMesh/buildSea/refreshRiverDepths next to the engine water materials. zones.js in both games becomes: accept predicates + species manifest + grass accept.\n6. Extract the building placer into sindicate/world/settlement.js: road-gradient nudge, lowest-footprint seat, LOD3/impostor registration, bubble/deferred split, grouped prop stamper, with a decorate hook (fable passes snowifyBuilding). town.js/villages.js/bracklow.js/camps.js shrink to data; western town.js buildArea adopts the same kit.\n7. Promote the two byte-identical files: regionAt mechanism → engine (regions data stays per game, ideally feeding cartograph the way western's chartData.js does), road.js laned-path instancer → engine (piece path + polyline as args).\n8. Fable adopts western's already-extracted lamp machinery (lampField/lampGlow) to delete world.js's hand-rolled lamp pool, and walkHumps if wanted.\n9. Fable-extracted (western has no counterpart, but the GTA game will want it): a set-piece streamer base in sindicate/world/setpiece.js — LOAD_R gate, GLB piece cache, per-primitive texmap routing, optional Unity negX conjugation, mirrored-instance BackSide, COLLIDE/NO_COLLIDE filters, async BVH, _compiled reveal gate, door/gate animation + gate colliders. castleTown/ironkeep/bloodfang/goblinCamp become thin configs/subclasses. ironkeepMask.js generalizes into an interior-mask bake utility (box + BVH in, DataTexture + query out). dungeon.js's generateLayout could move later as an engine dungeon generator; its Synty-kit emit stays fable.\n\nStays fable-specific: all data (terrainData anchors/LAKES/RIVERS/ROADS/PADS, VILLAGES/CAMPS/BRACKLOW/POI/carriage layouts, regions data, zones species manifest, data/*), the four set-piece scenes' configs, dungeon content, kingsreachMarket, footstepSurfaces + floraFields (already seam-shaped registrations)."
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
"workflowProgress": [
|
|
337
|
+
{
|
|
338
|
+
"type": "workflow_phase",
|
|
339
|
+
"index": 1,
|
|
340
|
+
"title": "Recon"
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
"type": "workflow_agent",
|
|
344
|
+
"index": 1,
|
|
345
|
+
"label": "western-world",
|
|
346
|
+
"phaseIndex": 1,
|
|
347
|
+
"phaseTitle": "Recon",
|
|
348
|
+
"agentId": "afc91f46ae0cff8f3",
|
|
349
|
+
"model": "claude-fable-5",
|
|
350
|
+
"state": "done",
|
|
351
|
+
"startedAt": 1784914324020,
|
|
352
|
+
"queuedAt": 1784914324014,
|
|
353
|
+
"attempt": 1,
|
|
354
|
+
"lastToolName": "StructuredOutput",
|
|
355
|
+
"lastToolSummary": "WESTERN's world layer is ~6,100 lines of builder code over …",
|
|
356
|
+
"promptPreview": "Map WESTERN's world layer for extraction into the Sindicate engine. ls /Users/nick/Sites/western/src/world/ and read every file (large ones in chunks): terrain solvers, the town kit-stamper, zones/areas machinery, scatter, tiles, water — everything. For each module report mechanism vs content vs deps and an extractability verdict. Context: the engine lives at /Users/nick/Sites/Sindicate (src/syste…",
|
|
357
|
+
"lastProgressAt": 1784914660557,
|
|
358
|
+
"tokens": 229421,
|
|
359
|
+
"toolCalls": 30,
|
|
360
|
+
"durationMs": 336536,
|
|
361
|
+
"resultPreview": "{\"summary\":\"WESTERN's world layer is ~6,100 lines of builder code over ~6,000 lines of data (src/world/data/). The engine (Sindicate/src/world/) already holds the leaf systems — scatter, tileManager, grass, water materials, sky/weather, collision BVH, flora, walkHumps, lampField/Glow, footprints — and western already feeds them through the established setX(content)/self-register seams (setFloraTer…"
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
"type": "workflow_agent",
|
|
365
|
+
"index": 2,
|
|
366
|
+
"label": "fable-world",
|
|
367
|
+
"phaseIndex": 1,
|
|
368
|
+
"phaseTitle": "Recon",
|
|
369
|
+
"agentId": "a01592a2ed924126f",
|
|
370
|
+
"model": "claude-fable-5",
|
|
371
|
+
"state": "done",
|
|
372
|
+
"startedAt": 1784914324021,
|
|
373
|
+
"queuedAt": 1784914324014,
|
|
374
|
+
"attempt": 1,
|
|
375
|
+
"lastToolName": "StructuredOutput",
|
|
376
|
+
"lastToolSummary": "Fable's world layer (22 modules + data/, ~6,850 lines) is t…",
|
|
377
|
+
"promptPreview": "Map FABLE's world layer the same way. ls /Users/nick/Sites/fable/src/world/ and read every file (large ones in chunks): terrain, tiles, zones, villages, scatter, water, regions — everything. For each module report mechanism vs content vs deps and extractability. Context: the engine lives at /Users/nick/Sites/Sindicate; fable consumes it via file: symlink (check fable's imports from 'sindicate/' to…",
|
|
378
|
+
"lastProgressAt": 1784914643912,
|
|
379
|
+
"tokens": 217811,
|
|
380
|
+
"toolCalls": 40,
|
|
381
|
+
"durationMs": 319891,
|
|
382
|
+
"resultPreview": "{\"summary\":\"Fable's world layer (22 modules + data/, ~6,850 lines) is the starter code western forked, already half-ported onto the Sindicate engine (sky/weather/scatter/tileManager/grass/water/flora/collision/footprints all consumed from 'sindicate/world/*', with setX content registries in use). It splits into three strata: (1) shared machinery that western also carries near-verbatim and belongs …"
|
|
383
|
+
}
|
|
384
|
+
],
|
|
385
|
+
"totalTokens": 447232,
|
|
386
|
+
"totalToolCalls": 70
|
|
387
|
+
}
|
package/package.json
CHANGED
package/src/core/assets.js
CHANGED
|
@@ -13,6 +13,12 @@ export function setAtlasEmissiveHook(fn) { atlasEmissiveHook = fn; }
|
|
|
13
13
|
// it), not the engine — register per game: setClipCacheVersion(2). Defaults to 2
|
|
14
14
|
// (western's current rebake generation).
|
|
15
15
|
export function setClipCacheVersion(v) { CLIP_JSON_VER = v; }
|
|
16
|
+
|
|
17
|
+
// A registered CLIP PACK (e.g. Accentimations) is consulted BEFORE the per-game cooked
|
|
18
|
+
// cache: registerClipPack({ urlFor: (sourceStem) => url|null }). Misses fall through to
|
|
19
|
+
// the cooked cache, then the FBX parse — fully backwards compatible.
|
|
20
|
+
let clipPack = null;
|
|
21
|
+
export function registerClipPack(pack) { clipPack = pack; }
|
|
16
22
|
// Patched loader from vendor/ — merges ALL FBX animation layers per stack. The
|
|
17
23
|
// engine does NOT depend on a node_modules postinstall patch.
|
|
18
24
|
import { FBXLoader } from '../vendor/FBXLoader.js';
|
|
@@ -547,6 +553,18 @@ export async function loadClip(url, name) {
|
|
|
547
553
|
if (!clipCache.has(url)) {
|
|
548
554
|
const jsonName = clipJsonName(url);
|
|
549
555
|
const p = (async () => {
|
|
556
|
+
const packUrl = clipPack?.urlFor?.(url.split('/').pop().replace(/\.fbx$/i, ''));
|
|
557
|
+
if (packUrl) {
|
|
558
|
+
try {
|
|
559
|
+
const r = await fetch(packUrl);
|
|
560
|
+
if (r.ok) {
|
|
561
|
+
const clip = THREE.AnimationClip.parse(await r.json());
|
|
562
|
+
clip.name = name ?? url.split('/').pop().replace('.fbx', '');
|
|
563
|
+
tick(clip.name);
|
|
564
|
+
return clip;
|
|
565
|
+
}
|
|
566
|
+
} catch (e) { /* pack miss — fall through to the cooked cache */ }
|
|
567
|
+
}
|
|
550
568
|
try {
|
|
551
569
|
const r = await fetch(`/assets/clips/${jsonName}`);
|
|
552
570
|
if (r.ok) {
|
package/src/core/engine.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// engine.onFps((fps) => …) // 1s-window fps readout
|
|
16
16
|
// engine.start() // hands the loop to the renderer
|
|
17
17
|
import * as THREE from 'three/webgpu';
|
|
18
|
+
import { tune } from './tune.js';
|
|
18
19
|
|
|
19
20
|
export class Engine {
|
|
20
21
|
constructor({ renderer, scene, camera, input, post = null, isPaused = () => false, isUiModal = () => false }) {
|
|
@@ -43,6 +44,14 @@ export class Engine {
|
|
|
43
44
|
});
|
|
44
45
|
this.shakeT = 0;
|
|
45
46
|
|
|
47
|
+
// world-speed dial for the dev GUI — a plain contributor that reads the tune
|
|
48
|
+
// registry at call time (never an onChange closure over `this`: a second Engine
|
|
49
|
+
// construction would strand the first mid-tune and pin it against GC)
|
|
50
|
+
this.addTimeScale(() => tune.value('time', 'scale', 1));
|
|
51
|
+
tune.group('time', {
|
|
52
|
+
scale: { value: 1, min: 0.05, max: 2, step: 0.05 },
|
|
53
|
+
});
|
|
54
|
+
|
|
46
55
|
// culling frustum — built by buildCullFrustum() once per unpaused frame; MAY BE
|
|
47
56
|
// NULL and null means "don't cull" (a stale frustum mis-culls; see render contract)
|
|
48
57
|
this.cullFrustum = null;
|
package/src/core/retarget.js
CHANGED
|
@@ -193,7 +193,10 @@ export function buildRetargetContext(targetRoot, rigKey = null) {
|
|
|
193
193
|
// Synty rigs, where Root and Hips are coincident) used to yield posScale = 0, which
|
|
194
194
|
// multiplied every Hips.position key by ZERO: the pelvis got pinned to the root and the
|
|
195
195
|
// character sank into the ground. Only rescale when BOTH hip heights are real; else 1:1.
|
|
196
|
-
|
|
196
|
+
// BOUNDS: a metre-scale target fed cm clips has posScale ≈ 0.01 EXACTLY (fable's modular
|
|
197
|
+
// hero — the old `> 0.01` guard rejected its own legitimate ratio and the pelvis flew
|
|
198
|
+
// 87m up). 0.002 still excludes the genuine degenerate-bind zero case.
|
|
199
|
+
if (!(posScale > 0.002 && posScale < 500)) posScale = 1;
|
|
197
200
|
|
|
198
201
|
// Same bone NAMES don't guarantee the same bind POSE: the Fantasy Rivals bosses each carry a
|
|
199
202
|
// custom rest stance (bent legs, etc.) on anim-rig bone names. The clip can only be used as-is
|
package/src/core/tune.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// TUNABLES REGISTRY (ENGINE_DESIGN §5b) — systems declare their tweakable values;
|
|
2
|
+
// the dev GUI (/__sindicate/, served by sindicateDevTools) edits them LIVE in the
|
|
3
|
+
// running game through the game bus. Ships inert in production: registration is
|
|
4
|
+
// cheap, the window handle only exists in dev.
|
|
5
|
+
//
|
|
6
|
+
// import { tune } from '../core/tune.js';
|
|
7
|
+
// tune.group('weaponWheel', {
|
|
8
|
+
// slowMo: { value: 0.3, min: 0.05, max: 1, step: 0.05 },
|
|
9
|
+
// holdDelay: { value: 0.18, min: 0, max: 0.6, step: 0.02 },
|
|
10
|
+
// }, { onChange: (key, v) => { wheel[key] = v; } });
|
|
11
|
+
//
|
|
12
|
+
// `onChange` applies the edit to the live system. `file` (optional) names the game
|
|
13
|
+
// data file these values persist to — the GUI shows it so a keeper tune can be
|
|
14
|
+
// copied home (write-through persistence arrives with the ctx.tune contract).
|
|
15
|
+
const groups = new Map();
|
|
16
|
+
|
|
17
|
+
export const tune = {
|
|
18
|
+
group(name, spec, { onChange = null, file = null } = {}) {
|
|
19
|
+
// re-registration (bench soft-restart, second Engine in a page) keeps tuned values —
|
|
20
|
+
// a fresh constructor must not silently snap live dials back to defaults
|
|
21
|
+
const prev = groups.get(name);
|
|
22
|
+
if (prev) for (const [k, v] of Object.entries(spec)) {
|
|
23
|
+
if (prev.spec[k]?.value !== undefined) v.value = prev.spec[k].value;
|
|
24
|
+
}
|
|
25
|
+
groups.set(name, { name, spec, onChange, file });
|
|
26
|
+
},
|
|
27
|
+
// read-at-call-time accessor — systems that poll every frame use THIS instead of an
|
|
28
|
+
// onChange closure, so re-registration can never strand an instance mid-tune
|
|
29
|
+
value(name, key, fallback = undefined) {
|
|
30
|
+
const v = groups.get(name)?.spec[key]?.value;
|
|
31
|
+
return v === undefined ? fallback : v;
|
|
32
|
+
},
|
|
33
|
+
set(name, key, value) {
|
|
34
|
+
const g = groups.get(name);
|
|
35
|
+
if (!g || !(key in g.spec)) return false;
|
|
36
|
+
g.spec[key].value = value;
|
|
37
|
+
try { g.onChange?.(key, value, g.spec); } catch (e) { console.warn(`[tune] ${name}.${key} onChange`, e); }
|
|
38
|
+
return true;
|
|
39
|
+
},
|
|
40
|
+
list() {
|
|
41
|
+
return [...groups.values()].map((g) => ({
|
|
42
|
+
name: g.name,
|
|
43
|
+
file: g.file,
|
|
44
|
+
spec: Object.fromEntries(Object.entries(g.spec).map(([k, v]) => [k, { ...v }])),
|
|
45
|
+
}));
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// the dev GUI reaches the registry through the game bus via this handle
|
|
50
|
+
if (typeof window !== 'undefined' && import.meta.env?.DEV) window.__sindTune = tune;
|
package/src/systems/nav.js
CHANGED
|
@@ -89,7 +89,10 @@ class NavGrid {
|
|
|
89
89
|
const wx = this.cx - HALF + (ix + 0.5) * CELL;
|
|
90
90
|
const wz = this.cz - HALF + (iz + 0.5) * CELL;
|
|
91
91
|
const gterrain = world.groundAt(wx, wz);
|
|
92
|
-
|
|
92
|
+
// navFloorAt is the WESTERN world's interior-floor oracle; a game without it
|
|
93
|
+
// (fable pre-upgrade) falls back to groundAt — outdoor obstacles still bake
|
|
94
|
+
// correctly, interiors just stay ungraded until the game grows the oracle.
|
|
95
|
+
const y = world.navFloorAt ? world.navFloorAt(wx, wz) : world.groundAt(wx, wz); // the FLOOR a walker stands on — terrain outdoors, the raised floor indoors — so a building INTERIOR is navigable instead of being blocked by its own floorboards. (navFloorAt, not walkSurfaceAt: the latter returns the highest surface = a TABLETOP, which made tables read walkable and speckled the interior; the floor lets the furniture ray below catch the tabletops instead.)
|
|
93
96
|
// ── STATIC geometry (BOTH grids) ──────────────────────────────────────────────────────────
|
|
94
97
|
// 2D circles/rects (trees/fences/wells/stall posts): blocked = the point gets corrected away.
|
|
95
98
|
// Then BVH (buildings/props): knee-height cross + counter-height cross + a DOWNWARD post. The
|
package/src/ui/cartograph.js
CHANGED
|
@@ -26,7 +26,11 @@ let heightAt = () => 0, roadDistance = () => 1e9, fbm = () => 0;
|
|
|
26
26
|
let ROADS = [], RIVERS = [], LAKES = [], TOWN = null, RAIL_PATHS = [];
|
|
27
27
|
let OUTPOSTS = [], CAMPFIRE_SPOTS = [], COACH_STOP = null, STOPS = [], DUSTWATER = [];
|
|
28
28
|
export let WORLD_SIZE = 4320;
|
|
29
|
+
// Registration order must NOT be load-bearing: every setter invalidates the derived
|
|
30
|
+
// memos (shopSpots/allPOIs/allBuildings), so data landing after a first read still lands.
|
|
31
|
+
function invalidateChart() { _shops = _pois = _blds = null; }
|
|
29
32
|
export function setChartWorld(w = {}) {
|
|
33
|
+
invalidateChart();
|
|
30
34
|
if (w.heightAt) heightAt = w.heightAt;
|
|
31
35
|
if (w.roadDistance) roadDistance = w.roadDistance;
|
|
32
36
|
if (w.fbm) fbm = w.fbm;
|
|
@@ -47,6 +51,7 @@ export function setChartWorld(w = {}) {
|
|
|
47
51
|
let AREAS = [], RAILWAY_AREAS = [], COUNTY_DEFS = [];
|
|
48
52
|
let shopIdFor = () => null, SHOPS = {}, isMerchant = () => false;
|
|
49
53
|
export function setChartGazetteer(g = {}) {
|
|
54
|
+
invalidateChart();
|
|
50
55
|
if (g.areas) AREAS = g.areas;
|
|
51
56
|
if (g.railwayAreas) RAILWAY_AREAS = g.railwayAreas;
|
|
52
57
|
if (g.countyDefs) COUNTY_DEFS = g.countyDefs;
|
|
@@ -59,6 +64,7 @@ export function setChartGazetteer(g = {}) {
|
|
|
59
64
|
// worked only game-side; an engine file cannot glob a game's data directory).
|
|
60
65
|
let SETTLEMENTS = [], SETTLEMENT_AREAS = {};
|
|
61
66
|
export function setChartSettlements(s = {}) {
|
|
67
|
+
invalidateChart();
|
|
62
68
|
if (s.settlements) SETTLEMENTS = s.settlements;
|
|
63
69
|
if (s.settlementAreas) SETTLEMENT_AREAS = s.settlementAreas;
|
|
64
70
|
}
|