import type { FighterInstance } from './types' import { MOVES, matchCombo } from './moves' import type { InputEvent } from './types' /** * Update fighter state machine based on current input and state. * Returns the name of a combo move to execute, or null. */ export function updateStateMachine( fighter: FighterInstance, inputBuffer: InputEvent[], ): string | null { const { combat, physics, input } = fighter const state = combat.state combat.stateTimer++ // --- Terminal states --- if (state === 'ko' || state === 'win') return null // --- Stun states: count down and return to idle --- if (state === 'hit') { combat.stunTimer-- if (combat.stunTimer <= 0) { transition(fighter, 'idle') } return null } if (state === 'knockback') { combat.stunTimer-- if (combat.stunTimer <= 0 && physics.grounded) { transition(fighter, 'idle') } return null } if (state === 'blocking') { if (combat.blockTimer > 0) { combat.blockTimer-- return null } // Holding back = stay blocking; release = idle const holdingBack = isHoldingBack(fighter) if (!holdingBack || !physics.grounded) { transition(fighter, 'idle') } return null } // --- Attack states: advance frame, return to idle on completion --- if (state === 'attacking' || state === 'kicking' || state === 'special') { combat.attackFrame++ const move = combat.currentMove ? MOVES[combat.currentMove] : null if (move && combat.attackFrame >= move.totalFrames) { transition(fighter, 'idle') } return null } // --- Actionable states: idle, walking, jumping, crouching --- // Check for combo input first (highest priority) const combo = matchCombo(inputBuffer, fighter.physics.facingRight) if (combo && physics.grounded) { return combo } // Check attack buttons if (input.punch) { if (physics.grounded) { if (input.down) { startAttack(fighter, 'crouchPunch') } else { startAttack(fighter, 'punch') } } else { startAttack(fighter, 'airPunch') } return null } if (input.kick) { if (physics.grounded) { if (input.down) { startAttack(fighter, 'crouchKick') } else { startAttack(fighter, 'kick') } } else { startAttack(fighter, 'airKick') } return null } // Blocking: holding back while grounded if (isHoldingBack(fighter) && physics.grounded) { if ((state as string) !== 'blocking') transition(fighter, 'blocking') return null } // Crouching if (input.down && physics.grounded) { if (state !== 'crouching') transition(fighter, 'crouching') return null } // Walking if ((input.left || input.right) && physics.grounded) { if (state !== 'walking') transition(fighter, 'walking') return null } // Jumping (handled in physics, but update state) if (!physics.grounded) { if (state !== 'jumping') transition(fighter, 'jumping') return null } // Default: idle if (state !== 'idle') transition(fighter, 'idle') return null } function transition(fighter: FighterInstance, newState: FighterInstance['combat']['state']): void { fighter.combat.state = newState fighter.combat.stateTimer = 0 fighter.combat.attackFrame = 0 fighter.combat.currentMove = null fighter.combat.hasHitThisAttack = false } function startAttack(fighter: FighterInstance, moveName: string): void { const move = MOVES[moveName] if (!move) return const stateMap: Record = { attack: 'attacking', kick: 'kicking', special: 'special', } fighter.combat.state = stateMap[move.animation] || 'attacking' fighter.combat.stateTimer = 0 fighter.combat.attackFrame = 0 fighter.combat.currentMove = moveName fighter.combat.hasHitThisAttack = false } export function startComboMove(fighter: FighterInstance, moveName: string): void { startAttack(fighter, moveName) } export function enterHitstun(fighter: FighterInstance, stunFrames: number, knockbackX: number, knockbackY: number): void { const isKnockback = knockbackY < 0 || Math.abs(knockbackX) > 150 fighter.combat.state = isKnockback ? 'knockback' : 'hit' fighter.combat.stateTimer = 0 fighter.combat.stunTimer = stunFrames fighter.combat.attackFrame = 0 fighter.combat.currentMove = null const dir = fighter.physics.facingRight ? -1 : 1 // knock away from attacker fighter.physics.vx = knockbackX * dir fighter.physics.vy = knockbackY if (knockbackY < 0) fighter.physics.grounded = false } export function enterBlockstun(fighter: FighterInstance, stunFrames: number, pushback: number): void { fighter.combat.state = 'blocking' fighter.combat.stateTimer = 0 fighter.combat.blockTimer = stunFrames const dir = fighter.physics.facingRight ? -1 : 1 fighter.physics.vx = pushback * dir } export function enterKO(fighter: FighterInstance): void { fighter.combat.state = 'ko' fighter.combat.stateTimer = 0 fighter.combat.currentMove = null } export function enterWin(fighter: FighterInstance): void { fighter.combat.state = 'win' fighter.combat.stateTimer = 0 fighter.combat.currentMove = null } function isHoldingBack(fighter: FighterInstance): boolean { if (fighter.physics.facingRight) { return fighter.input.left && !fighter.input.right } return fighter.input.right && !fighter.input.left }