import React, { useEffect, useState } from 'react'; import { DraggableWindow } from './DraggableWindow'; import { apiFetch } from '../../api/client'; interface Props { id: string; charName: string; zIndex: number; vitals?: any; liveStats?: any; } // Property ID maps — verbatim from v1 script.js lines 1843-1876 const TS_AUGMENTATIONS: Record = { 218:'Reinforcement of the Lugians',219:"Bleeargh's Fortitude",220:"Oswald's Enhancement", 221:"Siraluun's Blessing",222:'Enduring Calm',223:'Steadfast Will', 224:"Ciandra's Essence",225:"Yoshi's Essence",226:"Jibril's Essence", 227:"Celdiseth's Essence",228:"Koga's Essence",229:'Shadow of the Seventh Mule', 230:'Might of the Seventh Mule',231:'Clutch of the Miser',232:'Enduring Enchantment', 233:'Critical Protection',234:'Quick Learner',235:"Ciandra's Fortune", 236:'Charmed Smith',237:'Innate Renewal',238:"Archmage's Endurance", 239:'Enhancement of the Blade Turner',240:'Enhancement of the Arrow Turner', 241:'Enhancement of the Mace Turner',242:'Caustic Enhancement',243:'Fierce Impaler', 244:'Iron Skin of the Invincible',245:'Eye of the Remorseless',246:'Hand of the Remorseless', 294:'Master of the Steel Circle',295:'Master of the Focused Eye', 296:'Master of the Five Fold Path',297:'Frenzy of the Slayer', 298:'Iron Skin of the Invincible',299:'Jack of All Trades', 300:'Infused Void Magic',301:'Infused War Magic', 302:'Infused Life Magic',309:'Infused Item Magic', 310:'Infused Creature Magic',326:'Clutch of the Miser', 328:'Enduring Enchantment', }; const TS_AURAS: Record = { 333:'Valor / Destruction',334:'Protection',335:'Glory / Retribution', 336:'Temperance / Hardening',338:'Aetheric Vision',339:'Mana Flow', 340:'Mana Infusion',342:'Purity',343:'Craftsman',344:'Specialization',365:'World', }; const TS_RATINGS: Record = { 370:'Damage',371:'Damage Resistance',372:'Critical',373:'Critical Resistance', 374:'Critical Damage',375:'Critical Damage Resistance',376:'Healing Boost',379:'Vitality', }; const TS_SOCIETY: Record = { 287:'Celestial Hand',288:'Eldrytch Web',289:'Radiant Blood' }; const TS_MASTERIES: Record = { 354:'Melee',355:'Ranged',362:'Summoning' }; const TS_MASTERY_NAMES: Record = { 1:'Unarmed',2:'Swords',3:'Axes',4:'Maces',5:'Spears',6:'Daggers',7:'Staves',8:'Bows',9:'Crossbows',10:'Thrown',11:'Two-Handed',12:'Void',13:'War',14:'Life' }; const TS_GENERAL: Record = { 181:'Chess Rank',192:'Fishing Skill',199:'Total Augmentations',322:'Aetheria Slots',390:'Enlightenment' }; function societyRank(v: number): string { if (v >= 1001) return 'Master'; if (v >= 301) return 'Lord'; if (v >= 151) return 'Knight'; if (v >= 31) return 'Adept'; return 'Initiate'; } const gold = '#af7a30'; const navy = '#000022'; export const CharacterWindow: React.FC = ({ id, charName, zIndex, vitals, liveStats }) => { const [fetchedData, setFetchedData] = useState(null); const [leftTab, setLeftTab] = useState(0); const [rightTab, setRightTab] = useState(0); // Initial fetch from API useEffect(() => { apiFetch(`/character-stats/${encodeURIComponent(charName)}`).then(setFetchedData).catch(() => {}); }, [charName]); // Use live WS data if available (more current), fall back to API fetch const data = liveStats || fetchedData; const fmt = (n: any) => n != null ? Number(n).toLocaleString() : '\u2014'; const sd = data?.stats_data || data || {}; const attrs = sd.attributes || {}; const skills = sd.skills || {}; const vit = sd.vitals || {}; const titles = sd.titles || []; const props = sd.properties || {}; // Group skills const specSkills = Object.entries(skills).filter(([,v]:any) => v?.training === 'Specialized').sort(([a],[b]) => a.localeCompare(b)); const trainedSkills = Object.entries(skills).filter(([,v]:any) => v?.training === 'Trained').sort(([a],[b]) => a.localeCompare(b)); // Property-based data const augs = Object.entries(props).filter(([id,v]) => TS_AUGMENTATIONS[parseInt(id)] && Number(v) > 0).map(([id,v]) => ({ name: TS_AUGMENTATIONS[parseInt(id)], uses: Number(v) })); const auras = Object.entries(props).filter(([id,v]) => TS_AURAS[parseInt(id)] && Number(v) > 0).map(([id,v]) => ({ name: TS_AURAS[parseInt(id)], uses: Number(v) })); const ratings = Object.entries(props).filter(([id,v]) => TS_RATINGS[parseInt(id)] && Number(v) > 0).map(([id,v]) => ({ name: TS_RATINGS[parseInt(id)], value: Number(v) })); const generalRows: Array<{name:string;value:any}> = []; if (data?.birth) generalRows.push({ name: 'Birth', value: data.birth }); if (data?.deaths != null) generalRows.push({ name: 'Deaths', value: fmt(data.deaths) }); Object.entries(props).forEach(([id,v]) => { const nid = parseInt(id); if (TS_GENERAL[nid]) generalRows.push({ name: TS_GENERAL[nid], value: v }); }); const masteryRows: Array<{name:string;value:string}> = []; Object.entries(props).forEach(([id,v]) => { const nid = parseInt(id); if (TS_MASTERIES[nid]) masteryRows.push({ name: TS_MASTERIES[nid], value: TS_MASTERY_NAMES[Number(v)] || `Unknown (${v})` }); }); const societyRows: Array<{name:string;rank:string;value:number}> = []; Object.entries(props).forEach(([id,v]) => { const nid = parseInt(id); if (TS_SOCIETY[nid] && Number(v) > 0) societyRows.push({ name: TS_SOCIETY[nid], rank: societyRank(Number(v)), value: Number(v) }); }); const tabStyle = (active: boolean): React.CSSProperties => ({ padding: '5px 8px', fontSize: 12, fontWeight: 'bold', color: '#fff', cursor: 'pointer', userSelect: 'none', borderTop: `2px solid ${active ? gold : navy}`, borderLeft: `2px solid ${active ? gold : navy}`, borderRight: `2px solid ${active ? gold : navy}`, background: active ? 'rgba(0,100,0,0.4)' : 'transparent', }); const boxStyle: React.CSSProperties = { background: '#000', border: `2px solid ${gold}`, maxHeight: 400, overflowY: 'auto', overflowX: 'hidden' }; const colNameStyle: React.CSSProperties = { background: '#222', fontWeight: 'bold', fontSize: 12, padding: '2px 6px' }; const cellL: React.CSSProperties = { padding: '2px 6px', background: 'rgba(0,100,0,0.4)', whiteSpace: 'nowrap' }; const cellR: React.CSSProperties = { padding: '2px 6px', background: 'rgba(0,0,100,0.4)', textAlign: 'right', whiteSpace: 'nowrap' }; const cellCreation: React.CSSProperties = { padding: '2px 6px', color: '#ccc' }; return (
{/* Header */}

{charName} {data?.level || ''}

{[data?.gender, data?.race].filter(Boolean).join(' ') || 'Awaiting character data...'}
{/* XP / Luminance */}
Total XP: {fmt(data?.total_xp)}
Unassigned XP: {fmt(data?.unassigned_xp)}
Luminance: {data?.luminance_earned != null ? `${fmt(data.luminance_earned)} / ${fmt(data.luminance_total)}` : '\u2014'}
Deaths: {fmt(data?.deaths)}
{/* Tab row: two side-by-side containers */}
{/* Left tabs */}
{['Attributes', 'Skills', 'Titles'].map((t, i) => (
setLeftTab(i)}>{t}
))}
{leftTab === 0 && ( <> {/* Vitals bars */}
{[ { label: 'Health', pct: vitals?.health_percentage ?? 0, cur: vitals?.health_current, max: vitals?.health_max, bg: '#cc3333' }, { label: 'Stamina', pct: vitals?.stamina_percentage ?? 0, cur: vitals?.stamina_current, max: vitals?.stamina_max, bg: '#ccaa33' }, { label: 'Mana', pct: vitals?.mana_percentage ?? 0, cur: vitals?.mana_current, max: vitals?.mana_max, bg: '#3366cc' }, ].map(v => (
{v.label}
{v.cur ?? '\u2014'} / {v.max ?? '\u2014'}
))}
{/* Attributes table */} {['strength','endurance','coordination','quickness','focus','self'].map(a => ( ))}
AttributeCreationBase
{a.charAt(0).toUpperCase() + a.slice(1)}{attrs[a]?.creation ?? '\u2014'}{attrs[a]?.base ?? '\u2014'}
{/* Vitals base table */} {['health','stamina','mana'].map(v => ( ))}
VitalBase
{v.charAt(0).toUpperCase() + v.slice(1)}{vit[v]?.base ?? '\u2014'}
Skill Credits{fmt(sd.skill_credits)}
)} {leftTab === 1 && ( {specSkills.map(([k, v]: any) => ( ))} {trainedSkills.map(([k, v]: any) => ( ))} {specSkills.length === 0 && trainedSkills.length === 0 && }
SkillLevel
{k.replace(/_/g,' ').replace(/\b\w/g, (c:string) => c.toUpperCase())} {v.base}
{k.replace(/_/g,' ').replace(/\b\w/g, (c:string) => c.toUpperCase())} {v.base}
No skill data
)} {leftTab === 2 && (
{titles.length > 0 ? titles.map((t: string, i: number) =>
{t}
) :
No titles
}
)}
{/* Right tabs */}
{['Augmentations', 'Ratings', 'Other'].map((t, i) => (
setRightTab(i)}>{t}
))}
{rightTab === 0 && ( augs.length || auras.length ? ( <> {augs.length > 0 && ( <>
Augmentations
{augs.map(a => )}
NameUses
{a.name}{a.uses}
)} {auras.length > 0 && ( <>
Auras
{auras.map(a => )}
NameUses
{a.name}{a.uses}
)} ) :
No augmentation data
)} {rightTab === 1 && ( ratings.length > 0 ? ( {ratings.map(r => )}
RatingValue
{r.name}{r.value}
) :
No rating data
)} {rightTab === 2 && (
{generalRows.length > 0 && ( <>
General
{generalRows.map(r => )}
{r.name}{r.value}
)} {masteryRows.length > 0 && ( <>
Masteries
{masteryRows.map(m => )}
{m.name}{m.value}
)} {societyRows.length > 0 && ( <>
Society
{societyRows.map(s => )}
{s.name}{s.rank} ({s.value})
)} {generalRows.length === 0 && masteryRows.length === 0 && societyRows.length === 0 &&
No additional data
}
)}
{/* Allegiance section */} {data?.allegiance && (
Allegiance
{data.allegiance.name && } {data.allegiance.monarch?.name && } {data.allegiance.patron?.name && } {data.allegiance.rank != null && } {data.allegiance.followers != null && }
Name{data.allegiance.name}
Monarch{data.allegiance.monarch.name}
Patron{data.allegiance.patron.name}
Rank{data.allegiance.rank}
Followers{data.allegiance.followers}
)}
); };