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 = ` -
-
-

Remote

-
`; +// ================================================================ +// 01_ui.js — Toolbar button + pause toggle +// Depends on: uw, log (00_config.js) +// ================================================================ - let paused = false; +const btnHtml = ` +
+
+

Remote

+
`; - 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); +});