mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Adv 2.0 D&D Phase 11 D6: boss phase-two narration
Wires the eight prewritten *PhaseTwoLines pools (Valdris / HollowKing / Aldric / Thyrak / Ilvaras / Thornmother / Infernax / Belaxath) into resolveBossRoom. When the combat event log shows enemy HP crossing the zone's PhaseTwoAt threshold, a TwinBee phase-two callout is injected between the boss-stat header and the win/loss line. Bosses without a phase-two transition (Grol, Aboleth) are unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -448,6 +448,12 @@ func (p *AdventurePlugin) resolveBossRoom(userID id.UserID, run *DungeonRun, zon
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString(fmt.Sprintf("👑 **Boss — %s** (HP %d, AC %d)\n", monster.Name, monster.HP, monster.AC))
|
||||
if phaseTwoCrossedInEvents(result.Events, result.EnemyStartHP, zone.Boss.PhaseTwoAt) {
|
||||
if line := bossPhaseTwoLine(zone.ID, run.RunID, run.CurrentRoom); line != "" {
|
||||
b.WriteString(line)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
if !result.PlayerWon {
|
||||
_, _ = applyMoodEvent(run.RunID, MoodEventPlayerDeath)
|
||||
_ = abandonZoneRun(userID)
|
||||
|
||||
@@ -214,6 +214,61 @@ func bossSignaturePool(zoneID ZoneID) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// bossPhaseTwoPool returns the zone-boss phase-two callout pool — the
|
||||
// line surfaced when the boss crosses its PhaseTwoAt HP threshold mid-fight.
|
||||
// Returns nil for bosses with no phase-two transition (Grol, Aboleth).
|
||||
func bossPhaseTwoPool(zoneID ZoneID) []string {
|
||||
switch zoneID {
|
||||
case ZoneCryptValdris:
|
||||
return flavor.ValdrisPhaseTwoLines
|
||||
case ZoneForestShadows:
|
||||
return flavor.HollowKingPhaseTwoLines
|
||||
case ZoneManorBlackspire:
|
||||
return flavor.AldricPhaseTwoLines
|
||||
case ZoneUnderforge:
|
||||
return flavor.ThyrakPhaseTwoLines
|
||||
case ZoneUnderdark:
|
||||
return flavor.IlvarasPhaseTwoLines
|
||||
case ZoneFeywildCrossing:
|
||||
return flavor.ThornmotherPhaseTwoLines
|
||||
case ZoneDragonsLair:
|
||||
return flavor.InfernaxPhaseTwoLines
|
||||
case ZoneAbyssPortal:
|
||||
return flavor.BelaxathPhaseTwoLines
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// bossPhaseTwoLine renders the zone-specific phase-two transition line.
|
||||
// Empty string when the zone has no phase-two pool.
|
||||
func bossPhaseTwoLine(zoneID ZoneID, runID string, roomIdx int) string {
|
||||
pool := bossPhaseTwoPool(zoneID)
|
||||
if len(pool) == 0 {
|
||||
return ""
|
||||
}
|
||||
line := pickLineDeterministic(pool, runID, roomIdx^0x71F4A2D3)
|
||||
if line == "" {
|
||||
return ""
|
||||
}
|
||||
return "🎭 **TwinBee:** " + line
|
||||
}
|
||||
|
||||
// phaseTwoCrossedInEvents reports whether enemy HP crossed the
|
||||
// PhaseTwoAt fraction of startHP at any point in the events log. Returns
|
||||
// false when threshold <= 0 (no phase 2) or startHP <= 0.
|
||||
func phaseTwoCrossedInEvents(events []CombatEvent, startHP int, threshold float64) bool {
|
||||
if threshold <= 0 || startHP <= 0 {
|
||||
return false
|
||||
}
|
||||
cutoff := int(float64(startHP) * threshold)
|
||||
for _, ev := range events {
|
||||
if ev.EnemyHP <= cutoff {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// eliteRoomEntryPool returns the zone-specific elite-room atmospheric
|
||||
// pool. Returns nil when the zone has no dedicated elite prose.
|
||||
func eliteRoomEntryPool(zoneID ZoneID) []string {
|
||||
|
||||
@@ -445,3 +445,58 @@ func TestRoomEntryPool_Tier4And5PrependsZoneSpecific(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 11 D6 — boss phase-two narration ──────────────────────────────────
|
||||
|
||||
func TestBossPhaseTwoPool_RoutesEightZones(t *testing.T) {
|
||||
cases := map[ZoneID]int{
|
||||
ZoneCryptValdris: len(flavor.ValdrisPhaseTwoLines),
|
||||
ZoneForestShadows: len(flavor.HollowKingPhaseTwoLines),
|
||||
ZoneManorBlackspire: len(flavor.AldricPhaseTwoLines),
|
||||
ZoneUnderforge: len(flavor.ThyrakPhaseTwoLines),
|
||||
ZoneUnderdark: len(flavor.IlvarasPhaseTwoLines),
|
||||
ZoneFeywildCrossing: len(flavor.ThornmotherPhaseTwoLines),
|
||||
ZoneDragonsLair: len(flavor.InfernaxPhaseTwoLines),
|
||||
ZoneAbyssPortal: len(flavor.BelaxathPhaseTwoLines),
|
||||
}
|
||||
for z, want := range cases {
|
||||
got := bossPhaseTwoPool(z)
|
||||
if len(got) == 0 || len(got) != want {
|
||||
t.Errorf("%s phase-two pool len mismatch: got %d, want %d", z, len(got), want)
|
||||
}
|
||||
}
|
||||
// Bosses without a phase-two transition return nil.
|
||||
for _, z := range []ZoneID{ZoneGoblinWarrens, ZoneSunkenTemple} {
|
||||
if got := bossPhaseTwoPool(z); got != nil {
|
||||
t.Errorf("%s should not have a phase-two pool (no PhaseTwoAt)", z)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBossPhaseTwoLine_PrependsTwinBee(t *testing.T) {
|
||||
line := bossPhaseTwoLine(ZoneCryptValdris, "run-d6", 4)
|
||||
if !strings.HasPrefix(line, "🎭 **TwinBee:**") {
|
||||
t.Errorf("phase-two line missing TwinBee prefix: %q", line)
|
||||
}
|
||||
if bossPhaseTwoLine(ZoneGoblinWarrens, "run-d6", 4) != "" {
|
||||
t.Errorf("Grol has no phase-two pool — expected empty string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhaseTwoCrossedInEvents(t *testing.T) {
|
||||
startHP := 100
|
||||
threshold := 0.5
|
||||
events := []CombatEvent{{EnemyHP: 80}, {EnemyHP: 60}, {EnemyHP: 40}}
|
||||
if !phaseTwoCrossedInEvents(events, startHP, threshold) {
|
||||
t.Errorf("expected crossing at EnemyHP=40 (cutoff=50)")
|
||||
}
|
||||
if phaseTwoCrossedInEvents(events[:2], startHP, threshold) {
|
||||
t.Errorf("did not cross 50%% by event 2 (EnemyHP=60); should be false")
|
||||
}
|
||||
if phaseTwoCrossedInEvents(events, startHP, 0) {
|
||||
t.Errorf("threshold 0 means no phase 2 — should be false")
|
||||
}
|
||||
if phaseTwoCrossedInEvents(events, 0, threshold) {
|
||||
t.Errorf("startHP 0 — should be false (degenerate)")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user