From b824144a6a16d5f0638cb31cb8332616dac7ff9f Mon Sep 17 00:00:00 2001 From: haunter Date: Wed, 6 May 2026 23:41:52 +0300 Subject: [PATCH] agoara update --- GrepolisRemoteControl.user.js | 81 ++++ bot_modules/02_state.js | 22 +- bot_modules/04d_execute_culture.js | 101 +++++ db.py | 43 ++ future_ideas.md | 32 ++ routes/api.py | 50 ++- routes/dashboard.py | 276 +++++++++++- templates/agora.html | 672 +++++++++++++++++++++++++++++ templates/hub.html | 12 + 9 files changed, 1283 insertions(+), 6 deletions(-) create mode 100644 bot_modules/04d_execute_culture.js create mode 100644 templates/agora.html diff --git a/GrepolisRemoteControl.user.js b/GrepolisRemoteControl.user.js index 994a1cf..fb27d9f 100644 --- a/GrepolisRemoteControl.user.js +++ b/GrepolisRemoteControl.user.js @@ -816,6 +816,84 @@ return { ok: true, msg: `Research ${research_id} queued` }; } + // ---------------------------------------------------------------- + // Execute: Culture (Αγορά) + // Fires a Γιορτή πόλης (party) or Παρέλαση θριάμβου (triumph) + // celebration for a specific town via building_place/start_celebration. + // ---------------------------------------------------------------- + async function executeCultureCommand(cmd) { + const { town_id, payload } = cmd; + const { celebration_type } = payload || {}; + + if (!celebration_type || !town_id) { + return { ok: false, msg: 'Invalid culture payload: missing celebration_type or town_id' }; + } + if (!['party', 'triumph'].includes(celebration_type)) { + return { ok: false, msg: `Unknown celebration_type: ${celebration_type}` }; + } + + const label = celebration_type === 'party' ? 'Γιορτή πόλης' : 'Παρέλαση θριάμβου'; + log(`[αγορά] Εκτέλεση ${label} για πόλη ${town_id}`); + + // Validate town exists in game memory + const town = uw.ITowns?.towns?.[town_id]; + if (!town) { + return { ok: false, msg: `Η πόλη ${town_id} δεν βρέθηκε στη μνήμη του παιχνιδιού.` }; + } + + // Double-check: no active celebration of this type running + try { + const celebModels = uw.MM.getModels()?.Celebration; + if (celebModels) { + const nowTs = Math.floor(Date.now() / 1000); + for (const cel of Object.values(celebModels)) { + const a = cel.attributes; + if (String(a.town_id) === String(town_id) + && a.celebration_type === celebration_type + && (a.finished_at ?? 0) > nowTs) { + return { ok: false, msg: `${label} ήδη ενεργή στην πόλη ${town_id}.` }; + } + } + } + } catch (e) { /* model not loaded — proceed; server already validated */ } + + const reactionMs = randInt(800, 2500); + log(`[αγορά] Waiting ${reactionMs}ms (reaction time)...`); + await sleep(reactionMs); + + if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' }; + + // Fire the Grepolis AJAX + let ajaxSuccess = false; + let ajaxError = null; + + await new Promise(resolve => { + try { + uw.gpAjax.ajaxPost( + 'building_place', + 'start_celebration', + { town_id: parseInt(town_id, 10), celebration_type }, + false, + () => { ajaxSuccess = true; resolve(); }, + (err) => { ajaxError = err ? JSON.stringify(err) : 'Άγνωστο σφάλμα AJAX'; resolve(); } + ); + } catch (e) { + ajaxError = String(e); + resolve(); + } + // 12-second safety timeout + setTimeout(() => { if (!ajaxSuccess && !ajaxError) ajaxError = 'Timeout (12s)'; resolve(); }, 12000); + }); + + if (ajaxSuccess) { + log(`[αγορά] ✅ ${label} ξεκίνησε επιτυχώς (πόλη ${town_id})`); + return { ok: true, msg: `${label} ξεκίνησε επιτυχώς.` }; + } else { + log(`[αγορά] ❌ Αποτυχία: ${ajaxError}`); + return { ok: false, msg: ajaxError || 'Αποτυχία εκτέλεσης εορτής.' }; + } + } + // ---------------------------------------------------------------- // Poll for and execute pending commands (build + recruit + market) // ---------------------------------------------------------------- @@ -841,6 +919,7 @@ const researchCmd = cmdData.research; const farmCmd = cmdData.farm; const farmUpgradeCmd = cmdData.farm_upgrade; + const cultureCmd = cmdData.culture; @@ -865,6 +944,7 @@ else if (cmd.type === 'research') result = await executeResearch(cmd); else if (cmd.type === 'farm_loot') result = await executeFarmLoot(cmd); else if (cmd.type === 'farm_upgrade') result = await executeFarmUpgrade(cmd); + else if (cmd.type === 'culture') result = await executeCultureCommand(cmd); else result = { ok: false, msg: `Unknown type: ${cmd.type}` }; } catch (e) { result = { ok: false, msg: `Exception: ${e}` }; @@ -889,6 +969,7 @@ await execute(researchCmd); await execute(farmCmd); await execute(farmUpgradeCmd); + await execute(cultureCmd); // Auto-farm: if enabled, claim all ready farms (no explicit command needed) const farmSettings = cmdData.farm_settings || {}; diff --git a/bot_modules/02_state.js b/bot_modules/02_state.js index 78aecc3..e9e34c1 100644 --- a/bot_modules/02_state.js +++ b/bot_modules/02_state.js @@ -247,11 +247,31 @@ function gatherState() { } } catch (e) { log(`unit speed gather failed: ${e}`); } + + // Active celebrations — tracks running parties/processions per town + // finished_at is a unix timestamp (seconds). 0 means not running. + const celebrations = []; + try { + const celebModels = uw.MM.getModels()?.Celebration; + if (celebModels) { + for (const cel of Object.values(celebModels)) { + const a = cel.attributes; + if (!a || !a.town_id) continue; + celebrations.push({ + town_id: String(a.town_id), + celebration_type: a.celebration_type, // 'party' | 'triumph' + finished_at: a.finished_at ?? 0 + }); + } + } + } catch (e) { log(`celebrations gather failed: ${e}`); } + return { player, player_id, alliance_id, total_points, battle_points, world_id: world, - world_speed, unit_speeds, towns: townList }; + world_speed, unit_speeds, towns: townList, celebrations }; } + function pushState() { if (paused) return; try { diff --git a/bot_modules/04d_execute_culture.js b/bot_modules/04d_execute_culture.js new file mode 100644 index 0000000..24ff05e --- /dev/null +++ b/bot_modules/04d_execute_culture.js @@ -0,0 +1,101 @@ +// ================================================================ +// 04d_execute_culture.js — Execute Αγορά celebration commands +// +// Handles commands of type 'culture' from the poll response. +// Supported celebration_type values: +// 'party' → Γιορτή πόλης (costs wood/stone/iron) +// 'triumph' → Παρέλαση θριάμβου (costs battle points) +// +// On success/failure, reports back to /api/commands//result +// so the culture_log row is updated from 'pending' to 'success'/'failed'. +// ================================================================ + +async function executeCultureCommand(cmd) { + if (!cmd || !cmd.id || !cmd.payload) return; + + const { celebration_type, town_id } = cmd.payload; + if (!celebration_type || !town_id) { + reportResult(cmd.id, 'failed', 'Invalid culture payload: missing celebration_type or town_id'); + return; + } + + if (!['party', 'triumph'].includes(celebration_type)) { + reportResult(cmd.id, 'failed', `Unknown celebration_type: ${celebration_type}`); + return; + } + + log(`[αγορά] Εκτέλεση ${celebration_type === 'party' ? 'Γιορτής πόλης' : 'Παρέλασης θριάμβου'} για πόλη ${town_id}`); + + // Validate town still exists in game memory + const town = uw.ITowns?.towns?.[town_id]; + if (!town) { + reportResult(cmd.id, 'failed', `Η πόλη ${town_id} δεν βρέθηκε στη μνήμη του παιχνιδιού.`); + return; + } + + // Double-check: is there already a celebration of this type running? + try { + const celebModels = uw.MM.getModels()?.Celebration; + if (celebModels) { + const nowTs = Math.floor(Date.now() / 1000); + for (const cel of Object.values(celebModels)) { + const a = cel.attributes; + if (String(a.town_id) === String(town_id) + && a.celebration_type === celebration_type + && (a.finished_at ?? 0) > nowTs) { + reportResult(cmd.id, 'failed', + `${celebration_type === 'party' ? 'Γιορτή' : 'Παρέλαση'} ήδη ενεργή στην πόλη ${town_id}.`); + return; + } + } + } + } catch (e) { /* model not loaded — proceed anyway, server will error if invalid */ } + + // Fire the Grepolis AJAX + let ajaxSuccess = false; + let ajaxError = null; + + await new Promise(resolve => { + try { + uw.gpAjax.ajaxPost( + 'building_place', + 'start_celebration', + { + town_id: parseInt(town_id, 10), + celebration_type: celebration_type + }, + false, // no cache-busting + // success callback + () => { + ajaxSuccess = true; + resolve(); + }, + // error callback + (err) => { + ajaxError = err ? JSON.stringify(err) : 'Άγνωστο σφάλμα AJAX'; + resolve(); + } + ); + } catch (e) { + ajaxError = String(e); + resolve(); + } + + // Safety timeout: if neither callback fires within 12 s, treat as failed + setTimeout(() => { + if (!ajaxSuccess && !ajaxError) { + ajaxError = 'Timeout — δεν ελήφθη απάντηση από το παιχνίδι εντός 12δ.'; + } + resolve(); + }, 12000); + }); + + if (ajaxSuccess) { + const label = celebration_type === 'party' ? 'Γιορτή πόλης' : 'Παρέλαση θριάμβου'; + log(`[αγορά] ✅ ${label} ξεκίνησε επιτυχώς (πόλη ${town_id})`); + reportResult(cmd.id, 'done', `${label} ξεκίνησε επιτυχώς.`); + } else { + log(`[αγορά] ❌ Αποτυχία: ${ajaxError}`); + reportResult(cmd.id, 'failed', ajaxError || 'Αποτυχία εκτέλεσης εορτής.'); + } +} diff --git a/db.py b/db.py index fe5dd33..a4058e8 100644 --- a/db.py +++ b/db.py @@ -85,6 +85,7 @@ def init_db(): CREATE TABLE IF NOT EXISTS bot_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, player_id TEXT NOT NULL, + world_id TEXT NOT NULL DEFAULT '', feature TEXT NOT NULL, -- 'bootcamp' | 'rural_trade' message TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) @@ -92,6 +93,46 @@ def init_db(): ''') c.execute('CREATE INDEX IF NOT EXISTS idx_bot_logs_player_feature ON bot_logs(player_id, feature)') + # Celebrations — active celebration state per town, pushed by the bot + # One row per town_id + celebration_type. Upserted on every state push. + # finished_at = 0 means no active celebration of that type. + c.execute(''' + CREATE TABLE IF NOT EXISTS celebrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + player_id TEXT NOT NULL, + world_id TEXT NOT NULL, + town_id TEXT NOT NULL, + town_name TEXT, + celebration_type TEXT NOT NULL, -- 'party' | 'triumph' + finished_at INTEGER NOT NULL DEFAULT 0, -- unix timestamp (0 = not running) + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + UNIQUE(player_id, world_id, town_id, celebration_type) + ) + ''') + c.execute('CREATE INDEX IF NOT EXISTS idx_celebrations_player_world ON celebrations(player_id, world_id)') + + # Culture log — immutable audit log of every Αγορά command fired from the dashboard + # Records what was fired, what it cost, and whether the bot confirmed success. + c.execute(''' + CREATE TABLE IF NOT EXISTS culture_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + player_id TEXT NOT NULL, + world_id TEXT NOT NULL, + town_id TEXT NOT NULL, + town_name TEXT, + celebration_type TEXT NOT NULL, -- 'party' | 'triumph' + cost_wood INTEGER NOT NULL DEFAULT 0, + cost_stone INTEGER NOT NULL DEFAULT 0, + cost_iron INTEGER NOT NULL DEFAULT 0, + cost_battle_pts INTEGER NOT NULL DEFAULT 0, + status TEXT NOT NULL DEFAULT 'pending', -- pending | success | failed + result_msg TEXT, + fired_at TEXT NOT NULL DEFAULT (datetime('now')), + confirmed_at TEXT + ) + ''') + c.execute('CREATE INDEX IF NOT EXISTS idx_culture_log_player_world ON culture_log(player_id, world_id)') + # Blueprints - assigns a blueprint to a specific town c.execute(''' CREATE TABLE IF NOT EXISTS town_blueprints ( @@ -184,6 +225,8 @@ def init_db(): "ALTER TABLE clan_members ADD COLUMN features TEXT NOT NULL DEFAULT 'farm,admin'", 'ALTER TABLE clan_members ADD COLUMN world_id TEXT', 'ALTER TABLE users ADD COLUMN clan_id INTEGER REFERENCES clans(id)', + # bot_logs gained world_id for per-world isolation + "ALTER TABLE bot_logs ADD COLUMN world_id TEXT NOT NULL DEFAULT ''", ]: try: c.execute(_col) diff --git a/future_ideas.md b/future_ideas.md index 86ba640..160186b 100644 --- a/future_ideas.md +++ b/future_ideas.md @@ -139,3 +139,35 @@ Players manually open the World Wonder window and use UI helpers to calculate ho **Benefits:** - Maximizes alliance contributions to World Wonders around the clock. - Perfectly balances resources sent so the player doesn't accidentally empty their town of a critical resource. + +## 6. On-Demand Market Capacity & Trade Order Monitor +**Inspired by:** `3rdparty/ModernBot.user.js` (`getAllTrades`) and `3rdparty/DIO-TOOLS-David1327 stamasPacket.user.js` (transport capacity logic) + +**Background & Why Previous Attempts Failed:** +Earlier attempts to read market capacity via `town.getAvailableTradeCapacity()` and active orders via `uw.MM.getCollections().Trade` consistently returned empty/zero values. The root cause: both APIs depend on Grepolis having pre-loaded the town's trade models in memory, which only happens if the user has recently opened that specific town's market window. For towns the user hasn't visited this session, the models are simply absent. + +**Proposed Implementation (On-Demand, Single AJAX Call):** +- Add a **[Sync Market 🔄]** button in the dashboard town details panel. +- On click, the dashboard sends a `POST /api/market-sync-request` to Flask. +- The bot detects the pending request during its next poll cycle and executes a single AJAX call: + ```javascript + uw.gpAjax.ajaxGet('town_overviews', 'trade_overview', {}, true, e => { + // e.towns_merchants → available & total merchant capacity per town + // e.movements → all active incoming & outgoing shipments + }); + ``` +- The `true` flag fetches data for **all towns simultaneously** — one call, full empire snapshot. +- The bot pushes the result to Flask via `POST /api/market-state`. +- The dashboard polls and renders: + - **Capacity** inline in the town detail panel (e.g., `Merchants: 8 / 15`) + - **Shipments** as a small list: `→ Αθήνα: 2,500 🪵 (12m 30s)` / `← Σπάρτη: 1,800 ⛏️ (3m)` + +**UI Layout (TBD):** +- Capacity shown inline with resources (compact). +- Shipments shown in a collapsible section or modal — to be decided with user. + +**Benefits:** +- Zero automatic polling — completely stealthy, fires only when the user explicitly requests it. +- Bypasses the model-dependency problem entirely by going straight to the Grepolis server. +- A single AJAX call populates market data for all towns at once, making it highly efficient. +- Follows the existing Live Sync button pattern — no new architectural concepts needed. diff --git a/routes/api.py b/routes/api.py index f6a829f..bbcb496 100644 --- a/routes/api.py +++ b/routes/api.py @@ -120,11 +120,38 @@ def receive_state(): except Exception as e: print("Error evaluating blueprints:", e) + # Upsert active celebrations (party / triumph cooldowns per town) + celebrations = data.get('celebrations', []) + if celebrations and player_id and world_id: + now_iso = datetime.utcnow().isoformat() + for cel in celebrations: + town_id_cel = str(cel.get('town_id', '')) + cel_type = cel.get('celebration_type', '') + finished_at = int(cel.get('finished_at', 0)) + if not town_id_cel or not cel_type: + continue + # Resolve town_name from what we just upserted + t_name_row = c.execute( + 'SELECT town_name FROM town_state WHERE town_id = ?', (town_id_cel,) + ).fetchone() + t_name = t_name_row['town_name'] if t_name_row else '' + c.execute('''\ + INSERT INTO celebrations + (player_id, world_id, town_id, town_name, celebration_type, finished_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(player_id, world_id, town_id, celebration_type) DO UPDATE SET + town_name = excluded.town_name, + finished_at = excluded.finished_at, + updated_at = excluded.updated_at + ''', (str(player_id), world_id, town_id_cel, t_name, cel_type, finished_at, now_iso)) + conn.commit() + conn.close() return jsonify({'ok': True, 'towns_updated': len(towns)}) + # ------------------------------------------------------------------ # GET /api/commands/pending # Tampermonkey polls this to get the next command to execute. @@ -266,11 +293,12 @@ def get_pending_command(): ''', (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) + 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_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) + research_cmd = _fetch_pending_of_type(c, 'research', player_id, world_id) + culture_cmd = _fetch_pending_of_type(c, 'culture', player_id, world_id) sync_req = _check_and_reset_sync(c, player_id) # Determine player_key for world-specific settings if world_id is provided @@ -324,6 +352,7 @@ def get_pending_command(): 'research': research_cmd, 'farm': farm_cmd, 'farm_upgrade': farm_upgrade_cmd, + 'culture': culture_cmd, 'farm_settings': farm_settings, 'bot_settings': bot_settings, 'enabled_features': enabled_features, @@ -393,11 +422,26 @@ def command_result(cmd_id): ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at ''', (lf_key, now, now)) + # When a culture command finishes, update the matching culture_log row + if cmd and cmd['type'] == 'culture' and cmd['player_id']: + log_status = 'success' if status == 'done' else ('failed' if status == 'failed' else 'pending') + conn.execute('''\ + UPDATE culture_log + SET status = ?, result_msg = ?, confirmed_at = ? + WHERE player_id = ? AND status = 'pending' + AND id = ( + SELECT id FROM culture_log + WHERE player_id = ? AND status = 'pending' + ORDER BY id DESC LIMIT 1 + ) + ''', (log_status, msg, now, str(cmd['player_id']), str(cmd['player_id']))) + conn.commit() conn.close() return jsonify({'ok': True}) + # ------------------------------------------------------------------ # POST /api/captcha/alert # Tampermonkey reports when #hcaptcha_window appears/disappears. diff --git a/routes/dashboard.py b/routes/dashboard.py index 270c4f6..76a7431 100644 --- a/routes/dashboard.py +++ b/routes/dashboard.py @@ -97,6 +97,11 @@ def player_farm(player_id, world_id): def player_tracker(player_id, world_id): return render_template('tracker.html', player_id=player_id, world_id=world_id) +@dashboard.route('/player///agora') +@login_required +def player_agora(player_id, world_id): + return render_template('agora.html', player_id=player_id, world_id=world_id) + # ------------------------------------------------------------------ # GET /dashboard/farm-settings — returns current farm config # POST /dashboard/farm-settings — updates farm config @@ -464,8 +469,9 @@ def create_command(): return jsonify({'error': f'missing field: {field}'}), 400 cmd_type = data['type'] - if cmd_type not in ('build', 'recruit', 'market_offer', 'farm_loot', 'farm_upgrade', 'research'): - return jsonify({'error': 'type must be build, recruit, market_offer, farm_loot, farm_upgrade, or research'}), 400 + if cmd_type not in ('build', 'recruit', 'market_offer', 'farm_loot', 'farm_upgrade', 'research', 'culture'): + return jsonify({'error': 'type must be build, recruit, market_offer, farm_loot, farm_upgrade, research, or culture'}), 400 + # Reject if the Tampermonkey client is offline (no state push in last 150 s) conn = get_db() @@ -682,3 +688,269 @@ def bootcamp_attack_now(): conn.commit() conn.close() return jsonify({'ok': True}) + + +# ------------------------------------------------------------------ +# GET /dashboard/agora +# Returns per-town celebration eligibility for the Αγορά tab. +# For each town owned by player_id + world_id we calculate: +# • party — has 15k wood / 18k stone / 15k iron? on cooldown? +# • triumph — has 300+ battle points? on cooldown? +# ------------------------------------------------------------------ +PARTY_COST = {'wood': 15000, 'stone': 18000, 'iron': 15000} +TRIUMPH_COST = 300 # battle points + +@dashboard.route('/dashboard/agora', methods=['GET']) +@login_required +def get_agora(): + player_id = request.args.get('player_id') + world_id = request.args.get('world_id', '') + if not player_id: + return jsonify({'error': 'missing player_id'}), 400 + + conn = get_db() + now_ts = int(datetime.utcnow().timestamp()) + + # ── Town snapshots ─────────────────────────────────────────────── + town_rows = conn.execute(''' + SELECT town_id, town_name, data + FROM town_state + WHERE player_id = ? AND world_id = ? + ORDER BY town_name ASC + ''', (player_id, world_id)).fetchall() + + # ── Active celebrations (cooldowns) ────────────────────────────── + cel_rows = conn.execute(''' + SELECT town_id, celebration_type, finished_at + FROM celebrations + WHERE player_id = ? AND world_id = ? AND finished_at > ? + ''', (player_id, world_id, now_ts)).fetchall() + + # Build a lookup: (town_id, cel_type) → finished_at + cooldowns = {} + for r in cel_rows: + cooldowns[(r['town_id'], r['celebration_type'])] = r['finished_at'] + + conn.close() + + # ── Per-town eligibility ───────────────────────────────────────── + towns_out = [] + for row in town_rows: + d = json.loads(row['data']) + wood = d.get('wood', 0) + stone = d.get('stone', 0) + iron = d.get('iron', 0) + bp = d.get('battle_points', {}).get('available', 0) + academy = d.get('buildings', {}).get('academy', 0) + + tid = row['town_id'] + + # ── Party (Γιορτή πόλης) ───────────────────────────────────── + party_cd = cooldowns.get((tid, 'party'), 0) + if party_cd: + party_status = 'cooldown' + party_reason = f'Ενεργή — λήγει σε {_fmt_seconds(party_cd - now_ts)}' + party_ok = False + elif academy < 30: + party_status = 'unavailable' + party_reason = f'Ακαδημία {academy}/30' + party_ok = False + elif wood < PARTY_COST['wood'] or stone < PARTY_COST['stone'] or iron < PARTY_COST['iron']: + missing = [] + if wood < PARTY_COST['wood']: missing.append(f'ξύλο {wood:,}/{PARTY_COST["wood"]:,}') + if stone < PARTY_COST['stone']: missing.append(f'πέτρα {stone:,}/{PARTY_COST["stone"]:,}') + if iron < PARTY_COST['iron']: missing.append(f'σίδερο {iron:,}/{PARTY_COST["iron"]:,}') + party_status = 'unavailable' + party_reason = 'Ανεπαρκείς πόροι: ' + ', '.join(missing) + party_ok = False + else: + party_status = 'available' + party_reason = '' + party_ok = True + + # ── Triumph (Παρέλαση θριάμβου) ────────────────────────────── + triumph_cd = cooldowns.get((tid, 'triumph'), 0) + if triumph_cd: + triumph_status = 'cooldown' + triumph_reason = f'Ενεργή — λήγει σε {_fmt_seconds(triumph_cd - now_ts)}' + triumph_ok = False + elif bp < TRIUMPH_COST: + triumph_status = 'unavailable' + triumph_reason = f'Ανεπαρκείς πόντοι μάχης: {bp}/{TRIUMPH_COST}' + triumph_ok = False + else: + triumph_status = 'available' + triumph_reason = '' + triumph_ok = True + + towns_out.append({ + 'town_id': tid, + 'town_name': row['town_name'], + 'resources': {'wood': wood, 'stone': stone, 'iron': iron}, + 'battle_points': bp, + 'academy': academy, + 'party': { + 'status': party_status, + 'reason': party_reason, + 'available': party_ok, + 'cooldown_until': party_cd or None, + }, + 'triumph': { + 'status': triumph_status, + 'reason': triumph_reason, + 'available': triumph_ok, + 'cooldown_until': triumph_cd or None, + }, + }) + + return jsonify({'towns': towns_out, 'costs': {'party': PARTY_COST, 'triumph': TRIUMPH_COST}}) + + +def _fmt_seconds(secs): + """Format a duration in seconds to a human-readable Greek string.""" + secs = max(0, int(secs)) + h, rem = divmod(secs, 3600) + m, s = divmod(rem, 60) + if h: + return f'{h}ω {m}λ' + if m: + return f'{m}λ {s}δ' + return f'{s}δ' + + +# ------------------------------------------------------------------ +# POST /dashboard/culture-command +# Dashboard fires when the user confirms a celebration. +# Body: { player_id, world_id, town_id, town_name, celebration_type } +# • Validates client is online. +# • Validates eligibility (resources / battle_points / cooldown). +# • Inserts a 'culture' command into the commands queue. +# • Inserts a 'pending' row into culture_log. +# ------------------------------------------------------------------ +@dashboard.route('/dashboard/culture-command', methods=['POST']) +@login_required +def culture_command(): + data = request.get_json(silent=True) or {} + player_id = data.get('player_id') + world_id = data.get('world_id', '') + town_id = str(data.get('town_id', '')) + town_name = data.get('town_name', '') + cel_type = data.get('celebration_type', '') # 'party' | 'triumph' + + if not all([player_id, town_id, cel_type]): + return jsonify({'error': 'missing player_id, town_id or celebration_type'}), 400 + if cel_type not in ('party', 'triumph'): + return jsonify({'error': 'celebration_type must be party or triumph'}), 400 + + conn = get_db() + + # ── Client online check ────────────────────────────────────────── + row = conn.execute( + 'SELECT MAX(updated_at) AS last_seen FROM town_state WHERE player_id = ?', (player_id,) + ).fetchone() + last_seen = row['last_seen'] if row else None + client_online = False + if last_seen: + try: + dt = datetime.fromisoformat(last_seen) + client_online = (datetime.utcnow() - dt).total_seconds() <= 150 + except Exception: + pass + if not client_online: + conn.close() + return jsonify({'error': 'client_offline', + 'message': 'Το script είναι offline — δεν μπορείτε να στείλετε εντολές.'}), 503 + + # ── Re-validate eligibility server-side ───────────────────────── + town_row = conn.execute( + 'SELECT data FROM town_state WHERE town_id = ? AND player_id = ?', (town_id, player_id) + ).fetchone() + if not town_row: + conn.close() + return jsonify({'error': 'town not found'}), 404 + + d = json.loads(town_row['data']) + now_t = int(datetime.utcnow().timestamp()) + + # Cooldown check + cel_row = conn.execute(''' + SELECT finished_at FROM celebrations + WHERE player_id = ? AND world_id = ? AND town_id = ? AND celebration_type = ? AND finished_at > ? + ''', (player_id, world_id, town_id, cel_type, now_t)).fetchone() + if cel_row: + conn.close() + return jsonify({'error': 'on_cooldown', + 'message': f'Η εορτή τρέχει ήδη — λήγει σε {_fmt_seconds(cel_row["finished_at"] - now_t)}'}), 409 + + # Cost determination & resource check + if cel_type == 'party': + cost_wood = PARTY_COST['wood'] + cost_stone = PARTY_COST['stone'] + cost_iron = PARTY_COST['iron'] + cost_bp = 0 + if (d.get('wood', 0) < cost_wood or d.get('stone', 0) < cost_stone + or d.get('iron', 0) < cost_iron): + conn.close() + return jsonify({'error': 'insufficient_resources', + 'message': 'Ανεπαρκείς πόροι για τη Γιορτή πόλης.'}), 409 + else: # triumph + cost_wood = cost_stone = cost_iron = 0 + cost_bp = TRIUMPH_COST + available_bp = d.get('battle_points', {}).get('available', 0) + if available_bp < TRIUMPH_COST: + conn.close() + return jsonify({'error': 'insufficient_battle_points', + 'message': f'Ανεπαρκείς πόντοι μάχης ({available_bp}/{TRIUMPH_COST}).'}), 409 + + # ── Insert command into bot queue ──────────────────────────────── + now_iso = datetime.utcnow().isoformat() + c = conn.cursor() + c.execute(''' + INSERT INTO commands (town_id, town_name, type, payload, status, created_at, updated_at, player_id) + VALUES (?, ?, 'culture', ?, 'pending', ?, ?, ?) + ''', ( + town_id, town_name, + json.dumps({'celebration_type': cel_type, 'town_id': int(town_id)}), + now_iso, now_iso, str(player_id) + )) + cmd_id = c.lastrowid + + # ── Append culture_log row (status=pending until bot confirms) ─── + c.execute(''' + INSERT INTO culture_log + (player_id, world_id, town_id, town_name, celebration_type, + cost_wood, cost_stone, cost_iron, cost_battle_pts, status, fired_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?) + ''', (str(player_id), world_id, town_id, town_name, cel_type, + cost_wood, cost_stone, cost_iron, cost_bp, now_iso)) + log_id = c.lastrowid + + conn.commit() + conn.close() + return jsonify({'ok': True, 'cmd_id': cmd_id, 'log_id': log_id}) + + +# ------------------------------------------------------------------ +# GET /dashboard/culture-log +# Returns the last 50 Αγορά log entries for a player + world. +# ------------------------------------------------------------------ +@dashboard.route('/dashboard/culture-log', methods=['GET']) +@login_required +def get_culture_log(): + player_id = request.args.get('player_id') + world_id = request.args.get('world_id', '') + if not player_id: + return jsonify({'error': 'missing player_id'}), 400 + + conn = get_db() + rows = conn.execute(''' + SELECT id, town_id, town_name, celebration_type, + cost_wood, cost_stone, cost_iron, cost_battle_pts, + status, result_msg, fired_at, confirmed_at + FROM culture_log + WHERE player_id = ? AND world_id = ? + ORDER BY id DESC + LIMIT 50 + ''', (player_id, world_id)).fetchall() + conn.close() + return jsonify([dict(r) for r in rows]) diff --git a/templates/agora.html b/templates/agora.html new file mode 100644 index 0000000..b115267 --- /dev/null +++ b/templates/agora.html @@ -0,0 +1,672 @@ + + + + + +Αγορά — Grepolis Remote + + + + + + + +
+
🎭 Αγορά
+ Φόρτωση... + ← Hub +
+ + +
+ + +
+
Πόλεις (0)
+ +
+
Φόρτωση...
+
+
+ + +
+ +
+
⬅ Επέλεξε πόλη για να δεις τις διαθέσιμες εορτές
+
+ + +
+
+ 📜 Αρχείο Αγοράς + +
+
+
Δεν υπάρχουν εγγραφές ακόμα.
+
+
+ +
+
+ + + + + +
+ + + + diff --git a/templates/hub.html b/templates/hub.html index 0350bba..396298d 100644 --- a/templates/hub.html +++ b/templates/hub.html @@ -100,6 +100,11 @@ .hub-card.tracker::before { background: radial-gradient(circle at top left, rgba(111,207,207,0.08), transparent 70%); } .hub-card.tracker:hover { border-color: #6fcfcf; box-shadow: 0 12px 40px rgba(111,207,207,0.15); } + /* Αγορά — purple/gold */ + .hub-card.agora { border-color: #2d1f3f; } + .hub-card.agora::before { background: radial-gradient(circle at top left, rgba(180,130,220,0.08), transparent 70%); } + .hub-card.agora:hover { border-color: #b482dc; box-shadow: 0 12px 40px rgba(180,130,220,0.18); } + .card-icon { font-size: 2.8rem; margin-bottom: 1rem; @@ -113,6 +118,7 @@ .hub-card.admin .card-title { color: #c8a44a; } .hub-card.farm .card-title { color: #4acc64; } .hub-card.tracker .card-title { color: #6fcfcf; } + .hub-card.agora .card-title { color: #b482dc; } .card-desc { font-size: 0.875rem; @@ -173,6 +179,12 @@
Παρακολούθηση κινήσεων στρατού σε πραγματικό χρόνο. Εισερχόμενες επιθέσεις και ενισχύσεις.
+ + 🎭 +
Αγορά
+
Εκκίνηση Γιορτής πόλης και Παρέλασης θριάμβου ανά πόλη. Έλεγχος πόρων, cooldown και ιστορικό εκτελέσεων.
+
+ ← Επιστροφή στην επιλογή παίκτη