activeOmen() is a pure function of the UTC ISO (year, week): omenTable
indexed by (year*53+week)%len, so it advances weekly with no schema, no
ticker state, no persistence. Five non-combat seams read it — harvest yield,
supply freebie, expedition start mood, arena payout (scales gross earnings
before the pot tax), and ingredient drop chance. TwinBee reveals the active
omen in the existing morning DM (no net-new scheduled message).
Launch set is buffs-with-texture on non-combat levers only: Bountiful
Harvest, Quartermaster's Blessing, Golden Purse, Overflowing Satchels, Still
Waters. Nothing touches SimulateCombat or the turn engine — the omen is keyed
on the real clock, so a combat mutator would make the golden and the balance
corpus week-dependent. The plan's "elites +2 ATK" is deliberately dropped for
that reason.
The balance sim drives the real expedition loop and would otherwise traverse
all five seams, making corpus sweeps depend on the wall-clock week. NewSimRunner
sets simOmenDisabled (mirrors simAutoArmEnabled), so activeOmen returns a
no-effect omen under the sim. Still Waters subtracts from the daily threat
*rise* only, floored at hold-steady — it never forces active decay.
Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
Mechanical `gofmt -w ./internal ./cmd`. Mostly struct-field realignment that
had drifted, plus a few trailing-newline fixes. No behaviour change — gofmt is
semantics-preserving, and build/vet/test are green either side.
Split out from the code-review fixes that follow so those stay reviewable
instead of hiding inside a wall of realignment.
Every ownership lookup in the adventure module keys on a user id, and a
party member owns neither the expedition row nor the zone run. So each
player-facing read quietly told them they were not playing.
Rewire them. Reads a member should see resolve through activeExpeditionFor
/ activeZoneRunFor. Leader-only actions answer with copy that names the
leader instead of denying the expedition. Three busy-guards had to start
refusing a member outright: !zone enter, !expedition start and !sell all
keyed on the sender's own row, so a seated member could open a private
dungeon, outfit a rival expedition, or run a shop from the boss room.
Four things the rewire itself exposed:
!resources looks like a read but seed-persists harvest nodes, and
saveHarvestNodes rewrites the entire region_state blob — kills, event
gates, temporal stack — last-write-wins. Reaching it as a member would
revert the leader's walk from a stale snapshot. Persist only for the owner;
seedRoomNodes is pure, so a member re-derives the same nodes.
!zone taunt moves the party's shared mood, which is intended and safe:
applyMoodEvent lands an atomic delta. Its neighbour applyMoodDecayIfStale
writes an absolute gm_mood from the caller's snapshot, and every command
takes the *sender's* lock — a member running it against the leader's run
holds the wrong mutex. The owner check now lives on that function.
A seat outlives status='active'. releaseParty deliberately skips the
seven-day 'extracting' limbo, so the roster persists while
activeExpeditionFor goes blind — long enough for a member to open a run
that wins every lookup once the leader !resumes. seatedExpeditionFor spans
both statuses; it is what the busy-guards ask.
!expedition run was still member-blind. It is the same walk as !zone
advance, reached by its other name.
isPartyMember replaces `run != nil && !isLeader`: activeZoneRunFor reports
isLeader=false for a player with no run anywhere, so the bare test sends a
solo player to go ask their leader.
Golden byte-identical; solo T1 expedition clears end-to-end.
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>
GM→DM rename across docs and code (GMNarrationType→DMNarrationType,
GMState→DMState, narration constants, comments) so the system reads as
"Dungeon Master" everywhere. Player-visible "GM mood" wording stays
where it appears in flavor.
Streamed zone/expedition combat: zone advance now stages patrol →
patrol play-by-play → patrol resolution → room intro → room play-by-play
→ final outcome through sendZoneCombatMessages with 2–3s pacing
(arena keeps its 5–8s window). Combat narrative lines pick up a compact
d20-vs-AC roll annotation for hit/crit/miss/block events.
Combat outcome polish: dndHPSnapshot lets narration show sheet HP
rather than legacy combat-engine HP, and markAdventureDead clears the
zombie state where hp_current was 0 but the legacy alive flag stayed
true after a D&D-layer KO.
Adv 2.0 announcement (ADVENTURE_2.0_ANNOUNCEMENT.md), README rewrite
covering the new layer, and adv2_scenario_test.go — a full
zone-run + expedition + harvest playthrough against a copy of the prod
DB asserting persisted state end-to-end.
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>
§14 surfaces the threat clock as a first-class command. !threat shows
level/100, current band, the per-band combat & supply effects (built
from ThreatBandInfo so display tracks any future band re-tuning), and
the last 5 ThreatEvent log entries with deltas + reasons. Adds a
"siege is active" / "past 70" footer when those gates have triggered.
Active-enemy camp guard and close-call evening recap stay deferred —
both need combat state inside an expedition, which doesn't exist yet
(expeditions still don't host their own combat loop). They land in the
combat-link phase along with the §8.1 combat-driven threat modifiers.
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>
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>