From 9361c5a2c438b0ec01b73de5846378fafc268d4b Mon Sep 17 00:00:00 2001 From: Eva Date: Sat, 30 May 2026 20:31:50 +0700 Subject: [PATCH] feat(openworlds): seed BG world_graph edges with route_kind (Closes #381) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Loop-10 verification of #261: the atlas/world_graph pipeline already carries route_kind end-to-end (WorldGraphEdge model + _seed_world_graph loader + _atlas_edge_payload projection + edgeStyle viewer branching) but the baldurs-gate world has ZERO world_graph edges — so every BG inter-district connection projects as the same untyped default stroke. This PR delivers the content + the two adjacent gaps the Loop-10 sub-agent flagged. What this lands --------------- 1. servers/engine/models.py (WorldGraphEdge.route_kind Literal) Extends the Literal to include "ferry", "bridge", "underground": - "ferry": was already a dead branch in screen-map.jsx edgeStyle — the viewer styled it, but the model rejected it on author. Now authorable. - "bridge": Wyrm's Crossing over the Chionthar (the canonical BG load-bearing crossing) is structurally a bridge, not a road. First-class kind so the styling can mark it as distinct. - "underground": Underdark passages, sewers, Bhaal Temple stairs. Lands ahead of #380 (BG canon POIs incl. Underdark) so the data surface is ready. The existing 7 kinds are unchanged + default value "road" preserved — no behavior change for any existing authored content (which is currently zero edges across all worlds, but the contract is intact). 2. content/worlds/baldurs-gate/world.json (NEW world_graph block) Authors 6 directed edges, one per canonical region-pair, each with minutes / difficulty / danger / tags grounded in BG3 + Forgotten Realms canon: loc-lower-city → loc-upper-city street 8m easy d=1 tags: urban, uphill, fist-checkpoint loc-lower-city → loc-outer-city street 12m easy d=2 tags: urban, basilisk-gate, slums-edge loc-outer-city → loc-wyrms-crossing bridge 18m easy d=2 tags: chionthar, fist-toll, wyrms-rock loc-wyrms-crossing → loc-elturel road 24h normal d=3 tags: risen-road, south, hellrider-patrol loc-wyrms-crossing → loc-reithwin road 36h hard d=5 tags: shadow-cursed, north, lifting loc-wyrms-crossing → loc-candlekeep road 96h normal d=2 tags: coast-road, west, avowed-watch All 6 verified against canonical Location.connections — the _seed_world_graph loader's "edge must reference an existing connection in either direction" guard PASSes for every edge (verified standalone via the loader's exact predicate). 3. viewer/openworlds/screen-map.jsx (edgeStyle extension) The existing edgeStyle had styles for 3 kinds (street/road, river/ ferry/sea, default). The Literal supports 7 (now 10). This PR adds styles for the remaining kinds so atlas reading isn't ambiguous: - bridge : thicker amber-brown solid (load-bearing crossing) - underground : tight-dotted dark brown (you go down) - passage : same as underground - portal : violet dashed (extraplanar) - trail : light brown looser-dashed (wilds-grade path) The pre-existing high-danger override (danger >= 6) still wins regardless of kind — that's the right precedence (the player should see "dangerous" before "what kind of road"). Why grouped, not 3 PRs ---------------------- The 3 changes form one logical unit: the Literal opens what authors can write, the world.json exercises that authorship, the edgeStyle makes the new kinds visually distinguishable. Shipping any 1 or 2 without the third leaves the surface incomplete (e.g. authoring "bridge" without a distinct style means the player can't tell the bridge from a road; styling "bridge" without authoring leaves the new branch dead). Verification ------------ - world.json: `json.load()` passes; 26 top-level keys (was 25); 6 edges; 3 route_kinds used (street, road, bridge). Validated all 6 edges satisfy the loader's "edge.to_id in src.connections OR edge.from_id in dst.connections" guard. - models.py: `ast.parse()` passes. Literal extended by 3 members, default unchanged. - screen-map.jsx: brace balance 450/450, paren balance 601/601. Existing kinds (street, road, river, ferry, sea) styled exactly as before — diff is purely additive new branches. Acceptance criteria (per #381) ------------------------------ - [x] world_graph block authored for the BG world (zero before, 6 now) - [x] route_kind values match the AC's intended palette (street, road, bridge in this PR; ferry/sea/river already supported; underground + portal + trail ready for #380's POIs) - [x] Literal extended to include "ferry" (closing the existing dead branch in edgeStyle) + "bridge" + "underground" - [x] edgeStyle extended to give each kind a distinct visual signal - [x] Edges validated against canonical Location.connections — no drift between regions[] and world_graph Out of scope ------------ - New POIs / regions (#380) — required for portal + underground edges to actually appear; this PR makes the Literal ready, the seed waits - Edge metadata for non-BG worlds — pure-data work per world - A symmetric-edges convention (today: 6 directed edges; could be 12 for full symmetry, but viewer renders fine either way) Refs ---- - Closes #381 (Loop-10 follow-up to #261) - Parent #261 (atlas-seed, PR #371 closed the discovered-tier strand) - Builds toward #380 (BG POIs — Steel Watch, Underdark, Bhaal Temple, Avernus portal — which will exercise the new "underground" + "portal" Literal members) --- content/worlds/baldurs-gate/world.json | 12 ++++++++++++ servers/engine/models.py | 9 ++++++++- viewer/openworlds/screen-map.jsx | 18 ++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/content/worlds/baldurs-gate/world.json b/content/worlds/baldurs-gate/world.json index c5aff823..85bdbf33 100644 --- a/content/worlds/baldurs-gate/world.json +++ b/content/worlds/baldurs-gate/world.json @@ -50,6 +50,18 @@ {"id": "loc-reithwin", "name": "Reithwin & the Lifting Shadow", "description": "The Shadow-Cursed Lands around Moonrise Towers, where Ketheric Thorm's curse of Shar is slowly lifting since his fall — Last Light Inn rekindles, the dead grow quieter, and survivors return to a poisoned country to see what crawled out of the dark.", "connections": ["loc-wyrms-crossing"], "tags": ["wilds", "shadow", "ruins"]}, {"id": "loc-candlekeep", "name": "Candlekeep", "description": "The great fortress-library on the Sword Coast, hoarding the world's knowledge behind a one-book toll. If the truth about the Crown of Karsus, the Emperor's past, or a loose illithid is written anywhere, it is here — guarded by Avowed who do not love visitors.", "connections": ["loc-wyrms-crossing"], "tags": ["lore", "library", "secrets"]} ], + "world_graph": { + "seed": "baldurs-gate-loop-10-route-kinds", + "provenance": "authored", + "edges": [ + {"from_id": "loc-lower-city", "to_id": "loc-upper-city", "route_kind": "street", "minutes": 8, "difficulty": "easy", "danger": 1, "tags": ["urban", "uphill", "fist-checkpoint"]}, + {"from_id": "loc-lower-city", "to_id": "loc-outer-city", "route_kind": "street", "minutes": 12, "difficulty": "easy", "danger": 2, "tags": ["urban", "basilisk-gate", "slums-edge"]}, + {"from_id": "loc-outer-city", "to_id": "loc-wyrms-crossing", "route_kind": "bridge", "minutes": 18, "difficulty": "easy", "danger": 2, "tags": ["chionthar", "fist-toll", "wyrms-rock"]}, + {"from_id": "loc-wyrms-crossing", "to_id": "loc-elturel", "route_kind": "road", "minutes": 1440, "difficulty": "normal", "danger": 3, "tags": ["risen-road", "south", "hellrider-patrol"]}, + {"from_id": "loc-wyrms-crossing", "to_id": "loc-reithwin", "route_kind": "road", "minutes": 2160, "difficulty": "hard", "danger": 5, "tags": ["shadow-cursed", "north", "lifting"]}, + {"from_id": "loc-wyrms-crossing", "to_id": "loc-candlekeep", "route_kind": "road", "minutes": 5760, "difficulty": "normal", "danger": 2, "tags": ["coast-road", "west", "avowed-watch"]} + ] + }, "factions": [ {"id": "fac-flaming-fist", "name": "The Flaming Fist", "description": "The Gate's mercenary army and de facto police, overstretched and leaderless-feeling with the Steel Watch gone and the dukedom contested. Honorable in the ranks, political at the top.", "reputation": 1}, {"id": "fac-guild", "name": "The Guild", "description": "Baldur's Gate's vast thieves' network — smuggling, protection, information. Pragmatic, everywhere, and the ones who actually know what moves in the dark.", "reputation": 0}, diff --git a/servers/engine/models.py b/servers/engine/models.py index 0bc60dbe..49b0c82a 100644 --- a/servers/engine/models.py +++ b/servers/engine/models.py @@ -858,7 +858,14 @@ class WorldGraphEdge(_StrictModel): from_id: str to_id: str - route_kind: Literal["street", "road", "trail", "sea", "river", "passage", "portal"] = "road" + # Loop-10 #381: added "ferry" (was already a dead branch in screen-map.jsx + # edgeStyle), "bridge" (Wyrm's Crossing — the BG canon load-bearing crossing + # over the Chionthar), "underground" (Underdark passages — coming with #380 + # when the Underdark / Bhaal Temple POIs land). + route_kind: Literal[ + "street", "road", "trail", "sea", "river", "passage", "portal", + "ferry", "bridge", "underground", + ] = "road" minutes: Optional[int] = Field(None, ge=1) distance: Optional[float] = Field(None, ge=0) difficulty: Literal["easy", "normal", "hard", "hazardous"] = "normal" diff --git a/viewer/openworlds/screen-map.jsx b/viewer/openworlds/screen-map.jsx index 73da06f0..ce1fb942 100644 --- a/viewer/openworlds/screen-map.jsx +++ b/viewer/openworlds/screen-map.jsx @@ -427,9 +427,27 @@ function computeAtlasLayout(locations, edges) { function edgeStyle(edge) { const kind = (edge.route_kind || "").toLowerCase(); const danger = typeof edge.danger === "number" ? edge.danger : 0; + // High-danger routes dominate the style: dark red dashed, regardless of kind. if (danger >= 6) return { stroke: "rgba(120,32,32,0.62)", width: 0.7, dash: "1.4 1" }; + // Urban + extramural surface routes — solid brown. if (kind === "street" || kind === "road") return { stroke: "rgba(60,30,10,0.6)", width: 0.85, dash: "" }; + // Water + crossing — solid blue. "ferry" is now a valid Literal member (#381). if (kind === "river" || kind === "ferry" || kind === "sea") return { stroke: "rgba(40,90,120,0.6)", width: 0.7, dash: "" }; + // Bridge — slightly thicker amber-brown to mark a load-bearing crossing + // (Wyrm's Crossing over the Chionthar, etc.) as distinct from a regular road. + // Added Loop-10 / #381. + if (kind === "bridge") return { stroke: "rgba(110,75,30,0.72)", width: 1.05, dash: "" }; + // Underground — Underdark passages, Bhaal Temple stairs, sewers. Tight dotted + // dash reads as "you go down, not across." Added Loop-10 / #381 ahead of the + // #380 Underdark POIs. + if (kind === "underground" || kind === "passage") return { stroke: "rgba(40,25,10,0.66)", width: 0.7, dash: "0.7 1.8" }; + // Portal — extraplanar / arcane (Avernus gate, Astral, etc.). A subtle violet + // tint flags the route as "not walking distance" even when the connection is + // visually short. + if (kind === "portal") return { stroke: "rgba(95,55,135,0.62)", width: 0.7, dash: "1.2 1.2" }; + // Trail — wilds-grade path. Lighter and looser-dashed than the default to + // read as "you'll get there, just not quickly." + if (kind === "trail") return { stroke: "rgba(85,60,30,0.5)", width: 0.6, dash: "1.4 1.6" }; return { stroke: "rgba(60,30,10,0.46)", width: 0.5, dash: "1.6 1.2" }; }