diff --git a/internal/plugin/dnd_expedition_extract.go b/internal/plugin/dnd_expedition_extract.go index 6cd971d..b5290c5 100644 --- a/internal/plugin/dnd_expedition_extract.go +++ b/internal/plugin/dnd_expedition_extract.go @@ -170,6 +170,29 @@ func (p *AdventurePlugin) finalizeExpeditionOnZoneClear(userID id.UserID, runID return p.AwardCompletionMilestones(exp, false) } +// midZoneRegionClear reports whether the just-completed run (runID) is the +// active expedition's current run AND finishes a non-boss region of a +// multi-region zone — i.e. a region clear that leaves the expedition active +// with a next region to cross into. Returns the cleared region, the next +// region, and true in that case; zero-values + false for a full zone clear, +// a standalone run, or any read error. Used to word the run-complete message +// (region-cleared vs zone-cleared) without re-deriving region state inline. +func midZoneRegionClear(userID id.UserID, runID string) (cur, next ExpeditionRegion, ok bool) { + exp, err := getActiveExpedition(userID) + if err != nil || exp == nil || exp.RunID != runID || !IsMultiRegionZone(exp.ZoneID) { + return ExpeditionRegion{}, ExpeditionRegion{}, false + } + region, found := CurrentRegion(exp) + if !found || region.IsZoneBoss { + return ExpeditionRegion{}, ExpeditionRegion{}, false + } + nxt, found := nextRegion(exp.ZoneID, region.ID) + if !found { + return ExpeditionRegion{}, ExpeditionRegion{}, false + } + return region, nxt, true +} + // getResumableExpedition returns the most recent 'extracting' row for the // user, regardless of age (caller checks the 7-day window). func getResumableExpedition(userID id.UserID) (*Expedition, error) { diff --git a/internal/plugin/dnd_zone_cmd.go b/internal/plugin/dnd_zone_cmd.go index 41fa98f..a1cfdef 100644 --- a/internal/plugin/dnd_zone_cmd.go +++ b/internal/plugin/dnd_zone_cmd.go @@ -593,7 +593,16 @@ func (p *AdventurePlugin) advanceOnceWithOpts(ctx MessageContext, compact bool) b.WriteString(outcome) b.WriteString("\n\n") } - b.WriteString(fmt.Sprintf("🏆 **Cleared %s.** Run complete.\n\n", zone.Display)) + // A "complete" run is only a full zone clear when it isn't a mid-zone + // region clear of a multi-region zone. For the latter, name the region + // and point at the next one — "Cleared {zone}. Run complete." reads + // wrong right before the auto-advance transit block (and is shared with + // manual `!region travel`, which advances next). + if region, next, midZone := midZoneRegionClear(ctx.Sender, run.RunID); midZone { + b.WriteString(fmt.Sprintf("🏁 **Cleared %s.** The way to %s opens ahead.\n\n", region.Name, next.Name)) + } else { + b.WriteString(fmt.Sprintf("🏆 **Cleared %s.** Run complete.\n\n", zone.Display)) + } if line := twinBeeLine(zone.ID, DMZoneComplete, run.RunID, prevIdx); line != "" { b.WriteString(line) b.WriteString("\n\n") diff --git a/internal/plugin/expedition_sim.go b/internal/plugin/expedition_sim.go index 11add7c..1d17b2e 100644 --- a/internal/plugin/expedition_sim.go +++ b/internal/plugin/expedition_sim.go @@ -363,6 +363,29 @@ func (s *SimRunner) RunExpedition(uid id.UserID, zoneID ZoneID, walkCap int) (*S res.Outcome = "tpk" i = walkCap // exit case stopComplete: + // A stopComplete that reaches the sim is normally a full zone + // clear (the walk auto-advances mid-zone region boundaries + // internally). But a mid-zone region clear can still surface + // here — e.g. the walk's auto-advance hit a transit error and + // returned stopComplete. Mirror runAutopilotWalk: if the + // expedition is still active in a multi-region zone with a + // next region, cross into it and keep simulating instead of + // scoring a premature "cleared". + if fresh, ferr := getActiveExpedition(uid); ferr == nil && fresh != nil && + IsMultiRegionZone(fresh.ZoneID) { + if cur, ok := CurrentRegion(fresh); ok { + if next, ok := nextRegion(fresh.ZoneID, cur.ID); ok { + if _, terr := s.P.advanceToNextRegion(uid, fresh, cur, next); terr != nil { + res.Outcome = "halted" + res.StopCode = "region_transit:" + terr.Error() + i = walkCap + break + } + exp = fresh + break // continue the outer walk loop in the next region + } + } + } res.Outcome = "cleared" i = walkCap case stopBoss, stopElite: