You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

217 lines
7.1 KiB
TypeScript

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, string> = {
[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, string> = {
[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 (
<div style={statRow}>
<span>{label}</span>
<span style={{ color, transition: 'color 200ms ease' }}>
{value}{arrow}
</span>
</div>
);
}
/** 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 (
<div style={bodyStyle}>
{/* Combat Stats */}
<StatValue label={tr.atk} value={String(hero.damage)} buffed={atkBuffed} nerfed={atkNerfed} />
<StatValue label={tr.def} value={String(hero.defense)} buffed={defBuffed} nerfed={false} />
<StatValue label={tr.spd} value={`${hero.attackSpeed.toFixed(2)}/s`} buffed={spdBuffed} nerfed={spdNerfed} />
<StatValue
label={tr.moveSpd}
value={`${(hero.moveSpeed ?? 1).toFixed(2)}x`}
buffed={hasActiveBuff(hero.activeBuffs, BuffType.Rush, nowMs)}
nerfed={hasActiveDebuff(hero.debuffs, DebuffType.Slow, nowMs)}
/>
<div style={statRow}><span>{tr.str}</span><span>{hero.strength}</span></div>
<div style={statRow}><span>{tr.con}</span><span>{hero.constitution}</span></div>
<div style={statRow}><span>{tr.agi}</span><span>{hero.agility}</span></div>
<div style={statRow}><span>{tr.luck}</span><span>{hero.luck}</span></div>
{/* XP */}
<div style={{ ...sectionTitle, marginTop: 12 }}>{tr.experience}</div>
<div style={{ marginBottom: 2 }}>
<HPBar
current={hero.xp}
max={Math.max(1, hero.xpToNext)}
color="#44aaff"
height={12}
showText
label={tr.xp}
/>
</div>
{/* Buffs */}
<div style={sectionTitle}>{tr.activeBuffs}</div>
{buffsLive.length === 0 ? (
<div style={{ opacity: 0.55, fontStyle: 'italic' }}>{tr.none}</div>
) : (
<div style={{ display: 'flex', flexWrap: 'wrap' }}>
{buffsLive.map((b) => (
<span
key={b.type}
style={{
...fxChip,
backgroundColor: 'rgba(68, 170, 255, 0.35)',
border: '1px solid rgba(68, 170, 255, 0.6)',
}}
>
{buffLabel(tr, b.type)}
<span style={{ opacity: 0.9 }}>{Math.ceil(b.remainingMs / 1000)}s</span>
</span>
))}
</div>
)}
{/* Debuffs */}
<div style={sectionTitle}>{tr.activeDebuffs}</div>
{debuffsLive.length === 0 ? (
<div style={{ opacity: 0.55, fontStyle: 'italic' }}>{tr.none}</div>
) : (
<div style={{ display: 'flex', flexWrap: 'wrap' }}>
{debuffsLive.map((d) => {
const color = DEBUFF_COLORS[d.type] ?? '#999';
return (
<span
key={d.type}
style={{
...fxChip,
backgroundColor: `${color}44`,
border: `1px solid ${color}99`,
}}
>
{debuffLabel(tr, d.type)}
<span style={{ opacity: 0.9 }}>{Math.ceil(d.remainingMs / 1000)}s</span>
</span>
);
})}
</div>
)}
</div>
);
}