// ================================================================ // 04c_execute_bootcamp_trade.js // Auto-Bootcamp & Auto-Rural-Trade loops // // Both are driven by jitterLoop (registered in 05_main.js boot()). // Settings arrive via cmdData.bot_settings in the poll response — // no extra network call is needed. // // Timers (human-like, randomised): // Bootcamp : 12–22 min (camp cooldown is ~12–15 min) // RuralTrade: 25–45 min (trading is low-urgency) // ================================================================ // Shared cache — set by pollAndExecute every 8-18 s let lastKnownBotSettings = {}; // ---------------------------------------------------------------- // botLog — sends a log entry to the server // ---------------------------------------------------------------- async function botLog(player_id, feature, message) { log(`[${feature}] ${message}`); try { await apiFetch(`${BASE_URL}/api/bot-logs`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ player_id, feature, message }) }); } catch (e) { /* non-critical */ } } // ================================================================ // AUTO BOOTCAMP // ================================================================ async function autoBootcampLoop() { if (paused) return; const settings = lastKnownBotSettings; if (!settings.bootcamp_enabled) return; const player_id = uw.Game?.player_id; if (!player_id) return; let model; try { model = uw.MM.getModelByNameAndPlayerId('PlayerAttackSpot'); } catch (e) { return; } if (!model) return; // ── 1. Claim reward if available ────────────────────────────── try { const hasReward = model.hasReward?.(); if (hasReward) { const reward = model.getReward?.(); if (reward) { const isInstant = reward.power_id?.includes('instant'); const isFavor = reward.power_id?.includes('favor'); const stashable = reward.stashable; if (isInstant && !isFavor) { // Use instant rewards immediately uw.gpAjax.ajaxPost('frontend_bridge', 'execute', { model_url: `PlayerAttackSpot/${player_id}`, action_name: 'useReward', arguments: {} }); await botLog(player_id, 'bootcamp', `Reward used: ${reward.power_id}`); } else if (stashable) { uw.gpAjax.ajaxPost('frontend_bridge', 'execute', { model_url: `PlayerAttackSpot/${player_id}`, action_name: 'stashReward', arguments: {} }); await botLog(player_id, 'bootcamp', `Reward stashed: ${reward.power_id}`); } else { uw.gpAjax.ajaxPost('frontend_bridge', 'execute', { model_url: `PlayerAttackSpot/${player_id}`, action_name: 'useReward', arguments: {} }); await botLog(player_id, 'bootcamp', `Reward used (fallback): ${reward.power_id}`); } await sleep(randInt(3000, 7000)); return; // Wait for next cycle to attack } } } catch (e) { /* reward check failed — continue to attack check */ } // ── 2. Attack if no cooldown ─────────────────────────────────── try { const cooldown = model.getCooldownDuration?.() ?? 1; if (cooldown > 0) { const minRemaining = Math.round(cooldown / 60); await botLog(player_id, 'bootcamp', `Camp on cooldown — ${minRemaining} min remaining`); return; } // Check no existing attack movement to/from camp const movements = uw.MM.getModels()?.MovementsUnits; if (movements) { for (const mv of Object.values(movements)) { if (mv.attributes.destination_is_attack_spot || mv.attributes.origin_is_attack_spot) { await botLog(player_id, 'bootcamp', 'Attack already in flight — skipping'); return; } } } // Collect units from current town const currentTownId = uw.Game?.townId; if (!currentTownId) return; const town = uw.ITowns?.towns?.[currentTownId]; if (!town) return; const units = { ...town.units?.() }; delete units.militia; // Remove naval for (const unit in units) { if (uw.GameData?.units?.[unit]?.is_naval) delete units[unit]; } // Remove defensive if use_def is off if (!settings.bootcamp_use_def) { delete units.sword; delete units.archer; } // Remove zero-count for (const unit in units) { if (!units[unit] || units[unit] <= 0) delete units[unit]; } if (Object.keys(units).length === 0) { await botLog(player_id, 'bootcamp', 'No available units — skipping attack'); return; } uw.gpAjax.ajaxPost('frontend_bridge', 'execute', { model_url: `PlayerAttackSpot/${player_id}`, action_name: 'attack', arguments: units }); const unitSummary = Object.entries(units).map(([u, n]) => `${n}x${u}`).join(', '); await botLog(player_id, 'bootcamp', `Attack sent — ${unitSummary}`); } catch (e) { await botLog(player_id, 'bootcamp', `Error during attack: ${e}`); } } // ================================================================ // AUTO RURAL TRADE // Triggers only when a town's pending build command is stuck due to // insufficient resources. Trades for the specific missing resource. // // Ratio map: 1→0.25, 2→0.5, 3→0.75, 4→1.0, 5→1.25 // ================================================================ const RATIO_MAP = { 1: 0.25, 2: 0.50, 3: 0.75, 4: 1.00, 5: 1.25 }; async function autoRuralTradeLoop() { if (paused) return; const settings = lastKnownBotSettings; if (!settings.rural_trade_enabled) return; const player_id = uw.Game?.player_id; if (!player_id) return; const minRatio = RATIO_MAP[settings.rural_trade_ratio] ?? 0.75; let farmModels, relModels; try { farmModels = uw.MM.getOnlyCollectionByName('FarmTown')?.models; relModels = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation')?.models; } catch (e) { return; } if (!farmModels || !relModels) return; // ── Find towns with stuck build commands ─────────────────────── // We look at each town's in-game build queue. If the queue slot // is empty but we have a pending remote command, the build is // waiting — check which resource it needs. const towns = uw.ITowns?.towns; if (!towns) return; let tradesTotal = 0; for (const [town_id_str, town] of Object.entries(towns)) { if (paused) return; // Get next pending build for this town from BuildingBuildData let missingResource = null; try { const buildData = uw.MM.getModels()?.BuildingBuildData?.[town_id_str]; if (!buildData) continue; const orders = town.buildingOrders?.()?.models ?? []; // Only check if the in-game queue has room (build could be submitted) const queueCap = uw.GameDataPremium?.isAdvisorActivated('curator') ? 7 : 2; if (orders.length >= queueCap) continue; // Queue full — not stuck on resources // Check all building types to find one we might be trying to build // Heuristic: look for any building where we have less resources than needed const allBuildingData = buildData.attributes?.building_data ?? {}; const res = town.resources?.() ?? {}; for (const [building_id, bdata] of Object.entries(allBuildingData)) { if (!bdata.resources_for) continue; const cost = bdata.resources_for; const w = cost.wood || 0; const s = cost.stone || 0; const ir = cost.iron || 0; // Skip if we can already afford it if (res.wood >= w && res.stone >= s && res.iron >= ir) continue; // Find which resource is most lacking (relative to cost) const shortfalls = []; if (w > 0) shortfalls.push({ res: 'wood', ratio: res.wood / w }); if (s > 0) shortfalls.push({ res: 'stone', ratio: res.stone / s }); if (ir > 0) shortfalls.push({ res: 'iron', ratio: res.iron / ir }); if (shortfalls.length === 0) continue; shortfalls.sort((a, b) => a.ratio - b.ratio); missingResource = shortfalls[0].res; break; } } catch (e) { continue; } if (!missingResource) continue; // ── Find farm villages on this island offering missingResource ── const town_obj = town; const ix = town_obj.getIslandCoordinateX?.(); const iy = town_obj.getIslandCoordinateY?.(); if (ix == null || iy == null) continue; let tradeMade = false; for (const farm of farmModels) { if (farm.attributes.island_x !== ix || farm.attributes.island_y !== iy) continue; if (farm.attributes.resource_offer !== missingResource) continue; for (const rel of relModels) { if (rel.attributes.farm_town_id !== farm.attributes.id) continue; if (rel.attributes.relation_status !== 1) continue; // must be allied // Check ratio meets minimum const tradeRatio = rel.attributes.current_trade_ratio ?? 0; if (tradeRatio < minRatio) continue; const tradeCapacity = town_obj.getAvailableTradeCapacity?.() ?? 0; if (tradeCapacity < 100) continue; const amount = Math.min(tradeCapacity, 3000); if (paused) return; try { uw.gpAjax.ajaxPost('frontend_bridge', 'execute', { model_url: `FarmTownPlayerRelation/${rel.id}`, action_name: 'trade', arguments: { farm_town_id: farm.attributes.id, amount }, town_id: parseInt(town_id_str) }); await botLog(player_id, 'rural_trade', `Traded ${amount} ${missingResource} ← ${farm.attributes.name} via ${town_obj.getName?.() ?? town_id_str}`); tradesTotal++; tradeMade = true; } catch (e) { await botLog(player_id, 'rural_trade', `Trade error: ${e}`); } await sleep(randInt(800, 1800)); } if (tradeMade) break; // One trade per town per cycle is enough } if (!tradeMade && missingResource) { await botLog(player_id, 'rural_trade', `${town_obj.getName?.() ?? town_id_str} needs ${missingResource} but no suitable village found`); } await sleep(randInt(500, 1200)); } if (tradesTotal === 0) { log('[rural_trade] No stuck builds or no tradeable villages — nothing to do'); } }