From e4518c9c39c25c6053c919c9be450c879f5c35ae Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Tue, 19 May 2026 22:17:39 -0700 Subject: [PATCH] Camp: reject (not downgrade) sub-par camps; map: show visited path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Camp rest rules: • Standard camp now rejects an uncleared room with a clear message instead of silently downgrading to rough — let the player make the call rather than spending supplies on a worse rest they didn't pick. • campLocationCheck treats a room with a resolved (non-active) combat session as cleared, so the natural "just killed, not yet advanced" pause still qualifies for a standard camp. • Reword fortified-camp gating + babysit help to "zone boss defeated" (cache sites aren't a thing yet) so the requirement isn't misleading. Map: renderVisitedPath adds a 1-indexed breadcrumb of visited rooms to !expedition map, so players can reference rooms by index (e.g. !revisit 2). (cherry picked from commit a38fc77eed888e9790c7a7cff24369b98910b43e) --- internal/plugin/adventure_babysit.go | 2 +- internal/plugin/dnd_expedition_camp.go | 34 +++++++++++++++----------- internal/plugin/dnd_expedition_map.go | 3 +++ internal/plugin/zone_graph_map.go | 22 +++++++++++++++++ 4 files changed, 46 insertions(+), 15 deletions(-) diff --git a/internal/plugin/adventure_babysit.go b/internal/plugin/adventure_babysit.go index 0ceb4b6..1f86e57 100644 --- a/internal/plugin/adventure_babysit.go +++ b/internal/plugin/adventure_babysit.go @@ -98,7 +98,7 @@ func (p *AdventurePlugin) handleBabysitCmd(ctx MessageContext, args string) erro return p.SendDM(ctx.Sender, "🍼 **Adventurer Babysitting Service**\n\n"+ "Hire a babysitter to look after your camp and tend the pet while you sleep:\n"+ " • Daily pet XP trickle (your pet still grows while you focus elsewhere)\n"+ - " • Standard camps act like fortified ones — rest deeply, no need for boss-cleared rooms\n"+ + " • Standard camps act like fortified ones — rest deeply, no need to have downed the zone boss\n"+ " • Rival duels declined on your behalf\n\n"+ "`!adventure babysit week` — 7 days of service\n"+ "`!adventure babysit month` — 30 days of service\n"+ diff --git a/internal/plugin/dnd_expedition_camp.go b/internal/plugin/dnd_expedition_camp.go index 6031ee5..80f4836 100644 --- a/internal/plugin/dnd_expedition_camp.go +++ b/internal/plugin/dnd_expedition_camp.go @@ -79,12 +79,9 @@ func (p *AdventurePlugin) handleCampCmd(ctx MessageContext, args string) error { case "rough", "standard": // allowed in E1e case "fortified": - // E2d: §5.1 — boss-cleared room or cache site. Cache sites - // are zone-specific waypoints (E3+), so for now we require the - // expedition's boss to have been defeated. if !exp.BossDefeated { return p.SendDM(ctx.Sender, - "Fortified camps require a boss-cleared room or cache site. Defeat the zone boss (or find a cache) first.") + "Fortified camps require a defeated zone boss. Clear the zone first.") } case "base": // E4d: §11.1 — base camps unlock per region after the region @@ -122,7 +119,7 @@ func campHelpText(exp *Expedition) string { b.WriteString("**!camp ** — establish camp during an expedition.\n\n") b.WriteString("`!camp rough` — partial rest (any location, +0.5 SU, high night risk)\n") b.WriteString("`!camp standard` — full long rest (cleared room, +1 SU, medium risk)\n") - b.WriteString("`!camp fortified` — long rest + bonus (boss-cleared room, +2 SU, low risk)\n") + b.WriteString("`!camp fortified` — long rest + bonus (zone boss defeated, +2 SU, low risk)\n") b.WriteString("`!camp base` — persistent waypoint (region-boss-cleared base-camp site, +3 SU, very low risk)\n") b.WriteString("`!camp break` — break camp\n\n") if exp.Camp != nil && exp.Camp.Active { @@ -146,10 +143,10 @@ func (p *AdventurePlugin) campPitch(ctx MessageContext, exp *Expedition, kind st return p.SendDM(ctx.Sender, problem) } if !cleared && kind == CampTypeStandard { - // §5.2: non-cleared room forces rough. - kind = CampTypeRough - _ = appendExpeditionLog(exp.ID, exp.CurrentDay, "narrative", - "intended standard camp; downgraded to rough (room not cleared)", "") + // §5.2: standard camp requires a cleared room. Reject explicitly + // rather than silently downgrading — the player should make the call. + return p.SendDM(ctx.Sender, + "Standard camp needs a cleared room. This room isn't cleared yet — clear it first, or `!camp rough` for a partial rest here.") } cost, ok := campSupplyCost[kind] @@ -359,15 +356,24 @@ func campLocationCheck(exp *Expedition) (cleared bool, problem string) { case RoomTrap: return false, "You can't camp in a trap room — even a disarmed one." } - // Active-enemy detection requires combat-state lookup; defer to E2. - cleared = false for _, idx := range run.RoomsCleared { if idx == run.CurrentRoom { - cleared = true - break + return true, "" } } - return cleared, "" + // Not yet advanced-past, but if there's no live combat encounter in + // this room, it's still safe to rest in. Forward-only navigation means + // players naturally pause right after a kill before advancing — the + // "cleared" flag would otherwise force-downgrade their camp to rough. + encID := encounterIDForRoom(run.CurrentRoom) + sess, err := getCombatSessionForEncounter(run.RunID, encID) + if err != nil { + return false, "" + } + if sess != nil && sess.Status != CombatStatusActive { + return true, "" + } + return false, "" } // campCurrentRoomIndex returns 0 (entry) when no room context exists. diff --git a/internal/plugin/dnd_expedition_map.go b/internal/plugin/dnd_expedition_map.go index a5c4e75..68777cf 100644 --- a/internal/plugin/dnd_expedition_map.go +++ b/internal/plugin/dnd_expedition_map.go @@ -119,6 +119,9 @@ func (p *AdventurePlugin) handleExpeditionMapCmd(ctx MessageContext, _ string) e b.WriteString(renderZoneGraphMap(g, run)) b.WriteString("\n```\n") b.WriteString("_E=Entry ?=Exploration T=Trap ★=Elite ☠=Boss ⚿=Secret · ✓ cleared ▶ here · pending ╳ locked_") + if path := renderVisitedPath(g, run); path != "" { + b.WriteString("\n**Path:** " + path) + } if chain != "" { b.WriteString("\n_Regions: ") b.WriteString(renderRegionLegend(exp)) diff --git a/internal/plugin/zone_graph_map.go b/internal/plugin/zone_graph_map.go index 8edbd80..9390f31 100644 --- a/internal/plugin/zone_graph_map.go +++ b/internal/plugin/zone_graph_map.go @@ -172,6 +172,28 @@ func nodeGlyph(n ZoneNode) string { return "·" } +// renderVisitedPath renders a one-line numbered breadcrumb of the +// player's path so they can reference rooms by index (e.g. !revisit 2). +// Numbers are 1-indexed to match every other surface ("Room 2/7", etc.). +func renderVisitedPath(g ZoneGraph, run *DungeonRun) string { + if run == nil || len(run.VisitedNodes) == 0 { + return "" + } + parts := make([]string, 0, len(run.VisitedNodes)) + for i, nodeID := range run.VisitedNodes { + glyph := "·" + if n, ok := g.Nodes[nodeID]; ok { + glyph = nodeGlyph(n) + } + mark := "✓" + if nodeID == run.CurrentNode { + mark = "▶" + } + parts = append(parts, fmt.Sprintf("[%d]%s%s", i+1, glyph, mark)) + } + return strings.Join(parts, " → ") +} + // debugDump (test-only crutch) prints the raw column layout. Currently // unused outside potential debugging — kept off the unused-warning list // by routing fmt through it.