139 lines
3.9 KiB
TypeScript
139 lines
3.9 KiB
TypeScript
import type { FighterInstance, Hitbox } from './types'
|
|
import {
|
|
HURTBOX_WIDTH, HURTBOX_HEIGHT, CROUCH_HURTBOX_HEIGHT,
|
|
CHIP_DAMAGE_RATIO, COMBO_DAMAGE_SCALING,
|
|
} from './constants'
|
|
import { MOVES } from './moves'
|
|
|
|
export interface HitResult {
|
|
type: 'hit' | 'blocked'
|
|
damage: number
|
|
hitstun: number
|
|
blockstun: number
|
|
knockbackX: number
|
|
knockbackY: number
|
|
hitbox: Hitbox
|
|
}
|
|
|
|
/**
|
|
* Check all active hitboxes of the attacker's current move against the defender.
|
|
* Returns HitResult if any hitbox connects, null otherwise.
|
|
*/
|
|
export function checkHit(attacker: FighterInstance, defender: FighterInstance): HitResult | null {
|
|
const { combat, physics, obj } = attacker
|
|
if (!combat.currentMove || combat.hasHitThisAttack) return null
|
|
|
|
const move = MOVES[combat.currentMove]
|
|
if (!move) return null
|
|
|
|
const frame = combat.attackFrame
|
|
|
|
for (const hitbox of move.hitboxes) {
|
|
if (frame < hitbox.activeFrames[0] || frame > hitbox.activeFrames[1]) continue
|
|
|
|
const result = testHitbox(attacker, defender, hitbox)
|
|
if (result) return result
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
function testHitbox(
|
|
attacker: FighterInstance,
|
|
defender: FighterInstance,
|
|
hitbox: Hitbox,
|
|
): HitResult | null {
|
|
const dir = attacker.physics.facingRight ? 1 : -1
|
|
|
|
// Hitbox world position
|
|
const hx = attacker.obj.pos.x + hitbox.offsetX * dir
|
|
const hy = attacker.obj.pos.y + hitbox.offsetY
|
|
const hLeft = hx - hitbox.width / 2
|
|
const hRight = hx + hitbox.width / 2
|
|
const hTop = hy - hitbox.height / 2
|
|
const hBottom = hy + hitbox.height / 2
|
|
|
|
// Defender hurtbox (centered on position, extends upward)
|
|
const isCrouching = defender.combat.state === 'crouching' || defender.combat.state === 'blocking'
|
|
const hurtH = isCrouching ? CROUCH_HURTBOX_HEIGHT : HURTBOX_HEIGHT
|
|
const dLeft = defender.obj.pos.x - HURTBOX_WIDTH / 2
|
|
const dRight = defender.obj.pos.x + HURTBOX_WIDTH / 2
|
|
const dTop = defender.obj.pos.y - hurtH
|
|
const dBottom = defender.obj.pos.y
|
|
|
|
// AABB overlap test
|
|
if (hRight < dLeft || hLeft > dRight || hBottom < dTop || hTop > dBottom) {
|
|
return null
|
|
}
|
|
|
|
// Check if defender is blocking
|
|
const isBlocking = isDefenderBlocking(attacker, defender)
|
|
|
|
if (isBlocking) {
|
|
return {
|
|
type: 'blocked',
|
|
damage: Math.round(hitbox.damage * CHIP_DAMAGE_RATIO),
|
|
hitstun: 0,
|
|
blockstun: hitbox.blockstun,
|
|
knockbackX: hitbox.knockbackX * 0.3,
|
|
knockbackY: 0,
|
|
hitbox,
|
|
}
|
|
}
|
|
|
|
// Apply combo damage scaling
|
|
const comboScale = Math.pow(COMBO_DAMAGE_SCALING, defender.combat.comboCount)
|
|
const scaledDamage = Math.round(hitbox.damage * comboScale)
|
|
|
|
return {
|
|
type: 'hit',
|
|
damage: scaledDamage,
|
|
hitstun: hitbox.hitstun,
|
|
blockstun: 0,
|
|
knockbackX: hitbox.knockbackX,
|
|
knockbackY: hitbox.knockbackY,
|
|
hitbox,
|
|
}
|
|
}
|
|
|
|
function isDefenderBlocking(attacker: FighterInstance, defender: FighterInstance): boolean {
|
|
if (defender.combat.state !== 'blocking') return false
|
|
if (!defender.physics.grounded) return false
|
|
|
|
// Must be holding direction away from attacker
|
|
const holdingBack = defender.physics.facingRight
|
|
? defender.input.left && !defender.input.right
|
|
: defender.input.right && !defender.input.left
|
|
|
|
return holdingBack
|
|
}
|
|
|
|
/**
|
|
* Apply hit result to the defender. Mutates defender state.
|
|
*/
|
|
export function applyHit(
|
|
attacker: FighterInstance,
|
|
defender: FighterInstance,
|
|
result: HitResult,
|
|
): void {
|
|
// Deal damage
|
|
defender.combat.hp = Math.max(0, defender.combat.hp - result.damage)
|
|
|
|
// Mark attacker's attack as having connected (prevent multi-hit per hitbox window)
|
|
attacker.combat.hasHitThisAttack = true
|
|
|
|
if (result.type === 'hit') {
|
|
// Increment combo counter
|
|
attacker.combat.comboCount++
|
|
attacker.combat.comboDamage += result.damage
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reset combo counter (called when the opponent recovers from hitstun).
|
|
*/
|
|
export function resetCombo(fighter: FighterInstance): void {
|
|
fighter.combat.comboCount = 0
|
|
fighter.combat.comboDamage = 0
|
|
}
|