fix: page-breaking bugs — duplicate onMounted, SSE race, canvas lifecycle, ghost animation

- ArenaPage: merge duplicate onMounted hooks into single parallel fetch
- FightPage: queue SSE events until liveFightData ready, add reconnect with backoff
- FightViewer: fix stale canvas ref with container ref, add sceneReady guard
- HomePage: replace stopCycling boolean with AbortController for clean unmount

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-07 23:37:43 +00:00
co-authored by Claude Opus 4.6
parent 84625bc4d2
commit e31b15ed28
4 changed files with 104 additions and 65 deletions
+23 -14
View File
@@ -42,8 +42,10 @@ const props = defineProps<{ fight: FightData; autoplay?: boolean }>()
const emit = defineEmits<{ 'replay-done': [] }>()
const canvasRef = ref<HTMLCanvasElement>()
const canvasContainer = ref<HTMLElement>()
const logEl = ref<HTMLElement>()
let scene: FightSceneController | null = null
const sceneReady = ref(false)
const isReplaying = ref(false)
const displayHpA = ref(100)
@@ -94,32 +96,39 @@ function mapHp(hp: number, winnerId: string | null, botId: string | undefined):
onUnmounted(() => {
stopAllAudio()
sceneReady.value = false
if (scene) { scene.destroy(); scene = null }
})
async function initScene() {
if (!canvasRef.value || !props.fight.botA || !props.fight.botB) return
// Destroy previous scene fully — replace canvas to avoid "KAPLAY already initialized"
if (!props.fight.botA || !props.fight.botB) return
// Destroy previous scene fully
sceneReady.value = false
if (scene) { scene.destroy(); scene = null }
const container = canvasRef.value.parentElement
if (container) {
// Replace canvas element so Kaplay gets a fresh context
const oldCanvas = canvasRef.value
const newCanvas = document.createElement('canvas')
newCanvas.className = oldCanvas.className
newCanvas.width = container.clientWidth
newCanvas.height = container.clientHeight
const container = canvasContainer.value
if (!container) return
// Replace canvas element so Kaplay gets a fresh context
const oldCanvas = canvasRef.value
const newCanvas = document.createElement('canvas')
newCanvas.className = 'w-full h-full block'
newCanvas.width = container.clientWidth
newCanvas.height = container.clientHeight
if (oldCanvas) {
oldCanvas.replaceWith(newCanvas)
canvasRef.value = newCanvas
} else {
container.prepend(newCanvas)
}
canvasRef.value = newCanvas
scene = await createFightScene({
canvas: canvasRef.value,
canvas: newCanvas,
botA: { name: props.fight.botA.name, seed: props.fight.botA.avatarSeed || props.fight.botA.name, tier: props.fight.botA.tier, archetype: props.fight.botA.archetype, customization: props.fight.botA.customization as any, wins: props.fight.botA.wins, losses: props.fight.botA.losses },
botB: { name: props.fight.botB.name, seed: props.fight.botB.avatarSeed || props.fight.botB.name, tier: props.fight.botB.tier, archetype: props.fight.botB.archetype, customization: props.fight.botB.customization as any, wins: props.fight.botB.wins, losses: props.fight.botB.losses },
arena: props.fight.arena,
})
sceneReady.value = true
}
const challengeLabel = (type: string) => {
@@ -262,7 +271,7 @@ async function replay() {
const bWon = round.winnerId === props.fight.botB!.id
try {
await scene!.playRound({
await scene?.playRound({
round: round.roundNumber,
challengeType: round.challengeType,
winnerId: round.winnerId,
@@ -501,7 +510,7 @@ async function replay() {
</div>
<!-- Canvas + floating overlays -->
<div class="flex-1 relative min-h-0" :class="{ 'glitch-container': glitching }">
<div ref="canvasContainer" class="flex-1 relative min-h-0" :class="{ 'glitch-container': glitching }">
<canvas ref="canvasRef" class="w-full h-full block" />
<!-- Floating announcement -->