Compare commits

...

30 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
17 changed files with 970 additions and 332 deletions

View File

@@ -822,11 +822,12 @@
async function pollAndExecute() { async function pollAndExecute() {
if (paused) return; if (paused) return;
const player_id = uw.Game?.player_id; const player_id = uw.Game?.player_id;
const world_id = uw.Game?.world_id;
if (!player_id) return; if (!player_id) return;
let cmdData; let cmdData;
try { try {
const res = await fetch(`${BASE_URL}/api/commands/pending?player_id=${player_id}`); const res = await fetch(`${BASE_URL}/api/commands/pending?player_id=${player_id}&world_id=${world_id}`);
cmdData = await res.json(); cmdData = await res.json();
} catch (e) { } catch (e) {
log(`Poll failed: ${e}`); log(`Poll failed: ${e}`);

6
app.py
View File

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

221
blueprint_engine.py Normal file
View File

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

View File

@@ -17,13 +17,13 @@ let lastKnownBotSettings = {};
// ---------------------------------------------------------------- // ----------------------------------------------------------------
// botLog — sends a log entry to the server // botLog — sends a log entry to the server
// ---------------------------------------------------------------- // ----------------------------------------------------------------
async function botLog(player_id, feature, message) { async function botLog(player_id, world_id, feature, message) {
log(`[${feature}] ${message}`); log(`[${feature}] ${message}`);
try { try {
await apiFetch(`${BASE_URL}/api/bot-logs`, { await apiFetch(`${BASE_URL}/api/bot-logs`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ player_id, feature, message }) body: JSON.stringify({ player_id, world_id, feature, message })
}); });
} catch (e) { /* non-critical */ } } catch (e) { /* non-critical */ }
} }
@@ -38,7 +38,8 @@ async function autoBootcampLoop() {
if (!settings.bootcamp_enabled) return; if (!settings.bootcamp_enabled) return;
const player_id = uw.Game?.player_id; const player_id = uw.Game?.player_id;
if (!player_id) return; const world_id = uw.Game?.world_id;
if (!player_id || !world_id) return;
let model; let model;
try { try {
@@ -61,27 +62,30 @@ async function autoBootcampLoop() {
const isFavor = reward.power_id?.includes('favor'); const isFavor = reward.power_id?.includes('favor');
const stashable = reward.stashable; const stashable = reward.stashable;
if (isInstant && !isFavor) { const useReward = () => {
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', { uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
model_url: `PlayerAttackSpot/${player_id}`, model_url: `PlayerAttackSpot/${player_id}`,
action_name: 'useReward', action_name: 'useReward',
arguments: {} arguments: {}
}); });
await botLog(player_id, 'bootcamp', `Reward used: ${reward.power_id}`); botLog(player_id, world_id, 'bootcamp', `Reward used: ${reward.power_id}`);
};
if (isInstant && !isFavor) {
useReward();
} else if (stashable) { } else if (stashable) {
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', { uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
model_url: `PlayerAttackSpot/${player_id}`, model_url: `PlayerAttackSpot/${player_id}`,
action_name: 'stashReward', action_name: 'stashReward',
arguments: {} arguments: {}
}, 0, {
success: () => {
botLog(player_id, world_id, 'bootcamp', `Reward stashed: ${reward.power_id}`);
},
error: useReward
}); });
await botLog(player_id, 'bootcamp', `Reward stashed: ${reward.power_id}`);
} else { } else {
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', { useReward();
model_url: `PlayerAttackSpot/${player_id}`,
action_name: 'useReward',
arguments: {}
});
await botLog(player_id, 'bootcamp', `Reward used (fallback): ${reward.power_id}`);
} }
await sleep(randInt(3000, 7000)); await sleep(randInt(3000, 7000));
return; // Wait for next cycle to attack return; // Wait for next cycle to attack
@@ -99,7 +103,7 @@ async function autoBootcampLoop() {
const cooldown = model.getCooldownDuration(); const cooldown = model.getCooldownDuration();
if (cooldown > 0) { if (cooldown > 0) {
const minRemaining = Math.round(cooldown / 60); const minRemaining = Math.round(cooldown / 60);
await botLog(player_id, 'bootcamp', `Camp on cooldown — ${minRemaining} min remaining`); await botLog(player_id, world_id, 'bootcamp', `Camp on cooldown — ${minRemaining} min remaining`);
return; return;
} }
@@ -108,7 +112,7 @@ async function autoBootcampLoop() {
if (movements) { if (movements) {
for (const mv of Object.values(movements)) { for (const mv of Object.values(movements)) {
if (mv.attributes.destination_is_attack_spot || mv.attributes.origin_is_attack_spot) { if (mv.attributes.destination_is_attack_spot || mv.attributes.origin_is_attack_spot) {
await botLog(player_id, 'bootcamp', 'Attack already in flight — skipping'); await botLog(player_id, world_id, 'bootcamp', 'Attack already in flight — skipping');
return; return;
} }
} }
@@ -141,7 +145,7 @@ async function autoBootcampLoop() {
} }
if (Object.keys(units).length === 0) { if (Object.keys(units).length === 0) {
await botLog(player_id, 'bootcamp', 'No available units — skipping attack'); await botLog(player_id, world_id, 'bootcamp', 'No available units — skipping attack. (Χωρίς αμυντικά)');
return; return;
} }
@@ -152,10 +156,10 @@ async function autoBootcampLoop() {
}); });
const unitSummary = Object.entries(units).map(([u, n]) => `${n}x${u}`).join(', '); const unitSummary = Object.entries(units).map(([u, n]) => `${n}x${u}`).join(', ');
await botLog(player_id, 'bootcamp', `Attack sent — ${unitSummary}`); await botLog(player_id, world_id, 'bootcamp', `Στέλνω ${JSON.stringify(units)} στο camp...`);
} catch (e) { } catch (e) {
await botLog(player_id, 'bootcamp', `Error during attack: ${e}`); await botLog(player_id, world_id, 'bootcamp', `Error during attack: ${e}`);
} }
} }
@@ -176,7 +180,8 @@ async function autoRuralTradeLoop() {
if (!settings.rural_trade_enabled) return; if (!settings.rural_trade_enabled) return;
const player_id = uw.Game?.player_id; const player_id = uw.Game?.player_id;
if (!player_id) return; const world_id = uw.Game?.world_id;
if (!player_id || !world_id) return;
const minRatio = RATIO_MAP[settings.rural_trade_ratio] ?? 0.75; const minRatio = RATIO_MAP[settings.rural_trade_ratio] ?? 0.75;
@@ -274,12 +279,12 @@ async function autoRuralTradeLoop() {
arguments: { farm_town_id: farm.attributes.id, amount }, arguments: { farm_town_id: farm.attributes.id, amount },
town_id: parseInt(town_id_str) town_id: parseInt(town_id_str)
}); });
await botLog(player_id, 'rural_trade', await botLog(player_id, world_id, 'rural_trade',
`Traded ${amount} ${missingResource}${farm.attributes.name} via ${town_obj.getName?.() ?? town_id_str}`); `Traded ${amount} ${missingResource}${farm.attributes.name} via ${town_obj.getName?.() ?? town_id_str}`);
tradesTotal++; tradesTotal++;
tradeMade = true; tradeMade = true;
} catch (e) { } catch (e) {
await botLog(player_id, 'rural_trade', `Trade error: ${e}`); await botLog(player_id, world_id, 'rural_trade', `Trade error: ${e}`);
} }
await sleep(randInt(800, 1800)); await sleep(randInt(800, 1800));
@@ -289,7 +294,7 @@ async function autoRuralTradeLoop() {
} }
if (!tradeMade && missingResource) { if (!tradeMade && missingResource) {
await botLog(player_id, 'rural_trade', await botLog(player_id, world_id, 'rural_trade',
`${town_obj.getName?.() ?? town_id_str} needs ${missingResource} but no suitable village found`); `${town_obj.getName?.() ?? town_id_str} needs ${missingResource} but no suitable village found`);
} }

View File

@@ -17,15 +17,18 @@ const LOOT_TIMINGS = { 1: 300000, 2: 1200000, 3: 5400000, 4: 14400000 };
// Delay = loot_option cooldown + random 30-120s human jitter. // Delay = loot_option cooldown + random 30-120s human jitter.
// This mirrors ModernBot's pattern: run exactly when farms are ready. // This mirrors ModernBot's pattern: run exactly when farms are ready.
// ---------------------------------------------------------------- // ----------------------------------------------------------------
function scheduleNextFarm() { 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 option = lastKnownFarmSettings.loot_option || 1;
const baseMs = LOOT_TIMINGS[option] || 300000; const baseMs = LOOT_TIMINGS[option] || 300000;
const jitterMs = randInt(30000, 120000); // +30 to +120 s const jitterMs = randInt(30000, 120000); // +30 to +120 s
const totalMs = baseMs + jitterMs; totalMs = baseMs + jitterMs;
log(`⏰ Next auto-farm in ${(totalMs / 60000).toFixed(1)} min (option ${option} + ${(jitterMs/1000).toFixed(0)}s jitter)`); }
log(`⏰ Next auto-farm in ${(totalMs / 60000).toFixed(1)} min`);
setTimeout(async () => { setTimeout(async () => {
await autoFarmLoop(); await autoFarmLoop();
scheduleNextFarm(); scheduleNextFarm(false);
}, totalMs); }, totalMs);
} }
@@ -37,11 +40,12 @@ function scheduleNextFarm() {
async function pollAndExecute() { async function pollAndExecute() {
if (paused) return; if (paused) return;
const player_id = uw.Game?.player_id; const player_id = uw.Game?.player_id;
const world_id = uw.Game?.world_id;
if (!player_id) return; if (!player_id) return;
let cmdData; let cmdData;
try { try {
const res = await apiFetch(`${BASE_URL}/api/commands/pending?player_id=${player_id}`); const res = await apiFetch(`${BASE_URL}/api/commands/pending?player_id=${player_id}&world_id=${world_id}`);
cmdData = await res.json(); cmdData = await res.json();
} catch (e) { } catch (e) {
log(`Poll failed: ${e}`); log(`Poll failed: ${e}`);
@@ -184,7 +188,8 @@ async function autoFarmLoop() {
if (allFull) { if (allFull) {
log('⚠️ Auto-farm: ALL warehouses are full (>95%) — skipping loot this cycle'); log('⚠️ Auto-farm: ALL warehouses are full (>95%) — skipping loot this cycle');
try { try {
await apiFetch(`${BASE_URL}/api/farm_status?player_id=${player_id}`, { const world_id = uw.Game?.world_id || '';
await apiFetch(`${BASE_URL}/api/farm_status?player_id=${player_id}&world_id=${world_id}`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ warehouse_full: true }) body: JSON.stringify({ warehouse_full: true })
@@ -199,7 +204,8 @@ async function autoFarmLoop() {
await executeFarmLoot({ payload: { loot_option: farmSettings.loot_option } }); await executeFarmLoot({ payload: { loot_option: farmSettings.loot_option } });
// Report success so dashboard shows last_farmed_at // Report success so dashboard shows last_farmed_at
try { try {
await apiFetch(`${BASE_URL}/api/farm_status?player_id=${player_id}`, { const world_id = uw.Game?.world_id || '';
await apiFetch(`${BASE_URL}/api/farm_status?player_id=${player_id}&world_id=${world_id}`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ warehouse_full: false }) body: JSON.stringify({ warehouse_full: false })
@@ -222,8 +228,8 @@ function boot() {
detectCaptcha(); detectCaptcha();
setTimeout(pushState, 5000); setTimeout(pushState, 5000);
jitterLoop(pushState, 60000, 120000); // state sync every 12 min jitterLoop(pushState, 60000, 120000); // state sync every 12 min
jitterLoop(pollAndExecute, 8000, 18000); // command poll every 818 s jitterLoop(pollAndExecute, 8000, 18000, 2000); // command poll every 818 s, but start in 2s
scheduleNextFarm(); // auto-farm timer-based (loot_option + 30120s) scheduleNextFarm(true); // auto-farm timer-based (loot_option + 30120s)
jitterLoop(autoBootcampLoop, 720000, 1320000); // bootcamp every 1222 min jitterLoop(autoBootcampLoop, 720000, 1320000); // bootcamp every 1222 min
jitterLoop(autoRuralTradeLoop, 1500000, 2700000); // rural trade every 2545 min jitterLoop(autoRuralTradeLoop, 1500000, 2700000); // rural trade every 2545 min
} }

10
db.py
View File

@@ -92,6 +92,16 @@ def init_db():
''') ''')
c.execute('CREATE INDEX IF NOT EXISTS idx_bot_logs_player_feature ON bot_logs(player_id, feature)') c.execute('CREATE INDEX IF NOT EXISTS idx_bot_logs_player_feature ON bot_logs(player_id, feature)')
# Blueprints - assigns a blueprint to a specific town
c.execute('''
CREATE TABLE IF NOT EXISTS town_blueprints (
town_id TEXT PRIMARY KEY,
blueprint_name TEXT NOT NULL,
is_active INTEGER NOT NULL DEFAULT 1,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
)
''')
# Migration: add new columns if upgrading an existing database # Migration: add new columns if upgrading an existing database
for _col in [ for _col in [
'ALTER TABLE town_state ADD COLUMN player_id TEXT', 'ALTER TABLE town_state ADD COLUMN player_id TEXT',

View File

@@ -1,9 +1,13 @@
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
from db import get_db from db import get_db
import json import json
from datetime import datetime from datetime import datetime, timedelta
import os import os
from flask import make_response from flask import make_response
from blueprint_engine import evaluate_blueprints
import logging
_log = logging.getLogger(__name__)
api = Blueprint('api', __name__) api = Blueprint('api', __name__)
@@ -94,6 +98,13 @@ def receive_state():
datetime.utcnow().isoformat() datetime.utcnow().isoformat()
)) ))
conn.commit() conn.commit()
try:
evaluate_blueprints(conn)
conn.commit()
except Exception as e:
print("Error evaluating blueprints:", e)
conn.close() conn.close()
return jsonify({'ok': True, 'towns_updated': len(towns)}) return jsonify({'ok': True, 'towns_updated': len(towns)})
@@ -104,14 +115,30 @@ def receive_state():
# Returns one 'build' AND one 'recruit' command independently, # Returns one 'build' AND one 'recruit' command independently,
# so both queues are served in parallel without blocking each other. # so both queues are served in parallel without blocking each other.
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def _fetch_pending_of_type(c, cmd_type, player_id): def _fetch_pending_of_type(c, cmd_type, player_id, world_id):
"""Fetch a single oldest pending command of a given type (recruit, market, etc.).""" """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(''' row = c.execute('''
SELECT * FROM commands SELECT * FROM commands
WHERE status = 'pending' AND type = ? AND player_id = ? WHERE status = 'pending' AND type = ? AND player_id = ?
ORDER BY updated_at ASC, id ASC ORDER BY updated_at ASC, id ASC
LIMIT 1 LIMIT 1
''', (cmd_type, player_id)).fetchone() ''', (cmd_type, player_id)).fetchone()
if not row: if not row:
return None return None
c.execute(''' c.execute('''
@@ -127,15 +154,43 @@ def _fetch_pending_of_type(c, cmd_type, player_id):
} }
def _fetch_pending_builds_all_towns(c, player_id): def _fetch_pending_builds_all_towns(c, player_id, world_id):
""" """
Fetch ONE pending 'build' command per distinct town_id. Fetch ONE pending 'build' command per distinct town_id.
This allows all towns to build in parallel within a single poll cycle. 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 Within each town the oldest-updated command is picked first, so requeued
commands (updated_at = now) naturally sort behind fresh ones. 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 # Get every town that has at least one pending build, ordered by
# which town has been waiting longest (MIN updated_at across its commands). # 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(''' town_rows = c.execute('''
SELECT town_id SELECT town_id
FROM commands FROM commands
@@ -143,11 +198,17 @@ def _fetch_pending_builds_all_towns(c, player_id):
GROUP BY town_id GROUP BY town_id
ORDER BY MIN(updated_at) ASC ORDER BY MIN(updated_at) ASC
''', (player_id,)).fetchall() ''', (player_id,)).fetchall()
_log.warning(f"[poll] build towns found: {[r['town_id'] for r in town_rows]}, busy: {busy_towns}")
results = [] results = []
now = datetime.utcnow().isoformat() now = datetime.utcnow().isoformat()
for town_row in town_rows: for town_row in town_rows:
town_id = town_row['town_id'] 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(''' row = c.execute('''
SELECT * FROM commands SELECT * FROM commands
WHERE status = 'pending' AND type = 'build' WHERE status = 'pending' AND type = 'build'
@@ -172,23 +233,36 @@ def _fetch_pending_builds_all_towns(c, player_id):
@api.route('/api/commands/pending', methods=['GET']) @api.route('/api/commands/pending', methods=['GET'])
def get_pending_command(): def get_pending_command():
player_id = request.args.get('player_id') player_id = request.args.get('player_id')
world_id = request.args.get('world_id')
_log.warning(f"[poll] player_id={player_id!r} world_id={world_id!r}")
if not player_id: if not player_id:
return jsonify({'error': 'no player_id provided'}), 400 return jsonify({'error': 'no player_id provided'}), 400
conn = get_db() conn = get_db()
c = conn.cursor() c = conn.cursor()
build_cmds = _fetch_pending_builds_all_towns(c, player_id) # one per town # Free up stuck 'executing' commands (e.g. if the game page was refreshed mid-execution)
recruit_cmd = _fetch_pending_of_type(c, 'recruit', player_id) two_minutes_ago = (datetime.utcnow() - timedelta(minutes=2)).isoformat()
market_cmd = _fetch_pending_of_type(c, 'market_offer', player_id) c.execute('''
farm_cmd = _fetch_pending_of_type(c, 'farm_loot', player_id) UPDATE commands
farm_upgrade_cmd = _fetch_pending_of_type(c, 'farm_upgrade', player_id) SET status = 'pending', result_msg = 'Requeued (timeout)'
research_cmd = _fetch_pending_of_type(c, 'research', player_id) WHERE status = 'executing' AND updated_at < ? AND player_id = ?
''', (two_minutes_ago, player_id))
build_cmds = _fetch_pending_builds_all_towns(c, player_id, world_id) # one per town
recruit_cmd = _fetch_pending_of_type(c, 'recruit', player_id, world_id)
market_cmd = _fetch_pending_of_type(c, 'market_offer', player_id, world_id)
farm_cmd = _fetch_pending_of_type(c, 'farm_loot', player_id, world_id)
farm_upgrade_cmd = _fetch_pending_of_type(c, 'farm_upgrade', player_id, world_id)
research_cmd = _fetch_pending_of_type(c, 'research', player_id, world_id)
sync_req = _check_and_reset_sync(c, player_id) sync_req = _check_and_reset_sync(c, player_id)
# Determine player_key for world-specific settings if world_id is provided
player_key = f"{player_id}_{world_id}" if world_id else player_id
# Farm settings # Farm settings
farm_row = c.execute( farm_row = c.execute(
'SELECT enabled, loot_option FROM farm_settings WHERE player_id = ?', (player_id,) 'SELECT enabled, loot_option FROM farm_settings WHERE player_id = ?', (player_key,)
).fetchone() ).fetchone()
farm_settings = { farm_settings = {
'enabled': bool(farm_row['enabled']) if farm_row else False, 'enabled': bool(farm_row['enabled']) if farm_row else False,
@@ -197,7 +271,7 @@ def get_pending_command():
# Bot settings (bootcamp + rural trade) # Bot settings (bootcamp + rural trade)
bot_row = c.execute( bot_row = c.execute(
'SELECT * FROM bot_settings WHERE player_id = ?', (str(player_id),) 'SELECT * FROM bot_settings WHERE player_id = ?', (player_key,)
).fetchone() ).fetchone()
bot_settings = { bot_settings = {
'bootcamp_enabled': bool(bot_row['bootcamp_enabled']) if bot_row else False, 'bootcamp_enabled': bool(bot_row['bootcamp_enabled']) if bot_row else False,
@@ -207,7 +281,7 @@ def get_pending_command():
} }
# One-shot manual attack flag # One-shot manual attack flag
attack_now_key = f'bootcamp_attack_now_{player_id}' attack_now_key = f'bootcamp_attack_now_{player_key}'
flag_row = c.execute('SELECT value FROM kv_store WHERE key = ?', (attack_now_key,)).fetchone() flag_row = c.execute('SELECT value FROM kv_store WHERE key = ?', (attack_now_key,)).fetchone()
if flag_row and flag_row['value'] == '1': if flag_row and flag_row['value'] == '1':
bot_settings['attack_now'] = True bot_settings['attack_now'] = True
@@ -295,10 +369,13 @@ def command_result(cmd_id):
# When an explicit farm_loot command succeeds, record the timestamp # When an explicit farm_loot command succeeds, record the timestamp
if cmd and cmd['type'] == 'farm_loot' and status == 'done' and cmd['player_id']: 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(''' conn.execute('''
INSERT INTO kv_store (key, value, updated_at) VALUES (?, ?, ?) INSERT INTO kv_store (key, value, updated_at) VALUES (?, ?, ?)
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at
''', (f'last_farmed_{cmd["player_id"]}', now, now)) ''', (lf_key, now, now))
conn.commit() conn.commit()
conn.close() conn.close()
@@ -363,9 +440,12 @@ def upload_market_data():
@api.route('/api/farm_status', methods=['POST', 'GET']) @api.route('/api/farm_status', methods=['POST', 'GET'])
def farm_status(): def farm_status():
player_id = request.args.get('player_id') player_id = request.args.get('player_id')
world_id = request.args.get('world_id')
if not player_id: if not player_id:
return jsonify({'error': 'no player_id'}), 400 return jsonify({'error': 'no player_id'}), 400
kv_key = f'farm_status_{player_id}'
player_key = f"{player_id}_{world_id}" if world_id else player_id
kv_key = f'farm_status_{player_key}'
conn = get_db() conn = get_db()
if request.method == 'POST': if request.method == 'POST':
data = request.get_json(silent=True) or {} data = request.get_json(silent=True) or {}
@@ -379,7 +459,7 @@ def farm_status():
conn.execute(''' conn.execute('''
INSERT INTO kv_store (key, value, updated_at) VALUES (?, ?, ?) INSERT INTO kv_store (key, value, updated_at) VALUES (?, ?, ?)
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at
''', (f'last_farmed_{player_id}', now, now)) ''', (f'last_farmed_{player_key}', now, now))
conn.commit() conn.commit()
conn.close() conn.close()
return jsonify({'ok': True}) return jsonify({'ok': True})
@@ -401,16 +481,19 @@ def api_bot_logs():
data = request.get_json(silent=True) or {} data = request.get_json(silent=True) or {}
player_id = str(data.get('player_id', '')) player_id = str(data.get('player_id', ''))
world_id = str(data.get('world_id', ''))
feature = data.get('feature', '') feature = data.get('feature', '')
message = data.get('message', '') message = data.get('message', '')
if not player_id or not feature or not message: if not player_id or not feature or not message:
return jsonify({'error': 'missing fields'}), 400 return jsonify({'error': 'missing fields'}), 400
player_key = f"{player_id}_{world_id}" if world_id else player_id
conn = get_db() conn = get_db()
conn.execute( conn.execute(
'INSERT INTO bot_logs (player_id, feature, message) VALUES (?, ?, ?)', 'INSERT INTO bot_logs (player_id, feature, message) VALUES (?, ?, ?)',
(player_id, feature, message) (player_key, feature, message)
) )
# Keep only latest 50 per player/feature # Keep only latest 50 per player/feature
conn.execute(''' conn.execute('''
@@ -421,7 +504,7 @@ def api_bot_logs():
WHERE player_id = ? AND feature = ? WHERE player_id = ? AND feature = ?
ORDER BY id DESC LIMIT 50 ORDER BY id DESC LIMIT 50
) )
''', (player_id, feature, player_id, feature)) ''', (player_key, feature, player_key, feature))
conn.commit() conn.commit()
conn.close() conn.close()
return jsonify({'ok': True}) return jsonify({'ok': True})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,8 @@
<header> <header>
<h1><a href="/" style="text-decoration: none; margin-right: 15px; cursor: pointer;" title="Back to Players">⬅️</a> ⚔️ Grepolis Remote</h1> <h1><a href="/" style="text-decoration: none; margin-right: 15px; cursor: pointer;" title="Back to Players">⬅️</a> ⚔️ Grepolis Remote</h1>
<div class="status-indicator"> <div class="status-indicator" style="display: flex; align-items: center; gap: 10px;">
<button class="btn btn-gold btn-sm" id="live-btn" onclick="window.requestLiveSync()" title="Request immediate data update from game" style="padding: 4px 8px; font-size: 0.72rem; border-radius: 4px; border: 1px solid #c8a44a;">⚡ Live Sync</button>
<div id="server-status" class="conn-badge">Server…</div> <div id="server-status" class="conn-badge">Server…</div>
<div id="client-status" class="conn-badge">Client…</div> <div id="client-status" class="conn-badge">Client…</div>
</div> </div>
@@ -29,7 +30,6 @@
<div id="town-panel"> <div id="town-panel">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<h2 style="margin: 0;">Towns</h2> <h2 style="margin: 0;">Towns</h2>
<button class="btn btn-gold btn-sm" id="live-btn" onclick="window.requestLiveSync()" title="Request immediate data update from game" style="padding: 4px 8px; font-size: 0.72rem;">⚡ Live Sync</button>
</div> </div>
<div id="town-filters" style="margin-bottom: 15px; padding: 10px; background: #0f3460; border-radius: 6px; border: 1px solid #2a4a6a;"> <div id="town-filters" style="margin-bottom: 15px; padding: 10px; background: #0f3460; border-radius: 6px; border: 1px solid #2a4a6a;">
@@ -41,11 +41,7 @@
</label> </label>
<label style="font-size: 0.8rem; cursor: pointer; display: flex; align-items: center; gap: 4px; background: rgba(0,0,0,0.2); padding: 4px 8px; border-radius: 12px; border: 1px solid #2a4a6a;"> <label style="font-size: 0.8rem; cursor: pointer; display: flex; align-items: center; gap: 4px; background: rgba(0,0,0,0.2); padding: 4px 8px; border-radius: 12px; border: 1px solid #2a4a6a;">
<input type="checkbox" id="filter-festival" onchange="window.renderTowns()"> 🎭 Ελεύθεροι Πόροι <input type="checkbox" id="filter-not-building" onchange="window.renderTowns()"> 🏗️ Δεν χτίζει
</label>
<label style="font-size: 0.8rem; cursor: pointer; display: flex; align-items: center; gap: 4px; background: rgba(0,0,0,0.2); padding: 4px 8px; border-radius: 12px; border: 1px solid #2a4a6a;">
<input type="checkbox" id="filter-points" onchange="window.renderTowns()"> 📈 10k+ Πόντοι
</label> </label>
</div> </div>
</div> </div>
@@ -87,132 +83,31 @@
</div> </div>
<div id="command-form-wrap" style="display:none"> <div id="command-form-wrap" style="display:none">
<div class="command-form"> <div class="command-form" style="display: flex; flex-direction: column; gap: 15px; align-items: flex-start;">
<div class="form-group"> <!-- Segmented Control Row -->
<label>Command Type</label> <div class="segmented-control" id="cmd-type-buttons" style="display: flex; gap: 5px; background: #16213e; padding: 4px; border-radius: 8px; border: 1px solid #2a4a6a;">
<select id="cmd-type" onchange="onCmdTypeChange()"> <button class="seg-btn" id="seg-build" onclick="window.setCmdType('build', true)" style="flex: 1; padding: 10px; border: none; background: transparent; color: #888; border-radius: 6px; cursor: pointer; transition: 0.2s;">🏗️ Κατασκευές</button>
<option value="" disabled selected>-- Επιλέξτε --</option> <button class="seg-btn" id="seg-recruit" onclick="window.setCmdType('recruit', true)" style="flex: 1; padding: 10px; border: none; background: transparent; color: #888; border-radius: 6px; cursor: pointer; transition: 0.2s;">⚔️ Στρατός</button>
<option value="build">Build / Upgrade</option> <button class="seg-btn" id="seg-market" onclick="window.setCmdType('market_offer', true)" style="flex: 1; padding: 10px; border: none; background: transparent; color: #888; border-radius: 6px; cursor: pointer; transition: 0.2s;">🛒 Παζάρι</button>
<option value="recruit">Recruit Troops</option> <button class="seg-btn" id="seg-research" onclick="window.setCmdType('research', true)" style="flex: 1; padding: 10px; border: none; background: transparent; color: #888; border-radius: 6px; cursor: pointer; transition: 0.2s;">🦉 Έρευνα</button>
<option value="market_offer">Παζάρι - Προσφορά</option> <button class="seg-btn" id="seg-blueprints" onclick="window.setCmdType('blueprints', true)" style="flex: 1; padding: 10px; border: none; background: transparent; color: #888; border-radius: 6px; cursor: pointer; transition: 0.2s;">📜 Blueprints</button>
<option value="research">Ακαδημία - Έρευνες</option>
</select>
</div> </div>
<!-- Build options - now a button that opens the visual picker --> <!-- Dynamic Selection Area -->
<div class="form-group" id="build-options" style="display:none"> <div id="selection-area" style="display:none; align-items: center; gap: 10px; flex-wrap: wrap;">
<label>Building</label> <button class="btn btn-gold" id="active-selection-display" onclick="window.reopenActiveModal()" style="min-width: 250px; padding: 10px 15px; border-radius: 6px; display: flex; justify-content: space-between; align-items: center; font-weight: bold;">
<button class="btn btn-gold" id="open-building-modal" onclick="window.openBuildingModal()" style="text-align:left; min-width:200px;"> <span id="selection-label">-- Επιλέξτε --</span>
<span id="selected-building-label">-- Επιλέξτε Κατασκευή --</span>
</button> </button>
<div id="recruit-amount-wrap" style="display:none; align-items: center; gap: 5px; background: #16213e; padding: 8px 12px; border-radius: 6px; border: 1px solid #2a4a6a;">
<label style="font-size:0.85rem; color:#ccc; margin:0;">Ποσότητα:</label>
<input type="number" id="recruit-amount" value="1" min="1" max="9999" style="width: 70px; background: #0f3460; color: #fff; border: 1px solid #c8a44a; border-radius: 4px; padding: 4px 8px;">
</div> </div>
<!-- Research options --> <button id="btn-send" class="btn btn-gold" onclick="window.sendCommand()" style="margin-left: auto; padding: 10px 20px; font-size: 1rem;">Send ⚡</button>
<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>
<!-- Market options -->
<!-- Market options -->
<div class="form-group" id="market-options" style="display:none">
<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%;">
</div>
<div style="flex:1;">
<label>Πόρος Προσφοράς</label>
<select id="market-offer-type">
<option value="wood">Ξύλο</option>
<option value="stone">Πέτρα</option>
<option value="iron">Ασήμι</option>
</select>
</div>
</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%;">
</div>
<div style="flex:1;">
<label>Πόρος Ζήτησης</label>
<select id="market-demand-type">
<option value="iron">Ασήμι</option>
<option value="stone">Πέτρα</option>
<option value="wood">Ξύλο</option>
</select>
</div>
</div>
<div style="display:flex; gap:10px;">
<div style="flex:1;">
<label>Χρόνος (Ώρες)</label>
<select id="market-max-time">
<option value="1800">0.5</option>
<option value="3600">1</option>
<option value="7200">2</option>
<option value="14400">4</option>
<option value="28800">8</option>
<option value="43200">12</option>
<option value="86400">24</option>
<option value="172800">48</option>
</select>
</div>
<div style="flex:1;">
<label>Ορατότητα</label>
<select id="market-visibility">
<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>
<div id="build-queue-preview"></div> <div id="build-queue-preview"></div>
@@ -233,6 +128,94 @@
</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 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 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>
</select>
</div>
</div>
<div style="display:flex; gap:10px; margin-bottom:10px;">
<div style="flex:1;">
<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 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; margin-bottom:20px;">
<div style="flex:1;">
<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>
<option value="14400">4</option>
<option value="28800">8</option>
<option value="43200">12</option>
<option value="86400">24</option>
<option value="172800">48</option>
</select>
</div>
<div style="flex:1;">
<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>
<button class="btn btn-gold" onclick="window.saveMarketModal()" style="width:100%; padding:10px; font-weight:bold;">✅ Αποθήκευση Προσφοράς</button>
</div>
</div>
</div>
<!-- ====== Building Picker Modal ====== --> <!-- ====== Building Picker Modal ====== -->
<div id="building-modal-overlay" onclick="window.closeBuildingModal(event)"> <div id="building-modal-overlay" onclick="window.closeBuildingModal(event)">
<div id="building-modal"> <div id="building-modal">
@@ -247,6 +230,17 @@
</div> </div>
</div> </div>
</div> </div>
<!-- ====== Unit Picker Modal ====== -->
<div id="unit-modal-overlay" class="modal-overlay" onclick="window.closeUnitModal(event)">
<div id="unit-modal" class="custom-modal">
<div class="modal-header">
<h3>⚔️ Επιλογή Μονάδας</h3>
<button id="unit-modal-close" onclick="window.closeUnitModal()"></button>
</div>
<div id="unit-grid" style="display:flex; flex-wrap:wrap; gap:10px; max-height:70vh; overflow-y:auto; padding:10px 5px;"></div>
</div>
</div>
<!-- ====== Academy Picker Modal ====== --> <!-- ====== Academy Picker Modal ====== -->
<div id="academy-modal-overlay" onclick="window.closeAcademyModal(event)"> <div id="academy-modal-overlay" onclick="window.closeAcademyModal(event)">
<div id="academy-modal"> <div id="academy-modal">
@@ -324,6 +318,7 @@
</style> </style>
<script> <script>
window.PLAYER_ID = "{{ player_id }}"; window.PLAYER_ID = "{{ player_id }}";
window.WORLD_ID = "{{ world_id }}";
</script> </script>
<script src="/static/js/state.js?v=6"></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/townViewer.js?v=6"></script>

View File

@@ -233,7 +233,7 @@
<body> <body>
<div class="topbar"> <div class="topbar">
<a href="/player/{{ player_id }}">← Πίσω</a> <a href="/player/{{ player_id }}/{{ world_id }}">← Πίσω</a>
<h1>🌾 Farm Manager</h1> <h1>🌾 Farm Manager</h1>
<span class="online-dot" id="online-dot" title="Κατάσταση Script"></span> <span class="online-dot" id="online-dot" title="Κατάσταση Script"></span>
<button class="sync-btn" onclick="requestSync()">Live Sync</button> <button class="sync-btn" onclick="requestSync()">Live Sync</button>
@@ -411,6 +411,7 @@
<script> <script>
const PLAYER_ID = '{{ player_id }}'; const PLAYER_ID = '{{ player_id }}';
const WORLD_ID = '{{ world_id }}';
let selectedOption = 1; let selectedOption = 1;
// -- Loot option buttons -- // -- Loot option buttons --
@@ -445,7 +446,7 @@
fetch('/dashboard/farm-settings', { fetch('/dashboard/farm-settings', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ player_id: PLAYER_ID, enabled, loot_option: selectedOption }) body: JSON.stringify({ player_id: PLAYER_ID, world_id: WORLD_ID, enabled, loot_option: selectedOption })
}) })
.then(r => r.json()) .then(r => r.json())
.then(() => { .then(() => {
@@ -457,7 +458,7 @@
// -- Load current settings -- // -- Load current settings --
function loadSettings() { function loadSettings() {
fetch(`/dashboard/farm-settings?player_id=${PLAYER_ID}`) fetch(`/dashboard/farm-settings?player_id=${PLAYER_ID}&world_id=${WORLD_ID}`)
.then(r => r.json()) .then(r => r.json())
.then(cfg => { .then(cfg => {
document.getElementById('farm-enabled').checked = cfg.enabled; document.getElementById('farm-enabled').checked = cfg.enabled;
@@ -480,7 +481,7 @@
} }
function loadFarmData() { function loadFarmData() {
fetch(`/dashboard/farm-data?player_id=${PLAYER_ID}`) fetch(`/dashboard/farm-data?player_id=${PLAYER_ID}&world_id=${WORLD_ID}`)
.then(r => r.json()) .then(r => r.json())
.then(resp => { .then(resp => {
const data = resp.towns || []; const data = resp.towns || [];
@@ -563,7 +564,7 @@
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
player_id: PLAYER_ID, player_id: PLAYER_ID,
town_id: 0, town_id: WORLD_ID ? `0_${WORLD_ID}` : "0",
type: 'farm_upgrade', type: 'farm_upgrade',
payload: { threshold: threshold, action_type: actionType } payload: { threshold: threshold, action_type: actionType }
}) })
@@ -590,7 +591,7 @@
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
player_id: PLAYER_ID, player_id: PLAYER_ID,
town_id: 0, town_id: WORLD_ID ? `0_${WORLD_ID}` : "0",
type: 'farm_loot', type: 'farm_loot',
payload: { loot_option: selectedOption } payload: { loot_option: selectedOption }
}) })
@@ -616,7 +617,7 @@
fetch('/dashboard/bootcamp-attack-now', { fetch('/dashboard/bootcamp-attack-now', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ player_id: PLAYER_ID }) body: JSON.stringify({ player_id: PLAYER_ID, world_id: WORLD_ID })
}) })
.then(r => r.json()) .then(r => r.json())
.then(() => { .then(() => {
@@ -632,7 +633,7 @@
async function checkWarehouseStatus() { async function checkWarehouseStatus() {
try { try {
const res = await fetch(`/api/farm_status?player_id=${PLAYER_ID}`); const res = await fetch(`/api/farm_status?player_id=${PLAYER_ID}&world_id=${WORLD_ID}`);
const data = await res.json(); const data = await res.json();
const banner = document.getElementById('warehouse-full-banner'); const banner = document.getElementById('warehouse-full-banner');
if (banner) banner.style.display = data.warehouse_full ? 'flex' : 'none'; if (banner) banner.style.display = data.warehouse_full ? 'flex' : 'none';
@@ -650,7 +651,7 @@
} }
function loadBotSettings() { function loadBotSettings() {
fetch(`/dashboard/bot-settings?player_id=${PLAYER_ID}`) fetch(`/dashboard/bot-settings?player_id=${PLAYER_ID}&world_id=${WORLD_ID}`)
.then(r => r.json()) .then(r => r.json())
.then(cfg => { .then(cfg => {
document.getElementById('bootcamp-enabled').checked = !!cfg.bootcamp_enabled; document.getElementById('bootcamp-enabled').checked = !!cfg.bootcamp_enabled;
@@ -668,6 +669,7 @@
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
player_id: PLAYER_ID, player_id: PLAYER_ID,
world_id: WORLD_ID,
bootcamp_enabled: document.getElementById('bootcamp-enabled').checked, bootcamp_enabled: document.getElementById('bootcamp-enabled').checked,
bootcamp_use_def: document.getElementById('bootcamp-use-def').checked, bootcamp_use_def: document.getElementById('bootcamp-use-def').checked,
rural_trade_enabled: document.getElementById('rural-trade-enabled').checked, rural_trade_enabled: document.getElementById('rural-trade-enabled').checked,
@@ -707,9 +709,9 @@
} }
function loadBotLogs() { function loadBotLogs() {
fetch(`/dashboard/bot-logs?player_id=${PLAYER_ID}&feature=bootcamp`) fetch(`/dashboard/bot-logs?player_id=${PLAYER_ID}&world_id=${WORLD_ID}&feature=bootcamp`)
.then(r => r.json()).then(data => renderBotLog('bootcamp-log', data)); .then(r => r.json()).then(data => renderBotLog('bootcamp-log', data));
fetch(`/dashboard/bot-logs?player_id=${PLAYER_ID}&feature=rural_trade`) 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)); .then(r => r.json()).then(data => renderBotLog('rural-trade-log', data));
} }

View File

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

View File

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