From 9499c46798e362af8b380a331d0141d09cabe85d Mon Sep 17 00:00:00 2001 From: SYMBaiEX Date: Wed, 1 Apr 2026 00:40:30 -0500 Subject: [PATCH 001/266] refactor(client): remove unused WebSocketManager dead code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete websocket-manager.ts (~435 LOC) and its barrel export from lib/index.ts. This module was never imported by any consumer — the CharacterSelectScreen uses its own local connection state, and the game client uses Hyperscape's internal world.init() networking. --- packages/client/src/lib/index.ts | 7 - packages/client/src/lib/websocket-manager.ts | 435 ------------------- 2 files changed, 442 deletions(-) delete mode 100644 packages/client/src/lib/websocket-manager.ts diff --git a/packages/client/src/lib/index.ts b/packages/client/src/lib/index.ts index f8dbbf946..0a51705e7 100644 --- a/packages/client/src/lib/index.ts +++ b/packages/client/src/lib/index.ts @@ -19,13 +19,6 @@ export { ELIZAOS_URL, ELIZAOS_API, } from "./api-config"; -export { - WebSocketManager, - createWebSocketManager, - ConnectionState, - type WebSocketManagerConfig, - type WebSocketManagerCallbacks, -} from "./websocket-manager"; export { withRetry, tryWithRetry, diff --git a/packages/client/src/lib/websocket-manager.ts b/packages/client/src/lib/websocket-manager.ts deleted file mode 100644 index 681d3f2c0..000000000 --- a/packages/client/src/lib/websocket-manager.ts +++ /dev/null @@ -1,435 +0,0 @@ -/** - * WebSocket Manager with Exponential Backoff Reconnection - * - * Provides a robust WebSocket wrapper with: - * - Automatic reconnection with exponential backoff - * - Connection state tracking - * - Event queuing during reconnection - * - Max retry limit with user notification - * - * @remarks - * Note: CharacterSelectScreen uses custom binary packet handling (readPacket/writePacket) - * and requires careful integration. GameClient and EmbeddedGameClient use Hyperscape's - * internal world.init() which manages its own networking layer. - * - * This manager is available for: - * - Future reconnection improvements to character selection - * - Custom WebSocket connections outside of Hyperscape core - * - Agent/dashboard WebSocket connections - * - * @packageDocumentation - */ - -/** Connection state enum */ -export enum ConnectionState { - DISCONNECTED = "disconnected", - CONNECTING = "connecting", - CONNECTED = "connected", - RECONNECTING = "reconnecting", -} - -/** WebSocket manager configuration */ -export interface WebSocketManagerConfig { - /** Base URL for WebSocket connection */ - url: string; - /** Initial retry delay in milliseconds (default: 1000) */ - initialRetryDelay?: number; - /** Maximum retry delay in milliseconds (default: 30000) */ - maxRetryDelay?: number; - /** Maximum number of retry attempts (default: 10) */ - maxRetries?: number; - /** Backoff multiplier (default: 2) */ - backoffMultiplier?: number; - /** Protocols to use for WebSocket connection */ - protocols?: string | string[]; -} - -/** Event callbacks for WebSocket manager */ -export interface WebSocketManagerCallbacks { - /** Called when connection is established */ - onConnected?: () => void; - /** Called when connection is lost */ - onDisconnected?: (event: CloseEvent) => void; - /** Called when reconnecting */ - onReconnecting?: (attempt: number, nextDelay: number) => void; - /** Called when max retries exceeded */ - onMaxRetriesExceeded?: () => void; - /** Called when a message is received */ - onMessage?: (event: MessageEvent) => void; - /** Called on error */ - onError?: (event: Event) => void; -} - -/** - * WebSocket Manager class - * - * Manages WebSocket connections with automatic reconnection using exponential backoff. - * - * @example - * ```typescript - * const manager = new WebSocketManager({ - * url: 'wss://example.com/socket', - * maxRetries: 10, - * }); - * - * manager.setCallbacks({ - * onConnected: () => console.log('Connected!'), - * onReconnecting: (attempt, delay) => console.log(`Retrying in ${delay}ms...`), - * }); - * - * manager.connect(); - * ``` - */ -export class WebSocketManager { - private ws: WebSocket | null = null; - private config: Required> & { - protocols?: string | string[]; - }; - private callbacks: WebSocketManagerCallbacks = {}; - private state: ConnectionState = ConnectionState.DISCONNECTED; - private retryCount = 0; - private retryTimeoutId: ReturnType | null = null; - private messageQueue: Array = []; - private intentionallyClosed = false; - - /** Heartbeat interval handle */ - private heartbeatInterval: ReturnType | null = null; - /** Heartbeat timeout for pong response */ - private heartbeatTimeout: ReturnType | null = null; - /** Heartbeat interval in milliseconds */ - private readonly HEARTBEAT_INTERVAL = 30000; - /** Heartbeat timeout in milliseconds */ - private readonly HEARTBEAT_TIMEOUT = 10000; - /** Last ping timestamp */ - private lastPingTime = 0; - /** Last pong received timestamp */ - private lastPongTime = 0; - - constructor(config: WebSocketManagerConfig) { - this.config = { - url: config.url, - initialRetryDelay: config.initialRetryDelay ?? 1000, - maxRetryDelay: config.maxRetryDelay ?? 30000, - maxRetries: config.maxRetries ?? 10, - backoffMultiplier: config.backoffMultiplier ?? 2, - protocols: config.protocols, - }; - } - - /** - * Set callback functions for WebSocket events - */ - setCallbacks(callbacks: WebSocketManagerCallbacks): void { - this.callbacks = { ...this.callbacks, ...callbacks }; - } - - /** - * Get current connection state - */ - getState(): ConnectionState { - return this.state; - } - - /** - * Get current retry count - */ - getRetryCount(): number { - return this.retryCount; - } - - /** - * Check if WebSocket is connected and ready - */ - isConnected(): boolean { - return ( - this.state === ConnectionState.CONNECTED && - this.ws?.readyState === WebSocket.OPEN - ); - } - - /** - * Update the connection URL (for reconnection with new auth tokens) - */ - updateUrl(url: string): void { - this.config.url = url; - } - - /** - * Connect to the WebSocket server - */ - connect(): void { - if ( - this.state === ConnectionState.CONNECTING || - this.state === ConnectionState.CONNECTED - ) { - return; - } - - this.intentionallyClosed = false; - this.setState(ConnectionState.CONNECTING); - this.createConnection(); - } - - /** - * Disconnect from the WebSocket server - */ - disconnect(): void { - this.intentionallyClosed = true; - this.clearRetryTimeout(); - this.stopHeartbeat(); - this.retryCount = 0; - this.messageQueue = []; - - if (this.ws) { - this.ws.close(1000, "Client disconnected"); - this.ws = null; - } - - this.setState(ConnectionState.DISCONNECTED); - } - - /** - * Send a message through the WebSocket - * If not connected, the message will be queued and sent when connection is established - */ - send(data: string | ArrayBuffer | Blob): boolean { - if (this.isConnected()) { - this.ws!.send(data); - return true; - } - - // Queue message for later delivery - this.messageQueue.push(data); - return false; - } - - /** - * Get the underlying WebSocket instance (use with caution) - */ - getWebSocket(): WebSocket | null { - return this.ws; - } - - private createConnection(): void { - try { - this.ws = this.config.protocols - ? new WebSocket(this.config.url, this.config.protocols) - : new WebSocket(this.config.url); - - this.ws.onopen = this.handleOpen.bind(this); - this.ws.onclose = this.handleClose.bind(this); - this.ws.onerror = this.handleError.bind(this); - this.ws.onmessage = this.handleMessage.bind(this); - } catch (error) { - console.error("[WebSocketManager] Failed to create WebSocket:", error); - this.scheduleReconnect(); - } - } - - private handleOpen(): void { - this.setState(ConnectionState.CONNECTED); - this.retryCount = 0; - - // Start heartbeat - this.startHeartbeat(); - - // Flush queued messages - while (this.messageQueue.length > 0) { - const message = this.messageQueue.shift()!; - this.ws!.send(message); - } - - this.callbacks.onConnected?.(); - } - - private handleClose(event: CloseEvent): void { - this.ws = null; - this.stopHeartbeat(); - - // Don't reconnect if intentionally closed or server sent normal close - if (this.intentionallyClosed) { - this.setState(ConnectionState.DISCONNECTED); - return; - } - - this.callbacks.onDisconnected?.(event); - - // Attempt reconnection - this.scheduleReconnect(); - } - - private handleError(event: Event): void { - console.error("[WebSocketManager] WebSocket error:", event); - this.callbacks.onError?.(event); - } - - private handleMessage(event: MessageEvent): void { - // Check for pong response - if (typeof event.data === "string") { - try { - const data = JSON.parse(event.data); - if (data.type === "pong") { - this.handlePong(); - return; - } - } catch { - // Not a JSON message, pass through - } - } - - this.callbacks.onMessage?.(event); - } - - /** - * Starts the heartbeat ping/pong mechanism - */ - private startHeartbeat(): void { - this.stopHeartbeat(); - - this.heartbeatInterval = setInterval(() => { - if (this.isConnected()) { - this.sendPing(); - } - }, this.HEARTBEAT_INTERVAL); - } - - /** - * Stops the heartbeat mechanism - */ - private stopHeartbeat(): void { - if (this.heartbeatInterval) { - clearInterval(this.heartbeatInterval); - this.heartbeatInterval = null; - } - if (this.heartbeatTimeout) { - clearTimeout(this.heartbeatTimeout); - this.heartbeatTimeout = null; - } - } - - /** - * Sends a ping message to the server - */ - private sendPing(): void { - if (!this.isConnected()) return; - - this.lastPingTime = Date.now(); - this.send(JSON.stringify({ type: "ping", timestamp: this.lastPingTime })); - - // Set timeout for pong response - this.heartbeatTimeout = setTimeout(() => { - console.warn("[WebSocketManager] Heartbeat timeout - no pong received"); - // Connection may be stale, force reconnect - if (this.ws) { - this.ws.close(4000, "Heartbeat timeout"); - } - }, this.HEARTBEAT_TIMEOUT); - } - - /** - * Handles a pong response from the server - */ - private handlePong(): void { - this.lastPongTime = Date.now(); - - if (this.heartbeatTimeout) { - clearTimeout(this.heartbeatTimeout); - this.heartbeatTimeout = null; - } - } - - /** - * Gets the current connection latency (ping-pong round trip time) - */ - getLatency(): number { - if (this.lastPingTime === 0 || this.lastPongTime === 0) return 0; - return this.lastPongTime - this.lastPingTime; - } - - private scheduleReconnect(): void { - if (this.intentionallyClosed) { - return; - } - - if (this.retryCount >= this.config.maxRetries) { - this.setState(ConnectionState.DISCONNECTED); - this.callbacks.onMaxRetriesExceeded?.(); - return; - } - - this.setState(ConnectionState.RECONNECTING); - - // Calculate delay with exponential backoff - const delay = Math.min( - this.config.initialRetryDelay * - Math.pow(this.config.backoffMultiplier, this.retryCount), - this.config.maxRetryDelay, - ); - - this.retryCount++; - - this.callbacks.onReconnecting?.(this.retryCount, delay); - - this.retryTimeoutId = setTimeout(() => { - this.createConnection(); - }, delay); - } - - private clearRetryTimeout(): void { - if (this.retryTimeoutId !== null) { - clearTimeout(this.retryTimeoutId); - this.retryTimeoutId = null; - } - } - - private setState(state: ConnectionState): void { - this.state = state; - } - - /** - * Force a reconnection attempt (resets retry count) - */ - forceReconnect(): void { - this.clearRetryTimeout(); - this.retryCount = 0; - this.intentionallyClosed = false; - - if (this.ws) { - this.ws.close(); - this.ws = null; - } - - this.connect(); - } -} - -/** - * React hook for using WebSocket manager state - * - * @example - * ```typescript - * function MyComponent() { - * const [state, setState] = useState(ConnectionState.DISCONNECTED); - * const [retryInfo, setRetryInfo] = useState({ attempt: 0, delay: 0 }); - * - * useEffect(() => { - * const manager = new WebSocketManager({ url: 'wss://...' }); - * manager.setCallbacks({ - * onConnected: () => setState(ConnectionState.CONNECTED), - * onDisconnected: () => setState(ConnectionState.DISCONNECTED), - * onReconnecting: (attempt, delay) => { - * setState(ConnectionState.RECONNECTING); - * setRetryInfo({ attempt, delay }); - * }, - * }); - * manager.connect(); - * return () => manager.disconnect(); - * }, []); - * } - * ``` - */ -export function createWebSocketManager( - config: WebSocketManagerConfig, -): WebSocketManager { - return new WebSocketManager(config); -} From 4c6e4d2f99beb37facf778615a1bbb4fc1cfbbb2 Mon Sep 17 00:00:00 2001 From: SYMBaiEX Date: Wed, 1 Apr 2026 01:01:28 -0500 Subject: [PATCH 002/266] refactor(client): consolidate duplicate types into game/types/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Delete types/player.ts — its types were duplicated in game/types/ - Add quantity field to RawEquipmentSlot in game/types/equipment.ts - Create game/types/events.ts as canonical NetworkEvents definition - Update useModalPanels to import from game/types instead of local copy - Re-route types/index.ts barrel to re-export from game/types/ Eliminates type duplication between types/player.ts and game/types/, establishing game/types/ as the single source of truth for client UI type definitions. --- packages/client/src/game/types/equipment.ts | 6 +- packages/client/src/game/types/events.ts | 35 +++++++++++ packages/client/src/game/types/index.ts | 4 ++ packages/client/src/hooks/useModalPanels.ts | 11 +--- packages/client/src/types/index.ts | 6 +- packages/client/src/types/player.ts | 70 --------------------- 6 files changed, 48 insertions(+), 84 deletions(-) create mode 100644 packages/client/src/game/types/events.ts delete mode 100644 packages/client/src/types/player.ts diff --git a/packages/client/src/game/types/equipment.ts b/packages/client/src/game/types/equipment.ts index 9ffa5a377..bea7228ba 100644 --- a/packages/client/src/game/types/equipment.ts +++ b/packages/client/src/game/types/equipment.ts @@ -10,7 +10,11 @@ import type { Item } from "@hyperscape/shared"; /** Raw equipment slot format from server network cache */ -export type RawEquipmentSlot = { item: Item | null; itemId?: string } | null; +export type RawEquipmentSlot = { + item: Item | null; + itemId?: string; + quantity?: number; +} | null; /** Raw equipment data structure from server network cache */ export type RawEquipmentData = { diff --git a/packages/client/src/game/types/events.ts b/packages/client/src/game/types/events.ts new file mode 100644 index 000000000..a65c68906 --- /dev/null +++ b/packages/client/src/game/types/events.ts @@ -0,0 +1,35 @@ +/** + * Network Event Names + * + * Canonical event name constants for server→client network events + * used by UI components to subscribe to game state changes. + * + * @packageDocumentation + */ + +/** Network event names for UI interactions */ +export const NetworkEvents = { + INVENTORY_UPDATE: "inventoryUpdate", + EQUIPMENT_UPDATE: "equipmentUpdate", + STATS_UPDATE: "statsUpdate", + BANK_OPEN: "bankOpen", + BANK_CLOSE: "bankClose", + STORE_OPEN: "storeOpen", + STORE_CLOSE: "storeClose", + DIALOGUE_START: "dialogueStart", + DIALOGUE_END: "dialogueEnd", + SMELTING_OPEN: "smeltingOpen", + SMELTING_CLOSE: "smeltingClose", + SMITHING_OPEN: "smithingOpen", + SMITHING_CLOSE: "smithingClose", + CRAFTING_CLOSE: "craftingClose", + TANNING_CLOSE: "tanningClose", + FLETCHING_CLOSE: "fletchingClose", + QUEST_START_SCREEN: "questStartScreen", + QUEST_COMPLETE_SCREEN: "questCompleteScreen", + XP_LAMP_USE: "xpLampUse", + DUEL_ERROR: "duelError", +} as const; + +export type NetworkEventName = + (typeof NetworkEvents)[keyof typeof NetworkEvents]; diff --git a/packages/client/src/game/types/index.ts b/packages/client/src/game/types/index.ts index 11b6dfece..bfa1bd213 100644 --- a/packages/client/src/game/types/index.ts +++ b/packages/client/src/game/types/index.ts @@ -16,3 +16,7 @@ export { EQUIPMENT_SLOT_NAMES } from "./equipment"; // UI types export type { InventorySlotViewItem, StatusValue, PanelState } from "./ui"; + +// Network event types +export { NetworkEvents } from "./events"; +export type { NetworkEventName } from "./events"; diff --git a/packages/client/src/hooks/useModalPanels.ts b/packages/client/src/hooks/useModalPanels.ts index 680f96a0b..13c3b845e 100644 --- a/packages/client/src/hooks/useModalPanels.ts +++ b/packages/client/src/hooks/useModalPanels.ts @@ -12,16 +12,7 @@ import { EventType } from "@hyperscape/shared"; import { useNotificationStore } from "@/ui/stores/notificationStore"; import type { InventoryItem, TradeOfferItem } from "@hyperscape/shared"; import type { ClientWorld } from "../types"; - -/** Network event names for UI interactions */ -const NetworkEvents = { - SMELTING_CLOSE: "smeltingClose", - SMITHING_CLOSE: "smithingClose", - CRAFTING_CLOSE: "craftingClose", - TANNING_CLOSE: "tanningClose", - FLETCHING_CLOSE: "fletchingClose", - DUEL_ERROR: "duelError", -} as const; +import { NetworkEvents } from "../game/types"; interface LegacyUIUpdatePayload { component: string; diff --git a/packages/client/src/types/index.ts b/packages/client/src/types/index.ts index 7025ff2d1..3d624fe50 100644 --- a/packages/client/src/types/index.ts +++ b/packages/client/src/types/index.ts @@ -51,14 +51,14 @@ export type { SelectOption, } from "./ui"; -// Player state types +// Player state types (canonical definitions in game/types/) export type { RawEquipmentSlot, RawEquipmentData, InventorySlotViewItem, NetworkEventName, -} from "./player"; -export { NetworkEvents } from "./player"; +} from "../game/types"; +export { NetworkEvents } from "../game/types"; // Type guards for runtime validation export { diff --git a/packages/client/src/types/player.ts b/packages/client/src/types/player.ts deleted file mode 100644 index 5be7ac570..000000000 --- a/packages/client/src/types/player.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Player State Types - * - * Shared type definitions for player state management. - * Used by InterfaceManager, MobileInterfaceManager, and related hooks. - * - * @packageDocumentation - */ - -import type { Item } from "@hyperscape/shared"; - -/** - * Raw equipment slot format from server network cache - */ -export type RawEquipmentSlot = { - item: Item | null; - itemId?: string; - quantity?: number; // For stackable items like arrows -} | null; - -/** - * Raw equipment data structure from server network cache - */ -export type RawEquipmentData = { - weapon?: RawEquipmentSlot; - shield?: RawEquipmentSlot; - helmet?: RawEquipmentSlot; - body?: RawEquipmentSlot; - legs?: RawEquipmentSlot; - boots?: RawEquipmentSlot; - gloves?: RawEquipmentSlot; - cape?: RawEquipmentSlot; - amulet?: RawEquipmentSlot; - ring?: RawEquipmentSlot; - arrows?: RawEquipmentSlot; -}; - -/** - * Inventory slot view item (simplified for UI display) - */ -export type InventorySlotViewItem = { - slot: number; - itemId: string; - quantity: number; -}; - -/** - * Network event names for UI interactions - */ -export const NetworkEvents = { - INVENTORY_UPDATE: "inventoryUpdate", - EQUIPMENT_UPDATE: "equipmentUpdate", - STATS_UPDATE: "statsUpdate", - BANK_OPEN: "bankOpen", - BANK_CLOSE: "bankClose", - STORE_OPEN: "storeOpen", - STORE_CLOSE: "storeClose", - DIALOGUE_START: "dialogueStart", - DIALOGUE_END: "dialogueEnd", - SMELTING_OPEN: "smeltingOpen", - SMELTING_CLOSE: "smeltingClose", - SMITHING_OPEN: "smithingOpen", - SMITHING_CLOSE: "smithingClose", - QUEST_START_SCREEN: "questStartScreen", - QUEST_COMPLETE_SCREEN: "questCompleteScreen", - XP_LAMP_USE: "xpLampUse", -} as const; - -export type NetworkEventName = - (typeof NetworkEvents)[keyof typeof NetworkEvents]; From 7e2e80040a224f61a0f9f521bc8f465a2d25c780 Mon Sep 17 00:00:00 2001 From: SYMBaiEX Date: Wed, 1 Apr 2026 01:04:18 -0500 Subject: [PATCH 003/266] refactor(client): consolidate formatGoldValue into currency utils Move OSRS-style gold formatting to the canonical location in game/systems/currency/currencyUtils.ts. Remove duplicate definitions from DuelPanel/utils.ts and TradePanel/utils.ts. Update all consumers (StakesScreen, ConfirmScreen, DuelResultModal, StakeGrid, TradePanel) to import from the central currency system. --- .../game/panels/DuelPanel/ConfirmScreen.tsx | 3 ++- .../game/panels/DuelPanel/DuelResultModal.tsx | 2 +- .../game/panels/DuelPanel/StakesScreen.tsx | 3 ++- .../panels/DuelPanel/components/StakeGrid.tsx | 2 +- .../client/src/game/panels/DuelPanel/utils.ts | 14 ----------- .../src/game/panels/TradePanel/TradePanel.tsx | 3 ++- .../src/game/panels/TradePanel/utils.ts | 25 ------------------- .../game/systems/currency/currencyUtils.ts | 21 ++++++++++++++++ .../client/src/game/systems/currency/index.ts | 1 + 9 files changed, 30 insertions(+), 44 deletions(-) diff --git a/packages/client/src/game/panels/DuelPanel/ConfirmScreen.tsx b/packages/client/src/game/panels/DuelPanel/ConfirmScreen.tsx index 1a5a67f96..cab9f84b2 100644 --- a/packages/client/src/game/panels/DuelPanel/ConfirmScreen.tsx +++ b/packages/client/src/game/panels/DuelPanel/ConfirmScreen.tsx @@ -21,7 +21,8 @@ import { DUEL_RULE_LABELS, EQUIPMENT_SLOT_LABELS, } from "@hyperscape/shared"; -import { formatQuantity, formatGoldValue, calculateTotalValue } from "./utils"; +import { formatGoldValue } from "../../systems/currency"; +import { formatQuantity, calculateTotalValue } from "./utils"; // ============================================================================ // Types diff --git a/packages/client/src/game/panels/DuelPanel/DuelResultModal.tsx b/packages/client/src/game/panels/DuelPanel/DuelResultModal.tsx index cc74768bc..ced051b3b 100644 --- a/packages/client/src/game/panels/DuelPanel/DuelResultModal.tsx +++ b/packages/client/src/game/panels/DuelPanel/DuelResultModal.tsx @@ -16,7 +16,7 @@ import { useCallback, useState, useEffect, type CSSProperties } from "react"; import { ModalWindow, useThemeStore } from "@/ui"; import { getPanelSurfaceStyle } from "@/ui/theme/themes"; import { getItem } from "@hyperscape/shared"; -import { formatGoldValue } from "./utils"; +import { formatGoldValue } from "../../systems/currency"; // ============================================================================ // Types diff --git a/packages/client/src/game/panels/DuelPanel/StakesScreen.tsx b/packages/client/src/game/panels/DuelPanel/StakesScreen.tsx index 93a019028..86445f8a5 100644 --- a/packages/client/src/game/panels/DuelPanel/StakesScreen.tsx +++ b/packages/client/src/game/panels/DuelPanel/StakesScreen.tsx @@ -15,7 +15,8 @@ import { useState, useCallback, useMemo, type CSSProperties } from "react"; import { useThemeStore, type Theme } from "@/ui"; import { getPanelSurfaceStyle } from "@/ui/theme/themes"; import type { StakedItem, SlotNumber } from "@hyperscape/shared"; -import { formatGoldValue, calculateTotalValue } from "./utils"; +import { formatGoldValue } from "../../systems/currency"; +import { calculateTotalValue } from "./utils"; import { StakeGrid, StakeInventoryPanel, StakeContextMenu } from "./components"; // ============================================================================ diff --git a/packages/client/src/game/panels/DuelPanel/components/StakeGrid.tsx b/packages/client/src/game/panels/DuelPanel/components/StakeGrid.tsx index 627d8575c..7dd5f9c60 100644 --- a/packages/client/src/game/panels/DuelPanel/components/StakeGrid.tsx +++ b/packages/client/src/game/panels/DuelPanel/components/StakeGrid.tsx @@ -8,7 +8,7 @@ import type { CSSProperties } from "react"; import type { Theme } from "@/ui"; import { getItem, type StakedItem } from "@hyperscape/shared"; import { SlotItem } from "./SlotItem"; -import { formatGoldValue } from "../utils"; +import { formatGoldValue } from "../../../systems/currency"; // ============================================================================ // Types diff --git a/packages/client/src/game/panels/DuelPanel/utils.ts b/packages/client/src/game/panels/DuelPanel/utils.ts index 1a7aa5e3b..7328857b8 100644 --- a/packages/client/src/game/panels/DuelPanel/utils.ts +++ b/packages/client/src/game/panels/DuelPanel/utils.ts @@ -17,20 +17,6 @@ export function formatQuantity(quantity: number): string { return quantity.toString(); } -/** - * Format gold value with K/M/B suffixes - */ -export function formatGoldValue(value: number): string { - if (value >= 1_000_000_000) { - return `${(value / 1_000_000_000).toFixed(1)}B`; - } else if (value >= 1_000_000) { - return `${(value / 1_000_000).toFixed(1)}M`; - } else if (value >= 1_000) { - return `${(value / 1_000).toFixed(1)}K`; - } - return value.toString(); -} - /** * Calculate total value of staked items */ diff --git a/packages/client/src/game/panels/TradePanel/TradePanel.tsx b/packages/client/src/game/panels/TradePanel/TradePanel.tsx index 285e37ee8..6bc65bccc 100644 --- a/packages/client/src/game/panels/TradePanel/TradePanel.tsx +++ b/packages/client/src/game/panels/TradePanel/TradePanel.tsx @@ -39,7 +39,8 @@ import type { QuantityPromptState, } from "./types"; import { TRADE_GRID_COLS, TRADE_SLOTS } from "./constants"; -import { formatGoldValue, getWealthDifferenceColor } from "./utils"; +import { formatGoldValue } from "../../systems/currency"; +import { getWealthDifferenceColor } from "./utils"; import { TradeSlot, InventoryMiniPanel } from "./components"; import { TradeContextMenu, QuantityPrompt } from "./modals"; import { useRemovedItemTracking } from "./hooks"; diff --git a/packages/client/src/game/panels/TradePanel/utils.ts b/packages/client/src/game/panels/TradePanel/utils.ts index d98d841c1..3d26602af 100644 --- a/packages/client/src/game/panels/TradePanel/utils.ts +++ b/packages/client/src/game/panels/TradePanel/utils.ts @@ -24,31 +24,6 @@ export function formatQuantity(qty: number): { text: string; color: string } { } } -// ============================================================================ -// Gold Value Formatting -// ============================================================================ - -/** - * Format gold value for wealth indicator display (OSRS-style) - * Shows K/M/B suffixes with decimal places - */ -export function formatGoldValue(value: number): string { - if (value < 1000) { - return value.toLocaleString(); - } else if (value < 1000000) { - const k = Math.floor(value / 1000); - const remainder = Math.floor((value % 1000) / 100); - return remainder > 0 ? `${k}.${remainder}K` : `${k}K`; - } else if (value < 1000000000) { - const m = Math.floor(value / 1000000); - const remainder = Math.floor((value % 1000000) / 100000); - return remainder > 0 ? `${m}.${remainder}M` : `${m}M`; - } else { - const b = Math.floor(value / 1000000000); - return `${b}B`; - } -} - /** * Get color for wealth difference indicator * Green = gaining value, Red = losing value, White = neutral diff --git a/packages/client/src/game/systems/currency/currencyUtils.ts b/packages/client/src/game/systems/currency/currencyUtils.ts index e3bc48dfc..11f4af08f 100644 --- a/packages/client/src/game/systems/currency/currencyUtils.ts +++ b/packages/client/src/game/systems/currency/currencyUtils.ts @@ -357,6 +357,27 @@ export function formatBreakdown(breakdown: { return parts.join(" "); } +/** + * Format gold value for OSRS-style wealth display (K/M/B suffixes). + * Used by DuelPanel and TradePanel for stake/trade value indicators. + */ +export function formatGoldValue(value: number): string { + if (value < 1000) { + return value.toLocaleString(); + } else if (value < 1000000) { + const k = Math.floor(value / 1000); + const remainder = Math.floor((value % 1000) / 100); + return remainder > 0 ? `${k}.${remainder}K` : `${k}K`; + } else if (value < 1000000000) { + const m = Math.floor(value / 1000000); + const remainder = Math.floor((value % 1000000) / 100000); + return remainder > 0 ? `${m}.${remainder}M` : `${m}M`; + } else { + const b = Math.floor(value / 1000000000); + return `${b}B`; + } +} + /** Change indicator for value changes */ export type ChangeIndicator = "gain" | "loss" | "neutral"; diff --git a/packages/client/src/game/systems/currency/index.ts b/packages/client/src/game/systems/currency/index.ts index 9ee3292c8..6af7b89b6 100644 --- a/packages/client/src/game/systems/currency/index.ts +++ b/packages/client/src/game/systems/currency/index.ts @@ -26,6 +26,7 @@ export { getChangeIndicator, getChangeColor, formatChange, + formatGoldValue, } from "./currencyUtils"; // Hooks From be9c0600d432d7193f87e8cd95066ef8f8f96c32 Mon Sep 17 00:00:00 2001 From: SYMBaiEX Date: Wed, 1 Apr 2026 01:06:44 -0500 Subject: [PATCH 004/266] refactor(client): move SettingsPanel.tsx into SettingsPanel/ directory Resolve naming conflict where SettingsPanel.tsx (1,547 LOC) sat as a sibling file next to the SettingsPanel/ directory containing its sub-components. Move the file into the directory and update index.ts to export from the local file instead of the parent. Fix relative import paths that now need an additional level of traversal. --- .../panels/{ => SettingsPanel}/SettingsPanel.tsx | 14 +++++++------- .../client/src/game/panels/SettingsPanel/index.ts | 4 +--- 2 files changed, 8 insertions(+), 10 deletions(-) rename packages/client/src/game/panels/{ => SettingsPanel}/SettingsPanel.tsx (99%) diff --git a/packages/client/src/game/panels/SettingsPanel.tsx b/packages/client/src/game/panels/SettingsPanel/SettingsPanel.tsx similarity index 99% rename from packages/client/src/game/panels/SettingsPanel.tsx rename to packages/client/src/game/panels/SettingsPanel/SettingsPanel.tsx index abf3c5960..74387b1d0 100644 --- a/packages/client/src/game/panels/SettingsPanel.tsx +++ b/packages/client/src/game/panels/SettingsPanel/SettingsPanel.tsx @@ -14,8 +14,8 @@ import { type LucideIcon, } from "lucide-react"; import { isTouch } from "@hyperscape/shared"; -import type { ClientWorld } from "../../types"; -import { useFullscreen } from "../../hooks/useFullscreen"; +import type { ClientWorld } from "../../../types"; +import { useFullscreen } from "../../../hooks/useFullscreen"; import { ToggleSwitch, Slider } from "@/ui"; import { getInteractiveTileStyle, @@ -23,7 +23,7 @@ import { getPanelInsetStyle, getPanelSurfaceStyle, } from "@/ui/theme/themes"; -import { NAME_SANITIZE_REGEX } from "../../utils/validation"; +import { NAME_SANITIZE_REGEX } from "../../../utils/validation"; import { useComplexityStore, useComplexityMode, @@ -35,16 +35,16 @@ import { type StatusBarsConfig, STATUSBAR_CONFIG_CHANGED_EVENT, STATUSBAR_CONFIG_STORAGE_KEY, -} from "../hud/StatusBars"; -import { privyAuthManager } from "../../auth/PrivyAuthManager"; -import { useSolanaWallet } from "../../hooks/useSolanaWallet"; +} from "../../hud/StatusBars"; +import { privyAuthManager } from "../../../auth/PrivyAuthManager"; +import { useSolanaWallet } from "../../../hooks/useSolanaWallet"; import { type GraphicsQuality, QUALITY_PRESETS, QUALITY_LEVELS, getQualityDisplayName, detectRecommendedQuality, -} from "../../types/embeddedConfig"; +} from "../../../types/embeddedConfig"; /** Minimum name length required for submission */ const MIN_NAME_LENGTH = 1; diff --git a/packages/client/src/game/panels/SettingsPanel/index.ts b/packages/client/src/game/panels/SettingsPanel/index.ts index 8662af99f..f978b0a66 100644 --- a/packages/client/src/game/panels/SettingsPanel/index.ts +++ b/packages/client/src/game/panels/SettingsPanel/index.ts @@ -6,8 +6,6 @@ * @packageDocumentation */ -// Re-export the main panel from the original location -// This allows gradual migration to the new structure -export { SettingsPanel } from "../SettingsPanel"; +export { SettingsPanel } from "./SettingsPanel"; export * from "./components"; export * from "./constants"; From 3275d175d2a7b3e06b763c4ee20aa9893de9c5a7 Mon Sep 17 00:00:00 2001 From: SYMBaiEX Date: Wed, 1 Apr 2026 01:08:06 -0500 Subject: [PATCH 005/266] refactor(client): remove dead ItemData type, verify manifest consumption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove unused ItemData interface from game/systems/types.ts — it was never imported by any consumer. The client correctly uses getItem() from @hyperscape/shared (152 usages across 58 files) to read from the ITEMS Map populated by DataManager from server manifests. No client-side item definitions found that bypass the manifest system. --- packages/client/src/game/systems/types.ts | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/packages/client/src/game/systems/types.ts b/packages/client/src/game/systems/types.ts index 59b1be468..b2dbe78cd 100644 --- a/packages/client/src/game/systems/types.ts +++ b/packages/client/src/game/systems/types.ts @@ -21,23 +21,3 @@ export interface Size { /** Rectangle combining position and size */ export interface Rect extends Point, Size {} - -// ============================================================================ -// Item Types -// ============================================================================ - -/** Basic item data structure */ -export interface ItemData { - /** Unique item identifier */ - id: string; - /** Display name */ - name: string; - /** Item type/category */ - type: string; - /** Stack quantity (1 for non-stackable) */ - quantity?: number; - /** Item icon URL or identifier */ - icon?: string; - /** Item description */ - description?: string; -} From 8c4422c6e81c9433d822ff46629259857f54547b Mon Sep 17 00:00:00 2001 From: SYMBaiEX Date: Wed, 1 Apr 2026 01:10:58 -0500 Subject: [PATCH 006/266] refactor(client): remove unused useQuestLog hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete useQuestLog.ts (~440 LOC) — it was never imported by any consumer. QuestsPanel manages quest data directly from server events, and the quest selection store (questStore.ts) handles cross-panel quest state. The quest status sync (useQuestStatusSync) correctly bridges server events into the Zustand store for minimap quest pips. --- .../client/src/game/systems/quest/index.ts | 7 - .../src/game/systems/quest/useQuestLog.ts | 439 ------------------ 2 files changed, 446 deletions(-) delete mode 100644 packages/client/src/game/systems/quest/useQuestLog.ts diff --git a/packages/client/src/game/systems/quest/index.ts b/packages/client/src/game/systems/quest/index.ts index c64019028..0260a14de 100644 --- a/packages/client/src/game/systems/quest/index.ts +++ b/packages/client/src/game/systems/quest/index.ts @@ -41,13 +41,6 @@ export { OBJECTIVE_TYPE_CONFIG, } from "./questUtils"; -// Quest log hook -export { - useQuestLog, - type UseQuestLogOptions, - type UseQuestLogResult, -} from "./useQuestLog"; - // Quest tracker hook export { useQuestTracker, diff --git a/packages/client/src/game/systems/quest/useQuestLog.ts b/packages/client/src/game/systems/quest/useQuestLog.ts deleted file mode 100644 index 98d58d4ea..000000000 --- a/packages/client/src/game/systems/quest/useQuestLog.ts +++ /dev/null @@ -1,439 +0,0 @@ -/** - * Quest Log Hook - * - * Hook for managing quest state including filtering, sorting, - * and CRUD operations on quest data. - * - * @packageDocumentation - */ - -import { useState, useCallback, useMemo } from "react"; -import { - type Quest, - type QuestState, - type QuestCategory, - type QuestSortOption, - type SortDirection, - type QuestFilterOptions, - filterQuests, - sortQuests, - groupQuestsByCategory, - calculateQuestProgress, - areAllObjectivesComplete, -} from "./questUtils"; - -// ============================================================================ -// Types -// ============================================================================ - -/** Configuration for useQuestLog hook */ -export interface UseQuestLogOptions { - /** Initial quests */ - initialQuests?: Quest[]; - /** Default sort option */ - defaultSort?: QuestSortOption; - /** Default sort direction */ - defaultSortDirection?: SortDirection; - /** Default filter options */ - defaultFilters?: QuestFilterOptions; - /** Callback when a quest state changes */ - onQuestStateChange?: (quest: Quest, newState: QuestState) => void; - /** Callback when quest is pinned/unpinned */ - onQuestPinChange?: (quest: Quest, pinned: boolean) => void; - /** Callback when objective progress changes */ - onObjectiveProgress?: ( - questId: string, - objectiveId: string, - current: number, - target: number, - ) => void; -} - -/** Return value from useQuestLog hook */ -export interface UseQuestLogResult { - /** All quests (unfiltered) */ - quests: Quest[]; - /** Filtered and sorted quests */ - filteredQuests: Quest[]; - /** Quests grouped by category */ - questsByCategory: Record; - /** Currently active quests count */ - activeCount: number; - /** Total available quests count */ - availableCount: number; - /** Completed quests count */ - completedCount: number; - - // State management - /** Add a new quest */ - addQuest: (quest: Quest) => void; - /** Remove a quest by ID */ - removeQuest: (questId: string) => void; - /** Update a quest */ - updateQuest: (questId: string, updates: Partial) => void; - /** Set quests (replace all) */ - setQuests: (quests: Quest[]) => void; - - // Quest actions - /** Accept/start a quest */ - acceptQuest: (questId: string) => void; - /** Complete a quest */ - completeQuest: (questId: string) => void; - /** Fail a quest */ - failQuest: (questId: string) => void; - /** Toggle quest pin status */ - togglePinQuest: (questId: string) => void; - /** Update objective progress */ - updateObjectiveProgress: ( - questId: string, - objectiveId: string, - current: number, - ) => void; - - // Filtering - /** Current filter options */ - filters: QuestFilterOptions; - /** Set filter options */ - setFilters: (filters: QuestFilterOptions) => void; - /** Update filter options (merge) */ - updateFilters: (filters: Partial) => void; - /** Clear all filters */ - clearFilters: () => void; - /** Search text */ - searchText: string; - /** Set search text */ - setSearchText: (text: string) => void; - - // Sorting - /** Current sort option */ - sortBy: QuestSortOption; - /** Current sort direction */ - sortDirection: SortDirection; - /** Set sort option */ - setSortBy: (option: QuestSortOption) => void; - /** Set sort direction */ - setSortDirection: (direction: SortDirection) => void; - /** Toggle sort direction */ - toggleSortDirection: () => void; - - // Helpers - /** Get a quest by ID */ - getQuest: (questId: string) => Quest | undefined; - /** Get progress for a quest */ - getQuestProgress: (questId: string) => number; - /** Check if quest can be completed */ - canCompleteQuest: (questId: string) => boolean; - /** Get pinned quests */ - pinnedQuests: Quest[]; -} - -// ============================================================================ -// Hook Implementation -// ============================================================================ - -/** - * Quest log management hook - * - * Provides complete quest management with filtering, sorting, - * and state management capabilities. - * - * @example - * ```tsx - * function QuestLogPanel() { - * const { - * filteredQuests, - * searchText, - * setSearchText, - * sortBy, - * setSortBy, - * acceptQuest, - * completeQuest, - * } = useQuestLog({ - * initialQuests: myQuests, - * defaultSort: 'level', - * }); - * - * return ( - *
- * setSearchText(e.target.value)} - * placeholder="Search quests..." - * /> - * {filteredQuests.map(quest => ( - * acceptQuest(quest.id)} - * /> - * ))} - *
- * ); - * } - * ``` - */ -export function useQuestLog( - options: UseQuestLogOptions = {}, -): UseQuestLogResult { - const { - initialQuests = [], - defaultSort = "category", - defaultSortDirection = "asc", - defaultFilters = {}, - onQuestStateChange, - onQuestPinChange, - onObjectiveProgress, - } = options; - - // Core state - const [quests, setQuests] = useState(initialQuests); - const [filters, setFilters] = useState(defaultFilters); - const [searchText, setSearchText] = useState(""); - const [sortBy, setSortBy] = useState(defaultSort); - const [sortDirection, setSortDirection] = - useState(defaultSortDirection); - - // Combined filters with search - const combinedFilters = useMemo( - (): QuestFilterOptions => ({ - ...filters, - searchText: searchText || undefined, - }), - [filters, searchText], - ); - - // Filtered and sorted quests - const filteredQuests = useMemo(() => { - const filtered = filterQuests(quests, combinedFilters); - return sortQuests(filtered, sortBy, sortDirection); - }, [quests, combinedFilters, sortBy, sortDirection]); - - // Grouped by category - const questsByCategory = useMemo( - () => groupQuestsByCategory(filteredQuests), - [filteredQuests], - ); - - // Counts - const activeCount = useMemo( - () => quests.filter((q) => q.state === "active").length, - [quests], - ); - - const availableCount = useMemo( - () => quests.filter((q) => q.state === "available").length, - [quests], - ); - - const completedCount = useMemo( - () => quests.filter((q) => q.state === "completed").length, - [quests], - ); - - // Pinned quests - const pinnedQuests = useMemo( - () => - quests - .filter((q) => q.pinned && q.state === "active") - .sort((a, b) => { - // Pinned quests sorted by progress (most complete first) - return calculateQuestProgress(b) - calculateQuestProgress(a); - }), - [quests], - ); - - // CRUD operations - const addQuest = useCallback((quest: Quest) => { - setQuests((prev) => [...prev, quest]); - }, []); - - const removeQuest = useCallback((questId: string) => { - setQuests((prev) => prev.filter((q) => q.id !== questId)); - }, []); - - const updateQuest = useCallback( - (questId: string, updates: Partial) => { - setQuests((prev) => - prev.map((q) => (q.id === questId ? { ...q, ...updates } : q)), - ); - }, - [], - ); - - // Quest state actions - const updateQuestState = useCallback( - ( - questId: string, - newState: QuestState, - additionalUpdates?: Partial, - ) => { - setQuests((prev) => - prev.map((q) => { - if (q.id !== questId) return q; - - const updatedQuest = { - ...q, - state: newState, - ...additionalUpdates, - }; - - // Trigger callback - onQuestStateChange?.(updatedQuest, newState); - - return updatedQuest; - }), - ); - }, - [onQuestStateChange], - ); - - const acceptQuest = useCallback( - (questId: string) => { - updateQuestState(questId, "active", { - startedAt: Date.now(), - }); - }, - [updateQuestState], - ); - - const completeQuest = useCallback( - (questId: string) => { - updateQuestState(questId, "completed", { - completedAt: Date.now(), - pinned: false, - }); - }, - [updateQuestState], - ); - - const failQuest = useCallback( - (questId: string) => { - updateQuestState(questId, "failed", { - completedAt: Date.now(), - pinned: false, - }); - }, - [updateQuestState], - ); - - const togglePinQuest = useCallback( - (questId: string) => { - setQuests((prev) => - prev.map((q) => { - if (q.id !== questId) return q; - - const newPinned = !q.pinned; - onQuestPinChange?.(q, newPinned); - - return { ...q, pinned: newPinned }; - }), - ); - }, - [onQuestPinChange], - ); - - const updateObjectiveProgress = useCallback( - (questId: string, objectiveId: string, current: number) => { - setQuests((prev) => - prev.map((q) => { - if (q.id !== questId) return q; - - const objectives = q.objectives.map((obj) => { - if (obj.id !== objectiveId) return obj; - - const newCurrent = Math.min(current, obj.target); - onObjectiveProgress?.(questId, objectiveId, newCurrent, obj.target); - - return { ...obj, current: newCurrent }; - }); - - return { ...q, objectives }; - }), - ); - }, - [onObjectiveProgress], - ); - - // Filter operations - const updateFilters = useCallback((updates: Partial) => { - setFilters((prev) => ({ ...prev, ...updates })); - }, []); - - const clearFilters = useCallback(() => { - setFilters({}); - setSearchText(""); - }, []); - - // Sort operations - const toggleSortDirection = useCallback(() => { - setSortDirection((prev) => (prev === "asc" ? "desc" : "asc")); - }, []); - - // Helpers - const getQuest = useCallback( - (questId: string): Quest | undefined => { - return quests.find((q) => q.id === questId); - }, - [quests], - ); - - const getQuestProgress = useCallback( - (questId: string): number => { - const quest = quests.find((q) => q.id === questId); - return quest ? calculateQuestProgress(quest) : 0; - }, - [quests], - ); - - const canCompleteQuest = useCallback( - (questId: string): boolean => { - const quest = quests.find((q) => q.id === questId); - return quest ? areAllObjectivesComplete(quest) : false; - }, - [quests], - ); - - return { - // Data - quests, - filteredQuests, - questsByCategory, - activeCount, - availableCount, - completedCount, - pinnedQuests, - - // CRUD - addQuest, - removeQuest, - updateQuest, - setQuests, - - // Quest actions - acceptQuest, - completeQuest, - failQuest, - togglePinQuest, - updateObjectiveProgress, - - // Filtering - filters, - setFilters, - updateFilters, - clearFilters, - searchText, - setSearchText, - - // Sorting - sortBy, - sortDirection, - setSortBy, - setSortDirection, - toggleSortDirection, - - // Helpers - getQuest, - getQuestProgress, - canCompleteQuest, - }; -} From ce5d94ad7413afad71e8c8ca6264ba04239bb586 Mon Sep 17 00:00:00 2001 From: SYMBaiEX Date: Wed, 1 Apr 2026 03:06:49 -0500 Subject: [PATCH 007/266] refactor(client): remove dead Fields, CurvePane, CurvePreview components Delete ~950 LOC of unreferenced field editor components. Fields.tsx defined 10 React field components never imported by any consumer. CurvePane.tsx and CurvePreview.tsx were only imported by Fields.tsx. --- packages/client/src/game/CurvePane.tsx | 170 ---- packages/client/src/game/CurvePreview.tsx | 103 --- packages/client/src/game/Fields.tsx | 956 --------------------- packages/client/src/ui/components/index.ts | 1 - 4 files changed, 1230 deletions(-) delete mode 100644 packages/client/src/game/CurvePane.tsx delete mode 100644 packages/client/src/game/CurvePreview.tsx delete mode 100644 packages/client/src/game/Fields.tsx diff --git a/packages/client/src/game/CurvePane.tsx b/packages/client/src/game/CurvePane.tsx deleted file mode 100644 index 8d7c28fed..000000000 --- a/packages/client/src/game/CurvePane.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import React from "react"; -import { useEffect, useRef } from "react"; -import { Curve } from "@hyperscape/shared"; -import { usePane } from "../hooks/usePane"; - -interface CurvePaneProps { - curve: Curve; - xLabel: string; - xRange?: [number, number]; - yLabel: string; - yMin: number; - yMax: number; - onCommit: () => void; - onCancel: () => void; -} - -export function CurvePane({ - curve, - xLabel, - xRange, - yLabel, - yMin, - yMax, - onCommit, - onCancel, -}: CurvePaneProps) { - const paneRef = useRef(null); - const headRef = useRef(null); - const canvasRef = useRef(null); - const editorRef = useRef<{ curve: Curve; canvas: HTMLCanvasElement } | null>( - null, - ); - - usePane( - "curve", - paneRef as React.RefObject, - headRef as React.RefObject, - ); - - useEffect(() => { - if (!canvasRef.current) return; - - // Initialize curve editor - const canvas = canvasRef.current; - const ctx = canvas.getContext("2d"); - if (!ctx) return; - - // Set canvas size - const rect = canvas.getBoundingClientRect(); - canvas.width = rect.width * window.devicePixelRatio; - canvas.height = rect.height * window.devicePixelRatio; - ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - - // Create editor - const editor = { - curve, - canvas, - }; - editorRef.current = editor; - - // Render curve - renderCurve(ctx, curve, rect.width, rect.height, xRange, yMin, yMax); - - return () => { - editorRef.current = null; - }; - }, [curve, xRange, yMin, yMax]); - - const renderCurve = ( - ctx: CanvasRenderingContext2D, - curve: Curve, - width: number, - height: number, - xRange: [number, number] | undefined, - yMin: number, - yMax: number, - ) => { - ctx.clearRect(0, 0, width, height); - - // Draw grid - ctx.strokeStyle = "rgba(255, 255, 255, 0.1)"; - ctx.lineWidth = 1; - - for (let i = 0; i <= 10; i++) { - const x = (i / 10) * width; - ctx.beginPath(); - ctx.moveTo(x, 0); - ctx.lineTo(x, height); - ctx.stroke(); - - const y = (i / 10) * height; - ctx.beginPath(); - ctx.moveTo(0, y); - ctx.lineTo(width, y); - ctx.stroke(); - } - - // Draw curve - if (curve) { - ctx.strokeStyle = "#00a7ff"; - ctx.lineWidth = 2; - ctx.beginPath(); - - const range = xRange || [0, 1]; - for (let x = 0; x < width; x++) { - const t = (x / width) * (range[1] - range[0]) + range[0]; - const y = curve.evaluate(t); - const normalizedY = 1 - (y - yMin) / (yMax - yMin); - const pixelY = normalizedY * height; - - if (x === 0) { - ctx.moveTo(x, pixelY); - } else { - ctx.lineTo(x, pixelY); - } - } - ctx.stroke(); - } - }; - - return ( -
-
-
- Curve Editor -
-
- × -
-
-
- -
- - {xLabel} ({xRange ? `${xRange[0]} - ${xRange[1]}` : "0 - 1"}) - - - {yLabel} ({yMin} - {yMax}) - -
-
-
- - -
-
- ); -} diff --git a/packages/client/src/game/CurvePreview.tsx b/packages/client/src/game/CurvePreview.tsx deleted file mode 100644 index b67d73073..000000000 --- a/packages/client/src/game/CurvePreview.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { useEffect, useRef } from "react"; -import type { Curve } from "@hyperscape/shared"; - -interface CurvePreviewProps { - curve: Curve; - width?: number; - height?: number; - xRange?: [number, number]; - yMin?: number; - yMax?: number; -} - -export function CurvePreview({ - curve, - width = 200, - height = 100, - xRange = [0, 1], - yMin = 0, - yMax = 1, -}: CurvePreviewProps) { - const canvasRef = useRef(null); - const divRef = useRef(null); - - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) return; - - const ctx = canvas.getContext("2d"); - if (!ctx) return; - - // Set canvas size to match display size - canvas.width = width * window.devicePixelRatio; - canvas.height = height * window.devicePixelRatio; - ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - - // Clear canvas - ctx.clearRect(0, 0, width, height); - - // Draw background - ctx.fillStyle = "#1a1a1a"; - ctx.fillRect(0, 0, width, height); - - // Draw grid - ctx.strokeStyle = "rgba(255, 255, 255, 0.1)"; - ctx.lineWidth = 1; - - // Vertical grid lines - for (let i = 0; i <= 4; i++) { - const x = (i / 4) * width; - ctx.beginPath(); - ctx.moveTo(x, 0); - ctx.lineTo(x, height); - ctx.stroke(); - } - - // Horizontal grid lines - for (let i = 0; i <= 4; i++) { - const y = (i / 4) * height; - ctx.beginPath(); - ctx.moveTo(0, y); - ctx.lineTo(width, y); - ctx.stroke(); - } - - // Draw curve - if (curve) { - ctx.strokeStyle = "#00a7ff"; - ctx.lineWidth = 2; - ctx.beginPath(); - - let first = true; - for (let x = 0; x < width; x++) { - const t = (x / width) * (xRange[1] - xRange[0]) + xRange[0]; - const y = curve.evaluate(t); - - // Normalize y value to canvas coordinates - const normalizedY = 1 - (y - yMin) / (yMax - yMin); - const pixelY = Math.max(0, Math.min(height, normalizedY * height)); - - if (first) { - ctx.moveTo(x, pixelY); - first = false; - } else { - ctx.lineTo(x, pixelY); - } - } - ctx.stroke(); - } - }, [curve, width, height, xRange, yMin, yMax]); - - return ( -
- -
- ); -} diff --git a/packages/client/src/game/Fields.tsx b/packages/client/src/game/Fields.tsx deleted file mode 100644 index 4c9ad7199..000000000 --- a/packages/client/src/game/Fields.tsx +++ /dev/null @@ -1,956 +0,0 @@ -import { useContext, useEffect, useMemo, useRef, useState } from "react"; -import { Curve, downloadFile, hashFile } from "@hyperscape/shared"; -import type { ClientWorld } from "../types"; -import { CurvePane } from "./CurvePane"; -import { CurvePreview } from "./CurvePreview"; -import { HintContext, Portal } from "@/ui"; -import { useUpdate } from "../hooks/useUpdate"; - -interface LoadingFile { - type: string; - name: string; - url: string; -} - -interface SwitchOption { - label: string; - value: unknown; -} - -interface FieldTextProps { - label: string; - hint?: string; - placeholder?: string; - value: string; - onChange: (value: string) => void; -} - -interface FieldTextareaProps { - label: string; - hint?: string; - placeholder?: string; - value: string; - onChange: (value: string) => void; -} - -interface FieldSwitchProps { - label: string; - hint?: string; - options: SwitchOption[]; - value: unknown; - onChange: (value: unknown) => void; -} - -interface FieldToggleProps { - label: string; - hint?: string; - trueLabel?: string; - falseLabel?: string; - value: boolean; - onChange: (value: boolean) => void; -} - -interface FieldRangeProps { - label: string; - hint?: string; - min?: number; - max?: number; - step?: number; - instant?: boolean; - value: number; - onChange: (value: number) => void; -} - -interface FieldFileProps { - world: ClientWorld; - label: string; - hint?: string; - kind: keyof typeof fileKinds; - value: { type?: string; name?: string; url?: string } | null; - onChange: (value: LoadingFile | null) => void; -} - -interface FieldNumberProps { - label: string; - hint?: string; - dp?: number; - min?: number; - max?: number; - step?: number; - bigStep?: number; - value: number; - onChange: (value: number) => void; -} - -interface FieldVec3Props { - label: string; - hint?: string; - dp?: number; - min?: number; - max?: number; - step?: number; - bigStep?: number; - value: [number, number, number]; - onChange: (value: [number, number, number]) => void; -} - -interface FieldCurveProps { - label: string; - hint?: string; - x: string; - xRange?: number; - y: string; - yMin: number; - yMax: number; - value: string; - onChange: (value: string) => void; -} - -interface FieldBtnProps { - label: string; - note?: string; - hint?: string; - nav?: boolean; - onClick: () => void; -} - -export function FieldText({ - label, - hint, - placeholder, - value, - onChange, -}: FieldTextProps) { - const hintContext = useContext(HintContext); - if (!hintContext) { - throw new Error("HintContext not found"); - } - const setHint = hintContext.setHint; - return ( - - ); -} - -export function FieldTextarea({ - label, - hint, - placeholder, - value, - onChange, -}: FieldTextareaProps) { - const hintContext = useContext(HintContext); - if (!hintContext) { - throw new Error("HintContext not found"); - } - const setHint = hintContext.setHint; - const textareaRef = useRef(null); - useEffect(() => { - const textarea = textareaRef.current; - if (!textarea) return; - function update() { - if (!textarea) return; - textarea.style.height = "auto"; - textarea.style.height = textarea.scrollHeight + "px"; - } - update(); - textarea.addEventListener("input", update); - return () => { - textarea.removeEventListener("input", update); - }; - }, []); - return ( -