compw00ter — Power / Trait / Incident Model + Builder Realign + Balance Pass
Date: 2026-07-06
Status: Design — approved, ready for implementation plan.
Refines: the game-loop model (2026-06-21-compw00ter-main-game-loop.md) and the
Hero Builder. Nails down what powers and traits do, realigns the builder UI to
match, and re-balances the existing roster.
Goal
Make the hero model simple and mechanically concrete, and make the Hero Builder express exactly that model (no legacy cruft). Then re-balance the 10 restored heroes into the new model.
The model
Powers (abilities)
A player fires a power with a key; it is on cooldown and acts on incidents and bosses (both have HP) within range.
| Field | Meaning |
|---|---|
name, description |
— |
strong vs (counters_type) |
the incident type this power is strong against (Tech / Social / Supply / Chaos / Nicotine / Food / …). Extra damage vs that type; chips others. |
damage (power) |
HP removed per use. |
| cooldown | seconds before the key can be used again. |
| range | radius in tiles (integer). 0 = the power's own tile; larger = wider; a big value (~999) = game-wide. Lets us author local-only and venue-wide powers. |
Powers do not refill meters. There is exactly one power effect: counter (damage) an incident/boss. Meters refill only at map locations (keg, food spot, bed, etc.), not powers.
Removed from the model/builder: flavour, target (small/big/boss/any),
provides_meter, boosted_while_indulging.
Traits
A trait fires automatically and spawns an incident — this is the single thing a trait does, and it feeds the same incident queue that powers clear.
| Field | Meaning |
|---|---|
name, description |
description is flavour text. |
| trigger | one of: a meter too high or too low (Food / Booze / Sleep / Unusualness), or a boss is present. |
spawns (spawn_type) |
the type of incident it creates (so a power strong-vs that type clears it). |
| anchor | where the spawned incident sits — room (a fire), player (a puller: the hero is off their rig until it's cleared), or rig (hits a gaming station). |
| severity | minor / major / catastrophic → scales the spawned incident's HP. |
This closes the loop: traits feed the incident queue, powers clear it — one
mechanic both directions. A player-anchored incident is how a hero gets pulled
off their rig; clearing it (a teammate's counter-power strong vs its type) frees them.
Incidents (and bosses)
- Incidents have a type, an anchor (room / player / rig), and HP.
- Incident types carry a baseline HP (edited on the Vocabularies page). A trait's severity scales the HP of what it spawns.
- Bosses also have HP and are damaged by powers (range permitting).
Data model changes
Additive columns (idempotent via addColumnIfMissing; no seeding — see
[[never-seed-db]]):
abilities.range INTEGER NOT NULL DEFAULT 0traits.spawn_type TEXT NOT NULL DEFAULT ''(an incident type)traits.anchor TEXT NOT NULL DEFAULT 'room'(room | player | rig)incident_types.hp INTEGER NOT NULL DEFAULT 100
Vestigial columns kept but unused (SQLite column-drop is risky; leave them at
their defaults, stop reading/writing them): abilities.flavour,
abilities.target, abilities.provides_meter, abilities.boosted_while_indulging,
traits.trigger_state, traits.effect. schema.sql keeps them so existing DBs
are untouched; the form and repository simply ignore them.
Config (includes/config.php)
- Remove:
W00T_FLAVOURS,W00T_TARGETS(no longer used anywhere). - Add:
W00T_ANCHORS = ['room', 'player', 'rig']. - Keep:
W00T_DIRECTIONS = ['high','low'],W00T_SEVERITIES,W00T_DEFAULT_METERS. - Trait trigger is presented as one dropdown built from the meters ×
{high, low} plus a
Bossoption. Stored astrigger_meter+trigger_direction: a meter trigger → (<Meter>,high|low); a boss trigger → (Boss,''). A sentinelconst W00T_BOSS_TRIGGER = 'Boss';documents the reserved value.
Hero Builder UI (includes/_hero_form.php, hero_save.php, HeroRepository)
Power row: Name · Strong vs (incident type) · Damage · Cooldown · Range · Description. Trait row: Name · Trigger (single dropdown: "Food too high", "Food too low", … "Unusualness too low", "Boss present") · Spawns (incident type) · Anchor (room / player / rig) · Severity · Description.
hero_save.phpparses the new fields, validates:counters_type/spawn_type∈ incident types (or blank),range≥ 0 integer,anchor∈W00T_ANCHORS, severity ∈W00T_SEVERITIES, and the trigger resolves to a valid (meter+direction) or Boss. Drops the old flavour/target/provides/boost parsing.HeroRepository::save()writes the new column set (name, description, counters_type, power, cooldown,range, sort_order for abilities; name, description, trigger_meter, trigger_direction,spawn_type,anchor, severity, sort_order for traits) and stops writing the vestigial columns.HeroRepository::all()/find()alreadySELECT *, so new columns flow through.
Vocabularies UI (vocab.php, VocabRepository)
- Incident types gain an editable HP number (baseline). The vocab editor
shows
name+HPper incident type;VocabRepositoryreads/writeshp. - Other vocab lists (vice states, vices, meters) unchanged.
Balance pass (data, via the builder/repository)
Rewrite the 10 existing heroes (chris, b0n1, zazbob, sarah-envelopes, mr-bond, taz, tim, franzlicious, james-lovett, the-doctor) into the new model:
- Each power: a sensible strong-vs type, damage, cooldown, and range (mostly local; a few venue-wide signatures).
- Each trait: a trigger (meter high/low or boss), a spawn type, an anchor (room / player / rig), and severity — migrating the old vice-state triggers (Drunk→Booze/high, Sleepy→Sleep/low, High/Wired/Tripping→Unusualness/high, Angry→Booze/high, Tilted→Boss/event) which already match the documented spread.
- Give incident types baseline HP so damage-vs-HP feels fair (a light power clears a minor incident in ~2 hits; catastrophic incidents need several heroes).
- Ensure every incident type has at least one counter somewhere in the roster.
- Applied via a one-off, reviewed reconstruction using
HeroRepository::save()+VocabRepository(back updata/compw00ter.dbfirst). Output: a one-screen summary table (hero → powers[strong-vs/dmg/cd/range] and traits[trigger→spawn/anchor/sev]).
Out of scope here: adding the newer heroes (Pebblefeline, The Butcher, Jack, "Luke") — a separate step once confirmed, to avoid inventing a roster onto just-recovered data. Also out of scope: the map-location meter sources, the Node engine, boss vocab.
Testing
tests/smoke.php— update the throwaway-hero CRUD to the new ability/trait fields (range, spawn_type, anchor); assert they round-trip.- New assertions:
rangepersists as int;anchordefaults to a valid value; incident-typehpreads/writes viaVocabRepository. - UI:
php -S+ curl/Playwright — the builder renders the new rows, a hero saves and re-loads with the new fields; the vocab page saves an HP. - Error log two levels up:
../../logs/playground.crisprain.co.uk.error.log. - No seeding anywhere; the DB is the source of truth.
Migration / safety
- All schema changes are additive
addColumnIfMissing— existing hero data is preserved; the balance pass overwrites power/trait content deliberately (and is backed up first). - No reseed path is introduced.