import { create } from 'zustand'; // Use environment variables or fallback to relative URLs for proxy const WS_URL = import.meta.env.VITE_WS_URL || `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`; const API_URL = import.meta.env.VITE_API_URL || `${window.location.protocol}//${window.location.host}/api`; console.log('🔌 WebSocket URL:', WS_URL); console.log('📡 API URL:', API_URL); export const useTurtleStore = create((set, get) => ({ // State turtles: {}, players: {}, worldBlocks: [], chunkAnalyses: {}, selectedTurtleId: null, connected: false, ws: null, // WebSocket connection connect: () => { const ws = new WebSocket(WS_URL); ws.onopen = () => { console.log('✅ Connected to server'); set({ connected: true, ws }); }; ws.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.type === 'initial_state') { const turtlesMap = {}; data.turtles.forEach(turtle => { turtlesMap[turtle.turtleID] = turtle; }); const playersMap = {}; if (data.players && Array.isArray(data.players)) { data.players.forEach(player => { playersMap[player.playerID] = player; }); } set({ turtles: turtlesMap, players: playersMap, worldBlocks: data.blocks || [] }); } else if (data.type === 'turtle_update') { set(state => ({ turtles: { ...state.turtles, [data.turtle.turtleID]: data.turtle } })); // Update world blocks from turtle surroundings if (data.turtle.surroundings && data.turtle.position && data.turtle.facing !== undefined) { get().updateBlocksFromSurroundings(data.turtle); } } else if (data.type === 'turtle_removed' || data.type === 'turtle_disconnected') { console.log(`🔌 Turtle ${data.turtleID} removed`); set(state => { const newTurtles = { ...state.turtles }; delete newTurtles[data.turtleID]; // If the removed turtle was selected, deselect it const newSelectedId = state.selectedTurtleId === data.turtleID ? null : state.selectedTurtleId; return { turtles: newTurtles, selectedTurtleId: newSelectedId }; }); } else if (data.type === 'player_update') { set(state => ({ players: { ...state.players, [data.playerID]: { playerID: data.playerID, position: data.position, label: data.label || null, timestamp: data.timestamp || Date.now() } } })); } else if (data.type === 'block_discovered') { if (data.block) { set(state => { const blockMap = new Map(state.worldBlocks.map(b => [`${b.x},${b.y},${b.z}`, b])); const key = `${data.block.x},${data.block.y},${data.block.z}`; blockMap.set(key, data.block); return { worldBlocks: Array.from(blockMap.values()) }; }); } } else if (data.type === 'blocks_discovered') { if (data.blocks && Array.isArray(data.blocks)) { set(state => { const blockMap = new Map(state.worldBlocks.map(b => [`${b.x},${b.y},${b.z}`, b])); data.blocks.forEach(block => { const key = `${block.x},${block.y},${block.z}`; blockMap.set(key, block); }); return { worldBlocks: Array.from(blockMap.values()) }; }); } } else if (data.type === 'turtle_event') { // Handle live turtle events (inventory, peripherals) const turtleId = data.turtleID; if (turtleId && data.eventType === 'inventory_update' && data.inventory) { set(state => { const turtle = state.turtles[turtleId]; if (!turtle) return state; return { turtles: { ...state.turtles, [turtleId]: { ...turtle, inventory: data.inventory } } }; }); } else if (turtleId && (data.eventType === 'peripheral_attached' || data.eventType === 'peripheral_detached')) { set(state => { const turtle = state.turtles[turtleId]; if (!turtle) return state; return { turtles: { ...state.turtles, [turtleId]: { ...turtle, peripherals: data.peripherals || turtle.peripherals } } }; }); } } else if (data.type === 'chunk_analysis') { // Store chunk analysis results if (data.chunk) { set(state => ({ chunkAnalyses: { ...state.chunkAnalyses, [`${data.chunk.x},${data.chunk.z}`]: data.chunk } })); } } else if (data.type === 'block_deleted') { // Remove a block from the world map (turtle moved into it) if (data.x !== undefined && data.y !== undefined && data.z !== undefined) { set(state => { const key = `${data.x},${data.y},${data.z}`; return { worldBlocks: state.worldBlocks.filter(b => `${b.x},${b.y},${b.z}` !== key) }; }); } } } catch (error) { console.error('Error processing message:', error); } }; ws.onclose = () => { console.log('❌ Disconnected from server'); set({ connected: false, ws: null }); // Attempt reconnect after 3 seconds setTimeout(() => { get().connect(); }, 3000); }; ws.onerror = (error) => { console.error('WebSocket error:', error); }; }, // Actions selectTurtle: (turtleId) => { set({ selectedTurtleId: turtleId }); }, updateBlocksFromSurroundings: (turtle) => { const { surroundings, position, facing } = turtle; if (!surroundings || !position || facing === undefined) return; const newBlocks = []; const blockMap = new Map(get().worldBlocks.map(b => [`${b.x},${b.y},${b.z}`, b])); // Calculate block positions based on turtle position and facing const directions = { forward: { x: 0, y: 0, z: 0 }, up: { x: 0, y: 1, z: 0 }, down: { x: 0, y: -1, z: 0 } }; // Facing: 0=North(-Z), 1=East(+X), 2=South(+Z), 3=West(-X) if (surroundings.forward) { if (facing === 0) directions.forward = { x: 0, y: 0, z: -1 }; else if (facing === 1) directions.forward = { x: 1, y: 0, z: 0 }; else if (facing === 2) directions.forward = { x: 0, y: 0, z: 1 }; else if (facing === 3) directions.forward = { x: -1, y: 0, z: 0 }; } Object.entries(surroundings).forEach(([direction, blockData]) => { const offset = directions[direction]; if (!offset) return; const blockPos = { x: position.x + offset.x, y: position.y + offset.y, z: position.z + offset.z }; const key = `${blockPos.x},${blockPos.y},${blockPos.z}`; if (!blockMap.has(key)) { blockMap.set(key, { ...blockPos, ...blockData, discoveredBy: turtle.turtleID, timestamp: Date.now() }); } }); set({ worldBlocks: Array.from(blockMap.values()) }); }, // Set turtle state machine state via REST API setTurtleState: async (turtleId, stateName, stateData = {}) => { console.log(`🔄 Setting state for turtle ${turtleId}: ${stateName}`, stateData); try { const response = await fetch(`${API_URL}/turtle/${turtleId}/state`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ state: stateName, data: stateData }) }); const result = await response.json(); console.log(' ✅ State set:', result); return result; } catch (error) { console.error(' ❌ Error setting state:', error); // Fallback: send via WebSocket as a command const { ws } = get(); if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'command', turtleID: turtleId, command: 'set_state', param: { state: stateName, data: stateData } })); } } }, // Execute arbitrary Lua code on a turtle execOnTurtle: async (turtleId, code) => { console.log(`💻 Exec on turtle ${turtleId}:`, code); try { const response = await fetch(`${API_URL}/turtle/${turtleId}/exec`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code }) }); return await response.json(); } catch (error) { console.error(' ❌ Error executing:', error); return { success: false, error: error.message }; } }, // ========== Server-Side Movement & Actions ========== // All movement/actions are routed through the server's Turtle.js exec() pipeline _turtleAction: async (turtleId, action, body = {}) => { try { const response = await fetch(`${API_URL}/turtle/${turtleId}/${action}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return await response.json(); } catch (error) { console.error(` ❌ Error ${action}:`, error); return { success: false, error: error.message }; } }, moveForward: async (turtleId) => get()._turtleAction(turtleId, 'forward'), moveBack: async (turtleId) => get()._turtleAction(turtleId, 'back'), moveUp: async (turtleId) => get()._turtleAction(turtleId, 'up'), moveDown: async (turtleId) => get()._turtleAction(turtleId, 'down'), turnLeft: async (turtleId) => get()._turtleAction(turtleId, 'turnLeft'), turnRight: async (turtleId) => get()._turtleAction(turtleId, 'turnRight'), digBlock: async (turtleId) => get()._turtleAction(turtleId, 'dig'), digBlockUp: async (turtleId) => get()._turtleAction(turtleId, 'digUp'), digBlockDown: async (turtleId) => get()._turtleAction(turtleId, 'digDown'), placeBlock: async (turtleId, text) => get()._turtleAction(turtleId, 'place', { text }), placeBlockUp: async (turtleId, text) => get()._turtleAction(turtleId, 'placeUp', { text }), placeBlockDown: async (turtleId, text) => get()._turtleAction(turtleId, 'placeDown', { text }), refuelTurtle: async (turtleId, count) => get()._turtleAction(turtleId, 'refuel-action', { count }), // Fetch chunk analyses from server fetchChunkAnalyses: async () => { try { const response = await fetch(`${API_URL}/chunks`); if (response.ok) { const data = await response.json(); const analyses = {}; data.forEach(c => { analyses[`${c.x},${c.z}`] = c; }); set({ chunkAnalyses: analyses }); } } catch (error) { console.error(' ❌ Error fetching chunks:', error); } }, // Request chunk analysis for a specific chunk analyzeChunk: async (chunkX, chunkZ) => { try { const response = await fetch(`${API_URL}/chunks/${chunkX}/${chunkZ}/analyze`, { method: 'POST' }); if (response.ok) { const data = await response.json(); set(state => ({ chunkAnalyses: { ...state.chunkAnalyses, [`${chunkX},${chunkZ}`]: data } })); return data; } } catch (error) { console.error(' ❌ Error analyzing chunk:', error); } }, // Search blocks by name pattern searchBlocks: async (namePattern) => { try { const response = await fetch(`${API_URL}/world/blocks/search?name=${encodeURIComponent(namePattern)}`); if (response.ok) { return await response.json(); } } catch (error) { console.error(' ❌ Error searching blocks:', error); } return []; }, // Rename a turtle renameTurtle: async (turtleId, name) => { try { const response = await fetch(`${API_URL}/turtle/${turtleId}/rename`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }) }); return await response.json(); } catch (error) { console.error(' ❌ Error renaming turtle:', error); return { success: false, error: error.message }; } }, // Equip left equipLeft: async (turtleId) => { try { const response = await fetch(`${API_URL}/turtle/${turtleId}/equip-left`, { method: 'POST' }); return await response.json(); } catch (error) { return { success: false, error: error.message }; } }, // Equip right equipRight: async (turtleId) => { try { const response = await fetch(`${API_URL}/turtle/${turtleId}/equip-right`, { method: 'POST' }); return await response.json(); } catch (error) { return { success: false, error: error.message }; } }, // Select inventory slot selectSlot: async (turtleId, slot) => { try { const response = await fetch(`${API_URL}/turtle/${turtleId}/select`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ slot }) }); return await response.json(); } catch (error) { return { success: false, error: error.message }; } }, // Transfer items between slots transferItems: async (turtleId, fromSlot, toSlot, count) => { try { const response = await fetch(`${API_URL}/turtle/${turtleId}/transfer`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fromSlot, toSlot, count }) }); return await response.json(); } catch (error) { return { success: false, error: error.message }; } }, // Sort/compact inventory sortInventory: async (turtleId) => { try { const response = await fetch(`${API_URL}/turtle/${turtleId}/sort`, { method: 'POST' }); return await response.json(); } catch (error) { return { success: false, error: error.message }; } }, // Connect to adjacent inventory connectToInventory: async (turtleId, side) => { try { const response = await fetch(`${API_URL}/turtle/${turtleId}/connect-inventory`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ side }) }); return await response.json(); } catch (error) { return { success: false, error: error.message }; } }, // Drop items dropItems: async (turtleId, direction = 'front', count) => { try { const response = await fetch(`${API_URL}/turtle/${turtleId}/drop`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ direction, count }) }); return await response.json(); } catch (error) { return { success: false, error: error.message }; } }, // Suck items suckItems: async (turtleId, direction = 'front', count) => { try { const response = await fetch(`${API_URL}/turtle/${turtleId}/suck`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ direction, count }) }); return await response.json(); } catch (error) { return { success: false, error: error.message }; } }, // Update turtle config updateTurtleConfig: async (turtleId, config) => { try { const response = await fetch(`${API_URL}/turtle/${turtleId}/config`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) }); return await response.json(); } catch (error) { return { success: false, error: error.message }; } }, // Get turtle config getTurtleConfig: async (turtleId) => { try { const response = await fetch(`${API_URL}/turtle/${turtleId}/config`); return await response.json(); } catch (error) { return { success: false, error: error.message }; } }, // Explore (batched 3-direction inspect) exploreTurtle: async (turtleId) => { try { const response = await fetch(`${API_URL}/turtle/${turtleId}/explore`, { method: 'POST' }); return await response.json(); } catch (error) { return { success: false, error: error.message }; } }, // GPS locate gpsLocateTurtle: async (turtleId) => { try { const response = await fetch(`${API_URL}/turtle/${turtleId}/gps`, { method: 'POST' }); return await response.json(); } catch (error) { return { success: false, error: error.message }; } }, // Helper getters getTurtleArray: () => { return Object.values(get().turtles); }, getSelectedTurtle: () => { const { turtles, selectedTurtleId } = get(); return selectedTurtleId ? turtles[selectedTurtleId] : null; } }));