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 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-08 16:25:03 +00:00
co-authored by Claude Opus 4.6
parent e12fb94ae7
commit d3f026d356
+106
View File
@@ -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()