1. React.memo on WindowRenderer — prevents re-renders when parent
state changes but no windows are affected
2. Coordinate display via direct DOM ref — no React state updates
on mouse move (was triggering re-renders on every pixel)
3. useDeferredValue for sidebar vitals + player list — React
prioritizes map interactions over stat text updates
4. Chat messages in ref — stores in useRef instead of useState,
only bumps a version counter for re-render. Eliminates a
new Map() allocation on every chat message.
5. Lazy-load 8 window components — InventoryWindow, CharacterWindow,
RadarWindow, CombatStatsWindow, IssuesWindow, VitalSharingWindow,
StatsWindow, CombatPickerWindow all loaded on first open.
Main bundle dropped from 278KB to 211KB (24% reduction).
6. Preload critical assets — dereth.png, backpack icon, dungeon_tiles.json
via <link rel="preload"> in index.html for instant map render.
7. Bundle splitting — React runtime extracted to separate 12KB chunk
(cached independently). Window components split into 8 chunks.
Total: 13 chunks vs previous 2.
8. Service worker — caches map images, icon sprites, and dungeon tiles.
Icon images cached on first fetch. Repeat page loads serve from
cache instantly. Auto-cleans old cache versions.
Net result:
- Initial load: 211KB main + 17KB CSS (was 278KB + 17KB)
- React cached separately: 12KB
- Windows load on demand: 1-15KB each
- Dashboard with Recharts: 425KB (unchanged, still lazy)
- Map images/icons: cached by service worker after first load
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All features functional: map view, sidebar, player dots/trails/heatmap/portals,
draggable windows (chat/stats/inventory/character/radar/combat/issues/vitals),
session+lifetime combat stats, 60-color palette, rare notifications, dungeon
radar, version display. Performance: code-split Recharts, direct DOM pan/zoom,
deferred player list, memoized derived data.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The combat_stats DB column stores stats_data as JSON, but SQLAlchemy
returns it as a string (not a parsed dict). This caused:
- _combat_lifetime_cache loaded a string, merge failed silently
- API endpoints returned string instead of object for lifetime
- Frontend saw lifetime as a string, couldn't read .monsters
Fix: parse JSON string with json.loads() wherever stats_data is read
from DB — in the lifetime cache loader, single-character endpoint,
and all-characters endpoint.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Plugin now sends only session data (lifetime=null). Backend computes
the delta between consecutive session snapshots (new - previous) and
merges it into a persisted lifetime record in the combat_stats table.
- _combat_last_session: tracks last-seen session per char:session_id
- _combat_lifetime_cache: in-memory lifetime per character
- _combat_session_delta(): computes diff between two cumulative snapshots
- _combat_merge_into_lifetime(): adds delta totals into lifetime record
- First snapshot for a new session = entire session is the delta
- Lifetime loaded from DB on first message, cached in memory after
- Broadcast enriched with computed lifetime so frontend gets both
This means session resets on login but lifetime persists in DB across
all sessions. The two will now show different data.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Dashboard toggle moved from sidebar header to tool links area
alongside Suitbuilder, Inv Search, Debug, Quests
- Combat sidebar button now opens a character picker window
(combatpicker prefix) that lists all online characters — click
one to open their full combat stats window
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The zoom-to-player effect was re-triggering on every telemetry
update (every 2s) because selectedPlayer stayed set and players
array kept changing. Now tracks lastZoomedRef — zoom only fires
once per selection. Map is immediately free to pan/zoom after.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
apiFetch adds /api prefix, so /api-version became /api/api-version
which was wrong. Use raw fetch with correct path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Player click → zoom: clicking a player in sidebar or on map dot
zooms to their position at 3x zoom, centered on screen. Click
again to deselect. Uses direct DOM transform (no React state).
2. Selected dot blink: selected player dot gets 10px size + blink
animation (0.6s step-end infinite) matching v1's .dot.highlight.
3. Version display: fetches /api-version on mount, shows "vX.Y.Z"
in small text positioned just right of sidebar (fixed, top: 6px).
4. Missing sidebar buttons: added Combat Stats (⚔️) alongside
existing Issues (📋) and Vital Sharing (🤝) in SidebarWindowButtons.
5. Rare notification: added 🎆 emojis to "LEGENDARY RARE!" title
matching v1's notification text.
6. Dungeon map in radar — verbatim port from v1 lines 3596-3930:
- loadDungeonTiles(): fetches dungeon_tiles.json, processes each
tile image (color remap: UB source colors → display colors,
white → transparent, black → semi-transparent)
- cellRotation(): maps rotation values to radians (v1's exact logic)
- Dungeon rendering: sorts z_levels (current floor on top at 85%
opacity, others at 12%), draws each cell with per-cell rotation,
uses processed tile canvases or colored rectangle fallback
- Requests dungeon map via WebSocket when radar detects dungeon
- Caches dungeon maps on window.__dungeonMapCache
- Overworld map: fixed srcSize calculation to use range * pixPerCoord
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Map pan/zoom via direct DOM mutation (bypass React state)
- txRef stores {scale, offX, offY}, applyTransform() writes
directly to groupRef.style.transform
- Zero React re-renders during pan/zoom — smooth 60fps
- Removed MapTransformContext dependency (dead code now)
2. Code-split Recharts via React.lazy()
- DashboardView (with all Recharts components) is a separate chunk
- Main bundle: 274KB (was 694KB — 60% reduction)
- Dashboard chunk: 425KB (loaded only on demand)
- Map view loads instantly without Recharts overhead
3. useDeferredValue for player list
- Kill counters, KPH, rares in sidebar use deferred rendering
- React prioritizes map interactions over stat text updates
- Reduces unnecessary re-renders during WS message bursts
4. useMemo for derived data in MapLayout
- players array and vitalsMap memoized on characters ref
- Prevents child component re-renders when Map identity changes
but content is the same
5. Removed MapTransformProvider wrapper (no longer needed)
Total impact: ~60% smaller initial load, ~10x fewer re-renders
during active WebSocket streaming, zero-latency pan/zoom.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Property ID maps were wrong (made-up IDs 360-390). Now uses the
exact same IDs as v1 script.js lines 1843-1876:
- TS_AUGMENTATIONS: IDs 218-328 (30 augmentations)
- TS_AURAS: IDs 333-365 (11 luminance auras)
- TS_RATINGS: IDs 370-379 (8 ratings)
- TS_SOCIETY: IDs 287-289 (3 societies)
- TS_MASTERIES: IDs 354-362 with TS_MASTERY_NAMES lookup
- TS_GENERAL: IDs 181-390 (chess, fishing, total augs, aetheria, enlightenment)
- societyRank() function matching v1's _tsSocietyRank()
Other tab now shows General + Masteries + Society sections (was
only showing allegiance). Each section has its own header matching
v1's ts-section-title styling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The inventory service doesn't return items_capacity directly — it's
in enhanced_properties.ItemSlots_Decal. Updated normalizer to read
from there. Also defaults to 24 (standard AC pack size) with ||
instead of ?? to catch 0/undefined/null. Removed debug console.logs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instead of relying on the packItems Map (which may have key matching
issues), count pack children directly by filtering the normalized
items array for items whose container_id matches the pack's item_id.
Also removed debug console.log spam from WindowRenderer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Increased bar width to 7px with #222 background and #666 border for
better contrast. Added tooltip showing "X% full" on hover. Minimum
2px fill height when non-empty so even nearly-empty packs show a sliver.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The pack fill bars were always empty because items weren't mapping
to container key 0 (main backpack). The bodyContainerId detection
failed when the inventory service doesn't include container_id on
wielded items. Now falls back to using the largest non-container
item group as the main backpack if key 0 is empty.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Pack capacity bars: gave the fill bar div explicit height (30px)
instead of relying on alignItems:stretch which produced 0 height.
Bar now visibly fills green/orange/red beside each pack icon.
2. Mana panel: added 20px item icons back to each mana row, between
the status dot and item name. Uses the same ItemIcon component
with 3-layer compositing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaced the cramped 3-column CSS Grid mana layout with simple
flexbox rows matching the player sidebar style:
- Status dot (green/red) + item name + mana current/max + time remaining
- Font sizes use rem units (0.65-0.72rem) matching sidebar buttons/stats
- tabular-nums for aligned numbers
- Time column has min-width so it doesn't get clipped
- No more horizontal scrolling or cut-off text
- Empty state message when no mana items equipped
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Item normalization: normalizeItem() handles ALL formats:
- Inventory service (snake_case): current_wielded_location, object_class
- Plugin raw (PascalCase): CurrentWieldedLocation, ObjectClass
- Plugin IntValues: IntValues['10'] for wielded, ['5'] for burden
- Sentinel filtering: -1 values properly excluded
2. Equipment slots: armor (object_class=2) fills ALL matching slots.
Non-armor uses exact mask match first, then first bit overlap.
Body container ID detected to separate worn from pack items.
3. Slot colors: per-slot-type backgrounds matching v1:
purple (#3a2555) for jewelry, blue (#1e2e55) for armor,
teal (#1e3e3e) for clothing, dark blue (#142040) for weapons
4. Burden: fetches /character-stats/{name} for burden_units and
encumbrance_capacity. Shows percentage when available, raw burden
otherwise. Bar fills 0-200% mapped to 0-100% height with
green/orange/red thresholds.
5. Mana panel: shows equipped items with current/max mana + estimated
time remaining. State dot green/red. Sorted by mana ascending.
6. Fonts: switched to system font stack (-apple-system etc.) instead
of Palatino for crisp rendering.
7. Tooltip: proper system font, larger text (13px), structured sections
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Equipment slots: armor (object_class=2) now renders in ALL matching
slots via bitmask, not just the first. E.g. a chest piece covering
upper arm + chest + abdomen appears in all 3 slots. Non-armor items
still use first-match. Matches v1's exact logic.
2. Pack fill bars: changed from horizontal-below to vertical-right of
each pack icon. 4px wide bar with fill from bottom, color-coded:
green <70%, orange 70-90%, red >90%.
3. Burden: removed garbled percentage (was dividing by 10). Now shows
"Burden" label with total burden in tooltip. Bar shows 50% as
placeholder until character_stats provides encumbrance_capacity.
4. PackIcon component: reusable for main backpack + sub-packs, shows
game icon + vertical fill bar + green active glow + gold ▶ arrow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rebuilt inventory window to match v1 pixel-for-pixel:
Left column (316px):
- Equipment grid: 6×7 slots at 44px spacing, beveled 3D borders,
cyan glow (#00ffff) when equipped, faded ghost icon when empty
- Item grid: 6-column CSS Grid with purple gradient cells,
minimum 24 empty cells to fill grid
Center sidebar (38px):
- Burden bar: 14×40px vertical bar, green/orange/red thresholds,
percentage label, tooltip with burden units
- Pack icons: 32×32px with actual game icon images (not emoji)
- Active pack: green border + glow + gold ▶ arrow indicator
- Fill indicator: 4px green bar below each pack showing capacity %
- Main backpack (icon 0600127E) + sub-packs with actual container icons
Right panel (flex):
- Mana panel: header + equipped items with mana tracking
- Per-item: 16px icon, name, mana state dot (green/red),
current/max mana values in v1's grid layout
Hover tooltip:
- Follows mouse cursor (fixed position)
- Shows: name (gold), value, burden, material (green), armor level,
max damage, damage range/type, attack/defense bonuses as %,
skill requirements (orange), imbue, set, tinks, workmanship,
ratings, spellcraft, mana, spell list (blue)
- Black semi-transparent background matching v1's inventory-tooltip
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Character Window — now matches v1 exactly:
- Navy blue background (#000022) with gold/bronze borders (#af7a30)
- Two side-by-side 320px tab containers
- Left tabs: Attributes (vital bars with gold borders + attribute
table with green/blue cell backgrounds + vitals base + skill
credits) | Skills (specialized=purple gradient, trained=teal
gradient, grouped and sorted) | Titles
- Right tabs: Augmentations (with auras section) | Ratings | Other
(allegiance with followers)
- Active tab: green tint background with gold top/side borders
- Header: large name + level (gold, right-floated) + race/gender
- XP grid: total, unassigned, luminance earned/total, deaths
- Live vital bars from WebSocket vitals data
- Augmentation/aura/rating property ID maps from v1
Radar — passes full radarData message (not just objects array)
so canvas can render map background + entity positions properly
WindowRenderer — passes live vitals to CharacterWindow
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Radar — now pixel-accurate reproduction of v1:
- 300×300 canvas with dark circular background
- Semi-transparent dereth.png map overlay (heading-rotated)
- 4 range rings + crosshair lines
- Compass labels (N=red, E/S/W=gray) rotating with heading
- Facing direction indicator line
- Entity dots color-coded by type (Monster=red, Player=blue,
NPC=green, Portal=purple, Corpse=orange, Container=yellow)
- Player dot: gold center with white border
- Heading-up rotation for all entity positions
- Click to select entity (white selection ring)
- Scroll to zoom (0.02-5.0 AC units range)
- Entity list with color dot, name, type, distance, compass direction
- Selected entity highlighted with blue left border
Inventory — v1-style icon composites + slot styling:
- 3-layer icon composite: underlay → base → overlay images
using portal.dat offset formula + icon_overlay_id/IntValues
- Equipment slots: 3D beveled border + cyan glow when equipped
(matching v1's outset border + #00ffff shadow)
- Pack item cells: purple gradient background (v1's #3d007a)
- Proper 36×36px icon rendering with pixelated scaling
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Primary buttons: soft blue translucent background with blue text,
glows slightly on hover. Secondary buttons: dark subtle with gray
text. Cleaner, more modern feel vs the old solid #88f blocks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Stats now use a 3-column CSS Grid so values align across rows
- Fixed icons: ☠️ for deaths (was 💀), prismatic-taper-icon.png
for tapers (was wrong emoji 🔮), 🕐 for time (was 🕑)
- Added action buttons row (Chat, Stats, Inv, Char, Radar) matching
v1's button bar — accent-colored for primary actions
- Buttons are present but not wired to windows yet (Phase 3)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Stats were bare numbers with no visual hint what they represent.
Now each stat has the same emoji prefix as v1:
⚔️ session kills, 🏆 total kills, KPH suffix
💎 rares (session/total), 📊 KPR suffix
🕑 online time, 💀 deaths, 🔮 prismatic tapers
Meta state pill still color-coded (green=active, gray=idle).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rebuilds the v1 map-centric experience in React:
Layout:
- 400px sidebar on left, interactive map on right (flex, 100vh)
- Exact same proportions and dark theme as v1
Sidebar (top→bottom):
- Header with active player count + Dashboard toggle button
- Server status dot (Coldeve online/offline with pulse)
- Aggregate counters: Rares (gold), Server KPH (blue glow), Kills (red)
- 6 sort buttons (Name, KPH, S.Kills, S.Rares, T.Kills, KPR)
- Player name filter
- Scrollable player list with per-row:
- Name + coordinates
- HP/Stamina/Mana vital bars (red/orange/blue gradients)
- Session kills, total kills, KPH
- Session rares, total rares, VTank meta state pill
- Online time, deaths, prismatic tapers
- Color-coded left border per player
Map:
- dereth.png with CSS transform pan (drag) + zoom (wheel, 1.1x factor, max 20x)
- Player dots (6px circles, color-matched to sidebar)
- Hover tooltip (name, coords, kph, kills)
- World coordinate display at cursor position
- Fit-to-window on first load
View toggle: Map View ↔ Dashboard with localStorage persistence.
All v1 CSS ported under ml-* prefix, scoped via map-layout.css.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Below the character cards grid, adds four tabbed analytics sections:
Combat Tab (Recharts):
- Kills per hour horizontal bar chart (all characters, sorted)
- Total damage session bar chart
- Damage by element pie chart (aggregated across all characters)
Rares Tab:
- Summary cards: total rares, total kills, drop rate (1 in N)
- Recent rare drops timeline (from WebSocket events)
- Rares per character lifetime bar chart
Map Tab:
- Dereth map (dereth_highres.png) with SVG overlay
- Character position dots (green=hunting, yellow=other)
- Hover to see character name + coordinates
- Responsive, maintains aspect ratio
Inventory Tab:
- Cross-character item search with debounced input
- Results table: character, item, type, material, set, workmanship
- Powered by existing /search/items API
All tabs lazy-rendered (only active tab mounts). Horizontal scroll
tab bar on mobile. Dark theme consistent with cards.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
vt_state values from plugins include "Hunt", "combat", "Default",
"turn_in_quests" etc. Previously only "combat" showed as green badge,
everything else was "Idle". Now Hunt shows green, unknown states show
their actual name.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New modern dashboard at /v2 running alongside the existing UI at /.
Same backend, same APIs, same WebSocket — zero backend changes.
Stack: React 19 + Vite + TypeScript + Recharts
Source: frontend/ — build output: static/v2/
Phase 1 delivers:
- Character overview cards in a responsive CSS Grid
- Live HP/Stamina/Mana bars via WebSocket vitals
- Kills/hr, total kills, deaths, session uptime
- VTank state badge (Combat/Nav/Idle)
- Location coordinates
- Click to expand: combat stats, prismatic count, CPU/RAM
- Global stats header: active chars, total kills, total rares, server health
- WebSocket hook with auto-reconnect
- HTTP poll fallback for initial load + server health
- Mobile responsive (single column on narrow screens)
- Dark theme matching the MosswartOverlord palette
Build: cd frontend && npm run build
Access: /v2 (served by existing NoCacheStaticFiles mount)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The element breakdown grid previously only showed damage RECEIVED
(defense) in the Mel/Msl and Magic columns, which was mostly empty
for characters who evade/resist everything. Now shows both:
- Given M/M + Given Mag: damage dealt by element (offense)
- Recv M/M + Recv Mag: damage taken by element (defense)
This makes the element breakdown immediately useful — you can see
that you're dealing Slash damage via melee, for example.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Peers who unsubscribed or disconnected from vital sharing were lingering
forever in the Vital Sharing browser window because nothing ever deleted
them from the server-side state or told the browser to drop them.
Backend:
- share_unsubscribe now pops the character from _vital_sharing_peer_state
(not just flips connected=false) and broadcasts a share_peer_removed
envelope to browser clients.
- On real plugin disconnect, do the same: pop the state entry and
broadcast share_peer_removed so the NetworkUI updates immediately.
Frontend:
- New removeVitalSharingPeer(name) deletes from the local
vitalSharingPeers dict and re-renders.
- socket.onmessage now routes share_peer_removed to it.
- refreshVitalSharingPeers() reconciles against the server's list and
prunes any local entries the server no longer knows about, catching
any race where the broadcast was missed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
_broadcast_share_to_plugin_clients was discarding a character from
_vital_sharing_subscribers whenever a single send_json hit the 1-second
timeout or raised any exception. Under heavy load this permanently
dropped clients from the subscriber set even though their WebSocket was
still fully connected — the user had to toggle vital sharing off and on
to get peer updates flowing again.
Now we log the send failure but leave the subscriber intact. Actual
eviction still happens on real WebSocket disconnect via the finally
block in the plugin receive loop.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously the NetworkUI window used #c44/#4c4/#46c which rendered
stamina as green. Updated to the gradients used in .vital-fill.* in
style.css (#ff4444, #ffaa00, #4488ff) so the network UI matches the
player sidebar.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Accepts new share_subscribe / share_unsubscribe / share_* WebSocket
messages from MM plugin clients and fans them out to other opted-in
plugin clients (excluding origin) and to browser clients for the
NetworkUI window.
- main.py: _vital_sharing_subscribers set, _vital_sharing_peer_state
snapshot, _broadcast_share_to_plugin_clients relay, disconnect
cleanup, GET /vital-sharing/peers endpoint.
- static/index.html: new sidebar link for Vital Sharing window.
- static/script.js: showVitalSharingWindow with live HP/STA/MANA bars,
per-peer status dot/tags/position, 5s /vital-sharing/peers poll, and
share_* routing through the existing browser WebSocket handler.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>