import { type CSSProperties } from 'react'; import type { HeroState } from '../game/types'; import { BuffType, DebuffType, type ActiveBuff, type ActiveDebuff } from '../game/types'; import { DEBUFF_COLORS } from '../shared/constants'; import { HPBar } from './HPBar'; import { useT, type Translations } from '../i18n'; function buffLabel(tr: Translations, type: BuffType): string { const map: Record = { [BuffType.Rush]: tr.buffRush, [BuffType.Rage]: tr.buffRage, [BuffType.Shield]: tr.buffShield, [BuffType.Luck]: tr.buffLuck, [BuffType.Resurrection]: tr.buffResurrection, [BuffType.Heal]: tr.buffHeal, [BuffType.PowerPotion]: tr.buffPowerPotion, [BuffType.WarCry]: tr.buffWarCry, }; return map[type] ?? type; } function debuffLabel(tr: Translations, type: DebuffType): string { const map: Record = { [DebuffType.Poison]: tr.debuffPoison, [DebuffType.Freeze]: tr.debuffFreeze, [DebuffType.Burn]: tr.debuffBurn, [DebuffType.Stun]: tr.debuffStun, [DebuffType.Slow]: tr.debuffSlow, [DebuffType.Weaken]: tr.debuffWeaken, [DebuffType.IceSlow]: tr.debuffIceSlow, }; return map[type] ?? type; } const bodyStyle: CSSProperties = { padding: '8px 10px 10px', fontSize: 11, color: '#ccc', }; const statRow: CSSProperties = { display: 'flex', justifyContent: 'space-between', padding: '2px 0', borderBottom: '1px solid rgba(255,255,255,0.06)', }; const sectionTitle: CSSProperties = { fontSize: 10, fontWeight: 700, color: '#888', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 10, marginBottom: 4, }; const fxChip: CSSProperties = { display: 'inline-flex', alignItems: 'center', gap: 4, padding: '3px 8px', borderRadius: 6, fontSize: 10, fontWeight: 600, marginRight: 4, marginBottom: 4, color: '#fff', textShadow: '0 1px 2px rgba(0,0,0,0.7)', }; function liveBuffRemaining(b: ActiveBuff, nowMs: number): number { if (b.expiresAtMs != null) return Math.max(0, b.expiresAtMs - nowMs); return b.remainingMs; } function liveDebuffRemaining(d: ActiveDebuff, nowMs: number): number { if (d.expiresAtMs != null) return Math.max(0, d.expiresAtMs - nowMs); return d.remainingMs; } interface HeroPanelProps { hero: HeroState; nowMs: number; } function hasActiveBuff(buffs: ActiveBuff[], type: BuffType, nowMs: number): boolean { return buffs.some((b) => b.type === type && liveBuffRemaining(b, nowMs) > 0); } function hasActiveDebuff(debuffs: ActiveDebuff[], type: DebuffType, nowMs: number): boolean { return debuffs.some((d) => d.type === type && liveDebuffRemaining(d, nowMs) > 0); } const buffedColor = '#44ff88'; const nerfedColor = '#ff6666'; function StatValue({ value, label, buffed, nerfed }: { value: string; label: string; buffed: boolean; nerfed: boolean }) { const color = buffed ? buffedColor : nerfed ? nerfedColor : '#ccc'; const arrow = buffed ? ' \u25B2' : nerfed ? ' \u25BC' : ''; return (
{label} {value}{arrow}
); } /** Full hero stats block (Stats sheet tab). */ export function HeroStatsContent({ hero, nowMs }: HeroPanelProps) { const tr = useT(); const buffsLive = hero.activeBuffs .map((b) => ({ ...b, remainingMs: liveBuffRemaining(b, nowMs), })) .filter((b) => b.remainingMs > 0); const debuffsLive = hero.debuffs .map((d) => ({ ...d, remainingMs: liveDebuffRemaining(d, nowMs), })) .filter((d) => d.remainingMs > 0); const atkBuffed = hasActiveBuff(hero.activeBuffs, BuffType.Rage, nowMs) || hasActiveBuff(hero.activeBuffs, BuffType.PowerPotion, nowMs); const atkNerfed = hasActiveDebuff(hero.debuffs, DebuffType.Weaken, nowMs); const spdBuffed = hasActiveBuff(hero.activeBuffs, BuffType.WarCry, nowMs); const spdNerfed = hasActiveDebuff(hero.debuffs, DebuffType.Freeze, nowMs) || hasActiveDebuff(hero.debuffs, DebuffType.IceSlow, nowMs); const defBuffed = hasActiveBuff(hero.activeBuffs, BuffType.Shield, nowMs); return (
{/* Combat Stats */}
{tr.str}{hero.strength}
{tr.con}{hero.constitution}
{tr.agi}{hero.agility}
{tr.luck}{hero.luck}
{/* XP */}
{tr.experience}
{/* Buffs */}
{tr.activeBuffs}
{buffsLive.length === 0 ? (
{tr.none}
) : (
{buffsLive.map((b) => ( {buffLabel(tr, b.type)} {Math.ceil(b.remainingMs / 1000)}s ))}
)} {/* Debuffs */}
{tr.activeDebuffs}
{debuffsLive.length === 0 ? (
{tr.none}
) : (
{debuffsLive.map((d) => { const color = DEBUFF_COLORS[d.type] ?? '#999'; return ( {debuffLabel(tr, d.type)} {Math.ceil(d.remainingMs / 1000)}s ); })}
)}
); }