import { AttackType } from '../data/Interfaces'; import { PlayerStateMachine, DamageOutcome } from './PlayerStateMachine'; /** * DamageSystem — the single funnel through which **every** player-facing * damage event must flow (req 10.1-10.6). * * Decision precedence (req 10.3): * 1. i-frames → no effect * 2. sword parry → no effect (only vs. shuriken/sword; see PSM) * 3. attack type × distance → dispatch to PlayerStateMachine.takeHit * * Distance thresholds: * - fireball: lethal within 100px (req 10.4) * - smoke bomb: lethal within 80px (req 10.5) * - shuriken / sword: any contact is eligible * * Enemy-facing damage is a separate, simpler `applyToEnemy` helper. */ export interface IDamageContext { attackType: AttackType; attackerX: number; attackerY: number; victimX: number; victimY: number; } export const FIREBALL_KILL_RADIUS = 100; export const SMOKE_KILL_RADIUS = 80; export class DamageSystem { constructor(private readonly psm: PlayerStateMachine) {} /** Try to damage the player. Returns `null` if the attack missed by distance. */ public applyToPlayer(ctx: IDamageContext): DamageOutcome | null { const distance = Math.hypot(ctx.attackerX - ctx.victimX, ctx.attackerY - ctx.victimY); if (ctx.attackType === 'fireball' && distance > FIREBALL_KILL_RADIUS) return null; if (ctx.attackType === 'smoke_bomb' && distance > SMOKE_KILL_RADIUS) return null; // shuriken / sword rely on caller-side hitbox; reaching here means "hit". return this.psm.takeHit(ctx.attackType); } /** * Apply flat damage to an enemy HP bucket. The damage number comes from * the active weapon config. Returns the remaining HP (0 means killed). */ public applyToEnemy(currentHp: number, damage: number): number { return Math.max(0, currentHp - damage); } }