Files
grepo-remote/GrepolisRemoteControl.user.js
2026-04-24 21:46:21 +03:00

943 lines
40 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 3.6.2
// @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 = `
<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;
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 >= 0) {
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 800ms2000ms 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;
}
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: 800ms 2000ms
await sleep(randInt(800, 2000));
}
}
}
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 500ms1500ms (never below 500ms)
// Between-town-group delay: random 30s90s
// ----------------------------------------------------------------
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) {
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: 500ms 1500ms (never below 500ms)
await sleep(randInt(500, 1500));
}
// 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) {
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);
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` };
}
// ----------------------------------------------------------------
// 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);
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}` };
}
// ----------------------------------------------------------------
// 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, Recruit queue and Market queue are independent
const buildCmd = cmdData.build;
const recruitCmd = cmdData.recruit;
const marketCmd = cmdData.market;
const farmCmd = cmdData.farm;
const farmUpgradeCmd = cmdData.farm_upgrade;
// 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 town has ready farms before triggering
const hasFarms = Object.values(uw.ITowns?.towns || {}).some(t => {
try {
const coll = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation');
return coll?.models?.some(r => r.attributes.relation_status === 1 && (r.attributes.lootable_at || 0) <= nowTs);
} catch (e) { return false; }
});
if (hasFarms) {
log('⚡ Auto-farm: ready farms detected, triggering loot...');
await executeFarmLoot({ payload: { loot_option: farmSettings.loot_option } });
pushState();
}
}
// Auto Bandit Camp: if enabled, attack/claim when ready
if (farmSettings.bandit_camp_enabled) {
try {
const spotColl = uw.MM.getOnlyCollectionByName('PlayerAttackSpot');
const spotModel = spotColl?.models?.[0];
if (spotModel) {
const spotId = spotModel.id || spotModel.attributes.id;
const nowTs = Math.floor(Date.now() / 1000);
const townId = spotModel.attributes.town_id;
// Debug log every cycle
log(`⚔️ Bandit Monitor -> Cooldown in: ${Math.max(0, spotModel.attributes.cooldown_at - nowTs)}s | Reward: ${spotModel.attributes.reward_available} | TownID: ${townId}`);
if (spotModel.attributes.reward_available) {
log('⚔️ Bandit Camp: Reward available! Waiting before claiming...');
await sleep(randInt(8000, 24000));
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
model_url: `PlayerAttackSpot/${spotId}`,
action_name: 'useReward',
captcha: null,
arguments: {},
town_id: townId,
nl_init: true
});
log('⚔️ Bandit Camp: Reward claimed!');
} else if (spotModel.attributes.cooldown_at <= nowTs) {
// Check if troops are still marching back
let hasMovements = false;
try {
const movements = uw.MM.getOnlyCollectionByName('MovementCommand')?.models || [];
hasMovements = movements.length > 0;
} catch (e) {}
if (!hasMovements) {
const town = uw.ITowns?.getTown?.(townId) || uw.ITowns?.towns?.[townId];
if (town) {
const myUnits = town.units() || {};
const allowedUnits = ['sword', 'slinger', 'archer', 'hoplite', 'rider', 'chariot', 'catapult'];
const sendUnits = {};
let totalUnits = 0;
for (let u of allowedUnits) {
if ((myUnits[u] || 0) > 0) {
sendUnits[u] = myUnits[u];
totalUnits += myUnits[u];
}
}
if (totalUnits > 0) {
log(`⚔️ Bandit Camp: Cooldown over! Attacking with ${totalUnits} units...`);
await sleep(randInt(8000, 24000));
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
model_url: `PlayerAttackSpot/${spotId}`,
action_name: 'attack',
captcha: null,
arguments: sendUnits,
town_id: townId,
nl_init: true
});
log('⚔️ Bandit Camp: Attack sent!');
} else {
log('⚔️ Bandit Camp: No units available to send.');
}
} else {
log(`⚔️ Bandit Camp: Town ${townId} not found in local state.`);
}
} else {
log('⚔️ Bandit Camp: Troops still marching — waiting for them to return...');
}
}
} else {
log('⚔️ Bandit Camp: PlayerAttackSpot collection not found in memory. Is the camp open in-game?');
}
} catch (e) {
log(`⚔️ Bandit camp error: ${e.message}`);
}
}
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}`);
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 === '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.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 concurrently — they do NOT block each other
await Promise.all([execute(buildCmd), execute(recruitCmd), execute(marketCmd)]);
if (farmCmd) await execute(farmCmd);
if (farmUpgradeCmd) await execute(farmUpgradeCmd);
}
// ----------------------------------------------------------------
// Boot
// ----------------------------------------------------------------
window.addEventListener('load', () => {
log('Grepolis Remote Control v3.5 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 818 seconds (randomized jitter)
jitterLoop(pollAndExecute, 8000, 18000);
});
})();