feat(keys_highway_3d): hit FX — vibrancy, timing-colored sparks, hit-line kick (K2) (#699)

- Note vibrancy: gem opacity 0.8 -> slider-driven (default 0.92),
  NOTE_EMISSIVE_BASE 0.08 -> 0.22, lane guides scale with the slider,
  _applyVibrancy retints the live scene without a chart rebuild
- Sparks (PORTED highway_3d, pool 96) at the struck key, colored by
  _timingHex/_classifyTiming (±100ms window, inner 40% = on-time; delta
  recovered from judgeHit's noteKey prefix — contract untouched);
  streak-scaled counts
- Hit-line brightness kick on scored presses (exp(-t*6) decay, hitFx
  slider), folded into the existing pulse incl. the bloom damp gate
- Settings: Hit sparks / Timing colours / Streak feedback + Hit feedback
  intensity + Note vibrancy sliders (keys3d_bg_*, live-applying)
- Tests: classifier boundaries, noteKey time round-trip, FX defaults
  (26 total)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Byron Gamatos
2026-07-02 08:45:09 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 749af31cc3
commit 95b0786725
5 changed files with 267 additions and 11 deletions
@@ -80,3 +80,32 @@ test('keys3dSetFx: persists, coerces, and ignores unknown keys', () => {
assert.equal(events.length, 4);
assert.ok(!('keys3d_bg_nonsense' in store));
});
test('_classifyTiming: OK band is 40% of the window, sign maps early/late', () => {
const { _classifyTiming } = load().slopsmithViz_keys_highway_3d.__test;
const tol = 0.10; // keys HIT_TOLERANCE_S
assert.equal(_classifyTiming(0, tol), 'OK');
assert.equal(_classifyTiming(tol * 0.4, tol), 'OK');
assert.equal(_classifyTiming(-tol * 0.4, tol), 'OK');
// delta = note.t - now: positive → struck before the note → EARLY.
assert.equal(_classifyTiming(tol * 0.41, tol), 'EARLY');
assert.equal(_classifyTiming(-tol * 0.41, tol), 'LATE');
assert.equal(_classifyTiming(NaN, tol), 'OK');
});
test('noteKey prefix round-trips the matched note time (timing-delta source)', () => {
const { noteKey } = load().slopsmithViz_keys_highway_3d.__test;
// _checkHit derives the timing delta as parseFloat(judgeHit's key) - t;
// this pins the serialization that makes that recovery valid.
assert.equal(parseFloat(noteKey(12.3456, 60)), 12.346);
assert.equal(parseFloat(noteKey(0, 21)), 0);
});
test('FX defaults: hit-FX + vibrancy controls ship enabled', () => {
const { FX_DEFAULTS } = load().slopsmithViz_keys_highway_3d.__test;
assert.equal(FX_DEFAULTS.sparks, true);
assert.equal(FX_DEFAULTS.timingFx, true);
assert.equal(FX_DEFAULTS.streakFx, true);
assert.equal(FX_DEFAULTS.hitFx, 0.7);
assert.equal(FX_DEFAULTS.vibrancy, 0.85);
});