A per-player NPC rival who "runs" the same zone progression on a midnight
ticker at ~1.3x the player's own clear pace, staying just ahead so it's a
race you can always see and nearly catch. Pure theatre: no combat, no
punishment, only race pressure and two payoffs at each zone clear.
- New adventure_shadow table, deliberately OUTSIDE the player_meta save
fan-out so a character save can never clobber the ticker's advance (the
isolation journal_pages earns by being grant-only, made structural). No
bootstrap: absent row == no Shadow, minted lazily on first advance.
- midnightReset advances every player's Shadow once per UTC day (own
idempotency guard); lead-capped so it never runs >2.5 zones ahead. When it
clears a zone the player hasn't, it leaves a journal page waiting (D1 tie-in).
- Morning-briefing race-pressure one-liners (TwinBee voice, deterministic).
- Zone-clear payoff in finalizeExpeditionOnZoneClear: a bonus-XP crow when the
player got there first, or the Shadow's waiting page when it did.
- !adventure shadow status view.
Review fixes (3 finders + verify) folded in before commit:
- Crow XP is now set-once per zone (crowed_mask), so re-running a zone the
Shadow hasn't reached can't farm it.
- The waiting page is granted BEFORE the pending bit is retired, so a transient
grant failure leaves the debt for the next clear instead of swallowing a page.
- The crow line no longer claims "+XP" when the grant errored.
Combat golden byte-identical (Shadow never touches SimulateCombat); go
build/vet/test green repo-wide.
Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
A. An armed ability lasted one round of a turn-based fight.
buildZoneCombatants called applyArmedAbility, which applies an ability's mods
*and* clears ArmedAbility and saves the sheet. The turn engine calls that
builder again on every !attack / !cast / !consume, so round 1 fired the ability
and disarmed the character, and every later round rebuilt them with none of its
mods. A Berserker paid stamina for a single round of BerserkerRage /
RageMeleeDmg / PhysicalResistRage / FrenzyDmgBonus. Every entry in
dndActiveAbilities had the same shape. mods.BerserkerRage was not merely unread
at close-out — by then it no longer existed.
Split arming into its two halves:
consumeArmedAbility(c) mutates: disarms, saves, returns the id. Once,
at fight start.
applyAbilityByID(c, id, mods) pure: no DB write, no disarm. Safe on every
rebuild. (No ability's Apply writes to the
character, so this really is pure.)
armAbilityForFight(c, mods) consume + apply, for the auto-resolve callers
that build and fight in one breath.
buildZoneCombatants now takes the already-consumed id and re-applies it. The id
rides on ActorStatuses.ArmedAbility, seeded per seat at fight start, so
partyCombatantsForSession reproduces the ability every rebuild and the close-out
can still see that a rage fired.
The close-out itself: postCombatBookkeeping now carries grantCombatAchievements
+ persistDnDPostCombatSubclass, and all four close-outs route through it —
runDungeonCombat, runZoneCombatRoster, finishCombatSession,
finishPartyCombatSession. It fires on every terminal status, not just a win: a
Berserker who rages and loses is still exhausted, which is what auto-resolve
always did.
Also: buildFightSeats and runZoneCombatRoster consumed the ability before the
checks that could sit a seat out, so a downed member was disarmed for a fight
they never joined. The refusals now run first.
B. Six unlocked read-modify-writes against the shared supply pool.
updateSupplies rewrites supplies_json wholesale, so a caller folding its delta
onto an *Expedition it read earlier discards whatever landed in between.
Handlers run one goroutine per event, so those writers genuinely interleave.
All six now go through withExpeditionSupplies, which takes advExpeditionLock,
re-reads the row, hands the closure the fresh copy and persists what it returns:
nightRolloverBurn (forage + burn in one write), grantTwoWeeksCache,
advanceToNextRegion's transit burn, campPitch, pitchAutopilotCamp, and the
ambient pack-rat drain. expeditionCmdAccept's hand-rolled lock folds onto the
same helper. expedition_sim.go is left alone: single-threaded, takes no locks.
Known consequence, for the balance track: trySimAutoArm used to live inside the
rebuild, so a simulated Fighter (second_wind) or Cleric (healing_word) re-armed
and re-spent a resource every round of every elite/boss fight. expedition-sim
drives those through the turn engine, so every prior expedition-sim corpus
overstates those two classes. Re-baseline after this, not before.
Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
`!fight` seats the expedition's roster instead of the one player who typed
it. Seat 0 is the leader, always: the session row is theirs, the lock is
theirs, and `!flee`, the fork, and `!extract` stay their call.
A monster that wins initiative now swings before anyone speaks. The session
layer used to park every new fight on a player_turn, which is true of the
hardcoded solo order and a lie about a party's -- the enemy would forfeit
round 1 and nobody would notice. `startPartyCombatSession` rolls the order
and sets the phase from it; `handleFightCmd` settles the round before it
announces, so the opening block narrates the hit rather than quietly
showing its damage.
Members were invisible to two commands that had no business ignoring them:
`!cast` queued a spell for "next combat" while its caster was standing in
one, and `!rest` healed a seated member to full mid boss fight. Both now
resolve through the party.
Nobody leaves without an answer. A downed member's `!fight` opens the
party's fight and tells them why they are not in it. The leader's `!extract`
reaches everyone it drags out of the dungeon, and everyone rolls for what
moved into their house while they were gone.
Supplies burn at 50% x N x 4/5 -- a party eats more than one and less than
N. The ratio is exact: 0.8 as a float truncates a party of three to 119%,
a permanent tax nobody would have found.
Solo is untouched, byte for byte. One seat means one build, one INSERT, no
participant rows, the same RNG draws in the same order -- the combat
characterization golden does not move, and neither does the balance corpus.
Every ownership lookup in the adventure module keys on a user id, and a
party member owns no row: not the expedition, not the zone run. P4 gave
them activeExpeditionFor; this gives them activeZoneRunFor, and gives the
DM seams the audience they never had.
- activeZoneRunFor(user) -> (run, isLeader, err). An owner's lookup is
exactly getActiveZoneRun, side effects and all -- in particular the
§4.3 idle reap, which force-extracts the wrapping expedition. A member
must never re-enter it, or glancing at the map would end the leader's
run. Pinned.
- expeditionAudience / fanOutExpeditionDM. Briefing, recap and digest all
DM'd id.UserID(e.UserID) alone. They now loop the roster, which
partyMemberIDs collapses to exactly the owner when there is none -- so
a solo expedition sends the same bytes to the same user it always has.
The briefing's body is expedition-scoped but its pet prefix is not:
each member has their own pet and their own sheet, so the roll rides a
per-reader decorator (the shape P5 settled on for combat narration).
The digest's A6 event anchor rolls per member for the same reason.
- releaseParty on every terminal transition. A seated member is barred
from adventuring elsewhere, so a roster outliving its expedition
strands the party. Deliberately NOT on 'extracting': that is a 7-day
resumable limbo and !resume must bring everyone back. The roster clears
when the window lapses to 'failed', which routes through
completeExpedition like the rest.
Rosters are still empty in production -- nothing seats a member yet -- so
every loop here has exactly one element and the whole change is a no-op
until P6b. Golden byte-identical, go test ./... green.
§4.2's "Ranger, 1d4 SU/day" perk had been dead since Phase 12 E1b —
SupplyForageMaxSU was defined but unreferenced, ForagedToday was only
ever reset to false. New applyRangerForage helper grants 1d4 SU once
per day (headroom-capped, Ranger-only), fires at the top of
nightRolloverBurn, and surfaces as a "forage" line in the end-of-day
digest. No DC roll — accessibility over crunch, and the D5-a caps
already give all loadouts comfortable headroom.
Event-anchored expeditions no longer pin their re-engagement DM to 06:00
UTC. The ticker skips idle players (last_activity older than today's
threshold, inside the 28h safety net); maybeDeliverDeferredBriefing fires
the owed briefing on the next inbound message in any room. The OnMessage
hook also stamps last_activity so chat presence (not just bot commands)
counts toward "the player is here."
Splits the legacy briefing body into nightRolloverBurn + nightRolloverDrift,
plus a processNightCamp convenience. For expeditions started after the new
eventAnchoredCutoff, the 06:00 UTC briefing stops mutating: it posts a
re-engagement DM, and only force-fires processNightCamp itself after a 28h
safety-net window. Day++/burn/threat-drift now ride along the autopilot
night-camp pitch (decideAutopilotCamp sets Night=true when ≥16h since the
last rollover) and on the first player !camp since the last rollover. The
legacy UTC-anchored flow still works via the same staged helpers, with
processOvernightCamp interleaved to preserve the rest-before-drift ordering.
Tests pin eventAnchoredCutoff to year 9999 in TestMain so the existing
legacy assertions still run; new tests cover the night decision, the
night-camp pitch advancing the day, the event-anchored skip, and the
safety-net force-rollover.
Recovers the genuinely-missing half of ade0335 (orphaned on the unmerged
phase-H-harvest-charges branch). 3ed2e1d redid pet *arrival* via the
emergence seam and explicitly deferred the morning event; this closes
that gap.
- deliverBriefing now rolls the 25% morning pet event (petMorningEvent,
already present but only wired into the overworld DM that underground
players never see) and prepends it to the briefing body, granting the
one-day PetMorningDefense buff.
- Add resetAllPetMorningDefense + wire it into midnightReset. The buff was
leaking permanently even on HEAD's existing overworld path — it was set
on the 25% roll but never cleared. Resets the pet_flags_json key in place.
Deliberately omits ade0335's ArrivalPending machinery: arrival is now the
emergence seam's job (3ed2e1d + 978dc5e + 513cf32), so queuing arrival
from the briefing would double-roll it.
The pet-arrival roll only ran in sendMorningDMs (the 08:00 overworld
morning DM), which is skipped for anyone underground. Expedition players
are almost never in the overworld at 08:00, so the roll never reached
them — the encounter never fired despite the conditions being met.
Move the roll onto the emergence seam via a shared helper
maybeRollPetArrivalOnEmerge, called when a player surfaces alive
(voluntary extract, abandon, survived forced extraction) and on respawn
for underground deaths. Remove the now-dead morning-DM arrival roll.
Story-wise: while the player was out, an animal wandered into the empty
house looking for food.
The 25% morning pet event still rolls only in the overworld DM and has
the same reachability gap; left for a follow-up.
Tedium-removal pass driven by live play feedback. Three big threads:
Expedition autopilot Phase 4 — background auto-run + harvest-until-dry:
- New expeditionAutoRunTicker walks active expeditions every 15min
(5min tick, per-expedition CAS on new last_autorun_at column). Skips
combat sessions, briefing/recap quiet windows, expeditions <30min old.
- Walks up to 3 rooms/tick with compact narration; suppresses DMs when
0 rooms walked or just hitting the per-tick cap (no "stretch complete"
filler). Player only hears from the bot when a real decision is needed.
- Compact mode: one-line combat narration for trash/elite, auto-resolves
elite doorways via the forward-sim engine (boss still pauses). Threaded
via new advanceOnceWithOpts → resolveRoom → resolveCombatRoom.
- Auto-harvest now grinds each Common/Uncommon node until dry (cap at 8
attempts/visit), mirroring manual !scavenge retries. Rare+ still pauses.
- Ambient ticker: anti-repeat by Kind via new last_ambient_kind column
(avoids two pack_rat DMs in a row when the pool only has 6 lines).
- !expedition go <n> now routes to the fork-choice handler when active +
numeric; fork footer rewritten to suggest !expedition go instead of
!zone go. Boss/Elite doorway: formatNextRoomMessage routes the action
hint by next room type and autopilot loop breaks via new nextRoomType
field so "Room X/Y — Boss" doesn't double-print with contradictory
hints (!zone advance vs !fight).
- runAutopilotWalk extracted from expeditionCmdRun so foreground and
background share the loop body.
Rogue Sneak Attack actually exists now:
- Audit found the rogue's "Sneak Attack" passive was AutoCritFirst +
5% damage rider. No Nd6, ever. Phase 2 Monte Carlo masked this
with a small flat buff; the class's defining mechanic never matched
its tooltip or 5e identity.
- Added SneakAttackDie int to CombatModifiers, per-hit Nd6 in
combat_primitives.go (same lane as DivineStrikePerHit). Scales with
level: 1d6 at L1-2 ... 4d6 at L7-8 ... capped at 10d6 at L19-20.
- AutoCritFirst + 5% rider retained as bonuses on top of working sneak
attack — preserves the opener-burst feel.
- L7 rogue expected per-hit ~8 → ~22 damage. Valdris fight math goes
from "16 rounds to kill, die in 8" to winnable.
TwinBee voice sweep (Phase B3):
- ~530 line changes across 24 files replacing third-person "TwinBee
notes/files/tracks/respects/..." constructions with first-person
inside flavor string literals. Code identifiers (TwinBeeLine,
twinBeeLine, etc.), chat-prefix labels (🎭 **TwinBee:**), item names
(TwinBee's Bell), achievements, and game-title references in
fun.go (Konami TwinBee ship) left intact.
- Catches a real bug: Valdris fight rendered "TwinBee marks the
Legendary Resistance" mid-combat in third person, contradicting
the Phase B2 convention.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the 'fairly breezy with some death' target the user picked
for Phase 5. Five-piece ship; Phase 1 matrix lands T1 88%, T2 74%,
T4 72%, T5 ~57% in or above band. T3 remains the design hump at
~45% (manor 39, underforge 47) — Wraith promotion to elite was
already done in Phase 4-B, the remaining standard-pool deaths are
the irreducible part of T3.
Pieces:
1. computeMaxHP × 1.5 (phase5BHPMult in dnd.go). Uniform across
class/level so the class-balance harness's in-tier parity
assertion stays green. Bootstrap (bootstrap_phase5b_hp.go)
refreshes hp_max for existing characters at startup;
idempotent via db.JobCompleted. hp_current is bumped by the
same delta so a full-HP character stays at full.
2. applyPhase5BPlayerFloor (dnd_combat.go): +3 AC, +3 AttackBonus,
+3 weapon.MagicBonus (damage). Applied at the END of
applyDnDEquipmentLayer (after computeArmorAC's AC override)
and inside buildHarnessPlayer so live and harness measurement
match.
3. Elite bracket 19 → 23 (resolveCombatInterrupt). Case order
puts Elite (≥23) before Patrol (≥22) so a 23+ total prefers
the single dangerous fight. Elite is now effectively a
high-threat event reachable only via the +1-per-20-threat-
above-40 mod — Phase 4-B's elite-pool monsters still appear,
just less often.
4. dailyThreatDrift base 3 → 1. Slows the threat clock so
players have the days they need before threat tips zones
into the new 23+ elite band.
5. applyDailyBurn default → 50% (phase5BDailyBurnRatePct). Also
applied in the temporal-override branch in
dnd_expedition_cycle.go so tidal / unraveling days scale by
the same 0.5× — otherwise those days would be
disproportionately harsh against the new baseline.
The harness's expedition_balance.go reads phase5BDailyBurnRatePct
as the default-burn fallback when the override knob is zero, so
Phase 1 matrix measurements now reflect what live players
experience.
Test debt: 13 pinned-numbers unit tests across combat_stats_test,
dnd_test, dnd_xp_test, dnd_equipment_profiles_test,
dnd_expedition_supplies_test, dnd_expedition_cycle_test,
dnd_expedition_extract_test, dnd_expedition_region_cmd_test,
dnd_expedition_combat_test, dnd_expedition_threat_test,
dnd_expedition_temporal_test, expedition_balance_test were
pinning pre-Phase-5-B baselines; updated with comments noting
the cause. Class-balance suite stayed green (uniform buff
preserves spread).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
§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>
§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>
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>
§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>
§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>
§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>
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>
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>