import type { GameWebSocket, ServerMessage } from '../network/websocket'; import type { GameEngine } from './engine'; import type { HeroMovePayload, PositionSyncPayload, RouteAssignedPayload, CombatStartPayload, AttackPayload, CombatEndPayload, HeroDiedPayload, HeroRevivedPayload, BuffAppliedPayload, TownEnterPayload, TownNPCVisitPayload, AdventureLogLinePayload, NPCEncounterPayload, NPCEncounterEndPayload, LevelUpPayload, EquipmentChangePayload, PotionCollectedPayload, QuestProgressPayload, QuestCompletePayload, QuestAvailablePayload, ServerErrorPayload, EnemyState, LootDrop, MerchantLootPayload, } from './types'; import { EnemyType, Rarity } from './types'; // ---- Callback types for UI layer (App.tsx) ---- export interface WSHandlerCallbacks { onCombatEnd?: (payload: CombatEndPayload) => void; onHeroDied?: (payload: HeroDiedPayload) => void; onHeroRevived?: (payload: HeroRevivedPayload) => void; onBuffApplied?: (payload: BuffAppliedPayload) => void; onTownEnter?: (payload: TownEnterPayload) => void; onTownNPCVisit?: (payload: TownNPCVisitPayload) => void; onAdventureLogLine?: (payload: AdventureLogLinePayload) => void; onTownExit?: () => void; onNPCEncounter?: (payload: NPCEncounterPayload) => void; onNPCEncounterEnd?: (payload: NPCEncounterEndPayload) => void; onLevelUp?: (payload: LevelUpPayload) => void; onEquipmentChange?: (payload: EquipmentChangePayload) => void; onPotionCollected?: (payload: PotionCollectedPayload) => void; onQuestProgress?: (payload: QuestProgressPayload) => void; onQuestComplete?: (payload: QuestCompletePayload) => void; onQuestAvailable?: (payload: QuestAvailablePayload) => void; onError?: (payload: ServerErrorPayload) => void; onHeroStateReceived?: (hero: Record) => void; onMerchantLoot?: (payload: MerchantLootPayload) => void; } /** * Bridges WebSocket messages to engine state updates and UI callbacks. * * This is a stateless dispatcher: it parses typed envelopes from the * server and calls the appropriate engine method or UI callback. * No game logic lives here -- just routing. */ export function wireWSHandler( ws: GameWebSocket, engine: GameEngine, callbacks: WSHandlerCallbacks, ): void { // ---- Server -> Client: Movement ---- ws.on('hero_move', (msg: ServerMessage) => { const p = msg.payload as HeroMovePayload; engine.applyHeroMove(p.x, p.y, p.targetX, p.targetY, p.speed); }); ws.on('position_sync', (msg: ServerMessage) => { const p = msg.payload as PositionSyncPayload; engine.applyPositionSync(p.x, p.y, p.state); }); ws.on('route_assigned', (msg: ServerMessage) => { const p = msg.payload as RouteAssignedPayload; engine.applyRouteAssigned(p.waypoints, p.speed); }); // ---- Server -> Client: Hero state snapshot ---- ws.on('hero_state', (msg: ServerMessage) => { const payload = msg.payload as Record; callbacks.onHeroStateReceived?.(payload); }); // ---- Server -> Client: Combat ---- ws.on('combat_start', (msg: ServerMessage) => { const p = msg.payload as CombatStartPayload; const enemy: EnemyState = { id: Date.now(), name: p.enemy.name, hp: p.enemy.hp, maxHp: p.enemy.maxHp, position: { x: 0, y: 0 }, // engine will position relative to hero attackSpeed: p.enemy.speed, damage: p.enemy.attack, defense: p.enemy.defense, enemyType: (p.enemy.type as EnemyType) || EnemyType.Wolf, }; engine.applyCombatStart(enemy); }); ws.on('attack', (msg: ServerMessage) => { const p = msg.payload as AttackPayload; engine.applyAttack(p.source, p.damage, p.isCrit, p.heroHp, p.enemyHp); }); ws.on('combat_end', (msg: ServerMessage) => { const p = msg.payload as CombatEndPayload; engine.applyCombatEnd(); callbacks.onCombatEnd?.(p); }); ws.on('merchant_loot', (msg: ServerMessage) => { const p = msg.payload as MerchantLootPayload; callbacks.onMerchantLoot?.(p); }); // ---- Server -> Client: Death / Revive ---- ws.on('hero_died', (msg: ServerMessage) => { const p = msg.payload as HeroDiedPayload; engine.applyHeroDied(); callbacks.onHeroDied?.(p); }); ws.on('hero_revived', (msg: ServerMessage) => { const p = msg.payload as HeroRevivedPayload; engine.applyHeroRevived(p.hp); callbacks.onHeroRevived?.(p); }); // ---- Server -> Client: Buffs ---- ws.on('buff_applied', (msg: ServerMessage) => { const p = msg.payload as BuffAppliedPayload; callbacks.onBuffApplied?.(p); }); // ---- Server -> Client: Town ---- ws.on('town_enter', (msg: ServerMessage) => { const p = msg.payload as TownEnterPayload; engine.applyTownEnter(p.townId, p.buildings as any); callbacks.onTownEnter?.(p); }); ws.on('town_exit', () => { engine.applyTownExit(); callbacks.onTownExit?.(); }); ws.on('town_npc_visit', (msg: ServerMessage) => { const p = msg.payload as TownNPCVisitPayload; engine.applyTownNPCVisit(p.name, p.type); callbacks.onTownNPCVisit?.(p); }); ws.on('adventure_log_line', (msg: ServerMessage) => { const p = msg.payload as AdventureLogLinePayload; engine.applyAdventureLogLine(p.message); callbacks.onAdventureLogLine?.(p); }); // ---- Server -> Client: NPC Encounter ---- ws.on('npc_encounter', (msg: ServerMessage) => { const p = msg.payload as NPCEncounterPayload; callbacks.onNPCEncounter?.(p); }); ws.on('npc_encounter_end', (msg: ServerMessage) => { const p = msg.payload as NPCEncounterEndPayload; callbacks.onNPCEncounterEnd?.(p); }); // ---- Server -> Client: Progression ---- ws.on('level_up', (msg: ServerMessage) => { const p = msg.payload as LevelUpPayload; callbacks.onLevelUp?.(p); }); ws.on('equipment_change', (msg: ServerMessage) => { const p = msg.payload as EquipmentChangePayload; callbacks.onEquipmentChange?.(p); }); ws.on('potion_collected', (msg: ServerMessage) => { const p = msg.payload as PotionCollectedPayload; callbacks.onPotionCollected?.(p); }); // ---- Server -> Client: Quests ---- ws.on('quest_progress', (msg: ServerMessage) => { const p = msg.payload as QuestProgressPayload; callbacks.onQuestProgress?.(p); }); ws.on('quest_complete', (msg: ServerMessage) => { const p = msg.payload as QuestCompletePayload; callbacks.onQuestComplete?.(p); }); ws.on('quest_available', (msg: ServerMessage) => { const p = msg.payload as QuestAvailablePayload; callbacks.onQuestAvailable?.(p); }); // ---- Server -> Client: Error ---- ws.on('error', (msg: ServerMessage) => { const p = msg.payload as ServerErrorPayload; console.warn('[WS] Server error:', p.code, p.message); callbacks.onError?.(p); }); } // ---- Client -> Server command helpers ---- export function sendActivateBuff(ws: GameWebSocket, buffType: string): void { ws.send('activate_buff', { buffType }); } export function sendUsePotion(ws: GameWebSocket): void { ws.send('use_potion', {}); } export function sendRevive(ws: GameWebSocket): void { ws.send('revive', {}); } export function sendAcceptQuest(ws: GameWebSocket, questId: number): void { ws.send('accept_quest', { questId }); } export function sendClaimQuest(ws: GameWebSocket, questId: number): void { ws.send('claim_quest', { questId }); } export function sendNPCInteract(ws: GameWebSocket, npcId: number): void { ws.send('npc_interact', { npcId }); } export function sendNPCAlmsAccept(ws: GameWebSocket): void { ws.send('npc_alms_accept', {}); } export function sendNPCAlmsDecline(ws: GameWebSocket): void { ws.send('npc_alms_decline', {}); } /** * Build a LootDrop from combat_end payload for the loot popup UI. */ export function buildLootFromCombatEnd(p: CombatEndPayload): LootDrop | null { if (p.goldGained <= 0 && p.loot.length === 0) return null; const equip = p.loot.find( (l) => l.itemType === 'weapon' || l.itemType === 'armor', ); return { itemType: 'gold', rarity: equip ? ((equip.rarity?.toLowerCase() ?? 'common') as Rarity) : Rarity.Common, goldAmount: Math.max(0, p.goldGained), itemName: equip?.name, bonusItem: equip ? { itemType: equip.itemType as 'weapon' | 'armor', rarity: (equip.rarity?.toLowerCase() ?? 'common') as Rarity, itemName: equip.name, } : undefined, }; } /** * Build a LootDrop for the popup after a wandering merchant trade (gear only; equip or auto-sell). */ export function buildMerchantLootDrop(p: MerchantLootPayload): LootDrop | null { const rarity = (p.rarity?.toLowerCase() ?? 'common') as Rarity; if (p.goldAmount != null && p.goldAmount > 0) { return { itemType: 'gold', rarity, goldAmount: p.goldAmount, itemName: p.itemName ? `Sold: ${p.itemName}` : undefined, }; } if (p.itemName) { return { itemType: 'gold', rarity, goldAmount: 0, itemName: p.itemName, }; } return null; }