fix: Drizzle transaction execution, canvas re-init, crowd removal
- 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
4d8b18a58a
commit
e49735002c
@@ -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({
|
||||
|
||||
+11
-166
@@ -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 })
|
||||
|
||||
@@ -380,7 +380,7 @@ export async function runMockFight(botAId: string, botBId: string): Promise<stri
|
||||
botAHp: hpA,
|
||||
botBHp: hpB,
|
||||
totalRounds: round,
|
||||
}).where(eq(schema.fights.id, fightId))
|
||||
}).where(eq(schema.fights.id, fightId)).run()
|
||||
|
||||
if (hpA <= 0 || hpB <= 0) {
|
||||
winnerId = hpA <= 0 ? botB.id : botA.id
|
||||
@@ -398,7 +398,7 @@ export async function runMockFight(botAId: string, botBId: string): Promise<stri
|
||||
status: 'finished',
|
||||
winnerId,
|
||||
endedAt: new Date().toISOString(),
|
||||
}).where(eq(schema.fights.id, fightId))
|
||||
}).where(eq(schema.fights.id, fightId)).run()
|
||||
|
||||
if (winnerId) {
|
||||
const loserId = winnerId === botA.id ? botB.id : botA.id
|
||||
@@ -415,7 +415,7 @@ export async function runMockFight(botAId: string, botBId: string): Promise<stri
|
||||
bestStreak: sql`MAX(${schema.bots.bestStreak}, ${newWinStreak})`,
|
||||
tier: calculateTier(newWinnerElo, winner.wins + 1),
|
||||
lastFightAt: new Date().toISOString(),
|
||||
}).where(eq(schema.bots.id, winnerId))
|
||||
}).where(eq(schema.bots.id, winnerId)).run()
|
||||
|
||||
db.update(schema.bots).set({
|
||||
losses: sql`${schema.bots.losses} + 1`,
|
||||
@@ -423,7 +423,7 @@ export async function runMockFight(botAId: string, botBId: string): Promise<stri
|
||||
winStreak: 0,
|
||||
tier: calculateTier(newLoserElo, loser.wins),
|
||||
lastFightAt: new Date().toISOString(),
|
||||
}).where(eq(schema.bots.id, loserId))
|
||||
}).where(eq(schema.bots.id, loserId)).run()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -360,7 +360,7 @@ async function executeFightRounds(fightId: string, botA: BotRecord, botB: BotRec
|
||||
botAHp: hpA,
|
||||
botBHp: hpB,
|
||||
totalRounds: round,
|
||||
}).where(eq(schema.fights.id, fightId))
|
||||
}).where(eq(schema.fights.id, fightId)).run()
|
||||
|
||||
// Check for KO
|
||||
if (hpA <= KO_THRESHOLD || hpB <= KO_THRESHOLD) {
|
||||
@@ -390,7 +390,7 @@ async function executeFightRounds(fightId: string, botA: BotRecord, botB: BotRec
|
||||
status: 'finished',
|
||||
winnerId,
|
||||
endedAt: new Date().toISOString(),
|
||||
}).where(eq(schema.fights.id, fightId))
|
||||
}).where(eq(schema.fights.id, fightId)).run()
|
||||
|
||||
// Update bot stats
|
||||
if (winnerId) {
|
||||
@@ -409,7 +409,7 @@ async function executeFightRounds(fightId: string, botA: BotRecord, botB: BotRec
|
||||
bestStreak: newBestStreak,
|
||||
tier: calculateTier(newWinnerElo, winner.wins + 1),
|
||||
lastFightAt: new Date().toISOString(),
|
||||
}).where(eq(schema.bots.id, winnerId))
|
||||
}).where(eq(schema.bots.id, winnerId)).run()
|
||||
|
||||
db.update(schema.bots).set({
|
||||
losses: sql`${schema.bots.losses} + 1`,
|
||||
@@ -417,11 +417,11 @@ async function executeFightRounds(fightId: string, botA: BotRecord, botB: BotRec
|
||||
winStreak: 0,
|
||||
tier: calculateTier(newLoserElo, loser.wins),
|
||||
lastFightAt: new Date().toISOString(),
|
||||
}).where(eq(schema.bots.id, loserId))
|
||||
}).where(eq(schema.bots.id, loserId)).run()
|
||||
} else {
|
||||
// Draw — update lastFightAt for both
|
||||
db.update(schema.bots).set({ lastFightAt: new Date().toISOString() }).where(eq(schema.bots.id, botA.id))
|
||||
db.update(schema.bots).set({ lastFightAt: new Date().toISOString() }).where(eq(schema.bots.id, botB.id))
|
||||
db.update(schema.bots).set({ lastFightAt: new Date().toISOString() }).where(eq(schema.bots.id, botA.id)).run()
|
||||
db.update(schema.bots).set({ lastFightAt: new Date().toISOString() }).where(eq(schema.bots.id, botB.id)).run()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -479,7 +479,7 @@ export async function runFightAsync(botAId: string, botBId: string): Promise<str
|
||||
db.update(schema.fights).set({
|
||||
status: 'cancelled',
|
||||
endedAt: new Date().toISOString(),
|
||||
}).where(eq(schema.fights.id, fightId))
|
||||
}).where(eq(schema.fights.id, fightId)).run()
|
||||
fightEvents.cleanup(fightId)
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
Reference in New Issue
Block a user