feat(v2): 13 improvements — functional, visual, UX, backend
Functional:
1. Chat: "▼ New messages below" indicator when scrolled up, click to jump
2. Combat stats: "Clear Session" button (red, with confirm dialog)
3. Inventory: live updates via inventory_delta WS (re-fetches on change)
4. Inventory: real mana time from equipment_cantrip_state WS (live
countdown with state dot: green=active, red=inactive, yellow=unknown)
Visual:
5. Thin separator line between tool links and sort buttons
6. Selected player row highlighted with darker background (#2a3344)
7. Scroll-to-top button (▲) appears when scrolled past 200px in player list
UX:
8. Double-click player dot on map opens their chat window
9. Right-click player dot shows context menu (Chat/Stats/Inv/Char/Combat/Radar)
10. Ctrl+D keyboard shortcut toggles between map and dashboard views
11. Sound notification on rare drops (880Hz sine beep via Web Audio API)
Backend:
12. Deep-merge lifetime offense/defense per element — accumulates
total_attacks, failed_attacks, crits, damage per AttackType×Element
instead of overwriting with latest session data
13. Startup cleanup: deletes stale combat_stats records from before
the lifetime fix (pre-2026-04-14T09:00Z)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0b64c6ccff
commit
0112c59514
41 changed files with 404 additions and 112 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useRef, useState, useCallback } from 'react';
|
||||
import { PlayerRow } from './PlayerRow';
|
||||
import type { TelemetrySnapshot, VitalsMessage } from '../../types';
|
||||
|
||||
|
|
@ -7,18 +7,39 @@ interface Props {
|
|||
vitals: Map<string, VitalsMessage>;
|
||||
getColor: (name: string) => string;
|
||||
onSelect: (name: string) => void;
|
||||
selectedPlayer?: string | null;
|
||||
}
|
||||
|
||||
export const PlayerList: React.FC<Props> = ({ players, vitals, getColor, onSelect }) => (
|
||||
<ul className="ml-player-list">
|
||||
{players.map(p => (
|
||||
<PlayerRow
|
||||
key={p.character_name}
|
||||
player={p}
|
||||
vitals={vitals.get(p.character_name) ?? null}
|
||||
color={getColor(p.character_name)}
|
||||
onSelect={() => onSelect(p.character_name)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
export const PlayerList: React.FC<Props> = ({ players, vitals, getColor, onSelect, selectedPlayer }) => {
|
||||
const listRef = useRef<HTMLUListElement>(null);
|
||||
const [showTop, setShowTop] = useState(false);
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
if (listRef.current) setShowTop(listRef.current.scrollTop > 200);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', flex: 1, minHeight: 0 }}>
|
||||
<ul className="ml-player-list" ref={listRef} onScroll={handleScroll}>
|
||||
{players.map(p => (
|
||||
<PlayerRow
|
||||
key={p.character_name}
|
||||
player={p}
|
||||
vitals={vitals.get(p.character_name) ?? null}
|
||||
color={getColor(p.character_name)}
|
||||
onSelect={() => onSelect(p.character_name)}
|
||||
isSelected={selectedPlayer === p.character_name}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
{showTop && (
|
||||
<button onClick={() => { listRef.current?.scrollTo({ top: 0, behavior: 'smooth' }); }}
|
||||
style={{ position: 'absolute', bottom: 8, right: 8, width: 28, height: 28, borderRadius: '50%',
|
||||
background: 'rgba(68,136,255,0.2)', border: '1px solid rgba(68,136,255,0.4)', color: '#6af',
|
||||
cursor: 'pointer', fontSize: '0.8rem', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
▲
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ interface Props {
|
|||
vitals: VitalsMessage | null;
|
||||
color: string;
|
||||
onSelect: () => void;
|
||||
isSelected?: boolean;
|
||||
}
|
||||
|
||||
export const PlayerRow: React.FC<Props> = React.memo(({ player: p, vitals: v, color, onSelect }) => {
|
||||
export const PlayerRow: React.FC<Props> = React.memo(({ player: p, vitals: v, color, onSelect, isSelected }) => {
|
||||
const { openWindow } = useWindowManager();
|
||||
const vtState = (p.vt_state || 'idle').toLowerCase();
|
||||
const isActive = vtState === 'combat' || vtState === 'hunt';
|
||||
|
|
@ -21,7 +22,7 @@ export const PlayerRow: React.FC<Props> = React.memo(({ player: p, vitals: v, co
|
|||
const name = p.character_name;
|
||||
|
||||
return (
|
||||
<li className="ml-player-row" style={{ borderLeftColor: color }}>
|
||||
<li className={`ml-player-row ${isSelected ? 'ml-player-selected' : ''}`} style={{ borderLeftColor: color }}>
|
||||
<div className="ml-pr-header" onClick={onSelect}>
|
||||
<span className="ml-pr-name">{name}</span>
|
||||
<span className="ml-pr-coords">{formatCoord(p.ns, p.ew)}</span>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue