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