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
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import { formatCoord } from '../../utils/coordinates';
|
||||
import { useWindowManager } from '../../contexts/WindowManagerContext';
|
||||
import type { TelemetrySnapshot, VitalsMessage } from '../../types';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -10,34 +11,28 @@ interface Props {
|
|||
}
|
||||
|
||||
export const PlayerRow: React.FC<Props> = React.memo(({ player: p, vitals: v, color, onSelect }) => {
|
||||
const { openWindow } = useWindowManager();
|
||||
const vtState = (p.vt_state || 'idle').toLowerCase();
|
||||
const isActive = vtState === 'combat' || vtState === 'hunt';
|
||||
const kpr = (p.total_rares ?? 0) > 0
|
||||
? Math.round((p.total_kills ?? 0) / (p.total_rares ?? 1)).toLocaleString()
|
||||
: null;
|
||||
|
||||
const name = p.character_name;
|
||||
|
||||
return (
|
||||
<li className="ml-player-row" style={{ borderLeftColor: color }}>
|
||||
{/* Row 1: Name + coords */}
|
||||
<div className="ml-pr-header" onClick={onSelect}>
|
||||
<span className="ml-pr-name">{p.character_name}</span>
|
||||
<span className="ml-pr-name">{name}</span>
|
||||
<span className="ml-pr-coords">{formatCoord(p.ns, p.ew)}</span>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Vital bars */}
|
||||
<div className="ml-pr-vitals">
|
||||
<div className="ml-vital-bar hp">
|
||||
<div className="ml-vital-fill" style={{ width: `${v?.health_percentage ?? 0}%` }} />
|
||||
</div>
|
||||
<div className="ml-vital-bar sta">
|
||||
<div className="ml-vital-fill" style={{ width: `${v?.stamina_percentage ?? 0}%` }} />
|
||||
</div>
|
||||
<div className="ml-vital-bar mana">
|
||||
<div className="ml-vital-fill" style={{ width: `${v?.mana_percentage ?? 0}%` }} />
|
||||
</div>
|
||||
<div className="ml-vital-bar hp"><div className="ml-vital-fill" style={{ width: `${v?.health_percentage ?? 0}%` }} /></div>
|
||||
<div className="ml-vital-bar sta"><div className="ml-vital-fill" style={{ width: `${v?.stamina_percentage ?? 0}%` }} /></div>
|
||||
<div className="ml-vital-bar mana"><div className="ml-vital-fill" style={{ width: `${v?.mana_percentage ?? 0}%` }} /></div>
|
||||
</div>
|
||||
|
||||
{/* Row 3-5: Stats grid — 3 columns × 3 rows for alignment */}
|
||||
<div className="ml-pr-grid">
|
||||
<span className="ml-gs" title="Session kills">⚔️ {p.kills?.toLocaleString() ?? 0}</span>
|
||||
<span className="ml-gs" title="Total kills">🏆 {(p.total_kills ?? 0).toLocaleString()}</span>
|
||||
|
|
@ -52,13 +47,12 @@ export const PlayerRow: React.FC<Props> = React.memo(({ player: p, vitals: v, co
|
|||
<span className="ml-gs" title="Prismatic tapers"><img src="/prismatic-taper-icon.png" className="ml-taper-icon" alt="" />{p.prismatic_taper_count ?? '0'}</span>
|
||||
</div>
|
||||
|
||||
{/* Row 6: Action buttons */}
|
||||
<div className="ml-pr-buttons">
|
||||
<button className="ml-btn accent" title="Chat">Chat</button>
|
||||
<button className="ml-btn accent" title="Stats">Stats</button>
|
||||
<button className="ml-btn accent" title="Inventory">Inv</button>
|
||||
<button className="ml-btn" title="Character">Char</button>
|
||||
<button className="ml-btn" title="Radar">Radar</button>
|
||||
<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(`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>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue