// ==UserScript== // @name Grepolis Remote Control // @namespace http://tampermonkey.net/ // @version 1.1 // @description Polls grepo.haunter-pets.top for remote commands and executes them in-game // @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'; const POLL_INTERVAL_MS = 5000; // poll for commands const STATE_INTERVAL_MS = 30000; // push town state // ---------------------------------------------------------------- // 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 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 }; } } catch (e) { log(`Failed to gather build data: ${e}`); } return { town_id: town.id, town_name: town.name, wood: res.wood, stone: res.stone, iron: res.iron, storage: res.storage || res.storage_capacity || 0, population: res.population, points: town.getPoints?.() ?? 0, god: town.god?.() ?? null, buildings, units: unitsObj, buildingOrder: buildQueue, buildData: buildDataMap, }; }); return { player, 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}`); } } // ---------------------------------------------------------------- // 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}`)); } // ---------------------------------------------------------------- // 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, 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, msg: `Not enough resources for ${building_id}` }; } } } catch (e) { log(`Resource check skipped: ${e}`); } // Fire the build request 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'; 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` }; } // ---------------------------------------------------------------- // Poll for and execute a pending command // ---------------------------------------------------------------- async function pollAndExecute() { if (paused) return; let cmdData; try { const res = await fetch(`${BASE_URL}/api/commands/pending`); cmdData = await res.json(); } catch (e) { log(`Poll failed: ${e}`); return; } const cmd = cmdData.command; if (!cmd) return; // nothing pending log(`Executing command #${cmd.id} — type:${cmd.type} town:${cmd.town_id}`); let result; try { if (cmd.type === 'build') { result = await executeBuild(cmd); } else if (cmd.type === 'recruit') { result = await executeRecruit(cmd); } else { result = { ok: false, msg: `Unknown command type: ${cmd.type}` }; } } catch (e) { result = { ok: false, msg: `Exception: ${e.message}` }; } const finalStatus = result.requeue ? 'pending' : (result.ok ? 'done' : 'failed'); log(`Command #${cmd.id} result: ${finalStatus === 'done' ? '✅' : (finalStatus === 'pending' ? '⏳' : '❌')} ${result.msg}`); reportResult(cmd.id, finalStatus, result.msg); } // ---------------------------------------------------------------- // Boot // ---------------------------------------------------------------- window.addEventListener('load', () => { log('Grepolis Remote Control loaded'); // Push state immediately, then on interval setTimeout(pushState, 5000); setInterval(pushState, STATE_INTERVAL_MS); // Poll for commands setInterval(pollAndExecute, POLL_INTERVAL_MS); }); })();