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:
parent
183d662bb9
commit
de7b547349
20 changed files with 1040 additions and 193 deletions
58
frontend/src/components/map/HeatmapCanvas.tsx
Normal file
58
frontend/src/components/map/HeatmapCanvas.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { worldToPx } from '../../utils/coordinates';
|
||||
import { apiFetch } from '../../api/client';
|
||||
|
||||
interface HeatmapPoint {
|
||||
ew: number;
|
||||
ns: number;
|
||||
intensity: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
imgW: number;
|
||||
imgH: number;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export const HeatmapCanvas: React.FC<Props> = ({ imgW, imgH, enabled }) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [data, setData] = useState<HeatmapPoint[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
const fetch = async () => {
|
||||
try {
|
||||
const resp = await apiFetch<{ spawn_points: HeatmapPoint[] }>('/spawns/heatmap?hours=24&limit=50000');
|
||||
setData(resp.spawn_points ?? []);
|
||||
} catch { /* ignore */ }
|
||||
};
|
||||
fetch();
|
||||
}, [enabled]);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas || !enabled || data.length === 0 || imgW === 0) return;
|
||||
|
||||
canvas.width = imgW;
|
||||
canvas.height = imgH;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
ctx.clearRect(0, 0, imgW, imgH);
|
||||
|
||||
for (const point of data) {
|
||||
const { x, y } = worldToPx(point.ew, point.ns, imgW, imgH);
|
||||
const radius = Math.max(5, Math.min(12, 5 + Math.sqrt(point.intensity * 0.5)));
|
||||
const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius);
|
||||
gradient.addColorStop(0, `rgba(255, 0, 0, ${Math.min(0.9, point.intensity / 40)})`);
|
||||
gradient.addColorStop(0.6, `rgba(255, 100, 0, ${Math.min(0.4, point.intensity / 120)})`);
|
||||
gradient.addColorStop(1, 'rgba(255, 150, 0, 0)');
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(x - radius, y - radius, radius * 2, radius * 2);
|
||||
}
|
||||
}, [data, imgW, imgH, enabled]);
|
||||
|
||||
if (!enabled) return null;
|
||||
|
||||
return <canvas ref={canvasRef} className="ml-heatmap-canvas" />;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue