fix(v2): v1-faithful character window + improved inventory/radar

Character Window — now matches v1 exactly:
- Navy blue background (#000022) with gold/bronze borders (#af7a30)
- Two side-by-side 320px tab containers
- Left tabs: Attributes (vital bars with gold borders + attribute
  table with green/blue cell backgrounds + vitals base + skill
  credits) | Skills (specialized=purple gradient, trained=teal
  gradient, grouped and sorted) | Titles
- Right tabs: Augmentations (with auras section) | Ratings | Other
  (allegiance with followers)
- Active tab: green tint background with gold top/side borders
- Header: large name + level (gold, right-floated) + race/gender
- XP grid: total, unassigned, luminance earned/total, deaths
- Live vital bars from WebSocket vitals data
- Augmentation/aura/rating property ID maps from v1

Radar — passes full radarData message (not just objects array)
so canvas can render map background + entity positions properly

WindowRenderer — passes live vitals to CharacterWindow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-12 21:10:15 +02:00
parent cf078b7765
commit 3cb8768dc1
5 changed files with 310 additions and 254 deletions

View file

@ -2,171 +2,226 @@ import React, { useEffect, useState } from 'react';
import { DraggableWindow } from './DraggableWindow';
import { apiFetch } from '../../api/client';
interface Props { id: string; charName: string; zIndex: number; }
interface Props { id: string; charName: string; zIndex: number; vitals?: any; }
export const CharacterWindow: React.FC<Props> = ({ id, charName, zIndex }) => {
// Property ID maps matching v1's TS_AUGMENTATIONS, TS_AURAS, TS_RATINGS, etc.
const TS_AUGMENTATIONS: Record<number, string> = {
369:'Blade Turner',370:'Arrow Turner',371:'Mace Turner',
372:'Caustic Enhancement',373:'Fiery Enhancement',374:'Icy Enhancement',375:'Lightning Enhancement',
376:'Critical Protection',377:'Frenzy',362:'Iron Skin',363:'Eye of the Remorseless',364:'Hand of the Remorseless',
365:'Ciandra\'s Essence',366:'Yoshi\'s Essence',367:'Jibril\'s Essence',368:'Celdiseth\'s Essence',
};
const TS_AURAS: Record<number, string> = { 378:'Valor',379:'Protection',380:'Glory',381:'Temperance',382:'Aetheric Vision',383:'Mana Flow',384:'Mana Infusion',385:'Purity',386:'Craftsman',387:'Specialization',388:'World' };
const TS_RATINGS: Record<number, string> = { 354:'Damage Rating',355:'Damage Resist Rating',356:'Crit Rating',357:'Crit Resist Rating',358:'Crit Damage Rating',359:'Crit Damage Resist Rating',360:'Heal Boost Rating',361:'Vitality Rating' };
const gold = '#af7a30';
const navy = '#000022';
export const CharacterWindow: React.FC<Props> = ({ id, charName, zIndex, vitals }) => {
const [data, setData] = useState<any>(null);
const [tab, setTab] = useState<'attr' | 'skills' | 'titles'>('attr');
const [rightTab, setRightTab] = useState<'augs' | 'ratings' | 'other'>('augs');
const [leftTab, setLeftTab] = useState(0);
const [rightTab, setRightTab] = useState(0);
useEffect(() => {
apiFetch<any>(`/character-stats/${encodeURIComponent(charName)}`)
.then(d => setData(d)).catch(() => {});
apiFetch<any>(`/character-stats/${encodeURIComponent(charName)}`).then(setData).catch(() => {});
}, [charName]);
if (!data) {
return (
<DraggableWindow id={id} title={`Character: ${charName}`} zIndex={zIndex} width={600} height={500}>
<div style={{ padding: 20, color: '#666' }}>Loading character data...</div>
</DraggableWindow>
);
}
const sd = data.stats_data || data;
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 vitals = sd.vitals || {};
const vit = sd.vitals || {};
const titles = sd.titles || [];
const properties = sd.properties || {};
const props = sd.properties || {};
const specSkills = Object.entries(skills).filter(([, v]: [string, any]) => v?.training === 'Specialized');
const trainedSkills = Object.entries(skills).filter(([, v]: [string, any]) => v?.training === 'Trained');
// 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));
// Augmentations
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 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 (
<DraggableWindow id={id} title={`Character: ${charName}`} zIndex={zIndex} width={620} height={520}>
<div style={{ flex: 1, overflowY: 'auto', fontSize: '0.75rem', color: '#ccc' }}>
{/* Header: Level + Race + XP */}
<div style={{ padding: '8px 10px', borderBottom: '1px solid #333', display: 'flex', flexWrap: 'wrap', gap: '12px', fontSize: '0.78rem' }}>
{data.level && <span><strong>Lv {data.level}</strong></span>}
{data.race && <span>{data.race}</span>}
{data.gender && <span>{data.gender}</span>}
{data.total_xp != null && <span>XP: {Number(data.total_xp).toLocaleString()}</span>}
{data.unassigned_xp != null && <span>Unasgn: {Number(data.unassigned_xp).toLocaleString()}</span>}
{data.luminance_earned != null && <span>Lum: {Number(data.luminance_earned).toLocaleString()}</span>}
{data.deaths != null && <span>Deaths: {data.deaths}</span>}
{sd.skill_credits != null && <span>Skill Credits: {sd.skill_credits}</span>}
<DraggableWindow id={id} title={`Character: ${charName}`} zIndex={zIndex} width={740} height={600}>
<div style={{ background: navy, color: '#fff', font: '14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif', overflowY: 'auto', padding: '10px 15px 15px', flex: 1 }}>
{/* Header */}
<div style={{ marginBottom: 10 }}>
<h1 style={{ margin: '0 0 2px', fontSize: 28, fontWeight: 'bold' }}>
{charName}
<span style={{ fontSize: '200%', color: '#fff27f', float: 'right' }}>{data?.level || ''}</span>
</h1>
<div style={{ fontSize: '85%', color: 'gold' }}>
{[data?.gender, data?.race].filter(Boolean).join(' ') || 'Awaiting character data...'}
</div>
</div>
<div style={{ display: 'flex', minHeight: 350 }}>
{/* Left panel */}
<div style={{ flex: 1, borderRight: '1px solid #333' }}>
<div style={{ display: 'flex', gap: 2, padding: '4px 6px', borderBottom: '1px solid #333' }}>
{(['attr', 'skills', 'titles'] as const).map(t => (
<button key={t} className={`ml-stats-range-btn ${tab === t ? 'active' : ''}`}
onClick={() => setTab(t)} style={{ flex: 1 }}>
{t === 'attr' ? 'Attributes' : t === 'skills' ? 'Skills' : 'Titles'}
</button>
{/* XP / Luminance */}
<div style={{ fontSize: '85%', margin: '6px 0 10px', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0 20px' }}>
<div>Total XP: {fmt(data?.total_xp)}</div>
<div style={{ textAlign: 'right' }}>Unassigned XP: {fmt(data?.unassigned_xp)}</div>
<div>Luminance: {data?.luminance_earned != null ? `${fmt(data.luminance_earned)} / ${fmt(data.luminance_total)}` : '\u2014'}</div>
<div style={{ textAlign: 'right' }}>Deaths: {fmt(data?.deaths)}</div>
</div>
{/* Tab row: two side-by-side containers */}
<div style={{ display: 'flex', gap: 13, flexWrap: 'wrap' }}>
{/* Left tabs */}
<div style={{ width: 320 }}>
<div style={{ height: 30, display: 'flex' }}>
{['Attributes', 'Skills', 'Titles'].map((t, i) => (
<div key={t} style={tabStyle(leftTab === i)} onClick={() => setLeftTab(i)}>{t}</div>
))}
</div>
<div style={{ padding: 8 }}>
{tab === 'attr' && (
<div style={boxStyle}>
{leftTab === 0 && (
<>
{/* Vital bars */}
{Object.entries(vitals).map(([k, v]: [string, any]) => (
<div key={k} style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 3 }}>
<span style={{ width: 55, color: '#888', fontSize: '0.7rem' }}>{k}</span>
<div style={{ flex: 1, height: 6, background: '#222', borderRadius: 3, overflow: 'hidden' }}>
<div style={{ width: '100%', height: '100%', background: k === 'health' ? '#c44' : k === 'stamina' ? '#ca0' : '#48f', borderRadius: 3 }} />
{/* Vitals bars */}
<div style={{ padding: '6px 8px', display: 'flex', flexDirection: 'column', gap: 8, borderBottom: `2px solid ${gold}` }}>
{[
{ 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 => (
<div key={v.label} style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ width: 55, fontSize: 12, color: '#ccc' }}>{v.label}</span>
<div style={{ flex: 1, height: 14, overflow: 'hidden', position: 'relative', border: `1px solid ${gold}` }}>
<div style={{ height: '100%', width: `${v.pct}%`, background: v.bg, transition: 'width 0.5s ease' }} />
</div>
<span style={{ width: 80, textAlign: 'right', fontSize: 12, color: '#ccc' }}>{v.cur ?? '\u2014'} / {v.max ?? '\u2014'}</span>
</div>
<span style={{ fontSize: '0.68rem', color: '#aaa', width: 40, textAlign: 'right' }}>{v?.base ?? v}</span>
</div>
))}
{/* Attributes table */}
<div style={{ marginTop: 8 }}>
<div style={{ fontWeight: 600, color: '#6aadff', marginBottom: 4, fontSize: '0.72rem' }}>Attributes</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1px 16px' }}>
{Object.entries(attrs).map(([k, v]: [string, any]) => (
<div key={k} style={{ display: 'flex', justifyContent: 'space-between', padding: '1px 0' }}>
<span style={{ color: '#888', textTransform: 'capitalize' }}>{k}</span>
<span>{v?.base ?? v} {v?.creation != null ? `(${v.creation})` : ''}</span>
</div>
))}
</div>
))}
</div>
{/* Attributes table */}
<table style={{ width: '100%', fontSize: 13, borderCollapse: 'collapse' }}>
<thead><tr><td style={colNameStyle}>Attribute</td><td style={colNameStyle}>Creation</td><td style={colNameStyle}>Base</td></tr></thead>
<tbody>
{['strength','endurance','coordination','quickness','focus','self'].map(a => (
<tr key={a}><td style={cellL}>{a.charAt(0).toUpperCase() + a.slice(1)}</td><td style={cellCreation}>{attrs[a]?.creation ?? '\u2014'}</td><td style={cellR}>{attrs[a]?.base ?? '\u2014'}</td></tr>
))}
</tbody>
</table>
{/* Vitals base table */}
<table style={{ width: '100%', fontSize: 13, borderCollapse: 'collapse' }}>
<thead><tr><td style={colNameStyle}>Vital</td><td style={colNameStyle}>Base</td></tr></thead>
<tbody>
{['health','stamina','mana'].map(v => (
<tr key={v}><td style={cellL}>{v.charAt(0).toUpperCase() + v.slice(1)}</td><td style={cellR}>{vit[v]?.base ?? '\u2014'}</td></tr>
))}
</tbody>
</table>
<table style={{ width: '100%', fontSize: 13, borderCollapse: 'collapse' }}>
<tbody><tr><td style={cellL}>Skill Credits</td><td style={cellR}>{fmt(sd.skill_credits)}</td></tr></tbody>
</table>
</>
)}
{tab === 'skills' && (
<>
{specSkills.length > 0 && (
<div style={{ marginBottom: 8 }}>
<div style={{ fontWeight: 600, color: '#6aadff', marginBottom: 3, fontSize: '0.7rem' }}>Specialized</div>
{specSkills.sort(([a], [b]) => a.localeCompare(b)).map(([k, v]: [string, any]) => (
<div key={k} style={{ display: 'flex', justifyContent: 'space-between', padding: '1px 0' }}>
<span style={{ color: '#ccc' }}>{k}</span>
<span style={{ color: '#8f8' }}>{v?.base ?? v}</span>
</div>
))}
</div>
)}
{trainedSkills.length > 0 && (
<div>
<div style={{ fontWeight: 600, color: '#888', marginBottom: 3, fontSize: '0.7rem' }}>Trained</div>
{trainedSkills.sort(([a], [b]) => a.localeCompare(b)).map(([k, v]: [string, any]) => (
<div key={k} style={{ display: 'flex', justifyContent: 'space-between', padding: '1px 0' }}>
<span style={{ color: '#999' }}>{k}</span>
<span>{v?.base ?? v}</span>
</div>
))}
</div>
)}
{specSkills.length === 0 && trainedSkills.length === 0 && (
<div style={{ color: '#555' }}>No skill data available</div>
)}
</>
{leftTab === 1 && (
<table style={{ width: '100%', fontSize: 13, borderCollapse: 'collapse' }}>
<thead><tr><td style={colNameStyle}>Skill</td><td style={colNameStyle}>Level</td></tr></thead>
<tbody>
{specSkills.map(([k, v]: any) => (
<tr key={k}><td style={{ padding: '2px 6px', background: 'linear-gradient(to right, #392067, #392067, black)' }}>{k.replace(/_/g,' ').replace(/\b\w/g, (c:string) => c.toUpperCase())}</td>
<td style={{ ...cellR, background: 'linear-gradient(to right, #392067, #392067, black)' }}>{v.base}</td></tr>
))}
{trainedSkills.map(([k, v]: any) => (
<tr key={k}><td style={{ padding: '2px 6px', background: 'linear-gradient(to right, #0f3c3e, #0f3c3e, black)' }}>{k.replace(/_/g,' ').replace(/\b\w/g, (c:string) => c.toUpperCase())}</td>
<td style={{ ...cellR, background: 'linear-gradient(to right, #0f3c3e, #0f3c3e, black)' }}>{v.base}</td></tr>
))}
{specSkills.length === 0 && trainedSkills.length === 0 && <tr><td colSpan={2} style={{ padding: 10, color: '#666', fontStyle: 'italic', textAlign: 'center' }}>No skill data</td></tr>}
</tbody>
</table>
)}
{tab === 'titles' && (
titles.length > 0 ? (
<ul style={{ paddingLeft: 16, margin: 0 }}>
{titles.map((t: string, i: number) => <li key={i} style={{ padding: '1px 0' }}>{t}</li>)}
</ul>
) : <div style={{ color: '#555' }}>No titles</div>
{leftTab === 2 && (
<div style={{ padding: '6px 10px', fontSize: 13 }}>
{titles.length > 0 ? titles.map((t: string, i: number) => <div key={i} style={{ padding: '1px 0' }}>{t}</div>) :
<div style={{ color: '#666', fontStyle: 'italic', textAlign: 'center', padding: 10 }}>No titles</div>}
</div>
)}
</div>
</div>
{/* Right panel */}
<div style={{ width: 220 }}>
<div style={{ display: 'flex', gap: 2, padding: '4px 4px', borderBottom: '1px solid #333' }}>
{(['augs', 'ratings', 'other'] as const).map(t => (
<button key={t} className={`ml-stats-range-btn ${rightTab === t ? 'active' : ''}`}
onClick={() => setRightTab(t)} style={{ flex: 1, fontSize: '0.6rem' }}>
{t === 'augs' ? 'Augs' : t === 'ratings' ? 'Ratings' : 'Other'}
</button>
{/* Right tabs */}
<div style={{ width: 320 }}>
<div style={{ height: 30, display: 'flex' }}>
{['Augmentations', 'Ratings', 'Other'].map((t, i) => (
<div key={t} style={tabStyle(rightTab === i)} onClick={() => setRightTab(i)}>{t}</div>
))}
</div>
<div style={{ padding: 6, fontSize: '0.7rem' }}>
{rightTab === 'augs' && (
Object.keys(properties).length > 0 ? (
Object.entries(properties).slice(0, 20).map(([k, v]) => (
<div key={k} style={{ display: 'flex', justifyContent: 'space-between', padding: '1px 0' }}>
<span style={{ color: '#888' }}>{k}</span>
<span>{String(v)}</span>
</div>
))
) : <div style={{ color: '#555' }}>No augmentation data</div>
<div style={boxStyle}>
{rightTab === 0 && (
augs.length || auras.length ? (
<>
{augs.length > 0 && (
<><div style={{ background: '#222', padding: '4px 8px', fontWeight: 'bold', fontSize: 13, borderBottom: `1px solid ${gold}` }}>Augmentations</div>
<table style={{ width: '100%', fontSize: 13, borderCollapse: 'collapse' }}>
<thead><tr><td style={colNameStyle}>Name</td><td style={colNameStyle}>Uses</td></tr></thead>
<tbody>{augs.map(a => <tr key={a.name}><td style={{ padding: '2px 6px' }}>{a.name}</td><td style={{ padding: '2px 6px', textAlign: 'right' }}>{a.uses}</td></tr>)}</tbody>
</table></>
)}
{auras.length > 0 && (
<><div style={{ background: '#222', padding: '4px 8px', fontWeight: 'bold', fontSize: 13, borderBottom: `1px solid ${gold}` }}>Auras</div>
<table style={{ width: '100%', fontSize: 13, borderCollapse: 'collapse' }}>
<thead><tr><td style={colNameStyle}>Name</td><td style={colNameStyle}>Uses</td></tr></thead>
<tbody>{auras.map(a => <tr key={a.name}><td style={{ padding: '2px 6px' }}>{a.name}</td><td style={{ padding: '2px 6px', textAlign: 'right' }}>{a.uses}</td></tr>)}</tbody>
</table></>
)}
</>
) : <div style={{ color: '#666', fontStyle: 'italic', textAlign: 'center', padding: 10 }}>No augmentation data</div>
)}
{rightTab === 'ratings' && (
<div style={{ color: '#555' }}>Rating data will appear here from character_stats events</div>
{rightTab === 1 && (
ratings.length > 0 ? (
<table style={{ width: '100%', fontSize: 13, borderCollapse: 'collapse' }}>
<thead><tr><td style={colNameStyle}>Rating</td><td style={colNameStyle}>Value</td></tr></thead>
<tbody>{ratings.map(r => <tr key={r.name}><td style={{ padding: '2px 6px' }}>{r.name}</td><td style={{ padding: '2px 6px', textAlign: 'right' }}>{r.value}</td></tr>)}</tbody>
</table>
) : <div style={{ color: '#666', fontStyle: 'italic', textAlign: 'center', padding: 10 }}>No rating data</div>
)}
{rightTab === 'other' && (
<>
{data.allegiance && (
<div>
<div style={{ fontWeight: 600, color: '#6aadff', marginBottom: 3 }}>Allegiance</div>
{data.allegiance.name && <div>Name: {data.allegiance.name}</div>}
{data.allegiance.monarch?.name && <div>Monarch: {data.allegiance.monarch.name}</div>}
{data.allegiance.patron?.name && <div>Patron: {data.allegiance.patron.name}</div>}
{data.allegiance.rank != null && <div>Rank: {data.allegiance.rank}</div>}
</div>
)}
</>
{rightTab === 2 && (
<div style={{ padding: 6 }}>
{data?.allegiance ? (
<><div style={{ background: '#222', padding: '4px 8px', fontWeight: 'bold', fontSize: 13, borderBottom: `1px solid ${gold}` }}>Allegiance</div>
<table style={{ width: '100%', fontSize: 13, borderCollapse: 'collapse' }}>
<tbody>
{data.allegiance.name && <tr><td style={{ padding: '2px 6px', color: '#ccc', width: 100 }}>Name</td><td style={{ padding: '2px 6px' }}>{data.allegiance.name}</td></tr>}
{data.allegiance.monarch?.name && <tr><td style={{ padding: '2px 6px', color: '#ccc' }}>Monarch</td><td style={{ padding: '2px 6px' }}>{data.allegiance.monarch.name}</td></tr>}
{data.allegiance.patron?.name && <tr><td style={{ padding: '2px 6px', color: '#ccc' }}>Patron</td><td style={{ padding: '2px 6px' }}>{data.allegiance.patron.name}</td></tr>}
{data.allegiance.rank != null && <tr><td style={{ padding: '2px 6px', color: '#ccc' }}>Rank</td><td style={{ padding: '2px 6px' }}>{data.allegiance.rank}</td></tr>}
</tbody>
</table></>
) : <div style={{ color: '#666', fontStyle: 'italic', textAlign: 'center', padding: 10 }}>No additional data</div>}
</div>
)}
</div>
</div>
</div>
{/* Allegiance section */}
{data?.allegiance && (
<div style={{ marginTop: 5, border: `2px solid ${gold}`, background: '#000' }}>
<div style={{ background: '#222', padding: '4px 8px', fontWeight: 'bold', fontSize: 13, borderBottom: `1px solid ${gold}` }}>Allegiance</div>
<table style={{ width: '100%', fontSize: 13, borderCollapse: 'collapse' }}>
<tbody>
{data.allegiance.name && <tr><td style={{ padding: '2px 6px', color: '#ccc', width: 100 }}>Name</td><td style={{ padding: '2px 6px' }}>{data.allegiance.name}</td></tr>}
{data.allegiance.monarch?.name && <tr><td style={{ padding: '2px 6px', color: '#ccc' }}>Monarch</td><td style={{ padding: '2px 6px' }}>{data.allegiance.monarch.name}</td></tr>}
{data.allegiance.patron?.name && <tr><td style={{ padding: '2px 6px', color: '#ccc' }}>Patron</td><td style={{ padding: '2px 6px' }}>{data.allegiance.patron.name}</td></tr>}
{data.allegiance.rank != null && <tr><td style={{ padding: '2px 6px', color: '#ccc' }}>Rank</td><td style={{ padding: '2px 6px' }}>{data.allegiance.rank}</td></tr>}
{data.allegiance.followers != null && <tr><td style={{ padding: '2px 6px', color: '#ccc' }}>Followers</td><td style={{ padding: '2px 6px' }}>{data.allegiance.followers}</td></tr>}
</tbody>
</table>
</div>
)}
</div>
</DraggableWindow>
);

View file

@ -33,7 +33,8 @@ export const WindowRenderer: React.FC<Props> = ({ characters, chatMessages, near
case 'stats':
return <StatsWindow key={w.id} id={w.id} charName={charName} zIndex={w.zIndex} />;
case 'char':
return <CharacterWindow key={w.id} id={w.id} charName={charName} zIndex={w.zIndex} />;
return <CharacterWindow key={w.id} id={w.id} charName={charName} zIndex={w.zIndex}
vitals={characters.get(charName)?.vitals ?? undefined} />;
case 'inv':
return <InventoryWindow key={w.id} id={w.id} charName={charName} zIndex={w.zIndex} />;
case 'radar':