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:
co-authored by
Claude Opus 4.6
parent
e12fb94ae7
commit
d3f026d356
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user