Too Old + Bedtime Grandpa โ design
Date: 2026-07-16 Status: approved (autonomous goal), building
Goal
- Sleep runs out โ "Too old" incident (universal). When any hero's Sleep
meter hits 0 they become too old: they can't move, slump on the floor, and
emit floating zzz's. This registers a
too_oldincident in the shared queue. - New sprite. A slumped/sleeping pixel pose (with zzz's) rendered for that player โ locally and to everyone else.
- New power "Bedtime Grandpa" assigned to Jae = PebbleFeline (
the-doctor). When Jae fires it within AOE of a too old player, both are auto-walked to the furthest empty bed; Jae is then released and the sleeper is revived fully slept after 10 seconds.
How it fits the existing model
too_oldjoins the fixedW00T_INCIDENTSset inincludes/config.php(incidents are NOT UI-addable โ there is no add-incident form). Markedkind=status,halts=false(personal; does not stop global w00ting โ no party softlock),clear=power,universal=sleep,noTrait=true(universal-only; not a trait cause). It then auto-appears on the Incidents & Meters editor for content.- Powers already reference the incident they clear via
abilities.counters_type(โ clientpw.clears). Bedtime Grandpa is simply the doctor's ability whosecounters_type='too_old'.launch.jsspecial-casespw.clears==='too_old'to run the escort sequence instead of the generic "clear incident in AOE". - The sleep-runs-out trigger is a built-in universal mechanic in
assets/game.js, exactly like the existing bladderโpee and drunkโpoo accidents.
Who is asleep โ single source of truth
- Local:
assets/game.jsowns it.meter.Sleep <= 0โ internalasleep=true, exposed aswindow.__game.asleep. While asleep the player is frozen (no keyboard control, no actions) UNLESS an escort pull-lock is active (then they walk to the bed). - Remote: derived from the shared
issueslist โ a player is asleep iff atoo_oldissue exists whosevictim== that player's public id (substr(md5(token),0,8), already exposed asplayers[].id). No newplayerscolumn, no token leak.
Escort coordination (issues table)
The too_old issue is the addressable (public integer id) coordination record.
New columns (added idempotently in Database::migrate, like expires_at):
| col | meaning |
|---|---|
victim |
public id of the sleeping player |
helper |
public id of the rescuer (Jae) once assigned |
tfloor |
escort target floor |
tx,ty |
escort target = a standable bedside cell of the chosen bed |
wake_at |
unix time the sleeper is revived (also mirrored to expires_at) |
Lifecycle
- Fall asleep (sleeper client) โ
game.jsfiresHOOKS.onTooOld(floor,x,y);launch.jsPOSTsissue.php action=toooldโ server creates thetoo_oldissue withvictim=publicId, and a generous fallbackwake_at = now+45(expires_attoo). So even with no doctor present, they wake on the floor after 45s (fully slept). Onetoo_oldper victim (server dedupes). - Fire Bedtime Grandpa (Jae) โ client finds the nearest
too_oldissue withinW00T_AOE_TILESon the same floor that isn't already being rescued (tx==0), picks the furthest empty bed (__gameApi.beds(floor)โ bedside cell; empty = no other asleep player near it), and POSTsaction=bedtime {id,tfloor,tx,ty}. Server setstx,ty,tfloor,helperiff not already assigned. - Escort (both clients, from state poll) โ
- Sleeper: its
too_oldhastx>0โsetPull(tfloor,tx,ty,'lock')(walks to bed). On arrival (dist<0.5) POSTaction=bed_arrive {id}once. - Jae: the issue with
helper==myIdandwake_atunchanged โsetPullto the same bedside (escorts alongside). Released (clearPull) oncewake_atis bumped by arrival.
- Sleeper: its
- Arrive (server) โ
bed_arrivesetswake_at = now+10,expires_at = now+10(10s in a proper bed โ faster than the 45s floor fallback). - Revive โ server auto-prunes the issue at
expires_at(existingpruneExpired). The sleeper client, seeing itstoo_oldvanish, calls__gameApi.wake()โSleep=100,asleep=false,clearPull. Remotes stop slumping because thevictimid is gone from the issues list.
Movement control is unified per poll: asleep (escort-pull or frozen) and Jae-escorting take precedence over the existing smoke-break / the-Martin pulls.
Sprite
assets/sprite.js gains an opts.asleep branch: a slumped figure lying on the
floor (skin/hair/outfit palette reused) with three rising, fading z z Z glyphs.
Used by launch.js for local + remote sleepers. assets/game.js drawPlayer
gets a matching simple slump so the single-player /game walkaround also shows it.
Single-player /game
Same universal trigger; with no multiplayer hooks the sleeper self-naps (Sleep
refills over ~12s on the spot, then wakes) so the dev tool never softlocks and the
sprite is demonstrable via __gameApi.setMeter('Sleep',0).
Data change (one-off)
scripts/add_bedtime_grandpa.php โ idempotently adds the Bedtime Grandpa
ability (counters_type='too_old', scope='aoe', cooldown=30) to the-doctor
if absent. Respects "never seed": a deliberate one-off feature edit, not a deploy
reseed. Run once, manually.
Files
includes/config.phpโ addtoo_old;noTraitexclusion inw00t_trait_incident_keys.classes/Database.phpโ 6 newissuescolumns.classes/IssueRepository.phpโaddTooOld,tooOldByVictim,assignBed,bedArrive.launch/issue.phpโ actionstooold/bedtime/bed_arrive.launch/state.phpโ expose new issue fields.assets/sprite.jsโ asleep pose + zzz.assets/game.jsโ universal sleep trigger, asleep freeze,wake(),beds(),standable(), single-player slump + self-nap.launch/launch.jsโ onTooOld, escort coordination, Bedtime Grandpa, slump render.scripts/add_bedtime_grandpa.phpโ assign the power.
Testing
php tests/smoke.phpโ data layer still round-trips.- Single-player
/game: forceSleep=0, see slump + zzz + self-nap wake. - Multiplayer
/launchvia two Playwright contexts: sleeper falls asleep; the doctor fires Bedtime Grandpa; both walk to a bed; sleeper revives after 10s. - Error log clean (
../../logs/...error.log, grepw00t).