Files
Magic-Garden-Bot/extension/modules/ui.js
2025-12-09 23:21:09 +00:00

857 lines
42 KiB
JavaScript

(function () {
const MB = window.MagicBot;
// --- UI CREATION ---
const sidebar = document.createElement('div');
sidebar.id = 'magic-bot-sidebar';
sidebar.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
width: 280px;
background: rgba(20, 20, 25, 0.95);
color: #eee;
padding: 20px;
border-radius: 12px;
z-index: 9999999;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
box-shadow: 0 4px 15px rgba(0,0,0,0.6);
border: 1px solid #444;
backdrop-filter: blur(5px);
`;
sidebar.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h3 style="margin: 0; color: #bada55; font-size: 18px;">🌱 Magic Bot</h3>
<div id="status-indicator" style="font-size: 12px; font-weight: bold; color: #ff5252;">● Disconnected</div>
</div>
<!-- Teleport -->
<div style="background: rgba(255,255,255,0.05); padding: 10px; border-radius: 8px; margin-bottom: 15px;">
<div id="head-tp" style="display: flex; justify-content: space-between; cursor: pointer; margin-bottom: 5px; user-select: none;">
<label style="font-size: 12px; color: #aaa; cursor: pointer;">Teleport</label>
<span id="arrow-tp" style="font-size: 10px; color: #aaa;">▼</span>
</div>
<div id="section-tp">
<div style="display: flex; gap: 8px;">
<input type="number" id="tp-x" placeholder="X" value="15" style="width: 100%; padding: 6px; background: #333; border: 1px solid #555; color: white; border-radius: 4px;">
<input type="number" id="tp-y" placeholder="Y" value="15" style="width: 100%; padding: 6px; background: #333; border: 1px solid #555; color: white; border-radius: 4px;">
<button id="btn-tp" style="cursor: pointer; background: #448aff; color: white; border: none; padding: 0 15px; border-radius: 4px; font-weight: bold;">Go</button>
</div>
</div>
</div>
<!-- Automation -->
<div style="background: rgba(255,255,255,0.05); padding: 10px; border-radius: 8px; margin-bottom: 15px;">
<div id="head-auto" style="display: flex; justify-content: space-between; cursor: pointer; margin-bottom: 5px; user-select: none;">
<label style="font-size: 12px; color: #aaa; cursor: pointer;">Automation</label>
<span id="arrow-auto" style="font-size: 10px; color: #aaa;">▼</span>
</div>
<div id="section-auto">
<div style="display: flex; flex-direction: column; gap: 8px;">
<label style="display: flex; align-items: center; justify-content: space-between; cursor: pointer;">
<span>Auto Plant</span>
<input type="checkbox" id="chk-auto-plant" style="cursor: pointer;">
</label>
<label style="display: flex; align-items: center; justify-content: space-between; cursor: pointer;">
<span>Auto Harvest</span>
<input type="checkbox" id="chk-auto-harvest" style="cursor: pointer;">
</label>
<label style="display: flex; align-items: center; justify-content: space-between; cursor: pointer;">
<span>Auto Sell</span>
<input type="checkbox" id="chk-auto-sell" style="cursor: pointer;">
</label>
<label style="display: flex; align-items: center; justify-content: space-between; cursor: pointer;">
<span>Auto Feed Pets</span>
<input type="checkbox" id="chk-auto-feed" style="cursor: pointer;">
</label>
<div style="border-top: 1px solid #444; margin: 4px 0;"></div>
<label style="display: flex; align-items: center; justify-content: space-between; cursor: pointer;" title="Waits for optimal mutations (Gold->Frozen, Long->Wet)">
<span style="color: #bada55;">Smart Harvest</span>
<input type="checkbox" id="chk-smart-harvest" style="cursor: pointer;">
</label>
</div>
</div>
</div>
<!-- Harvest -->
<div style="background: rgba(255,255,255,0.05); padding: 10px; border-radius: 8px; margin-bottom: 15px;">
<div id="head-hv" style="display: flex; justify-content: space-between; cursor: pointer; margin-bottom: 5px; user-select: none;">
<label style="font-size: 12px; color: #aaa; cursor: pointer;">Harvest</label>
<span id="arrow-hv" style="font-size: 10px; color: #aaa;">▼</span>
</div>
<div id="section-hv">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px;">
<div>
<span style="font-size: 10px; color: #888;">Start Slot</span>
<input type="number" id="hv-start" value="140" style="width: 100%; padding: 6px; background: #333; border: 1px solid #555; color: white; border-radius: 4px;">
</div>
<div>
<span style="font-size: 10px; color: #888;">End Slot</span>
<input type="number" id="hv-end" value="160" style="width: 100%; padding: 6px; background: #333; border: 1px solid #555; color: white; border-radius: 4px;">
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 10px;">
<div>
<span style="font-size: 10px; color: #888;">Iter. (Count)</span>
<input type="number" id="hv-count" value="1" style="width: 100%; padding: 6px; background: #333; border: 1px solid #555; color: white; border-radius: 4px;">
</div>
<div>
<span style="font-size: 10px; color: #888;">Delay (ms)</span>
<input type="number" id="hv-delay" value="20" style="width: 100%; padding: 6px; background: #333; border: 1px solid #555; color: white; border-radius: 4px;">
</div>
</div>
<button id="btn-harvest" style="width: 100%; cursor: pointer; background: #66bb6a; color: white; border: none; padding: 8px; border-radius: 4px; font-weight: bold; transition: background 0.2s;">Run Harvest</button>
</div>
</div>
<button id="btn-sell" style="width: 100%; cursor: pointer; background: #ffa726; color: white; border: none; padding: 10px; border-radius: 8px; font-weight: bold; box-shadow: 0 2px 5px rgba(0,0,0,0.2);">Sell All Crops</button>
<div style="border-top: 1px solid #444; margin: 10px 0;"></div>
<div style="display: flex; gap: 5px;">
<button id="btn-logs" style="flex: 1; cursor: pointer; background: #5c6bc0; color: white; border: none; padding: 8px; border-radius: 4px; font-weight: bold;">Logs</button>
<button id="btn-state" style="flex: 1; cursor: pointer; background: #ab47bc; color: white; border: none; padding: 8px; border-radius: 4px; font-weight: bold;">State</button>
</div>
`;
document.body.appendChild(sidebar);
// Stop keyboard propagation
sidebar.addEventListener('keydown', (e) => e.stopPropagation());
sidebar.addEventListener('keyup', (e) => e.stopPropagation());
sidebar.addEventListener('keypress', (e) => e.stopPropagation());
// --- UI LOG UPDATE ---
MB.on('log', (msg) => {
logToOverlay(msg.type, msg.data);
});
MB.on('socket_connected', (connected) => {
const el = document.getElementById('status-indicator');
if (connected) {
el.innerHTML = '● Connected';
el.style.color = '#66bb6a';
} else {
el.innerHTML = '● Disconnected';
el.style.color = '#ff5252';
}
});
// --- BINDINGS ---
const setupCollapse = (headId, bodyId, arrowId) => {
document.getElementById(headId).onclick = () => {
const el = document.getElementById(bodyId);
const arrow = document.getElementById(arrowId);
if (el.style.display === 'none') {
el.style.display = 'block';
arrow.textContent = '▼';
} else {
el.style.display = 'none';
arrow.textContent = '▶';
}
};
};
setupCollapse('head-tp', 'section-tp', 'arrow-tp');
setupCollapse('head-hv', 'section-hv', 'arrow-hv');
setupCollapse('head-auto', 'section-auto', 'arrow-auto');
setupCollapse('head-diet', 'section-diet', 'arrow-diet');
// Automation Toggles
document.getElementById('chk-auto-plant').onchange = (e) => {
if (MB.automation) MB.automation.autoPlant = e.target.checked;
};
document.getElementById('chk-auto-harvest').onchange = (e) => {
if (MB.automation) MB.automation.autoHarvest = e.target.checked;
};
document.getElementById('chk-auto-sell').onchange = (e) => {
if (MB.automation) MB.automation.autoSell = e.target.checked;
};
document.getElementById('chk-smart-harvest').onchange = (e) => {
if (MB.automation) MB.automation.smartHarvest = e.target.checked;
};
/* Auto Feed listener is likely already added if chunk 1 ran, but let's double check context */
/* Wait, the previous chunk 4 relied on chk-auto-feed onchange being added. Check if it's there. */
/* Viewing the file, I see the HTML for chk-auto-feed. I do NOT see the listener in the lines I viewed (up to 120). Listeners are further down. */
/* Assuming listener is missing. */
document.getElementById('chk-auto-feed').onchange = (e) => {
if (MB.automation) MB.automation.autoFeed = e.target.checked;
};
// Populate Diet List
const populateDietList = () => {
const list = document.getElementById('diet-list');
if (!list) return;
list.innerHTML = '';
// Known crops list - could be dynamic from shop but let's hardcode common ones or try to read from state if available
// Reading from shop state is better if available
let crops = ["Carrot", "Tomato", "Potato", "Corn", "Wheat", "Strawberry", "Blueberry", "Pumpkin", "Watermelon", "Radish", "Cabbage", "Lettuce", "Pepper", "Apple", "Orange", "Banana", "Coconut", "Grape", "Lemon", "Lime", "Peach", "Pear", "Plum", "Cherry", "Mango", "Pineapple", "Aloe", "Daffodil", "OrangeTulip"];
// Try to merge with shop data if present
if (MB.state && MB.state.shops && MB.state.shops.seed && MB.state.shops.seed.inventory) {
const shopCrops = MB.state.shops.seed.inventory.map(i => i.species);
// Union
crops = [...new Set([...crops, ...shopCrops])];
}
crops.sort();
crops.forEach(crop => {
const label = document.createElement('label');
label.style.cssText = "display: flex; align-items: center; gap: 4px; font-size: 10px; cursor: pointer; user-select: none;";
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.style.cursor = 'pointer';
if (MB.automation && MB.automation.petDiet && MB.automation.petDiet.has(crop)) {
checkbox.checked = true;
}
checkbox.onchange = (e) => {
if (!MB.automation) return;
if (!MB.automation.petDiet) MB.automation.petDiet = new Set();
if (e.target.checked) MB.automation.petDiet.add(crop);
else MB.automation.petDiet.delete(crop);
};
label.appendChild(checkbox);
label.appendChild(document.createTextNode(crop));
list.appendChild(label);
});
};
// Initial populate
setTimeout(populateDietList, 2000); // Wait for state to load maybe?
// Also re-populate on state update if empty?
MB.on('state_updated', () => {
const list = document.getElementById('diet-list');
if (list && list.children.length === 0) populateDietList();
});
document.getElementById('btn-diet-all').onclick = () => {
const list = document.getElementById('diet-list');
if (!list) return;
const checkboxes = list.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(cb => {
if (!cb.checked) {
cb.click(); // Trigger change event
}
});
};
document.getElementById('btn-diet-none').onclick = () => {
const list = document.getElementById('diet-list');
if (!list) return;
const checkboxes = list.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(cb => {
if (cb.checked) {
cb.click();
}
});
};
document.getElementById('chk-auto-feed').onchange = (e) => {
if (MB.automation) MB.automation.autoFeed = e.target.checked;
};
// Populate Diet List
const populateDietList = () => {
const list = document.getElementById('diet-list');
if (!list) return;
list.innerHTML = '';
// Known crops list - could be dynamic from shop but let's hardcode common ones or try to read from state if available
// Reading from shop state is better if available
let crops = ["Carrot", "Tomato", "Potato", "Corn", "Wheat", "Strawberry", "Blueberry", "Pumpkin", "Watermelon", "Radish", "Cabbage", "Lettuce", "Pepper", "Apple", "Orange", "Banana", "Coconut", "Grape", "Lemon", "Lime", "Peach", "Pear", "Plum", "Cherry", "Mango", "Pineapple", "Aloe", "Daffodil", "OrangeTulip"];
// Try to merge with shop data if present
if (MB.state && MB.state.shops && MB.state.shops.seed && MB.state.shops.seed.inventory) {
const shopCrops = MB.state.shops.seed.inventory.map(i => i.species);
// Union
crops = [...new Set([...crops, ...shopCrops])];
}
crops.sort();
crops.forEach(crop => {
const label = document.createElement('label');
label.style.cssText = "display: flex; align-items: center; gap: 4px; font-size: 10px; cursor: pointer; user-select: none;";
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.style.cursor = 'pointer';
if (MB.automation && MB.automation.petDiet && MB.automation.petDiet.has(crop)) {
checkbox.checked = true;
}
checkbox.onchange = (e) => {
if (!MB.automation) return;
if (!MB.automation.petDiet) MB.automation.petDiet = new Set();
if (e.target.checked) MB.automation.petDiet.add(crop);
else MB.automation.petDiet.delete(crop);
};
label.appendChild(checkbox);
label.appendChild(document.createTextNode(crop));
list.appendChild(label);
});
};
// Initial populate
setTimeout(populateDietList, 2000); // Wait for state to load maybe?
// Also re-populate on state update if empty?
MB.on('state_updated', () => {
const list = document.getElementById('diet-list');
if (list && list.children.length === 0) populateDietList();
});
document.getElementById('btn-diet-all').onclick = () => {
const list = document.getElementById('diet-list');
if (!list) return;
const checkboxes = list.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(cb => {
if (!cb.checked) {
cb.click(); // Trigger change event
}
});
};
document.getElementById('btn-diet-none').onclick = () => {
const list = document.getElementById('diet-list');
if (!list) return;
const checkboxes = list.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(cb => {
if (cb.checked) {
cb.click();
}
});
};
document.getElementById('btn-tp').onclick = () => {
const x = parseInt(document.getElementById('tp-x').value);
const y = parseInt(document.getElementById('tp-y').value);
MB.teleport(x, y);
};
document.getElementById('btn-harvest').onclick = () => {
const start = parseInt(document.getElementById('hv-start').value);
const end = parseInt(document.getElementById('hv-end').value);
const count = parseInt(document.getElementById('hv-count').value);
const delay = parseInt(document.getElementById('hv-delay').value);
MB.harvestLoop(start, end, count, delay);
};
document.getElementById('btn-sell').onclick = () => {
MB.sellAll();
};
document.getElementById('btn-logs').onclick = () => {
const el = document.getElementById('magic-bot-logs');
el.style.display = el.style.display === 'none' ? 'flex' : 'none';
if (el.style.display === 'flex') {
el.style.top = 'auto';
el.style.bottom = '20px';
el.style.left = '20px';
}
};
document.getElementById('btn-state').onclick = () => {
createFloatingWindow('window-wallet', '💰 Wallet', 20, 20, 200, 80);
createFloatingWindow('window-garden', '🌻 Garden', 240, 20, 600, 320);
createFloatingWindow('window-inventory', '🎒 Inventory', 560, 20, 200, 200);
createFloatingWindow('window-shop', '🏪 Shop', 780, 20, 250, 300);
createFloatingWindow('window-players', '👥 Players', 20, 120, 200, 150);
updateVisuals();
};
// --- VISUALIZERS ---
MB.on('state_updated', updateVisuals);
function updateVisuals() {
console.log("[MagicBot] Updating visuals...", MB.state);
// --- Wallet ---
const wallet = document.getElementById('window-wallet-content');
if (wallet) {
if (MB.state.coins !== undefined) {
wallet.innerHTML = `
<div style="font-size: 24px; color: #ffd700; text-align: center;">CA$H: ${MB.state.coins.toLocaleString()}</div>
`;
} else {
console.warn("[MagicBot] Wallet: MB.state.coins is undefined");
wallet.innerHTML = '<div style="color:red">No Coin Data</div>';
}
}
// --- Garden ---
const garden = document.getElementById('window-garden-content');
if (garden) {
if (MB.state.garden && MB.state.garden.tileObjects) {
console.log("[MagicBot] Render Garden: Split Grid 1-200");
const now = Date.now();
// Containers for the two grids
const container = document.createElement('div');
container.style.cssText = "display: flex; gap: 10px; justify-content: center;";
// Harvest All Button
const btnHarvestAll = document.createElement('button');
btnHarvestAll.textContent = "Harvest All Ready";
btnHarvestAll.style.cssText = "width: 100%; margin-bottom: 10px; padding: 8px; background: #66bb6a; color: white; border: none; border-radius: 4px; font-weight: bold; cursor: pointer;";
btnHarvestAll.onclick = async () => {
console.log("[MagicBot] Harvest All Triggered");
let count = 0;
const now = Date.now();
// Iterate all slots
for (let i = 0; i < 200; i++) {
const slotId = i.toString();
const tile = MB.state.garden.tileObjects[slotId];
const slots = tile && tile.slots ? tile.slots : [];
slots.forEach((s, idx) => {
if (now >= s.endTime) {
MB.sendMsg({
type: 'HarvestCrop',
slot: i,
slotsIndex: idx,
scopePath: ["Room", "Quinoa"]
});
count++;
}
});
}
console.log(`[MagicBot] Sent harvest for ${count} crops.`);
btnHarvestAll.textContent = `Harvested ${count}!`;
setTimeout(() => btnHarvestAll.textContent = "Harvest All Ready", 1000);
};
// Wrapper to hold button + grids
const wrapper = document.createElement('div');
wrapper.appendChild(btnHarvestAll);
wrapper.appendChild(container);
const leftGrid = document.createElement('div');
leftGrid.style.cssText = "display: grid; grid-template-columns: repeat(10, 1fr); gap: 2px;";
const rightGrid = document.createElement('div');
rightGrid.style.cssText = "display: grid; grid-template-columns: repeat(10, 1fr); gap: 2px;";
container.appendChild(leftGrid);
container.appendChild(rightGrid);
// Helper to get color based on progress (0.0 to 1.0)
const getProgressColor = (p) => {
// Red (0) -> Yellow (0.5) -> Green (1.0)
// Simple HSL: 0 (Red) -> 120 (Green)
const hue = Math.floor(Math.max(0, Math.min(1, p)) * 120);
return `hsl(${hue}, 70%, 50%)`;
};
// Loop 0 to 199
for (let i = 0; i < 200; i++) {
const slotId = i.toString();
const tile = MB.state.garden.tileObjects[slotId];
// tile.slots is an array of harvestable instances
const slots = tile && tile.slots ? tile.slots : [];
const crop = slots.length > 0 ? slots[0] : null;
const cell = document.createElement('div');
cell.style.cssText = "width: 25px; height: 25px; border: 1px solid #333; border-radius: 3px; background: rgba(255,255,255,0.02); position: relative; cursor: pointer;";
if (crop) {
// Calculate visuals based on first crop (or average? First is simplest)
// Use the most mature crop for color? Or just the first one. First one is fine for background.
const totalTime = crop.endTime - crop.startTime;
const elapsed = Math.max(0, now - crop.startTime);
const progress = totalTime > 0 ? (elapsed / totalTime) : 1;
const color = getProgressColor(progress);
// Check readiness of ALL slots
let anyReady = false;
let tooltip = `Slot Index: ${i}\nSpecies: ${tile.species}`;
// Build tooltip and check anyReady
slots.forEach((s, idx) => {
const isReady = now >= s.endTime; // Individual readiness
if (isReady) anyReady = true;
const timeLeft = Math.max(0, Math.ceil((s.endTime - now) / 1000));
// Format: "#0: READY" or "#0: 45s"
tooltip += `\n#${idx}: ${isReady ? 'READY' : timeLeft + 's'}`;
if (s.mutations && s.mutations.length > 0) {
tooltip += ` [${s.mutations.join(',')}]`;
}
});
cell.style.background = color;
// Bright border if ANY are ready
cell.style.border = `1px solid ${anyReady ? '#fff' : '#555'}`;
cell.title = tooltip;
// Optional: Small letter for species
cell.innerHTML = `<span style="font-size: 10px; color: rgba(0,0,0,0.5); font-weight: bold; position: absolute; top: 1px; left: 2px;">${tile.species[0]}</span>`;
// Click to harvest ALL ready slots
cell.onclick = () => {
let harvestedCount = 0;
slots.forEach((s, idx) => {
if (now >= s.endTime) {
console.log(`[MagicBot] Harvesting Slot ${i} Index ${idx}`);
MB.sendMsg({
type: 'HarvestCrop',
slot: i,
slotsIndex: idx,
scopePath: ["Room", "Quinoa"]
});
harvestedCount++;
}
});
if (harvestedCount > 0) {
cell.style.opacity = '0.5';
} else {
console.log("[MagicBot] No ready crops in slot", i);
}
};
} else {
cell.title = `Slot Index: ${i}\nEmpty`;
}
// Logic: 0-9 -> Left Grid, 10-19 -> Right Grid
// i % 20 gives 0-19 column index
const colIndex = i % 20;
if (colIndex < 10) {
leftGrid.appendChild(cell);
} else {
rightGrid.appendChild(cell);
}
}
garden.innerHTML = '';
garden.appendChild(wrapper);
} else {
console.warn("[MagicBot] Garden: MB.state.garden is missing or invalid", MB.state.garden);
garden.innerHTML = '<div style="color:red">No Garden Data</div>';
}
}
// --- Inventory ---
const inv = document.getElementById('window-inventory-content');
if (inv) {
if (MB.state.inventory && MB.state.inventory.items) {
console.log("[MagicBot] Render Inventory:", MB.state.inventory.items.length, "items");
console.log("[MagicBot] Render Inventory:", MB.state.inventory.items.length, "items");
const container = document.createElement('div');
container.style.cssText = "display: grid; grid-template-columns: repeat(2, 1fr); gap: 5px; font-size: 11px;";
MB.state.inventory.items.forEach(item => {
// Try to resolve name from parameters (e.g. Seed -> Carrot)
let name = item.itemType;
let species = null;
if (item.species) {
species = item.species;
} else if (item.parameters) {
if (item.parameters.species) {
species = item.parameters.species;
} else if (item.parameters.speciesIds) {
// Handle speciesIds map/array (e.g. { "0": "Carrot" })
const vals = Object.values(item.parameters.speciesIds);
if (vals.length > 0) species = vals[0];
}
}
if (species) {
name = species + " " + item.itemType;
} else if (item.itemType === 'Seed') {
console.warn("[MagicBot] Seed item missing species:", item);
}
const count = item.quantity || item.count || 1;
const itemDiv = document.createElement('div');
itemDiv.style.cssText = "background: rgba(255,255,255,0.05); padding: 3px; border-radius: 3px; display: flex; justify-content: space-between; cursor: pointer; transition: background 0.2s;";
itemDiv.innerHTML = `
<span>${name}</span>
<span style="font-weight: bold; color: #aaa;">x${count}</span>
`;
// Click to Plant
itemDiv.onclick = () => {
// Check if it's a seed
if (item.itemType === 'Seed' && species) {
console.log("[MagicBot] Attempting to plant:", species);
// Find first empty slot (0-199)
let emptySlot = -1;
for (let i = 0; i < 200; i++) {
const slotId = i.toString();
const tile = MB.state.garden.tileObjects[slotId];
const hasCrop = tile && tile.slots && tile.slots.length > 0;
if (!hasCrop) {
emptySlot = i;
break;
}
}
if (emptySlot !== -1) {
console.log("[MagicBot] Found empty slot:", emptySlot);
MB.sendMsg({
type: "PlantSeed",
slot: emptySlot,
species: species,
scopePath: ["Room", "Quinoa"]
});
// Visual feedback? Maybe flash the inventory item or something?
itemDiv.style.background = 'rgba(100,255,100,0.2)';
setTimeout(() => itemDiv.style.background = 'rgba(255,255,255,0.05)', 200);
} else {
console.warn("[MagicBot] No empty slots available!");
alert("Garden is full!");
}
} else {
console.log("[MagicBot] Item is not a plantable seed.");
}
};
// Hover
itemDiv.onmouseenter = () => itemDiv.style.background = 'rgba(255,255,255,0.1)';
itemDiv.onmouseleave = () => itemDiv.style.background = 'rgba(255,255,255,0.05)';
container.appendChild(itemDiv);
});
if (MB.state.inventory.items.length === 0) {
container.innerHTML = '<div style="color: #666; font-style: italic; text-align: center; margin-top: 10px; grid-column: span 2;">Inventory empty</div>';
}
inv.innerHTML = '';
inv.appendChild(container);
} else {
console.warn("[MagicBot] Inventory: MB.state.inventory is missing", MB.state.inventory);
inv.innerHTML = '<div style="color:red">No Inventory Data</div>';
}
}
// --- Shop (Seeds) ---
const shop = document.getElementById('window-shop-content');
if (shop) {
if (MB.state.shops && MB.state.shops.seed) {
console.log("[MagicBot] Render Shop:", MB.state.shops.seed.inventory.length, "items");
console.log("[MagicBot] Render Shop:", MB.state.shops.seed.inventory.length, "items");
const container = document.createElement('div');
container.style.cssText = "display: grid; grid-template-columns: repeat(2, 1fr); gap: 5px; font-size: 11px;";
MB.state.shops.seed.inventory.forEach(item => {
// Calculate dynamic stock
// Purchases are stored in MB.state.shopPurchases.seed.purchases[SpeciesName] = quantity
let purchased = 0;
if (MB.state.shopPurchases && MB.state.shopPurchases.seed && MB.state.shopPurchases.seed.purchases) {
purchased = MB.state.shopPurchases.seed.purchases[item.species] || 0;
}
const currentStock = Math.max(0, item.initialStock - purchased);
const stockColor = currentStock > 0 ? '#66bb6a' : '#ff5252';
const isAutoBuying = MB.automation && MB.automation.autoBuyItems.has(item.species);
const borderColor = isAutoBuying ? '#ffd700' : 'transparent';
// If not auto-buying, fallback to stock indicator on left border
const itemDiv = document.createElement('div');
itemDiv.style.cssText = `background: rgba(255,255,255,0.05); padding: 5px; border-radius: 4px; border-left: 2px solid ${stockColor}; border-right: 2px solid ${borderColor}; cursor: pointer; transition: background 0.2s;`;
itemDiv.innerHTML = `
<div style="font-weight: bold;">${item.species} ${isAutoBuying ? '✅' : ''}</div>
<div style="color: #888;">Stock: ${currentStock}/${item.initialStock}</div>
`;
// Right-click to toggle Auto-Buy
itemDiv.oncontextmenu = (e) => {
e.preventDefault();
if (MB.automation) {
if (MB.automation.autoBuyItems.has(item.species)) {
MB.automation.autoBuyItems.delete(item.species);
console.log("[MagicBot] Stopped auto-buying:", item.species);
} else {
MB.automation.autoBuyItems.add(item.species);
console.log("[MagicBot] Started auto-buying:", item.species);
}
updateVisuals();
}
};
// Click to Buy
itemDiv.onclick = () => {
if (currentStock > 0) {
console.log("[MagicBot] Buying Seed:", item.species);
MB.sendMsg({
scopePath: ["Room", "Quinoa"],
type: "PurchaseSeed",
species: item.species
});
} else {
console.log("[MagicBot] Out of stock:", item.species);
}
};
// Hover effect
itemDiv.onmouseenter = () => itemDiv.style.background = 'rgba(255,255,255,0.1)';
itemDiv.onmouseleave = () => itemDiv.style.background = 'rgba(255,255,255,0.05)';
container.appendChild(itemDiv);
});
shop.innerHTML = '';
shop.appendChild(container);
} else {
console.warn("[MagicBot] Shop: MB.state.shops is missing", MB.state.shops);
shop.innerHTML = '<div style="color:red">No Shop Data</div>';
}
}
// --- Players ---
const players = document.getElementById('window-players-content');
if (players) {
if (MB.state.players) {
console.log("[MagicBot] Render Players:", MB.state.players.length);
let html = '<div style="display: flex; flex-direction: column; gap: 4px; font-size: 11px;">';
MB.state.players.forEach(p => {
const isSelf = p.id === MB.state.playerId;
const statusColor = p.isConnected ? '#66bb6a' : '#666';
html += `
<div style="background: rgba(255,255,255,${isSelf ? '0.1' : '0.05'}); padding: 4px; border-radius: 4px; display: flex; align-items: center; gap: 8px;">
<div style="width: 8px; height: 8px; border-radius: 50%; background: ${statusColor};"></div>
<span style="font-weight: ${isSelf ? 'bold' : 'normal'}; color: ${isSelf ? '#448aff' : '#eee'};">${p.name}</span>
${isSelf ? '<span style="font-size: 9px; background: #448aff; color: white; padding: 1px 4px; border-radius: 3px;">YOU</span>' : ''}
</div>
`;
});
html += '</div>';
players.innerHTML = html;
} else {
console.warn("[MagicBot] Players: MB.state.players is missing", MB.state.players);
players.innerHTML = '<div style="color:red">No Player Data</div>';
}
}
}
// --- OVERLAYS ---
// Log Overlay
const logOverlay = document.createElement('div');
logOverlay.id = 'magic-bot-logs';
logOverlay.style.cssText = `display: none; position: fixed; bottom: 20px; left: 20px; width: 600px; height: 300px; background: rgba(10, 10, 15, 0.95); border: 1px solid #444; border-radius: 8px; z-index: 9999999; font-family: 'Consolas', 'Monaco', monospace; font-size: 11px; color: #eee; display: flex; flex-direction: column; box-shadow: 0 4px 15px rgba(0,0,0,0.6); resize: both; overflow: hidden;`;
logOverlay.innerHTML = `
<div style="padding: 5px 10px; background: #222; border-bottom: 1px solid #444; display: flex; justify-content: space-between; align-items: center; cursor: move;" id="log-header">
<span style="font-weight: bold; color: #aaa;">Network Logs</span>
<div>
<button id="btn-clear-logs" style="background: none; border: none; color: #888; cursor: pointer; margin-right: 10px;">Clear</button>
<button id="btn-close-logs" style="background: none; border: none; color: #ff5252; cursor: pointer;" onclick="this.parentElement.parentElement.parentElement.style.display='none'">X</button>
</div>
</div>
<div id="log-content" style="flex: 1; overflow-y: auto; padding: 5px; word-break: break-all;"></div>
`;
document.body.appendChild(logOverlay);
makeDraggable(logOverlay, 'log-header');
// Stop logs propagation
logOverlay.addEventListener('keydown', (e) => e.stopPropagation());
logOverlay.addEventListener('keyup', (e) => e.stopPropagation());
logOverlay.addEventListener('keypress', (e) => e.stopPropagation());
document.getElementById('btn-clear-logs').onclick = () => {
document.getElementById('log-content').innerHTML = '';
};
function logToOverlay(type, data) {
const container = document.getElementById('log-content');
if (!container) return;
const isAtBottom = (container.scrollHeight - container.scrollTop - container.clientHeight) < 50;
const line = document.createElement('div');
line.style.borderBottom = '1px solid #222';
line.style.padding = '2px 0';
line.style.cursor = 'pointer';
line.title = 'Click to copy raw message';
const timestamp = new Date().toLocaleTimeString();
let color = type === 'TX' ? '#66bb6a' : '#448aff';
let content = data;
try {
if (typeof data === 'string' && (data.startsWith('{') || data.startsWith('['))) {
content = JSON.stringify(JSON.parse(data));
}
} catch (e) { }
line.innerHTML = `<span style="color: #666;">[${timestamp}]</span> <span style="color: ${color}; font-weight: bold;">${type}</span> <span style="color: #ccc;">${content}</span>`;
line.addEventListener('click', () => {
navigator.clipboard.writeText(data).then(() => {
line.style.background = 'rgba(255, 255, 255, 0.1)';
setTimeout(() => line.style.background = 'transparent', 200);
}).catch(e => console.error(e));
});
container.appendChild(line);
if (isAtBottom) container.scrollTop = container.scrollHeight;
if (container.children.length > 500) container.removeChild(container.firstChild);
}
// --- HELPER: Draggable Windows ---
function makeDraggable(el, headerId) {
const header = el.querySelector('#' + headerId);
let isDragging = false, offX = 0, offY = 0;
header.addEventListener('mousedown', (e) => {
isDragging = true;
offX = e.clientX - el.offsetLeft;
offY = e.clientY - el.offsetTop;
});
window.addEventListener('mousemove', (e) => {
if (isDragging) {
el.style.left = (e.clientX - offX) + 'px';
el.style.top = (e.clientY - offY) + 'px';
}
});
window.addEventListener('mouseup', () => isDragging = false);
}
function createFloatingWindow(id, title, x, y, width, height) {
if (document.getElementById(id)) return;
const win = document.createElement('div');
win.id = id;
win.style.cssText = `position: fixed; top: ${y}px; left: ${x}px; width: ${width}px; height: ${height}px; background: rgba(10, 10, 20, 0.9); border: 1px solid #444; border-radius: 8px; z-index: 9999995; display: flex; flex-direction: column; box-shadow: 0 4px 15px rgba(0,0,0,0.5); font-family: sans-serif; color: #eee; backdrop-filter: blur(5px); resize: both; overflow: hidden;`;
win.innerHTML = `
<div id="${id}-header" style="padding: 8px; background: rgba(255,255,255,0.05); border-bottom: 1px solid #444; cursor: move; font-weight: bold; font-size: 12px; display: flex; justify-content: space-between;">
<span>${title}</span>
<span style="cursor: pointer; color: #ff5252;" onclick="document.getElementById('${id}').remove()">✕</span>
</div>
<div id="${id}-content" style="flex: 1; overflow-y: auto; padding: 10px;"></div>
`;
document.body.appendChild(win);
makeDraggable(win, `${id}-header`);
}
// Expose helpers globally if needed by other modules (e.g. state)
window.createFloatingWindow = createFloatingWindow;
console.log('[MagicBot] UI module loaded.');
})();