UI Modernization (March 25-26, 2026)
Comprehensive UI panel redesign with unified layout system, optimistic updates, and cross-player data leak fixes. Pull Requests: #1088, #1089, #1087Files Changed: 54 files, ~4,600 additions, ~2,700 deletions
Date: March 25-26, 2026
Overview
This update modernizes the game’s UI panels with a focus on:- Visual Consistency: Unified layout constants and theme utilities across all panels
- Responsiveness: Optimistic UI updates for instant feedback
- Reliability: Fixed cross-player data leaks and event ordering races
- Immersion: Heraldic shield combat banners and live 3D equipment preview
Combat Panel Heraldic Shield Redesign
Visual Changes
Replaced vertical combat style list with horizontal heraldic shield banners featuring:- SVG Shield Shapes: Custom shield/crest geometry with theme-derived gradients
- Protruding Icons: Filled geometric icons at top of each shield (accurate = concentric circles, aggressive = double arrows, defensive = shield, controlled = crosshair, rapid = lightning bolt, longrange = arrow, autocast = sparkle)
- Active State Tinting: Color overlay gradients when style is active
- Compact Layout: 4 shields in a row with responsive gap sizing
Implementation
Shield SVG Paths (packages/client/src/game/panels/CombatPanel.tsx):
Optimistic Updates
Combat controls now update instantly before server confirmation:- Zero perceived latency for combat controls
- Matches OSRS behavior (instant style switching)
- Server remains authoritative (can reject invalid changes)
Equipment Panel Paperdoll Portrait
Live 3D Character Preview
Added interactive 3D character preview showing equipped gear in real-time. Features:- Live Rendering: Dedicated WebGPU viewport with player’s VRM avatar
- Equipment Visuals: Dynamically loads and attaches equipped items to VRM skeleton
- Interactive Controls: Drag to rotate, scroll to zoom
- Fallback Graphics: Stylized silhouette when avatar unavailable
- Performance: Shared equipment visual logic between system and portrait
Implementation
Avatar Preview Viewport (packages/client/src/game/character/avatarPreviewViewport.ts):
packages/shared/src/systems/client/EquipmentVisualHelpers.ts):
Extracted shared logic from EquipmentVisualSystem for reuse in portrait:
packages/client/src/game/panels/equipment/EquipmentPaperdollPortrait.tsx):
- Immersive equipment preview matching OSRS/RS3 aesthetic
- Shared equipment visual logic reduces code duplication
- Reusable viewport factory for other character previews
- Interactive controls enhance player engagement
Unified Panel Layout Constants
Single Source of Truth
Extracted shared panel dimensions intopanelLayout.ts for consistency across all icon-grid panels.
Constants (packages/client/src/constants/panelLayout.ts):
Usage Pattern
Before (scattered magic numbers):InventoryPanel.tsxEquipmentPanel.tsxPrayerPanel.tsxSpellsPanel.tsxSkillsPanel.tsxQuestLog.tsx
- Consistent spacing across all panels
- Single place to adjust panel dimensions
- Eliminates scattered magic numbers
- Mobile and desktop variants clearly defined
CursorTooltip Component
Reusable Tooltip Primitive
Created portal-based mouse-following tooltip with auto-measurement and viewport-edge flipping. Component (packages/client/src/ui/core/tooltip/CursorTooltip.tsx):
Before/After Comparison
Old Pattern (duplicated across 5+ panels):InventoryPanel.tsx- Item tooltipsPrayerPanel.tsx- Prayer tooltipsSpellsPanel.tsx- Spell tooltipsSkillsPanel.tsx- Skill tooltipsEquipmentPanel.tsx- Equipment tooltips
- Eliminates ~50 lines of duplicated tooltip code per panel
- Consistent tooltip behavior across all panels
- Auto-measurement prevents clipping
- Viewport-edge flipping for better UX
Tab Persistence System
Problem
Switching tabs would unmount the inactive panel, losing scroll position and component state.Solution
Render all window tabs simultaneously withdisplay:none/flex toggling instead of unmounting.
Implementation (packages/client/src/game/interface/InterfacePanels.tsx):
- Scroll position preserved across tab switches
- Component state (expanded sections, filters) retained
- Smoother tab switching experience
- Matches modern browser tab behavior
Optimistic UI Updates
Combat Controls
Attack Style Changes:Inventory Actions
Consolidated Rollback System (packages/shared/src/systems/client/ClientNetwork.ts):
- Timeout: 5 seconds (if server doesn’t confirm)
- Pruner: Runs every 1 second to check for stale actions
- Cleanup: Clears on
INVENTORY_UPDATED(server confirmation) or disconnect - Shared Tracker: Single
PendingActionTrackerinClientNetworkused by all callers
- Instant feedback for eat/drop/bury/firemaking actions
- Eliminates duplicate tracker instances (two timers, two listeners)
- Single source of truth for optimistic inventory mutations
- Reduced ~70 lines of boilerplate across callers
- Automatic rollback if server doesn’t respond within 5s
Cross-Player Data Leak Fixes
Equipment Panel Leak
Problem: Equipment panel was displaying AI agents’ weapons becauseequipmentUpdated broadcasts hit all players without filtering.
Root Cause (packages/client/src/hooks/usePlayerData.ts):
packages/shared/src/systems/client/ClientNetwork.ts):
Combat Damage Deduplication
Problem
sendToNearby publishes to 9 region topics (player’s region + 8 adjacent), causing players near region boundaries to receive the same damage packet 2-3 times, resulting in duplicate damage splats.
Solution
Deduplicate using tick-based keys with periodic sweep. Implementation (packages/shared/src/systems/client/ClientNetwork.ts):
packages/server/src/systems/ServerNetwork/event-bridge.ts):
- Soft Sweep: Clears entries >500ms old when map exceeds 150 entries
- Hard Cap: Trims to 100 entries if map exceeds 200 (prevents unbounded growth)
- Tick-Based Keys: Distinguishes same-damage rapid hits on different ticks
- Rolling Deploy Fallback: Uses
performance.now() / 125when server tick field is missing
Attack Style System Cleanup
Removed Dead Code
Removed attack style cooldown infrastructure that was hardcoded to 0ms. Removed:STYLE_CHANGE_COOLDOWN = 0constantstyleChangeTimersMap and timer cleanup logiccombatStyleHistoryarray (write-only, never displayed)lastStyleChangetimestamp tracking- Dead API methods:
canPlayerChangeStyle()- Always returnedtruegetRemainingStyleCooldown()- Always returned0getPlayerStyleHistory()- Always returned[]
packages/shared/src/systems/shared/character/PlayerSystem.ts- Removed cooldown logic (~150 lines)packages/shared/src/systems/shared/infrastructure/SystemLoader.ts- Removed API bindingspackages/shared/src/types/entities/player-types.ts- RemovedPlayerAttackStyleStatefields
- Cleaner codebase with ~200 lines of dead code removed
- No functional changes (cooldown was already 0ms)
- Simpler attack style system without unnecessary complexity
Auto-Initialization for Event Ordering Races
Problem
UI events (attack style change, auto-retaliate toggle, equipment updates) can arrive beforeonPlayerRegister fires, causing “no state for player” errors.
Solution
Added auto-initialization guards that create default state if player exists but hasn’t been registered yet. Attack Style Auto-Init (packages/shared/src/systems/shared/character/PlayerSystem.ts):
- Eliminates “no state for player” errors from event ordering races
- Player choices take precedence over DB-saved values during session
- Reconnection preserves in-session equipment and combat preferences
Weapon Change Auto-Style Switching
OSRS-Accurate Behavior
Auto-switch attack style when weapon changes and current style is invalid for new weapon. Implementation (packages/shared/src/systems/shared/character/PlayerSystem.ts):
Additional Fixes
Starter Equipment
Change: FixedSTARTER_EQUIPMENT referencing non-existent bronze_sword → bronze_shortsword.
Files Changed:
packages/shared/src/systems/shared/character/InventorySystem.tspackages/shared/src/systems/shared/character/PlayerSystem.tspackages/shared/src/systems/shared/entities/ItemSpawnerSystem.ts
Fire Model Asset Path
Change: Corrected fire model path frommodels/firemaking-fire/ to models/misc/firemaking-fire/.
Files Changed: packages/shared/src/systems/shared/interaction/ProcessingSystem.ts
Impact: Eliminates 404 errors when spawning firemaking fires.
Targeting Mode UI
Changes:- Immediate Clear: Targeting state clears immediately after target selection (no server round-trip wait)
- Hover State: Removed
isTargetingActivefrom slot hover condition to prevent grey flash on all filled slots - System Registration: Registered
InventoryInteractionSystemon client for targeting support
- Targeting mode feels more responsive
- No stale highlights after target selection
- Cleaner visual feedback
Panel Data Synchronization
Problem:WindowRenderer and WindowItem are wrapped in React.memo(), which blocked prop updates when inventory/equipment/stats changed.
Solution (packages/client/src/game/interface/InterfaceManager.tsx):
- Inventory panels update in real-time when data changes
- Lightweight counter (number) breaks memo without forcing panel re-mount
renderPanelstays stable (no unnecessary panel recreation)
Event Type Consistency
Change: Replaced raw string event names withEventType enum constants.
Implementation (packages/shared/src/systems/shared/entities/Entities.ts):
Migration Guide
For Developers
Panel Layout Constants:For Players
No Breaking Changes: All updates are backward-compatible. Existing characters, inventory, and progress are preserved. New Features:- Combat panel now shows horizontal shield banners (more compact)
- Equipment panel has live 3D character preview (drag to rotate, scroll to zoom)
- Spells panel added to default layout (check right-column window)
- Combat controls feel more responsive (instant feedback)
- Inventory actions (eat, drop, firemaking) update instantly
- Quest log uses themed tiles and badges
- Panel spacing is more consistent across all panels
- Tooltips have consistent styling and positioning
Testing
New Test Coverage
Equipment Panel (packages/client/tests/unit/EquipmentPanel.test.tsx):
- Paperdoll slots render correctly (11 slots)
- Portrait container exists and shows loading state
- Equipped items display with icons (no visible item names in slots)
- Props updates trigger re-render
- Mobile layout maintains portrait
packages/client/tests/e2e/panels.spec.ts):
- Equipment panel renders paperdoll layout on mobile viewport
- Portrait stays stable during equipment interactions
- All 11 equipment slots present and functional
Test Commands
Performance Considerations
Tab Persistence Trade-offs
Benefit: Preserves scroll position and component state across tab switches. Cost: All tabs are mounted simultaneously (hidden withdisplay:none). For windows with heavy panels (e.g., 3D equipment portrait), this means the portrait’s WebGPU renderer stays alive even when viewing other tabs.
Mitigation: Portrait renderer is lightweight (separate viewport, minimal scene complexity). Future optimization could pause rendering when tab is hidden.
Equipment Portrait WebGPU Context
Resource Usage: Each equipment panel creates its own WebGPU renderer, animation loop, and avatar scene. Considerations:- Second WebGPU context alongside main game renderer
- On lower-end GPUs (especially mobile), this could cause context loss
- Portrait only renders when equipment panel is visible
- Share main renderer via render-to-texture
- Only initialize portrait when panel is actually visible
- Add cleanup when panel is hidden (not just unmounted)
Optimistic Update Rollback
Memory:PendingActionTracker stores inventory snapshots for up to 5 seconds.
Cleanup: Automatic cleanup on server confirmation or disconnect.
Bounded: Single shared tracker prevents duplicate instances.
Known Issues
Optimistic Updates Without Rollback
Combat Controls: Optimistic updates for attack style and auto-retaliate don’t have explicit rollback if server rejects the change. Server will send authoritative value, but there could be a brief flash of wrong state. Mitigation: Server rejection is rare (only for invalid weapon/style combinations), and server confirmation arrives within ~100-200ms.Panel Data Version Pattern
Implementation: UsesuseMemo with side effects (mutating panelDataVersionRef.current), which is technically an anti-pattern in React concurrent mode.
Risk: React may call memo factories more than once in concurrent mode.
Mitigation: Works correctly in current React 19 implementation. Future React upgrades may require refactoring to useRef + useEffect pattern.
Files Changed
PR #1088 (UI Panel Upgrade)
- 33 files, 4,211 additions, 2,320 deletions
- Combat panel redesign with heraldic shields
- Equipment panel paperdoll portrait
- Unified panel layout constants
- CursorTooltip component
- Tab persistence system
- Quest UI theme modernization
PR #1089 (Equipment Panel Cross-Player Leak)
- 12 files, 250 additions, 194 deletions
- Equipment panel
playerIdfiltering - Optimistic combat UI updates
- Attack style cooldown removal
- Combat damage deduplication
- Auto-initialization guards
- Weapon change auto-style switching
PR #1087 (Inventory Firemaking UI)
- 9 files, 149 additions, 171 deletions
- Optimistic inventory rollback consolidation
- Firemaking optimistic removal
- Fire model asset path fix
- Targeting mode UI fixes
- Panel data synchronization fix
References
- PR #1088: feat(ui): comprehensive UI panel upgrade
- PR #1089: Fix/equipment panel cross player leak
- PR #1087: fix(client): inventory UI fixes for firemaking and targeting mode
- CLAUDE.md: Development guidelines
- README.md: Project overview