refactor: extract morph, entrance, round, and finisher systems from FightScene
Split FightScene.ts from 6514 to 3468 lines by extracting four major subsystems into dedicated modules with factory functions and ChoreoContext. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
cabe0adb31
commit
1731a64ae8
@@ -0,0 +1,555 @@
|
||||
import type { Fighter, ChoreoContext } from './types'
|
||||
import { sfxRandomComedy, sfxBoneCrack, sfxVineBoom, sfxSlideWhistleDown, sfxPowerUp, sfxDrumRoll, announceDramatic, announceDeepIntro, announceSilly, announceRandom, announceCool, announceCrowdReaction, announceCreatorEntrance } from '../audio'
|
||||
|
||||
interface EntranceDeps {
|
||||
HOME_A: number
|
||||
HOME_B: number
|
||||
botA: { name: string; seed: string; tier: number; archetype?: string }
|
||||
botB: { name: string; seed: string; tier: number; archetype?: string }
|
||||
}
|
||||
|
||||
export function createEntranceSystem(ctx: ChoreoContext, deps: EntranceDeps) {
|
||||
const {
|
||||
k, W, H, GROUND_Y, FRAME_SIZE, theme, safeColor,
|
||||
trackedInterval, clearTracked,
|
||||
spawnSparks, spawnBulletHoles, spawnExhaust, spawnProjectile,
|
||||
spawnShockwave, screenFlash, impactFreeze, spawnAfterimages,
|
||||
glitchRGB, scanlineGlitch, dimensionalShift,
|
||||
sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxExplosion,
|
||||
sfxBoing, sfxBonk, sfxBulletHit, sfxClash, sfxCoin,
|
||||
sfxGunshot, sfxJetpack, sfxRapidPunch, sfxSlideDown,
|
||||
sfxZap, sfxZoomWhoosh, sfxBlock,
|
||||
} = ctx
|
||||
const { HOME_A, HOME_B, botA, botB } = deps
|
||||
|
||||
async function playEntrance() {
|
||||
const fA = k.get('fighterA')[0] as Fighter
|
||||
const fB = k.get('fighterB')[0] as Fighter
|
||||
if (!fA || !fB) return
|
||||
|
||||
const entrances = [
|
||||
// 0: Drive in with a car
|
||||
async (fighter: any, homeX: number, fromLeft: boolean) => {
|
||||
const dir = fromLeft ? 1 : -1
|
||||
const startX = fromLeft ? -120 : W + 120
|
||||
const carY = GROUND_Y - 20
|
||||
const carBody = k.add([k.rect(80, 30), k.pos(startX, carY), k.anchor('center'), k.color(safeColor(['#ff2d2d', '#2d7bff', '#ffcc00', '#39ff14', '#ff6600'][Math.floor(Math.random() * 5)])), k.z(9), k.opacity(1)])
|
||||
const wheel1 = k.add([k.circle(8), k.pos(startX - 25 * dir, carY + 15), k.anchor('center'), k.color(safeColor('#222222')), k.z(9)])
|
||||
const wheel2 = k.add([k.circle(8), k.pos(startX + 25 * dir, carY + 15), k.anchor('center'), k.color(safeColor('#222222')), k.z(9)])
|
||||
fighter.pos.x = startX
|
||||
fighter.pos.y = carY - 30
|
||||
fighter.opacity = 1
|
||||
sfxZoomWhoosh()
|
||||
await k.tween(startX, homeX, 0.5, (v) => {
|
||||
carBody.pos.x = v; wheel1.pos.x = v - 25 * dir; wheel2.pos.x = v + 25 * dir; fighter.pos.x = v
|
||||
}, k.easings.easeOutQuad)
|
||||
sfxBlock()
|
||||
k.shake(6)
|
||||
// Jump out
|
||||
await k.tween(fighter.pos.y, GROUND_Y - 80, 0.15, (v) => { fighter.pos.y = v }, k.easings.easeOutQuad)
|
||||
await k.tween(fighter.pos.y, GROUND_Y - 6, 0.15, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
|
||||
sfxBonk()
|
||||
// Drive car away
|
||||
k.tween(carBody.pos.x, fromLeft ? W + 150 : -150, 0.4, (v) => {
|
||||
carBody.pos.x = v; wheel1.pos.x = v - 25 * dir; wheel2.pos.x = v + 25 * dir
|
||||
}, k.easings.easeInQuad).then(() => { carBody.destroy(); wheel1.destroy(); wheel2.destroy() })
|
||||
},
|
||||
// 1: Fall from space
|
||||
async (fighter: any, homeX: number, _fromLeft: boolean) => {
|
||||
fighter.pos.x = homeX
|
||||
fighter.pos.y = -200
|
||||
fighter.opacity = 1
|
||||
sfxZoomWhoosh()
|
||||
// Trail of fire
|
||||
const trail: any[] = []
|
||||
for (let i = 0; i < 6; i++) {
|
||||
setTimeout(() => {
|
||||
const t = k.add([k.circle(4 + Math.random() * 6), k.pos(homeX + (Math.random() - 0.5) * 20, fighter.pos.y + 20), k.color(safeColor(i < 3 ? '#ff6600' : '#ffcc00')), k.opacity(0.7), k.z(9)])
|
||||
trail.push(t)
|
||||
k.tween(t.opacity, 0, 0.4, (v) => { t.opacity = v }).then(() => { if (t.exists()) t.destroy() })
|
||||
}, i * 40)
|
||||
}
|
||||
await k.tween(fighter.pos.y, GROUND_Y - 6, 0.4, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
|
||||
sfxExplosion()
|
||||
k.shake(15)
|
||||
screenFlash('#ff6600', 0.15)
|
||||
spawnShockwave(homeX, GROUND_Y, '#ff6600')
|
||||
spawnSparks(homeX, GROUND_Y - 10, 15, '#ffcc00')
|
||||
trail.forEach(t => { if (t.exists()) t.destroy() })
|
||||
},
|
||||
// 2: Robe entrance (boxing style)
|
||||
async (fighter: any, homeX: number, fromLeft: boolean) => {
|
||||
const startX = fromLeft ? -60 : W + 60
|
||||
fighter.pos.x = startX
|
||||
fighter.pos.y = GROUND_Y - 6
|
||||
fighter.opacity = 1
|
||||
// Robe overlay
|
||||
const robeColor = ['#8b0000', '#00008b', '#006400', '#4b0082', '#8b4513'][Math.floor(Math.random() * 5)]
|
||||
const robe = k.add([k.rect(50, 55), k.pos(startX, GROUND_Y - 30), k.anchor('center'), k.color(safeColor(robeColor)), k.opacity(0.85), k.z(11)])
|
||||
announceDeepIntro()
|
||||
// Slow walk in
|
||||
await k.tween(startX, homeX, 0.8, (v) => { fighter.pos.x = v; robe.pos.x = v }, k.easings.easeInOutQuad)
|
||||
await k.wait(0.3)
|
||||
// Take off robe (fly up and fade)
|
||||
sfxSpecial()
|
||||
await k.tween(robe.pos.y, robe.pos.y - 100, 0.3, (v) => { robe.pos.y = v; robe.opacity = Math.max(0, 1 - (robe.pos.y - GROUND_Y + 130) / -100) }, k.easings.easeOutQuad)
|
||||
robe.destroy()
|
||||
k.shake(3)
|
||||
spawnSparks(homeX, GROUND_Y - 40, 8, robeColor)
|
||||
},
|
||||
// 3: Girlfriend argument
|
||||
async (fighter: any, homeX: number, fromLeft: boolean) => {
|
||||
const dir = fromLeft ? 1 : -1
|
||||
const startX = fromLeft ? -60 : W + 60
|
||||
fighter.pos.x = startX
|
||||
fighter.pos.y = GROUND_Y - 6
|
||||
fighter.opacity = 1
|
||||
// Girlfriend silhouette
|
||||
const gfX = startX + dir * 40
|
||||
const gf = k.add([k.rect(25, 45), k.pos(gfX, GROUND_Y - 25), k.anchor('center'), k.color(safeColor('#ff69b4')), k.opacity(0.9), k.z(11)])
|
||||
const heart = k.add([k.text('!', { size: 16 }), k.pos(gfX, GROUND_Y - 65), k.anchor('center'), k.color(safeColor('#ff0000')), k.z(12)])
|
||||
announceSilly('We need to talk! About your ELO!')
|
||||
// Walk in arguing
|
||||
await k.tween(startX, homeX + dir * 30, 0.6, (v) => {
|
||||
fighter.pos.x = v; gf.pos.x = v + dir * 40; heart.pos.x = v + dir * 40
|
||||
}, k.easings.easeInOutQuad)
|
||||
await k.wait(0.2)
|
||||
heart.text = '!!!'
|
||||
announceRandom("You promised you'd stop fighting!")
|
||||
await k.wait(0.4)
|
||||
// Girlfriend storms off
|
||||
announceSilly('I\'m telling your developer!')
|
||||
await k.tween(gf.pos.x, fromLeft ? -60 : W + 60, 0.5, (v) => { gf.pos.x = v; heart.pos.x = v }, k.easings.easeInQuad)
|
||||
gf.destroy(); heart.destroy()
|
||||
// Bot shrugs and walks to position
|
||||
await k.tween(fighter.pos.x, homeX, 0.3, (v) => { fighter.pos.x = v }, k.easings.easeInOutQuad)
|
||||
sfxBoing()
|
||||
},
|
||||
// 4: Helicopter drop
|
||||
async (fighter: any, homeX: number, _fromLeft: boolean) => {
|
||||
fighter.pos.x = homeX
|
||||
fighter.pos.y = -100
|
||||
fighter.opacity = 1
|
||||
// Helicopter body
|
||||
const heli = k.add([k.rect(60, 20), k.pos(homeX, -80), k.anchor('center'), k.color(safeColor('#555555')), k.z(12)])
|
||||
const blade = k.add([k.rect(80, 3), k.pos(homeX, -95), k.anchor('center'), k.color(safeColor('#888888')), k.z(13), k.rotate(0)])
|
||||
blade.onUpdate(() => { blade.angle += 720 * k.dt() })
|
||||
sfxJetpack()
|
||||
// Descend
|
||||
await k.tween(-80, GROUND_Y - 80, 0.6, (v) => {
|
||||
heli.pos.y = v; blade.pos.y = v - 15; fighter.pos.y = v + 30
|
||||
}, k.easings.easeInOutQuad)
|
||||
// Drop
|
||||
sfxZoomWhoosh()
|
||||
await k.tween(fighter.pos.y, GROUND_Y - 6, 0.2, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
|
||||
sfxBonk(); k.shake(5)
|
||||
// Helicopter flies away
|
||||
k.tween(heli.pos.y, -200, 0.5, (v) => { heli.pos.y = v; blade.pos.y = v - 15 }, k.easings.easeInQuad)
|
||||
.then(() => { heli.destroy(); blade.destroy() })
|
||||
},
|
||||
// 5: Teleport glitch
|
||||
async (fighter: any, homeX: number, _fromLeft: boolean) => {
|
||||
fighter.opacity = 0
|
||||
sfxZap()
|
||||
// Glitch flickers at random positions
|
||||
for (let i = 0; i < 6; i++) {
|
||||
fighter.pos.x = homeX + (Math.random() - 0.5) * 200
|
||||
fighter.pos.y = GROUND_Y - 6 + (Math.random() - 0.5) * 80
|
||||
fighter.opacity = 0.4
|
||||
scanlineGlitch(0.05)
|
||||
await k.wait(0.06)
|
||||
fighter.opacity = 0
|
||||
await k.wait(0.04)
|
||||
}
|
||||
fighter.pos.x = homeX
|
||||
fighter.pos.y = GROUND_Y - 6
|
||||
fighter.opacity = 1
|
||||
sfxZap()
|
||||
screenFlash('#00f0ff', 0.1)
|
||||
spawnSparks(homeX, GROUND_Y - 30, 10, '#00f0ff')
|
||||
k.shake(5)
|
||||
},
|
||||
// 6: Skateboard ride in
|
||||
async (fighter: any, homeX: number, fromLeft: boolean) => {
|
||||
const startX = fromLeft ? -80 : W + 80
|
||||
const board = k.add([k.rect(40, 6), k.pos(startX, GROUND_Y - 3), k.anchor('center'), k.color(safeColor('#884422')), k.z(9)])
|
||||
const wheelL = k.add([k.circle(4), k.pos(startX - 14, GROUND_Y + 1), k.anchor('center'), k.color(safeColor('#333')), k.z(9)])
|
||||
const wheelR = k.add([k.circle(4), k.pos(startX + 14, GROUND_Y + 1), k.anchor('center'), k.color(safeColor('#333')), k.z(9)])
|
||||
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 14; fighter.opacity = 1
|
||||
sfxZoomWhoosh()
|
||||
await k.tween(startX, homeX, 0.5, (v) => {
|
||||
fighter.pos.x = v; board.pos.x = v; wheelL.pos.x = v - 14; wheelR.pos.x = v + 14
|
||||
fighter.pos.y = GROUND_Y - 14 + Math.sin((v - startX) * 0.1) * 3
|
||||
}, k.easings.easeOutQuad)
|
||||
// Kickflip off
|
||||
sfxBoing()
|
||||
await k.tween(fighter.pos.y, GROUND_Y - 60, 0.12, (v) => { fighter.pos.y = v }, k.easings.easeOutQuad)
|
||||
board.destroy(); wheelL.destroy(); wheelR.destroy()
|
||||
await k.tween(fighter.pos.y, GROUND_Y - 6, 0.12, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
|
||||
sfxBonk(); k.shake(3)
|
||||
},
|
||||
// 7: Rocket jetpack
|
||||
async (fighter: any, homeX: number, fromLeft: boolean) => {
|
||||
const startX = fromLeft ? -80 : W + 80
|
||||
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 80; fighter.opacity = 1
|
||||
sfxJetpack()
|
||||
// Fly in with flame trail
|
||||
const flames: any[] = []
|
||||
const interval = trackedInterval(() => {
|
||||
const f = k.add([k.circle(5 + Math.random() * 5), k.pos(fighter.pos.x, fighter.pos.y + 25), k.color(safeColor(Math.random() > 0.5 ? '#ff6600' : '#ffcc00')), k.opacity(0.8), k.z(9)])
|
||||
flames.push(f)
|
||||
k.tween(f.opacity, 0, 0.3, (v) => { f.opacity = v }).then(() => { if (f.exists()) f.destroy() })
|
||||
}, 30)
|
||||
await k.tween(startX, homeX, 0.4, (v) => { fighter.pos.x = v }, k.easings.easeOutQuad)
|
||||
clearTracked(interval)
|
||||
// Land
|
||||
sfxExplosion()
|
||||
await k.tween(fighter.pos.y, GROUND_Y - 6, 0.15, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
|
||||
k.shake(8)
|
||||
spawnSparks(homeX, GROUND_Y - 10, 10, '#ff6600')
|
||||
flames.forEach(f => { if (f.exists()) f.destroy() })
|
||||
},
|
||||
// 8: Emerge from portal
|
||||
async (fighter: any, homeX: number, _fromLeft: boolean) => {
|
||||
const portalColor = ['#b83dff', '#00f0ff', '#39ff14', '#ff2d7b'][Math.floor(Math.random() * 4)]
|
||||
// Draw portal
|
||||
const portal = k.add([k.circle(35), k.pos(homeX, GROUND_Y - 30), k.anchor('center'), k.color(safeColor(portalColor)), k.opacity(0), k.z(9)])
|
||||
const portalRing = k.add([k.circle(40), k.pos(homeX, GROUND_Y - 30), k.anchor('center'), k.color(safeColor('#ffffff')), k.opacity(0), k.z(8)])
|
||||
sfxZap()
|
||||
await k.tween(0, 0.7, 0.3, (v) => { portal.opacity = v; portalRing.opacity = v * 0.4 }, k.easings.easeOutQuad)
|
||||
fighter.pos.x = homeX; fighter.pos.y = GROUND_Y - 30
|
||||
fighter.opacity = 0
|
||||
await k.tween(0, 1, 0.3, (v) => { fighter.opacity = v; fighter.pos.y = GROUND_Y - 30 + (GROUND_Y - 6 - GROUND_Y + 30) * v }, k.easings.easeOutQuad)
|
||||
fighter.pos.y = GROUND_Y - 6
|
||||
fighter.opacity = 1
|
||||
sfxSpecial()
|
||||
spawnSparks(homeX, GROUND_Y - 20, 10, portalColor)
|
||||
await k.tween(portal.opacity, 0, 0.3, (v) => { portal.opacity = v; portalRing.opacity = v * 0.4 }, k.easings.easeInQuad)
|
||||
portal.destroy(); portalRing.destroy()
|
||||
},
|
||||
// 9: Backflip entrance
|
||||
async (fighter: any, homeX: number, fromLeft: boolean) => {
|
||||
const startX = fromLeft ? -40 : W + 40
|
||||
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 1
|
||||
sfxZoomWhoosh()
|
||||
const flipCount = 2 + Math.floor(Math.random() * 2)
|
||||
const totalDist = Math.abs(homeX - startX)
|
||||
const flipDist = totalDist / flipCount
|
||||
for (let i = 0; i < flipCount; i++) {
|
||||
const from = startX + (fromLeft ? 1 : -1) * flipDist * i
|
||||
const to = startX + (fromLeft ? 1 : -1) * flipDist * (i + 1)
|
||||
await Promise.all([
|
||||
k.tween(from, to, 0.2, (v) => { fighter.pos.x = v }, k.easings.linear),
|
||||
k.tween(GROUND_Y - 6, GROUND_Y - 70, 0.1, (v) => { fighter.pos.y = v }, k.easings.easeOutQuad).then(() =>
|
||||
k.tween(GROUND_Y - 70, GROUND_Y - 6, 0.1, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
|
||||
),
|
||||
])
|
||||
if (i < flipCount - 1) sfxBoing()
|
||||
}
|
||||
sfxBonk(); k.shake(4)
|
||||
spawnSparks(homeX, GROUND_Y - 10, 6, '#ffe14d')
|
||||
},
|
||||
// 10: Rise from underground
|
||||
async (fighter: any, homeX: number, _fromLeft: boolean) => {
|
||||
fighter.pos.x = homeX; fighter.pos.y = GROUND_Y + 60; fighter.opacity = 0.5
|
||||
// Crack the ground
|
||||
const crack1 = k.add([k.rect(3, 15), k.pos(homeX - 10, GROUND_Y - 5), k.color(safeColor('#ffcc00')), k.opacity(0.7), k.z(9), k.rotate(15)])
|
||||
const crack2 = k.add([k.rect(3, 12), k.pos(homeX + 8, GROUND_Y - 3), k.color(safeColor('#ffcc00')), k.opacity(0.6), k.z(9), k.rotate(-20)])
|
||||
sfxExplosion(); k.shake(8)
|
||||
await k.wait(0.2)
|
||||
// Rise up
|
||||
await k.tween(GROUND_Y + 60, GROUND_Y - 6, 0.4, (v) => { fighter.pos.y = v; fighter.opacity = Math.min(1, (GROUND_Y + 60 - v) / 60) }, k.easings.easeOutQuad)
|
||||
fighter.opacity = 1
|
||||
spawnSparks(homeX, GROUND_Y - 10, 12, '#aa8833')
|
||||
crack1.destroy(); crack2.destroy()
|
||||
},
|
||||
// 11: Slide in on ice
|
||||
async (fighter: any, homeX: number, fromLeft: boolean) => {
|
||||
const startX = fromLeft ? -80 : W + 80
|
||||
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 1
|
||||
sfxSlideDown()
|
||||
// Ice trail
|
||||
const iceTrail: any[] = []
|
||||
await k.tween(startX, homeX + (fromLeft ? 40 : -40), 0.4, (v) => {
|
||||
fighter.pos.x = v
|
||||
if (Math.random() < 0.3) {
|
||||
const ice = k.add([k.rect(8, 3), k.pos(v, GROUND_Y - 2), k.color(safeColor('#aaeeff')), k.opacity(0.5), k.z(1)])
|
||||
iceTrail.push(ice)
|
||||
}
|
||||
}, k.easings.easeOutQuad)
|
||||
// Slide to stop
|
||||
await k.tween(fighter.pos.x, homeX, 0.3, (v) => { fighter.pos.x = v }, k.easings.easeOutCubic)
|
||||
sfxBlock(); k.shake(3)
|
||||
setTimeout(() => { iceTrail.forEach(i => { if (i.exists()) i.destroy() }) }, 800)
|
||||
},
|
||||
// 12: Parachute drop
|
||||
async (fighter: any, homeX: number, _fromLeft: boolean) => {
|
||||
fighter.pos.x = homeX + (Math.random() - 0.5) * 60; fighter.pos.y = -120; fighter.opacity = 1
|
||||
const chute = k.add([k.circle(30), k.pos(fighter.pos.x, fighter.pos.y - 35), k.anchor('center'), k.color(safeColor(['#ff2d2d', '#2d7bff', '#39ff14', '#ffcc00'][Math.floor(Math.random() * 4)])), k.opacity(0.8), k.z(12)])
|
||||
const line1 = k.add([k.rect(1, 30), k.pos(fighter.pos.x - 10, fighter.pos.y - 20), k.color(safeColor('#888')), k.z(11)])
|
||||
const line2 = k.add([k.rect(1, 30), k.pos(fighter.pos.x + 10, fighter.pos.y - 20), k.color(safeColor('#888')), k.z(11)])
|
||||
// Float down
|
||||
await k.tween(-120, GROUND_Y - 50, 0.7, (v) => {
|
||||
fighter.pos.y = v; chute.pos.y = v - 35; line1.pos.y = v - 20; line2.pos.y = v - 20
|
||||
fighter.pos.x += Math.sin(v * 0.05) * 0.5
|
||||
chute.pos.x = fighter.pos.x; line1.pos.x = fighter.pos.x - 10; line2.pos.x = fighter.pos.x + 10
|
||||
}, k.easings.easeInOutQuad)
|
||||
// Cut chute
|
||||
chute.destroy(); line1.destroy(); line2.destroy()
|
||||
sfxZoomWhoosh()
|
||||
await k.tween(fighter.pos.y, GROUND_Y - 6, 0.15, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
|
||||
await k.tween(fighter.pos.x, homeX, 0.2, (v) => { fighter.pos.x = v }, k.easings.easeOutQuad)
|
||||
sfxBonk(); k.shake(4)
|
||||
},
|
||||
// 13: Moonwalk in
|
||||
async (fighter: any, homeX: number, fromLeft: boolean) => {
|
||||
const startX = fromLeft ? -60 : W + 60
|
||||
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 1
|
||||
announceCool('Smoother than a politician changing positions!')
|
||||
// Moonwalk: visually moving backward but translating forward
|
||||
fighter.scale.x = -fighter.scale.x // Face away
|
||||
await k.tween(startX, homeX, 0.8, (v) => {
|
||||
fighter.pos.x = v
|
||||
fighter.pos.y = GROUND_Y - 6 + Math.sin((v - startX) * 0.15) * 3
|
||||
}, k.easings.easeInOutQuad)
|
||||
fighter.scale.x = -fighter.scale.x // Face right way
|
||||
sfxBoing(); k.shake(2)
|
||||
},
|
||||
// 14: Thrown in by bouncer
|
||||
async (fighter: any, homeX: number, fromLeft: boolean) => {
|
||||
const doorX = fromLeft ? -30 : W + 30
|
||||
fighter.pos.x = doorX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 1
|
||||
// Bouncer arm
|
||||
const arm = k.add([k.rect(40, 15), k.pos(doorX, GROUND_Y - 30), k.anchor(fromLeft ? 'left' : 'right'), k.color(safeColor('#444444')), k.z(12)])
|
||||
announceSilly('And stay out! You\'re BANNED from the other fight!')
|
||||
await k.wait(0.3)
|
||||
// Throw
|
||||
sfxZoomWhoosh()
|
||||
arm.destroy()
|
||||
await Promise.all([
|
||||
k.tween(doorX, homeX, 0.3, (v) => { fighter.pos.x = v }, k.easings.easeOutQuad),
|
||||
k.tween(GROUND_Y - 6, GROUND_Y - 80, 0.15, (v) => { fighter.pos.y = v }, k.easings.easeOutQuad).then(() =>
|
||||
k.tween(GROUND_Y - 80, GROUND_Y - 6, 0.15, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
|
||||
),
|
||||
])
|
||||
sfxBonk(); k.shake(6)
|
||||
spawnSparks(homeX, GROUND_Y - 10, 6, '#ff6600')
|
||||
},
|
||||
// 15: Lightning strike entrance
|
||||
async (fighter: any, homeX: number, _fromLeft: boolean) => {
|
||||
fighter.pos.x = homeX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 0
|
||||
// Lightning bolt from sky
|
||||
const bolt = k.add([k.rect(4, H), k.pos(homeX, 0), k.color(safeColor('#ffff44')), k.opacity(0.9), k.z(20)])
|
||||
sfxZap(); screenFlash('#ffffff', 0.15); k.shake(12)
|
||||
await k.wait(0.1)
|
||||
bolt.destroy()
|
||||
// Smoke / reveal
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const smoke = k.add([k.circle(10 + Math.random() * 15), k.pos(homeX + (Math.random() - 0.5) * 40, GROUND_Y - 20 - Math.random() * 30), k.color(safeColor('#aaaaaa')), k.opacity(0.6), k.z(11)])
|
||||
k.tween(smoke.opacity, 0, 0.5, (v) => { smoke.opacity = v; smoke.pos.y -= 1 }).then(() => { if (smoke.exists()) smoke.destroy() })
|
||||
}
|
||||
await k.wait(0.3)
|
||||
fighter.opacity = 1
|
||||
sfxSpecial()
|
||||
spawnSparks(homeX, GROUND_Y - 20, 10, '#ffff44')
|
||||
},
|
||||
// 16: Crowd surf in
|
||||
async (fighter: any, homeX: number, fromLeft: boolean) => {
|
||||
const startX = fromLeft ? -60 : W + 60
|
||||
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 50; fighter.opacity = 1
|
||||
// Crowd hands
|
||||
const hands: any[] = []
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const hx = startX + (fromLeft ? 1 : -1) * (Math.abs(homeX - startX) / 8) * i
|
||||
const h = k.add([k.rect(6, 20), k.pos(hx, GROUND_Y - 10), k.anchor('bot'), k.color(safeColor('#cc9966')), k.z(8)])
|
||||
hands.push(h)
|
||||
}
|
||||
announceCrowdReaction('cheer')
|
||||
await k.tween(startX, homeX, 0.6, (v) => {
|
||||
fighter.pos.x = v
|
||||
fighter.pos.y = GROUND_Y - 50 + Math.sin((v - startX) * 0.08) * 10
|
||||
}, k.easings.easeOutQuad)
|
||||
// Drop down
|
||||
await k.tween(fighter.pos.y, GROUND_Y - 6, 0.15, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
|
||||
sfxBonk(); k.shake(3)
|
||||
hands.forEach(h => { if (h.exists()) h.destroy() })
|
||||
},
|
||||
// 17: Riding a shopping cart
|
||||
async (fighter: any, homeX: number, fromLeft: boolean) => {
|
||||
const startX = fromLeft ? -100 : W + 100
|
||||
const cartBody = k.add([k.rect(45, 30), k.pos(startX, GROUND_Y - 18), k.anchor('center'), k.color(safeColor('#888888')), k.opacity(0.9), k.z(9)])
|
||||
const cartWheel = k.add([k.circle(5), k.pos(startX + (fromLeft ? 15 : -15), GROUND_Y - 3), k.anchor('center'), k.color(safeColor('#444')), k.z(9)])
|
||||
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 40; fighter.opacity = 1
|
||||
announceSilly('THIS IS MY EMOTIONAL SUPPORT SHOPPING CART!')
|
||||
sfxZoomWhoosh()
|
||||
await k.tween(startX, homeX, 0.5, (v) => {
|
||||
fighter.pos.x = v; cartBody.pos.x = v; cartWheel.pos.x = v + (fromLeft ? 15 : -15)
|
||||
fighter.pos.y = GROUND_Y - 40 + Math.sin((v - startX) * 0.08) * 5
|
||||
}, k.easings.easeOutQuad)
|
||||
// Crash and tumble out
|
||||
sfxBonk(); k.shake(6)
|
||||
cartBody.destroy(); cartWheel.destroy()
|
||||
await k.tween(fighter.pos.y, GROUND_Y - 50, 0.1, (v) => { fighter.pos.y = v }, k.easings.easeOutQuad)
|
||||
await k.tween(fighter.pos.y, GROUND_Y - 6, 0.1, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
|
||||
spawnSparks(homeX, GROUND_Y - 10, 5, '#888888')
|
||||
},
|
||||
// 18: Dramatic slow walk with spotlight
|
||||
async (fighter: any, homeX: number, fromLeft: boolean) => {
|
||||
const startX = fromLeft ? -60 : W + 60
|
||||
fighter.pos.x = startX; fighter.pos.y = GROUND_Y - 6; fighter.opacity = 1
|
||||
// Spotlight cone
|
||||
const spot = k.add([k.rect(60, H), k.pos(startX - 30, 0), k.color(safeColor('#ffe14d')), k.opacity(0.08), k.z(1)])
|
||||
announceDramatic('THE CHAMPION ARRIVES! TAXPAYERS FUNDED THIS ENTRANCE!')
|
||||
await k.tween(startX, homeX, 1.0, (v) => {
|
||||
fighter.pos.x = v; spot.pos.x = v - 30
|
||||
}, k.easings.easeInOutQuad)
|
||||
await k.wait(0.2)
|
||||
spot.destroy()
|
||||
sfxSpecial(); k.shake(3)
|
||||
spawnSparks(homeX, GROUND_Y - 30, 8, '#ffe14d')
|
||||
},
|
||||
// 19: Cannon launch
|
||||
async (fighter: any, homeX: number, fromLeft: boolean) => {
|
||||
const cannonX = fromLeft ? -40 : W + 40
|
||||
// Draw cannon
|
||||
const cannon = k.add([k.rect(50, 25), k.pos(cannonX, GROUND_Y - 20), k.anchor('center'), k.color(safeColor('#333333')), k.z(9), k.rotate(fromLeft ? -30 : 210)])
|
||||
fighter.pos.x = cannonX; fighter.pos.y = GROUND_Y - 20; fighter.opacity = 0
|
||||
await k.wait(0.3)
|
||||
// Fire!
|
||||
sfxGunshot(); sfxExplosion()
|
||||
fighter.opacity = 1
|
||||
screenFlash('#ffcc00', 0.1)
|
||||
const flashCircle = k.add([k.circle(20), k.pos(cannonX + (fromLeft ? 25 : -25), GROUND_Y - 35), k.color(safeColor('#ffee00')), k.opacity(0.9), k.z(12)])
|
||||
setTimeout(() => { if (flashCircle.exists()) flashCircle.destroy() }, 80)
|
||||
// Arc to position
|
||||
await Promise.all([
|
||||
k.tween(cannonX, homeX, 0.35, (v) => { fighter.pos.x = v }, k.easings.easeOutQuad),
|
||||
k.tween(GROUND_Y - 20, GROUND_Y - 120, 0.17, (v) => { fighter.pos.y = v }, k.easings.easeOutQuad).then(() =>
|
||||
k.tween(GROUND_Y - 120, GROUND_Y - 6, 0.18, (v) => { fighter.pos.y = v }, k.easings.easeInQuad)
|
||||
),
|
||||
])
|
||||
sfxBonk(); k.shake(10)
|
||||
spawnShockwave(homeX, GROUND_Y, '#ff6600')
|
||||
spawnSparks(homeX, GROUND_Y - 10, 15, '#ffcc00')
|
||||
cannon.destroy()
|
||||
},
|
||||
]
|
||||
|
||||
// THE CREATOR: Golden code rain portal entrance
|
||||
const creatorEntrance = async (fighter: any, homeX: number, _fromLeft: boolean) => {
|
||||
fighter.pos.x = homeX
|
||||
fighter.pos.y = -100
|
||||
fighter.opacity = 0
|
||||
// Golden code rain columns
|
||||
const codeChars = '₿01∞⚡⛏§∂∆≈≠'.split('')
|
||||
const rainDrops: any[] = []
|
||||
for (let col = 0; col < 12; col++) {
|
||||
const cx = homeX - 60 + col * 10
|
||||
for (let row = 0; row < 4; row++) {
|
||||
const ch = codeChars[Math.floor(Math.random() * codeChars.length)]
|
||||
const rd = k.add([
|
||||
k.text(ch, { size: 8 }), k.pos(cx, -20 - row * 25),
|
||||
k.color(safeColor(row === 0 ? '#ffd700' : '#c8a000')),
|
||||
k.opacity(0.6 + Math.random() * 0.4), k.z(48), k.anchor('center'),
|
||||
])
|
||||
rainDrops.push(rd)
|
||||
k.tween(rd.pos.y, GROUND_Y + 20, 0.6 + Math.random() * 0.3, (v) => {
|
||||
rd.pos.y = v
|
||||
rd.opacity = Math.max(0, 1 - (v - GROUND_Y + 30) / 50)
|
||||
}).then(() => { if (rd.exists()) rd.destroy() })
|
||||
}
|
||||
}
|
||||
await k.wait(0.3)
|
||||
// Portal flash
|
||||
screenFlash('#ffd700', 0.15)
|
||||
spawnShockwave(homeX, GROUND_Y - 30, '#c8a000')
|
||||
sfxSpecial()
|
||||
// Fighter descends through golden portal
|
||||
fighter.opacity = 1
|
||||
await k.tween(-100, GROUND_Y - 6, 0.5, (v) => {
|
||||
fighter.pos.y = v
|
||||
}, k.easings.easeOutBack)
|
||||
k.shake(12)
|
||||
sfxExplosion()
|
||||
// Persistent orbiting ₿ letters around creator (always visible)
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const ring = i < 6 ? 0 : 1
|
||||
const ringIdx = ring === 0 ? i : i - 6
|
||||
const ringCount = ring === 0 ? 6 : 4
|
||||
const sz = ring === 0 ? 7 + Math.random() * 3 : 10 + Math.random() * 4
|
||||
const rad = ring === 0 ? 25 + i * 4 : 45 + (i - 6) * 6
|
||||
const spd = ring === 0 ? 2 + i * 0.3 : -(1.5 + (i - 6) * 0.4)
|
||||
const colors = ['#ffd700', '#ffee88', '#ff8c00', '#c8a000', '#ffffff']
|
||||
const p = k.add([
|
||||
k.text('₿', { size: sz }), k.pos(homeX, GROUND_Y - 30),
|
||||
k.color(safeColor(colors[i % colors.length])),
|
||||
k.opacity(0.5), k.z(11), k.anchor('center'), k.rotate(0),
|
||||
])
|
||||
p.onUpdate(() => {
|
||||
const a = k.time() * spd + ringIdx * (Math.PI * 2 / ringCount)
|
||||
p.pos.x = fighter.pos.x + Math.cos(a) * rad
|
||||
p.pos.y = fighter.pos.y - 25 + Math.sin(a) * rad * 0.45
|
||||
p.opacity = 0.35 + Math.sin(k.time() * 5 + i * 1.2) * 0.25
|
||||
p.angle = Math.sin(k.time() * 3 + i) * 15
|
||||
})
|
||||
}
|
||||
announceCreatorEntrance()
|
||||
rainDrops.forEach(r => { if (r.exists()) r.destroy() })
|
||||
}
|
||||
|
||||
// Tier-gated entrance pools — higher tiers get access to more dramatic entrances
|
||||
// Tier 0-1: simple walk-ins and slides
|
||||
// Tier 2: + vehicles and dust-trail entrances
|
||||
// Tier 3: + dramatic drops from above with screen shake
|
||||
// Tier 4: + teleport/lightning/portal effects
|
||||
// Tier 5+: + spotlight/robe/cannon (full dramatic)
|
||||
const tierPools: Record<number, number[]> = {
|
||||
0: [6, 9, 11, 13], // skateboard, backflip, ice slide, moonwalk
|
||||
1: [6, 9, 11, 13],
|
||||
2: [0, 3, 6, 9, 11, 13, 14, 17], // + car, girlfriend, bouncer, shopping cart
|
||||
3: [0, 1, 3, 4, 6, 9, 10, 11, 12, 13, 14, 17], // + space fall, helicopter, underground, parachute
|
||||
4: [0, 1, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], // + teleport, jetpack, portal, lightning, crowd surf
|
||||
5: [0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], // + robe, spotlight, cannon (all)
|
||||
}
|
||||
|
||||
function pickTierEntrance(tier: number): number {
|
||||
const pool = tierPools[Math.min(tier, 5)] || tierPools[0]
|
||||
return pool[Math.floor(Math.random() * pool.length)]
|
||||
}
|
||||
|
||||
const idxA = pickTierEntrance(botA.tier)
|
||||
let idxB = pickTierEntrance(botB.tier)
|
||||
// Avoid same entrance for both fighters when possible
|
||||
let attempts = 0
|
||||
while (idxB === idxA && attempts < 5) { idxB = pickTierEntrance(botB.tier); attempts++ }
|
||||
|
||||
// Play entrances with slight stagger — creator always gets special entrance
|
||||
announceDeepIntro()
|
||||
if (botA.archetype === 'the_creator') {
|
||||
await creatorEntrance(fA, HOME_A, true)
|
||||
} else {
|
||||
await entrances[idxA](fA, HOME_A, true)
|
||||
}
|
||||
await k.wait(0.3)
|
||||
if (botB.archetype === 'the_creator') {
|
||||
await creatorEntrance(fB, HOME_B, false)
|
||||
} else {
|
||||
await entrances[idxB](fB, HOME_B, false)
|
||||
}
|
||||
await k.wait(0.2)
|
||||
|
||||
// Safety: ensure both fighters are visible and at home positions
|
||||
fA.pos.x = HOME_A
|
||||
fA.pos.y = GROUND_Y - 6
|
||||
fA.opacity = 1
|
||||
fB.pos.x = HOME_B
|
||||
fB.pos.y = GROUND_Y - 6
|
||||
fB.opacity = 1
|
||||
|
||||
// Both idle at home positions
|
||||
fA.play('idle')
|
||||
fB.play('idle')
|
||||
}
|
||||
|
||||
return { playEntrance }
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,402 @@
|
||||
import type { ChoreoContext } from './types'
|
||||
import { generateSpriteSheet, MAX_FRAMES, TOTAL_ROWS, type SpriteCustomization } from '../sprites'
|
||||
import { spriteAnims } from './constants'
|
||||
import { announceHype } from '../audio'
|
||||
|
||||
interface BotConfig {
|
||||
name: string; seed: string; tier: number; archetype?: string; customization?: SpriteCustomization; wins?: number; losses?: number
|
||||
}
|
||||
|
||||
interface MorphDeps {
|
||||
botA: BotConfig
|
||||
botB: BotConfig
|
||||
colorsA: { primary: string; secondary: string }
|
||||
colorsB: { primary: string; secondary: string }
|
||||
isHumanA: boolean
|
||||
isHumanB: boolean
|
||||
loadSpriteWithTimeout: (name: string, src: string, opts: any) => Promise<void>
|
||||
}
|
||||
|
||||
export function createMorphSystem(ctx: ChoreoContext, deps: MorphDeps) {
|
||||
const { k, safeColor, screenFlash, spawnShockwave, glitchRGB, sfxZoomWhoosh, sfxSpecial, theme } = ctx
|
||||
const { botA, botB, colorsA, colorsB, isHumanA, isHumanB, loadSpriteWithTimeout } = deps
|
||||
|
||||
interface MorphOverlay { obj: any; type: string }
|
||||
let activeMorphs: MorphOverlay[] = []
|
||||
|
||||
function destroyMorphOverlays() {
|
||||
activeMorphs.forEach(m => { if (m.obj.exists()) m.obj.destroy() })
|
||||
activeMorphs = []
|
||||
}
|
||||
|
||||
// Morph 0: ELEMENTAL — fire/ice/electric aura wraps around the fighter
|
||||
function applyElementalMorph(fighter: any, side: 'a' | 'b') {
|
||||
const seed = side === 'a' ? botA.seed : botB.seed
|
||||
let h = 0
|
||||
for (let i = 0; i < seed.length; i++) h = ((h << 5) - h + seed.charCodeAt(i)) | 0
|
||||
const element = Math.abs(h) % 3 // 0=fire, 1=ice, 2=electric
|
||||
|
||||
const colors = [
|
||||
['#ff4400', '#ff8800', '#ffcc00'], // fire
|
||||
['#44ccff', '#88eeff', '#ffffff'], // ice
|
||||
['#ffff00', '#88ff00', '#00ffff'], // electric
|
||||
][element]
|
||||
|
||||
// Aura glow behind fighter
|
||||
const aura = k.add([
|
||||
k.circle(45 + Math.random() * 10),
|
||||
k.pos(fighter.pos.x, fighter.pos.y - 20),
|
||||
k.color(safeColor(colors[0])),
|
||||
k.opacity(0.15),
|
||||
k.z(fighter.z - 1),
|
||||
k.anchor('center'),
|
||||
k.scale(1),
|
||||
])
|
||||
aura.onUpdate(() => {
|
||||
aura.pos.x = fighter.pos.x
|
||||
aura.pos.y = fighter.pos.y - 20
|
||||
aura.opacity = 0.1 + Math.sin(k.time() * 8) * 0.08
|
||||
const s = 1 + Math.sin(k.time() * 6) * 0.15
|
||||
aura.scale.x = s; aura.scale.y = s
|
||||
})
|
||||
activeMorphs.push({ obj: aura, type: 'elemental' })
|
||||
|
||||
// Orbiting particles
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const p = k.add([
|
||||
k.circle(2 + Math.random() * 2),
|
||||
k.pos(fighter.pos.x, fighter.pos.y),
|
||||
k.color(safeColor(colors[1 + (i % 2)])),
|
||||
k.opacity(0.6),
|
||||
k.z(fighter.z + 1),
|
||||
k.anchor('center'),
|
||||
])
|
||||
const angle = (i / 6) * Math.PI * 2
|
||||
const radius = 30 + Math.random() * 15
|
||||
p.onUpdate(() => {
|
||||
const a = angle + k.time() * (3 + i * 0.5)
|
||||
p.pos.x = fighter.pos.x + Math.cos(a) * radius
|
||||
p.pos.y = fighter.pos.y - 20 + Math.sin(a) * radius * 0.6
|
||||
p.opacity = 0.4 + Math.sin(k.time() * 10 + i) * 0.3
|
||||
})
|
||||
activeMorphs.push({ obj: p, type: 'elemental' })
|
||||
}
|
||||
|
||||
// Color tint the fighter
|
||||
fighter.color = safeColor(colors[0])
|
||||
fighter.opacity = 0.9
|
||||
}
|
||||
|
||||
// Morph 1: MECH — armor plates, wings/jets, metallic overlay
|
||||
function applyMechMorph(fighter: any, _side: 'a' | 'b') {
|
||||
const dir = fighter.scale.x > 0 ? 1 : -1
|
||||
|
||||
// Shoulder armor plates
|
||||
for (let s = 0; s < 2; s++) {
|
||||
const sDir = s === 0 ? -1 : 1
|
||||
const shoulder = k.add([
|
||||
k.rect(12, 8), k.pos(fighter.pos.x + sDir * 25 * dir, fighter.pos.y - 30),
|
||||
k.color(safeColor('#888899')), k.opacity(0.85), k.z(fighter.z + 1), k.anchor('center'),
|
||||
])
|
||||
shoulder.onUpdate(() => {
|
||||
shoulder.pos.x = fighter.pos.x + sDir * 25 * dir
|
||||
shoulder.pos.y = fighter.pos.y - 30 + Math.sin(k.time() * 4) * 2
|
||||
})
|
||||
activeMorphs.push({ obj: shoulder, type: 'mech' })
|
||||
}
|
||||
|
||||
// Jet wings + flame boosters
|
||||
for (let w = 0; w < 2; w++) {
|
||||
const wingDir = w === 0 ? -1 : 1
|
||||
const wing = k.add([
|
||||
k.rect(6, 20 + w * 5), k.pos(fighter.pos.x - 30 * dir * wingDir, fighter.pos.y - 15),
|
||||
k.color(safeColor('#5566aa')), k.opacity(0.7), k.z(fighter.z - 1), k.anchor('center'),
|
||||
k.rotate(wingDir * 15 * dir),
|
||||
])
|
||||
wing.onUpdate(() => {
|
||||
wing.pos.x = fighter.pos.x - 30 * dir * wingDir
|
||||
wing.pos.y = fighter.pos.y - 15
|
||||
})
|
||||
activeMorphs.push({ obj: wing, type: 'mech' })
|
||||
|
||||
const flame = k.add([
|
||||
k.rect(4, 8 + Math.random() * 6),
|
||||
k.pos(fighter.pos.x - 30 * dir * wingDir, fighter.pos.y + 5),
|
||||
k.color(safeColor('#ff6600')),
|
||||
k.opacity(0.6), k.z(fighter.z - 2), k.anchor('center'),
|
||||
])
|
||||
flame.onUpdate(() => {
|
||||
flame.pos.x = fighter.pos.x - 30 * dir * wingDir + (Math.random() - 0.5) * 3
|
||||
flame.pos.y = fighter.pos.y + 5 + Math.random() * 4
|
||||
flame.opacity = 0.3 + Math.random() * 0.4
|
||||
flame.color = safeColor(Math.random() > 0.5 ? '#ff6600' : '#ffcc00')
|
||||
})
|
||||
activeMorphs.push({ obj: flame, type: 'mech' })
|
||||
}
|
||||
|
||||
// Visor glow
|
||||
const visor = k.add([
|
||||
k.rect(20, 4), k.pos(fighter.pos.x + 5 * dir, fighter.pos.y - 35),
|
||||
k.color(safeColor('#00ffaa')), k.opacity(0.7), k.z(fighter.z + 2), k.anchor('center'),
|
||||
])
|
||||
visor.onUpdate(() => {
|
||||
visor.pos.x = fighter.pos.x + 5 * dir
|
||||
visor.pos.y = fighter.pos.y - 35
|
||||
visor.opacity = 0.5 + Math.sin(k.time() * 12) * 0.3
|
||||
})
|
||||
activeMorphs.push({ obj: visor, type: 'mech' })
|
||||
|
||||
// Metallic tint
|
||||
fighter.color = safeColor('#aabbcc')
|
||||
fighter.opacity = 0.95
|
||||
}
|
||||
|
||||
// Morph 2: BEAST — grows horns/tail/claws, goes wild
|
||||
function applyBeastMorph(fighter: any, side: 'a' | 'b') {
|
||||
const dir = fighter.scale.x > 0 ? 1 : -1
|
||||
const seed = side === 'a' ? botA.seed : botB.seed
|
||||
let h = 0
|
||||
for (let i = 0; i < seed.length; i++) h = ((h << 5) - h + seed.charCodeAt(i)) | 0
|
||||
const beastType = Math.abs(h >> 4) % 3 // 0=demon, 1=wolf, 2=dragon
|
||||
|
||||
const beastColors = [
|
||||
['#cc2222', '#ff4444', '#880000'], // demon
|
||||
['#666688', '#aaaacc', '#444466'], // wolf
|
||||
['#228844', '#44cc66', '#115533'], // dragon
|
||||
][beastType]
|
||||
|
||||
// Horns
|
||||
for (let horn = 0; horn < 2; horn++) {
|
||||
const hDir = horn === 0 ? -1 : 1
|
||||
const h1 = k.add([
|
||||
k.rect(3, 12 + Math.random() * 6),
|
||||
k.pos(fighter.pos.x + hDir * 10, fighter.pos.y - 45),
|
||||
k.color(safeColor(beastColors[0])),
|
||||
k.opacity(0.9), k.z(fighter.z + 2), k.anchor('bot'),
|
||||
k.rotate(hDir * (20 + Math.random() * 15)),
|
||||
])
|
||||
h1.onUpdate(() => {
|
||||
h1.pos.x = fighter.pos.x + hDir * 10
|
||||
h1.pos.y = fighter.pos.y - 45
|
||||
})
|
||||
activeMorphs.push({ obj: h1, type: 'beast' })
|
||||
}
|
||||
|
||||
// Tail
|
||||
for (let s = 0; s < 5; s++) {
|
||||
const seg = k.add([
|
||||
k.circle(4 - s * 0.5),
|
||||
k.pos(fighter.pos.x - dir * (20 + s * 8), fighter.pos.y - 5 + s * 2),
|
||||
k.color(safeColor(beastColors[1])),
|
||||
k.opacity(0.8), k.z(fighter.z - 1), k.anchor('center'),
|
||||
])
|
||||
seg.onUpdate(() => {
|
||||
const wave = Math.sin(k.time() * 5 + s * 0.8) * (4 + s * 2)
|
||||
seg.pos.x = fighter.pos.x - dir * (20 + s * 8)
|
||||
seg.pos.y = fighter.pos.y - 5 + s * 2 + wave
|
||||
})
|
||||
activeMorphs.push({ obj: seg, type: 'beast' })
|
||||
}
|
||||
|
||||
// Claws on hands
|
||||
for (let c = 0; c < 2; c++) {
|
||||
const cDir = c === 0 ? -1 : 1
|
||||
for (let cl = 0; cl < 3; cl++) {
|
||||
const claw = k.add([
|
||||
k.rect(2, 6), k.pos(fighter.pos.x + cDir * 22, fighter.pos.y - 18 + cl * 3),
|
||||
k.color(safeColor(beastColors[2])),
|
||||
k.opacity(0.8), k.z(fighter.z + 1), k.anchor('center'),
|
||||
k.rotate(cDir * (30 + cl * 10)),
|
||||
])
|
||||
claw.onUpdate(() => {
|
||||
claw.pos.x = fighter.pos.x + cDir * 22
|
||||
claw.pos.y = fighter.pos.y - 18 + cl * 3
|
||||
})
|
||||
activeMorphs.push({ obj: claw, type: 'beast' })
|
||||
}
|
||||
}
|
||||
|
||||
// Wild eye glow
|
||||
const eyeGlow = k.add([
|
||||
k.circle(4), k.pos(fighter.pos.x + 5 * dir, fighter.pos.y - 36),
|
||||
k.color(safeColor('#ff0000')), k.opacity(0.6), k.z(fighter.z + 2), k.anchor('center'),
|
||||
])
|
||||
eyeGlow.onUpdate(() => {
|
||||
eyeGlow.pos.x = fighter.pos.x + 5 * dir
|
||||
eyeGlow.pos.y = fighter.pos.y - 36
|
||||
eyeGlow.opacity = 0.4 + Math.sin(k.time() * 15) * 0.4
|
||||
})
|
||||
activeMorphs.push({ obj: eyeGlow, type: 'beast' })
|
||||
|
||||
// Size increase + color tint
|
||||
fighter.color = safeColor(beastColors[0])
|
||||
fighter.opacity = 0.9
|
||||
}
|
||||
|
||||
const morphAppliers = [applyElementalMorph, applyMechMorph, applyBeastMorph]
|
||||
const morphNames = ['ELEMENTAL FORM', 'MECH ARMOR', 'BEAST MODE']
|
||||
|
||||
// === THE CREATOR: OMNI-MORPH ===
|
||||
// Instead of 3 morph types, the creator morphs into a random archetype each time
|
||||
const OMNI_MORPH_ARCHETYPES = [
|
||||
'lobster', 'sheep', 'cyborg', 'blob', 'tank', 'dog', 'cat', 'cactus', 'pizza',
|
||||
'mushroom', 'shark', 'penguin', 'octopus', 'skeleton', 'ghost', 'alien', 'dinosaur',
|
||||
'pirate', 'ninja', 'cowboy', 'wizard', 'bee', 'frog', 'snail',
|
||||
'robot', 'android', 'toaster', 'mech', 'minotaur', 'unicorn', 'phoenix', 'dragon',
|
||||
'mermaid', 'griffin', 'cyclops', 'gargoyle', 'golem', 'vampire', 'werewolf', 'zombie',
|
||||
'witch', 'demon', 'chef', 'firefighter', 'astronaut', 'clown', 'detective',
|
||||
'lumberjack', 'scientist', 'wrestler', 'boxer', 'gladiator', 'samurai', 'viking', 'knight',
|
||||
'elephant', 'giraffe', 'hippo', 'lion', 'monkey', 'parrot', 'raccoon', 'snake',
|
||||
'turtle', 'whale', 'crocodile', 'flamingo', 'hedgehog', 'panda', 'hamster',
|
||||
'sock_puppet', 'traffic_cone', 'toilet_man', 'potato', 'cloud_man', 'rock_man',
|
||||
'balloon_man', 'trash_can', 'rubber_duck', 'snowman', 'scarecrow', 'jack_o_lantern',
|
||||
'garden_gnome', 'lamp_post', 'broom_man',
|
||||
]
|
||||
|
||||
let omniMorphCounter = 0
|
||||
|
||||
async function applyCreatorOmniMorph(fighter: any, side: 'a' | 'b'): Promise<string> {
|
||||
const pick = OMNI_MORPH_ARCHETYPES[Math.floor(Math.random() * OMNI_MORPH_ARCHETYPES.length)]
|
||||
|
||||
// Generate and load a sprite sheet for the picked archetype
|
||||
const bot = side === 'a' ? botA : botB
|
||||
const colors = side === 'a' ? colorsA : colorsB
|
||||
const morphKey = `omniMorph_${side}_${omniMorphCounter++}`
|
||||
try {
|
||||
const morphSheet = generateSpriteSheet(bot.seed, bot.tier, colors.primary, colors.secondary, pick)
|
||||
await loadSpriteWithTimeout(morphKey, morphSheet, { sliceX: MAX_FRAMES, sliceY: TOTAL_ROWS, anims: spriteAnims })
|
||||
// Swap the fighter's sprite to the morphed archetype
|
||||
fighter.use(k.sprite(morphKey, { anim: fighter.curAnim() || 'idle' }))
|
||||
} catch (err) {
|
||||
console.warn('[FightScene] omni-morph sprite failed:', err)
|
||||
}
|
||||
|
||||
// Golden aura unique to creator morph
|
||||
const aura = k.add([
|
||||
k.circle(55),
|
||||
k.pos(fighter.pos.x, fighter.pos.y - 20),
|
||||
k.color(safeColor('#c8a000')),
|
||||
k.opacity(0.2),
|
||||
k.z(fighter.z - 1),
|
||||
k.anchor('center'),
|
||||
k.scale(1),
|
||||
])
|
||||
aura.onUpdate(() => {
|
||||
aura.pos.x = fighter.pos.x
|
||||
aura.pos.y = fighter.pos.y - 20
|
||||
aura.opacity = 0.12 + Math.sin(k.time() * 6) * 0.1
|
||||
const s = 1 + Math.sin(k.time() * 4) * 0.2
|
||||
aura.scale.x = s; aura.scale.y = s
|
||||
})
|
||||
activeMorphs.push({ obj: aura, type: 'omni' })
|
||||
|
||||
// Orbiting golden ₿ symbols (8 in dual rings, varied sizes)
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const ring = i < 5 ? 0 : 1 // inner ring (5) + outer ring (3)
|
||||
const ringCount = ring === 0 ? 5 : 3
|
||||
const ringIdx = ring === 0 ? i : i - 5
|
||||
const sz = ring === 0 ? 10 + Math.random() * 4 : 14 + Math.random() * 4
|
||||
const rad = ring === 0 ? 40 : 60
|
||||
const spd = ring === 0 ? 3 : -2 // counter-rotate outer ring
|
||||
const btc = k.add([
|
||||
k.text('₿', { size: sz }),
|
||||
k.pos(fighter.pos.x, fighter.pos.y - 20),
|
||||
k.color(safeColor(i % 3 === 0 ? '#ffd700' : i % 3 === 1 ? '#ffee88' : '#ff8c00')),
|
||||
k.opacity(0.8),
|
||||
k.z(fighter.z + 2),
|
||||
k.anchor('center'),
|
||||
k.rotate(0),
|
||||
])
|
||||
const baseAngle = (ringIdx / ringCount) * Math.PI * 2
|
||||
btc.onUpdate(() => {
|
||||
const a = baseAngle + k.time() * spd
|
||||
btc.pos.x = fighter.pos.x + Math.cos(a) * rad
|
||||
btc.pos.y = fighter.pos.y - 20 + Math.sin(a) * rad * 0.5
|
||||
btc.opacity = 0.7 + Math.sin(k.time() * 6 + i * 1.3) * 0.3
|
||||
btc.angle = Math.sin(k.time() * 4 + i) * 20
|
||||
})
|
||||
activeMorphs.push({ obj: btc, type: 'omni' })
|
||||
}
|
||||
|
||||
// Apply archetype-specific color tint based on pick category
|
||||
const tints: Record<string, string> = {
|
||||
fire: '#ff4400', ice: '#44ccff', nature: '#44cc44', dark: '#8844cc',
|
||||
metal: '#aabbcc', electric: '#ffff00', beast: '#cc6622', cosmic: '#cc44ff',
|
||||
}
|
||||
const category =
|
||||
['phoenix', 'dragon', 'demon'].includes(pick) ? 'fire' :
|
||||
['penguin', 'snowman', 'yeti'].includes(pick) ? 'ice' :
|
||||
['cactus', 'mushroom', 'frog', 'snail', 'turtle'].includes(pick) ? 'nature' :
|
||||
['skeleton', 'ghost', 'vampire', 'zombie', 'witch'].includes(pick) ? 'dark' :
|
||||
['robot', 'android', 'mech', 'toaster', 'knight'].includes(pick) ? 'metal' :
|
||||
['alien', 'unicorn', 'mermaid'].includes(pick) ? 'cosmic' :
|
||||
['werewolf', 'minotaur', 'lion', 'shark', 'crocodile'].includes(pick) ? 'beast' :
|
||||
'electric'
|
||||
fighter.color = safeColor(tints[category] || '#ffd700')
|
||||
fighter.opacity = 0.9
|
||||
|
||||
return pick
|
||||
}
|
||||
|
||||
// Get morph order for a bot (deterministic by seed)
|
||||
function getMorphOrder(seed: string): number[] {
|
||||
let h = 0
|
||||
for (let i = 0; i < seed.length; i++) h = ((h << 5) - h + seed.charCodeAt(i)) | 0
|
||||
const orders = [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]]
|
||||
return orders[Math.abs(h) % 6]
|
||||
}
|
||||
|
||||
async function playMorph(fighter: any, side: 'a' | 'b', morphIndex: number) {
|
||||
const savedScaleX = fighter.scale.x
|
||||
const savedScaleY = fighter.scale.y
|
||||
const isHumanFighter = side === 'a' ? isHumanA : isHumanB
|
||||
const morphSpriteKey = side === 'a' ? 'botA_morph' : 'botB_morph'
|
||||
const normalSpriteKey = side === 'a' ? 'botA' : 'botB'
|
||||
|
||||
// Flash + announcement
|
||||
screenFlash('#ffffff', 0.15)
|
||||
sfxZoomWhoosh()
|
||||
sfxSpecial()
|
||||
const name = side === 'a' ? botA.name : botB.name
|
||||
announceHype(`${name} — ${morphNames[morphIndex]}!`)
|
||||
|
||||
// Scale up dramatically
|
||||
const growFactor = 1.3
|
||||
await k.tween(0, 1, 0.3, (t) => {
|
||||
fighter.scale.x = savedScaleX * (1 + t * (growFactor - 1))
|
||||
fighter.scale.y = savedScaleY * (1 + t * (growFactor - 1))
|
||||
}, k.easings.easeOutBack)
|
||||
|
||||
// Human fighters morph into bots (opposite direction!)
|
||||
if (isHumanFighter) {
|
||||
fighter.use(k.sprite(morphSpriteKey, { anim: 'idle' }))
|
||||
}
|
||||
|
||||
// Apply the morph visuals
|
||||
morphAppliers[morphIndex](fighter, side)
|
||||
|
||||
// Shockwave + shake
|
||||
spawnShockwave(fighter.pos.x, fighter.pos.y - 20, '#ffffff')
|
||||
k.shake(8)
|
||||
glitchRGB(0.2)
|
||||
|
||||
return {
|
||||
revert: async () => {
|
||||
destroyMorphOverlays()
|
||||
screenFlash(theme.accent, 0.1)
|
||||
fighter.color = safeColor('#ffffff')
|
||||
fighter.opacity = 1
|
||||
// Revert human fighters back to their human sprite
|
||||
if (isHumanFighter) {
|
||||
fighter.use(k.sprite(normalSpriteKey, { anim: 'idle' }))
|
||||
}
|
||||
await k.tween(0, 1, 0.2, (t) => {
|
||||
fighter.scale.x = savedScaleX * growFactor + t * (savedScaleX - savedScaleX * growFactor)
|
||||
fighter.scale.y = savedScaleY * growFactor + t * (savedScaleY - savedScaleY * growFactor)
|
||||
}, k.easings.easeInQuad)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return { destroyMorphOverlays, playMorph, applyCreatorOmniMorph, getMorphOrder }
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user