feat: v5 — boxing poster fight cards, 12-char names, diverse mock bots
- Fight Card page: dramatic poster background with cross-hatch, spotlights, vignettes, corner brackets, scan lines; 3D VS orb with punch animation; selectable undercard with main event always pinned at top - PosterSprite: high-quality 480px poster frame with 6-pass renderer (aura, glow, bevel, specular, particles); PixelGlove component - 12-char bot name limit across all forms and server validation - Mock bots: all 100 now have diverse archetypes (25 types), 25% human fighters; seedMockBots updates existing bots on restart - Leaderboard: inline SpritePreview next to each bot name - Nostr auth: persistent login, nsec copy button - Wallet: NWC + Lightning Address, ranked fight flow - Server: payments, ranked queue, customization endpoint Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
ccf4196647
commit
f6eb7d2845
+44
-24
@@ -5,23 +5,27 @@ let sfxGain: GainNode | null = null
|
||||
let musicPlaying = false
|
||||
let musicTimeout: number | null = null
|
||||
|
||||
// Lazily create AudioContext + gain nodes on first use.
|
||||
// Chrome may warn about autoplay policy but the context will resume once ensureAudioContext()
|
||||
// is called from a user gesture. All SFX calls before that are queued/silent.
|
||||
// AudioContext is ONLY created inside ensureAudioContext() (called from user gesture).
|
||||
// Before that, all SFX calls silently no-op to avoid Chrome autoplay warnings.
|
||||
let _audioUnlocked = false
|
||||
|
||||
function initCtx() {
|
||||
if (ctx) return
|
||||
ctx = new AudioContext()
|
||||
musicGain = ctx.createGain()
|
||||
musicGain.gain.value = masterMuted ? 0 : 0.12
|
||||
musicGain.connect(ctx.destination)
|
||||
sfxGain = ctx.createGain()
|
||||
sfxGain.gain.value = masterMuted ? 0 : 0.25
|
||||
sfxGain.connect(ctx.destination)
|
||||
}
|
||||
|
||||
function getCtx(): AudioContext {
|
||||
if (!ctx) {
|
||||
ctx = new AudioContext()
|
||||
musicGain = ctx.createGain()
|
||||
musicGain.gain.value = masterMuted ? 0 : 0.12
|
||||
musicGain.connect(ctx.destination)
|
||||
sfxGain = ctx.createGain()
|
||||
sfxGain.gain.value = masterMuted ? 0 : 0.25
|
||||
sfxGain.connect(ctx.destination)
|
||||
if (!ctx) initCtx()
|
||||
if (ctx!.state === 'suspended') {
|
||||
ctx!.resume().catch(() => {})
|
||||
}
|
||||
if (ctx.state === 'suspended') {
|
||||
ctx.resume().catch(() => {})
|
||||
}
|
||||
return ctx
|
||||
return ctx!
|
||||
}
|
||||
|
||||
// Get the SFX destination node (gain is 0 when muted, so audio is silent but context stays valid)
|
||||
@@ -340,13 +344,21 @@ export function stopAllAudio() {
|
||||
stopMusic()
|
||||
if (typeof speechSynthesis !== 'undefined') speechSynthesis.cancel()
|
||||
_speechQueueDepth = 0
|
||||
// Disconnect sfxGain to instantly kill all in-flight oscillators/buffers,
|
||||
// Disconnect gain nodes to instantly kill all in-flight oscillators/buffers,
|
||||
// then reconnect so future sounds still work
|
||||
if (sfxGain && ctx) {
|
||||
sfxGain.disconnect()
|
||||
sfxGain = ctx.createGain()
|
||||
sfxGain.gain.value = masterMuted ? 0 : 0.25
|
||||
sfxGain.connect(ctx.destination)
|
||||
if (ctx) {
|
||||
if (musicGain) {
|
||||
musicGain.disconnect()
|
||||
musicGain = ctx.createGain()
|
||||
musicGain.gain.value = masterMuted ? 0 : MUSIC_VOL
|
||||
musicGain.connect(ctx.destination)
|
||||
}
|
||||
if (sfxGain) {
|
||||
sfxGain.disconnect()
|
||||
sfxGain = ctx.createGain()
|
||||
sfxGain.gain.value = masterMuted ? 0 : SFX_VOL
|
||||
sfxGain.connect(ctx.destination)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1891,14 +1903,22 @@ export function isMasterMuted(): boolean {
|
||||
return masterMuted
|
||||
}
|
||||
|
||||
let _speechUnlocked = false
|
||||
export async function ensureAudioContext() {
|
||||
const c = getCtx()
|
||||
if (c.state === 'suspended') {
|
||||
try { await c.resume() } catch {}
|
||||
}
|
||||
// Prime speech synthesis on user gesture — some browsers need this
|
||||
if (typeof speechSynthesis !== 'undefined' && !voicesLoaded) {
|
||||
loadVoices()
|
||||
// Prime speech synthesis on user gesture — mobile browsers require
|
||||
// a speak() call inside a user gesture to unlock speechSynthesis
|
||||
if (typeof speechSynthesis !== 'undefined') {
|
||||
if (!voicesLoaded) loadVoices()
|
||||
if (!_speechUnlocked) {
|
||||
_speechUnlocked = true
|
||||
const unlock = new SpeechSynthesisUtterance('')
|
||||
unlock.volume = 0
|
||||
speechSynthesis.speak(unlock)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user