auto trade and auto bandit
This commit is contained in:
294
bot_modules/04c_execute_bootcamp_trade.js
Normal file
294
bot_modules/04c_execute_bootcamp_trade.js
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
// ================================================================
|
||||||
|
// 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,8 +28,9 @@ async function pollAndExecute() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache farm settings so autoFarmLoop can read them without an extra call
|
// Cache farm + bot settings so autonomous loops can read them without extra calls
|
||||||
lastKnownFarmSettings = cmdData.farm_settings || {};
|
lastKnownFarmSettings = cmdData.farm_settings || {};
|
||||||
|
lastKnownBotSettings = cmdData.bot_settings || {};
|
||||||
|
|
||||||
// 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'];
|
||||||
@@ -190,12 +191,14 @@ async function autoFarmLoop() {
|
|||||||
// 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.1.0 (remote) loaded');
|
log('Grepolis Remote Control v4.2.0 (remote) loaded');
|
||||||
detectCaptcha();
|
detectCaptcha();
|
||||||
setTimeout(pushState, 5000);
|
setTimeout(pushState, 5000);
|
||||||
jitterLoop(pushState, 60000, 120000); // state sync every 1-2 min
|
jitterLoop(pushState, 60000, 120000); // state sync every 1–2 min
|
||||||
jitterLoop(pollAndExecute, 8000, 18000); // command poll every 8-18 s
|
jitterLoop(pollAndExecute, 8000, 18000); // command poll every 8–18 s
|
||||||
jitterLoop(autoFarmLoop, 60000, 120000); // auto-farm every 1-2 min (independent)
|
jitterLoop(autoFarmLoop, 60000, 120000); // auto-farm every 1–2 min
|
||||||
|
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') {
|
||||||
|
|||||||
24
db.py
24
db.py
@@ -68,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',
|
||||||
|
|||||||
@@ -195,6 +195,17 @@ 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,
|
||||||
|
}
|
||||||
|
|
||||||
# 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),)
|
||||||
@@ -215,6 +226,7 @@ def get_pending_command():
|
|||||||
'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
|
||||||
})
|
})
|
||||||
@@ -367,6 +379,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
|
||||||
|
|||||||
@@ -441,3 +441,105 @@ 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})
|
||||||
|
|
||||||
|
|||||||
@@ -313,6 +313,75 @@
|
|||||||
</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>
|
||||||
|
|
||||||
|
<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>
|
||||||
@@ -334,6 +403,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const PLAYER_ID = '{{ player_id }}';
|
const PLAYER_ID = '{{ player_id }}';
|
||||||
let selectedOption = 1;
|
let selectedOption = 1;
|
||||||
@@ -539,14 +609,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