193 lines
5.3 KiB
TypeScript
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
|
||
|
|
}
|