compw00tergame designer
2026-07-16 2026-07-16-too-old-bedtime-grandpa-design.md

Too Old + Bedtime Grandpa โ€” design

Date: 2026-07-16 Status: approved (autonomous goal), building

Goal

  1. 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_old incident in the shared queue.
  2. New sprite. A slumped/sleeping pixel pose (with zzz's) rendered for that player โ€” locally and to everyone else.
  3. 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_old joins the fixed W00T_INCIDENTS set in includes/config.php (incidents are NOT UI-addable โ€” there is no add-incident form). Marked kind=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 (โ†’ client pw.clears). Bedtime Grandpa is simply the doctor's ability whose counters_type='too_old'. launch.js special-cases pw.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.js owns it. meter.Sleep <= 0 โ†’ internal asleep=true, exposed as window.__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 issues list โ€” a player is asleep iff a too_old issue exists whose victim == that player's public id (substr(md5(token),0,8), already exposed as players[].id). No new players column, 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

  1. Fall asleep (sleeper client) โ€” game.js fires HOOKS.onTooOld(floor,x,y); launch.js POSTs issue.php action=tooold โ†’ server creates the too_old issue with victim=publicId, and a generous fallback wake_at = now+45 (expires_at too). So even with no doctor present, they wake on the floor after 45s (fully slept). One too_old per victim (server dedupes).
  2. Fire Bedtime Grandpa (Jae) โ€” client finds the nearest too_old issue within W00T_AOE_TILES on 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 POSTs action=bedtime {id,tfloor,tx,ty}. Server sets tx,ty,tfloor,helper iff not already assigned.
  3. Escort (both clients, from state poll) โ€”
    • Sleeper: its too_old has tx>0 โ†’ setPull(tfloor,tx,ty,'lock') (walks to bed). On arrival (dist<0.5) POST action=bed_arrive {id} once.
    • Jae: the issue with helper==myId and wake_at unchanged โ†’ setPull to the same bedside (escorts alongside). Released (clearPull) once wake_at is bumped by arrival.
  4. Arrive (server) โ€” bed_arrive sets wake_at = now+10, expires_at = now+10 (10s in a proper bed โ€” faster than the 45s floor fallback).
  5. Revive โ€” server auto-prunes the issue at expires_at (existing pruneExpired). The sleeper client, seeing its too_old vanish, calls __gameApi.wake() โ†’ Sleep=100, asleep=false, clearPull. Remotes stop slumping because the victim id 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 โ€” add too_old; noTrait exclusion in w00t_trait_incident_keys.
  • classes/Database.php โ€” 6 new issues columns.
  • classes/IssueRepository.php โ€” addTooOld, tooOldByVictim, assignBed, bedArrive.
  • launch/issue.php โ€” actions tooold / 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: force Sleep=0, see slump + zzz + self-nap wake.
  • Multiplayer /launch via 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, grep w00t).