Camp: reject (not downgrade) sub-par camps; map: show visited path

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)
This commit is contained in:
prosolis
2026-05-19 22:17:39 -07:00
parent 20b0d027b2
commit e4518c9c39
4 changed files with 46 additions and 15 deletions

View File

@@ -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"+

View File

@@ -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 <type>** — 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.

View File

@@ -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))

View File

@@ -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.