Compare commits

47 Commits

Author SHA1 Message Date
76b991a62b blue fix 2026-05-02 11:42:05 +03:00
f5231a2524 try to fix 2026-05-02 02:58:41 +03:00
84de7082ec blue fix 2026-05-02 02:45:42 +03:00
83b8c85557 timestamp 2026-05-02 02:36:48 +03:00
d8ba139d07 2 bugs / farm and bandit 2026-05-02 02:21:32 +03:00
6157ae1034 fix ? 2026-05-02 01:40:21 +03:00
4272edf432 farm fix between worlds 2026-05-02 01:28:20 +03:00
502b330ac5 ?? 2026-05-02 01:23:30 +03:00
1c65043eb3 final fix? 2026-05-02 01:20:25 +03:00
5c4d415fdd debug 2026-05-02 01:14:12 +03:00
f22b92ae89 debug 2026-05-02 01:11:42 +03:00
0f54ef9191 ficx 4 2026-05-02 01:07:01 +03:00
45e71ed90b debug 3 2026-05-02 01:03:44 +03:00
66dcb71a8d debug 2 2026-05-02 01:00:23 +03:00
b36b11393f debug 1 2026-05-02 00:57:23 +03:00
90ce6a029d fix 3 2026-05-02 00:53:47 +03:00
c9e6522f12 fix 2 2026-05-02 00:48:37 +03:00
1cb5dca3c2 fix 1 2026-05-02 00:36:38 +03:00
2552d3d075 fix different world different admin 2026-05-02 00:25:02 +03:00
5f6855ec69 blueprint function 2026-05-02 00:08:43 +03:00
05785c294e fix 2 2026-05-01 22:42:16 +03:00
614029e527 ui revamp 2026-05-01 22:32:52 +03:00
a572feef14 fix executing 2026-05-01 21:42:32 +03:00
b18e2e8f97 fix 2 2026-05-01 21:25:40 +03:00
2769091b74 fix 1 2026-05-01 21:16:20 +03:00
f4a0e18686 redesign of recruit troops 2026-05-01 21:11:47 +03:00
d6c2252f5c stash reward fix 2 2026-05-01 17:50:39 +03:00
bcda80e127 stashreward / use reward fix 2026-05-01 16:56:23 +03:00
731a7b2f3b ui tweak 2026-05-01 16:34:46 +03:00
f82893164e stucked commands 2026-05-01 16:02:49 +03:00
efa63f761f attack now button 2026-05-01 02:37:46 +03:00
ae37674bcc bandit fix 2026-05-01 02:33:35 +03:00
cf3c2e7b4f fix farm resourses 2026-05-01 02:30:21 +03:00
2921dff257 auto trade and auto bandit 2026-05-01 01:54:09 +03:00
2a73e46a7b fix 1 2026-05-01 01:24:12 +03:00
76ad37c1db admin order line 2026-05-01 01:13:18 +03:00
f250fbd5b6 test again 2026-04-29 23:26:11 +03:00
bb01b90889 test 2026-04-29 23:24:39 +03:00
0643422a30 enchance farming/fix 2026-04-29 23:22:25 +03:00
2517538b88 fix remove grepo.db 2026-04-29 22:43:31 +03:00
1db8d744c8 fix for buildinds 2026-04-29 22:05:57 +03:00
d952e7ca56 market revert back 2026-04-28 23:28:13 +03:00
edd7666905 fix 4 2026-04-28 23:17:36 +03:00
76ab83620b fix 3 2026-04-28 23:08:35 +03:00
53f1176ef8 fix 2 2026-04-28 22:50:54 +03:00
ef6946365c fix 1 2026-04-28 22:33:43 +03:00
0ef0bef036 fix market capacity 2026-04-28 22:17:36 +03:00
22 changed files with 2165 additions and 357 deletions

View File

@@ -822,19 +822,20 @@
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;
const world_id = uw.Game?.world_id;
if (!player_id) return; if (!player_id) return;
let cmdData; let cmdData;
try { try {
const res = await fetch(`${BASE_URL}/api/commands/pending?player_id=${player_id}`); const res = await fetch(`${BASE_URL}/api/commands/pending?player_id=${player_id}&world_id=${world_id}`);
cmdData = await res.json(); cmdData = await res.json();
} catch (e) { } catch (e) {
log(`Poll failed: ${e}`); log(`Poll failed: ${e}`);
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 +874,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);

View File

@@ -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.* . .

6
app.py
View File

@@ -5,6 +5,12 @@ from db import init_db, get_db
from routes.api import api from routes.api import api
from routes.dashboard import dashboard from routes.dashboard import dashboard
from routes.auth import auth from routes.auth import auth
import logging
logging.basicConfig(
level=logging.WARNING,
format='%(asctime)s [%(name)s] %(levelname)s: %(message)s'
)
# Initialise DB schema when the app starts # Initialise DB schema when the app starts
init_db() init_db()

221
blueprint_engine.py Normal file
View File

@@ -0,0 +1,221 @@
import json
import logging
from datetime import datetime, timedelta
log = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# STANDARD_BLUEPRINT — ordered list of phases.
# Each phase is a dict of { building_name: required_level }.
# The engine works through phases in order, queueing one building at a time.
# ---------------------------------------------------------------------------
STANDARD_BLUEPRINT = [
{"barracks": 1, "farm": 3, "lumber": 2, "stoner": 2, "ironer": 2, "storage": 2, "main": 2, "temple": 1},
{"barracks": 1, "farm": 3, "lumber": 3, "stoner": 3, "ironer": 3, "storage": 6, "main": 8},
{"farm": 8, "lumber": 8, "ironer": 8, "stoner": 8, "market": 5, "temple": 5, "barracks": 5},
{"academy": 13},
{"storage": 12, "farm": 12},
{"main": 25},
{"storage": 21, "farm": 15},
{"lumber": 15, "stoner": 10, "ironer": 12},
{"docks": 10},
{"academy": 30},
{"farm": 20, "storage": 25},
{"market": 15, "trade_office": 1, "hide": 10},
{"market": 30, "farm": 35, "thermal": 1, "academy": 36},
{"farm": 45, "storage": 35, "lumber": 40, "ironer": 40, "stoner": 40},
{"temple": 30}
]
RESEARCH_LIST = [
"booty", "pottery", "architecture", "building_crane",
"shipwright", "plow", "mathematics", "combat_experience",
"strong_wine", "take_over", "colonize_ship"
]
RESEARCH_LEVELS = {
"booty": 7,
"pottery": 7,
"architecture": 10,
"building_crane": 13,
"shipwright": 13,
"plow": 22,
"mathematics": 25,
"combat_experience": 34,
"strong_wine": 34,
"take_over": 28,
"colonize_ship": 13
}
MAX_LOOKAHEAD_PHASES = 2 # How many phases ahead to look if current phase is fully blocked
def evaluate_blueprints(conn):
blueprints = conn.execute('SELECT town_id, blueprint_name FROM town_blueprints WHERE is_active = 1').fetchall()
log.warning(f"[blueprint] Active blueprints: {len(blueprints)}")
if not blueprints:
return
for row in blueprints:
town_id = str(row['town_id'])
log.warning(f"[blueprint] Evaluating town_id={town_id}")
town_row = conn.execute(
'SELECT data, player_id, town_name, world_id FROM town_state WHERE town_id = ?', (town_id,)
).fetchone()
if not town_row:
log.warning(f"[blueprint] No town_state row for town_id={town_id} — skipping")
continue
player_id = town_row['player_id']
town_name_db = town_row['town_name']
town_world_id = town_row['world_id']
log.warning(f"[blueprint] Town: {town_name_db}, player_id={player_id}, world_id={town_world_id!r}")
try:
town = json.loads(town_row['data'])
except Exception as e:
log.warning(f"[blueprint] Failed to parse town JSON: {e}")
continue
build_queue = town.get('buildingOrder', [])
buildings = town.get('buildings', {})
build_data = town.get('buildData', {})
log.warning(f"[blueprint] buildingOrder length: {len(build_queue)}")
# ── Guard: don't queue if there's already a pending/executing command ──────
# Ghost detection: if a 'pending' command has sat untouched for >5 min,
# it's stale — delete it so we can re-evaluate fresh.
five_min_ago = (datetime.utcnow() - timedelta(minutes=5)).isoformat()
db_pending = conn.execute(
"SELECT id, type, status, updated_at FROM commands "
"WHERE town_id = ? AND type IN ('build', 'research') AND status IN ('pending', 'executing')",
(town_id,)
).fetchall()
if db_pending:
ghost_ids = [
r['id'] for r in db_pending
if r['status'] == 'pending' and (r['updated_at'] is None or r['updated_at'] < five_min_ago)
]
if ghost_ids:
log.warning(f"[blueprint] Deleting {len(ghost_ids)} ghost commands {ghost_ids}")
conn.execute(
f"DELETE FROM commands WHERE id IN ({','.join('?' for _ in ghost_ids)})",
ghost_ids
)
conn.commit()
db_pending = conn.execute(
"SELECT id, type, status, updated_at FROM commands "
"WHERE town_id = ? AND type IN ('build', 'research') AND status IN ('pending', 'executing')",
(town_id,)
).fetchall()
if db_pending:
details = [(r['id'], r['type'], r['status']) for r in db_pending]
log.warning(f"[blueprint] Already has {len(db_pending)} queued commands — skipping. {details}")
continue
# ── Calculate future levels: current buildings + anything in the game build queue ──
future_levels = {k: v for k, v in buildings.items()}
for q_item in build_queue:
b_type = q_item.get('building_type') or q_item.get('name')
if b_type:
future_levels[b_type] = future_levels.get(b_type, 0) + 1
log.warning(f"[blueprint] future_levels: {future_levels}")
# ── Simple phase search ────────────────────────────────────────────────────
# Strategy:
# 1. Find the first phase that has at least one building below its target.
# 2. Queue the first incomplete building from that phase — no resource checks,
# no dependency checks, exactly like adding it manually.
# 3. If every incomplete building in that phase is flagged has_max_level=True
# (meaning the game truly refuses to build it), look up to MAX_LOOKAHEAD_PHASES
# phases ahead for something to queue instead.
target_building = None
lookahead_used = 0
first_incomplete_phase_idx = None
for phase_idx, phase in enumerate(STANDARD_BLUEPRINT):
# Collect buildings that still need work in this phase
incomplete = [b for b, req in phase.items() if future_levels.get(b, 0) < req]
if not incomplete:
continue # Phase is fully complete, move on
# Track the very first incomplete phase we encounter
if first_incomplete_phase_idx is None:
first_incomplete_phase_idx = phase_idx
log.warning(f"[blueprint] Phase {phase_idx} incomplete: {incomplete}")
# Separate into: genuinely blocked (has_max=True) vs. queueable
blocked = [b for b in incomplete if build_data.get(b, {}).get('has_max_level', False)]
queueable = [b for b in incomplete if b not in blocked]
if queueable:
# We are in the current phase (or a valid lookahead phase) — queue it!
target_building = queueable[0]
log.warning(f"[blueprint] -> SELECTED '{target_building}' from phase {phase_idx}"
+ (f" (lookahead +{lookahead_used})" if lookahead_used else ""))
break
# Everything incomplete in this phase is has_max_level blocked.
# Allow limited lookahead.
if phase_idx == first_incomplete_phase_idx or lookahead_used < MAX_LOOKAHEAD_PHASES:
if phase_idx != first_incomplete_phase_idx:
lookahead_used += 1
log.warning(f"[blueprint] Phase {phase_idx}: all incomplete buildings are has_max — "
f"looking ahead (lookahead_used={lookahead_used})")
continue
# Ran out of lookahead budget — stop
log.warning(f"[blueprint] Lookahead exhausted after {lookahead_used} extra phases — giving up")
break
# ── Academy Research (fallback when no building target) ───────────────────
target_research = None
if not target_building:
academy_level = future_levels.get('academy', 0)
researched = town.get('researches', {})
for r_name in RESEARCH_LIST:
if not researched.get(r_name):
req_level = RESEARCH_LEVELS.get(r_name, 99)
if academy_level >= req_level:
target_research = r_name
log.warning(f"[blueprint] -> Research target: {r_name}")
break
log.warning(f"[blueprint] Final: target_building={target_building}, target_research={target_research}")
# ── Insert command ─────────────────────────────────────────────────────────
now = datetime.utcnow().isoformat()
if target_building:
pos_row = conn.execute(
"SELECT MAX(position) as max_pos FROM commands"
" WHERE player_id = ? AND town_id = ? AND type = 'build'"
" AND status IN ('pending', 'executing')",
(str(player_id), str(town_id))
).fetchone()
position = (pos_row['max_pos'] or 0) + 1
payload_str = json.dumps({"building_id": target_building})
conn.execute('''
INSERT INTO commands (town_id, town_name, type, payload, status, position, created_at, updated_at, player_id)
VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?)
''', (str(town_id), town_name_db, 'build', payload_str, position, now, now, str(player_id)))
log.warning(f"[blueprint] Inserted build command: {target_building} for {town_name_db}")
elif target_research:
payload_str = json.dumps({"research_id": target_research})
conn.execute('''
INSERT INTO commands (town_id, town_name, type, payload, status, created_at, updated_at, player_id)
VALUES (?, ?, ?, ?, 'pending', ?, ?, ?)
''', (str(town_id), town_name_db, 'research', payload_str, now, now, str(player_id)))
log.warning(f"[blueprint] Inserted research command: {target_research} for {town_name_db}")
else:
log.warning(f"[blueprint] Nothing to do for {town_name_db}")

View File

@@ -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` };

View File

@@ -0,0 +1,307 @@
// ================================================================
// 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 : 1222 min (camp cooldown is ~1215 min)
// RuralTrade: 2545 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, world_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, world_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;
const world_id = uw.Game?.world_id;
if (!player_id || !world_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;
const useReward = () => {
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
model_url: `PlayerAttackSpot/${player_id}`,
action_name: 'useReward',
arguments: {}
});
botLog(player_id, world_id, 'bootcamp', `Reward used: ${reward.power_id}`);
};
if (isInstant && !isFavor) {
useReward();
} else if (stashable) {
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
model_url: `PlayerAttackSpot/${player_id}`,
action_name: 'stashReward',
arguments: {}
}, 0, {
success: () => {
botLog(player_id, world_id, 'bootcamp', `Reward stashed: ${reward.power_id}`);
},
error: useReward
});
} else {
useReward();
}
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, world_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, world_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, world_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, world_id, 'bootcamp', `Στέλνω ${JSON.stringify(units)} στο camp...`);
} catch (e) {
await botLog(player_id, world_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;
const world_id = uw.Game?.world_id;
if (!player_id || !world_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, world_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, world_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, world_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');
}
}

View File

@@ -3,26 +3,73 @@
// 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(isFirstRun = false) {
let totalMs = 15000; // 15 seconds for the first run to catch already-ready farms
if (!isFirstRun) {
const option = lastKnownFarmSettings.loot_option || 1;
const baseMs = LOOT_TIMINGS[option] || 300000;
const jitterMs = randInt(30000, 120000); // +30 to +120 s
totalMs = baseMs + jitterMs;
}
log(`⏰ Next auto-farm in ${(totalMs / 60000).toFixed(1)} min`);
setTimeout(async () => {
await autoFarmLoop();
scheduleNextFarm(false);
}, totalMs);
}
// ----------------------------------------------------------------
// pollAndExecute — runs every 818 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;
const world_id = uw.Game?.world_id;
if (!player_id) return; if (!player_id) return;
let cmdData; let cmdData;
try { try {
const res = await apiFetch(`${BASE_URL}/api/commands/pending?player_id=${player_id}`); const res = await apiFetch(`${BASE_URL}/api/commands/pending?player_id=${player_id}&world_id=${world_id}`);
cmdData = await res.json(); cmdData = await res.json();
} catch (e) { } catch (e) {
log(`Poll failed: ${e}`); log(`Poll failed: ${e}`);
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 +95,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 +116,122 @@ 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 60120 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 {
const world_id = uw.Game?.world_id || '';
await apiFetch(`${BASE_URL}/api/farm_status?player_id=${player_id}&world_id=${world_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 {
const world_id = uw.Game?.world_id || '';
await apiFetch(`${BASE_URL}/api/farm_status?player_id=${player_id}&world_id=${world_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 12 min
jitterLoop(pollAndExecute, 8000, 18000); jitterLoop(pollAndExecute, 8000, 18000, 2000); // command poll every 818 s, but start in 2s
scheduleNextFarm(true); // auto-farm timer-based (loot_option + 30120s)
jitterLoop(autoBootcampLoop, 720000, 1320000); // bootcamp every 1222 min
jitterLoop(autoRuralTradeLoop, 1500000, 2700000); // rural trade every 2545 min
} }
if (document.readyState === 'complete') { if (document.readyState === 'complete') {

42
db.py
View File

@@ -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,40 @@ 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)')
# Blueprints - assigns a blueprint to a specific town
c.execute('''
CREATE TABLE IF NOT EXISTS town_blueprints (
town_id TEXT PRIMARY KEY,
blueprint_name TEXT NOT NULL,
is_active INTEGER NOT NULL DEFAULT 1,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
)
''')
# 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 +110,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 +120,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 (

BIN
grepo.db

Binary file not shown.

View File

@@ -1,9 +1,13 @@
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
from db import get_db from db import get_db
import json import json
from datetime import datetime from datetime import datetime, timedelta
import os import os
from flask import make_response from flask import make_response
from blueprint_engine import evaluate_blueprints
import logging
_log = logging.getLogger(__name__)
api = Blueprint('api', __name__) api = Blueprint('api', __name__)
@@ -94,6 +98,13 @@ def receive_state():
datetime.utcnow().isoformat() datetime.utcnow().isoformat()
)) ))
conn.commit() conn.commit()
try:
evaluate_blueprints(conn)
conn.commit()
except Exception as e:
print("Error evaluating blueprints:", e)
conn.close() conn.close()
return jsonify({'ok': True, 'towns_updated': len(towns)}) return jsonify({'ok': True, 'towns_updated': len(towns)})
@@ -104,13 +115,30 @@ def receive_state():
# Returns one 'build' AND one 'recruit' command independently, # Returns one 'build' AND one 'recruit' command independently,
# 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, world_id):
row = c.execute(''' """Fetch a single oldest pending command of a given type (recruit, market, etc.)."""
SELECT * FROM commands
WHERE status = 'pending' AND type = ? AND player_id = ? # We use LEFT JOIN because global commands (like farm_loot) use a pseudo town_id like "0_gr121"
ORDER BY id ASC # which does not exist in town_state.
LIMIT 1 global_town_id = f"0_{world_id}" if world_id else "0"
''', (cmd_type, player_id)).fetchone()
if world_id:
row = c.execute('''
SELECT c.* FROM commands c
LEFT JOIN town_state ts ON c.town_id = ts.town_id
WHERE c.status = 'pending' AND c.type = ? AND c.player_id = ?
AND (ts.world_id = ? OR c.town_id = ?)
ORDER BY c.updated_at ASC, c.id ASC
LIMIT 1
''', (cmd_type, player_id, world_id, global_town_id)).fetchone()
else:
row = c.execute('''
SELECT * FROM commands
WHERE status = 'pending' AND type = ? AND player_id = ?
ORDER BY updated_at ASC, id ASC
LIMIT 1
''', (cmd_type, player_id)).fetchone()
if not row: if not row:
return None return None
c.execute(''' c.execute('''
@@ -125,32 +153,142 @@ 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, world_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.
Towns that already have a command in 'executing' state are skipped —
this prevents a second build from being dispatched before the first one
has reported its result (which was causing commands to pile up in EXECUTING).
"""
# Towns that currently have a build already in-flight — don't touch those.
if world_id:
executing_rows = c.execute('''
SELECT DISTINCT c.town_id FROM commands c
JOIN town_state ts ON c.town_id = ts.town_id
WHERE c.status = 'executing' AND c.type = 'build' AND c.player_id = ? AND ts.world_id = ?
''', (player_id, world_id)).fetchall()
else:
executing_rows = c.execute('''
SELECT DISTINCT town_id FROM commands
WHERE status = 'executing' AND type = 'build' AND player_id = ?
''', (player_id,)).fetchall()
busy_towns = {r['town_id'] for r in executing_rows}
# Get every town that has at least one pending build, ordered by
# which town has been waiting longest (MIN updated_at across its commands).
if world_id:
town_rows = c.execute('''
SELECT c.town_id
FROM commands c
JOIN town_state ts ON c.town_id = ts.town_id
WHERE c.status = 'pending' AND c.type = 'build' AND c.player_id = ? AND ts.world_id = ?
GROUP BY c.town_id
ORDER BY MIN(c.updated_at) ASC
''', (player_id, world_id)).fetchall()
else:
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()
_log.warning(f"[poll] build towns found: {[r['town_id'] for r in town_rows]}, busy: {busy_towns}")
results = []
now = datetime.utcnow().isoformat()
for town_row in town_rows:
town_id = town_row['town_id']
# Skip this town if a build is already executing for it
if town_id in busy_towns:
continue
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')
world_id = request.args.get('world_id')
_log.warning(f"[poll] player_id={player_id!r} world_id={world_id!r}")
if not player_id: if not player_id:
return jsonify({'error': 'no player_id provided'}), 400 return jsonify({'error': 'no player_id provided'}), 400
conn = get_db() conn = get_db()
c = conn.cursor() c = conn.cursor()
build_cmd = _fetch_pending_of_type(c, 'build', player_id) # Free up stuck 'executing' commands (e.g. if the game page was refreshed mid-execution)
recruit_cmd = _fetch_pending_of_type(c, 'recruit', player_id) two_minutes_ago = (datetime.utcnow() - timedelta(minutes=2)).isoformat()
market_cmd = _fetch_pending_of_type(c, 'market_offer', player_id) c.execute('''
farm_cmd = _fetch_pending_of_type(c, 'farm_loot', player_id) UPDATE commands
farm_upgrade_cmd = _fetch_pending_of_type(c, 'farm_upgrade', player_id) SET status = 'pending', result_msg = 'Requeued (timeout)'
research_cmd = _fetch_pending_of_type(c, 'research', player_id) WHERE status = 'executing' AND updated_at < ? AND player_id = ?
''', (two_minutes_ago, player_id))
build_cmds = _fetch_pending_builds_all_towns(c, player_id, world_id) # one per town
recruit_cmd = _fetch_pending_of_type(c, 'recruit', player_id, world_id)
market_cmd = _fetch_pending_of_type(c, 'market_offer', player_id, world_id)
farm_cmd = _fetch_pending_of_type(c, 'farm_loot', player_id, world_id)
farm_upgrade_cmd = _fetch_pending_of_type(c, 'farm_upgrade', player_id, world_id)
research_cmd = _fetch_pending_of_type(c, 'research', player_id, world_id)
sync_req = _check_and_reset_sync(c, player_id) sync_req = _check_and_reset_sync(c, player_id)
# Determine player_key for world-specific settings if world_id is provided
player_key = f"{player_id}_{world_id}" if world_id else player_id
# Farm settings # Farm settings
farm_row = c.execute( farm_row = c.execute(
'SELECT enabled, loot_option FROM farm_settings WHERE player_id = ?', (player_id,) 'SELECT enabled, loot_option FROM farm_settings WHERE player_id = ?', (player_key,)
).fetchone() ).fetchone()
farm_settings = { farm_settings = {
'enabled': bool(farm_row['enabled']) if farm_row else False, 'enabled': bool(farm_row['enabled']) if farm_row else False,
'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 = ?', (player_key,)
).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_key}'
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 +302,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 +351,32 @@ 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']:
town_id = str(cmd['town_id'])
world_id = town_id.split('_')[1] if '_' in town_id else None
lf_key = f'last_farmed_{cmd["player_id"]}_{world_id}' if world_id else f'last_farmed_{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
''', (lf_key, now, now))
conn.commit() conn.commit()
conn.close() conn.close()
return jsonify({'ok': True}) return jsonify({'ok': True})
@@ -284,16 +440,26 @@ def upload_market_data():
@api.route('/api/farm_status', methods=['POST', 'GET']) @api.route('/api/farm_status', methods=['POST', 'GET'])
def farm_status(): def farm_status():
player_id = request.args.get('player_id') player_id = request.args.get('player_id')
world_id = request.args.get('world_id')
if not player_id: if not player_id:
return jsonify({'error': 'no player_id'}), 400 return jsonify({'error': 'no player_id'}), 400
kv_key = f'farm_status_{player_id}'
player_key = f"{player_id}_{world_id}" if world_id else player_id
kv_key = f'farm_status_{player_key}'
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_key}', now, now))
conn.commit() conn.commit()
conn.close() conn.close()
return jsonify({'ok': True}) return jsonify({'ok': True})
@@ -302,6 +468,49 @@ 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', ''))
world_id = str(data.get('world_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
player_key = f"{player_id}_{world_id}" if world_id else player_id
conn = get_db()
conn.execute(
'INSERT INTO bot_logs (player_id, feature, message) VALUES (?, ?, ?)',
(player_key, 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_key, feature, player_key, 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

View File

@@ -26,12 +26,12 @@ def index():
# Only fetch players that are members of this clan # Only fetch players that are members of this clan
rows = conn.execute(''' rows = conn.execute('''
SELECT ts.player, ts.player_id, MAX(ts.updated_at) as last_seen, MAX(ts.world_id) as world_id SELECT ts.player, ts.player_id, ts.world_id, MAX(ts.updated_at) as last_seen
FROM town_state ts FROM town_state ts
INNER JOIN clan_members cm ON cm.player_id = ts.player_id AND cm.clan_id = ? INNER JOIN clan_members cm ON cm.player_id = ts.player_id AND cm.clan_id = ?
WHERE ts.player IS NOT NULL WHERE ts.player IS NOT NULL
GROUP BY ts.player, ts.player_id GROUP BY ts.player, ts.player_id, ts.world_id
ORDER BY ts.player ASC ORDER BY ts.player ASC, ts.world_id ASC
''', (clan_id,)).fetchall() ''', (clan_id,)).fetchall()
captcha_rows = conn.execute("SELECT key, value FROM kv_store WHERE key LIKE 'captcha_active_%'").fetchall() captcha_rows = conn.execute("SELECT key, value FROM kv_store WHERE key LIKE 'captcha_active_%'").fetchall()
@@ -61,20 +61,20 @@ def index():
return render_template('index.html', players=players, no_clan=False) return render_template('index.html', players=players, no_clan=False)
@dashboard.route('/player/<player_id>') @dashboard.route('/player/<player_id>/<world_id>')
@login_required @login_required
def player_hub(player_id): def player_hub(player_id, world_id):
return render_template('hub.html', player_id=player_id) return render_template('hub.html', player_id=player_id, world_id=world_id)
@dashboard.route('/player/<player_id>/admin') @dashboard.route('/player/<player_id>/<world_id>/admin')
@login_required @login_required
def player_dashboard(player_id): def player_dashboard(player_id, world_id):
return render_template('dashboard.html', player_id=player_id) return render_template('dashboard.html', player_id=player_id, world_id=world_id)
@dashboard.route('/player/<player_id>/farm') @dashboard.route('/player/<player_id>/<world_id>/farm')
@login_required @login_required
def player_farm(player_id): def player_farm(player_id, world_id):
return render_template('farm.html', player_id=player_id) return render_template('farm.html', player_id=player_id, world_id=world_id)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -84,9 +84,12 @@ def player_farm(player_id):
@dashboard.route('/dashboard/farm-settings', methods=['GET']) @dashboard.route('/dashboard/farm-settings', methods=['GET'])
def get_farm_settings(): def get_farm_settings():
player_id = request.args.get('player_id') player_id = request.args.get('player_id')
world_id = request.args.get('world_id', '')
player_key = f"{player_id}_{world_id}" if world_id else player_id
conn = get_db() conn = get_db()
row = conn.execute( row = conn.execute(
'SELECT enabled, loot_option FROM farm_settings WHERE player_id = ?', (player_id,) 'SELECT enabled, loot_option FROM farm_settings WHERE player_id = ?', (player_key,)
).fetchone() ).fetchone()
conn.close() conn.close()
if row: if row:
@@ -99,6 +102,9 @@ def set_farm_settings():
if not data or 'player_id' not in data: if not data or 'player_id' not in data:
return jsonify({'error': 'missing player_id'}), 400 return jsonify({'error': 'missing player_id'}), 400
player_id = data['player_id'] player_id = data['player_id']
world_id = data.get('world_id', '')
player_key = f"{player_id}_{world_id}" if world_id else player_id
enabled = 1 if data.get('enabled') else 0 enabled = 1 if data.get('enabled') else 0
loot_option = int(data.get('loot_option', 1)) loot_option = int(data.get('loot_option', 1))
conn = get_db() conn = get_db()
@@ -109,7 +115,7 @@ def set_farm_settings():
enabled = excluded.enabled, enabled = excluded.enabled,
loot_option = excluded.loot_option, loot_option = excluded.loot_option,
updated_at = excluded.updated_at updated_at = excluded.updated_at
''', (player_id, enabled, loot_option, datetime.utcnow().isoformat())) ''', (player_key, enabled, loot_option, datetime.utcnow().isoformat()))
conn.commit() conn.commit()
conn.close() conn.close()
return jsonify({'ok': True}) return jsonify({'ok': True})
@@ -122,10 +128,24 @@ def set_farm_settings():
@dashboard.route('/dashboard/farm-data', methods=['GET']) @dashboard.route('/dashboard/farm-data', methods=['GET'])
def get_farm_data(): def get_farm_data():
player_id = request.args.get('player_id') player_id = request.args.get('player_id')
world_id = request.args.get('world_id')
conn = get_db() conn = get_db()
rows = conn.execute( if world_id:
'SELECT town_id, town_name, data FROM town_state WHERE player_id = ?', (player_id,) rows = conn.execute(
).fetchall() 'SELECT town_id, town_name, data FROM town_state WHERE player_id = ? AND world_id = ?',
(player_id, world_id)
).fetchall()
else:
rows = conn.execute(
'SELECT town_id, town_name, data FROM town_state WHERE player_id = ?', (player_id,)
).fetchall()
# Also fetch when the bot last farmed (per world)
lf_key = f'last_farmed_{player_id}_{world_id}' if world_id else f'last_farmed_{player_id}'
lf_row = conn.execute(
"SELECT value FROM kv_store WHERE key = ?", (lf_key,)
).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 +162,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})
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -170,14 +190,26 @@ def get_market_data():
@dashboard.route('/dashboard/towns', methods=['GET']) @dashboard.route('/dashboard/towns', methods=['GET'])
def get_towns(): def get_towns():
player_id = request.args.get('player_id') player_id = request.args.get('player_id')
world_id = request.args.get('world_id')
conn = get_db() conn = get_db()
rows = conn.execute('''
SELECT town_id, town_name, player, player_id, alliance_id, query = '''
world_id, x, y, sea, data, updated_at SELECT ts.town_id, ts.town_name, ts.player, ts.player_id, ts.alliance_id,
FROM town_state ts.world_id, ts.x, ts.y, ts.sea, ts.data, ts.updated_at,
WHERE player_id = ? tb.blueprint_name, tb.is_active as blueprint_active
ORDER BY town_name ASC FROM town_state ts
''', (player_id, )).fetchall() LEFT JOIN town_blueprints tb ON ts.town_id = tb.town_id AND tb.is_active = 1
WHERE ts.player_id = ?
'''
params = [player_id]
if world_id:
query += ' AND ts.world_id = ?'
params.append(world_id)
query += ' ORDER BY ts.town_name ASC'
rows = conn.execute(query, params).fetchall()
conn.close() conn.close()
towns = [] towns = []
@@ -214,11 +246,51 @@ def get_towns():
'bonuses': d.get('bonuses', {}), 'bonuses': d.get('bonuses', {}),
'wonder_points': d.get('wonder_points', 0), 'wonder_points': d.get('wonder_points', 0),
'total_points': d.get('total_points', 0), 'total_points': d.get('total_points', 0),
'alliance_name': d.get('alliance_name', None) 'alliance_name': d.get('alliance_name', None),
'blueprint_name': row['blueprint_name'],
'blueprint_active': bool(row['blueprint_active'])
}) })
return jsonify(towns) return jsonify(towns)
# ------------------------------------------------------------------
# POST /dashboard/blueprints
# Toggle a blueprint for a specific town
# ------------------------------------------------------------------
@dashboard.route('/dashboard/blueprints', methods=['POST'])
@login_required
def toggle_blueprint():
data = request.get_json(silent=True) or {}
town_id = data.get('town_id')
blueprint_name = data.get('blueprint_name', 'Standard Growth')
if not town_id:
return jsonify({'error': 'missing town_id'}), 400
conn = get_db()
# Check if currently active
row = conn.execute('SELECT is_active FROM town_blueprints WHERE town_id = ?', (town_id,)).fetchone()
new_state = 1
if row and row['is_active'] == 1:
new_state = 0 # Toggle off
conn.execute('''
INSERT INTO town_blueprints (town_id, blueprint_name, is_active, updated_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(town_id) DO UPDATE SET
blueprint_name = excluded.blueprint_name,
is_active = excluded.is_active,
updated_at = excluded.updated_at
''', (town_id, blueprint_name, new_state, datetime.utcnow().isoformat()))
conn.commit()
conn.close()
return jsonify({'ok': True, 'is_active': bool(new_state), 'blueprint_name': blueprint_name})
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# GET /dashboard/client-status # GET /dashboard/client-status
# Returns whether the Tampermonkey client is considered online. # Returns whether the Tampermonkey client is considered online.
@@ -263,6 +335,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.
@@ -271,13 +391,24 @@ def captcha_status():
def get_commands(): def get_commands():
player_id = request.args.get('player_id') player_id = request.args.get('player_id')
conn = get_db() conn = get_db()
rows = conn.execute(''' world_id = request.args.get('world_id')
SELECT id, town_id, town_name, type, payload, status, result_msg, created_at, updated_at conn = get_db()
FROM commands
WHERE player_id = ? query = '''
ORDER BY id DESC SELECT c.id, c.town_id, c.town_name, c.type, c.payload, c.status, c.result_msg, c.created_at, c.updated_at
LIMIT 50 FROM commands c
''', (player_id, )).fetchall() JOIN town_state ts ON c.town_id = ts.town_id
WHERE c.player_id = ?
'''
params = [player_id]
if world_id:
query += ' AND ts.world_id = ?'
params.append(world_id)
query += ' ORDER BY c.id DESC LIMIT 50'
rows = conn.execute(query, params).fetchall()
conn.close() conn.close()
return jsonify([dict(r) for r in rows]) return jsonify([dict(r) for r in rows])
@@ -320,14 +451,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 +518,134 @@ 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')
world_id = request.args.get('world_id') or (request.json or {}).get('world_id', '')
if not player_id:
return jsonify({'error': 'missing player_id'}), 400
player_key = f"{player_id}_{world_id}" if world_id else player_id
conn = get_db()
c = conn.cursor()
if request.method == 'GET':
row = c.execute(
'SELECT * FROM bot_settings WHERE player_id = ?', (player_key,)
).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_key,
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')
world_id = request.args.get('world_id') or (request.json or {}).get('world_id', '')
if not player_id:
return jsonify({'error': 'missing player_id'}), 400
player_key = f"{player_id}_{world_id}" if world_id else player_id
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_key]
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_key, 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_key, feature, player_key, 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')
world_id = (request.json or {}).get('world_id', '')
if not player_id:
return jsonify({'error': 'missing player_id'}), 400
player_key = f"{player_id}_{world_id}" if world_id else player_id
key = f'bootcamp_attack_now_{player_key}'
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})

View File

@@ -163,6 +163,10 @@ select:focus, input:focus { outline: none; border-color: #c8a44a; }
.btn-danger { background: #8b2222; color: #fff; } .btn-danger { background: #8b2222; color: #fff; }
.btn-sm { padding: 5px 10px; font-size: 0.75rem; } .btn-sm { padding: 5px 10px; font-size: 0.75rem; }
.seg-btn:hover { background: rgba(200, 164, 74, 0.1) !important; color: #c8a44a !important; }
.seg-btn.active { background: #c8a44a !important; color: #1a1a2e !important; font-weight: bold; box-shadow: 0 2px 5px rgba(0,0,0,0.3); }
#no-town-selected { #no-town-selected {
color: #666; color: #666;
font-size: 0.85rem; font-size: 0.85rem;
@@ -314,9 +318,9 @@ tr:hover td { background: #1e1e40; }
} }
/* ========================================================================== /* ==========================================================================
Building & Academy Picker Modal Building, Academy, Unit, Market & Blueprint Modals
========================================================================== */ ========================================================================== */
#building-modal-overlay, #academy-modal-overlay { #building-modal-overlay, #academy-modal-overlay, #unit-modal-overlay, #market-modal-overlay, #blueprints-modal-overlay {
display: none; display: none;
position: fixed; position: fixed;
inset: 0; inset: 0;
@@ -325,9 +329,9 @@ tr:hover td { background: #1e1e40; }
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
#building-modal-overlay.open, #academy-modal-overlay.open { display: flex; } #building-modal-overlay.open, #academy-modal-overlay.open, #unit-modal-overlay.open, #market-modal-overlay.open, #blueprints-modal-overlay.open { display: flex; }
#building-modal, #academy-modal { #building-modal, #academy-modal, #unit-modal, #market-modal, #blueprints-modal {
background: #16213e; background: #16213e;
border: 2px solid #c8a44a; border: 2px solid #c8a44a;
border-radius: 10px; border-radius: 10px;
@@ -342,7 +346,7 @@ tr:hover td { background: #1e1e40; }
from { transform: scale(0.92); opacity: 0; } from { transform: scale(0.92); opacity: 0; }
to { transform: scale(1); opacity: 1; } to { transform: scale(1); opacity: 1; }
} }
#building-modal-header, #academy-modal-header { #building-modal-header, #academy-modal-header, .modal-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@@ -350,12 +354,12 @@ tr:hover td { background: #1e1e40; }
padding-bottom: 10px; padding-bottom: 10px;
border-bottom: 1px solid #2a4a6a; border-bottom: 1px solid #2a4a6a;
} }
#building-modal-header h3, #academy-modal-header h3 { #building-modal-header h3, #academy-modal-header h3, .modal-header h3 {
color: #c8a44a; color: #c8a44a;
font-size: 1rem; font-size: 1rem;
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
#building-modal-close, #academy-modal-close { #building-modal-close, #academy-modal-close, #unit-modal-close {
background: none; background: none;
border: none; border: none;
color: #888; color: #888;
@@ -364,7 +368,7 @@ tr:hover td { background: #1e1e40; }
line-height: 1; line-height: 1;
padding: 0 4px; padding: 0 4px;
} }
#building-modal-close:hover, #academy-modal-close:hover { color: #fff; } #building-modal-close:hover, #academy-modal-close:hover, #unit-modal-close:hover { color: #fff; }
#building-grid { #building-grid {
display: grid; display: grid;

View File

@@ -4,7 +4,7 @@
window.fetchTowns = async function() { window.fetchTowns = async function() {
try { try {
const res = await fetch('/dashboard/towns?player_id=' + window.PLAYER_ID); const res = await fetch(`/dashboard/towns?player_id=${window.PLAYER_ID}&world_id=${window.WORLD_ID}`);
window.towns = await res.json(); window.towns = await res.json();
window.renderTowns(); window.renderTowns();
window.updateServerStatus(true); window.updateServerStatus(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);
@@ -48,10 +52,12 @@ window.fetchClientStatus = async function() {
window.fetchLog = async function() { window.fetchLog = async function() {
try { try {
const res = await fetch('/dashboard/commands?player_id=' + window.PLAYER_ID); const res = await fetch(`/dashboard/commands?player_id=${window.PLAYER_ID}&world_id=${window.WORLD_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) {}
}; };
@@ -91,8 +97,8 @@ window.sendCommand = async function() {
const town = window.getSelectedTown(); const town = window.getSelectedTown();
if (!town) return alert('Select a town first.'); if (!town) return alert('Select a town first.');
const type = document.getElementById('cmd-type').value; const type = window.currentCmdType;
if (!type) return alert('Παρακαλώ επιλέξτε Ενέργεια (Command Type) πρώτα.'); if (!type) return alert('Παρακαλώ επιλέξτε Ενέργεια (Κατασκευή/Στρατός/Παζάρι/Έρευνα) πρώτα.');
let payload = {}; let payload = {};
@@ -126,7 +132,7 @@ window.sendCommand = async function() {
payload = { building_id }; payload = { building_id };
} else if (type === 'recruit') { } else if (type === 'recruit') {
const unit_id = document.getElementById('unit-select').value; const unit_id = window.selectedUnitId;
if (!unit_id) return alert('Παρακαλώ επιλέξτε Μονάδα προς εκπαίδευση.'); if (!unit_id) return alert('Παρακαλώ επιλέξτε Μονάδα προς εκπαίδευση.');
const amount = parseInt(document.getElementById('recruit-amount').value) || 1; const amount = parseInt(document.getElementById('recruit-amount').value) || 1;
@@ -176,6 +182,30 @@ window.sendCommand = async function() {
} }
payload = { research_id }; payload = { research_id };
} else if (type === 'blueprints') {
const blueprint_name = window.selectedBlueprintName;
if (!blueprint_name) return alert('Παρακαλώ επιλέξτε Blueprint.');
try {
const res = await fetch('/dashboard/blueprints', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
town_id: town.town_id,
blueprint_name: blueprint_name
})
});
const data = await res.json();
if (data.ok) {
alert(data.is_active ? 'Blueprint ενεργοποιήθηκε!' : 'Blueprint απενεργοποιήθηκε!');
window.fetchTowns(); // refresh towns to update UI state
} else {
alert('Σφάλμα: ' + data.error);
}
} catch(e) {
alert('Failed to toggle blueprint: ' + e);
}
return; // Exit here as we don't send this to /dashboard/commands
} }
try { try {
@@ -192,7 +222,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 {

View File

@@ -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
}); });

View File

@@ -2,13 +2,106 @@
// Command Form Component // Command Form Component
// ================================================================ // ================================================================
window.onCmdTypeChange = function() { window.currentCmdType = null;
const type = document.getElementById('cmd-type').value;
document.getElementById('build-options').style.display = type === 'build' ? '' : 'none'; window.setCmdType = function(type, openModal = false) {
document.getElementById('recruit-options').style.display = type === 'recruit' ? '' : 'none'; window.currentCmdType = type;
document.getElementById('amount-group').style.display = type === 'recruit' ? '' : 'none';
document.getElementById('market-options').style.display = type === 'market_offer' ? '' : 'none'; // Update segmented control active state
document.getElementById('research-options').style.display = type === 'research' ? '' : 'none'; document.querySelectorAll('.seg-btn').forEach(btn => btn.classList.remove('active'));
const activeBtn = document.getElementById('seg-' + (type === 'market_offer' ? 'market' : type));
if (activeBtn) activeBtn.classList.add('active');
// Show selection area
document.getElementById('selection-area').style.display = 'flex';
document.getElementById('recruit-amount-wrap').style.display = type === 'recruit' ? 'flex' : 'none';
// Update selection label with current choice if any, or default
window.updateSelectionDisplay();
if (openModal) {
window.reopenActiveModal();
}
};
window.updateSelectionDisplay = function() {
const type = window.currentCmdType;
const labelEl = document.getElementById('selection-label');
if (type === 'build') {
if (window.selectedBuildingId) {
labelEl.innerHTML = `🏗️ ${window.BUILDING_NAMES_GR[window.selectedBuildingId] || window.selectedBuildingId}`;
} else {
labelEl.innerHTML = `-- Επιλέξτε Κατασκευή --`;
}
} else if (type === 'recruit') {
if (window.selectedUnitId) {
labelEl.innerHTML = `⚔️ ${window.UNIT_NAMES_GR[window.selectedUnitId] || window.selectedUnitId}`;
} else {
labelEl.innerHTML = `-- Επιλέξτε Μονάδα --`;
}
} else if (type === 'research') {
if (window.selectedResearchId) {
const rd = window.RESEARCH_DATA[window.selectedResearchId];
labelEl.innerHTML = `🦉 ${rd ? rd.name : window.selectedResearchId}`;
} else {
labelEl.innerHTML = `-- Επιλέξτε Έρευνα --`;
}
} else if (type === 'market_offer') {
labelEl.innerHTML = `🛒 Ρυθμίσεις Παζαριού`;
} else if (type === 'blueprints') {
if (window.selectedBlueprintName) {
labelEl.innerHTML = `📜 ${window.selectedBlueprintName}`;
} else {
labelEl.innerHTML = `-- Επιλέξτε Blueprint --`;
}
}
};
window.reopenActiveModal = function() {
const type = window.currentCmdType;
if (type === 'build') window.openBuildingModal();
else if (type === 'recruit') window.openUnitModal();
else if (type === 'research') window.openAcademyModal();
else if (type === 'market_offer') window.openMarketModal();
else if (type === 'blueprints') window.openBlueprintsModal();
};
window.selectedBlueprintName = null;
window.selectBlueprint = function(name) {
window.selectedBlueprintName = name;
window.updateSelectionDisplay();
window.closeBlueprintsModal();
};
window.openBlueprintsModal = function() {
document.getElementById('blueprints-modal-overlay').classList.add('open');
};
window.closeBlueprintsModal = function(e) {
if (e && e.target.id !== 'blueprints-modal-overlay' && !e.target.classList.contains('modal-close')) return;
document.getElementById('blueprints-modal-overlay').classList.remove('open');
};
window.openMarketModal = function() {
document.getElementById('market-modal-overlay').classList.add('open');
};
window.closeMarketModal = function(e) {
if (e && !e.target.classList.contains('modal-overlay') && !e.target.classList.contains('modal-close')) return;
document.getElementById('market-modal-overlay').classList.remove('open');
};
window.saveMarketModal = function() {
const oType = document.getElementById('market-offer-type').value;
const oAmt = document.getElementById('market-offer-amount').value;
const dType = document.getElementById('market-demand-type').value;
const dAmt = document.getElementById('market-demand-amount').value;
const labelEl = document.getElementById('selection-label');
const resGr = { wood: 'Ξύλο', stone: 'Πέτρα', iron: 'Ασήμι' };
labelEl.innerHTML = `🛒 ${oAmt} ${resGr[oType]}${dAmt} ${resGr[dType]}`;
window.closeMarketModal();
}; };
// Building emoji icons for the visual grid // Building emoji icons for the visual grid
@@ -118,11 +211,8 @@ window.closeBuildingModal = function(e) {
window.selectBuilding = function(key, nameGr) { window.selectBuilding = function(key, nameGr) {
window.selectedBuildingId = key; window.selectedBuildingId = key;
// Update the trigger button label window.updateSelectionDisplay();
document.getElementById('selected-building-label').textContent = `🏗️ ${nameGr}`;
// Re-render grid to show new selection highlight
window.openBuildingModal(); window.openBuildingModal();
// Close after brief visual feedback
setTimeout(() => document.getElementById('building-modal-overlay').classList.remove('open'), 180); setTimeout(() => document.getElementById('building-modal-overlay').classList.remove('open'), 180);
}; };
@@ -233,77 +323,140 @@ window.closeAcademyModal = function(e) {
window.selectResearch = function(key, name) { window.selectResearch = function(key, name) {
window.selectedResearchId = key; window.selectedResearchId = key;
document.getElementById('selected-research-label').textContent = `🧪 ${name}`; window.updateSelectionDisplay();
window.openAcademyModal(); window.openAcademyModal();
setTimeout(() => document.getElementById('academy-modal-overlay').classList.remove('open'), 180); setTimeout(() => document.getElementById('academy-modal-overlay').classList.remove('open'), 180);
}; };
window.selectedUnitId = null;
window.UNIT_ICONS = {
sword: '⚔️', slinger: '🪨', archer: '🏹', hoplite: '🛡️',
rider: '🐎', chariot: '🛷', catapult: '☄️', godsent: '👼',
big_transporter: '⛴️', small_transporter: '🚤', bireme: '🛶',
attack_ship: '🔥', trireme: '🔱', colonize_ship: '⚓',
medusa: '🐍', zyklop: '👁️', harpy: '🦅', pegasus: '🐴',
minotaur: '🐂', manticore: '🦁', cerberus: '🐕',
hydra: '🐉', sea_monster: '🦑', militia: '🧑‍🌾'
};
window.renderUnitDropdown = function() { window.renderUnitDropdown = function() {
// No-op - selection now happens via modal
};
window.openUnitModal = function() {
const town = window.getSelectedTown(); const town = window.getSelectedTown();
if (!town) return; if (!town) return;
const uSelect = document.getElementById('unit-select'); const grid = document.getElementById('unit-grid');
const uData = town.unit_data || {}; const uData = town.unit_data || {};
const currentVal = uSelect.value; // Group units into categories for better display
uSelect.innerHTML = '<option value="" disabled selected>-- Επιλέξτε Μονάδα --</option>'; const categories = {
'Ξηρά': ['sword', 'slinger', 'archer', 'hoplite', 'rider', 'chariot', 'catapult', 'godsent'],
'Ναυτικές': ['big_transporter', 'small_transporter', 'bireme', 'attack_ship', 'trireme', 'colonize_ship'],
'Μυθικές': ['medusa', 'zyklop', 'harpy', 'pegasus', 'minotaur', 'manticore', 'cerberus', 'hydra', 'sea_monster']
};
for (const [key, nameGr] of Object.entries(window.UNIT_NAMES_GR)) { let html = '';
if (key === 'militia') continue;
for (const [catName, units] of Object.entries(categories)) {
let catHtml = '';
const data = uData[key]; for (const key of units) {
let text = `${nameGr}`; if (key === 'militia') continue;
if (data) {
const w = window.fmt(data.wood || 0);
const st = window.fmt(data.stone || 0);
const i = window.fmt(data.iron || 0);
const pop = data.pop || 0;
// Unit build_time is usually raw seconds in GameData const nameGr = window.UNIT_NAMES_GR[key] || key;
let t = data.build_time || 0; const data = uData[key];
let tStr = `${t}s`; const icon = window.UNIT_ICONS[key] || '💂';
if (t > 60) { const isSelected = key === window.selectedUnitId;
let m = Math.floor(t / 60);
let s = t % 60;
tStr = `${m}m ${s}s`;
}
const costStr = `Ξ:${w} Π:${st} Α:${i} 🧔:${pop} · ⏱ ${tStr}`; let statusClass, statusLabel, cardClass = '';
let costStr = '';
let clickable = false;
const requiredGod = window.UNIT_GODS ? window.UNIT_GODS[key] : null;
const isWrongGod = requiredGod && town.god !== requiredGod;
const isNoGod = key === 'godsent' && !town.god;
const missingKeys = data.missing_dependencies ? Object.keys(data.missing_dependencies) : []; if (isWrongGod) {
const isLocked = missingKeys.length > 0; statusClass = 'locked'; statusLabel = '🔒 Άλλος Θεός'; cardClass = 'bld-locked';
const greekGods = { zeus: 'Δία', poseidon: 'Ποσειδώνα', hera: 'Ήρα', athena: 'Αθηνά', hades: 'Άδη', artemis: 'Άρτεμις', aphrodite: 'Αφροδίτη', ares: 'Άρη' };
const option = document.createElement('option'); costStr = `Απαιτεί: ${greekGods[requiredGod] || requiredGod}`;
option.value = key; } else if (isNoGod) {
statusClass = 'locked'; statusLabel = '🔒 Χωρίς Θεό'; cardClass = 'bld-locked';
if (isLocked) { costStr = `Απαιτείται Ναός`;
option.textContent = `${text} — 🔒 Κλειδωμένο`; } else if (data) {
option.style.color = '#ff4444'; const missingKeys = data.missing_dependencies ? Object.keys(data.missing_dependencies) : [];
} else if (data.enough_resources === false) { const isLocked = missingKeys.length > 0;
option.textContent = `${text} — ❌ ${costStr} (Λείπουν Πόροι 1x)`;
option.style.color = '#aa5555'; const w = window.fmt(data.wood || 0);
const st = window.fmt(data.stone || 0);
const i = window.fmt(data.iron || 0);
const pop = data.pop || 0;
let t = data.build_time || 0;
let tStr = `${t}s`;
if (t > 60) {
let m = Math.floor(t / 60);
let s = t % 60;
tStr = `${m}m ${s}s`;
}
// Show favor if it's a mythical unit or godsent
const favorStr = (data.favor && data.favor > 0) ? ` ⚡:${data.favor}` : '';
costStr = `Ξ:${w} Π:${st} Α:${i}${favorStr} 🧔:${pop} · ⏱ ${tStr}`;
if (isLocked) {
statusClass = 'locked'; statusLabel = '🔒 Κλειδωμένο'; cardClass = 'bld-locked';
} else if (data.enough_resources === false) {
statusClass = 'no-resources'; statusLabel = '❌ Λείπουν Πόροι';
} else {
statusClass = 'can-build'; statusLabel = '✅ Διαθέσιμο';
clickable = true;
}
} else { } else {
option.textContent = `${text} — ✅ ${costStr}`; // If no data is available for this unit, treat it as locked/unknown
statusClass = 'locked'; statusLabel = '🔒 Άγνωστο'; cardClass = 'bld-locked';
} }
uSelect.appendChild(option); const onclick = clickable ? `onclick="window.selectUnit('${key}', '${nameGr}')"` : '';
} else {
const option = document.createElement('option'); catHtml += `<div class="bld-card ${cardClass}${isSelected ? ' bld-selected' : ''}" ${onclick} style="width:140px; justify-content:flex-start;">
option.value = key; <span class="bld-icon">${icon}</span>
option.textContent = text; <span class="bld-name" style="margin-top:6px; font-size:0.85rem;">${nameGr}</span>
uSelect.appendChild(option); <span class="bld-status ${statusClass}">${statusLabel}</span>
<span class="bld-cost" style="font-size:0.65rem;">${costStr}</span>
</div>`;
}
if (catHtml) {
html += `<div style="width:100%; margin-top:10px;"><h4 style="color:#c8a44a; margin-bottom:10px; border-bottom:1px solid #2a4a6a; padding-bottom:4px;">${catName}</h4>
<div style="display:flex; flex-wrap:wrap; gap:10px;">${catHtml}</div>
</div>`;
} }
} }
if (currentVal && Array.from(uSelect.options).some(o => o.value === currentVal)) { grid.innerHTML = html;
uSelect.value = currentVal; document.getElementById('unit-modal-overlay').classList.add('open');
}
}; };
window.closeUnitModal = function(e) {
if (e && e.target !== document.getElementById('unit-modal-overlay') && e.target !== document.getElementById('unit-modal-close')) return;
document.getElementById('unit-modal-overlay').classList.remove('open');
};
window.selectUnit = function(key, nameGr) {
window.selectedUnitId = key;
window.updateSelectionDisplay();
window.openUnitModal();
setTimeout(() => document.getElementById('unit-modal-overlay').classList.remove('open'), 180);
};
// ================================================================
window.renderBuildQueuePreview = function() { window.renderBuildQueuePreview = function() {
const town = window.getSelectedTown(); const town = window.getSelectedTown();
const el = document.getElementById('build-queue-preview'); const el = document.getElementById('build-queue-preview');

View File

@@ -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>

View File

@@ -12,8 +12,7 @@ window.renderTowns = function() {
// Get active filters // Get active filters
const searchTerm = (document.getElementById('town-search')?.value || '').toLowerCase(); const searchTerm = (document.getElementById('town-search')?.value || '').toLowerCase();
const reqFullWh = document.getElementById('filter-full-wh')?.checked; const reqFullWh = document.getElementById('filter-full-wh')?.checked;
const reqFestival = document.getElementById('filter-festival')?.checked; const reqNotBuilding = document.getElementById('filter-not-building')?.checked;
const reqPoints = document.getElementById('filter-points')?.checked;
const filteredTowns = window.towns.filter(t => { const filteredTowns = window.towns.filter(t => {
// 1. Search by name // 1. Search by name
@@ -29,13 +28,8 @@ window.renderTowns = function() {
// 2. Full WH (>95%) // 2. Full WH (>95%)
if (reqFullWh && Math.max(wPct, sPct, iPct) < 0.95) return false; if (reqFullWh && Math.max(wPct, sPct, iPct) < 0.95) return false;
// 3. Festival (Wood>15k, Stone>18k, Iron>15k) // 3. Not Building (no items in build_queue)
// City Festival exact costs = 15000, 18000, 15000 if (reqNotBuilding && t.build_queue && t.build_queue.length > 0) return false;
if (reqFestival && ((res.wood || 0) < 15000 || (res.stone || 0) < 18000 || (res.iron || 0) < 15000)) return false;
// 4. Large Points
const pts = typeof t.points === 'number' ? t.points : parseInt(t.points) || 0;
if (reqPoints && pts < 10000) return false;
return true; return true;
}); });
@@ -62,9 +56,6 @@ window.renderTowns = function() {
if (wPct >= 0.95 || sPct >= 0.95 || iPct >= 0.95) { if (wPct >= 0.95 || sPct >= 0.95 || iPct >= 0.95) {
markers += '<span title="Γεμάτη Αποθήκη!" style="margin-right:4px;">⚠️</span>'; markers += '<span title="Γεμάτη Αποθήκη!" style="margin-right:4px;">⚠️</span>';
} }
if ((res.wood || 0) >= 15000 && (res.stone || 0) >= 18000 && (res.iron || 0) >= 15000) {
markers += '<span title="Αρκετοί πόροι για Φεστιβάλ!" style="margin-right:4px;">🎭</span>';
}
const getC = (pct) => pct >= 0.95 ? 'color:#ff4a4a;font-weight:bold;' : ''; const getC = (pct) => pct >= 0.95 ? 'color:#ff4a4a;font-weight:bold;' : '';
@@ -99,6 +90,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() {
@@ -217,4 +212,43 @@ window.renderTownDetails = function() {
if(unitsHtml === '') unitsHtml = '<div style="color:#666">Κανένα στράτευμα</div>'; if(unitsHtml === '') unitsHtml = '<div style="color:#666">Κανένα στράτευμα</div>';
document.getElementById('td-units').innerHTML = unitsHtml; document.getElementById('td-units').innerHTML = unitsHtml;
// ---- Blueprint Lock & Indicator ----
const isBlueprintActive = !!t.blueprint_active;
const bpName = t.blueprint_name || 'Standard Growth';
if (isBlueprintActive) {
document.getElementById('td-general').innerHTML += `
<div style="margin-top:10px; padding: 4px 8px; background: rgba(200,164,74,0.15); border-left: 3px solid #c8a44a; border-radius:4px; font-size:0.8rem;">
🤖 Blueprint: <strong style="color:#c8a44a">${bpName}</strong> <span style="color:#2ecc71">(ΕΝΕΡΓΟ)</span>
</div>
`;
}
const btnBuild = document.getElementById('seg-build');
const btnResearch = document.getElementById('seg-research');
if (btnBuild && btnResearch) {
if (isBlueprintActive) {
btnBuild.style.opacity = '0.3';
btnBuild.style.pointerEvents = 'none';
btnBuild.title = 'Απενεργοποιημένο λόγω Blueprint';
btnResearch.style.opacity = '0.3';
btnResearch.style.pointerEvents = 'none';
btnResearch.title = 'Απενεργοποιημένο λόγω Blueprint';
if (window.currentCmdType === 'build' || window.currentCmdType === 'research') {
window.setCmdType('recruit');
}
} else {
btnBuild.style.opacity = '1';
btnBuild.style.pointerEvents = 'auto';
btnBuild.title = '';
btnResearch.style.opacity = '1';
btnResearch.style.pointerEvents = 'auto';
btnResearch.title = '';
}
}
}; };

View File

@@ -24,8 +24,8 @@ window.BUILDING_NAMES_GR = {
}; };
window.UNIT_NAMES_GR = { window.UNIT_NAMES_GR = {
sword: "Ξιφομάχος", slinger: "Σφενδονήτης", archer: "Τοξότης", hoplite: "Οπλίτης", sword: "Ξιφομάχος", slinger: "Εκσφενδονιστής", archer: "Τοξότης", hoplite: "Οπλίτης",
rider: "Ιππέας", chariot: "Άρμα", catapult: "Καταπέλτης", rider: "Ιππέας", chariot: "Άρμα", catapult: "Καταπέλτης", godsent: "Θεόσταλτος",
big_transporter: "Μεταφορικό", small_transporter: "Γρήγ. Μεταφορικό", bireme: "Διήρης", big_transporter: "Μεταφορικό", small_transporter: "Γρήγ. Μεταφορικό", bireme: "Διήρης",
attack_ship: "Πλοίο Φάρος", trireme: "Τριήρης", colonize_ship: "Αποικιακό", attack_ship: "Πλοίο Φάρος", trireme: "Τριήρης", colonize_ship: "Αποικιακό",
medusa: "Μέδουσα", zyklop: "Κύκλωπας", harpy: "Άρπυια", pegasus: "Πήγασος", medusa: "Μέδουσα", zyklop: "Κύκλωπας", harpy: "Άρπυια", pegasus: "Πήγασος",
@@ -33,6 +33,17 @@ window.UNIT_NAMES_GR = {
hydra: "Ύδρα", sea_monster: "Τέρας Θάλασσας", militia: "Εθνοφρουρά" hydra: "Ύδρα", sea_monster: "Τέρας Θάλασσας", militia: "Εθνοφρουρά"
}; };
window.UNIT_GODS = {
minotaur: 'zeus', manticore: 'zeus',
zyklop: 'poseidon', hydra: 'poseidon',
harpy: 'hera', medusa: 'hera',
pegasus: 'athena', centaur: 'athena',
cerberus: 'hades', erinys: 'hades',
griffon: 'artemis', calydonian_boar: 'artemis',
siren: 'aphrodite', satyr: 'aphrodite',
spartoi: 'ares', ladon: 'ares'
};
window.RES_ICONS = { window.RES_ICONS = {
wood: '<span class="res-icon res-wood" style="display:inline-block; margin-right:4px;"></span>', wood: '<span class="res-icon res-wood" style="display:inline-block; margin-right:4px;"></span>',
stone: '<span class="res-icon res-stone" style="display:inline-block; margin-right:4px;"></span>', stone: '<span class="res-icon res-stone" style="display:inline-block; margin-right:4px;"></span>',

View File

@@ -10,7 +10,8 @@
<header> <header>
<h1><a href="/" style="text-decoration: none; margin-right: 15px; cursor: pointer;" title="Back to Players">⬅️</a> ⚔️ Grepolis Remote</h1> <h1><a href="/" style="text-decoration: none; margin-right: 15px; cursor: pointer;" title="Back to Players">⬅️</a> ⚔️ Grepolis Remote</h1>
<div class="status-indicator"> <div class="status-indicator" style="display: flex; align-items: center; gap: 10px;">
<button class="btn btn-gold btn-sm" id="live-btn" onclick="window.requestLiveSync()" title="Request immediate data update from game" style="padding: 4px 8px; font-size: 0.72rem; border-radius: 4px; border: 1px solid #c8a44a;">⚡ Live Sync</button>
<div id="server-status" class="conn-badge">Server…</div> <div id="server-status" class="conn-badge">Server…</div>
<div id="client-status" class="conn-badge">Client…</div> <div id="client-status" class="conn-badge">Client…</div>
</div> </div>
@@ -29,7 +30,6 @@
<div id="town-panel"> <div id="town-panel">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<h2 style="margin: 0;">Towns</h2> <h2 style="margin: 0;">Towns</h2>
<button class="btn btn-gold btn-sm" id="live-btn" onclick="window.requestLiveSync()" title="Request immediate data update from game" style="padding: 4px 8px; font-size: 0.72rem;">⚡ Live Sync</button>
</div> </div>
<div id="town-filters" style="margin-bottom: 15px; padding: 10px; background: #0f3460; border-radius: 6px; border: 1px solid #2a4a6a;"> <div id="town-filters" style="margin-bottom: 15px; padding: 10px; background: #0f3460; border-radius: 6px; border: 1px solid #2a4a6a;">
@@ -41,11 +41,7 @@
</label> </label>
<label style="font-size: 0.8rem; cursor: pointer; display: flex; align-items: center; gap: 4px; background: rgba(0,0,0,0.2); padding: 4px 8px; border-radius: 12px; border: 1px solid #2a4a6a;"> <label style="font-size: 0.8rem; cursor: pointer; display: flex; align-items: center; gap: 4px; background: rgba(0,0,0,0.2); padding: 4px 8px; border-radius: 12px; border: 1px solid #2a4a6a;">
<input type="checkbox" id="filter-festival" onchange="window.renderTowns()"> 🎭 Ελεύθεροι Πόροι <input type="checkbox" id="filter-not-building" onchange="window.renderTowns()"> 🏗️ Δεν χτίζει
</label>
<label style="font-size: 0.8rem; cursor: pointer; display: flex; align-items: center; gap: 4px; background: rgba(0,0,0,0.2); padding: 4px 8px; border-radius: 12px; border: 1px solid #2a4a6a;">
<input type="checkbox" id="filter-points" onchange="window.renderTowns()"> 📈 10k+ Πόντοι
</label> </label>
</div> </div>
</div> </div>
@@ -87,81 +83,92 @@
</div> </div>
<div id="command-form-wrap" style="display:none"> <div id="command-form-wrap" style="display:none">
<div class="command-form"> <div class="command-form" style="display: flex; flex-direction: column; gap: 15px; align-items: flex-start;">
<div class="form-group"> <!-- Segmented Control Row -->
<label>Command Type</label> <div class="segmented-control" id="cmd-type-buttons" style="display: flex; gap: 5px; background: #16213e; padding: 4px; border-radius: 8px; border: 1px solid #2a4a6a;">
<select id="cmd-type" onchange="onCmdTypeChange()"> <button class="seg-btn" id="seg-build" onclick="window.setCmdType('build', true)" style="flex: 1; padding: 10px; border: none; background: transparent; color: #888; border-radius: 6px; cursor: pointer; transition: 0.2s;">🏗️ Κατασκευές</button>
<option value="" disabled selected>-- Επιλέξτε --</option> <button class="seg-btn" id="seg-recruit" onclick="window.setCmdType('recruit', true)" style="flex: 1; padding: 10px; border: none; background: transparent; color: #888; border-radius: 6px; cursor: pointer; transition: 0.2s;">⚔️ Στρατός</button>
<option value="build">Build / Upgrade</option> <button class="seg-btn" id="seg-market" onclick="window.setCmdType('market_offer', true)" style="flex: 1; padding: 10px; border: none; background: transparent; color: #888; border-radius: 6px; cursor: pointer; transition: 0.2s;">🛒 Παζάρι</button>
<option value="recruit">Recruit Troops</option> <button class="seg-btn" id="seg-research" onclick="window.setCmdType('research', true)" style="flex: 1; padding: 10px; border: none; background: transparent; color: #888; border-radius: 6px; cursor: pointer; transition: 0.2s;">🦉 Έρευνα</button>
<option value="market_offer">Παζάρι - Προσφορά</option> <button class="seg-btn" id="seg-blueprints" onclick="window.setCmdType('blueprints', true)" style="flex: 1; padding: 10px; border: none; background: transparent; color: #888; border-radius: 6px; cursor: pointer; transition: 0.2s;">📜 Blueprints</button>
<option value="research">Ακαδημία - Έρευνες</option>
</select>
</div> </div>
<!-- Build options - now a button that opens the visual picker --> <!-- Dynamic Selection Area -->
<div class="form-group" id="build-options" style="display:none"> <div id="selection-area" style="display:none; align-items: center; gap: 10px; flex-wrap: wrap;">
<label>Building</label> <button class="btn btn-gold" id="active-selection-display" onclick="window.reopenActiveModal()" style="min-width: 250px; padding: 10px 15px; border-radius: 6px; display: flex; justify-content: space-between; align-items: center; font-weight: bold;">
<button class="btn btn-gold" id="open-building-modal" onclick="window.openBuildingModal()" style="text-align:left; min-width:200px;"> <span id="selection-label">-- Επιλέξτε --</span>
<span id="selected-building-label">-- Επιλέξτε Κατασκευή --</span> </button>
</button>
<div id="recruit-amount-wrap" style="display:none; align-items: center; gap: 5px; background: #16213e; padding: 8px 12px; border-radius: 6px; border: 1px solid #2a4a6a;">
<label style="font-size:0.85rem; color:#ccc; margin:0;">Ποσότητα:</label>
<input type="number" id="recruit-amount" value="1" min="1" max="9999" style="width: 70px; background: #0f3460; color: #fff; border: 1px solid #c8a44a; border-radius: 4px; padding: 4px 8px;">
</div>
<button id="btn-send" class="btn btn-gold" onclick="window.sendCommand()" style="margin-left: auto; padding: 10px 20px; font-size: 1rem;">Send ⚡</button>
</div> </div>
<!-- Research options --> </div>
<div class="form-group" id="research-options" style="display:none">
<label>Έρευνα</label>
<button class="btn btn-gold" id="open-academy-modal" onclick="window.openAcademyModal()" style="text-align:left; min-width:200px;">
<span id="selected-research-label">-- Επιλέξτε Έρευνα --</span>
</button>
</div>
<!-- Recruit options --> <div id="build-queue-preview"></div>
<div class="form-group" id="recruit-options" style="display:none"> </div>
<label>Unit</label> </div>
<select id="unit-select">
<optgroup label="Ξηρά">
<option value="sword">Ξιφομάχος</option>
<option value="slinger">Σφενδονήτης</option>
<option value="archer">Τοξότης</option>
<option value="hoplite">Οπλίτης</option>
<option value="rider">Ιππέας</option>
<option value="chariot">Άρμα</option>
<option value="catapult">Καταπέλτης</option>
</optgroup>
<optgroup label="Ναυτικές">
<option value="big_transporter">Μεταφορικό Πλοίο</option>
<option value="small_transporter">Γρήγορο Μεταφορικό Πλοίο</option>
<option value="bireme">Διήρης</option>
<option value="attack_ship">Πλοίο Φάρος</option>
<option value="trireme">Τριήρης</option>
<option value="colonize_ship">Αποικιακό Πλοίο</option>
</optgroup>
<optgroup label="Μυθικές">
<option value="medusa">Μέδουσα</option>
<option value="zyklop">Κύκλωπας</option>
<option value="harpy">Άρπυια</option>
<option value="pegasus">Πήγασος</option>
<option value="minotaur">Μινώταυρος</option>
<option value="manticore">Μαντιχώρας</option>
<option value="cerberus">Κέρβερος</option>
<option value="hydra">Ύδρα</option>
<option value="sea_monster">Τέρας της Θάλασσας</option>
</optgroup>
</select>
</div>
<!-- Market options --> <!-- Bottom right: Build Queue / Command Log (tabbed) -->
<!-- Market options --> <div id="log-panel">
<div class="form-group" id="market-options" style="display:none"> <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">
<p style="color:#555;font-size:0.85rem;padding:12px 0;">← Επιλέξτε πόλη για να δείτε την ουρά.</p>
</div>
</div>
</div>
<!-- ====== Blueprints Modal ====== -->
<div class="modal-overlay" id="blueprints-modal-overlay" onclick="window.closeBlueprintsModal(event)">
<div class="custom-modal" id="blueprints-modal" style="max-width: 400px;">
<div class="modal-header">
<h3>📜 Επιλογή Blueprint</h3>
<button class="modal-close" onclick="window.closeBlueprintsModal()"></button>
</div>
<div style="padding: 15px;">
<p style="color:#ccc; font-size:0.85rem; margin-bottom:15px;">Επιλέξτε ένα Blueprint για να αναλάβει το Python την αυτόματη κατασκευή της πόλης.</p>
<div id="blueprint-list" style="display:flex; flex-direction:column; gap:10px; margin-bottom:20px;">
<!-- Currently just one blueprint -->
<div class="bld-card" id="bp-card-standard" onclick="window.selectBlueprint('Standard Growth')" style="width:100%; justify-content:flex-start; cursor:pointer;">
<span class="bld-icon" style="font-size:2rem;">🏙️</span>
<div style="display:flex; flex-direction:column; align-items:flex-start;">
<span class="bld-name" style="margin-top:0; font-size:1rem; font-weight:bold;">Standard Growth</span>
<span style="font-size:0.75rem; color:#888;">Αυτόματη ανάπτυξη κτιρίων & ακαδημίας</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ====== Market Modal ====== -->
<div class="modal-overlay" id="market-modal-overlay" onclick="window.closeMarketModal(event)">
<div class="custom-modal" id="market-modal" style="max-width: 500px;">
<div class="modal-header">
<h3>🛒 Ρυθμίσεις Παζαριού</h3>
<button class="modal-close" onclick="window.closeMarketModal()"></button>
</div>
<div style="padding: 15px;">
<div style="display:flex; gap:10px; margin-bottom:10px;"> <div style="display:flex; gap:10px; margin-bottom:10px;">
<div style="flex:1;"> <div style="flex:1;">
<label>Προσφορά</label> <label style="color:#ccc; font-size:0.85rem;">Προσφορά</label>
<input type="number" id="market-offer-amount" value="1000" min="1" max="99999" style="width:100%;"> <input type="number" id="market-offer-amount" value="1000" min="1" max="99999" style="width:100%; background:#0f3460; color:#fff; border:1px solid #2a4a6a; border-radius:4px; padding:6px;">
</div> </div>
<div style="flex:1;"> <div style="flex:1;">
<label>Πόρος Προσφοράς</label> <label style="color:#ccc; font-size:0.85rem;">Πόρος Προσφοράς</label>
<select id="market-offer-type"> <select id="market-offer-type" style="width:100%; background:#0f3460; color:#fff; border:1px solid #2a4a6a; border-radius:4px; padding:6px;">
<option value="wood">Ξύλο</option> <option value="wood">Ξύλο</option>
<option value="stone">Πέτρα</option> <option value="stone">Πέτρα</option>
<option value="iron">Ασήμι</option> <option value="iron">Ασήμι</option>
@@ -170,22 +177,22 @@
</div> </div>
<div style="display:flex; gap:10px; margin-bottom:10px;"> <div style="display:flex; gap:10px; margin-bottom:10px;">
<div style="flex:1;"> <div style="flex:1;">
<label>Ζήτηση</label> <label style="color:#ccc; font-size:0.85rem;">Ζήτηση</label>
<input type="number" id="market-demand-amount" value="1000" min="1" max="99999" style="width:100%;"> <input type="number" id="market-demand-amount" value="1000" min="1" max="99999" style="width:100%; background:#0f3460; color:#fff; border:1px solid #2a4a6a; border-radius:4px; padding:6px;">
</div> </div>
<div style="flex:1;"> <div style="flex:1;">
<label>Πόρος Ζήτησης</label> <label style="color:#ccc; font-size:0.85rem;">Πόρος Ζήτησης</label>
<select id="market-demand-type"> <select id="market-demand-type" style="width:100%; background:#0f3460; color:#fff; border:1px solid #2a4a6a; border-radius:4px; padding:6px;">
<option value="iron">Ασήμι</option> <option value="iron">Ασήμι</option>
<option value="stone">Πέτρα</option> <option value="stone">Πέτρα</option>
<option value="wood">Ξύλο</option> <option value="wood">Ξύλο</option>
</select> </select>
</div> </div>
</div> </div>
<div style="display:flex; gap:10px;"> <div style="display:flex; gap:10px; margin-bottom:20px;">
<div style="flex:1;"> <div style="flex:1;">
<label>Χρόνος (Ώρες)</label> <label style="color:#ccc; font-size:0.85rem;">Χρόνος (Ώρες)</label>
<select id="market-max-time"> <select id="market-max-time" style="width:100%; background:#0f3460; color:#fff; border:1px solid #2a4a6a; border-radius:4px; padding:6px;">
<option value="1800">0.5</option> <option value="1800">0.5</option>
<option value="3600">1</option> <option value="3600">1</option>
<option value="7200">2</option> <option value="7200">2</option>
@@ -197,36 +204,16 @@
</select> </select>
</div> </div>
<div style="flex:1;"> <div style="flex:1;">
<label>Ορατότητα</label> <label style="color:#ccc; font-size:0.85rem;">Ορατότητα</label>
<select id="market-visibility"> <select id="market-visibility" style="width:100%; background:#0f3460; color:#fff; border:1px solid #2a4a6a; border-radius:4px; padding:6px;">
<option value="allies">Συμμαχία Μόνο</option> <option value="allies">Συμμαχία Μόνο</option>
<option value="all">Όλοι</option> <option value="all">Όλοι</option>
</select> </select>
</div> </div>
</div> </div>
<button class="btn btn-gold" onclick="window.saveMarketModal()" style="width:100%; padding:10px; font-weight:bold;">✅ Αποθήκευση Προσφοράς</button>
</div>
<div class="form-group" id="amount-group" style="display:none">
<label>Amount</label>
<input type="number" id="recruit-amount" value="1" min="1" max="9999" style="width:80px;">
</div>
<button id="btn-send" class="btn btn-gold" onclick="window.sendCommand()">Send ⚡</button>
</div>
<div id="build-queue-preview"></div>
</div> </div>
</div> </div>
<!-- Bottom right: Command log -->
<div id="log-panel">
<h2>Command Log</h2>
<div id="log-content">
<p id="empty-log">No commands sent yet.</p>
</div>
</div>
</div> </div>
<!-- ====== Building Picker Modal ====== --> <!-- ====== Building Picker Modal ====== -->
@@ -243,6 +230,17 @@
</div> </div>
</div> </div>
</div> </div>
<!-- ====== Unit Picker Modal ====== -->
<div id="unit-modal-overlay" class="modal-overlay" onclick="window.closeUnitModal(event)">
<div id="unit-modal" class="custom-modal">
<div class="modal-header">
<h3>⚔️ Επιλογή Μονάδας</h3>
<button id="unit-modal-close" onclick="window.closeUnitModal()"></button>
</div>
<div id="unit-grid" style="display:flex; flex-wrap:wrap; gap:10px; max-height:70vh; overflow-y:auto; padding:10px 5px;"></div>
</div>
</div>
<!-- ====== Academy Picker Modal ====== --> <!-- ====== Academy Picker Modal ====== -->
<div id="academy-modal-overlay" onclick="window.closeAcademyModal(event)"> <div id="academy-modal-overlay" onclick="window.closeAcademyModal(event)">
<div id="academy-modal"> <div id="academy-modal">
@@ -254,14 +252,79 @@
</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 }}";
window.WORLD_ID = "{{ world_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>

View File

@@ -233,7 +233,7 @@
<body> <body>
<div class="topbar"> <div class="topbar">
<a href="/player/{{ player_id }}">← Πίσω</a> <a href="/player/{{ player_id }}/{{ world_id }}">← Πίσω</a>
<h1>🌾 Farm Manager</h1> <h1>🌾 Farm Manager</h1>
<span class="online-dot" id="online-dot" title="Κατάσταση Script"></span> <span class="online-dot" id="online-dot" title="Κατάσταση Script"></span>
<button class="sync-btn" onclick="requestSync()">Live Sync</button> <button class="sync-btn" onclick="requestSync()">Live Sync</button>
@@ -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>1222 λεπτά</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>2545 λεπτά</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,17 +398,20 @@
<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 }}';
const WORLD_ID = '{{ world_id }}';
let selectedOption = 1; let selectedOption = 1;
// -- Loot option buttons -- // -- Loot option buttons --
@@ -369,7 +446,7 @@
fetch('/dashboard/farm-settings', { fetch('/dashboard/farm-settings', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ player_id: PLAYER_ID, enabled, loot_option: selectedOption }) body: JSON.stringify({ player_id: PLAYER_ID, world_id: WORLD_ID, enabled, loot_option: selectedOption })
}) })
.then(r => r.json()) .then(r => r.json())
.then(() => { .then(() => {
@@ -381,7 +458,7 @@
// -- Load current settings -- // -- Load current settings --
function loadSettings() { function loadSettings() {
fetch(`/dashboard/farm-settings?player_id=${PLAYER_ID}`) fetch(`/dashboard/farm-settings?player_id=${PLAYER_ID}&world_id=${WORLD_ID}`)
.then(r => r.json()) .then(r => r.json())
.then(cfg => { .then(cfg => {
document.getElementById('farm-enabled').checked = cfg.enabled; document.getElementById('farm-enabled').checked = cfg.enabled;
@@ -395,17 +472,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}&world_id=${WORLD_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 +507,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('');
}); });
@@ -472,7 +564,7 @@
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
player_id: PLAYER_ID, player_id: PLAYER_ID,
town_id: 0, town_id: WORLD_ID ? `0_${WORLD_ID}` : "0",
type: 'farm_upgrade', type: 'farm_upgrade',
payload: { threshold: threshold, action_type: actionType } payload: { threshold: threshold, action_type: actionType }
}) })
@@ -499,7 +591,7 @@
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
player_id: PLAYER_ID, player_id: PLAYER_ID,
town_id: 0, town_id: WORLD_ID ? `0_${WORLD_ID}` : "0",
type: 'farm_loot', type: 'farm_loot',
payload: { loot_option: selectedOption } payload: { loot_option: selectedOption }
}) })
@@ -514,24 +606,127 @@
}); });
} }
// -- 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, world_id: WORLD_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}&world_id=${WORLD_ID}`);
const data = await res.json(); const data = await res.json();
const banner = document.getElementById('warehouse-full-banner'); const banner = document.getElementById('warehouse-full-banner');
if (banner) banner.style.display = data.warehouse_full ? 'flex' : 'none'; if (banner) banner.style.display = data.warehouse_full ? 'flex' : 'none';
} 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}&world_id=${WORLD_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,
world_id: WORLD_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}&world_id=${WORLD_ID}&feature=bootcamp`)
.then(r => r.json()).then(data => renderBotLog('bootcamp-log', data));
fetch(`/dashboard/bot-logs?player_id=${PLAYER_ID}&world_id=${WORLD_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>

View File

@@ -154,13 +154,13 @@
<div class="hub-grid"> <div class="hub-grid">
<a href="/player/{{ player_id }}/admin" class="hub-card admin"> <a href="/player/{{ player_id }}/{{ world_id }}/admin" class="hub-card admin">
<span class="card-icon">🏛️</span> <span class="card-icon">🏛️</span>
<div class="card-title">Admin Mode</div> <div class="card-title">Admin Mode</div>
<div class="card-desc">Κτίρια, στρατολόγηση, αγορά, ουρά κατασκευών και πλήρης έλεγχος πόλεων.</div> <div class="card-desc">Κτίρια, στρατολόγηση, αγορά, ουρά κατασκευών και πλήρης έλεγχος πόλεων.</div>
</a> </a>
<a href="/player/{{ player_id }}/farm" class="hub-card farm"> <a href="/player/{{ player_id }}/{{ world_id }}/farm" class="hub-card farm">
<span class="card-icon">🌾</span> <span class="card-icon">🌾</span>
<div class="card-title">Farm Manager</div> <div class="card-title">Farm Manager</div>
<div class="card-desc">Αυτόματη συλλογή πόρων από χωριά. Ρυθμίσεις χρόνου λεηλασίας και έλεγχος με ένα κλικ.</div> <div class="card-desc">Αυτόματη συλλογή πόρων από χωριά. Ρυθμίσεις χρόνου λεηλασίας και έλεγχος με ένα κλικ.</div>
@@ -180,7 +180,8 @@
<script> <script>
// Fetch player name to show in the badge // Fetch player name to show in the badge
const playerId = '{{ player_id }}'; const playerId = '{{ player_id }}';
fetch(`/dashboard/towns?player_id=${playerId}`) const worldId = '{{ world_id }}';
fetch(`/dashboard/towns?player_id=${playerId}&world_id=${worldId}`)
.then(r => r.json()) .then(r => r.json())
.then(towns => { .then(towns => {
if (towns && towns.length > 0) { if (towns && towns.length > 0) {

View File

@@ -123,7 +123,7 @@
{% endif %} {% endif %}
{% for p in players %} {% for p in players %}
<a href="/player/{{ p.player_id }}" class="player-card"> <a href="/player/{{ p.player_id }}/{{ p.world_id }}" class="player-card">
<div style="display: flex; justify-content: space-between; align-items: center;"> <div style="display: flex; justify-content: space-between; align-items: center;">
<div> <div>
<strong>{{ p.player }}</strong> <strong>{{ p.player }}</strong>