From 4638cb83a105b61c4b0a84ec943975d65b5893fc Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Fri, 8 May 2026 14:34:42 -0700 Subject: [PATCH] Adv 2.0 D&D Phase 11 D9: !zone lore subcommand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- internal/plugin/dnd_zone_cmd.go | 29 +++++++++++++++++++- internal/plugin/dnd_zone_cmd_test.go | 41 ++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/internal/plugin/dnd_zone_cmd.go b/internal/plugin/dnd_zone_cmd.go index f276277..6368806 100644 --- a/internal/plugin/dnd_zone_cmd.go +++ b/internal/plugin/dnd_zone_cmd.go @@ -60,6 +60,8 @@ func (p *AdventurePlugin) handleDnDZoneCmd(ctx MessageContext, args string) erro return p.zoneCmdTaunt(ctx) case "compliment": return p.zoneCmdCompliment(ctx) + case "lore", "history": + return p.zoneCmdLore(ctx) default: 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 abandon` — end the active run (no rewards)\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() } @@ -541,6 +544,30 @@ func (p *AdventurePlugin) zoneMoodInteraction( 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 1–5 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 `.") + } + 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 ───────────────────────────────────────────────────────────────── func (p *AdventurePlugin) zoneCmdAbandon(ctx MessageContext) error { diff --git a/internal/plugin/dnd_zone_cmd_test.go b/internal/plugin/dnd_zone_cmd_test.go index fca0b5b..6fbd092 100644 --- a/internal/plugin/dnd_zone_cmd_test.go +++ b/internal/plugin/dnd_zone_cmd_test.go @@ -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) { const runID = "run-d7-test" t1 := tauntResponseLine(runID, 0)