fix(v2): radar data flow + inventory icons
Radar:
- nearby_objects WebSocket messages now tracked in useLiveData state
- Passed through MapLayout → WindowRenderer → RadarWindow
- Objects list updates live as radar data streams in
Inventory:
- Items now render actual game icons via /icons/{hexId}.png
using the portal.dat offset formula (iconRaw + 0x06000000)
- Hover tooltip shows: name, material, AL, damage, workmanship,
tinks, set, imbue (multi-line)
- Equipment grid slots show item icons instead of text names
- Pack item grid shows item icons with proper tooltips
- Fallback icon (06000133.png) on load error
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
863adb0c3c
commit
e5c982d6f5
6 changed files with 185 additions and 167 deletions
|
|
@ -57,7 +57,8 @@ export const MapLayout: React.FC<Props> = ({ data, onViewToggle }) => {
|
|||
showHeatmap={showHeatmap}
|
||||
showPortals={showPortals}
|
||||
/>
|
||||
<WindowRenderer characters={data.characters} chatMessages={data.chatMessages} socket={data.socketRef.current} />
|
||||
<WindowRenderer characters={data.characters} chatMessages={data.chatMessages}
|
||||
nearbyObjects={data.nearbyObjects} socket={data.socketRef.current} />
|
||||
<RareNotification recentRares={data.recentRares} />
|
||||
</div>
|
||||
</WindowManagerProvider>
|
||||
|
|
|
|||
|
|
@ -1,41 +1,91 @@
|
|||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
||||
import { DraggableWindow } from './DraggableWindow';
|
||||
import { apiFetch } from '../../api/client';
|
||||
|
||||
interface Props { id: string; charName: string; zIndex: number; }
|
||||
|
||||
interface Item {
|
||||
item_id?: number; Name: string; ObjectClass?: number; object_class?: number;
|
||||
Icon?: number; Value?: number; Burden?: number; ArmorLevel?: number;
|
||||
MaxDamage?: number; Workmanship?: number; Material?: string; ItemSet?: string;
|
||||
Imbue?: string; EquipSkill?: string; Tinks?: number; ContainerId?: number;
|
||||
container_id?: number; current_wielded_location?: number;
|
||||
item_id?: number; Id?: number; Name?: string; name?: string;
|
||||
ObjectClass?: number; object_class?: number;
|
||||
Icon?: number; icon?: number;
|
||||
Value?: number; value?: number; Burden?: number; burden?: number;
|
||||
ArmorLevel?: number; armor_level?: number;
|
||||
MaxDamage?: number; max_damage?: number;
|
||||
Workmanship?: number; workmanship?: number;
|
||||
Material?: string; material?: string; material_name?: string;
|
||||
ItemSet?: string; item_set?: string;
|
||||
Imbue?: string; imbue?: string;
|
||||
EquipSkill?: string; equip_skill?: string;
|
||||
Tinks?: number; tinks?: number;
|
||||
ContainerId?: number; container_id?: number;
|
||||
current_wielded_location?: number; CurrentWieldedLocation?: number;
|
||||
IntValues?: Record<string, number>;
|
||||
}
|
||||
|
||||
// Equipment slot positions matching v1's EQUIP_SLOTS
|
||||
const EQUIP_SLOTS: Record<number, { name: string; row: number; col: number }> = {
|
||||
32768: { name: 'Neck', row: 1, col: 1 }, 1: { name: 'Head', row: 1, col: 3 },
|
||||
268435456: { name: 'Sigil (Blue)', row: 1, col: 5 }, 536870912: { name: 'Sigil (Yellow)', row: 1, col: 6 },
|
||||
1073741824: { name: 'Sigil (Red)', row: 1, col: 7 },
|
||||
67108864: { name: 'Trinket', row: 2, col: 1 }, 2048: { name: 'Upper Arm', row: 2, col: 2 },
|
||||
512: { name: 'Chest Armor', row: 2, col: 3 }, 134217728: { name: 'Cloak', row: 2, col: 7 },
|
||||
65536: { name: 'Bracelet (L)', row: 3, col: 1 }, 4096: { name: 'Lower Arm', row: 3, col: 2 },
|
||||
1024: { name: 'Abdomen', row: 3, col: 3 }, 8192: { name: 'Upper Leg', row: 3, col: 4 },
|
||||
131072: { name: 'Bracelet (R)', row: 3, col: 5 }, 2: { name: 'Shirt', row: 3, col: 7 },
|
||||
262144: { name: 'Ring (L)', row: 4, col: 1 }, 32: { name: 'Hands', row: 4, col: 2 },
|
||||
16384: { name: 'Lower Leg', row: 4, col: 4 }, 524288: { name: 'Ring (R)', row: 4, col: 5 },
|
||||
4: { name: 'Pants', row: 4, col: 7 },
|
||||
256: { name: 'Feet', row: 5, col: 4 },
|
||||
2097152: { name: 'Shield', row: 6, col: 1 }, 1048576: { name: 'Melee', row: 6, col: 3 },
|
||||
4194304: { name: 'Missile', row: 6, col: 3 }, 16777216: { name: 'Held', row: 6, col: 3 },
|
||||
33554432: { name: 'Two Handed', row: 6, col: 3 }, 8388608: { name: 'Ammo', row: 6, col: 7 },
|
||||
};
|
||||
// Icon helper: convert raw icon ID to hex filename
|
||||
function iconUrl(item: Item): string {
|
||||
const raw = item.icon ?? item.Icon ?? 0;
|
||||
if (raw === 0) return '/icons/06000133.png'; // fallback
|
||||
const hex = (raw + 0x06000000).toString(16).toUpperCase().padStart(8, '0');
|
||||
return `/icons/${hex}.png`;
|
||||
}
|
||||
|
||||
const SLOT_COLOR: Record<number, string> = {};
|
||||
[32768,67108864,65536,131072,262144,524288].forEach(m => SLOT_COLOR[m] = '#4a3060');
|
||||
[1,512,2048,1024,4096,8192,16384,32,256].forEach(m => SLOT_COLOR[m] = '#2a3a5a');
|
||||
[2,4,134217728,268435456,536870912,1073741824].forEach(m => SLOT_COLOR[m] = '#2a4a4a');
|
||||
[2097152,1048576,4194304,16777216,33554432,8388608].forEach(m => SLOT_COLOR[m] = '#1a2a4a');
|
||||
function itemName(item: Item): string { return item.name ?? item.Name ?? 'Unknown'; }
|
||||
function val(v: number | undefined | null, sentinel = -1): number | undefined {
|
||||
return (v !== undefined && v !== null && v !== sentinel) ? v : undefined;
|
||||
}
|
||||
|
||||
// Build tooltip text
|
||||
function itemTooltip(item: Item): string {
|
||||
const parts = [itemName(item)];
|
||||
const mat = item.material ?? item.material_name ?? item.Material;
|
||||
if (mat) parts.push(`Material: ${mat}`);
|
||||
const al = val(item.armor_level ?? item.ArmorLevel);
|
||||
if (al) parts.push(`AL: ${al}`);
|
||||
const dmg = val(item.max_damage ?? item.MaxDamage);
|
||||
if (dmg) parts.push(`Damage: ${dmg}`);
|
||||
const wk = val(item.workmanship ?? item.Workmanship);
|
||||
if (wk) parts.push(`Work: ${wk}`);
|
||||
const tk = val(item.tinks ?? item.Tinks);
|
||||
if (tk) parts.push(`Tinks: ${tk}`);
|
||||
const set = item.item_set ?? item.ItemSet;
|
||||
if (set) parts.push(`Set: ${set}`);
|
||||
const imb = item.imbue ?? item.Imbue;
|
||||
if (imb) parts.push(`Imbue: ${imb}`);
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
// Equipment slot map matching v1
|
||||
const EQUIP_SLOTS: Record<number, { name: string; row: number; col: number }> = {
|
||||
32768:{name:'Neck',row:1,col:1},1:{name:'Head',row:1,col:3},
|
||||
268435456:{name:'Sigil',row:1,col:5},536870912:{name:'Sigil',row:1,col:6},1073741824:{name:'Sigil',row:1,col:7},
|
||||
67108864:{name:'Trinket',row:2,col:1},2048:{name:'U.Arm',row:2,col:2},
|
||||
512:{name:'Chest',row:2,col:3},134217728:{name:'Cloak',row:2,col:7},
|
||||
65536:{name:'Brace L',row:3,col:1},4096:{name:'L.Arm',row:3,col:2},
|
||||
1024:{name:'Abdomen',row:3,col:3},8192:{name:'U.Leg',row:3,col:4},
|
||||
131072:{name:'Brace R',row:3,col:5},2:{name:'Shirt',row:3,col:7},
|
||||
262144:{name:'Ring L',row:4,col:1},32:{name:'Hands',row:4,col:2},
|
||||
16384:{name:'L.Leg',row:4,col:4},524288:{name:'Ring R',row:4,col:5},4:{name:'Pants',row:4,col:7},
|
||||
256:{name:'Feet',row:5,col:4},
|
||||
2097152:{name:'Shield',row:6,col:1},1048576:{name:'Melee',row:6,col:3},
|
||||
4194304:{name:'Missile',row:6,col:3},16777216:{name:'Held',row:6,col:3},
|
||||
33554432:{name:'2H',row:6,col:3},8388608:{name:'Ammo',row:6,col:7},
|
||||
};
|
||||
const SLOT_BG: Record<number, string> = {};
|
||||
[32768,67108864,65536,131072,262144,524288].forEach(m => SLOT_BG[m] = '#3a2050');
|
||||
[1,512,2048,1024,4096,8192,16384,32,256].forEach(m => SLOT_BG[m] = '#1e2e4e');
|
||||
[2,4,134217728,268435456,536870912,1073741824].forEach(m => SLOT_BG[m] = '#1e3e3e');
|
||||
[2097152,1048576,4194304,16777216,33554432,8388608].forEach(m => SLOT_BG[m] = '#142040');
|
||||
|
||||
function ItemIcon({ item, size = 38 }: { item: Item; size?: number }) {
|
||||
return (
|
||||
<div title={itemTooltip(item)} style={{ width: size, height: size, position: 'relative', cursor: 'help' }}>
|
||||
<img src={iconUrl(item)} alt={itemName(item)}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'contain', imageRendering: 'pixelated' }}
|
||||
onError={e => { (e.target as HTMLImageElement).src = '/icons/06000133.png'; }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const InventoryWindow: React.FC<Props> = ({ id, charName, zIndex }) => {
|
||||
const [items, setItems] = useState<Item[]>([]);
|
||||
|
|
@ -50,44 +100,29 @@ export const InventoryWindow: React.FC<Props> = ({ id, charName, zIndex }) => {
|
|||
.finally(() => setLoading(false));
|
||||
}, [charName]);
|
||||
|
||||
// Build unique slot positions
|
||||
const slotPositions = useMemo(() => {
|
||||
const seen = new Set<string>();
|
||||
const slots: Array<{ key: string; row: number; col: number; mask: number; name: string; color: string }> = [];
|
||||
const slots: Array<{ key: string; row: number; col: number; mask: number; name: string; bg: string }> = [];
|
||||
Object.entries(EQUIP_SLOTS).forEach(([maskStr, def]) => {
|
||||
const k = `${def.row}-${def.col}`;
|
||||
if (!seen.has(k)) {
|
||||
seen.add(k);
|
||||
const mask = parseInt(maskStr);
|
||||
slots.push({ key: k, ...def, mask, color: SLOT_COLOR[mask] ?? '#1a2a4a' });
|
||||
}
|
||||
if (!seen.has(k)) { seen.add(k); const mask = parseInt(maskStr); slots.push({ key: k, ...def, mask, bg: SLOT_BG[mask] ?? '#142040' }); }
|
||||
});
|
||||
return slots;
|
||||
}, []);
|
||||
|
||||
// Categorize items
|
||||
const { equippedMap, containers, packItems } = useMemo(() => {
|
||||
const equippedMap = new Map<string, Item>();
|
||||
const containers: Item[] = [];
|
||||
const containerIds = new Set<number>();
|
||||
const packItems = new Map<number, Item[]>();
|
||||
|
||||
items.forEach(item => { const oc = item.ObjectClass ?? item.object_class ?? 0; if (oc === 10) { containers.push(item); containerIds.add(item.item_id ?? item.Id ?? 0); } });
|
||||
items.forEach(item => {
|
||||
const oc = item.ObjectClass ?? item.object_class ?? 0;
|
||||
if (oc === 10) { containers.push(item); containerIds.add(item.item_id ?? 0); }
|
||||
});
|
||||
|
||||
items.forEach(item => {
|
||||
if (containerIds.has(item.item_id ?? 0)) return;
|
||||
const wielded = item.current_wielded_location ?? 0;
|
||||
if (containerIds.has(item.item_id ?? item.Id ?? 0)) return;
|
||||
const wielded = item.current_wielded_location ?? item.CurrentWieldedLocation ?? (item.IntValues?.['10'] ? Number(item.IntValues['10']) : 0);
|
||||
if (wielded > 0) {
|
||||
// Place in first matching slot
|
||||
for (const [maskStr, def] of Object.entries(EQUIP_SLOTS)) {
|
||||
const slotMask = parseInt(maskStr);
|
||||
if ((wielded & slotMask) === slotMask) {
|
||||
const key = `${def.row}-${def.col}`;
|
||||
if (!equippedMap.has(key)) { equippedMap.set(key, item); break; }
|
||||
}
|
||||
if ((wielded & slotMask) === slotMask) { const key = `${def.row}-${def.col}`; if (!equippedMap.has(key)) { equippedMap.set(key, item); break; } }
|
||||
}
|
||||
} else {
|
||||
const cid = item.container_id ?? item.ContainerId ?? 0;
|
||||
|
|
@ -95,98 +130,68 @@ export const InventoryWindow: React.FC<Props> = ({ id, charName, zIndex }) => {
|
|||
packItems.get(cid)!.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return { equippedMap, containers, packItems };
|
||||
}, [items]);
|
||||
|
||||
const activeItems = packItems.get(activePack) ?? packItems.get(0) ?? [];
|
||||
const activeItems = packItems.get(activePack) ?? [...packItems.values()].flat().slice(0, 200);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<DraggableWindow id={id} title={`Inventory: ${charName}`} zIndex={zIndex} width={580} height={700}>
|
||||
<div style={{ padding: 20, color: '#666' }}>Loading inventory...</div>
|
||||
</DraggableWindow>
|
||||
);
|
||||
return <DraggableWindow id={id} title={`Inventory: ${charName}`} zIndex={zIndex} width={580} height={700}>
|
||||
<div style={{ padding: 20, color: '#666' }}>Loading inventory...</div>
|
||||
</DraggableWindow>;
|
||||
}
|
||||
|
||||
return (
|
||||
<DraggableWindow id={id} title={`Inventory: ${charName}`} zIndex={zIndex} width={580} height={700}>
|
||||
<div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
|
||||
{/* Left column: equipment grid + pack items */}
|
||||
{/* Left: equipment + pack items */}
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
{/* Equipment grid */}
|
||||
<div style={{ position: 'relative', height: 270, minHeight: 270, background: '#111', borderBottom: '1px solid #333' }}>
|
||||
<div style={{ position: 'relative', height: 270, minHeight: 270, background: '#0d0d0d', borderBottom: '1px solid #333' }}>
|
||||
{slotPositions.map(slot => {
|
||||
const item = equippedMap.get(slot.key);
|
||||
return (
|
||||
<div key={slot.key} title={item ? `${item.Name}${item.Material ? ` (${item.Material})` : ''}` : slot.name}
|
||||
<div key={slot.key} title={item ? itemTooltip(item) : slot.name}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: (slot.col - 1) * 44 + 4,
|
||||
top: (slot.row - 1) * 44 + 4,
|
||||
width: 40, height: 40,
|
||||
background: item ? slot.color : `${slot.color}44`,
|
||||
border: `1px solid ${item ? '#666' : '#333'}`,
|
||||
borderRadius: 3,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: item ? '0.58rem' : '0.5rem',
|
||||
color: item ? '#ddd' : '#555',
|
||||
overflow: 'hidden', textOverflow: 'ellipsis',
|
||||
cursor: item ? 'help' : 'default',
|
||||
padding: 1, textAlign: 'center', lineHeight: 1.1,
|
||||
position: 'absolute', left: (slot.col - 1) * 44 + 4, top: (slot.row - 1) * 44 + 4,
|
||||
width: 40, height: 40, background: item ? slot.bg : `${slot.bg}55`,
|
||||
border: `1px solid ${item ? '#555' : '#2a2a2a'}`, borderRadius: 3,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden',
|
||||
}}>
|
||||
{item ? item.Name?.split(' ').slice(0, 2).join(' ') : slot.name}
|
||||
{item ? <ItemIcon item={item} size={36} /> :
|
||||
<span style={{ fontSize: '0.45rem', color: '#444', textAlign: 'center', lineHeight: 1 }}>{slot.name}</span>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Pack contents header */}
|
||||
{/* Pack contents */}
|
||||
<div style={{ padding: '4px 8px', fontWeight: 600, fontSize: '0.7rem', color: '#888', borderBottom: '1px solid #333' }}>
|
||||
Contents ({activeItems.length} items)
|
||||
Contents ({activeItems.length})
|
||||
</div>
|
||||
|
||||
{/* Item grid */}
|
||||
<div style={{ flex: 1, overflowY: 'auto', display: 'flex', flexWrap: 'wrap', gap: 2, padding: 4, alignContent: 'flex-start' }}>
|
||||
{activeItems.map((item, i) => (
|
||||
<div key={item.item_id ?? i}
|
||||
title={`${item.Name}${item.Material ? ` [${item.Material}]` : ''}${item.ArmorLevel && item.ArmorLevel > 0 ? ` AL:${item.ArmorLevel}` : ''}${item.MaxDamage && item.MaxDamage > 0 ? ` Dmg:${item.MaxDamage}` : ''}${item.Workmanship ? ` WK:${item.Workmanship}` : ''}`}
|
||||
style={{
|
||||
width: 40, height: 40,
|
||||
background: '#252525', border: '1px solid #333', borderRadius: 2,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: '0.48rem', color: '#aaa', padding: 1,
|
||||
textAlign: 'center', lineHeight: 1.1, cursor: 'help', overflow: 'hidden',
|
||||
}}>
|
||||
{item.Name?.split(' ').slice(0, 2).join('\n')}
|
||||
<div key={item.item_id ?? item.Id ?? i} title={itemTooltip(item)}
|
||||
style={{ width: 40, height: 40, background: '#1a1a1a', border: '1px solid #2a2a2a', borderRadius: 2,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'help', overflow: 'hidden' }}>
|
||||
<ItemIcon item={item} size={36} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right sidebar: packs + burden */}
|
||||
{/* Right: packs */}
|
||||
<div style={{ width: 100, borderLeft: '1px solid #333', display: 'flex', flexDirection: 'column', fontSize: '0.65rem' }}>
|
||||
<div style={{ padding: '4px 6px', fontWeight: 600, color: '#888', borderBottom: '1px solid #333' }}>Packs</div>
|
||||
|
||||
{/* Main backpack */}
|
||||
<div style={{ padding: '3px 6px', cursor: 'pointer', background: activePack === 0 ? '#2a3a4a' : '',
|
||||
borderBottom: '1px solid #222', color: '#ccc' }}
|
||||
onClick={() => setActivePack(0)}>
|
||||
borderBottom: '1px solid #222', color: '#ccc' }} onClick={() => setActivePack(0)}>
|
||||
🎒 Backpack
|
||||
<span style={{ color: '#666', marginLeft: 4 }}>({(packItems.get(0) ?? []).length})</span>
|
||||
</div>
|
||||
|
||||
{/* Sub-packs */}
|
||||
{containers.map(c => {
|
||||
const cid = c.item_id ?? 0;
|
||||
const count = (packItems.get(cid) ?? []).length;
|
||||
const cid = c.item_id ?? c.Id ?? 0;
|
||||
return (
|
||||
<div key={cid} style={{ padding: '3px 6px', cursor: 'pointer',
|
||||
background: activePack === cid ? '#2a3a4a' : '',
|
||||
borderBottom: '1px solid #222', color: '#aaa' }}
|
||||
background: activePack === cid ? '#2a3a4a' : '', borderBottom: '1px solid #222', color: '#aaa' }}
|
||||
onClick={() => setActivePack(cid)}>
|
||||
📦 {c.Name?.split(' ')[0] ?? 'Pack'}
|
||||
<span style={{ color: '#666', marginLeft: 4 }}>({count})</span>
|
||||
📦 {(c.name ?? c.Name ?? 'Pack').split(' ')[0]}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -13,10 +13,11 @@ import type { CharacterState } from '../../types';
|
|||
interface Props {
|
||||
characters: Map<string, CharacterState>;
|
||||
chatMessages: Map<string, Array<{ text: string; color?: number; timestamp: string }>>;
|
||||
nearbyObjects: Map<string, any>;
|
||||
socket: WebSocket | null;
|
||||
}
|
||||
|
||||
export const WindowRenderer: React.FC<Props> = ({ characters, chatMessages, socket }) => {
|
||||
export const WindowRenderer: React.FC<Props> = ({ characters, chatMessages, nearbyObjects, socket }) => {
|
||||
const { windows } = useWindowManager();
|
||||
|
||||
return (
|
||||
|
|
@ -35,9 +36,11 @@ export const WindowRenderer: React.FC<Props> = ({ characters, chatMessages, sock
|
|||
return <CharacterWindow key={w.id} id={w.id} charName={charName} zIndex={w.zIndex} />;
|
||||
case 'inv':
|
||||
return <InventoryWindow key={w.id} id={w.id} charName={charName} zIndex={w.zIndex} />;
|
||||
case 'radar':
|
||||
case 'radar': {
|
||||
const radarData = nearbyObjects.get(charName);
|
||||
return <RadarWindow key={w.id} id={w.id} charName={charName} zIndex={w.zIndex}
|
||||
socket={socket} nearbyObjects={[]} />;
|
||||
socket={socket} nearbyObjects={radarData?.objects ?? []} />;
|
||||
}
|
||||
case 'combat':
|
||||
return <CombatStatsWindow key={w.id} id={w.id} charName={charName} zIndex={w.zIndex} />;
|
||||
case 'issues':
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export interface DashboardState {
|
|||
totalKills: number;
|
||||
recentRares: RareMessage[];
|
||||
chatMessages: Map<string, Array<{ text: string; color?: number; timestamp: string }>>;
|
||||
nearbyObjects: Map<string, any>;
|
||||
socketRef: React.RefObject<WebSocket | null>;
|
||||
}
|
||||
|
||||
|
|
@ -23,6 +24,7 @@ export function useLiveData(): DashboardState {
|
|||
const [totalKills, setTotalKills] = useState(0);
|
||||
const [recentRares, setRecentRares] = useState<RareMessage[]>([]);
|
||||
const [chatMessages, setChatMessages] = useState<Map<string, Array<{ text: string; color?: number; timestamp: string }>>>(new Map());
|
||||
const [nearbyObjects, setNearbyObjects] = useState<Map<string, any>>(new Map());
|
||||
const charsRef = useRef(characters);
|
||||
charsRef.current = characters;
|
||||
|
||||
|
|
@ -52,6 +54,13 @@ export function useLiveData(): DashboardState {
|
|||
} else if (msg.type === 'rare') {
|
||||
const r = msg as RareMessage;
|
||||
setRecentRares(prev => [r, ...prev].slice(0, 50));
|
||||
} else if (msg.type === 'nearby_objects') {
|
||||
const no = msg as unknown as { character_name: string; objects: any[] };
|
||||
setNearbyObjects(prev => {
|
||||
const next = new Map(prev);
|
||||
next.set(no.character_name, no);
|
||||
return next;
|
||||
});
|
||||
} else if (msg.type === 'chat') {
|
||||
const m = msg as unknown as { character_name: string; text: string; color?: number; timestamp: string };
|
||||
setChatMessages(prev => {
|
||||
|
|
@ -141,5 +150,5 @@ export function useLiveData(): DashboardState {
|
|||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
return { characters, serverHealth, totalRares, totalKills, recentRares, chatMessages, socketRef };
|
||||
return { characters, serverHealth, totalRares, totalKills, recentRares, chatMessages, nearbyObjects, socketRef };
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -5,7 +5,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Mosswart Overlord v2</title>
|
||||
<link rel="icon" type="image/png" href="/icons/7735.png" />
|
||||
<script type="module" crossorigin src="/v2/assets/index-BXNfpUzm.js"></script>
|
||||
<script type="module" crossorigin src="/v2/assets/index-WeObtuRG.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/v2/assets/index-DrsM2PEe.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue