mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
§7.2 cycle: every 3rd day's morning briefing in Blackspire fires the "manor reset" temporal log — non-boss rooms respawn one enemy each. Boss-defeated suppresses. Wrong-zone is a no-op. Exposes ManorReset(e, day) bool for the combat-link phase to read at !advance time when applying the actual respawn (per the deferral pattern from E2b's wandering-encounter combat). Reuses flavor.HauntedManorResetMorning per feedback_reuse_existing_flavor. Deferred: Night-2 quiet warning narration on the recap path — needs a new hook in deliverRecap; not load-bearing for the reset itself. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
140 lines
5.8 KiB
Go
140 lines
5.8 KiB
Go
package plugin
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
|
||
"gogobee/internal/flavor"
|
||
)
|
||
|
||
// Phase 12 E3 — Zone-specific Temporal Events.
|
||
// Spec: gogobee_expedition_system.md §7.
|
||
//
|
||
// Temporal events fire at briefing rollover and produce two outputs:
|
||
// - extraHarsh: forces double supply burn for the burn fired by THIS
|
||
// briefing (overrides the §4.1 zone HarshMod when the multiplier
|
||
// should be 2× regardless of tier — e.g. tidal flooding §7.1).
|
||
// - one or more flavor-bearing log entries (warnings, event narration).
|
||
//
|
||
// applyZoneTemporalPreBurn runs BEFORE applyDailyBurn so the burn
|
||
// reflects the temporal effect for the day being entered. State that
|
||
// persists across days (Heat stacks, Portal instability) is mutated
|
||
// here as well — the per-day increment is the temporal event.
|
||
//
|
||
// Combat-side knobs (tidal +1d6 cold, Underforge +1d4 fire, etc.) are
|
||
// exposed via ZoneTemporalEffects for the combat engine to read; the
|
||
// engine hook lands with the combat-link phase.
|
||
|
||
// ZoneTemporalEffects bundles the per-zone, per-day combat-side
|
||
// modifications produced by temporal events. Combat code reads this
|
||
// from the active expedition; nothing reads it yet (combat-link phase).
|
||
type ZoneTemporalEffects struct {
|
||
// TidalActive — Sunken Temple Day 6 only. Combat: +1d6 cold per
|
||
// turn while wading; kuo-toa +2 AC, +1d4 attack rolls.
|
||
TidalActive bool
|
||
}
|
||
|
||
// applyZoneTemporalPreBurn runs at the top of deliverBriefing, before
|
||
// applyDailyBurn. It mutates e (state increments, in-memory only —
|
||
// caller persists the relevant fields) and returns whether this
|
||
// briefing's burn should be force-doubled regardless of HarshMod.
|
||
//
|
||
// nextDay is the day-number we're rolling INTO (i.e. e.CurrentDay+1
|
||
// at call site, since the briefing increments after burn).
|
||
func applyZoneTemporalPreBurn(e *Expedition, nextDay int) (extraHarsh bool) {
|
||
switch e.ZoneID {
|
||
case ZoneSunkenTemple:
|
||
extraHarsh = sunkenTempleTemporalPreBurn(e, nextDay)
|
||
}
|
||
return extraHarsh
|
||
}
|
||
|
||
// ManorResetCycleDays is the §7.2 reset cadence — non-boss rooms
|
||
// respawn one enemy each on the morning of every Nth day.
|
||
const ManorResetCycleDays = 3
|
||
|
||
// applyZoneTemporalPostRollover runs AFTER advance + threat drift.
|
||
// It appends per-zone narration log entries for the day that just
|
||
// became current. Returns lines to append to the briefing body.
|
||
func applyZoneTemporalPostRollover(e *Expedition) []string {
|
||
switch e.ZoneID {
|
||
case ZoneSunkenTemple:
|
||
return sunkenTempleTemporalPostRollover(e)
|
||
case ZoneManorBlackspire:
|
||
return manorTemporalPostRollover(e)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// §7.1 — Sunken Temple, Tidal Event (Day 6)
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
|
||
// sunkenTempleTemporalPreBurn doubles burn on Day 6 (tidal peak). Skips
|
||
// entirely if the boss has been defeated — the spec's no-op clause.
|
||
func sunkenTempleTemporalPreBurn(e *Expedition, nextDay int) bool {
|
||
if e.BossDefeated {
|
||
return false
|
||
}
|
||
return nextDay == 6
|
||
}
|
||
|
||
// sunkenTempleTemporalPostRollover writes warnings on Day 4/5 and the
|
||
// event narration on Day 6. Boss-kill before Day 6 silences all of it.
|
||
func sunkenTempleTemporalPostRollover(e *Expedition) []string {
|
||
if e.BossDefeated {
|
||
return nil
|
||
}
|
||
day := e.CurrentDay
|
||
switch day {
|
||
case 4, 5:
|
||
line := flavor.Pick(flavor.SunkenTempleTidalWarning)
|
||
line = strings.ReplaceAll(line, "[N]", fmt.Sprintf("%d", day))
|
||
summary := fmt.Sprintf("tidal warning — peak in %d day(s)", 6-day)
|
||
_ = appendExpeditionLog(e.ID, day, "temporal", summary, line)
|
||
return []string{line}
|
||
case 6:
|
||
line := flavor.Pick(flavor.SunkenTempleTidalEvent)
|
||
_ = appendExpeditionLog(e.ID, day, "temporal",
|
||
"tidal peak — flooded rooms hostile, supply burn doubled", line)
|
||
return []string{line}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
// §7.2 — Haunted Manor of Blackspire, Nightly Reset
|
||
// ─────────────────────────────────────────────────────────────────────
|
||
|
||
// ManorReset reports whether the briefing rolling onto `day` is the
|
||
// reset morning (every 3rd day: 3, 6, 9, ...). Boss-defeated suppresses.
|
||
func ManorReset(e *Expedition, day int) bool {
|
||
if e.BossDefeated || e.ZoneID != ZoneManorBlackspire {
|
||
return false
|
||
}
|
||
return day >= ManorResetCycleDays && day%ManorResetCycleDays == 0
|
||
}
|
||
|
||
// manorTemporalPostRollover writes a "house reset" entry on Day 3, 6,
|
||
// 9, ... and a quiet pre-reset warning on the day after that (Day 4,
|
||
// 7, ...) — which corresponds to the briefing fired on the morning
|
||
// after the warning night, so we anchor narration at the briefing.
|
||
//
|
||
// Spec §7.2: "TwinBee notes on Night 2: 'The house has been quiet.
|
||
// That will not last.' On Night 3 morning: 'Overnight, something moved
|
||
// back in.'" — we fire the reset narration on the reset morning. The
|
||
// quiet-warning Night 2 line lives on the recap path (E2b's deliverRecap
|
||
// will add it as a separate hook in a future commit if desired); for
|
||
// E3b we keep the rule simple: only the reset morning fires here.
|
||
func manorTemporalPostRollover(e *Expedition) []string {
|
||
day := e.CurrentDay
|
||
if !ManorReset(e, day) {
|
||
return nil
|
||
}
|
||
line := flavor.Pick(flavor.HauntedManorResetMorning)
|
||
_ = appendExpeditionLog(e.ID, day, "temporal",
|
||
fmt.Sprintf("manor reset — non-boss rooms respawned one enemy each (cycle day %d)", day),
|
||
line)
|
||
return []string{line}
|
||
}
|