From d3f026d356a80db92af669c026c7cda75363c11a Mon Sep 17 00:00:00 2001 From: Dorian Date: Sun, 8 Mar 2026 16:25:03 +0000 Subject: [PATCH] feat: bullet time attack choreography for fight scenes Adds cinematic bullet-time combat sequence with slow-mo golden projectiles, afterimage dodge trails, and devastating counter-rush with multi-hit impact. Co-Authored-By: Claude Opus 4.6 --- frontend/src/game/FightScene.ts | 106 ++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/frontend/src/game/FightScene.ts b/frontend/src/game/FightScene.ts index 228c53b..85f5dc8 100644 --- a/frontend/src/game/FightScene.ts +++ b/frontend/src/game/FightScene.ts @@ -2902,6 +2902,112 @@ export async function createFightScene(config: FightSceneConfig) { ]) } + // BULLET TIME: slow-mo dodge, afterimages trail behind, golden traces, devastating counter + async function bulletTimeAttack(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { + // Phase 1: Darken screen — entering bullet time + const dim = k.add([k.rect(W, H), k.pos(0, 0), k.color(safeColor(k, '#000011')), k.opacity(0), k.z(25)]) + await k.tween(0, 0.45, 0.15, (v) => { dim.opacity = v }) + scanlineGlitch(0.1) + + // Phase 2: Attacker fires slow golden projectiles (bullets visible in slow-mo) + atk.play('special') + sfxGunshot() + const bulletCount = isCritical ? 5 : 3 + const bullets: any[] = [] + for (let i = 0; i < bulletCount; i++) { + const bx = atk.pos.x + dir * 25 + const by = atk.pos.y - 30 + (i - 1) * 12 + const bullet = k.add([ + k.rect(isCritical ? 14 : 10, 3), + k.pos(bx, by), + k.color(safeColor(k, '#ffd700')), + k.opacity(0.9), + k.z(26), + k.anchor('center'), + k.rotate(dir === 1 ? 0 : 180), + ]) + bullets.push(bullet) + // Golden trace trail behind each bullet + const trace = k.add([ + k.rect(40, 1), + k.pos(bx - dir * 20, by), + k.color(safeColor(k, '#ffaa00')), + k.opacity(0.5), + k.z(25), + ]) + k.tween(trace.opacity, 0, 0.4, (v) => { if (trace.exists()) trace.opacity = v }).then(() => { if (trace.exists()) trace.destroy() }) + await k.wait(0.08) // Slow-mo pacing between shots + } + + // Phase 3: Bullets travel slowly toward defender + await Promise.all(bullets.map((b, i) => + k.tween(b.pos.x, def.pos.x + (Math.random() - 0.5) * 15, 0.25 + i * 0.03, (v) => { b.pos.x = v }, k.easings.easeInQuad) + )) + bullets.forEach(b => { if (b.exists()) b.destroy() }) + + // Phase 4: Attacker dodges (lean back in slow-mo with afterimages) + const leanBack = dir * -30 + for (let i = 0; i < 4; i++) { + const ghost = k.add([ + k.rect(FRAME_SIZE * Math.abs(atk.scale.x) * 0.8, FRAME_SIZE * Math.abs(atk.scale.y)), + k.pos(atk.pos.x + i * leanBack * 0.15, atk.pos.y), + k.color(safeColor(k, '#ffd700')), + k.opacity(0.25 - i * 0.05), + k.z(24), + k.anchor('bot'), + ]) + setTimeout(() => { if (ghost.exists()) ghost.destroy() }, 400) + } + await k.tween(atk.pos.x, atk.pos.x + leanBack, 0.2, (v) => { atk.pos.x = v }, k.easings.easeOutQuad) + + // Phase 5: Time resumes — attacker dashes in for devastating counter + dim.destroy() + sfxZoomWhoosh() + const contactX = origDX - dir * 40 + // Afterimage trail during rush + const rushStart = atk.pos.x + for (let i = 0; i < 5; i++) { + const ghost = k.add([ + k.rect(FRAME_SIZE * Math.abs(atk.scale.x) * 0.7, FRAME_SIZE * Math.abs(atk.scale.y) * 0.9), + k.pos(rushStart + ((contactX - rushStart) * i / 5), atk.pos.y), + k.color(safeColor(k, theme.accent)), + k.opacity(0.3 - i * 0.05), + k.z(9), + k.anchor('bot'), + ]) + setTimeout(() => { if (ghost.exists()) ghost.destroy() }, 250) + } + await k.tween(atk.pos.x, contactX, 0.08, (v) => { atk.pos.x = v }, k.easings.easeInQuad) + atk.play('attack') + + // Phase 6: Impact + const hits = isCritical ? 4 : 2 + for (let i = 0; i < hits; i++) { + sfxPunch() + def.play('hit') + k.shake(6 + i * 3) + spawnSparks(def.pos.x + (Math.random() - 0.5) * 15, def.pos.y - 20 - Math.random() * 20, 6, '#ffd700') + if (i < hits - 1) await k.wait(0.06) + } + // Final devastating blow + sfxCritical() + if (isCritical) { sfxExplosion(); screenFlash('#ffd700', 0.2) } + def.play(isCritical ? 'knockback' : 'hit') + k.shake(isCritical ? 22 : 12) + spawnSparks(def.pos.x, def.pos.y - 25, isCritical ? 20 : 12, '#ffd700') + if (isCritical) spawnShockwave(def.pos.x, GROUND_Y, '#ffd700') + + // Return to positions + const push = dir * (isCritical ? 70 : 35) + k.tween(def.pos.x, origDX + push, 0.25, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad) + await k.wait(0.3) + await Promise.all([ + k.tween(atk.pos.x, origAX, 0.2, (v) => { atk.pos.x = v }, k.easings.easeInQuad), + k.tween(def.pos.x, origDX, 0.3, (v) => { def.pos.x = v }, k.easings.easeInOutQuad), + ]) + atk.play('idle') + } + // INVISIBLE SPEED: so fast you can barely see — just blur lines and impact async function lightningRush(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) { sfxSpecial()