Compare commits
17 Commits
backup2404
...
efa63f761f
| Author | SHA1 | Date | |
|---|---|---|---|
| efa63f761f | |||
| ae37674bcc | |||
| cf3c2e7b4f | |||
| 2921dff257 | |||
| 2a73e46a7b | |||
| 76ad37c1db | |||
| f250fbd5b6 | |||
| bb01b90889 | |||
| 0643422a30 | |||
| 2517538b88 | |||
| 1db8d744c8 | |||
| d952e7ca56 | |||
| edd7666905 | |||
| 76ab83620b | |||
| 53f1176ef8 | |||
| ef6946365c | |||
| 0ef0bef036 |
@@ -833,8 +833,8 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build queue, Recruit queue and Market queue are independent
|
// Build queue: one command per town (all towns build in the same poll cycle)
|
||||||
const buildCmd = cmdData.build;
|
const buildCmds = cmdData.builds || [];
|
||||||
const recruitCmd = cmdData.recruit;
|
const recruitCmd = cmdData.recruit;
|
||||||
const marketCmd = cmdData.market;
|
const marketCmd = cmdData.market;
|
||||||
const researchCmd = cmdData.research;
|
const researchCmd = cmdData.research;
|
||||||
@@ -873,8 +873,16 @@
|
|||||||
reportResult(cmd.id, finalStatus, result.msg);
|
reportResult(cmd.id, finalStatus, result.msg);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Run sequentially — humans cannot perform 3 actions simultaneously!
|
// Execute ALL town build commands (one per town, sequential with inter-town delay)
|
||||||
await execute(buildCmd);
|
for (let i = 0; i < buildCmds.length; i++) {
|
||||||
|
await execute(buildCmds[i]);
|
||||||
|
if (i < buildCmds.length - 1) {
|
||||||
|
// Random gap between towns so it doesn't look like a macro
|
||||||
|
const gap = randInt(1500, 3000);
|
||||||
|
log(`Build: town done. Waiting ${gap}ms before next town...`);
|
||||||
|
await sleep(gap);
|
||||||
|
}
|
||||||
|
}
|
||||||
await execute(recruitCmd);
|
await execute(recruitCmd);
|
||||||
await execute(marketCmd);
|
await execute(marketCmd);
|
||||||
await execute(researchCmd);
|
await execute(researchCmd);
|
||||||
|
|||||||
@@ -61,4 +61,4 @@ By default, the server runs on `http://localhost:5050` (or your configured domai
|
|||||||
This is an automation tool. Using scripts like this may violate the game's Terms of Service. Use responsibly and at your own risk.
|
This is an automation tool. Using scripts like this may violate the game's Terms of Service. Use responsibly and at your own risk.
|
||||||
|
|
||||||
---
|
---
|
||||||
*Created with ❤️ for Grepolis players.*
|
*Created with ❤️ for Grepolis players.* . .
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ async function executeFarmUpgrade(cmd) {
|
|||||||
isLocked ? unlocked++ : upgraded++;
|
isLocked ? unlocked++ : upgraded++;
|
||||||
} catch (e) { errors++; }
|
} catch (e) { errors++; }
|
||||||
|
|
||||||
await sleep(randInt(1200, 2500));
|
await sleep(randInt(4000, 10000));
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,17 +205,10 @@ async function executeFarmLoot(cmd) {
|
|||||||
claimed++;
|
claimed++;
|
||||||
} catch (e) { errors++; }
|
} catch (e) { errors++; }
|
||||||
|
|
||||||
await sleep(randInt(1000, 2200));
|
await sleep(randInt(500, 1500));
|
||||||
}
|
}
|
||||||
|
|
||||||
try { uw.WMap.removeFarmTownLootCooldownIconAndRefreshLootTimers(); } catch (e) {}
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ok: true, msg: `Farm done: ${claimed} claimed, ${skipped} islands skipped, ${errors} errors` };
|
return { ok: true, msg: `Farm done: ${claimed} claimed, ${skipped} islands skipped, ${errors} errors` };
|
||||||
|
|||||||
302
bot_modules/04c_execute_bootcamp_trade.js
Normal file
302
bot_modules/04c_execute_bootcamp_trade.js
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
// ================================================================
|
||||||
|
// 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; }
|
||||||
|
|
||||||
|
// Model not loaded yet (player hasn't opened the camp UI this session)
|
||||||
|
if (!model || typeof model.getLevel?.() === 'undefined') {
|
||||||
|
log('[bootcamp] PlayerAttackSpot model not ready — skipping');
|
||||||
|
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) {
|
||||||
|
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 {
|
||||||
|
// If getCooldownDuration is unavailable, skip safely
|
||||||
|
if (typeof model.getCooldownDuration !== 'function') {
|
||||||
|
log('[bootcamp] getCooldownDuration unavailable — skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cooldown = model.getCooldownDuration();
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,37 @@
|
|||||||
// Depends on: everything above
|
// Depends on: everything above
|
||||||
// ================================================================
|
// ================================================================
|
||||||
|
|
||||||
|
// Shared farm state — prevents auto-farm and explicit farm_loot commands
|
||||||
|
// from running concurrently. Also caches last-known farm settings so the
|
||||||
|
// auto-farm loop doesn't need its own API call.
|
||||||
|
let farmLootRunning = false;
|
||||||
|
let lastKnownFarmSettings = {};
|
||||||
|
|
||||||
|
// Loot option → cooldown ms (matches game's farm timer options)
|
||||||
|
const LOOT_TIMINGS = { 1: 300000, 2: 1200000, 3: 5400000, 4: 14400000 };
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// scheduleNextFarm — fires autoFarmLoop once, then reschedules
|
||||||
|
// Delay = loot_option cooldown + random 30-120s human jitter.
|
||||||
|
// This mirrors ModernBot's pattern: run exactly when farms are ready.
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
function scheduleNextFarm() {
|
||||||
|
const option = lastKnownFarmSettings.loot_option || 1;
|
||||||
|
const baseMs = LOOT_TIMINGS[option] || 300000;
|
||||||
|
const jitterMs = randInt(30000, 120000); // +30 to +120 s
|
||||||
|
const totalMs = baseMs + jitterMs;
|
||||||
|
log(`⏰ Next auto-farm in ${(totalMs / 60000).toFixed(1)} min (option ${option} + ${(jitterMs/1000).toFixed(0)}s jitter)`);
|
||||||
|
setTimeout(async () => {
|
||||||
|
await autoFarmLoop();
|
||||||
|
scheduleNextFarm();
|
||||||
|
}, totalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// pollAndExecute — runs every 8–18 s (main command loop)
|
||||||
|
// Handles builds, recruits, market, research, explicit farm commands.
|
||||||
|
// Auto-farm has its own separate loop below.
|
||||||
|
// ----------------------------------------------------------------
|
||||||
async function pollAndExecute() {
|
async function pollAndExecute() {
|
||||||
if (paused) return;
|
if (paused) return;
|
||||||
const player_id = uw.Game?.player_id;
|
const player_id = uw.Game?.player_id;
|
||||||
@@ -17,12 +48,24 @@ async function pollAndExecute() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache farm + bot settings so autonomous loops can read them without extra calls
|
||||||
|
lastKnownFarmSettings = cmdData.farm_settings || {};
|
||||||
|
lastKnownBotSettings = cmdData.bot_settings || {};
|
||||||
|
|
||||||
|
// Handle manual bootcamp attack trigger
|
||||||
|
if (lastKnownBotSettings.attack_now) {
|
||||||
|
log('Manual bootcamp attack requested! Firing immediately...');
|
||||||
|
// Fire asynchronously so it doesn't block the rest of pollAndExecute
|
||||||
|
setTimeout(autoBootcampLoop, 0);
|
||||||
|
}
|
||||||
|
|
||||||
// Feature flags — default to all on if server doesn't send them (backward compatible)
|
// Feature flags — default to all on if server doesn't send them (backward compatible)
|
||||||
const features = cmdData.enabled_features || ['farm', 'admin'];
|
const features = cmdData.enabled_features || ['farm', 'admin'];
|
||||||
const farmOn = features.includes('farm');
|
const farmOn = features.includes('farm');
|
||||||
const adminOn = features.includes('admin');
|
const adminOn = features.includes('admin');
|
||||||
|
|
||||||
const buildCmd = adminOn ? cmdData.build : null;
|
// Build: one command per town (server returns an array)
|
||||||
|
const buildCmds = adminOn ? (cmdData.builds || []) : [];
|
||||||
const recruitCmd = adminOn ? cmdData.recruit : null;
|
const recruitCmd = adminOn ? cmdData.recruit : null;
|
||||||
const marketCmd = adminOn ? cmdData.market : null;
|
const marketCmd = adminOn ? cmdData.market : null;
|
||||||
const researchCmd = adminOn ? cmdData.research : null;
|
const researchCmd = adminOn ? cmdData.research : null;
|
||||||
@@ -48,8 +91,17 @@ async function pollAndExecute() {
|
|||||||
else if (cmd.type === 'recruit') result = await executeRecruit(cmd);
|
else if (cmd.type === 'recruit') result = await executeRecruit(cmd);
|
||||||
else if (cmd.type === 'market_offer') result = await executeMarketOffer(cmd);
|
else if (cmd.type === 'market_offer') result = await executeMarketOffer(cmd);
|
||||||
else if (cmd.type === 'research') result = await executeResearch(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 if (cmd.type === 'farm_upgrade') result = await executeFarmUpgrade(cmd);
|
||||||
|
else if (cmd.type === 'farm_loot') {
|
||||||
|
// Guard: if auto-farm is mid-run, requeue rather than overlap
|
||||||
|
if (farmLootRunning) {
|
||||||
|
result = { ok: false, requeue: true, msg: 'Auto-farm in progress — requeueing' };
|
||||||
|
} else {
|
||||||
|
farmLootRunning = true;
|
||||||
|
try { result = await executeFarmLoot(cmd); }
|
||||||
|
finally { farmLootRunning = false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
else result = { ok: false, msg: `Unknown type: ${cmd.type}` };
|
else result = { ok: false, msg: `Unknown type: ${cmd.type}` };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
result = { ok: false, msg: `Exception: ${e}` };
|
result = { ok: false, msg: `Exception: ${e}` };
|
||||||
@@ -60,95 +112,120 @@ async function pollAndExecute() {
|
|||||||
reportResult(cmd.id, finalStatus, result.msg);
|
reportResult(cmd.id, finalStatus, result.msg);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Run sequentially — humans cannot perform 3 actions simultaneously!
|
// Execute one build command per town (simultaneous queue draining across all villages)
|
||||||
await execute(buildCmd);
|
for (let i = 0; i < buildCmds.length; i++) {
|
||||||
|
await execute(buildCmds[i]);
|
||||||
|
if (i < buildCmds.length - 1) {
|
||||||
|
// Random inter-town gap — avoids looking like a macro
|
||||||
|
const gap = randInt(1500, 3000);
|
||||||
|
log(`Build: town done. Waiting ${gap}ms before next town...`);
|
||||||
|
await sleep(gap);
|
||||||
|
}
|
||||||
|
}
|
||||||
await execute(recruitCmd);
|
await execute(recruitCmd);
|
||||||
await execute(marketCmd);
|
await execute(marketCmd);
|
||||||
await execute(researchCmd);
|
await execute(researchCmd);
|
||||||
await execute(farmCmd);
|
await execute(farmCmd);
|
||||||
await execute(farmUpgradeCmd);
|
await execute(farmUpgradeCmd);
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-farm: only if farm feature is enabled
|
|
||||||
const farmSettings = cmdData.farm_settings || {};
|
|
||||||
if (farmOn && 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;
|
// autoFarmLoop — runs every 60–120 s (independent of main poll)
|
||||||
let claimedAny = false;
|
// Checks warehouse capacity and loots ready farms automatically.
|
||||||
|
// Completely decoupled from pollAndExecute so builds/recruits
|
||||||
|
// are never blocked by the long inter-island farm delays.
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
async function autoFarmLoop() {
|
||||||
|
if (paused) return;
|
||||||
|
|
||||||
const towns = Object.values(uw.ITowns?.towns || {});
|
const farmSettings = lastKnownFarmSettings;
|
||||||
for (const town of towns) {
|
if (!farmSettings.enabled) return;
|
||||||
// Use same multi-strategy lookup as gatherState() — res.storage is often 0 in Grepolis
|
|
||||||
const res = town.resources?.() || {};
|
|
||||||
let storage = town.getStorageCapacity?.() || 0;
|
|
||||||
if (!storage) {
|
|
||||||
const buildings = town.buildings?.()?.attributes || {};
|
|
||||||
const storageLevel = buildings.storage ?? 0;
|
|
||||||
const gd = uw.GameData?.buildingData?.storage;
|
|
||||||
storage = gd?.max_storage?.[storageLevel] || gd?.storage?.[storageLevel] || 0;
|
|
||||||
}
|
|
||||||
if (!storage) storage = res.capacity || res.storage_capacity || res.storage || 0;
|
|
||||||
|
|
||||||
const wood = res.wood || 0;
|
// Don't overlap with an explicit farm_loot command running in main loop
|
||||||
const stone = res.stone || 0;
|
if (farmLootRunning) {
|
||||||
const iron = res.iron || 0;
|
log('Auto-farm: explicit farm_loot in progress — skipping this cycle');
|
||||||
if (!storage) continue;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const maxRes = Math.max(wood, stone, iron);
|
// Check if any farms are actually ready before doing anything heavy
|
||||||
const pct = maxRes / storage;
|
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) { return; }
|
||||||
|
|
||||||
if (pct < 0.95) {
|
if (readyFarms.length === 0) return;
|
||||||
allFull = false;
|
log(`⚡ Auto-farm: ${readyFarms.length} ready farms found`);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allFull) {
|
// Check if ALL warehouses are already full (>95%) — no point looting
|
||||||
log('⚠️ Auto-farm: ALL warehouses are full (>95%) — skipping loot this cycle');
|
const towns = Object.values(uw.ITowns?.towns || {});
|
||||||
try {
|
let allFull = true;
|
||||||
await apiFetch(`${BASE_URL}/api/farm_status?player_id=${uw.Game.player_id}`, {
|
for (const town of towns) {
|
||||||
method: 'POST',
|
const res = town.resources?.() || {};
|
||||||
headers: { 'Content-Type': 'application/json' },
|
let storage = town.getStorageCapacity?.() || 0;
|
||||||
body: JSON.stringify({ warehouse_full: true })
|
if (!storage) {
|
||||||
});
|
const buildings = town.buildings?.()?.attributes || {};
|
||||||
} catch (e) {}
|
const storageLevel = buildings.storage ?? 0;
|
||||||
} else if (claimedAny) {
|
const gd = uw.GameData?.buildingData?.storage;
|
||||||
try {
|
storage = gd?.max_storage?.[storageLevel] || gd?.storage?.[storageLevel] || 0;
|
||||||
await apiFetch(`${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 (!storage) storage = res.capacity || res.storage_capacity || res.storage || 0;
|
||||||
|
if (!storage) continue;
|
||||||
|
const maxRes = Math.max(res.wood || 0, res.stone || 0, res.iron || 0);
|
||||||
|
if (maxRes / storage < 0.95) { allFull = false; break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
const player_id = uw.Game?.player_id;
|
||||||
|
if (allFull) {
|
||||||
|
log('⚠️ Auto-farm: ALL warehouses are full (>95%) — skipping loot this cycle');
|
||||||
|
try {
|
||||||
|
await apiFetch(`${BASE_URL}/api/farm_status?player_id=${player_id}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ warehouse_full: true })
|
||||||
|
});
|
||||||
|
} catch (e) {}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All clear — run the loot
|
||||||
|
farmLootRunning = true;
|
||||||
|
try {
|
||||||
|
await executeFarmLoot({ payload: { loot_option: farmSettings.loot_option } });
|
||||||
|
// Report success so dashboard shows last_farmed_at
|
||||||
|
try {
|
||||||
|
await apiFetch(`${BASE_URL}/api/farm_status?player_id=${player_id}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ warehouse_full: false })
|
||||||
|
});
|
||||||
|
} catch (e) {}
|
||||||
|
pushState();
|
||||||
|
} finally {
|
||||||
|
farmLootRunning = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
// Boot — works whether page is already loaded or not.
|
// Boot — works whether page is already loaded or not.
|
||||||
// When eval()'d dynamically the 'load' event has already fired,
|
// When eval()'d dynamically the 'load' event has already fired,
|
||||||
// so we check readyState and boot immediately in that case.
|
// so we check readyState and boot immediately in that case.
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
function boot() {
|
function boot() {
|
||||||
log('Grepolis Remote Control v4.0.0 (remote) loaded');
|
log('Grepolis Remote Control v4.2.0 (remote) loaded');
|
||||||
detectCaptcha();
|
detectCaptcha();
|
||||||
setTimeout(pushState, 5000);
|
setTimeout(pushState, 5000);
|
||||||
jitterLoop(pushState, 60000, 120000);
|
jitterLoop(pushState, 60000, 120000); // state sync every 1–2 min
|
||||||
jitterLoop(pollAndExecute, 8000, 18000);
|
jitterLoop(pollAndExecute, 8000, 18000); // command poll every 8–18 s
|
||||||
|
scheduleNextFarm(); // auto-farm timer-based (loot_option + 30–120s)
|
||||||
|
jitterLoop(autoBootcampLoop, 720000, 1320000); // bootcamp every 12–22 min
|
||||||
|
jitterLoop(autoRuralTradeLoop, 1500000, 2700000); // rural trade every 25–45 min
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.readyState === 'complete') {
|
if (document.readyState === 'complete') {
|
||||||
|
|||||||
32
db.py
32
db.py
@@ -25,6 +25,7 @@ def init_db():
|
|||||||
payload TEXT NOT NULL, -- JSON string
|
payload TEXT NOT NULL, -- JSON string
|
||||||
status TEXT NOT NULL DEFAULT 'pending', -- pending | executing | done | failed
|
status TEXT NOT NULL DEFAULT 'pending', -- pending | executing | done | failed
|
||||||
result_msg TEXT,
|
result_msg TEXT,
|
||||||
|
position INTEGER, -- manual sort order for build queue (lower = first)
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
)
|
)
|
||||||
@@ -67,6 +68,30 @@ def init_db():
|
|||||||
)
|
)
|
||||||
''')
|
''')
|
||||||
|
|
||||||
|
# Bot settings — per-player config for bootcamp & rural-trade auto-loops
|
||||||
|
c.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS bot_settings (
|
||||||
|
player_id TEXT PRIMARY KEY,
|
||||||
|
bootcamp_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
bootcamp_use_def INTEGER NOT NULL DEFAULT 0,
|
||||||
|
rural_trade_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
rural_trade_ratio INTEGER NOT NULL DEFAULT 3, -- 1=0.25 2=0.5 3=0.75 4=1.0 5=1.25
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Bot logs — ring buffer of last 50 entries per player per feature
|
||||||
|
c.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS bot_logs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
player_id TEXT NOT NULL,
|
||||||
|
feature TEXT NOT NULL, -- 'bootcamp' | 'rural_trade'
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
c.execute('CREATE INDEX IF NOT EXISTS idx_bot_logs_player_feature ON bot_logs(player_id, feature)')
|
||||||
|
|
||||||
# Migration: add new columns if upgrading an existing database
|
# Migration: add new columns if upgrading an existing database
|
||||||
for _col in [
|
for _col in [
|
||||||
'ALTER TABLE town_state ADD COLUMN player_id TEXT',
|
'ALTER TABLE town_state ADD COLUMN player_id TEXT',
|
||||||
@@ -75,6 +100,7 @@ def init_db():
|
|||||||
'ALTER TABLE town_state ADD COLUMN y REAL',
|
'ALTER TABLE town_state ADD COLUMN y REAL',
|
||||||
'ALTER TABLE town_state ADD COLUMN sea INTEGER',
|
'ALTER TABLE town_state ADD COLUMN sea INTEGER',
|
||||||
'ALTER TABLE commands ADD COLUMN player_id TEXT',
|
'ALTER TABLE commands ADD COLUMN player_id TEXT',
|
||||||
|
'ALTER TABLE commands ADD COLUMN position INTEGER',
|
||||||
'ALTER TABLE farm_settings ADD COLUMN bandit_camp_enabled INTEGER NOT NULL DEFAULT 0',
|
'ALTER TABLE farm_settings ADD COLUMN bandit_camp_enabled INTEGER NOT NULL DEFAULT 0',
|
||||||
"ALTER TABLE clan_members ADD COLUMN features TEXT NOT NULL DEFAULT 'farm,admin'",
|
"ALTER TABLE clan_members ADD COLUMN features TEXT NOT NULL DEFAULT 'farm,admin'",
|
||||||
'ALTER TABLE users ADD COLUMN clan_id INTEGER REFERENCES clans(id)',
|
'ALTER TABLE users ADD COLUMN clan_id INTEGER REFERENCES clans(id)',
|
||||||
@@ -84,6 +110,12 @@ def init_db():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass # column already exists
|
pass # column already exists
|
||||||
|
|
||||||
|
# Back-fill position for existing rows that have NULL position
|
||||||
|
try:
|
||||||
|
c.execute('UPDATE commands SET position = id WHERE position IS NULL')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Users — website admin accounts
|
# Users — website admin accounts
|
||||||
c.execute('''
|
c.execute('''
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
|||||||
138
routes/api.py
138
routes/api.py
@@ -105,10 +105,11 @@ def receive_state():
|
|||||||
# so both queues are served in parallel without blocking each other.
|
# so both queues are served in parallel without blocking each other.
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
def _fetch_pending_of_type(c, cmd_type, player_id):
|
def _fetch_pending_of_type(c, cmd_type, player_id):
|
||||||
|
"""Fetch a single oldest pending command of a given type (recruit, market, etc.)."""
|
||||||
row = c.execute('''
|
row = c.execute('''
|
||||||
SELECT * FROM commands
|
SELECT * FROM commands
|
||||||
WHERE status = 'pending' AND type = ? AND player_id = ?
|
WHERE status = 'pending' AND type = ? AND player_id = ?
|
||||||
ORDER BY id ASC
|
ORDER BY updated_at ASC, id ASC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
''', (cmd_type, player_id)).fetchone()
|
''', (cmd_type, player_id)).fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
@@ -125,6 +126,49 @@ def _fetch_pending_of_type(c, cmd_type, player_id):
|
|||||||
'payload': json.loads(row['payload'])
|
'payload': json.loads(row['payload'])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_pending_builds_all_towns(c, player_id):
|
||||||
|
"""
|
||||||
|
Fetch ONE pending 'build' command per distinct town_id.
|
||||||
|
This allows all towns to build in parallel within a single poll cycle.
|
||||||
|
Within each town the oldest-updated command is picked first, so requeued
|
||||||
|
commands (updated_at = now) naturally sort behind fresh ones.
|
||||||
|
"""
|
||||||
|
# Get every town that has at least one pending build, ordered by
|
||||||
|
# which town has been waiting longest (MIN updated_at across its commands).
|
||||||
|
town_rows = c.execute('''
|
||||||
|
SELECT town_id
|
||||||
|
FROM commands
|
||||||
|
WHERE status = 'pending' AND type = 'build' AND player_id = ?
|
||||||
|
GROUP BY town_id
|
||||||
|
ORDER BY MIN(updated_at) ASC
|
||||||
|
''', (player_id,)).fetchall()
|
||||||
|
|
||||||
|
results = []
|
||||||
|
now = datetime.utcnow().isoformat()
|
||||||
|
for town_row in town_rows:
|
||||||
|
town_id = town_row['town_id']
|
||||||
|
row = c.execute('''
|
||||||
|
SELECT * FROM commands
|
||||||
|
WHERE status = 'pending' AND type = 'build'
|
||||||
|
AND player_id = ? AND town_id = ?
|
||||||
|
ORDER BY position ASC, id ASC
|
||||||
|
LIMIT 1
|
||||||
|
''', (player_id, town_id)).fetchone()
|
||||||
|
if not row:
|
||||||
|
continue
|
||||||
|
c.execute('''
|
||||||
|
UPDATE commands SET status = 'executing', updated_at = ?
|
||||||
|
WHERE id = ?
|
||||||
|
''', (now, row['id']))
|
||||||
|
results.append({
|
||||||
|
'id': row['id'],
|
||||||
|
'town_id': row['town_id'],
|
||||||
|
'type': row['type'],
|
||||||
|
'payload': json.loads(row['payload'])
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
@api.route('/api/commands/pending', methods=['GET'])
|
@api.route('/api/commands/pending', methods=['GET'])
|
||||||
def get_pending_command():
|
def get_pending_command():
|
||||||
player_id = request.args.get('player_id')
|
player_id = request.args.get('player_id')
|
||||||
@@ -134,7 +178,7 @@ def get_pending_command():
|
|||||||
conn = get_db()
|
conn = get_db()
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
|
|
||||||
build_cmd = _fetch_pending_of_type(c, 'build', player_id)
|
build_cmds = _fetch_pending_builds_all_towns(c, player_id) # one per town
|
||||||
recruit_cmd = _fetch_pending_of_type(c, 'recruit', player_id)
|
recruit_cmd = _fetch_pending_of_type(c, 'recruit', player_id)
|
||||||
market_cmd = _fetch_pending_of_type(c, 'market_offer', player_id)
|
market_cmd = _fetch_pending_of_type(c, 'market_offer', player_id)
|
||||||
farm_cmd = _fetch_pending_of_type(c, 'farm_loot', player_id)
|
farm_cmd = _fetch_pending_of_type(c, 'farm_loot', player_id)
|
||||||
@@ -151,6 +195,26 @@ def get_pending_command():
|
|||||||
'loot_option': farm_row['loot_option'] if farm_row else 1
|
'loot_option': farm_row['loot_option'] if farm_row else 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Bot settings (bootcamp + rural trade)
|
||||||
|
bot_row = c.execute(
|
||||||
|
'SELECT * FROM bot_settings WHERE player_id = ?', (str(player_id),)
|
||||||
|
).fetchone()
|
||||||
|
bot_settings = {
|
||||||
|
'bootcamp_enabled': bool(bot_row['bootcamp_enabled']) if bot_row else False,
|
||||||
|
'bootcamp_use_def': bool(bot_row['bootcamp_use_def']) if bot_row else False,
|
||||||
|
'rural_trade_enabled': bool(bot_row['rural_trade_enabled']) if bot_row else False,
|
||||||
|
'rural_trade_ratio': bot_row['rural_trade_ratio'] if bot_row else 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
# One-shot manual attack flag
|
||||||
|
attack_now_key = f'bootcamp_attack_now_{player_id}'
|
||||||
|
flag_row = c.execute('SELECT value FROM kv_store WHERE key = ?', (attack_now_key,)).fetchone()
|
||||||
|
if flag_row and flag_row['value'] == '1':
|
||||||
|
bot_settings['attack_now'] = True
|
||||||
|
c.execute("UPDATE kv_store SET value = '0' WHERE key = ?", (attack_now_key,))
|
||||||
|
else:
|
||||||
|
bot_settings['attack_now'] = False
|
||||||
|
|
||||||
# Feature flags — look up this player's authorized features from their clan
|
# Feature flags — look up this player's authorized features from their clan
|
||||||
member_row = c.execute(
|
member_row = c.execute(
|
||||||
'SELECT features FROM clan_members WHERE player_id = ?', (str(player_id),)
|
'SELECT features FROM clan_members WHERE player_id = ?', (str(player_id),)
|
||||||
@@ -164,13 +228,14 @@ def get_pending_command():
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'build': build_cmd,
|
'builds': build_cmds, # list: one build command per town
|
||||||
'recruit': recruit_cmd,
|
'recruit': recruit_cmd,
|
||||||
'market': market_cmd,
|
'market': market_cmd,
|
||||||
'research': research_cmd,
|
'research': research_cmd,
|
||||||
'farm': farm_cmd,
|
'farm': farm_cmd,
|
||||||
'farm_upgrade': farm_upgrade_cmd,
|
'farm_upgrade': farm_upgrade_cmd,
|
||||||
'farm_settings': farm_settings,
|
'farm_settings': farm_settings,
|
||||||
|
'bot_settings': bot_settings,
|
||||||
'enabled_features': enabled_features,
|
'enabled_features': enabled_features,
|
||||||
'sync_requested': sync_req
|
'sync_requested': sync_req
|
||||||
})
|
})
|
||||||
@@ -212,15 +277,29 @@ def sync_request():
|
|||||||
@api.route('/api/commands/<int:cmd_id>/result', methods=['POST'])
|
@api.route('/api/commands/<int:cmd_id>/result', methods=['POST'])
|
||||||
def command_result(cmd_id):
|
def command_result(cmd_id):
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
status = data.get('status', 'done') # 'done' | 'failed'
|
status = data.get('status', 'done') # 'done' | 'failed' | 'pending' (requeue)
|
||||||
msg = data.get('message', '')
|
msg = data.get('message', '')
|
||||||
|
now = datetime.utcnow().isoformat()
|
||||||
|
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
|
# Look up type + player_id for post-update hooks
|
||||||
|
cmd = conn.execute(
|
||||||
|
'SELECT type, player_id FROM commands WHERE id = ?', (cmd_id,)
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
conn.execute('''
|
conn.execute('''
|
||||||
UPDATE commands
|
UPDATE commands
|
||||||
SET status = ?, result_msg = ?, updated_at = ?
|
SET status = ?, result_msg = ?, updated_at = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
''', (status, msg, datetime.utcnow().isoformat(), cmd_id))
|
''', (status, msg, now, cmd_id))
|
||||||
|
|
||||||
|
# When an explicit farm_loot command succeeds, record the timestamp
|
||||||
|
if cmd and cmd['type'] == 'farm_loot' and status == 'done' and cmd['player_id']:
|
||||||
|
conn.execute('''
|
||||||
|
INSERT INTO kv_store (key, value, updated_at) VALUES (?, ?, ?)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at
|
||||||
|
''', (f'last_farmed_{cmd["player_id"]}', now, now))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({'ok': True})
|
return jsonify({'ok': True})
|
||||||
@@ -290,10 +369,17 @@ def farm_status():
|
|||||||
conn = get_db()
|
conn = get_db()
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
|
now = datetime.utcnow().isoformat()
|
||||||
conn.execute('''
|
conn.execute('''
|
||||||
INSERT INTO kv_store (key, value, updated_at) VALUES (?, ?, ?)
|
INSERT INTO kv_store (key, value, updated_at) VALUES (?, ?, ?)
|
||||||
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at
|
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at
|
||||||
''', (kv_key, json.dumps(data), datetime.utcnow().isoformat()))
|
''', (kv_key, json.dumps(data), now))
|
||||||
|
# Auto-farm reports warehouse_full=false when it successfully looted something
|
||||||
|
if not data.get('warehouse_full', True):
|
||||||
|
conn.execute('''
|
||||||
|
INSERT INTO kv_store (key, value, updated_at) VALUES (?, ?, ?)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at
|
||||||
|
''', (f'last_farmed_{player_id}', now, now))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({'ok': True})
|
return jsonify({'ok': True})
|
||||||
@@ -302,6 +388,46 @@ def farm_status():
|
|||||||
conn.close()
|
conn.close()
|
||||||
return jsonify(json.loads(row['value']) if row else {'warehouse_full': False})
|
return jsonify(json.loads(row['value']) if row else {'warehouse_full': False})
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# POST /api/bot-logs
|
||||||
|
# TM bot reports log entries for bootcamp / rural_trade loops.
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@api.route('/api/bot-logs', methods=['POST'])
|
||||||
|
def api_bot_logs():
|
||||||
|
clan = _get_clan_from_request()
|
||||||
|
if not clan:
|
||||||
|
return make_response('Unauthorized', 403)
|
||||||
|
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
player_id = str(data.get('player_id', ''))
|
||||||
|
feature = data.get('feature', '')
|
||||||
|
message = data.get('message', '')
|
||||||
|
|
||||||
|
if not player_id or not feature or not message:
|
||||||
|
return jsonify({'error': 'missing fields'}), 400
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
conn.execute(
|
||||||
|
'INSERT INTO bot_logs (player_id, feature, message) VALUES (?, ?, ?)',
|
||||||
|
(player_id, feature, message)
|
||||||
|
)
|
||||||
|
# Keep only latest 50 per player/feature
|
||||||
|
conn.execute('''
|
||||||
|
DELETE FROM bot_logs
|
||||||
|
WHERE player_id = ? AND feature = ?
|
||||||
|
AND id NOT IN (
|
||||||
|
SELECT id FROM bot_logs
|
||||||
|
WHERE player_id = ? AND feature = ?
|
||||||
|
ORDER BY id DESC LIMIT 50
|
||||||
|
)
|
||||||
|
''', (player_id, feature, player_id, feature))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'ok': True})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# GET /api/bot
|
# GET /api/bot
|
||||||
# Serves the modular bot code concatenated into a single response
|
# Serves the modular bot code concatenated into a single response
|
||||||
|
|||||||
@@ -126,6 +126,12 @@ def get_farm_data():
|
|||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
'SELECT town_id, town_name, data FROM town_state WHERE player_id = ?', (player_id,)
|
'SELECT town_id, town_name, data FROM town_state WHERE player_id = ?', (player_id,)
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
|
||||||
|
# Also fetch when the bot last farmed
|
||||||
|
lf_row = conn.execute(
|
||||||
|
"SELECT value FROM kv_store WHERE key = ?", (f'last_farmed_{player_id}',)
|
||||||
|
).fetchone()
|
||||||
|
last_farmed_at = lf_row['value'] if lf_row else None
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
now_ts = int(datetime.utcnow().timestamp())
|
now_ts = int(datetime.utcnow().timestamp())
|
||||||
@@ -142,7 +148,7 @@ def get_farm_data():
|
|||||||
'ready_farms': len(ready),
|
'ready_farms': len(ready),
|
||||||
'next_ready_at': min((f['lootable_at'] for f in farm_data if f.get('lootable_at', 0) > now_ts and f.get('relation_status', 0) == 1), default=None)
|
'next_ready_at': min((f['lootable_at'] for f in farm_data if f.get('lootable_at', 0) > now_ts and f.get('relation_status', 0) == 1), default=None)
|
||||||
})
|
})
|
||||||
return jsonify(farms_summary)
|
return jsonify({'towns': farms_summary, 'last_farmed_at': last_farmed_at})
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -263,6 +269,54 @@ def captcha_status():
|
|||||||
return jsonify({'captcha_active': active})
|
return jsonify({'captcha_active': active})
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET /dashboard/commands/queue
|
||||||
|
# Returns pending+executing BUILD commands for a specific town,
|
||||||
|
# ordered by their manual position (for the per-town build queue UI).
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@dashboard.route('/dashboard/commands/queue', methods=['GET'])
|
||||||
|
def get_town_build_queue():
|
||||||
|
player_id = request.args.get('player_id')
|
||||||
|
town_id = request.args.get('town_id')
|
||||||
|
conn = get_db()
|
||||||
|
rows = conn.execute('''
|
||||||
|
SELECT id, town_id, town_name, type, payload, status, result_msg, position, created_at, updated_at
|
||||||
|
FROM commands
|
||||||
|
WHERE player_id = ? AND town_id = ? AND type = 'build'
|
||||||
|
AND status IN ('pending', 'executing')
|
||||||
|
ORDER BY position ASC, id ASC
|
||||||
|
''', (player_id, town_id)).fetchall()
|
||||||
|
conn.close()
|
||||||
|
return jsonify([dict(r) for r in rows])
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# POST /dashboard/commands/reorder
|
||||||
|
# Accepts { player_id, town_id, order: [id1, id2, ...] }
|
||||||
|
# and updates position for each command in the list.
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@dashboard.route('/dashboard/commands/reorder', methods=['POST'])
|
||||||
|
def reorder_commands():
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
player_id = data.get('player_id')
|
||||||
|
town_id = data.get('town_id')
|
||||||
|
order = data.get('order', []) # list of command ids in desired order
|
||||||
|
|
||||||
|
if not player_id or not town_id or not order:
|
||||||
|
return jsonify({'error': 'missing player_id, town_id or order'}), 400
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
for idx, cmd_id in enumerate(order):
|
||||||
|
conn.execute('''
|
||||||
|
UPDATE commands
|
||||||
|
SET position = ?
|
||||||
|
WHERE id = ? AND player_id = ? AND town_id = ?
|
||||||
|
''', (idx + 1, cmd_id, player_id, str(town_id)))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'ok': True})
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# GET /dashboard/commands
|
# GET /dashboard/commands
|
||||||
# Returns command history (last 50) for the log panel.
|
# Returns command history (last 50) for the log panel.
|
||||||
@@ -320,14 +374,28 @@ def create_command():
|
|||||||
return jsonify({'error': 'client_offline', 'message': 'Το script είναι offline — δεν μπορείτε να στείλετε εντολές.'}), 503
|
return jsonify({'error': 'client_offline', 'message': 'Το script είναι offline — δεν μπορείτε να στείλετε εντολές.'}), 503
|
||||||
|
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
|
|
||||||
|
# Assign position = one more than the current max for this town's pending build queue
|
||||||
|
if cmd_type == 'build':
|
||||||
|
pos_row = c.execute(
|
||||||
|
"SELECT MAX(position) as max_pos FROM commands"
|
||||||
|
" WHERE player_id = ? AND town_id = ? AND type = 'build'"
|
||||||
|
" AND status IN ('pending', 'executing')",
|
||||||
|
(str(data['player_id']), str(data['town_id']))
|
||||||
|
).fetchone()
|
||||||
|
position = (pos_row['max_pos'] or 0) + 1
|
||||||
|
else:
|
||||||
|
position = None
|
||||||
|
|
||||||
c.execute('''
|
c.execute('''
|
||||||
INSERT INTO commands (town_id, town_name, type, payload, status, created_at, updated_at, player_id)
|
INSERT INTO commands (town_id, town_name, type, payload, status, position, created_at, updated_at, player_id)
|
||||||
VALUES (?, ?, ?, ?, 'pending', ?, ?, ?)
|
VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?)
|
||||||
''', (
|
''', (
|
||||||
str(data['town_id']),
|
str(data['town_id']),
|
||||||
data.get('town_name', ''),
|
data.get('town_name', ''),
|
||||||
cmd_type,
|
cmd_type,
|
||||||
json.dumps(data['payload']),
|
json.dumps(data['payload']),
|
||||||
|
position,
|
||||||
datetime.utcnow().isoformat(),
|
datetime.utcnow().isoformat(),
|
||||||
datetime.utcnow().isoformat(),
|
datetime.utcnow().isoformat(),
|
||||||
str(data['player_id'])
|
str(data['player_id'])
|
||||||
@@ -373,3 +441,125 @@ def fail_stale_commands():
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({'ok': True, 'failed': affected})
|
return jsonify({'ok': True, 'failed': affected})
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET /dashboard/bot-settings — fetch bootcamp + rural trade config
|
||||||
|
# POST /dashboard/bot-settings — save config
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@dashboard.route('/dashboard/bot-settings', methods=['GET', 'POST'])
|
||||||
|
def bot_settings():
|
||||||
|
player_id = request.args.get('player_id') or (request.json or {}).get('player_id')
|
||||||
|
if not player_id:
|
||||||
|
return jsonify({'error': 'missing player_id'}), 400
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
c = conn.cursor()
|
||||||
|
|
||||||
|
if request.method == 'GET':
|
||||||
|
row = c.execute(
|
||||||
|
'SELECT * FROM bot_settings WHERE player_id = ?', (player_id,)
|
||||||
|
).fetchone()
|
||||||
|
conn.close()
|
||||||
|
if row:
|
||||||
|
return jsonify(dict(row))
|
||||||
|
return jsonify({
|
||||||
|
'player_id': player_id,
|
||||||
|
'bootcamp_enabled': 0,
|
||||||
|
'bootcamp_use_def': 0,
|
||||||
|
'rural_trade_enabled': 0,
|
||||||
|
'rural_trade_ratio': 3,
|
||||||
|
})
|
||||||
|
|
||||||
|
# POST — upsert
|
||||||
|
data = request.json or {}
|
||||||
|
c.execute('''
|
||||||
|
INSERT INTO bot_settings (player_id, bootcamp_enabled, bootcamp_use_def,
|
||||||
|
rural_trade_enabled, rural_trade_ratio, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(player_id) DO UPDATE SET
|
||||||
|
bootcamp_enabled = excluded.bootcamp_enabled,
|
||||||
|
bootcamp_use_def = excluded.bootcamp_use_def,
|
||||||
|
rural_trade_enabled = excluded.rural_trade_enabled,
|
||||||
|
rural_trade_ratio = excluded.rural_trade_ratio,
|
||||||
|
updated_at = excluded.updated_at
|
||||||
|
''', (
|
||||||
|
player_id,
|
||||||
|
int(bool(data.get('bootcamp_enabled', 0))),
|
||||||
|
int(bool(data.get('bootcamp_use_def', 0))),
|
||||||
|
int(bool(data.get('rural_trade_enabled', 0))),
|
||||||
|
int(data.get('rural_trade_ratio', 3)),
|
||||||
|
datetime.utcnow().isoformat()
|
||||||
|
))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'ok': True})
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET /dashboard/bot-logs?player_id=&feature= — last 50 log lines
|
||||||
|
# POST /dashboard/bot-logs — append + prune
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@dashboard.route('/dashboard/bot-logs', methods=['GET', 'POST'])
|
||||||
|
def bot_logs():
|
||||||
|
player_id = request.args.get('player_id') or (request.json or {}).get('player_id')
|
||||||
|
if not player_id:
|
||||||
|
return jsonify({'error': 'missing player_id'}), 400
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
c = conn.cursor()
|
||||||
|
|
||||||
|
if request.method == 'GET':
|
||||||
|
feature = request.args.get('feature', '')
|
||||||
|
query = 'SELECT * FROM bot_logs WHERE player_id = ?'
|
||||||
|
params = [player_id]
|
||||||
|
if feature:
|
||||||
|
query += ' AND feature = ?'
|
||||||
|
params.append(feature)
|
||||||
|
query += ' ORDER BY id DESC LIMIT 50'
|
||||||
|
rows = c.execute(query, params).fetchall()
|
||||||
|
conn.close()
|
||||||
|
return jsonify([dict(r) for r in rows])
|
||||||
|
|
||||||
|
# POST — append entry and prune to last 50
|
||||||
|
data = request.json or {}
|
||||||
|
feature = data.get('feature', 'bootcamp')
|
||||||
|
message = data.get('message', '')
|
||||||
|
c.execute(
|
||||||
|
'INSERT INTO bot_logs (player_id, feature, message) VALUES (?, ?, ?)',
|
||||||
|
(player_id, feature, message)
|
||||||
|
)
|
||||||
|
# Prune: keep only the latest 50 per player/feature
|
||||||
|
c.execute('''
|
||||||
|
DELETE FROM bot_logs
|
||||||
|
WHERE player_id = ? AND feature = ?
|
||||||
|
AND id NOT IN (
|
||||||
|
SELECT id FROM bot_logs
|
||||||
|
WHERE player_id = ? AND feature = ?
|
||||||
|
ORDER BY id DESC LIMIT 50
|
||||||
|
)
|
||||||
|
''', (player_id, feature, player_id, feature))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'ok': True})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# POST /dashboard/bootcamp-attack-now
|
||||||
|
# Sets a one-shot flag consumed by the TM bot on the next poll.
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@dashboard.route('/dashboard/bootcamp-attack-now', methods=['POST'])
|
||||||
|
def bootcamp_attack_now():
|
||||||
|
player_id = (request.json or {}).get('player_id')
|
||||||
|
if not player_id:
|
||||||
|
return jsonify({'error': 'missing player_id'}), 400
|
||||||
|
key = f'bootcamp_attack_now_{player_id}'
|
||||||
|
conn = get_db()
|
||||||
|
conn.execute('''
|
||||||
|
INSERT INTO kv_store (key, value, updated_at) VALUES (?, '1', ?)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET value='1', updated_at=excluded.updated_at
|
||||||
|
''', (key, datetime.utcnow().isoformat()))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'ok': True})
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ window.fetchTowns = async function() {
|
|||||||
window.renderBuildingDropdown();
|
window.renderBuildingDropdown();
|
||||||
window.renderUnitDropdown();
|
window.renderUnitDropdown();
|
||||||
window.renderTownDetails();
|
window.renderTownDetails();
|
||||||
|
// Refresh the build queue panel if in queue mode
|
||||||
|
if (window._logPanelMode === 'queue') {
|
||||||
|
window.fetchBuildQueue(window.selectedTownId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
window.updateServerStatus(false);
|
window.updateServerStatus(false);
|
||||||
@@ -51,7 +55,9 @@ window.fetchLog = async function() {
|
|||||||
const res = await fetch('/dashboard/commands?player_id=' + window.PLAYER_ID);
|
const res = await fetch('/dashboard/commands?player_id=' + window.PLAYER_ID);
|
||||||
const cmds = await res.json();
|
const cmds = await res.json();
|
||||||
window.cmds = cmds; // Save globally so viewer can see reserved resources
|
window.cmds = cmds; // Save globally so viewer can see reserved resources
|
||||||
window.renderLog(cmds);
|
if (window._logPanelMode === 'log') {
|
||||||
|
window.renderLog(cmds);
|
||||||
|
}
|
||||||
if (window.selectedTownId) window.renderTownDetails();
|
if (window.selectedTownId) window.renderTownDetails();
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
};
|
};
|
||||||
@@ -192,7 +198,12 @@ window.sendCommand = async function() {
|
|||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.ok) {
|
if (data.ok) {
|
||||||
window.fetchLog();
|
// Refresh whichever panel is active
|
||||||
|
if (type === 'build' && window._logPanelMode === 'queue') {
|
||||||
|
window.fetchBuildQueue(town.town_id);
|
||||||
|
} else {
|
||||||
|
window.fetchLog();
|
||||||
|
}
|
||||||
} else if (data.error === 'client_offline') {
|
} else if (data.error === 'client_offline') {
|
||||||
alert(data.message || 'Το script είναι offline.');
|
alert(data.message || 'Το script είναι offline.');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -4,11 +4,15 @@
|
|||||||
|
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
window.fetchTowns();
|
window.fetchTowns();
|
||||||
window.fetchLog();
|
window.fetchLog(); // pre-loads cmds globally even in queue mode
|
||||||
window.fetchClientStatus();
|
window.fetchClientStatus();
|
||||||
window.fetchCaptchaStatus();
|
window.fetchCaptchaStatus();
|
||||||
setInterval(window.fetchTowns, window.POLL_INTERVAL);
|
setInterval(window.fetchTowns, window.POLL_INTERVAL);
|
||||||
setInterval(window.fetchLog, window.POLL_INTERVAL);
|
// In log mode: fetchLog refreshes the panel. In queue mode: refreshLogPanel polls the queue.
|
||||||
|
setInterval(() => {
|
||||||
|
window.fetchLog(); // always keep cmds cache fresh for resource display
|
||||||
|
window.refreshLogPanel(); // refresh whichever panel is visible
|
||||||
|
}, window.POLL_INTERVAL);
|
||||||
setInterval(window.fetchClientStatus, window.POLL_INTERVAL);
|
setInterval(window.fetchClientStatus, window.POLL_INTERVAL);
|
||||||
setInterval(window.fetchCaptchaStatus, 5000); // check every 5s
|
setInterval(window.fetchCaptchaStatus, 5000); // check every 5s
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,171 @@
|
|||||||
// ================================================================
|
// ================================================================
|
||||||
// Command Log Component
|
// Command Log & Build Queue Component
|
||||||
// ================================================================
|
// ================================================================
|
||||||
|
|
||||||
|
// -- Panel state: 'queue' | 'log' ----------------------------
|
||||||
|
window._logPanelMode = 'queue';
|
||||||
|
|
||||||
|
// ---- Toggle buttons -------------------------------------------
|
||||||
|
window.switchToQueueMode = function() {
|
||||||
|
window._logPanelMode = 'queue';
|
||||||
|
document.getElementById('tab-queue').classList.add('tab-active');
|
||||||
|
document.getElementById('tab-log').classList.remove('tab-active');
|
||||||
|
window.refreshLogPanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.switchToLogMode = function() {
|
||||||
|
window._logPanelMode = 'log';
|
||||||
|
document.getElementById('tab-log').classList.add('tab-active');
|
||||||
|
document.getElementById('tab-queue').classList.remove('tab-active');
|
||||||
|
window.fetchLog();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- Main dispatcher ------------------------------------------
|
||||||
|
window.refreshLogPanel = function() {
|
||||||
|
if (window._logPanelMode === 'queue') {
|
||||||
|
const town = window.getSelectedTown();
|
||||||
|
if (town) {
|
||||||
|
window.fetchBuildQueue(town.town_id);
|
||||||
|
} else {
|
||||||
|
document.getElementById('log-content').innerHTML =
|
||||||
|
'<p style="color:#555;font-size:0.85rem;padding:12px 0;">← Επιλέξτε πόλη για να δείτε την ουρά.</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// BUILD QUEUE (per-town, draggable)
|
||||||
|
// ================================================================
|
||||||
|
window.fetchBuildQueue = async function(townId) {
|
||||||
|
if (window._logPanelMode !== 'queue') return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/dashboard/commands/queue?player_id=${window.PLAYER_ID}&town_id=${encodeURIComponent(townId)}`);
|
||||||
|
const cmds = await res.json();
|
||||||
|
window.renderBuildQueue(cmds, townId);
|
||||||
|
} catch(e) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Drag state
|
||||||
|
let _dragSrcIdx = null;
|
||||||
|
|
||||||
|
window.renderBuildQueue = function(cmds, townId) {
|
||||||
|
const el = document.getElementById('log-content');
|
||||||
|
|
||||||
|
if (!cmds || cmds.length === 0) {
|
||||||
|
el.innerHTML = `
|
||||||
|
<div style="text-align:center;padding:2rem 1rem;color:#444;">
|
||||||
|
<div style="font-size:2rem;margin-bottom:0.5rem;">🏗️</div>
|
||||||
|
<p style="font-size:0.85rem;">Η ουρά κατασκευών είναι κενή.</p>
|
||||||
|
<p style="font-size:0.75rem;color:#333;margin-top:0.3rem;">Χρησιμοποιήστε την φόρμα για να προσθέσετε κατασκευές.</p>
|
||||||
|
</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = cmds.map((cmd, idx) => {
|
||||||
|
const p = typeof cmd.payload === 'string' ? JSON.parse(cmd.payload) : cmd.payload;
|
||||||
|
const nameGr = window.BUILDING_NAMES_GR?.[p.building_id] || p.building_id || '?';
|
||||||
|
const icon = window.BUILDING_ICONS?.[p.building_id] || '🏗️';
|
||||||
|
const isExec = cmd.status === 'executing';
|
||||||
|
|
||||||
|
const statusDot = isExec
|
||||||
|
? `<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#4acc64;box-shadow:0 0 5px #4acc64;flex-shrink:0;" title="Εκτελείται"></span>`
|
||||||
|
: `<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#555;flex-shrink:0;" title="Σε αναμονή"></span>`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="bq-row" draggable="true"
|
||||||
|
data-idx="${idx}" data-id="${cmd.id}" data-town="${townId}"
|
||||||
|
ondragstart="window._bqDragStart(event,${idx})"
|
||||||
|
ondragover="window._bqDragOver(event)"
|
||||||
|
ondrop="window._bqDrop(event,${idx},'${townId}')"
|
||||||
|
ondragend="window._bqDragEnd(event)">
|
||||||
|
<span class="bq-handle" title="Σύρε για αναδιάταξη">⠿</span>
|
||||||
|
<span class="bq-pos">${idx + 1}</span>
|
||||||
|
${statusDot}
|
||||||
|
<span class="bq-icon">${icon}</span>
|
||||||
|
<span class="bq-name">${nameGr}</span>
|
||||||
|
<button class="bq-cancel-btn" onclick="window._bqCancel(${cmd.id})" title="Ακύρωση">✕</button>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
el.innerHTML = `<div id="bq-list">${rows}</div>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- Drag-and-drop handlers -----------------------------------
|
||||||
|
window._bqDragStart = function(e, idx) {
|
||||||
|
_dragSrcIdx = idx;
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
setTimeout(() => {
|
||||||
|
const rows = document.querySelectorAll('.bq-row');
|
||||||
|
if (rows[idx]) rows[idx].style.opacity = '0.4';
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
window._bqDragOver = function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
// Highlight target row
|
||||||
|
document.querySelectorAll('.bq-row').forEach(r => r.classList.remove('bq-drag-over'));
|
||||||
|
const row = e.currentTarget;
|
||||||
|
if (row) row.classList.add('bq-drag-over');
|
||||||
|
};
|
||||||
|
|
||||||
|
window._bqDrop = function(e, targetIdx, townId) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (_dragSrcIdx === null || _dragSrcIdx === targetIdx) return;
|
||||||
|
|
||||||
|
// Re-order the DOM
|
||||||
|
const list = document.getElementById('bq-list');
|
||||||
|
const rows = Array.from(list.querySelectorAll('.bq-row'));
|
||||||
|
const movedRow = rows.splice(_dragSrcIdx, 1)[0];
|
||||||
|
rows.splice(targetIdx, 0, movedRow);
|
||||||
|
|
||||||
|
// Update numbering & opacity
|
||||||
|
rows.forEach((r, i) => {
|
||||||
|
r.style.opacity = '1';
|
||||||
|
r.classList.remove('bq-drag-over');
|
||||||
|
r.dataset.idx = i;
|
||||||
|
r.querySelector('.bq-pos').textContent = i + 1;
|
||||||
|
r.setAttribute('ondragstart', `window._bqDragStart(event,${i})`);
|
||||||
|
r.setAttribute('ondrop', `window._bqDrop(event,${i},'${townId}')`);
|
||||||
|
});
|
||||||
|
list.innerHTML = '';
|
||||||
|
rows.forEach(r => list.appendChild(r));
|
||||||
|
|
||||||
|
// Persist new order to server
|
||||||
|
const orderedIds = rows.map(r => parseInt(r.dataset.id));
|
||||||
|
fetch('/dashboard/commands/reorder', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ player_id: window.PLAYER_ID, town_id: townId, order: orderedIds })
|
||||||
|
});
|
||||||
|
|
||||||
|
_dragSrcIdx = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
window._bqDragEnd = function(e) {
|
||||||
|
document.querySelectorAll('.bq-row').forEach(r => {
|
||||||
|
r.style.opacity = '1';
|
||||||
|
r.classList.remove('bq-drag-over');
|
||||||
|
});
|
||||||
|
_dragSrcIdx = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
window._bqCancel = async function(id) {
|
||||||
|
await fetch(`/dashboard/commands/${id}`, { method: 'DELETE' });
|
||||||
|
// Refresh the queue for the currently selected town
|
||||||
|
const town = window.getSelectedTown();
|
||||||
|
if (town) window.fetchBuildQueue(town.town_id);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// COMMAND LOG (full history, existing behaviour)
|
||||||
|
// ================================================================
|
||||||
window.renderLog = function(cmds) {
|
window.renderLog = function(cmds) {
|
||||||
|
if (window._logPanelMode !== 'log') return;
|
||||||
const el = document.getElementById('log-content');
|
const el = document.getElementById('log-content');
|
||||||
if (!cmds.length) {
|
if (!cmds.length) {
|
||||||
el.innerHTML = '<p id="empty-log">No commands sent yet.</p>';
|
el.innerHTML = '<p id="empty-log" style="color:#555;font-size:0.85rem;padding:12px 0;">No commands sent yet.</p>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,12 +180,13 @@ window.renderLog = function(cmds) {
|
|||||||
desc = `Recruit: ${p.amount}x ${nameGr}`;
|
desc = `Recruit: ${p.amount}x ${nameGr}`;
|
||||||
} else if (cmd.type === 'market_offer') {
|
} else if (cmd.type === 'market_offer') {
|
||||||
desc = `Market: ${p.offer} ${p.offer_type} ➞ ${p.demand} ${p.demand_type}`;
|
desc = `Market: ${p.offer} ${p.offer_type} ➞ ${p.demand} ${p.demand_type}`;
|
||||||
|
} else {
|
||||||
|
desc = cmd.type;
|
||||||
}
|
}
|
||||||
const statusClass = `status-${cmd.status}`;
|
const statusClass = `status-${cmd.status}`;
|
||||||
const cancelBtn = `<button class="btn btn-danger btn-sm" onclick="cancelCommand(${cmd.id})">✕</button>`;
|
const cancelBtn = `<button class="btn btn-danger btn-sm" onclick="cancelCommand(${cmd.id})">✕</button>`;
|
||||||
|
|
||||||
const timeStr = new Date(cmd.created_at + 'Z').toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
|
const timeStr = new Date(cmd.created_at + 'Z').toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
|
||||||
|
|
||||||
return `<tr>
|
return `<tr>
|
||||||
<td style="color:#888;font-size:0.75rem">#${cmd.id}<br><span style="font-size:0.65rem;color:#555;">${timeStr}</span></td>
|
<td style="color:#888;font-size:0.75rem">#${cmd.id}<br><span style="font-size:0.65rem;color:#555;">${timeStr}</span></td>
|
||||||
<td>${cmd.town_name || cmd.town_id}</td>
|
<td>${cmd.town_name || cmd.town_id}</td>
|
||||||
|
|||||||
@@ -99,6 +99,10 @@ window.selectTown = function(id) {
|
|||||||
window.renderBuildingDropdown();
|
window.renderBuildingDropdown();
|
||||||
window.renderUnitDropdown();
|
window.renderUnitDropdown();
|
||||||
window.renderTownDetails();
|
window.renderTownDetails();
|
||||||
|
// Refresh build queue panel for the newly selected town
|
||||||
|
if (window._logPanelMode === 'queue') {
|
||||||
|
window.fetchBuildQueue(id);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.getSelectedTown = function() {
|
window.getSelectedTown = function() {
|
||||||
|
|||||||
@@ -219,11 +219,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bottom right: Command log -->
|
<!-- Bottom right: Build Queue / Command Log (tabbed) -->
|
||||||
<div id="log-panel">
|
<div id="log-panel">
|
||||||
<h2>Command Log</h2>
|
<div style="display:flex; align-items:center; gap:8px; margin-bottom:12px; border-bottom:1px solid #2a3a5a; padding-bottom:10px;">
|
||||||
|
<h2 style="margin:0; flex:1;">Ουρά Κατασκευών</h2>
|
||||||
|
<button id="tab-queue" class="log-tab-btn tab-active" onclick="window.switchToQueueMode()">🏗️ Ουρά</button>
|
||||||
|
<button id="tab-log" class="log-tab-btn" onclick="window.switchToLogMode()">📋 Ιστορικό</button>
|
||||||
|
</div>
|
||||||
<div id="log-content">
|
<div id="log-content">
|
||||||
<p id="empty-log">No commands sent yet.</p>
|
<p style="color:#555;font-size:0.85rem;padding:12px 0;">← Επιλέξτε πόλη για να δείτε την ουρά.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -254,14 +258,78 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Tab buttons for queue / log toggle */
|
||||||
|
.log-tab-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #2a3a5a;
|
||||||
|
color: #666;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
.log-tab-btn:hover { border-color: #4a7aaa; color: #aaa; }
|
||||||
|
.log-tab-btn.tab-active { border-color: #c8a44a; color: #c8a44a; background: rgba(200,164,74,0.1); }
|
||||||
|
|
||||||
|
/* Draggable build queue row */
|
||||||
|
.bq-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #1a2a3a;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
background: #0d1e30;
|
||||||
|
cursor: default;
|
||||||
|
transition: background 0.15s, border-color 0.15s;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.bq-row:hover { background: #112038; border-color: #2a4a6a; }
|
||||||
|
.bq-row.bq-drag-over { border-color: #c8a44a; background: rgba(200,164,74,0.08); }
|
||||||
|
.bq-handle {
|
||||||
|
cursor: grab;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #3a5a7a;
|
||||||
|
line-height: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
.bq-handle:hover { color: #c8a44a; }
|
||||||
|
.bq-pos {
|
||||||
|
width: 18px;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: #3a5a7a;
|
||||||
|
font-weight: 700;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.bq-icon { font-size: 1.1rem; flex-shrink: 0; }
|
||||||
|
.bq-name { flex: 1; font-size: 0.88rem; color: #d0d0d0; }
|
||||||
|
.bq-cancel-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #3a2a2a;
|
||||||
|
color: #884444;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.bq-cancel-btn:hover { background: rgba(200,80,80,0.15); border-color: #cc4444; color: #ff6666; }
|
||||||
|
</style>
|
||||||
<script>
|
<script>
|
||||||
window.PLAYER_ID = "{{ player_id }}";
|
window.PLAYER_ID = "{{ player_id }}";
|
||||||
</script>
|
</script>
|
||||||
<script src="/static/js/state.js"></script>
|
<script src="/static/js/state.js?v=6"></script>
|
||||||
<script src="/static/js/components/townViewer.js"></script>
|
<script src="/static/js/components/townViewer.js?v=6"></script>
|
||||||
<script src="/static/js/components/commandForm.js"></script>
|
<script src="/static/js/components/commandForm.js?v=6"></script>
|
||||||
<script src="/static/js/components/commandLog.js"></script>
|
<script src="/static/js/components/commandLog.js?v=6"></script>
|
||||||
<script src="/static/js/api.js"></script>
|
<script src="/static/js/api.js?v=6"></script>
|
||||||
<script src="/static/js/app.js"></script>
|
<script src="/static/js/app.js?v=6"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -243,7 +243,7 @@
|
|||||||
<div class="status-bar" id="status-bar"></div>
|
<div class="status-bar" id="status-bar"></div>
|
||||||
|
|
||||||
<!-- Warehouse-full notice (hidden by default) -->
|
<!-- Warehouse-full notice (hidden by default) -->
|
||||||
<div id="warehouse-full-banner" style="display:none; background: linear-gradient(90deg, #5a1a00, #8b2500); border: 1px solid #ff6600; border-radius: 8px; padding: 12px 18px; margin-bottom: 1rem; display: flex; align-items: center; gap: 12px; font-weight: 600;">
|
<div id="warehouse-full-banner" style="display:none; background: linear-gradient(90deg, #5a1a00, #8b2500); border: 1px solid #ff6600; border-radius: 8px; padding: 12px 18px; margin-bottom: 1rem; align-items: center; gap: 12px; font-weight: 600;">
|
||||||
<span style="font-size: 1.4rem;">📦</span>
|
<span style="font-size: 1.4rem;">📦</span>
|
||||||
<span>
|
<span>
|
||||||
<strong style="color:#ff9933;">Αποθήκη Γεμάτη!</strong>
|
<strong style="color:#ff9933;">Αποθήκη Γεμάτη!</strong>
|
||||||
@@ -313,6 +313,80 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Bandit Camp Panel -->
|
||||||
|
<div class="panel">
|
||||||
|
<h2>🏕️ Αυτόματο Bandit Camp</h2>
|
||||||
|
<p style="font-size:0.85rem;color:#888;margin-bottom:1rem;">
|
||||||
|
Το bot επιτίθεται αυτόματα στο στρατόπεδο ληστών και διεκδικεί αμοιβές.<br>
|
||||||
|
Ελέγχει κάθε <strong>12–22 λεπτά</strong> (τυχαίο) — ανθρώπινος ρυθμός.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="toggle-row" style="margin-bottom:1rem;">
|
||||||
|
<span class="toggle-label">Αυτόματη Επίθεση</span>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" id="bootcamp-enabled">
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
<span style="color:#888;font-size:0.85rem;" id="bootcamp-hint">Ανενεργό</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toggle-row" style="margin-bottom:1.5rem;">
|
||||||
|
<span class="toggle-label">Συμπ. Αμυντικές Μονάδες (Σπαθ/Τοξ)</span>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" id="bootcamp-use-def">
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="save-btn" onclick="saveBotSettings()">💾 Αποθήκευση</button>
|
||||||
|
<span class="save-status" id="bot-save-status">✓ Αποθηκεύτηκε</span>
|
||||||
|
|
||||||
|
<div style="margin-top: 1rem; border-top: 1px solid #1a3040; padding-top: 1rem;">
|
||||||
|
<button class="save-btn" id="bootcamp-attack-btn" onclick="attackBootcampNow()" style="background: linear-gradient(135deg, #7a2a2a, #cc4a4a); width: 100%;">⚔️ Επίθεση Τώρα</button>
|
||||||
|
<div style="text-align: center; margin-top: 5px;"><span class="save-status" id="bootcamp-attack-status">Εντολή εστάλη!</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="margin-top:1.5rem;font-size:0.9rem;color:#aaa;">📋 Ιστορικό</h3>
|
||||||
|
<div id="bootcamp-log" style="background:#0a1520;border:1px solid #1a3040;border-radius:8px;padding:10px;max-height:180px;overflow-y:auto;font-size:0.78rem;font-family:monospace;color:#8ab4d0;">
|
||||||
|
<span style="color:#444;">Αναμονή δεδομένων...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rural Trade Panel -->
|
||||||
|
<div class="panel">
|
||||||
|
<h2>🔄 Αυτόματο Trade Χωριών</h2>
|
||||||
|
<p style="font-size:0.85rem;color:#888;margin-bottom:1rem;">
|
||||||
|
Ενεργοποιείται <strong>μόνο όταν μια κατασκευή κολλάει λόγω πόρων</strong>.<br>
|
||||||
|
Ψάχνει χωριά στο νησί που προσφέρουν τον ελλείποντα πόρο και κάνει trade.<br>
|
||||||
|
Ελέγχει κάθε <strong>25–45 λεπτά</strong> (τυχαίο).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="toggle-row" style="margin-bottom:1rem;">
|
||||||
|
<span class="toggle-label">Αυτόματο Trade</span>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" id="rural-trade-enabled">
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
<span style="color:#888;font-size:0.85rem;" id="rural-trade-hint">Ανενεργό</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom:0.75rem;font-size:0.85rem;color:#888;">Ελάχιστο Ratio Trade (τιμή χωριού):</div>
|
||||||
|
<div class="option-grid" style="margin-bottom:1.5rem;">
|
||||||
|
<button class="option-btn" data-ratio="1" onclick="selectRatio(1)"><span class="opt-time">0.25</span><span class="opt-name">Ελάχ.</span></button>
|
||||||
|
<button class="option-btn" data-ratio="2" onclick="selectRatio(2)"><span class="opt-time">0.50</span><span class="opt-name">Χαμηλό</span></button>
|
||||||
|
<button class="option-btn selected" data-ratio="3" onclick="selectRatio(3)"><span class="opt-time">0.75</span><span class="opt-name">Κανον.</span></button>
|
||||||
|
<button class="option-btn" data-ratio="4" onclick="selectRatio(4)"><span class="opt-time">1.00</span><span class="opt-name">Υψηλό</span></button>
|
||||||
|
<button class="option-btn" data-ratio="5" onclick="selectRatio(5)"><span class="opt-time">1.25</span><span class="opt-name">Μέγιστο</span></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="save-btn" onclick="saveBotSettings()">💾 Αποθήκευση</button>
|
||||||
|
|
||||||
|
<h3 style="margin-top:1.5rem;font-size:0.9rem;color:#aaa;">📋 Ιστορικό</h3>
|
||||||
|
<div id="rural-trade-log" style="background:#0a1520;border:1px solid #1a3040;border-radius:8px;padding:10px;max-height:180px;overflow-y:auto;font-size:0.78rem;font-family:monospace;color:#8ab4d0;">
|
||||||
|
<span style="color:#444;">Αναμονή δεδομένων...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Farm Status Table -->
|
<!-- Farm Status Table -->
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>🏘️ Κατάσταση Χωριών</h2>
|
<h2>🏘️ Κατάσταση Χωριών</h2>
|
||||||
@@ -324,15 +398,17 @@
|
|||||||
<th>Έτοιμα</th>
|
<th>Έτοιμα</th>
|
||||||
<th>Σύνολο</th>
|
<th>Σύνολο</th>
|
||||||
<th>Επόμενο</th>
|
<th>Επόμενο</th>
|
||||||
|
<th>Τελευταία Λεηλασία</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="farm-table-body">
|
<tbody id="farm-table-body">
|
||||||
<tr><td colspan="4"><div class="empty-state">⏳ <p>Φόρτωση δεδομένων...</p></div></td></tr>
|
<tr><td colspan="5"><div class="empty-state">⏳ <p>Φόρτωση δεδομένων...</p></div></td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const PLAYER_ID = '{{ player_id }}';
|
const PLAYER_ID = '{{ player_id }}';
|
||||||
let selectedOption = 1;
|
let selectedOption = 1;
|
||||||
@@ -395,17 +471,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -- Load farm data table --
|
// -- Load farm data table --
|
||||||
|
function timeAgo(isoStr) {
|
||||||
|
if (!isoStr) return '—';
|
||||||
|
const diff = Math.floor((Date.now() - new Date(isoStr + (isoStr.endsWith('Z') ? '' : 'Z'))) / 1000);
|
||||||
|
if (diff < 60) return `${diff}δ πριν`;
|
||||||
|
if (diff < 3600) return `${Math.floor(diff / 60)}λ πριν`;
|
||||||
|
return `${Math.floor(diff / 3600)}ω πριν`;
|
||||||
|
}
|
||||||
|
|
||||||
function loadFarmData() {
|
function loadFarmData() {
|
||||||
fetch(`/dashboard/farm-data?player_id=${PLAYER_ID}`)
|
fetch(`/dashboard/farm-data?player_id=${PLAYER_ID}`)
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(resp => {
|
||||||
|
const data = resp.towns || [];
|
||||||
|
const lastFarmed = resp.last_farmed_at ? timeAgo(resp.last_farmed_at) : '—';
|
||||||
const tbody = document.getElementById('farm-table-body');
|
const tbody = document.getElementById('farm-table-body');
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
tbody.innerHTML = '<tr><td colspan="4"><div class="empty-state">🌱 <p>Δεν υπάρχουν δεδομένα χωριών ακόμη.<br>Βεβαιώσου ότι το script v3.3+ τρέχει στο παιχνίδι.</p></div></td></tr>';
|
tbody.innerHTML = '<tr><td colspan="5"><div class="empty-state">🌱 <p>Δεν υπάρχουν δεδομένα χωριών ακόμη.<br>Βεβαιώσου ότι το script v3.3+ τρέχει στο παιχνίδι.</p></div></td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
tbody.innerHTML = data.map(t => {
|
tbody.innerHTML = data.map((t, idx) => {
|
||||||
const readyBadge = t.ready_farms > 0
|
const readyBadge = t.ready_farms > 0
|
||||||
? `<span class="badge ready">✓ ${t.ready_farms} Έτοιμα</span>`
|
? `<span class="badge ready">✓ ${t.ready_farms} Έτοιμα</span>`
|
||||||
: `<span class="badge waiting">Αναμονή</span>`;
|
: `<span class="badge waiting">Αναμονή</span>`;
|
||||||
@@ -420,11 +506,16 @@
|
|||||||
nextStr = '<span class="countdown">Τώρα</span>';
|
nextStr = '<span class="countdown">Τώρα</span>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Show last farmed only in first row — same value for all rows
|
||||||
|
const lastFarmedCell = idx === 0
|
||||||
|
? `<td rowspan="${data.length}" style="color:#4acc64;font-size:0.82rem;vertical-align:middle;">${lastFarmed}</td>`
|
||||||
|
: '';
|
||||||
return `<tr>
|
return `<tr>
|
||||||
<td><strong>${t.town_name}</strong></td>
|
<td><strong>${t.town_name}</strong></td>
|
||||||
<td>${readyBadge}</td>
|
<td>${readyBadge}</td>
|
||||||
<td><span style="color:#888">${t.total_farms}</span></td>
|
<td><span style="color:#888">${t.total_farms}</span></td>
|
||||||
<td>${nextStr}</td>
|
<td>${nextStr}</td>
|
||||||
|
${lastFarmedCell}
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
});
|
});
|
||||||
@@ -514,7 +605,31 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Warehouse full notice --
|
function attackBootcampNow() {
|
||||||
|
const btn = document.getElementById('bootcamp-attack-btn');
|
||||||
|
const status = document.getElementById('bootcamp-attack-status');
|
||||||
|
const originalText = btn.innerText;
|
||||||
|
|
||||||
|
btn.innerText = '⏳ Αποστολή...';
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
fetch('/dashboard/bootcamp-attack-now', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ player_id: PLAYER_ID })
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(() => {
|
||||||
|
btn.innerText = '✓ Εστάλη!';
|
||||||
|
status.style.opacity = '1';
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.innerText = originalText;
|
||||||
|
btn.disabled = false;
|
||||||
|
status.style.opacity = '0';
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function checkWarehouseStatus() {
|
async function checkWarehouseStatus() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/farm_status?player_id=${PLAYER_ID}`);
|
const res = await fetch(`/api/farm_status?player_id=${PLAYER_ID}`);
|
||||||
@@ -524,14 +639,92 @@
|
|||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Bot Settings (Bootcamp + Rural Trade) ─────────────────────
|
||||||
|
let selectedRatio = 3;
|
||||||
|
|
||||||
|
function selectRatio(n) {
|
||||||
|
selectedRatio = n;
|
||||||
|
document.querySelectorAll('[data-ratio]').forEach(b => {
|
||||||
|
b.classList.toggle('selected', parseInt(b.dataset.ratio) === n);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadBotSettings() {
|
||||||
|
fetch(`/dashboard/bot-settings?player_id=${PLAYER_ID}`)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(cfg => {
|
||||||
|
document.getElementById('bootcamp-enabled').checked = !!cfg.bootcamp_enabled;
|
||||||
|
document.getElementById('bootcamp-hint').textContent = cfg.bootcamp_enabled ? '🟢 Ενεργό' : 'Ανενεργό';
|
||||||
|
document.getElementById('bootcamp-use-def').checked = !!cfg.bootcamp_use_def;
|
||||||
|
document.getElementById('rural-trade-enabled').checked = !!cfg.rural_trade_enabled;
|
||||||
|
document.getElementById('rural-trade-hint').textContent = cfg.rural_trade_enabled ? '🟢 Ενεργό' : 'Ανενεργό';
|
||||||
|
selectRatio(cfg.rural_trade_ratio || 3);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveBotSettings() {
|
||||||
|
fetch('/dashboard/bot-settings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
player_id: PLAYER_ID,
|
||||||
|
bootcamp_enabled: document.getElementById('bootcamp-enabled').checked,
|
||||||
|
bootcamp_use_def: document.getElementById('bootcamp-use-def').checked,
|
||||||
|
rural_trade_enabled: document.getElementById('rural-trade-enabled').checked,
|
||||||
|
rural_trade_ratio: selectedRatio
|
||||||
|
})
|
||||||
|
}).then(r => r.json()).then(() => {
|
||||||
|
// Update hints
|
||||||
|
document.getElementById('bootcamp-hint').textContent =
|
||||||
|
document.getElementById('bootcamp-enabled').checked ? '🟢 Ενεργό' : 'Ανενεργό';
|
||||||
|
document.getElementById('rural-trade-hint').textContent =
|
||||||
|
document.getElementById('rural-trade-enabled').checked ? '🟢 Ενεργό' : 'Ανενεργό';
|
||||||
|
const s = document.getElementById('bot-save-status');
|
||||||
|
s.classList.add('visible');
|
||||||
|
setTimeout(() => s.classList.remove('visible'), 2500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wire toggle hints live
|
||||||
|
document.getElementById('bootcamp-enabled').addEventListener('change', function() {
|
||||||
|
document.getElementById('bootcamp-hint').textContent = this.checked ? '🟢 Ενεργό' : 'Ανενεργό';
|
||||||
|
});
|
||||||
|
document.getElementById('rural-trade-enabled').addEventListener('change', function() {
|
||||||
|
document.getElementById('rural-trade-hint').textContent = this.checked ? '🟢 Ενεργό' : 'Ανενεργό';
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Bot Logs ───────────────────────────────────────────────────
|
||||||
|
function renderBotLog(containerId, entries) {
|
||||||
|
const el = document.getElementById(containerId);
|
||||||
|
if (!entries || entries.length === 0) {
|
||||||
|
el.innerHTML = '<span style="color:#444;">Δεν υπάρχουν εγγραφές ακόμη.</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.innerHTML = entries.map(e => {
|
||||||
|
const t = new Date(e.created_at + 'Z').toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
|
||||||
|
return `<div><span style="color:#3a6a8a;">[${t}]</span> ${e.message}</div>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadBotLogs() {
|
||||||
|
fetch(`/dashboard/bot-logs?player_id=${PLAYER_ID}&feature=bootcamp`)
|
||||||
|
.then(r => r.json()).then(data => renderBotLog('bootcamp-log', data));
|
||||||
|
fetch(`/dashboard/bot-logs?player_id=${PLAYER_ID}&feature=rural_trade`)
|
||||||
|
.then(r => r.json()).then(data => renderBotLog('rural-trade-log', data));
|
||||||
|
}
|
||||||
|
|
||||||
// -- Boot --
|
// -- Boot --
|
||||||
loadSettings();
|
loadSettings();
|
||||||
|
loadBotSettings();
|
||||||
loadFarmData();
|
loadFarmData();
|
||||||
|
loadBotLogs();
|
||||||
checkOnline();
|
checkOnline();
|
||||||
checkWarehouseStatus();
|
checkWarehouseStatus();
|
||||||
setInterval(loadFarmData, 15000);
|
setInterval(loadFarmData, 15000);
|
||||||
setInterval(checkOnline, 20000);
|
setInterval(loadBotLogs, 15000);
|
||||||
|
setInterval(checkOnline, 20000);
|
||||||
setInterval(checkWarehouseStatus, 20000);
|
setInterval(checkWarehouseStatus, 20000);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user