refactor: DRY consolidation, health de-sync fix, component decomposition#1146
refactor: DRY consolidation, health de-sync fix, component decomposition#1146SYMBaiEX wants to merge 11 commits into
Conversation
Extract 1,626-line monolith into 8 focused files under combat/: - StyleIcons.tsx: SVG icon components for attack styles - CombatStyleSelector.tsx: style banner grid with draggable shields - CombatBonusesDisplay.tsx: HP bar, combat level, stat rows - AutoRetaliateToggle.tsx: OSRS-style on/off toggle - SpecialAttackBar.tsx: special attack energy bar - types.ts: shared interfaces for combat sub-components - typeGuards.ts: runtime event validation CombatPanel.tsx reduced to 448 lines as orchestrator managing state and world event subscriptions. Zero behavior changes.
Extract 1,813-line canvas minimap into focused modules: - MinimapRenderer.ts (937 LOC): pure canvas 2D drawing functions with zero React dependencies — terrain, roads, buildings, entity pips, destination marker, location icons with flyweight cache - useMinimapInteraction.ts (389 LOC): zoom, collapse, resize drag, click-to-teleport interaction handlers Minimap.tsx reduced to 667 lines as orchestrator managing canvas ref, requestAnimationFrame loop, and composing satellite hooks. Zero behavior changes — all drawing, caching, and interaction preserved.
…lient use SkillsSystem XP methods are instance-only, so add standalone XPCalculator.ts for client-side UI (progress bars, tooltips, XP orbs). Add getHpPercent and getHpColor to existing CombatUtils.ts. Add isPrayerStateSyncPayload and isPrayerToggledPayload type guards to existing prayer-types.ts. Export calculateCombatLevel, normalizeCombatSkills from CombatLevelCalculator.ts.
Remove 14 local duplicate implementations across client:
- 5x calculateCombatLevel (SkillsPanel, AgentSkillsPanel, AgentSummaryCard)
now use calculateCombatLevel(normalizeCombatSkills(...)) from shared
- 5x HP percentage/color (StatusBar, StatusBars, DuelHUD, CombatBonuses,
AgentMonitor) now use getHpPercent/getHpColor from shared
- 2x prayer type guards (PrayerPanel) now import from shared
- 2x XP table + functions (useXPOrbState, AgentMonitorScreen) now use
getXPForLevel/getLevelForXP from shared
- 2x local type stubs (useMinimapInteraction) replaced with shared
TerrainSystem import and world.network?.send?.()
- 1x unsafe cast (useModalPanels) replaced with isDuelCompletedPayload
type guard
- Fix CombatPanel || fallback to ?? (prevented {current:100,max:100} flash)
- Fix CompactStatusHUD hardcoded 10/10 defaults to 0/1
…was stale
Root cause: StatsComponent has TWO health objects — this.health (public property)
and this.data.health (Component base class dict). damagePlayer() only updated
this.data.health, leaving this.health at spawn-time max. When constitution XP
was gained on mob kill, SkillsSystem.handleLevelUp() read the stale
this.health.current (still at max), causing emitPlayerUpdate() to broadcast
max health to the client.
Fix: damagePlayer() now syncs BOTH statsComponent.health AND
statsComponent.data.health so they stay consistent. handleSkillsUpdate() now
also syncs player.health.max from constitution level on every skills update
(matching the initial registration formula at line 693).
Also fixes CombatPanel.tsx || fallback (was { current:100, max:100 }, now ??
with { current:0, max:1 }) and CompactStatusHUD.tsx hardcoded 10/10 defaults.
- CombatPanel.tsx: replace inline combat level formula with shared calculateCombatLevel(normalizeCombatSkills(...)) - StatsPanel.tsx: same — was using melee-only formula missing ranged/magic - useMinimapTerrainCache.ts: delete local TerrainSystemLike stub, import TerrainSystem from shared + use world.getSystem<TerrainSystem>() generic - PlayerSystem.ts damagePlayer(): cast to StatsComponent instead of anonymous structural type - PlayerSystem.ts handleSkillsUpdate(): sync statsComponent.health on every skills update (defense-in-depth against event ordering changes)
…TTL, React.memo - XPCalculator.ts: align accumulative formula with SkillsSystem and Solidity XPTable.sol contract (13,034,394 at level 99, not 13,034,431) - PlayerSystem.calculateCombatLevel(): replace simplified melee-only formula with shared calculateCombatLevel(normalizeCombatSkills(...)) — now includes prayer and magic contributions matching the client display - imageCache.ts: add 30s TTL on error entries so transient CDN failures don't permanently block image loading for the session - CombatStyleSelector, CombatBonusesDisplay, AutoRetaliateToggle: wrap in React.memo to prevent unnecessary re-renders during combat state changes
…ruth Replace 9 hardcoded 2147483647 values across shared, server, and client with a single exported constant from CoinPouchSystem.ts. No new files — just changed `const` to `export const` on the existing definition. Updated: BankingConstants.MAX_ITEM_STACK, bank-operations.MAX_QUANTITY, NoteGenerator.maxStackSize, duel-settlement overflow checks (x2), currencyUtils DEFAULT_CURRENCIES maxValue (x3).
…ve dead toTotalCopper formatGoldValue reimplemented the same K/M/B formatting that compactNumber already provided in the same file. Replaced the 18-line body with a 2-line delegation to compactNumber. All 8 callers (DuelPanel, TradePanel) unchanged. Removed toTotalCopper() — exported but never imported by any consumer.
Caught two more instances missed in the first pass: - interaction.ts INPUT_LIMITS.MAX_QUANTITY (used by server InputValidation) - InventorySystem.ts MAX_QUANTITY (stack size cap) Both now import MAX_COINS from CoinPouchSystem. Only the source of truth definition and unrelated MAX_TICK in PlayerDeathSystem remain as literals.
…a Omit
Export the canonical ContextMenuAction from shared/index.ts (alongside
existing LabelSegment export). Replace the 8-line local interface in
EntityContextMenu.tsx with a derived type:
Omit<SharedContextMenuAction, "handler" | "priority"> & { onClick }
This makes the relationship explicit: the client render type IS the shared
domain type minus handler/priority (stripped by ContextMenuController),
plus an onClick callback for React event handling.
The shared type uses `handler` (domain action dispatch) while the client
uses `onClick` (React event handler) — they exist at different architectural
layers and cannot be fully unified, but derivation via Omit ensures
field additions to the shared type automatically flow to the client.
6321724 to
dca126f
Compare
PR Review: DRY Consolidation, Health De-sync Fix, Component DecompositionThorough PR that addresses real duplication, fixes a legitimate bug, and improves code organization. Here's my analysis: Health De-sync Fix — Well DiagnosedThe root cause analysis in One concern in const healthMaxChanged = constitutionLevel !== player.health.max;
if (healthMaxChanged) {
player.health.max = constitutionLevel;
if (player.health.current > player.health.max) {
player.health.current = player.health.max;
}
}This implicitly couples constitution level to health max (1 level = 1 HP). If the formula ever changes (e.g., constitution level × 10 like OSRS Hitpoints), this will silently break. The comment says "same formula as initial registration at line 693" — consider extracting a shared helper like DRY Consolidation — Clean Execution
Minor observation on XPCalculator.ts — Correct but QuadraticThe accumulative formula produces the correct values (13,034,394 at level 99, matching SkillsSystem and the Solidity contract). However:
Not a blocker — just flagging the regression in algorithmic complexity versus the removed code. Component Decomposition — Well Structured
One note on Bug Fixes — Good Catches
|
There was a problem hiding this comment.
Pull request overview
This PR consolidates previously duplicated combat/XP/HP/currency utilities into @hyperscape/shared, fixes a server-side health desync edge case after constitution level-ups, and decomposes large UI components (CombatPanel/Minimap) into smaller, testable units.
Changes:
- Add shared OSRS-style XP ↔ level utilities, HP bar utilities, combat-level helpers, prayer payload guards, and shared currency cap constants.
- Fix health max/current synchronization across
PlayerSystem/StatsComponentto prevent post-combat health snapping to max after constitution XP gain. - Refactor CombatPanel into subcomponents and Minimap into a renderer + interaction hook; improve image-cache error retry behavior.
Reviewed changes
Copilot reviewed 43 out of 43 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/shared/src/utils/game/XPCalculator.ts | New shared XP/level conversion utilities for UI use. |
| packages/shared/src/utils/game/index.ts | Re-export XP calculator from shared game utils barrel. |
| packages/shared/src/utils/game/CombatUtils.ts | Add shared HP percent + HP color utilities. |
| packages/shared/src/types/game/prayer-types.ts | Add exported runtime type guards for prayer payloads. |
| packages/shared/src/types/bank-operations.ts | Replace hardcoded max quantity with shared MAX_COINS. |
| packages/shared/src/systems/shared/character/PlayerSystem.ts | Sync health current/max with StatsComponent + constitution-derived max health. |
| packages/shared/src/systems/shared/character/InventorySystem.ts | Replace hardcoded max stack quantity with MAX_COINS. |
| packages/shared/src/systems/shared/character/CoinPouchSystem.ts | Export DEFAULT_STARTING_COINS and MAX_COINS for reuse. |
| packages/shared/src/index.ts | Export new shared utilities/constants/types from the main shared entrypoint. |
| packages/shared/src/index.client.ts | Export new shared utilities/constants/types from the client shared entrypoint. |
| packages/shared/src/extras/ui/imageCache.ts | Add error TTL to retry transient CDN image failures. |
| packages/shared/src/data/NoteGenerator.ts | Use MAX_COINS for noted item max stack size. |
| packages/shared/src/constants/interaction.ts | Use MAX_COINS for input MAX_QUANTITY. |
| packages/shared/src/constants/BankingConstants.ts | Use MAX_COINS for MAX_ITEM_STACK. |
| packages/server/src/systems/ServerNetwork/duel-settlement.ts | Use MAX_COINS for overflow checks during stake transfers. |
| packages/client/src/ui/components/StatusBar.tsx | Swap local HP color logic for shared getHpColor. |
| packages/client/src/screens/AgentMonitorScreen.tsx | Replace local XP table + HP color logic with shared utilities. |
| packages/client/src/hooks/useModalPanels.ts | Add payload type guard for duel completion modal handling. |
| packages/client/src/game/systems/currency/index.ts | Stop exporting removed dead helper toTotalCopper. |
| packages/client/src/game/systems/currency/currencyUtils.ts | Use MAX_COINS in currency definitions; dedupe formatGoldValue via compactNumber. |
| packages/client/src/game/panels/StatsPanel.tsx | Use shared combat-level helpers instead of local formula. |
| packages/client/src/game/panels/SkillsPanel.tsx | Use shared combat-level + XP threshold helpers. |
| packages/client/src/game/panels/PrayerPanel.tsx | Use shared prayer payload guards instead of local duplicates. |
| packages/client/src/game/panels/DuelPanel/DuelHUD.tsx | Use shared HP percent/color helpers; map to theme colors. |
| packages/client/src/game/panels/CombatPanel.tsx | Orchestrator refactor to compose new combat subcomponents and shared combat-level helpers. |
| packages/client/src/game/panels/combat/types.ts | New shared prop/event/type definitions for combat panel decomposition. |
| packages/client/src/game/panels/combat/typeGuards.ts | New runtime event guards extracted from CombatPanel. |
| packages/client/src/game/panels/combat/StyleIcons.tsx | Extract combat SVG icon components + constants. |
| packages/client/src/game/panels/combat/SpecialAttackBar.tsx | New special attack bar component (future-facing). |
| packages/client/src/game/panels/combat/index.ts | Barrel exports for combat panel subcomponents/types/guards. |
| packages/client/src/game/panels/combat/CombatStyleSelector.tsx | New combat style banner grid component (drag + cooldown UI). |
| packages/client/src/game/panels/combat/CombatBonusesDisplay.tsx | New HP/level/target HP + stat row component. |
| packages/client/src/game/panels/combat/AutoRetaliateToggle.tsx | New auto-retaliate toggle component extracted from CombatPanel. |
| packages/client/src/game/interface/CompactStatusHUD.tsx | Fix HP defaults to avoid incorrect 10/10 display. |
| packages/client/src/game/hud/xp-orb/useXPOrbState.ts | Replace local XP table/search with shared getXPForLevel/getLevelForXP. |
| packages/client/src/game/hud/useMinimapTerrainCache.ts | Type the terrain system as TerrainSystem and use generic getSystem(). |
| packages/client/src/game/hud/useMinimapInteraction.ts | New hook extracting minimap zoom/resize/click-to-move/collapse behavior. |
| packages/client/src/game/hud/StatusBars.tsx | Use shared getHpPercent for HP bar percent calculation. |
| packages/client/src/game/hud/MinimapRenderer.ts | New pure drawing module for minimap rendering + icon cache helpers. |
| packages/client/src/game/hud/Minimap.tsx | Minimap orchestrator refactor to use renderer + interaction hook. |
| packages/client/src/game/hud/EntityContextMenu.tsx | Use shared ContextMenuAction type and derive client type via Omit. |
| packages/client/src/game/dashboard/AgentSummaryCard.tsx | Use shared combat-level helpers instead of local formula. |
| packages/client/src/game/dashboard/AgentSkillsPanel.tsx | Use shared combat-level + XP utilities; align skills typing with shared Skills. |
Comments suppressed due to low confidence (1)
packages/client/src/hooks/useModalPanels.ts:817
duelCompletedpayload coming from ClientNetwork uses awinner: booleanfield, but this handler/type guard expectswon. With the new guard in place, valid duelCompleted events will be ignored and the duel result modal will never show. Align the payload type/guard and mapping to usewinner(or accept both and normalize).
const rawData: unknown = payload.data;
if (!isDuelCompletedPayload(rawData)) return;
const completedData = rawData;
setDuelData(null);
setDuelResultData({
visible: true,
won: completedData.won ?? false,
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| import React from "react"; | ||
| import { useDraggable } from "@dnd-kit/core"; | ||
| import { getPanelInsetStyle } from "@/ui/theme/themes"; |
| // Accumulative formula matching SkillsSystem.generateXPTable() exactly: | ||
| // xpDelta(L) = floor((L-1 + 300 * 2^((L-1)/7)) / 4) | ||
| // xpForLevel(N) = sum(xpDelta(2)..xpDelta(N)) | ||
| let cumulative = 0; | ||
| for (let l = 2; l <= clampedLevel; l++) { | ||
| const xp = Math.floor(l - 1 + 300 * Math.pow(2, (l - 1) / 7)) / 4; | ||
| cumulative = Math.floor(cumulative + xp); | ||
| } |
| export function getLevelForXP(xp: number): number { | ||
| if (xp <= 0) return 1; | ||
| for (let level = MAX_LEVEL; level >= 1; level--) { | ||
| if (xp >= getXPForLevel(level)) { | ||
| return level; | ||
| } | ||
| } |
PR Review: DRY consolidation, health de-sync fix, component decompositionOverall this is a high-quality PR — the DRY consolidation is well-motivated, the health de-sync fix addresses a real root cause, and the component decompositions are cleanly done. A few items worth addressing before merge: Bug: HP color threshold mismatch in
|
Summary
Depends on: #1145 (PR1)
DRY Consolidation
calculateCombatLevel,normalizeCombatSkills,getXPForLevel,getHpPercent,getHpColor, prayer type guards from sharedContextMenuActionfrom shared, derive client type viaOmitMAX_COINSfrom CoinPouchSystem, replace 9+ hardcoded2147483647valuesformatGoldValuevia existingcompactNumber, remove deadtoTotalCopperBug Fix
StatsComponenthad TWO health objects (this.healthandthis.data.health) that were out of sync.damagePlayer()only updatedthis.data.health, leavingthis.healthstale. When constitution XP was gained on mob kill,handleLevelUp()read the stale value, causingemitPlayerUpdate()to broadcast max health to the client.CombatPanel.tsx||→??for health fallback (was showing 100/100 flash)CompactStatusHUD.tsxhardcoded 10/10 defaults → 0/1Component Decomposition
Audit Fixes
calculateCombatLevelTest plan
bun run build:sharedsucceedsnpx tsc --noEmit— 0 new errors across all packages