Compare commits
32 Commits
cf3c2e7b4f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 76b991a62b | |||
| f5231a2524 | |||
| 84de7082ec | |||
| 83b8c85557 | |||
| d8ba139d07 | |||
| 6157ae1034 | |||
| 4272edf432 | |||
| 502b330ac5 | |||
| 1c65043eb3 | |||
| 5c4d415fdd | |||
| f22b92ae89 | |||
| 0f54ef9191 | |||
| 45e71ed90b | |||
| 66dcb71a8d | |||
| b36b11393f | |||
| 90ce6a029d | |||
| c9e6522f12 | |||
| 1cb5dca3c2 | |||
| 2552d3d075 | |||
| 5f6855ec69 | |||
| 05785c294e | |||
| 614029e527 | |||
| a572feef14 | |||
| b18e2e8f97 | |||
| 2769091b74 | |||
| f4a0e18686 | |||
| d6c2252f5c | |||
| bcda80e127 | |||
| 731a7b2f3b | |||
| f82893164e | |||
| efa63f761f | |||
| ae37674bcc |
@@ -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
6
app.py
@@ -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
221
blueprint_engine.py
Normal 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}")
|
||||||
@@ -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,14 +38,19 @@ 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 {
|
||||||
model = uw.MM.getModelByNameAndPlayerId('PlayerAttackSpot');
|
model = uw.MM.getModelByNameAndPlayerId('PlayerAttackSpot');
|
||||||
} catch (e) { return; }
|
} catch (e) { return; }
|
||||||
|
|
||||||
if (!model) return;
|
// Model not loaded yet (player hasn't opened the camp UI this session)
|
||||||
|
if (!model || typeof model.getLevel?.() === 'undefined') {
|
||||||
|
log('[bootcamp] PlayerAttackSpot model not ready — skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// ── 1. Claim reward if available ──────────────────────────────
|
// ── 1. Claim reward if available ──────────────────────────────
|
||||||
try {
|
try {
|
||||||
@@ -57,28 +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 = () => {
|
||||||
// Use instant rewards immediately
|
|
||||||
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
|
||||||
@@ -88,10 +95,15 @@ async function autoBootcampLoop() {
|
|||||||
|
|
||||||
// ── 2. Attack if no cooldown ───────────────────────────────────
|
// ── 2. Attack if no cooldown ───────────────────────────────────
|
||||||
try {
|
try {
|
||||||
const cooldown = model.getCooldownDuration?.() ?? 1;
|
// If getCooldownDuration is unavailable, skip safely
|
||||||
|
if (typeof model.getCooldownDuration !== 'function') {
|
||||||
|
log('[bootcamp] getCooldownDuration unavailable — skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,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;
|
||||||
|
|
||||||
@@ -266,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));
|
||||||
@@ -281,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`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
const option = lastKnownFarmSettings.loot_option || 1;
|
let totalMs = 15000; // 15 seconds for the first run to catch already-ready farms
|
||||||
const baseMs = LOOT_TIMINGS[option] || 300000;
|
if (!isFirstRun) {
|
||||||
const jitterMs = randInt(30000, 120000); // +30 to +120 s
|
const option = lastKnownFarmSettings.loot_option || 1;
|
||||||
const totalMs = baseMs + jitterMs;
|
const baseMs = LOOT_TIMINGS[option] || 300000;
|
||||||
log(`⏰ Next auto-farm in ${(totalMs / 60000).toFixed(1)} min (option ${option} + ${(jitterMs/1000).toFixed(0)}s jitter)`);
|
const jitterMs = randInt(30000, 120000); // +30 to +120 s
|
||||||
|
totalMs = baseMs + jitterMs;
|
||||||
|
}
|
||||||
|
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}`);
|
||||||
@@ -52,6 +56,13 @@ async function pollAndExecute() {
|
|||||||
lastKnownFarmSettings = cmdData.farm_settings || {};
|
lastKnownFarmSettings = cmdData.farm_settings || {};
|
||||||
lastKnownBotSettings = cmdData.bot_settings || {};
|
lastKnownBotSettings = cmdData.bot_settings || {};
|
||||||
|
|
||||||
|
// Handle manual bootcamp attack trigger
|
||||||
|
if (lastKnownBotSettings.attack_now) {
|
||||||
|
log('Manual bootcamp attack requested! Firing immediately...');
|
||||||
|
// Fire asynchronously so it doesn't block the rest of pollAndExecute
|
||||||
|
setTimeout(autoBootcampLoop, 0);
|
||||||
|
}
|
||||||
|
|
||||||
// Feature flags — default to all on if server doesn't send them (backward compatible)
|
// Feature flags — default to all on if server doesn't send them (backward compatible)
|
||||||
const features = cmdData.enabled_features || ['farm', 'admin'];
|
const features = cmdData.enabled_features || ['farm', 'admin'];
|
||||||
const farmOn = features.includes('farm');
|
const farmOn = features.includes('farm');
|
||||||
@@ -177,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 })
|
||||||
@@ -192,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 })
|
||||||
@@ -215,8 +228,8 @@ function boot() {
|
|||||||
detectCaptcha();
|
detectCaptcha();
|
||||||
setTimeout(pushState, 5000);
|
setTimeout(pushState, 5000);
|
||||||
jitterLoop(pushState, 60000, 120000); // state sync every 1–2 min
|
jitterLoop(pushState, 60000, 120000); // state sync every 1–2 min
|
||||||
jitterLoop(pollAndExecute, 8000, 18000); // command poll every 8–18 s
|
jitterLoop(pollAndExecute, 8000, 18000, 2000); // command poll every 8–18 s, but start in 2s
|
||||||
scheduleNextFarm(); // auto-farm timer-based (loot_option + 30–120s)
|
scheduleNextFarm(true); // auto-farm timer-based (loot_option + 30–120s)
|
||||||
jitterLoop(autoBootcampLoop, 720000, 1320000); // bootcamp every 12–22 min
|
jitterLoop(autoBootcampLoop, 720000, 1320000); // bootcamp every 12–22 min
|
||||||
jitterLoop(autoRuralTradeLoop, 1500000, 2700000); // rural trade every 25–45 min
|
jitterLoop(autoRuralTradeLoop, 1500000, 2700000); // rural trade every 25–45 min
|
||||||
}
|
}
|
||||||
|
|||||||
10
db.py
10
db.py
@@ -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',
|
||||||
|
|||||||
150
routes/api.py
150
routes/api.py
@@ -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.)."""
|
||||||
row = c.execute('''
|
|
||||||
SELECT * FROM commands
|
# We use LEFT JOIN because global commands (like farm_loot) use a pseudo town_id like "0_gr121"
|
||||||
WHERE status = 'pending' AND type = ? AND player_id = ?
|
# which does not exist in town_state.
|
||||||
ORDER BY updated_at ASC, id ASC
|
global_town_id = f"0_{world_id}" if world_id else "0"
|
||||||
LIMIT 1
|
|
||||||
''', (cmd_type, player_id)).fetchone()
|
if world_id:
|
||||||
|
row = c.execute('''
|
||||||
|
SELECT c.* FROM commands c
|
||||||
|
LEFT JOIN town_state ts ON c.town_id = ts.town_id
|
||||||
|
WHERE c.status = 'pending' AND c.type = ? AND c.player_id = ?
|
||||||
|
AND (ts.world_id = ? OR c.town_id = ?)
|
||||||
|
ORDER BY c.updated_at ASC, c.id ASC
|
||||||
|
LIMIT 1
|
||||||
|
''', (cmd_type, player_id, world_id, global_town_id)).fetchone()
|
||||||
|
else:
|
||||||
|
row = c.execute('''
|
||||||
|
SELECT * FROM commands
|
||||||
|
WHERE status = 'pending' AND type = ? AND player_id = ?
|
||||||
|
ORDER BY updated_at ASC, id ASC
|
||||||
|
LIMIT 1
|
||||||
|
''', (cmd_type, player_id)).fetchone()
|
||||||
|
|
||||||
if not row:
|
if not row:
|
||||||
return None
|
return None
|
||||||
c.execute('''
|
c.execute('''
|
||||||
@@ -127,27 +154,61 @@ 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).
|
||||||
town_rows = c.execute('''
|
if world_id:
|
||||||
SELECT town_id
|
town_rows = c.execute('''
|
||||||
FROM commands
|
SELECT c.town_id
|
||||||
WHERE status = 'pending' AND type = 'build' AND player_id = ?
|
FROM commands c
|
||||||
GROUP BY town_id
|
JOIN town_state ts ON c.town_id = ts.town_id
|
||||||
ORDER BY MIN(updated_at) ASC
|
WHERE c.status = 'pending' AND c.type = 'build' AND c.player_id = ? AND ts.world_id = ?
|
||||||
''', (player_id,)).fetchall()
|
GROUP BY c.town_id
|
||||||
|
ORDER BY MIN(c.updated_at) ASC
|
||||||
|
''', (player_id, world_id)).fetchall()
|
||||||
|
else:
|
||||||
|
town_rows = c.execute('''
|
||||||
|
SELECT town_id
|
||||||
|
FROM commands
|
||||||
|
WHERE status = 'pending' AND type = 'build' AND player_id = ?
|
||||||
|
GROUP BY town_id
|
||||||
|
ORDER BY MIN(updated_at) ASC
|
||||||
|
''', (player_id,)).fetchall()
|
||||||
|
_log.warning(f"[poll] build towns found: {[r['town_id'] for r in town_rows]}, busy: {busy_towns}")
|
||||||
|
|
||||||
results = []
|
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,
|
||||||
@@ -206,6 +280,15 @@ def get_pending_command():
|
|||||||
'rural_trade_ratio': bot_row['rural_trade_ratio'] if bot_row else 3,
|
'rural_trade_ratio': bot_row['rural_trade_ratio'] if bot_row else 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# One-shot manual attack flag
|
||||||
|
attack_now_key = f'bootcamp_attack_now_{player_key}'
|
||||||
|
flag_row = c.execute('SELECT value FROM kv_store WHERE key = ?', (attack_now_key,)).fetchone()
|
||||||
|
if flag_row and flag_row['value'] == '1':
|
||||||
|
bot_settings['attack_now'] = True
|
||||||
|
c.execute("UPDATE kv_store SET value = '0' WHERE key = ?", (attack_now_key,))
|
||||||
|
else:
|
||||||
|
bot_settings['attack_now'] = False
|
||||||
|
|
||||||
# Feature flags — look up this player's authorized features from their clan
|
# Feature flags — look up this player's authorized features from their clan
|
||||||
member_row = c.execute(
|
member_row = c.execute(
|
||||||
'SELECT features FROM clan_members WHERE player_id = ?', (str(player_id),)
|
'SELECT features FROM clan_members WHERE player_id = ?', (str(player_id),)
|
||||||
@@ -286,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()
|
||||||
@@ -354,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 {}
|
||||||
@@ -370,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})
|
||||||
@@ -392,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('''
|
||||||
@@ -412,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})
|
||||||
|
|||||||
@@ -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()
|
||||||
rows = conn.execute(
|
if world_id:
|
||||||
'SELECT town_id, town_name, data FROM town_state WHERE player_id = ?', (player_id,)
|
rows = conn.execute(
|
||||||
).fetchall()
|
'SELECT town_id, town_name, data FROM town_state WHERE player_id = ? AND world_id = ?',
|
||||||
|
(player_id, world_id)
|
||||||
|
).fetchall()
|
||||||
|
else:
|
||||||
|
rows = conn.execute(
|
||||||
|
'SELECT town_id, town_name, data FROM town_state WHERE player_id = ?', (player_id,)
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
# Also fetch when the bot last farmed
|
# 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,8 +583,11 @@ 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()
|
||||||
@@ -512,7 +595,7 @@ def bot_logs():
|
|||||||
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,8 +621,31 @@ 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})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# POST /dashboard/bootcamp-attack-now
|
||||||
|
# Sets a one-shot flag consumed by the TM bot on the next poll.
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@dashboard.route('/dashboard/bootcamp-attack-now', methods=['POST'])
|
||||||
|
def bootcamp_attack_now():
|
||||||
|
player_id = (request.json or {}).get('player_id')
|
||||||
|
world_id = (request.json or {}).get('world_id', '')
|
||||||
|
if not player_id:
|
||||||
|
return jsonify({'error': 'missing player_id'}), 400
|
||||||
|
|
||||||
|
player_key = f"{player_id}_{world_id}" if world_id else player_id
|
||||||
|
key = f'bootcamp_attack_now_{player_key}'
|
||||||
|
conn = get_db()
|
||||||
|
conn.execute('''
|
||||||
|
INSERT INTO kv_store (key, value, updated_at) VALUES (?, '1', ?)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET value='1', updated_at=excluded.updated_at
|
||||||
|
''', (key, datetime.utcnow().isoformat()))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'ok': True})
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -2,13 +2,106 @@
|
|||||||
// Command Form Component
|
// Command Form Component
|
||||||
// ================================================================
|
// ================================================================
|
||||||
|
|
||||||
window.onCmdTypeChange = function() {
|
window.currentCmdType = null;
|
||||||
const type = document.getElementById('cmd-type').value;
|
|
||||||
document.getElementById('build-options').style.display = type === 'build' ? '' : 'none';
|
window.setCmdType = function(type, openModal = false) {
|
||||||
document.getElementById('recruit-options').style.display = type === 'recruit' ? '' : 'none';
|
window.currentCmdType = type;
|
||||||
document.getElementById('amount-group').style.display = type === 'recruit' ? '' : 'none';
|
|
||||||
document.getElementById('market-options').style.display = type === 'market_offer' ? '' : 'none';
|
// Update segmented control active state
|
||||||
document.getElementById('research-options').style.display = type === 'research' ? '' : 'none';
|
document.querySelectorAll('.seg-btn').forEach(btn => btn.classList.remove('active'));
|
||||||
|
const activeBtn = document.getElementById('seg-' + (type === 'market_offer' ? 'market' : type));
|
||||||
|
if (activeBtn) activeBtn.classList.add('active');
|
||||||
|
|
||||||
|
// Show selection area
|
||||||
|
document.getElementById('selection-area').style.display = 'flex';
|
||||||
|
document.getElementById('recruit-amount-wrap').style.display = type === 'recruit' ? 'flex' : 'none';
|
||||||
|
|
||||||
|
// Update selection label with current choice if any, or default
|
||||||
|
window.updateSelectionDisplay();
|
||||||
|
|
||||||
|
if (openModal) {
|
||||||
|
window.reopenActiveModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.updateSelectionDisplay = function() {
|
||||||
|
const type = window.currentCmdType;
|
||||||
|
const labelEl = document.getElementById('selection-label');
|
||||||
|
|
||||||
|
if (type === 'build') {
|
||||||
|
if (window.selectedBuildingId) {
|
||||||
|
labelEl.innerHTML = `🏗️ ${window.BUILDING_NAMES_GR[window.selectedBuildingId] || window.selectedBuildingId}`;
|
||||||
|
} else {
|
||||||
|
labelEl.innerHTML = `-- Επιλέξτε Κατασκευή --`;
|
||||||
|
}
|
||||||
|
} else if (type === 'recruit') {
|
||||||
|
if (window.selectedUnitId) {
|
||||||
|
labelEl.innerHTML = `⚔️ ${window.UNIT_NAMES_GR[window.selectedUnitId] || window.selectedUnitId}`;
|
||||||
|
} else {
|
||||||
|
labelEl.innerHTML = `-- Επιλέξτε Μονάδα --`;
|
||||||
|
}
|
||||||
|
} else if (type === 'research') {
|
||||||
|
if (window.selectedResearchId) {
|
||||||
|
const rd = window.RESEARCH_DATA[window.selectedResearchId];
|
||||||
|
labelEl.innerHTML = `🦉 ${rd ? rd.name : window.selectedResearchId}`;
|
||||||
|
} else {
|
||||||
|
labelEl.innerHTML = `-- Επιλέξτε Έρευνα --`;
|
||||||
|
}
|
||||||
|
} else if (type === 'market_offer') {
|
||||||
|
labelEl.innerHTML = `🛒 Ρυθμίσεις Παζαριού`;
|
||||||
|
} else if (type === 'blueprints') {
|
||||||
|
if (window.selectedBlueprintName) {
|
||||||
|
labelEl.innerHTML = `📜 ${window.selectedBlueprintName}`;
|
||||||
|
} else {
|
||||||
|
labelEl.innerHTML = `-- Επιλέξτε Blueprint --`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.reopenActiveModal = function() {
|
||||||
|
const type = window.currentCmdType;
|
||||||
|
if (type === 'build') window.openBuildingModal();
|
||||||
|
else if (type === 'recruit') window.openUnitModal();
|
||||||
|
else if (type === 'research') window.openAcademyModal();
|
||||||
|
else if (type === 'market_offer') window.openMarketModal();
|
||||||
|
else if (type === 'blueprints') window.openBlueprintsModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.selectedBlueprintName = null;
|
||||||
|
window.selectBlueprint = function(name) {
|
||||||
|
window.selectedBlueprintName = name;
|
||||||
|
window.updateSelectionDisplay();
|
||||||
|
window.closeBlueprintsModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.openBlueprintsModal = function() {
|
||||||
|
document.getElementById('blueprints-modal-overlay').classList.add('open');
|
||||||
|
};
|
||||||
|
window.closeBlueprintsModal = function(e) {
|
||||||
|
if (e && e.target.id !== 'blueprints-modal-overlay' && !e.target.classList.contains('modal-close')) return;
|
||||||
|
document.getElementById('blueprints-modal-overlay').classList.remove('open');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.openMarketModal = function() {
|
||||||
|
document.getElementById('market-modal-overlay').classList.add('open');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.closeMarketModal = function(e) {
|
||||||
|
if (e && !e.target.classList.contains('modal-overlay') && !e.target.classList.contains('modal-close')) return;
|
||||||
|
document.getElementById('market-modal-overlay').classList.remove('open');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.saveMarketModal = function() {
|
||||||
|
const oType = document.getElementById('market-offer-type').value;
|
||||||
|
const oAmt = document.getElementById('market-offer-amount').value;
|
||||||
|
const dType = document.getElementById('market-demand-type').value;
|
||||||
|
const dAmt = document.getElementById('market-demand-amount').value;
|
||||||
|
|
||||||
|
const labelEl = document.getElementById('selection-label');
|
||||||
|
const resGr = { wood: 'Ξύλο', stone: 'Πέτρα', iron: 'Ασήμι' };
|
||||||
|
labelEl.innerHTML = `🛒 ${oAmt} ${resGr[oType]} ➔ ${dAmt} ${resGr[dType]}`;
|
||||||
|
|
||||||
|
window.closeMarketModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Building emoji icons for the visual grid
|
// Building emoji icons for the visual grid
|
||||||
@@ -118,11 +211,8 @@ window.closeBuildingModal = function(e) {
|
|||||||
|
|
||||||
window.selectBuilding = function(key, nameGr) {
|
window.selectBuilding = function(key, nameGr) {
|
||||||
window.selectedBuildingId = key;
|
window.selectedBuildingId = key;
|
||||||
// Update the trigger button label
|
window.updateSelectionDisplay();
|
||||||
document.getElementById('selected-building-label').textContent = `🏗️ ${nameGr}`;
|
|
||||||
// Re-render grid to show new selection highlight
|
|
||||||
window.openBuildingModal();
|
window.openBuildingModal();
|
||||||
// Close after brief visual feedback
|
|
||||||
setTimeout(() => document.getElementById('building-modal-overlay').classList.remove('open'), 180);
|
setTimeout(() => document.getElementById('building-modal-overlay').classList.remove('open'), 180);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -233,77 +323,140 @@ window.closeAcademyModal = function(e) {
|
|||||||
|
|
||||||
window.selectResearch = function(key, name) {
|
window.selectResearch = function(key, name) {
|
||||||
window.selectedResearchId = key;
|
window.selectedResearchId = key;
|
||||||
document.getElementById('selected-research-label').textContent = `🧪 ${name}`;
|
window.updateSelectionDisplay();
|
||||||
window.openAcademyModal();
|
window.openAcademyModal();
|
||||||
setTimeout(() => document.getElementById('academy-modal-overlay').classList.remove('open'), 180);
|
setTimeout(() => document.getElementById('academy-modal-overlay').classList.remove('open'), 180);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
window.selectedUnitId = null;
|
||||||
|
|
||||||
|
window.UNIT_ICONS = {
|
||||||
|
sword: '⚔️', slinger: '🪨', archer: '🏹', hoplite: '🛡️',
|
||||||
|
rider: '🐎', chariot: '🛷', catapult: '☄️', godsent: '👼',
|
||||||
|
big_transporter: '⛴️', small_transporter: '🚤', bireme: '🛶',
|
||||||
|
attack_ship: '🔥', trireme: '🔱', colonize_ship: '⚓',
|
||||||
|
medusa: '🐍', zyklop: '👁️', harpy: '🦅', pegasus: '🐴',
|
||||||
|
minotaur: '🐂', manticore: '🦁', cerberus: '🐕',
|
||||||
|
hydra: '🐉', sea_monster: '🦑', militia: '🧑🌾'
|
||||||
|
};
|
||||||
|
|
||||||
window.renderUnitDropdown = function() {
|
window.renderUnitDropdown = function() {
|
||||||
|
// No-op - selection now happens via modal
|
||||||
|
};
|
||||||
|
|
||||||
|
window.openUnitModal = function() {
|
||||||
const town = window.getSelectedTown();
|
const town = window.getSelectedTown();
|
||||||
if (!town) return;
|
if (!town) return;
|
||||||
const uSelect = document.getElementById('unit-select');
|
const grid = document.getElementById('unit-grid');
|
||||||
const uData = town.unit_data || {};
|
const uData = town.unit_data || {};
|
||||||
|
|
||||||
const currentVal = uSelect.value;
|
// Group units into categories for better display
|
||||||
uSelect.innerHTML = '<option value="" disabled selected>-- Επιλέξτε Μονάδα --</option>';
|
const categories = {
|
||||||
|
'Ξηρά': ['sword', 'slinger', 'archer', 'hoplite', 'rider', 'chariot', 'catapult', 'godsent'],
|
||||||
|
'Ναυτικές': ['big_transporter', 'small_transporter', 'bireme', 'attack_ship', 'trireme', 'colonize_ship'],
|
||||||
|
'Μυθικές': ['medusa', 'zyklop', 'harpy', 'pegasus', 'minotaur', 'manticore', 'cerberus', 'hydra', 'sea_monster']
|
||||||
|
};
|
||||||
|
|
||||||
for (const [key, nameGr] of Object.entries(window.UNIT_NAMES_GR)) {
|
let html = '';
|
||||||
if (key === 'militia') continue;
|
|
||||||
|
for (const [catName, units] of Object.entries(categories)) {
|
||||||
|
let catHtml = '';
|
||||||
|
|
||||||
const data = uData[key];
|
for (const key of units) {
|
||||||
let text = `${nameGr}`;
|
if (key === 'militia') continue;
|
||||||
|
|
||||||
if (data) {
|
|
||||||
const w = window.fmt(data.wood || 0);
|
|
||||||
const st = window.fmt(data.stone || 0);
|
|
||||||
const i = window.fmt(data.iron || 0);
|
|
||||||
const pop = data.pop || 0;
|
|
||||||
|
|
||||||
// Unit build_time is usually raw seconds in GameData
|
const nameGr = window.UNIT_NAMES_GR[key] || key;
|
||||||
let t = data.build_time || 0;
|
const data = uData[key];
|
||||||
let tStr = `${t}s`;
|
const icon = window.UNIT_ICONS[key] || '💂';
|
||||||
if (t > 60) {
|
const isSelected = key === window.selectedUnitId;
|
||||||
let m = Math.floor(t / 60);
|
|
||||||
let s = t % 60;
|
|
||||||
tStr = `${m}m ${s}s`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const costStr = `Ξ:${w} Π:${st} Α:${i} 🧔:${pop} · ⏱ ${tStr}`;
|
let statusClass, statusLabel, cardClass = '';
|
||||||
|
let costStr = '';
|
||||||
|
let clickable = false;
|
||||||
|
|
||||||
|
const requiredGod = window.UNIT_GODS ? window.UNIT_GODS[key] : null;
|
||||||
|
const isWrongGod = requiredGod && town.god !== requiredGod;
|
||||||
|
const isNoGod = key === 'godsent' && !town.god;
|
||||||
|
|
||||||
const missingKeys = data.missing_dependencies ? Object.keys(data.missing_dependencies) : [];
|
if (isWrongGod) {
|
||||||
const isLocked = missingKeys.length > 0;
|
statusClass = 'locked'; statusLabel = '🔒 Άλλος Θεός'; cardClass = 'bld-locked';
|
||||||
|
const greekGods = { zeus: 'Δία', poseidon: 'Ποσειδώνα', hera: 'Ήρα', athena: 'Αθηνά', hades: 'Άδη', artemis: 'Άρτεμις', aphrodite: 'Αφροδίτη', ares: 'Άρη' };
|
||||||
const option = document.createElement('option');
|
costStr = `Απαιτεί: ${greekGods[requiredGod] || requiredGod}`;
|
||||||
option.value = key;
|
} else if (isNoGod) {
|
||||||
|
statusClass = 'locked'; statusLabel = '🔒 Χωρίς Θεό'; cardClass = 'bld-locked';
|
||||||
if (isLocked) {
|
costStr = `Απαιτείται Ναός`;
|
||||||
option.textContent = `${text} — 🔒 Κλειδωμένο`;
|
} else if (data) {
|
||||||
option.style.color = '#ff4444';
|
const missingKeys = data.missing_dependencies ? Object.keys(data.missing_dependencies) : [];
|
||||||
} else if (data.enough_resources === false) {
|
const isLocked = missingKeys.length > 0;
|
||||||
option.textContent = `${text} — ❌ ${costStr} (Λείπουν Πόροι 1x)`;
|
|
||||||
option.style.color = '#aa5555';
|
const w = window.fmt(data.wood || 0);
|
||||||
|
const st = window.fmt(data.stone || 0);
|
||||||
|
const i = window.fmt(data.iron || 0);
|
||||||
|
const pop = data.pop || 0;
|
||||||
|
|
||||||
|
let t = data.build_time || 0;
|
||||||
|
let tStr = `${t}s`;
|
||||||
|
if (t > 60) {
|
||||||
|
let m = Math.floor(t / 60);
|
||||||
|
let s = t % 60;
|
||||||
|
tStr = `${m}m ${s}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show favor if it's a mythical unit or godsent
|
||||||
|
const favorStr = (data.favor && data.favor > 0) ? ` ⚡:${data.favor}` : '';
|
||||||
|
costStr = `Ξ:${w} Π:${st} Α:${i}${favorStr} 🧔:${pop} · ⏱ ${tStr}`;
|
||||||
|
|
||||||
|
if (isLocked) {
|
||||||
|
statusClass = 'locked'; statusLabel = '🔒 Κλειδωμένο'; cardClass = 'bld-locked';
|
||||||
|
} else if (data.enough_resources === false) {
|
||||||
|
statusClass = 'no-resources'; statusLabel = '❌ Λείπουν Πόροι';
|
||||||
|
} else {
|
||||||
|
statusClass = 'can-build'; statusLabel = '✅ Διαθέσιμο';
|
||||||
|
clickable = true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
option.textContent = `${text} — ✅ ${costStr}`;
|
// If no data is available for this unit, treat it as locked/unknown
|
||||||
|
statusClass = 'locked'; statusLabel = '🔒 Άγνωστο'; cardClass = 'bld-locked';
|
||||||
}
|
}
|
||||||
|
|
||||||
uSelect.appendChild(option);
|
const onclick = clickable ? `onclick="window.selectUnit('${key}', '${nameGr}')"` : '';
|
||||||
} else {
|
|
||||||
const option = document.createElement('option');
|
catHtml += `<div class="bld-card ${cardClass}${isSelected ? ' bld-selected' : ''}" ${onclick} style="width:140px; justify-content:flex-start;">
|
||||||
option.value = key;
|
<span class="bld-icon">${icon}</span>
|
||||||
option.textContent = text;
|
<span class="bld-name" style="margin-top:6px; font-size:0.85rem;">${nameGr}</span>
|
||||||
uSelect.appendChild(option);
|
<span class="bld-status ${statusClass}">${statusLabel}</span>
|
||||||
|
<span class="bld-cost" style="font-size:0.65rem;">${costStr}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (catHtml) {
|
||||||
|
html += `<div style="width:100%; margin-top:10px;"><h4 style="color:#c8a44a; margin-bottom:10px; border-bottom:1px solid #2a4a6a; padding-bottom:4px;">${catName}</h4>
|
||||||
|
<div style="display:flex; flex-wrap:wrap; gap:10px;">${catHtml}</div>
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentVal && Array.from(uSelect.options).some(o => o.value === currentVal)) {
|
grid.innerHTML = html;
|
||||||
uSelect.value = currentVal;
|
document.getElementById('unit-modal-overlay').classList.add('open');
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.closeUnitModal = function(e) {
|
||||||
|
if (e && e.target !== document.getElementById('unit-modal-overlay') && e.target !== document.getElementById('unit-modal-close')) return;
|
||||||
|
document.getElementById('unit-modal-overlay').classList.remove('open');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.selectUnit = function(key, nameGr) {
|
||||||
|
window.selectedUnitId = key;
|
||||||
|
window.updateSelectionDisplay();
|
||||||
|
window.openUnitModal();
|
||||||
|
setTimeout(() => document.getElementById('unit-modal-overlay').classList.remove('open'), 180);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
|
||||||
window.renderBuildQueuePreview = function() {
|
window.renderBuildQueuePreview = function() {
|
||||||
const town = window.getSelectedTown();
|
const town = window.getSelectedTown();
|
||||||
const el = document.getElementById('build-queue-preview');
|
const el = document.getElementById('build-queue-preview');
|
||||||
|
|||||||
@@ -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 = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
<button id="btn-send" class="btn btn-gold" onclick="window.sendCommand()" style="margin-left: auto; padding: 10px 20px; font-size: 1rem;">Send ⚡</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Research options -->
|
|
||||||
<div class="form-group" id="research-options" style="display:none">
|
|
||||||
<label>Έρευνα</label>
|
|
||||||
<button class="btn btn-gold" id="open-academy-modal" onclick="window.openAcademyModal()" style="text-align:left; min-width:200px;">
|
|
||||||
<span id="selected-research-label">-- Επιλέξτε Έρευνα --</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Recruit options -->
|
|
||||||
<div 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -341,6 +341,11 @@
|
|||||||
<button class="save-btn" onclick="saveBotSettings()">💾 Αποθήκευση</button>
|
<button class="save-btn" onclick="saveBotSettings()">💾 Αποθήκευση</button>
|
||||||
<span class="save-status" id="bot-save-status">✓ Αποθηκεύτηκε</span>
|
<span class="save-status" id="bot-save-status">✓ Αποθηκεύτηκε</span>
|
||||||
|
|
||||||
|
<div style="margin-top: 1rem; border-top: 1px solid #1a3040; padding-top: 1rem;">
|
||||||
|
<button class="save-btn" id="bootcamp-attack-btn" onclick="attackBootcampNow()" style="background: linear-gradient(135deg, #7a2a2a, #cc4a4a); width: 100%;">⚔️ Επίθεση Τώρα</button>
|
||||||
|
<div style="text-align: center; margin-top: 5px;"><span class="save-status" id="bootcamp-attack-status">Εντολή εστάλη!</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3 style="margin-top:1.5rem;font-size:0.9rem;color:#aaa;">📋 Ιστορικό</h3>
|
<h3 style="margin-top:1.5rem;font-size:0.9rem;color:#aaa;">📋 Ιστορικό</h3>
|
||||||
<div id="bootcamp-log" style="background:#0a1520;border:1px solid #1a3040;border-radius:8px;padding:10px;max-height:180px;overflow-y:auto;font-size:0.78rem;font-family:monospace;color:#8ab4d0;">
|
<div id="bootcamp-log" style="background:#0a1520;border:1px solid #1a3040;border-radius:8px;padding:10px;max-height:180px;overflow-y:auto;font-size:0.78rem;font-family:monospace;color:#8ab4d0;">
|
||||||
<span style="color:#444;">Αναμονή δεδομένων...</span>
|
<span style="color:#444;">Αναμονή δεδομένων...</span>
|
||||||
@@ -406,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 --
|
||||||
@@ -440,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(() => {
|
||||||
@@ -452,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;
|
||||||
@@ -475,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 || [];
|
||||||
@@ -558,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 }
|
||||||
})
|
})
|
||||||
@@ -585,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 }
|
||||||
})
|
})
|
||||||
@@ -600,9 +606,34 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function attackBootcampNow() {
|
||||||
|
const btn = document.getElementById('bootcamp-attack-btn');
|
||||||
|
const status = document.getElementById('bootcamp-attack-status');
|
||||||
|
const originalText = btn.innerText;
|
||||||
|
|
||||||
|
btn.innerText = '⏳ Αποστολή...';
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
fetch('/dashboard/bootcamp-attack-now', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ player_id: PLAYER_ID, world_id: WORLD_ID })
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(() => {
|
||||||
|
btn.innerText = '✓ Εστάλη!';
|
||||||
|
status.style.opacity = '1';
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.innerText = originalText;
|
||||||
|
btn.disabled = false;
|
||||||
|
status.style.opacity = '0';
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function checkWarehouseStatus() {
|
async function checkWarehouseStatus() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/farm_status?player_id=${PLAYER_ID}`);
|
const res = await fetch(`/api/farm_status?player_id=${PLAYER_ID}&world_id=${WORLD_ID}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const banner = document.getElementById('warehouse-full-banner');
|
const banner = document.getElementById('warehouse-full-banner');
|
||||||
if (banner) banner.style.display = data.warehouse_full ? 'flex' : 'none';
|
if (banner) banner.style.display = data.warehouse_full ? 'flex' : 'none';
|
||||||
@@ -620,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;
|
||||||
@@ -638,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,
|
||||||
@@ -677,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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user