feat: 9 diverse brawl variants — knockdowns, wall bounces, suplexes, haymakers, dizzy staggers
Add knockdown, wall-bounce, dizzy-stagger, suplex, ping-pong, ground-pound, haymaker, body-check brawl variants with dizzy stars, angle physics, and screen-edge impacts. Fix missing synthStyle on music tracks 10. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
ba1deab0f6
commit
63960b5faf
+491
-99
@@ -9136,57 +9136,491 @@ export async function createFightScene(config: FightSceneConfig) {
|
||||
},
|
||||
|
||||
// === PHYSICAL CONTACT SYSTEMS ===
|
||||
// Brawl exchange: fighters close distance and trade rapid blows at close range
|
||||
async _brawlExchange(winningSide: 'a' | 'b' | null, intensity: number) {
|
||||
// Helper: spawn dizzy stars orbiting a fighter's head
|
||||
_spawnDizzyStars(fighter: any, duration: number) {
|
||||
const stars: any[] = []
|
||||
const starChars = ['*', '\u2605', '\u00d7', '!', '?']
|
||||
const starColors = ['#ffff00', '#ffffff', '#ff4444', '#44ff44', '#ff88ff']
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const s = k.add([
|
||||
k.text(starChars[i % starChars.length], { size: 8 }),
|
||||
k.pos(fighter.pos.x, fighter.pos.y - 55),
|
||||
k.color(safeColor(k, starColors[i % starColors.length])),
|
||||
k.opacity(0.9), k.z(55), k.anchor('center'),
|
||||
])
|
||||
stars.push(s)
|
||||
}
|
||||
const startTime = k.time()
|
||||
const cancel = k.onUpdate(() => {
|
||||
const elapsed = k.time() - startTime
|
||||
if (elapsed > duration || !fighter.exists()) {
|
||||
stars.forEach(s => { if (s.exists()) s.destroy() })
|
||||
cancel.cancel()
|
||||
return
|
||||
}
|
||||
for (let i = 0; i < stars.length; i++) {
|
||||
if (!stars[i].exists()) continue
|
||||
const angle = elapsed * 4 + (i * Math.PI * 2) / stars.length
|
||||
stars[i].pos.x = fighter.pos.x + Math.cos(angle) * 22
|
||||
stars[i].pos.y = fighter.pos.y - 55 + Math.sin(angle) * 8
|
||||
stars[i].opacity = Math.max(0, 1 - elapsed / duration)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// Helper: reset both fighters to home positions
|
||||
async _resetPositions() {
|
||||
const fA = k.get('fighterA')[0]
|
||||
const fB = k.get('fighterB')[0]
|
||||
if (!fA || !fB) return
|
||||
|
||||
// Close distance — both fighters move to center
|
||||
const midX = W / 2
|
||||
const contactA = midX - 30
|
||||
const contactB = midX + 30
|
||||
sfxZoomWhoosh()
|
||||
await Promise.all([
|
||||
k.tween(fA.pos.x, contactA, 0.12, (v) => { fA.pos.x = v }, k.easings.easeOutQuad),
|
||||
k.tween(fB.pos.x, contactB, 0.12, (v) => { fB.pos.x = v }, k.easings.easeOutQuad),
|
||||
])
|
||||
|
||||
// Trade blows — number of hits scales with intensity
|
||||
const hitCount = 4 + Math.floor(intensity * 6) + Math.floor(Math.random() * 3) // 4-13 hits
|
||||
const anims = ['attack', 'kick', 'special', 'attack', 'kick'] as const
|
||||
const hitColors = ['#ff2d7b', '#ff6600', '#ffcc00', '#00f0ff', '#ff2d2d', '#39ff14', '#ffffff', '#b83dff']
|
||||
|
||||
for (let i = 0; i < hitCount; i++) {
|
||||
const aAttacks = winningSide === 'a' ? Math.random() < 0.65 : winningSide === 'b' ? Math.random() < 0.35 : Math.random() < 0.5
|
||||
const attacker = aAttacks ? fA : fB
|
||||
const defender = aAttacks ? fB : fA
|
||||
const anim = anims[i % anims.length]
|
||||
|
||||
attacker.play(anim)
|
||||
sfxPunch()
|
||||
defender.play('hit')
|
||||
k.shake(2 + Math.floor(intensity * 3))
|
||||
|
||||
// Sparks at contact point
|
||||
const sparkX = (contactA + contactB) / 2 + (Math.random() - 0.5) * 20
|
||||
const sparkY = GROUND_Y - 20 - Math.random() * 30
|
||||
spawnSparks(sparkX, sparkY, 3, hitColors[i % hitColors.length])
|
||||
|
||||
// Small positional shifts during brawl — fighters push each other
|
||||
const pushDir = aAttacks ? 1 : -1
|
||||
defender.pos.x += pushDir * (3 + Math.random() * 4)
|
||||
|
||||
// Timing varies — faster hits at higher intensity
|
||||
await k.wait(0.04 + (1 - intensity) * 0.04)
|
||||
}
|
||||
|
||||
// After brawl: both stagger apart
|
||||
fA.play('idle'); fB.play('idle')
|
||||
await Promise.all([
|
||||
k.tween(fA.pos.x, HOME_A, 0.2, (v) => { fA.pos.x = v }, k.easings.easeOutQuad),
|
||||
k.tween(fB.pos.x, HOME_B, 0.2, (v) => { fB.pos.x = v }, k.easings.easeOutQuad),
|
||||
k.tween(fA.pos.y, GROUND_Y, 0.15, (v) => { fA.pos.y = v }),
|
||||
k.tween(fB.pos.y, GROUND_Y, 0.15, (v) => { fB.pos.y = v }),
|
||||
])
|
||||
fA.angle = 0; fB.angle = 0; fA.opacity = 1; fB.opacity = 1
|
||||
fA.play('idle'); fB.play('idle')
|
||||
},
|
||||
|
||||
// 0: RAPID BRAWL — close distance, trade rapid blows
|
||||
async _brawlRapid(winningSide: 'a' | 'b' | null, intensity: number) {
|
||||
const fA = k.get('fighterA')[0]
|
||||
const fB = k.get('fighterB')[0]
|
||||
if (!fA || !fB) return
|
||||
const midX = W / 2
|
||||
sfxZoomWhoosh()
|
||||
await Promise.all([
|
||||
k.tween(fA.pos.x, midX - 30, 0.12, (v) => { fA.pos.x = v }, k.easings.easeOutQuad),
|
||||
k.tween(fB.pos.x, midX + 30, 0.12, (v) => { fB.pos.x = v }, k.easings.easeOutQuad),
|
||||
])
|
||||
const hitCount = 4 + Math.floor(intensity * 6) + Math.floor(Math.random() * 3)
|
||||
const anims = ['attack', 'kick', 'special', 'attack', 'kick'] as const
|
||||
const hitColors = ['#ff2d7b', '#ff6600', '#ffcc00', '#00f0ff', '#ff2d2d', '#39ff14', '#ffffff', '#b83dff']
|
||||
for (let i = 0; i < hitCount; i++) {
|
||||
const aHits = winningSide === 'a' ? Math.random() < 0.65 : winningSide === 'b' ? Math.random() < 0.35 : Math.random() < 0.5
|
||||
const atk = aHits ? fA : fB; const def = aHits ? fB : fA
|
||||
atk.play(anims[i % anims.length]); sfxPunch(); def.play('hit')
|
||||
k.shake(2 + Math.floor(intensity * 3))
|
||||
spawnSparks(midX + (Math.random() - 0.5) * 20, GROUND_Y - 20 - Math.random() * 30, 3, hitColors[i % hitColors.length])
|
||||
def.pos.x += (aHits ? 1 : -1) * (3 + Math.random() * 4)
|
||||
await k.wait(0.04 + (1 - intensity) * 0.04)
|
||||
}
|
||||
await this._resetPositions()
|
||||
},
|
||||
|
||||
// 1: KNOCKDOWN BRAWL — combo ends with loser knocked flat, has to get back up
|
||||
async _brawlKnockdown(winningSide: 'a' | 'b' | null, intensity: number) {
|
||||
const fA = k.get('fighterA')[0]
|
||||
const fB = k.get('fighterB')[0]
|
||||
if (!fA || !fB) return
|
||||
const winner = winningSide === 'a' ? fA : winningSide === 'b' ? fB : (Math.random() > 0.5 ? fA : fB)
|
||||
const loser = winner === fA ? fB : fA
|
||||
const dir = winner === fA ? 1 : -1
|
||||
|
||||
// Rush in, quick combo
|
||||
sfxZoomWhoosh()
|
||||
const contactX = loser.pos.x - dir * 40
|
||||
await k.tween(winner.pos.x, contactX, 0.1, (v) => { winner.pos.x = v }, k.easings.easeInQuad)
|
||||
const hits = 3 + Math.floor(intensity * 3)
|
||||
for (let i = 0; i < hits; i++) {
|
||||
winner.play(i % 2 === 0 ? 'attack' : 'kick'); sfxPunch()
|
||||
loser.play('hit'); k.shake(3 + i)
|
||||
spawnSparks(loser.pos.x, loser.pos.y - 20 - Math.random() * 20, 3, '#ff6600')
|
||||
loser.pos.x += dir * 5
|
||||
await k.wait(0.05)
|
||||
}
|
||||
// Final hit — KNOCKDOWN
|
||||
winner.play('special'); sfxCritical(); sfxExplosion()
|
||||
loser.play('knockback'); k.shake(12)
|
||||
spawnSparks(loser.pos.x, loser.pos.y - 25, 12, '#ff2d2d')
|
||||
screenFlash('#ff2d2d', 0.1)
|
||||
if (Math.random() < 0.5) sfxRandomComedy()
|
||||
// Loser falls to ground — scale Y to flatten, rotate
|
||||
await Promise.all([
|
||||
k.tween(loser.pos.x, loser.pos.x + dir * 80, 0.25, (v) => { loser.pos.x = v }, k.easings.easeOutQuad),
|
||||
k.tween(loser.pos.y, GROUND_Y + 5, 0.2, (v) => { loser.pos.y = v }, k.easings.easeInQuad),
|
||||
k.tween(0, dir * 90, 0.25, (v) => { loser.angle = v }),
|
||||
])
|
||||
sfxBonk(); k.shake(6)
|
||||
spawnEmoteText(loser.pos.x, loser.pos.y - 20, 'DOWN!', '#ff4444')
|
||||
// Loser lies there for a beat
|
||||
await k.wait(0.4 + (1 - intensity) * 0.4)
|
||||
// Slowly get back up — stagger, wobble
|
||||
this._spawnDizzyStars(loser, 1.5)
|
||||
spawnEmoteText(loser.pos.x, loser.pos.y - 40, '...ugh', '#aaaaaa')
|
||||
await k.tween(loser.angle, 0, 0.3, (v) => { loser.angle = v }, k.easings.easeOutBack)
|
||||
loser.play('hit')
|
||||
// Stagger wobble while getting up
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await k.tween(loser.pos.x, loser.pos.x + (i % 2 === 0 ? -12 : 12), 0.12, (v) => { loser.pos.x = v })
|
||||
}
|
||||
await k.wait(0.2)
|
||||
await this._resetPositions()
|
||||
},
|
||||
|
||||
// 2: WALL BOUNCE — punch sends fighter flying into screen edge, bounces off
|
||||
async _brawlWallBounce(winningSide: 'a' | 'b' | null, intensity: number) {
|
||||
const fA = k.get('fighterA')[0]
|
||||
const fB = k.get('fighterB')[0]
|
||||
if (!fA || !fB) return
|
||||
const winner = winningSide === 'a' ? fA : winningSide === 'b' ? fB : (Math.random() > 0.5 ? fA : fB)
|
||||
const loser = winner === fA ? fB : fA
|
||||
const dir = winner === fA ? 1 : -1
|
||||
const wallX = dir === 1 ? W - 15 : 15
|
||||
|
||||
// Rush in for the big hit
|
||||
sfxZoomWhoosh()
|
||||
await k.tween(winner.pos.x, loser.pos.x - dir * 40, 0.1, (v) => { winner.pos.x = v }, k.easings.easeInQuad)
|
||||
winner.play('special'); sfxCritical()
|
||||
await k.wait(0.06)
|
||||
loser.play('knockback'); k.shake(15)
|
||||
spawnSparks(loser.pos.x, loser.pos.y - 25, 15, '#ff2d7b')
|
||||
screenFlash('#ffffff', 0.08)
|
||||
|
||||
// Loser flies across to wall
|
||||
await Promise.all([
|
||||
k.tween(loser.pos.x, wallX, 0.15, (v) => { loser.pos.x = v }, k.easings.easeInQuad),
|
||||
k.tween(loser.pos.y, GROUND_Y - 30, 0.08, (v) => { loser.pos.y = v }, k.easings.easeOutQuad).then(() =>
|
||||
k.tween(loser.pos.y, GROUND_Y, 0.07, (v) => { loser.pos.y = v }, k.easings.easeInQuad)
|
||||
),
|
||||
])
|
||||
// WALL IMPACT
|
||||
sfxExplosion(); k.shake(20)
|
||||
spawnShockwave(wallX, GROUND_Y, '#ff6600')
|
||||
spawnSparks(wallX, GROUND_Y - 40, 15, '#ffcc00')
|
||||
screenFlash('#ff6600', 0.12)
|
||||
sfxBoneCrack()
|
||||
spawnEmoteText(wallX, GROUND_Y - 70, 'WALL!', '#ff6600')
|
||||
|
||||
// Bounce off wall — loser rebounds back toward center
|
||||
loser.play('hit')
|
||||
const bounceX = wallX - dir * (60 + Math.random() * 40)
|
||||
await Promise.all([
|
||||
k.tween(loser.pos.x, bounceX, 0.2, (v) => { loser.pos.x = v }, k.easings.easeOutQuad),
|
||||
k.tween(loser.pos.y, GROUND_Y - 50, 0.1, (v) => { loser.pos.y = v }, k.easings.easeOutQuad).then(() =>
|
||||
k.tween(loser.pos.y, GROUND_Y, 0.1, (v) => { loser.pos.y = v }, k.easings.easeInQuad)
|
||||
),
|
||||
k.tween(0, dir * -360, 0.2, (v) => { loser.angle = v }),
|
||||
])
|
||||
loser.angle = 0
|
||||
sfxBonk(); k.shake(5)
|
||||
|
||||
// Dizzy stagger after wall bounce
|
||||
this._spawnDizzyStars(loser, 1.2)
|
||||
loser.play('hit')
|
||||
for (let w = 0; w < 4; w++) {
|
||||
await k.tween(loser.pos.x, loser.pos.x + (w % 2 === 0 ? -10 : 10), 0.1, (v) => { loser.pos.x = v })
|
||||
}
|
||||
await k.wait(0.3)
|
||||
winner.play('win'); spawnEmoteText(winner.pos.x, winner.pos.y - 55, 'GET REKT', '#39ff14')
|
||||
await k.wait(0.3)
|
||||
await this._resetPositions()
|
||||
},
|
||||
|
||||
// 3: DIZZY STAGGER — sustained combo leaves loser wobbling around the screen
|
||||
async _brawlDizzyStagger(winningSide: 'a' | 'b' | null, intensity: number) {
|
||||
const fA = k.get('fighterA')[0]
|
||||
const fB = k.get('fighterB')[0]
|
||||
if (!fA || !fB) return
|
||||
const winner = winningSide === 'a' ? fA : winningSide === 'b' ? fB : (Math.random() > 0.5 ? fA : fB)
|
||||
const loser = winner === fA ? fB : fA
|
||||
const dir = winner === fA ? 1 : -1
|
||||
|
||||
// Rush in, big combo
|
||||
sfxZoomWhoosh()
|
||||
await k.tween(winner.pos.x, loser.pos.x - dir * 35, 0.1, (v) => { winner.pos.x = v }, k.easings.easeInQuad)
|
||||
const combo = 5 + Math.floor(intensity * 4)
|
||||
for (let i = 0; i < combo; i++) {
|
||||
winner.play(['attack', 'kick', 'special'][i % 3]); sfxPunch()
|
||||
loser.play('hit'); k.shake(3 + Math.floor(i * 0.5))
|
||||
spawnSparks((winner.pos.x + loser.pos.x) / 2, GROUND_Y - 25 - Math.random() * 20, 3, i % 2 === 0 ? '#ff2d7b' : '#ffcc00')
|
||||
loser.pos.x += dir * 4
|
||||
await k.wait(0.04)
|
||||
}
|
||||
// Final uppercut
|
||||
winner.play('special'); sfxCritical(); k.shake(10)
|
||||
spawnSparks(loser.pos.x, loser.pos.y - 30, 10, '#ffe14d')
|
||||
await k.tween(loser.pos.y, GROUND_Y - 40, 0.1, (v) => { loser.pos.y = v }, k.easings.easeOutQuad)
|
||||
await k.tween(loser.pos.y, GROUND_Y, 0.1, (v) => { loser.pos.y = v }, k.easings.easeInQuad)
|
||||
sfxBonk()
|
||||
|
||||
// DIZZY PHASE — loser staggers around aimlessly
|
||||
this._spawnDizzyStars(loser, 2.5)
|
||||
loser.play('hit')
|
||||
spawnEmoteText(loser.pos.x, loser.pos.y - 50, '...wha?', '#ffff88')
|
||||
winner.play('idle')
|
||||
// Stagger path — zigzag drunkenly
|
||||
const staggerPoints = [
|
||||
loser.pos.x + dir * 30, loser.pos.x - dir * 50,
|
||||
loser.pos.x + dir * 20, loser.pos.x - dir * 40,
|
||||
loser.pos.x + dir * 10,
|
||||
]
|
||||
for (let s = 0; s < staggerPoints.length; s++) {
|
||||
await Promise.all([
|
||||
k.tween(loser.pos.x, staggerPoints[s], 0.2, (v) => { loser.pos.x = v }),
|
||||
k.tween(loser.angle, (s % 2 === 0 ? 10 : -10), 0.2, (v) => { loser.angle = v }),
|
||||
])
|
||||
if (s % 2 === 0) sfxBoing()
|
||||
}
|
||||
loser.angle = 0
|
||||
// Shake it off
|
||||
spawnEmoteText(loser.pos.x, loser.pos.y - 40, '...ok', '#88ff88')
|
||||
await k.wait(0.3)
|
||||
await this._resetPositions()
|
||||
},
|
||||
|
||||
// 4: SUPLEX SLAM — grab, lift overhead, slam to ground with bounce
|
||||
async _brawlSuplex(attackerSide: 'a' | 'b', intensity: number) {
|
||||
const attacker = k.get(attackerSide === 'a' ? 'fighterA' : 'fighterB')[0]
|
||||
const defender = k.get(attackerSide === 'a' ? 'fighterB' : 'fighterA')[0]
|
||||
if (!attacker || !defender) return
|
||||
const dir = attackerSide === 'a' ? 1 : -1
|
||||
|
||||
// Rush in and grab
|
||||
sfxZoomWhoosh()
|
||||
await k.tween(attacker.pos.x, defender.pos.x - dir * 30, 0.1, (v) => { attacker.pos.x = v }, k.easings.easeInQuad)
|
||||
attacker.play('special'); sfxBlock()
|
||||
await k.wait(0.1)
|
||||
// Lift defender overhead
|
||||
attacker.play('attack')
|
||||
const liftX = attacker.pos.x
|
||||
await Promise.all([
|
||||
k.tween(defender.pos.x, liftX + dir * 5, 0.15, (v) => { defender.pos.x = v }),
|
||||
k.tween(defender.pos.y, GROUND_Y - 120, 0.2, (v) => { defender.pos.y = v }, k.easings.easeOutQuad),
|
||||
k.tween(0, 180, 0.2, (v) => { defender.angle = v }),
|
||||
])
|
||||
sfxPowerUp()
|
||||
spawnEmoteText(attacker.pos.x, attacker.pos.y - 70, 'SUPLEX!', '#ff4444')
|
||||
await k.wait(0.25)
|
||||
// SLAM DOWN
|
||||
sfxExplosion()
|
||||
await Promise.all([
|
||||
k.tween(defender.pos.y, GROUND_Y + 5, 0.1, (v) => { defender.pos.y = v }, k.easings.easeInQuad),
|
||||
k.tween(defender.angle, 360, 0.1, (v) => { defender.angle = v }),
|
||||
])
|
||||
defender.angle = 0; defender.play('knockback')
|
||||
k.shake(22); screenFlash('#ff2d2d', 0.15)
|
||||
spawnShockwave(defender.pos.x, GROUND_Y, '#ff2d2d')
|
||||
spawnSparks(defender.pos.x, GROUND_Y - 10, 20, '#ff6600')
|
||||
sfxBoneCrack()
|
||||
// Ground bounce
|
||||
await k.tween(defender.pos.y, GROUND_Y - 40, 0.1, (v) => { defender.pos.y = v }, k.easings.easeOutQuad)
|
||||
sfxBoing()
|
||||
await k.tween(defender.pos.y, GROUND_Y, 0.1, (v) => { defender.pos.y = v }, k.easings.easeInQuad)
|
||||
sfxBonk(); k.shake(5)
|
||||
// Bounce 2 (smaller)
|
||||
await k.tween(defender.pos.y, GROUND_Y - 15, 0.08, (v) => { defender.pos.y = v }, k.easings.easeOutQuad)
|
||||
await k.tween(defender.pos.y, GROUND_Y, 0.08, (v) => { defender.pos.y = v }, k.easings.easeInQuad)
|
||||
// Dizzy on the ground
|
||||
this._spawnDizzyStars(defender, 1.5)
|
||||
defender.play('hit')
|
||||
await k.wait(0.5)
|
||||
attacker.play('win')
|
||||
await k.wait(0.3)
|
||||
await this._resetPositions()
|
||||
},
|
||||
|
||||
// 5: PING PONG VOLLEY — fighters knock each other back and forth across the screen
|
||||
async _brawlPingPong(winningSide: 'a' | 'b' | null, intensity: number) {
|
||||
const fA = k.get('fighterA')[0]
|
||||
const fB = k.get('fighterB')[0]
|
||||
if (!fA || !fB) return
|
||||
const volleys = 3 + Math.floor(intensity * 3) + Math.floor(Math.random() * 2) // 3-8
|
||||
|
||||
// Start in center
|
||||
sfxZoomWhoosh()
|
||||
await Promise.all([
|
||||
k.tween(fA.pos.x, W / 2 - 30, 0.1, (v) => { fA.pos.x = v }, k.easings.easeOutQuad),
|
||||
k.tween(fB.pos.x, W / 2 + 30, 0.1, (v) => { fB.pos.x = v }, k.easings.easeOutQuad),
|
||||
])
|
||||
|
||||
for (let v = 0; v < volleys; v++) {
|
||||
const aHits = v % 2 === 0
|
||||
const atk = aHits ? fA : fB
|
||||
const def = aHits ? fB : fA
|
||||
const pDir = aHits ? 1 : -1
|
||||
const pushDist = 50 + Math.random() * 40
|
||||
|
||||
atk.play(v % 3 === 0 ? 'attack' : v % 3 === 1 ? 'kick' : 'special')
|
||||
sfxPunch(); def.play('hit'); k.shake(5 + v)
|
||||
spawnSparks((atk.pos.x + def.pos.x) / 2, GROUND_Y - 30, 5, v % 2 === 0 ? '#ff2d7b' : '#00f0ff')
|
||||
|
||||
// Defender flies back
|
||||
await Promise.all([
|
||||
k.tween(def.pos.x, def.pos.x + pDir * pushDist, 0.1, (v) => { def.pos.x = v }, k.easings.easeOutQuad),
|
||||
k.tween(def.pos.y, GROUND_Y - 25, 0.05, (v) => { def.pos.y = v }, k.easings.easeOutQuad).then(() =>
|
||||
k.tween(def.pos.y, GROUND_Y, 0.05, (v) => { def.pos.y = v }, k.easings.easeInQuad)
|
||||
),
|
||||
])
|
||||
sfxBonk()
|
||||
// Attacker chases
|
||||
await k.tween(atk.pos.x, def.pos.x - pDir * 35, 0.08, (v) => { atk.pos.x = v }, k.easings.easeInQuad)
|
||||
await k.wait(0.03)
|
||||
}
|
||||
// Final hit sends loser sliding
|
||||
const finalAtk = winningSide === 'a' ? fA : winningSide === 'b' ? fB : (volleys % 2 === 0 ? fA : fB)
|
||||
const finalDef = finalAtk === fA ? fB : fA
|
||||
finalAtk.play('special'); sfxCritical(); k.shake(12)
|
||||
finalDef.play('knockback')
|
||||
spawnSparks(finalDef.pos.x, finalDef.pos.y - 25, 12, '#ffe14d')
|
||||
if (Math.random() < 0.5) sfxRandomComedy()
|
||||
await k.wait(0.3)
|
||||
await this._resetPositions()
|
||||
},
|
||||
|
||||
// 6: GROUND AND POUND — attacker pins defender, hits them on the ground
|
||||
async _brawlGroundPound(attackerSide: 'a' | 'b', intensity: number) {
|
||||
const attacker = k.get(attackerSide === 'a' ? 'fighterA' : 'fighterB')[0]
|
||||
const defender = k.get(attackerSide === 'a' ? 'fighterB' : 'fighterA')[0]
|
||||
if (!attacker || !defender) return
|
||||
const dir = attackerSide === 'a' ? 1 : -1
|
||||
|
||||
// Rush in and tackle
|
||||
sfxZoomWhoosh()
|
||||
await k.tween(attacker.pos.x, defender.pos.x - dir * 30, 0.08, (v) => { attacker.pos.x = v }, k.easings.easeInQuad)
|
||||
attacker.play('attack'); sfxPunch()
|
||||
defender.play('knockback'); k.shake(8)
|
||||
// Defender falls to ground (flatten angle)
|
||||
await Promise.all([
|
||||
k.tween(defender.pos.x, defender.pos.x + dir * 20, 0.15, (v) => { defender.pos.x = v }),
|
||||
k.tween(0, dir * 85, 0.15, (v) => { defender.angle = v }),
|
||||
])
|
||||
sfxBonk()
|
||||
// Attacker moves on top
|
||||
await k.tween(attacker.pos.x, defender.pos.x - dir * 15, 0.08, (v) => { attacker.pos.x = v })
|
||||
|
||||
// Ground pound — rapid hits on downed fighter
|
||||
const poundHits = 4 + Math.floor(intensity * 5)
|
||||
for (let i = 0; i < poundHits; i++) {
|
||||
attacker.play(i % 2 === 0 ? 'attack' : 'kick')
|
||||
sfxPunch()
|
||||
k.shake(2 + Math.floor(i * 0.4))
|
||||
spawnSparks(defender.pos.x + (Math.random() - 0.5) * 20, defender.pos.y - 10, 2, i % 2 === 0 ? '#ff2d7b' : '#ff6600')
|
||||
defender.pos.y += (i % 2 === 0 ? -2 : 2) // jostle
|
||||
await k.wait(0.06 + (1 - intensity) * 0.04)
|
||||
}
|
||||
// Attacker gets up, backs off
|
||||
attacker.play('win')
|
||||
sfxRandomComedy()
|
||||
await k.wait(0.3)
|
||||
// Defender slowly gets up
|
||||
this._spawnDizzyStars(defender, 1.8)
|
||||
await k.tween(defender.angle, 0, 0.4, (v) => { defender.angle = v }, k.easings.easeOutBack)
|
||||
defender.play('hit')
|
||||
spawnEmoteText(defender.pos.x, defender.pos.y - 45, '...ouch', '#ff8888')
|
||||
await k.wait(0.4)
|
||||
await this._resetPositions()
|
||||
},
|
||||
|
||||
// 7: HAYMAKER — slow dramatic wind-up, pause, devastating single hit with knockdown
|
||||
async _brawlHaymaker(attackerSide: 'a' | 'b', _intensity: number) {
|
||||
const attacker = k.get(attackerSide === 'a' ? 'fighterA' : 'fighterB')[0]
|
||||
const defender = k.get(attackerSide === 'a' ? 'fighterB' : 'fighterA')[0]
|
||||
if (!attacker || !defender) return
|
||||
const dir = attackerSide === 'a' ? 1 : -1
|
||||
|
||||
// Slow walk toward defender
|
||||
attacker.play('kick')
|
||||
await k.tween(attacker.pos.x, defender.pos.x - dir * 45, 0.4, (v) => { attacker.pos.x = v }, k.easings.easeInOutQuad)
|
||||
// Wind-up — lean back
|
||||
attacker.play('special')
|
||||
sfxDrumRoll()
|
||||
await k.tween(attacker.pos.x, attacker.pos.x - dir * 20, 0.3, (v) => { attacker.pos.x = v }, k.easings.easeOutQuad)
|
||||
spawnEmoteText(attacker.pos.x, attacker.pos.y - 55, '...', '#ffffff')
|
||||
await k.wait(0.5) // dramatic pause
|
||||
// HAYMAKER
|
||||
sfxCritical(); sfxExplosion()
|
||||
attacker.play('attack')
|
||||
await k.tween(attacker.pos.x, defender.pos.x - dir * 25, 0.04, (v) => { attacker.pos.x = v }, k.easings.easeInQuad)
|
||||
defender.play('knockback'); k.shake(25)
|
||||
screenFlash('#ffffff', 0.15)
|
||||
spawnSparks(defender.pos.x, defender.pos.y - 25, 20, '#ff2d2d')
|
||||
spawnShockwave(defender.pos.x, GROUND_Y, '#ff2d2d')
|
||||
sfxVineBoom()
|
||||
// Defender flies across screen with spin
|
||||
const flyX = defender.pos.x + dir * 150
|
||||
await Promise.all([
|
||||
k.tween(defender.pos.x, Math.min(W - 10, Math.max(10, flyX)), 0.25, (v) => { defender.pos.x = v }, k.easings.easeOutQuad),
|
||||
k.tween(defender.pos.y, GROUND_Y - 80, 0.12, (v) => { defender.pos.y = v }, k.easings.easeOutQuad).then(() =>
|
||||
k.tween(defender.pos.y, GROUND_Y, 0.13, (v) => { defender.pos.y = v }, k.easings.easeInQuad)
|
||||
),
|
||||
k.tween(0, dir * 720, 0.25, (v) => { defender.angle = v }),
|
||||
])
|
||||
defender.angle = 0; sfxBonk(); k.shake(8)
|
||||
// Knockdown — rotate flat
|
||||
await k.tween(0, dir * 90, 0.15, (v) => { defender.angle = v })
|
||||
spawnEmoteText(defender.pos.x, defender.pos.y - 20, 'K.O.?!', '#ff4444')
|
||||
this._spawnDizzyStars(defender, 2.0)
|
||||
await k.wait(0.6)
|
||||
// Get up slowly
|
||||
await k.tween(defender.angle, 0, 0.35, (v) => { defender.angle = v }, k.easings.easeOutBack)
|
||||
defender.play('hit')
|
||||
await k.wait(0.3)
|
||||
attacker.play('idle')
|
||||
await this._resetPositions()
|
||||
},
|
||||
|
||||
// 8: BODY CHECK BOUNCER — both charge, collide, one bounces off the other
|
||||
async _brawlBodyCheck(winningSide: 'a' | 'b' | null, _intensity: number) {
|
||||
const fA = k.get('fighterA')[0]
|
||||
const fB = k.get('fighterB')[0]
|
||||
if (!fA || !fB) return
|
||||
const winner = winningSide === 'a' ? fA : winningSide === 'b' ? fB : (Math.random() > 0.5 ? fA : fB)
|
||||
const loser = winner === fA ? fB : fA
|
||||
const dir = winner === fA ? 1 : -1
|
||||
const midX = W / 2
|
||||
|
||||
// Both charge at each other
|
||||
fA.play('special'); fB.play('special')
|
||||
sfxZoomWhoosh()
|
||||
await Promise.all([
|
||||
k.tween(fA.pos.x, midX - 20, 0.12, (v) => { fA.pos.x = v }, k.easings.easeInQuad),
|
||||
k.tween(fB.pos.x, midX + 20, 0.12, (v) => { fB.pos.x = v }, k.easings.easeInQuad),
|
||||
])
|
||||
// COLLISION
|
||||
sfxCritical(); k.shake(18)
|
||||
spawnSparks(midX, GROUND_Y - 40, 15, '#ffffff')
|
||||
screenFlash('#ffffff', 0.1)
|
||||
spawnShockwave(midX, GROUND_Y, '#ffcc00')
|
||||
// Winner holds ground, loser bounces off
|
||||
winner.play('win')
|
||||
loser.play('knockback')
|
||||
const bounceX = loser === fA ? -20 : W + 20
|
||||
await Promise.all([
|
||||
k.tween(loser.pos.x, bounceX, 0.2, (v) => { loser.pos.x = v }, k.easings.easeOutQuad),
|
||||
k.tween(loser.pos.y, GROUND_Y - 60, 0.1, (v) => { loser.pos.y = v }, k.easings.easeOutQuad).then(() =>
|
||||
k.tween(loser.pos.y, GROUND_Y, 0.1, (v) => { loser.pos.y = v }, k.easings.easeInQuad)
|
||||
),
|
||||
k.tween(0, (loser === fA ? -1 : 1) * 540, 0.2, (v) => { loser.angle = v }),
|
||||
])
|
||||
// Off screen! Slide back in
|
||||
loser.angle = 0; loser.play('hit')
|
||||
sfxSlideWhistleDown()
|
||||
const slideFrom = loser === fA ? -30 : W + 30
|
||||
loser.pos.x = slideFrom
|
||||
await k.tween(loser.pos.x, loser === fA ? HOME_A : HOME_B, 0.4, (v) => { loser.pos.x = v }, k.easings.easeOutBounce)
|
||||
this._spawnDizzyStars(loser, 1.5)
|
||||
sfxBoing()
|
||||
spawnEmoteText(loser.pos.x, loser.pos.y - 50, '...what happened?', '#ffff88')
|
||||
await k.wait(0.5)
|
||||
await this._resetPositions()
|
||||
},
|
||||
|
||||
// Dispatch: pick a random physical exchange variant
|
||||
async _brawlExchange(winningSide: 'a' | 'b' | null, intensity: number) {
|
||||
const variant = Math.floor(Math.random() * 9)
|
||||
switch (variant) {
|
||||
case 0: return this._brawlRapid(winningSide, intensity)
|
||||
case 1: return this._brawlKnockdown(winningSide, intensity)
|
||||
case 2: return this._brawlWallBounce(winningSide, intensity)
|
||||
case 3: return this._brawlDizzyStagger(winningSide, intensity)
|
||||
case 4: return this._brawlSuplex(winningSide === 'a' ? 'a' : winningSide === 'b' ? 'b' : (Math.random() > 0.5 ? 'a' : 'b'), intensity)
|
||||
case 5: return this._brawlPingPong(winningSide, intensity)
|
||||
case 6: return this._brawlGroundPound(winningSide === 'a' ? 'a' : winningSide === 'b' ? 'b' : (Math.random() > 0.5 ? 'a' : 'b'), intensity)
|
||||
case 7: return this._brawlHaymaker(winningSide === 'a' ? 'a' : winningSide === 'b' ? 'b' : (Math.random() > 0.5 ? 'a' : 'b'), intensity)
|
||||
case 8: return this._brawlBodyCheck(winningSide, intensity)
|
||||
}
|
||||
},
|
||||
|
||||
// Clinch combo: grapple at close range, one fighter dominates with sustained contact
|
||||
@@ -9194,63 +9628,33 @@ export async function createFightScene(config: FightSceneConfig) {
|
||||
const attacker = k.get(attackerSide === 'a' ? 'fighterA' : 'fighterB')[0]
|
||||
const defender = k.get(attackerSide === 'a' ? 'fighterB' : 'fighterA')[0]
|
||||
if (!attacker || !defender) return
|
||||
|
||||
const dir = attackerSide === 'a' ? 1 : -1
|
||||
const origAX = attackerSide === 'a' ? HOME_A : HOME_B
|
||||
const origDX = attackerSide === 'a' ? HOME_B : HOME_A
|
||||
const clinchX = (origAX + origDX) / 2
|
||||
|
||||
// Rush in to clinch
|
||||
sfxZoomWhoosh()
|
||||
attacker.play('attack')
|
||||
await k.tween(attacker.pos.x, clinchX - dir * 25, 0.1, (v) => { attacker.pos.x = v }, k.easings.easeInQuad)
|
||||
await k.tween(defender.pos.x, clinchX + dir * 25, 0.08, (v) => { defender.pos.x = v }, k.easings.easeOutQuad)
|
||||
|
||||
// Clinch phase — sustained close-range hits with body contact
|
||||
const clinchHits = 3 + Math.floor(intensity * 5) + Math.floor(Math.random() * 3) // 3-11 hits
|
||||
const clinchHits = 3 + Math.floor(intensity * 5) + Math.floor(Math.random() * 3)
|
||||
const movePool = ['attack', 'kick', 'special'] as const
|
||||
|
||||
for (let i = 0; i < clinchHits; i++) {
|
||||
const move = movePool[i % movePool.length]
|
||||
attacker.play(move)
|
||||
sfxPunch()
|
||||
attacker.play(movePool[i % movePool.length]); sfxPunch()
|
||||
await k.wait(0.03)
|
||||
defender.play('hit')
|
||||
k.shake(2 + Math.floor(i * 0.5))
|
||||
|
||||
// Defender gets pushed back slightly, attacker follows
|
||||
defender.pos.x += dir * 4
|
||||
attacker.pos.x += dir * 3
|
||||
|
||||
// Sparks at the contact point between fighters
|
||||
spawnSparks(
|
||||
(attacker.pos.x + defender.pos.x) / 2,
|
||||
GROUND_Y - 25 - Math.random() * 20,
|
||||
2 + Math.floor(intensity * 3),
|
||||
i % 2 === 0 ? '#ff2d7b' : '#ffcc00',
|
||||
)
|
||||
|
||||
// Defender occasionally blocks or fights back (30% chance)
|
||||
defender.play('hit'); k.shake(2 + Math.floor(i * 0.5))
|
||||
defender.pos.x += dir * 4; attacker.pos.x += dir * 3
|
||||
spawnSparks((attacker.pos.x + defender.pos.x) / 2, GROUND_Y - 25 - Math.random() * 20, 2 + Math.floor(intensity * 3), i % 2 === 0 ? '#ff2d7b' : '#ffcc00')
|
||||
if (Math.random() < 0.3) {
|
||||
defender.play(movePool[Math.floor(Math.random() * movePool.length)])
|
||||
sfxBlock()
|
||||
attacker.play('hit')
|
||||
k.shake(3)
|
||||
attacker.pos.x -= dir * 5
|
||||
await k.wait(0.04)
|
||||
defender.play(movePool[Math.floor(Math.random() * movePool.length)]); sfxBlock()
|
||||
attacker.play('hit'); k.shake(3); attacker.pos.x -= dir * 5; await k.wait(0.04)
|
||||
}
|
||||
|
||||
await k.wait(0.04 + (1 - intensity) * 0.03)
|
||||
}
|
||||
|
||||
// Break apart — attacker shoves defender away
|
||||
sfxKick()
|
||||
defender.play('knockback')
|
||||
k.shake(6 + Math.floor(intensity * 6))
|
||||
sfxKick(); defender.play('knockback'); k.shake(6 + Math.floor(intensity * 6))
|
||||
spawnSparks(defender.pos.x, defender.pos.y - 25, 8, '#ff2d2d')
|
||||
await k.tween(defender.pos.x, origDX + dir * 30, 0.15, (v) => { defender.pos.x = v }, k.easings.easeOutQuad)
|
||||
attacker.play('idle')
|
||||
|
||||
await Promise.all([
|
||||
k.tween(attacker.pos.x, origAX, 0.2, (v) => { attacker.pos.x = v }, k.easings.easeOutQuad),
|
||||
k.tween(defender.pos.x, origDX, 0.25, (v) => { defender.pos.x = v }, k.easings.easeInOutQuad),
|
||||
@@ -9263,30 +9667,18 @@ export async function createFightScene(config: FightSceneConfig) {
|
||||
const defender = k.get(defenderSide === 'a' ? 'fighterA' : 'fighterB')[0]
|
||||
const attacker = k.get(defenderSide === 'a' ? 'fighterB' : 'fighterA')[0]
|
||||
if (!defender || !attacker) return
|
||||
|
||||
const dir = defenderSide === 'a' ? 1 : -1
|
||||
const origDX = defenderSide === 'a' ? HOME_A : HOME_B
|
||||
|
||||
// Quick retaliation — defender rushes in
|
||||
defender.play('attack')
|
||||
sfxZoomWhoosh()
|
||||
const contactX = attacker.pos.x - dir * 35
|
||||
await k.tween(defender.pos.x, contactX, 0.08, (v) => { defender.pos.x = v }, k.easings.easeInQuad)
|
||||
|
||||
// 2-3 quick counter hits
|
||||
defender.play('attack'); sfxZoomWhoosh()
|
||||
await k.tween(defender.pos.x, attacker.pos.x - dir * 35, 0.08, (v) => { defender.pos.x = v }, k.easings.easeInQuad)
|
||||
const counterHits = 2 + Math.floor(Math.random() * 2)
|
||||
for (let i = 0; i < counterHits; i++) {
|
||||
defender.play(i % 2 === 0 ? 'attack' : 'kick')
|
||||
sfxPunch()
|
||||
attacker.play('hit')
|
||||
k.shake(4)
|
||||
defender.play(i % 2 === 0 ? 'attack' : 'kick'); sfxPunch()
|
||||
attacker.play('hit'); k.shake(4)
|
||||
spawnSparks(attacker.pos.x + (Math.random() - 0.5) * 15, attacker.pos.y - 20 - Math.random() * 15, 3, '#00f0ff')
|
||||
await k.wait(0.05)
|
||||
}
|
||||
|
||||
// Return
|
||||
attacker.play('idle')
|
||||
defender.play('idle')
|
||||
attacker.play('idle'); defender.play('idle')
|
||||
await k.tween(defender.pos.x, origDX, 0.15, (v) => { defender.pos.x = v }, k.easings.easeOutQuad)
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user