update add modules

This commit is contained in:
2026-04-23 18:45:54 +03:00
parent aec99fa00d
commit 54ec9a3db6
6 changed files with 826 additions and 10 deletions

View File

@@ -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 500ms1500ms (never below 500ms)
// Between-town-group delay: random 30s90s
// ----------------------------------------------------------------
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
View File

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

View File

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

View File

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