oh fuck i forgot
This commit is contained in:
46
extension/modules/commands.js
Normal file
46
extension/modules/commands.js
Normal 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
19
extension/modules/core.js
Normal 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.');
|
||||
252
extension/modules/decision.js
Normal file
252
extension/modules/decision.js
Normal 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
385
extension/modules/main.js
Normal 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
|
||||
};
|
||||
}
|
||||
|
||||
})();
|
||||
49
extension/modules/socket.js
Normal file
49
extension/modules/socket.js
Normal 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
173
extension/modules/state.js
Normal 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
856
extension/modules/ui.js
Normal 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.');
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user