Files
grepo-remote/GrepolisRemoteControl.user.js
2026-04-21 19:25:36 +03:00

473 lines
19 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.
// ==UserScript==
// @name Grepolis Remote Control
// @namespace http://tampermonkey.net/
// @version 2.6
// @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';
// ---- 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 = `
<div class="divider"></div>
<div class="activity" id="grc_btn"
style="filter: brightness(70%) sepia(100%) hue-rotate(200deg) saturate(1000%) contrast(0.8);">
<p id="grc_label" style="position:relative;top:-8px;font-weight:bold;z-index:6;">Remote</p>
</div>`;
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;
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}`);
}
// ---- 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}`); }
// ---- Extra town flags -----------------------------------------------
let has_premium = false;
let bonuses = {};
let wonder_points = 0;
try {
// Proper premium check for Admin/Curator
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,
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,
};
});
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}`);
}
}
// ----------------------------------------------------------------
// 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) {
fetch(`${BASE_URL}/api/captcha/alert`, {
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: 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);
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);
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 pending commands (build + recruit in parallel)
// ----------------------------------------------------------------
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;
}
// Build queue and Recruit queue are now independent
const buildCmd = cmdData.build;
const recruitCmd = cmdData.recruit;
const execute = async (cmd) => {
if (!cmd) return;
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 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}: ${finalStatus === 'done' ? '✅' : finalStatus === 'pending' ? '⏳' : '❌'} ${result.msg}`);
reportResult(cmd.id, finalStatus, result.msg);
};
// Run both queues concurrently — they do NOT block each other
await Promise.all([execute(buildCmd), execute(recruitCmd)]);
}
// ----------------------------------------------------------------
// Boot
// ----------------------------------------------------------------
window.addEventListener('load', () => {
log('Grepolis Remote Control v2.5 loaded');
// Start captcha watcher immediately
detectCaptcha();
// Push state once after load, then every 4590 seconds (randomized)
setTimeout(pushState, 5000);
jitterLoop(pushState, 45000, 90000);
// Poll for commands every 818 seconds (randomized jitter)
jitterLoop(pollAndExecute, 8000, 18000);
});
})();