857 lines
42 KiB
JavaScript
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.');
|
|
|
|
})();
|