Compare commits

...
Author SHA1 Message Date
byrongamatos cfc24145fc Merge remote-tracking branch 'origin/feat/career-plugin' into test/career-mode-combined 2026-07-13 13:15:26 +02:00
byrongamatosandClaude Fable 5 a12b459fcc feat(career): crowd-SFX settings toggle + sfx/intro manifest validation
Settings → System → Career panel with the 'Crowd sound reactions' toggle
(writes the localStorage key the crowd layer reads). Pack manifests may
carry sfx {up, down} mp3s, validated like intro files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 13:15:01 +02:00
byrongamatos 9f91fc46e2 Merge remote-tracking branch 'origin/feat/career-venue-crowd' into test/career-mode-combined 2026-07-13 13:13:53 +02:00
byrongamatosandClaude Fable 5 523feb161e feat(venue-crowd): toggleable crowd sound reactions on mood transitions
Rank up → the venue's cheer sample, rank down → boos, gated behind the
feedBack-venue-crowd-sfx setting (default off), rate-limited by the
existing transition hysteresis and suppressed during the intro. Sounds
ship per-venue in the pack manifest (sfx: {up, down}).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 13:09:17 +02:00
byrongamatos b7ff77fb6e Merge remote-tracking branch 'origin/main' into test/career-mode-combined
# Conflicts:
#	plugins/career/routes.py
#	plugins/career/screen.js
#	tests/js/career_plugin.test.js
#	tests/plugins/career/test_routes.py
2026-07-12 22:58:51 +02:00
byrongamatos 52e7ffa5f8 Merge remote-tracking branch 'origin/feat/career-plugin' into test/career-mode-combined 2026-07-12 22:39:30 +02:00
byrongamatos 36d984fffd fix(career): refresh tailwind output 2026-07-12 22:32:44 +02:00
byrongamatos e1562559ba Merge remote-tracking branch 'origin/feat/career-plugin' into test/career-mode-combined 2026-07-12 21:45:34 +02:00
byrongamatos cfc138ab5e Merge remote-tracking branch 'origin/feat/career-venue-crowd' into test/career-mode-combined 2026-07-12 21:45:34 +02:00
byrongamatosandClaude Fable 5 f8fc5e6a5f fix(career): let pushCrowdManifest clear the manifest on Leave venue
Codex preflight: nulling _appliedManifestVenue before refresh skipped the
setManifest(null) cleanup branch, leaving the crowd playing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:42:03 +02:00
byrongamatosandClaude Fable 5 803193046e feat(career): venue select/unselect UX, intro manifest support, fullmatch guards
- 'Play here' now also defaults the visualization to Venue (remembering
  the prior viz); active venues show 'Leave venue' which restores it and
  sets the '__none__' override so no installed venue silently reapplies.
- Pack manifests may ship an intro block (flyover video + ambience mp3);
  files validate like loops/stingers, .mp3 added to the serving whitelist.
- Codex preflight: whitelist regexes use fullmatch (trailing-newline names
  could validate but 500 on serving).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:39:41 +02:00
byrongamatosandClaude Fable 5 4abdfb5f5c fix(venue-crowd): fall back to the loop when the intro fails to load
Codex preflight: a failed/timed-out intro left the song with no crowd
loop at all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:25:26 +02:00
byrongamatosandClaude Fable 5 99812b8ed3 feat(venue-crowd): flyover intro with crowd-ambience ducking
On song:loaded, an optional pack intro plays once: a camera flyover video
(idle layer, one-shot) with bar-crowd ambience audio that ducks out on
song:play, near the flyover's landing, or at handoff — whichever first.
Machine commits and stingers defer during the intro; stop()/song-change
abort it. Packs without an intro behave as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:21:22 +02:00
byrongamatosandClaude Fable 5 702a9c6daa feat(career): 50/150 star thresholds + star collection overview
Byron's progression tuning: club at 50★, arena at 150★. /state now
returns star_detail rows (title/artist joined from the library, stars,
best accuracy, next-star threshold) sorted closest-to-next-star first,
and the career screen renders a collection panel: tier summary plus a
per-song list with a 'N% to next star' practice hint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:14:00 +02:00
byrongamatosandClaude Fable 5 9d8fda6d46 fix(venue-crowd): always detach load listeners, gate only the callback
Codex preflight: superseded loads left canplaythrough/error listeners
attached to the persistent video elements — unbounded growth over a
session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:09:00 +02:00
byrongamatosandClaude Fable 5 663e8ff4a1 fix(venue-crowd): abort stale stinger state on song load
Codex preflight: a stinger straddling a song change could fade back into
the previous song's layer or flush its pending loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:06:04 +02:00
byrongamatosandClaude Fable 5 f362c9a083 fix(venue-crowd): don't let null accuracy resets wipe the end-of-song value
Codex preflight: Number(null) is 0, so idle HUD resets overwrote
_lastAccuracyPct before stats:recorded consumed it, suppressing the
end-of-song stinger.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:03:48 +02:00
byrongamatosandClaude Fable 5 be78b4f29e fix(venue-crowd): cancel in-flight fade when a stinger preempts it
Codex preflight: the orphaned ramp kept pushing the mix toward the layer
whose src the stinger had just replaced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:01:28 +02:00
byrongamatosandClaude Fable 5 f9a57ba044 fix(venue-crowd): reset crowd mood to neutral on song load
Codex preflight: a song ending in ecstatic/bored left the next song's
crowd stuck in that mood until the hysteresis window passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 20:58:15 +02:00
byrongamatosandClaude Fable 5 c83fec267e fix(venue-crowd): reset active layer with mix on stop
Codex preflight: stop() zeroed the mix but left _activeLayer at 1, so a
restart flashed layer 0's stale frame until the new loop loaded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 20:54:20 +02:00
byrongamatosandClaude Fable 5 c73ff96dfe fix(venue-crowd): keep rear video layer opaque during crossfades
Two half-transparent layers let the static bg plate bleed through (~25%
at mid-fade) — visible as a flash of the old still image on every state
transition. The crossfade is now always the front layer fading over an
opaque rear layer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 20:51:41 +02:00
byrongamatos c78e7e04d3 Merge remote-tracking branch 'origin/feat/career-plugin' into test/career-mode-combined 2026-07-12 15:37:33 +02:00
byrongamatos d539421a03 Merge remote-tracking branch 'origin/feat/career-venue-crowd' into test/career-mode-combined 2026-07-12 15:37:32 +02:00
byrongamatosandClaude Fable 5 99b6d3c384 fix(career): exclude orphaned song_stats from star totals
Codex preflight: scans hide rather than delete stats of removed songs, so
stars now apply the same existing-song filter other stats surfaces use
(filename IN (SELECT filename FROM songs)).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 01:56:48 +02:00
byrongamatosandClaude Fable 5 0cc08ebebf fix(career): generation-guard in-flight manifest fetches
Codex preflight: a manifest fetch resolving after a newer refresh (pack
deleted, venue switched) could re-apply a stale pack over the user's
newer selection — fetches now carry a generation token and bail when
superseded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 01:54:02 +02:00
byrongamatosandClaude Fable 5 a2a48b3912 fix(career): keep manifest cleanup path alive on delete; badge only for installed venues
Codex preflight: nulling _appliedManifestVenue on delete skipped
pushCrowdManifest's setManifest(null) cleanup, leaving the crowd layer on
a deleted pack; and the 'playing here' badge showed for an override venue
whose pack was removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 01:51:19 +02:00
byrongamatosandClaude Fable 5 05a9bee38f feat(career): career plugin — stars from song_stats, venue tiers, pack downloads (career mode PR2)
Bundled plugin: per-song stars from best_accuracy (60/75/85% → 1/2/3★),
cumulative stars unlock bar → club → arena (data-driven venues.json).
Venue packs (UE-rendered crowd loops) download on demand to
CONFIG_DIR/plugin_uploads/career/ on a background thread with sha256 +
zip-slip validation, served via FileResponse. Career screen (promoted
sidebar entry) shows progress and pushes the active venue's manifest
into the crowd video layer (v3VenueCrowd, PR1) — degrades cleanly when
either side is absent. Pack URLs land in venues.json in PR3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 01:48:02 +02:00
byrongamatosandClaude Fable 5 286a24214f fix(venue-crowd): bail loop-fade completion when a stinger preempted the layer
Codex preflight round 6: the loop crossfade's completion callback could
still run between a stinger's start and its canplaythrough, promoting the
stinger's layer to active and pausing the real loop — it now bails when
the fading loop was preempted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 01:35:17 +02:00
byrongamatosandClaude Fable 5 9178959dbd fix(venue-crowd): generation-gate stinger handlers; recrop on video size change
Codex preflight round 5: (1) an ended/timeout handler orphaned by stop()
could fire into a later stinger's lifecycle on the reused element — handlers
now detach unconditionally and carry a generation token; (2) the renderer
only re-applied cover-crop on camera aspect changes, so a src swap with a
different intrinsic size kept stale repeat/offset — it now recrops when
videoWidth/Height change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 01:33:12 +02:00
byrongamatosandClaude Fable 5 de10e81259 fix(venue-crowd): requeue mid-crossfade loops preempted by stingers; hard-stop on manifest swap
Codex preflight round 4: (1) idleLayer() still points at the fading-in
layer during a crossfade, so a stinger firing mid-fade overwrote the new
loop with nothing requeued — the fading loop is now tracked and requeued
like an in-flight load; (2) swapping venue packs while active now goes
through stop() so _stopGen invalidates the old manifest's in-flight loads.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 01:29:38 +02:00
byrongamatosandClaude Fable 5 1172bc30cb fix(venue-crowd): flush deferred loop on stinger failure, source accuracy from perf events
Codex preflight round 3: (1) a failed/timed-out stinger left a deferred
loop switch queued forever; the failure path now flushes it. (2)
stats:recorded only carries {filename, arrangement}, so the end-of-song
reaction now uses the accuracyPct from the song's last
v3:live-performance-state event (a real percentage) instead of a field
that never existed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 01:26:47 +02:00
byrongamatosandClaude Fable 5 5e5d892a63 fix(venue-crowd): per-video load tokens + unbind renderer on stop
Codex preflight round 2: (1) the global load token let a stinger cancel a
committed loop load on the other layer — tokens are now per-element, and a
stinger preempting an in-flight loop on its own layer requeues that loop
for when the stinger ends; (2) setManifest(null)/deactivate left the last
crowd frame bound and visible over the static plate — stop() now unbinds
both layers from the renderer and zeroes the mix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 01:24:11 +02:00
byrongamatosandClaude Fable 5 cab652d145 fix(venue-crowd): retry renderer binding + preserve mid-stinger transitions
Codex preflight P2s: (1) videos created before highway_3d registered its
globals never reached the backdrop planes — binding is now idempotent and
retried from start/perf-event/re-activation paths; (2) a crowd-state
switch committing while a stinger played was dropped because the machine
had already advanced — it is now deferred and played when the stinger ends.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 01:21:19 +02:00
byrongamatosandClaude Fable 5 78dbb039b7 feat(venue): reactive crowd video layer behind the 3D highway (career mode PR1)
Two crossfading video backdrop planes in the highway_3d venue background
style, driven by a new venue-crowd.js state machine that maps
v3:live-performance-state to crowd states (bored/neutral/engaged/ecstatic)
with 3s stability + 8s dwell hysteresis, plus one-shot reaction stingers
on streak milestones and end-of-song accuracy. Inert without a venue pack
manifest (career plugin, PR2) or the feedBack-venue-crowd-dev flag — the
static bg plate behaves exactly as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 01:17:58 +02:00
4 changed files with 64 additions and 6 deletions
+6 -2
View File
@@ -4,9 +4,13 @@
"version": "0.1.0",
"bundled": true,
"private": false,
"description": "Career mode gig your way from a local bar to the arena. Earn stars per song; the crowd reacts to how you play.",
"description": "Career mode \u2014 gig your way from a local bar to the arena. Earn stars per song; the crowd reacts to how you play.",
"screen": "screen.html",
"script": "screen.js",
"styles": "assets/career.css",
"routes": "routes.py"
"routes": "routes.py",
"settings": {
"html": "settings.html",
"category": "system"
}
}
+4 -3
View File
@@ -134,9 +134,10 @@ def _validate_pack_dir(pack_dir: Path):
for name in (manifest.get("stingers") or {}).values():
if name and (not PACK_FILENAME_RE.fullmatch(name) or not (pack_dir / name).is_file()):
raise ValueError(f"stinger file '{name}' invalid or missing")
for name in (manifest.get("intro") or {}).values():
if name and (not PACK_FILENAME_RE.fullmatch(name) or not (pack_dir / name).is_file()):
raise ValueError(f"intro file '{name}' invalid or missing")
for block in ("intro", "sfx"):
for name in (manifest.get(block) or {}).values():
if name and (not PACK_FILENAME_RE.fullmatch(name) or not (pack_dir / name).is_file()):
raise ValueError(f"{block} file '{name}' invalid or missing")
def _download_pack(venue_id, pack, progress):
+21
View File
@@ -0,0 +1,21 @@
<div class="space-y-3 text-sm">
<label class="flex items-center justify-between gap-4">
<span>
<span class="text-gray-200 font-medium">Crowd sound reactions</span>
<span class="block text-xs text-gray-500">Cheers when the crowd's mood rises, boos when it drops. Uses each venue's own recordings.</span>
</span>
<input type="checkbox" id="career-sfx-toggle" class="accent-cyan-500 w-4 h-4">
</label>
</div>
<script>
(function () {
'use strict';
var KEY = 'feedBack-venue-crowd-sfx';
var box = document.getElementById('career-sfx-toggle');
if (!box) return;
try { box.checked = localStorage.getItem(KEY) === 'on'; } catch (e) { /* ok */ }
box.addEventListener('change', function () {
try { localStorage.setItem(KEY, box.checked ? 'on' : 'off'); } catch (e) { /* ok */ }
});
}());
</script>
+33 -1
View File
@@ -34,6 +34,7 @@
const STREAK_MILESTONES = [25, 50, 100];
const CANPLAY_TIMEOUT_MS = 4000;
const DEV_FLAG_KEY = 'feedBack-venue-crowd-dev';
const SFX_KEY = 'feedBack-venue-crowd-sfx'; // 'on' | 'off' (default off)
// ---------------------------------------------------------------------
// Pure, clock-injected decision logic (unit-tested in
@@ -144,7 +145,11 @@
video: abs(m.intro && m.intro.video),
audio: abs(m.intro && m.intro.audio),
};
return { loops, stingers, intro };
const sfx = {
up: abs(m.sfx && m.sfx.up),
down: abs(m.sfx && m.sfx.down),
};
return { loops, stingers, intro, sfx };
}
function ensureVideos() {
@@ -410,6 +415,30 @@
return true;
}
let _sfxEl = null;
function sfxEnabled() {
try { return localStorage.getItem(SFX_KEY) === 'on'; } catch (_) { return false; }
}
// One-shot crowd reaction on committed mood transitions (toggleable):
// up the ladder → cheer, down → boos. Committed transitions are already
// hysteresis-limited, so this can't spam.
function playMoodSfx(direction) {
if (!sfxEnabled() || !_manifest || !_manifest.sfx || _introActive) return;
const url = direction > 0 ? _manifest.sfx.up : _manifest.sfx.down;
if (!url || typeof document === 'undefined') return;
if (!_sfxEl) {
_sfxEl = document.createElement('audio');
_sfxEl.preload = 'auto';
_sfxEl.style.display = 'none';
document.body.appendChild(_sfxEl);
}
_sfxEl.src = url;
_sfxEl.volume = 0.6;
_sfxEl.play().catch(() => { /* pre-gesture; skip silently */ });
}
function onSongPlay() {
// Song audio starting is the hard cue: the ambience must yield.
fadeAudioOut(1000);
@@ -430,8 +459,10 @@
if (sting && !_introActive && CROWD_RANK[machine.current] >= CROWD_RANK.neutral) {
playStinger(sting);
}
const prevRank = CROWD_RANK[machine.current];
const next = machine.update(d.state, now());
if (next) {
playMoodSfx(CROWD_RANK[next] - prevRank);
// A stinger or the intro owns the idle layer; defer the switch.
if (_stingerUntilEnded || _introActive) _pendingLoop = next;
else showLoop(next, FADE_MS);
@@ -489,6 +520,7 @@
_introGen++;
_introActive = false;
stopAudio();
if (_sfxEl && !_sfxEl.paused) _sfxEl.pause();
_stingerUntilEnded = false;
_pendingLoop = null;
_loadingLoop = null;