import { useState, useEffect, useRef } from 'react' import api from '../services/api' import GameHeader from './GameHeader' import './Game.css' interface PlayerState { location_id: string location_name: string health: number max_health: number stamina: number max_stamina: number inventory: any[] status_effects: any[] } interface DirectionDetail { direction: string stamina_cost: number distance: number destination: string destination_name?: string } interface Location { id: string name: string description: string directions: string[] directions_detailed?: DirectionDetail[] danger_level?: number npcs: any[] items: any[] image_url?: string interactables?: any[] other_players?: any[] corpses?: any[] tags?: string[] // Tags for special location features like workbench } interface Profile { name: string level: number xp: number hp: number max_hp: number stamina: number max_stamina: number strength: number agility: number endurance: number intellect: number unspent_points: number is_dead: boolean max_weight?: number current_weight?: number max_volume?: number current_volume?: number } function Game() { const [playerState, setPlayerState] = useState(null) const [location, setLocation] = useState(null) const [profile, setProfile] = useState(null) const [loading, setLoading] = useState(true) const [message, setMessage] = useState('') const [selectedItem, setSelectedItem] = useState(null) const [combatState, setCombatState] = useState(null) const [combatLog, setCombatLog] = useState>([]) const [enemyName, setEnemyName] = useState('') const [enemyImage, setEnemyImage] = useState('') const [collapsedCategories, setCollapsedCategories] = useState>(new Set()) const [expandedCorpse, setExpandedCorpse] = useState(null) const [corpseDetails, setCorpseDetails] = useState(null) const [movementCooldown, setMovementCooldown] = useState(0) const [enemyTurnMessage, setEnemyTurnMessage] = useState('') const [equipment, setEquipment] = useState({}) const [showCraftingMenu, setShowCraftingMenu] = useState(false) const [showRepairMenu, setShowRepairMenu] = useState(false) const [craftableItems, setCraftableItems] = useState([]) const [repairableItems, setRepairableItems] = useState([]) const [workbenchTab, setWorkbenchTab] = useState<'craft' | 'repair' | 'uncraft'>('craft') const [craftFilter, setCraftFilter] = useState('') const [craftCategoryFilter, setCraftCategoryFilter] = useState('all') const [repairFilter, setRepairFilter] = useState('') const [uncraftFilter, setUncraftFilter] = useState('') const [uncraftableItems, setUncraftableItems] = useState([]) const [lastSeenPvPAction, setLastSeenPvPAction] = useState(null) // Use ref for synchronous duplicate checking (state updates are async) const lastSeenPvPActionRef = useRef(null) // Mobile menu state const [mobileMenuOpen, setMobileMenuOpen] = useState<'none' | 'left' | 'right' | 'bottom'>('none') const [mobileHeaderOpen, setMobileHeaderOpen] = useState(false) useEffect(() => { fetchGameData() // Set up polling for location updates and PvP combat detection const pollInterval = setInterval(() => { // Stop polling if combat is over (save server resources) if (combatState?.pvp_combat?.combat_over) { return } // Always poll if page is visible - need to detect incoming PvP if (!document.hidden) { // Check combat state at the time of polling (not from stale closure) fetchGameData(true) } }, 5000) // Poll every 5 seconds // Cleanup on unmount return () => clearInterval(pollInterval) }, [combatState?.pvp_combat?.combat_over]) // Re-run if combat_over state changes // Auto-dismiss messages after 4 seconds on mobile useEffect(() => { if (message && window.innerWidth <= 768) { const timer = setTimeout(() => { setMessage('') }, 4000) return () => clearTimeout(timer) } }, [message]) // Countdown effect for movement cooldown useEffect(() => { if (movementCooldown > 0) { const timer = setTimeout(() => { setMovementCooldown(prev => Math.max(0, prev - 1)) }, 1000) return () => clearTimeout(timer) } }, [movementCooldown]) const fetchGameData = async (skipCombatLogInit: boolean = false) => { try { const [stateRes, locationRes, profileRes, combatRes, pvpRes] = await Promise.all([ api.get('/api/game/state'), api.get('/api/game/location'), api.get('/api/game/profile'), api.get('/api/game/combat'), api.get('/api/game/pvp/status') ]) // Map game state to player state format const gameState = stateRes.data setPlayerState({ location_id: gameState.player.location_id, location_name: gameState.location?.name || 'Unknown', health: gameState.player.hp, max_health: gameState.player.max_hp, stamina: gameState.player.stamina, max_stamina: gameState.player.max_stamina, inventory: gameState.inventory || [], status_effects: [] }) setLocation(locationRes.data) setProfile(profileRes.data.player || profileRes.data) setEquipment(gameState.equipment || {}) // Set movement cooldown if available (add 1 second buffer only if there's actual cooldown) if (gameState.player.movement_cooldown !== undefined) { const cooldown = gameState.player.movement_cooldown setMovementCooldown(cooldown > 0 ? Math.ceil(cooldown) + 1 : 0) } // Check for PvP combat first (takes priority) if (pvpRes.data.in_pvp_combat) { const newCombatState = { ...pvpRes.data, is_pvp: true } setCombatState(newCombatState) // Check if there's a new last_action to add to combat log (avoid duplicates) // Use ref for synchronous check to prevent race conditions with state updates if (pvpRes.data.pvp_combat.last_action && pvpRes.data.pvp_combat.last_action !== lastSeenPvPActionRef.current) { // Update both state and ref setLastSeenPvPAction(pvpRes.data.pvp_combat.last_action) lastSeenPvPActionRef.current = pvpRes.data.pvp_combat.last_action const now = new Date() const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) // Parse the action message (format: "message|timestamp") const lastActionRaw = pvpRes.data.pvp_combat.last_action const [lastAction, _actionTimestamp] = lastActionRaw.split('|') const yourUsername = pvpRes.data.pvp_combat.is_attacker ? pvpRes.data.pvp_combat.attacker.username : pvpRes.data.pvp_combat.defender.username // Check if the message starts with your username (e.g., "YourName attacks" or "YourName fled") const isYourAction = lastAction.startsWith(yourUsername + ' ') setCombatLog((prev: any) => [{ time: timeStr, message: lastAction, isPlayer: isYourAction }, ...prev]) } // Initialize combat log if empty if (!skipCombatLogInit && combatLog.length === 0) { const now = new Date() const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) const opponent = pvpRes.data.pvp_combat.is_attacker ? pvpRes.data.pvp_combat.defender : pvpRes.data.pvp_combat.attacker setCombatLog([{ time: timeStr, message: `PvP combat with ${opponent.username} (Lv. ${opponent.level})!`, isPlayer: true }]) } // Combat over state is handled in the UI with an acknowledgment button // Don't auto-close anymore } // If not in PvP combat anymore, clear the tracking else if (lastSeenPvPAction !== null) { setLastSeenPvPAction(null) lastSeenPvPActionRef.current = null } // Check for active PvE combat else if (combatRes.data.in_combat) { setCombatState(combatRes.data) // Only initialize combat log if it's empty AND we're not skipping initialization // Skip initialization after encounters since they already set the combat log if (!skipCombatLogInit && combatLog.length === 0) { const now = new Date() const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) setCombatLog([{ time: timeStr, message: 'Combat in progress...', isPlayer: true }]) } } } catch (error) { console.error('Failed to fetch game data:', error) setMessage('Failed to load game data') } finally { setLoading(false) } } const handleMove = async (direction: string) => { // Prevent movement during combat if (combatState) { setMessage('Cannot move while in combat!') return } // Close workbench menu when moving if (showCraftingMenu || showRepairMenu) { handleCloseCrafting() } // Close mobile menu after movement setMobileMenuOpen('none') try { setMessage('Moving...') const response = await api.post('/api/game/move', { direction }) setMessage(response.data.message) // Check if an encounter was triggered if (response.data.encounter && response.data.encounter.triggered) { const encounter = response.data.encounter setMessage(encounter.message) // Store enemy info setEnemyName(encounter.combat.npc_name) setEnemyImage(encounter.combat.npc_image) // Set combat state setCombatState({ in_combat: true, combat_over: false, player_won: false, combat: encounter.combat }) // Clear combat log for new encounter setCombatLog([]) // Add initial message to combat log const now = new Date() const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) setCombatLog([{ time: timeStr, message: `⚠️ ${encounter.combat.npc_name} ambushes you!`, isPlayer: false }]) // Refresh all game data after movement, but skip combat log init since we just set it await fetchGameData(true) } else { // Normal movement, refresh game data normally await fetchGameData() } } catch (error: any) { setMessage(error.response?.data?.detail || 'Move failed') } } const handlePickup = async (itemId: number, quantity: number = 1) => { try { setMessage(`Picking up ${quantity > 1 ? quantity + ' items' : 'item'}...`) const response = await api.post('/api/game/pickup', { item_id: itemId, quantity }) setMessage(response.data.message || 'Item picked up!') fetchGameData() // Refresh to update inventory and ground items } catch (error: any) { setMessage(error.response?.data?.detail || 'Failed to pick up item') // Refresh to remove items that no longer exist fetchGameData() } } const handleOpenCrafting = async () => { try { const response = await api.get('/api/game/craftable') setCraftableItems(response.data.craftable_items) setShowCraftingMenu(true) setShowRepairMenu(false) setWorkbenchTab('craft') } catch (error: any) { setMessage(error.response?.data?.detail || 'Failed to load crafting menu') } } const handleCloseCrafting = () => { setShowCraftingMenu(false) setShowRepairMenu(false) setCraftableItems([]) setRepairableItems([]) setUncraftableItems([]) setCraftFilter('') setRepairFilter('') setUncraftFilter('') } const handleCraft = async (itemId: string) => { try { setMessage('Crafting...') const response = await api.post('/api/game/craft_item', { item_id: itemId }) setMessage(response.data.message || 'Item crafted!') await fetchGameData() // Refresh craftable items list const craftableRes = await api.get('/api/game/craftable') setCraftableItems(craftableRes.data.craftable_items) // Refresh salvageable items if on that tab if (workbenchTab === 'uncraft') { const salvageableRes = await api.get('/api/game/salvageable') setUncraftableItems(salvageableRes.data.salvageable_items) } } catch (error: any) { setMessage(error.response?.data?.detail || 'Failed to craft item') } } const handleOpenRepair = async () => { try { const response = await api.get('/api/game/repairable') setRepairableItems(response.data.repairable_items) setShowRepairMenu(true) setShowCraftingMenu(false) setWorkbenchTab('repair') } catch (error: any) { setMessage(error.response?.data?.detail || 'Failed to load repair menu') } } const handleRepairFromMenu = async (uniqueItemId: number, inventoryId?: number) => { try { setMessage('Repairing...') const response = await api.post('/api/game/repair_item', { unique_item_id: uniqueItemId, inventory_id: inventoryId }) setMessage(response.data.message || 'Item repaired!') await fetchGameData() // Refresh repairable items list const repairableRes = await api.get('/api/game/repairable') setRepairableItems(repairableRes.data.repairable_items) } catch (error: any) { setMessage(error.response?.data?.detail || 'Failed to repair item') } } const handleUncraft = async (uniqueItemId: number, inventoryId: number) => { try { setMessage('Salvaging...') const response = await api.post('/api/game/uncraft_item', { unique_item_id: uniqueItemId, inventory_id: inventoryId }) const data = response.data let msg = data.message || 'Item salvaged!' if (data.materials_yielded && data.materials_yielded.length > 0) { msg += '\n✅ Yielded: ' + data.materials_yielded.map((m: any) => `${m.emoji} ${m.name} x${m.quantity}`).join(', ') } if (data.materials_lost && data.materials_lost.length > 0) { msg += '\n⚠️ Lost: ' + data.materials_lost.map((m: any) => `${m.emoji} ${m.name} x${m.quantity}`).join(', ') } setMessage(msg) await fetchGameData() // Refresh salvageable items list const salvageableRes = await api.get('/api/game/salvageable') setUncraftableItems(salvageableRes.data.salvageable_items) } catch (error: any) { setMessage(error.response?.data?.detail || 'Failed to uncraft item') } } const handleSwitchWorkbenchTab = async (tab: 'craft' | 'repair' | 'uncraft') => { setWorkbenchTab(tab) try { if (tab === 'craft') { const response = await api.get('/api/game/craftable') setCraftableItems(response.data.craftable_items) } else if (tab === 'repair') { const response = await api.get('/api/game/repairable') setRepairableItems(response.data.repairable_items) } else if (tab === 'uncraft') { const salvageableRes = await api.get('/api/game/salvageable') setUncraftableItems(salvageableRes.data.salvageable_items) } } catch (error: any) { setMessage(error.response?.data?.detail || 'Failed to load items') } } const handleSpendPoint = async (stat: string) => { try { setMessage(`Increasing ${stat}...`) const response = await api.post(`/api/game/spend_point?stat=${stat}`) setMessage(response.data.message || 'Stat increased!') fetchGameData() // Refresh to update stats } catch (error: any) { setMessage(error.response?.data?.detail || 'Failed to spend point') } } const handleUseItem = async (itemId: string) => { try { setMessage('Using item...') const response = await api.post('/api/game/use_item', { item_id: itemId }) const data = response.data // If in combat, add to combat log if (combatState && data.in_combat) { const now = new Date() const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) const messages = data.message.split('\n').filter((m: string) => m.trim()) const newEntries = messages.map((msg: string) => ({ time: timeStr, message: msg, isPlayer: !msg.includes('attacks') })) setCombatLog((prev: any) => [...newEntries, ...prev]) // Check if combat ended if (data.combat_over) { setCombatState({ ...combatState, combat_over: true, player_won: data.player_won }) } } else { setMessage(data.message || 'Item used!') } fetchGameData() } catch (error: any) { setMessage(error.response?.data?.detail || 'Failed to use item') } } const handleEquipItem = async (inventoryId: number) => { try { setMessage('Equipping item...') const response = await api.post('/api/game/equip', { inventory_id: inventoryId }) setMessage(response.data.message || 'Item equipped!') fetchGameData() } catch (error: any) { setMessage(error.response?.data?.detail || 'Failed to equip item') } } const handleUnequipItem = async (slot: string) => { try { setMessage('Unequipping item...') const response = await api.post('/api/game/unequip', { slot }) setMessage(response.data.message || 'Item unequipped!') fetchGameData() } catch (error: any) { setMessage(error.response?.data?.detail || 'Failed to unequip item') } } const handleDropItem = async (itemId: string, quantity: number = 1) => { try { setMessage(`Dropping ${quantity} item(s)...`) const response = await api.post('/api/game/item/drop', { item_id: itemId, quantity }) setMessage(response.data.message || 'Item dropped!') fetchGameData() } catch (error: any) { setMessage(error.response?.data?.detail || 'Failed to drop item') } } const handleInteract = async (interactableId: string, actionId: string) => { if (combatState) { setMessage('Cannot interact with objects while in combat!') return } // Close mobile menu to show result setMobileMenuOpen('none') try { const response = await api.post('/api/game/interact', { interactable_id: interactableId, action_id: actionId }) const data = response.data let msg = data.message if (data.items_found && data.items_found.length > 0) { // items_found is already an array of strings like "Item Name x2" msg += '\n\n📦 Found: ' + data.items_found.join(', ') } if (data.hp_change) { msg += `\n❤️ HP: ${data.hp_change > 0 ? '+' : ''}${data.hp_change}` } setMessage(msg) fetchGameData() // Refresh stats } catch (error: any) { setMessage(error.response?.data?.detail || 'Interaction failed') } } const handleViewCorpseDetails = async (corpseId: string) => { try { const response = await api.get(`/api/game/corpse/${corpseId}`) setCorpseDetails(response.data) setExpandedCorpse(corpseId) // Don't show "examining" message - just open the details } catch (error: any) { setMessage(error.response?.data?.detail || 'Failed to examine corpse') } } const handleLootCorpseItem = async (corpseId: string, itemIndex: number | null = null) => { try { setMessage('Looting...') const response = await api.post('/api/game/loot_corpse', { corpse_id: corpseId, item_index: itemIndex }) // Show message for longer setMessage(response.data.message) setTimeout(() => { // Keep message visible for 5 seconds }, 5000) // If corpse is empty, close the details view if (response.data.corpse_empty) { setExpandedCorpse(null) setCorpseDetails(null) } else if (expandedCorpse === corpseId) { // Refresh corpse details if still viewing (without clearing message) try { const detailsResponse = await api.get(`/api/game/corpse/${corpseId}`) setCorpseDetails(detailsResponse.data) } catch (err) { // If corpse details fail, just close setExpandedCorpse(null) setCorpseDetails(null) } } fetchGameData() // Refresh location and inventory } catch (error: any) { setMessage(error.response?.data?.detail || 'Failed to loot corpse') } } const handleLootCorpse = async (corpseId: string) => { // Show corpse details instead of looting all at once handleViewCorpseDetails(corpseId) } const handleInitiateCombat = async (enemyId: number) => { try { // Close mobile menu to show combat setMobileMenuOpen('none') const response = await api.post('/api/game/combat/initiate', { enemy_id: enemyId }) setCombatState(response.data) // Store enemy info to prevent it from disappearing setEnemyName(response.data.combat.npc_name) setEnemyImage(response.data.combat.npc_image) // Initialize combat log with timestamp const now = new Date() const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) setCombatLog([{ time: timeStr, message: `Combat started with ${response.data.combat.npc_name}!`, isPlayer: true }]) // Refresh location to remove enemy from list const locationRes = await api.get('/api/game/location') setLocation(locationRes.data) } catch (error: any) { setMessage(error.response?.data?.detail || 'Failed to initiate combat') } } const handleCombatAction = async (action: string) => { try { const response = await api.post('/api/game/combat/action', { action }) const data = response.data // Add message to combat log with timestamp const now = new Date() const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) // Parse the message to separate player and enemy actions const messages = data.message.split('\n').filter((m: string) => m.trim()) // Find player action and enemy action const playerMessages = messages.filter((msg: string) => msg.includes('You ') || msg.includes('Your ')) const enemyMessages = messages.filter((msg: string) => msg.includes('attacks') || msg.includes('hits') || msg.includes('misses') || msg.includes('The ')) // Add player actions immediately if (playerMessages.length > 0) { const playerEntries = playerMessages.map((msg: string) => ({ time: timeStr, message: msg, isPlayer: true })) setCombatLog((prev: any) => [...playerEntries, ...prev]) // Update enemy HP immediately (but not player HP) if (data.combat && !data.combat_over) { setCombatState({ ...combatState, combat: { ...combatState.combat, npc_hp: data.combat.npc_hp, turn: data.combat.turn } }) } } // If there are enemy actions and combat is not over, show "Enemy's turn..." then delay if (enemyMessages.length > 0 && !data.combat_over) { // Show "Enemy's turn..." message setEnemyTurnMessage("🗡️ Enemy's turn...") // Wait 2 seconds before showing enemy attack await new Promise(resolve => setTimeout(resolve, 2000)) // Clear the turn message and add enemy actions to log setEnemyTurnMessage('') const enemyEntries = enemyMessages.map((msg: string) => ({ time: timeStr, message: msg, isPlayer: false })) setCombatLog((prev: any) => [...enemyEntries, ...prev]) // NOW refresh to show updated player HP after enemy attack fetchGameData() } else if (enemyMessages.length > 0) { // Combat is over, add enemy messages without delay const enemyEntries = enemyMessages.map((msg: string) => ({ time: timeStr, message: msg, isPlayer: false })) setCombatLog((prev: any) => [...enemyEntries, ...prev]) } if (data.combat_over) { // Combat ended - keep combat view but show result with preserved enemy info // Check if player fled successfully (message contains "fled") const playerFled = data.message && data.message.toLowerCase().includes('fled') setCombatState({ ...combatState, // Keep existing state combat_over: true, player_won: data.player_won, player_fled: playerFled, // Track if player fled combat: { ...combatState.combat, npc_name: enemyName, // Keep original enemy name npc_image: enemyImage, // Keep original enemy image npc_hp: data.player_won ? 0 : (combatState.combat?.npc_hp || 0) // Don't set HP to 0 on flee } }) } else { // Update combat state for next turn, but preserve enemy info // Keep the original stored enemy name/image (from state variables) setCombatState({ ...data, combat: { ...data.combat, npc_name: enemyName, // Use stored enemy name npc_image: enemyImage // Use stored enemy image } }) } } catch (error: any) { const now = new Date() const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) setCombatLog((prev: any) => [{ time: timeStr, message: error.response?.data?.detail || 'Combat action failed', isPlayer: false }, ...prev]) } } const handleExitCombat = () => { setCombatState(null) setCombatLog([]) setEnemyName('') setEnemyImage('') fetchGameData() // Refresh game state } const handleExitPvPCombat = async () => { if (combatState?.pvp_combat?.id) { try { await api.post('/api/game/pvp/acknowledge', { combat_id: combatState.pvp_combat.id }) } catch (error) { console.error('Failed to acknowledge PvP combat:', error) } } setCombatState(null) setCombatLog([]) setLastSeenPvPAction(null) lastSeenPvPActionRef.current = null // Clear ref too fetchGameData() // Refresh game state } const handleInitiatePvP = async (targetPlayerId: number) => { try { const response = await api.post('/api/game/pvp/initiate', { target_player_id: targetPlayerId }) setMessage(response.data.message || 'PvP combat initiated!') await fetchGameData() // Refresh to show combat state } catch (error: any) { setMessage(error.response?.data?.detail || 'Failed to initiate PvP') } } const handlePvPAction = async (action: string) => { try { const response = await api.post('/api/game/pvp/action', { action }) const data = response.data // Add message to combat log const now = new Date() const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) if (data.message) { const messages = data.message.split('\n').filter((m: string) => m.trim()) const logEntries = messages.map((msg: string) => ({ time: timeStr, message: msg, isPlayer: msg.includes('You ') || msg.includes('Your ') })) setCombatLog((prev: any) => [...logEntries, ...prev]) } // Refresh combat state await fetchGameData() // If combat is over, show message if (data.combat_over) { setMessage(data.message || 'Combat ended!') } } catch (error: any) { const now = new Date() const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) setCombatLog((prev: any) => [{ time: timeStr, message: error.response?.data?.detail || 'PvP action failed', isPlayer: false }, ...prev]) } } const handleItemAction = async (action: string, itemId: number) => { switch (action) { case 'use': await handleUseItem(itemId.toString()) break case 'equip': await handleEquipItem(itemId) break case 'unequip': // Find the slot this item is equipped in const equippedSlot = Object.keys(equipment).find(slot => equipment[slot]?.id === itemId) if (equippedSlot) { await handleUnequipItem(equippedSlot) } break case 'drop': await handleDropItem(itemId.toString(), 1) break } setSelectedItem(null) } if (loading) { return
Loading game...
} if (!playerState || !location) { return
Failed to load game state
} // Helper function to get direction details const getDirectionDetail = (direction: string) => { if (!location.directions_detailed) return null return location.directions_detailed.find(d => d.direction === direction) } // Helper function to get stamina cost for a direction const getStaminaCost = (direction: string): number => { const detail = getDirectionDetail(direction) return detail ? detail.stamina_cost : 5 } // Helper function to get destination name for a direction const getDestinationName = (direction: string): string => { const detail = getDirectionDetail(direction) return detail ? (detail.destination_name || detail.destination) : '' } // Helper function to get distance for a direction const getDistance = (direction: string): number => { const detail = getDirectionDetail(direction) return detail ? detail.distance : 0 } // Helper function to check if direction is available const hasDirection = (direction: string): boolean => { return location.directions.includes(direction) } // Helper function to render compass button const renderCompassButton = (direction: string, arrow: string, className: string) => { const available = hasDirection(direction) const stamina = getStaminaCost(direction) const destination = getDestinationName(direction) const distance = getDistance(direction) const disabled = !available || !!combatState || movementCooldown > 0 // Build detailed tooltip text const tooltipText = movementCooldown > 0 ? `Wait ${movementCooldown}s before moving` : combatState ? 'Cannot travel during combat' : available ? `${destination}\nDistance: ${distance}m\nStamina: ${stamina}` : `Cannot go ${direction}` return ( ) } const renderExploreTab = () => (
{/* Left Sidebar: Movement & Surroundings */}
{/* Movement Controls */}

🧭 Travel

{/* Top row */} {renderCompassButton('northwest', '↖', 'nw')} {renderCompassButton('north', '↑', 'n')} {renderCompassButton('northeast', '↗', 'ne')} {/* Middle row */} {renderCompassButton('west', '←', 'w')}
🧭
{renderCompassButton('east', '→', 'e')} {/* Bottom row */} {renderCompassButton('southwest', '↙', 'sw')} {renderCompassButton('south', '↓', 's')} {renderCompassButton('southeast', '↘', 'se')}
{/* Cooldown indicator */} {movementCooldown > 0 && (
⏳ Wait {movementCooldown}s before moving
)} {/* Special movements */}
{location.directions.includes('up') && ( )} {location.directions.includes('down') && ( )} {location.directions.includes('enter') && ( )} {location.directions.includes('inside') && ( )} {location.directions.includes('exit') && ( )} {location.directions.includes('outside') && ( )}
{/* Surroundings */} {(location.interactables && location.interactables.length > 0) && (

🌿 Surroundings

{/* Interactables */} {location.interactables && location.interactables.map((interactable: any) => (
{interactable.image_path && (
{interactable.name} { e.currentTarget.style.display = 'none'; }} />
)}
{interactable.name} {interactable.on_cooldown && }
{interactable.actions && interactable.actions.length > 0 && (
{interactable.actions.map((action: any) => ( ))}
)}
))}
)}
{/* Close left-sidebar */} {/* Center: Location/Combat Content */}
{combatState ? ( /* Combat View */

{combatState.is_pvp ? '⚔️ PvP Combat' : `⚔️ Combat - ${enemyName || combatState.combat?.npc_name || 'Enemy'}`}

{combatState.is_pvp ? ( /* PvP Combat UI */
{/* Opponent Info */}
{(() => { const opponent = combatState.pvp_combat.is_attacker ? combatState.pvp_combat.defender : combatState.pvp_combat.attacker return ( <>

🗡️ {opponent.username}

Level {opponent.level}
HP: {opponent.hp} / {opponent.max_hp}
) })()}
{/* Your Info */}
{(() => { const you = combatState.pvp_combat.is_attacker ? combatState.pvp_combat.attacker : combatState.pvp_combat.defender return ( <>

🛡️ You

Level {you.level}
HP: {you.hp} / {you.max_hp}
) })()}
{combatState.pvp_combat.combat_over ? ( {combatState.pvp_combat.attacker_fled || combatState.pvp_combat.defender_fled ? "🏃 Combat Ended" : "💀 Combat Over"} ) : combatState.pvp_combat.your_turn ? ( ✅ Your Turn ({combatState.pvp_combat.time_remaining}s) ) : ( ⏳ Opponent's Turn ({combatState.pvp_combat.time_remaining}s) )}
{!combatState.pvp_combat.combat_over ? ( <> ) : ( )}
) : ( /* PvE Combat UI */ <>
{enemyName
Enemy HP: {combatState.combat?.npc_hp || 0} / {combatState.combat?.npc_max_hp || 100}
{playerState && (
Your HP: {playerState.health} / {playerState.max_health}
)}
{!combatState.combat_over ? ( enemyTurnMessage ? ( 🗡️ Enemy's turn... ) : combatState.combat?.turn === 'player' ? ( ✅ Your Turn ) : ( ⚠️ Enemy Turn ) ) : ( {combatState.player_won ? "✅ Victory!" : combatState.player_fled ? "🏃 Escaped!" : "💀 Defeated"} )}
{!combatState.combat_over ? ( <> ) : ( )}
)} {/* Combat Log */}

Combat Log:

{combatLog.map((entry: any, i: number) => (
{entry.time} {entry.isPlayer ? '→' : '←'} {entry.message}
))}
) : ( /* Normal Location View */ <>

{location.name} {location.danger_level !== undefined && location.danger_level === 0 && ( ✓ Safe )} {location.danger_level !== undefined && location.danger_level > 0 && ( ⚠️ {location.danger_level} )}

{location.tags && location.tags.length > 0 && (
{location.tags.map((tag: string, i: number) => { const isClickable = tag === 'workbench' || tag === 'repair_station' const handleClick = () => { if (tag === 'workbench') handleOpenCrafting() else if (tag === 'repair_station') handleOpenRepair() } return ( {tag === 'workbench' && '🔧 Workbench'} {tag === 'repair_station' && '🛠️ Repair Station'} {tag === 'safe_zone' && '🛡️ Safe Zone'} {tag === 'shop' && '🏪 Shop'} {tag === 'shelter' && '🏠 Shelter'} {tag === 'medical' && '⚕️ Medical'} {tag === 'storage' && '📦 Storage'} {tag === 'water_source' && '💧 Water'} {tag === 'food_source' && '🍎 Food'} {tag !== 'workbench' && tag !== 'repair_station' && tag !== 'safe_zone' && tag !== 'shop' && tag !== 'shelter' && tag !== 'medical' && tag !== 'storage' && tag !== 'water_source' && tag !== 'food_source' && `🏷️ ${tag}`} ) })}
)} {/* Workbench Menu (Crafting, Repair, Uncraft) */} {(showCraftingMenu || showRepairMenu) && (

🔧 Workbench

{/* Tabs */}
{/* Craft Tab */} {workbenchTab === 'craft' && (
setCraftFilter(e.target.value)} className="filter-input" />
{craftableItems.filter(item => item.name.toLowerCase().includes(craftFilter.toLowerCase()) && (craftCategoryFilter === 'all' || item.category === craftCategoryFilter) ).length === 0 &&

No craftable items found

} {craftableItems .filter(item => item.name.toLowerCase().includes(craftFilter.toLowerCase()) && (craftCategoryFilter === 'all' || item.category === craftCategoryFilter) ) .map((item: any) => (
{item.emoji} {item.name} {item.slot && [{item.slot}]}
{item.description &&

{item.description}

} {/* Level requirement */} {item.craft_level && item.craft_level > 1 && (
📊 Requires Level {item.craft_level} {item.meets_level ? '✅' : `❌ (You are level ${profile?.level || 1})`}
)} {/* Tool requirements */} {item.tools && item.tools.length > 0 && (

🔧 Required Tools:

{item.tools.map((tool: any, i: number) => (
{tool.emoji} {tool.name} (-{tool.durability_cost} durability) {tool.has_tool && ` [${tool.tool_durability} available]`} {!tool.has_tool && ' ❌'}
))}
)} {/* Materials */}

📦 Materials:

{item.materials.map((mat: any, i: number) => (
{mat.emoji} {mat.name} {mat.available}/{mat.required}
))}
))}
)} {/* Repair Tab */} {workbenchTab === 'repair' && (
setRepairFilter(e.target.value)} className="filter-input" />
{repairableItems.filter(item => item.name.toLowerCase().includes(repairFilter.toLowerCase()) ).length === 0 &&

No repairable items found

} {repairableItems .filter(item => item.name.toLowerCase().includes(repairFilter.toLowerCase())) .map((item: any, idx: number) => (
{item.emoji} {item.name} {item.location === 'equipped' && ⚔️ Equipped} {item.location === 'inventory' && 🎒 Inventory}
{item.current_durability}/{item.max_durability}
{!item.needs_repair && (

✅ At full durability

)} {item.needs_repair && ( <> {/* Tool requirements */} {item.tools && item.tools.length > 0 && (

🔧 Required Tools:

{item.tools.map((tool: any, i: number) => (
{tool.emoji} {tool.name} (-{tool.durability_cost} durability) {tool.has_tool && ` [${tool.tool_durability} available]`} {!tool.has_tool && ' ❌'}
))}
)} {/* Materials */}

Restores {item.repair_percentage}% durability

{item.materials.map((mat: any, i: number) => (
{mat.emoji} {mat.name} {mat.available}/{mat.quantity}
))}
)}
))}
)} {/* Uncraft Tab */} {workbenchTab === 'uncraft' && (
setUncraftFilter(e.target.value)} className="filter-input" />
{uncraftableItems.filter(item => item.name.toLowerCase().includes(uncraftFilter.toLowerCase()) ).length === 0 &&

No uncraftable items found

} {uncraftableItems .filter((item: any) => item.name.toLowerCase().includes(uncraftFilter.toLowerCase())) .map((item: any, idx: number) => { // Calculate adjusted yield based on durability const durabilityRatio = item.unique_item_data ? item.unique_item_data.durability_percent / 100 : 1.0 const adjustedYield = item.base_yield.map((mat: any) => ({ ...mat, adjusted_quantity: Math.floor(mat.quantity * durabilityRatio) })) return (
{item.emoji} {item.name}
{/* Unique item details */} {item.unique_item_data && (
{/* Durability bar */}
🔧 Durability: {item.unique_item_data.current_durability}/{item.unique_item_data.max_durability} ({item.unique_item_data.durability_percent}%)
{/* Format stats nicely */} {item.unique_item_data.unique_stats && Object.keys(item.unique_item_data.unique_stats).length > 0 && (
{Object.entries(item.unique_item_data.unique_stats).map(([stat, value]: [string, any]) => { // Format stat names and values let displayName = stat.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()) let displayValue = value // Combine min/max stats if (stat === 'damage_min' && item.unique_item_data.unique_stats.damage_max) { displayName = 'Damage' displayValue = `${value}-${item.unique_item_data.unique_stats.damage_max}` return ( ⚔️ {displayName}: {displayValue} ) } else if (stat === 'damage_max') { return null // Skip, already shown with damage_min } else if (stat === 'armor') { return ( 🛡️ {displayName}: {displayValue} ) } else { return ( {displayName}: {displayValue} ) } })}
)}
)} {/* Durability impact warning */} {durabilityRatio < 1.0 && (
⚠️ Item condition will reduce yield by {Math.round((1 - durabilityRatio) * 100)}%
)} {durabilityRatio < 0.1 && (
❌ Item too damaged - will yield NO materials!
)} {/* Loss chance warning */} {item.loss_chance && (
⚠️ {Math.round(item.loss_chance * 100)}% chance to lose each material
)} {/* Yield materials with durability adjustment */} {adjustedYield && adjustedYield.length > 0 && (

♻️ Expected yield:

{adjustedYield.map((mat: any, i: number) => (
{mat.emoji} {mat.name} {durabilityRatio < 1.0 && durabilityRatio >= 0.1 ? ( <> x{mat.quantity} {' → '} x{mat.adjusted_quantity} ) : durabilityRatio < 0.1 ? ( x0 ) : ( <>x{mat.quantity} )}
))} {durabilityRatio >= 0.1 && (

* Subject to {Math.round((item.loss_chance || 0.3) * 100)}% random loss per material

)}
)}
) })}
)}
)} {location.image_url && (
{location.name} (e.currentTarget.style.display = 'none')} />
)}

{location.description}

{message && (
setMessage('')}> {message}
)} {/* NPCs, Items, and Entities on ground - below the location image */}
{/* Enemies */} {location.npcs.filter((npc: any) => npc.type === 'enemy').length > 0 && (

⚔️ Enemies

{location.npcs.filter((npc: any) => npc.type === 'enemy').map((enemy: any, i: number) => (
{enemy.id && (
{enemy.name} { e.currentTarget.style.display = 'none'; }} />
)}
{enemy.name}
{enemy.level &&
Lv. {enemy.level}
}
))}
)} {/* Corpses */} {location.corpses && location.corpses.length > 0 && (

💀 Corpses

{location.corpses.map((corpse: any) => (
{corpse.emoji} {corpse.name}
{corpse.loot_count} item(s)
{/* Expanded corpse details */} {expandedCorpse === corpse.id && corpseDetails && (

Lootable Items:

{corpseDetails.loot_items.map((item: any) => (
{item.emoji} {item.item_name}
Qty: {item.quantity_min}{item.quantity_min !== item.quantity_max ? `-${item.quantity_max}` : ''}
{item.required_tool && (
🔧 {item.required_tool_name} {item.has_tool ? '✓' : '✗'}
)}
))}
)}
))}
)} {/* NPCs */} {location.npcs.filter((npc: any) => npc.type !== 'enemy').length > 0 && (

👥 NPCs

{location.npcs.filter((npc: any) => npc.type !== 'enemy').map((npc: any, i: number) => (
🧑
{npc.name}
{npc.level &&
Lv. {npc.level}
}
))}
)} {location.items.length > 0 && (

📦 Items on Ground

{location.items.map((item: any, i: number) => (
{item.emoji || '📦'}
{item.name || 'Unknown Item'}
{item.quantity > 1 &&
×{item.quantity}
}
{item.description &&
{item.description}
} {item.weight !== undefined && item.weight > 0 && (
⚖️ Weight: {item.weight}kg {item.quantity > 1 && `(Total: ${(item.weight * item.quantity).toFixed(2)}kg)`}
)} {item.volume !== undefined && item.volume > 0 && (
📦 Volume: {item.volume}L {item.quantity > 1 && `(Total: ${(item.volume * item.quantity).toFixed(2)}L)`}
)} {item.hp_restore && item.hp_restore > 0 && (
❤️ HP Restore: +{item.hp_restore}
)} {item.stamina_restore && item.stamina_restore > 0 && (
⚡ Stamina Restore: +{item.stamina_restore}
)} {item.damage_min !== undefined && item.damage_max !== undefined && (item.damage_min > 0 || item.damage_max > 0) && (
⚔️ Damage: {item.damage_min}-{item.damage_max}
)} {item.durability !== undefined && item.durability !== null && item.max_durability !== undefined && item.max_durability !== null && (
🔧 Durability: {item.durability}/{item.max_durability}
)} {item.tier !== undefined && item.tier !== null && item.tier > 0 && (
⭐ Tier: {item.tier}
)}
{item.quantity === 1 ? ( ) : (
{item.quantity >= 5 && ( )} {item.quantity >= 10 && ( )}
)}
))}
)} {/* Other Players */} {location.other_players && location.other_players.length > 0 && (

👥 Other Players

{location.other_players.map((player: any, i: number) => (
🧍
{player.username}
Lv. {player.level}
{player.level_diff !== undefined && (
{player.level_diff > 0 ? `+${player.level_diff}` : player.level_diff} levels
)}
{player.can_pvp && ( )} {!player.can_pvp && player.level_diff !== undefined && Math.abs(player.level_diff) > 3 && (
Level difference too high
)} {!player.can_pvp && location.danger_level !== undefined && location.danger_level < 3 && (
Area too safe for PvP
)}
))}
)}
)}
{/* Right Sidebar: Profile & Inventory */}
{/* Profile Stats */}

👤 Character

{/* Health & Stamina Bars */}
❤️ HP {playerState.health}/{playerState.max_health}
{Math.round((playerState.health / playerState.max_health) * 100)}%
⚡ Stamina {playerState.stamina}/{playerState.max_stamina}
{Math.round((playerState.stamina / playerState.max_stamina) * 100)}%
{/* Character Info */} {profile && (
Level: {profile.level}
{/* XP Progress Bar */}
⭐ XP {profile.xp} / {(profile.level * 100)}
{Math.round((profile.xp / (profile.level * 100)) * 100)}%
{profile.unspent_points > 0 && (
⭐ Unspent: {profile.unspent_points}
)}
💪 STR: {profile.strength} {profile.unspent_points > 0 && ( )}
🏃 AGI: {profile.agility} {profile.unspent_points > 0 && ( )}
🛡️ END: {profile.endurance} {profile.unspent_points > 0 && ( )}
🧠 INT: {profile.intellect} {profile.unspent_points > 0 && ( )}
)}
{/* Equipment Display */}

⚔️ Equipment

{/* Row 1: Head */}
{equipment.head ? ( <>
{equipment.head.emoji} {equipment.head.name} {equipment.head.durability && equipment.head.durability !== null && ( {equipment.head.durability}/{equipment.head.max_durability} )}
{equipment.head.description &&
{equipment.head.description}
} {equipment.head.stats && Object.keys(equipment.head.stats).length > 0 && (
📊 Stats: {Object.entries(equipment.head.stats).map(([key, val]) => `${key}: ${val}`).join(', ')}
)} {equipment.head.durability !== undefined && equipment.head.durability !== null && (
🔧 Durability: {equipment.head.durability}/{equipment.head.max_durability}
)} {equipment.head.tier !== undefined && equipment.head.tier !== null && equipment.head.tier > 0 && (
⭐ Tier: {equipment.head.tier}
)}
) : ( <> 🪖 Head )}
{/* Row 2: Weapon, Torso, Backpack */}
{equipment.weapon ? ( <>
{equipment.weapon.emoji} {equipment.weapon.name} {equipment.weapon.durability && equipment.weapon.durability !== null && ( {equipment.weapon.durability}/{equipment.weapon.max_durability} )}
{equipment.weapon.description &&
{equipment.weapon.description}
} {equipment.weapon.stats && Object.keys(equipment.weapon.stats).length > 0 && (
⚔️ Damage: {equipment.weapon.stats.damage_min}-{equipment.weapon.stats.damage_max}
)} {equipment.weapon.weapon_effects && Object.keys(equipment.weapon.weapon_effects).length > 0 && (
✨ Effects: {Object.entries(equipment.weapon.weapon_effects).map(([key, val]: [string, any]) => `${key} (${(val.chance * 100).toFixed(0)}%)`).join(', ')}
)} {equipment.weapon.durability !== undefined && equipment.weapon.durability !== null && (
🔧 Durability: {equipment.weapon.durability}/{equipment.weapon.max_durability}
)} {equipment.weapon.tier !== undefined && equipment.weapon.tier !== null && equipment.weapon.tier > 0 && (
⭐ Tier: {equipment.weapon.tier}
)}
) : ( <> ⚔️ Weapon )}
{equipment.torso ? ( <>
{equipment.torso.emoji} {equipment.torso.name} {equipment.torso.durability && equipment.torso.durability !== null && ( {equipment.torso.durability}/{equipment.torso.max_durability} )}
{equipment.torso.description &&
{equipment.torso.description}
} {equipment.torso.stats && Object.keys(equipment.torso.stats).length > 0 && (
📊 Stats: {Object.entries(equipment.torso.stats).map(([key, val]) => `${key}: ${val}`).join(', ')}
)} {equipment.torso.durability !== undefined && equipment.torso.durability !== null && (
🔧 Durability: {equipment.torso.durability}/{equipment.torso.max_durability}
)} {equipment.torso.tier !== undefined && equipment.torso.tier !== null && equipment.torso.tier > 0 && (
⭐ Tier: {equipment.torso.tier}
)}
) : ( <> 👕 Torso )}
{equipment.backpack ? ( <>
{equipment.backpack.emoji} {equipment.backpack.name} {equipment.backpack.durability && equipment.backpack.durability !== null && ( {equipment.backpack.durability}/{equipment.backpack.max_durability} )}
{equipment.backpack.description &&
{equipment.backpack.description}
} {equipment.backpack.stats && Object.keys(equipment.backpack.stats).length > 0 && (
📦 Capacity: Weight +{equipment.backpack.stats.weight_capacity}kg, Volume +{equipment.backpack.stats.volume_capacity}L
)} {equipment.backpack.durability !== undefined && equipment.backpack.durability !== null && (
🔧 Durability: {equipment.backpack.durability}/{equipment.backpack.max_durability}
)} {equipment.backpack.tier !== undefined && equipment.backpack.tier !== null && equipment.backpack.tier > 0 && (
⭐ Tier: {equipment.backpack.tier}
)}
) : ( <> 🎒 Backpack )}
{/* Row 3: Legs */}
{equipment.legs ? ( <>
{equipment.legs.emoji} {equipment.legs.name} {equipment.legs.durability && equipment.legs.durability !== null && ( {equipment.legs.durability}/{equipment.legs.max_durability} )}
{equipment.legs.description &&
{equipment.legs.description}
} {equipment.legs.stats && Object.keys(equipment.legs.stats).length > 0 && (
📊 Stats: {Object.entries(equipment.legs.stats).map(([key, val]) => `${key}: ${val}`).join(', ')}
)} {equipment.legs.durability !== undefined && equipment.legs.durability !== null && (
🔧 Durability: {equipment.legs.durability}/{equipment.legs.max_durability}
)} {equipment.legs.tier !== undefined && equipment.legs.tier !== null && equipment.legs.tier > 0 && (
⭐ Tier: {equipment.legs.tier}
)}
) : ( <> 👖 Legs )}
{/* Row 4: Feet */}
{equipment.feet ? ( <>
{equipment.feet.emoji} {equipment.feet.name} {equipment.feet.durability && equipment.feet.durability !== null && ( {equipment.feet.durability}/{equipment.feet.max_durability} )}
{equipment.feet.description &&
{equipment.feet.description}
} {equipment.feet.stats && Object.keys(equipment.feet.stats).length > 0 && (
📊 Stats: {Object.entries(equipment.feet.stats).map(([key, val]) => `${key}: ${val}`).join(', ')}
)} {equipment.feet.durability !== undefined && equipment.feet.durability !== null && (
🔧 Durability: {equipment.feet.durability}/{equipment.feet.max_durability}
)} {equipment.feet.tier !== undefined && equipment.feet.tier !== null && equipment.feet.tier > 0 && (
⭐ Tier: {equipment.feet.tier}
)}
) : ( <> 👟 Feet )}
{/* Enhanced Inventory */}

🎒 Inventory

{/* Weight and Volume Bars */}
⚖️ Weight {profile?.current_weight || 0}/{profile?.max_weight || 100}
{Math.round(Math.min(((profile?.current_weight || 0) / (profile?.max_weight || 100)) * 100, 100))}%
📦 Volume {profile?.current_volume || 0}/{profile?.max_volume || 100}
{Math.round(Math.min(((profile?.current_volume || 0) / (profile?.max_volume || 100)) * 100, 100))}%
{/* Inventory Items - Grouped by Category */}
{playerState.inventory.filter((item: any) => !item.is_equipped).length === 0 ? (

No items

) : ( Object.entries( playerState.inventory .filter((item: any) => !item.is_equipped) .reduce((acc: any, item: any) => { const category = item.type || 'misc' if (!acc[category]) acc[category] = [] acc[category].push(item) return acc }, {}) ).sort(([catA], [catB]) => catA.localeCompare(catB)) .map(([category, items]: [string, any]) => { const isCollapsed = collapsedCategories.has(category) const sortedItems = (items as any[]).sort((a, b) => a.name.localeCompare(b.name)) return (
{ const newSet = new Set(collapsedCategories) if (isCollapsed) { newSet.delete(category) } else { newSet.add(category) } setCollapsedCategories(newSet) }} > {isCollapsed ? '▶' : '▼'} {category === 'weapon' ? '⚔️ Weapons' : category === 'armor' ? '🛡️ Armor' : category === 'consumable' ? '🍖 Consumables' : category === 'resource' ? '📦 Resources' : category === 'quest' ? '📜 Quest Items' : `📦 ${category.charAt(0).toUpperCase() + category.slice(1)}`} ({sortedItems.length})
{!isCollapsed && sortedItems.map((item: any, i: number) => (
{item.emoji || '📦'}
{item.name} {item.quantity > 1 && ×{item.quantity}} {item.hp_restore > 0 && +{item.hp_restore}❤️} {item.stamina_restore > 0 && +{item.stamina_restore}⚡}
{item.consumable && ( )} {item.equippable && !item.is_equipped && ( )}
{item.description &&
{item.description}
} {item.weight !== undefined && item.weight > 0 && (
⚖️ Weight: {item.weight}kg {item.quantity > 1 && `(Total: ${(item.weight * item.quantity).toFixed(2)}kg)`}
)} {item.volume !== undefined && item.volume > 0 && (
📦 Volume: {item.volume}L {item.quantity > 1 && `(Total: ${(item.volume * item.quantity).toFixed(2)}L)`}
)} {item.hp_restore && item.hp_restore > 0 && (
❤️ HP Restore: +{item.hp_restore}
)} {item.stamina_restore && item.stamina_restore > 0 && (
⚡ Stamina Restore: +{item.stamina_restore}
)} {item.damage_min !== undefined && item.damage_max !== undefined && (item.damage_min > 0 || item.damage_max > 0) && (
⚔️ Damage: {item.damage_min}-{item.damage_max}
)} {item.durability !== undefined && item.durability !== null && item.max_durability !== undefined && item.max_durability !== null && (
🔧 Durability: {item.durability}/{item.max_durability}
)} {item.tier !== undefined && item.tier !== null && item.tier > 0 && (
⭐ Tier: {item.tier}
)}
{item.quantity === 1 ? ( ) : (
{item.quantity >= 5 && ( )} {item.quantity >= 10 && ( )}
)}
))}
) }) )}
{/* Item Actions Panel */} {selectedItem && (
{selectedItem.name}
{selectedItem.description && (

{selectedItem.description}

)}
{selectedItem.usable && ( )} {selectedItem.equippable && !selectedItem.is_equipped && ( )} {selectedItem.is_equipped && ( )}
)}
) return (
{/* Mobile Header Toggle - only show in main view */} {mobileMenuOpen === 'none' && ( )}
{renderExploreTab()} {/* Mobile Tab Navigation */}
{/* Mobile Menu Overlays */} {mobileMenuOpen !== 'none' && (
setMobileMenuOpen('none')} /> )}
) } export default Game