Adv 2.0 D&D Phase 11 D9: !zone lore subcommand

Wires the existing GMLore narration pool (zone-specific LoreLines* +
generic LoreLines fallback, all prewritten) into a !zone lore command.
Deterministic per (run, room) — re-asking in the same room yields the
same line; cross-room calls vary. No mood or state side-effects.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-05-08 14:34:42 -07:00
parent 7bb922ae62
commit 4638cb83a1
2 changed files with 69 additions and 1 deletions

View File

@@ -60,6 +60,8 @@ func (p *AdventurePlugin) handleDnDZoneCmd(ctx MessageContext, args string) erro
return p.zoneCmdTaunt(ctx) return p.zoneCmdTaunt(ctx)
case "compliment": case "compliment":
return p.zoneCmdCompliment(ctx) return p.zoneCmdCompliment(ctx)
case "lore", "history":
return p.zoneCmdLore(ctx)
default: default:
return p.SendDM(ctx.Sender, zoneHelpText()) return p.SendDM(ctx.Sender, zoneHelpText())
} }
@@ -75,7 +77,8 @@ func zoneHelpText() string {
b.WriteString("`!zone advance` — resolve the current room and move on\n") b.WriteString("`!zone advance` — resolve the current room and move on\n")
b.WriteString("`!zone abandon` — end the active run (no rewards)\n") b.WriteString("`!zone abandon` — end the active run (no rewards)\n")
b.WriteString("`!zone taunt` — poke TwinBee (mood 10)\n") b.WriteString("`!zone taunt` — poke TwinBee (mood 10)\n")
b.WriteString("`!zone compliment` — flatter TwinBee (mood +5)") b.WriteString("`!zone compliment` — flatter TwinBee (mood +5)\n")
b.WriteString("`!zone lore` — TwinBee shares zone history")
return b.String() return b.String()
} }
@@ -541,6 +544,30 @@ func (p *AdventurePlugin) zoneMoodInteraction(
return p.SendDM(ctx.Sender, b.String()) return p.SendDM(ctx.Sender, b.String())
} }
// ── lore ────────────────────────────────────────────────────────────────────
// zoneCmdLore renders a deterministic TwinBee lore line for the active
// run's current room. Pulls from the zone-specific LoreLines* pool when
// one exists (Tier 15 ship dedicated lore), falls back to the generic
// LoreLines pool otherwise. roomIdx is folded into the selector so
// repeated `!zone lore` in the same room returns the same prose, but
// cross-room calls vary.
func (p *AdventurePlugin) zoneCmdLore(ctx MessageContext) error {
run, err := getActiveZoneRun(ctx.Sender)
if err != nil {
return p.SendDM(ctx.Sender, "Couldn't read run state: "+err.Error())
}
if run == nil {
return p.SendDM(ctx.Sender, "No active zone run. Use `!zone enter <id>`.")
}
zone, _ := getZone(run.ZoneID)
line := twinBeeLine(zone.ID, GMLore, run.RunID, run.CurrentRoom)
if line == "" {
return p.SendDM(ctx.Sender, "TwinBee has nothing to say about this place.")
}
return p.SendDM(ctx.Sender, fmt.Sprintf("📖 **%s — Lore**\n\n%s", zone.Display, line))
}
// ── abandon ───────────────────────────────────────────────────────────────── // ── abandon ─────────────────────────────────────────────────────────────────
func (p *AdventurePlugin) zoneCmdAbandon(ctx MessageContext) error { func (p *AdventurePlugin) zoneCmdAbandon(ctx MessageContext) error {

View File

@@ -318,6 +318,47 @@ func TestZoneCmd_TauntWithoutRunNoCrash(t *testing.T) {
} }
} }
// Phase 11 D9 — !zone lore.
func TestZoneCmd_LoreWithoutRunNoCrash(t *testing.T) {
setupAuditTestDB(t)
uid := id.UserID("@zone-cmd-lore-norun:example")
zoneCmdTestCharacter(t, uid, 1)
defer cleanupZoneRuns(uid)
p := &AdventurePlugin{}
if err := p.handleDnDZoneCmd(MessageContext{Sender: uid}, "lore"); err != nil {
t.Fatalf("lore no-run: %v", err)
}
}
func TestZoneCmd_LoreWithActiveRunNoSideEffects(t *testing.T) {
setupAuditTestDB(t)
uid := id.UserID("@zone-cmd-lore-run:example")
zoneCmdTestCharacter(t, uid, 1)
defer cleanupZoneRuns(uid)
p := &AdventurePlugin{}
if err := p.handleDnDZoneCmd(MessageContext{Sender: uid}, "enter goblin_warrens"); err != nil {
t.Fatalf("enter: %v", err)
}
before, err := getActiveZoneRun(uid)
if err != nil || before == nil {
t.Fatalf("active run after enter: %v", err)
}
if err := p.handleDnDZoneCmd(MessageContext{Sender: uid}, "lore"); err != nil {
t.Fatalf("lore: %v", err)
}
after, err := getActiveZoneRun(uid)
if err != nil || after == nil {
t.Fatalf("active run after lore: %v", err)
}
if after.GMMood != before.GMMood {
t.Errorf("lore should not move mood: before %d, after %d", before.GMMood, after.GMMood)
}
if after.CurrentRoom != before.CurrentRoom {
t.Errorf("lore should not advance room: before %d, after %d", before.CurrentRoom, after.CurrentRoom)
}
}
func TestTauntComplimentResponseLines_Deterministic(t *testing.T) { func TestTauntComplimentResponseLines_Deterministic(t *testing.T) {
const runID = "run-d7-test" const runID = "run-d7-test"
t1 := tauntResponseLine(runID, 0) t1 := tauntResponseLine(runID, 0)