import { create } from 'zustand'; import type { Ship } from '../types/ship'; import type { Weapon } from '../types/weapon'; import socket from '../socket'; interface ShipStore { ship: Ship | null; weapons: Weapon[]; loading: boolean; currentShipId: string | null; joinShip: (id: string) => void; leaveShip: () => void; updateShip: (patch: Partial) => void; createWeapon: (weapon: Record) => void; updateWeapon: (weaponId: string, patch: Partial) => void; deleteWeapon: (weaponId: string) => void; } export const useShipStore = create((set, get) => ({ ship: null, weapons: [], loading: false, currentShipId: null, joinShip: (id: string) => { const current = get().currentShipId; if (current === id) return; if (current) { // Leave previous room first get().leaveShip(); } set({ loading: true, currentShipId: id, ship: null, weapons: [] }); // Register socket listeners const onState = (data: { ship: Ship; weapons: Weapon[] }) => { set({ ship: data.ship, weapons: data.weapons, loading: false }); }; const onPatched = (data: { shipId: string; patch: Partial; updated_at: number }) => { const state = get(); if (state.ship && state.ship.id === data.shipId) { set({ ship: { ...state.ship, ...data.patch, updated_at: data.updated_at } }); } }; const onWeaponCreated = (data: { shipId: string; weapon: Weapon }) => { set({ weapons: [...get().weapons, data.weapon] }); }; const onWeaponPatched = (data: { shipId: string; weaponId: string; patch: Partial; updated_at: number; }) => { set({ weapons: get().weapons.map((w) => w.id === data.weaponId ? { ...w, ...data.patch, updated_at: data.updated_at } : w, ), }); }; const onWeaponDeleted = (data: { shipId: string; weaponId: string }) => { set({ weapons: get().weapons.filter((w) => w.id !== data.weaponId) }); }; socket.on('ship:state', onState); socket.on('ship:patched', onPatched); socket.on('weapon:created', onWeaponCreated); socket.on('weapon:patched', onWeaponPatched); socket.on('weapon:deleted', onWeaponDeleted); // Join the room socket.emit('ship:join', { shipId: id }); // Re-join on reconnect const onReconnect = () => { if (get().currentShipId === id) { socket.emit('ship:join', { shipId: id }); } }; socket.on('connect', onReconnect); // Store cleanup references (socket as any).__shipCleanup = () => { socket.off('ship:state', onState); socket.off('ship:patched', onPatched); socket.off('weapon:created', onWeaponCreated); socket.off('weapon:patched', onWeaponPatched); socket.off('weapon:deleted', onWeaponDeleted); socket.off('connect', onReconnect); }; }, leaveShip: () => { const id = get().currentShipId; if (id) { socket.emit('ship:leave', { shipId: id }); } // Clean up listeners if ((socket as any).__shipCleanup) { (socket as any).__shipCleanup(); delete (socket as any).__shipCleanup; } set({ ship: null, weapons: [], currentShipId: null, loading: false }); }, updateShip: (patch) => { const id = get().currentShipId; const ship = get().ship; if (!id || !ship) return; // Optimistic: apply locally immediately set({ ship: { ...ship, ...patch } }); // Accumulate patches and debounce the server send const pending = (socket as any).__pendingPatch ?? {}; Object.assign(pending, patch); (socket as any).__pendingPatch = pending; if ((socket as any).__patchTimer) clearTimeout((socket as any).__patchTimer); (socket as any).__patchTimer = setTimeout(() => { const merged = (socket as any).__pendingPatch; delete (socket as any).__pendingPatch; delete (socket as any).__patchTimer; if (merged) socket.emit('ship:update', { shipId: id, patch: merged }); }, 300); }, createWeapon: (weapon) => { const id = get().currentShipId; if (!id) return; socket.emit('weapon:create', { shipId: id, weapon }); }, updateWeapon: (weaponId, patch) => { // Optimistic: apply locally immediately set({ weapons: get().weapons.map((w) => w.id === weaponId ? { ...w, ...patch } : w, ), }); // Debounce per-weapon server sends const timerKey = `__weaponTimer_${weaponId}`; const patchKey = `__weaponPatch_${weaponId}`; const pending = (socket as any)[patchKey] ?? {}; Object.assign(pending, patch); (socket as any)[patchKey] = pending; if ((socket as any)[timerKey]) clearTimeout((socket as any)[timerKey]); (socket as any)[timerKey] = setTimeout(() => { const merged = (socket as any)[patchKey]; delete (socket as any)[patchKey]; delete (socket as any)[timerKey]; if (merged) socket.emit('weapon:update', { weaponId, patch: merged }); }, 300); }, deleteWeapon: (weaponId) => { socket.emit('weapon:delete', { weaponId }); }, }));