mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-12 06:18:31 +00:00
Compare commits
34
Commits
@@ -4,9 +4,13 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"private": false,
|
"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",
|
"screen": "screen.html",
|
||||||
"script": "screen.js",
|
"script": "screen.js",
|
||||||
"styles": "assets/career.css",
|
"styles": "assets/career.css",
|
||||||
"routes": "routes.py"
|
"routes": "routes.py",
|
||||||
|
"settings": {
|
||||||
|
"html": "settings.html",
|
||||||
|
"category": "system"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,9 +134,10 @@ def _validate_pack_dir(pack_dir: Path):
|
|||||||
for name in (manifest.get("stingers") or {}).values():
|
for name in (manifest.get("stingers") or {}).values():
|
||||||
if name and (not PACK_FILENAME_RE.fullmatch(name) or not (pack_dir / name).is_file()):
|
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")
|
raise ValueError(f"stinger file '{name}' invalid or missing")
|
||||||
for name in (manifest.get("intro") or {}).values():
|
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()):
|
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")
|
raise ValueError(f"{block} file '{name}' invalid or missing")
|
||||||
|
|
||||||
|
|
||||||
def _download_pack(venue_id, pack, progress):
|
def _download_pack(venue_id, pack, progress):
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
const STREAK_MILESTONES = [25, 50, 100];
|
const STREAK_MILESTONES = [25, 50, 100];
|
||||||
const CANPLAY_TIMEOUT_MS = 4000;
|
const CANPLAY_TIMEOUT_MS = 4000;
|
||||||
const DEV_FLAG_KEY = 'feedBack-venue-crowd-dev';
|
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
|
// Pure, clock-injected decision logic (unit-tested in
|
||||||
@@ -144,7 +145,11 @@
|
|||||||
video: abs(m.intro && m.intro.video),
|
video: abs(m.intro && m.intro.video),
|
||||||
audio: abs(m.intro && m.intro.audio),
|
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() {
|
function ensureVideos() {
|
||||||
@@ -410,6 +415,30 @@
|
|||||||
return true;
|
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() {
|
function onSongPlay() {
|
||||||
// Song audio starting is the hard cue: the ambience must yield.
|
// Song audio starting is the hard cue: the ambience must yield.
|
||||||
fadeAudioOut(1000);
|
fadeAudioOut(1000);
|
||||||
@@ -430,8 +459,10 @@
|
|||||||
if (sting && !_introActive && CROWD_RANK[machine.current] >= CROWD_RANK.neutral) {
|
if (sting && !_introActive && CROWD_RANK[machine.current] >= CROWD_RANK.neutral) {
|
||||||
playStinger(sting);
|
playStinger(sting);
|
||||||
}
|
}
|
||||||
|
const prevRank = CROWD_RANK[machine.current];
|
||||||
const next = machine.update(d.state, now());
|
const next = machine.update(d.state, now());
|
||||||
if (next) {
|
if (next) {
|
||||||
|
playMoodSfx(CROWD_RANK[next] - prevRank);
|
||||||
// A stinger or the intro owns the idle layer; defer the switch.
|
// A stinger or the intro owns the idle layer; defer the switch.
|
||||||
if (_stingerUntilEnded || _introActive) _pendingLoop = next;
|
if (_stingerUntilEnded || _introActive) _pendingLoop = next;
|
||||||
else showLoop(next, FADE_MS);
|
else showLoop(next, FADE_MS);
|
||||||
@@ -489,6 +520,7 @@
|
|||||||
_introGen++;
|
_introGen++;
|
||||||
_introActive = false;
|
_introActive = false;
|
||||||
stopAudio();
|
stopAudio();
|
||||||
|
if (_sfxEl && !_sfxEl.paused) _sfxEl.pause();
|
||||||
_stingerUntilEnded = false;
|
_stingerUntilEnded = false;
|
||||||
_pendingLoop = null;
|
_pendingLoop = null;
|
||||||
_loadingLoop = null;
|
_loadingLoop = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user