diff --git a/bot_modules/00_config.js b/bot_modules/00_config.js
index 2c4bfce..d53724a 100644
--- a/bot_modules/00_config.js
+++ b/bot_modules/00_config.js
@@ -1,21 +1,31 @@
+// ================================================================
+// 00_config.js — Shared constants and utility helpers
+// Runs first; everything here is available to all other modules.
+// ================================================================
- const uw = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
- const BASE_URL = 'https://grepo.haunter-pets.top';
+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();
+// 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();
+ }, randInt(minMs, maxMs));
}
+ schedule();
+}
- // ----------------------------------------------------------------
+function log(msg) {
+ console.log(`[GRC] ${msg}`);
+}
+
+function sleep(ms) {
+ return new Promise(r => setTimeout(r, ms));
+}
diff --git a/bot_modules/01_ui.js b/bot_modules/01_ui.js
index f34f8a0..e75b5a2 100644
--- a/bot_modules/01_ui.js
+++ b/bot_modules/01_ui.js
@@ -1,45 +1,35 @@
- // Toolbar indicator button
- // ----------------------------------------------------------------
- const btnHtml = `
-
- `;
+// ================================================================
+// 01_ui.js — Toolbar button + pause toggle
+// Depends on: uw, log (00_config.js)
+// ================================================================
- let paused = false;
+const btnHtml = `
+
+ `;
- 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'}`);
+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}`);
+setTimeout(() => {
+ if (!document.getElementById('grc_btn')) {
+ uw.$('.tb_activities, .toolbar_activities').find('.middle').append(btnHtml);
}
+}, 4000);
- function sleep(ms) {
- return new Promise(r => setTimeout(r, ms));
- }
-
- // ----------------------------------------------------------------
+uw.$(document).on('click', '#grc_btn', togglePause);
diff --git a/bot_modules/02_state.js b/bot_modules/02_state.js
index 9052e7c..4aadd5c 100644
--- a/bot_modules/02_state.js
+++ b/bot_modules/02_state.js
@@ -1,285 +1,244 @@
- // 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;
+// ================================================================
+// 02_state.js — Gather & push town state to the relay server
+// Depends on: uw, BASE_URL, log, paused (00_config.js / 01_ui.js)
+// ================================================================
+
+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;
+ } catch (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 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}`);
+ 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) {}
- // ---- 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;
+ let buildQueue = [];
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}`);
- }
- }
+ const bo = town.buildingOrders?.();
+ if (bo?.models) buildQueue = bo.models.map(m => m.attributes);
+ } catch (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();
+ 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}`); }
- // ----------------------------------------------------------------
- // Report command result back to relay
- // ----------------------------------------------------------------
- function reportResult(cmdId, status, message) {
- fetch(`${BASE_URL}/api/commands/${cmdId}/result`, {
+ // ---- Storage capacity -----------------------------------------------
+ let storageCapacity = 0;
+ try {
+ storageCapacity = town.getStorageCapacity?.() || 0;
+ if (!storageCapacity) {
+ const storageLevel = buildings.storage ?? 0;
+ const gd = uw.GameData?.buildingData?.storage;
+ storageCapacity = gd?.max_storage?.[storageLevel] || gd?.storage?.[storageLevel] || 0;
+ }
+ 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;
+ 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, s = cost.stone || 0, i = cost.iron || 0;
+ let enough = !(res.wood < w || res.stone < s || res.iron < i);
+ 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;
+ 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: 0,
+ 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({ status, message })
- }).catch(e => log(`reportResult failed: ${e}`));
+ 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);
+}
+
+if (uw.$) {
+ uw.$(document).ajaxComplete(function (e, xhr, opt) {
+ if (!opt || !opt.url) return;
+ if (opt.url.includes(BASE_URL)) return;
+ if (opt.url.includes('map_tiles')) return;
+ 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}`));
+}
diff --git a/bot_modules/03_captcha.js b/bot_modules/03_captcha.js
index 4e649e0..0e4c210 100644
--- a/bot_modules/03_captcha.js
+++ b/bot_modules/03_captcha.js
@@ -1,48 +1,45 @@
- // 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;
+// ================================================================
+// 03_captcha.js — hCaptcha detection & server alert
+// Depends on: uw, BASE_URL, log, paused (00_config.js / 01_ui.js)
+// ================================================================
- 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}`));
- }
+let captchaActive = false;
- function detectCaptcha() {
- setInterval(() => {
- const win = document.getElementById('hcaptcha_window');
- let isVisible = 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}`));
+}
- 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;
- }
+function detectCaptcha() {
+ setInterval(() => {
+ const win = document.getElementById('hcaptcha_window');
+ let isVisible = false;
+
+ if (win) {
+ 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);
- }
-
- // ----------------------------------------------------------------
+ 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;
+ log('✅ Captcha resolved — alert cleared (bot remains paused)');
+ reportCaptcha(false);
+ }
+ }, 1000);
+}
diff --git a/bot_modules/04_execute.js b/bot_modules/04_execute.js
index c6dfaf3..41a3ead 100644
--- a/bot_modules/04_execute.js
+++ b/bot_modules/04_execute.js
@@ -1,406 +1,356 @@
- // 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);
+// ================================================================
+// 04_execute.js — All command executors
+// Depends on: uw, BASE_URL, log, sleep, randInt, paused, pushState
+// ================================================================
- 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' };
- }
+// ----------------------------------------------------------------
+// Execute: Farm Upgrade / Unlock
+// ----------------------------------------------------------------
+async function executeFarmUpgrade(cmd) {
+ const now = Math.floor(Date.now() / 1000);
- // 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` };
+ 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' };
}
- // ----------------------------------------------------------------
- // 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 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}` };
+ }
- // 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);
+ let upgraded = 0, unlocked = 0, skipped = 0, errors = 0;
- log(`Farm: processing ${islandList.length} islands with option=${option}`);
+ 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;
- let claimed = 0;
- let skipped = 0;
- let errors = 0;
+ for (const farm of farmModels) {
+ if (farm.attributes.island_x !== ix || farm.attributes.island_y !== iy) continue;
- for (let i = 0; i < islandList.length; i++) {
- const island_id = islandList[i];
- const townIds = islandTownsMap[island_id];
+ for (const rel of relModels) {
+ if (rel.attributes.farm_town_id !== farm.attributes.id) continue;
- let selected_town_id = null;
- let lowest_total_res = Infinity;
+ const status = rel.attributes.relation_status;
+ const level = rel.attributes.expansion_stage || 0;
+ const expAt = rel.attributes.expansion_at || 0;
- for (const t_id of townIds) {
- const t = uw.ITowns?.towns?.[t_id];
- if (!t) continue;
+ if (expAt > now) { skipped++; continue; }
+ if (status === 1 && level >= 5) { skipped++; continue; }
+ if (status < 0) { skipped++; 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 isLocked = status === 0;
+ const action = isLocked ? 'unlock' : 'upgrade';
- const res = t.resources?.() || {};
- const w = res.wood || 0;
- const s = res.stone || 0;
- const ir = res.iron || 0;
+ const requestedAction = cmd.payload?.action_type;
+ if (requestedAction && requestedAction !== action) { skipped++; continue; }
- // 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' };
- }
+ 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/${farm.relation_id}`,
- action_name: 'claim',
- arguments: { farm_town_id: farm.farm_town_id, type: 'resources', option },
- town_id: farm.town_id
+ model_url: `FarmTownPlayerRelation/${rel.id}`,
+ action_name: action,
+ arguments: { farm_town_id: farm.attributes.id },
+ town_id
});
- claimed++;
+ isLocked ? unlocked++ : upgraded++;
} 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);
+ await sleep(randInt(1200, 2500));
}
}
-
- return { ok: true, msg: `Farm done: ${claimed} claimed, ${skipped} islands skipped, ${errors} errors` };
}
+ pushState();
+ return { ok: true, msg: `Farm upgrade done: ${unlocked} unlocked, ${upgraded} upgraded, ${skipped} skipped, ${errors} errors` };
+}
- // ----------------------------------------------------------------
- // Execute: Build
- // ----------------------------------------------------------------
- async function executeBuild(cmd) {
- const { town_id, payload } = cmd;
- const { building_id } = payload;
+// ----------------------------------------------------------------
+// Execute: Farm Loot
+// ----------------------------------------------------------------
+async function executeFarmLoot(cmd) {
+ const { loot_option } = cmd.payload || {};
+ const option = parseInt(loot_option) || 1;
+ const now = Math.floor(Date.now() / 1000);
- 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` };
+ 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 = {};
+ 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, skipped = 0, 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, s = res.stone || 0, ir = res.iron || 0;
+
+ // Skip completely full towns
+ if (storageCapacity > 0 && w >= storageCapacity && s >= storageCapacity && ir >= storageCapacity) continue;
+
+ const total_res = w + s + ir;
+ if (total_res < lowest_total_res) {
+ lowest_total_res = total_res;
+ selected_town_id = t_id;
+ }
}
- // 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})` };
+ if (!selected_town_id) {
+ log(`Farm: Skipping island ${island_id} (All towns are 100% full)`);
+ skipped++;
+ continue;
}
- // 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}` };
+ const town_id = selected_town_id;
+ const town = uw.ITowns?.towns?.[town_id];
+ if (!town) { skipped++; continue; }
+
+ const ix = town.getIslandCoordinateX();
+ const iy = town.getIslandCoordinateY();
+ if (ix == null || iy == null) { skipped++; continue; }
+
+ 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
+ });
}
}
- } 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 (readyFarms.length === 0) { skipped++; continue; }
+ log(`Farm: ${readyFarms.length} ready on island of town ${town_id}`);
- if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
+ 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++; }
- uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
- model_url: 'BuildingOrder',
- action_name: 'buildUp',
- arguments: { building_id },
- town_id
- });
+ await sleep(randInt(1000, 2200));
+ }
- await sleep(500);
- return { ok: true, msg: `buildUp ${building_id} queued` };
+ try { uw.WMap.removeFarmTownLootCooldownIconAndRefreshLootTimers(); } catch (e) {}
+
+ 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);
+ }
}
- // ----------------------------------------------------------------
- // Execute: Recruit
- // ----------------------------------------------------------------
- async function executeRecruit(cmd) {
- const { town_id, payload } = cmd;
- const { unit_id, amount } = payload;
+ return { ok: true, msg: `Farm done: ${claimed} claimed, ${skipped} islands skipped, ${errors} errors` };
+}
- const town = uw.ITowns?.getTown?.(town_id) || uw.ITowns?.towns?.[town_id];
- if (!town) {
- return { ok: false, msg: `Town ${town_id} not found` };
- }
+// ----------------------------------------------------------------
+// Execute: Build
+// ----------------------------------------------------------------
+async function executeBuild(cmd) {
+ const { town_id, payload } = cmd;
+ const { building_id } = payload;
- // 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';
+ 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` };
- // 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` };
+ 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})` };
}
- // ----------------------------------------------------------------
- // 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
+ 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}` };
}
- });
-
- 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` };
}
+ } catch (e) { log(`Resource check skipped: ${e}`); }
- const reactionMs = randInt(800, 2500);
- log(`Waiting ${reactionMs}ms before firing research (reaction time)...`);
- await sleep(reactionMs);
+ 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' };
+ 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
- });
+ uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
+ model_url: 'BuildingOrder',
+ action_name: 'buildUp',
+ arguments: { building_id },
+ town_id
+ });
- await sleep(500);
- return { ok: true, msg: `Research ${research_id} queued` };
- }
+ 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` };
+
+ const navalUnits = [
+ 'big_transporter', 'small_transporter', 'bireme',
+ 'attack_ship', 'trireme', 'colonize_ship', 'sea_monster'
+ ];
+ const endpoint = navalUnits.includes(unit_id) ? 'building_docks' : 'building_barracks';
+
+ 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 Offer
+// ----------------------------------------------------------------
+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
+ });
+
+ await sleep(500);
+ return { ok: true, msg: `Research ${research_id} queued` };
+}
diff --git a/bot_modules/05_main.js b/bot_modules/05_main.js
index a74d776..a2d3206 100644
--- a/bot_modules/05_main.js
+++ b/bot_modules/05_main.js
@@ -1,148 +1,137 @@
- // 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;
+// ================================================================
+// 05_main.js — Poll loop, command dispatch, boot
+// Depends on: everything above
+// ================================================================
- 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}`);
+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;
+ }
+
+ const buildCmd = cmdData.build;
+ 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;
}
- // Build queue, Recruit queue and Market queue are independent
- const buildCmd = cmdData.build;
- 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();
+ 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 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;
+ 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 sequentially — humans cannot perform 3 actions simultaneously!
+ await execute(buildCmd);
+ 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);
+ 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) {
+ let allFull = true;
+ let claimedAny = false;
+
+ 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) {
+ 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;
+ }
}
- 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);
- };
-
- // Run sequentially — humans cannot perform 3 actions simultaneously!
- await execute(buildCmd);
- 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) {}
- }
+ if (allFull) {
+ log('⚠️ Auto-farm: ALL warehouses are full (>95%) — skipping loot this cycle');
+ 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) {
+ 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');
+// ----------------------------------------------------------------
+// Boot
+// ----------------------------------------------------------------
+window.addEventListener('load', () => {
+ log('Grepolis Remote Control v4.0.0 (remote) 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);
- });
+ detectCaptcha();
+ setTimeout(pushState, 5000);
+ jitterLoop(pushState, 60000, 120000);
+ jitterLoop(pollAndExecute, 8000, 18000);
+});