compw00tergame designer
2026-07-06 2026-07-06-compw00ter-power-trait-incident-model.md

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 0
  • traits.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 Boss option. Stored as trigger_meter + trigger_direction: a meter trigger → (<Meter>, high|low); a boss trigger → (Boss, ''). A sentinel const 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.php parses the new fields, validates: counters_type/spawn_type ∈ incident types (or blank), range ≥ 0 integer, anchorW00T_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() already SELECT *, so new columns flow through.

Vocabularies UI (vocab.php, VocabRepository)

  • Incident types gain an editable HP number (baseline). The vocab editor shows name + HP per incident type; VocabRepository reads/writes hp.
  • 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 up data/compw00ter.db first). 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: range persists as int; anchor defaults to a valid value; incident-type hp reads/writes via VocabRepository.
  • 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.