refactor: extract particles, effects, projectiles from FightScene
Move spawnSparks, spawnBulletHoles, spawnExhaust to fight/particles.ts, glitchRGB, scanlineGlitch, vhsTracking, dimensionalShift to fight/effects.ts, and spawnProjectile to fight/projectiles.ts. All accept FightContext as first param. FightScene.ts reduced by ~180 lines. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
020ab3f1f8
commit
08786a9b06
@@ -0,0 +1,78 @@
|
||||
import type { GameObj } from 'kaplay'
|
||||
import type { FightContext } from './types'
|
||||
|
||||
export function glitchRGB(ctx: FightContext, duration: number = 0.2): void {
|
||||
const { k, W, H, safeColor } = ctx
|
||||
const layers = [
|
||||
{ color: '#ff0000', dx: 4 + Math.random() * 6, dy: -2 },
|
||||
{ color: '#0000ff', dx: -(4 + Math.random() * 6), dy: 2 },
|
||||
]
|
||||
for (const l of layers) {
|
||||
const overlay = k.add([
|
||||
k.rect(W, H), k.pos(l.dx, l.dy),
|
||||
k.color(safeColor(l.color)), k.opacity(0.12), k.z(55),
|
||||
])
|
||||
overlay.onUpdate(() => {
|
||||
overlay.pos.x = l.dx + (Math.random() - 0.5) * 4
|
||||
overlay.pos.y = l.dy + (Math.random() - 0.5) * 2
|
||||
})
|
||||
setTimeout(() => { if (overlay.exists()) overlay.destroy() }, duration * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
export function scanlineGlitch(ctx: FightContext, duration: number = 0.3): void {
|
||||
const { k, W, H, safeColor } = ctx
|
||||
const lines: GameObj[] = []
|
||||
const count = 6 + Math.floor(Math.random() * 8)
|
||||
for (let i = 0; i < count; i++) {
|
||||
const ly = Math.random() * H
|
||||
const lh = 1 + Math.random() * 3
|
||||
const line = k.add([
|
||||
k.rect(W, lh), k.pos(0, ly),
|
||||
k.color(safeColor(Math.random() > 0.5 ? '#ffffff' : '#000000')),
|
||||
k.opacity(0.15 + Math.random() * 0.2), k.z(54),
|
||||
])
|
||||
line.onUpdate(() => {
|
||||
line.pos.x = (Math.random() - 0.5) * 8
|
||||
line.opacity = Math.random() > 0.3 ? 0.15 : 0
|
||||
})
|
||||
lines.push(line)
|
||||
}
|
||||
setTimeout(() => { lines.forEach(l => { if (l.exists()) l.destroy() }) }, duration * 1000)
|
||||
}
|
||||
|
||||
export function vhsTracking(ctx: FightContext, duration: number = 0.4): void {
|
||||
const { k, W, H, safeColor, theme } = ctx
|
||||
const bands: GameObj[] = []
|
||||
const bandCount = 3
|
||||
for (let i = 0; i < bandCount; i++) {
|
||||
const by = Math.random() * H
|
||||
const bh = 10 + Math.random() * 30
|
||||
const band = k.add([
|
||||
k.rect(W, bh), k.pos(0, by),
|
||||
k.color(safeColor(theme.accent)), k.opacity(0.06), k.z(53),
|
||||
])
|
||||
band.onUpdate(() => {
|
||||
band.pos.x = Math.sin(k.time() * 20 + i * 3) * 15
|
||||
band.pos.y += (Math.random() - 0.5) * 2
|
||||
})
|
||||
bands.push(band)
|
||||
}
|
||||
setTimeout(() => { bands.forEach(b => { if (b.exists()) b.destroy() }) }, duration * 1000)
|
||||
}
|
||||
|
||||
export function dimensionalShift(ctx: FightContext, duration: number = 0.6): void {
|
||||
const { k, W, H, safeColor, trackedInterval, trackedTimeout, clearTracked } = ctx
|
||||
const colors = ['#ff00ff', '#00ffff', '#ff0000', '#0000ff', '#ffff00']
|
||||
let idx = 0
|
||||
const overlay = k.add([
|
||||
k.rect(W, H), k.pos(0, 0),
|
||||
k.color(safeColor(colors[0])), k.opacity(0.08), k.z(1),
|
||||
])
|
||||
const interval = trackedInterval(() => {
|
||||
idx = (idx + 1) % colors.length
|
||||
overlay.color = safeColor(colors[idx])
|
||||
overlay.opacity = 0.05 + Math.random() * 0.06
|
||||
}, 80)
|
||||
trackedTimeout(() => { clearTracked(interval); if (overlay.exists()) overlay.destroy() }, duration * 1000)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { FightContext } from './types'
|
||||
|
||||
export function spawnSparks(ctx: FightContext, x: number, y: number, count: number, color: string): void {
|
||||
const { k, safeColor } = ctx
|
||||
const intense = count > 8
|
||||
for (let i = 0; i < count; i++) {
|
||||
const angle = Math.random() * Math.PI * 2
|
||||
const speed = (intense ? 150 : 100) + Math.random() * (intense ? 400 : 300)
|
||||
const size = (intense ? 3 : 2) + Math.random() * (intense ? 6 : 4)
|
||||
const rotSpeed = (Math.random() - 0.5) * 600
|
||||
const spark = k.add([
|
||||
k.rect(size, size * (0.5 + Math.random() * 0.5)),
|
||||
k.pos(x, y),
|
||||
k.color(safeColor(color)),
|
||||
k.opacity(1),
|
||||
k.z(20),
|
||||
k.rotate(Math.random() * 360),
|
||||
k.anchor('center'),
|
||||
{ vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed - 100, rotV: rotSpeed },
|
||||
])
|
||||
spark.onUpdate(() => {
|
||||
spark.pos.x += (spark as any).vx * k.dt()
|
||||
spark.pos.y += (spark as any).vy * k.dt()
|
||||
;(spark as any).vy += 500 * k.dt()
|
||||
spark.angle += (spark as any).rotV * k.dt()
|
||||
spark.opacity -= 1.5 * k.dt()
|
||||
if (spark.opacity <= 0) spark.destroy()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function spawnBulletHoles(ctx: FightContext, x: number, y: number, count: number): void {
|
||||
const { k, safeColor, trackedTimeout, trackedInterval, clearTracked } = ctx
|
||||
for (let i = 0; i < count; i++) {
|
||||
const hole = k.add([
|
||||
k.circle(3 + Math.random() * 3),
|
||||
k.pos(x + (Math.random() - 0.5) * 40, y + (Math.random() - 0.5) * 60),
|
||||
k.color(safeColor('#000000')),
|
||||
k.opacity(0.8),
|
||||
k.z(9),
|
||||
])
|
||||
trackedTimeout(() => {
|
||||
const fadeInterval = trackedInterval(() => {
|
||||
hole.opacity -= 0.02
|
||||
if (hole.opacity <= 0) { hole.destroy(); clearTracked(fadeInterval) }
|
||||
}, 50)
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
|
||||
export function spawnExhaust(ctx: FightContext, x: number, y: number, duration: number): ReturnType<typeof setInterval> {
|
||||
const { k, safeColor, trackedInterval, clearTracked } = ctx
|
||||
const endTime = performance.now() + duration * 1000
|
||||
const interval = trackedInterval(() => {
|
||||
if (performance.now() > endTime) { clearTracked(interval); return }
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const p = k.add([
|
||||
k.circle(3 + Math.random() * 5),
|
||||
k.pos(x + (Math.random() - 0.5) * 15, y),
|
||||
k.color(safeColor(Math.random() > 0.5 ? '#ff6600' : '#ffcc00')),
|
||||
k.opacity(0.8),
|
||||
k.z(8),
|
||||
])
|
||||
p.onUpdate(() => {
|
||||
p.pos.y += 100 * k.dt()
|
||||
p.pos.x += (Math.random() - 0.5) * 50 * k.dt()
|
||||
p.opacity -= 2.5 * k.dt()
|
||||
if (p.opacity <= 0) p.destroy()
|
||||
})
|
||||
}
|
||||
}, 40)
|
||||
return interval
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { FightContext } from './types'
|
||||
import { spawnSparks } from './particles'
|
||||
|
||||
export function spawnProjectile(ctx: FightContext, fromX: number, fromY: number, toX: number, toY: number, color: string, size: number = 8): Promise<void> {
|
||||
const { k, safeColor, trackedInterval, clearTracked } = ctx
|
||||
return new Promise(resolve => {
|
||||
const proj = k.add([
|
||||
k.circle(size),
|
||||
k.pos(fromX, fromY),
|
||||
k.color(safeColor(color)),
|
||||
k.opacity(1),
|
||||
k.z(15),
|
||||
])
|
||||
// Trail
|
||||
const trail = trackedInterval(() => {
|
||||
const t = k.add([
|
||||
k.circle(size * 0.6),
|
||||
k.pos(proj.pos.x, proj.pos.y),
|
||||
k.color(safeColor(color)),
|
||||
k.opacity(0.5),
|
||||
k.z(14),
|
||||
])
|
||||
t.onUpdate(() => { t.opacity -= 3 * k.dt(); if (t.opacity <= 0) t.destroy() })
|
||||
}, 30)
|
||||
|
||||
const dx = toX - fromX
|
||||
const dy = toY - fromY
|
||||
const dist = Math.sqrt(dx * dx + dy * dy)
|
||||
const speed = 800
|
||||
const dur = dist / speed
|
||||
|
||||
k.tween(0, 1, dur, (t) => {
|
||||
proj.pos.x = fromX + dx * t
|
||||
proj.pos.y = fromY + dy * t
|
||||
}, k.easings.linear).then(() => {
|
||||
clearTracked(trail)
|
||||
proj.destroy()
|
||||
spawnSparks(ctx, toX, toY, 8, color)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,8 +1,22 @@
|
||||
import type kaplay from 'kaplay'
|
||||
import type { GameObj, SpriteComp, PosComp, ScaleComp, AnchorComp, OpacityComp, ColorComp, RotateComp, ZComp } from 'kaplay'
|
||||
import type { SpriteCustomization } from '../sprites'
|
||||
|
||||
export type Fighter = GameObj<SpriteComp | PosComp | ScaleComp | AnchorComp | OpacityComp | ColorComp | RotateComp | ZComp>
|
||||
|
||||
export type KaplayInstance = ReturnType<typeof kaplay>
|
||||
|
||||
export interface FightContext {
|
||||
k: KaplayInstance
|
||||
W: number
|
||||
H: number
|
||||
theme: { bg: string; ground: string; accent: string }
|
||||
trackedTimeout: (fn: () => void, ms: number) => ReturnType<typeof setTimeout>
|
||||
trackedInterval: (fn: () => void, ms: number) => ReturnType<typeof setInterval>
|
||||
clearTracked: (id: ReturnType<typeof setTimeout>) => void
|
||||
safeColor: (color: string) => ReturnType<KaplayInstance['Color']['fromHex']>
|
||||
}
|
||||
|
||||
export interface FightSceneConfig {
|
||||
canvas: HTMLCanvasElement
|
||||
botA: { name: string; seed: string; tier: number; archetype?: string; customization?: SpriteCustomization; wins?: number; losses?: number }
|
||||
|
||||
Reference in New Issue
Block a user