From 19808555a9c0fd029745c23f3cec45e94b3e4388 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Feb 2026 16:12:30 +0000 Subject: [PATCH 1/3] fix: address code quality issues across networking and workers - Make STUN servers configurable via NEXT_PUBLIC_ICE_SERVERS env var with additional fallback servers (stun2, stun3.l.google.com) - Fix silent error swallowing in usePublicLobbies.ts by logging non-rate-limit errors before returning empty array - Replace console statements in workers with debug logger: - pathfinding.worker.ts: Use debugPathfinding - vision.worker.ts: Use debugVision (via terrain category) - ai-decisions.worker.ts: Use debugAI - All workers now support setDebugSettings message for live toggle - Remove legacy faction names (terran/protoss/zerg) from FACTION_COLORS in rendering.config.ts - only dominion and neutral are used - Fix stim_pack cooldown mismatch: TS now matches JSON (cooldown: 10) - Add documentation noting JSON is authoritative for ability data https://claude.ai/code/session_01EpqD3m816v9JtwW5DcVbNt --- src/data/abilities/abilities.ts | 7 ++- src/data/rendering.config.ts | 17 +------ src/hooks/useMultiplayer.ts | 26 +++++++++- src/hooks/usePublicLobbies.ts | 8 ++- src/workers/ai-decisions.worker.ts | 19 +++++-- src/workers/pathfinding.worker.ts | 80 +++++++++++++++--------------- src/workers/vision.worker.ts | 27 ++++++++-- 7 files changed, 113 insertions(+), 71 deletions(-) diff --git a/src/data/abilities/abilities.ts b/src/data/abilities/abilities.ts index 383019d0..7eb6039a 100644 --- a/src/data/abilities/abilities.ts +++ b/src/data/abilities/abilities.ts @@ -96,6 +96,11 @@ export interface AbilityDataDefinition { } // ==================== ABILITY DEFINITIONS ==================== +// NOTE: These TypeScript definitions are for reference and type checking only. +// The authoritative ability data is loaded from JSON files at runtime: +// public/data/factions/{faction}/abilities.json +// Any gameplay-affecting values should be updated in the JSON files. +// This TS file may be removed in a future refactor. export const ABILITY_DEFINITIONS: Record = { // === INFANTRY ABILITIES === @@ -106,7 +111,7 @@ export const ABILITY_DEFINITIONS: Record = { description: 'Increases attack speed and movement speed at the cost of health.', targetType: 'instant', healthCost: 10, - cooldown: 0, // Can use any time + cooldown: 10, // Matches JSON authority: public/data/factions/dominion/abilities.json duration: 11, effects: [ { type: 'buff', value: 1.5, duration: 11 }, // 50% speed boost diff --git a/src/data/rendering.config.ts b/src/data/rendering.config.ts index 13f3b077..510ccaa5 100644 --- a/src/data/rendering.config.ts +++ b/src/data/rendering.config.ts @@ -339,26 +339,11 @@ export const RENDER_ORDER = { /** Faction-specific projectile colors */ export const FACTION_COLORS = { - terran: { - primary: 0xffaa00, // Orange-yellow tracer - secondary: 0xff6600, // Orange trail - glow: 0xffdd44, // Bright yellow glow - }, dominion: { - primary: 0xffaa00, // Orange-yellow tracer (same as terran for now) + primary: 0xffaa00, // Orange-yellow tracer secondary: 0xff6600, // Orange trail glow: 0xffdd44, // Bright yellow glow }, - protoss: { - primary: 0x4488ff, // Blue energy - secondary: 0x8844ff, // Purple trail - glow: 0x66aaff, // Cyan glow - }, - zerg: { - primary: 0x88ff44, // Acid green - secondary: 0x44aa00, // Dark green trail - glow: 0xaaff66, // Bright green glow - }, neutral: { primary: 0xffaa00, secondary: 0xff6600, diff --git a/src/hooks/useMultiplayer.ts b/src/hooks/useMultiplayer.ts index 0f87b728..4b735331 100644 --- a/src/hooks/useMultiplayer.ts +++ b/src/hooks/useMultiplayer.ts @@ -67,12 +67,34 @@ export interface GameStartPayload { startTime: number; } -// STUN servers -const ICE_SERVERS: RTCIceServer[] = [ +// Default STUN servers (configurable via NEXT_PUBLIC_ICE_SERVERS env var) +const DEFAULT_ICE_SERVERS: RTCIceServer[] = [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }, + { urls: 'stun:stun2.l.google.com:19302' }, + { urls: 'stun:stun3.l.google.com:19302' }, ]; +function getIceServers(): RTCIceServer[] { + // Next.js inlines NEXT_PUBLIC_* env vars at build time + const envServers = ( + typeof process !== 'undefined' ? process.env?.NEXT_PUBLIC_ICE_SERVERS : undefined + ) as string | undefined; + if (envServers) { + try { + const parsed = JSON.parse(envServers) as RTCIceServer[]; + if (Array.isArray(parsed) && parsed.length > 0) { + return parsed; + } + } catch { + debugNetworking.warn('[Lobby] Invalid NEXT_PUBLIC_ICE_SERVERS format, using defaults'); + } + } + return DEFAULT_ICE_SERVERS; +} + +const ICE_SERVERS = getIceServers(); + export type LobbyStatus = | 'disabled' // Nostr not active (single-player mode) | 'initializing' diff --git a/src/hooks/usePublicLobbies.ts b/src/hooks/usePublicLobbies.ts index cbc37dfd..5c0b01f2 100644 --- a/src/hooks/usePublicLobbies.ts +++ b/src/hooks/usePublicLobbies.ts @@ -227,7 +227,13 @@ export async function publishPublicLobby( // Publish to relays const results = await Promise.allSettled( relays.map(r => - Promise.all(pool.publish([r], event)).catch(() => []) + Promise.all(pool.publish([r], event)).catch((err) => { + // Log non-rate-limit errors for debugging + if (!err?.message?.includes('rate-limit')) { + debugNetworking.warn(`[LobbyBrowser] Relay publish failed for ${r}:`, err); + } + return []; + }) ) ); diff --git a/src/workers/ai-decisions.worker.ts b/src/workers/ai-decisions.worker.ts index e41de077..8194414b 100644 --- a/src/workers/ai-decisions.worker.ts +++ b/src/workers/ai-decisions.worker.ts @@ -12,9 +12,8 @@ */ import { distance, SeededRandom } from '@/utils/math'; - -// Debug flag for worker logging (workers can't access UI store) -const _DEBUG = false; +import { debugAI, setWorkerDebugSettings } from '@/utils/debugLogger'; +import type { DebugSettings } from '@/store/uiStore'; // Unit priority for focus fire (higher = more important to kill) const DEFAULT_UNIT_PRIORITY: Record = { @@ -105,7 +104,12 @@ interface EvaluateMicroMessage { seed: number; } -type WorkerMessage = InitMessage | EvaluateMicroMessage; +interface SetDebugSettingsMessage { + type: 'setDebugSettings'; + settings: DebugSettings; +} + +type WorkerMessage = InitMessage | EvaluateMicroMessage | SetDebugSettingsMessage; // Decision types returned to main thread interface MicroDecision { @@ -147,7 +151,7 @@ function init(workerConfig: AIWorkerConfig): boolean { initialized = true; return true; } catch (error) { - console.error('[AIWorker] Init failed:', error); + debugAI.error('[AIWorker] Init failed:', error); return false; } } @@ -567,6 +571,11 @@ self.onmessage = (event: MessageEvent) => { }); break; } + + case 'setDebugSettings': { + setWorkerDebugSettings(message.settings); + break; + } } }; diff --git a/src/workers/pathfinding.worker.ts b/src/workers/pathfinding.worker.ts index 7dad286a..6d92a3b0 100644 --- a/src/workers/pathfinding.worker.ts +++ b/src/workers/pathfinding.worker.ts @@ -32,6 +32,8 @@ import { import { generateTileCache, generateSoloNavMesh } from '@recast-navigation/generators'; import { distance } from '@/utils/math'; import { NAVMESH_CONFIG, SOLO_NAVMESH_CONFIG } from '@/data/pathfinding.config'; +import { debugPathfinding, setWorkerDebugSettings } from '@/utils/debugLogger'; +import type { DebugSettings } from '@/store/uiStore'; const WALKABLE_DISTANCE_TOLERANCE = Math.max( NAVMESH_CONFIG.walkableRadius * 1.5, @@ -39,9 +41,6 @@ const WALKABLE_DISTANCE_TOLERANCE = Math.max( ); const WALKABLE_HEIGHT_TOLERANCE = NAVMESH_CONFIG.walkableClimb * 2; -// Debug flag for worker logging (workers can't access UI store) -const DEBUG = false; - // Message types interface InitMessage { type: 'init'; @@ -130,6 +129,11 @@ interface IsWaterWalkableMessage { y: number; } +interface SetDebugSettingsMessage { + type: 'setDebugSettings'; + settings: DebugSettings; +} + type WorkerMessage = | InitMessage | LoadNavMeshMessage @@ -141,7 +145,8 @@ type WorkerMessage = | FindNearestPointMessage | LoadWaterNavMeshMessage | FindWaterPathMessage - | IsWaterWalkableMessage; + | IsWaterWalkableMessage + | SetDebugSettingsMessage; // Ground navmesh state let navMesh: NavMesh | null = null; @@ -174,7 +179,7 @@ async function initialize(): Promise { initialized = true; return true; } catch (error) { - console.error('[PathfindingWorker] WASM init failed:', error); + debugPathfinding.error('[PathfindingWorker] WASM init failed:', error); return false; } } @@ -184,14 +189,14 @@ async function initialize(): Promise { */ function loadNavMesh(data: Uint8Array): boolean { if (!initialized) { - console.error('[PathfindingWorker] WASM not initialized'); + debugPathfinding.error('[PathfindingWorker] WASM not initialized'); return false; } try { const result = importNavMesh(data); if (!result.navMesh) { - console.error('[PathfindingWorker] Failed to import navmesh'); + debugPathfinding.error('[PathfindingWorker] Failed to import navmesh'); return false; } @@ -211,7 +216,7 @@ function loadNavMesh(data: Uint8Array): boolean { return true; } catch (error) { - console.error('[PathfindingWorker] Error loading navmesh:', error); + debugPathfinding.error('[PathfindingWorker] Error loading navmesh:', error); return false; } } @@ -227,19 +232,16 @@ function loadNavMeshFromGeometry( heightMapH?: number ): boolean { if (!initialized) { - console.error('[PathfindingWorker] WASM not initialized'); + debugPathfinding.error('[PathfindingWorker] WASM not initialized'); return false; } - if (DEBUG) { - // eslint-disable-next-line no-console -- Debug logging gated by DEBUG flag - console.log('[PathfindingWorker] Generating navmesh from geometry:', { - vertices: positions.length / 3, - triangles: indices.length / 3, - positionsType: positions.constructor.name, - indicesType: indices.constructor.name, - }); - } + debugPathfinding.log('[PathfindingWorker] Generating navmesh from geometry:', { + vertices: positions.length / 3, + triangles: indices.length / 3, + positionsType: positions.constructor.name, + indicesType: indices.constructor.name, + }); if (heightMapData && heightMapW && heightMapH) { heightMap = heightMapData; @@ -264,17 +266,13 @@ function loadNavMeshFromGeometry( heightMapWidth = heightMapW; heightMapHeight = heightMapH; } - // eslint-disable-next-line no-console -- Debug logging gated by DEBUG flag - if (DEBUG) console.log('[PathfindingWorker] TileCache navmesh generated successfully'); + debugPathfinding.log('[PathfindingWorker] TileCache navmesh generated successfully'); return true; } // TileCache failed - try solo navmesh fallback - if (DEBUG) { - console.warn('[PathfindingWorker] TileCache generation failed:', (result as { error?: string }).error); - // eslint-disable-next-line no-console -- Debug logging gated by DEBUG flag - console.log('[PathfindingWorker] Trying solo navmesh fallback...'); - } + debugPathfinding.warn('[PathfindingWorker] TileCache generation failed:', (result as { error?: string }).error); + debugPathfinding.log('[PathfindingWorker] Trying solo navmesh fallback...'); // Solo navmesh fallback (no dynamic obstacles but more robust) const soloResult = generateSoloNavMesh(positions, indices, SOLO_NAVMESH_CONFIG); @@ -288,15 +286,14 @@ function loadNavMeshFromGeometry( heightMapWidth = heightMapW; heightMapHeight = heightMapH; } - // eslint-disable-next-line no-console -- Debug logging gated by DEBUG flag - if (DEBUG) console.log('[PathfindingWorker] Solo navmesh generated successfully'); + debugPathfinding.log('[PathfindingWorker] Solo navmesh generated successfully'); return true; } - console.error('[PathfindingWorker] Solo navmesh also failed:', (soloResult as { error?: string }).error); + debugPathfinding.error('[PathfindingWorker] Solo navmesh also failed:', (soloResult as { error?: string }).error); return false; } catch (error) { - console.error('[PathfindingWorker] Error generating navmesh:', error); + debugPathfinding.error('[PathfindingWorker] Error generating navmesh:', error); return false; } } @@ -552,17 +549,14 @@ function removeObstacle(entityId: number): boolean { */ function loadWaterNavMesh(positions: Float32Array, indices: Uint32Array): boolean { if (!initialized) { - console.error('[PathfindingWorker] WASM not initialized'); + debugPathfinding.error('[PathfindingWorker] WASM not initialized'); return false; } - if (DEBUG) { - // eslint-disable-next-line no-console -- Debug logging gated by DEBUG flag - console.log('[PathfindingWorker] Generating water navmesh:', { - vertices: positions.length / 3, - triangles: indices.length / 3, - }); - } + debugPathfinding.log('[PathfindingWorker] Generating water navmesh:', { + vertices: positions.length / 3, + triangles: indices.length / 3, + }); try { // Use solo navmesh for water (no dynamic obstacles needed on water) @@ -571,15 +565,14 @@ function loadWaterNavMesh(positions: Float32Array, indices: Uint32Array): boolea if (result.success && result.navMesh) { waterNavMesh = result.navMesh; waterNavMeshQuery = new NavMeshQuery(waterNavMesh); - // eslint-disable-next-line no-console -- Debug logging gated by DEBUG flag - if (DEBUG) console.log('[PathfindingWorker] Water navmesh generated successfully'); + debugPathfinding.log('[PathfindingWorker] Water navmesh generated successfully'); return true; } - console.error('[PathfindingWorker] Water navmesh generation failed'); + debugPathfinding.error('[PathfindingWorker] Water navmesh generation failed'); return false; } catch (error) { - console.error('[PathfindingWorker] Error generating water navmesh:', error); + debugPathfinding.error('[PathfindingWorker] Error generating water navmesh:', error); return false; } } @@ -771,6 +764,11 @@ self.onmessage = async (event: MessageEvent) => { }); break; } + + case 'setDebugSettings': { + setWorkerDebugSettings(message.settings); + break; + } } }; diff --git a/src/workers/vision.worker.ts b/src/workers/vision.worker.ts index 022fd6da..1502603c 100644 --- a/src/workers/vision.worker.ts +++ b/src/workers/vision.worker.ts @@ -11,8 +11,15 @@ * Output: { type: 'visionResult', playerVisions: Map, version } */ -// Debug flag for worker logging (workers can't access UI store) -const _DEBUG = false; +import { debugLog, setWorkerDebugSettings } from '@/utils/debugLogger'; +import type { DebugSettings } from '@/store/uiStore'; + +// Vision debug uses the general 'terrain' category since there's no specific vision category +const debugVision = { + log: (...args: unknown[]) => debugLog.log('terrain', ...args), + warn: (...args: unknown[]) => debugLog.warn('terrain', ...args), + error: (...args: unknown[]) => debugLog.error('terrain', ...args), +}; // Vision states (encoded as numbers for efficient TypedArray transfer) const VISION_UNEXPLORED = 0; @@ -62,7 +69,12 @@ interface UpdateVisionMessage { version: number; } -type WorkerMessage = InitMessage | UpdateVisionMessage; +interface SetDebugSettingsMessage { + type: 'setDebugSettings'; + settings: DebugSettings; +} + +type WorkerMessage = InitMessage | UpdateVisionMessage | SetDebugSettingsMessage; // State let gridWidth = 0; @@ -91,7 +103,7 @@ function init(mapWidth: number, mapHeight: number, newCellSize: number): boolean initialized = true; return true; } catch (error) { - console.error('[VisionWorker] Init failed:', error); + debugVision.error('[VisionWorker] Init failed:', error); return false; } } @@ -235,7 +247,7 @@ self.onmessage = (event: MessageEvent) => { case 'updateVision': { if (!initialized) { - console.error('[VisionWorker] Not initialized'); + debugVision.error('[VisionWorker] Not initialized'); return; } @@ -265,6 +277,11 @@ self.onmessage = (event: MessageEvent) => { }); break; } + + case 'setDebugSettings': { + setWorkerDebugSettings(message.settings); + break; + } } }; From e541e26b6235a8e66071a1da0ed1dca06cc16475 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Feb 2026 16:20:55 +0000 Subject: [PATCH 2/3] fix: move STUN config to JSON, remove duplicate TS ability definitions - Move STUN/ICE server config from env vars to public/data/networking.json - Remove ABILITY_DEFINITIONS constant from abilities.ts (JSON is authority) - Remove AbilityRegistry class and related getAbility/getUnitAbilities - Remove unused getAbilityDefinitions() from DefinitionRegistry - Keep type definitions (AbilityDataDefinition, etc.) for type checking - Keep utility functions (canUseAbility, calculateAbilityDamage) https://claude.ai/code/session_01EpqD3m816v9JtwW5DcVbNt --- public/data/networking.json | 8 + src/data/abilities/abilities.ts | 574 +------------------ src/data/index.ts | 4 - src/engine/definitions/DefinitionRegistry.ts | 12 - src/hooks/useMultiplayer.ts | 43 +- 5 files changed, 38 insertions(+), 603 deletions(-) create mode 100644 public/data/networking.json diff --git a/public/data/networking.json b/public/data/networking.json new file mode 100644 index 00000000..6c178e59 --- /dev/null +++ b/public/data/networking.json @@ -0,0 +1,8 @@ +{ + "iceServers": [ + { "urls": "stun:stun.l.google.com:19302" }, + { "urls": "stun:stun1.l.google.com:19302" }, + { "urls": "stun:stun2.l.google.com:19302" }, + { "urls": "stun:stun3.l.google.com:19302" } + ] +} diff --git a/src/data/abilities/abilities.ts b/src/data/abilities/abilities.ts index 7eb6039a..5e6e72cc 100644 --- a/src/data/abilities/abilities.ts +++ b/src/data/abilities/abilities.ts @@ -1,16 +1,9 @@ /** - * Ability System - Data-Driven Ability Definitions + * Ability System - Type Definitions * - * This file defines all unit and building abilities in a data-driven way. - * The ability system supports various targeting modes, effects, and cooldowns. - * - * Ability Types: - * - instant: Immediate effect, no targeting (e.g., stim pack) - * - targeted: Requires target selection (e.g., snipe) - * - ground: Targets a ground location (e.g., nuke) - * - passive: Always active, no activation (e.g., cloak detection) - * - toggle: Can be turned on/off (e.g., cloak) - * - autocast: Can be set to auto-use (e.g., heal) + * This file defines types for the ability system. + * Actual ability data is loaded from JSON files at runtime: + * public/data/factions/{faction}/abilities.json */ // ==================== ABILITY TYPES ==================== @@ -95,559 +88,8 @@ export interface AbilityDataDefinition { visualEffect?: string; } -// ==================== ABILITY DEFINITIONS ==================== -// NOTE: These TypeScript definitions are for reference and type checking only. -// The authoritative ability data is loaded from JSON files at runtime: -// public/data/factions/{faction}/abilities.json -// Any gameplay-affecting values should be updated in the JSON files. -// This TS file may be removed in a future refactor. - -export const ABILITY_DEFINITIONS: Record = { - // === INFANTRY ABILITIES === - - stim_pack: { - id: 'stim_pack', - name: 'Stim Pack', - description: 'Increases attack speed and movement speed at the cost of health.', - targetType: 'instant', - healthCost: 10, - cooldown: 10, // Matches JSON authority: public/data/factions/dominion/abilities.json - duration: 11, - effects: [ - { type: 'buff', value: 1.5, duration: 11 }, // 50% speed boost - ], - requiresResearch: ['combat_stim'], - interruptsMovement: false, - }, - - concussive_shells: { - id: 'concussive_shells', - name: 'Concussive Shells', - description: 'Attacks slow enemy movement speed.', - targetType: 'passive', - effects: [ - { type: 'debuff', value: 0.5, duration: 2 }, // 50% slow for 2s - ], - requiresResearch: ['concussive_shells'], - }, - - snipe: { - id: 'snipe', - name: 'Snipe', - description: 'Deals heavy damage to a biological target.', - targetType: 'targeted', - range: 10, - energyCost: 50, - cooldown: 1.43, - effects: [ - { - type: 'damage', - value: 170, - targetFilter: { requiresBiological: true, includeEnemies: true }, - }, - ], - channeled: true, - interruptsMovement: true, - }, - - emp_round: { - id: 'emp_round', - name: 'EMP Round', - description: 'Drains energy and shields in an area.', - targetType: 'ground', - range: 10, - radius: 1.5, - energyCost: 75, - cooldown: 0.71, - effects: [ - { type: 'debuff', value: 100, radius: 1.5 }, // Drain 100 energy - ], - interruptsMovement: false, - }, - - cloak: { - id: 'cloak', - name: 'Cloak', - description: 'Become invisible to enemies without detection.', - targetType: 'toggle', - energyCost: 1, // Per second - effects: [ - { type: 'cloak' }, - ], - requiresResearch: ['stealth_systems'], - interruptsMovement: false, - }, - - nuke: { - id: 'nuke', - name: 'Nuclear Strike', - description: 'Call down a devastating nuclear strike.', - targetType: 'ground', - range: 12, - radius: 8, - energyCost: 75, - cooldown: 0, - castTime: 14, // 14 second warning - effects: [ - { type: 'damage', value: 300, radius: 8 }, - ], - requiresResearch: ['nuke'], - channeled: true, - interruptsMovement: true, - visualEffect: 'nuke_warning', - soundEffect: 'nuke_launch', - }, - - jet_pack: { - id: 'jet_pack', - name: 'Jet Pack', - description: 'Jump to a target location.', - targetType: 'ground', - range: 8, - cooldown: 10, - effects: [ - { type: 'teleport' }, - ], - interruptsMovement: true, - }, - - grenade: { - id: 'grenade', - name: 'Grenade', - description: 'Throw a grenade that deals splash damage.', - targetType: 'ground', - range: 6, - radius: 2, - cooldown: 8, - effects: [ - { type: 'damage', value: 30, radius: 2 }, - ], - interruptsMovement: false, - }, - - // === VEHICLE ABILITIES === - - transform_inferno: { - id: 'transform_inferno', - name: 'Transform: Inferno', - description: 'Transform into Inferno mode for increased close-range damage.', - targetType: 'instant', - cooldown: 2.25, - effects: [ - { type: 'transform' }, - ], - interruptsMovement: true, - }, - - bombardment_mode: { - id: 'bombardment_mode', - name: 'Siege Mode', - description: 'Deploy into siege mode for long-range artillery.', - targetType: 'instant', - cooldown: 2, - effects: [ - { type: 'transform' }, - ], - requiresResearch: ['bombardment_systems'], - interruptsMovement: true, - }, - - high_impact_payload: { - id: 'high_impact_payload', - name: 'High Impact Payload', - description: 'Fire a devastating shot that deals massive damage.', - targetType: 'targeted', - range: 9, - cooldown: 10, - effects: [ - { type: 'damage', value: 100 }, - ], - interruptsMovement: false, - }, - - // === SHIP ABILITIES === - - transform_assault: { - id: 'transform_assault', - name: 'Transform: Assault', - description: 'Land and transform into ground assault mode.', - targetType: 'instant', - cooldown: 2.25, - effects: [ - { type: 'transform' }, - ], - interruptsMovement: true, - }, - - power_cannon: { - id: 'power_cannon', - name: 'Power Cannon', - description: 'Fire the main cannon for devastating damage.', - targetType: 'targeted', - range: 10, - energyCost: 100, - cooldown: 5, - effects: [ - { type: 'damage', value: 200 }, - ], - interruptsMovement: false, - }, - - warp_jump: { - id: 'warp_jump', - name: 'Warp Jump', - description: 'Teleport to a visible location.', - targetType: 'ground', - range: 30, - energyCost: 125, - cooldown: 60, - effects: [ - { type: 'teleport' }, - ], - interruptsMovement: true, - }, - - // === SUPPORT ABILITIES === - - heal: { - id: 'heal', - name: 'Heal', - description: 'Restore health to a biological unit.', - targetType: 'autocast', - range: 4, - energyCost: 1, // Per health point - effects: [ - { - type: 'heal', - value: 12.6, // Per second - targetFilter: { requiresBiological: true, includeAllies: true, includeSelf: false }, - }, - ], - canAutocast: true, - interruptsMovement: false, - }, - - load: { - id: 'load', - name: 'Load', - description: 'Load units into the transport.', - targetType: 'targeted', - range: 5, - effects: [ - { type: 'custom', customHandler: 'loadUnit' }, - ], - interruptsMovement: false, - }, - - unload: { - id: 'unload', - name: 'Unload', - description: 'Unload all units from the transport.', - targetType: 'ground', - range: 0, - effects: [ - { type: 'custom', customHandler: 'unloadUnits' }, - ], - interruptsMovement: true, - }, - - afterburners: { - id: 'afterburners', - name: 'Afterburners', - description: 'Temporarily increase movement speed.', - targetType: 'instant', - cooldown: 20, - duration: 5, - effects: [ - { type: 'buff', value: 2.0, duration: 5 }, // Double speed - ], - interruptsMovement: false, - }, - - // === DETECTOR ABILITIES === - - auto_turret: { - id: 'auto_turret', - name: 'Auto-Turret', - description: 'Deploy an automated turret.', - targetType: 'ground', - range: 7, - energyCost: 50, - cooldown: 0, - effects: [ - { type: 'spawn', value: 1 }, // Spawn turret entity - ], - interruptsMovement: false, - }, - - interference_matrix: { - id: 'interference_matrix', - name: 'Interference Matrix', - description: 'Disable a mechanical unit.', - targetType: 'targeted', - range: 9, - energyCost: 75, - cooldown: 0, - duration: 8, - effects: [ - { - type: 'debuff', - duration: 8, - targetFilter: { requiresMechanical: true, includeEnemies: true }, - }, - ], - interruptsMovement: false, - }, - - anti_armor_missile: { - id: 'anti_armor_missile', - name: 'Anti-Armor Missile', - description: 'Fire a missile that reduces target armor.', - targetType: 'targeted', - range: 10, - energyCost: 75, - cooldown: 0, - duration: 21, - effects: [ - { type: 'debuff', value: 3, duration: 21 }, // -3 armor - ], - interruptsMovement: false, - }, - - // === BUILDING ABILITIES === - - sensor_sweep: { - id: 'sensor_sweep', - name: 'Sensor Sweep', - description: 'Reveal an area of the map.', - targetType: 'ground', - range: 999, // Global - radius: 12, - energyCost: 50, - cooldown: 0, - duration: 12, - effects: [ - { type: 'detect', radius: 12, duration: 12 }, - ], - interruptsMovement: false, - }, - - supply_drop: { - id: 'supply_drop', - name: 'Supply Drop', - description: 'Call in additional supply containers.', - targetType: 'instant', - cooldown: 0, - resourceCost: { minerals: 400 }, - effects: [ - { type: 'resource' }, // Grants supply - ], - requiresResearch: ['supply_drop'], - interruptsMovement: false, - }, - - // === NAVAL ABILITIES === - - boost: { - id: 'boost', - name: 'Boost', - description: 'Engage emergency thrusters for a burst of speed.', - targetType: 'instant', - cooldown: 15, - duration: 4, - effects: [ - { type: 'buff', value: 1.75, duration: 4 }, // 75% speed boost - ], - interruptsMovement: false, - soundEffect: 'ability_boost', - }, - - flak_barrage: { - id: 'flak_barrage', - name: 'Flak Barrage', - description: 'Release a barrage of flak rounds, dealing damage to all air units in the area.', - targetType: 'instant', - energyCost: 50, - cooldown: 10, - radius: 6, - effects: [ - { - type: 'damage', - value: 40, - radius: 6, - targetFilter: { requiresAir: true, includeEnemies: true }, - }, - ], - interruptsMovement: false, - visualEffect: 'flak_explosion', - soundEffect: 'ability_flak', - }, - - submerge: { - id: 'submerge', - name: 'Submerge', - description: 'Dive beneath the surface, becoming invisible to units without detection.', - targetType: 'toggle', - cooldown: 3, // Brief cooldown to prevent rapid toggle abuse - effects: [ - { type: 'cloak' }, - { type: 'custom', customHandler: 'toggleSubmerge' }, - ], - interruptsMovement: false, - soundEffect: 'submarine_dive', - }, - - depth_charge_defense: { - id: 'depth_charge_defense', - name: 'Depth Charge', - description: 'Launch depth charges that deal heavy damage to naval units.', - targetType: 'targeted', - range: 5, - energyCost: 25, - cooldown: 8, - effects: [ - { - type: 'damage', - value: 75, - targetFilter: { includeEnemies: true }, - }, - ], - interruptsMovement: false, - visualEffect: 'depth_charge_explosion', - soundEffect: 'ability_depth_charge', - }, - - beach_assault: { - id: 'beach_assault', - name: 'Beach Assault', - description: 'Prepare for amphibious landing, granting loaded units a combat bonus upon disembark.', - targetType: 'instant', - cooldown: 30, - duration: 15, - effects: [ - { type: 'buff', value: 1.25, duration: 15 }, // 25% damage boost to unloaded units - ], - interruptsMovement: false, - soundEffect: 'ability_beach_assault', - }, - - amphibious_mode: { - id: 'amphibious_mode', - name: 'Amphibious Mode', - description: 'Toggle between water and land movement. Slower on land but can traverse both terrains.', - targetType: 'toggle', - cooldown: 2, - effects: [ - { type: 'custom', customHandler: 'toggleAmphibious' }, - ], - interruptsMovement: true, - soundEffect: 'vehicle_transform', - }, - - shore_bombardment: { - id: 'shore_bombardment', - name: 'Shore Bombardment', - description: 'Fire a salvo of heavy shells at a ground target location.', - targetType: 'ground', - range: 14, - radius: 3, - energyCost: 75, - cooldown: 12, - castTime: 1.5, // Brief aiming delay - effects: [ - { type: 'damage', value: 100, radius: 3 }, - ], - interruptsMovement: false, - visualEffect: 'artillery_strike', - soundEffect: 'ability_bombardment', - }, - - yamato_cannon: { - id: 'yamato_cannon', - name: 'Yamato Cannon', - description: 'Fire the main battery, dealing devastating damage to a single target.', - targetType: 'targeted', - range: 10, - energyCost: 150, - cooldown: 60, - castTime: 2, // Charge-up time - effects: [ - { type: 'damage', value: 300 }, - ], - channeled: true, - interruptsMovement: true, - visualEffect: 'yamato_beam', - soundEffect: 'ability_yamato', - }, -}; - -// ==================== ABILITY REGISTRY ==================== - -/** - * Ability Registry class for managing and querying abilities. - */ -class AbilityRegistryClass { - private abilities: Map = new Map(); - private initialized: boolean = false; - - public initialize(): void { - if (this.initialized) return; - - for (const [id, def] of Object.entries(ABILITY_DEFINITIONS)) { - this.abilities.set(id, def); - } - - this.initialized = true; - } - - public get(abilityId: string): AbilityDataDefinition | undefined { - this.initialize(); - return this.abilities.get(abilityId); - } - - public getAll(): Map { - this.initialize(); - return this.abilities; - } - - public register(ability: AbilityDataDefinition): void { - this.abilities.set(ability.id, ability); - } - - public getByActivationMode(mode: AbilityActivationMode): AbilityDataDefinition[] { - this.initialize(); - return Array.from(this.abilities.values()).filter(a => a.targetType === mode); - } - - public getAutocastAbilities(): AbilityDataDefinition[] { - this.initialize(); - return Array.from(this.abilities.values()).filter(a => a.canAutocast); - } - - public getAbilitiesRequiringResearch(researchId: string): AbilityDataDefinition[] { - this.initialize(); - return Array.from(this.abilities.values()).filter( - a => a.requiresResearch?.includes(researchId) - ); - } - - public clear(): void { - this.abilities.clear(); - this.initialized = false; - } -} - -export const AbilityRegistry = new AbilityRegistryClass(); - // ==================== HELPER FUNCTIONS ==================== -/** - * Get an ability data definition by ID. - */ -export function getAbility(abilityId: string): AbilityDataDefinition | undefined { - return AbilityRegistry.get(abilityId); -} - /** * Check if a unit has a specific ability. */ @@ -655,14 +97,6 @@ export function unitHasAbility(unitAbilities: string[] | undefined, abilityId: s return unitAbilities?.includes(abilityId) ?? false; } -/** - * Get all abilities for a unit. - */ -export function getUnitAbilities(abilityIds: string[] | undefined): AbilityDataDefinition[] { - if (!abilityIds) return []; - return abilityIds.map(id => AbilityRegistry.get(id)).filter((a): a is AbilityDataDefinition => a !== undefined); -} - /** * Check if an ability can be used (energy, cooldown, etc.). */ diff --git a/src/data/index.ts b/src/data/index.ts index 2e380657..6a9e3b96 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -83,11 +83,7 @@ export { // ==================== ABILITIES ==================== export { - ABILITY_DEFINITIONS, - AbilityRegistry, - getAbility, unitHasAbility, - getUnitAbilities, canUseAbility, calculateAbilityDamage, type AbilityDataDefinition, diff --git a/src/engine/definitions/DefinitionRegistry.ts b/src/engine/definitions/DefinitionRegistry.ts index 4be93e27..e58e5308 100644 --- a/src/engine/definitions/DefinitionRegistry.ts +++ b/src/engine/definitions/DefinitionRegistry.ts @@ -587,18 +587,6 @@ class DefinitionRegistryClass { }; } - /** - * Get ability definitions. - * @see @/data/abilities/abilities.ts - */ - public getAbilityDefinitions() { - const abilities = require('@/data/abilities/abilities'); - return { - abilities: abilities.ABILITY_DEFINITIONS, - registry: abilities.AbilityRegistry, - getAbility: abilities.getAbility, - }; - } } // Export singleton instance diff --git a/src/hooks/useMultiplayer.ts b/src/hooks/useMultiplayer.ts index 4b735331..412828ef 100644 --- a/src/hooks/useMultiplayer.ts +++ b/src/hooks/useMultiplayer.ts @@ -67,7 +67,7 @@ export interface GameStartPayload { startTime: number; } -// Default STUN servers (configurable via NEXT_PUBLIC_ICE_SERVERS env var) +// Default STUN servers (loaded from public/data/networking.json at runtime) const DEFAULT_ICE_SERVERS: RTCIceServer[] = [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }, @@ -75,25 +75,31 @@ const DEFAULT_ICE_SERVERS: RTCIceServer[] = [ { urls: 'stun:stun3.l.google.com:19302' }, ]; -function getIceServers(): RTCIceServer[] { - // Next.js inlines NEXT_PUBLIC_* env vars at build time - const envServers = ( - typeof process !== 'undefined' ? process.env?.NEXT_PUBLIC_ICE_SERVERS : undefined - ) as string | undefined; - if (envServers) { - try { - const parsed = JSON.parse(envServers) as RTCIceServer[]; - if (Array.isArray(parsed) && parsed.length > 0) { - return parsed; +let cachedIceServers: RTCIceServer[] | null = null; + +async function loadIceServers(): Promise { + if (cachedIceServers) return cachedIceServers; + + try { + const response = await fetch('/data/networking.json'); + if (response.ok) { + const config = await response.json(); + if (Array.isArray(config.iceServers) && config.iceServers.length > 0) { + cachedIceServers = config.iceServers; + return cachedIceServers; } - } catch { - debugNetworking.warn('[Lobby] Invalid NEXT_PUBLIC_ICE_SERVERS format, using defaults'); } + } catch { + debugNetworking.warn('[Lobby] Failed to load networking.json, using defaults'); } - return DEFAULT_ICE_SERVERS; + + cachedIceServers = DEFAULT_ICE_SERVERS; + return cachedIceServers; } -const ICE_SERVERS = getIceServers(); +function getIceServersSync(): RTCIceServer[] { + return cachedIceServers || DEFAULT_ICE_SERVERS; +} export type LobbyStatus = | 'disabled' // Nostr not active (single-player mode) @@ -308,6 +314,9 @@ export function useLobby(options: UseLobbyOptions = {}): UseLobbyReturn { const initLobby = async () => { try { + // Preload ICE servers from config + await loadIceServers(); + // Generate keys and code const secretKey = generateSecretKey(); const pubkey = getPublicKey(secretKey); @@ -412,7 +421,7 @@ export function useLobby(options: UseLobbyOptions = {}): UseLobbyReturn { } // Create peer connection for this guest - const pc = new RTCPeerConnection({ iceServers: ICE_SERVERS }); + const pc = new RTCPeerConnection({ iceServers: getIceServersSync() }); // Create data channel const channel = pc.createDataChannel('game', { ordered: true }); @@ -635,7 +644,7 @@ export function useLobby(options: UseLobbyOptions = {}): UseLobbyReturn { const hostPubkey = event.pubkey; // Create peer connection - const pc = new RTCPeerConnection({ iceServers: ICE_SERVERS }); + const pc = new RTCPeerConnection({ iceServers: getIceServersSync() }); pcRef.current = pc; // Handle incoming data channel From 7b5a47c31c4abac780c0f117b038d71709948c15 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Feb 2026 16:24:15 +0000 Subject: [PATCH 3/3] fix: remove all SC2 race references, use 'dominion' as default faction - Replace 'terran' fallback with 'dominion' in CombatSystem.ts - Replace 'terran' fallback with 'dominion' in BattleEffectsRenderer.ts - Remove SC2 references from comments in AssetManager.ts - Fix type error in useMultiplayer.ts ICE server loading https://claude.ai/code/session_01EpqD3m816v9JtwW5DcVbNt --- src/assets/AssetManager.ts | 6 +++--- src/engine/systems/CombatSystem.ts | 8 ++++---- src/hooks/useMultiplayer.ts | 7 ++++--- src/rendering/effects/BattleEffectsRenderer.ts | 4 ++-- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/assets/AssetManager.ts b/src/assets/AssetManager.ts index faccdbfb..5866c260 100644 --- a/src/assets/AssetManager.ts +++ b/src/assets/AssetManager.ts @@ -703,7 +703,7 @@ function cloneModel(original: THREE.Object3D, assetId: string): THREE.Object3D { // Standard materials library export const Materials = { - // Dominion (Terran-like) faction + // Dominion faction dominion: { metal: new THREE.MeshStandardMaterial({ color: 0x6080a0, @@ -735,7 +735,7 @@ export const Materials = { }), }, - // Synthesis (Protoss-like) faction + // Synthesis faction (placeholder) synthesis: { crystal: new THREE.MeshStandardMaterial({ color: 0x4080ff, @@ -758,7 +758,7 @@ export const Materials = { }), }, - // Swarm (Zerg-like) faction + // Swarm faction (placeholder) swarm: { chitin: new THREE.MeshStandardMaterial({ color: 0x4a3040, diff --git a/src/engine/systems/CombatSystem.ts b/src/engine/systems/CombatSystem.ts index 37c85927..2b79361a 100644 --- a/src/engine/systems/CombatSystem.ts +++ b/src/engine/systems/CombatSystem.ts @@ -31,7 +31,7 @@ const attackEventPayload = { targetPlayerId: undefined as string | undefined, attackerIsFlying: false, targetIsFlying: false, - attackerFaction: 'terran' as string, + attackerFaction: 'dominion' as string, }; const splashEventPayload = { @@ -1100,7 +1100,7 @@ export class CombatSystem extends System { attackEventPayload.targetPlayerId = targetSelectable?.playerId; attackEventPayload.attackerIsFlying = attacker.isFlying; attackEventPayload.targetIsFlying = targetIsFlying; - attackEventPayload.attackerFaction = attacker.faction || 'terran'; + attackEventPayload.attackerFaction = attacker.faction || 'dominion'; this.game.eventBus.emit('combat:attack', attackEventPayload); // Emit damage:dealt for Phaser damage number system @@ -1144,7 +1144,7 @@ export class CombatSystem extends System { const projectileEntity = this.game.projectileSystem.spawnProjectile({ sourceEntityId: attackerId, sourcePlayerId: attackerSelectable?.playerId ?? '', - sourceFaction: attacker.faction || 'terran', + sourceFaction: attacker.faction || 'dominion', startX: attackerTransform.x, startY: attackerTransform.y, startZ: attackerTransform.z + startZ, @@ -1180,7 +1180,7 @@ export class CombatSystem extends System { attackEventPayload.targetPlayerId = targetSelectable?.playerId; attackEventPayload.attackerIsFlying = attacker.isFlying; attackEventPayload.targetIsFlying = targetIsFlying; - attackEventPayload.attackerFaction = attacker.faction || 'terran'; + attackEventPayload.attackerFaction = attacker.faction || 'dominion'; this.game.eventBus.emit('combat:attack', attackEventPayload); } diff --git a/src/hooks/useMultiplayer.ts b/src/hooks/useMultiplayer.ts index 412828ef..18459c6d 100644 --- a/src/hooks/useMultiplayer.ts +++ b/src/hooks/useMultiplayer.ts @@ -85,8 +85,9 @@ async function loadIceServers(): Promise { if (response.ok) { const config = await response.json(); if (Array.isArray(config.iceServers) && config.iceServers.length > 0) { - cachedIceServers = config.iceServers; - return cachedIceServers; + const servers = config.iceServers as RTCIceServer[]; + cachedIceServers = servers; + return servers; } } } catch { @@ -94,7 +95,7 @@ async function loadIceServers(): Promise { } cachedIceServers = DEFAULT_ICE_SERVERS; - return cachedIceServers; + return DEFAULT_ICE_SERVERS; } function getIceServersSync(): RTCIceServer[] { diff --git a/src/rendering/effects/BattleEffectsRenderer.ts b/src/rendering/effects/BattleEffectsRenderer.ts index c83c34fc..793dac9a 100644 --- a/src/rendering/effects/BattleEffectsRenderer.ts +++ b/src/rendering/effects/BattleEffectsRenderer.ts @@ -718,7 +718,7 @@ export class BattleEffectsRenderer { data.targetPos.y ); - const faction = (data.attackerFaction as keyof typeof FACTION_COLORS) || 'terran'; + const faction = (data.attackerFaction as keyof typeof FACTION_COLORS) || 'dominion'; // Only create instant weapon visuals (lasers, melee) when damage > 0 // Projectile-based attacks (damage === 0) are handled by projectile:spawned event @@ -758,7 +758,7 @@ export class BattleEffectsRenderer { data.targetPos.y ); - const faction = (data.faction as keyof typeof FACTION_COLORS) || 'terran'; + const faction = (data.faction as keyof typeof FACTION_COLORS) || 'dominion'; // Calculate duration based on distance and a base speed const distance = startPos.distanceTo(endPos);