diff --git a/api/src/jsons/npcs.json b/api/src/jsons/npcs.json new file mode 100644 index 00000000..36a48761 --- /dev/null +++ b/api/src/jsons/npcs.json @@ -0,0 +1,110 @@ +{ + "1": { + "name": "Comerciante de Pociones", + "npcType": 10, + "idHead": 1, + "idBody": 1, + "movement": 0, + "desc": "Vende pociones curativas, de maná y consumibles básicos.", + "objs": [ + { + "item": 1, + "cant": 1000 + }, + { + "item": 2, + "cant": 1000 + }, + { + "item": 3, + "cant": 1000 + } + ], + "drop": [] + }, + "2": { + "name": "Herrero de Ullathorpe", + "npcType": 10, + "idHead": 2, + "idBody": 2, + "movement": 0, + "desc": "Vende armas de hierro y armaduras de combate.", + "objs": [ + { + "item": 10, + "cant": 100 + }, + { + "item": 11, + "cant": 100 + }, + { + "item": 12, + "cant": 100 + } + ], + "drop": [] + }, + "3": { + "name": "Sacerdote", + "npcType": 11, + "idHead": 3, + "idBody": 3, + "movement": 0, + "desc": "Cura heridas y bendice a los aventureros.", + "objs": [], + "drop": [] + }, + "4": { + "name": "Banquero", + "npcType": 12, + "idHead": 1, + "idBody": 1, + "movement": 0, + "desc": "Custodia el oro y los objetos de los aventureros.", + "objs": [], + "drop": [] + }, + "5": { + "name": "Lobo", + "npcType": 1, + "idHead": 0, + "idBody": 10, + "movement": 1, + "exp": 15, + "gold": 25, + "hp": 30, + "maxHp": 30, + "minHit": 2, + "maxHit": 6, + "def": 2, + "objs": [], + "drop": [ + { + "item": 101, + "cant": 1 + } + ] + }, + "6": { + "name": "Orco Guerrero", + "npcType": 1, + "idHead": 4, + "idBody": 20, + "movement": 1, + "exp": 60, + "gold": 90, + "hp": 85, + "maxHp": 85, + "minHit": 8, + "maxHit": 16, + "def": 6, + "objs": [], + "drop": [ + { + "item": 102, + "cant": 1 + } + ] + } +} diff --git a/frontend/components/game/session/useGameSession.ts b/frontend/components/game/session/useGameSession.ts index 6c54293d..6efa8f41 100644 --- a/frontend/components/game/session/useGameSession.ts +++ b/frontend/components/game/session/useGameSession.ts @@ -301,74 +301,87 @@ export function useGameSession({ return; } - disconnectSocket(); - activeSessionKeyRef.current = connection.sessionKey; - - const socket = new WebSocket(connection.wsUrl); - socket.binaryType = "arraybuffer"; - websocketRef.current = socket; - - emitStatusRef.current({ connected: false, connecting: true }); + let isUnmounted = false; + let reconnectAttempt = 0; + const maxReconnectAttempts = 5; + let reconnectTimer: number | null = null; + + const clearReconnectTimer = () => { + if (reconnectTimer !== null) { + window.clearTimeout(reconnectTimer); + reconnectTimer = null; + } + }; - socket.onopen = () => { - if ( - activeSessionKeyRef.current !== connection.sessionKey || - !isCurrentSocketInstance(socket) - ) { - socket.close(); + const connectSocket = () => { + if (isUnmounted || !connection || !isClientReadyForConnection) { return; } - const sendPing = () => { - if (socket.readyState !== WebSocket.OPEN) { - return; - } + disconnectSocket(); + activeSessionKeyRef.current = connection.sessionKey; - const token = nextPingTokenRef.current++; - pendingPingRef.current = { - token, - sentAt: performance.now(), - }; - socket.send(createPingPacket(token)); - }; + const socket = new WebSocket(connection.wsUrl); + socket.binaryType = "arraybuffer"; + websocketRef.current = socket; - socket.send( - createConnectCharacterPacket({ - ticket: connection.ticket, - typeGame: connection.typeGame, - idChar: connection.idChar, - }), - ); - - flushPendingChatRequestRef.current(); - sendPing(); - pingIntervalRef.current = window.setInterval(sendPing, 10000); - }; + emitStatusRef.current({ + connected: false, + connecting: true, + error: + reconnectAttempt > 0 + ? `Reconectando (${reconnectAttempt}/${maxReconnectAttempts})...` + : undefined, + }); - const processIncomingPacketQueue = async () => { - if (isProcessingIncomingPacketsRef.current) { - return; - } + socket.onopen = () => { + if ( + isUnmounted || + activeSessionKeyRef.current !== connection.sessionKey || + !isCurrentSocketInstance(socket) + ) { + socket.close(); + return; + } - isProcessingIncomingPacketsRef.current = true; + reconnectAttempt = 0; + clearReconnectTimer(); - try { - while (incomingPacketQueueRef.current.length > 0) { - if (activeSessionKeyRef.current !== connection.sessionKey) { - incomingPacketQueueRef.current = []; + const sendPing = () => { + if (socket.readyState !== WebSocket.OPEN) { return; } - const queuedMessage = - incomingPacketQueueRef.current.shift(); - if (!queuedMessage) { - continue; - } + const token = nextPingTokenRef.current++; + pendingPingRef.current = { + token, + sentAt: performance.now(), + }; + socket.send(createPingPacket(token)); + }; + + socket.send( + createConnectCharacterPacket({ + ticket: connection.ticket, + typeGame: connection.typeGame, + idChar: connection.idChar, + }), + ); + + flushPendingChatRequestRef.current(); + sendPing(); + pingIntervalRef.current = window.setInterval(sendPing, 10000); + }; + + const processIncomingPacketQueue = async () => { + if (isProcessingIncomingPacketsRef.current) { + return; + } - try { - const { packets } = - await parseQueuedSocketMessage(queuedMessage); + isProcessingIncomingPacketsRef.current = true; + try { + while (incomingPacketQueueRef.current.length > 0) { if ( activeSessionKeyRef.current !== connection.sessionKey @@ -377,136 +390,220 @@ export function useGameSession({ return; } - for (const packet of packets) { - const engine = engineRef.current; - const renderedMapNumber = - engine?.mapNumber ?? currentMapRef.current; - - await onPacketRef.current({ - packet, - engine, - renderedMapNumber, - disconnectSocket, - }); + const queuedMessage = + incomingPacketQueueRef.current.shift(); + if (!queuedMessage) { + continue; + } + + try { + const { packets } = + await parseQueuedSocketMessage(queuedMessage); + + if ( + activeSessionKeyRef.current !== + connection.sessionKey + ) { + incomingPacketQueueRef.current = []; + return; + } + + for (const packet of packets) { + const engine = engineRef.current; + const renderedMapNumber = + engine?.mapNumber ?? currentMapRef.current; + + await onPacketRef.current({ + packet, + engine, + renderedMapNumber, + disconnectSocket, + }); + } + } catch (packetError) { + console.error( + "Error parsing server packet", + packetError, + ); } - } catch (packetError) { - console.error( - "Error parsing server packet", - packetError, - ); } + } finally { + isProcessingIncomingPacketsRef.current = false; } - } finally { - isProcessingIncomingPacketsRef.current = false; - } - }; + }; - const tryHandlePongMessage = async ( - rawMessage: Blob | ArrayBuffer | ArrayBufferView, - ): Promise => { - if (rawMessage instanceof ArrayBuffer) { - if ( - new DataView(rawMessage).getUint8(0) !== - CLIENT_PACKET_ID.pong - ) { + const tryHandlePongMessage = async ( + rawMessage: Blob | ArrayBuffer | ArrayBufferView, + ): Promise => { + if (rawMessage instanceof ArrayBuffer) { + if ( + new DataView(rawMessage).getUint8(0) !== + CLIENT_PACKET_ID.pong + ) { + return false; + } + } else if (ArrayBuffer.isView(rawMessage)) { + if ( + new DataView( + rawMessage.buffer, + rawMessage.byteOffset, + rawMessage.byteLength, + ).getUint8(0) !== CLIENT_PACKET_ID.pong + ) { + return false; + } + } + + const messageData = await normalizeSocketMessageData(rawMessage); + const packet = parseServerPacket(messageData); + + if (packet.type !== "pong") { return false; } - } else if (ArrayBuffer.isView(rawMessage)) { + + const pendingPing = pendingPingRef.current; if ( - new DataView( - rawMessage.buffer, - rawMessage.byteOffset, - rawMessage.byteLength, - ).getUint8(0) !== CLIENT_PACKET_ID.pong + pendingPing && + (packet.payload.token === 0 || + packet.payload.token === pendingPing.token) ) { - return false; + updatePingDisplay( + Math.max( + 0, + Math.round(performance.now() - pendingPing.sentAt), + ), + ); + + pendingPingRef.current = null; } - } - const messageData = await normalizeSocketMessageData(rawMessage); - const packet = parseServerPacket(messageData); + return true; + }; - if (packet.type !== "pong") { - return false; - } + socket.onmessage = (event) => { + if (!isCurrentSocketInstance(socket)) { + return; + } - const pendingPing = pendingPingRef.current; - if ( - pendingPing && - (packet.payload.token === 0 || - packet.payload.token === pendingPing.token) - ) { - updatePingDisplay( - Math.max( - 0, - Math.round(performance.now() - pendingPing.sentAt), - ), - ); + const rawMessage = event.data as + | Blob + | ArrayBuffer + | ArrayBufferView; - pendingPingRef.current = null; - } + void tryHandlePongMessage(rawMessage) + .then((wasPong) => { + if (wasPong) { + return; + } - return true; - }; + incomingPacketQueueRef.current.push(rawMessage); + void processIncomingPacketQueue(); + }) + .catch(() => { + incomingPacketQueueRef.current.push(rawMessage); + void processIncomingPacketQueue(); + }); + }; - socket.onmessage = (event) => { - if (!isCurrentSocketInstance(socket)) { - return; - } + socket.onerror = () => { + if (!isCurrentSocketInstance(socket)) { + return; + } + + setIsSceneReadyRef.current(false); + emitStatusRef.current({ + connected: false, + connecting: false, + error: "No se pudo establecer la conexion websocket.", + }); + }; - const rawMessage = event.data as - | Blob - | ArrayBuffer - | ArrayBufferView; + socket.onclose = (event: CloseEvent) => { + clearPing(); + if (isUnmounted || !isCurrentSocketInstance(socket)) { + return; + } - void tryHandlePongMessage(rawMessage) - .then((wasPong) => { - if (wasPong) { + if (activeSessionKeyRef.current === connection.sessionKey) { + setIsSceneReadyRef.current(false); + + // Skip auto-reconnect on intentional clean closures (1000 Normal Closure) + if (event.code === 1000) { + const previousError = latestStatusRef.current.error; + emitStatusRef.current({ + connected: false, + connecting: false, + error: previousError || "Conexion cerrada.", + }); return; } - incomingPacketQueueRef.current.push(rawMessage); - void processIncomingPacketQueue(); - }) - .catch(() => { - incomingPacketQueueRef.current.push(rawMessage); - void processIncomingPacketQueue(); - }); - }; - - socket.onerror = () => { - if (!isCurrentSocketInstance(socket)) { - return; - } - - setIsSceneReadyRef.current(false); - emitStatusRef.current({ - connected: false, - connecting: false, - error: "No se pudo establecer la conexion websocket.", - }); + if (reconnectAttempt < maxReconnectAttempts) { + reconnectAttempt += 1; + const backoffMs = Math.min( + 10000, + 1000 * Math.pow(2, reconnectAttempt - 1), + ); + emitStatusRef.current({ + connected: false, + connecting: true, + error: `Conexion perdida. Reconectando (${reconnectAttempt}/${maxReconnectAttempts})...`, + }); + + clearReconnectTimer(); + reconnectTimer = window.setTimeout(() => { + if ( + !isUnmounted && + activeSessionKeyRef.current === + connection.sessionKey + ) { + connectSocket(); + } + }, backoffMs); + } else { + const previousError = latestStatusRef.current.error; + emitStatusRef.current({ + connected: false, + connecting: false, + error: + previousError || + "Conexion cerrada. No se pudo reconectar automaticamente.", + }); + } + } + }; }; - socket.onclose = () => { - clearPing(); + const handleVisibilityOrOnline = () => { if ( - activeSessionKeyRef.current === connection.sessionKey && - isCurrentSocketInstance(socket) + !isUnmounted && + document.visibilityState === "visible" && + connection && + isClientReadyForConnection && + (!websocketRef.current || + websocketRef.current.readyState === WebSocket.CLOSED) ) { - setIsSceneReadyRef.current(false); - const previousError = latestStatusRef.current.error; - emitStatusRef.current({ - connected: false, - connecting: false, - error: previousError || "Conexion cerrada.", - }); + clearReconnectTimer(); + reconnectAttempt = 0; + connectSocket(); } }; + window.addEventListener("online", handleVisibilityOrOnline); + document.addEventListener("visibilitychange", handleVisibilityOrOnline); + + connectSocket(); + return () => { + isUnmounted = true; + clearReconnectTimer(); + window.removeEventListener("online", handleVisibilityOrOnline); + document.removeEventListener( + "visibilitychange", + handleVisibilityOrOnline, + ); if ( - activeSessionKeyRef.current === connection.sessionKey && - isCurrentSocketInstance(socket) + activeSessionKeyRef.current === connection.sessionKey ) { activeSessionKeyRef.current = null; }