Files

411 lines
18 KiB
JavaScript

// 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.settings = {
debug: false
};
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();
// Update Weather Stats for UI
if (MB.automation.weatherTracker) {
MB.automation.weatherTracker.probabilities = getWorldEvents(MB.state);
}
// 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) => {
// Debug Logging
if (MB.settings && MB.settings.debug) {
console.log(`[AutoHarvest] Checking Slot ${slotId} Index ${idx}:`, {
species: s.species,
ready: now >= s.endTime,
timeLeft: (s.endTime - now) / 1000,
mutations: s.mutations
});
}
if (now >= s.endTime) {
const mutations = s.mutations || [];
// 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')) {
if (MB.settings && MB.settings.debug) console.log(`[AutoHarvest] Skipping Egg at slot ${slotId}`);
return;
}
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.
if (MB.settings && MB.settings.debug) console.log(`[AutoHarvest] Harvesting Rainbow at ${slotId}`);
}
// 2. Gold Strategy: Wait for Frozen (x200 EV)
else if (isGold && !isFrozen) {
if (MB.settings && MB.settings.debug) console.log(`[AutoHarvest] Skipping Gold (Waiting for Frozen) at ${slotId}`);
return; // Skip
}
// 3. Long Crop Strategy (> 10 mins): Wait for Wet (x2 EV)
// Continues to Bellman Logic below...
// 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);
if (MB.settings && MB.settings.debug) {
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"]
});
if (MB.settings && MB.settings.debug) console.log(`[AutoHarvest] Sent HarvestCrop for ${slotId}`);
}
});
}
});
}
// 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
};
}
})();