import { useMemo, type CSSProperties } from 'react'; import { getViewport } from '../shared/telegram'; export type CombatOverlayKind = 'damage' | 'blocked' | 'evaded' | 'regen' | 'stunned' | 'heal'; export type CombatOverlayTarget = 'hero' | 'enemy'; export interface CombatOverlayEvent { id: number; kind: CombatOverlayKind; target: CombatOverlayTarget; value: number; isCrit?: boolean; createdAt: number; } const FLOAT_DURATION_MS = 6000; const FEEDBACK_DURATION_MS = 4800; const CRIT_DURATION_MS = 6000; const HEAL_DURATION_MS = 7000; const FLOAT_RISE_PX = 96; const FLOAT_DRIFT_PX = 44; const FLOAT_KEYFRAMES = ` @keyframes autoheroCombatFloat { from { opacity: 1; transform: translate(-50%, -50%) translate(0, 0) scale(var(--ah-s0, 1)); } to { opacity: 0; transform: translate(-50%, -50%) translate(var(--ah-drift, 0px), calc(-1 * var(--ah-rise, 0px))) scale(1); } } `; interface CombatOverlayProps { events: CombatOverlayEvent[]; onExpire?: (id: number) => void; } function durationMs(evt: CombatOverlayEvent): number { if (evt.kind === 'blocked' || evt.kind === 'evaded' || evt.kind === 'stunned') { return FEEDBACK_DURATION_MS; } if (evt.kind === 'damage' && evt.isCrit) { return CRIT_DURATION_MS; } if (evt.kind === 'heal') { return HEAL_DURATION_MS; } return FLOAT_DURATION_MS; } function overlayText(evt: CombatOverlayEvent): string { if (evt.kind === 'damage') { const prefix = evt.isCrit ? 'CRIT ' : ''; return `${prefix}${Math.round(evt.value)}`; } if (evt.kind === 'regen') { return `+${Math.round(evt.value)}`; } if (evt.kind === 'blocked') return 'BLOCKED'; if (evt.kind === 'evaded') return 'EVADED'; if (evt.kind === 'heal') return `HEAL ${Math.round(evt.value)}`; return 'STUNNED'; } function overlayColor(evt: CombatOverlayEvent): string { if (evt.kind === 'regen') { return evt.target === 'hero' ? '#44dd66' : '#ff5566'; } if (evt.kind === 'heal') { return '#44dd66'; } if (evt.kind === 'stunned') return '#ffaa44'; if (evt.kind === 'blocked' || evt.kind === 'evaded') { return evt.target === 'hero' ? '#44dd66' : '#ff5566'; } return evt.isCrit ? '#ffdd44' : '#ffffff'; } function overlayFontSize(evt: CombatOverlayEvent): number { if (evt.kind === 'blocked' || evt.kind === 'evaded' || evt.kind === 'stunned') { return 16; } if (evt.kind === 'damage' && evt.isCrit) return 24; return 18; } export function CombatOverlay({ events, onExpire }: CombatOverlayProps) { const viewport = getViewport(); const anchors = useMemo(() => { const xEnemySide = viewport.width / 2 + 88; const xHeroSide = viewport.width / 2 - 88; const yMid = viewport.height / 2 - 42; return { hero: { x: xHeroSide, y: yMid, driftDir: -1 }, potion: { x: xHeroSide, y: yMid, driftDir: -1 }, enemy: { x: xEnemySide, y: yMid, driftDir: 1 }, }; }, [viewport.height, viewport.width]); return (