diff --git a/frontend/src/game/FightScene.ts b/frontend/src/game/FightScene.ts index dc9ad55..60cbdea 100644 --- a/frontend/src/game/FightScene.ts +++ b/frontend/src/game/FightScene.ts @@ -586,9 +586,44 @@ export async function createFightScene(config: FightSceneConfig) { k.tween(0.4, 0, duration, (v) => { flash.opacity = v }).then(() => flash.destroy()) } - // Grotesque close-up stubs (disabled — looked bad) - function spawnGrotesqueDetails(_fighter: any, _scaleFactor: number) {} - function destroyGrotesqueDetails() {} + // Grotesque close-up: spawn exaggerated impact details during zoom-ins + let grotesqueObjs: any[] = [] + function spawnGrotesqueDetails(fighter: any, scaleFactor: number) { + destroyGrotesqueDetails() + if (!fighter?.exists()) return + const fx = fighter.pos.x, fy = fighter.pos.y + const s = Math.max(1, scaleFactor) + // Impact lines radiating from fighter + for (let i = 0; i < 6; i++) { + const angle = (i / 6) * Math.PI * 2 + Math.random() * 0.3 + const len = 30 + Math.random() * 40 + const line = k.add([ + k.rect(3 * s, len * s), + k.pos(fx + Math.cos(angle) * 20, fy - 30 + Math.sin(angle) * 20), + k.color(safeColor(k, i % 2 === 0 ? '#ffffff' : '#ff2d2d')), + k.opacity(0.7), + k.z(25), + k.rotate(angle * (180 / Math.PI) + 90), + k.anchor('center'), + ]) + grotesqueObjs.push(line) + } + // Speed/impact dots + for (let i = 0; i < 4; i++) { + const dot = k.add([ + k.circle(4 + Math.random() * 6), + k.pos(fx + (Math.random() - 0.5) * 60, fy - 30 + (Math.random() - 0.5) * 50), + k.color(safeColor(k, '#ffe14d')), + k.opacity(0.8), + k.z(25), + ]) + grotesqueObjs.push(dot) + } + } + function destroyGrotesqueDetails() { + for (const o of grotesqueObjs) { if (o.exists()) o.destroy() } + grotesqueObjs = [] + } // Arena-specific background decoration function drawArenaDecor() { const a = arena @@ -6278,8 +6313,56 @@ export async function createFightScene(config: FightSceneConfig) { k.tween(0.8, 0, 1.5, (v) => { heart.opacity = v }).then(() => { if (heart.exists()) heart.destroy() }) } - // Crowd signs stub (disabled — too small to look good) - function spawnCrowdSigns(_count: number, _color: string, _text?: string) {} + // Crowd signs: pop up from bottom with text, bob, then fade out + function spawnCrowdSigns(count: number, color: string, text?: string) { + const labels = text ? [text] : ['GO!', 'WOW', 'KO!', 'LOL', 'GG', 'OMG'] + for (let i = 0; i < count; i++) { + const x = W * 0.15 + Math.random() * W * 0.7 + const startY = H + 20 + const endY = GROUND_Y + 10 + Math.random() * 20 + const label = labels[Math.floor(Math.random() * labels.length)] + // Sign stick + const stick = k.add([ + k.rect(3, 25), + k.pos(x, startY), + k.color(safeColor(k, '#886644')), + k.opacity(0.8), + k.z(8), + k.anchor('bot'), + ]) + // Sign face + const sign = k.add([ + k.rect(Math.max(30, label.length * 10), 18), + k.pos(x, startY - 25), + k.color(safeColor(k, color)), + k.opacity(0.85), + k.z(8), + k.anchor('center'), + ]) + // Sign text + const txt = k.add([ + k.text(label, { size: 10 }), + k.pos(x, startY - 25), + k.color(safeColor(k, '#ffffff')), + k.opacity(0.9), + k.z(9), + k.anchor('center'), + ]) + const delay = i * 0.08 + // Rise up + k.wait(delay, () => { + k.tween(startY, endY, 0.3, (v) => { stick.pos.y = v; sign.pos.y = v - 25; txt.pos.y = v - 25 }, k.easings.easeOutBack) + }) + // Bob + fade out after 1.5s + k.wait(delay + 1.5, () => { + k.tween(0.85, 0, 0.5, (v) => { stick.opacity = v; sign.opacity = v; txt.opacity = v }).then(() => { + if (stick.exists()) stick.destroy() + if (sign.exists()) sign.destroy() + if (txt.exists()) txt.destroy() + }) + }) + } + } // === HUMAN OWNER APPEARANCES === // The silly human behind each bot occasionally shows up