Branching zones G6: dependent surfaces re-keyed on graph nodes

- Harvest tables (expedition + standalone) now keyed by node_id; legacy
  room-idx entries auto-migrate via read-fallback + drop-on-save.
- Narration salts swapped from run.CurrentRoom to narrationCadence(run)
  (= len(visited_nodes)-1) so flavor pickers survive G9 column drop.
- !zone map renders the graph (BFS by PosX/PosY) when the gate is on:
  ✓/▶/· status, ╳ for locked-only edges, secrets hidden until visited.
- Region-boundary hook in graph advance/!zone go updates expedition
  CurrentRegion + visited list when from/to nodes differ — without
  burning supplies or retiring runs (the graph IS the run state).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-05-09 15:17:30 -07:00
parent 2d249d7d0a
commit 893d3dacad
14 changed files with 562 additions and 64 deletions

View File

@@ -6,6 +6,8 @@ package plugin
import (
"fmt"
"strings"
"maunium.net/go/mautrix/id"
)
// advanceTransitionGraph is the graph-mode replacement for
@@ -64,6 +66,13 @@ func (p *AdventurePlugin) advanceTransitionGraph(run *DungeonRun, zone ZoneDefin
err = aerr
return
}
// G6d — region boundary hook. Multi-region branching graphs
// can cross a region edge mid-run; emit the existing transit
// flavor + log so the player gets the same "you've crossed
// into X" beat the !region travel command produces. Doesn't
// retire the run (the graph is the authoritative state) — it
// just narrates the crossing.
fireGraphRegionTransition(run.UserID, g.Nodes[currentNode], g.Nodes[chosen.To])
next = nodeKindToRoomType(g.Nodes[chosen.To].Kind)
return
}
@@ -80,6 +89,36 @@ func (p *AdventurePlugin) advanceTransitionGraph(run *DungeonRun, zone ZoneDefin
return
}
// fireGraphRegionTransition compares the from/to graph node region ids
// and, when they differ, mirrors the existing !region travel surface:
// updates Expedition.CurrentRegion + visited list and writes a transit
// log entry. Unlike full !region travel, it does NOT burn supplies,
// advance the day, or retire/spawn DungeonRuns — the graph is the
// authoritative run state, and the player crossed via the graph edge,
// not a separate transit day. Standalone (no expedition) is a no-op.
func fireGraphRegionTransition(userID string, from, to ZoneNode) {
if from.RegionID == to.RegionID {
return
}
if to.RegionID == "" {
return
}
exp, err := getActiveExpedition(id.UserID(userID))
if err != nil || exp == nil {
return
}
if exp.CurrentRegion == to.RegionID {
return
}
if err := setCurrentRegion(exp, to.RegionID); err != nil {
return
}
_, _ = MarkRegionVisited(exp, to.RegionID)
_ = appendExpeditionLog(exp.ID, exp.CurrentDay, "narrative",
fmt.Sprintf("graph region transit: %s → %s (node %s → %s)",
from.RegionID, to.RegionID, from.NodeID, to.NodeID), "")
}
func unlockedChoices(cs []pendingChoice) []pendingChoice {
out := make([]pendingChoice, 0, len(cs))
for _, c := range cs {
@@ -147,7 +186,9 @@ func (p *AdventurePlugin) zoneCmdGo(ctx MessageContext, rest string) error {
}
g, _ := loadZoneGraph(run.ZoneID)
zone := zoneOrFallback(run.ZoneID)
fromNode := g.Nodes[run.CurrentNode]
nextNode := g.Nodes[chosen.To]
fireGraphRegionTransition(run.UserID, fromNode, nextNode)
nextRoom := nodeKindToRoomType(nextNode.Kind)
nextIdx := run.CurrentRoom + 1
var b strings.Builder