Objective feel metrics: what can pre-screen 'feel' before Chris plays (2026-10-17)
Objective feel metrics: what can pre-screen “feel” before Chris plays
Section titled “Objective feel metrics: what can pre-screen “feel” before Chris plays”Ask (Chris, 2026-10-16 23:40): “figure out how you can make tools that let you make ‘feel’
judgements; like we did for the sound, there may be more objective ways to identify when something
is more or less likely to feel good.” The sound precedent is bh-synth-sfx +
tools/sound_map/analyze_ref.py: measure a candidate against references Chris already liked, so
Claude can throw out the obvious misses before his ear.
Outcome: tools/feel_probe/ (MVP, branch feel-probe) + scripts/feel-probe.sh + skill
bh-feel-probe. It runs seeded headless autopilot runs with a read-only observer and prints nine
run metrics, a per-weapon dead-pick table, and a static snap-vs-ramp scan, each with PASS / WARN /
FAIL bands.
The uncomfortable part first: most bands below are not measured on players. They come from practitioner convention (Swink, Vlambeer, Valve) or from this game’s own history of Chris verdicts. The probe is a filter that says “this is likely to read as X”. It cannot say “this feels good”. Bands move only when a Chris verdict disagrees with them (see Calibration).
1. Literature: which feel proxies are measurable at all
Section titled “1. Literature: which feel proxies are measurable at all”A literature pass (one research agent, web search). Only four of these sources were re-checked against the web this session: the Eiserloh trauma model, Claypool’s latency work, Clark et al. on near-misses and the KOF XIII hit-stop table. The other citations are from memory of the literature. Verify them before quoting.
| Topic | Metric | Reference band | Source | Confidence |
|---|---|---|---|---|
| Response | input → first visible response | < 100 ms reads as “direct”; ~240 ms is the human correction cycle | Swink, Game Feel (2008) | expert practice |
| Response | latency vs. precision/deadline | tight-deadline actions degrade from < 100 ms of local lag | Claypool & Claypool, “Latency Can Kill” (2010), https://sites.cs.ucsb.edu/~almeroth/classes/W10.290F/papers/claypool-10.pdf | measured (FPS games, not twin-stick) |
| Hit feedback | hit-stop frames | 7 frames (light) to 11 (heavy) @60 fps in fighting games; 2-6 frames for rare events in a swarm game | KOF XIII frame data https://srk.shib.live/w/The_King_of_Fighters_XIII/Character_States/Hitstun_Blockstun_state | measured for fighting games, folklore for swarms |
| Hit feedback | flash duration | 1-3 frames of white flash | Nijman, “The Art of Screenshake” (2013); Jonasson & Purho, “Juice It or Lose It” (GDC EU 2012) | expert practice |
| Shake | trauma model | shake = trauma² × max offset; linear decay 0.5-1.0/s; events add 0.2-0.6, clamp at 1 | Eiserloh, “Juicing Your Cameras With Math” (GDC 2016) | model solid; numbers are implementers’ defaults |
| Shake | saturation | share of run time with trauma > 0.3; near 100% means the shake carries no information | derived | guess |
| Envelope | attack vs decay | attack < 100 ms (1-3 frames); decay 3-10× longer reads “punchy” | Thomas & Johnston, The Illusion of Life (1981); Swink | expert practice |
| Telegraph | warning before damage | ≥ 240 ms floor, ≥ 400 ms comfortable | derived from Swink’s correction cycle | derived |
| Pacing | build-up / peak / relax | L4D Director: peak held 3-5 s, relax 30-45 s | Booth, “The AI Systems of Left 4 Dead” (AIIDE 2009) | expert practice |
| Pacing | flow channel | challenge tracks skill | Csikszentmihalyi (1990); Chen, “Flow in Games” (2006) | theory, no numbers |
| Reward | first reward latency | first reward within 10-20 s, then growing gaps (VS-likes) | practitioner heuristic | folklore |
| Reward | variable ratio | unpredictable timing sustains engagement | Ferster & Skinner (1957) | measured (in animals) |
| Choice | dead picks | an option picked far below uniform, or a dominant one above ~40%, is a smell | StS/Hades pick-rate vs. win-rate practice | thresholds are guesses |
| Readability | bullet vs. background contrast | ≥ 3:1 (WCAG non-text), aim ≥ 4.5:1 for hostile bullets | WCAG 2.1 §1.4.3/1.4.11 | measured for text; adaptation |
| Near miss | graze / near-miss rate | near-misses raise the urge to keep playing only when the player controls the risk | Clark et al., Neuron (2009), https://pmc.ncbi.nlm.nih.gov/articles/PMC2658737/ | measured (gambling) |
The most defensible proxies are response latency, the intensity series, and graze/near-miss rate. Shake, hit-stop and choice-variety numbers are practitioner folklore. Calibrate those on our own data or don’t use them.
2. What the repo already measures (inventory)
Section titled “2. What the repo already measures (inventory)”| Tool | Measures | Feel relevance |
|---|---|---|
tools/balance_probe |
survival median/p25/p75, dead < 30 s, HP lost/s, damage share of watched weapons (Sim.damage_by_weapon), investment |
dead picks (the Orbit 0.1% and Nova 1.4-1.7% numbers came from it), difficulty |
tools/soak (SoakRun) |
invariants, per-upgrade “dud”/“overpowered” impact (dealt/taken ±180 ticks), softlock, digests | dud upgrades are a choice-feel signal; on_tick is the read-only hook the feel probe uses |
tools/economy_sim |
runs-to-buy per shop item, career curves | meta reward cadence (across runs, not within one) |
telemetry D1 (bh-telemetry) |
runs (DPS, level, duration), samples (fps, render_ms, weapons), hitches, deaths | latency and frame pacing on real devices: the only real source for “response” metrics |
tools/autopilot_bench |
autopilot turn rate, snaps > 60°/s, boss damage per minute | humanised-play realism, which feel metrics depend on |
tools/vfx_library |
0-5 subjective scores per effect, MISSING cards for undrawn sim state | a reviewer’s labels, not a metric |
tools/look_metrics |
saturation/value/contrast traits on captured frames vs. Nova Drift | readability and “too bright” verdicts |
tools/weapon_vfx/supernova_bench |
clipped-pixel counts for stacked bursts | a direct proxy for “too white hot” |
docs/research/2026-10-12-unremapped-alpha-audit.md |
glow alpha vs. 8-bit (2.6-9× too bright) | the “hazy / glows soft” verdicts |
polish categories §2 (bh-overnight-audit-loop) |
snap-vs-ramp grep heuristic (manual) | the “felt sudden” class of verdicts |
There was no per-second pacing series, no reward-cadence metric (one data point: first level-up panel at 18 s in the alpha audit), no automated snap scan, and no near-miss measure. The probe adds those.
3. Chris’s past verdicts: the labelled data
Section titled “3. Chris’s past verdicts: the labelled data”Mined from memory, release docs, specs and commits. 34 verdicts. The ones a metric can be checked against:
| # | When | Subject | Verdict | Measured state then |
|---|---|---|---|---|
| V1 | 2026-07-16 | low-HP border | “too much too quickly… should gradually get stronger from about half hp” | set_low_hp(hp <= max_hp*0.3), a boolean snap to 70-100% glow |
| V2 | 2025-07 → 2026-10 | Orbit | ruled dead twice (“near-useless”) | damage share median 0.1% (Toby run: 79 dmg vs Blaster 68.7k) |
| V3 | 2026-10-01 | Supernova | “increase its dmg… halve its rate of fire” | 2.3% of run damage |
| V4 | 2026-10-13 | Supernova | “doesn’t trigger until enemies close” | actually the 10 s cooldown: a dead-air gap, not range |
| V5 | 2026-10-16 | App Clip 5× XP | “way too many upgrades”, “spent most of the time pressing upgrade cards” | settled on 3× XP plus a ≥ 10 s floor between level-ups |
| V6 | 2026-10-15 | overall difficulty | “we have been tuning for Toby” | Hard median 58.7 s, 8/40 dead < 30 s |
| V7 | 2026-07 | late snowball | Toby “became seriously overpowered” | DPS 321 at 31 min |
| V8 | 2026-09-30 | evolved Nova / FX v2 | “too white hot”, “makes the screen very white” | 8 stacked additive bursts, core ×1.6 |
| V9 | 2026-10-10…13 | HDR | “washed out”, later “glows a bit soft” | saturation seeds 1.0; glow alphas 2.6-9× 8-bit |
| V10 | 2026-10-11 | death sequence | hard cut → wanted “huge explosion… slow motion… zoom” | 0.3× slow-mo 0.8 s, 1.3× zoom, 3.2 s total, “all good” |
| V11 | 2026-07 | render timing | “effects don’t fire… then suddenly all fire together” | sim/render tick desync |
| V12 | 2026-10-07 | elites | “too much health” | Orbiter 432 HP |
There are no Chris verdicts yet on hit-stop, shake strength or enemy hit flash. Any band for those is uncalibrated.
4. The probe’s metrics: definition, band, provenance, which verdict it predicts
Section titled “4. The probe’s metrics: definition, band, provenance, which verdict it predicts”Run: scripts/feel-probe.sh (tools/feel_probe/feel_probe.gd). The observer (FeelObserver) is
read-only (tests/test_feel_probe.gd pins a run bit-identical with it attached) and bins
per-second series. Rows are medians across seeds.
| Metric | How measured (headless sim) | Band (PASS / WARN) | Band source | Predicts | Confidence |
|---|---|---|---|---|---|
| dead air share | seconds with zero kills after 10 s / counted seconds | ≤ 0.10 / ≤ 0.25 | L4D build-up rhythm + VS-likes’ constant kill stream; Hard main = 0.04 | V4 (Supernova “doesn’t trigger”) at run scale; untested on a Chris verdict | low: boss fights legitimately have few kills |
| dead air longest | longest zero-kill streak | ≤ 6 s / ≤ 12 s | same | same | low |
| intensity build | mean threat near pilot (bodies + bullets within 360 px), last third / first third of 10 s windows | ≥ 1.5× / ≥ 1.1× | L4D Director build-up | V7 (snowball: build collapses when DPS outruns spawns) | medium |
| intensity variation | CV of 10 s window means | ≥ 0.20 / ≥ 0.10 | L4D peak/relax | none yet | low |
| first level-up | run seconds to level 2 | ≤ 15 s / ≤ 25 s | practitioner 10-20 s; alpha-audit capture 18 s | none yet | low-medium |
| median level-up gap | median gap between level-ups, two-sided | 10-25 s / 7-40 s | low side calibrated on V5 (5× XP → 6.5 s “way too many”; 3× → 8.7 s accepted with a 10 s floor) | V5 | medium (one verdict) |
| longest reward drought | longest gap incl. the open tail to run end | ≤ 45 s / ≤ 75 s | folklore | none yet | low |
| worst 3 s HP loss | max HP lost in any 3 s / max HP | ≤ 0.35 / ≤ 0.60 | “came out of nowhere” deaths | V6 partly; does NOT separate Hard (0.36) from Normal (0.40) | low |
| graze-s per hit | seconds with a hostile within 32 px of contact and no hard hit ÷ hard hits taken (Sim.player_hard_hits delta; DoT ticks count as HP loss, never as hits) |
≥ 3 / ≥ 1 | danmaku graze; Clark 2009 near-miss | none yet | low: autopilot dodging is not human dodging, so compare runs and don’t quote the absolute value |
| dmg share per weapon | damage_by_weapon[w] ÷ all damage dealt since w was first owned (≥ 60 s owned); weapons with no damage archetype are util |
≥ 5% / ≥ 2% | B1 patch targets (Orbit/Nova → ~12%) | V2, V3 directly | high for the direction, medium for the cut |
| unattributed damage | damage_by_weapon["unattributed"] ÷ total |
≤ 2% / ≤ 8% | guards the share rows | n/a | high |
| snap-vs-ramp scan | static regex over render/ ui/ fx/ audio/ main.gd: a visual property set by A if <qty> < x else B, or a set_*() fed a comparison on HP/charge/danger/distance/time; zero-compares and lines with lerp/clamp/ease skipped |
0 findings | polish categories §2 | V1 directly (the unit test reproduces the original line) | high for recall on that pattern; it misses snaps written any other way |
Deliberately NOT in the MVP:
- Hit-feedback latency (damage → fx event, in ticks). The sim emits
deathand other fx events in the same tick by construction, so a headless measure is always 0. The real latency is in the render path and device frame pacing. It belongs to telemetry (render_cpu_ms, hitches) andbh-diagnose-render-timing. - Shake/flash/hit-stop budgets. These need a render capture, and there are no Chris verdicts to calibrate them against.
- Bullet contrast. It needs frames (
tools/look_metricsstyle). This is a good next metric: hostile-bullet pixels vs. the local background luminance, with a ≥ 3:1 band.
5. Current main, measured (2026-10-17, origin/main 43a88350)
Section titled “5. Current main, measured (2026-10-17, origin/main 43a88350)”10 seeds (5000-5009), cap 600 s, cobalt, fresh save, autopilot (SkilledAutoPlayer + skill
profile):
| Metric | Normal / median | Normal / weak | Hard / median |
|---|---|---|---|
| run length median | 262 s | 202 s | 64 s |
| kills/s | 0.95 | 0.97 | 4.22 |
| dead air share | 0.51 FAIL | 0.42 FAIL | 0.04 PASS |
| dead air longest | 19 s FAIL | 13 s FAIL | 1 s PASS |
| intensity build | 2.01× PASS | 1.76× PASS | 3.85× PASS |
| intensity CV | 0.60 PASS | 0.51 PASS | 0.48 PASS |
| first level-up | 27.8 s FAIL | 28.3 s FAIL | 18.5 s WARN |
| median level-up gap | 17.5 s PASS | 16.4 s PASS | 8.0 s WARN (card spam side) |
| longest drought | 50 s WARN | 37 s PASS | 19 s PASS |
| worst 3 s HP loss | 0.40 WARN | 0.18 PASS | 0.36 WARN |
| graze-s per hit | 1.12 WARN | 0.83 FAIL | 0.98 FAIL |
| unattributed | 0.0% (max 1.7%) | 0.0% (max 3.5%) | 0.0% (max 1.7%) |
Dead-pick rows, Normal/median (the share while owned; n = runs owning it ≥ 60 s): blaster 24.3%
(10), railgun 9.9% (2), pulse 18.9% (1), momentum_core 18.5% (1), thunder_wake 17.6% (1),
beam 11.5% (1), blade 4.9% WARN (3), turret 4.3% WARN (3), scythe 2.1% WARN (1),
scatter 2.0% WARN (1); chalice and scar_tissue print n/a: their damage is credited to no
weapon on main (see §6.2), so their 0.0% is an attribution gap, not a measured share. An earlier
12-seed run with the plain autopilot (no profile) had orbit 0.1% FAIL and chronowell 0.0% (now
util). The Normal/median shares do not add up to 100% because Rift Bolt damage is credited to
aim, which the table does not list.
Snap scan on main: 2 findings, ui/hud.gd:271 and :323. The HP bar fill (P1 and P2) switches to
a fast 9 rad/s alpha pulse the instant HP crosses 30%. That is the same pattern as V1’s border,
on the bar instead of the screen edge.
What-if runs (measurement only, nothing changed on main)
Section titled “What-if runs (measurement only, nothing changed on main)”- App Clip XP (
--force=Difficulty.NORMAL_XP_MULT=x,…BOOST_LEVELS=99, 8 seeds, 180 s): median gap 14.5 s at 1.5×, 8.7 s at 3×, 6.5 s at 5×. This is the calibration for the low side of the median-gap band. - Denser Normal start (
--force=Difficulty.NORMAL_BIOMASS_START_FRAC=0.35, 10 seeds, 600 s): survival unchanged (264 s vs. 262 s), kills/s 0.95 → 1.56, dead air 0.51 → 0.36, first level-up 27.8 → 20.3 s. This is a candidate dial for Chris, not a proposal to merge.
6. Top feel issues the probe flags on main now
Section titled “6. Top feel issues the probe flags on main now”- Normal mode’s first minutes are probably empty. Half of all seconds have no kill and the
first level-up takes ~28 s, against 18.5 s on Hard. The cause is the design knob:
NORMAL_BIOMASS_START_FRAC0.15 ramping over 600 s. Normal is the 1.1 default, so this is the first thing a new player feels. Confidence: medium. The direction is clear, but no Chris verdict exists on Normal pacing yet, and the autopilot kills less efficiently than a human. The 0.35 what-if fixes a third of it without moving survival. DECISION (Chris). - Damage attribution blind spot. Two weapons’ damage is credited to no weapon on main, so they
read 0 on the death screen, in telemetry and in this probe. Chalice pool damage is applied in
Sim._update_chalice_zones, outside the per-weapondamage_contextloop. Scar Tissue’s discharge runs insideSim._hurt_player(sim.gd ~3373 →weapon_scar_tissue.gd) under whatever context is current, neverscar_tissue. In the two Chalice runsunattributedwas ≤ 1.7% of all damage over 130-217 s of ownership, so Chalice is probably near-dead too, but that bound is loose. Scar Tissue’s real share is unknown. The probe marks bothn/a. Branchdamage-attribution(W10, 2026-10-17) is fixing the crediting; drop them fromUNATTRIBUTED_WEAPONSinfeel_probe.gdonce it lands, then re-run. Orbit was still 0.1% in the one run that took it (B1 is only live on dev). - HP-bar 30% snap (
ui/hud.gd:271/323): a fast pulse switches on at a hard threshold. This is the exact class Chris called “too much too quickly” for the border. The fix pattern is a ramp from ~50% HP with eased pulse depth.
Weaker flags: graze-per-hit is about 1 grazing second per hard hit (WARN on Normal/median, FAIL on Normal/weak and Hard), well below the ≥ 3 band. The band is a guess and the number is autopilot-dependent, so compare branches rather than reading the absolute value. (Round 1 of this doc counted every DoT tick as a hit and reported 0.24-0.73; fixed after review.) Hard’s median level-up gap of 8.0 s sits on the card-spam side of the V5-calibrated band.
7. Calibration log (move a band only with a verdict)
Section titled “7. Calibration log (move a band only with a verdict)”| Date | Band | Change | Verdict that forced it |
|---|---|---|---|
| 2026-10-17 | median level-up gap | made two-sided, low side 10 s pass / 7 s warn | V5 App Clip 5× XP (probe: 6.5 s) |
8. Next steps
Section titled “8. Next steps”- Bullet-contrast metric from captured frames (
tools/look_metricspattern, band ≥ 3:1). - Exclude boss-alive seconds from dead air (a boss fight is not dead air).
- A humanised-autopilot run (
humanize) as the default for graze metrics. - A shake/flash budget once Chris gives a verdict on either.
- Ask Chris for a verdict on Normal’s opening minute. That one answer calibrates the dead-air and first-level bands.