A player who stops tending their adventurer doesn't stop having one. After
24h with no action against Adventure, the character gets restless and leaves
on an expedition by itself: the easiest zone its level band allows, the
cheapest supplies it can afford, and whatever gear was already on the rack.
Everything downstream of the start was already autonomous — the autopilot
walks rooms, drives elite and boss fights on the turn engine, camps, harvests
and picks forks. The only thing that ever needed a human was `!expedition
start`, so that's all this adds: a 30m ticker plus a clock.
It never buys or equips anything, and that is the whole mechanic: a neglected
adventurer grinds half-starved runs on rusting gear and comes home taxed. The
prodexercise killed an L4 mage four rooms in on its first run.
The clock is a new column. Every existing timestamp is unusable: last_active_at
is auto-bumped by saveAdvCharacter (the autopilot would refresh a bored
character's own idle clock), loadAdvDailyActivity counts the autopilot's own
expedition logs, and user_stats.updated_at is chat presence, not a game action.
last_player_action_at is written only by markPlayerAction, from a real player
action against Adventure — any interface, not just Matrix.
Raid zones (raidContentWarning: the party-tuned T5 bosses with a 0% solo clear)
are avoided while anything else is in band. At L13+ they're all that's left, and
the adventurer goes in anyway and loses. That's intended.
dnd_expedition.boredom + isBoredomDriven stop a run nobody asked for from
shielding an absent player from the idle reaper or holding their streak. A
manually-started expedition still holds it while the autopilot walks it.
Robbie visits and pays silently for idle players — he was going to file a daily
public bulletin about people who stopped playing weeks ago.
Note for anyone touching the time-scanning queries here: modernc.org/sqlite
rebuilds a time.Time from the column's declared type, and COALESCE()/MAX() erase
it. playerIsIdle fails open, so a broken scan there declares the whole server
idle. Both it and lastExpeditionByZone select declared columns and fold in Go,
and the tests seed real rows so the scan actually executes.