(() => { 'use strict'; const POLL_MS = 1000; const $ = (id) => document.getElementById(id); let itemNames = {}; let lastInventorySignature = ''; let lastState = null; let selectedInventoryItem = null; let menuAnchor = null; let toastTimer = 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 componentText(value) { if (value == null) return ''; if (typeof value === 'string' || typeof value === 'number') return String(value); if (Array.isArray(value)) return value.map(componentText).filter(Boolean).join(''); if (typeof value !== 'object') return ''; if ('raw' in value) return componentText(value.raw); if ('text' in value) return componentText(value.text) + componentText(value.extra); if ('translate' in value) { const args = Array.isArray(value.with) ? value.with.map(componentText).join(' ') : ''; return `${prettyName(value.translate)}${args ? ` ${args}` : ''}${componentText(value.extra)}`; } if ('value' in value) return componentText(value.value); return ''; } function itemDisplayName(item, itemName) { const components = item?.components || {}; return componentText(components.custom_name || components.item_name) || prettyName(itemName); } 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, item && item.components ])]); } function selectedHotbarSlot(state) { if (!state.connected) return null; const selected = Number(state.holding_slot); return Number.isInteger(selected) && selected >= 0 && selected <= 8 ? selected + 36 : null; } function updateSelectedSlot(slot) { const grid = $('inventory-grid'); grid.querySelectorAll('.selected-slot').forEach((element) => { element.classList.remove('selected-slot'); element.removeAttribute('aria-current'); }); if (slot == null) return; const selected = grid.querySelector(`.inventory-slot[data-slot="${slot}"]`); if (selected) { selected.classList.add('selected-slot'); selected.setAttribute('aria-current', 'true'); } } function renderInventory(state) { const inv = state.inv || {}; const signature = inventorySignature(inv, state.inventory_partial); const selectedSlot = selectedHotbarSlot(state); if (signature === lastInventorySignature) { updateSelectedSlot(selectedSlot); return; } lastInventorySignature = signature; hideItemTooltip(); closeInventoryMenu(); 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}); } const bySlot = new Map(items.map((entry) => [entry.slot, entry.item])); const slotHTML = (slot, layoutClass = '') => { const item = bySlot.get(slot); 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 displayName = present ? itemDisplayName(item, name) : `Empty slot ${slot}`; const label = present ? `${displayName}, ${count} item${count === 1 ? '' : 's'}, slot ${slot}` : displayName; return ``; }; const slotsHTML = (first, last) => { let html = ''; for (let slot = first; slot <= last; slot++) html += slotHTML(slot); return html; }; grid.innerHTML = `
OFFHAND
CRAFTING
OUTPUT
${slotHTML(45, 'crafting-offhand')} ${slotHTML(1, 'crafting-input crafting-input-1')} ${slotHTML(2, 'crafting-input crafting-input-2')} ${slotHTML(3, 'crafting-input crafting-input-3')} ${slotHTML(4, 'crafting-input crafting-input-4')} → ${slotHTML(0, 'crafting-result')}
ARMOUR
INVENTORY
${slotsHTML(5, 8)}
${slotsHTML(9, 35)}
HOTBAR
${slotsHTML(36, 44)}
`; items.forEach(({slot, item}) => { if (!item || !item.present) return; const element = grid.querySelector(`.inventory-slot[data-slot="${slot}"]`); const icon = element?.querySelector('img'); const fallback = element?.querySelector('.icon-fallback'); if (icon && fallback) renderItemIcon(icon, fallback, itemNames[String(item.item_id)] || `item_${item.item_id}`); }); updateSelectedSlot(selectedSlot); $('inventory-count').textContent = `${totalCount} items`; $('inventory-slots').textContent = `${stackCount} / 46 slots`; $('inventory-warning').hidden = !state.inventory_partial; } function romanNumeral(number) { let value = Math.max(0, Math.floor(Number(number) || 0)); const numerals = [[1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'], [100, 'C'], [90, 'XC'], [50, 'L'], [40, 'XL'], [10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I']]; let result = ''; for (const [amount, glyph] of numerals) { while (value >= amount) { result += glyph; value -= amount; } } return result; } function summarizeProperty(value) { if (typeof value === 'boolean') return value ? 'Yes' : 'No'; if (typeof value === 'string' || typeof value === 'number') return String(value); if (Array.isArray(value)) return `${value.length} entries`; if (value && typeof value === 'object') { const text = componentText(value); return text || `${Object.keys(value).length} fields`; } return ''; } function tooltipHTML(item, itemName, slot) { const components = item.components || {}; const displayName = itemDisplayName(item, itemName); const rarity = String(components.rarity || 'common').toLowerCase(); const titleClass = /^[a-z]+$/.test(rarity) ? ` rarity-${rarity}` : ''; const rows = []; const add = (text, className = '') => { if (text) rows.push(`
${escapeHTML(text)}
`); }; for (const key of ['enchantments', 'stored_enchantments']) { const enchantments = components[key]; if (!enchantments || typeof enchantments !== 'object') continue; for (const [name, level] of Object.entries(enchantments)) { add(`${prettyName(name)} ${romanNumeral(level)}`, 'tooltip-enchantment'); } } const damage = Number(components.damage); const maxDamage = Number(components.max_damage); if (Number.isFinite(damage) && Number.isFinite(maxDamage) && maxDamage > 0) { const remaining = Math.max(0, maxDamage - damage); const percent = Math.max(0, Math.min(100, remaining / maxDamage * 100)); rows.push(`
Durability${remaining} / ${maxDamage}
`); } else if (damage > 0) { add(`Damage: ${damage}`); } if (components.unbreakable) add('Unbreakable', 'tooltip-enchantment'); if (components.max_stack_size != null) add(`Maximum stack size: ${components.max_stack_size}`); const lore = Array.isArray(components.lore) ? components.lore : []; for (const line of lore) add(componentText(line), 'tooltip-lore'); const container = components.container; if (Array.isArray(container)) { const contents = container.filter((entry) => entry && entry.present).length; add(`${contents} item${contents === 1 ? '' : 's'} inside`); } const book = components.written_book_content; if (book) { const title = componentText(book.title) || 'Untitled'; add(`Book: ${title} (${(book.pages || []).length} pages)`); } if (components.dyed_color != null) { add(`Dyed color: #${(Number(components.dyed_color) & 0xffffff).toString(16).padStart(6, '0').toUpperCase()}`); } const shown = new Set([ 'custom_name', 'item_name', 'rarity', 'enchantments', 'stored_enchantments', 'damage', 'max_damage', 'unbreakable', 'max_stack_size', 'lore', 'container', 'written_book_content', 'writable_book_content', 'dyed_color', 'custom_data', 'custom_model_data', 'tooltip_display', ]); for (const [key, value] of Object.entries(components)) { if (shown.has(key) || value == null || value === false) continue; add(`${prettyName(key)}: ${summarizeProperty(value)}`); } if (item.components_unknown) add('Some item properties could not be decoded.', 'tooltip-muted'); if (!rows.length) add('No additional properties', 'tooltip-muted'); return `
${escapeHTML(displayName)}
Stack ${Number(item.item_count || 0)} · Slot ${slot} · ID ${item.item_id}
${rows.join('')}`; } function positionFloating(element, x, y) { element.style.left = '0px'; element.style.top = '0px'; const bounds = element.getBoundingClientRect(); const margin = 8; const left = Math.max(margin, Math.min(x, window.innerWidth - bounds.width - margin)); const top = y + bounds.height + margin <= window.innerHeight ? y : Math.max(margin, y - bounds.height - margin); element.style.left = `${left}px`; element.style.top = `${top}px`; } function hideItemTooltip() { const tooltip = $('item-tooltip'); if (tooltip) tooltip.hidden = true; } function showItemTooltip(slotElement, point) { if (selectedInventoryItem) return; const slot = Number(slotElement.dataset.slot); const item = lastState?.inv?.[String(slot)] || lastState?.inv?.[slot]; if (!item?.present) return hideItemTooltip(); const name = itemNames[String(item.item_id)] || `item_${item.item_id}`; const tooltip = $('item-tooltip'); tooltip.innerHTML = tooltipHTML(item, name, slot); tooltip.hidden = false; positionFloating(tooltip, point.x + 14, point.y + 14); } function closeInventoryMenu() { const menu = $('inventory-menu'); if (menu) menu.hidden = true; if (menuAnchor) menuAnchor.setAttribute('aria-expanded', 'false'); menuAnchor = null; selectedInventoryItem = null; } function openInventoryMenu(slotElement) { const slot = Number(slotElement.dataset.slot); const item = lastState?.inv?.[String(slot)] || lastState?.inv?.[slot]; if (!item?.present) return closeInventoryMenu(); hideItemTooltip(); closeInventoryMenu(); const name = itemNames[String(item.item_id)] || `item_${item.item_id}`; selectedInventoryItem = {slot, itemId: item.item_id, count: Number(item.item_count || 0)}; menuAnchor = slotElement; menuAnchor.setAttribute('aria-expanded', 'true'); $('inventory-menu-title').textContent = itemDisplayName(item, name); $('inventory-menu-meta').textContent = `Slot ${slot} · ${selectedInventoryItem.count} item${selectedInventoryItem.count === 1 ? '' : 's'}`; $('inventory-menu-status').classList.remove('is-error'); $('inventory-menu-status').textContent = slot < 5 ? 'Crafting slots cannot be dropped from the dashboard.' : ''; $('inventory-menu-status').hidden = slot >= 5; const unavailable = !lastState?.connected || slot < 5; $('inventory-menu').querySelectorAll('[data-inventory-action]:not([data-inventory-action="cancel"])') .forEach((button) => { button.disabled = unavailable; }); const rect = slotElement.getBoundingClientRect(); const menu = $('inventory-menu'); menu.hidden = false; positionFloating(menu, rect.left, rect.bottom + 4); } function showToast(message, isError = false) { const toast = $('toast'); toast.textContent = message; toast.classList.toggle('is-error', isError); toast.classList.add('visible'); window.clearTimeout(toastTimer); toastTimer = window.setTimeout(() => toast.classList.remove('visible'), 2400); } async function runInventoryAction(action) { if (!selectedInventoryItem) return; const selected = {...selectedInventoryItem}; const buttons = $('inventory-menu').querySelectorAll('button'); buttons.forEach((button) => { button.disabled = true; }); $('inventory-menu-status').hidden = false; $('inventory-menu-status').textContent = 'Sending action…'; try { const response = await fetch('/api/inventory/drop', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({slot: selected.slot, action}), }); const result = await response.json(); if (!response.ok || !result.ok) throw new Error(result.error || `API ${response.status}`); closeInventoryMenu(); const message = action === 'matching' ? `Dropping ${result.stacks} matching stack${result.stacks === 1 ? '' : 's'}` : action === 'one' ? 'Dropped one item' : 'Dropped the stack'; showToast(message); lastInventorySignature = ''; await poll(); } catch (error) { $('inventory-menu-status').textContent = error.message; $('inventory-menu-status').classList.add('is-error'); buttons.forEach((button) => { const cancel = button.dataset.inventoryAction === 'cancel'; button.disabled = !cancel && (!lastState?.connected || selected.slot < 5); }); } } 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}`; } } const inventoryGrid = $('inventory-grid'); inventoryGrid.addEventListener('pointerover', (event) => { const slot = event.target.closest('.inventory-slot.interactive'); if (!slot || selectedInventoryItem) return; showItemTooltip(slot, {x: event.clientX, y: event.clientY}); }); inventoryGrid.addEventListener('pointermove', (event) => { const tooltip = $('item-tooltip'); if (!tooltip.hidden && !selectedInventoryItem) { positionFloating(tooltip, event.clientX + 14, event.clientY + 14); } }); inventoryGrid.addEventListener('pointerout', (event) => { const slot = event.target.closest('.inventory-slot.interactive'); if (slot && !slot.contains(event.relatedTarget)) hideItemTooltip(); }); inventoryGrid.addEventListener('focusin', (event) => { const slot = event.target.closest('.inventory-slot.interactive'); if (!slot) return; const rect = slot.getBoundingClientRect(); showItemTooltip(slot, {x: rect.right, y: rect.top}); }); inventoryGrid.addEventListener('focusout', (event) => { if (!event.relatedTarget?.closest('.inventory-slot.interactive')) hideItemTooltip(); }); inventoryGrid.addEventListener('click', (event) => { const slot = event.target.closest('.inventory-slot'); if (!slot) return; if (slot.classList.contains('interactive')) openInventoryMenu(slot); else closeInventoryMenu(); }); inventoryGrid.addEventListener('keydown', (event) => { if (event.key !== 'Enter' && event.key !== ' ') return; const slot = event.target.closest('.inventory-slot.interactive'); if (!slot) return; event.preventDefault(); openInventoryMenu(slot); }); $('inventory-menu').addEventListener('click', (event) => { const button = event.target.closest('button[data-inventory-action]'); if (!button || button.disabled) return; const action = button.dataset.inventoryAction; if (action === 'cancel') closeInventoryMenu(); else runInventoryAction(action); }); document.addEventListener('click', (event) => { if ($('inventory-menu').hidden) return; if (event.target.closest('#inventory-menu') || event.target.closest('.inventory-slot.interactive')) return; closeInventoryMenu(); }); document.addEventListener('keydown', (event) => { if (event.key === 'Escape') { hideItemTooltip(); closeInventoryMenu(); } }); window.addEventListener('resize', () => { if (!selectedInventoryItem || !menuAnchor?.isConnected) return; const rect = menuAnchor.getBoundingClientRect(); positionFloating($('inventory-menu'), rect.left, rect.bottom + 4); }); loadItems(); poll(); window.setInterval(poll, POLL_MS); })();