fix(v2): comprehensive bug fix round — all reported issues
1. Server stats: now shows player count, latency (rounded), uptime hours
2. Rares/Kills counters: fixed API response fields (all_time/total)
3. Chat send: wired socket.send with v1 envelope { player_name, command }
4. Stats button: opens Grafana iframe grid (4 panels, time range selector)
5. Char button: opens character window with attributes/skills/vitals from
/character-stats/{name} API, structured display with sections
6. Inventory button: full inventory window with equipment table (material,
set, imbue, AL, dmg, work, tink) + pack contents pill grid + filter
7. Radar button: opens radar window, sends start/stop commands via socket
8. Sidebar links: added Inventory Search, Suitbuilder, Player Debug
9. Color palette: expanded from 30 to 60 distinct colors matching v1
10. Window types properly routed: stats- prefix → Grafana, char- → character
data, inv- → inventory, radar- → radar with socket commands
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
de7b547349
commit
b77450b6eb
19 changed files with 529 additions and 223 deletions
|
|
@ -2,58 +2,88 @@ 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; }
|
||||
|
||||
export const CharacterWindow: React.FC<Props> = ({ id, charName, zIndex }) => {
|
||||
const [stats, setStats] = useState<Record<string, unknown> | null>(null);
|
||||
const [data, setData] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
apiFetch<Record<string, unknown>>(`/combat-stats/${encodeURIComponent(charName)}`)
|
||||
.then(setStats).catch(() => {});
|
||||
apiFetch<any>(`/character-stats/${encodeURIComponent(charName)}`)
|
||||
.then(setData).catch(() => {});
|
||||
}, [charName]);
|
||||
|
||||
const session = (stats as any)?.session;
|
||||
const sd = data?.stats_data;
|
||||
|
||||
return (
|
||||
<DraggableWindow id={id} title={`Character: ${charName}`} zIndex={zIndex} width={500} height={400}>
|
||||
<div style={{ padding: 8, fontSize: '0.8rem', color: '#ccc', overflowY: 'auto', flex: 1 }}>
|
||||
{session ? (
|
||||
<>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<strong>Session</strong>: {session.total_kills ?? 0} kills, {(session.total_damage_given ?? 0).toLocaleString()} dmg given
|
||||
</div>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<strong>Monsters fought</strong>: {Object.keys(session.monsters ?? {}).filter((k: string) => k !== '__cloak_surges__').length}
|
||||
</div>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.75rem' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '1px solid #444', color: '#888' }}>
|
||||
<th style={{ textAlign: 'left', padding: '2px 4px' }}>Monster</th>
|
||||
<th style={{ textAlign: 'right', padding: '2px 4px' }}>Kills</th>
|
||||
<th style={{ textAlign: 'right', padding: '2px 4px' }}>Dmg Given</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.values(session.monsters ?? {})
|
||||
.filter((m: any) => m.name !== '__cloak_surges__')
|
||||
.sort((a: any, b: any) => (b.damage_given ?? 0) - (a.damage_given ?? 0))
|
||||
.slice(0, 30)
|
||||
.map((m: any) => (
|
||||
<tr key={m.name} style={{ borderBottom: '1px solid #222' }}>
|
||||
<td style={{ padding: '2px 4px' }}>{m.name}</td>
|
||||
<td style={{ textAlign: 'right', padding: '2px 4px' }}>{m.kill_count}</td>
|
||||
<td style={{ textAlign: 'right', padding: '2px 4px' }}>{(m.damage_given ?? 0).toLocaleString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
<DraggableWindow id={id} title={`Character: ${charName}`} zIndex={zIndex} width={550} height={500}>
|
||||
<div style={{ padding: 10, fontSize: '0.78rem', color: '#ccc', overflowY: 'auto', flex: 1 }}>
|
||||
{!sd ? (
|
||||
<div style={{ color: '#666' }}>Loading character data...</div>
|
||||
) : (
|
||||
<div style={{ color: '#666' }}>Loading combat data...</div>
|
||||
<>
|
||||
{/* Level + XP header */}
|
||||
<div style={{ display: 'flex', gap: 16, marginBottom: 10, flexWrap: 'wrap' }}>
|
||||
{data?.level && <span><strong>Level:</strong> {data.level}</span>}
|
||||
{data?.total_xp != null && <span><strong>Total XP:</strong> {Number(data.total_xp).toLocaleString()}</span>}
|
||||
{data?.unassigned_xp != null && <span><strong>Unassigned:</strong> {Number(data.unassigned_xp).toLocaleString()}</span>}
|
||||
{data?.luminance_earned != null && <span><strong>Luminance:</strong> {Number(data.luminance_earned).toLocaleString()}</span>}
|
||||
{data?.deaths != null && <span><strong>Deaths:</strong> {data.deaths}</span>}
|
||||
</div>
|
||||
|
||||
{/* Attributes */}
|
||||
{sd.attributes && (
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<div style={{ fontWeight: 600, color: '#88f', marginBottom: 4 }}>Attributes</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '2px 12px' }}>
|
||||
{Object.entries(sd.attributes).map(([k, v]: [string, any]) => (
|
||||
<div key={k} style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#888' }}>{k}</span>
|
||||
<span>{typeof v === 'object' ? (v.buffed ?? v.base ?? JSON.stringify(v)) : v}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Skills */}
|
||||
{sd.skills && (
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<div style={{ fontWeight: 600, color: '#88f', marginBottom: 4 }}>Skills</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1px 12px', fontSize: '0.72rem' }}>
|
||||
{Object.entries(sd.skills)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([k, v]: [string, any]) => (
|
||||
<div key={k} style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#888' }}>{k}</span>
|
||||
<span>{typeof v === 'object' ? (v.buffed ?? v.base ?? JSON.stringify(v)) : v}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Vitals */}
|
||||
{sd.vitals && (
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, color: '#88f', marginBottom: 4 }}>Vitals</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '2px 12px' }}>
|
||||
{Object.entries(sd.vitals).map(([k, v]: [string, any]) => (
|
||||
<div key={k} style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#888' }}>{k}</span>
|
||||
<span>{typeof v === 'object' ? (v.current ?? JSON.stringify(v)) : v}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Raw data fallback if no structured sections */}
|
||||
{!sd.attributes && !sd.skills && !sd.vitals && (
|
||||
<pre style={{ fontSize: '0.68rem', color: '#888', whiteSpace: 'pre-wrap' }}>
|
||||
{JSON.stringify(sd, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</DraggableWindow>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue