refactor: extract pickChoreography to fight/choreography/picker.ts

Moves 103 lines of choreography selection logic out of FightScene.ts
into a dedicated module. FightScene re-exports for backward compat.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-09 06:31:22 +00:00
co-authored by Claude Opus 4.6
parent 38d7236be3
commit b6033a90e5
2 changed files with 114 additions and 114 deletions
+4 -114
View File
@@ -4,12 +4,10 @@ import type { GameObj, SpriteComp, PosComp, ScaleComp, AnchorComp, OpacityComp,
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, FightContext, ChoreoContext } from './fight/types' import type { Fighter, FightSceneConfig, RoundEvent, FightContext, ChoreoContext } 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 } from './fight/constants'
import { import { pickChoreography } from './fight/choreography/picker'
CREATOR_ULTIMATE_CHANCE, CREATOR_THEMED_MOVE_RATIO, export { pickChoreography }
CRIT_THEMED_RATIO, CHOREOGRAPHY_THEMED_RATIO, CHOREOGRAPHY_GENERIC_CUTOFF, import { isPerfMode, perfParticles, isReducedMotion, haptic } from './fight/config'
TIER_ULTIMATE_CHANCES, isPerfMode, perfParticles, isReducedMotion, haptic,
} from './fight/config'
import { spawnSparks as _spawnSparks, spawnBulletHoles as _spawnBulletHoles, spawnExhaust as _spawnExhaust } from './fight/particles' 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 { glitchRGB as _glitchRGB, scanlineGlitch as _scanlineGlitch, vhsTracking as _vhsTracking, dimensionalShift as _dimensionalShift } from './fight/effects'
import { spawnProjectile as _spawnProjectile } from './fight/projectiles' import { spawnProjectile as _spawnProjectile } from './fight/projectiles'
@@ -30,114 +28,6 @@ import {
export function pickChoreography(challengeType: string, isCritical: boolean, _round: number, attackerTier: number = 0, attackerArchetype?: string): string {
// THE CREATOR: special move selection
if (attackerArchetype === 'the_creator') {
const ultimateChance = isCritical ? 1.0 : CREATOR_ULTIMATE_CHANCE
if (Math.random() < ultimateChance) {
// Creator gets ALL ultimates + their 4 exclusive ones
const all: string[] = [...CREATOR_ULTIMATES]
for (let t = 2; t <= 5; t++) {
if (TIER_ULTIMATES[t]) all.push(...TIER_ULTIMATES[t])
}
return all[Math.floor(Math.random() * all.length)]
}
if (Math.random() < CREATOR_THEMED_MOVE_RATIO) {
return CREATOR_MOVES[Math.floor(Math.random() * CREATOR_MOVES.length)]
}
// Fall through to normal selection
}
// Tier-gated ultimates: higher tier = higher chance of spectacular ultimate moves
if (attackerTier >= 2) {
const ultimateChance = TIER_ULTIMATE_CHANCES[attackerTier] ?? TIER_ULTIMATE_CHANCES[5]
// On crits, double the chance
const roll = isCritical ? ultimateChance * 2 : ultimateChance
if (Math.random() < roll) {
// Collect all ultimates available at this tier (cumulative — higher tiers get lower tier moves too)
const available: string[] = []
for (let t = 2; t <= attackerTier; t++) {
if (TIER_ULTIMATES[t]) available.push(...TIER_ULTIMATES[t])
}
if (available.length > 0) {
return available[Math.floor(Math.random() * available.length)]
}
}
}
// Critical hits always get BIG moves (but can still be themed)
if (isCritical) {
const entry = CHALLENGE_THEMED[challengeType]
if (entry && Math.random() < CRIT_THEMED_RATIO) {
return entry.themed[Math.floor(Math.random() * entry.themed.length)]
}
const critMoves = [
'groundPound', 'jetpackDive', 'gunBurst', 'fullScreenDash', 'multiHit', 'fireBreath',
'lightningRush', 'zoomRush', 'rapidFlurry', 'rocketLauncher', 'laserBeam', 'katanaCombo',
'hammerSmash', 'sniperShot', 'minigunSpray', 'chainsawRev', 'carSmash', 'motorbikeCharge',
'anvilDrop', 'pocketCannon', 'bodySlam', 'suplex', 'pinballCombo', 'grappleFlurry',
// New epic crits
'kamehameha', 'nukeStrike', 'tankRoll', 'helicopterStrike', 'chokeslam', 'tombstone',
'rko', 'piledriver', 'powerbomb', 'meteorStrike', 'spiritBomb', 'blackHole',
'alienAbduction', 'tsunamiWave', 'earthquakeStrike', 'dragonBreath', 'pianoDrop',
'fridgeDrop', 'volcanoErupt', 'warpDrive', 'dubstepCannon', 'tornadoSpin',
'trampolineBounce', 'watermelonBomb', 'nftRugPull', 'blueScreen', 'yeetThrow',
'bulletTimeAttack',
]
return critMoves[Math.floor(Math.random() * critMoves.length)]
}
const entry = CHALLENGE_THEMED[challengeType]
if (!entry) {
const fallback = ['dashPunch', 'flyingKick', 'aerialSlam', 'backflipKick', 'fingerGuns', 'rubberChicken', 'pizzaSlam', 'fryingPan', 'snowballFight']
return fallback[Math.floor(Math.random() * fallback.length)]
}
const roll = Math.random()
if (roll < CHOREOGRAPHY_THEMED_RATIO) {
return entry.themed[Math.floor(Math.random() * entry.themed.length)]
} else if (roll < CHOREOGRAPHY_GENERIC_CUTOFF) {
return entry.generic[Math.floor(Math.random() * entry.generic.length)]
} else {
const wild = [
'jetpackDive', 'gunBurst', 'groundPound', 'teleportStrike', 'fullScreenDash', 'fireBreath',
'golfClubSmash', 'zoomRush', 'rapidFlurry', 'swordSlash', 'hammerSmash', 'laserBeam',
'rocketLauncher', 'bombThrow', 'minigunSpray', 'sniperShot', 'whipCrack', 'katanaCombo',
'chainsawRev', 'motorbikeCharge', 'carSmash', 'boatCannon', 'anvilDrop', 'pocketCannon',
'grappleFlurry', 'bodySlam', 'pinballCombo', 'suplex',
// New wild cards — food
'pizzaSlam', 'bananaFling', 'pieSmash', 'burgerToss', 'tomatoBarrage', 'fishSlap',
// New wild cards — weapons
'fryingPan', 'baseballBat', 'guitarSmash', 'umbrellaWhack', 'plungerSlam', 'wrenchSmash',
// New wild cards — drops
'pianoDrop', 'toiletDrop', 'tvDrop', 'chandelierDrop', 'micDrop',
// New wild cards — vehicles
'shoppingCart', 'zamboniCrush', 'unicycleRun', 'tankRoll',
// New wild cards — spins/flips
'tornadoSpin', 'backflipKick', 'corkscrewDive', 'barrelRoll', 'skateTrick',
// New wild cards — beams
'kamehameha', 'freezeRay', 'heatVision', 'rainbowBeam', 'glitchBeam',
// New wild cards — silly
'rubberChicken', 'selfieStrike', 'fingerGuns', 'yeetThrow', 'emojiBarrage', 'touchGrass',
// New wild cards — wrestling
'clothesline', 'rko', 'stunner', 'piledriver',
// New wild cards — magic
'fireball', 'portalPunch', 'crystalShards', 'spiritBomb',
// New wild cards — tech
'stackOverflow', 'blueScreen', 'aiUprising', 'popupSpam',
// New wild cards — music
'bassDrop', 'dubstepCannon', 'vinylScratch',
// New wild cards — nature
'avalanche', 'lavaSplash', 'tornadoFling',
]
return wild[Math.floor(Math.random() * wild.length)]
}
}
// Convert any CSS color string to a kaplay Color object safely // Convert any CSS color string to a kaplay Color object safely
function safeColor(k: ReturnType<typeof kaplay>, color: string) { function safeColor(k: ReturnType<typeof kaplay>, color: string) {
if (color.startsWith('#')) { if (color.startsWith('#')) {
@@ -0,0 +1,110 @@
import { CHALLENGE_THEMED, TIER_ULTIMATES, CREATOR_MOVES, CREATOR_ULTIMATES } from '../constants'
import {
CREATOR_ULTIMATE_CHANCE, CREATOR_THEMED_MOVE_RATIO,
CRIT_THEMED_RATIO, CHOREOGRAPHY_THEMED_RATIO, CHOREOGRAPHY_GENERIC_CUTOFF,
TIER_ULTIMATE_CHANCES,
} from '../config'
export function pickChoreography(challengeType: string, isCritical: boolean, _round: number, attackerTier: number = 0, attackerArchetype?: string): string {
// THE CREATOR: special move selection
if (attackerArchetype === 'the_creator') {
const ultimateChance = isCritical ? 1.0 : CREATOR_ULTIMATE_CHANCE
if (Math.random() < ultimateChance) {
// Creator gets ALL ultimates + their 4 exclusive ones
const all: string[] = [...CREATOR_ULTIMATES]
for (let t = 2; t <= 5; t++) {
if (TIER_ULTIMATES[t]) all.push(...TIER_ULTIMATES[t])
}
return all[Math.floor(Math.random() * all.length)]
}
if (Math.random() < CREATOR_THEMED_MOVE_RATIO) {
return CREATOR_MOVES[Math.floor(Math.random() * CREATOR_MOVES.length)]
}
// Fall through to normal selection
}
// Tier-gated ultimates: higher tier = higher chance of spectacular ultimate moves
if (attackerTier >= 2) {
const ultimateChance = TIER_ULTIMATE_CHANCES[attackerTier] ?? TIER_ULTIMATE_CHANCES[5]
// On crits, double the chance
const roll = isCritical ? ultimateChance * 2 : ultimateChance
if (Math.random() < roll) {
// Collect all ultimates available at this tier (cumulative — higher tiers get lower tier moves too)
const available: string[] = []
for (let t = 2; t <= attackerTier; t++) {
if (TIER_ULTIMATES[t]) available.push(...TIER_ULTIMATES[t])
}
if (available.length > 0) {
return available[Math.floor(Math.random() * available.length)]
}
}
}
// Critical hits always get BIG moves (but can still be themed)
if (isCritical) {
const entry = CHALLENGE_THEMED[challengeType]
if (entry && Math.random() < CRIT_THEMED_RATIO) {
return entry.themed[Math.floor(Math.random() * entry.themed.length)]
}
const critMoves = [
'groundPound', 'jetpackDive', 'gunBurst', 'fullScreenDash', 'multiHit', 'fireBreath',
'lightningRush', 'zoomRush', 'rapidFlurry', 'rocketLauncher', 'laserBeam', 'katanaCombo',
'hammerSmash', 'sniperShot', 'minigunSpray', 'chainsawRev', 'carSmash', 'motorbikeCharge',
'anvilDrop', 'pocketCannon', 'bodySlam', 'suplex', 'pinballCombo', 'grappleFlurry',
// New epic crits
'kamehameha', 'nukeStrike', 'tankRoll', 'helicopterStrike', 'chokeslam', 'tombstone',
'rko', 'piledriver', 'powerbomb', 'meteorStrike', 'spiritBomb', 'blackHole',
'alienAbduction', 'tsunamiWave', 'earthquakeStrike', 'dragonBreath', 'pianoDrop',
'fridgeDrop', 'volcanoErupt', 'warpDrive', 'dubstepCannon', 'tornadoSpin',
'trampolineBounce', 'watermelonBomb', 'nftRugPull', 'blueScreen', 'yeetThrow',
'bulletTimeAttack',
]
return critMoves[Math.floor(Math.random() * critMoves.length)]
}
const entry = CHALLENGE_THEMED[challengeType]
if (!entry) {
const fallback = ['dashPunch', 'flyingKick', 'aerialSlam', 'backflipKick', 'fingerGuns', 'rubberChicken', 'pizzaSlam', 'fryingPan', 'snowballFight']
return fallback[Math.floor(Math.random() * fallback.length)]
}
const roll = Math.random()
if (roll < CHOREOGRAPHY_THEMED_RATIO) {
return entry.themed[Math.floor(Math.random() * entry.themed.length)]
} else if (roll < CHOREOGRAPHY_GENERIC_CUTOFF) {
return entry.generic[Math.floor(Math.random() * entry.generic.length)]
} else {
const wild = [
'jetpackDive', 'gunBurst', 'groundPound', 'teleportStrike', 'fullScreenDash', 'fireBreath',
'golfClubSmash', 'zoomRush', 'rapidFlurry', 'swordSlash', 'hammerSmash', 'laserBeam',
'rocketLauncher', 'bombThrow', 'minigunSpray', 'sniperShot', 'whipCrack', 'katanaCombo',
'chainsawRev', 'motorbikeCharge', 'carSmash', 'boatCannon', 'anvilDrop', 'pocketCannon',
'grappleFlurry', 'bodySlam', 'pinballCombo', 'suplex',
// New wild cards — food
'pizzaSlam', 'bananaFling', 'pieSmash', 'burgerToss', 'tomatoBarrage', 'fishSlap',
// New wild cards — weapons
'fryingPan', 'baseballBat', 'guitarSmash', 'umbrellaWhack', 'plungerSlam', 'wrenchSmash',
// New wild cards — drops
'pianoDrop', 'toiletDrop', 'tvDrop', 'chandelierDrop', 'micDrop',
// New wild cards — vehicles
'shoppingCart', 'zamboniCrush', 'unicycleRun', 'tankRoll',
// New wild cards — spins/flips
'tornadoSpin', 'backflipKick', 'corkscrewDive', 'barrelRoll', 'skateTrick',
// New wild cards — beams
'kamehameha', 'freezeRay', 'heatVision', 'rainbowBeam', 'glitchBeam',
// New wild cards — silly
'rubberChicken', 'selfieStrike', 'fingerGuns', 'yeetThrow', 'emojiBarrage', 'touchGrass',
// New wild cards — wrestling
'clothesline', 'rko', 'stunner', 'piledriver',
// New wild cards — magic
'fireball', 'portalPunch', 'crystalShards', 'spiritBomb',
// New wild cards — tech
'stackOverflow', 'blueScreen', 'aiUprising', 'popupSpam',
// New wild cards — music
'bassDrop', 'dubstepCannon', 'vinylScratch',
// New wild cards — nature
'avalanche', 'lavaSplash', 'tornadoFling',
]
return wild[Math.floor(Math.random() * wild.length)]
}
}