(() => { 'use strict'; const POLL_MS = 1000; const $ = (id) => document.getElementById(id); let itemNames = {}; let lastInventorySignature = ''; let lastState = null; const escapeHTML = (value) => String(value ?? '').replace(/[&<>"']/g, (ch) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[ch]); function prettyName(name) { return String(name || 'unknown item').replace(/^minecraft:/, '') .replaceAll('_', ' ').replace(/\b\w/g, (letter) => letter.toUpperCase()); } function vectorText(pos) { if (!pos) return '—'; if (typeof pos === 'object' && ['x', 'y', 'z'].every((key) => key in pos)) { return `${Number(pos.x).toFixed(1)}, ${Number(pos.y).toFixed(1)}, ${Number(pos.z).toFixed(1)}`; } const match = String(pos).match(/-?\d+(?:\.\d+)?/g); return match && match.length >= 3 ? match.slice(0, 3).join(', ') : String(pos); } function dimensionName(name) { return String(name || 'unknown').replace(/^minecraft:/, '').replaceAll('_', ' ').toUpperCase(); } function formatMode(mode) { const modes = {0: 'Survival', 1: 'Creative', 2: 'Adventure', 3: 'Spectator'}; return modes[mode] || (mode == null ? '—' : String(mode)); } function textureCandidates(name) { const id = String(name || '').replace(/^minecraft:/, '').toLowerCase(); if (!/^[a-z0-9_]+$/.test(id) || id === 'air') return []; // Prefer bundled icons, then the same-origin asset proxy. The external // sources remain a last resort for uncached items on older bot processes. const local = `/static/item-icons/${encodeURIComponent(id)}.png`; const proxy = `/api/item-icon/${encodeURIComponent(id)}`; const root = 'https://assets.mcasset.cloud/26.3/assets/minecraft/textures'; const paths = [`item/${id}.png`, `block/${id}.png`]; const simplified = id.replace(/_(slab|stairs|wall|fence_gate|fence|door|trapdoor|button|pressure_plate|sign|hanging_sign|carpet|banner|bed|candle)$/, ''); if (simplified !== id) paths.push(`block/${simplified}.png`, `item/${simplified}.png`); return [local, proxy, ...[...new Set(paths)].map((path) => `${root}/${path}`)]; } function renderItemIcon(img, fallback, itemName) { const candidates = textureCandidates(itemName); let index = 0; const tryNext = () => { if (index >= candidates.length) { img.remove(); fallback.hidden = false; fallback.textContent = prettyName(itemName).slice(0, 2).toUpperCase(); return; } img.src = candidates[index++]; }; img.onerror = tryNext; img.onload = () => { fallback.hidden = true; }; tryNext(); } function inventorySignature(inv, partial) { return JSON.stringify([Boolean(partial), Object.entries(inv || {}).map(([slot, item]) => [ slot, item && item.present, item && item.item_id, item && item.item_count ])]); } function renderInventory(state) { const inv = state.inv || {}; const signature = inventorySignature(inv, state.inventory_partial); if (signature === lastInventorySignature) return; lastInventorySignature = signature; const grid = $('inventory-grid'); const items = []; let stackCount = 0; let totalCount = 0; for (let slot = 0; slot < 46; slot++) { const item = inv[String(slot)] || inv[slot]; if (item && item.present) { stackCount++; totalCount += Number(item.item_count || 0); } items.push({slot, item}); } grid.innerHTML = items.map(({slot, item}) => { const present = Boolean(item && item.present); const itemId = present ? item.item_id : null; const name = present ? (itemNames[String(itemId)] || `item_${itemId}`) : ''; const count = present ? Number(item.item_count || 0) : 0; const title = present ? `${prettyName(name)} × ${count} (id ${itemId}, slot ${slot})` : `Empty slot ${slot}`; return `
${slot} ${present ? ` ${count > 1 ? count : ''}` : ''}
`; }).join(''); items.forEach(({slot, item}) => { if (!item || !item.present) return; const icon = grid.children[slot]?.querySelector('img'); const fallback = grid.children[slot]?.querySelector('.icon-fallback'); if (icon && fallback) renderItemIcon(icon, fallback, itemNames[String(item.item_id)] || `item_${item.item_id}`); }); $('inventory-count').textContent = `${totalCount} items`; $('inventory-slots').textContent = `${stackCount} / 46 slots`; $('inventory-warning').hidden = !state.inventory_partial; } function renderPlayers(state) { const players = Object.values(state.players || {}); $('player-count').textContent = String(players.length); if (!players.length) { $('player-list').innerHTML = '
No player data in range
'; return; } $('player-list').innerHTML = players.map((player) => { const uuid = String(player.player_uuid || ''); const name = state.player_names?.[uuid] || state.player_names?.[uuid.toLowerCase()] || `Player ${player.entity_id}`; const pos = [player.x, player.y, player.z].map((n) => Number(n).toFixed(0)).join(', '); return `
${escapeHTML(name.slice(0, 1).toUpperCase())} ${escapeHTML(name)}${escapeHTML(pos)}
`; }).join(''); } function render(state) { lastState = state; const online = Boolean(state.connected) && state.connection_status === 'in_game'; const pill = $('connection-pill'); pill.classList.toggle('is-online', online); pill.classList.toggle('is-offline', !online); pill.querySelector('span').textContent = online ? 'ONLINE' : (state.connection_status || 'OFFLINE').toUpperCase(); $('bot-name').textContent = state.name || 'Mosfet'; $('session-subtitle').textContent = online ? `${formatMode(state.game_mode)} mode · protocol 777 · AuthMe session active` : `Connection state: ${state.connection_status || 'disconnected'}`; $('position').textContent = vectorText(state.pos || state.player_position); $('dimension').textContent = dimensionName(state.dimension); const health = state.health == null ? NaN : Number(state.health); const food = state.food == null ? NaN : Number(state.food); const saturation = state.food_saturation == null ? NaN : Number(state.food_saturation); $('health-value').textContent = Number.isFinite(health) ? Number(health.toFixed(1)).toString() : '—'; $('food-value').textContent = Number.isFinite(food) ? String(food) : '—'; $('food-saturation').textContent = Number.isFinite(saturation) ? `Saturation ${saturation.toFixed(2)}` : 'Saturation —'; $('health-meter').style.width = `${Math.max(0, Math.min(100, (health || 0) * 5))}%`; $('food-meter').style.width = `${Math.max(0, Math.min(100, (food || 0) * 5))}%`; const movement = state.movement_status || (state.path?.length ? 'following_path' : 'idle'); $('movement-value').textContent = String(movement).replaceAll('_', ' ').toUpperCase(); $('movement-detail').textContent = state.path?.length ? `${state.path.length} nodes remaining` : 'No active path'; $('chunk-count').textContent = state.chunk_state?.loaded ?? '—'; const center = state.chunk_state?.center; $('chunk-detail').textContent = center ? `Center ${center[0]}, ${center[1]} · radius ${state.chunk_state.radius ?? '—'}` : 'Waiting for world data'; $('game-mode').textContent = formatMode(state.game_mode); $('job-state').textContent = prettyName(state.job_state || 'idle').toUpperCase(); $('path-length').textContent = String(state.path?.length || 0); $('entity-count').textContent = `${Object.keys(state.mobs || {}).length} mobs · ${Object.keys(state.objects || {}).length} drops`; $('afk-state').textContent = state.afk ? 'YES' : 'NO'; $('movement-error').hidden = !state.movement_error; $('movement-error').textContent = state.movement_error ? `Movement error: ${state.movement_error}` : ''; renderInventory(state); renderPlayers(state); $('last-update').textContent = `Updated ${new Date().toLocaleTimeString()}`; } async function loadItems() { try { const response = await fetch('/api/items', {cache: 'force-cache'}); if (response.ok) itemNames = await response.json(); if (lastState) { lastInventorySignature = ''; renderInventory(lastState); } } catch (_) { // Inventory still renders with numeric IDs if the name endpoint is absent. } } async function poll() { try { const response = await fetch('/api/global', {cache: 'no-store'}); if (!response.ok) throw new Error(`API ${response.status}`); render(await response.json()); } catch (error) { const pill = $('connection-pill'); pill.classList.add('is-offline'); pill.classList.remove('is-online'); pill.querySelector('span').textContent = 'API OFFLINE'; $('session-subtitle').textContent = 'Cannot reach the bot web API'; $('last-update').textContent = `Last update failed · ${error.message}`; } } loadItems(); poll(); window.setInterval(poll, POLL_MS); })();