The Siege has never once spawned in prod. `select count(*) from
world_boss` is 0 and daily_prefetch has no worldboss_spawn row at all.
worldBossTick only auto-spawned when now.Day() == 1. The world boss
landed on main 2026-07-10..13 and the first deploy carrying it was after
July 1, so prod has never run a first-of-the-month tick with the code in
it. The feature has been live and unreachable for weeks, and the next
natural spawn would have been August 1.
The same gate also silently skipped any month where the bot happened to
be down or redeploying across the 1st, with no catch-up — one missed
minute costs the town a month.
The month key is already the whole dedup, so drop the day check and let
the rule be what it always read as: one Siege per calendar month, as
early as the process is up to run it. A missed 1st now self-heals on the
next tick.
Mischief Makers M1 — the core engine, Matrix-only. `!mischief send elite @user`
debits the buyer, tells the games room a hit is out, and an hour later a monster
from the target's own level bracket walks into whatever dungeon they're in.
Survive it and they keep a cut of the money and the buyer is named; don't, and
they wake up on a cart home.
The monster comes from the target's bracket zone pool, not the arena ladder and
not the dungeon they happen to be standing in — the same selection code the M0
pricing sweep ran through, so the fee table can't drift away from the fight it
priced.
Three things that are load-bearing and don't look it:
* Survival is read off the target's HP, not PlayerWon. The engine's timeout is
a retreat, not a lethal blow — somebody who ran out the clock with HP left
held the thing off, and a bought monster that merely outlasted them hasn't
earned a maiming.
* Nobody dies for money, and that includes the party. The delivery skips
closeOutZoneWin/Loss (the fight is extrinsic to the dungeon — crediting it
would let a buyer unlock the target's kill-gated resources for them), so
nothing else floors a downed seat. Without floorMischiefRoster on BOTH
outcomes, a member the leader outlived is left alive at 0 HP, which every
`HPCurrent <= 0` gate reads as broken rather than dead.
* One live contract per target is a partial UNIQUE INDEX, not a read-then-write
check. Placement holds only the *buyer's* lock, so two buyers racing at the
same victim would both pass an in-code test. The loser is refunded.
Payouts are a percentage of the base fee, never of what the buyer actually paid —
the sign surcharge is a pure sink. Capped at 75%, so a survival purse is always
strictly less than the outlay and collusion loses to !baltransfer, which is free.
That cap is the entire anti-collusion story; no danger multiplier needed.
A crash between claiming a contract and closing it out used to be unrecoverable
in the design: the row would strand, the target could never be targeted again,
and the buyer's money was gone. The stale sweep refunds those in full — that one
is our fault, not a bet they lost.
Contract timestamps bind as Go time.Time, never CURRENT_TIMESTAMP. The driver
stores RFC3339 and SQLite's own stamp is space-separated; the two compare
lexicographically wrong.
Pete learns four mischief_* event types in a separate commit, and has to deploy
BEFORE this does — an unknown event_type is a 400, which retries and then parks
the bulletin forever.
Two findings from an adversarial review of the C3 diff, both fixed:
1. (medium) A killing blow commits the shared pool to 0 inline, but the status
flip to 'defeated' was deferred until AFTER the multi-second narration stream.
In that window a crash/redeploy or the ticker's survive path could resolve a
boss the town KILLED as a survival — debiting the pot and paying no bounties.
Fix: resolve the defeat BEFORE streaming narration, and add a ticker safety
net that resolves any active 0-HP boss as defeated (never falls through to the
survive/pot-debit branch). Both guarded by setWorldBossStatus, so still once.
2. (minor) Pool damage was applied before the best-effort contrib/gate write, so
a failed upsert let a player refight a pool they had already drained and lose
the credit. Fix: write the contribution (which sets the once-per-day gate)
BEFORE draining the pool, as a hard error that aborts the bout with the pool
untouched.
Also corrected the applyWorldBossDamage comment (two concurrent killers CAN both
see killed=true; the status guard dedupes the payout — the old comment claimed
only one would). New ticker tests cover both resolution paths; suite green,
golden byte-identical.
Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
Wires the player-facing half of the Siege on top of W1's model + lifecycle.
- !adventure worldboss [status|fight|spawn] (alias !adventure siege). Status
shows the shared-pool board + your daily bout state + the muster; fight
takes today's bout; spawn is an admin override (the "both" spawn decision).
- The bout is an arena-style solo fight through runZoneCombat vs a disposable
per-tier stat block; the damage dealt (EnemyEntryHP-EnemyEndHP) is subtracted
from the shared pool atomically (MAX(0, …) WHERE status='active') win or lose.
Real HP cost like the arena, but no death/no hospital: worldBossFloorHP
raises a 0-HP loser to 1 so a loss reads as "battered", not a corpse.
- One bout per player per UTC day (worldBossBoutUsedToday off the contrib's
last_fight_date). The per-user advUserLock serialises a player's own repeat
submits so the gate can't be raced; cross-player pool-crossing is safe
because only the setWorldBossStatus winner resolves a defeat.
- A killing bout trips resolveWorldBossDefeated (minted bounty by fights +
cache + treasure roll), after the fighter's own bout DM has streamed.
Bout core (resolveWorldBossBout) + the once/day predicate + the HP floor are
factored out of the DM path so they're unit-tested end to end with a real
fightable character; the DM/games-room emission stays thin (no client stub).
Combat golden byte-identical; full plugin suite green.
Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
The monthly communal "Siege": a named boss camps outside town for 72h with
a single shared HP pool. This lands the model and the automatic lifecycle;
the player-facing bout command is W2.
- New tables world_boss + world_boss_contrib (own tables, outside the
saveAdvCharacter fan-out, so a char save can't clobber the shared pool —
the isolation adventure_shadow earns). Absent active row == no event; no
bootstrap.
- Boss sized to the town it will fight: tier from the MEDIAN combined level
of any-chat-active players (feedback_presence_is_any_chat, off
daily_activity), pool HP = arena per-bout HP × ~2 bouts/active player,
clamped [4,60] bouts. Floored at T3.
- Lifecycle rides the 1-min eventTicker (no net-new goroutine): auto-spawn
on the 1st of each UTC month (JobCompleted dedup) + resolve a lapsed
window as a survival. Operator override + the daily bout are W2.
- Resolution: defeat mints a bounty scaled by fights fought (not damage —
accessibility) + a consumable cache + one low-rate treasure roll each;
survival debits 20% of the community pot as a tribute (a pot sink). Both
close-outs are guarded on status='active' so they fire once.
- Announcements post to the games room (no-op when GAMES_ROOM unset).
Pure logic (median, tier bucket, HP scaling, pool subtract/clamp, payout
split) is unit-tested; euro/DM emission left thin per the repo's no-client
-stub convention. Combat golden byte-identical (never touches SimulateCombat);
full plugin suite green.
Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa