mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 08:52:42 +00:00
Adv 2.0 D&D Phase 12 E3e: Dragon's Lair awareness pulses
§7.5 awareness pulses on Day 3, 6, 9, 12, ... apply +10 threat at briefing rollover and log a temporal entry (DragonsLairAwarenessPulse flavor). Day-14 awakening sets RegionState["infernax_awake"]=true once, with the §7.5 awakening narration; the pulse line is suppressed that day so the awakening narration carries the moment alone. Combat-side knobs (kobold patrols +1 enemy per active room, 6h wandering cadence, dragon-encounter weighting in the wandering table, forced extraction pressure once awake) are exposed via the infernax_awake region flag — combat-link phase reads it to switch behavior. Boss-defeated suppresses entirely. Reuses flavor.DragonsLairAwarenessPulse / DragonsLairAwakenWarning per feedback_reuse_existing_flavor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -138,10 +138,18 @@ func applyZoneTemporalPostRollover(e *Expedition) []string {
|
|||||||
return underforgeTemporalPostRollover(e)
|
return underforgeTemporalPostRollover(e)
|
||||||
case ZoneFeywildCrossing:
|
case ZoneFeywildCrossing:
|
||||||
return feywildTemporalPostRollover(e)
|
return feywildTemporalPostRollover(e)
|
||||||
|
case ZoneDragonsLair:
|
||||||
|
return dragonsLairTemporalPostRollover(e)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DragonsLairAwarenessCycleDays — §7.5 awareness pulse cadence.
|
||||||
|
const DragonsLairAwarenessCycleDays = 3
|
||||||
|
|
||||||
|
// DragonsLairAwakenDay — §7.5 Infernax wakes if not yet at the boss.
|
||||||
|
const DragonsLairAwakenDay = 14
|
||||||
|
|
||||||
// UnderforgeMaxHeat is the §7.3 cap.
|
// UnderforgeMaxHeat is the §7.3 cap.
|
||||||
const UnderforgeMaxHeat = 10
|
const UnderforgeMaxHeat = 10
|
||||||
|
|
||||||
@@ -360,6 +368,65 @@ func feywildTemporalPostRollover(e *Expedition) []string {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
// §7.5 — Dragon's Lair, Awareness Pulses + Infernax Awakening
|
||||||
|
// ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// dragonsLairTemporalPostRollover fires an awareness pulse every 3
|
||||||
|
// days (threat +10, narration) and the Day-14 awakening event if the
|
||||||
|
// boss hasn't been killed yet.
|
||||||
|
func dragonsLairTemporalPostRollover(e *Expedition) []string {
|
||||||
|
if e.BossDefeated {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
day := e.CurrentDay
|
||||||
|
var lines []string
|
||||||
|
|
||||||
|
// Day 14 awakening: one-time. Mark on RegionState; downstream
|
||||||
|
// combat-link reads "infernax_awake" to switch wandering cadence
|
||||||
|
// to every 6 hours and add the dragon encounter weighting.
|
||||||
|
if day >= DragonsLairAwakenDay {
|
||||||
|
if awake, _ := e.RegionState["infernax_awake"].(bool); !awake {
|
||||||
|
e.RegionState["infernax_awake"] = true
|
||||||
|
_ = persistRegionState(e)
|
||||||
|
line := flavor.Pick(flavor.DragonsLairAwakenWarning)
|
||||||
|
_ = appendExpeditionLog(e.ID, day, "temporal",
|
||||||
|
"infernax awakens — active hunting begins, 6h wandering checks",
|
||||||
|
line)
|
||||||
|
lines = append(lines, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Awareness pulse every 3 days (3, 6, 9, 12, ...). On Day 14, the
|
||||||
|
// awakening narration takes precedence — we still apply the +10
|
||||||
|
// since the cadence math says it's a pulse day too.
|
||||||
|
if day >= DragonsLairAwarenessCycleDays && day%DragonsLairAwarenessCycleDays == 0 {
|
||||||
|
_ = applyThreatDelta(e.ID, 10, fmt.Sprintf("dragon awareness pulse (day %d)", day))
|
||||||
|
// Refresh in-memory threat after the bump.
|
||||||
|
e.ThreatLevel += 10
|
||||||
|
if e.ThreatLevel > 100 {
|
||||||
|
e.ThreatLevel = 100
|
||||||
|
}
|
||||||
|
// Skip the pulse narration if the awaken line already fired
|
||||||
|
// today — they say largely the same thing in different keys.
|
||||||
|
alreadyAwoke := false
|
||||||
|
for _, l := range lines {
|
||||||
|
if l != "" {
|
||||||
|
alreadyAwoke = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !alreadyAwoke {
|
||||||
|
line := flavor.Pick(flavor.DragonsLairAwarenessPulse)
|
||||||
|
_ = appendExpeditionLog(e.ID, day, "temporal",
|
||||||
|
fmt.Sprintf("awareness pulse — threat +10 (day %d)", day),
|
||||||
|
line)
|
||||||
|
lines = append(lines, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
// reduceUnderforgeHeat is called by processOvernightCamp when the
|
// reduceUnderforgeHeat is called by processOvernightCamp when the
|
||||||
// active camp is fortified (or base) in the Underforge — long rest
|
// active camp is fortified (or base) in the Underforge — long rest
|
||||||
// drops heat stacks by 2 per §7.3. Returns the new stack count.
|
// drops heat stacks by 2 per §7.3. Returns the new stack count.
|
||||||
|
|||||||
@@ -527,6 +527,148 @@ func TestFeywild_NormalDay_NoTemporalLog(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDragonsLair_AwarenessPulseFiresEveryThreeDays(t *testing.T) {
|
||||||
|
setupZoneRunTestDB(t)
|
||||||
|
uid := id.UserID("@exp-dragon-pulse:example")
|
||||||
|
defer cleanupExpeditions(uid)
|
||||||
|
|
||||||
|
exp, err := startExpedition(uid, ZoneDragonsLair, "",
|
||||||
|
ExpeditionSupplies{Current: 60, Max: 60, DailyBurn: 4, HarshMod: 3})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
p := &AdventurePlugin{}
|
||||||
|
|
||||||
|
// Roll forward to Day 3 — first awareness pulse.
|
||||||
|
setExpDay(t, exp.ID, 2)
|
||||||
|
if _, err := db.Get().Exec(
|
||||||
|
`UPDATE dnd_expedition SET last_briefing_at = NULL, supplies_json = ? WHERE expedition_id = ?`,
|
||||||
|
`{"current":99,"max":99,"daily_burn":4,"harsh_mod":3,"foraged_today":false,"packs_standard":3,"packs_deluxe":0}`,
|
||||||
|
exp.ID); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
fresh, _ := getExpedition(exp.ID)
|
||||||
|
prevThreat := fresh.ThreatLevel
|
||||||
|
if err := p.deliverBriefing(fresh, time.Now().UTC()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, _ := getExpedition(exp.ID)
|
||||||
|
// +10 awareness pulse + ~3 daily threat drift = 13 (mod GMMood 50 = neutral).
|
||||||
|
if got.ThreatLevel < prevThreat+10 {
|
||||||
|
t.Errorf("threat = %d, want ≥ %d (awareness pulse +10)", got.ThreatLevel, prevThreat+10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify pulse log entry.
|
||||||
|
rows, _ := db.Get().Query(
|
||||||
|
`SELECT summary FROM dnd_expedition_log WHERE expedition_id = ? AND entry_type = 'temporal' ORDER BY entry_id`,
|
||||||
|
exp.ID)
|
||||||
|
defer rows.Close()
|
||||||
|
foundPulse := false
|
||||||
|
for rows.Next() {
|
||||||
|
var s string
|
||||||
|
_ = rows.Scan(&s)
|
||||||
|
if strings.Contains(s, "awareness pulse") {
|
||||||
|
foundPulse = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundPulse {
|
||||||
|
t.Error("expected an 'awareness pulse' temporal log entry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDragonsLair_NonPulseDay_NoThreatJump(t *testing.T) {
|
||||||
|
setupZoneRunTestDB(t)
|
||||||
|
uid := id.UserID("@exp-dragon-nonpulse:example")
|
||||||
|
defer cleanupExpeditions(uid)
|
||||||
|
|
||||||
|
exp, err := startExpedition(uid, ZoneDragonsLair, "",
|
||||||
|
ExpeditionSupplies{Current: 60, Max: 60, DailyBurn: 4, HarshMod: 3})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
setExpDay(t, exp.ID, 1) // → Day 2; not a pulse day.
|
||||||
|
fresh, _ := getExpedition(exp.ID)
|
||||||
|
prev := fresh.ThreatLevel
|
||||||
|
if err := (&AdventurePlugin{}).deliverBriefing(fresh, time.Now().UTC()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, _ := getExpedition(exp.ID)
|
||||||
|
// Only the +3 daily drift should apply.
|
||||||
|
if got.ThreatLevel-prev > 5 {
|
||||||
|
t.Errorf("non-pulse day threat jump = %d, want ≤ 5", got.ThreatLevel-prev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDragonsLair_Day14_Awakens(t *testing.T) {
|
||||||
|
setupZoneRunTestDB(t)
|
||||||
|
uid := id.UserID("@exp-dragon-wake:example")
|
||||||
|
defer cleanupExpeditions(uid)
|
||||||
|
|
||||||
|
exp, err := startExpedition(uid, ZoneDragonsLair, "",
|
||||||
|
ExpeditionSupplies{Current: 60, Max: 60, DailyBurn: 4, HarshMod: 3})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
setExpDay(t, exp.ID, 13) // → Day 14
|
||||||
|
if _, err := db.Get().Exec(
|
||||||
|
`UPDATE dnd_expedition SET supplies_json = ? WHERE expedition_id = ?`,
|
||||||
|
`{"current":99,"max":99,"daily_burn":4,"harsh_mod":3,"foraged_today":false,"packs_standard":3,"packs_deluxe":0}`,
|
||||||
|
exp.ID); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
fresh, _ := getExpedition(exp.ID)
|
||||||
|
if err := (&AdventurePlugin{}).deliverBriefing(fresh, time.Now().UTC()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, _ := getExpedition(exp.ID)
|
||||||
|
if awake, _ := got.RegionState["infernax_awake"].(bool); !awake {
|
||||||
|
t.Error("infernax_awake should be true on Day 14")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify both Awaken (and possibly pulse) log entries fired.
|
||||||
|
rows, _ := db.Get().Query(
|
||||||
|
`SELECT summary FROM dnd_expedition_log WHERE expedition_id = ? AND entry_type = 'temporal'`,
|
||||||
|
exp.ID)
|
||||||
|
defer rows.Close()
|
||||||
|
foundAwaken := false
|
||||||
|
for rows.Next() {
|
||||||
|
var s string
|
||||||
|
_ = rows.Scan(&s)
|
||||||
|
if strings.Contains(s, "infernax awakens") {
|
||||||
|
foundAwaken = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundAwaken {
|
||||||
|
t.Error("expected 'infernax awakens' temporal entry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDragonsLair_BossDefeatedSilencesPulses(t *testing.T) {
|
||||||
|
setupZoneRunTestDB(t)
|
||||||
|
uid := id.UserID("@exp-dragon-boss:example")
|
||||||
|
defer cleanupExpeditions(uid)
|
||||||
|
|
||||||
|
exp, err := startExpedition(uid, ZoneDragonsLair, "",
|
||||||
|
ExpeditionSupplies{Current: 60, Max: 60, DailyBurn: 4, HarshMod: 3})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := db.Get().Exec(
|
||||||
|
`UPDATE dnd_expedition SET boss_defeated = 1, current_day = 5 WHERE expedition_id = ?`,
|
||||||
|
exp.ID); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
fresh, _ := getExpedition(exp.ID)
|
||||||
|
prev := fresh.ThreatLevel
|
||||||
|
if err := (&AdventurePlugin{}).deliverBriefing(fresh, time.Now().UTC()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, _ := getExpedition(exp.ID)
|
||||||
|
if got.ThreatLevel != prev {
|
||||||
|
t.Errorf("post-boss threat changed: %d → %d", prev, got.ThreatLevel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSunkenTemple_NoTidalOnNonZone(t *testing.T) {
|
func TestSunkenTemple_NoTidalOnNonZone(t *testing.T) {
|
||||||
setupZoneRunTestDB(t)
|
setupZoneRunTestDB(t)
|
||||||
uid := id.UserID("@exp-tidal-other:example")
|
uid := id.UserID("@exp-tidal-other:example")
|
||||||
|
|||||||
Reference in New Issue
Block a user