Adv 2.0 D&D Phase 12 E4b: region progression hooks + status display

Multi-region zones now default CurrentRegion to the entry region at
startExpedition; that region is also written to regions_visited so
the visited/cleared distinction is meaningful from Day 1.

MarkRegionVisited / MarkRegionBossDefeated expose the combat-link
hook surface. MarkRegionBossDefeated also flips Expedition.BossDefeated
when the cleared region's IsZoneBoss is set, since the zone-boss kill
is the same event from §7's POV (suppresses temporal accumulation,
kills further awareness pulses, etc.). setCurrentRegion is the
companion writer used by E4c's travel command.

!expedition status now shows the region line ("🗺 Region: Drow
Outpost (2/4)") with a ✓ marker when the region boss is down.
This commit is contained in:
prosolis
2026-05-08 16:16:24 -07:00
parent 9484dae146
commit c2bed4282d
4 changed files with 153 additions and 15 deletions

View File

@@ -1,5 +1,7 @@
package plugin
import "gogobee/internal/db"
// Phase 12 E4a — Multi-Region Zone data model and registry.
// Spec: gogobee_expedition_system.md §11.
//
@@ -267,3 +269,56 @@ func IsRegionVisited(e *Expedition, regionID string) bool {
func HasBaseCampAt(e *Expedition, regionID string) bool {
return regionListContains(regionListFromState(e, regionStateBaseCampKey), regionID)
}
// MarkRegionVisited records that the player has set foot in `regionID`.
// Idempotent. Returns true if state changed.
func MarkRegionVisited(e *Expedition, regionID string) (bool, error) {
if e == nil || regionID == "" {
return false, nil
}
return addRegionListEntry(e, regionStateVisitedKey, regionID)
}
// MarkRegionBossDefeated records that the region boss for `regionID`
// has been killed. If the region's IsZoneBoss flag is set, the
// expedition's BossDefeated zone-level flag is also flipped (§7
// suppression hooks read it). Idempotent. Returns true if state
// changed.
//
// Combat-link phase calls this from the per-region boss-kill resolver.
func MarkRegionBossDefeated(e *Expedition, regionID string) (bool, error) {
if e == nil || regionID == "" {
return false, nil
}
region, ok := findRegion(e.ZoneID, regionID)
if !ok {
return false, nil
}
changed, err := addRegionListEntry(e, regionStateClearedKey, regionID)
if err != nil {
return changed, err
}
if region.IsZoneBoss && !e.BossDefeated {
e.BossDefeated = true
if _, err := db.Get().Exec(`
UPDATE dnd_expedition
SET boss_defeated = 1,
last_activity = CURRENT_TIMESTAMP
WHERE expedition_id = ?`, e.ID); err != nil {
return changed, err
}
changed = true
}
return changed, nil
}
// setCurrentRegion writes a new CurrentRegion. Used by E4c travel.
func setCurrentRegion(e *Expedition, regionID string) error {
e.CurrentRegion = regionID
_, err := db.Get().Exec(`
UPDATE dnd_expedition
SET current_region = ?,
last_activity = CURRENT_TIMESTAMP
WHERE expedition_id = ?`, regionID, e.ID)
return err
}