update add modules
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name Grepolis Remote Control
|
// @name Grepolis Remote Control
|
||||||
// @namespace http://tampermonkey.net/
|
// @namespace http://tampermonkey.net/
|
||||||
// @version 3.2
|
// @version 3.3
|
||||||
// @description Polls grepo.haunter-pets.top for remote commands and executes them in-game (Multi-Player)
|
// @description Polls grepo.haunter-pets.top for remote commands and executes them in-game (Multi-Player)
|
||||||
// @author Dimitrios
|
// @author Dimitrios
|
||||||
// @match https://*.grepolis.com/game/*
|
// @match https://*.grepolis.com/game/*
|
||||||
@@ -241,12 +241,35 @@
|
|||||||
}
|
}
|
||||||
} catch (e) { log(`Failed to gather unit data: ${e}`); }
|
} catch (e) { log(`Failed to gather unit data: ${e}`); }
|
||||||
|
|
||||||
|
// ---- Farm town data -----------------------------------------------
|
||||||
|
let farms = [];
|
||||||
|
try {
|
||||||
|
const farmCollection = uw.MM.getOnlyCollectionByName('FarmTown');
|
||||||
|
const relCollection = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation');
|
||||||
|
if (farmCollection && relCollection) {
|
||||||
|
const ix = town.getIslandCoordinateX?.();
|
||||||
|
const iy = town.getIslandCoordinateY?.();
|
||||||
|
farmCollection.models.forEach(farm => {
|
||||||
|
if (farm.attributes.island_x !== ix || farm.attributes.island_y !== iy) return;
|
||||||
|
relCollection.models.forEach(rel => {
|
||||||
|
if (rel.attributes.farm_town_id === farm.attributes.id &&
|
||||||
|
rel.attributes.relation_status === 1) {
|
||||||
|
farms.push({
|
||||||
|
farm_town_id: farm.attributes.id,
|
||||||
|
relation_id: rel.id,
|
||||||
|
lootable_at: rel.attributes.lootable_at || 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
|
||||||
// ---- Extra town flags -----------------------------------------------
|
// ---- Extra town flags -----------------------------------------------
|
||||||
let has_premium = false;
|
let has_premium = false;
|
||||||
let bonuses = {};
|
let bonuses = {};
|
||||||
let wonder_points = 0;
|
let wonder_points = 0;
|
||||||
try {
|
try {
|
||||||
// Proper premium check for Admin/Curator
|
|
||||||
has_premium = uw.GameDataPremium?.isAdvisorActivated?.('curator') || false;
|
has_premium = uw.GameDataPremium?.isAdvisorActivated?.('curator') || false;
|
||||||
} catch (e) { }
|
} catch (e) { }
|
||||||
|
|
||||||
@@ -272,6 +295,7 @@
|
|||||||
bonuses,
|
bonuses,
|
||||||
wonder_points,
|
wonder_points,
|
||||||
alliance_name,
|
alliance_name,
|
||||||
|
farms,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -382,6 +406,94 @@
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Execute: Farm Loot
|
||||||
|
// Claims all ready farm towns across all towns that match
|
||||||
|
// the cmd payload (town_ids list + loot_option).
|
||||||
|
// Between-claim delay: random 500ms–1500ms (never below 500ms)
|
||||||
|
// Between-town-group delay: random 30s–90s
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
async function executeFarmLoot(cmd) {
|
||||||
|
const { loot_option } = cmd.payload || {};
|
||||||
|
const option = parseInt(loot_option) || 1;
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
let farmCollection, relCollection;
|
||||||
|
try {
|
||||||
|
farmCollection = uw.MM.getOnlyCollectionByName('FarmTown');
|
||||||
|
relCollection = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation');
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, msg: `Cannot access farm collections: ${e.message}` };
|
||||||
|
}
|
||||||
|
if (!farmCollection || !relCollection) {
|
||||||
|
return { ok: false, msg: 'Farm collections not loaded yet' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const towns = uw.ITowns?.towns || {};
|
||||||
|
const islandsSeen = new Set();
|
||||||
|
const polisList = []; // one town per island
|
||||||
|
for (const town of Object.values(towns)) {
|
||||||
|
const iid = town.attributes?.island_id;
|
||||||
|
if (town.attributes?.on_small_island || !iid) continue;
|
||||||
|
if (!islandsSeen.has(iid)) {
|
||||||
|
islandsSeen.add(iid);
|
||||||
|
polisList.push(town.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let claimed = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
let errors = 0;
|
||||||
|
|
||||||
|
for (const town_id of polisList) {
|
||||||
|
const town = uw.ITowns.towns[town_id];
|
||||||
|
if (!town) continue;
|
||||||
|
const ix = town.getIslandCoordinateX?.();
|
||||||
|
const iy = town.getIslandCoordinateY?.();
|
||||||
|
|
||||||
|
const readyFarms = [];
|
||||||
|
farmCollection.models.forEach(farm => {
|
||||||
|
if (farm.attributes.island_x !== ix || farm.attributes.island_y !== iy) return;
|
||||||
|
relCollection.models.forEach(rel => {
|
||||||
|
if (rel.attributes.farm_town_id === farm.attributes.id &&
|
||||||
|
rel.attributes.relation_status === 1 &&
|
||||||
|
(rel.attributes.lootable_at || 0) <= now) {
|
||||||
|
readyFarms.push({
|
||||||
|
farm_town_id: rel.attributes.farm_town_id,
|
||||||
|
relation_id: rel.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (readyFarms.length === 0) { skipped++; continue; }
|
||||||
|
|
||||||
|
for (const farm of readyFarms) {
|
||||||
|
try {
|
||||||
|
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
|
||||||
|
model_url: `FarmTownPlayerRelation/${farm.relation_id}`,
|
||||||
|
action_name: 'claim',
|
||||||
|
arguments: { farm_town_id: farm.farm_town_id, type: 'resources', option },
|
||||||
|
town_id
|
||||||
|
});
|
||||||
|
claimed++;
|
||||||
|
} catch (e) { errors++; }
|
||||||
|
|
||||||
|
// Random per-claim delay: 500ms – 1500ms
|
||||||
|
await sleep(randInt(500, 1500));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Random between-town-group delay: 30s – 90s
|
||||||
|
if (polisList.indexOf(town_id) < polisList.length - 1) {
|
||||||
|
const gap = randInt(30000, 90000);
|
||||||
|
log(`Farm: done with town ${town_id}. Waiting ${(gap/1000).toFixed(0)}s before next...`);
|
||||||
|
await sleep(gap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true, msg: `Farm done: ${claimed} claimed, ${skipped} towns skipped, ${errors} errors` };
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
// Execute: Build
|
// Execute: Build
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
@@ -520,6 +632,25 @@
|
|||||||
const buildCmd = cmdData.build;
|
const buildCmd = cmdData.build;
|
||||||
const recruitCmd = cmdData.recruit;
|
const recruitCmd = cmdData.recruit;
|
||||||
const marketCmd = cmdData.market;
|
const marketCmd = cmdData.market;
|
||||||
|
const farmCmd = cmdData.farm;
|
||||||
|
|
||||||
|
// Auto-farm: if enabled, claim all ready farms (no explicit command needed)
|
||||||
|
const farmSettings = cmdData.farm_settings || {};
|
||||||
|
if (farmSettings.enabled && !farmCmd) {
|
||||||
|
const nowTs = Math.floor(Date.now() / 1000);
|
||||||
|
// Check if any town has ready farms before triggering
|
||||||
|
const hasFarms = Object.values(uw.ITowns?.towns || {}).some(t => {
|
||||||
|
try {
|
||||||
|
const coll = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation');
|
||||||
|
return coll?.models?.some(r => r.attributes.relation_status === 1 && (r.attributes.lootable_at || 0) <= nowTs);
|
||||||
|
} catch(e) { return false; }
|
||||||
|
});
|
||||||
|
if (hasFarms) {
|
||||||
|
log('⚡ Auto-farm: ready farms detected, triggering loot...');
|
||||||
|
await executeFarmLoot({ payload: { loot_option: farmSettings.loot_option } });
|
||||||
|
pushState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (cmdData.sync_requested) {
|
if (cmdData.sync_requested) {
|
||||||
log('Sync requested by server — pushing state immediately');
|
log('Sync requested by server — pushing state immediately');
|
||||||
@@ -534,6 +665,7 @@
|
|||||||
if (cmd.type === 'build') result = await executeBuild(cmd);
|
if (cmd.type === 'build') result = await executeBuild(cmd);
|
||||||
else if (cmd.type === 'recruit') result = await executeRecruit(cmd);
|
else if (cmd.type === 'recruit') result = await executeRecruit(cmd);
|
||||||
else if (cmd.type === 'market_offer') result = await executeMarketOffer(cmd);
|
else if (cmd.type === 'market_offer') result = await executeMarketOffer(cmd);
|
||||||
|
else if (cmd.type === 'farm_loot') result = await executeFarmLoot(cmd);
|
||||||
else result = { ok: false, msg: `Unknown type: ${cmd.type}` };
|
else result = { ok: false, msg: `Unknown type: ${cmd.type}` };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
result = { ok: false, msg: `Exception: ${e.message}` };
|
result = { ok: false, msg: `Exception: ${e.message}` };
|
||||||
@@ -545,13 +677,14 @@
|
|||||||
|
|
||||||
// Run concurrently — they do NOT block each other
|
// Run concurrently — they do NOT block each other
|
||||||
await Promise.all([execute(buildCmd), execute(recruitCmd), execute(marketCmd)]);
|
await Promise.all([execute(buildCmd), execute(recruitCmd), execute(marketCmd)]);
|
||||||
|
if (farmCmd) await execute(farmCmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
// Boot
|
// Boot
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
log('Grepolis Remote Control v2.8 loaded');
|
log('Grepolis Remote Control v3.3 loaded');
|
||||||
|
|
||||||
// Start captcha watcher immediately
|
// Start captcha watcher immediately
|
||||||
detectCaptcha();
|
detectCaptcha();
|
||||||
|
|||||||
10
db.py
10
db.py
@@ -55,6 +55,16 @@ def init_db():
|
|||||||
)
|
)
|
||||||
''')
|
''')
|
||||||
|
|
||||||
|
# Farm settings — per-player auto-farm configuration
|
||||||
|
c.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS farm_settings (
|
||||||
|
player_id TEXT PRIMARY KEY,
|
||||||
|
enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
loot_option INTEGER NOT NULL DEFAULT 1, -- 1=5min, 2=20min, 3=90min, 4=4h
|
||||||
|
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',
|
||||||
|
|||||||
@@ -98,8 +98,18 @@ def get_pending_command():
|
|||||||
build_cmd = _fetch_pending_of_type(c, 'build', player_id)
|
build_cmd = _fetch_pending_of_type(c, 'build', player_id)
|
||||||
recruit_cmd = _fetch_pending_of_type(c, 'recruit', player_id)
|
recruit_cmd = _fetch_pending_of_type(c, 'recruit', player_id)
|
||||||
market_cmd = _fetch_pending_of_type(c, 'market_offer', player_id)
|
market_cmd = _fetch_pending_of_type(c, 'market_offer', player_id)
|
||||||
|
farm_cmd = _fetch_pending_of_type(c, 'farm_loot', player_id)
|
||||||
sync_req = _check_and_reset_sync(c, player_id)
|
sync_req = _check_and_reset_sync(c, player_id)
|
||||||
|
|
||||||
|
# Also return current farm settings so TM knows loot_option
|
||||||
|
farm_row = c.execute(
|
||||||
|
'SELECT enabled, loot_option FROM farm_settings WHERE player_id = ?', (player_id,)
|
||||||
|
).fetchone()
|
||||||
|
farm_settings = {
|
||||||
|
'enabled': bool(farm_row['enabled']) if farm_row else False,
|
||||||
|
'loot_option': farm_row['loot_option'] if farm_row else 1
|
||||||
|
}
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
@@ -107,6 +117,8 @@ def get_pending_command():
|
|||||||
'build': build_cmd,
|
'build': build_cmd,
|
||||||
'recruit': recruit_cmd,
|
'recruit': recruit_cmd,
|
||||||
'market': market_cmd,
|
'market': market_cmd,
|
||||||
|
'farm': farm_cmd,
|
||||||
|
'farm_settings': farm_settings,
|
||||||
'sync_requested': sync_req
|
'sync_requested': sync_req
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -49,9 +49,85 @@ def index():
|
|||||||
return render_template('index.html', players=players)
|
return render_template('index.html', players=players)
|
||||||
|
|
||||||
@dashboard.route('/player/<player_id>')
|
@dashboard.route('/player/<player_id>')
|
||||||
|
def player_hub(player_id):
|
||||||
|
return render_template('hub.html', player_id=player_id)
|
||||||
|
|
||||||
|
@dashboard.route('/player/<player_id>/admin')
|
||||||
def player_dashboard(player_id):
|
def player_dashboard(player_id):
|
||||||
return render_template('dashboard.html', player_id=player_id)
|
return render_template('dashboard.html', player_id=player_id)
|
||||||
|
|
||||||
|
@dashboard.route('/player/<player_id>/farm')
|
||||||
|
def player_farm(player_id):
|
||||||
|
return render_template('farm.html', player_id=player_id)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET /dashboard/farm-settings — returns current farm config
|
||||||
|
# POST /dashboard/farm-settings — updates farm config
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@dashboard.route('/dashboard/farm-settings', methods=['GET'])
|
||||||
|
def get_farm_settings():
|
||||||
|
player_id = request.args.get('player_id')
|
||||||
|
conn = get_db()
|
||||||
|
row = conn.execute(
|
||||||
|
'SELECT enabled, loot_option FROM farm_settings WHERE player_id = ?', (player_id,)
|
||||||
|
).fetchone()
|
||||||
|
conn.close()
|
||||||
|
if row:
|
||||||
|
return jsonify({'enabled': bool(row['enabled']), 'loot_option': row['loot_option']})
|
||||||
|
return jsonify({'enabled': False, 'loot_option': 1})
|
||||||
|
|
||||||
|
@dashboard.route('/dashboard/farm-settings', methods=['POST'])
|
||||||
|
def set_farm_settings():
|
||||||
|
data = request.get_json(silent=True)
|
||||||
|
if not data or 'player_id' not in data:
|
||||||
|
return jsonify({'error': 'missing player_id'}), 400
|
||||||
|
player_id = data['player_id']
|
||||||
|
enabled = 1 if data.get('enabled') else 0
|
||||||
|
loot_option = int(data.get('loot_option', 1))
|
||||||
|
conn = get_db()
|
||||||
|
conn.execute('''
|
||||||
|
INSERT INTO farm_settings (player_id, enabled, loot_option, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
ON CONFLICT(player_id) DO UPDATE SET
|
||||||
|
enabled = excluded.enabled,
|
||||||
|
loot_option = excluded.loot_option,
|
||||||
|
updated_at = excluded.updated_at
|
||||||
|
''', (player_id, enabled, loot_option, datetime.utcnow().isoformat()))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'ok': True})
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET /dashboard/farm-data
|
||||||
|
# Returns ready-to-loot farm towns for a player across all towns
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@dashboard.route('/dashboard/farm-data', methods=['GET'])
|
||||||
|
def get_farm_data():
|
||||||
|
player_id = request.args.get('player_id')
|
||||||
|
conn = get_db()
|
||||||
|
rows = conn.execute(
|
||||||
|
'SELECT town_id, town_name, data FROM town_state WHERE player_id = ?', (player_id,)
|
||||||
|
).fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
now_ts = int(datetime.utcnow().timestamp())
|
||||||
|
farms_summary = []
|
||||||
|
for row in rows:
|
||||||
|
d = json.loads(row['data'])
|
||||||
|
farm_data = d.get('farms', [])
|
||||||
|
ready = [f for f in farm_data if f.get('lootable_at', 0) <= now_ts]
|
||||||
|
if farm_data:
|
||||||
|
farms_summary.append({
|
||||||
|
'town_id': row['town_id'],
|
||||||
|
'town_name': row['town_name'],
|
||||||
|
'total_farms': len(farm_data),
|
||||||
|
'ready_farms': len(ready),
|
||||||
|
'next_ready_at': min((f['lootable_at'] for f in farm_data if f.get('lootable_at', 0) > now_ts), default=None)
|
||||||
|
})
|
||||||
|
return jsonify(farms_summary)
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# GET /dashboard/towns
|
# GET /dashboard/towns
|
||||||
@@ -190,8 +266,8 @@ def create_command():
|
|||||||
return jsonify({'error': f'missing field: {field}'}), 400
|
return jsonify({'error': f'missing field: {field}'}), 400
|
||||||
|
|
||||||
cmd_type = data['type']
|
cmd_type = data['type']
|
||||||
if cmd_type not in ('build', 'recruit', 'market_offer'):
|
if cmd_type not in ('build', 'recruit', 'market_offer', 'farm_loot'):
|
||||||
return jsonify({'error': 'type must be build, recruit, or market_offer'}), 400
|
return jsonify({'error': 'type must be build, recruit, market_offer, or farm_loot'}), 400
|
||||||
|
|
||||||
# Reject if the Tampermonkey client is offline (no state push in last 150 s)
|
# Reject if the Tampermonkey client is offline (no state push in last 150 s)
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
|
|||||||
387
templates/farm.html
Normal file
387
templates/farm.html
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="el">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Farm Manager — Grepolis Remote</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
background: #0f0f1a;
|
||||||
|
font-family: 'Inter', 'Segoe UI', sans-serif;
|
||||||
|
color: #e0e0e0;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Top bar ---- */
|
||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.topbar a {
|
||||||
|
color: #666;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
.topbar a:hover { color: #c8a44a; }
|
||||||
|
.topbar h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #4acc64;
|
||||||
|
}
|
||||||
|
.online-dot {
|
||||||
|
width: 9px; height: 9px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #cc7b7b;
|
||||||
|
box-shadow: 0 0 6px #cc7b7b;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.online-dot.live { background: #7bcc7b; box-shadow: 0 0 6px #7bcc7b; }
|
||||||
|
|
||||||
|
/* ---- Control Panel ---- */
|
||||||
|
.panel {
|
||||||
|
background: #1a1a28;
|
||||||
|
border: 1px solid #2a2a40;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 1.75rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.panel h2 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #aaa;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle switch */
|
||||||
|
.toggle-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.toggle-label { font-size: 1rem; font-weight: 600; }
|
||||||
|
.toggle {
|
||||||
|
position: relative;
|
||||||
|
width: 56px;
|
||||||
|
height: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.toggle input { opacity: 0; width: 0; height: 0; }
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: #2a2a3a;
|
||||||
|
border-radius: 30px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
border: 1px solid #3a3a50;
|
||||||
|
}
|
||||||
|
.slider::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 22px; height: 22px;
|
||||||
|
left: 4px; top: 3px;
|
||||||
|
background: #666;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: transform 0.3s, background 0.3s;
|
||||||
|
}
|
||||||
|
input:checked + .slider { background: rgba(74,204,100,0.25); border-color: #4acc64; }
|
||||||
|
input:checked + .slider::before { transform: translateX(26px); background: #4acc64; }
|
||||||
|
|
||||||
|
/* Loot option selector */
|
||||||
|
.option-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.option-btn {
|
||||||
|
background: #12121e;
|
||||||
|
border: 2px solid #2a2a40;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0.75rem 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
transition: border-color 0.2s, background 0.2s;
|
||||||
|
color: #888;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.option-btn .opt-time { font-size: 1.1rem; font-weight: 700; color: #ccc; display: block; }
|
||||||
|
.option-btn .opt-name { font-size: 0.72rem; color: #666; margin-top: 2px; display: block; }
|
||||||
|
.option-btn:hover { border-color: #4acc64; background: rgba(74,204,100,0.05); }
|
||||||
|
.option-btn.selected { border-color: #4acc64; background: rgba(74,204,100,0.12); color: #4acc64; }
|
||||||
|
.option-btn.selected .opt-time { color: #4acc64; }
|
||||||
|
.option-btn.selected .opt-name { color: #4acc64bb; }
|
||||||
|
|
||||||
|
.save-btn {
|
||||||
|
background: linear-gradient(135deg, #2a7a3a, #4acc64);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0.7rem 2rem;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s, transform 0.1s;
|
||||||
|
}
|
||||||
|
.save-btn:hover { opacity: 0.9; transform: translateY(-1px); }
|
||||||
|
.save-btn:active { transform: translateY(0); }
|
||||||
|
|
||||||
|
.save-status {
|
||||||
|
margin-left: 1rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #4acc64;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
.save-status.visible { opacity: 1; }
|
||||||
|
|
||||||
|
/* ---- Farm Table ---- */
|
||||||
|
.farm-table-wrap { overflow-x: auto; }
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
thead th {
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
border-bottom: 1px solid #2a2a40;
|
||||||
|
}
|
||||||
|
tbody tr { border-bottom: 1px solid #1e1e2e; transition: background 0.15s; }
|
||||||
|
tbody tr:hover { background: rgba(255,255,255,0.03); }
|
||||||
|
tbody td { padding: 0.75rem 1rem; }
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.badge.ready { background: rgba(74,204,100,0.2); color: #4acc64; border: 1px solid rgba(74,204,100,0.35); }
|
||||||
|
.badge.waiting { background: rgba(150,150,200,0.1); color: #888; border: 1px solid #2a2a40; }
|
||||||
|
.countdown { font-size: 0.82rem; color: #6fcfcf; }
|
||||||
|
|
||||||
|
/* Status bar */
|
||||||
|
.status-bar {
|
||||||
|
background: rgba(74,204,100,0.08);
|
||||||
|
border: 1px solid rgba(74,204,100,0.2);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #4acc64;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.status-bar.visible { display: block; }
|
||||||
|
.status-bar.off {
|
||||||
|
background: rgba(200,100,100,0.08);
|
||||||
|
border-color: rgba(200,100,100,0.2);
|
||||||
|
color: #cc7b7b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty state */
|
||||||
|
.empty-state { text-align: center; padding: 3rem 1rem; color: #555; }
|
||||||
|
.empty-state p { margin-top: 0.5rem; font-size: 0.9rem; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="topbar">
|
||||||
|
<a href="/player/{{ player_id }}">← Πίσω</a>
|
||||||
|
<h1>🌾 Farm Manager</h1>
|
||||||
|
<span class="online-dot" id="online-dot" title="Κατάσταση Script"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status banner -->
|
||||||
|
<div class="status-bar" id="status-bar"></div>
|
||||||
|
|
||||||
|
<!-- Control Panel -->
|
||||||
|
<div class="panel">
|
||||||
|
<h2>⚙️ Ρυθμίσεις</h2>
|
||||||
|
|
||||||
|
<div class="toggle-row">
|
||||||
|
<span class="toggle-label">Αυτόματη Λεηλασία</span>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" id="farm-enabled">
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
<span style="color:#888; font-size:0.85rem;" id="toggle-hint">Ανενεργό</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 0.75rem; font-size: 0.85rem; color: #888;">Επίπεδο Λεηλασίας:</div>
|
||||||
|
<div class="option-grid">
|
||||||
|
<button class="option-btn selected" data-option="1">
|
||||||
|
<span class="opt-time">5'</span>
|
||||||
|
<span class="opt-name">Γρήγορο</span>
|
||||||
|
</button>
|
||||||
|
<button class="option-btn" data-option="2">
|
||||||
|
<span class="opt-time">20'</span>
|
||||||
|
<span class="opt-name">Κανονικό</span>
|
||||||
|
</button>
|
||||||
|
<button class="option-btn" data-option="3">
|
||||||
|
<span class="opt-time">90'</span>
|
||||||
|
<span class="opt-name">Αργό</span>
|
||||||
|
</button>
|
||||||
|
<button class="option-btn" data-option="4">
|
||||||
|
<span class="opt-time">4h</span>
|
||||||
|
<span class="opt-name">Ύπνος</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="save-btn" id="save-btn" onclick="saveSettings()">💾 Αποθήκευση</button>
|
||||||
|
<span class="save-status" id="save-status">✓ Αποθηκεύτηκε</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Farm Status Table -->
|
||||||
|
<div class="panel">
|
||||||
|
<h2>🏘️ Κατάσταση Χωριών</h2>
|
||||||
|
<div class="farm-table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Πόλη</th>
|
||||||
|
<th>Έτοιμα</th>
|
||||||
|
<th>Σύνολο</th>
|
||||||
|
<th>Επόμενο</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="farm-table-body">
|
||||||
|
<tr><td colspan="4"><div class="empty-state">⏳ <p>Φόρτωση δεδομένων...</p></div></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const PLAYER_ID = '{{ player_id }}';
|
||||||
|
let selectedOption = 1;
|
||||||
|
|
||||||
|
// -- Loot option buttons --
|
||||||
|
document.querySelectorAll('.option-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('.option-btn').forEach(b => b.classList.remove('selected'));
|
||||||
|
btn.classList.add('selected');
|
||||||
|
selectedOption = parseInt(btn.dataset.option);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -- Toggle hint text --
|
||||||
|
document.getElementById('farm-enabled').addEventListener('change', function () {
|
||||||
|
document.getElementById('toggle-hint').textContent = this.checked ? '🟢 Ενεργό' : 'Ανενεργό';
|
||||||
|
updateStatusBar(this.checked);
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateStatusBar(enabled) {
|
||||||
|
const bar = document.getElementById('status-bar');
|
||||||
|
if (enabled) {
|
||||||
|
bar.className = 'status-bar visible';
|
||||||
|
bar.textContent = '🤖 Ο αυτόματος farmer είναι ενεργός. Το script θα λεηλατεί χωριά με τυχαίες καθυστερήσεις.';
|
||||||
|
} else {
|
||||||
|
bar.className = 'status-bar visible off';
|
||||||
|
bar.textContent = '⏸ Η αυτόματη λεηλασία είναι ανενεργή.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Save settings --
|
||||||
|
function saveSettings() {
|
||||||
|
const enabled = document.getElementById('farm-enabled').checked;
|
||||||
|
fetch('/dashboard/farm-settings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ player_id: PLAYER_ID, enabled, loot_option: selectedOption })
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(() => {
|
||||||
|
const s = document.getElementById('save-status');
|
||||||
|
s.classList.add('visible');
|
||||||
|
setTimeout(() => s.classList.remove('visible'), 2500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Load current settings --
|
||||||
|
function loadSettings() {
|
||||||
|
fetch(`/dashboard/farm-settings?player_id=${PLAYER_ID}`)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(cfg => {
|
||||||
|
document.getElementById('farm-enabled').checked = cfg.enabled;
|
||||||
|
document.getElementById('toggle-hint').textContent = cfg.enabled ? '🟢 Ενεργό' : 'Ανενεργό';
|
||||||
|
if (cfg.enabled) updateStatusBar(true);
|
||||||
|
selectedOption = cfg.loot_option || 1;
|
||||||
|
document.querySelectorAll('.option-btn').forEach(b => {
|
||||||
|
b.classList.toggle('selected', parseInt(b.dataset.option) === selectedOption);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Load farm data table --
|
||||||
|
function loadFarmData() {
|
||||||
|
fetch(`/dashboard/farm-data?player_id=${PLAYER_ID}`)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
const tbody = document.getElementById('farm-table-body');
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="4"><div class="empty-state">🌱 <p>Δεν υπάρχουν δεδομένα χωριών ακόμη.<br>Βεβαιώσου ότι το script v3.3+ τρέχει στο παιχνίδι.</p></div></td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
tbody.innerHTML = data.map(t => {
|
||||||
|
const readyBadge = t.ready_farms > 0
|
||||||
|
? `<span class="badge ready">✓ ${t.ready_farms} Έτοιμα</span>`
|
||||||
|
: `<span class="badge waiting">Αναμονή</span>`;
|
||||||
|
let nextStr = '—';
|
||||||
|
if (t.next_ready_at) {
|
||||||
|
const diff = t.next_ready_at - now;
|
||||||
|
if (diff > 0) {
|
||||||
|
const m = Math.floor(diff / 60);
|
||||||
|
const s = diff % 60;
|
||||||
|
nextStr = `<span class="countdown">${m}λ ${s}δ</span>`;
|
||||||
|
} else {
|
||||||
|
nextStr = '<span class="countdown">Τώρα</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `<tr>
|
||||||
|
<td><strong>${t.town_name}</strong></td>
|
||||||
|
<td>${readyBadge}</td>
|
||||||
|
<td><span style="color:#888">${t.total_farms}</span></td>
|
||||||
|
<td>${nextStr}</td>
|
||||||
|
</tr>`;
|
||||||
|
}).join('');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Online status check --
|
||||||
|
function checkOnline() {
|
||||||
|
fetch(`/dashboard/client-status?player_id=${PLAYER_ID}`)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => {
|
||||||
|
const dot = document.getElementById('online-dot');
|
||||||
|
dot.className = 'online-dot' + (d.online ? ' live' : '');
|
||||||
|
dot.title = d.online ? 'Script Online' : 'Script Offline';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Boot --
|
||||||
|
loadSettings();
|
||||||
|
loadFarmData();
|
||||||
|
checkOnline();
|
||||||
|
setInterval(loadFarmData, 15000);
|
||||||
|
setInterval(checkOnline, 20000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
198
templates/hub.html
Normal file
198
templates/hub.html
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="el">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Grepolis Remote — Hub</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #0f0f1a;
|
||||||
|
font-family: 'Inter', 'Segoe UI', sans-serif;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
.hub-header h1 {
|
||||||
|
font-size: 2.4rem;
|
||||||
|
font-weight: 800;
|
||||||
|
background: linear-gradient(135deg, #c8a44a, #f0c96e, #c8a44a);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
.hub-header p {
|
||||||
|
color: #888;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.player-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: rgba(200, 164, 74, 0.15);
|
||||||
|
border: 1px solid rgba(200, 164, 74, 0.35);
|
||||||
|
color: #c8a44a;
|
||||||
|
padding: 4px 14px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
max-width: 900px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-card {
|
||||||
|
background: linear-gradient(160deg, #1e1e30 0%, #181824 100%);
|
||||||
|
border: 1px solid #2a2a40;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 2rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.hub-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 18px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
.hub-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 12px 40px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
.hub-card:hover::before { opacity: 1; }
|
||||||
|
|
||||||
|
/* Admin Mode — gold */
|
||||||
|
.hub-card.admin { border-color: #3a3020; }
|
||||||
|
.hub-card.admin::before { background: radial-gradient(circle at top left, rgba(200,164,74,0.08), transparent 70%); }
|
||||||
|
.hub-card.admin:hover { border-color: #c8a44a; box-shadow: 0 12px 40px rgba(200,164,74,0.15); }
|
||||||
|
|
||||||
|
/* Farm Manager — green */
|
||||||
|
.hub-card.farm { border-color: #1a3020; }
|
||||||
|
.hub-card.farm::before { background: radial-gradient(circle at top left, rgba(74,200,100,0.08), transparent 70%); }
|
||||||
|
.hub-card.farm:hover { border-color: #4acc64; box-shadow: 0 12px 40px rgba(74,200,100,0.15); }
|
||||||
|
|
||||||
|
/* Live Tracker — blue (coming soon, dimmed) */
|
||||||
|
.hub-card.tracker { border-color: #1a2030; opacity: 0.65; cursor: not-allowed; }
|
||||||
|
.hub-card.tracker:hover { transform: none; box-shadow: none; border-color: #1a2030; }
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
font-size: 2.8rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.card-title {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.hub-card.admin .card-title { color: #c8a44a; }
|
||||||
|
.hub-card.farm .card-title { color: #4acc64; }
|
||||||
|
.hub-card.tracker .card-title { color: #6fcfcf; }
|
||||||
|
|
||||||
|
.card-desc {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #888;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.soon-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
background: rgba(111,207,207,0.15);
|
||||||
|
border: 1px solid rgba(111,207,207,0.3);
|
||||||
|
color: #6fcfcf;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
margin-top: 2.5rem;
|
||||||
|
color: #555;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
.back-link:hover { color: #aaa; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="hub-header">
|
||||||
|
<h1>⚔️ Grepolis Remote</h1>
|
||||||
|
<p>Επέλεξε λειτουργία</p>
|
||||||
|
<span class="player-badge" id="player-badge">Φόρτωση...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hub-grid">
|
||||||
|
|
||||||
|
<a href="/player/{{ player_id }}/admin" class="hub-card admin">
|
||||||
|
<span class="card-icon">🏛️</span>
|
||||||
|
<div class="card-title">Admin Mode</div>
|
||||||
|
<div class="card-desc">Κτίρια, στρατολόγηση, αγορά, ουρά κατασκευών και πλήρης έλεγχος πόλεων.</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/player/{{ player_id }}/farm" class="hub-card farm">
|
||||||
|
<span class="card-icon">🌾</span>
|
||||||
|
<div class="card-title">Farm Manager</div>
|
||||||
|
<div class="card-desc">Αυτόματη συλλογή πόρων από χωριά. Ρυθμίσεις χρόνου λεηλασίας και έλεγχος με ένα κλικ.</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="hub-card tracker">
|
||||||
|
<span class="soon-badge">Σύντομα</span>
|
||||||
|
<span class="card-icon">🛡️</span>
|
||||||
|
<div class="card-title">Live Tracker</div>
|
||||||
|
<div class="card-desc">Παρακολούθηση κινήσεων στρατού σε πραγματικό χρόνο. Εισερχόμενες επιθέσεις και ενισχύσεις.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="/" class="back-link">← Επιστροφή στην επιλογή παίκτη</a>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Fetch player name to show in the badge
|
||||||
|
const playerId = '{{ player_id }}';
|
||||||
|
fetch(`/dashboard/towns?player_id=${playerId}`)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(towns => {
|
||||||
|
if (towns && towns.length > 0) {
|
||||||
|
const t = towns[0];
|
||||||
|
document.getElementById('player-badge').textContent =
|
||||||
|
`${t.player} · ${t.world_id}${t.alliance_name ? ' · ' + t.alliance_name : ''}`;
|
||||||
|
} else {
|
||||||
|
document.getElementById('player-badge').textContent = `ID: ${playerId}`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => { document.getElementById('player-badge').textContent = `ID: ${playerId}`; });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user