feat: UI Overhaul

This commit is contained in:
2025-12-09 23:49:52 +00:00
parent 68271579bf
commit 07ae21973e
21 changed files with 4546 additions and 86 deletions

View File

@@ -0,0 +1,105 @@
import { WindowManager } from './window_manager.js';
export const GardenOverlay = {
open() {
const { win, content } = WindowManager.create('window-garden', '🌻 Garden', { x: 240, y: 20, width: 600, height: 320 });
// Render happens via update()
this.update(content);
},
update(container) {
if (!container) container = document.getElementById('window-garden-content');
if (!container) return;
const MB = window.MagicBot;
if (!MB || !MB.state || !MB.state.garden || !MB.state.garden.tileObjects) {
container.innerHTML = '<div style="color:red">No Garden Data</div>';
return;
}
const now = Date.now();
// Re-render completely for simplicity (virtual DOM would be better but vanilla JS...)
// To avoid flicker, we could diff, but for now just clear and rebuild.
container.innerHTML = '';
// 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 = () => {
let count = 0;
for (let i = 0; i < 200; i++) {
const tile = MB.state.garden.tileObjects[i.toString()];
const slots = tile?.slots || [];
slots.forEach((s, idx) => {
if (Date.now() >= s.endTime) {
MB.sendMsg({ type: 'HarvestCrop', slot: i, slotsIndex: idx, scopePath: ["Room", "Quinoa"] });
count++;
}
});
}
btnHarvestAll.textContent = `Harvested ${count}!`;
setTimeout(() => btnHarvestAll.textContent = "Harvest All Ready", 1000);
};
container.appendChild(btnHarvestAll);
const gridContainer = document.createElement('div');
gridContainer.style.cssText = "display: flex; gap: 10px; justify-content: center;";
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;";
gridContainer.appendChild(leftGrid);
gridContainer.appendChild(rightGrid);
container.appendChild(gridContainer);
const getProgressColor = (p) => {
const hue = Math.floor(Math.max(0, Math.min(1, p)) * 120);
return `hsl(${hue}, 70%, 50%)`;
};
for (let i = 0; i < 200; i++) {
const tile = MB.state.garden.tileObjects[i.toString()];
const slots = tile?.slots || [];
const crop = slots[0];
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) {
const totalTime = crop.endTime - crop.startTime;
const progress = totalTime > 0 ? (Math.max(0, now - crop.startTime) / totalTime) : 1;
cell.style.background = getProgressColor(progress);
let anyReady = false;
let tooltip = `Slot: ${i}\nSpecies: ${tile.species}`;
slots.forEach((s, idx) => {
const isReady = now >= s.endTime;
if (isReady) anyReady = true;
const left = Math.max(0, Math.ceil((s.endTime - now) / 1000));
tooltip += `\n#${idx}: ${isReady ? 'READY' : left + 's'}`;
if (s.mutations?.length) tooltip += ` [${s.mutations.join(',')}]`;
});
cell.style.border = `1px solid ${anyReady ? '#fff' : '#555'}`;
cell.title = tooltip;
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>`;
cell.onclick = () => {
slots.forEach((s, idx) => {
if (Date.now() >= s.endTime) {
MB.sendMsg({ type: 'HarvestCrop', slot: i, slotsIndex: idx, scopePath: ["Room", "Quinoa"] });
}
});
cell.style.opacity = '0.5';
};
}
if (i % 20 < 10) leftGrid.appendChild(cell);
else rightGrid.appendChild(cell);
}
}
};

View File

@@ -0,0 +1,54 @@
import { WindowManager } from './window_manager.js';
export const InventoryOverlay = {
open() {
const { win, content } = WindowManager.create('window-inventory', '🎒 Inventory', { x: 560, y: 20, width: 220, height: 300 });
this.update(content);
},
update(container) {
if (!container) container = document.getElementById('window-inventory-content');
if (!container) return;
const MB = window.MagicBot;
if (!MB || !MB.state || !MB.state.inventory || !MB.state.inventory.items) {
container.innerHTML = '<div style="color:red">No Data</div>';
return;
}
container.innerHTML = '';
const grid = document.createElement('div');
grid.style.cssText = "display: grid; grid-template-columns: 1fr; gap: 5px; font-size: 11px;";
MB.state.inventory.items.forEach(item => {
let name = item.itemType;
let species = item.species || item.parameters?.species || Object.values(item.parameters?.speciesIds || {})[0];
if (species) name = species + " " + item.itemType;
const div = document.createElement('div');
div.style.cssText = "background: rgba(255,255,255,0.05); padding: 4px; border-radius: 4px; display: flex; justify-content: space-between; cursor: pointer;";
div.innerHTML = `<span>${name}</span><span style="color:#aaa; font-weight:bold;">x${item.quantity || item.count || 1}</span>`;
// Plant logic on click
div.onclick = () => {
if (item.itemType === 'Seed' && species) {
// Find empty slot
for (let i = 0; i < 200; i++) {
if (!MB.state.garden.tileObjects[i.toString()]?.slots?.length) {
MB.sendMsg({ type: "PlantSeed", slot: i, species: species, scopePath: ["Room", "Quinoa"] });
div.style.background = 'rgba(100,255,100,0.2)';
setTimeout(() => div.style.background = 'rgba(255,255,255,0.05)', 200);
return;
}
}
alert("Garden Full!");
}
};
grid.appendChild(div);
});
if (MB.state.inventory.items.length === 0) grid.innerHTML = '<div style="color:#666; text-align:center;">Empty</div>';
container.appendChild(grid);
}
};

View File

@@ -0,0 +1,117 @@
import { WindowManager } from './window_manager.js';
import { Styles, createElement } from '../ui_styles.js';
export const ModelOverlay = {
interval: null,
open() {
const { win, content } = WindowManager.create('window-model', '🧠 Model Visualization', { x: 260, y: 340, width: 300, height: 400 });
this.update(content);
if (this.interval) clearInterval(this.interval);
this.interval = setInterval(() => this.update(content), 1000); // 1s refresh
// Cleanup on close (hacky but works for now if window maps to ID)
const closeBtn = win.querySelector('button'); // THe X button
if (closeBtn) {
const oldClick = closeBtn.onclick;
closeBtn.onclick = () => {
if (this.interval) clearInterval(this.interval);
if (oldClick) oldClick();
}
}
},
update(container) {
if (!container) container = document.getElementById('window-model-content');
if (!container) {
if (this.interval) clearInterval(this.interval);
return;
}
const MB = window.MagicBot;
if (!MB || !MB.state || !MB.state.inventory) {
container.innerHTML = '<div style="color:red">No State Data</div>';
return;
}
container.innerHTML = '';
const list = createElement('div', 'display: flex; flex-direction: column; gap: 4px; font-size: 10px;');
// Header Info
const header = createElement('div', 'padding: 5px; background: rgba(0,0,0,0.2); margin-bottom: 5px; border-radius: 4px;');
const rainProb = MB.weatherTracker ? (MB.weatherTracker.probabilities.rain * 100).toFixed(0) : '?';
const frostProb = MB.weatherTracker ? (MB.weatherTracker.probabilities.frost * 100).toFixed(0) : '?';
header.innerHTML = `
<div><b>Weather Probabilities:</b> Rain: ${rainProb}% | Frost: ${frostProb}%</div>
<div><b>Smart Harvest:</b> ${MB.automation && MB.automation.smartHarvest ? '<span style="color:#bada55">ON</span>' : '<span style="color:#ff5252">OFF</span>'}</div>
`;
list.appendChild(header);
const plots = MB.state.inventory.items.filter(i => i.itemType === 'Plot');
if (plots.length === 0) {
container.innerHTML += '<div style="padding:10px">No Plots Found</div>';
return;
}
plots.forEach(plot => {
if (!plot.properties) return;
const crop = plot.properties.crop;
if (!crop) return; // Empty plot
const row = createElement('div', 'background: rgba(255,255,255,0.05); padding: 5px; border-radius: 4px; display: flex; flex-direction: column; gap: 2px;');
// Basic Crop Info
const name = crop.name;
const stage = crop.stage;
const totalStages = crop.stages;
const isReady = stage >= totalStages;
// Value Calculation (Simple approximation if actual vals not available)
const sellPrice = crop.sellPrice || 0;
const currentVal = isReady ? sellPrice : 0;
// Decision Logic Inspection
// We can't easily "spy" on the exact decision function result without modifying it to store state,
// but we can replicate the visual indicators.
let statusColor = '#aaa';
let statusText = 'Growing';
if (isReady) {
// Check mutation potential
const nextWeather = MB.weatherTracker ? MB.weatherTracker.nextEvent : 'Unknown';
const hasMutation = crop.mutations && crop.mutations.length > 0;
if (hasMutation) {
statusText = 'Ready (Mutated!)';
statusColor = '#bada55'; // Green
} else {
// This is where we'd ideally show the "Wait vs Harvest" logic
// For now, let's show the current value and a theoretical max if waiting
statusText = 'Ready';
statusColor = '#fff';
}
}
row.innerHTML = `
<div style="font-weight:bold; display:flex; justify-content:space-between;">
<span style="color:${Styles.colors.primary}">${name}</span>
<span style="color:${statusColor}">${statusText}</span>
</div>
<div style="display:flex; justify-content:space-between; color:#888;">
<span>Stage: ${stage}/${totalStages}</span>
<span>Value: ${currentVal}</span>
</div>
<!--
<div style="font-size:9px; color:#666;">
EV: ??? | Boundary: ???
</div>
-->
`;
list.appendChild(row);
});
container.appendChild(list);
}
};

View File

@@ -0,0 +1,86 @@
import { WindowManager } from './window_manager.js';
export const PetsOverlay = {
open() {
// Position next to others
const { win, content } = WindowManager.create('window-pets', '🐾 My Pets', { x: 20, y: 340, width: 220, height: 250 });
this.update(content);
},
update(container) {
if (!container) container = document.getElementById('window-pets-content');
if (!container) return;
const MB = window.MagicBot;
if (!MB || !MB.state || !MB.state.inventory || !MB.state.inventory.items) {
container.innerHTML = '<div style="color:red">No Data</div>';
return;
}
container.innerHTML = '';
const list = document.createElement('div');
list.style.cssText = "display: flex; flex-direction: column; gap: 8px; font-size: 11px;";
const pets = MB.state.inventory.items.filter(i => i.itemType === 'Pet');
if (pets.length === 0) {
container.innerHTML = '<div style="color:#aaa; text-align:center; padding: 10px;">No Pets Found</div>';
return;
}
pets.forEach(p => {
const row = document.createElement('div');
row.style.cssText = `background: rgba(255,255,255,0.05); padding: 8px; border-radius: 4px; display: flex; flex-direction: column; gap: 2px;`;
// Name/Species
const name = p.name || p.petSpecies;
const species = p.petSpecies;
const isAutoFeed = MB.automation && MB.automation.pets && MB.automation.pets[p.id] && MB.automation.pets[p.id].autoFeed;
const diets = {
'Bee': ['Strawberry', 'Blueberry', 'Tulip', 'Daffodil', 'Lily'],
'Goat': ['Pumpkin', 'Coconut', 'Cactus', 'Pepper']
};
const diet = diets[species] ? diets[species].join(', ') : 'Unknown Diet';
row.innerHTML = `
<div style="font-weight:bold; color: #bada55; display: flex; justify-content: space-between; align-items:center;">
<span>${name}</span>
<label style="font-size:9px; color:#eee; display:flex; align-items:center;">
<input type="checkbox" class="chk-pet-feed" data-id="${p.id}" ${isAutoFeed ? 'checked' : ''}> A-Feed
</label>
</div>
<div style="display: flex; justify-content: space-between; font-size: 9px; color: #666;">
<span>${species}</span>
<span>XP: ${p.xp}</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span>Hunger: ${Math.floor(p.hunger || 0)}</span>
</div>
<div style="color: #aaa; font-size: 9px; margin-top: 4px;">
Diet: ${diet}
</div>
`;
// Event listener for checkbox (needs to be attached after innerHTML reflow)
// We'll trust the parent re-render or attach globally for now to avoid complexity in this simple render loop.
// Actually, let's attach immediately after this loop finishes or use a delegate.
list.appendChild(row);
});
// Delegate listener
list.onchange = (e) => {
if (e.target.classList.contains('chk-pet-feed')) {
const id = e.target.getAttribute('data-id');
if (!MB.automation.pets) MB.automation.pets = {};
if (!MB.automation.pets[id]) MB.automation.pets[id] = {};
MB.automation.pets[id].autoFeed = e.target.checked;
console.log(`[MagicBot] Pet ${id} Auto-Feed: ${e.target.checked}`);
}
};
container.appendChild(list);
}
};
```

View File

@@ -0,0 +1,34 @@
import { WindowManager } from './window_manager.js';
export const PlayersOverlay = {
open() {
const { win, content } = WindowManager.create('window-players', '👥 Players', { x: 20, y: 120, width: 200, height: 200 });
this.update(content);
},
update(container) {
if (!container) container = document.getElementById('window-players-content');
if (!container) return;
const MB = window.MagicBot;
if (!MB || !MB.state || !MB.state.players) {
container.innerHTML = '<div style="color:red">No Data</div>';
return;
}
container.innerHTML = '';
const list = document.createElement('div');
list.style.cssText = "display: flex; flex-direction: column; gap: 4px; font-size: 11px;";
MB.state.players.forEach(p => {
const isSelf = p.id === MB.state.playerId;
const color = p.isConnected ? '#66bb6a' : '#666';
const row = document.createElement('div');
row.style.cssText = `background: rgba(255,255,255,${isSelf ? '0.1' : '0.05'}); padding: 4px; border-radius: 4px; display: flex; align-items: center; gap: 8px;`;
row.innerHTML = `<div style="width:8px; height:8px; border-radius:50%; background:${color};"></div><span style="color:${isSelf ? '#448aff' : '#eee'}; font-weight:${isSelf ? 'bold' : 'normal'};">${p.name}</span>`;
list.appendChild(row);
});
container.appendChild(list);
}
};

View File

@@ -0,0 +1,50 @@
import { WindowManager } from './window_manager.js';
export const ShopOverlay = {
open() {
const { win, content } = WindowManager.create('window-shop', '🏪 Shop', { x: 790, y: 20, width: 250, height: 350 });
this.update(content);
},
update(container) {
if (!container) container = document.getElementById('window-shop-content');
if (!container) return;
const MB = window.MagicBot;
if (!MB || !MB.state || !MB.state.shops?.seed) {
container.innerHTML = '<div style="color:red">No Shop Data</div>';
return;
}
container.innerHTML = '';
const grid = document.createElement('div');
grid.style.cssText = "display: grid; grid-template-columns: repeat(2, 1fr); gap: 5px; font-size: 11px;";
MB.state.shops.seed.inventory.forEach(item => {
const purchased = MB.state.shopPurchases?.seed?.purchases?.[item.species] || 0;
const stock = Math.max(0, item.initialStock - purchased);
const stockColor = stock > 0 ? '#66bb6a' : '#ff5252';
const auto = MB.automation?.autoBuyItems?.has(item.species);
const div = document.createElement('div');
div.style.cssText = `background: rgba(255,255,255,0.05); padding: 5px; border-radius: 4px; border-left: 2px solid ${stockColor}; border-right: 2px solid ${auto ? '#ffd700' : 'transparent'}; cursor: pointer;`;
div.innerHTML = `<div style="font-weight:bold;">${item.species}</div><div style="color:#888;">${stock}/${item.initialStock}</div>`;
div.oncontextmenu = (e) => {
e.preventDefault();
if (auto) MB.automation.autoBuyItems.delete(item.species);
else MB.automation.autoBuyItems.add(item.species);
this.update(container); // Refresh immediately to show border change
};
div.onclick = () => {
if (stock > 0) MB.sendMsg({ scopePath: ["Room", "Quinoa"], type: "PurchaseSeed", species: item.species });
};
grid.appendChild(div);
});
container.appendChild(grid);
}
};

View File

@@ -0,0 +1,112 @@
export const WindowManager = (function () {
let zIndexCounter = 9999990;
function makeDraggable(el, header) {
let isDragging = false, offX = 0, offY = 0;
header.addEventListener('mousedown', (e) => {
isDragging = true;
offX = e.clientX - el.offsetLeft;
offY = e.clientY - el.offsetTop;
// Bring to front
zIndexCounter++;
el.style.zIndex = zIndexCounter;
});
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 createWindow(id, title, props = {}) {
let win = document.getElementById(id);
if (win) {
// Just bring to front if exists
zIndexCounter++;
win.style.zIndex = zIndexCounter;
return { win, content: document.getElementById(id + '-content') };
}
const x = props.x || 100;
const y = props.y || 100;
const w = props.width || 300;
const h = props.height || 200;
win = document.createElement('div');
win.id = id;
win.style.cssText = `
position: fixed;
top: ${y}px;
left: ${x}px;
width: ${w}px;
height: ${h}px;
background: rgba(10, 10, 20, 0.95);
border: 1px solid #444;
border-radius: 8px;
z-index: ${++zIndexCounter};
display: flex;
flex-direction: column;
box-shadow: 0 4px 20px rgba(0,0,0,0.6);
font-family: sans-serif;
color: #eee;
backdrop-filter: blur(5px);
resize: both;
overflow: hidden;
`;
const header = document.createElement('div');
header.id = id + '-header';
header.style.cssText = `
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;
align-items: center;
`;
const titleSpan = document.createElement('span');
titleSpan.textContent = title;
const closeBtn = document.createElement('span');
closeBtn.textContent = '✕';
closeBtn.style.cssText = 'cursor: pointer; color: #ff5252; font-size: 14px;';
closeBtn.onclick = () => win.remove();
header.appendChild(titleSpan);
header.appendChild(closeBtn);
const content = document.createElement('div');
content.id = id + '-content';
content.style.cssText = 'flex: 1; overflow-y: auto; padding: 10px;';
win.appendChild(header);
win.appendChild(content);
document.body.appendChild(win);
makeDraggable(win, header);
// Allow clicking anywhere on window to bring to front
win.addEventListener('mousedown', () => {
if (win.style.zIndex != zIndexCounter) {
zIndexCounter++;
win.style.zIndex = zIndexCounter;
}
});
return { win, content };
}
return {
create: createWindow
};
})();