Files
botfights/frontend/src/game/arcade/state-machine.ts
T
2026-04-11 19:46:37 +01:00

193 lines
5.3 KiB
TypeScript

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<string, FighterInstance['combat']['state']> = {
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
}