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:
Erik 2026-04-12 18:31:06 +02:00
parent de7b547349
commit b77450b6eb
19 changed files with 529 additions and 223 deletions

View file

@ -57,7 +57,7 @@ export const MapLayout: React.FC<Props> = ({ data, onViewToggle }) => {
showHeatmap={showHeatmap}
showPortals={showPortals}
/>
<WindowRenderer characters={data.characters} chatMessages={data.chatMessages} />
<WindowRenderer characters={data.characters} chatMessages={data.chatMessages} socket={data.socketRef.current} />
<RareNotification recentRares={data.recentRares} />
</div>
</WindowManagerProvider>

View file

@ -58,8 +58,12 @@ export const Sidebar: React.FC<Props> = ({
<div className="ml-server-status">
<span className={`ml-status-dot ${isOnline ? 'online' : 'offline'}`} />
<span className="ml-status-text">Coldeve</span>
{serverHealth?.latency_ms != null && <span className="ml-status-latency">{serverHealth.latency_ms}ms</span>}
<span className="ml-status-text">Coldeve {isOnline ? 'Online' : 'Offline'}</span>
{serverHealth?.player_count != null && <span className="ml-status-detail">👥 {serverHealth.player_count}</span>}
{serverHealth?.latency_ms != null && <span className="ml-status-detail">{Math.round(serverHealth.latency_ms)}ms</span>}
{serverHealth?.uptime_seconds != null && (
<span className="ml-status-detail">Up: {Math.floor(serverHealth.uptime_seconds / 3600)}h</span>
)}
</div>
<div className="ml-counters">
@ -68,6 +72,13 @@ export const Sidebar: React.FC<Props> = ({
<div className="ml-counter kills"><span className="ml-counter-val">{totalKills.toLocaleString()}</span><span className="ml-counter-lbl">Kills</span></div>
</div>
{/* Tool links */}
<div className="ml-tool-links">
<a href="/inventory.html" className="ml-tool-link">🔍 Inventory Search</a>
<a href="/suitbuilder.html" className="ml-tool-link">🛡 Suitbuilder</a>
<a href="/debug.html" className="ml-tool-link">🐛 Player Debug</a>
</div>
{/* Map toggles */}
<div className="ml-toggles">
<label className="ml-toggle-label">

View file

@ -49,7 +49,7 @@ export const PlayerRow: React.FC<Props> = React.memo(({ player: p, vitals: v, co
<div className="ml-pr-buttons">
<button className="ml-btn accent" onClick={() => openWindow(`chat-${name}`, `Chat: ${name}`, name)}>Chat</button>
<button className="ml-btn accent" onClick={() => openWindow(`combat-${name}`, `Combat: ${name}`, name)}>Stats</button>
<button className="ml-btn accent" onClick={() => openWindow(`stats-${name}`, `Stats: ${name}`, name)}>Stats</button>
<button className="ml-btn accent" onClick={() => openWindow(`inv-${name}`, `Inventory: ${name}`, name)}>Inv</button>
<button className="ml-btn" onClick={() => openWindow(`char-${name}`, `Character: ${name}`, name)}>Char</button>
<button className="ml-btn" onClick={() => openWindow(`radar-${name}`, `Radar: ${name}`, name)}>Radar</button>

View file

@ -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>

View file

@ -1,5 +1,6 @@
import React, { useEffect, useRef, useState } from 'react';
import { DraggableWindow } from './DraggableWindow';
import { wsUrl } from '../../api/client';
interface ChatMsg {
text: string;
@ -18,9 +19,10 @@ interface Props {
charName: string;
zIndex: number;
messages: ChatMsg[];
socket: WebSocket | null;
}
export const ChatWindow: React.FC<Props> = ({ id, charName, zIndex, messages }) => {
export const ChatWindow: React.FC<Props> = ({ id, charName, zIndex, messages, socket }) => {
const msgsRef = useRef<HTMLDivElement>(null);
const [input, setInput] = useState('');
@ -28,6 +30,15 @@ export const ChatWindow: React.FC<Props> = ({ id, charName, zIndex, messages })
if (msgsRef.current) msgsRef.current.scrollTop = msgsRef.current.scrollHeight;
}, [messages.length]);
const handleSend = (e: React.FormEvent) => {
e.preventDefault();
const text = input.trim();
if (!text || !socket || socket.readyState !== WebSocket.OPEN) return;
// v1 envelope: { player_name, command }
socket.send(JSON.stringify({ player_name: charName, command: text }));
setInput('');
};
return (
<DraggableWindow id={id} title={`Chat: ${charName}`} zIndex={zIndex} width={600} height={300}>
<div className="ml-chat-messages" ref={msgsRef}>
@ -37,7 +48,7 @@ export const ChatWindow: React.FC<Props> = ({ id, charName, zIndex, messages })
</div>
))}
</div>
<form className="ml-chat-form" onSubmit={e => { e.preventDefault(); setInput(''); }}>
<form className="ml-chat-form" onSubmit={handleSend}>
<input className="ml-chat-input" value={input} onChange={e => setInput(e.target.value)} placeholder="Enter chat..." />
</form>
</DraggableWindow>

View file

@ -17,52 +17,108 @@ interface Item {
ItemSet?: string;
Imbue?: string;
EquipSkill?: string;
Mastery?: string;
Tinks?: number;
WieldLevel?: number;
ContainerId?: number;
}
export const InventoryWindow: React.FC<Props> = ({ id, charName, zIndex }) => {
const [items, setItems] = useState<Item[]>([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState('');
useEffect(() => {
setLoading(true);
apiFetch<{ items: Item[] }>(`/inventory/${encodeURIComponent(charName)}?limit=500`)
apiFetch<{ items: Item[] }>(`/inventory/${encodeURIComponent(charName)}?limit=1000`)
.then(d => setItems(d.items ?? []))
.catch(() => {})
.finally(() => setLoading(false));
}, [charName]);
const filtered = filter
? items.filter(i => i.Name?.toLowerCase().includes(filter.toLowerCase()))
: items;
// Separate equipped items (those with equipment-related fields) from pack contents
const equipped = filtered.filter(i =>
(i.ArmorLevel && i.ArmorLevel > 0) || (i.MaxDamage && i.MaxDamage > 0) ||
i.Imbue || i.ItemSet || (i.Tinks && i.Tinks > 0)
);
const other = filtered.filter(i => !equipped.includes(i));
return (
<DraggableWindow id={id} title={`Inventory: ${charName}`} zIndex={zIndex} width={650} height={450}>
<div style={{ overflowY: 'auto', flex: 1, fontSize: '0.75rem' }}>
<DraggableWindow id={id} title={`Inventory: ${charName}`} zIndex={zIndex} width={620} height={550}>
<div style={{ padding: '4px 8px', borderBottom: '1px solid #333' }}>
<input
type="text"
value={filter}
onChange={e => setFilter(e.target.value)}
placeholder="Filter items..."
style={{ width: '100%', padding: '4px 8px', fontSize: '0.75rem', background: '#222', color: '#eee', border: '1px solid #444', borderRadius: 3, outline: 'none', boxSizing: 'border-box' }}
/>
</div>
<div style={{ overflowY: 'auto', flex: 1, fontSize: '0.73rem' }}>
{loading ? (
<div style={{ padding: 16, color: '#666' }}>Loading inventory...</div>
) : items.length === 0 ? (
<div style={{ padding: 16, color: '#666' }}>No inventory data</div>
) : (
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid #444', color: '#888', fontSize: '0.7rem' }}>
<th style={{ textAlign: 'left', padding: '3px 4px' }}>Item</th>
<th style={{ textAlign: 'left', padding: '3px 4px' }}>Material</th>
<th style={{ textAlign: 'left', padding: '3px 4px' }}>Set</th>
<th style={{ textAlign: 'right', padding: '3px 4px' }}>AL</th>
<th style={{ textAlign: 'right', padding: '3px 4px' }}>Dmg</th>
<th style={{ textAlign: 'right', padding: '3px 4px' }}>Work</th>
</tr>
</thead>
<tbody>
{items.map((item, i) => (
<tr key={i} style={{ borderBottom: '1px solid #1a1a1a', color: '#ccc' }}>
<td style={{ padding: '2px 4px', maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{item.Name}</td>
<td style={{ padding: '2px 4px', color: '#888' }}>{item.Material || ''}</td>
<td style={{ padding: '2px 4px', color: '#888' }}>{item.ItemSet || ''}</td>
<td style={{ textAlign: 'right', padding: '2px 4px' }}>{item.ArmorLevel && item.ArmorLevel > 0 ? item.ArmorLevel : ''}</td>
<td style={{ textAlign: 'right', padding: '2px 4px' }}>{item.MaxDamage && item.MaxDamage > 0 ? item.MaxDamage : ''}</td>
<td style={{ textAlign: 'right', padding: '2px 4px' }}>{item.Workmanship && item.Workmanship > 0 ? item.Workmanship : ''}</td>
</tr>
))}
</tbody>
</table>
<>
{equipped.length > 0 && (
<>
<div style={{ padding: '6px 8px', fontWeight: 600, color: '#88f', fontSize: '0.7rem', borderBottom: '1px solid #333' }}>
Equipment &amp; Notable Items ({equipped.length})
</div>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid #333', color: '#777', fontSize: '0.65rem' }}>
<th style={{ textAlign: 'left', padding: '3px 6px' }}>Item</th>
<th style={{ textAlign: 'left', padding: '3px 4px' }}>Material</th>
<th style={{ textAlign: 'left', padding: '3px 4px' }}>Set</th>
<th style={{ textAlign: 'left', padding: '3px 4px' }}>Imbue</th>
<th style={{ textAlign: 'right', padding: '3px 4px' }}>AL</th>
<th style={{ textAlign: 'right', padding: '3px 4px' }}>Dmg</th>
<th style={{ textAlign: 'right', padding: '3px 4px' }}>Wk</th>
<th style={{ textAlign: 'right', padding: '3px 4px' }}>Tink</th>
</tr>
</thead>
<tbody>
{equipped.map((item, i) => (
<tr key={i} style={{ borderBottom: '1px solid #1a1a1a', color: '#ccc' }}>
<td style={{ padding: '2px 6px', maxWidth: 180, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontWeight: 500 }}>{item.Name}</td>
<td style={{ padding: '2px 4px', color: '#888', fontSize: '0.68rem' }}>{item.Material || ''}</td>
<td style={{ padding: '2px 4px', color: '#9d9', fontSize: '0.68rem' }}>{item.ItemSet || ''}</td>
<td style={{ padding: '2px 4px', color: '#da8', fontSize: '0.68rem' }}>{item.Imbue || ''}</td>
<td style={{ textAlign: 'right', padding: '2px 4px' }}>{item.ArmorLevel && item.ArmorLevel > 0 ? item.ArmorLevel : ''}</td>
<td style={{ textAlign: 'right', padding: '2px 4px', color: '#f88' }}>{item.MaxDamage && item.MaxDamage > 0 ? item.MaxDamage : ''}</td>
<td style={{ textAlign: 'right', padding: '2px 4px' }}>{item.Workmanship && item.Workmanship > 0 ? item.Workmanship : ''}</td>
<td style={{ textAlign: 'right', padding: '2px 4px', color: '#8af' }}>{item.Tinks && item.Tinks > 0 ? item.Tinks : ''}</td>
</tr>
))}
</tbody>
</table>
</>
)}
{other.length > 0 && (
<>
<div style={{ padding: '6px 8px', fontWeight: 600, color: '#888', fontSize: '0.7rem', borderBottom: '1px solid #333' }}>
Pack Contents ({other.length})
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 2, padding: 4 }}>
{other.map((item, i) => (
<div
key={i}
style={{ fontSize: '0.65rem', padding: '2px 6px', background: '#252525', borderRadius: 3, color: '#aaa', cursor: 'default' }}
title={`${item.Name}${item.Value ? ` (${item.Value} pyreal)` : ''}`}
>
{item.Name}
</div>
))}
</div>
</>
)}
</>
)}
</div>
</DraggableWindow>

View file

@ -0,0 +1,58 @@
import React, { useEffect } from 'react';
import { DraggableWindow } from './DraggableWindow';
interface Props {
id: string;
charName: string;
zIndex: number;
socket: WebSocket | null;
nearbyObjects: any[];
}
export const RadarWindow: React.FC<Props> = ({ id, charName, zIndex, socket, nearbyObjects }) => {
// Send start_radar when window opens, stop_radar when it closes
useEffect(() => {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ player_name: charName, command: '/mm radar start' }));
}
return () => {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ player_name: charName, command: '/mm radar stop' }));
}
};
}, [charName, socket]);
const objects = nearbyObjects || [];
return (
<DraggableWindow id={id} title={`Radar: ${charName}`} zIndex={zIndex} width={450} height={400}>
<div style={{ flex: 1, overflowY: 'auto', fontSize: '0.73rem' }}>
{objects.length === 0 ? (
<div style={{ padding: 16, color: '#666', textAlign: 'center' }}>
Waiting for nearby objects data...<br/>
<span style={{ fontSize: '0.65rem' }}>The plugin will start sending radar data shortly.</span>
</div>
) : (
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid #444', color: '#888', fontSize: '0.65rem' }}>
<th style={{ textAlign: 'left', padding: '3px 6px' }}>Name</th>
<th style={{ textAlign: 'left', padding: '3px 4px' }}>Type</th>
<th style={{ textAlign: 'right', padding: '3px 4px' }}>Dist</th>
</tr>
</thead>
<tbody>
{objects.map((obj: any, i: number) => (
<tr key={i} style={{ borderBottom: '1px solid #1a1a1a', color: '#ccc' }}>
<td style={{ padding: '2px 6px' }}>{obj.name}</td>
<td style={{ padding: '2px 4px', color: '#888' }}>{obj.type || obj.object_class || ''}</td>
<td style={{ textAlign: 'right', padding: '2px 4px' }}>{obj.distance ? `${Math.round(obj.distance)}m` : ''}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</DraggableWindow>
);
};

View file

@ -0,0 +1,54 @@
import React, { useState } from 'react';
import { DraggableWindow } from './DraggableWindow';
interface Props { id: string; charName: string; zIndex: number; }
const PANELS = [
{ title: 'Kills per Hour', id: 1 },
{ title: 'Memory (MB)', id: 2 },
{ title: 'CPU (%)', id: 3 },
{ title: 'Mem Handles', id: 4 },
];
const TIME_RANGES = [
{ label: '1H', value: 'now-1h' },
{ label: '6H', value: 'now-6h' },
{ label: '24H', value: 'now-24h' },
{ label: '7D', value: 'now-7d' },
];
export const StatsWindow: React.FC<Props> = ({ id, charName, zIndex }) => {
const [timeRange, setTimeRange] = useState('now-24h');
const iframeUrl = (panelId: number) =>
`/grafana/d-solo/dereth-tracker/dereth-tracker-dashboard?panelId=${panelId}&var-character=${encodeURIComponent(charName)}&from=${timeRange}&to=now&theme=light`;
return (
<DraggableWindow id={id} title={`Stats: ${charName}`} zIndex={zIndex} width={750} height={480}>
<div className="ml-stats-controls">
{TIME_RANGES.map(r => (
<button
key={r.value}
className={`ml-stats-range-btn ${timeRange === r.value ? 'active' : ''}`}
onClick={() => setTimeRange(r.value)}
>
{r.label}
</button>
))}
</div>
<div className="ml-stats-grid">
{PANELS.map(p => (
<div key={p.id} className="ml-stats-panel">
<iframe
src={iframeUrl(p.id)}
width="100%"
height="100%"
frameBorder="0"
title={p.title}
/>
</div>
))}
</div>
</DraggableWindow>
);
};

View file

@ -1,16 +1,19 @@
import React from 'react';
import { useWindowManager } from '../../contexts/WindowManagerContext';
import { ChatWindow } from './ChatWindow';
import { StatsWindow } from './StatsWindow';
import { CharacterWindow } from './CharacterWindow';
import { InventoryWindow } from './InventoryWindow';
import { RadarWindow } from './RadarWindow';
import type { CharacterState } from '../../types';
interface Props {
characters: Map<string, CharacterState>;
chatMessages: Map<string, Array<{ text: string; color?: number; timestamp: string }>>;
socket: WebSocket | null;
}
export const WindowRenderer: React.FC<Props> = ({ characters, chatMessages }) => {
export const WindowRenderer: React.FC<Props> = ({ characters, chatMessages, socket }) => {
const { windows } = useWindowManager();
return (
@ -19,18 +22,26 @@ export const WindowRenderer: React.FC<Props> = ({ characters, chatMessages }) =>
const charName = w.charName ?? '';
if (w.id.startsWith('chat-')) {
return <ChatWindow key={w.id} id={w.id} charName={charName} zIndex={w.zIndex} messages={chatMessages.get(charName) ?? []} />;
return <ChatWindow key={w.id} id={w.id} charName={charName} zIndex={w.zIndex}
messages={chatMessages.get(charName) ?? []} socket={socket} />;
}
if (w.id.startsWith('char-') || w.id.startsWith('combat-')) {
if (w.id.startsWith('stats-')) {
return <StatsWindow key={w.id} id={w.id} charName={charName} zIndex={w.zIndex} />;
}
if (w.id.startsWith('char-')) {
return <CharacterWindow key={w.id} id={w.id} charName={charName} zIndex={w.zIndex} />;
}
if (w.id.startsWith('inv-')) {
return <InventoryWindow key={w.id} id={w.id} charName={charName} zIndex={w.zIndex} />;
}
// Fallback: generic window with placeholder
return (
<ChatWindow key={w.id} id={w.id} charName={charName} zIndex={w.zIndex} messages={[]} />
);
if (w.id.startsWith('radar-')) {
return <RadarWindow key={w.id} id={w.id} charName={charName} zIndex={w.zIndex}
socket={socket} nearbyObjects={[]} />;
}
if (w.id.startsWith('combat-')) {
return <CharacterWindow key={w.id} id={w.id} charName={charName} zIndex={w.zIndex} />;
}
return null;
})}
</>
);