oh fuck i forgot

This commit is contained in:
2025-12-09 23:21:09 +00:00
commit 68271579bf
51 changed files with 21652 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
(function () {
const MB = window.MagicBot;
// --- LOGIC: Commands ---
MB.sendMsg = function (msg) {
if (!MB.socket) {
alert("Socket not connected! Wait for game to load.");
return;
}
MB.socket.send(JSON.stringify(msg));
};
MB.harvestLoop = async function (start, end, count, delay) {
let sent = 0;
for (let slot = start; slot <= end; slot++) {
for (let i = 0; i < count; i++) {
MB.sendMsg({
type: 'HarvestCrop',
slot: slot,
slotsIndex: i,
scopePath: ["Room", "Quinoa"]
});
sent++;
await new Promise(r => setTimeout(r, delay));
}
}
console.log(`[MagicBot] Harvested ${sent} items.`);
};
MB.teleport = function (x, y) {
MB.sendMsg({
type: 'Teleport',
position: { x, y },
scopePath: ["Room", "Quinoa"]
});
}
MB.sellAll = function () {
MB.sendMsg({
type: 'SellAllCrops',
scopePath: ["Room", "Quinoa"]
});
}
console.log('[MagicBot] Commands module loaded.');
})();

19
extension/modules/core.js Normal file
View File

@@ -0,0 +1,19 @@
window.MagicBot = {
socket: null,
events: new EventTarget(),
state: {
coins: 0,
garden: {},
inventory: {},
playerId: null
},
// Helper to dispatch events
emit: (name, detail) => {
window.MagicBot.events.dispatchEvent(new CustomEvent(name, { detail }));
},
// Helper to listen
on: (name, callback) => {
window.MagicBot.events.addEventListener(name, (e) => callback(e.detail));
}
};
console.log('[MagicBot] Core loaded.');

View File

@@ -0,0 +1,252 @@
const MULTIPLIERS = {
"Rainbow": 50,
"Golden": 20,
"Frozen": 10,
"Bloodlit": 4,
"Pollinated": 3,
"Wet": 2,
"Chilled": 2
};
const BASELINE_GOLDEN_CHANCE = 0.01; // 1.0%
class Decision {
/**
* @param {Object} crop - { species, baseValue, scale, mutations: [] }
* @param {Object} worldEvents - {
* Time_Until_Next_Event_Hrs,
* Time_Assuming_Growth_Speed_Boost,
* Time_Remaining_Hrs,
* P_Next_Rain_Thunderstorm,
* P_Next_Frost,
* Time_To_Next_Blood_Moon_Hrs,
* Event_Active
* }
*/
static shouldHarvest(crop, worldEvents) {
// Step 3.1: Deterministic Value (V_HarvestNow)
const vHarvestNow = this.calculateDeterministicValue(crop);
// Step 3.2: Growth Progression & Time Compression
// Delta T effective
// If Event_Active is true, speed is 1.5x, else 1.0x
const speedMultiplier = worldEvents.Event_Active ? 1.5 : 1.0;
const deltaTEff = worldEvents.Time_Until_Next_Event_Hrs * speedMultiplier;
const tRemNext = worldEvents.Time_Remaining_Hrs - deltaTEff;
// Critical Check: If crop matures during wait, harvest NOW (continuing value is capped/complicated)
// Ignoring complicated capped value logic for now as per instructions "E is capped" -> imply we might just harvest?
// Actually instructions say: "If T_rem <= 0 ... calculation focuses only on expected mutation gains over Delta t"
// But usually if it matures, we might as well harvest or we risk rotting?
// For this V1 implementation, if it matures, let's assume we proceed with E calculation but T_rem is 0.
// However, the prompt says "If T_rem > 0, the harvest is delayed". Implicitly if <=0, maybe we consider it 'mature' state.
// Let's stick to the prompt's implied logic for now: calculate E regardless, but note the state.
// Step 3.3: Stochastic Transition Value (E)
const E = this.calculateStochasticTransition(crop, vHarvestNow, worldEvents);
// Step 3.4: Strategic Interval Check (Blood Moon)
let maxE = E;
let strategy = "Standard Wait";
// Check if we can wait for Blood Moon
// Use simplified logic: if generic wait leaves us with time, AND we have enough time for BM
// Note: T_rem(t + Delta_t) > 0 check from prompt
if (tRemNext > 0 && worldEvents.Time_To_Next_Blood_Moon_Hrs <= worldEvents.Time_Remaining_Hrs) {
const eBM = this.calculateBloodMoonValue(vHarvestNow);
if (eBM > E) {
maxE = eBM;
strategy = "Blood Moon Wait";
}
}
// Step 3.5: Optimal Decision Synthesis
const decision = {
action: vHarvestNow >= maxE ? "HARVEST_NOW" : "WAIT",
vHarvestNow: vHarvestNow,
expectedValue: maxE,
strategy: strategy,
details: {
E_Standard: E,
tRemNext: tRemNext
}
};
return decision;
}
static calculateDeterministicValue(crop) {
let multiplier = 1.0;
if (crop.mutations) {
crop.mutations.forEach(m => {
const val = MULTIPLIERS[m] || 1.0; // Default to 1 if unknown or wait? Assume multiplicative
multiplier *= val;
});
}
// Base Value * Mass * Multipliers
// Mass is 'scale'
return (crop.baseValue || 1) * (crop.scale || 1) * multiplier;
}
static calculateStochasticTransition(crop, vNow, worldEvents) {
const mutations = new Set(crop.mutations || []);
// A. Rain/Thunderstorm
// Base Gain: 50% chance of Wet (2x)
// Fusion: If Chilled present, Wet can fuse to Frozen (10x). Chilled (2x) is lost.
// Fusion Logic: "Rain ... has a chance to fuse it" - let's assume 100% chance IF Rain happens for this calculation?
// Prompt says: "If Chilled is present, Rain... has a chance". Let's assume the event IS Rain.
// Wait, structure is: E = P_Rain * V_Rain + ...
// Inside V_Rain: What is the gain?
// Prompt: "1. Calculate expected fusion gain factor".
// Assumption: If 'Chilled' exists, 2x becomes 10x? Or is it 50% chance?
// Prompt says "Rain... has a chance". Usually fusion is guaranteed if conditions met in these games?
// Let's assume for V1: If Rain hits, and Chilled exists => Frozen (10x).
// If Chilled NOT exists => Wet (2x) with 50% chance.
let multiplierGainA = 1.0;
if (mutations.has("Chilled")) {
// Fusion: Chilled (2x) -> Frozen (10x). Gain factor = 10 / 2 = 5x?
// Wait, vNow already includes Chilled(2x).
// New Multiplier Product would be: (Old_Prod / 2) * 10 = Old_Prod * 5.
// So Gain Factor is 5.
// Probability of fusion? "has a chance". Let's assume 100% GIVEN Rain for now, or 50%?
// "Base Gain: 50% chance of 2x Wet".
// Let's assume Fusion is also 50% chance if Chilled is there? Or maybe the event IS the fusion?
// Let's use 50% for fusion chance given Rain.
multiplierGainA = 1.0 + 0.5 * (5.0 - 1.0); // Exp Value of factor?
// Actually, let's look at value:
// avg_mult = 0.5 * 5 (Fusion) + 0.5 * 1 (Nothing) = 3.0 relative to current?
// No, if nothing happens, it stays 1.0.
// So E[Factor] = 0.5 * 5 + 0.5 * 1 = 3.0?
} else {
// Wet (2x). Gain from 1.0 to 2.0.
// If we already have Wet? Usually mutations are unique?
// Prompt says "Identify all current, UNIQUE mutations".
// If we have Wet, can we get double Wet? Assume No.
if (!mutations.has("Wet") && !mutations.has("Frozen")) { // Assuming Frozen prevents Wet?
multiplierGainA = 1.0 + 0.5 * (2.0 - 1.0); // 0.5 * 2 + 0.5 * 1 = 1.5
}
}
const V_A = vNow * multiplierGainA;
// B. Frost
// Base Gain: 50% chance of Chilled (2x).
// Fusion: If Wet present -> Frozen (10x). Replaces Wet (2x).
let multiplierGainB = 1.0;
if (mutations.has("Wet")) {
// Fusion: Wet(2x) -> Frozen(10x). Factor = 5.
// Chance: 50%?
multiplierGainB = 1.0 + 0.5 * (5.0 - 1.0); // 3.0
} else {
if (!mutations.has("Chilled") && !mutations.has("Frozen")) {
multiplierGainB = 1.0 + 0.5 * (2.0 - 1.0); // 1.5
}
}
const V_B = vNow * multiplierGainB;
// C. Other Events (Baseline)
// 1.0% chance of Golden (20x).
// Gain factor = 20.
// V_C = vNow * (1 + P_Golden * 20) ???
// Wait, "V_HarvestNow * (1 + P_Golden * 20)" -> This implies P_Golden is small,
// e.g. 0.01. So val = vNow * (1 + 0.2) = 1.2 * vNow?
// "Calculate the expected baseline gain V_C"
// Prompt formula: V_C = V_HarvestNow * (1 + P_Golden * 20)
// Is it additive? Gain = 20x value? Or is specific mutation 20x?
// If I have 1x multiplier. Get Golden -> 20x. Gain is +19x?
// Factor = 20.
// Expected Factor = (1 - P) * 1 + P * 20 = 1 - P + 20P = 1 + 19P.
// Prompt says "1 + P_Golden * 20". This is approx 1 + 20P. Close enough.
// Let's use Prompt Formula EXACTLY.
const V_C = vNow * (1 + BASELINE_GOLDEN_CHANCE * 20); // 1 + 0.01*20 = 1.2
// Total E
let P_A = worldEvents.P_Next_Rain_Thunderstorm;
let P_B = worldEvents.P_Next_Frost;
// CRITICAL CHECK: If crop matures before the event, we can't get the event benefit
// worldEvents.Time_Until_Next_Event_Hrs is the wait time.
// worldEvents.Time_Remaining_Hrs is how much growth is left.
// If Wait > Remaining, we mature before event.
// Note: Speed multiplier is already applied to Calculate tRemNext, but that's what remains AFTER wait.
// If tRemNext < 0, it means we matured DURING the wait.
// If we mature during the wait, do we get the event?
// The event starts AT the end of the wait. So if we mature BEFORE the end of wait, we miss it.
const timeToEvent = worldEvents.Time_Until_Next_Event_Hrs; // Real hours
// Need to check if we mature before this real time passed?
// Crop matures in Time_Remaining_Hrs (assuming standard speed? or current speed?)
// If current speed is 1x.
// If speed is 1.5x (Event Active), Time_Remaining_Hrs is still "hours at 1x"?
// Usually Time_Remaining is "real time remaining"?
// Let's assume Time_Remaining_Hrs is "Time until maturity in real hours at current speed" provided by caller?
// Or is it "Mass needed"?
// Prompt Check 3.2: "Time Remaining in the next state: T_rem(t+dt) = Time_Remaining_Hrs - Delta_T_eff"
// Delta_T_eff = Time_Until_Next_Event * Speed.
// This implies Time_Remaining_Hrs is in "Growth Units" (Hours at 1x speed).
// If T_rem(t+dt) <= 0, we mature *during* or *before* the event.
// Since the event triggers *at* t+dt, we are already mature.
// Thus, we likely miss the specific "Event Mutation" which usually requires growing *during* the event (or the event affecting the plant).
// If the event is "Rain starts now", and we are already mature, does Rain affect us?
// Usually mutations happen *while growing*.
// So if tRemNext <= 0, we set P_A and P_B to 0.
// Also, from user prompt: "If T_rem(t+dt) <= 0 ... calculation focuses only on expected mutation gains over Delta t"
// This confirms we exclude the specific Event outcome that happens *at* the transition.
const deltaTEff = worldEvents.Time_Until_Next_Event_Hrs * (worldEvents.Event_Active ? 1.5 : 1.0);
const nextStateRem = worldEvents.Time_Remaining_Hrs - deltaTEff;
if (nextStateRem <= 0) {
P_A = 0;
P_B = 0;
// P_C absorbs the probability? Or do we just lose it?
// "The expected value of waiting, E, is the sum... for the three possible random event outcomes"
// If Rain is impossible, does P_Rain become "Nothing"?
// Yes, prob of Rain event occuring is real, but effect is null.
// So V_A becomes V_Now? Or just exclude from sum?
// Since probabilities must sum to 1?
// Actually, P_Next_Rain is prob of *Event*. The Event happens regardless of our crop.
// But our *Gain* is 0.
// So V_A = V_Now. V_B = V_Now.
// Effectively, we just take the Baseline gain.
}
const P_C = 1.0 - P_A - P_B;
// If we can't benefit from events, V_A and V_B should fallback to V_Now?
// Or V_C?
// If Event happens (Rain), but we are mature -> No effect -> Value = V_Now.
const final_V_A = (nextStateRem <= 0) ? vNow : V_A;
const final_V_B = (nextStateRem <= 0) ? vNow : V_B;
// Baseline gain applies over Delta t?
// "1.0% random chance per growth cycle (Delta t)"
// If we only grow for part of Delta t?
// Let's assume Baseline applies fully for now, or maybe only if we are growing?
// If we stop growing halfway, chance is lower.
// But let's stick to simple: V_C applies.
return (P_A * final_V_A) + (P_B * final_V_B) + (P_C * V_C);
}
static calculateBloodMoonValue(vNow) {
// 33% chance of 4x Bloodlit.
// Note: Bloodlit is 4x.
// Formula from prompt: vNow * (1 + 0.33 * 4x)
// "4x" probably means the number 4.
return vNow * (1 + 0.33 * 4);
}
}
// Browser / Node compatibility
if (typeof window !== 'undefined') {
window.MagicBot = window.MagicBot || {};
window.MagicBot.Decision = Decision;
}
if (typeof module !== 'undefined') {
module.exports = Decision;
}

385
extension/modules/main.js Normal file
View File

@@ -0,0 +1,385 @@
// Main Entry Point
console.log("%c Magic Garden Bot Starting... ", "background: #222; color: #bada55; font-size: 20px");
(function () {
const MB = window.MagicBot;
// --- AUTOMATION STATE ---
MB.automation = {
autoPlant: false,
autoHarvest: false,
autoSell: false,
autoSell: false,
autoFeed: false,
smartHarvest: false,
autoBuyItems: new Set(), // Set of species names to auto-buy
petDiet: new Set(), // Set of species names allowed for pets
petDiet: new Set(), // Set of species names allowed for pets
selectedSeed: null, // The seed user wants to auto-plant
weatherTracker: {
lastEndTime: Date.now(), // Assume clear sky starts now if unknown
currentEvent: null
}
};
function updateWeatherTracker() {
if (!MB.state || !MB.state.child || !MB.state.child.data) return;
const currentWeather = MB.state.child.data.weather; // 'Rain', 'Frost', or null
const tracker = MB.automation.weatherTracker;
// Detect end of event (Value -> Null)
if (tracker.currentEvent && !currentWeather) {
console.log(`[WeatherTracker] Event ${tracker.currentEvent} ended. Timer reset.`);
tracker.lastEndTime = Date.now();
}
// Detect start of event (Null -> Value)
if (!tracker.currentEvent && currentWeather) {
console.log(`[WeatherTracker] Event ${currentWeather} started!`);
}
tracker.currentEvent = currentWeather;
}
// --- AUTOMATION LOOP ---
setInterval(() => {
if (!MB.state || !MB.socket) return;
updateWeatherTracker();
// 1. Auto Harvest
if (MB.automation.autoHarvest && MB.state.garden && MB.state.garden.tileObjects) {
const now = Date.now();
Object.keys(MB.state.garden.tileObjects).forEach(slotId => {
const tile = MB.state.garden.tileObjects[slotId];
if (tile && tile.slots) {
tile.slots.forEach((s, idx) => {
if (now >= s.endTime) {
// Smart Harvest Logic
if (MB.automation.smartHarvest) {
// 0. Safety: Check for Planted Eggs
// Eggs usually have objectType 'egg' or similar.
// Based on standard state structures, if checks fail, better safe than sorry.
// Inspecting fullstate shows objectType property on the tile itself.
if (tile.objectType && tile.objectType.toLowerCase().includes('egg')) {
// console.log("Skipping Egg at slot", slotId);
return;
}
const mutations = s.mutations || [];
const isRainbow = mutations.includes('Rainbow');
const isGold = mutations.includes('Gold');
const isFrozen = mutations.includes('Frozen');
const isWet = mutations.includes('Wet');
const duration = s.endTime - s.startTime; // Duration in ms
// 1. Rainbow Strategy: Harvest Priority (x50)
// Rainbow doesn't stack well and is huge value. Cash out.
if (isRainbow) {
// Harvest immediately!
// Fall through to harvest call.
}
// 2. Gold Strategy: Wait for Frozen (x200 EV)
else if (isGold && !isFrozen) {
return; // Skip
}
// 3. Long Crop Strategy (> 10 mins): Wait for Wet (x2 EV)
return; // Skip
}
// 4. Bellman Step (Section 3) Smart Harvest
// Check if decision module is loaded
if (MB.Decision) {
const worldEvents = getWorldEvents(MB.state);
const cropState = {
species: s.species || tile.species,
baseValue: s.baseValue || 1, // Need base value source?
scale: s.scale || 1, // Can use targetScale or derive current scale?
mutations: mutations
};
const events = {
...worldEvents,
Time_Remaining_Hrs: (s.endTime - Date.now()) / 3600000
};
const logic = MB.Decision.shouldHarvest(cropState, events);
// Log logic occasionally?
// console.log(`[SmartHarvest] Slot ${slotId}: ${logic.action}`, logic);
if (logic.action === 'WAIT') {
return; // Skip harvest
}
}
}
// Don't spam: check if we just sent it?
// For simplicity, relying on server or next state update.
// A simple dedupe could be added if needed.
MB.sendMsg({
type: 'HarvestCrop',
slot: parseInt(slotId),
slotsIndex: idx,
scopePath: ["Room", "Quinoa"]
});
});
}
});
}
// 2. Auto Plant
if (MB.automation.autoPlant && MB.state.garden && MB.state.garden.tileObjects) {
// Find a seed to plant
let seedToPlant = MB.automation.selectedSeed;
// Fallback: If no selected seed, find FIRST available seed in inventory
if (!seedToPlant && MB.state.inventory && MB.state.inventory.items) {
const firstSeed = MB.state.inventory.items.find(i => i.itemType === 'Seed' && (i.quantity > 0 || i.count > 0));
if (firstSeed) {
// Extract species name similar to ui.js logic
if (firstSeed.species) seedToPlant = firstSeed.species;
else if (firstSeed.parameters && firstSeed.parameters.species) seedToPlant = firstSeed.parameters.species;
else if (firstSeed.parameters && firstSeed.parameters.speciesIds) {
const vals = Object.values(firstSeed.parameters.speciesIds);
if (vals.length > 0) seedToPlant = vals[0];
}
}
}
if (seedToPlant) {
// Find empty slot
// We'll plant ONE seed per tick to avoid flooding and ensuring state sync
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) {
MB.sendMsg({
type: "PlantSeed",
slot: emptySlot,
species: seedToPlant,
scopePath: ["Room", "Quinoa"]
});
}
}
}
// 3. Auto Sell
if (MB.automation.autoSell) {
// Check if we have any produce? For now, just trigger SellAll periodically.
// Just trigger it every loop? Might be too much.
// Let's check inventory for "Produce"
if (MB.state.inventory && MB.state.inventory.items) {
const hasProduce = MB.state.inventory.items.some(i => i.itemType === 'Produce');
if (hasProduce) {
MB.sellAll();
}
}
}
// 4. Auto Buy
if (MB.automation.autoBuyItems.size > 0 && MB.state.shops && MB.state.shops.seed) {
MB.state.shops.seed.inventory.forEach(item => {
if (MB.automation.autoBuyItems.has(item.species)) {
// Check stock
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);
if (currentStock > 0) {
// Buy ONE
MB.sendMsg({
scopePath: ["Room", "Quinoa"],
type: "PurchaseSeed",
species: item.species
});
console.log(`[MagicBot] Auto-buying ${item.species}`);
}
}
});
}
}, 1000); // Run every 1 second
// --- HELPER FUNCTIONS ---
MB.feedPets = function () {
if (!MB.state.inventory || !MB.state.inventory.items) return;
const inventory = MB.state.inventory.items;
const pets = inventory.filter(i => i.itemType === 'Pet');
if (pets.length === 0) return;
// Group food by species for easy lookup
const foodMap = {};
inventory.forEach(i => {
if (i.itemType === 'Produce' || i.itemType === 'Seed' || i.itemType === 'Plant') { // Assuming crops are Produce, but could be Plant? Wiki says "crops". Usually 'Produce' in inventory.
// Double check itemType for crops in inventory.
// In fullstate.json, crops aren't explicitly shown in inventory in the snippet, but seeds are.
// Assuming harvested crops are 'Produce'.
// We will check matches against petDiet.
let species = i.species;
// Fallback for complex objects
if (!species && i.parameters && i.parameters.species) species = i.parameters.species;
if (species && (i.quantity > 0 || i.count > 0)) {
if (!foodMap[species]) foodMap[species] = [];
foodMap[species].push(i);
}
}
});
pets.forEach(pet => {
// Check hunger?
// "hunger": 405.555... This likely means current fullness or hunger?
// Wiki says "restores hunger".
// If it's 0-100 scale, 405 is odd. Maybe it's max hunger?
// Or maybe it's "hunger points" where higher = more hungry?
// Or higher = more full?
// Let's assume we feed if we can. The server will reject if full.
if (MB.automation.petDiet.size === 0) return;
// Find valid food
for (const allowedSpecies of MB.automation.petDiet) {
if (foodMap[allowedSpecies]) {
const foodItems = foodMap[allowedSpecies];
// Use the first available stack
const foodItem = foodItems[0];
if (foodItem) {
// Send Feed Command
// We need the proper message structure.
// User request: {"scopePath":["Room","Quinoa"],"type":"FeedPet","petItemId":"...","cropItemId":"..."}
console.log(`[MagicBot] Feeding ${pet.petSpecies} with ${allowedSpecies}`);
MB.sendMsg({
scopePath: ["Room", "Quinoa"], // value from user request, should be dynamic? but hardcoded in other places in this file. "Quinoa" seems to be the game instance.
type: "FeedPet",
petItemId: pet.id,
cropItemId: foodItem.id
});
// Optimistically decrement count to prevent double usage in same tick (if we loop)?
// But we are sending async.
// Just one food per pet per tick is safer to avoid spam.
break;
}
}
}
});
};
// Override sellAll to chain feeding
const originalSellAll = MB.sellAll || (() => { });
MB.sellAll = function () {
if (MB.automation.autoFeed) {
MB.feedPets();
// Small delay? Or just send immediately after?
// WebSocket is ordered.
}
// originalSellAll implementation wasn't defined in the file view I saw,
// but 'MB.sellAll()' was called in line 131.
// It must be defined somewhere or attached to MB.
// Wait, line 85 of browser_cheats.js defines window.sell.
// line 184 of ui.js calls MB.sellAll().
// I don't see MB.sellAll defined in core.js or main.js PROPERLY.
// Let's look at ui.js again.
// ui.js calls `MB.sellAll()`.
// check `browser_cheats.js` - it defines `window.sell`.
// `main.js` calls `MB.sellAll()`.
// Detailed check:
// core.js defines MB = {...}.
// main.js uses MB.
// ui.js uses MB.
// I suspect MB.sellAll is NOT defined yet or I missed it in `main.js` or `core.js` or `ui.js`.
// Ah, `ui.js` line 184 calls it.
// `main.js` line 131 calls it.
// If it's not defined, it will crash.
// Let's define it if missing.
if (!MB.state.socket && window.gameSocket) {
// Fallback to browser cheats style if needed
}
MB.sendMsg({
type: 'SellAllCrops',
scopePath: ["Room", "Quinoa"]
});
console.log("[MagicBot] Selling all crops");
console.log("[MagicBot] Selling all crops");
};
function getWorldEvents(state) {
const defaults = {
Time_Until_Next_Event_Hrs: 0.5, // Fallback
P_Next_Rain_Thunderstorm: 0.0,
P_Next_Frost: 0.0,
Time_To_Next_Blood_Moon_Hrs: 999, // Unknown for now
Event_Active: false
};
if (!state || !MB.automation.weatherTracker) return defaults;
const tracker = MB.automation.weatherTracker;
const currentWeather = tracker.currentEvent;
// Logic: 20-35 mins interval (Avg 27.5m)
// Rain 75%, Frost 25%
const AVG_INTERVAL_MS = 27.5 * 60 * 1000;
let timeUntilNextHrs = 0;
let pRain = 0;
let pFrost = 0;
let active = false;
if (currentWeather) {
// Event is ACTIVE
active = true;
// If active, we are IN the weather.
// Probabilities for "Next Event types" are effectively 1.0 for the current type for short-term planning?
if (currentWeather.includes('Rain') || currentWeather.includes('Thunder')) {
pRain = 1.0;
} else if (currentWeather.includes('Frost') || currentWeather.includes('Snow')) {
pFrost = 1.0;
}
// Planning Horizon: 5 mins (0.083 hrs) to accumulate mutation checks
timeUntilNextHrs = 0.083;
} else {
// Clear Sky - Waiting for Next Event
const nextEventTime = tracker.lastEndTime + AVG_INTERVAL_MS;
const diffMs = nextEventTime - Date.now();
timeUntilNextHrs = Math.max(0, diffMs) / 3600000; // Hours
// If we heavily overdue (e.g. > 35 mins), probability increases?
// For V1, fixed probs:
pRain = 0.75;
pFrost = 0.25;
}
return {
Time_Until_Next_Event_Hrs: timeUntilNextHrs,
P_Next_Rain_Thunderstorm: pRain,
P_Next_Frost: pFrost,
Time_To_Next_Blood_Moon_Hrs: 999,
Event_Active: active
};
}
})();

View File

@@ -0,0 +1,49 @@
(function () {
const OriginalSend = WebSocket.prototype.send;
WebSocket.prototype.send = function (data) {
if (!window.MagicBot.socket || window.MagicBot.socket.readyState >= 2) {
window.MagicBot.socket = this;
window.MagicBot.emit('socket_connected', true);
console.log("[MagicBot] Socket captured.");
// Extract Player ID from URL
try {
const urlObj = new URL(this.url);
let pid = urlObj.searchParams.get('playerId');
if (pid) pid = pid.replace(/^"|"$/g, '');
window.MagicBot.state.playerId = pid;
console.log("[MagicBot] Player ID:", pid);
} catch (e) { }
this.addEventListener('close', () => {
window.MagicBot.emit('socket_connected', false);
});
// Capture Incoming
this.addEventListener('message', (e) => {
window.MagicBot.emit('log', { type: 'RX', data: e.data });
try {
const msg = JSON.parse(e.data);
window.MagicBot.emit('packet_received', msg);
} catch (err) { }
});
}
// Capture Outgoing
window.MagicBot.emit('log', { type: 'TX', data: data });
return OriginalSend.apply(this, arguments);
};
// Helper to send messages safely
window.MagicBot.send = function (msg) {
if (window.MagicBot.socket && window.MagicBot.socket.readyState === 1) {
window.MagicBot.socket.send(JSON.stringify(msg));
} else {
console.error("Socket not ready.");
}
};
console.log('[MagicBot] Socket hook loaded.');
})();

173
extension/modules/state.js Normal file
View File

@@ -0,0 +1,173 @@
(function () {
const MB = window.MagicBot;
// Store the raw full state to apply patches against
let rawState = null;
MB.on('packet_received', (msg) => {
// Log to discover flow
if (msg.type !== 'Ping' && msg.type !== 'Pong' && msg.type !== 'PartialState') {
console.log(`[MagicBot] RX: ${msg.type}`, Object.keys(msg));
}
// --- DATA COLLECTION STREAM ---
if (msg.type !== 'Ping' && msg.type !== 'Pong') { // Stream everything non-heartbeat
fetch('http://localhost:5454', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: msg.type, payload: msg })
}).catch(e => console.warn('[MagicBot] Data Stream Error:', e));
}
// ------------------------------
if (msg.type === 'Welcome') {
handleWelcome(msg);
} else if (msg.type === 'State') {
handleState(msg);
} else if (msg.type === 'PartialState') {
handlePartialState(msg);
}
});
function handleWelcome(msg) {
console.log("[MagicBot] Parsing Welcome...");
rawState = msg.fullState.data; // Store raw root
// Fix: Explicitly attach child (Quinoa/Game Scope) because 'data' doesn't contain it
if (msg.fullState.child) {
rawState.child = msg.fullState.child;
}
// Initial parse
parseRawState();
}
function handleState(msg) {
console.log("[MagicBot] Handling State message...", msg);
// Replace or merge? Usually 'State' is a full dump of a specific scope.
// For simplicity, we might just re-process.
// CAUTION: If it's just 'Quinoa' data, we attach it to rawState.child.data?
// Let's assume it updates the relevant part of rawState if we knew where it fits.
// For now, let's just try to process it directly.
const data = msg.data || msg.state || msg;
if (msg.scope === 'Quinoa' || (data.scope === 'Quinoa')) {
// It's the game state!
if (!rawState.child) rawState.child = {};
rawState.child.data = data.data || data;
parseRawState();
}
}
function handlePartialState(msg) {
if (!rawState) return;
// Apply patches to rawState
if (msg.patches && Array.isArray(msg.patches)) {
// console.log(`[MagicBot] Applying ${msg.patches.length} patches...`);
applyPatches(rawState, msg.patches);
// Re-eval the simplified state
parseRawState();
}
}
function parseRawState() {
if (!rawState) return;
const root = rawState;
// 1. Players
if (root.players) MB.state.players = root.players;
// 2. PlayerID
if (!MB.state.playerId && root.hostPlayerId) {
MB.state.playerId = root.hostPlayerId;
}
// 3. Game Data
let gameData = null;
if (root.child && root.child.data) {
gameData = root.child.data;
}
if (gameData) {
processGameData(gameData);
} else if (!MB.state.players) {
// Only warn if we have NOTHING
console.warn("[MagicBot] State looks empty.");
}
MB.emit('state_updated', MB.state);
}
function processGameData(gameData) {
if (gameData.shops) MB.state.shops = gameData.shops;
if (gameData.timer) MB.state.timer = gameData.timer;
if (gameData.userSlots && Array.isArray(gameData.userSlots)) {
const userSlot = gameData.userSlots.find(s => s.playerId === MB.state.playerId);
if (userSlot) {
MB.state.coins = userSlot.data.coinsCount;
MB.state.garden = userSlot.data.garden;
MB.state.inventory = userSlot.data.inventory;
MB.state.shopPurchases = userSlot.data.shopPurchases;
MB.state.self = userSlot;
} else {
console.warn("[MagicBot] State: Could not find user slot for self:", MB.state.playerId);
}
} else {
console.warn("[MagicBot] State: userSlots missing or invalid in gameData");
}
}
// --- JSON PATCH UTILS ---
function applyPatches(doc, patches) {
patches.forEach(patch => {
try {
applyOperation(doc, patch);
} catch (e) {
console.warn("[MagicBot] Patch failed:", e, patch);
}
});
}
function applyOperation(doc, patch) {
const path = patch.path.split('/').filter(p => p.length > 0);
let current = doc;
// Navigate to parent
for (let i = 0; i < path.length - 1; i++) {
const key = path[i];
if (current[key] === undefined) {
if (patch.op === 'add') current[key] = {}; // Auto-create path for add?
else return; // Path doesn't exist
}
current = current[key];
}
const lastKey = path[path.length - 1];
switch (patch.op) {
case 'add':
case 'replace':
if (Array.isArray(current) && !isNaN(lastKey)) {
// Array set/insert
const idx = parseInt(lastKey);
if (patch.op === 'add') current.splice(idx, 0, patch.value);
else current[idx] = patch.value;
} else {
current[lastKey] = patch.value;
}
break;
case 'remove':
if (Array.isArray(current)) {
current.splice(parseInt(lastKey), 1);
} else {
delete current[lastKey];
}
break;
}
}
console.log('[Magic Bot Extension] State module loaded.');
})();

856
extension/modules/ui.js Normal file
View File

@@ -0,0 +1,856 @@
(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.');
})();