From e49735002cbfe65e2df6e2576f1d764af8517635 Mon Sep 17 00:00:00 2001 From: Dorian Date: Sat, 7 Mar 2026 00:53:15 +0000 Subject: [PATCH] fix: Drizzle transaction execution, canvas re-init, crowd removal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add .run() to all Drizzle queries inside sqlite.transaction() in orchestrator.ts and mock.ts — queries were building but never executing, leaving fights stuck as 'live' forever - Remove spectator crowd from drawArenaDecor() - Rename duplicate spawnProp to spawnWeaponProp (was crashing module load) - Replace canvas element on re-init to avoid "KAPLAY already initialized" - Add safeText() to strip brackets from k.text() calls (Kaplay treats [ ] as style markup, crashes on unclosed tags) Co-Authored-By: Claude Opus 4.6 --- frontend/src/components/FightViewer.vue | 12 +- frontend/src/game/FightScene.ts | 177 ++---------------------- server/src/engine/mock.ts | 8 +- server/src/engine/orchestrator.ts | 14 +- 4 files changed, 31 insertions(+), 180 deletions(-) diff --git a/frontend/src/components/FightViewer.vue b/frontend/src/components/FightViewer.vue index 46d88d0..3fee4eb 100644 --- a/frontend/src/components/FightViewer.vue +++ b/frontend/src/components/FightViewer.vue @@ -90,13 +90,19 @@ onUnmounted(() => { async function initScene() { if (!canvasRef.value || !props.fight.botA || !props.fight.botB) return - // Destroy previous scene to avoid "KAPLAY already initialized" warning + // Destroy previous scene fully — replace canvas to avoid "KAPLAY already initialized" if (scene) { scene.destroy(); scene = null } const container = canvasRef.value.parentElement if (container) { - canvasRef.value.width = container.clientWidth - canvasRef.value.height = container.clientHeight + // 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 + oldCanvas.replaceWith(newCanvas) + canvasRef.value = newCanvas } scene = await createFightScene({ diff --git a/frontend/src/game/FightScene.ts b/frontend/src/game/FightScene.ts index 54e8cb6..dfceb77 100644 --- a/frontend/src/game/FightScene.ts +++ b/frontend/src/game/FightScene.ts @@ -1414,164 +1414,6 @@ export async function createFightScene(config: FightSceneConfig) { } } } - - // === SPECTATORS — multi-row pixel-art crowd === - const skinTones = ['#e8c49a', '#d4a574', '#c68c53', '#a0704a', '#7a5230', '#f5deb3', '#deb887', '#cc9966'] - const shirtColors = ['#cc3333', '#3366cc', '#33aa33', '#cc9933', '#9933cc', '#cc3399', '#33aaaa', '#ee6622', - '#5555dd', '#dd5555', '#55bb55', '#dd8822', '#aa44aa', '#44aaaa', '#ddaa33', '#cc5588'] - const hatColors = ['#222222', '#443322', '#663333', '#224466', '#336633', '#882222', '#444488', '#884400'] - const hairColors = ['#1a1a1a', '#3a2210', '#6b3a14', '#8b6340', '#c4a060', '#ddb870', '#883322', '#aa4422'] - - // 3 rows of crowd — back row (small/faded), middle row, front row - const rows = [ - { y: GROUND_Y - 22, count: 14 + Math.floor(Math.random() * 4), scale: 0.6, opacity: 0.35, z: 4 }, - { y: GROUND_Y - 13, count: 12 + Math.floor(Math.random() * 4), scale: 0.8, opacity: 0.5, z: 5 }, - { y: GROUND_Y - 3, count: 10 + Math.floor(Math.random() * 3), scale: 1.0, opacity: 0.65, z: 6 }, - ] - - for (const row of rows) { - for (let i = 0; i < row.count; i++) { - const sx = 15 + (i / (row.count - 1)) * (W - 30) + (Math.random() - 0.5) * 12 - const sy = row.y + (Math.random() - 0.5) * 4 - const sc = row.scale - const op = row.opacity - const skin = skinTones[Math.floor(Math.random() * skinTones.length)] - const shirt = shirtColors[Math.floor(Math.random() * shirtColors.length)] - const bobSpeed = 1.2 + Math.random() * 2.5 - const bobPhase = Math.random() * Math.PI * 2 - const bobAmt = (0.8 + Math.random() * 1.5) * sc - - // Torso - const torsoW = Math.round(7 * sc) - const torsoH = Math.round(9 * sc) - const torso = k.add([ - k.rect(torsoW, torsoH), k.pos(sx, sy - torsoH), - k.color(safeColor(k,shirt)), k.opacity(op), k.z(row.z), - ]) - - // Head - const headR = Math.round(4 * sc) - const head = k.add([ - k.circle(headR), k.pos(sx + torsoW / 2, sy - torsoH - headR + 1), - k.anchor('center'), - k.color(safeColor(k,skin)), k.opacity(op), k.z(row.z), - ]) - - // Hair or hat (70% chance) - let accessory: any = null - if (Math.random() < 0.7) { - if (Math.random() < 0.4) { - // Hat - const hatC = hatColors[Math.floor(Math.random() * hatColors.length)] - accessory = k.add([ - k.rect(Math.round(10 * sc), Math.round(3 * sc)), - k.pos(sx + torsoW / 2 - Math.round(5 * sc), sy - torsoH - headR * 2), - k.color(safeColor(k,hatC)), k.opacity(op), k.z(row.z + 0.1), - ]) - } else { - // Hair tuft - const hairC = hairColors[Math.floor(Math.random() * hairColors.length)] - accessory = k.add([ - k.rect(Math.round(6 * sc), Math.round(4 * sc)), - k.pos(sx + torsoW / 2 - Math.round(3 * sc), sy - torsoH - headR * 2 + Math.round(sc)), - k.color(safeColor(k,hairC)), k.opacity(op * 0.9), k.z(row.z + 0.1), - ]) - } - } - - // Arms — little rectangles on each side - const armW = Math.round(2 * sc) - const armH = Math.round(6 * sc) - const armL = k.add([ - k.rect(armW, armH), k.pos(sx - armW, sy - torsoH + Math.round(2 * sc)), - k.color(safeColor(k,shirt)), k.opacity(op * 0.9), k.z(row.z - 0.1), - ]) - const armR = k.add([ - k.rect(armW, armH), k.pos(sx + torsoW, sy - torsoH + Math.round(2 * sc)), - k.color(safeColor(k,shirt)), k.opacity(op * 0.9), k.z(row.z - 0.1), - ]) - - // Hands (skin-colored dots at arm tips) - const handL = k.add([ - k.circle(Math.max(1, Math.round(1.5 * sc))), - k.pos(sx - armW + Math.round(sc), sy - torsoH + Math.round(2 * sc) + armH), - k.anchor('center'), - k.color(safeColor(k,skin)), k.opacity(op * 0.8), k.z(row.z - 0.1), - ]) - const handR = k.add([ - k.circle(Math.max(1, Math.round(1.5 * sc))), - k.pos(sx + torsoW + Math.round(sc), sy - torsoH + Math.round(2 * sc) + armH), - k.anchor('center'), - k.color(safeColor(k,skin)), k.opacity(op * 0.8), k.z(row.z - 0.1), - ]) - - // Signs (10% of front row spectators) - let sign: any = null - let signText: any = null - if (row.z === 6 && Math.random() < 0.15) { - const signW = Math.round(18 * sc) - const signH = Math.round(12 * sc) - const signColors = ['#ffffff', '#ffee44', '#ff4444', '#44ff44', '#ff88ff'] - const signC = signColors[Math.floor(Math.random() * signColors.length)] - sign = k.add([ - k.rect(signW, signH), k.pos(sx + torsoW / 2 - signW / 2, sy - torsoH - headR * 2 - signH - 2), - k.color(safeColor(k,signC)), k.opacity(op * 0.8), k.z(row.z + 0.2), - ]) - const texts = ['GO!', 'KO!', 'WIN', '!!!', '#1', 'LOL', 'GG', 'WOW', 'BOT'] - signText = k.add([ - k.text(texts[Math.floor(Math.random() * texts.length)], { size: Math.round(6 * sc) }), - k.pos(sx + torsoW / 2, sy - torsoH - headR * 2 - signH / 2 - 2), - k.anchor('center'), - k.color(safeColor(k,'#111111')), k.opacity(op * 0.7), k.z(row.z + 0.3), - ]) - } - - // Store base positions for animation - const tBaseY = torso.pos.y - const hBaseY = head.pos.y - const alBaseY = armL.pos.y - const arBaseY = armR.pos.y - const hlBaseY = handL.pos.y - const hrBaseY = handR.pos.y - const accBaseY = accessory?.pos.y - const signBaseY = sign?.pos.y - const stBaseY = signText?.pos.y - - // Randomize behavior: some bob gently, some pump fists, some wave - const behavior = Math.random() - const isWaver = behavior < 0.25 - const isPumper = behavior < 0.45 && !isWaver - - torso.onUpdate(() => { - const bob = Math.sin(k.time() * bobSpeed + bobPhase) * bobAmt - const pump = isPumper ? Math.max(0, Math.sin(k.time() * 3 + bobPhase)) * 2 * sc : 0 - torso.pos.y = tBaseY + bob - pump - head.pos.y = hBaseY + bob - pump - if (accessory) accessory.pos.y = accBaseY! + bob - pump - - // Arms: wavers raise one arm rhythmically, pumpers raise both - if (isWaver) { - const wave = Math.sin(k.time() * 2.5 + bobPhase) * armH * 0.7 - armR.pos.y = arBaseY + bob - Math.max(0, -wave) - handR.pos.y = hrBaseY + bob - Math.max(0, -wave) - armL.pos.y = alBaseY + bob - handL.pos.y = hlBaseY + bob - } else if (isPumper) { - armL.pos.y = alBaseY + bob - pump * 1.5 - armR.pos.y = arBaseY + bob - pump * 1.5 - handL.pos.y = hlBaseY + bob - pump * 1.5 - handR.pos.y = hrBaseY + bob - pump * 1.5 - } else { - armL.pos.y = alBaseY + bob - armR.pos.y = arBaseY + bob - handL.pos.y = hlBaseY + bob - handR.pos.y = hrBaseY + bob - } - - if (sign) { sign.pos.y = signBaseY! + bob - pump; signText.pos.y = stBaseY! + bob - pump } - }) - } - } } k.scene('fight', () => { @@ -1912,7 +1754,7 @@ export async function createFightScene(config: FightSceneConfig) { const gunX = atk.pos.x + dir * 15 const gunY = atk.pos.y - 40 const gunDef = PROPS.gun - const gunObjs = spawnProp('gun', gunX, gunY, dir < 0, 18) + const gunObjs = spawnWeaponProp('gun', gunX, gunY, dir < 0, 18) await k.wait(0.15) const bulletCount = isCritical ? 8 : 4 const muzzleX = gunX + dir * 30 @@ -2943,7 +2785,7 @@ export async function createFightScene(config: FightSceneConfig) { const gpx = atk.pos.x + dir * 15 const gpy = atk.pos.y - 38 const mgDef = PROPS.minigun - const mgObjs = spawnProp('minigun', gpx, gpy, dir < 0, 18) + const mgObjs = spawnWeaponProp('minigun', gpx, gpy, dir < 0, 18) await k.wait(0.1) sfxSpecial() const bulletCount = isCritical ? 16 : 10 @@ -3765,7 +3607,7 @@ export async function createFightScene(config: FightSceneConfig) { } // Spawn a multi-part pixel prop at position, returns array of game objects - function spawnProp(propName: string, x: number, y: number, flipX: boolean = false, zIndex: number = 16): any[] { + function spawnWeaponProp(propName: string, x: number, y: number, flipX: boolean = false, zIndex: number = 16): any[] { const def = PROPS[propName] if (!def) { // Fallback: single colored circle @@ -3849,7 +3691,7 @@ export async function createFightScene(config: FightSceneConfig) { if (!defender) return const dir = side === 'a' ? 1 : -1 // Spawn shield in front - const shieldObjs = spawnProp('shield', defender.pos.x + dir * 20, defender.pos.y - 25, side === 'b', 19) + const shieldObjs = spawnWeaponProp('shield', defender.pos.x + dir * 20, defender.pos.y - 25, side === 'b', 19) sfxBlock() // Flash shield shieldObjs.forEach(o => { o.opacity = 0.9 }) @@ -3910,7 +3752,7 @@ export async function createFightScene(config: FightSceneConfig) { for (let i = 0; i < total; i++) { const fx = atk.pos.x + dir * 25, fy = atk.pos.y - 35 const tx = def.pos.x + (Math.random() - 0.5) * 30, ty = def.pos.y - 25 + (Math.random() - 0.5) * 20 - const objs = spawnProp(propName, fx, fy, dir < 0) + const objs = spawnWeaponProp(propName, fx, fy, dir < 0) k.tween(0, 1, 0.2, (t) => { const cx = fx + (tx - fx) * t const cy = fy + (ty - fy) * t - Math.sin(t * Math.PI) * arc @@ -3936,7 +3778,7 @@ export async function createFightScene(config: FightSceneConfig) { atk.play('special'); sfxSpecial() // Spawn gun prop on attacker const gx = atk.pos.x + dir * 15, gy = atk.pos.y - 35 - const gunObjs = spawnProp(propName, gx, gy, dir < 0, 18) + const gunObjs = spawnWeaponProp(propName, gx, gy, dir < 0, 18) const gunDef = PROPS[propName] await k.wait(0.15) // Fire bullets @@ -5697,10 +5539,13 @@ export async function createFightScene(config: FightSceneConfig) { 'No hard feelings!', 'Honor to fight you!', 'GG.', ] + // Sanitize text for Kaplay (treats [ ] as styled text tags) + function safeText(t: string): string { return t.replace(/[\[\]]/g, '') } + // Spawn floating text above a position function spawnEmoteText(x: number, y: number, text: string, color: string, duration: number = 1.2) { const label = k.add([ - k.text(text, { size: 10 }), + k.text(safeText(text), { size: 10 }), k.pos(x, y - 10), k.color(safeColor(k,color)), k.opacity(1), @@ -5753,7 +5598,7 @@ export async function createFightScene(config: FightSceneConfig) { }) if (text) { const label = k.add([ - k.text(text, { size: 6 }), k.pos(sx, sy), + k.text(safeText(text), { size: 6 }), k.pos(sx, sy), k.color(safeColor(k,'#000000')), k.opacity(0.8), k.z(3), k.anchor('center'), ]) label.onUpdate(() => { label.pos.x = sign.pos.x; label.pos.y = sign.pos.y }) diff --git a/server/src/engine/mock.ts b/server/src/engine/mock.ts index f0d7db2..8f91d86 100644 --- a/server/src/engine/mock.ts +++ b/server/src/engine/mock.ts @@ -380,7 +380,7 @@ export async function runMockFight(botAId: string, botBId: string): Promise {