diff --git a/frontend/src/game/FightScene.ts b/frontend/src/game/FightScene.ts index 4643e24..bf9e3b0 100644 --- a/frontend/src/game/FightScene.ts +++ b/frontend/src/game/FightScene.ts @@ -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 type { Fighter, FightSceneConfig, RoundEvent, FightContext, ChoreoContext } 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 { - CREATOR_ULTIMATE_CHANCE, CREATOR_THEMED_MOVE_RATIO, - CRIT_THEMED_RATIO, CHOREOGRAPHY_THEMED_RATIO, CHOREOGRAPHY_GENERIC_CUTOFF, - TIER_ULTIMATE_CHANCES, isPerfMode, perfParticles, isReducedMotion, haptic, -} from './fight/config' +import { ARENA_THEMES, spriteAnims } from './fight/constants' +import { pickChoreography } from './fight/choreography/picker' +export { pickChoreography } +import { isPerfMode, perfParticles, isReducedMotion, haptic } from './fight/config' 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' @@ -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 function safeColor(k: ReturnType, color: string) { if (color.startsWith('#')) { diff --git a/frontend/src/game/fight/choreography/picker.ts b/frontend/src/game/fight/choreography/picker.ts new file mode 100644 index 0000000..7d6c5bb --- /dev/null +++ b/frontend/src/game/fight/choreography/picker.ts @@ -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)] + } +}