411 lines
18 KiB
JavaScript
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
|
|
};
|
|
}
|
|
|
|
})();
|