Commit Graph

12 Commits

Author SHA1 Message Date
prosolis
c84682abf7 Combat: guard daily-cron paths against mid-fight combat sessions
A turn-based elite/boss combat session locks the run for up to 1h and
can straddle any scheduled tick. Add hasActiveCombatSession() and consult
it from the morning DM, midnight idle reaper, expedition briefing/recap,
and mid-day random event paths so none of them fire DMs or mutate run
state into a live fight.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 08:01:10 -07:00
prosolis
45863bd5f3 Adv 2.0 D&D Phase R7: post-completion audit hardening
High-priority fixes from the multi-agent audit of Adventure 2.0:

- Phase 11 nil-deref cluster: zoneOrFallback() helper replaces 8 unsafe
  getZone(_) sites in dnd_zone_cmd.go. Corrupted zone IDs render a
  placeholder instead of panicking.
- Briefing/recap idempotency: deliverBriefing/deliverRecap now claim
  the rollover via a conditional UPDATE. Double-fires from clock skew
  or restart become no-ops; supply burn and day++ no longer reapply.
- Graceful ticker shutdown: AdventurePlugin gains stopCh + Stop(); all
  11 background tickers now select on stopCh in addition to ticker.C.
- Mood decay (§3.2): math.Round(elapsed*2) replaces int() truncation
  so sub-hour gaps decay correctly.
- 24h auto-abandon (§4.3): getActiveZoneRun returns clean slate and
  abandons stale runs whose LastActionAt is over 24h old.
- Respec / auto-migrate orphan cleanup: !respec and the auto-migrated
  draft wipe now abandon active zone runs and expeditions before
  deleting the dnd_character row.
- Phase R combat-link: applyBossDefeatThreat now wired from
  resolveBossRoom (-20 threat); applyRoomCombatThreatForUser adds
  +5/+8 from non-boss/elite kills (§8.1).
- Starvation → forced extraction: briefing-time check forces extract
  with §10.2 coin tax when supplies hit zero.
- GMNat20/Nat1 narration wired into resolveCombatRoom and
  resolveBossRoom (nat-20 takes precedence over nat-1).
- Treasure-undo race: LoadAndDelete on both timer-fire and `undo`
  paths so only one side wins.
- Battle Master: Disarming, Menacing, Parry maneuvers added (3 → 6
  of 10). Remaining 4 (Pushing, Goading, Riposte, Commander's Strike)
  documented inline as needing ally/reaction mechanics the engine
  doesn't model.
- Threat-70 warning: tracked in RegionState["siege_warning_fired"]
  so a drop-and-recross doesn't re-fire the beat.
- region_state JSON decode error now logged via slog.Warn instead
  of silently discarded.

Failing TestProdDB_DnDLayer fixed via option (a): track migrated
characters and only run round-trip / idempotency assertions on those,
skipping pre-existing prod-DB rows accumulated from live bot use.

New tests in dnd_audit_phase_R7_test.go cover: 24h auto-abandon,
briefing double-fire idempotency, threat-70 warning idempotency,
multi-region extract→resume state preservation, and starvation
forced-extraction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:22 -07:00
prosolis
cbb2670bb8 Adv 2.0 D&D Phase 12 E6b: Expedition milestones (§13)
Daily milestones fire from the morning briefing once the day count
crosses the threshold (CurrentDay reflects the new day post-rollover):
- First Night (day 2, +50 XP)
- Week One (day 8, +200 XP, Thom Krooke discount flag — hookup deferred)
- Two Weeks (day 15, +500 XP, +1 primary stat — char-system hookup deferred)

Completion milestones via AwardCompletionMilestones, exported for the
combat-link boss-kill path to call once status flips to 'complete':
- Patient Zero (max threat ≤ 50, +10% of XPEarned bonus)
- Survivalist (tier ≥ 3, no forced extraction ever, title flag)
- Long Game (tier 5, legendary item — loot-grant hookup deferred)

Reuses MilestoneFirstNight/WeekOne/TwoWeeks/PatientZero/TheLongGame
flavor pools from twinbee_expedition_flavor.go. Cartographer and
Survivalist have no flavor pool (Survivalist uses an inline string,
Cartographer is fully deferred since it needs combat-link search hooks).

Awarded keys persist as RegionState["milestones"]. Max threat sampled
daily into RegionState["max_threat_seen"] before milestone checks
read it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:22 -07:00
prosolis
d179ca27a7 Adv 2.0 D&D Phase 12 E5: Extraction & Resume
Voluntary !extract burns 1 day, flips status to 'extracting', keeps
all rewards, resumable for 7 real days. Forced extraction (Abyss
collapse, future HP-0/supply-0 hooks) flips to 'abandoned' and applies
the §10.2 20% coin tax via the cycle layer's euro handle. !resume
within the 7-day window repurchases supplies and restores threat &
temporal stacks as-is; expired rows demote to failed.

getActiveExpedition / briefing / recap loaders narrow to status='active'
so 'extracting' rows don't get phantom day rollovers. Reuses
flavor.ExtractionVoluntary, ExtractionForced, ExpeditionResume from
twinbee_expedition_flavor.go.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:22 -07:00
prosolis
3a000ada8c Adv 2.0 D&D Phase 12 E3d: Feywild Crossing time distortion
§7.4 daily 1d6 distortion roll fires at briefing time:
  • 1–2 normal day (no override)
  • 3–4 half-day → 0.5× burn multiplier override
  • 5    double-day → 2.0× burn + extra wandering check at recap
  • 6    time loop → narration only; combat-link reads on next room

Refactors applyZoneTemporalPreBurn to return TemporalBurnOverride
(Multiplier-based) instead of a force-double bool. Sunken Temple's
Day-6 tidal piggybacks on the same override pipeline (2.0×) instead
of the siege-flag hack.

Persists the day's distortion bucket to RegionState["feywild_today"]
so the recap path can fire the §7.4 extra wandering check on
double-day.

Reuses flavor.FeywildTimeDistortionHalf / Double / Loop per
feedback_reuse_existing_flavor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:22 -07:00
prosolis
044baddcce Adv 2.0 D&D Phase 12 E3c: Underforge heat accumulation
§7.3 heat stacks: +1/day in the Underforge (cap 10), persisted via
new updateTemporalStack helper. Band thresholds:
  • 1–3 ambient (no narration)
  • 4–6 warning band (combat fire damage applies, narration logged)
  • 7–9 supply band (forces harsh-conditions burn = +50%, narration)
  • 10 critical (exhaustion narration, log entry)

Fortified/base camp long rest in the Underforge drops heat by 2
(updates briefing's "Fortified rest" summary line), per §7.3.

Replaces the placeholder `TemporalStack > 0 → harsh` predicate in
deliverBriefing with zoneTemporalHarsh(), which is now per-zone aware
— Heat 1–3 in the Underforge is correctly flavor-only and no longer
prematurely doubles supply burn from Day 1.

Combat-side knobs (+1d4 fire round-start damage at heat 4–6, dis CON
saves at 7–9, -2 to all rolls at 10) are exposed via heat band but
not yet applied — combat-link phase reads heat band off the active
expedition.

Reuses flavor.UnderforgHeapWarning / UnderforgHeapCritical per
feedback_reuse_existing_flavor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:22 -07:00
prosolis
41c60617db Adv 2.0 D&D Phase 12 E3a: Sunken Temple tidal event
Adds the §7.1 tidal event hook: warnings on Days 4 and 5, peak event
on Day 6 with forced 2× supply burn (overrides tier-2 HarshMod 1.5×),
silenced if the boss has been defeated.

Wires zone-temporal events into deliverBriefing as two new hook points
— pre-burn (mutates burn multiplier) and post-rollover (appends
flavor-bearing log entries). Future E3b–E3f layer onto the same hooks.

Reuses the prewritten flavor.SunkenTempleTidalWarning / TidalEvent
pools per feedback_reuse_existing_flavor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:22 -07:00
prosolis
f844068660 Adv 2.0 D&D Phase 12 E2d: Fortified Camp branch + overnight rest effects
§5.1 fortified camp unlocked in !camp, gated on exp.BossDefeated for
now (cache-site waypoints land in E3+). +2 SU cost was already in the
E1e cost table; this commit just stops rejecting the type.

§3 / §5.1 / §8.1 overnight rest applied at briefing rollover via
processOvernightCamp:
  - rough: HP floor of HPMax/2 (no spell refresh)
  - standard: full HP, exhaustion -1, refreshAllResources +
    refreshSpellSlots
  - fortified / base: standard rest + 1d6 HP bonus on wake +
    threat-clock -5 (§8.1 modifier)

The camp auto-breaks once the rest applies — staying camped multi-day
isn't a thing in the spec; the player can re-pitch on the next night
if they want to. Briefing body now includes a 💤 line summarizing the
HP/threat delta from the night's rest.

Combat-driven side of fortified ('+1d6 HP on wake' as a rare flavor
event) currently surfaces as the rest summary; a peaceful-night TwinBee
narration variant could come later if we add a dedicated pool — for
now reusing the existing CampEstablished ambience suffices.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:22 -07:00
prosolis
5b8ef740f8 Adv 2.0 D&D Phase 12 E2c: Siege Mode effects + threat-70 warning
§8.3 siege economics:
- applyDailyBurn now takes an explicit siege flag and enforces a 2× floor
  on supply burn even when HarshMod is below 2 (tier-1 zones still get
  starved out per spec).
- currentBurn mirrors the same precedence so status/briefing readouts
  stay consistent.
- Briefing rollover passes e.SiegeMode through, decoupling siege from
  the harsh-conditions composite.

§8.3 threat-70 warning: applyDailyThreatDrift emits a one-time
appendApproachingSiegeLog when the level crosses 70 (prevLevel<70 and
new level≥70). Pulls from a new flavor.ThreatClockApproachingSiege pool
seeded with the spec's verbatim warning beat plus two voice-matched
alternates.

Other siege effects (boss +20 HP / Legendary Resistance, cleared-room
respawn, no-short-rest enforcement) stay deferred to the combat-link
phase — ThreatBandInfo already exposes the flags that engine will read.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:22 -07:00
prosolis
9b48dda79e Adv 2.0 D&D Phase 12 E2b: Wandering monster table + night phase
§6 night-phase resolution. Fires once per recap when the player has an
active camp. resolveWanderingCheck rolls 1d20 + threat mod (+1 per full
10 above 30) + camp mod (rough +3, fortified -4, base -6) + class mod
(ranger -2 in wilderness zones), then buckets per §6.1 into Peaceful /
Passage / Minor / Standard / Elite / Ambush.

Encounter side picks a roster entry from the zone (weighted by
SpawnWeight, biased to elite for Elite/Ambush) and persists the result
to camp.NightEvents + a "night"-typed log entry. Signs-of-passage adds
+2 threat per spec. Combat resolution itself is deferred — expeditions
don't host their own combat loop yet — so encounter outcomes log as
pending and surface in the recap with an "encounter pending" hint;
the combat-link phase will read these on next !advance.

Flavor reuse: encounter narration pulls from the existing ThreatClock
{Stirring,Alert,Hostile,Siege} pools, matched to the current band so
voice tracks the dungeon's awareness level.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:22 -07:00
prosolis
b520b0ce2d Adv 2.0 D&D Phase 12 E2a: Threat Clock state machine
Threshold model on top of the E1a persistence: ThreatBand bracketing
(Quiet/Stirring/Alert/Hostile/Siege) with per-band combat/supply/rest
knobs (ThreatBandInfo). Daily threat drift (+3/day, GMMood-modded:
effusive→-3 elated, hostile→+5 wrathful) wired into the 06:00 briefing
rollover; no-op once the boss is down.

Threshold crossings emit a flavor-bearing log entry pulling from the
prewritten flavor.ThreatClock{Stirring,Alert,Hostile,Siege} pools.
applyBossDefeatThreat is the -20 hook for the eventual combat-completion
wiring (combat→expedition link is a later phase).

Combat-driven modifiers (loud-ability, escape, combat-in-new-room) are
deferred to the same combat-link phase since expeditions don't currently
host their own combat path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:22 -07:00
prosolis
7528d5c239 Adv 2.0 D&D Phase 12 E1d: real-time day cycle (06:00 briefing / 21:00 recap)
Two new tickers in adventure.go startup. Each minute, when UTC clock hits
the trigger hour, walks every active expedition whose last_briefing_at /
last_recap_at is stale relative to today's threshold and fires it. Per-
expedition gating (rather than global db.JobCompleted) means a player who
started mid-day still gets their first briefing the next morning.

Briefing: applies one day's supply burn (harsh mult kicks in when threat
> 60 / siege / temporal stack > 0), advances current_day by one, picks
flavor.MorningBriefing<DayBand> (Day1/Day3/Day7/Day14/Day21 → otherwise
Generic), DMs the §12.1 block, appends a 'briefing' log entry, stamps
last_briefing_at.

Recap: scans today's log entries — boss kill picks
flavor.EveningRecapBossKilled, no combat + no narrative entries picks
EveningRecapNothingHappened, otherwise EveningRecapGeneric. DMs the
§12.2 block (HP-aware close-call detection deferred to E2 since it
needs combat hooks). Stamps last_recap_at.

Tests cover day-band selection, day advance + supply burn (incl. harsh
mult), threshold filtering, recap log entry creation, and boss-kill
override.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:22 -07:00