Files
grepo-remote/bot_modules/05_main.js
2026-05-02 02:58:41 +03:00

244 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ================================================================
// 05_main.js — Poll loop, command dispatch, boot
// Depends on: everything above
// ================================================================
// Shared farm state — prevents auto-farm and explicit farm_loot commands
// from running concurrently. Also caches last-known farm settings so the
// auto-farm loop doesn't need its own API call.
let farmLootRunning = false;
let lastKnownFarmSettings = {};
// Loot option → cooldown ms (matches game's farm timer options)
const LOOT_TIMINGS = { 1: 300000, 2: 1200000, 3: 5400000, 4: 14400000 };
// ----------------------------------------------------------------
// scheduleNextFarm — fires autoFarmLoop once, then reschedules
// Delay = loot_option cooldown + random 30-120s human jitter.
// This mirrors ModernBot's pattern: run exactly when farms are ready.
// ----------------------------------------------------------------
function scheduleNextFarm(isFirstRun = false) {
let totalMs = 15000; // 15 seconds for the first run to catch already-ready farms
if (!isFirstRun) {
const option = lastKnownFarmSettings.loot_option || 1;
const baseMs = LOOT_TIMINGS[option] || 300000;
const jitterMs = randInt(30000, 120000); // +30 to +120 s
totalMs = baseMs + jitterMs;
}
log(`⏰ Next auto-farm in ${(totalMs / 60000).toFixed(1)} min`);
setTimeout(async () => {
await autoFarmLoop();
scheduleNextFarm(false);
}, totalMs);
}
// ----------------------------------------------------------------
// pollAndExecute — runs every 818 s (main command loop)
// Handles builds, recruits, market, research, explicit farm commands.
// Auto-farm has its own separate loop below.
// ----------------------------------------------------------------
async function pollAndExecute() {
if (paused) return;
const player_id = uw.Game?.player_id;
const world_id = uw.Game?.world_id;
if (!player_id) return;
let cmdData;
try {
const res = await apiFetch(`${BASE_URL}/api/commands/pending?player_id=${player_id}&world_id=${world_id}`);
cmdData = await res.json();
} catch (e) {
log(`Poll failed: ${e}`);
return;
}
// Cache farm + bot settings so autonomous loops can read them without extra calls
lastKnownFarmSettings = cmdData.farm_settings || {};
lastKnownBotSettings = cmdData.bot_settings || {};
// Handle manual bootcamp attack trigger
if (lastKnownBotSettings.attack_now) {
log('Manual bootcamp attack requested! Firing immediately...');
// Fire asynchronously so it doesn't block the rest of pollAndExecute
setTimeout(autoBootcampLoop, 0);
}
// Feature flags — default to all on if server doesn't send them (backward compatible)
const features = cmdData.enabled_features || ['farm', 'admin'];
const farmOn = features.includes('farm');
const adminOn = features.includes('admin');
// Build: one command per town (server returns an array)
const buildCmds = adminOn ? (cmdData.builds || []) : [];
const recruitCmd = adminOn ? cmdData.recruit : null;
const marketCmd = adminOn ? cmdData.market : null;
const researchCmd = adminOn ? cmdData.research : null;
const farmCmd = farmOn ? cmdData.farm : null;
const farmUpgradeCmd = farmOn ? cmdData.farm_upgrade : null;
if (cmdData.sync_requested) {
log('Sync requested by server — pushing state immediately');
pushState();
}
const execute = async (cmd) => {
if (!cmd) return;
log(`Executing command #${cmd.id} — type:${cmd.type} town:${cmd.town_id}`);
if (paused) {
log(`[Paused] Ignoring command #${cmd.id}`);
return;
}
let result;
try {
if (cmd.type === 'build') result = await executeBuild(cmd);
else if (cmd.type === 'recruit') result = await executeRecruit(cmd);
else if (cmd.type === 'market_offer') result = await executeMarketOffer(cmd);
else if (cmd.type === 'research') result = await executeResearch(cmd);
else if (cmd.type === 'farm_upgrade') result = await executeFarmUpgrade(cmd);
else if (cmd.type === 'farm_loot') {
// Guard: if auto-farm is mid-run, requeue rather than overlap
if (farmLootRunning) {
result = { ok: false, requeue: true, msg: 'Auto-farm in progress — requeueing' };
} else {
farmLootRunning = true;
try { result = await executeFarmLoot(cmd); }
finally { farmLootRunning = false; }
}
}
else result = { ok: false, msg: `Unknown type: ${cmd.type}` };
} catch (e) {
result = { ok: false, msg: `Exception: ${e}` };
}
const finalStatus = result.requeue ? 'pending' : (result.ok ? 'done' : 'failed');
log(`Command #${cmd.id}: ${finalStatus === 'done' ? '✅' : finalStatus === 'pending' ? '⏳' : '❌'} ${result.msg}`);
reportResult(cmd.id, finalStatus, result.msg);
};
// Execute one build command per town (simultaneous queue draining across all villages)
for (let i = 0; i < buildCmds.length; i++) {
await execute(buildCmds[i]);
if (i < buildCmds.length - 1) {
// Random inter-town gap — avoids looking like a macro
const gap = randInt(1500, 3000);
log(`Build: town done. Waiting ${gap}ms before next town...`);
await sleep(gap);
}
}
await execute(recruitCmd);
await execute(marketCmd);
await execute(researchCmd);
await execute(farmCmd);
await execute(farmUpgradeCmd);
}
// ----------------------------------------------------------------
// autoFarmLoop — runs every 60120 s (independent of main poll)
// Checks warehouse capacity and loots ready farms automatically.
// Completely decoupled from pollAndExecute so builds/recruits
// are never blocked by the long inter-island farm delays.
// ----------------------------------------------------------------
async function autoFarmLoop() {
if (paused) return;
const farmSettings = lastKnownFarmSettings;
if (!farmSettings.enabled) return;
// Don't overlap with an explicit farm_loot command running in main loop
if (farmLootRunning) {
log('Auto-farm: explicit farm_loot in progress — skipping this cycle');
return;
}
// Check if any farms are actually ready before doing anything heavy
const nowTs = Math.floor(Date.now() / 1000);
let readyFarms = [];
try {
const coll = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation');
readyFarms = coll?.models?.filter(r =>
r.attributes.relation_status === 1 &&
(r.attributes.lootable_at || 0) <= nowTs
) || [];
} catch (e) { return; }
if (readyFarms.length === 0) return;
log(`⚡ Auto-farm: ${readyFarms.length} ready farms found`);
// Check if ALL warehouses are already full (>95%) — no point looting
const towns = Object.values(uw.ITowns?.towns || {});
let allFull = true;
for (const town of towns) {
const res = town.resources?.() || {};
let storage = town.getStorageCapacity?.() || 0;
if (!storage) {
const buildings = town.buildings?.()?.attributes || {};
const storageLevel = buildings.storage ?? 0;
const gd = uw.GameData?.buildingData?.storage;
storage = gd?.max_storage?.[storageLevel] || gd?.storage?.[storageLevel] || 0;
}
if (!storage) storage = res.capacity || res.storage_capacity || res.storage || 0;
if (!storage) continue;
const maxRes = Math.max(res.wood || 0, res.stone || 0, res.iron || 0);
if (maxRes / storage < 0.95) { allFull = false; break; }
}
const player_id = uw.Game?.player_id;
if (allFull) {
log('⚠️ Auto-farm: ALL warehouses are full (>95%) — skipping loot this cycle');
try {
const world_id = uw.Game?.world_id || '';
await apiFetch(`${BASE_URL}/api/farm_status?player_id=${player_id}&world_id=${world_id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ warehouse_full: true })
});
} catch (e) {}
return;
}
// All clear — run the loot
farmLootRunning = true;
try {
await executeFarmLoot({ payload: { loot_option: farmSettings.loot_option } });
// Report success so dashboard shows last_farmed_at
try {
const world_id = uw.Game?.world_id || '';
await apiFetch(`${BASE_URL}/api/farm_status?player_id=${player_id}&world_id=${world_id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ warehouse_full: false })
});
} catch (e) {}
pushState();
} finally {
farmLootRunning = false;
}
}
// ----------------------------------------------------------------
// Boot — works whether page is already loaded or not.
// When eval()'d dynamically the 'load' event has already fired,
// so we check readyState and boot immediately in that case.
// ----------------------------------------------------------------
function boot() {
log('Grepolis Remote Control v4.2.0 (remote) loaded');
detectCaptcha();
setTimeout(pushState, 5000);
jitterLoop(pushState, 60000, 120000); // state sync every 12 min
jitterLoop(pollAndExecute, 8000, 18000, 2000); // command poll every 818 s, but start in 2s
scheduleNextFarm(true); // auto-farm timer-based (loot_option + 30120s)
jitterLoop(autoBootcampLoop, 720000, 1320000); // bootcamp every 1222 min
jitterLoop(autoRuralTradeLoop, 1500000, 2700000); // rural trade every 2545 min
}
if (document.readyState === 'complete') {
// Page already loaded (normal case when eval()'d dynamically)
boot();
} else {
// Fallback: wait for load event (shouldn't happen but safe to keep)
window.addEventListener('load', boot);
}