Skip to content

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 death and 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) and bh-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_metrics style). 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”
  1. 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_FRAC 0.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).
  2. 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-weapon damage_context loop. Scar Tissue’s discharge runs inside Sim._hurt_player (sim.gd ~3373 → weapon_scar_tissue.gd) under whatever context is current, never scar_tissue. In the two Chalice runs unattributed was ≤ 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 both n/a. Branch damage-attribution (W10, 2026-10-17) is fixing the crediting; drop them from UNATTRIBUTED_WEAPONS in feel_probe.gd once it lands, then re-run. Orbit was still 0.1% in the one run that took it (B1 is only live on dev).
  3. 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)
  • Bullet-contrast metric from captured frames (tools/look_metrics pattern, 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.