fix: human vs AI fight bugs — CSP for TTS, invisible sprites, fight end sequence

- Allow huggingface.co in CSP connect-src (fixes Kokoro TTS model download)
- Add registerSW.js route (fixes PWA service worker 404)
- Add _resetPositions() safety after entrance (fixes invisible fighters)
- Fight end sequence works without canvas scene (KO/overlays/log always play)
- Pre-fight instructions in battle log for human players
- NIP-55 visibility sync and cleanup handlers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-09 17:00:20 +00:00
co-authored by Claude Opus 4.6
parent 63cc00fcb6
commit 150ce7447d
5 changed files with 142 additions and 54 deletions
+63 -47
View File
@@ -396,6 +396,8 @@ async function _doReplay() {
// Entrance animations
if (scene) {
await scene.playEntrance()
// Safety: ensure fighters are visible after entrance (prevents invisible characters if entrance times out)
scene._resetPositions()
await sleep(300)
}
@@ -616,58 +618,72 @@ async function _doReplay() {
if (typeof speechSynthesis !== 'undefined') speechSynthesis.cancel()
// Always end with a dramatic death/KO sequence
if (scene) {
if (props.fight.winnerId) {
const winningSide = props.fight.winnerId === props.fight.botA!.id ? 'a' : 'b'
const winnerHp = winningSide === 'a' ? props.fight.botAHp : props.fight.botBHp
const isPerfect = winnerHp >= 200
const winnerName = winningSide === 'a' ? props.fight.botA!.name : props.fight.botB!.name
if (props.fight.winnerId) {
const winningSide = props.fight.winnerId === props.fight.botA!.id ? 'a' : 'b'
const winnerHp = winningSide === 'a' ? props.fight.botAHp : props.fight.botBHp
const isPerfect = winnerHp >= 200
const winnerName = winningSide === 'a' ? props.fight.botA!.name : props.fight.botB!.name
// "FINISH HIM!" moment before KO
sfxDrumRoll()
await sleep(500)
announceFinishHim()
await showOverlay('FINISH IT!', '#ff2d2d', 900)
await sleep(150)
// "FINISH HIM!" moment before KO
sfxDrumRoll()
await sleep(500)
announceFinishHim()
await showOverlay('FINISH IT!', '#ff2d2d', 900)
await sleep(150)
if (isPerfect) {
await scene.playPerfect(winningSide, winnerName)
announceFlawlessVictory()
await sleep(600) // let "flawless victory" voice land
} else {
await scene.playKO(winningSide, winnerName)
await sleep(300) // let fatality voice finish
if (scene) {
try {
if (isPerfect) {
await scene.playPerfect(winningSide, winnerName)
announceFlawlessVictory()
await sleep(600) // let "flawless victory" voice land
} else {
await scene.playKO(winningSide, winnerName)
await sleep(300) // let fatality voice finish
}
// Victory celebration — detect upsets (winner had lower Elo)
const winnerElo = winningSide === 'a' ? (props.fight.botA?.eloRating ?? 1200) : (props.fight.botB?.eloRating ?? 1200)
const loserElo = winningSide === 'a' ? (props.fight.botB?.eloRating ?? 1200) : (props.fight.botA?.eloRating ?? 1200)
const isUpset = winnerElo < loserElo - 100
await scene.playVictoryCelebration(winningSide, isUpset)
} catch (err) {
console.warn('[FightViewer] KO animation failed:', err)
}
// Victory celebration — detect upsets (winner had lower Elo)
const winnerElo = winningSide === 'a' ? (props.fight.botA?.eloRating ?? 1200) : (props.fight.botB?.eloRating ?? 1200)
const loserElo = winningSide === 'a' ? (props.fight.botB?.eloRating ?? 1200) : (props.fight.botA?.eloRating ?? 1200)
const isUpset = winnerElo < loserElo - 100
await scene.playVictoryCelebration(winningSide, isUpset)
glitching.value = true
sfxApplause()
sfxCrowdCheer()
await sleep(200)
glitching.value = false
const upsetTag = isUpset ? ' UPSET!' : ''
await showOverlay(`${winnerName} WINS!${upsetTag}`, isUpset ? '#ff6600' : '#00f0ff', 1800)
logItems.value.push(
{ type: 'divider', round: 99, text: '', color: '' },
{ type: 'result', round: 99, text: `${winnerName.toUpperCase()} WINS!${isPerfect ? ' PERFECT!' : ''}`, color: winningSide === 'a' ? 'neon-cyan' : 'neon-pink' },
)
scrollLog()
} else {
// Draws get a dramatic double-KO
await scene.playKO('a', 'NOBODY')
await showOverlay('DOUBLE K.O.!', '#ff2d2d', 1500)
logItems.value.push(
{ type: 'divider', round: 99, text: '', color: '' },
{ type: 'result', round: 99, text: 'DOUBLE K.O.! DRAW!', color: 'neon-purple' },
)
scrollLog()
// Scene unavailable — still announce vocally
if (isPerfect) announceFlawlessVictory()
await sleep(800)
}
const winnerElo = winningSide === 'a' ? (props.fight.botA?.eloRating ?? 1200) : (props.fight.botB?.eloRating ?? 1200)
const loserElo = winningSide === 'a' ? (props.fight.botB?.eloRating ?? 1200) : (props.fight.botA?.eloRating ?? 1200)
const isUpset = winnerElo < loserElo - 100
glitching.value = true
sfxApplause()
sfxCrowdCheer()
await sleep(200)
glitching.value = false
const upsetTag = isUpset ? ' UPSET!' : ''
await showOverlay(`${winnerName} WINS!${upsetTag}`, isUpset ? '#ff6600' : '#00f0ff', 1800)
logItems.value.push(
{ type: 'divider', round: 99, text: '', color: '' },
{ type: 'result', round: 99, text: `${winnerName.toUpperCase()} WINS!${isPerfect ? ' PERFECT!' : ''}`, color: winningSide === 'a' ? 'neon-cyan' : 'neon-pink' },
)
scrollLog()
} else {
// Draws get a dramatic double-KO
if (scene) {
try { await scene.playKO('a', 'NOBODY') } catch { /* continue */ }
}
await showOverlay('DOUBLE K.O.!', '#ff2d2d', 1500)
logItems.value.push(
{ type: 'divider', round: 99, text: '', color: '' },
{ type: 'result', round: 99, text: 'DOUBLE K.O.! DRAW!', color: 'neon-purple' },
)
scrollLog()
}
// Kill any remaining queued speech — fight is over