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() {
if (paused) return;
const player_id = uw.Game?.player_id;
const world_id = uw.Game?.world_id;
if (!player_id) return;
let cmdData;
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();
} catch (e) {
log(`Poll failed: ${e}`);
return;
}
// Build queue, Recruit queue and Market queue are independent
const buildCmd = cmdData.build;
// Build queue: one command per town (all towns build in the same poll cycle)
const buildCmds = cmdData.builds || [];
const recruitCmd = cmdData.recruit;
const marketCmd = cmdData.market;
const researchCmd = cmdData.research;
@@ -873,8 +874,16 @@
reportResult(cmd.id, finalStatus, result.msg);
};
// Run sequentially — humans cannot perform 3 actions simultaneously!
await execute(buildCmd);
// Execute ALL town build commands (one per town, sequential with inter-town delay)
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(marketCmd);
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.
---
*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.dashboard import dashboard
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
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++;
} catch (e) { errors++; }
await sleep(randInt(1200, 2500));
await sleep(randInt(4000, 10000));
}
}
}
@@ -204,17 +205,10 @@ async function executeFarmLoot(cmd) {
claimed++;
} catch (e) { errors++; }
await sleep(randInt(1000, 2200));
await sleep(randInt(500, 1500));
}
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` };

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
// ================================================================
// 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() {
if (paused) return;
const player_id = uw.Game?.player_id;
const world_id = uw.Game?.world_id;
if (!player_id) return;
let cmdData;
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();
} catch (e) {
log(`Poll failed: ${e}`);
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)
const features = cmdData.enabled_features || ['farm', 'admin'];
const farmOn = features.includes('farm');
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 marketCmd = adminOn ? cmdData.market : 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 === 'market_offer') result = await executeMarketOffer(cmd);
else if (cmd.type === 'research') result = await executeResearch(cmd);
else if (cmd.type === 'farm_loot') result = await executeFarmLoot(cmd);
else if (cmd.type === 'farm_upgrade') result = await executeFarmUpgrade(cmd);
else 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}` };
} catch (e) {
result = { ok: false, msg: `Exception: ${e}` };
@@ -60,95 +116,122 @@ async function pollAndExecute() {
reportResult(cmd.id, finalStatus, result.msg);
};
// Run sequentially — humans cannot perform 3 actions simultaneously!
await execute(buildCmd);
// Execute one build command per town (simultaneous queue draining across all villages)
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(marketCmd);
await execute(researchCmd);
await execute(farmCmd);
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;
let claimedAny = false;
// ----------------------------------------------------------------
// autoFarmLoop — runs every 60120 s (independent of main poll)
// 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 || {});
for (const town of towns) {
// 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 farmSettings = lastKnownFarmSettings;
if (!farmSettings.enabled) return;
const wood = res.wood || 0;
const stone = res.stone || 0;
const iron = res.iron || 0;
if (!storage) continue;
// Don't overlap with an explicit farm_loot command running in main loop
if (farmLootRunning) {
log('Auto-farm: explicit farm_loot in progress — skipping this cycle');
return;
}
const maxRes = Math.max(wood, stone, iron);
const pct = maxRes / storage;
// Check if any farms are actually ready before doing anything heavy
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) {
allFull = false;
log(`⚡ Auto-farm: looting into town ${town.get?.('name')} (${Math.round(pct * 100)}% full)`);
await executeFarmLoot({ payload: { loot_option: farmSettings.loot_option } });
claimedAny = true;
pushState();
break;
}
}
if (readyFarms.length === 0) return;
log(`⚡ Auto-farm: ${readyFarms.length} ready farms found`);
if (allFull) {
log('⚠️ Auto-farm: ALL warehouses are full (>95%) — skipping loot this cycle');
try {
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: true })
});
} catch (e) {}
} else if (claimedAny) {
try {
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) {}
}
// Check if ALL warehouses are already full (>95%) — no point looting
const towns = Object.values(uw.ITowns?.towns || {});
let allFull = true;
for (const town of towns) {
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;
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.
// When eval()'d dynamically the 'load' event has already fired,
// so we check readyState and boot immediately in that case.
// ----------------------------------------------------------------
function boot() {
log('Grepolis Remote Control v4.0.0 (remote) loaded');
log('Grepolis Remote Control v4.2.0 (remote) loaded');
detectCaptcha();
setTimeout(pushState, 5000);
jitterLoop(pushState, 60000, 120000);
jitterLoop(pollAndExecute, 8000, 18000);
jitterLoop(pushState, 60000, 120000); // state sync every 12 min
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') {

42
db.py
View File

@@ -25,6 +25,7 @@ def init_db():
payload TEXT NOT NULL, -- JSON string
status TEXT NOT NULL DEFAULT 'pending', -- pending | executing | done | failed
result_msg TEXT,
position INTEGER, -- manual sort order for build queue (lower = first)
created_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
for _col in [
'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 sea INTEGER',
'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 clan_members ADD COLUMN features TEXT NOT NULL DEFAULT 'farm,admin'",
'ALTER TABLE users ADD COLUMN clan_id INTEGER REFERENCES clans(id)',
@@ -84,6 +120,12 @@ def init_db():
except Exception:
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
c.execute('''
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 db import get_db
import json
from datetime import datetime
from datetime import datetime, timedelta
import os
from flask import make_response
from blueprint_engine import evaluate_blueprints
import logging
_log = logging.getLogger(__name__)
api = Blueprint('api', __name__)
@@ -94,6 +98,13 @@ def receive_state():
datetime.utcnow().isoformat()
))
conn.commit()
try:
evaluate_blueprints(conn)
conn.commit()
except Exception as e:
print("Error evaluating blueprints:", e)
conn.close()
return jsonify({'ok': True, 'towns_updated': len(towns)})
@@ -104,13 +115,30 @@ def receive_state():
# Returns one 'build' AND one 'recruit' command independently,
# so both queues are served in parallel without blocking each other.
# ------------------------------------------------------------------
def _fetch_pending_of_type(c, cmd_type, player_id):
row = c.execute('''
SELECT * FROM commands
WHERE status = 'pending' AND type = ? AND player_id = ?
ORDER BY id ASC
LIMIT 1
''', (cmd_type, player_id)).fetchone()
def _fetch_pending_of_type(c, cmd_type, player_id, world_id):
"""Fetch a single oldest pending command of a given type (recruit, market, etc.)."""
# We use LEFT JOIN because global commands (like farm_loot) use a pseudo town_id like "0_gr121"
# which does not exist in town_state.
global_town_id = f"0_{world_id}" if world_id else "0"
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:
return None
c.execute('''
@@ -125,32 +153,142 @@ def _fetch_pending_of_type(c, cmd_type, player_id):
'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'])
def get_pending_command():
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:
return jsonify({'error': 'no player_id provided'}), 400
conn = get_db()
c = conn.cursor()
build_cmd = _fetch_pending_of_type(c, 'build', player_id)
recruit_cmd = _fetch_pending_of_type(c, 'recruit', player_id)
market_cmd = _fetch_pending_of_type(c, 'market_offer', player_id)
farm_cmd = _fetch_pending_of_type(c, 'farm_loot', player_id)
farm_upgrade_cmd = _fetch_pending_of_type(c, 'farm_upgrade', player_id)
research_cmd = _fetch_pending_of_type(c, 'research', player_id)
# Free up stuck 'executing' commands (e.g. if the game page was refreshed mid-execution)
two_minutes_ago = (datetime.utcnow() - timedelta(minutes=2)).isoformat()
c.execute('''
UPDATE commands
SET status = 'pending', result_msg = 'Requeued (timeout)'
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)
# 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_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()
farm_settings = {
'enabled': bool(farm_row['enabled']) if farm_row else False,
'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
member_row = c.execute(
'SELECT features FROM clan_members WHERE player_id = ?', (str(player_id),)
@@ -164,13 +302,14 @@ def get_pending_command():
conn.close()
return jsonify({
'build': build_cmd,
'builds': build_cmds, # list: one build command per town
'recruit': recruit_cmd,
'market': market_cmd,
'research': research_cmd,
'farm': farm_cmd,
'farm_upgrade': farm_upgrade_cmd,
'farm_settings': farm_settings,
'bot_settings': bot_settings,
'enabled_features': enabled_features,
'sync_requested': sync_req
})
@@ -212,15 +351,32 @@ def sync_request():
@api.route('/api/commands/<int:cmd_id>/result', methods=['POST'])
def command_result(cmd_id):
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', '')
now = datetime.utcnow().isoformat()
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('''
UPDATE commands
SET status = ?, result_msg = ?, updated_at = ?
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.close()
return jsonify({'ok': True})
@@ -284,16 +440,26 @@ def upload_market_data():
@api.route('/api/farm_status', methods=['POST', 'GET'])
def farm_status():
player_id = request.args.get('player_id')
world_id = request.args.get('world_id')
if not player_id:
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()
if request.method == 'POST':
data = request.get_json(silent=True) or {}
now = datetime.utcnow().isoformat()
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
''', (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.close()
return jsonify({'ok': True})
@@ -302,6 +468,49 @@ def farm_status():
conn.close()
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
# 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
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
INNER JOIN clan_members cm ON cm.player_id = ts.player_id AND cm.clan_id = ?
WHERE ts.player IS NOT NULL
GROUP BY ts.player, ts.player_id
ORDER BY ts.player ASC
GROUP BY ts.player, ts.player_id, ts.world_id
ORDER BY ts.player ASC, ts.world_id ASC
''', (clan_id,)).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)
@dashboard.route('/player/<player_id>')
@dashboard.route('/player/<player_id>/<world_id>')
@login_required
def player_hub(player_id):
return render_template('hub.html', player_id=player_id)
def player_hub(player_id, world_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
def player_dashboard(player_id):
return render_template('dashboard.html', player_id=player_id)
def player_dashboard(player_id, world_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
def player_farm(player_id):
return render_template('farm.html', player_id=player_id)
def player_farm(player_id, world_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'])
def get_farm_settings():
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()
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()
conn.close()
if row:
@@ -99,6 +102,9 @@ def set_farm_settings():
if not data or 'player_id' not in data:
return jsonify({'error': 'missing player_id'}), 400
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
loot_option = int(data.get('loot_option', 1))
conn = get_db()
@@ -109,7 +115,7 @@ def set_farm_settings():
enabled = excluded.enabled,
loot_option = excluded.loot_option,
updated_at = excluded.updated_at
''', (player_id, enabled, loot_option, datetime.utcnow().isoformat()))
''', (player_key, enabled, loot_option, datetime.utcnow().isoformat()))
conn.commit()
conn.close()
return jsonify({'ok': True})
@@ -122,10 +128,24 @@ def set_farm_settings():
@dashboard.route('/dashboard/farm-data', methods=['GET'])
def get_farm_data():
player_id = request.args.get('player_id')
world_id = request.args.get('world_id')
conn = get_db()
rows = conn.execute(
'SELECT town_id, town_name, data FROM town_state WHERE player_id = ?', (player_id,)
).fetchall()
if world_id:
rows = conn.execute(
'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()
now_ts = int(datetime.utcnow().timestamp())
@@ -142,7 +162,7 @@ def get_farm_data():
'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)
})
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'])
def get_towns():
player_id = request.args.get('player_id')
world_id = request.args.get('world_id')
conn = get_db()
rows = conn.execute('''
SELECT town_id, town_name, player, player_id, alliance_id,
world_id, x, y, sea, data, updated_at
FROM town_state
WHERE player_id = ?
ORDER BY town_name ASC
''', (player_id, )).fetchall()
query = '''
SELECT ts.town_id, ts.town_name, ts.player, ts.player_id, ts.alliance_id,
ts.world_id, ts.x, ts.y, ts.sea, ts.data, ts.updated_at,
tb.blueprint_name, tb.is_active as blueprint_active
FROM town_state ts
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()
towns = []
@@ -214,11 +246,51 @@ def get_towns():
'bonuses': d.get('bonuses', {}),
'wonder_points': d.get('wonder_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)
# ------------------------------------------------------------------
# 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
# Returns whether the Tampermonkey client is considered online.
@@ -263,6 +335,54 @@ def captcha_status():
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
# Returns command history (last 50) for the log panel.
@@ -271,13 +391,24 @@ def captcha_status():
def get_commands():
player_id = request.args.get('player_id')
conn = get_db()
rows = conn.execute('''
SELECT id, town_id, town_name, type, payload, status, result_msg, created_at, updated_at
FROM commands
WHERE player_id = ?
ORDER BY id DESC
LIMIT 50
''', (player_id, )).fetchall()
world_id = request.args.get('world_id')
conn = get_db()
query = '''
SELECT c.id, c.town_id, c.town_name, c.type, c.payload, c.status, c.result_msg, c.created_at, c.updated_at
FROM commands c
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()
return jsonify([dict(r) for r in rows])
@@ -320,14 +451,28 @@ def create_command():
return jsonify({'error': 'client_offline', 'message': 'Το script είναι offline — δεν μπορείτε να στείλετε εντολές.'}), 503
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('''
INSERT INTO commands (town_id, town_name, type, payload, status, created_at, updated_at, player_id)
VALUES (?, ?, ?, ?, 'pending', ?, ?, ?)
INSERT INTO commands (town_id, town_name, type, payload, status, position, created_at, updated_at, player_id)
VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?)
''', (
str(data['town_id']),
data.get('town_name', ''),
cmd_type,
json.dumps(data['payload']),
position,
datetime.utcnow().isoformat(),
datetime.utcnow().isoformat(),
str(data['player_id'])
@@ -373,3 +518,134 @@ def fail_stale_commands():
conn.commit()
conn.close()
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-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 {
color: #666;
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;
position: fixed;
inset: 0;
@@ -325,9 +329,9 @@ tr:hover td { background: #1e1e40; }
align-items: 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;
border: 2px solid #c8a44a;
border-radius: 10px;
@@ -342,7 +346,7 @@ tr:hover td { background: #1e1e40; }
from { transform: scale(0.92); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
#building-modal-header, #academy-modal-header {
#building-modal-header, #academy-modal-header, .modal-header {
display: flex;
justify-content: space-between;
align-items: center;
@@ -350,12 +354,12 @@ tr:hover td { background: #1e1e40; }
padding-bottom: 10px;
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;
font-size: 1rem;
letter-spacing: 0.5px;
}
#building-modal-close, #academy-modal-close {
#building-modal-close, #academy-modal-close, #unit-modal-close {
background: none;
border: none;
color: #888;
@@ -364,7 +368,7 @@ tr:hover td { background: #1e1e40; }
line-height: 1;
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 {
display: grid;

View File

@@ -4,7 +4,7 @@
window.fetchTowns = async function() {
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.renderTowns();
window.updateServerStatus(true);
@@ -19,6 +19,10 @@ window.fetchTowns = async function() {
window.renderBuildingDropdown();
window.renderUnitDropdown();
window.renderTownDetails();
// Refresh the build queue panel if in queue mode
if (window._logPanelMode === 'queue') {
window.fetchBuildQueue(window.selectedTownId);
}
}
} catch (e) {
window.updateServerStatus(false);
@@ -48,10 +52,12 @@ window.fetchClientStatus = async function() {
window.fetchLog = async function() {
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();
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();
} catch (e) {}
};
@@ -91,8 +97,8 @@ window.sendCommand = async function() {
const town = window.getSelectedTown();
if (!town) return alert('Select a town first.');
const type = document.getElementById('cmd-type').value;
if (!type) return alert('Παρακαλώ επιλέξτε Ενέργεια (Command Type) πρώτα.');
const type = window.currentCmdType;
if (!type) return alert('Παρακαλώ επιλέξτε Ενέργεια (Κατασκευή/Στρατός/Παζάρι/Έρευνα) πρώτα.');
let payload = {};
@@ -126,7 +132,7 @@ window.sendCommand = async function() {
payload = { building_id };
} else if (type === 'recruit') {
const unit_id = document.getElementById('unit-select').value;
const unit_id = window.selectedUnitId;
if (!unit_id) return alert('Παρακαλώ επιλέξτε Μονάδα προς εκπαίδευση.');
const amount = parseInt(document.getElementById('recruit-amount').value) || 1;
@@ -176,6 +182,30 @@ window.sendCommand = async function() {
}
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 {
@@ -192,7 +222,12 @@ window.sendCommand = async function() {
});
const data = await res.json();
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') {
alert(data.message || 'Το script είναι offline.');
} else {

View File

@@ -4,11 +4,15 @@
window.addEventListener('DOMContentLoaded', () => {
window.fetchTowns();
window.fetchLog();
window.fetchLog(); // pre-loads cmds globally even in queue mode
window.fetchClientStatus();
window.fetchCaptchaStatus();
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.fetchCaptchaStatus, 5000); // check every 5s
});

View File

@@ -2,13 +2,106 @@
// Command Form Component
// ================================================================
window.onCmdTypeChange = function() {
const type = document.getElementById('cmd-type').value;
document.getElementById('build-options').style.display = type === 'build' ? '' : 'none';
document.getElementById('recruit-options').style.display = type === 'recruit' ? '' : 'none';
document.getElementById('amount-group').style.display = type === 'recruit' ? '' : 'none';
document.getElementById('market-options').style.display = type === 'market_offer' ? '' : 'none';
document.getElementById('research-options').style.display = type === 'research' ? '' : 'none';
window.currentCmdType = null;
window.setCmdType = function(type, openModal = false) {
window.currentCmdType = type;
// Update segmented control active state
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
@@ -118,11 +211,8 @@ window.closeBuildingModal = function(e) {
window.selectBuilding = function(key, nameGr) {
window.selectedBuildingId = key;
// Update the trigger button label
document.getElementById('selected-building-label').textContent = `🏗️ ${nameGr}`;
// Re-render grid to show new selection highlight
window.updateSelectionDisplay();
window.openBuildingModal();
// Close after brief visual feedback
setTimeout(() => document.getElementById('building-modal-overlay').classList.remove('open'), 180);
};
@@ -233,77 +323,140 @@ window.closeAcademyModal = function(e) {
window.selectResearch = function(key, name) {
window.selectedResearchId = key;
document.getElementById('selected-research-label').textContent = `🧪 ${name}`;
window.updateSelectionDisplay();
window.openAcademyModal();
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() {
// No-op - selection now happens via modal
};
window.openUnitModal = function() {
const town = window.getSelectedTown();
if (!town) return;
const uSelect = document.getElementById('unit-select');
const uData = town.unit_data || {};
const grid = document.getElementById('unit-grid');
const uData = town.unit_data || {};
const currentVal = uSelect.value;
uSelect.innerHTML = '<option value="" disabled selected>-- Επιλέξτε Μονάδα --</option>';
// Group units into categories for better display
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)) {
if (key === 'militia') continue;
let html = '';
const data = uData[key];
let text = `${nameGr}`;
for (const [catName, units] of Object.entries(categories)) {
let catHtml = '';
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;
for (const key of units) {
if (key === 'militia') continue;
// Unit build_time is usually raw seconds in GameData
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`;
}
const nameGr = window.UNIT_NAMES_GR[key] || key;
const data = uData[key];
const icon = window.UNIT_ICONS[key] || '💂';
const isSelected = key === window.selectedUnitId;
const costStr = `Ξ:${w} Π:${st} Α:${i} 🧔:${pop} · ⏱ ${tStr}`;
let statusClass, statusLabel, cardClass = '';
let costStr = '';
let clickable = false;
const missingKeys = data.missing_dependencies ? Object.keys(data.missing_dependencies) : [];
const isLocked = missingKeys.length > 0;
const requiredGod = window.UNIT_GODS ? window.UNIT_GODS[key] : null;
const isWrongGod = requiredGod && town.god !== requiredGod;
const isNoGod = key === 'godsent' && !town.god;
const option = document.createElement('option');
option.value = key;
if (isWrongGod) {
statusClass = 'locked'; statusLabel = '🔒 Άλλος Θεός'; cardClass = 'bld-locked';
const greekGods = { zeus: 'Δία', poseidon: 'Ποσειδώνα', hera: 'Ήρα', athena: 'Αθηνά', hades: 'Άδη', artemis: 'Άρτεμις', aphrodite: 'Αφροδίτη', ares: 'Άρη' };
costStr = `Απαιτεί: ${greekGods[requiredGod] || requiredGod}`;
} else if (isNoGod) {
statusClass = 'locked'; statusLabel = '🔒 Χωρίς Θεό'; cardClass = 'bld-locked';
costStr = `Απαιτείται Ναός`;
} else if (data) {
const missingKeys = data.missing_dependencies ? Object.keys(data.missing_dependencies) : [];
const isLocked = missingKeys.length > 0;
if (isLocked) {
option.textContent = `${text} — 🔒 Κλειδωμένο`;
option.style.color = '#ff4444';
} else if (data.enough_resources === false) {
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 {
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);
} else {
const option = document.createElement('option');
option.value = key;
option.textContent = text;
uSelect.appendChild(option);
const onclick = clickable ? `onclick="window.selectUnit('${key}', '${nameGr}')"` : '';
catHtml += `<div class="bld-card ${cardClass}${isSelected ? ' bld-selected' : ''}" ${onclick} style="width:140px; justify-content:flex-start;">
<span class="bld-icon">${icon}</span>
<span class="bld-name" style="margin-top:6px; font-size:0.85rem;">${nameGr}</span>
<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)) {
uSelect.value = currentVal;
}
grid.innerHTML = html;
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() {
const town = window.getSelectedTown();
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) {
if (window._logPanelMode !== 'log') return;
const el = document.getElementById('log-content');
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;
}
@@ -20,10 +180,11 @@ window.renderLog = function(cmds) {
desc = `Recruit: ${p.amount}x ${nameGr}`;
} else if (cmd.type === 'market_offer') {
desc = `Market: ${p.offer} ${p.offer_type}${p.demand} ${p.demand_type}`;
} else {
desc = cmd.type;
}
const statusClass = `status-${cmd.status}`;
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 });
return `<tr>

View File

@@ -12,8 +12,7 @@ window.renderTowns = function() {
// Get active filters
const searchTerm = (document.getElementById('town-search')?.value || '').toLowerCase();
const reqFullWh = document.getElementById('filter-full-wh')?.checked;
const reqFestival = document.getElementById('filter-festival')?.checked;
const reqPoints = document.getElementById('filter-points')?.checked;
const reqNotBuilding = document.getElementById('filter-not-building')?.checked;
const filteredTowns = window.towns.filter(t => {
// 1. Search by name
@@ -29,13 +28,8 @@ window.renderTowns = function() {
// 2. Full WH (>95%)
if (reqFullWh && Math.max(wPct, sPct, iPct) < 0.95) return false;
// 3. Festival (Wood>15k, Stone>18k, Iron>15k)
// City Festival exact costs = 15000, 18000, 15000
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;
// 3. Not Building (no items in build_queue)
if (reqNotBuilding && t.build_queue && t.build_queue.length > 0) return false;
return true;
});
@@ -62,9 +56,6 @@ window.renderTowns = function() {
if (wPct >= 0.95 || sPct >= 0.95 || iPct >= 0.95) {
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;' : '';
@@ -99,6 +90,10 @@ window.selectTown = function(id) {
window.renderBuildingDropdown();
window.renderUnitDropdown();
window.renderTownDetails();
// Refresh build queue panel for the newly selected town
if (window._logPanelMode === 'queue') {
window.fetchBuildQueue(id);
}
};
window.getSelectedTown = function() {
@@ -217,4 +212,43 @@ window.renderTownDetails = function() {
if(unitsHtml === '') unitsHtml = '<div style="color:#666">Κανένα στράτευμα</div>';
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 = {
sword: "Ξιφομάχος", slinger: "Σφενδονήτης", archer: "Τοξότης", hoplite: "Οπλίτης",
rider: "Ιππέας", chariot: "Άρμα", catapult: "Καταπέλτης",
sword: "Ξιφομάχος", slinger: "Εκσφενδονιστής", archer: "Τοξότης", hoplite: "Οπλίτης",
rider: "Ιππέας", chariot: "Άρμα", catapult: "Καταπέλτης", godsent: "Θεόσταλτος",
big_transporter: "Μεταφορικό", small_transporter: "Γρήγ. Μεταφορικό", bireme: "Διήρης",
attack_ship: "Πλοίο Φάρος", trireme: "Τριήρης", colonize_ship: "Αποικιακό",
medusa: "Μέδουσα", zyklop: "Κύκλωπας", harpy: "Άρπυια", pegasus: "Πήγασος",
@@ -33,6 +33,17 @@ window.UNIT_NAMES_GR = {
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 = {
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>',

View File

@@ -10,7 +10,8 @@
<header>
<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="client-status" class="conn-badge">Client…</div>
</div>
@@ -29,7 +30,6 @@
<div id="town-panel">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<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 id="town-filters" style="margin-bottom: 15px; padding: 10px; background: #0f3460; border-radius: 6px; border: 1px solid #2a4a6a;">
@@ -41,11 +41,7 @@
</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-festival" 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+ Πόντοι
<input type="checkbox" id="filter-not-building" onchange="window.renderTowns()"> 🏗️ Δεν χτίζει
</label>
</div>
</div>
@@ -87,81 +83,92 @@
</div>
<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">
<label>Command Type</label>
<select id="cmd-type" onchange="onCmdTypeChange()">
<option value="" disabled selected>-- Επιλέξτε --</option>
<option value="build">Build / Upgrade</option>
<option value="recruit">Recruit Troops</option>
<option value="market_offer">Παζάρι - Προσφορά</option>
<option value="research">Ακαδημία - Έρευνες</option>
</select>
<!-- Segmented Control Row -->
<div class="segmented-control" id="cmd-type-buttons" style="display: flex; gap: 5px; background: #16213e; padding: 4px; border-radius: 8px; border: 1px solid #2a4a6a;">
<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>
<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>
<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>
<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>
<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>
</div>
<!-- Build options - now a button that opens the visual picker -->
<div class="form-group" id="build-options" style="display:none">
<label>Building</label>
<button class="btn btn-gold" id="open-building-modal" onclick="window.openBuildingModal()" style="text-align:left; min-width:200px;">
<span id="selected-building-label">-- Επιλέξτε Κατασκευή --</span>
</button>
<!-- Dynamic Selection Area -->
<div id="selection-area" style="display:none; align-items: center; gap: 10px; flex-wrap: wrap;">
<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;">
<span id="selection-label">-- Επιλέξτε --</span>
</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>
<!-- Research options -->
<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>
</div>
<!-- Recruit options -->
<div class="form-group" id="recruit-options" style="display:none">
<label>Unit</label>
<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>
<div id="build-queue-preview"></div>
</div>
</div>
<!-- Market options -->
<!-- Market options -->
<div class="form-group" id="market-options" style="display:none">
<!-- Bottom right: Build Queue / Command Log (tabbed) -->
<div id="log-panel">
<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="flex:1;">
<label>Προσφορά</label>
<input type="number" id="market-offer-amount" value="1000" min="1" max="99999" style="width:100%;">
<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%; background:#0f3460; color:#fff; border:1px solid #2a4a6a; border-radius:4px; padding:6px;">
</div>
<div style="flex:1;">
<label>Πόρος Προσφοράς</label>
<select id="market-offer-type">
<label style="color:#ccc; font-size:0.85rem;">Πόρος Προσφοράς</label>
<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="stone">Πέτρα</option>
<option value="iron">Ασήμι</option>
@@ -170,22 +177,22 @@
</div>
<div style="display:flex; gap:10px; margin-bottom:10px;">
<div style="flex:1;">
<label>Ζήτηση</label>
<input type="number" id="market-demand-amount" value="1000" min="1" max="99999" style="width:100%;">
<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%; background:#0f3460; color:#fff; border:1px solid #2a4a6a; border-radius:4px; padding:6px;">
</div>
<div style="flex:1;">
<label>Πόρος Ζήτησης</label>
<select id="market-demand-type">
<label style="color:#ccc; font-size:0.85rem;">Πόρος Ζήτησης</label>
<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="stone">Πέτρα</option>
<option value="wood">Ξύλο</option>
</select>
</div>
</div>
<div style="display:flex; gap:10px;">
<div style="display:flex; gap:10px; margin-bottom:20px;">
<div style="flex:1;">
<label>Χρόνος (Ώρες)</label>
<select id="market-max-time">
<label style="color:#ccc; font-size:0.85rem;">Χρόνος (Ώρες)</label>
<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="3600">1</option>
<option value="7200">2</option>
@@ -197,36 +204,16 @@
</select>
</div>
<div style="flex:1;">
<label>Ορατότητα</label>
<select id="market-visibility">
<label style="color:#ccc; font-size:0.85rem;">Ορατότητα</label>
<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="all">Όλοι</option>
</select>
</div>
</div>
</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>
<button class="btn btn-gold" onclick="window.saveMarketModal()" style="width:100%; padding:10px; font-weight:bold;">✅ Αποθήκευση Προσφοράς</button>
</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>
<!-- ====== Building Picker Modal ====== -->
@@ -243,6 +230,17 @@
</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 ====== -->
<div id="academy-modal-overlay" onclick="window.closeAcademyModal(event)">
<div id="academy-modal">
@@ -254,14 +252,79 @@
</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>
window.PLAYER_ID = "{{ player_id }}";
window.WORLD_ID = "{{ world_id }}";
</script>
<script src="/static/js/state.js"></script>
<script src="/static/js/components/townViewer.js"></script>
<script src="/static/js/components/commandForm.js"></script>
<script src="/static/js/components/commandLog.js"></script>
<script src="/static/js/api.js"></script>
<script src="/static/js/app.js"></script>
<script src="/static/js/state.js?v=6"></script>
<script src="/static/js/components/townViewer.js?v=6"></script>
<script src="/static/js/components/commandForm.js?v=6"></script>
<script src="/static/js/components/commandLog.js?v=6"></script>
<script src="/static/js/api.js?v=6"></script>
<script src="/static/js/app.js?v=6"></script>
</body>
</html>

View File

@@ -233,7 +233,7 @@
<body>
<div class="topbar">
<a href="/player/{{ player_id }}">← Πίσω</a>
<a href="/player/{{ player_id }}/{{ world_id }}">← Πίσω</a>
<h1>🌾 Farm Manager</h1>
<span class="online-dot" id="online-dot" title="Κατάσταση Script"></span>
<button class="sync-btn" onclick="requestSync()">Live Sync</button>
@@ -243,7 +243,7 @@
<div class="status-bar" id="status-bar"></div>
<!-- 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>
<strong style="color:#ff9933;">Αποθήκη Γεμάτη!</strong>
@@ -313,6 +313,80 @@
</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 -->
<div class="panel">
<h2>🏘️ Κατάσταση Χωριών</h2>
@@ -324,17 +398,20 @@
<th>Έτοιμα</th>
<th>Σύνολο</th>
<th>Επόμενο</th>
<th>Τελευταία Λεηλασία</th>
</tr>
</thead>
<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>
</table>
</div>
</div>
<script>
const PLAYER_ID = '{{ player_id }}';
const WORLD_ID = '{{ world_id }}';
let selectedOption = 1;
// -- Loot option buttons --
@@ -369,7 +446,7 @@
fetch('/dashboard/farm-settings', {
method: 'POST',
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(() => {
@@ -381,7 +458,7 @@
// -- Load current settings --
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(cfg => {
document.getElementById('farm-enabled').checked = cfg.enabled;
@@ -395,17 +472,27 @@
}
// -- 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() {
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(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');
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;
}
const now = Math.floor(Date.now() / 1000);
tbody.innerHTML = data.map(t => {
tbody.innerHTML = data.map((t, idx) => {
const readyBadge = t.ready_farms > 0
? `<span class="badge ready">✓ ${t.ready_farms} Έτοιμα</span>`
: `<span class="badge waiting">Αναμονή</span>`;
@@ -420,11 +507,16 @@
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>
<td><strong>${t.town_name}</strong></td>
<td>${readyBadge}</td>
<td><span style="color:#888">${t.total_farms}</span></td>
<td>${nextStr}</td>
${lastFarmedCell}
</tr>`;
}).join('');
});
@@ -472,7 +564,7 @@
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
player_id: PLAYER_ID,
town_id: 0,
town_id: WORLD_ID ? `0_${WORLD_ID}` : "0",
type: 'farm_upgrade',
payload: { threshold: threshold, action_type: actionType }
})
@@ -499,7 +591,7 @@
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
player_id: PLAYER_ID,
town_id: 0,
town_id: WORLD_ID ? `0_${WORLD_ID}` : "0",
type: 'farm_loot',
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() {
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 banner = document.getElementById('warehouse-full-banner');
if (banner) banner.style.display = data.warehouse_full ? 'flex' : 'none';
} 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 --
loadSettings();
loadBotSettings();
loadFarmData();
loadBotLogs();
checkOnline();
checkWarehouseStatus();
setInterval(loadFarmData, 15000);
setInterval(checkOnline, 20000);
setInterval(loadFarmData, 15000);
setInterval(loadBotLogs, 15000);
setInterval(checkOnline, 20000);
setInterval(checkWarehouseStatus, 20000);
</script>
</body>
</html>

View File

@@ -154,13 +154,13 @@
<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>
<div class="card-title">Admin Mode</div>
<div class="card-desc">Κτίρια, στρατολόγηση, αγορά, ουρά κατασκευών και πλήρης έλεγχος πόλεων.</div>
</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>
<div class="card-title">Farm Manager</div>
<div class="card-desc">Αυτόματη συλλογή πόρων από χωριά. Ρυθμίσεις χρόνου λεηλασίας και έλεγχος με ένα κλικ.</div>
@@ -180,7 +180,8 @@
<script>
// Fetch player name to show in the badge
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(towns => {
if (towns && towns.length > 0) {

View File

@@ -123,7 +123,7 @@
{% endif %}
{% 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>
<strong>{{ p.player }}</strong>