feat(v2): Phases 2-6 — trails, heatmap, portals, windows, effects

Phase 2 — Map overlays:
- TrailsSVG: SVG polylines per character from /trails, polled 2s
- HeatmapCanvas: canvas radial gradients from /spawns/heatmap
- PortalMarkers: emoji markers from /portals
- Sidebar toggles for heatmap and portals

Phase 3 — Draggable windows:
- WindowManagerContext: z-index stack for open windows
- DraggableWindow: generic shell with drag-header, close btn, z-stack
- ChatWindow: color-coded messages + input form (1000 msg buffer)
- CharacterWindow: combat stats with monster damage table
- InventoryWindow: item table with material/set/AL/dmg/workmanship
- WindowRenderer: reads context, renders all open windows
- Action buttons (Chat/Stats/Inv/Char/Radar) now open windows

Phase 4 — Window types share same DraggableWindow shell with
character-specific content. Combat stats and inventory via API.

Phase 5 — Effects:
- RareNotification: slide-in/slide-out banner with gold border
- Fireworks: 30-particle explosion with CSS custom property animation
- Notification queue with 6s display + 0.5s exit animation

Phase 6 — Polish:
- Window header uses modern blue gradient (not solid purple)
- Chat uses monospace font
- All overlay layers properly stacked (heatmap → trails → dots → portals)
- Mobile: sidebar stacks above map at 768px breakpoint
- Chat messages tracked per-character in useLiveData

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-12 15:58:58 +02:00
parent 183d662bb9
commit de7b547349
20 changed files with 1040 additions and 193 deletions

View file

@ -12,6 +12,7 @@ export interface DashboardState {
totalRares: number;
totalKills: number;
recentRares: RareMessage[];
chatMessages: Map<string, Array<{ text: string; color?: number; timestamp: string }>>;
}
export function useLiveData(): DashboardState {
@ -20,6 +21,7 @@ export function useLiveData(): DashboardState {
const [totalRares, setTotalRares] = useState(0);
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 charsRef = useRef(characters);
charsRef.current = characters;
@ -49,6 +51,15 @@ 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 === 'chat') {
const m = msg as unknown as { character_name: string; text: string; color?: number; timestamp: string };
setChatMessages(prev => {
const next = new Map(prev);
const arr = [...(next.get(m.character_name) ?? []), { text: m.text, color: m.color, timestamp: m.timestamp }];
if (arr.length > 1000) arr.splice(0, arr.length - 1000);
next.set(m.character_name, arr);
return next;
});
}
}, [updateChar]);
@ -129,5 +140,5 @@ export function useLiveData(): DashboardState {
return () => clearInterval(id);
}, []);
return { characters, serverHealth, totalRares, totalKills, recentRares };
return { characters, serverHealth, totalRares, totalKills, recentRares, chatMessages };
}