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
+22
-179
@@ -2,9 +2,12 @@ import kaplay from 'kaplay'
|
|||||||
import type { GameObj, SpriteComp, PosComp, ScaleComp, AnchorComp, OpacityComp, ColorComp, RotateComp, ZComp, LoadSpriteOpt } from 'kaplay'
|
import type { GameObj, SpriteComp, PosComp, ScaleComp, AnchorComp, OpacityComp, ColorComp, RotateComp, ZComp, LoadSpriteOpt } from 'kaplay'
|
||||||
|
|
||||||
import { generateSpriteSheet, generateJudgeSpriteSheet, generateHumanSpriteSheet, generateHumanFighterSpriteSheet, getBotColors, FRAME_SIZE, MAX_FRAMES, TOTAL_ROWS, ANIMATIONS, JUDGE_ANIMATIONS, JUDGE_MAX_FRAMES, JUDGE_ROWS, HUMAN_ANIMATIONS, HUMAN_MAX_FRAMES, HUMAN_ROWS, type SpriteCustomization } from './sprites'
|
import { generateSpriteSheet, generateJudgeSpriteSheet, generateHumanSpriteSheet, generateHumanFighterSpriteSheet, getBotColors, FRAME_SIZE, MAX_FRAMES, TOTAL_ROWS, ANIMATIONS, JUDGE_ANIMATIONS, JUDGE_MAX_FRAMES, JUDGE_ROWS, HUMAN_ANIMATIONS, HUMAN_MAX_FRAMES, HUMAN_ROWS, type SpriteCustomization } from './sprites'
|
||||||
import type { Fighter, FightSceneConfig, RoundEvent } from './fight/types'
|
import type { Fighter, FightSceneConfig, RoundEvent, FightContext } from './fight/types'
|
||||||
export type { FightSceneConfig, RoundEvent } from './fight/types'
|
export type { FightSceneConfig, RoundEvent } from './fight/types'
|
||||||
import { ARENA_THEMES, spriteAnims, CHALLENGE_THEMED, TIER_ULTIMATES, CREATOR_MOVES, CREATOR_ULTIMATES } from './fight/constants'
|
import { ARENA_THEMES, spriteAnims, CHALLENGE_THEMED, TIER_ULTIMATES, CREATOR_MOVES, CREATOR_ULTIMATES } from './fight/constants'
|
||||||
|
import { spawnSparks as _spawnSparks, spawnBulletHoles as _spawnBulletHoles, spawnExhaust as _spawnExhaust } from './fight/particles'
|
||||||
|
import { glitchRGB as _glitchRGB, scanlineGlitch as _scanlineGlitch, vhsTracking as _vhsTracking, dimensionalShift as _dimensionalShift } from './fight/effects'
|
||||||
|
import { spawnProjectile as _spawnProjectile } from './fight/projectiles'
|
||||||
import {
|
import {
|
||||||
sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxGunshot, sfxBulletHit,
|
sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxGunshot, sfxBulletHit,
|
||||||
sfxJetpack, sfxExplosion, sfxKO, sfxWin, sfxWinAnnounce, sfxPerfect,
|
sfxJetpack, sfxExplosion, sfxKO, sfxWin, sfxWinAnnounce, sfxPerfect,
|
||||||
@@ -274,120 +277,32 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
cleanupTimers.delete(id)
|
cleanupTimers.delete(id)
|
||||||
}
|
}
|
||||||
// Scale factor so sprites shrink on small canvases (reference: 500px tall)
|
// Scale factor so sprites shrink on small canvases (reference: 500px tall)
|
||||||
|
|
||||||
|
// Fight context for extracted modules
|
||||||
|
const fightCtx: FightContext = {
|
||||||
|
k, W, H, theme,
|
||||||
|
trackedTimeout, trackedInterval, clearTracked,
|
||||||
|
safeColor: (color: string) => safeColor(k, color),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrappers for extracted particle/effect/projectile functions
|
||||||
|
const spawnSparks = (x: number, y: number, count: number, color: string) => _spawnSparks(fightCtx, x, y, count, color)
|
||||||
|
const spawnBulletHoles = (x: number, y: number, count: number) => _spawnBulletHoles(fightCtx, x, y, count)
|
||||||
|
const spawnExhaust = (x: number, y: number, duration: number) => _spawnExhaust(fightCtx, x, y, duration)
|
||||||
|
const spawnProjectile = (fromX: number, fromY: number, toX: number, toY: number, color: string, size: number = 8) => _spawnProjectile(fightCtx, fromX, fromY, toX, toY, color, size)
|
||||||
|
const glitchRGB = (duration: number = 0.2) => _glitchRGB(fightCtx, duration)
|
||||||
|
const scanlineGlitch = (duration: number = 0.3) => _scanlineGlitch(fightCtx, duration)
|
||||||
|
const vhsTracking = (duration: number = 0.4) => _vhsTracking(fightCtx, duration)
|
||||||
|
const dimensionalShift = (duration: number = 0.6) => _dimensionalShift(fightCtx, duration)
|
||||||
|
|
||||||
const SF = Math.min(1, H / 500)
|
const SF = Math.min(1, H / 500)
|
||||||
const GROUND_Y = H * 0.78
|
const GROUND_Y = H * 0.78
|
||||||
const HOME_A = W * 0.28
|
const HOME_A = W * 0.28
|
||||||
const HOME_B = W * 0.72
|
const HOME_B = W * 0.72
|
||||||
|
|
||||||
// Particle helpers
|
|
||||||
function spawnSparks(x: number, y: number, count: number, color: string) {
|
|
||||||
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(k,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() // gravity
|
|
||||||
spark.angle += (spark as any).rotV * k.dt()
|
|
||||||
spark.opacity -= 1.5 * k.dt()
|
|
||||||
if (spark.opacity <= 0) spark.destroy()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function spawnBulletHoles(x: number, y: number, count: number) {
|
|
||||||
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(k,'#000000')),
|
|
||||||
k.opacity(0.8),
|
|
||||||
k.z(9), // behind fighters
|
|
||||||
])
|
|
||||||
// Fade out after a bit
|
|
||||||
trackedTimeout(() => {
|
|
||||||
const fadeInterval = trackedInterval(() => {
|
|
||||||
hole.opacity -= 0.02
|
|
||||||
if (hole.opacity <= 0) { hole.destroy(); clearTracked(fadeInterval) }
|
|
||||||
}, 50)
|
|
||||||
}, 1500)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function spawnExhaust(x: number, y: number, duration: number) {
|
|
||||||
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(k,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
|
|
||||||
}
|
|
||||||
|
|
||||||
function spawnProjectile(fromX: number, fromY: number, toX: number, toY: number, color: string, size: number = 8): Promise<void> {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
const proj = k.add([
|
|
||||||
k.circle(size),
|
|
||||||
k.pos(fromX, fromY),
|
|
||||||
k.color(safeColor(k,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(k,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(toX, toY, 8, color)
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function spawnBullet(fromX: number, fromY: number, toX: number, toY: number): Promise<void> {
|
function spawnBullet(fromX: number, fromY: number, toX: number, toY: number): Promise<void> {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
@@ -427,81 +342,9 @@ export async function createFightScene(config: FightSceneConfig) {
|
|||||||
|
|
||||||
// === VISUAL CHAOS EFFECTS ===
|
// === VISUAL CHAOS EFFECTS ===
|
||||||
|
|
||||||
// RGB split glitch — offsets red/blue channels using colored overlays
|
|
||||||
function glitchRGB(duration: number = 0.2) {
|
|
||||||
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(k,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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scanline glitch — horizontal bands flicker
|
|
||||||
function scanlineGlitch(duration: number = 0.3) {
|
|
||||||
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(k,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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// VHS tracking distortion — horizontal offset bands
|
|
||||||
function vhsTracking(duration: number = 0.4) {
|
|
||||||
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(k,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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dimensional shift — background color/hue flash cycle
|
|
||||||
function dimensionalShift(duration: number = 0.6) {
|
|
||||||
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(k,colors[0])), k.opacity(0.08), k.z(1),
|
|
||||||
])
|
|
||||||
const interval = trackedInterval(() => {
|
|
||||||
idx = (idx + 1) % colors.length
|
|
||||||
overlay.color = safeColor(k,colors[idx])
|
|
||||||
overlay.opacity = 0.05 + Math.random() * 0.06
|
|
||||||
}, 80)
|
|
||||||
trackedTimeout(() => { clearTracked(interval); if (overlay.exists()) overlay.destroy() }, duration * 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schizo cut — rapid zoom/position jitter simulating jump cuts
|
// Schizo cut — rapid zoom/position jitter simulating jump cuts
|
||||||
async function schizoCut() {
|
async function schizoCut() {
|
||||||
|
|||||||
@@ -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 { GameObj, SpriteComp, PosComp, ScaleComp, AnchorComp, OpacityComp, ColorComp, RotateComp, ZComp } from 'kaplay'
|
||||||
import type { SpriteCustomization } from '../sprites'
|
import type { SpriteCustomization } from '../sprites'
|
||||||
|
|
||||||
export type Fighter = GameObj<SpriteComp | PosComp | ScaleComp | AnchorComp | OpacityComp | ColorComp | RotateComp | ZComp>
|
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 {
|
export interface FightSceneConfig {
|
||||||
canvas: HTMLCanvasElement
|
canvas: HTMLCanvasElement
|
||||||
botA: { name: string; seed: string; tier: number; archetype?: string; customization?: SpriteCustomization; wins?: number; losses?: number }
|
botA: { name: string; seed: string; tier: number; archetype?: string; customization?: SpriteCustomization; wins?: number; losses?: number }
|
||||||
|
|||||||
Reference in New Issue
Block a user