// ==UserScript== // @name Grepolis Remote Control // @namespace http://tampermonkey.net/ // @version 3.5.9 // @description Polls grepo.haunter-pets.top for remote commands and executes them in-game (Multi-Player) // @author Dimitrios // @match https://*.grepolis.com/game/* // @grant unsafeWindow // @updateURL https://git.haunter-pets.top/haunter/grepo-remote/raw/branch/main/GrepolisRemoteControl.user.js // @downloadURL https://git.haunter-pets.top/haunter/grepo-remote/raw/branch/main/GrepolisRemoteControl.user.js // ==/UserScript== (function () { 'use strict'; const uw = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; const BASE_URL = 'https://grepo.haunter-pets.top'; // ---- Jitter helpers ----------------------------------------------- // Returns a random integer between min and max (inclusive) function randInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } // Schedules fn to run after a random ms delay, then reschedules itself function jitterLoop(fn, minMs, maxMs) { function schedule() { setTimeout(async () => { await fn(); schedule(); // reschedule with a NEW random delay every time }, randInt(minMs, maxMs)); } schedule(); } // ---------------------------------------------------------------- // Toolbar indicator button // ---------------------------------------------------------------- const btnHtml = `

Remote

`; let paused = false; function togglePause() { paused = !paused; const label = document.getElementById('grc_label'); const btn = document.getElementById('grc_btn'); if (paused) { label.textContent = 'Paused'; btn.style.filter = 'brightness(70%) sepia(100%) hue-rotate(-50deg) saturate(1000%) contrast(0.8)'; } else { label.textContent = 'Remote'; btn.style.filter = 'brightness(294%) sepia(100%) hue-rotate(200deg) saturate(1000%) contrast(0.8)'; } log(`Remote is now ${paused ? 'PAUSED' : 'ACTIVE'}`); } setTimeout(() => { if (!document.getElementById('grc_btn')) { uw.$('.tb_activities, .toolbar_activities').find('.middle').append(btnHtml); } }, 4000); uw.$(document).on('click', '#grc_btn', togglePause); // ---------------------------------------------------------------- // Helpers // ---------------------------------------------------------------- function log(msg) { console.log(`[GRC] ${msg}`); } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } // ---------------------------------------------------------------- // Push town state to relay // ---------------------------------------------------------------- function gatherState() { const towns = uw.ITowns?.towns || {}; const player = uw.Game?.player_name || ''; const player_id = uw.Game?.player_id ?? null; const alliance_id = uw.Game?.alliance_id ?? null; let alliance_name = null; try { const pm = uw.MM.getModels().Player[player_id]; if (pm && pm.attributes) alliance_name = pm.attributes.alliance_name; console.log("GrepoRemote: Extracted alliance_name =", alliance_name); } catch (e) { console.log("GrepoRemote: Failed to extract alliance_name", e); } const total_points = uw.Game?.player_points ?? 0; const world = uw.Game?.world_id || ''; const townList = Object.values(towns).map(town => { const res = town.resources(); const buildings = town.buildings()?.attributes ?? {}; const unitsObj = {}; try { const units = town.units(); if (units) { Object.keys(units).forEach(k => { unitsObj[k] = typeof units[k] === 'number' ? units[k] : (units[k]?.getAmount?.() ?? 0); }); } } catch (e) { } let buildQueue = []; try { const bo = town.buildingOrders?.(); if (bo?.models) buildQueue = bo.models.map(m => m.attributes); } catch (e) { } let buildDataMap = {}; try { const buildDataRaw = uw.MM?.getModels?.()?.BuildingBuildData?.[town.id]?.attributes?.building_data || {}; for (const k in buildDataRaw) { buildDataMap[k] = { buildable: buildDataRaw[k].buildable, dependencies: buildDataRaw[k].dependencies_fulfilled !== false, wood: buildDataRaw[k].resources_for?.wood || 0, stone: buildDataRaw[k].resources_for?.stone || 0, iron: buildDataRaw[k].resources_for?.iron || 0, pop: buildDataRaw[k].population_for || 0, build_time: buildDataRaw[k].building_time || '', can_upgrade: !!buildDataRaw[k].can_upgrade, enough_resources: !!buildDataRaw[k].enough_resources, missing_dependencies: buildDataRaw[k].missing_dependencies || [], has_max_level: !!buildDataRaw[k].has_max_level }; } } catch (e) { log(`Failed to gather build data: ${e}`); } // ---- Storage capacity ----------------------------------------------- // res.storage / res.storage_capacity are always 0 in Grepolis. // Real capacity lives in GameData keyed by the storage building level. let storageCapacity = 0; try { // Strategy 1: dedicated getter (exists on some server versions) storageCapacity = town.getStorageCapacity?.() || 0; // Strategy 2: GameData.buildingData.storage.max_storage[level] if (!storageCapacity) { const storageLevel = buildings.storage ?? 0; const gd = uw.GameData?.buildingData?.storage; storageCapacity = gd?.max_storage?.[storageLevel] || gd?.storage?.[storageLevel] || 0; } // Strategy 3: resource object fallback keys if (!storageCapacity) { storageCapacity = res.capacity || res.storage_capacity || res.storage || 0; } } catch (e) { log(`storage capacity lookup failed: ${e}`); } // ---- Market / Trade capacity ----------------------------------------- let marketCapacity = 0; try { const marketLevel = buildings.market ?? 0; const gd = uw.GameData?.buildingData?.market; marketCapacity = gd?.capacity_per_level?.[marketLevel] || 0; // Add Trade Office bonus if present if (buildings.trade_office && buildings.trade_office > 0) { marketCapacity += (uw.GameData?.buildingData?.trade_office?.capacity_extra_per_level || 500) * marketLevel; } } catch (e) { log(`market capacity lookup failed: ${e}`); } // ---- Coordinates & sea zone ----------------------------------------- let x = null, y = null, sea = null; try { x = town.getIslandCoordinateX?.() ?? null; y = town.getIslandCoordinateY?.() ?? null; if (typeof x === 'number' && typeof y === 'number') { sea = Math.floor(x / 100) * 10 + Math.floor(y / 100); } } catch (e) { } // ---- Researches ----------------------------------------------------- let researches = {}; try { const r = town.researches?.(); if (r) researches = r.attributes ?? (typeof r === 'object' ? r : {}); } catch (e) { log(`[Debug] town.researches() failed: ${e}`); } // ---- Unit Data (Costs & Dependencies) ------------------------------- let unitDataMap = {}; try { const gdUnits = uw.GameData?.units || {}; for (const u in gdUnits) { if (u === 'militia') continue; const reqBuildings = gdUnits[u].building_dependencies || {}; const reqResearch = gdUnits[u].research_dependencies || []; let missing_deps = {}; for (const reqB in reqBuildings) { if ((buildings[reqB] || 0) < reqBuildings[reqB]) { missing_deps[reqB] = { name: reqB, needed_level: reqBuildings[reqB] }; } } for (const reqR of reqResearch) { if (!researches[reqR]) { missing_deps[reqR] = { name: reqR, needed_level: 'Έρευνα' }; } } const cost = gdUnits[u].resources || {}; const w = cost.wood || 0; const s = cost.stone || 0; const i = cost.iron || 0; let enough = true; if (res.wood < w || res.stone < s || res.iron < i) enough = false; unitDataMap[u] = { wood: w, stone: s, iron: i, pop: gdUnits[u].population || 0, build_time: gdUnits[u].build_time || 0, enough_resources: enough, missing_dependencies: missing_deps }; } } catch (e) { log(`Failed to gather unit data: ${e}`); } // ---- Farm town data ----------------------------------------------- let farms = []; try { const farmCollection = uw.MM.getOnlyCollectionByName('FarmTown'); const relCollection = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation'); if (farmCollection && relCollection) { const ix = town.getIslandCoordinateX?.(); const iy = town.getIslandCoordinateY?.(); farmCollection.models.forEach(farm => { if (farm.attributes.island_x !== ix || farm.attributes.island_y !== iy) return; relCollection.models.forEach(rel => { if (rel.attributes.farm_town_id === farm.attributes.id && rel.attributes.relation_status >= 1) { farms.push({ farm_town_id: farm.attributes.id, farm_name: farm.attributes.name || '', relation_id: rel.id, relation_status: rel.attributes.relation_status, expansion_stage: rel.attributes.expansion_stage || 0, expansion_at: rel.attributes.expansion_at || 0, lootable_at: rel.attributes.lootable_at || 0 }); } }); }); } } catch (e) { } // ---- Extra town flags ----------------------------------------------- let has_premium = false; let bonuses = {}; let wonder_points = 0; try { has_premium = uw.GameDataPremium?.isAdvisorActivated?.('curator') || false; } catch (e) { } return { town_id: town.id, town_name: town.name, x, y, sea, wood: res.wood, stone: res.stone, iron: res.iron, storage: storageCapacity, market_capacity: marketCapacity, population: res.population, points: town.getPoints?.() ?? 0, god: town.god?.() ?? null, buildings, units: unitsObj, buildingOrder: buildQueue, buildData: buildDataMap, unitData: unitDataMap, researches, has_premium, bonuses, wonder_points, alliance_name, farms, }; }); return { player, player_id, alliance_id, total_points, world_id: world, towns: townList }; } function pushState() { if (paused) return; try { const payload = gatherState(); fetch(`${BASE_URL}/api/state`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }) .then(() => log(`State pushed — ${payload.towns.length} towns`)) .catch(e => log(`State push failed: ${e}`)); } catch (e) { log(`gatherState error: ${e}`); } } // ---------------------------------------------------------------- // AJAX Interceptor for Zero-Delay Push // ---------------------------------------------------------------- let pushTimeout = null; function debouncedPushState() { if (paused) return; if (pushTimeout) clearTimeout(pushTimeout); pushTimeout = setTimeout(() => { log('⚡ State change detected (AJAX). Syncing to Remote...'); pushState(); }, 1200); // Wait 1.2s for memory models to update after AJAX finishes } if (uw.$) { uw.$(document).ajaxComplete(function (e, xhr, opt) { if (!opt || !opt.url) return; // Ignore requests to our own bot server or map data if (opt.url.includes(BASE_URL)) return; if (opt.url.includes('map_tiles')) return; // Most game actions use "action=" parameter // Switching towns uses "switch_town" if (opt.url.includes('action=') || opt.url.includes('switch_town')) { debouncedPushState(); } }); } // ---------------------------------------------------------------- // Report command result back to relay // ---------------------------------------------------------------- function reportResult(cmdId, status, message) { fetch(`${BASE_URL}/api/commands/${cmdId}/result`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status, message }) }).catch(e => log(`reportResult failed: ${e}`)); } // ---------------------------------------------------------------- // Captcha detection via MutationObserver // Watches document.body for #hcaptcha_window being added/removed. // Confirmed selector from live DOM: DIV#hcaptcha_window > DIV.h-captcha // ---------------------------------------------------------------- let captchaActive = false; function reportCaptcha(detected) { const player_id = uw.Game?.player_id; if (!player_id) return; fetch(`${BASE_URL}/api/captcha/alert?player_id=${player_id}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ detected }) }).catch(e => log(`captcha report failed: ${e}`)); } function detectCaptcha() { setInterval(() => { const win = document.getElementById('hcaptcha_window'); let isVisible = false; if (win) { // Check if it's actually visible on screen (not display: none by the game) const style = window.getComputedStyle(win); if (style.display !== 'none' && style.visibility !== 'hidden') { isVisible = true; } } if (isVisible && !captchaActive) { captchaActive = true; paused = true; const label = document.getElementById('grc_label'); const btn = document.getElementById('grc_btn'); if (label) label.textContent = '⚠ CAPTCHA'; if (btn) btn.style.filter = 'brightness(70%) sepia(100%) hue-rotate(300deg) saturate(1000%) contrast(0.8)'; log('⚠ CAPTCHA detected — bot paused, alerting server'); reportCaptcha(true); } else if (!isVisible && captchaActive) { captchaActive = false; // Don't auto-resume — let the user click the button manually log('✅ Captcha resolved — alert cleared (bot remains paused)'); reportCaptcha(false); } }, 1000); } // ---------------------------------------------------------------- // Execute: Farm Upgrade / Unlock // Iterates all farm relations. Locked villages (status 0) get // unlocked; unlocked villages below max level get upgraded. // Uses random 800ms–2000ms delay between each action. // payload.threshold = minimum kill points to keep (default 0) // ---------------------------------------------------------------- async function executeFarmUpgrade(cmd) { const threshold = parseInt(cmd.payload?.threshold ?? 0); const now = Math.floor(Date.now() / 1000); let farmModels, relModels; try { farmModels = uw.MM.getOnlyCollectionByName('FarmTown')?.models; relModels = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation')?.models; } catch (e) { return { ok: false, msg: `Cannot access farm collections: ${e.message}` }; } if (!farmModels || !relModels) { return { ok: false, msg: 'Farm collections not loaded yet' }; } // Build polis list (one town per island) const islandsSeen = new Set(); const polisList = []; try { for (const town of uw.MM.getCollections().Town[0].models) { const { on_small_island, island_id, id } = town.attributes; if (on_small_island) continue; if (!islandsSeen.has(island_id)) { islandsSeen.add(island_id); polisList.push(id); } } } catch (e) { return { ok: false, msg: `Cannot build town list: ${e.message}` }; } let upgraded = 0; let unlocked = 0; let skipped = 0; let errors = 0; for (const town_id of polisList) { const town = uw.ITowns?.towns?.[town_id]; if (!town) continue; const ix = town.getIslandCoordinateX(); const iy = town.getIslandCoordinateY(); if (ix == null || iy == null) continue; for (const farm of farmModels) { if (farm.attributes.island_x !== ix || farm.attributes.island_y !== iy) continue; for (const rel of relModels) { if (rel.attributes.farm_town_id !== farm.attributes.id) continue; const status = rel.attributes.relation_status; const level = rel.attributes.expansion_stage || 0; const expAt = rel.attributes.expansion_at || 0; // Skip if upgrade already in progress if (expAt > now) { skipped++; continue; } // Skip if already max level if (status === 1 && level >= 5) { skipped++; continue; } // Skip if locked and we can't unlock (status -1 means enemy) if (status < 0) { skipped++; continue; } const isLocked = status === 0; const action = isLocked ? 'unlock' : 'upgrade'; const requestedAction = cmd.payload?.action_type; if (requestedAction && requestedAction !== action) { skipped++; continue; } if (paused) { return { ok: false, msg: 'Aborted due to pause/captcha' }; } log(`Farm ${action}: farm_id=${farm.attributes.id} level=${level} town=${town_id}`); try { uw.gpAjax.ajaxPost('frontend_bridge', 'execute', { model_url: `FarmTownPlayerRelation/${rel.id}`, action_name: action, arguments: { farm_town_id: farm.attributes.id }, town_id }); isLocked ? unlocked++ : upgraded++; } catch (e) { errors++; } // Random delay between actions: 1200ms – 2500ms await sleep(randInt(1200, 2500)); } } } pushState(); // refresh farm data after upgrades return { ok: true, msg: `Farm upgrade done: ${unlocked} unlocked, ${upgraded} upgraded, ${skipped} skipped, ${errors} errors` }; } // ---------------------------------------------------------------- // Execute: Farm Loot // Claims all ready farm towns across all towns that match // the cmd payload (town_ids list + loot_option). // Between-claim delay: random 500ms–1500ms (never below 500ms) // Between-town-group delay: random 30s–90s // ---------------------------------------------------------------- async function executeFarmLoot(cmd) { const { loot_option } = cmd.payload || {}; const option = parseInt(loot_option) || 1; const now = Math.floor(Date.now() / 1000); let farmModels, relModels; try { farmModels = uw.MM.getOnlyCollectionByName('FarmTown')?.models; relModels = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation')?.models; } catch (e) { return { ok: false, msg: `Cannot access farm collections: ${e.message}` }; } if (!farmModels || !relModels) { return { ok: false, msg: 'Farm collections not loaded yet — open island view first' }; } // Build island groups using MM (all towns, not just visible) const islandTownsMap = {}; // island_id -> [town_id1, town_id2] try { const allTowns = uw.MM.getCollections().Town[0].models; for (const town of allTowns) { const { on_small_island, island_id, id } = town.attributes; if (on_small_island) continue; if (!islandTownsMap[island_id]) islandTownsMap[island_id] = []; islandTownsMap[island_id].push(id); } } catch (e) { return { ok: false, msg: `Cannot build town list: ${e.message}` }; } const islandList = Object.keys(islandTownsMap); log(`Farm: processing ${islandList.length} islands with option=${option}`); let claimed = 0; let skipped = 0; let errors = 0; for (let i = 0; i < islandList.length; i++) { const island_id = islandList[i]; const townIds = islandTownsMap[island_id]; let selected_town_id = null; let lowest_total_res = Infinity; for (const t_id of townIds) { const t = uw.ITowns?.towns?.[t_id]; if (!t) continue; let storageCapacity = t.getStorageCapacity?.() || 0; if (!storageCapacity) { const buildings = t.buildings?.()?.attributes || {}; const storageLevel = buildings.storage ?? 0; const gd = uw.GameData?.buildingData?.storage; storageCapacity = gd?.max_storage?.[storageLevel] || gd?.storage?.[storageLevel] || 0; } const res = t.resources?.() || {}; const w = res.wood || 0; const s = res.stone || 0; const ir = res.iron || 0; // If completely full (all 3 resources >= max storage), skip this town if (storageCapacity > 0 && w >= storageCapacity && s >= storageCapacity && ir >= storageCapacity) { continue; } // Pick town with most space (lowest total resources) const total_res = w + s + ir; if (total_res < lowest_total_res) { lowest_total_res = total_res; selected_town_id = t_id; } } if (!selected_town_id) { log(`Farm: Skipping island ${island_id} (All towns are 100% full)`); skipped++; continue; } const town_id = selected_town_id; const town = uw.ITowns?.towns?.[town_id]; if (!town) { skipped++; continue; } // Use the same method as the original script const ix = town.getIslandCoordinateX(); const iy = town.getIslandCoordinateY(); if (ix == null || iy == null) { skipped++; continue; } // Find ready farms on this island (mirrors original getLootableFarms exactly) const readyFarms = []; for (const farm of farmModels) { if (farm.attributes.island_x !== ix || farm.attributes.island_y !== iy) continue; for (const rel of relModels) { if ( rel.attributes.farm_town_id === farm.attributes.id && rel.attributes.relation_status === 1 && (!rel.attributes.lootable_at || now >= rel.attributes.lootable_at) ) { readyFarms.push({ town_id, farm_town_id: rel.attributes.farm_town_id, relation_id: rel.id }); } } } if (readyFarms.length === 0) { skipped++; continue; } log(`Farm: ${readyFarms.length} ready on island of town ${town_id}`); for (const farm of readyFarms) { if (paused) { return { ok: false, msg: 'Aborted due to pause/captcha' }; } try { uw.gpAjax.ajaxPost('frontend_bridge', 'execute', { model_url: `FarmTownPlayerRelation/${farm.relation_id}`, action_name: 'claim', arguments: { farm_town_id: farm.farm_town_id, type: 'resources', option }, town_id: farm.town_id }); claimed++; } catch (e) { errors++; } // Random per-claim delay: 1000ms – 2200ms await sleep(randInt(1000, 2200)); } // Refresh map icons after claiming (same as original) try { uw.WMap.removeFarmTownLootCooldownIconAndRefreshLootTimers(); } catch (e) { } // Random between-island delay: 30s – 90s (only if more islands remain) if (i < islandList.length - 1) { if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' }; const gap = randInt(30000, 90000); log(`Farm: island done. Waiting ${(gap / 1000).toFixed(0)}s before next island...`); await sleep(gap); } } return { ok: true, msg: `Farm done: ${claimed} claimed, ${skipped} islands skipped, ${errors} errors` }; } // ---------------------------------------------------------------- // Execute: Build // ---------------------------------------------------------------- async function executeBuild(cmd) { const { town_id, payload } = cmd; const { building_id } = payload; const town = uw.ITowns?.getTown?.(town_id) || uw.ITowns?.towns?.[town_id]; if (!town) { return { ok: false, msg: `Town ${town_id} not found in ITowns` }; } // Check build queue const queueLen = town.buildingOrders?.()?.length ?? 0; const hasCurator = uw.GameDataPremium?.isAdvisorActivated?.('curator'); const maxQueue = hasCurator ? 7 : 2; if (queueLen >= maxQueue) { return { ok: false, requeue: true, msg: `Build queue full (${queueLen}/${maxQueue})` }; } // Check resources try { const buildData = uw.MM.getModels?.()?.BuildingBuildData?.[town_id] ?.attributes?.building_data?.[building_id]; if (buildData) { const res = town.resources(); const { resources_for, population_for } = buildData; if (town.getAvailablePopulation?.() < population_for) { return { ok: false, requeue: true, msg: `Not enough population for ${building_id}` }; } if (res.wood < resources_for.wood || res.stone < resources_for.stone || res.iron < resources_for.iron) { return { ok: false, requeue: true, msg: `Not enough resources for ${building_id}` }; } } } catch (e) { log(`Resource check skipped: ${e}`); } // Fire the build request — with a human-like reaction delay const reactionMs = randInt(800, 2500); log(`Waiting ${reactionMs}ms before firing buildUp (reaction time)...`); await sleep(reactionMs); if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' }; uw.gpAjax.ajaxPost('frontend_bridge', 'execute', { model_url: 'BuildingOrder', action_name: 'buildUp', arguments: { building_id }, town_id }); await sleep(500); return { ok: true, msg: `buildUp ${building_id} queued` }; } // ---------------------------------------------------------------- // Execute: Recruit // ---------------------------------------------------------------- async function executeRecruit(cmd) { const { town_id, payload } = cmd; const { unit_id, amount } = payload; const town = uw.ITowns?.getTown?.(town_id) || uw.ITowns?.towns?.[town_id]; if (!town) { return { ok: false, msg: `Town ${town_id} not found` }; } // Determine endpoint based on unit type const navalUnits = [ 'big_transporter', 'small_transporter', 'bireme', 'attack_ship', 'trireme', 'colonize_ship', 'sea_monster' ]; const endpoint = navalUnits.includes(unit_id) ? 'building_docks' : 'building_barracks'; // Fire the recruit request — with a human-like reaction delay const reactionMs = randInt(800, 2500); log(`Waiting ${reactionMs}ms before firing recruit (reaction time)...`); await sleep(reactionMs); if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' }; uw.gpAjax.ajaxPost(endpoint, 'build', { unit_id, amount: parseInt(amount) || 1, town_id }); await sleep(500); return { ok: true, msg: `Recruit ${amount}x ${unit_id} submitted` }; } // ---------------------------------------------------------------- // Execute: Market // ---------------------------------------------------------------- async function executeMarketOffer(cmd) { const { town_id, payload } = cmd; const { offer, offer_type, demand, demand_type, max_delivery_time, visibility } = payload; const town = uw.ITowns?.getTown?.(town_id) || uw.ITowns?.towns?.[town_id]; if (!town) { return { ok: false, msg: `Town ${town_id} not found` }; } const reactionMs = randInt(800, 2500); log(`Waiting ${reactionMs}ms before firing market offer (reaction time)...`); await sleep(reactionMs); if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' }; uw.gpAjax.ajaxPost('frontend_bridge', 'execute', { model_url: 'CreateOffers/' + town_id, action_name: 'createOffer', captcha: null, arguments: { offer, offer_type, demand, demand_type, max_delivery_time, visibility } }); await sleep(500); return { ok: true, msg: `Market offer posted: ${offer} ${offer_type} => ${demand} ${demand_type}` }; } // ---------------------------------------------------------------- // Execute: Research (Academy) // ---------------------------------------------------------------- async function executeResearch(cmd) { const { town_id, payload } = cmd; const { research_id } = payload; const town = uw.ITowns?.getTown?.(town_id) || uw.ITowns?.towns?.[town_id]; if (!town) { return { ok: false, msg: `Town ${town_id} not found` }; } const reactionMs = randInt(800, 2500); log(`Waiting ${reactionMs}ms before firing research (reaction time)...`); await sleep(reactionMs); if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' }; uw.gpAjax.ajaxPost('frontend_bridge', 'execute', { model_url: 'ResearchOrder', action_name: 'research', arguments: { id: research_id }, town_id: town_id }); await sleep(500); return { ok: true, msg: `Research ${research_id} queued` }; } // ---------------------------------------------------------------- // Poll for and execute pending commands (build + recruit + market) // ---------------------------------------------------------------- async function pollAndExecute() { if (paused) return; const player_id = uw.Game?.player_id; if (!player_id) return; let cmdData; try { const res = await fetch(`${BASE_URL}/api/commands/pending?player_id=${player_id}`); cmdData = await res.json(); } catch (e) { log(`Poll failed: ${e}`); return; } // Build queue: one command per town (all towns build in the same poll cycle) const buildCmds = cmdData.builds || []; const recruitCmd = cmdData.recruit; const marketCmd = cmdData.market; const researchCmd = cmdData.research; const farmCmd = cmdData.farm; const farmUpgradeCmd = cmdData.farm_upgrade; 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_loot') result = await executeFarmLoot(cmd); else if (cmd.type === 'farm_upgrade') result = await executeFarmUpgrade(cmd); 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 ALL town build commands (one per town, sequential with inter-town delay) for (let i = 0; i < buildCmds.length; i++) { await execute(buildCmds[i]); if (i < buildCmds.length - 1) { // Random gap between towns so it doesn't look 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); // Auto-farm: if enabled, claim all ready farms (no explicit command needed) const farmSettings = cmdData.farm_settings || {}; if (farmSettings.enabled && !farmCmd) { const nowTs = Math.floor(Date.now() / 1000); // Check if ANY farm relation is ready 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) { /* silent */ } if (readyFarms.length > 0) { // Check if the CURRENT town's warehouse is full (>95%) let allFull = true; let claimedAny = false; // Iterate over all towns that have ready farms const towns = Object.values(uw.ITowns?.towns || {}); for (const town of towns) { const storage = town.resources?.()?.storage || town.get?.('storage') || 0; const wood = town.resources?.()?.wood || town.get?.('wood') || 0; const stone = town.resources?.()?.stone || town.get?.('stone') || 0; const iron = town.resources?.()?.iron || town.get?.('iron') || 0; if (!storage) continue; const maxRes = Math.max(wood, stone, iron); const pct = maxRes / storage; if (pct < 0.95) { // This town has room — loot using its town_id context allFull = false; log(`⚡ Auto-farm: looting into town ${town.get?.('name')} (${Math.round(pct*100)}% full)`); await executeFarmLoot({ payload: { loot_option: farmSettings.loot_option } }); claimedAny = true; pushState(); break; // one loot pass is enough per poll cycle } } if (allFull) { log('⚠️ Auto-farm: ALL warehouses are full (>95%) — skipping loot this cycle'); // Report full status to backend so farm.html can show notice try { await fetch(`${BASE_URL}/api/farm_status?player_id=${uw.Game.player_id}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ warehouse_full: true }) }); } catch(e) {} } else if (claimedAny) { // Clear the full flag try { await fetch(`${BASE_URL}/api/farm_status?player_id=${uw.Game.player_id}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ warehouse_full: false }) }); } catch(e) {} } } } } // ---------------------------------------------------------------- // Boot // ---------------------------------------------------------------- window.addEventListener('load', () => { log('Grepolis Remote Control v3.5.9 loaded'); // Start captcha watcher immediately detectCaptcha(); // Push state once after load, then heartbeat every 1-2 minutes // The AJAX interceptor handles the real-time syncing! setTimeout(pushState, 5000); jitterLoop(pushState, 60000, 120000); // Poll for commands every 8–18 seconds (randomized jitter) jitterLoop(pollAndExecute, 8000, 18000); }); })();