mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 00:32:40 +00:00
Adv 2.0 D&D Phase 11 D7: !zone taunt / !zone compliment
Wires the prewritten flavor.TauntResponses and flavor.ComplimentResponses pools to the existing MoodEventTaunt (-10) / MoodEventCompliment (+5) mood deltas. Both subcommands require an active run and report the new mood band after applying the delta. Deterministic line selection per (runID, roomIdx) so repeated taunts in the same room are stable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,8 @@ import (
|
||||
// !zone map → ASCII layout of the run with current marker
|
||||
// !zone advance → resolve the current room and move on
|
||||
// !zone abandon → end the active run (no rewards)
|
||||
// !zone taunt → poke TwinBee (mood −10, snarky reply)
|
||||
// !zone compliment → flatter TwinBee (mood +5, fond reply)
|
||||
//
|
||||
// TwinBee narration / mood triggers arrive in D1d. Combat / trap / boss
|
||||
// resolution per room is wired in D1e via dnd_zone_combat.go.
|
||||
@@ -54,6 +56,10 @@ func (p *AdventurePlugin) handleDnDZoneCmd(ctx MessageContext, args string) erro
|
||||
return p.zoneCmdAdvance(ctx)
|
||||
case "abandon", "leave", "quit":
|
||||
return p.zoneCmdAbandon(ctx)
|
||||
case "taunt":
|
||||
return p.zoneCmdTaunt(ctx)
|
||||
case "compliment":
|
||||
return p.zoneCmdCompliment(ctx)
|
||||
default:
|
||||
return p.SendDM(ctx.Sender, zoneHelpText())
|
||||
}
|
||||
@@ -67,7 +73,9 @@ func zoneHelpText() string {
|
||||
b.WriteString("`!zone status` — show your current run\n")
|
||||
b.WriteString("`!zone map` — show the room layout\n")
|
||||
b.WriteString("`!zone advance` — resolve the current room and move on\n")
|
||||
b.WriteString("`!zone abandon` — end the active run (no rewards)")
|
||||
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)")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
@@ -472,6 +480,53 @@ func (p *AdventurePlugin) resolveBossRoom(userID id.UserID, run *DungeonRun, zon
|
||||
return b.String(), false, nil
|
||||
}
|
||||
|
||||
// ── taunt / compliment ──────────────────────────────────────────────────────
|
||||
|
||||
func (p *AdventurePlugin) zoneCmdTaunt(ctx MessageContext) error {
|
||||
return p.zoneMoodInteraction(ctx, MoodEventTaunt, tauntResponseLine, "💢")
|
||||
}
|
||||
|
||||
func (p *AdventurePlugin) zoneCmdCompliment(ctx MessageContext) error {
|
||||
return p.zoneMoodInteraction(ctx, MoodEventCompliment, complimentResponseLine, "💐")
|
||||
}
|
||||
|
||||
// zoneMoodInteraction is the shared wiring for !zone taunt / !zone compliment:
|
||||
// require an active run, apply the mood event, render a deterministic line
|
||||
// from the prewritten pool, and report the new mood band.
|
||||
func (p *AdventurePlugin) zoneMoodInteraction(
|
||||
ctx MessageContext,
|
||||
ev GMMoodEvent,
|
||||
render func(runID string, roomIdx int) string,
|
||||
icon string,
|
||||
) 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>`.")
|
||||
}
|
||||
_ = applyMoodDecayIfStale(run)
|
||||
newMood, err := applyMoodEvent(run.RunID, ev)
|
||||
if err != nil {
|
||||
return p.SendDM(ctx.Sender, "Couldn't apply mood event: "+err.Error())
|
||||
}
|
||||
var b strings.Builder
|
||||
if line := render(run.RunID, run.CurrentRoom); line != "" {
|
||||
b.WriteString(line)
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
delta := MoodEventDelta(ev)
|
||||
sign := "+"
|
||||
if delta < 0 {
|
||||
sign = "−"
|
||||
delta = -delta
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("%s GM mood: %d/100 (%s) _(%s%d)_",
|
||||
icon, newMood, gmMoodLabel(newMood), sign, delta))
|
||||
return p.SendDM(ctx.Sender, b.String())
|
||||
}
|
||||
|
||||
// ── abandon ─────────────────────────────────────────────────────────────────
|
||||
|
||||
func (p *AdventurePlugin) zoneCmdAbandon(ctx MessageContext) error {
|
||||
|
||||
@@ -244,3 +244,93 @@ func TestGMMoodLabel_Bands(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 11 D7 — !zone taunt / !zone compliment.
|
||||
|
||||
func TestZoneCmd_TauntAppliesMoodPenalty(t *testing.T) {
|
||||
setupAuditTestDB(t)
|
||||
uid := id.UserID("@zone-cmd-taunt: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)
|
||||
}
|
||||
startMood := before.GMMood
|
||||
if err := p.handleDnDZoneCmd(MessageContext{Sender: uid}, "taunt"); err != nil {
|
||||
t.Fatalf("taunt: %v", err)
|
||||
}
|
||||
after, err := getActiveZoneRun(uid)
|
||||
if err != nil || after == nil {
|
||||
t.Fatalf("active run after taunt: %v", err)
|
||||
}
|
||||
want := clampMood(startMood + MoodEventDelta(MoodEventTaunt))
|
||||
if after.GMMood != want {
|
||||
t.Errorf("mood after taunt = %d, want %d (start %d, delta %d)",
|
||||
after.GMMood, want, startMood, MoodEventDelta(MoodEventTaunt))
|
||||
}
|
||||
}
|
||||
|
||||
func TestZoneCmd_ComplimentAppliesMoodBonus(t *testing.T) {
|
||||
setupAuditTestDB(t)
|
||||
uid := id.UserID("@zone-cmd-compliment: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)
|
||||
}
|
||||
startMood := before.GMMood
|
||||
if err := p.handleDnDZoneCmd(MessageContext{Sender: uid}, "compliment"); err != nil {
|
||||
t.Fatalf("compliment: %v", err)
|
||||
}
|
||||
after, err := getActiveZoneRun(uid)
|
||||
if err != nil || after == nil {
|
||||
t.Fatalf("active run after compliment: %v", err)
|
||||
}
|
||||
want := clampMood(startMood + MoodEventDelta(MoodEventCompliment))
|
||||
if after.GMMood != want {
|
||||
t.Errorf("mood after compliment = %d, want %d (start %d, delta %d)",
|
||||
after.GMMood, want, startMood, MoodEventDelta(MoodEventCompliment))
|
||||
}
|
||||
}
|
||||
|
||||
func TestZoneCmd_TauntWithoutRunNoCrash(t *testing.T) {
|
||||
setupAuditTestDB(t)
|
||||
uid := id.UserID("@zone-cmd-taunt-norun:example")
|
||||
zoneCmdTestCharacter(t, uid, 1)
|
||||
defer cleanupZoneRuns(uid)
|
||||
p := &AdventurePlugin{}
|
||||
// No active run — should return cleanly with the no-run nudge.
|
||||
if err := p.handleDnDZoneCmd(MessageContext{Sender: uid}, "taunt"); err != nil {
|
||||
t.Fatalf("taunt no-run: %v", err)
|
||||
}
|
||||
if err := p.handleDnDZoneCmd(MessageContext{Sender: uid}, "compliment"); err != nil {
|
||||
t.Fatalf("compliment no-run: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTauntComplimentResponseLines_Deterministic(t *testing.T) {
|
||||
const runID = "run-d7-test"
|
||||
t1 := tauntResponseLine(runID, 0)
|
||||
t2 := tauntResponseLine(runID, 0)
|
||||
if t1 == "" || t1 != t2 {
|
||||
t.Errorf("taunt lines not deterministic: %q vs %q", t1, t2)
|
||||
}
|
||||
c1 := complimentResponseLine(runID, 0)
|
||||
c2 := complimentResponseLine(runID, 0)
|
||||
if c1 == "" || c1 != c2 {
|
||||
t.Errorf("compliment lines not deterministic: %q vs %q", c1, c2)
|
||||
}
|
||||
if t1 == c1 {
|
||||
t.Errorf("taunt and compliment shouldn't collide on same key: %q", t1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,6 +360,27 @@ func twinBeeLine(zoneID ZoneID, kind GMNarrationType, runID string, roomIdx int)
|
||||
return "🎭 **TwinBee:** " + line
|
||||
}
|
||||
|
||||
// tauntResponseLine renders a deterministic TwinBee reply to !zone taunt
|
||||
// from the prewritten flavor.TauntResponses pool. roomIdx is folded so
|
||||
// repeated taunts in the same room are stable but cross-room taunts vary.
|
||||
func tauntResponseLine(runID string, roomIdx int) string {
|
||||
line := pickLineDeterministic(flavor.TauntResponses, runID, roomIdx^0x4A8D9F25)
|
||||
if line == "" {
|
||||
return ""
|
||||
}
|
||||
return "🎭 **TwinBee:** " + line
|
||||
}
|
||||
|
||||
// complimentResponseLine renders a deterministic TwinBee reply to
|
||||
// !zone compliment from the prewritten flavor.ComplimentResponses pool.
|
||||
func complimentResponseLine(runID string, roomIdx int) string {
|
||||
line := pickLineDeterministic(flavor.ComplimentResponses, runID, roomIdx^0x3E172B6C)
|
||||
if line == "" {
|
||||
return ""
|
||||
}
|
||||
return "🎭 **TwinBee:** " + line
|
||||
}
|
||||
|
||||
// ── Mood event table & application ───────────────────────────────────────────
|
||||
|
||||
// GMMoodEvent enumerates the mood-modifier triggers from design doc §3.2.
|
||||
|
||||
Reference in New Issue
Block a user