Compare commits
73 Commits
1dc96a53ee
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 76b991a62b | |||
| f5231a2524 | |||
| 84de7082ec | |||
| 83b8c85557 | |||
| d8ba139d07 | |||
| 6157ae1034 | |||
| 4272edf432 | |||
| 502b330ac5 | |||
| 1c65043eb3 | |||
| 5c4d415fdd | |||
| f22b92ae89 | |||
| 0f54ef9191 | |||
| 45e71ed90b | |||
| 66dcb71a8d | |||
| b36b11393f | |||
| 90ce6a029d | |||
| c9e6522f12 | |||
| 1cb5dca3c2 | |||
| 2552d3d075 | |||
| 5f6855ec69 | |||
| 05785c294e | |||
| 614029e527 | |||
| a572feef14 | |||
| b18e2e8f97 | |||
| 2769091b74 | |||
| f4a0e18686 | |||
| d6c2252f5c | |||
| bcda80e127 | |||
| 731a7b2f3b | |||
| f82893164e | |||
| efa63f761f | |||
| ae37674bcc | |||
| cf3c2e7b4f | |||
| 2921dff257 | |||
| 2a73e46a7b | |||
| 76ad37c1db | |||
| f250fbd5b6 | |||
| bb01b90889 | |||
| 0643422a30 | |||
| 2517538b88 | |||
| 1db8d744c8 | |||
| d952e7ca56 | |||
| edd7666905 | |||
| 76ab83620b | |||
| 53f1176ef8 | |||
| ef6946365c | |||
| 0ef0bef036 | |||
| 1717de8373 | |||
| 8ed964f3bb | |||
| bfdfaa142c | |||
| 8b42c7c2f9 | |||
| adb42c1649 | |||
| e8fd35105f | |||
| 5bff9a287d | |||
| a8b3e9f5ea | |||
| a5d57d55fc | |||
| 929af21d08 | |||
| 7beece5aaa | |||
| d20537983e | |||
| 037a84d6bb | |||
| ab1ba5c0ab | |||
| 95b38b1212 | |||
| b688b66275 | |||
| b577c95f7c | |||
| 8fd711f5a1 | |||
| 4fa6127aea | |||
| a143345831 | |||
| 8a64a7b4fc | |||
| 853525d8ad | |||
| 22a379c2a1 | |||
| e565c88eeb | |||
| b7bf1cf9ea | |||
| 9aba81960a |
77
GrepoRemoteLoader.user.js
Normal file
77
GrepoRemoteLoader.user.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name Grepolis Remote Loader
|
||||||
|
// @namespace http://tampermonkey.net/
|
||||||
|
// @version 4.0.1
|
||||||
|
// @description Dynamically loads the Grepolis Remote Control bot from the server
|
||||||
|
// @author Dimitrios
|
||||||
|
// @match https://*.grepolis.com/game/*
|
||||||
|
// @grant unsafeWindow
|
||||||
|
// @grant GM_xmlhttpRequest
|
||||||
|
// @connect grepo.haunter-pets.top
|
||||||
|
// @updateURL https://git.haunter-pets.top/haunter/grepo-remote/raw/branch/main/GrepoRemoteLoader.user.js
|
||||||
|
// @downloadURL https://git.haunter-pets.top/haunter/grepo-remote/raw/branch/main/GrepoRemoteLoader.user.js
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const BASE_URL = 'https://grepo.haunter-pets.top';
|
||||||
|
const MAX_TRIES = 3;
|
||||||
|
const RETRY_BASE = 3000; // 3 s, then 6 s
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// CLAN_KEY: Set this to your clan's unique key.
|
||||||
|
// Leave it empty ('') and the bot will refuse to run.
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
const CLAN_KEY = ''; // <-- paste your clan key here
|
||||||
|
|
||||||
|
if (!CLAN_KEY || CLAN_KEY.trim() === '') {
|
||||||
|
console.error('[Loader] ❌ CLAN_KEY is not set. Bot will not start. Contact your admin for the key.');
|
||||||
|
return; // stop everything
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadBot(attempt) {
|
||||||
|
attempt = attempt || 1;
|
||||||
|
console.log(`[Loader] Fetching bot code (attempt ${attempt}/${MAX_TRIES})...`);
|
||||||
|
|
||||||
|
GM_xmlhttpRequest({
|
||||||
|
method: 'GET',
|
||||||
|
url: `${BASE_URL}/api/bot?t=${Date.now()}`,
|
||||||
|
headers: {
|
||||||
|
'X-Clan-Key': CLAN_KEY
|
||||||
|
},
|
||||||
|
onload: function(response) {
|
||||||
|
if (response.status === 200) {
|
||||||
|
console.log('[Loader] Bot code downloaded. Executing...');
|
||||||
|
try {
|
||||||
|
window.__GRC_CLAN_KEY = CLAN_KEY; // make key available inside the bot's scope
|
||||||
|
eval(response.responseText);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Loader] Execution error:', e);
|
||||||
|
}
|
||||||
|
} else if (attempt < MAX_TRIES) {
|
||||||
|
const delay = RETRY_BASE * attempt;
|
||||||
|
console.warn(`[Loader] Server returned ${response.status}. Retrying in ${delay / 1000}s...`);
|
||||||
|
setTimeout(() => loadBot(attempt + 1), delay);
|
||||||
|
} else {
|
||||||
|
console.error(`[Loader] Failed after ${MAX_TRIES} attempts (status ${response.status}). Give up.`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onerror: function() {
|
||||||
|
if (attempt < MAX_TRIES) {
|
||||||
|
const delay = RETRY_BASE * attempt;
|
||||||
|
console.warn(`[Loader] Connection error. Retrying in ${delay / 1000}s...`);
|
||||||
|
setTimeout(() => loadBot(attempt + 1), delay);
|
||||||
|
} else {
|
||||||
|
console.error(`[Loader] Failed after ${MAX_TRIES} attempts. Check your server.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => loadBot(1));
|
||||||
|
} else {
|
||||||
|
loadBot(1);
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name Grepolis Remote Control
|
// @name Grepolis Remote Control
|
||||||
// @namespace http://tampermonkey.net/
|
// @namespace http://tampermonkey.net/
|
||||||
// @version 3.6.7
|
// @version 3.5.9
|
||||||
// @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/*
|
||||||
@@ -253,7 +253,7 @@
|
|||||||
if (farm.attributes.island_x !== ix || farm.attributes.island_y !== iy) return;
|
if (farm.attributes.island_x !== ix || farm.attributes.island_y !== iy) return;
|
||||||
relCollection.models.forEach(rel => {
|
relCollection.models.forEach(rel => {
|
||||||
if (rel.attributes.farm_town_id === farm.attributes.id &&
|
if (rel.attributes.farm_town_id === farm.attributes.id &&
|
||||||
rel.attributes.relation_status >= 0) {
|
rel.attributes.relation_status >= 1) {
|
||||||
farms.push({
|
farms.push({
|
||||||
farm_town_id: farm.attributes.id,
|
farm_town_id: farm.attributes.id,
|
||||||
farm_name: farm.attributes.name || '',
|
farm_name: farm.attributes.name || '',
|
||||||
@@ -485,6 +485,10 @@
|
|||||||
skipped++; continue;
|
skipped++; continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (paused) {
|
||||||
|
return { ok: false, msg: 'Aborted due to pause/captcha' };
|
||||||
|
}
|
||||||
|
|
||||||
log(`Farm ${action}: farm_id=${farm.attributes.id} level=${level} town=${town_id}`);
|
log(`Farm ${action}: farm_id=${farm.attributes.id} level=${level} town=${town_id}`);
|
||||||
try {
|
try {
|
||||||
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
|
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
|
||||||
@@ -496,8 +500,8 @@
|
|||||||
isLocked ? unlocked++ : upgraded++;
|
isLocked ? unlocked++ : upgraded++;
|
||||||
} catch (e) { errors++; }
|
} catch (e) { errors++; }
|
||||||
|
|
||||||
// Random delay between actions: 800ms – 2000ms
|
// Random delay between actions: 1200ms – 2500ms
|
||||||
await sleep(randInt(800, 2000));
|
await sleep(randInt(1200, 2500));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -626,6 +630,10 @@
|
|||||||
log(`Farm: ${readyFarms.length} ready on island of town ${town_id}`);
|
log(`Farm: ${readyFarms.length} ready on island of town ${town_id}`);
|
||||||
|
|
||||||
for (const farm of readyFarms) {
|
for (const farm of readyFarms) {
|
||||||
|
if (paused) {
|
||||||
|
return { ok: false, msg: 'Aborted due to pause/captcha' };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
|
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
|
||||||
model_url: `FarmTownPlayerRelation/${farm.relation_id}`,
|
model_url: `FarmTownPlayerRelation/${farm.relation_id}`,
|
||||||
@@ -636,8 +644,8 @@
|
|||||||
claimed++;
|
claimed++;
|
||||||
} catch (e) { errors++; }
|
} catch (e) { errors++; }
|
||||||
|
|
||||||
// Random per-claim delay: 500ms – 1500ms (never below 500ms)
|
// Random per-claim delay: 1000ms – 2200ms
|
||||||
await sleep(randInt(500, 1500));
|
await sleep(randInt(1000, 2200));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh map icons after claiming (same as original)
|
// Refresh map icons after claiming (same as original)
|
||||||
@@ -645,6 +653,7 @@
|
|||||||
|
|
||||||
// Random between-island delay: 30s – 90s (only if more islands remain)
|
// Random between-island delay: 30s – 90s (only if more islands remain)
|
||||||
if (i < islandList.length - 1) {
|
if (i < islandList.length - 1) {
|
||||||
|
if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
|
||||||
const gap = randInt(30000, 90000);
|
const gap = randInt(30000, 90000);
|
||||||
log(`Farm: island done. Waiting ${(gap / 1000).toFixed(0)}s before next island...`);
|
log(`Farm: island done. Waiting ${(gap / 1000).toFixed(0)}s before next island...`);
|
||||||
await sleep(gap);
|
await sleep(gap);
|
||||||
@@ -698,6 +707,8 @@
|
|||||||
log(`Waiting ${reactionMs}ms before firing buildUp (reaction time)...`);
|
log(`Waiting ${reactionMs}ms before firing buildUp (reaction time)...`);
|
||||||
await sleep(reactionMs);
|
await sleep(reactionMs);
|
||||||
|
|
||||||
|
if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
|
||||||
|
|
||||||
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
|
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
|
||||||
model_url: 'BuildingOrder',
|
model_url: 'BuildingOrder',
|
||||||
action_name: 'buildUp',
|
action_name: 'buildUp',
|
||||||
@@ -733,6 +744,8 @@
|
|||||||
log(`Waiting ${reactionMs}ms before firing recruit (reaction time)...`);
|
log(`Waiting ${reactionMs}ms before firing recruit (reaction time)...`);
|
||||||
await sleep(reactionMs);
|
await sleep(reactionMs);
|
||||||
|
|
||||||
|
if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
|
||||||
|
|
||||||
uw.gpAjax.ajaxPost(endpoint, 'build', {
|
uw.gpAjax.ajaxPost(endpoint, 'build', {
|
||||||
unit_id,
|
unit_id,
|
||||||
amount: parseInt(amount) || 1,
|
amount: parseInt(amount) || 1,
|
||||||
@@ -759,6 +772,8 @@
|
|||||||
log(`Waiting ${reactionMs}ms before firing market offer (reaction time)...`);
|
log(`Waiting ${reactionMs}ms before firing market offer (reaction time)...`);
|
||||||
await sleep(reactionMs);
|
await sleep(reactionMs);
|
||||||
|
|
||||||
|
if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
|
||||||
|
|
||||||
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
|
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
|
||||||
model_url: 'CreateOffers/' + town_id,
|
model_url: 'CreateOffers/' + town_id,
|
||||||
action_name: 'createOffer',
|
action_name: 'createOffer',
|
||||||
@@ -772,160 +787,61 @@
|
|||||||
return { ok: true, msg: `Market offer posted: ${offer} ${offer_type} => ${demand} ${demand_type}` };
|
return { ok: true, msg: `Market offer posted: ${offer} ${offer_type} => ${demand} ${demand_type}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Execute: Research (Academy)
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
async function executeResearch(cmd) {
|
||||||
|
const { town_id, payload } = cmd;
|
||||||
|
const { research_id } = payload;
|
||||||
|
|
||||||
|
const town = uw.ITowns?.getTown?.(town_id) || uw.ITowns?.towns?.[town_id];
|
||||||
|
if (!town) {
|
||||||
|
return { ok: false, msg: `Town ${town_id} not found` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const reactionMs = randInt(800, 2500);
|
||||||
|
log(`Waiting ${reactionMs}ms before firing research (reaction time)...`);
|
||||||
|
await sleep(reactionMs);
|
||||||
|
|
||||||
|
if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
|
||||||
|
|
||||||
|
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
|
||||||
|
model_url: 'ResearchOrder',
|
||||||
|
action_name: 'research',
|
||||||
|
arguments: { id: research_id },
|
||||||
|
town_id: town_id
|
||||||
|
});
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
return { ok: true, msg: `Research ${research_id} queued` };
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
// Poll for and execute pending commands (build + recruit + market)
|
// Poll for and execute pending commands (build + recruit + market)
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
async function pollAndExecute() {
|
async function pollAndExecute() {
|
||||||
if (paused) return;
|
if (paused) return;
|
||||||
const player_id = uw.Game?.player_id;
|
const player_id = uw.Game?.player_id;
|
||||||
|
const world_id = uw.Game?.world_id;
|
||||||
if (!player_id) return;
|
if (!player_id) return;
|
||||||
|
|
||||||
let cmdData;
|
let cmdData;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${BASE_URL}/api/commands/pending?player_id=${player_id}`);
|
const res = await fetch(`${BASE_URL}/api/commands/pending?player_id=${player_id}&world_id=${world_id}`);
|
||||||
cmdData = await res.json();
|
cmdData = await res.json();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(`Poll failed: ${e}`);
|
log(`Poll failed: ${e}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build queue, Recruit queue and Market queue are independent
|
// Build queue: one command per town (all towns build in the same poll cycle)
|
||||||
const buildCmd = cmdData.build;
|
const buildCmds = cmdData.builds || [];
|
||||||
const recruitCmd = cmdData.recruit;
|
const recruitCmd = cmdData.recruit;
|
||||||
const marketCmd = cmdData.market;
|
const marketCmd = cmdData.market;
|
||||||
|
const researchCmd = cmdData.research;
|
||||||
const farmCmd = cmdData.farm;
|
const farmCmd = cmdData.farm;
|
||||||
const farmUpgradeCmd = cmdData.farm_upgrade;
|
const farmUpgradeCmd = cmdData.farm_upgrade;
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto Bandit Camp: if enabled, attack/claim when ready
|
|
||||||
if (farmSettings.bandit_camp_enabled) {
|
|
||||||
try {
|
|
||||||
// player_id already declared above in pollAndExecute scope
|
|
||||||
const currentTownId = uw.ITowns?.getCurrentTown?.()?.id
|
|
||||||
|| Object.keys(uw.ITowns?.towns || {})[0]
|
|
||||||
|| null;
|
|
||||||
if (!player_id || !currentTownId) {
|
|
||||||
log(`⚔️ Bandit Camp: Missing globals — player_id=${player_id} town_id=${currentTownId}`);
|
|
||||||
} else {
|
|
||||||
// First try: MM collection (works once camp was opened in-game)
|
|
||||||
let spotData = uw.MM.getOnlyCollectionByName('PlayerAttackSpot')?.models?.[0]?.attributes || null;
|
|
||||||
|
|
||||||
// Second try: use gpAjax with a callback — it handles auth/hash internally
|
|
||||||
if (!spotData) {
|
|
||||||
spotData = await new Promise((resolve) => {
|
|
||||||
try {
|
|
||||||
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
|
|
||||||
model_url: `PlayerAttackSpot/${player_id}`,
|
|
||||||
action_name: 'get_own',
|
|
||||||
captcha: null,
|
|
||||||
arguments: {},
|
|
||||||
town_id: currentTownId,
|
|
||||||
nl_init: true
|
|
||||||
}, false, {
|
|
||||||
success: function(data) {
|
|
||||||
// Response may contain the spot data directly
|
|
||||||
const d = data?.PlayerAttackSpot
|
|
||||||
|| data?.data?.PlayerAttackSpot?.[player_id]
|
|
||||||
|| data?.[player_id]
|
|
||||||
|| data;
|
|
||||||
resolve((d && d.cooldown_at !== undefined) ? d : null);
|
|
||||||
},
|
|
||||||
error: function() { resolve(null); }
|
|
||||||
});
|
|
||||||
} catch(e) { resolve(null); }
|
|
||||||
});
|
|
||||||
|
|
||||||
// After the gpAjax call, MM might now have the model
|
|
||||||
if (!spotData) {
|
|
||||||
spotData = uw.MM.getOnlyCollectionByName('PlayerAttackSpot')?.models?.[0]?.attributes || null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!spotData) {
|
|
||||||
log('⚔️ Bandit Camp: Could not load spot data. Open the Bandit Camp window in-game once to let the game load it.');
|
|
||||||
} else {
|
|
||||||
const nowTs = Math.floor(Date.now() / 1000);
|
|
||||||
const spotId = spotData.id || player_id;
|
|
||||||
const townId = spotData.town_id || currentTownId;
|
|
||||||
|
|
||||||
log(`⚔️ Bandit Monitor -> Cooldown in: ${Math.max(0, spotData.cooldown_at - nowTs)}s | Reward: ${spotData.reward_available} | Level: ${spotData.level}`);
|
|
||||||
|
|
||||||
if (spotData.reward_available) {
|
|
||||||
log('⚔️ Bandit Camp: Reward available! Waiting before claiming...');
|
|
||||||
await sleep(randInt(8000, 24000));
|
|
||||||
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
|
|
||||||
model_url: `PlayerAttackSpot/${spotId}`,
|
|
||||||
action_name: 'useReward',
|
|
||||||
captcha: null,
|
|
||||||
arguments: {},
|
|
||||||
town_id: townId,
|
|
||||||
nl_init: true
|
|
||||||
});
|
|
||||||
log('⚔️ Bandit Camp: Reward claimed!');
|
|
||||||
} else if (spotData.cooldown_at <= nowTs) {
|
|
||||||
let hasMovements = false;
|
|
||||||
try {
|
|
||||||
const movements = uw.MM.getOnlyCollectionByName('MovementCommand')?.models || [];
|
|
||||||
hasMovements = movements.length > 0;
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
if (!hasMovements) {
|
|
||||||
const town = uw.ITowns?.getTown?.(townId) || uw.ITowns?.towns?.[townId];
|
|
||||||
if (town) {
|
|
||||||
const myUnits = town.units() || {};
|
|
||||||
const allowedUnits = ['sword', 'slinger', 'archer', 'hoplite', 'rider', 'chariot', 'catapult'];
|
|
||||||
const sendUnits = {};
|
|
||||||
let totalUnits = 0;
|
|
||||||
for (let u of allowedUnits) {
|
|
||||||
if ((myUnits[u] || 0) > 0) {
|
|
||||||
sendUnits[u] = myUnits[u];
|
|
||||||
totalUnits += myUnits[u];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (totalUnits > 0) {
|
|
||||||
log(`⚔️ Bandit Camp: Attacking with ${totalUnits} units...`);
|
|
||||||
await sleep(randInt(8000, 24000));
|
|
||||||
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
|
|
||||||
model_url: `PlayerAttackSpot/${spotId}`,
|
|
||||||
action_name: 'attack',
|
|
||||||
captcha: null,
|
|
||||||
arguments: sendUnits,
|
|
||||||
town_id: townId,
|
|
||||||
nl_init: true
|
|
||||||
});
|
|
||||||
log('⚔️ Bandit Camp: Attack sent!');
|
|
||||||
} else {
|
|
||||||
log('⚔️ Bandit Camp: No units available.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log('⚔️ Bandit Camp: Troops still returning — waiting...');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
log(`⚔️ Bandit camp error: ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (cmdData.sync_requested) {
|
if (cmdData.sync_requested) {
|
||||||
@@ -936,33 +852,115 @@
|
|||||||
const execute = async (cmd) => {
|
const execute = async (cmd) => {
|
||||||
if (!cmd) return;
|
if (!cmd) return;
|
||||||
log(`Executing command #${cmd.id} — type:${cmd.type} town:${cmd.town_id}`);
|
log(`Executing command #${cmd.id} — type:${cmd.type} town:${cmd.town_id}`);
|
||||||
|
if (paused) {
|
||||||
|
log(`[Paused] Ignoring command #${cmd.id}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let result;
|
let result;
|
||||||
try {
|
try {
|
||||||
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 === 'research') result = await executeResearch(cmd);
|
||||||
else if (cmd.type === 'farm_loot') result = await executeFarmLoot(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 === 'farm_upgrade') result = await executeFarmUpgrade(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}` };
|
||||||
}
|
}
|
||||||
const finalStatus = result.requeue ? 'pending' : (result.ok ? 'done' : 'failed');
|
const finalStatus = result.requeue ? 'pending' : (result.ok ? 'done' : 'failed');
|
||||||
log(`Command #${cmd.id}: ${finalStatus === 'done' ? '✅' : finalStatus === 'pending' ? '⏳' : '❌'} ${result.msg}`);
|
log(`Command #${cmd.id}: ${finalStatus === 'done' ? '✅' : finalStatus === 'pending' ? '⏳' : '❌'} ${result.msg}`);
|
||||||
reportResult(cmd.id, finalStatus, result.msg);
|
reportResult(cmd.id, finalStatus, result.msg);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Run concurrently — they do NOT block each other
|
// Execute ALL town build commands (one per town, sequential with inter-town delay)
|
||||||
await Promise.all([execute(buildCmd), execute(recruitCmd), execute(marketCmd)]);
|
for (let i = 0; i < buildCmds.length; i++) {
|
||||||
if (farmCmd) await execute(farmCmd);
|
await execute(buildCmds[i]);
|
||||||
if (farmUpgradeCmd) await execute(farmUpgradeCmd);
|
if (i < buildCmds.length - 1) {
|
||||||
|
// Random gap between towns so it doesn't look like a macro
|
||||||
|
const gap = randInt(1500, 3000);
|
||||||
|
log(`Build: town done. Waiting ${gap}ms before next town...`);
|
||||||
|
await sleep(gap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await execute(recruitCmd);
|
||||||
|
await execute(marketCmd);
|
||||||
|
await execute(researchCmd);
|
||||||
|
await execute(farmCmd);
|
||||||
|
await execute(farmUpgradeCmd);
|
||||||
|
|
||||||
|
// 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 farm relation is ready
|
||||||
|
let readyFarms = [];
|
||||||
|
try {
|
||||||
|
const coll = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation');
|
||||||
|
readyFarms = coll?.models?.filter(r =>
|
||||||
|
r.attributes.relation_status === 1 &&
|
||||||
|
(r.attributes.lootable_at || 0) <= nowTs
|
||||||
|
) || [];
|
||||||
|
} catch (e) { /* silent */ }
|
||||||
|
|
||||||
|
if (readyFarms.length > 0) {
|
||||||
|
// Check if the CURRENT town's warehouse is full (>95%)
|
||||||
|
let allFull = true;
|
||||||
|
let claimedAny = false;
|
||||||
|
|
||||||
|
// Iterate over all towns that have ready farms
|
||||||
|
const towns = Object.values(uw.ITowns?.towns || {});
|
||||||
|
for (const town of towns) {
|
||||||
|
const storage = town.resources?.()?.storage || town.get?.('storage') || 0;
|
||||||
|
const wood = town.resources?.()?.wood || town.get?.('wood') || 0;
|
||||||
|
const stone = town.resources?.()?.stone || town.get?.('stone') || 0;
|
||||||
|
const iron = town.resources?.()?.iron || town.get?.('iron') || 0;
|
||||||
|
if (!storage) continue;
|
||||||
|
|
||||||
|
const maxRes = Math.max(wood, stone, iron);
|
||||||
|
const pct = maxRes / storage;
|
||||||
|
|
||||||
|
if (pct < 0.95) {
|
||||||
|
// This town has room — loot using its town_id context
|
||||||
|
allFull = false;
|
||||||
|
log(`⚡ Auto-farm: looting into town ${town.get?.('name')} (${Math.round(pct*100)}% full)`);
|
||||||
|
await executeFarmLoot({ payload: { loot_option: farmSettings.loot_option } });
|
||||||
|
claimedAny = true;
|
||||||
|
pushState();
|
||||||
|
break; // one loot pass is enough per poll cycle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allFull) {
|
||||||
|
log('⚠️ Auto-farm: ALL warehouses are full (>95%) — skipping loot this cycle');
|
||||||
|
// Report full status to backend so farm.html can show notice
|
||||||
|
try {
|
||||||
|
await fetch(`${BASE_URL}/api/farm_status?player_id=${uw.Game.player_id}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ warehouse_full: true })
|
||||||
|
});
|
||||||
|
} catch(e) {}
|
||||||
|
} else if (claimedAny) {
|
||||||
|
// Clear the full flag
|
||||||
|
try {
|
||||||
|
await fetch(`${BASE_URL}/api/farm_status?player_id=${uw.Game.player_id}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ warehouse_full: false })
|
||||||
|
});
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
// Boot
|
// Boot
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
log('Grepolis Remote Control v3.5 loaded');
|
log('Grepolis Remote Control v3.5.9 loaded');
|
||||||
|
|
||||||
// Start captcha watcher immediately
|
// Start captcha watcher immediately
|
||||||
detectCaptcha();
|
detectCaptcha();
|
||||||
|
|||||||
@@ -61,4 +61,4 @@ By default, the server runs on `http://localhost:5050` (or your configured domai
|
|||||||
This is an automation tool. Using scripts like this may violate the game's Terms of Service. Use responsibly and at your own risk.
|
This is an automation tool. Using scripts like this may violate the game's Terms of Service. Use responsibly and at your own risk.
|
||||||
|
|
||||||
---
|
---
|
||||||
*Created with ❤️ for Grepolis players.*
|
*Created with ❤️ for Grepolis players.* . .
|
||||||
|
|||||||
55
app.py
55
app.py
@@ -1,33 +1,74 @@
|
|||||||
from flask import Flask, request, jsonify
|
from flask import Flask, request, jsonify, redirect, url_for
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
from db import init_db
|
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
|
||||||
|
from db import init_db, get_db
|
||||||
from routes.api import api
|
from routes.api import api
|
||||||
from routes.dashboard import dashboard
|
from routes.dashboard import dashboard
|
||||||
|
from routes.auth import auth
|
||||||
|
import logging
|
||||||
|
|
||||||
# Initialise DB schema (e.g. creating newly added tables) when the app starts
|
logging.basicConfig(
|
||||||
|
level=logging.WARNING,
|
||||||
|
format='%(asctime)s [%(name)s] %(levelname)s: %(message)s'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialise DB schema when the app starts
|
||||||
init_db()
|
init_db()
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
app.secret_key = 'grc-super-secret-key-change-in-production'
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
# Flask-Login setup
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
login_manager = LoginManager()
|
||||||
|
login_manager.init_app(app)
|
||||||
|
login_manager.login_view = 'auth.login'
|
||||||
|
login_manager.login_message = 'Παρακαλώ συνδεθείτε για να συνεχίσετε.'
|
||||||
|
|
||||||
|
class User(UserMixin):
|
||||||
|
def __init__(self, id, username, clan_id):
|
||||||
|
self.id = id
|
||||||
|
self.username = username
|
||||||
|
self.clan_id = clan_id
|
||||||
|
|
||||||
|
@login_manager.user_loader
|
||||||
|
def load_user(user_id):
|
||||||
|
conn = get_db()
|
||||||
|
row = conn.execute('SELECT id, username, clan_id FROM users WHERE id = ?', (user_id,)).fetchone()
|
||||||
|
conn.close()
|
||||||
|
if row:
|
||||||
|
return User(row['id'], row['username'], row['clan_id'])
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Make current_user available in all templates
|
||||||
|
@app.context_processor
|
||||||
|
def inject_user():
|
||||||
|
return dict(current_user=current_user)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
# CORS
|
||||||
|
# ----------------------------------------------------------------
|
||||||
CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=False)
|
CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=False)
|
||||||
|
|
||||||
# Belt-and-suspenders: inject CORS headers on every response,
|
|
||||||
# including error responses that flask-cors might miss.
|
|
||||||
@app.after_request
|
@app.after_request
|
||||||
def add_cors_headers(response):
|
def add_cors_headers(response):
|
||||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||||
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, DELETE, OPTIONS'
|
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, DELETE, OPTIONS'
|
||||||
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, X-Clan-Key'
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# Explicitly handle OPTIONS preflight for all routes
|
|
||||||
@app.route('/', defaults={'path': ''}, methods=['OPTIONS'])
|
@app.route('/', defaults={'path': ''}, methods=['OPTIONS'])
|
||||||
@app.route('/<path:path>', methods=['OPTIONS'])
|
@app.route('/<path:path>', methods=['OPTIONS'])
|
||||||
def handle_options(path):
|
def handle_options(path):
|
||||||
return jsonify({}), 200
|
return jsonify({}), 200
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
# Blueprints
|
||||||
|
# ----------------------------------------------------------------
|
||||||
app.register_blueprint(api)
|
app.register_blueprint(api)
|
||||||
app.register_blueprint(dashboard)
|
app.register_blueprint(dashboard)
|
||||||
|
app.register_blueprint(auth)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
print("✅ Grepolis Remote — DB initialised")
|
print("✅ Grepolis Remote — DB initialised")
|
||||||
|
|||||||
221
blueprint_engine.py
Normal file
221
blueprint_engine.py
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# STANDARD_BLUEPRINT — ordered list of phases.
|
||||||
|
# Each phase is a dict of { building_name: required_level }.
|
||||||
|
# The engine works through phases in order, queueing one building at a time.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
STANDARD_BLUEPRINT = [
|
||||||
|
{"barracks": 1, "farm": 3, "lumber": 2, "stoner": 2, "ironer": 2, "storage": 2, "main": 2, "temple": 1},
|
||||||
|
{"barracks": 1, "farm": 3, "lumber": 3, "stoner": 3, "ironer": 3, "storage": 6, "main": 8},
|
||||||
|
{"farm": 8, "lumber": 8, "ironer": 8, "stoner": 8, "market": 5, "temple": 5, "barracks": 5},
|
||||||
|
{"academy": 13},
|
||||||
|
{"storage": 12, "farm": 12},
|
||||||
|
{"main": 25},
|
||||||
|
{"storage": 21, "farm": 15},
|
||||||
|
{"lumber": 15, "stoner": 10, "ironer": 12},
|
||||||
|
{"docks": 10},
|
||||||
|
{"academy": 30},
|
||||||
|
{"farm": 20, "storage": 25},
|
||||||
|
{"market": 15, "trade_office": 1, "hide": 10},
|
||||||
|
{"market": 30, "farm": 35, "thermal": 1, "academy": 36},
|
||||||
|
{"farm": 45, "storage": 35, "lumber": 40, "ironer": 40, "stoner": 40},
|
||||||
|
{"temple": 30}
|
||||||
|
]
|
||||||
|
|
||||||
|
RESEARCH_LIST = [
|
||||||
|
"booty", "pottery", "architecture", "building_crane",
|
||||||
|
"shipwright", "plow", "mathematics", "combat_experience",
|
||||||
|
"strong_wine", "take_over", "colonize_ship"
|
||||||
|
]
|
||||||
|
|
||||||
|
RESEARCH_LEVELS = {
|
||||||
|
"booty": 7,
|
||||||
|
"pottery": 7,
|
||||||
|
"architecture": 10,
|
||||||
|
"building_crane": 13,
|
||||||
|
"shipwright": 13,
|
||||||
|
"plow": 22,
|
||||||
|
"mathematics": 25,
|
||||||
|
"combat_experience": 34,
|
||||||
|
"strong_wine": 34,
|
||||||
|
"take_over": 28,
|
||||||
|
"colonize_ship": 13
|
||||||
|
}
|
||||||
|
|
||||||
|
MAX_LOOKAHEAD_PHASES = 2 # How many phases ahead to look if current phase is fully blocked
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_blueprints(conn):
|
||||||
|
blueprints = conn.execute('SELECT town_id, blueprint_name FROM town_blueprints WHERE is_active = 1').fetchall()
|
||||||
|
log.warning(f"[blueprint] Active blueprints: {len(blueprints)}")
|
||||||
|
if not blueprints:
|
||||||
|
return
|
||||||
|
|
||||||
|
for row in blueprints:
|
||||||
|
town_id = str(row['town_id'])
|
||||||
|
log.warning(f"[blueprint] Evaluating town_id={town_id}")
|
||||||
|
|
||||||
|
town_row = conn.execute(
|
||||||
|
'SELECT data, player_id, town_name, world_id FROM town_state WHERE town_id = ?', (town_id,)
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if not town_row:
|
||||||
|
log.warning(f"[blueprint] No town_state row for town_id={town_id} — skipping")
|
||||||
|
continue
|
||||||
|
|
||||||
|
player_id = town_row['player_id']
|
||||||
|
town_name_db = town_row['town_name']
|
||||||
|
town_world_id = town_row['world_id']
|
||||||
|
log.warning(f"[blueprint] Town: {town_name_db}, player_id={player_id}, world_id={town_world_id!r}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
town = json.loads(town_row['data'])
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"[blueprint] Failed to parse town JSON: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
build_queue = town.get('buildingOrder', [])
|
||||||
|
buildings = town.get('buildings', {})
|
||||||
|
build_data = town.get('buildData', {})
|
||||||
|
|
||||||
|
log.warning(f"[blueprint] buildingOrder length: {len(build_queue)}")
|
||||||
|
|
||||||
|
# ── Guard: don't queue if there's already a pending/executing command ──────
|
||||||
|
# Ghost detection: if a 'pending' command has sat untouched for >5 min,
|
||||||
|
# it's stale — delete it so we can re-evaluate fresh.
|
||||||
|
five_min_ago = (datetime.utcnow() - timedelta(minutes=5)).isoformat()
|
||||||
|
db_pending = conn.execute(
|
||||||
|
"SELECT id, type, status, updated_at FROM commands "
|
||||||
|
"WHERE town_id = ? AND type IN ('build', 'research') AND status IN ('pending', 'executing')",
|
||||||
|
(town_id,)
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
if db_pending:
|
||||||
|
ghost_ids = [
|
||||||
|
r['id'] for r in db_pending
|
||||||
|
if r['status'] == 'pending' and (r['updated_at'] is None or r['updated_at'] < five_min_ago)
|
||||||
|
]
|
||||||
|
if ghost_ids:
|
||||||
|
log.warning(f"[blueprint] Deleting {len(ghost_ids)} ghost commands {ghost_ids}")
|
||||||
|
conn.execute(
|
||||||
|
f"DELETE FROM commands WHERE id IN ({','.join('?' for _ in ghost_ids)})",
|
||||||
|
ghost_ids
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
db_pending = conn.execute(
|
||||||
|
"SELECT id, type, status, updated_at FROM commands "
|
||||||
|
"WHERE town_id = ? AND type IN ('build', 'research') AND status IN ('pending', 'executing')",
|
||||||
|
(town_id,)
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
if db_pending:
|
||||||
|
details = [(r['id'], r['type'], r['status']) for r in db_pending]
|
||||||
|
log.warning(f"[blueprint] Already has {len(db_pending)} queued commands — skipping. {details}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ── Calculate future levels: current buildings + anything in the game build queue ──
|
||||||
|
future_levels = {k: v for k, v in buildings.items()}
|
||||||
|
for q_item in build_queue:
|
||||||
|
b_type = q_item.get('building_type') or q_item.get('name')
|
||||||
|
if b_type:
|
||||||
|
future_levels[b_type] = future_levels.get(b_type, 0) + 1
|
||||||
|
|
||||||
|
log.warning(f"[blueprint] future_levels: {future_levels}")
|
||||||
|
|
||||||
|
# ── Simple phase search ────────────────────────────────────────────────────
|
||||||
|
# Strategy:
|
||||||
|
# 1. Find the first phase that has at least one building below its target.
|
||||||
|
# 2. Queue the first incomplete building from that phase — no resource checks,
|
||||||
|
# no dependency checks, exactly like adding it manually.
|
||||||
|
# 3. If every incomplete building in that phase is flagged has_max_level=True
|
||||||
|
# (meaning the game truly refuses to build it), look up to MAX_LOOKAHEAD_PHASES
|
||||||
|
# phases ahead for something to queue instead.
|
||||||
|
target_building = None
|
||||||
|
lookahead_used = 0
|
||||||
|
first_incomplete_phase_idx = None
|
||||||
|
|
||||||
|
for phase_idx, phase in enumerate(STANDARD_BLUEPRINT):
|
||||||
|
# Collect buildings that still need work in this phase
|
||||||
|
incomplete = [b for b, req in phase.items() if future_levels.get(b, 0) < req]
|
||||||
|
|
||||||
|
if not incomplete:
|
||||||
|
continue # Phase is fully complete, move on
|
||||||
|
|
||||||
|
# Track the very first incomplete phase we encounter
|
||||||
|
if first_incomplete_phase_idx is None:
|
||||||
|
first_incomplete_phase_idx = phase_idx
|
||||||
|
|
||||||
|
log.warning(f"[blueprint] Phase {phase_idx} incomplete: {incomplete}")
|
||||||
|
|
||||||
|
# Separate into: genuinely blocked (has_max=True) vs. queueable
|
||||||
|
blocked = [b for b in incomplete if build_data.get(b, {}).get('has_max_level', False)]
|
||||||
|
queueable = [b for b in incomplete if b not in blocked]
|
||||||
|
|
||||||
|
if queueable:
|
||||||
|
# We are in the current phase (or a valid lookahead phase) — queue it!
|
||||||
|
target_building = queueable[0]
|
||||||
|
log.warning(f"[blueprint] -> SELECTED '{target_building}' from phase {phase_idx}"
|
||||||
|
+ (f" (lookahead +{lookahead_used})" if lookahead_used else ""))
|
||||||
|
break
|
||||||
|
|
||||||
|
# Everything incomplete in this phase is has_max_level blocked.
|
||||||
|
# Allow limited lookahead.
|
||||||
|
if phase_idx == first_incomplete_phase_idx or lookahead_used < MAX_LOOKAHEAD_PHASES:
|
||||||
|
if phase_idx != first_incomplete_phase_idx:
|
||||||
|
lookahead_used += 1
|
||||||
|
log.warning(f"[blueprint] Phase {phase_idx}: all incomplete buildings are has_max — "
|
||||||
|
f"looking ahead (lookahead_used={lookahead_used})")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Ran out of lookahead budget — stop
|
||||||
|
log.warning(f"[blueprint] Lookahead exhausted after {lookahead_used} extra phases — giving up")
|
||||||
|
break
|
||||||
|
|
||||||
|
# ── Academy Research (fallback when no building target) ───────────────────
|
||||||
|
target_research = None
|
||||||
|
if not target_building:
|
||||||
|
academy_level = future_levels.get('academy', 0)
|
||||||
|
researched = town.get('researches', {})
|
||||||
|
for r_name in RESEARCH_LIST:
|
||||||
|
if not researched.get(r_name):
|
||||||
|
req_level = RESEARCH_LEVELS.get(r_name, 99)
|
||||||
|
if academy_level >= req_level:
|
||||||
|
target_research = r_name
|
||||||
|
log.warning(f"[blueprint] -> Research target: {r_name}")
|
||||||
|
break
|
||||||
|
|
||||||
|
log.warning(f"[blueprint] Final: target_building={target_building}, target_research={target_research}")
|
||||||
|
|
||||||
|
# ── Insert command ─────────────────────────────────────────────────────────
|
||||||
|
now = datetime.utcnow().isoformat()
|
||||||
|
|
||||||
|
if target_building:
|
||||||
|
pos_row = conn.execute(
|
||||||
|
"SELECT MAX(position) as max_pos FROM commands"
|
||||||
|
" WHERE player_id = ? AND town_id = ? AND type = 'build'"
|
||||||
|
" AND status IN ('pending', 'executing')",
|
||||||
|
(str(player_id), str(town_id))
|
||||||
|
).fetchone()
|
||||||
|
position = (pos_row['max_pos'] or 0) + 1
|
||||||
|
payload_str = json.dumps({"building_id": target_building})
|
||||||
|
conn.execute('''
|
||||||
|
INSERT INTO commands (town_id, town_name, type, payload, status, position, created_at, updated_at, player_id)
|
||||||
|
VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?)
|
||||||
|
''', (str(town_id), town_name_db, 'build', payload_str, position, now, now, str(player_id)))
|
||||||
|
log.warning(f"[blueprint] Inserted build command: {target_building} for {town_name_db}")
|
||||||
|
|
||||||
|
elif target_research:
|
||||||
|
payload_str = json.dumps({"research_id": target_research})
|
||||||
|
conn.execute('''
|
||||||
|
INSERT INTO commands (town_id, town_name, type, payload, status, created_at, updated_at, player_id)
|
||||||
|
VALUES (?, ?, ?, ?, 'pending', ?, ?, ?)
|
||||||
|
''', (str(town_id), town_name_db, 'research', payload_str, now, now, str(player_id)))
|
||||||
|
log.warning(f"[blueprint] Inserted research command: {target_research} for {town_name_db}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
log.warning(f"[blueprint] Nothing to do for {town_name_db}")
|
||||||
43
bot_modules/00_config.js
Normal file
43
bot_modules/00_config.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// ================================================================
|
||||||
|
// 00_config.js — Shared constants and utility helpers
|
||||||
|
// Runs first; everything here is available to all other modules.
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
const uw = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
|
||||||
|
const BASE_URL = 'https://grepo.haunter-pets.top';
|
||||||
|
|
||||||
|
// Read the clan key injected by the Loader before eval()
|
||||||
|
const CLAN_KEY = window.__GRC_CLAN_KEY || '';
|
||||||
|
|
||||||
|
// Returns a random integer between min and max (inclusive)
|
||||||
|
function randInt(min, max) {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedules fn to run after a random ms delay, then reschedules itself
|
||||||
|
function jitterLoop(fn, minMs, maxMs) {
|
||||||
|
function schedule() {
|
||||||
|
setTimeout(async () => {
|
||||||
|
await fn();
|
||||||
|
schedule();
|
||||||
|
}, randInt(minMs, maxMs));
|
||||||
|
}
|
||||||
|
schedule();
|
||||||
|
}
|
||||||
|
|
||||||
|
function log(msg) {
|
||||||
|
console.log(`[GRC] ${msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(r => setTimeout(r, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapper around fetch() that automatically injects the X-Clan-Key header
|
||||||
|
// on every request. Use this instead of fetch() everywhere in the bot.
|
||||||
|
function apiFetch(url, options) {
|
||||||
|
options = options || {};
|
||||||
|
options.headers = options.headers || {};
|
||||||
|
options.headers['X-Clan-Key'] = CLAN_KEY;
|
||||||
|
return fetch(url, options);
|
||||||
|
}
|
||||||
35
bot_modules/01_ui.js
Normal file
35
bot_modules/01_ui.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// ================================================================
|
||||||
|
// 01_ui.js — Toolbar button + pause toggle
|
||||||
|
// Depends on: uw, log (00_config.js)
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
const btnHtml = `
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="activity" id="grc_btn"
|
||||||
|
style="filter: brightness(70%) sepia(100%) hue-rotate(200deg) saturate(1000%) contrast(0.8);">
|
||||||
|
<p id="grc_label" style="position:relative;top:-8px;font-weight:bold;z-index:6;">Remote</p>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
let paused = false;
|
||||||
|
|
||||||
|
function togglePause() {
|
||||||
|
paused = !paused;
|
||||||
|
const label = document.getElementById('grc_label');
|
||||||
|
const btn = document.getElementById('grc_btn');
|
||||||
|
if (paused) {
|
||||||
|
label.textContent = 'Paused';
|
||||||
|
btn.style.filter = 'brightness(70%) sepia(100%) hue-rotate(-50deg) saturate(1000%) contrast(0.8)';
|
||||||
|
} else {
|
||||||
|
label.textContent = 'Remote';
|
||||||
|
btn.style.filter = 'brightness(294%) sepia(100%) hue-rotate(200deg) saturate(1000%) contrast(0.8)';
|
||||||
|
}
|
||||||
|
log(`Remote is now ${paused ? 'PAUSED' : 'ACTIVE'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!document.getElementById('grc_btn')) {
|
||||||
|
uw.$('.tb_activities, .toolbar_activities').find('.middle').append(btnHtml);
|
||||||
|
}
|
||||||
|
}, 4000);
|
||||||
|
|
||||||
|
uw.$(document).on('click', '#grc_btn', togglePause);
|
||||||
244
bot_modules/02_state.js
Normal file
244
bot_modules/02_state.js
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
// ================================================================
|
||||||
|
// 02_state.js — Gather & push town state to the relay server
|
||||||
|
// Depends on: uw, BASE_URL, log, paused (00_config.js / 01_ui.js)
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
function gatherState() {
|
||||||
|
const towns = uw.ITowns?.towns || {};
|
||||||
|
const player = uw.Game?.player_name || '';
|
||||||
|
const player_id = uw.Game?.player_id ?? null;
|
||||||
|
const alliance_id = uw.Game?.alliance_id ?? null;
|
||||||
|
let alliance_name = null;
|
||||||
|
try {
|
||||||
|
const pm = uw.MM.getModels().Player[player_id];
|
||||||
|
if (pm && pm.attributes) alliance_name = pm.attributes.alliance_name;
|
||||||
|
} catch (e) { log(`Failed to extract alliance_name: ${e}`); }
|
||||||
|
|
||||||
|
const total_points = uw.Game?.player_points ?? 0;
|
||||||
|
const world = uw.Game?.world_id || '';
|
||||||
|
|
||||||
|
const townList = Object.values(towns).map(town => {
|
||||||
|
const res = town.resources();
|
||||||
|
const buildings = town.buildings()?.attributes ?? {};
|
||||||
|
|
||||||
|
const unitsObj = {};
|
||||||
|
try {
|
||||||
|
const units = town.units();
|
||||||
|
if (units) {
|
||||||
|
Object.keys(units).forEach(k => {
|
||||||
|
unitsObj[k] = typeof units[k] === 'number'
|
||||||
|
? units[k]
|
||||||
|
: (units[k]?.getAmount?.() ?? 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
let buildQueue = [];
|
||||||
|
try {
|
||||||
|
const bo = town.buildingOrders?.();
|
||||||
|
if (bo?.models) buildQueue = bo.models.map(m => m.attributes);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
let buildDataMap = {};
|
||||||
|
try {
|
||||||
|
const buildDataRaw = uw.MM?.getModels?.()?.BuildingBuildData?.[town.id]?.attributes?.building_data || {};
|
||||||
|
for (const k in buildDataRaw) {
|
||||||
|
buildDataMap[k] = {
|
||||||
|
buildable: buildDataRaw[k].buildable,
|
||||||
|
dependencies: buildDataRaw[k].dependencies_fulfilled !== false,
|
||||||
|
wood: buildDataRaw[k].resources_for?.wood || 0,
|
||||||
|
stone: buildDataRaw[k].resources_for?.stone || 0,
|
||||||
|
iron: buildDataRaw[k].resources_for?.iron || 0,
|
||||||
|
pop: buildDataRaw[k].population_for || 0,
|
||||||
|
build_time: buildDataRaw[k].building_time || '',
|
||||||
|
can_upgrade: !!buildDataRaw[k].can_upgrade,
|
||||||
|
enough_resources: !!buildDataRaw[k].enough_resources,
|
||||||
|
missing_dependencies: buildDataRaw[k].missing_dependencies || [],
|
||||||
|
has_max_level: !!buildDataRaw[k].has_max_level
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) { log(`Failed to gather build data: ${e}`); }
|
||||||
|
|
||||||
|
// ---- Storage capacity -----------------------------------------------
|
||||||
|
let storageCapacity = 0;
|
||||||
|
try {
|
||||||
|
storageCapacity = town.getStorageCapacity?.() || 0;
|
||||||
|
if (!storageCapacity) {
|
||||||
|
const storageLevel = buildings.storage ?? 0;
|
||||||
|
const gd = uw.GameData?.buildingData?.storage;
|
||||||
|
storageCapacity = gd?.max_storage?.[storageLevel] || gd?.storage?.[storageLevel] || 0;
|
||||||
|
}
|
||||||
|
if (!storageCapacity) {
|
||||||
|
storageCapacity = res.capacity || res.storage_capacity || res.storage || 0;
|
||||||
|
}
|
||||||
|
} catch (e) { log(`storage capacity lookup failed: ${e}`); }
|
||||||
|
|
||||||
|
// ---- Market / Trade capacity -----------------------------------------
|
||||||
|
let marketCapacity = 0;
|
||||||
|
try {
|
||||||
|
const marketLevel = buildings.market ?? 0;
|
||||||
|
const gd = uw.GameData?.buildingData?.market;
|
||||||
|
marketCapacity = gd?.capacity_per_level?.[marketLevel] || 0;
|
||||||
|
if (buildings.trade_office && buildings.trade_office > 0) {
|
||||||
|
marketCapacity += (uw.GameData?.buildingData?.trade_office?.capacity_extra_per_level || 500) * marketLevel;
|
||||||
|
}
|
||||||
|
} catch (e) { log(`market capacity lookup failed: ${e}`); }
|
||||||
|
|
||||||
|
// ---- Coordinates & sea zone -----------------------------------------
|
||||||
|
let x = null, y = null, sea = null;
|
||||||
|
try {
|
||||||
|
x = town.getIslandCoordinateX?.() ?? null;
|
||||||
|
y = town.getIslandCoordinateY?.() ?? null;
|
||||||
|
if (typeof x === 'number' && typeof y === 'number') {
|
||||||
|
sea = Math.floor(x / 100) * 10 + Math.floor(y / 100);
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
// ---- Researches -----------------------------------------------------
|
||||||
|
let researches = {};
|
||||||
|
try {
|
||||||
|
const r = town.researches?.();
|
||||||
|
if (r) researches = r.attributes ?? (typeof r === 'object' ? r : {});
|
||||||
|
} catch (e) { log(`[Debug] town.researches() failed: ${e}`); }
|
||||||
|
|
||||||
|
// ---- Unit Data (Costs & Dependencies) -------------------------------
|
||||||
|
let unitDataMap = {};
|
||||||
|
try {
|
||||||
|
const gdUnits = uw.GameData?.units || {};
|
||||||
|
for (const u in gdUnits) {
|
||||||
|
if (u === 'militia') continue;
|
||||||
|
const reqBuildings = gdUnits[u].building_dependencies || {};
|
||||||
|
const reqResearch = gdUnits[u].research_dependencies || [];
|
||||||
|
let missing_deps = {};
|
||||||
|
for (const reqB in reqBuildings) {
|
||||||
|
if ((buildings[reqB] || 0) < reqBuildings[reqB]) {
|
||||||
|
missing_deps[reqB] = { name: reqB, needed_level: reqBuildings[reqB] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const reqR of reqResearch) {
|
||||||
|
if (!researches[reqR]) {
|
||||||
|
missing_deps[reqR] = { name: reqR, needed_level: 'Έρευνα' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const cost = gdUnits[u].resources || {};
|
||||||
|
const w = cost.wood || 0, s = cost.stone || 0, i = cost.iron || 0;
|
||||||
|
let enough = !(res.wood < w || res.stone < s || res.iron < i);
|
||||||
|
unitDataMap[u] = {
|
||||||
|
wood: w, stone: s, iron: i,
|
||||||
|
pop: gdUnits[u].population || 0,
|
||||||
|
build_time: gdUnits[u].build_time || 0,
|
||||||
|
enough_resources: enough,
|
||||||
|
missing_dependencies: missing_deps
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} 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,
|
||||||
|
farm_name: farm.attributes.name || '',
|
||||||
|
relation_id: rel.id,
|
||||||
|
relation_status: rel.attributes.relation_status,
|
||||||
|
expansion_stage: rel.attributes.expansion_stage || 0,
|
||||||
|
expansion_at: rel.attributes.expansion_at || 0,
|
||||||
|
lootable_at: rel.attributes.lootable_at || 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
// ---- Extra town flags -----------------------------------------------
|
||||||
|
let has_premium = false;
|
||||||
|
try {
|
||||||
|
has_premium = uw.GameDataPremium?.isAdvisorActivated?.('curator') || false;
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
town_id: town.id,
|
||||||
|
town_name: town.name,
|
||||||
|
x, y, sea,
|
||||||
|
wood: res.wood,
|
||||||
|
stone: res.stone,
|
||||||
|
iron: res.iron,
|
||||||
|
storage: storageCapacity,
|
||||||
|
market_capacity: marketCapacity,
|
||||||
|
population: res.population,
|
||||||
|
points: town.getPoints?.() ?? 0,
|
||||||
|
god: town.god?.() ?? null,
|
||||||
|
buildings,
|
||||||
|
units: unitsObj,
|
||||||
|
buildingOrder: buildQueue,
|
||||||
|
buildData: buildDataMap,
|
||||||
|
unitData: unitDataMap,
|
||||||
|
researches,
|
||||||
|
has_premium,
|
||||||
|
bonuses: {},
|
||||||
|
wonder_points: 0,
|
||||||
|
alliance_name,
|
||||||
|
farms,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { player, player_id, alliance_id, total_points, world_id: world, towns: townList };
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushState() {
|
||||||
|
if (paused) return;
|
||||||
|
try {
|
||||||
|
const payload = gatherState();
|
||||||
|
apiFetch(`${BASE_URL}/api/state`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
.then(() => log(`State pushed — ${payload.towns.length} towns`))
|
||||||
|
.catch(e => log(`State push failed: ${e}`));
|
||||||
|
} catch (e) {
|
||||||
|
log(`gatherState error: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- AJAX Interceptor for Zero-Delay Push ----------------------------
|
||||||
|
let pushTimeout = null;
|
||||||
|
function debouncedPushState() {
|
||||||
|
if (paused) return;
|
||||||
|
if (pushTimeout) clearTimeout(pushTimeout);
|
||||||
|
pushTimeout = setTimeout(() => {
|
||||||
|
log('⚡ State change detected (AJAX). Syncing to Remote...');
|
||||||
|
pushState();
|
||||||
|
}, 1200);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uw.$) {
|
||||||
|
uw.$(document).ajaxComplete(function (e, xhr, opt) {
|
||||||
|
if (!opt || !opt.url) return;
|
||||||
|
if (opt.url.includes(BASE_URL)) return;
|
||||||
|
if (opt.url.includes('map_tiles')) return;
|
||||||
|
if (opt.url.includes('action=') || opt.url.includes('switch_town')) {
|
||||||
|
debouncedPushState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Report command result back to relay -----------------------------
|
||||||
|
function reportResult(cmdId, status, message) {
|
||||||
|
apiFetch(`${BASE_URL}/api/commands/${cmdId}/result`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ status, message })
|
||||||
|
}).catch(e => log(`reportResult failed: ${e}`));
|
||||||
|
}
|
||||||
45
bot_modules/03_captcha.js
Normal file
45
bot_modules/03_captcha.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// ================================================================
|
||||||
|
// 03_captcha.js — hCaptcha detection & server alert
|
||||||
|
// Depends on: uw, BASE_URL, log, paused (00_config.js / 01_ui.js)
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
let captchaActive = false;
|
||||||
|
|
||||||
|
function reportCaptcha(detected) {
|
||||||
|
const player_id = uw.Game?.player_id;
|
||||||
|
if (!player_id) return;
|
||||||
|
apiFetch(`${BASE_URL}/api/captcha/alert?player_id=${player_id}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ detected })
|
||||||
|
}).catch(e => log(`captcha report failed: ${e}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectCaptcha() {
|
||||||
|
setInterval(() => {
|
||||||
|
const win = document.getElementById('hcaptcha_window');
|
||||||
|
let isVisible = false;
|
||||||
|
|
||||||
|
if (win) {
|
||||||
|
const style = window.getComputedStyle(win);
|
||||||
|
if (style.display !== 'none' && style.visibility !== 'hidden') {
|
||||||
|
isVisible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVisible && !captchaActive) {
|
||||||
|
captchaActive = true;
|
||||||
|
paused = true;
|
||||||
|
const label = document.getElementById('grc_label');
|
||||||
|
const btn = document.getElementById('grc_btn');
|
||||||
|
if (label) label.textContent = '⚠ CAPTCHA';
|
||||||
|
if (btn) btn.style.filter = 'brightness(70%) sepia(100%) hue-rotate(300deg) saturate(1000%) contrast(0.8)';
|
||||||
|
log('⚠ CAPTCHA detected — bot paused, alerting server');
|
||||||
|
reportCaptcha(true);
|
||||||
|
} else if (!isVisible && captchaActive) {
|
||||||
|
captchaActive = false;
|
||||||
|
log('✅ Captcha resolved — alert cleared (bot remains paused)');
|
||||||
|
reportCaptcha(false);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
215
bot_modules/04a_execute_farm.js
Normal file
215
bot_modules/04a_execute_farm.js
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
// ================================================================
|
||||||
|
// 04a_execute_farm.js — Farm command executors
|
||||||
|
// Depends on: uw, log, sleep, randInt, paused, pushState
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Execute: Farm Upgrade / Unlock
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
async function executeFarmUpgrade(cmd) {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
let farmModels, relModels;
|
||||||
|
try {
|
||||||
|
farmModels = uw.MM.getOnlyCollectionByName('FarmTown')?.models;
|
||||||
|
relModels = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation')?.models;
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, msg: `Cannot access farm collections: ${e.message}` };
|
||||||
|
}
|
||||||
|
if (!farmModels || !relModels) {
|
||||||
|
return { ok: false, msg: 'Farm collections not loaded yet' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build polis list (one town per island)
|
||||||
|
const islandsSeen = new Set();
|
||||||
|
const polisList = [];
|
||||||
|
try {
|
||||||
|
for (const town of uw.MM.getCollections().Town[0].models) {
|
||||||
|
const { on_small_island, island_id, id } = town.attributes;
|
||||||
|
if (on_small_island) continue;
|
||||||
|
if (!islandsSeen.has(island_id)) {
|
||||||
|
islandsSeen.add(island_id);
|
||||||
|
polisList.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, msg: `Cannot build town list: ${e.message}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
let upgraded = 0, unlocked = 0, skipped = 0, 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();
|
||||||
|
if (ix == null || iy == null) continue;
|
||||||
|
|
||||||
|
for (const farm of farmModels) {
|
||||||
|
if (farm.attributes.island_x !== ix || farm.attributes.island_y !== iy) continue;
|
||||||
|
|
||||||
|
for (const rel of relModels) {
|
||||||
|
if (rel.attributes.farm_town_id !== farm.attributes.id) continue;
|
||||||
|
|
||||||
|
const status = rel.attributes.relation_status;
|
||||||
|
const level = rel.attributes.expansion_stage || 0;
|
||||||
|
const expAt = rel.attributes.expansion_at || 0;
|
||||||
|
|
||||||
|
if (expAt > now) { skipped++; continue; }
|
||||||
|
if (status === 1 && level >= 5) { skipped++; continue; }
|
||||||
|
if (status < 0) { skipped++; continue; }
|
||||||
|
|
||||||
|
const isLocked = status === 0;
|
||||||
|
const action = isLocked ? 'unlock' : 'upgrade';
|
||||||
|
|
||||||
|
const requestedAction = cmd.payload?.action_type;
|
||||||
|
if (requestedAction && requestedAction !== action) { skipped++; continue; }
|
||||||
|
|
||||||
|
if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
|
||||||
|
|
||||||
|
log(`Farm ${action}: farm_id=${farm.attributes.id} level=${level} town=${town_id}`);
|
||||||
|
try {
|
||||||
|
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
|
||||||
|
model_url: `FarmTownPlayerRelation/${rel.id}`,
|
||||||
|
action_name: action,
|
||||||
|
arguments: { farm_town_id: farm.attributes.id },
|
||||||
|
town_id
|
||||||
|
});
|
||||||
|
isLocked ? unlocked++ : upgraded++;
|
||||||
|
} catch (e) { errors++; }
|
||||||
|
|
||||||
|
await sleep(randInt(4000, 10000));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pushState();
|
||||||
|
return { ok: true, msg: `Farm upgrade done: ${unlocked} unlocked, ${upgraded} upgraded, ${skipped} skipped, ${errors} errors` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Execute: Farm Loot
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
async function executeFarmLoot(cmd) {
|
||||||
|
const { loot_option } = cmd.payload || {};
|
||||||
|
const option = parseInt(loot_option) || 1;
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
let farmModels, relModels;
|
||||||
|
try {
|
||||||
|
farmModels = uw.MM.getOnlyCollectionByName('FarmTown')?.models;
|
||||||
|
relModels = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation')?.models;
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, msg: `Cannot access farm collections: ${e.message}` };
|
||||||
|
}
|
||||||
|
if (!farmModels || !relModels) {
|
||||||
|
return { ok: false, msg: 'Farm collections not loaded yet — open island view first' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build island groups using MM (all towns, not just visible)
|
||||||
|
const islandTownsMap = {};
|
||||||
|
try {
|
||||||
|
const allTowns = uw.MM.getCollections().Town[0].models;
|
||||||
|
for (const town of allTowns) {
|
||||||
|
const { on_small_island, island_id, id } = town.attributes;
|
||||||
|
if (on_small_island) continue;
|
||||||
|
if (!islandTownsMap[island_id]) islandTownsMap[island_id] = [];
|
||||||
|
islandTownsMap[island_id].push(id);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, msg: `Cannot build town list: ${e.message}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const islandList = Object.keys(islandTownsMap);
|
||||||
|
log(`Farm: processing ${islandList.length} islands with option=${option}`);
|
||||||
|
|
||||||
|
let claimed = 0, skipped = 0, errors = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < islandList.length; i++) {
|
||||||
|
const island_id = islandList[i];
|
||||||
|
const townIds = islandTownsMap[island_id];
|
||||||
|
|
||||||
|
let selected_town_id = null;
|
||||||
|
let lowest_total_res = Infinity;
|
||||||
|
|
||||||
|
for (const t_id of townIds) {
|
||||||
|
const t = uw.ITowns?.towns?.[t_id];
|
||||||
|
if (!t) continue;
|
||||||
|
|
||||||
|
let storageCapacity = t.getStorageCapacity?.() || 0;
|
||||||
|
if (!storageCapacity) {
|
||||||
|
const buildings = t.buildings?.()?.attributes || {};
|
||||||
|
const storageLevel = buildings.storage ?? 0;
|
||||||
|
const gd = uw.GameData?.buildingData?.storage;
|
||||||
|
storageCapacity = gd?.max_storage?.[storageLevel] || gd?.storage?.[storageLevel] || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = t.resources?.() || {};
|
||||||
|
const w = res.wood || 0, s = res.stone || 0, ir = res.iron || 0;
|
||||||
|
|
||||||
|
// Skip completely full towns
|
||||||
|
if (storageCapacity > 0 && w >= storageCapacity && s >= storageCapacity && ir >= storageCapacity) continue;
|
||||||
|
|
||||||
|
const total_res = w + s + ir;
|
||||||
|
if (total_res < lowest_total_res) {
|
||||||
|
lowest_total_res = total_res;
|
||||||
|
selected_town_id = t_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selected_town_id) {
|
||||||
|
log(`Farm: Skipping island ${island_id} (All towns are 100% full)`);
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const town_id = selected_town_id;
|
||||||
|
const town = uw.ITowns?.towns?.[town_id];
|
||||||
|
if (!town) { skipped++; continue; }
|
||||||
|
|
||||||
|
const ix = town.getIslandCoordinateX();
|
||||||
|
const iy = town.getIslandCoordinateY();
|
||||||
|
if (ix == null || iy == null) { skipped++; continue; }
|
||||||
|
|
||||||
|
const readyFarms = [];
|
||||||
|
for (const farm of farmModels) {
|
||||||
|
if (farm.attributes.island_x !== ix || farm.attributes.island_y !== iy) continue;
|
||||||
|
for (const rel of relModels) {
|
||||||
|
if (
|
||||||
|
rel.attributes.farm_town_id === farm.attributes.id &&
|
||||||
|
rel.attributes.relation_status === 1 &&
|
||||||
|
(!rel.attributes.lootable_at || now >= rel.attributes.lootable_at)
|
||||||
|
) {
|
||||||
|
readyFarms.push({
|
||||||
|
town_id,
|
||||||
|
farm_town_id: rel.attributes.farm_town_id,
|
||||||
|
relation_id: rel.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (readyFarms.length === 0) { skipped++; continue; }
|
||||||
|
log(`Farm: ${readyFarms.length} ready on island of town ${town_id}`);
|
||||||
|
|
||||||
|
for (const farm of readyFarms) {
|
||||||
|
if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
|
||||||
|
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: farm.town_id
|
||||||
|
});
|
||||||
|
claimed++;
|
||||||
|
} catch (e) { errors++; }
|
||||||
|
|
||||||
|
await sleep(randInt(500, 1500));
|
||||||
|
}
|
||||||
|
|
||||||
|
try { uw.WMap.removeFarmTownLootCooldownIconAndRefreshLootTimers(); } catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true, msg: `Farm done: ${claimed} claimed, ${skipped} islands skipped, ${errors} errors` };
|
||||||
|
}
|
||||||
139
bot_modules/04b_execute_admin.js
Normal file
139
bot_modules/04b_execute_admin.js
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
// ================================================================
|
||||||
|
// 04b_execute_admin.js — Admin command executors
|
||||||
|
// Depends on: uw, log, sleep, randInt, paused
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Execute: Build
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
async function executeBuild(cmd) {
|
||||||
|
const { town_id, payload } = cmd;
|
||||||
|
const { building_id } = payload;
|
||||||
|
|
||||||
|
const town = uw.ITowns?.getTown?.(town_id) || uw.ITowns?.towns?.[town_id];
|
||||||
|
if (!town) return { ok: false, msg: `Town ${town_id} not found in ITowns` };
|
||||||
|
|
||||||
|
const queueLen = town.buildingOrders?.()?.length ?? 0;
|
||||||
|
const hasCurator = uw.GameDataPremium?.isAdvisorActivated?.('curator');
|
||||||
|
const maxQueue = hasCurator ? 7 : 2;
|
||||||
|
if (queueLen >= maxQueue) {
|
||||||
|
return { ok: false, requeue: true, msg: `Build queue full (${queueLen}/${maxQueue})` };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const buildData = uw.MM.getModels?.()?.BuildingBuildData?.[town_id]
|
||||||
|
?.attributes?.building_data?.[building_id];
|
||||||
|
if (buildData) {
|
||||||
|
const res = town.resources();
|
||||||
|
const { resources_for, population_for } = buildData;
|
||||||
|
if (town.getAvailablePopulation?.() < population_for) {
|
||||||
|
return { ok: false, requeue: true, msg: `Not enough population for ${building_id}` };
|
||||||
|
}
|
||||||
|
if (res.wood < resources_for.wood || res.stone < resources_for.stone || res.iron < resources_for.iron) {
|
||||||
|
return { ok: false, requeue: true, msg: `Not enough resources for ${building_id}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { log(`Resource check skipped: ${e}`); }
|
||||||
|
|
||||||
|
const reactionMs = randInt(800, 2500);
|
||||||
|
log(`Waiting ${reactionMs}ms before firing buildUp (reaction time)...`);
|
||||||
|
await sleep(reactionMs);
|
||||||
|
|
||||||
|
if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
|
||||||
|
|
||||||
|
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
|
||||||
|
model_url: 'BuildingOrder',
|
||||||
|
action_name: 'buildUp',
|
||||||
|
arguments: { building_id },
|
||||||
|
town_id
|
||||||
|
});
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
return { ok: true, msg: `buildUp ${building_id} queued` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Execute: Recruit
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
async function executeRecruit(cmd) {
|
||||||
|
const { town_id, payload } = cmd;
|
||||||
|
const { unit_id, amount } = payload;
|
||||||
|
|
||||||
|
const town = uw.ITowns?.getTown?.(town_id) || uw.ITowns?.towns?.[town_id];
|
||||||
|
if (!town) return { ok: false, msg: `Town ${town_id} not found` };
|
||||||
|
|
||||||
|
const navalUnits = [
|
||||||
|
'big_transporter', 'small_transporter', 'bireme',
|
||||||
|
'attack_ship', 'trireme', 'colonize_ship', 'sea_monster'
|
||||||
|
];
|
||||||
|
const endpoint = navalUnits.includes(unit_id) ? 'building_docks' : 'building_barracks';
|
||||||
|
|
||||||
|
const reactionMs = randInt(800, 2500);
|
||||||
|
log(`Waiting ${reactionMs}ms before firing recruit (reaction time)...`);
|
||||||
|
await sleep(reactionMs);
|
||||||
|
|
||||||
|
if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
|
||||||
|
|
||||||
|
uw.gpAjax.ajaxPost(endpoint, 'build', {
|
||||||
|
unit_id,
|
||||||
|
amount: parseInt(amount) || 1,
|
||||||
|
town_id
|
||||||
|
});
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
return { ok: true, msg: `Recruit ${amount}x ${unit_id} submitted` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Execute: Market Offer
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
async function executeMarketOffer(cmd) {
|
||||||
|
const { town_id, payload } = cmd;
|
||||||
|
const { offer, offer_type, demand, demand_type, max_delivery_time, visibility } = payload;
|
||||||
|
|
||||||
|
const town = uw.ITowns?.getTown?.(town_id) || uw.ITowns?.towns?.[town_id];
|
||||||
|
if (!town) return { ok: false, msg: `Town ${town_id} not found` };
|
||||||
|
|
||||||
|
const reactionMs = randInt(800, 2500);
|
||||||
|
log(`Waiting ${reactionMs}ms before firing market offer (reaction time)...`);
|
||||||
|
await sleep(reactionMs);
|
||||||
|
|
||||||
|
if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
|
||||||
|
|
||||||
|
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
|
||||||
|
model_url: 'CreateOffers/' + town_id,
|
||||||
|
action_name: 'createOffer',
|
||||||
|
captcha: null,
|
||||||
|
arguments: { offer, offer_type, demand, demand_type, max_delivery_time, visibility }
|
||||||
|
});
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
return { ok: true, msg: `Market offer posted: ${offer} ${offer_type} => ${demand} ${demand_type}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Execute: Research (Academy)
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
async function executeResearch(cmd) {
|
||||||
|
const { town_id, payload } = cmd;
|
||||||
|
const { research_id } = payload;
|
||||||
|
|
||||||
|
const town = uw.ITowns?.getTown?.(town_id) || uw.ITowns?.towns?.[town_id];
|
||||||
|
if (!town) return { ok: false, msg: `Town ${town_id} not found` };
|
||||||
|
|
||||||
|
const reactionMs = randInt(800, 2500);
|
||||||
|
log(`Waiting ${reactionMs}ms before firing research (reaction time)...`);
|
||||||
|
await sleep(reactionMs);
|
||||||
|
|
||||||
|
if (paused) return { ok: false, msg: 'Aborted due to pause/captcha' };
|
||||||
|
|
||||||
|
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
|
||||||
|
model_url: 'ResearchOrder',
|
||||||
|
action_name: 'research',
|
||||||
|
arguments: { id: research_id },
|
||||||
|
town_id
|
||||||
|
});
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
return { ok: true, msg: `Research ${research_id} queued` };
|
||||||
|
}
|
||||||
307
bot_modules/04c_execute_bootcamp_trade.js
Normal file
307
bot_modules/04c_execute_bootcamp_trade.js
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
// ================================================================
|
||||||
|
// 04c_execute_bootcamp_trade.js
|
||||||
|
// Auto-Bootcamp & Auto-Rural-Trade loops
|
||||||
|
//
|
||||||
|
// Both are driven by jitterLoop (registered in 05_main.js boot()).
|
||||||
|
// Settings arrive via cmdData.bot_settings in the poll response —
|
||||||
|
// no extra network call is needed.
|
||||||
|
//
|
||||||
|
// Timers (human-like, randomised):
|
||||||
|
// Bootcamp : 12–22 min (camp cooldown is ~12–15 min)
|
||||||
|
// RuralTrade: 25–45 min (trading is low-urgency)
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
// Shared cache — set by pollAndExecute every 8-18 s
|
||||||
|
let lastKnownBotSettings = {};
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// botLog — sends a log entry to the server
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
async function botLog(player_id, world_id, feature, message) {
|
||||||
|
log(`[${feature}] ${message}`);
|
||||||
|
try {
|
||||||
|
await apiFetch(`${BASE_URL}/api/bot-logs`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ player_id, world_id, feature, message })
|
||||||
|
});
|
||||||
|
} catch (e) { /* non-critical */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// AUTO BOOTCAMP
|
||||||
|
// ================================================================
|
||||||
|
async function autoBootcampLoop() {
|
||||||
|
if (paused) return;
|
||||||
|
|
||||||
|
const settings = lastKnownBotSettings;
|
||||||
|
if (!settings.bootcamp_enabled) return;
|
||||||
|
|
||||||
|
const player_id = uw.Game?.player_id;
|
||||||
|
const world_id = uw.Game?.world_id;
|
||||||
|
if (!player_id || !world_id) return;
|
||||||
|
|
||||||
|
let model;
|
||||||
|
try {
|
||||||
|
model = uw.MM.getModelByNameAndPlayerId('PlayerAttackSpot');
|
||||||
|
} catch (e) { return; }
|
||||||
|
|
||||||
|
// Model not loaded yet (player hasn't opened the camp UI this session)
|
||||||
|
if (!model || typeof model.getLevel?.() === 'undefined') {
|
||||||
|
log('[bootcamp] PlayerAttackSpot model not ready — skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 1. Claim reward if available ──────────────────────────────
|
||||||
|
try {
|
||||||
|
const hasReward = model.hasReward?.();
|
||||||
|
if (hasReward) {
|
||||||
|
const reward = model.getReward?.();
|
||||||
|
if (reward) {
|
||||||
|
const isInstant = reward.power_id?.includes('instant');
|
||||||
|
const isFavor = reward.power_id?.includes('favor');
|
||||||
|
const stashable = reward.stashable;
|
||||||
|
|
||||||
|
const useReward = () => {
|
||||||
|
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
|
||||||
|
model_url: `PlayerAttackSpot/${player_id}`,
|
||||||
|
action_name: 'useReward',
|
||||||
|
arguments: {}
|
||||||
|
});
|
||||||
|
botLog(player_id, world_id, 'bootcamp', `Reward used: ${reward.power_id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isInstant && !isFavor) {
|
||||||
|
useReward();
|
||||||
|
} else if (stashable) {
|
||||||
|
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
|
||||||
|
model_url: `PlayerAttackSpot/${player_id}`,
|
||||||
|
action_name: 'stashReward',
|
||||||
|
arguments: {}
|
||||||
|
}, 0, {
|
||||||
|
success: () => {
|
||||||
|
botLog(player_id, world_id, 'bootcamp', `Reward stashed: ${reward.power_id}`);
|
||||||
|
},
|
||||||
|
error: useReward
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
useReward();
|
||||||
|
}
|
||||||
|
await sleep(randInt(3000, 7000));
|
||||||
|
return; // Wait for next cycle to attack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { /* reward check failed — continue to attack check */ }
|
||||||
|
|
||||||
|
// ── 2. Attack if no cooldown ───────────────────────────────────
|
||||||
|
try {
|
||||||
|
// If getCooldownDuration is unavailable, skip safely
|
||||||
|
if (typeof model.getCooldownDuration !== 'function') {
|
||||||
|
log('[bootcamp] getCooldownDuration unavailable — skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cooldown = model.getCooldownDuration();
|
||||||
|
if (cooldown > 0) {
|
||||||
|
const minRemaining = Math.round(cooldown / 60);
|
||||||
|
await botLog(player_id, world_id, 'bootcamp', `Camp on cooldown — ${minRemaining} min remaining`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check no existing attack movement to/from camp
|
||||||
|
const movements = uw.MM.getModels()?.MovementsUnits;
|
||||||
|
if (movements) {
|
||||||
|
for (const mv of Object.values(movements)) {
|
||||||
|
if (mv.attributes.destination_is_attack_spot || mv.attributes.origin_is_attack_spot) {
|
||||||
|
await botLog(player_id, world_id, 'bootcamp', 'Attack already in flight — skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect units from current town
|
||||||
|
const currentTownId = uw.Game?.townId;
|
||||||
|
if (!currentTownId) return;
|
||||||
|
|
||||||
|
const town = uw.ITowns?.towns?.[currentTownId];
|
||||||
|
if (!town) return;
|
||||||
|
|
||||||
|
const units = { ...town.units?.() };
|
||||||
|
delete units.militia;
|
||||||
|
|
||||||
|
// Remove naval
|
||||||
|
for (const unit in units) {
|
||||||
|
if (uw.GameData?.units?.[unit]?.is_naval) delete units[unit];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove defensive if use_def is off
|
||||||
|
if (!settings.bootcamp_use_def) {
|
||||||
|
delete units.sword;
|
||||||
|
delete units.archer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove zero-count
|
||||||
|
for (const unit in units) {
|
||||||
|
if (!units[unit] || units[unit] <= 0) delete units[unit];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(units).length === 0) {
|
||||||
|
await botLog(player_id, world_id, 'bootcamp', 'No available units — skipping attack. (Χωρίς αμυντικά)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
|
||||||
|
model_url: `PlayerAttackSpot/${player_id}`,
|
||||||
|
action_name: 'attack',
|
||||||
|
arguments: units
|
||||||
|
});
|
||||||
|
|
||||||
|
const unitSummary = Object.entries(units).map(([u, n]) => `${n}x${u}`).join(', ');
|
||||||
|
await botLog(player_id, world_id, 'bootcamp', `Στέλνω ${JSON.stringify(units)} στο camp...`);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
await botLog(player_id, world_id, 'bootcamp', `Error during attack: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// AUTO RURAL TRADE
|
||||||
|
// Triggers only when a town's pending build command is stuck due to
|
||||||
|
// insufficient resources. Trades for the specific missing resource.
|
||||||
|
//
|
||||||
|
// Ratio map: 1→0.25, 2→0.5, 3→0.75, 4→1.0, 5→1.25
|
||||||
|
// ================================================================
|
||||||
|
const RATIO_MAP = { 1: 0.25, 2: 0.50, 3: 0.75, 4: 1.00, 5: 1.25 };
|
||||||
|
|
||||||
|
async function autoRuralTradeLoop() {
|
||||||
|
if (paused) return;
|
||||||
|
|
||||||
|
const settings = lastKnownBotSettings;
|
||||||
|
if (!settings.rural_trade_enabled) return;
|
||||||
|
|
||||||
|
const player_id = uw.Game?.player_id;
|
||||||
|
const world_id = uw.Game?.world_id;
|
||||||
|
if (!player_id || !world_id) return;
|
||||||
|
|
||||||
|
const minRatio = RATIO_MAP[settings.rural_trade_ratio] ?? 0.75;
|
||||||
|
|
||||||
|
let farmModels, relModels;
|
||||||
|
try {
|
||||||
|
farmModels = uw.MM.getOnlyCollectionByName('FarmTown')?.models;
|
||||||
|
relModels = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation')?.models;
|
||||||
|
} catch (e) { return; }
|
||||||
|
|
||||||
|
if (!farmModels || !relModels) return;
|
||||||
|
|
||||||
|
// ── Find towns with stuck build commands ───────────────────────
|
||||||
|
// We look at each town's in-game build queue. If the queue slot
|
||||||
|
// is empty but we have a pending remote command, the build is
|
||||||
|
// waiting — check which resource it needs.
|
||||||
|
const towns = uw.ITowns?.towns;
|
||||||
|
if (!towns) return;
|
||||||
|
|
||||||
|
let tradesTotal = 0;
|
||||||
|
|
||||||
|
for (const [town_id_str, town] of Object.entries(towns)) {
|
||||||
|
if (paused) return;
|
||||||
|
|
||||||
|
// Get next pending build for this town from BuildingBuildData
|
||||||
|
let missingResource = null;
|
||||||
|
try {
|
||||||
|
const buildData = uw.MM.getModels()?.BuildingBuildData?.[town_id_str];
|
||||||
|
if (!buildData) continue;
|
||||||
|
|
||||||
|
const orders = town.buildingOrders?.()?.models ?? [];
|
||||||
|
// Only check if the in-game queue has room (build could be submitted)
|
||||||
|
const queueCap = uw.GameDataPremium?.isAdvisorActivated('curator') ? 7 : 2;
|
||||||
|
if (orders.length >= queueCap) continue; // Queue full — not stuck on resources
|
||||||
|
|
||||||
|
// Check all building types to find one we might be trying to build
|
||||||
|
// Heuristic: look for any building where we have less resources than needed
|
||||||
|
const allBuildingData = buildData.attributes?.building_data ?? {};
|
||||||
|
const res = town.resources?.() ?? {};
|
||||||
|
|
||||||
|
for (const [building_id, bdata] of Object.entries(allBuildingData)) {
|
||||||
|
if (!bdata.resources_for) continue;
|
||||||
|
const cost = bdata.resources_for;
|
||||||
|
const w = cost.wood || 0;
|
||||||
|
const s = cost.stone || 0;
|
||||||
|
const ir = cost.iron || 0;
|
||||||
|
|
||||||
|
// Skip if we can already afford it
|
||||||
|
if (res.wood >= w && res.stone >= s && res.iron >= ir) continue;
|
||||||
|
|
||||||
|
// Find which resource is most lacking (relative to cost)
|
||||||
|
const shortfalls = [];
|
||||||
|
if (w > 0) shortfalls.push({ res: 'wood', ratio: res.wood / w });
|
||||||
|
if (s > 0) shortfalls.push({ res: 'stone', ratio: res.stone / s });
|
||||||
|
if (ir > 0) shortfalls.push({ res: 'iron', ratio: res.iron / ir });
|
||||||
|
|
||||||
|
if (shortfalls.length === 0) continue;
|
||||||
|
shortfalls.sort((a, b) => a.ratio - b.ratio);
|
||||||
|
missingResource = shortfalls[0].res;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (e) { continue; }
|
||||||
|
|
||||||
|
if (!missingResource) continue;
|
||||||
|
|
||||||
|
// ── Find farm villages on this island offering missingResource ──
|
||||||
|
const town_obj = town;
|
||||||
|
const ix = town_obj.getIslandCoordinateX?.();
|
||||||
|
const iy = town_obj.getIslandCoordinateY?.();
|
||||||
|
if (ix == null || iy == null) continue;
|
||||||
|
|
||||||
|
let tradeMade = false;
|
||||||
|
for (const farm of farmModels) {
|
||||||
|
if (farm.attributes.island_x !== ix || farm.attributes.island_y !== iy) continue;
|
||||||
|
if (farm.attributes.resource_offer !== missingResource) continue;
|
||||||
|
|
||||||
|
for (const rel of relModels) {
|
||||||
|
if (rel.attributes.farm_town_id !== farm.attributes.id) continue;
|
||||||
|
if (rel.attributes.relation_status !== 1) continue; // must be allied
|
||||||
|
|
||||||
|
// Check ratio meets minimum
|
||||||
|
const tradeRatio = rel.attributes.current_trade_ratio ?? 0;
|
||||||
|
if (tradeRatio < minRatio) continue;
|
||||||
|
|
||||||
|
const tradeCapacity = town_obj.getAvailableTradeCapacity?.() ?? 0;
|
||||||
|
if (tradeCapacity < 100) continue;
|
||||||
|
|
||||||
|
const amount = Math.min(tradeCapacity, 3000);
|
||||||
|
|
||||||
|
if (paused) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
|
||||||
|
model_url: `FarmTownPlayerRelation/${rel.id}`,
|
||||||
|
action_name: 'trade',
|
||||||
|
arguments: { farm_town_id: farm.attributes.id, amount },
|
||||||
|
town_id: parseInt(town_id_str)
|
||||||
|
});
|
||||||
|
await botLog(player_id, world_id, 'rural_trade',
|
||||||
|
`Traded ${amount} ${missingResource} ← ${farm.attributes.name} via ${town_obj.getName?.() ?? town_id_str}`);
|
||||||
|
tradesTotal++;
|
||||||
|
tradeMade = true;
|
||||||
|
} catch (e) {
|
||||||
|
await botLog(player_id, world_id, 'rural_trade', `Trade error: ${e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(randInt(800, 1800));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tradeMade) break; // One trade per town per cycle is enough
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tradeMade && missingResource) {
|
||||||
|
await botLog(player_id, world_id, 'rural_trade',
|
||||||
|
`${town_obj.getName?.() ?? town_id_str} needs ${missingResource} but no suitable village found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(randInt(500, 1200));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tradesTotal === 0) {
|
||||||
|
log('[rural_trade] No stuck builds or no tradeable villages — nothing to do');
|
||||||
|
}
|
||||||
|
}
|
||||||
243
bot_modules/05_main.js
Normal file
243
bot_modules/05_main.js
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
// ================================================================
|
||||||
|
// 05_main.js — Poll loop, command dispatch, boot
|
||||||
|
// Depends on: everything above
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
// Shared farm state — prevents auto-farm and explicit farm_loot commands
|
||||||
|
// from running concurrently. Also caches last-known farm settings so the
|
||||||
|
// auto-farm loop doesn't need its own API call.
|
||||||
|
let farmLootRunning = false;
|
||||||
|
let lastKnownFarmSettings = {};
|
||||||
|
|
||||||
|
// Loot option → cooldown ms (matches game's farm timer options)
|
||||||
|
const LOOT_TIMINGS = { 1: 300000, 2: 1200000, 3: 5400000, 4: 14400000 };
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// scheduleNextFarm — fires autoFarmLoop once, then reschedules
|
||||||
|
// Delay = loot_option cooldown + random 30-120s human jitter.
|
||||||
|
// This mirrors ModernBot's pattern: run exactly when farms are ready.
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
function scheduleNextFarm(isFirstRun = false) {
|
||||||
|
let totalMs = 15000; // 15 seconds for the first run to catch already-ready farms
|
||||||
|
if (!isFirstRun) {
|
||||||
|
const option = lastKnownFarmSettings.loot_option || 1;
|
||||||
|
const baseMs = LOOT_TIMINGS[option] || 300000;
|
||||||
|
const jitterMs = randInt(30000, 120000); // +30 to +120 s
|
||||||
|
totalMs = baseMs + jitterMs;
|
||||||
|
}
|
||||||
|
log(`⏰ Next auto-farm in ${(totalMs / 60000).toFixed(1)} min`);
|
||||||
|
setTimeout(async () => {
|
||||||
|
await autoFarmLoop();
|
||||||
|
scheduleNextFarm(false);
|
||||||
|
}, totalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// pollAndExecute — runs every 8–18 s (main command loop)
|
||||||
|
// Handles builds, recruits, market, research, explicit farm commands.
|
||||||
|
// Auto-farm has its own separate loop below.
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
async function pollAndExecute() {
|
||||||
|
if (paused) return;
|
||||||
|
const player_id = uw.Game?.player_id;
|
||||||
|
const world_id = uw.Game?.world_id;
|
||||||
|
if (!player_id) return;
|
||||||
|
|
||||||
|
let cmdData;
|
||||||
|
try {
|
||||||
|
const res = await apiFetch(`${BASE_URL}/api/commands/pending?player_id=${player_id}&world_id=${world_id}`);
|
||||||
|
cmdData = await res.json();
|
||||||
|
} catch (e) {
|
||||||
|
log(`Poll failed: ${e}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache farm + bot settings so autonomous loops can read them without extra calls
|
||||||
|
lastKnownFarmSettings = cmdData.farm_settings || {};
|
||||||
|
lastKnownBotSettings = cmdData.bot_settings || {};
|
||||||
|
|
||||||
|
// Handle manual bootcamp attack trigger
|
||||||
|
if (lastKnownBotSettings.attack_now) {
|
||||||
|
log('Manual bootcamp attack requested! Firing immediately...');
|
||||||
|
// Fire asynchronously so it doesn't block the rest of pollAndExecute
|
||||||
|
setTimeout(autoBootcampLoop, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feature flags — default to all on if server doesn't send them (backward compatible)
|
||||||
|
const features = cmdData.enabled_features || ['farm', 'admin'];
|
||||||
|
const farmOn = features.includes('farm');
|
||||||
|
const adminOn = features.includes('admin');
|
||||||
|
|
||||||
|
// Build: one command per town (server returns an array)
|
||||||
|
const buildCmds = adminOn ? (cmdData.builds || []) : [];
|
||||||
|
const recruitCmd = adminOn ? cmdData.recruit : null;
|
||||||
|
const marketCmd = adminOn ? cmdData.market : null;
|
||||||
|
const researchCmd = adminOn ? cmdData.research : null;
|
||||||
|
const farmCmd = farmOn ? cmdData.farm : null;
|
||||||
|
const farmUpgradeCmd = farmOn ? cmdData.farm_upgrade : null;
|
||||||
|
|
||||||
|
if (cmdData.sync_requested) {
|
||||||
|
log('Sync requested by server — pushing state immediately');
|
||||||
|
pushState();
|
||||||
|
}
|
||||||
|
|
||||||
|
const execute = async (cmd) => {
|
||||||
|
if (!cmd) return;
|
||||||
|
log(`Executing command #${cmd.id} — type:${cmd.type} town:${cmd.town_id}`);
|
||||||
|
if (paused) {
|
||||||
|
log(`[Paused] Ignoring command #${cmd.id}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
if (cmd.type === 'build') result = await executeBuild(cmd);
|
||||||
|
else if (cmd.type === 'recruit') result = await executeRecruit(cmd);
|
||||||
|
else if (cmd.type === 'market_offer') result = await executeMarketOffer(cmd);
|
||||||
|
else if (cmd.type === 'research') result = await executeResearch(cmd);
|
||||||
|
else if (cmd.type === 'farm_upgrade') result = await executeFarmUpgrade(cmd);
|
||||||
|
else if (cmd.type === 'farm_loot') {
|
||||||
|
// Guard: if auto-farm is mid-run, requeue rather than overlap
|
||||||
|
if (farmLootRunning) {
|
||||||
|
result = { ok: false, requeue: true, msg: 'Auto-farm in progress — requeueing' };
|
||||||
|
} else {
|
||||||
|
farmLootRunning = true;
|
||||||
|
try { result = await executeFarmLoot(cmd); }
|
||||||
|
finally { farmLootRunning = false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else result = { ok: false, msg: `Unknown type: ${cmd.type}` };
|
||||||
|
} catch (e) {
|
||||||
|
result = { ok: false, msg: `Exception: ${e}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalStatus = result.requeue ? 'pending' : (result.ok ? 'done' : 'failed');
|
||||||
|
log(`Command #${cmd.id}: ${finalStatus === 'done' ? '✅' : finalStatus === 'pending' ? '⏳' : '❌'} ${result.msg}`);
|
||||||
|
reportResult(cmd.id, finalStatus, result.msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute one build command per town (simultaneous queue draining across all villages)
|
||||||
|
for (let i = 0; i < buildCmds.length; i++) {
|
||||||
|
await execute(buildCmds[i]);
|
||||||
|
if (i < buildCmds.length - 1) {
|
||||||
|
// Random inter-town gap — avoids looking like a macro
|
||||||
|
const gap = randInt(1500, 3000);
|
||||||
|
log(`Build: town done. Waiting ${gap}ms before next town...`);
|
||||||
|
await sleep(gap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await execute(recruitCmd);
|
||||||
|
await execute(marketCmd);
|
||||||
|
await execute(researchCmd);
|
||||||
|
await execute(farmCmd);
|
||||||
|
await execute(farmUpgradeCmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// autoFarmLoop — runs every 60–120 s (independent of main poll)
|
||||||
|
// Checks warehouse capacity and loots ready farms automatically.
|
||||||
|
// Completely decoupled from pollAndExecute so builds/recruits
|
||||||
|
// are never blocked by the long inter-island farm delays.
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
async function autoFarmLoop() {
|
||||||
|
if (paused) return;
|
||||||
|
|
||||||
|
const farmSettings = lastKnownFarmSettings;
|
||||||
|
if (!farmSettings.enabled) return;
|
||||||
|
|
||||||
|
// Don't overlap with an explicit farm_loot command running in main loop
|
||||||
|
if (farmLootRunning) {
|
||||||
|
log('Auto-farm: explicit farm_loot in progress — skipping this cycle');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any farms are actually ready before doing anything heavy
|
||||||
|
const nowTs = Math.floor(Date.now() / 1000);
|
||||||
|
let readyFarms = [];
|
||||||
|
try {
|
||||||
|
const coll = uw.MM.getOnlyCollectionByName('FarmTownPlayerRelation');
|
||||||
|
readyFarms = coll?.models?.filter(r =>
|
||||||
|
r.attributes.relation_status === 1 &&
|
||||||
|
(r.attributes.lootable_at || 0) <= nowTs
|
||||||
|
) || [];
|
||||||
|
} catch (e) { return; }
|
||||||
|
|
||||||
|
if (readyFarms.length === 0) return;
|
||||||
|
log(`⚡ Auto-farm: ${readyFarms.length} ready farms found`);
|
||||||
|
|
||||||
|
// Check if ALL warehouses are already full (>95%) — no point looting
|
||||||
|
const towns = Object.values(uw.ITowns?.towns || {});
|
||||||
|
let allFull = true;
|
||||||
|
for (const town of towns) {
|
||||||
|
const res = town.resources?.() || {};
|
||||||
|
let storage = town.getStorageCapacity?.() || 0;
|
||||||
|
if (!storage) {
|
||||||
|
const buildings = town.buildings?.()?.attributes || {};
|
||||||
|
const storageLevel = buildings.storage ?? 0;
|
||||||
|
const gd = uw.GameData?.buildingData?.storage;
|
||||||
|
storage = gd?.max_storage?.[storageLevel] || gd?.storage?.[storageLevel] || 0;
|
||||||
|
}
|
||||||
|
if (!storage) storage = res.capacity || res.storage_capacity || res.storage || 0;
|
||||||
|
if (!storage) continue;
|
||||||
|
const maxRes = Math.max(res.wood || 0, res.stone || 0, res.iron || 0);
|
||||||
|
if (maxRes / storage < 0.95) { allFull = false; break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
const player_id = uw.Game?.player_id;
|
||||||
|
if (allFull) {
|
||||||
|
log('⚠️ Auto-farm: ALL warehouses are full (>95%) — skipping loot this cycle');
|
||||||
|
try {
|
||||||
|
const world_id = uw.Game?.world_id || '';
|
||||||
|
await apiFetch(`${BASE_URL}/api/farm_status?player_id=${player_id}&world_id=${world_id}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ warehouse_full: true })
|
||||||
|
});
|
||||||
|
} catch (e) {}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All clear — run the loot
|
||||||
|
farmLootRunning = true;
|
||||||
|
try {
|
||||||
|
await executeFarmLoot({ payload: { loot_option: farmSettings.loot_option } });
|
||||||
|
// Report success so dashboard shows last_farmed_at
|
||||||
|
try {
|
||||||
|
const world_id = uw.Game?.world_id || '';
|
||||||
|
await apiFetch(`${BASE_URL}/api/farm_status?player_id=${player_id}&world_id=${world_id}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ warehouse_full: false })
|
||||||
|
});
|
||||||
|
} catch (e) {}
|
||||||
|
pushState();
|
||||||
|
} finally {
|
||||||
|
farmLootRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Boot — works whether page is already loaded or not.
|
||||||
|
// When eval()'d dynamically the 'load' event has already fired,
|
||||||
|
// so we check readyState and boot immediately in that case.
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
function boot() {
|
||||||
|
log('Grepolis Remote Control v4.2.0 (remote) loaded');
|
||||||
|
detectCaptcha();
|
||||||
|
setTimeout(pushState, 5000);
|
||||||
|
jitterLoop(pushState, 60000, 120000); // state sync every 1–2 min
|
||||||
|
jitterLoop(pollAndExecute, 8000, 18000, 2000); // command poll every 8–18 s, but start in 2s
|
||||||
|
scheduleNextFarm(true); // auto-farm timer-based (loot_option + 30–120s)
|
||||||
|
jitterLoop(autoBootcampLoop, 720000, 1320000); // bootcamp every 12–22 min
|
||||||
|
jitterLoop(autoRuralTradeLoop, 1500000, 2700000); // rural trade every 25–45 min
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'complete') {
|
||||||
|
// Page already loaded (normal case when eval()'d dynamically)
|
||||||
|
boot();
|
||||||
|
} else {
|
||||||
|
// Fallback: wait for load event (shouldn't happen but safe to keep)
|
||||||
|
window.addEventListener('load', boot);
|
||||||
|
}
|
||||||
95
db.py
95
db.py
@@ -1,5 +1,6 @@
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
import os
|
import os
|
||||||
|
import secrets
|
||||||
|
|
||||||
DB_PATH = os.path.join(os.path.dirname(__file__), 'grepo.db')
|
DB_PATH = os.path.join(os.path.dirname(__file__), 'grepo.db')
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ def init_db():
|
|||||||
payload TEXT NOT NULL, -- JSON string
|
payload TEXT NOT NULL, -- JSON string
|
||||||
status TEXT NOT NULL DEFAULT 'pending', -- pending | executing | done | failed
|
status TEXT NOT NULL DEFAULT 'pending', -- pending | executing | done | failed
|
||||||
result_msg TEXT,
|
result_msg TEXT,
|
||||||
|
position INTEGER, -- manual sort order for build queue (lower = first)
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
)
|
)
|
||||||
@@ -66,6 +68,40 @@ def init_db():
|
|||||||
)
|
)
|
||||||
''')
|
''')
|
||||||
|
|
||||||
|
# Bot settings — per-player config for bootcamp & rural-trade auto-loops
|
||||||
|
c.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS bot_settings (
|
||||||
|
player_id TEXT PRIMARY KEY,
|
||||||
|
bootcamp_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
bootcamp_use_def INTEGER NOT NULL DEFAULT 0,
|
||||||
|
rural_trade_enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
rural_trade_ratio INTEGER NOT NULL DEFAULT 3, -- 1=0.25 2=0.5 3=0.75 4=1.0 5=1.25
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Bot logs — ring buffer of last 50 entries per player per feature
|
||||||
|
c.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS bot_logs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
player_id TEXT NOT NULL,
|
||||||
|
feature TEXT NOT NULL, -- 'bootcamp' | 'rural_trade'
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
c.execute('CREATE INDEX IF NOT EXISTS idx_bot_logs_player_feature ON bot_logs(player_id, feature)')
|
||||||
|
|
||||||
|
# Blueprints - assigns a blueprint to a specific town
|
||||||
|
c.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS town_blueprints (
|
||||||
|
town_id TEXT PRIMARY KEY,
|
||||||
|
blueprint_name TEXT NOT NULL,
|
||||||
|
is_active INTEGER NOT NULL DEFAULT 1,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
# Migration: add new columns if upgrading an existing database
|
# Migration: add new columns if upgrading an existing database
|
||||||
for _col in [
|
for _col in [
|
||||||
'ALTER TABLE town_state ADD COLUMN player_id TEXT',
|
'ALTER TABLE town_state ADD COLUMN player_id TEXT',
|
||||||
@@ -74,12 +110,71 @@ def init_db():
|
|||||||
'ALTER TABLE town_state ADD COLUMN y REAL',
|
'ALTER TABLE town_state ADD COLUMN y REAL',
|
||||||
'ALTER TABLE town_state ADD COLUMN sea INTEGER',
|
'ALTER TABLE town_state ADD COLUMN sea INTEGER',
|
||||||
'ALTER TABLE commands ADD COLUMN player_id TEXT',
|
'ALTER TABLE commands ADD COLUMN player_id TEXT',
|
||||||
|
'ALTER TABLE commands ADD COLUMN position INTEGER',
|
||||||
'ALTER TABLE farm_settings ADD COLUMN bandit_camp_enabled INTEGER NOT NULL DEFAULT 0',
|
'ALTER TABLE farm_settings ADD COLUMN bandit_camp_enabled INTEGER NOT NULL DEFAULT 0',
|
||||||
|
"ALTER TABLE clan_members ADD COLUMN features TEXT NOT NULL DEFAULT 'farm,admin'",
|
||||||
|
'ALTER TABLE users ADD COLUMN clan_id INTEGER REFERENCES clans(id)',
|
||||||
]:
|
]:
|
||||||
try:
|
try:
|
||||||
c.execute(_col)
|
c.execute(_col)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # column already exists
|
pass # column already exists
|
||||||
|
|
||||||
|
# Back-fill position for existing rows that have NULL position
|
||||||
|
try:
|
||||||
|
c.execute('UPDATE commands SET position = id WHERE position IS NULL')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Users — website admin accounts
|
||||||
|
c.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
clan_id INTEGER REFERENCES clans(id),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Clans — groups owned by a user, identified by a unique clan_key
|
||||||
|
c.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS clans (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
owner_id INTEGER NOT NULL REFERENCES users(id),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
clan_key TEXT NOT NULL UNIQUE,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Clan members — links Grepolis player_ids to a clan
|
||||||
|
c.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS clan_members (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
clan_id INTEGER NOT NULL REFERENCES clans(id),
|
||||||
|
player_id TEXT NOT NULL,
|
||||||
|
player_name TEXT,
|
||||||
|
features TEXT NOT NULL DEFAULT 'farm,admin',
|
||||||
|
joined_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
UNIQUE(clan_id, player_id)
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Migration: Auto-assign existing users to their clan_id if they are the owner
|
||||||
|
try:
|
||||||
|
c.execute('''
|
||||||
|
UPDATE users
|
||||||
|
SET clan_id = (SELECT id FROM clans WHERE owner_id = users.id)
|
||||||
|
WHERE clan_id IS NULL AND EXISTS (SELECT 1 FROM clans WHERE owner_id = users.id)
|
||||||
|
''')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def generate_clan_key():
|
||||||
|
"""Generate a short, unique, human-readable clan key."""
|
||||||
|
return secrets.token_urlsafe(8).upper()[:10]
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
flask
|
flask
|
||||||
flask-cors
|
flask-cors
|
||||||
|
flask-login
|
||||||
gunicorn
|
gunicorn
|
||||||
412
routes/api.py
412
routes/api.py
@@ -1,11 +1,49 @@
|
|||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from db import get_db
|
from db import get_db
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
|
import os
|
||||||
|
from flask import make_response
|
||||||
|
from blueprint_engine import evaluate_blueprints
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_log = logging.getLogger(__name__)
|
||||||
|
|
||||||
api = Blueprint('api', __name__)
|
api = Blueprint('api', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Helper — look up clan by the X-Clan-Key header.
|
||||||
|
# Returns the clan row dict, or None if key is missing / invalid.
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _get_clan_from_request():
|
||||||
|
key = request.headers.get('X-Clan-Key', '').strip()
|
||||||
|
if not key:
|
||||||
|
return None
|
||||||
|
conn = get_db()
|
||||||
|
clan = conn.execute('SELECT * FROM clans WHERE clan_key = ?', (key,)).fetchone()
|
||||||
|
conn.close()
|
||||||
|
return clan
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Helper — auto-register a player_id under a clan on first push.
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _auto_register_member(clan_id, player_id, player_name):
|
||||||
|
conn = get_db()
|
||||||
|
conn.execute('''
|
||||||
|
INSERT OR IGNORE INTO clan_members (clan_id, player_id, player_name)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
''', (clan_id, str(player_id), player_name or ''))
|
||||||
|
# Update name in case it changed
|
||||||
|
conn.execute('''
|
||||||
|
UPDATE clan_members SET player_name = ?
|
||||||
|
WHERE clan_id = ? AND player_id = ?
|
||||||
|
''', (player_name or '', clan_id, str(player_id)))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# POST /api/state
|
# POST /api/state
|
||||||
# Tampermonkey pushes a full town snapshot every poll cycle.
|
# Tampermonkey pushes a full town snapshot every poll cycle.
|
||||||
@@ -16,11 +54,16 @@ def receive_state():
|
|||||||
if not data:
|
if not data:
|
||||||
return jsonify({'error': 'no data'}), 400
|
return jsonify({'error': 'no data'}), 400
|
||||||
|
|
||||||
towns = data.get('towns', [])
|
towns = data.get('towns', [])
|
||||||
player = data.get('player', '')
|
player = data.get('player', '')
|
||||||
player_id = data.get('player_id', '')
|
player_id = data.get('player_id', '')
|
||||||
alliance_id = str(data.get('alliance_id', '') or '')
|
alliance_id = str(data.get('alliance_id', '') or '')
|
||||||
world_id = data.get('world_id', '')
|
world_id = data.get('world_id', '')
|
||||||
|
|
||||||
|
# Auto-register this player to the clan that matches the key (if any)
|
||||||
|
clan = _get_clan_from_request()
|
||||||
|
if clan:
|
||||||
|
_auto_register_member(clan['id'], player_id, player)
|
||||||
|
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
@@ -55,6 +98,13 @@ def receive_state():
|
|||||||
datetime.utcnow().isoformat()
|
datetime.utcnow().isoformat()
|
||||||
))
|
))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
try:
|
||||||
|
evaluate_blueprints(conn)
|
||||||
|
conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
print("Error evaluating blueprints:", e)
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({'ok': True, 'towns_updated': len(towns)})
|
return jsonify({'ok': True, 'towns_updated': len(towns)})
|
||||||
|
|
||||||
@@ -65,13 +115,30 @@ def receive_state():
|
|||||||
# Returns one 'build' AND one 'recruit' command independently,
|
# Returns one 'build' AND one 'recruit' command independently,
|
||||||
# so both queues are served in parallel without blocking each other.
|
# so both queues are served in parallel without blocking each other.
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
def _fetch_pending_of_type(c, cmd_type, player_id):
|
def _fetch_pending_of_type(c, cmd_type, player_id, world_id):
|
||||||
row = c.execute('''
|
"""Fetch a single oldest pending command of a given type (recruit, market, etc.)."""
|
||||||
SELECT * FROM commands
|
|
||||||
WHERE status = 'pending' AND type = ? AND player_id = ?
|
# We use LEFT JOIN because global commands (like farm_loot) use a pseudo town_id like "0_gr121"
|
||||||
ORDER BY id ASC
|
# which does not exist in town_state.
|
||||||
LIMIT 1
|
global_town_id = f"0_{world_id}" if world_id else "0"
|
||||||
''', (cmd_type, player_id)).fetchone()
|
|
||||||
|
if world_id:
|
||||||
|
row = c.execute('''
|
||||||
|
SELECT c.* FROM commands c
|
||||||
|
LEFT JOIN town_state ts ON c.town_id = ts.town_id
|
||||||
|
WHERE c.status = 'pending' AND c.type = ? AND c.player_id = ?
|
||||||
|
AND (ts.world_id = ? OR c.town_id = ?)
|
||||||
|
ORDER BY c.updated_at ASC, c.id ASC
|
||||||
|
LIMIT 1
|
||||||
|
''', (cmd_type, player_id, world_id, global_town_id)).fetchone()
|
||||||
|
else:
|
||||||
|
row = c.execute('''
|
||||||
|
SELECT * FROM commands
|
||||||
|
WHERE status = 'pending' AND type = ? AND player_id = ?
|
||||||
|
ORDER BY updated_at ASC, id ASC
|
||||||
|
LIMIT 1
|
||||||
|
''', (cmd_type, player_id)).fetchone()
|
||||||
|
|
||||||
if not row:
|
if not row:
|
||||||
return None
|
return None
|
||||||
c.execute('''
|
c.execute('''
|
||||||
@@ -86,45 +153,168 @@ def _fetch_pending_of_type(c, cmd_type, player_id):
|
|||||||
'payload': json.loads(row['payload'])
|
'payload': json.loads(row['payload'])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_pending_builds_all_towns(c, player_id, world_id):
|
||||||
|
"""
|
||||||
|
Fetch ONE pending 'build' command per distinct town_id.
|
||||||
|
This allows all towns to build in parallel within a single poll cycle.
|
||||||
|
Within each town the oldest-updated command is picked first, so requeued
|
||||||
|
commands (updated_at = now) naturally sort behind fresh ones.
|
||||||
|
|
||||||
|
Towns that already have a command in 'executing' state are skipped —
|
||||||
|
this prevents a second build from being dispatched before the first one
|
||||||
|
has reported its result (which was causing commands to pile up in EXECUTING).
|
||||||
|
"""
|
||||||
|
# Towns that currently have a build already in-flight — don't touch those.
|
||||||
|
if world_id:
|
||||||
|
executing_rows = c.execute('''
|
||||||
|
SELECT DISTINCT c.town_id FROM commands c
|
||||||
|
JOIN town_state ts ON c.town_id = ts.town_id
|
||||||
|
WHERE c.status = 'executing' AND c.type = 'build' AND c.player_id = ? AND ts.world_id = ?
|
||||||
|
''', (player_id, world_id)).fetchall()
|
||||||
|
else:
|
||||||
|
executing_rows = c.execute('''
|
||||||
|
SELECT DISTINCT town_id FROM commands
|
||||||
|
WHERE status = 'executing' AND type = 'build' AND player_id = ?
|
||||||
|
''', (player_id,)).fetchall()
|
||||||
|
busy_towns = {r['town_id'] for r in executing_rows}
|
||||||
|
|
||||||
|
# Get every town that has at least one pending build, ordered by
|
||||||
|
# which town has been waiting longest (MIN updated_at across its commands).
|
||||||
|
if world_id:
|
||||||
|
town_rows = c.execute('''
|
||||||
|
SELECT c.town_id
|
||||||
|
FROM commands c
|
||||||
|
JOIN town_state ts ON c.town_id = ts.town_id
|
||||||
|
WHERE c.status = 'pending' AND c.type = 'build' AND c.player_id = ? AND ts.world_id = ?
|
||||||
|
GROUP BY c.town_id
|
||||||
|
ORDER BY MIN(c.updated_at) ASC
|
||||||
|
''', (player_id, world_id)).fetchall()
|
||||||
|
else:
|
||||||
|
town_rows = c.execute('''
|
||||||
|
SELECT town_id
|
||||||
|
FROM commands
|
||||||
|
WHERE status = 'pending' AND type = 'build' AND player_id = ?
|
||||||
|
GROUP BY town_id
|
||||||
|
ORDER BY MIN(updated_at) ASC
|
||||||
|
''', (player_id,)).fetchall()
|
||||||
|
_log.warning(f"[poll] build towns found: {[r['town_id'] for r in town_rows]}, busy: {busy_towns}")
|
||||||
|
|
||||||
|
results = []
|
||||||
|
now = datetime.utcnow().isoformat()
|
||||||
|
for town_row in town_rows:
|
||||||
|
town_id = town_row['town_id']
|
||||||
|
|
||||||
|
# Skip this town if a build is already executing for it
|
||||||
|
if town_id in busy_towns:
|
||||||
|
continue
|
||||||
|
|
||||||
|
row = c.execute('''
|
||||||
|
SELECT * FROM commands
|
||||||
|
WHERE status = 'pending' AND type = 'build'
|
||||||
|
AND player_id = ? AND town_id = ?
|
||||||
|
ORDER BY position ASC, id ASC
|
||||||
|
LIMIT 1
|
||||||
|
''', (player_id, town_id)).fetchone()
|
||||||
|
if not row:
|
||||||
|
continue
|
||||||
|
c.execute('''
|
||||||
|
UPDATE commands SET status = 'executing', updated_at = ?
|
||||||
|
WHERE id = ?
|
||||||
|
''', (now, row['id']))
|
||||||
|
results.append({
|
||||||
|
'id': row['id'],
|
||||||
|
'town_id': row['town_id'],
|
||||||
|
'type': row['type'],
|
||||||
|
'payload': json.loads(row['payload'])
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
@api.route('/api/commands/pending', methods=['GET'])
|
@api.route('/api/commands/pending', methods=['GET'])
|
||||||
def get_pending_command():
|
def get_pending_command():
|
||||||
player_id = request.args.get('player_id')
|
player_id = request.args.get('player_id')
|
||||||
|
world_id = request.args.get('world_id')
|
||||||
|
_log.warning(f"[poll] player_id={player_id!r} world_id={world_id!r}")
|
||||||
if not player_id:
|
if not player_id:
|
||||||
return jsonify({'error': 'no player_id provided'}), 400
|
return jsonify({'error': 'no player_id provided'}), 400
|
||||||
|
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
|
|
||||||
build_cmd = _fetch_pending_of_type(c, 'build', player_id)
|
# Free up stuck 'executing' commands (e.g. if the game page was refreshed mid-execution)
|
||||||
recruit_cmd = _fetch_pending_of_type(c, 'recruit', player_id)
|
two_minutes_ago = (datetime.utcnow() - timedelta(minutes=2)).isoformat()
|
||||||
market_cmd = _fetch_pending_of_type(c, 'market_offer', player_id)
|
c.execute('''
|
||||||
farm_cmd = _fetch_pending_of_type(c, 'farm_loot', player_id)
|
UPDATE commands
|
||||||
farm_upgrade_cmd = _fetch_pending_of_type(c, 'farm_upgrade', player_id)
|
SET status = 'pending', result_msg = 'Requeued (timeout)'
|
||||||
sync_req = _check_and_reset_sync(c, player_id)
|
WHERE status = 'executing' AND updated_at < ? AND player_id = ?
|
||||||
|
''', (two_minutes_ago, player_id))
|
||||||
|
|
||||||
# Also return current farm settings so TM knows loot_option
|
build_cmds = _fetch_pending_builds_all_towns(c, player_id, world_id) # one per town
|
||||||
|
recruit_cmd = _fetch_pending_of_type(c, 'recruit', player_id, world_id)
|
||||||
|
market_cmd = _fetch_pending_of_type(c, 'market_offer', player_id, world_id)
|
||||||
|
farm_cmd = _fetch_pending_of_type(c, 'farm_loot', player_id, world_id)
|
||||||
|
farm_upgrade_cmd = _fetch_pending_of_type(c, 'farm_upgrade', player_id, world_id)
|
||||||
|
research_cmd = _fetch_pending_of_type(c, 'research', player_id, world_id)
|
||||||
|
sync_req = _check_and_reset_sync(c, player_id)
|
||||||
|
|
||||||
|
# Determine player_key for world-specific settings if world_id is provided
|
||||||
|
player_key = f"{player_id}_{world_id}" if world_id else player_id
|
||||||
|
|
||||||
|
# Farm settings
|
||||||
farm_row = c.execute(
|
farm_row = c.execute(
|
||||||
'SELECT enabled, bandit_camp_enabled, loot_option FROM farm_settings WHERE player_id = ?', (player_id,)
|
'SELECT enabled, loot_option FROM farm_settings WHERE player_id = ?', (player_key,)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
farm_settings = {
|
farm_settings = {
|
||||||
'enabled': bool(farm_row['enabled']) if farm_row else False,
|
'enabled': bool(farm_row['enabled']) if farm_row else False,
|
||||||
'bandit_camp_enabled': bool(farm_row['bandit_camp_enabled']) if farm_row else False,
|
'loot_option': farm_row['loot_option'] if farm_row else 1
|
||||||
'loot_option': farm_row['loot_option'] if farm_row else 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Bot settings (bootcamp + rural trade)
|
||||||
|
bot_row = c.execute(
|
||||||
|
'SELECT * FROM bot_settings WHERE player_id = ?', (player_key,)
|
||||||
|
).fetchone()
|
||||||
|
bot_settings = {
|
||||||
|
'bootcamp_enabled': bool(bot_row['bootcamp_enabled']) if bot_row else False,
|
||||||
|
'bootcamp_use_def': bool(bot_row['bootcamp_use_def']) if bot_row else False,
|
||||||
|
'rural_trade_enabled': bool(bot_row['rural_trade_enabled']) if bot_row else False,
|
||||||
|
'rural_trade_ratio': bot_row['rural_trade_ratio'] if bot_row else 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
# One-shot manual attack flag
|
||||||
|
attack_now_key = f'bootcamp_attack_now_{player_key}'
|
||||||
|
flag_row = c.execute('SELECT value FROM kv_store WHERE key = ?', (attack_now_key,)).fetchone()
|
||||||
|
if flag_row and flag_row['value'] == '1':
|
||||||
|
bot_settings['attack_now'] = True
|
||||||
|
c.execute("UPDATE kv_store SET value = '0' WHERE key = ?", (attack_now_key,))
|
||||||
|
else:
|
||||||
|
bot_settings['attack_now'] = False
|
||||||
|
|
||||||
|
# Feature flags — look up this player's authorized features from their clan
|
||||||
|
member_row = c.execute(
|
||||||
|
'SELECT features FROM clan_members WHERE player_id = ?', (str(player_id),)
|
||||||
|
).fetchone()
|
||||||
|
if member_row and member_row['features']:
|
||||||
|
enabled_features = [f.strip() for f in member_row['features'].split(',') if f.strip()]
|
||||||
|
else:
|
||||||
|
enabled_features = ['farm', 'admin'] # default: all on (backward-compatible)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'build': build_cmd,
|
'builds': build_cmds, # list: one build command per town
|
||||||
'recruit': recruit_cmd,
|
'recruit': recruit_cmd,
|
||||||
'market': market_cmd,
|
'market': market_cmd,
|
||||||
'farm': farm_cmd,
|
'research': research_cmd,
|
||||||
'farm_upgrade': farm_upgrade_cmd,
|
'farm': farm_cmd,
|
||||||
'farm_settings': farm_settings,
|
'farm_upgrade': farm_upgrade_cmd,
|
||||||
'sync_requested': sync_req
|
'farm_settings': farm_settings,
|
||||||
|
'bot_settings': bot_settings,
|
||||||
|
'enabled_features': enabled_features,
|
||||||
|
'sync_requested': sync_req
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def _check_and_reset_sync(c, player_id):
|
def _check_and_reset_sync(c, player_id):
|
||||||
key = f'sync_request_{player_id}'
|
key = f'sync_request_{player_id}'
|
||||||
row = c.execute("SELECT value FROM kv_store WHERE key = ?", (key,)).fetchone()
|
row = c.execute("SELECT value FROM kv_store WHERE key = ?", (key,)).fetchone()
|
||||||
@@ -161,15 +351,32 @@ def sync_request():
|
|||||||
@api.route('/api/commands/<int:cmd_id>/result', methods=['POST'])
|
@api.route('/api/commands/<int:cmd_id>/result', methods=['POST'])
|
||||||
def command_result(cmd_id):
|
def command_result(cmd_id):
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
status = data.get('status', 'done') # 'done' | 'failed'
|
status = data.get('status', 'done') # 'done' | 'failed' | 'pending' (requeue)
|
||||||
msg = data.get('message', '')
|
msg = data.get('message', '')
|
||||||
|
now = datetime.utcnow().isoformat()
|
||||||
|
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
|
# Look up type + player_id for post-update hooks
|
||||||
|
cmd = conn.execute(
|
||||||
|
'SELECT type, player_id FROM commands WHERE id = ?', (cmd_id,)
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
conn.execute('''
|
conn.execute('''
|
||||||
UPDATE commands
|
UPDATE commands
|
||||||
SET status = ?, result_msg = ?, updated_at = ?
|
SET status = ?, result_msg = ?, updated_at = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
''', (status, msg, datetime.utcnow().isoformat(), cmd_id))
|
''', (status, msg, now, cmd_id))
|
||||||
|
|
||||||
|
# When an explicit farm_loot command succeeds, record the timestamp
|
||||||
|
if cmd and cmd['type'] == 'farm_loot' and status == 'done' and cmd['player_id']:
|
||||||
|
town_id = str(cmd['town_id'])
|
||||||
|
world_id = town_id.split('_')[1] if '_' in town_id else None
|
||||||
|
lf_key = f'last_farmed_{cmd["player_id"]}_{world_id}' if world_id else f'last_farmed_{cmd["player_id"]}'
|
||||||
|
conn.execute('''
|
||||||
|
INSERT INTO kv_store (key, value, updated_at) VALUES (?, ?, ?)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at
|
||||||
|
''', (lf_key, now, now))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({'ok': True})
|
return jsonify({'ok': True})
|
||||||
@@ -201,3 +408,142 @@ def captcha_alert():
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({'ok': True})
|
return jsonify({'ok': True})
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# POST /api/market_data
|
||||||
|
# Tampermonkey uploads the market scan data.
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@api.route('/api/market_data', methods=['POST'])
|
||||||
|
def upload_market_data():
|
||||||
|
player_id = request.args.get('player_id')
|
||||||
|
if not player_id:
|
||||||
|
return jsonify({'error': 'no player_id provided'}), 400
|
||||||
|
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
kv_key = f'market_data_{player_id}'
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
conn.execute('''
|
||||||
|
INSERT INTO kv_store (key, value, updated_at)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET
|
||||||
|
value = excluded.value,
|
||||||
|
updated_at = excluded.updated_at
|
||||||
|
''', (kv_key, json.dumps(data), datetime.utcnow().isoformat()))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'ok': True})
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# POST/GET /api/farm_status — TM reports warehouse_full; dashboard reads it
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@api.route('/api/farm_status', methods=['POST', 'GET'])
|
||||||
|
def farm_status():
|
||||||
|
player_id = request.args.get('player_id')
|
||||||
|
world_id = request.args.get('world_id')
|
||||||
|
if not player_id:
|
||||||
|
return jsonify({'error': 'no player_id'}), 400
|
||||||
|
|
||||||
|
player_key = f"{player_id}_{world_id}" if world_id else player_id
|
||||||
|
kv_key = f'farm_status_{player_key}'
|
||||||
|
conn = get_db()
|
||||||
|
if request.method == 'POST':
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
now = datetime.utcnow().isoformat()
|
||||||
|
conn.execute('''
|
||||||
|
INSERT INTO kv_store (key, value, updated_at) VALUES (?, ?, ?)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at
|
||||||
|
''', (kv_key, json.dumps(data), now))
|
||||||
|
# Auto-farm reports warehouse_full=false when it successfully looted something
|
||||||
|
if not data.get('warehouse_full', True):
|
||||||
|
conn.execute('''
|
||||||
|
INSERT INTO kv_store (key, value, updated_at) VALUES (?, ?, ?)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at
|
||||||
|
''', (f'last_farmed_{player_key}', now, now))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'ok': True})
|
||||||
|
else:
|
||||||
|
row = conn.execute('SELECT value FROM kv_store WHERE key=?', (kv_key,)).fetchone()
|
||||||
|
conn.close()
|
||||||
|
return jsonify(json.loads(row['value']) if row else {'warehouse_full': False})
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# POST /api/bot-logs
|
||||||
|
# TM bot reports log entries for bootcamp / rural_trade loops.
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@api.route('/api/bot-logs', methods=['POST'])
|
||||||
|
def api_bot_logs():
|
||||||
|
clan = _get_clan_from_request()
|
||||||
|
if not clan:
|
||||||
|
return make_response('Unauthorized', 403)
|
||||||
|
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
player_id = str(data.get('player_id', ''))
|
||||||
|
world_id = str(data.get('world_id', ''))
|
||||||
|
feature = data.get('feature', '')
|
||||||
|
message = data.get('message', '')
|
||||||
|
|
||||||
|
if not player_id or not feature or not message:
|
||||||
|
return jsonify({'error': 'missing fields'}), 400
|
||||||
|
|
||||||
|
player_key = f"{player_id}_{world_id}" if world_id else player_id
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
conn.execute(
|
||||||
|
'INSERT INTO bot_logs (player_id, feature, message) VALUES (?, ?, ?)',
|
||||||
|
(player_key, feature, message)
|
||||||
|
)
|
||||||
|
# Keep only latest 50 per player/feature
|
||||||
|
conn.execute('''
|
||||||
|
DELETE FROM bot_logs
|
||||||
|
WHERE player_id = ? AND feature = ?
|
||||||
|
AND id NOT IN (
|
||||||
|
SELECT id FROM bot_logs
|
||||||
|
WHERE player_id = ? AND feature = ?
|
||||||
|
ORDER BY id DESC LIMIT 50
|
||||||
|
)
|
||||||
|
''', (player_key, feature, player_key, feature))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'ok': True})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET /api/bot
|
||||||
|
# Serves the modular bot code concatenated into a single response
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@api.route('/api/bot', methods=['GET'])
|
||||||
|
def serve_bot():
|
||||||
|
# Require a valid clan key — reject unknown clients
|
||||||
|
clan = _get_clan_from_request()
|
||||||
|
if not clan:
|
||||||
|
return make_response('Unauthorized: invalid or missing clan key', 403)
|
||||||
|
|
||||||
|
bot_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'bot_modules')
|
||||||
|
if not os.path.exists(bot_dir):
|
||||||
|
return make_response("Bot modules directory not found", 404)
|
||||||
|
|
||||||
|
|
||||||
|
modules = sorted([f for f in os.listdir(bot_dir) if f.endswith('.js')])
|
||||||
|
|
||||||
|
combined_code = []
|
||||||
|
combined_code.append("(function() {")
|
||||||
|
combined_code.append(" 'use strict';\n")
|
||||||
|
|
||||||
|
for module in modules:
|
||||||
|
with open(os.path.join(bot_dir, module), 'r', encoding='utf-8') as f:
|
||||||
|
combined_code.append(f" // --- BEGIN {module} ---")
|
||||||
|
combined_code.append(f.read())
|
||||||
|
combined_code.append(f" // --- END {module} ---\n")
|
||||||
|
|
||||||
|
combined_code.append("})();")
|
||||||
|
|
||||||
|
response = make_response("\n".join(combined_code))
|
||||||
|
response.headers['Content-Type'] = 'application/javascript'
|
||||||
|
# Prevent caching so updates are instant
|
||||||
|
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
|
||||||
|
return response
|
||||||
|
|
||||||
|
|||||||
319
routes/auth.py
Normal file
319
routes/auth.py
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, flash
|
||||||
|
from flask_login import login_user, logout_user, login_required, current_user
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
from db import get_db, generate_clan_key
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
auth = Blueprint('auth', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Helper — resolve the User class from app (avoid circular import)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _make_user(row):
|
||||||
|
from app import User
|
||||||
|
return User(row['id'], row['username'], row['clan_id'])
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET/POST /auth/login
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@auth.route('/auth/login', methods=['GET', 'POST'])
|
||||||
|
def login():
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
return redirect(url_for('dashboard.index'))
|
||||||
|
|
||||||
|
error = None
|
||||||
|
if request.method == 'POST':
|
||||||
|
username = request.form.get('username', '').strip()
|
||||||
|
password = request.form.get('password', '')
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
row = conn.execute(
|
||||||
|
'SELECT id, username, password_hash, clan_id FROM users WHERE username = ?', (username,)
|
||||||
|
).fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if row and check_password_hash(row['password_hash'], password):
|
||||||
|
user = _make_user(row)
|
||||||
|
login_user(user, remember=True)
|
||||||
|
return redirect(url_for('dashboard.index'))
|
||||||
|
else:
|
||||||
|
error = 'Λάθος όνομα χρήστη ή κωδικός.'
|
||||||
|
|
||||||
|
return render_template('login.html', error=error)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET/POST /auth/register
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@auth.route('/auth/register', methods=['GET', 'POST'])
|
||||||
|
def register():
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
return redirect(url_for('dashboard.index'))
|
||||||
|
|
||||||
|
error = None
|
||||||
|
if request.method == 'POST':
|
||||||
|
username = request.form.get('username', '').strip()
|
||||||
|
password = request.form.get('password', '')
|
||||||
|
confirm = request.form.get('confirm', '')
|
||||||
|
|
||||||
|
if not username or not password:
|
||||||
|
error = 'Συμπλήρωσε όνομα χρήστη και κωδικό.'
|
||||||
|
elif password != confirm:
|
||||||
|
error = 'Οι κωδικοί δεν ταιριάζουν.'
|
||||||
|
elif len(password) < 6:
|
||||||
|
error = 'Ο κωδικός πρέπει να έχει τουλάχιστον 6 χαρακτήρες.'
|
||||||
|
else:
|
||||||
|
conn = get_db()
|
||||||
|
existing = conn.execute(
|
||||||
|
'SELECT id FROM users WHERE username = ?', (username,)
|
||||||
|
).fetchone()
|
||||||
|
if existing:
|
||||||
|
error = 'Το όνομα χρήστη χρησιμοποιείται ήδη.'
|
||||||
|
conn.close()
|
||||||
|
else:
|
||||||
|
pw_hash = generate_password_hash(password)
|
||||||
|
conn.execute(
|
||||||
|
'INSERT INTO users (username, password_hash) VALUES (?, ?)',
|
||||||
|
(username, pw_hash)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
row = conn.execute(
|
||||||
|
'SELECT id, username, password_hash, clan_id FROM users WHERE username = ?', (username,)
|
||||||
|
).fetchone()
|
||||||
|
conn.close()
|
||||||
|
user = _make_user(row)
|
||||||
|
login_user(user, remember=True)
|
||||||
|
return redirect(url_for('auth.options'))
|
||||||
|
|
||||||
|
return render_template('register.html', error=error)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET /auth/logout
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@auth.route('/auth/logout')
|
||||||
|
@login_required
|
||||||
|
def logout():
|
||||||
|
logout_user()
|
||||||
|
return redirect(url_for('auth.login'))
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET/POST /auth/options — Clan management page
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@auth.route('/auth/options', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def options():
|
||||||
|
conn = get_db()
|
||||||
|
|
||||||
|
# Load clan based on current user's clan_id
|
||||||
|
clan = None
|
||||||
|
if current_user.clan_id:
|
||||||
|
clan = conn.execute(
|
||||||
|
'SELECT * FROM clans WHERE id = ?', (current_user.clan_id,)
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
# Fetch website admins (users belonging to this clan other than current user)
|
||||||
|
admins = []
|
||||||
|
if clan and clan['owner_id'] == current_user.id:
|
||||||
|
admins = conn.execute(
|
||||||
|
'SELECT id, username, created_at FROM users WHERE clan_id = ? AND id != ? ORDER BY created_at ASC',
|
||||||
|
(clan['id'], current_user.id)
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
members = []
|
||||||
|
if clan:
|
||||||
|
rows = conn.execute(
|
||||||
|
'''SELECT cm.id, cm.player_id, cm.player_name, cm.joined_at, cm.features,
|
||||||
|
ts.updated_at
|
||||||
|
FROM clan_members cm
|
||||||
|
LEFT JOIN town_state ts ON ts.player_id = cm.player_id
|
||||||
|
WHERE cm.clan_id = ?
|
||||||
|
GROUP BY cm.player_id
|
||||||
|
ORDER BY cm.joined_at DESC''',
|
||||||
|
(clan['id'],)
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
now = datetime.utcnow()
|
||||||
|
for row in rows:
|
||||||
|
is_online = False
|
||||||
|
if row['updated_at']:
|
||||||
|
try:
|
||||||
|
last_seen = datetime.fromisoformat(row['updated_at'])
|
||||||
|
if (now - last_seen).total_seconds() <= 150:
|
||||||
|
is_online = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
members.append({
|
||||||
|
'id': row['id'],
|
||||||
|
'player_id': row['player_id'],
|
||||||
|
'player_name': row['player_name'] or 'Άγνωστος',
|
||||||
|
'joined_at': row['joined_at'][:10] if row['joined_at'] else '',
|
||||||
|
'is_online': is_online,
|
||||||
|
'feat_farm': 'farm' in (row['features'] or 'farm,admin'),
|
||||||
|
'feat_admin': 'admin' in (row['features'] or 'farm,admin'),
|
||||||
|
})
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
return render_template('options.html', clan=clan, members=members, admins=admins)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# POST /auth/clan/create
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@auth.route('/auth/clan/create', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def create_clan():
|
||||||
|
clan_name = request.form.get('clan_name', '').strip()
|
||||||
|
if not clan_name:
|
||||||
|
return redirect(url_for('auth.options'))
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
existing = conn.execute(
|
||||||
|
'SELECT id FROM clans WHERE owner_id = ?', (current_user.id,)
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if not existing:
|
||||||
|
key = generate_clan_key()
|
||||||
|
cursor = conn.execute(
|
||||||
|
'INSERT INTO clans (owner_id, name, clan_key) VALUES (?, ?, ?)',
|
||||||
|
(current_user.id, clan_name, key)
|
||||||
|
)
|
||||||
|
clan_id = cursor.lastrowid
|
||||||
|
conn.execute('UPDATE users SET clan_id = ? WHERE id = ?', (clan_id, current_user.id))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# Update the current_user object dynamically to reflect the new clan_id without re-login
|
||||||
|
current_user.clan_id = clan_id
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
return redirect(url_for('auth.options'))
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# POST /auth/clan/regenerate-key
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@auth.route('/auth/clan/regenerate-key', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def regenerate_key():
|
||||||
|
new_key = generate_clan_key()
|
||||||
|
conn = get_db()
|
||||||
|
conn.execute(
|
||||||
|
'UPDATE clans SET clan_key = ? WHERE owner_id = ?',
|
||||||
|
(new_key, current_user.id)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return redirect(url_for('auth.options'))
|
||||||
|
|
||||||
|
|
||||||
|
@auth.route('/auth/clan/remove-member/<player_id>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def remove_member(player_id):
|
||||||
|
conn = get_db()
|
||||||
|
clan = conn.execute(
|
||||||
|
'SELECT id FROM clans WHERE owner_id = ?', (current_user.id,)
|
||||||
|
).fetchone()
|
||||||
|
if clan:
|
||||||
|
conn.execute(
|
||||||
|
'DELETE FROM clan_members WHERE clan_id = ? AND player_id = ?',
|
||||||
|
(clan['id'], player_id)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return redirect(url_for('auth.options'))
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# POST /auth/clan/update-features/<player_id>
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@auth.route('/auth/clan/update-features/<player_id>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def update_member_features(player_id):
|
||||||
|
farm = 'farm' if request.form.get('farm') else None
|
||||||
|
admin = 'admin' if request.form.get('admin') else None
|
||||||
|
features = ','.join(f for f in [farm, admin] if f) or ''
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
clan = conn.execute(
|
||||||
|
'SELECT id FROM clans WHERE owner_id = ?', (current_user.id,)
|
||||||
|
).fetchone()
|
||||||
|
if clan:
|
||||||
|
conn.execute(
|
||||||
|
'UPDATE clan_members SET features = ? WHERE clan_id = ? AND player_id = ?',
|
||||||
|
(features, clan['id'], player_id)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return redirect(url_for('auth.options'))
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# POST /auth/clan/add-admin
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@auth.route('/auth/clan/add-admin', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def add_admin():
|
||||||
|
username = request.form.get('admin_username', '').strip()
|
||||||
|
if not username:
|
||||||
|
return redirect(url_for('auth.options'))
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
clan = conn.execute(
|
||||||
|
'SELECT id FROM clans WHERE owner_id = ?', (current_user.id,)
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if clan:
|
||||||
|
# Check if user exists
|
||||||
|
user = conn.execute('SELECT id, clan_id FROM users WHERE username = ?', (username,)).fetchone()
|
||||||
|
if user:
|
||||||
|
# If user already belongs to a clan, we could show an error, but let's just overwrite for now
|
||||||
|
# or maybe only if clan_id is NULL
|
||||||
|
conn.execute('UPDATE users SET clan_id = ? WHERE id = ?', (clan['id'], user['id']))
|
||||||
|
conn.commit()
|
||||||
|
flash(f"Ο χρήστης {username} προστέθηκε ως διαχειριστής.", "success")
|
||||||
|
else:
|
||||||
|
flash(f"Ο χρήστης {username} δεν βρέθηκε.", "error")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
return redirect(url_for('auth.options'))
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# POST /auth/clan/remove-admin/<admin_id>
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@auth.route('/auth/clan/remove-admin/<int:admin_id>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def remove_admin(admin_id):
|
||||||
|
conn = get_db()
|
||||||
|
clan = conn.execute(
|
||||||
|
'SELECT id FROM clans WHERE owner_id = ?', (current_user.id,)
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if clan:
|
||||||
|
conn.execute('UPDATE users SET clan_id = NULL WHERE id = ? AND clan_id = ?', (admin_id, clan['id']))
|
||||||
|
conn.commit()
|
||||||
|
flash("Ο διαχειριστής αφαιρέθηκε.", "success")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
return redirect(url_for('auth.options'))
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# POST /auth/clan/leave
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@auth.route('/auth/clan/leave', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def leave_clan():
|
||||||
|
conn = get_db()
|
||||||
|
if current_user.clan_id:
|
||||||
|
clan = conn.execute('SELECT owner_id FROM clans WHERE id = ?', (current_user.clan_id,)).fetchone()
|
||||||
|
if clan and clan['owner_id'] != current_user.id:
|
||||||
|
conn.execute('UPDATE users SET clan_id = NULL WHERE id = ?', (current_user.id,))
|
||||||
|
conn.commit()
|
||||||
|
current_user.clan_id = None
|
||||||
|
flash("Έχετε αποχωρήσει από την ομάδα.", "success")
|
||||||
|
conn.close()
|
||||||
|
return redirect(url_for('auth.options'))
|
||||||
|
|
||||||
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
from flask import Blueprint, render_template, request, jsonify
|
from flask import Blueprint, render_template, request, jsonify
|
||||||
|
from flask_login import login_required, current_user
|
||||||
from db import get_db
|
from db import get_db
|
||||||
import json
|
import json
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
@@ -11,17 +12,28 @@ dashboard = Blueprint('dashboard', __name__)
|
|||||||
# Serve the dashboard HTML
|
# Serve the dashboard HTML
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@dashboard.route('/')
|
@dashboard.route('/')
|
||||||
|
@login_required
|
||||||
def index():
|
def index():
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
rows = conn.execute('''
|
|
||||||
SELECT player, player_id, MAX(updated_at) as last_seen, MAX(world_id) as world_id
|
|
||||||
FROM town_state
|
|
||||||
WHERE player IS NOT NULL
|
|
||||||
GROUP BY player, player_id
|
|
||||||
ORDER BY player ASC
|
|
||||||
''').fetchall()
|
|
||||||
|
|
||||||
# Pre-fetch all active captchas
|
# Get the clan the logged-in user belongs to
|
||||||
|
clan_id = current_user.clan_id
|
||||||
|
|
||||||
|
if not clan_id:
|
||||||
|
# User has no clan yet — send them to options to create/join one
|
||||||
|
conn.close()
|
||||||
|
return render_template('index.html', players=[], no_clan=True)
|
||||||
|
|
||||||
|
# Only fetch players that are members of this clan
|
||||||
|
rows = conn.execute('''
|
||||||
|
SELECT ts.player, ts.player_id, ts.world_id, MAX(ts.updated_at) as last_seen
|
||||||
|
FROM town_state ts
|
||||||
|
INNER JOIN clan_members cm ON cm.player_id = ts.player_id AND cm.clan_id = ?
|
||||||
|
WHERE ts.player IS NOT NULL
|
||||||
|
GROUP BY ts.player, ts.player_id, ts.world_id
|
||||||
|
ORDER BY ts.player ASC, ts.world_id ASC
|
||||||
|
''', (clan_id,)).fetchall()
|
||||||
|
|
||||||
captcha_rows = conn.execute("SELECT key, value FROM kv_store WHERE key LIKE 'captcha_active_%'").fetchall()
|
captcha_rows = conn.execute("SELECT key, value FROM kv_store WHERE key LIKE 'captcha_active_%'").fetchall()
|
||||||
active_captchas = {r['key'].replace('captcha_active_', ''): True for r in captcha_rows if r['value'] == '1'}
|
active_captchas = {r['key'].replace('captcha_active_', ''): True for r in captcha_rows if r['value'] == '1'}
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -39,26 +51,30 @@ def index():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
players.append({
|
players.append({
|
||||||
'player': r['player'],
|
'player': r['player'],
|
||||||
'player_id': r['player_id'],
|
'player_id': r['player_id'],
|
||||||
'world_id': r['world_id'] or 'Unknown',
|
'world_id': r['world_id'] or 'Unknown',
|
||||||
'is_online': is_online,
|
'is_online': is_online,
|
||||||
'captcha_active': active_captchas.get(r['player_id'], False)
|
'captcha_active': active_captchas.get(r['player_id'], False)
|
||||||
})
|
})
|
||||||
|
|
||||||
return render_template('index.html', players=players)
|
return render_template('index.html', players=players, no_clan=False)
|
||||||
|
|
||||||
@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')
|
@dashboard.route('/player/<player_id>/<world_id>')
|
||||||
def player_dashboard(player_id):
|
@login_required
|
||||||
return render_template('dashboard.html', player_id=player_id)
|
def player_hub(player_id, world_id):
|
||||||
|
return render_template('hub.html', player_id=player_id, world_id=world_id)
|
||||||
|
|
||||||
@dashboard.route('/player/<player_id>/farm')
|
@dashboard.route('/player/<player_id>/<world_id>/admin')
|
||||||
def player_farm(player_id):
|
@login_required
|
||||||
return render_template('farm.html', player_id=player_id)
|
def player_dashboard(player_id, world_id):
|
||||||
|
return render_template('dashboard.html', player_id=player_id, world_id=world_id)
|
||||||
|
|
||||||
|
@dashboard.route('/player/<player_id>/<world_id>/farm')
|
||||||
|
@login_required
|
||||||
|
def player_farm(player_id, world_id):
|
||||||
|
return render_template('farm.html', player_id=player_id, world_id=world_id)
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -68,14 +84,17 @@ def player_farm(player_id):
|
|||||||
@dashboard.route('/dashboard/farm-settings', methods=['GET'])
|
@dashboard.route('/dashboard/farm-settings', methods=['GET'])
|
||||||
def get_farm_settings():
|
def get_farm_settings():
|
||||||
player_id = request.args.get('player_id')
|
player_id = request.args.get('player_id')
|
||||||
|
world_id = request.args.get('world_id', '')
|
||||||
|
player_key = f"{player_id}_{world_id}" if world_id else player_id
|
||||||
|
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
'SELECT enabled, bandit_camp_enabled, loot_option FROM farm_settings WHERE player_id = ?', (player_id,)
|
'SELECT enabled, loot_option FROM farm_settings WHERE player_id = ?', (player_key,)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
if row:
|
if row:
|
||||||
return jsonify({'enabled': bool(row['enabled']), 'bandit_camp_enabled': bool(row['bandit_camp_enabled']), 'loot_option': row['loot_option']})
|
return jsonify({'enabled': bool(row['enabled']), 'loot_option': row['loot_option']})
|
||||||
return jsonify({'enabled': False, 'bandit_camp_enabled': False, 'loot_option': 1})
|
return jsonify({'enabled': False, 'loot_option': 1})
|
||||||
|
|
||||||
@dashboard.route('/dashboard/farm-settings', methods=['POST'])
|
@dashboard.route('/dashboard/farm-settings', methods=['POST'])
|
||||||
def set_farm_settings():
|
def set_farm_settings():
|
||||||
@@ -83,19 +102,20 @@ def set_farm_settings():
|
|||||||
if not data or 'player_id' not in data:
|
if not data or 'player_id' not in data:
|
||||||
return jsonify({'error': 'missing player_id'}), 400
|
return jsonify({'error': 'missing player_id'}), 400
|
||||||
player_id = data['player_id']
|
player_id = data['player_id']
|
||||||
|
world_id = data.get('world_id', '')
|
||||||
|
player_key = f"{player_id}_{world_id}" if world_id else player_id
|
||||||
|
|
||||||
enabled = 1 if data.get('enabled') else 0
|
enabled = 1 if data.get('enabled') else 0
|
||||||
bandit_camp_enabled = 1 if data.get('bandit_camp_enabled') else 0
|
|
||||||
loot_option = int(data.get('loot_option', 1))
|
loot_option = int(data.get('loot_option', 1))
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
conn.execute('''
|
conn.execute('''
|
||||||
INSERT INTO farm_settings (player_id, enabled, bandit_camp_enabled, loot_option, updated_at)
|
INSERT INTO farm_settings (player_id, enabled, loot_option, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
ON CONFLICT(player_id) DO UPDATE SET
|
ON CONFLICT(player_id) DO UPDATE SET
|
||||||
enabled = excluded.enabled,
|
enabled = excluded.enabled,
|
||||||
bandit_camp_enabled = excluded.bandit_camp_enabled,
|
|
||||||
loot_option = excluded.loot_option,
|
loot_option = excluded.loot_option,
|
||||||
updated_at = excluded.updated_at
|
updated_at = excluded.updated_at
|
||||||
''', (player_id, enabled, bandit_camp_enabled, loot_option, datetime.utcnow().isoformat()))
|
''', (player_key, enabled, loot_option, datetime.utcnow().isoformat()))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({'ok': True})
|
return jsonify({'ok': True})
|
||||||
@@ -108,10 +128,24 @@ def set_farm_settings():
|
|||||||
@dashboard.route('/dashboard/farm-data', methods=['GET'])
|
@dashboard.route('/dashboard/farm-data', methods=['GET'])
|
||||||
def get_farm_data():
|
def get_farm_data():
|
||||||
player_id = request.args.get('player_id')
|
player_id = request.args.get('player_id')
|
||||||
|
world_id = request.args.get('world_id')
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
rows = conn.execute(
|
if world_id:
|
||||||
'SELECT town_id, town_name, data FROM town_state WHERE player_id = ?', (player_id,)
|
rows = conn.execute(
|
||||||
).fetchall()
|
'SELECT town_id, town_name, data FROM town_state WHERE player_id = ? AND world_id = ?',
|
||||||
|
(player_id, world_id)
|
||||||
|
).fetchall()
|
||||||
|
else:
|
||||||
|
rows = conn.execute(
|
||||||
|
'SELECT town_id, town_name, data FROM town_state WHERE player_id = ?', (player_id,)
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
# Also fetch when the bot last farmed (per world)
|
||||||
|
lf_key = f'last_farmed_{player_id}_{world_id}' if world_id else f'last_farmed_{player_id}'
|
||||||
|
lf_row = conn.execute(
|
||||||
|
"SELECT value FROM kv_store WHERE key = ?", (lf_key,)
|
||||||
|
).fetchone()
|
||||||
|
last_farmed_at = lf_row['value'] if lf_row else None
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
now_ts = int(datetime.utcnow().timestamp())
|
now_ts = int(datetime.utcnow().timestamp())
|
||||||
@@ -128,7 +162,25 @@ def get_farm_data():
|
|||||||
'ready_farms': len(ready),
|
'ready_farms': len(ready),
|
||||||
'next_ready_at': min((f['lootable_at'] for f in farm_data if f.get('lootable_at', 0) > now_ts and f.get('relation_status', 0) == 1), default=None)
|
'next_ready_at': min((f['lootable_at'] for f in farm_data if f.get('lootable_at', 0) > now_ts and f.get('relation_status', 0) == 1), default=None)
|
||||||
})
|
})
|
||||||
return jsonify(farms_summary)
|
return jsonify({'towns': farms_summary, 'last_farmed_at': last_farmed_at})
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET /dashboard/market-data
|
||||||
|
# Returns the latest market scan data for a player.
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@dashboard.route('/dashboard/market-data', methods=['GET'])
|
||||||
|
def get_market_data():
|
||||||
|
player_id = request.args.get('player_id')
|
||||||
|
conn = get_db()
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT value, updated_at FROM kv_store WHERE key = ?", (f'market_data_{player_id}', )
|
||||||
|
).fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if row:
|
||||||
|
return jsonify({'data': json.loads(row['value']), 'updated_at': row['updated_at']})
|
||||||
|
return jsonify({'data': None, 'updated_at': None})
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -138,14 +190,26 @@ def get_farm_data():
|
|||||||
@dashboard.route('/dashboard/towns', methods=['GET'])
|
@dashboard.route('/dashboard/towns', methods=['GET'])
|
||||||
def get_towns():
|
def get_towns():
|
||||||
player_id = request.args.get('player_id')
|
player_id = request.args.get('player_id')
|
||||||
|
world_id = request.args.get('world_id')
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
rows = conn.execute('''
|
|
||||||
SELECT town_id, town_name, player, player_id, alliance_id,
|
query = '''
|
||||||
world_id, x, y, sea, data, updated_at
|
SELECT ts.town_id, ts.town_name, ts.player, ts.player_id, ts.alliance_id,
|
||||||
FROM town_state
|
ts.world_id, ts.x, ts.y, ts.sea, ts.data, ts.updated_at,
|
||||||
WHERE player_id = ?
|
tb.blueprint_name, tb.is_active as blueprint_active
|
||||||
ORDER BY town_name ASC
|
FROM town_state ts
|
||||||
''', (player_id, )).fetchall()
|
LEFT JOIN town_blueprints tb ON ts.town_id = tb.town_id AND tb.is_active = 1
|
||||||
|
WHERE ts.player_id = ?
|
||||||
|
'''
|
||||||
|
params = [player_id]
|
||||||
|
|
||||||
|
if world_id:
|
||||||
|
query += ' AND ts.world_id = ?'
|
||||||
|
params.append(world_id)
|
||||||
|
|
||||||
|
query += ' ORDER BY ts.town_name ASC'
|
||||||
|
|
||||||
|
rows = conn.execute(query, params).fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
towns = []
|
towns = []
|
||||||
@@ -182,11 +246,51 @@ def get_towns():
|
|||||||
'bonuses': d.get('bonuses', {}),
|
'bonuses': d.get('bonuses', {}),
|
||||||
'wonder_points': d.get('wonder_points', 0),
|
'wonder_points': d.get('wonder_points', 0),
|
||||||
'total_points': d.get('total_points', 0),
|
'total_points': d.get('total_points', 0),
|
||||||
'alliance_name': d.get('alliance_name', None)
|
'alliance_name': d.get('alliance_name', None),
|
||||||
|
'blueprint_name': row['blueprint_name'],
|
||||||
|
'blueprint_active': bool(row['blueprint_active'])
|
||||||
})
|
})
|
||||||
return jsonify(towns)
|
return jsonify(towns)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# POST /dashboard/blueprints
|
||||||
|
# Toggle a blueprint for a specific town
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@dashboard.route('/dashboard/blueprints', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def toggle_blueprint():
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
town_id = data.get('town_id')
|
||||||
|
blueprint_name = data.get('blueprint_name', 'Standard Growth')
|
||||||
|
|
||||||
|
if not town_id:
|
||||||
|
return jsonify({'error': 'missing town_id'}), 400
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
|
||||||
|
# Check if currently active
|
||||||
|
row = conn.execute('SELECT is_active FROM town_blueprints WHERE town_id = ?', (town_id,)).fetchone()
|
||||||
|
|
||||||
|
new_state = 1
|
||||||
|
if row and row['is_active'] == 1:
|
||||||
|
new_state = 0 # Toggle off
|
||||||
|
|
||||||
|
conn.execute('''
|
||||||
|
INSERT INTO town_blueprints (town_id, blueprint_name, is_active, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
ON CONFLICT(town_id) DO UPDATE SET
|
||||||
|
blueprint_name = excluded.blueprint_name,
|
||||||
|
is_active = excluded.is_active,
|
||||||
|
updated_at = excluded.updated_at
|
||||||
|
''', (town_id, blueprint_name, new_state, datetime.utcnow().isoformat()))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify({'ok': True, 'is_active': bool(new_state), 'blueprint_name': blueprint_name})
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# GET /dashboard/client-status
|
# GET /dashboard/client-status
|
||||||
# Returns whether the Tampermonkey client is considered online.
|
# Returns whether the Tampermonkey client is considered online.
|
||||||
@@ -231,6 +335,54 @@ def captcha_status():
|
|||||||
return jsonify({'captcha_active': active})
|
return jsonify({'captcha_active': active})
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET /dashboard/commands/queue
|
||||||
|
# Returns pending+executing BUILD commands for a specific town,
|
||||||
|
# ordered by their manual position (for the per-town build queue UI).
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@dashboard.route('/dashboard/commands/queue', methods=['GET'])
|
||||||
|
def get_town_build_queue():
|
||||||
|
player_id = request.args.get('player_id')
|
||||||
|
town_id = request.args.get('town_id')
|
||||||
|
conn = get_db()
|
||||||
|
rows = conn.execute('''
|
||||||
|
SELECT id, town_id, town_name, type, payload, status, result_msg, position, created_at, updated_at
|
||||||
|
FROM commands
|
||||||
|
WHERE player_id = ? AND town_id = ? AND type = 'build'
|
||||||
|
AND status IN ('pending', 'executing')
|
||||||
|
ORDER BY position ASC, id ASC
|
||||||
|
''', (player_id, town_id)).fetchall()
|
||||||
|
conn.close()
|
||||||
|
return jsonify([dict(r) for r in rows])
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# POST /dashboard/commands/reorder
|
||||||
|
# Accepts { player_id, town_id, order: [id1, id2, ...] }
|
||||||
|
# and updates position for each command in the list.
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@dashboard.route('/dashboard/commands/reorder', methods=['POST'])
|
||||||
|
def reorder_commands():
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
player_id = data.get('player_id')
|
||||||
|
town_id = data.get('town_id')
|
||||||
|
order = data.get('order', []) # list of command ids in desired order
|
||||||
|
|
||||||
|
if not player_id or not town_id or not order:
|
||||||
|
return jsonify({'error': 'missing player_id, town_id or order'}), 400
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
for idx, cmd_id in enumerate(order):
|
||||||
|
conn.execute('''
|
||||||
|
UPDATE commands
|
||||||
|
SET position = ?
|
||||||
|
WHERE id = ? AND player_id = ? AND town_id = ?
|
||||||
|
''', (idx + 1, cmd_id, player_id, str(town_id)))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'ok': True})
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# GET /dashboard/commands
|
# GET /dashboard/commands
|
||||||
# Returns command history (last 50) for the log panel.
|
# Returns command history (last 50) for the log panel.
|
||||||
@@ -239,13 +391,24 @@ def captcha_status():
|
|||||||
def get_commands():
|
def get_commands():
|
||||||
player_id = request.args.get('player_id')
|
player_id = request.args.get('player_id')
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
rows = conn.execute('''
|
world_id = request.args.get('world_id')
|
||||||
SELECT id, town_id, town_name, type, payload, status, result_msg, created_at, updated_at
|
conn = get_db()
|
||||||
FROM commands
|
|
||||||
WHERE player_id = ?
|
query = '''
|
||||||
ORDER BY id DESC
|
SELECT c.id, c.town_id, c.town_name, c.type, c.payload, c.status, c.result_msg, c.created_at, c.updated_at
|
||||||
LIMIT 50
|
FROM commands c
|
||||||
''', (player_id, )).fetchall()
|
JOIN town_state ts ON c.town_id = ts.town_id
|
||||||
|
WHERE c.player_id = ?
|
||||||
|
'''
|
||||||
|
params = [player_id]
|
||||||
|
|
||||||
|
if world_id:
|
||||||
|
query += ' AND ts.world_id = ?'
|
||||||
|
params.append(world_id)
|
||||||
|
|
||||||
|
query += ' ORDER BY c.id DESC LIMIT 50'
|
||||||
|
|
||||||
|
rows = conn.execute(query, params).fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
return jsonify([dict(r) for r in rows])
|
return jsonify([dict(r) for r in rows])
|
||||||
@@ -268,8 +431,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', 'farm_loot', 'farm_upgrade'):
|
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, or farm_upgrade'}), 400
|
return jsonify({'error': 'type must be build, recruit, market_offer, farm_loot, farm_upgrade, or research'}), 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()
|
||||||
@@ -288,14 +451,28 @@ def create_command():
|
|||||||
return jsonify({'error': 'client_offline', 'message': 'Το script είναι offline — δεν μπορείτε να στείλετε εντολές.'}), 503
|
return jsonify({'error': 'client_offline', 'message': 'Το script είναι offline — δεν μπορείτε να στείλετε εντολές.'}), 503
|
||||||
|
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
|
|
||||||
|
# Assign position = one more than the current max for this town's pending build queue
|
||||||
|
if cmd_type == 'build':
|
||||||
|
pos_row = c.execute(
|
||||||
|
"SELECT MAX(position) as max_pos FROM commands"
|
||||||
|
" WHERE player_id = ? AND town_id = ? AND type = 'build'"
|
||||||
|
" AND status IN ('pending', 'executing')",
|
||||||
|
(str(data['player_id']), str(data['town_id']))
|
||||||
|
).fetchone()
|
||||||
|
position = (pos_row['max_pos'] or 0) + 1
|
||||||
|
else:
|
||||||
|
position = None
|
||||||
|
|
||||||
c.execute('''
|
c.execute('''
|
||||||
INSERT INTO commands (town_id, town_name, type, payload, status, created_at, updated_at, player_id)
|
INSERT INTO commands (town_id, town_name, type, payload, status, position, created_at, updated_at, player_id)
|
||||||
VALUES (?, ?, ?, ?, 'pending', ?, ?, ?)
|
VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?)
|
||||||
''', (
|
''', (
|
||||||
str(data['town_id']),
|
str(data['town_id']),
|
||||||
data.get('town_name', ''),
|
data.get('town_name', ''),
|
||||||
cmd_type,
|
cmd_type,
|
||||||
json.dumps(data['payload']),
|
json.dumps(data['payload']),
|
||||||
|
position,
|
||||||
datetime.utcnow().isoformat(),
|
datetime.utcnow().isoformat(),
|
||||||
datetime.utcnow().isoformat(),
|
datetime.utcnow().isoformat(),
|
||||||
str(data['player_id'])
|
str(data['player_id'])
|
||||||
@@ -341,3 +518,134 @@ def fail_stale_commands():
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({'ok': True, 'failed': affected})
|
return jsonify({'ok': True, 'failed': affected})
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET /dashboard/bot-settings — fetch bootcamp + rural trade config
|
||||||
|
# POST /dashboard/bot-settings — save config
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@dashboard.route('/dashboard/bot-settings', methods=['GET', 'POST'])
|
||||||
|
def bot_settings():
|
||||||
|
player_id = request.args.get('player_id') or (request.json or {}).get('player_id')
|
||||||
|
world_id = request.args.get('world_id') or (request.json or {}).get('world_id', '')
|
||||||
|
if not player_id:
|
||||||
|
return jsonify({'error': 'missing player_id'}), 400
|
||||||
|
|
||||||
|
player_key = f"{player_id}_{world_id}" if world_id else player_id
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
c = conn.cursor()
|
||||||
|
|
||||||
|
if request.method == 'GET':
|
||||||
|
row = c.execute(
|
||||||
|
'SELECT * FROM bot_settings WHERE player_id = ?', (player_key,)
|
||||||
|
).fetchone()
|
||||||
|
conn.close()
|
||||||
|
if row:
|
||||||
|
return jsonify(dict(row))
|
||||||
|
return jsonify({
|
||||||
|
'player_id': player_id,
|
||||||
|
'bootcamp_enabled': 0,
|
||||||
|
'bootcamp_use_def': 0,
|
||||||
|
'rural_trade_enabled': 0,
|
||||||
|
'rural_trade_ratio': 3,
|
||||||
|
})
|
||||||
|
|
||||||
|
# POST — upsert
|
||||||
|
data = request.json or {}
|
||||||
|
c.execute('''
|
||||||
|
INSERT INTO bot_settings (player_id, bootcamp_enabled, bootcamp_use_def,
|
||||||
|
rural_trade_enabled, rural_trade_ratio, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(player_id) DO UPDATE SET
|
||||||
|
bootcamp_enabled = excluded.bootcamp_enabled,
|
||||||
|
bootcamp_use_def = excluded.bootcamp_use_def,
|
||||||
|
rural_trade_enabled = excluded.rural_trade_enabled,
|
||||||
|
rural_trade_ratio = excluded.rural_trade_ratio,
|
||||||
|
updated_at = excluded.updated_at
|
||||||
|
''', (
|
||||||
|
player_key,
|
||||||
|
int(bool(data.get('bootcamp_enabled', 0))),
|
||||||
|
int(bool(data.get('bootcamp_use_def', 0))),
|
||||||
|
int(bool(data.get('rural_trade_enabled', 0))),
|
||||||
|
int(data.get('rural_trade_ratio', 3)),
|
||||||
|
datetime.utcnow().isoformat()
|
||||||
|
))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'ok': True})
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET /dashboard/bot-logs?player_id=&feature= — last 50 log lines
|
||||||
|
# POST /dashboard/bot-logs — append + prune
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@dashboard.route('/dashboard/bot-logs', methods=['GET', 'POST'])
|
||||||
|
def bot_logs():
|
||||||
|
player_id = request.args.get('player_id') or (request.json or {}).get('player_id')
|
||||||
|
world_id = request.args.get('world_id') or (request.json or {}).get('world_id', '')
|
||||||
|
if not player_id:
|
||||||
|
return jsonify({'error': 'missing player_id'}), 400
|
||||||
|
|
||||||
|
player_key = f"{player_id}_{world_id}" if world_id else player_id
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
c = conn.cursor()
|
||||||
|
|
||||||
|
if request.method == 'GET':
|
||||||
|
feature = request.args.get('feature', '')
|
||||||
|
query = 'SELECT * FROM bot_logs WHERE player_id = ?'
|
||||||
|
params = [player_key]
|
||||||
|
if feature:
|
||||||
|
query += ' AND feature = ?'
|
||||||
|
params.append(feature)
|
||||||
|
query += ' ORDER BY id DESC LIMIT 50'
|
||||||
|
rows = c.execute(query, params).fetchall()
|
||||||
|
conn.close()
|
||||||
|
return jsonify([dict(r) for r in rows])
|
||||||
|
|
||||||
|
# POST — append entry and prune to last 50
|
||||||
|
data = request.json or {}
|
||||||
|
feature = data.get('feature', 'bootcamp')
|
||||||
|
message = data.get('message', '')
|
||||||
|
c.execute(
|
||||||
|
'INSERT INTO bot_logs (player_id, feature, message) VALUES (?, ?, ?)',
|
||||||
|
(player_key, feature, message)
|
||||||
|
)
|
||||||
|
# Prune: keep only the latest 50 per player/feature
|
||||||
|
c.execute('''
|
||||||
|
DELETE FROM bot_logs
|
||||||
|
WHERE player_id = ? AND feature = ?
|
||||||
|
AND id NOT IN (
|
||||||
|
SELECT id FROM bot_logs
|
||||||
|
WHERE player_id = ? AND feature = ?
|
||||||
|
ORDER BY id DESC LIMIT 50
|
||||||
|
)
|
||||||
|
''', (player_key, feature, player_key, feature))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'ok': True})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# POST /dashboard/bootcamp-attack-now
|
||||||
|
# Sets a one-shot flag consumed by the TM bot on the next poll.
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@dashboard.route('/dashboard/bootcamp-attack-now', methods=['POST'])
|
||||||
|
def bootcamp_attack_now():
|
||||||
|
player_id = (request.json or {}).get('player_id')
|
||||||
|
world_id = (request.json or {}).get('world_id', '')
|
||||||
|
if not player_id:
|
||||||
|
return jsonify({'error': 'missing player_id'}), 400
|
||||||
|
|
||||||
|
player_key = f"{player_id}_{world_id}" if world_id else player_id
|
||||||
|
key = f'bootcamp_attack_now_{player_key}'
|
||||||
|
conn = get_db()
|
||||||
|
conn.execute('''
|
||||||
|
INSERT INTO kv_store (key, value, updated_at) VALUES (?, '1', ?)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET value='1', updated_at=excluded.updated_at
|
||||||
|
''', (key, datetime.utcnow().isoformat()))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'ok': True})
|
||||||
|
|||||||
68
split_script.py
Normal file
68
split_script.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
input_file = '/media/haunter/e11cc3d4-c894-42cd-8c43-fe2cb25293fd/Vcode_raidbot/grepo-remote/GrepolisRemoteControl.user.js'
|
||||||
|
out_dir = '/media/haunter/e11cc3d4-c894-42cd-8c43-fe2cb25293fd/Vcode_raidbot/grepo-remote/bot_modules'
|
||||||
|
|
||||||
|
if not os.path.exists(out_dir):
|
||||||
|
os.makedirs(out_dir)
|
||||||
|
|
||||||
|
with open(input_file, 'r', encoding='utf-8') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
# Find the start of the IIFE
|
||||||
|
start_idx = 0
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line.strip().startswith('(function'):
|
||||||
|
start_idx = i + 1
|
||||||
|
break
|
||||||
|
|
||||||
|
# Extract the body of the IIFE (excluding the last '})();')
|
||||||
|
end_idx = len(lines)
|
||||||
|
for i in range(len(lines) - 1, -1, -1):
|
||||||
|
if line.strip().startswith('})();'):
|
||||||
|
end_idx = i
|
||||||
|
break
|
||||||
|
|
||||||
|
# Remove use strict if present
|
||||||
|
body_lines = [line for line in lines[start_idx:end_idx] if 'use strict' not in line]
|
||||||
|
|
||||||
|
# We will group them manually into files based on logical sections
|
||||||
|
sections = []
|
||||||
|
current_section = []
|
||||||
|
for line in body_lines:
|
||||||
|
current_section.append(line)
|
||||||
|
|
||||||
|
content = "".join(body_lines)
|
||||||
|
|
||||||
|
# Split by known markers
|
||||||
|
blocks = {
|
||||||
|
'00_config.js': [],
|
||||||
|
'01_ui.js': [],
|
||||||
|
'02_state.js': [],
|
||||||
|
'03_captcha.js': [],
|
||||||
|
'04_execute.js': [],
|
||||||
|
'05_main.js': []
|
||||||
|
}
|
||||||
|
|
||||||
|
current_file = '00_config.js'
|
||||||
|
|
||||||
|
for line in body_lines:
|
||||||
|
if '// Toolbar indicator button' in line:
|
||||||
|
current_file = '01_ui.js'
|
||||||
|
elif '// Push town state to relay' in line:
|
||||||
|
current_file = '02_state.js'
|
||||||
|
elif '// Captcha detection' in line:
|
||||||
|
current_file = '03_captcha.js'
|
||||||
|
elif '// Execute: Farm Upgrade' in line:
|
||||||
|
current_file = '04_execute.js'
|
||||||
|
elif '// Poll for and execute pending commands' in line:
|
||||||
|
current_file = '05_main.js'
|
||||||
|
|
||||||
|
blocks[current_file].append(line)
|
||||||
|
|
||||||
|
for filename, lines in blocks.items():
|
||||||
|
with open(os.path.join(out_dir, filename), 'w', encoding='utf-8') as f:
|
||||||
|
f.writelines(lines)
|
||||||
|
|
||||||
|
print("Split completed.")
|
||||||
@@ -163,6 +163,10 @@ select:focus, input:focus { outline: none; border-color: #c8a44a; }
|
|||||||
.btn-danger { background: #8b2222; color: #fff; }
|
.btn-danger { background: #8b2222; color: #fff; }
|
||||||
.btn-sm { padding: 5px 10px; font-size: 0.75rem; }
|
.btn-sm { padding: 5px 10px; font-size: 0.75rem; }
|
||||||
|
|
||||||
|
.seg-btn:hover { background: rgba(200, 164, 74, 0.1) !important; color: #c8a44a !important; }
|
||||||
|
.seg-btn.active { background: #c8a44a !important; color: #1a1a2e !important; font-weight: bold; box-shadow: 0 2px 5px rgba(0,0,0,0.3); }
|
||||||
|
|
||||||
|
|
||||||
#no-town-selected {
|
#no-town-selected {
|
||||||
color: #666;
|
color: #666;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
@@ -314,9 +318,9 @@ tr:hover td { background: #1e1e40; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
Building Picker Modal
|
Building, Academy, Unit, Market & Blueprint Modals
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
#building-modal-overlay {
|
#building-modal-overlay, #academy-modal-overlay, #unit-modal-overlay, #market-modal-overlay, #blueprints-modal-overlay {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -325,9 +329,9 @@ tr:hover td { background: #1e1e40; }
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
#building-modal-overlay.open { display: flex; }
|
#building-modal-overlay.open, #academy-modal-overlay.open, #unit-modal-overlay.open, #market-modal-overlay.open, #blueprints-modal-overlay.open { display: flex; }
|
||||||
|
|
||||||
#building-modal {
|
#building-modal, #academy-modal, #unit-modal, #market-modal, #blueprints-modal {
|
||||||
background: #16213e;
|
background: #16213e;
|
||||||
border: 2px solid #c8a44a;
|
border: 2px solid #c8a44a;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -342,7 +346,7 @@ tr:hover td { background: #1e1e40; }
|
|||||||
from { transform: scale(0.92); opacity: 0; }
|
from { transform: scale(0.92); opacity: 0; }
|
||||||
to { transform: scale(1); opacity: 1; }
|
to { transform: scale(1); opacity: 1; }
|
||||||
}
|
}
|
||||||
#building-modal-header {
|
#building-modal-header, #academy-modal-header, .modal-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -350,12 +354,12 @@ tr:hover td { background: #1e1e40; }
|
|||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
border-bottom: 1px solid #2a4a6a;
|
border-bottom: 1px solid #2a4a6a;
|
||||||
}
|
}
|
||||||
#building-modal-header h3 {
|
#building-modal-header h3, #academy-modal-header h3, .modal-header h3 {
|
||||||
color: #c8a44a;
|
color: #c8a44a;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
#building-modal-close {
|
#building-modal-close, #academy-modal-close, #unit-modal-close {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: #888;
|
color: #888;
|
||||||
@@ -364,7 +368,7 @@ tr:hover td { background: #1e1e40; }
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
}
|
}
|
||||||
#building-modal-close:hover { color: #fff; }
|
#building-modal-close:hover, #academy-modal-close:hover, #unit-modal-close:hover { color: #fff; }
|
||||||
|
|
||||||
#building-grid {
|
#building-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -436,3 +440,26 @@ tr:hover td { background: #1e1e40; }
|
|||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
#building-grid { grid-template-columns: repeat(2, 1fr); }
|
#building-grid { grid-template-columns: repeat(2, 1fr); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Academy Grid specific styling */
|
||||||
|
#academy-grid {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
.academy-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
.academy-col-header {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #c8a44a;
|
||||||
|
border-bottom: 1px solid #2a4a6a;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
window.fetchTowns = async function() {
|
window.fetchTowns = async function() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/dashboard/towns?player_id=' + window.PLAYER_ID);
|
const res = await fetch(`/dashboard/towns?player_id=${window.PLAYER_ID}&world_id=${window.WORLD_ID}`);
|
||||||
window.towns = await res.json();
|
window.towns = await res.json();
|
||||||
window.renderTowns();
|
window.renderTowns();
|
||||||
window.updateServerStatus(true);
|
window.updateServerStatus(true);
|
||||||
@@ -19,6 +19,10 @@ window.fetchTowns = async function() {
|
|||||||
window.renderBuildingDropdown();
|
window.renderBuildingDropdown();
|
||||||
window.renderUnitDropdown();
|
window.renderUnitDropdown();
|
||||||
window.renderTownDetails();
|
window.renderTownDetails();
|
||||||
|
// Refresh the build queue panel if in queue mode
|
||||||
|
if (window._logPanelMode === 'queue') {
|
||||||
|
window.fetchBuildQueue(window.selectedTownId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
window.updateServerStatus(false);
|
window.updateServerStatus(false);
|
||||||
@@ -48,10 +52,12 @@ window.fetchClientStatus = async function() {
|
|||||||
|
|
||||||
window.fetchLog = async function() {
|
window.fetchLog = async function() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/dashboard/commands?player_id=' + window.PLAYER_ID);
|
const res = await fetch(`/dashboard/commands?player_id=${window.PLAYER_ID}&world_id=${window.WORLD_ID}`);
|
||||||
const cmds = await res.json();
|
const cmds = await res.json();
|
||||||
window.cmds = cmds; // Save globally so viewer can see reserved resources
|
window.cmds = cmds; // Save globally so viewer can see reserved resources
|
||||||
window.renderLog(cmds);
|
if (window._logPanelMode === 'log') {
|
||||||
|
window.renderLog(cmds);
|
||||||
|
}
|
||||||
if (window.selectedTownId) window.renderTownDetails();
|
if (window.selectedTownId) window.renderTownDetails();
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
};
|
};
|
||||||
@@ -77,7 +83,7 @@ window.updateClientStatus = function(online) {
|
|||||||
el.className = 'conn-badge offline';
|
el.className = 'conn-badge offline';
|
||||||
}
|
}
|
||||||
// Enable/disable the Send button
|
// Enable/disable the Send button
|
||||||
const btn = document.querySelector('#command-form-wrap .btn-gold');
|
const btn = document.getElementById('btn-send');
|
||||||
if (btn) {
|
if (btn) {
|
||||||
btn.disabled = !online;
|
btn.disabled = !online;
|
||||||
btn.title = online ? '' : 'Script is offline — cannot send commands';
|
btn.title = online ? '' : 'Script is offline — cannot send commands';
|
||||||
@@ -91,8 +97,8 @@ window.sendCommand = async function() {
|
|||||||
const town = window.getSelectedTown();
|
const town = window.getSelectedTown();
|
||||||
if (!town) return alert('Select a town first.');
|
if (!town) return alert('Select a town first.');
|
||||||
|
|
||||||
const type = document.getElementById('cmd-type').value;
|
const type = window.currentCmdType;
|
||||||
if (!type) return alert('Παρακαλώ επιλέξτε Ενέργεια (Command Type) πρώτα.');
|
if (!type) return alert('Παρακαλώ επιλέξτε Ενέργεια (Κατασκευή/Στρατός/Παζάρι/Έρευνα) πρώτα.');
|
||||||
|
|
||||||
let payload = {};
|
let payload = {};
|
||||||
|
|
||||||
@@ -126,7 +132,7 @@ window.sendCommand = async function() {
|
|||||||
|
|
||||||
payload = { building_id };
|
payload = { building_id };
|
||||||
} else if (type === 'recruit') {
|
} else if (type === 'recruit') {
|
||||||
const unit_id = document.getElementById('unit-select').value;
|
const unit_id = window.selectedUnitId;
|
||||||
if (!unit_id) return alert('Παρακαλώ επιλέξτε Μονάδα προς εκπαίδευση.');
|
if (!unit_id) return alert('Παρακαλώ επιλέξτε Μονάδα προς εκπαίδευση.');
|
||||||
|
|
||||||
const amount = parseInt(document.getElementById('recruit-amount').value) || 1;
|
const amount = parseInt(document.getElementById('recruit-amount').value) || 1;
|
||||||
@@ -164,6 +170,42 @@ window.sendCommand = async function() {
|
|||||||
const visibility = document.getElementById('market-visibility').value;
|
const visibility = document.getElementById('market-visibility').value;
|
||||||
|
|
||||||
payload = { offer, offer_type, demand, demand_type, max_delivery_time, visibility };
|
payload = { offer, offer_type, demand, demand_type, max_delivery_time, visibility };
|
||||||
|
} else if (type === 'research') {
|
||||||
|
const research_id = window.selectedResearchId;
|
||||||
|
if (!research_id) return alert('Παρακαλώ επιλέξτε Έρευνα από την Ακαδημία.');
|
||||||
|
|
||||||
|
const academyLevel = town.buildings?.academy || 0;
|
||||||
|
const rData = window.RESEARCH_DATA[research_id];
|
||||||
|
|
||||||
|
if (rData && academyLevel < rData.academy_level) {
|
||||||
|
return alert(`❌ ΑΔΥΝΑΤΗ Η ΕΡΕΥΝΑ: Απαιτείται Ακαδημία Επίπεδο ${rData.academy_level} (Έχετε ${academyLevel})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = { research_id };
|
||||||
|
} else if (type === 'blueprints') {
|
||||||
|
const blueprint_name = window.selectedBlueprintName;
|
||||||
|
if (!blueprint_name) return alert('Παρακαλώ επιλέξτε Blueprint.');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/dashboard/blueprints', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
town_id: town.town_id,
|
||||||
|
blueprint_name: blueprint_name
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.ok) {
|
||||||
|
alert(data.is_active ? 'Blueprint ενεργοποιήθηκε!' : 'Blueprint απενεργοποιήθηκε!');
|
||||||
|
window.fetchTowns(); // refresh towns to update UI state
|
||||||
|
} else {
|
||||||
|
alert('Σφάλμα: ' + data.error);
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
alert('Failed to toggle blueprint: ' + e);
|
||||||
|
}
|
||||||
|
return; // Exit here as we don't send this to /dashboard/commands
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -180,7 +222,12 @@ window.sendCommand = async function() {
|
|||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.ok) {
|
if (data.ok) {
|
||||||
window.fetchLog();
|
// Refresh whichever panel is active
|
||||||
|
if (type === 'build' && window._logPanelMode === 'queue') {
|
||||||
|
window.fetchBuildQueue(town.town_id);
|
||||||
|
} else {
|
||||||
|
window.fetchLog();
|
||||||
|
}
|
||||||
} else if (data.error === 'client_offline') {
|
} else if (data.error === 'client_offline') {
|
||||||
alert(data.message || 'Το script είναι offline.');
|
alert(data.message || 'Το script είναι offline.');
|
||||||
} else {
|
} else {
|
||||||
@@ -248,3 +295,5 @@ window.requestLiveSync = async function() {
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,15 @@
|
|||||||
|
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
window.fetchTowns();
|
window.fetchTowns();
|
||||||
window.fetchLog();
|
window.fetchLog(); // pre-loads cmds globally even in queue mode
|
||||||
window.fetchClientStatus();
|
window.fetchClientStatus();
|
||||||
window.fetchCaptchaStatus();
|
window.fetchCaptchaStatus();
|
||||||
setInterval(window.fetchTowns, window.POLL_INTERVAL);
|
setInterval(window.fetchTowns, window.POLL_INTERVAL);
|
||||||
setInterval(window.fetchLog, window.POLL_INTERVAL);
|
// In log mode: fetchLog refreshes the panel. In queue mode: refreshLogPanel polls the queue.
|
||||||
|
setInterval(() => {
|
||||||
|
window.fetchLog(); // always keep cmds cache fresh for resource display
|
||||||
|
window.refreshLogPanel(); // refresh whichever panel is visible
|
||||||
|
}, window.POLL_INTERVAL);
|
||||||
setInterval(window.fetchClientStatus, window.POLL_INTERVAL);
|
setInterval(window.fetchClientStatus, window.POLL_INTERVAL);
|
||||||
setInterval(window.fetchCaptchaStatus, 5000); // check every 5s
|
setInterval(window.fetchCaptchaStatus, 5000); // check every 5s
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,16 +2,110 @@
|
|||||||
// Command Form Component
|
// Command Form Component
|
||||||
// ================================================================
|
// ================================================================
|
||||||
|
|
||||||
window.onCmdTypeChange = function() {
|
window.currentCmdType = null;
|
||||||
const type = document.getElementById('cmd-type').value;
|
|
||||||
document.getElementById('build-options').style.display = type === 'build' ? '' : 'none';
|
window.setCmdType = function(type, openModal = false) {
|
||||||
document.getElementById('recruit-options').style.display = type === 'recruit' ? '' : 'none';
|
window.currentCmdType = type;
|
||||||
document.getElementById('amount-group').style.display = type === 'recruit' ? '' : 'none';
|
|
||||||
document.getElementById('market-options').style.display = type === 'market_offer' ? '' : 'none';
|
// Update segmented control active state
|
||||||
|
document.querySelectorAll('.seg-btn').forEach(btn => btn.classList.remove('active'));
|
||||||
|
const activeBtn = document.getElementById('seg-' + (type === 'market_offer' ? 'market' : type));
|
||||||
|
if (activeBtn) activeBtn.classList.add('active');
|
||||||
|
|
||||||
|
// Show selection area
|
||||||
|
document.getElementById('selection-area').style.display = 'flex';
|
||||||
|
document.getElementById('recruit-amount-wrap').style.display = type === 'recruit' ? 'flex' : 'none';
|
||||||
|
|
||||||
|
// Update selection label with current choice if any, or default
|
||||||
|
window.updateSelectionDisplay();
|
||||||
|
|
||||||
|
if (openModal) {
|
||||||
|
window.reopenActiveModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.updateSelectionDisplay = function() {
|
||||||
|
const type = window.currentCmdType;
|
||||||
|
const labelEl = document.getElementById('selection-label');
|
||||||
|
|
||||||
|
if (type === 'build') {
|
||||||
|
if (window.selectedBuildingId) {
|
||||||
|
labelEl.innerHTML = `🏗️ ${window.BUILDING_NAMES_GR[window.selectedBuildingId] || window.selectedBuildingId}`;
|
||||||
|
} else {
|
||||||
|
labelEl.innerHTML = `-- Επιλέξτε Κατασκευή --`;
|
||||||
|
}
|
||||||
|
} else if (type === 'recruit') {
|
||||||
|
if (window.selectedUnitId) {
|
||||||
|
labelEl.innerHTML = `⚔️ ${window.UNIT_NAMES_GR[window.selectedUnitId] || window.selectedUnitId}`;
|
||||||
|
} else {
|
||||||
|
labelEl.innerHTML = `-- Επιλέξτε Μονάδα --`;
|
||||||
|
}
|
||||||
|
} else if (type === 'research') {
|
||||||
|
if (window.selectedResearchId) {
|
||||||
|
const rd = window.RESEARCH_DATA[window.selectedResearchId];
|
||||||
|
labelEl.innerHTML = `🦉 ${rd ? rd.name : window.selectedResearchId}`;
|
||||||
|
} else {
|
||||||
|
labelEl.innerHTML = `-- Επιλέξτε Έρευνα --`;
|
||||||
|
}
|
||||||
|
} else if (type === 'market_offer') {
|
||||||
|
labelEl.innerHTML = `🛒 Ρυθμίσεις Παζαριού`;
|
||||||
|
} else if (type === 'blueprints') {
|
||||||
|
if (window.selectedBlueprintName) {
|
||||||
|
labelEl.innerHTML = `📜 ${window.selectedBlueprintName}`;
|
||||||
|
} else {
|
||||||
|
labelEl.innerHTML = `-- Επιλέξτε Blueprint --`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.reopenActiveModal = function() {
|
||||||
|
const type = window.currentCmdType;
|
||||||
|
if (type === 'build') window.openBuildingModal();
|
||||||
|
else if (type === 'recruit') window.openUnitModal();
|
||||||
|
else if (type === 'research') window.openAcademyModal();
|
||||||
|
else if (type === 'market_offer') window.openMarketModal();
|
||||||
|
else if (type === 'blueprints') window.openBlueprintsModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.selectedBlueprintName = null;
|
||||||
|
window.selectBlueprint = function(name) {
|
||||||
|
window.selectedBlueprintName = name;
|
||||||
|
window.updateSelectionDisplay();
|
||||||
|
window.closeBlueprintsModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.openBlueprintsModal = function() {
|
||||||
|
document.getElementById('blueprints-modal-overlay').classList.add('open');
|
||||||
|
};
|
||||||
|
window.closeBlueprintsModal = function(e) {
|
||||||
|
if (e && e.target.id !== 'blueprints-modal-overlay' && !e.target.classList.contains('modal-close')) return;
|
||||||
|
document.getElementById('blueprints-modal-overlay').classList.remove('open');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.openMarketModal = function() {
|
||||||
|
document.getElementById('market-modal-overlay').classList.add('open');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.closeMarketModal = function(e) {
|
||||||
|
if (e && !e.target.classList.contains('modal-overlay') && !e.target.classList.contains('modal-close')) return;
|
||||||
|
document.getElementById('market-modal-overlay').classList.remove('open');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.saveMarketModal = function() {
|
||||||
|
const oType = document.getElementById('market-offer-type').value;
|
||||||
|
const oAmt = document.getElementById('market-offer-amount').value;
|
||||||
|
const dType = document.getElementById('market-demand-type').value;
|
||||||
|
const dAmt = document.getElementById('market-demand-amount').value;
|
||||||
|
|
||||||
|
const labelEl = document.getElementById('selection-label');
|
||||||
|
const resGr = { wood: 'Ξύλο', stone: 'Πέτρα', iron: 'Ασήμι' };
|
||||||
|
labelEl.innerHTML = `🛒 ${oAmt} ${resGr[oType]} ➔ ${dAmt} ${resGr[dType]}`;
|
||||||
|
|
||||||
|
window.closeMarketModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Building emoji icons for the visual grid
|
// Building emoji icons for the visual grid
|
||||||
const BUILDING_ICONS = {
|
window.BUILDING_ICONS = {
|
||||||
main: '🏛️', storage: '🏚️', farm: '🌾', academy: '📜',
|
main: '🏛️', storage: '🏚️', farm: '🌾', academy: '📜',
|
||||||
temple: '⛩️', barracks: '⚔️', docks: '⚓', market: '🛒',
|
temple: '⛩️', barracks: '⚔️', docks: '⚓', market: '🛒',
|
||||||
hide: '🕳️', lumber: '🪵', stoner: '🪨', ironer: '⛏️', wall: '🧱'
|
hide: '🕳️', lumber: '🪵', stoner: '🪨', ironer: '⛏️', wall: '🧱'
|
||||||
@@ -34,7 +128,7 @@ window.openBuildingModal = function() {
|
|||||||
grid.innerHTML = Object.entries(window.BUILDING_NAMES_GR).map(([key, nameGr]) => {
|
grid.innerHTML = Object.entries(window.BUILDING_NAMES_GR).map(([key, nameGr]) => {
|
||||||
const level = bLevels[key] !== undefined ? bLevels[key] : '?';
|
const level = bLevels[key] !== undefined ? bLevels[key] : '?';
|
||||||
const data = bData[key];
|
const data = bData[key];
|
||||||
const icon = BUILDING_ICONS[key] || '🏗️';
|
const icon = window.BUILDING_ICONS[key] || '🏗️';
|
||||||
const isSelected = key === window.selectedBuildingId;
|
const isSelected = key === window.selectedBuildingId;
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@@ -117,78 +211,252 @@ window.closeBuildingModal = function(e) {
|
|||||||
|
|
||||||
window.selectBuilding = function(key, nameGr) {
|
window.selectBuilding = function(key, nameGr) {
|
||||||
window.selectedBuildingId = key;
|
window.selectedBuildingId = key;
|
||||||
// Update the trigger button label
|
window.updateSelectionDisplay();
|
||||||
document.getElementById('selected-building-label').textContent = `🏗️ ${nameGr}`;
|
|
||||||
// Re-render grid to show new selection highlight
|
|
||||||
window.openBuildingModal();
|
window.openBuildingModal();
|
||||||
// Close after brief visual feedback
|
|
||||||
setTimeout(() => document.getElementById('building-modal-overlay').classList.remove('open'), 180);
|
setTimeout(() => document.getElementById('building-modal-overlay').classList.remove('open'), 180);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Academy Research Logic
|
||||||
|
// ================================================================
|
||||||
|
window.selectedResearchId = null;
|
||||||
|
|
||||||
|
window.RESEARCH_DATA = {
|
||||||
|
"slinger": { "name": "Εκσφενδονιστές", "academy_level": 1, "wood": 300, "stone": 500, "iron": 200, "points": 4 },
|
||||||
|
"archer": { "name": "Τοξότες", "academy_level": 1, "wood": 550, "stone": 100, "iron": 400, "points": 8 },
|
||||||
|
"town_guard": { "name": "Φρουρά πόλης", "academy_level": 1, "wood": 400, "stone": 300, "iron": 300, "points": 3 },
|
||||||
|
"hoplite": { "name": "Οπλίτες", "academy_level": 4, "wood": 600, "stone": 200, "iron": 850, "points": 8 },
|
||||||
|
"meteorology": { "name": "Μετεωρολογία", "academy_level": 4, "wood": 2500, "stone": 1700, "iron": 6500, "points": 4 },
|
||||||
|
"espionage": { "name": "Κατασκοπεία", "academy_level": 7, "wood": 900, "stone": 900, "iron": 1100, "points": 3 },
|
||||||
|
"booty": { "name": "Αφοσίωση χωρικών", "academy_level": 7, "wood": 1300, "stone": 1300, "iron": 1300, "points": 6 },
|
||||||
|
"pottery": { "name": "Κεραμικά", "academy_level": 7, "wood": 700, "stone": 1500, "iron": 900, "points": 4 },
|
||||||
|
"rider": { "name": "Ιππείς", "academy_level": 10, "wood": 1400, "stone": 700, "iron": 1800, "points": 8 },
|
||||||
|
"architecture": { "name": "Αρχιτεκτονική", "academy_level": 10, "wood": 1900, "stone": 2100, "iron": 1300, "points": 6 },
|
||||||
|
"instructor": { "name": "Εκπαιδευτής", "academy_level": 10, "wood": 800, "stone": 1300, "iron": 1600, "points": 4 },
|
||||||
|
"bireme": { "name": "Διήρεις", "academy_level": 13, "wood": 2800, "stone": 1300, "iron": 2200, "points": 8 },
|
||||||
|
"building_crane": { "name": "Γερανός", "academy_level": 13, "wood": 3000, "stone": 1800, "iron": 1400, "points": 4 },
|
||||||
|
"shipwright": { "name": "Ναυπηγός", "academy_level": 13, "wood": 5000, "stone": 2000, "iron": 3900, "points": 6 },
|
||||||
|
"colonize_ship": { "name": "Αποικιακά πλοία", "academy_level": 13, "wood": 7500, "stone": 7500, "iron": 9500, "points": 0 },
|
||||||
|
"chariot": { "name": "Άρματα", "academy_level": 16, "wood": 3700, "stone": 1900, "iron": 2800, "points": 8 },
|
||||||
|
"attack_ship": { "name": "Πλοία-φάροι", "academy_level": 16, "wood": 4400, "stone": 2000, "iron": 2400, "points": 8 },
|
||||||
|
"conscription": { "name": "Στρατολόγηση", "academy_level": 16, "wood": 3800, "stone": 4200, "iron": 6000, "points": 4 },
|
||||||
|
"demolition_ship": { "name": "Πυρπολικά", "academy_level": 19, "wood": 5300, "stone": 2600, "iron": 2700, "points": 8 },
|
||||||
|
"catapult": { "name": "Καταπέλτες", "academy_level": 19, "wood": 5500, "stone": 2900, "iron": 3600, "points": 8 },
|
||||||
|
"cryptography": { "name": "Κρυπτογραφία", "academy_level": 19, "wood": 2500, "stone": 3000, "iron": 5100, "points": 6 },
|
||||||
|
"small_transporter": { "name": "Γρήγορα μεταφορικά πλοία", "academy_level": 22, "wood": 6500, "stone": 2800, "iron": 3200, "points": 8 },
|
||||||
|
"plow": { "name": "Άροτρο", "academy_level": 22, "wood": 3000, "stone": 3300, "iron": 2100, "points": 4 },
|
||||||
|
"berth": { "name": "Κουκέτες", "academy_level": 22, "wood": 8900, "stone": 5200, "iron": 7800, "points": 6 },
|
||||||
|
"trireme": { "name": "Τριήρεις", "academy_level": 25, "wood": 6500, "stone": 3800, "iron": 4700, "points": 8 },
|
||||||
|
"phalanx": { "name": "Φάλαγγα", "academy_level": 25, "wood": 4000, "stone": 4000, "iron": 15000, "points": 9 },
|
||||||
|
"breach": { "name": "Διάσπαση εχθρικού μετώπου", "academy_level": 25, "wood": 8000, "stone": 8000, "iron": 9000, "points": 6 },
|
||||||
|
"mathematics": { "name": "Μαθηματικά", "academy_level": 25, "wood": 7100, "stone": 4400, "iron": 8600, "points": 6 },
|
||||||
|
"ram": { "name": "Πολιορκητικός κριός", "academy_level": 28, "wood": 7900, "stone": 9200, "iron": 14000, "points": 10 },
|
||||||
|
"cartography": { "name": "Χαρτογραφία", "academy_level": 28, "wood": 10000, "stone": 6700, "iron": 12500, "points": 8 },
|
||||||
|
"take_over": { "name": "Κατάκτηση", "academy_level": 28, "wood": 12000, "stone": 12000, "iron": 16000, "points": 0 },
|
||||||
|
"stone_storm": { "name": "Καταιγισμός από πέτρες", "academy_level": 31, "wood": 8500, "stone": 5900, "iron": 6600, "points": 4 },
|
||||||
|
"temple_looting": { "name": "Λεηλασία ναού", "academy_level": 31, "wood": 9200, "stone": 5300, "iron": 10000, "points": 6 },
|
||||||
|
"divine_selection": { "name": "Θεϊκή επιλογή", "academy_level": 31, "wood": 10000, "stone": 8000, "iron": 12000, "points": 10 },
|
||||||
|
"combat_experience": { "name": "Εμπειρία μάχης", "academy_level": 34, "wood": 9800, "stone": 11400, "iron": 14200, "points": 6 },
|
||||||
|
"strong_wine": { "name": "Δυνατό κρασί", "academy_level": 34, "wood": 8000, "stone": 6500, "iron": 11000, "points": 4 },
|
||||||
|
"set_sail": { "name": "Σαλπάρισμα!", "academy_level": 34, "wood": 13000, "stone": 9700, "iron": 15500, "points": 8 }
|
||||||
|
};
|
||||||
|
|
||||||
window.renderUnitDropdown = function() {
|
window.openAcademyModal = function() {
|
||||||
const town = window.getSelectedTown();
|
const town = window.getSelectedTown();
|
||||||
if (!town) return;
|
if (!town) return;
|
||||||
const uSelect = document.getElementById('unit-select');
|
const grid = document.getElementById('academy-grid');
|
||||||
const uData = town.unit_data || {};
|
|
||||||
|
|
||||||
const currentVal = uSelect.value;
|
// Group researches by academy level
|
||||||
uSelect.innerHTML = '<option value="" disabled selected>-- Επιλέξτε Μονάδα --</option>';
|
const levels = [1, 4, 7, 10, 13, 16, 19, 22, 25, 28, 31, 34];
|
||||||
|
const townResearches = town.researches || {};
|
||||||
|
const townBuildings = town.buildings || {};
|
||||||
|
const townResources = town.resources || { wood: 0, stone: 0, iron: 0 };
|
||||||
|
const academyLvl = townBuildings.academy || 0;
|
||||||
|
|
||||||
for (const [key, nameGr] of Object.entries(window.UNIT_NAMES_GR)) {
|
let html = '';
|
||||||
if (key === 'militia') continue;
|
for (const lvl of levels) {
|
||||||
|
const researchesInLvl = Object.entries(window.RESEARCH_DATA).filter(([k, v]) => v.academy_level === lvl);
|
||||||
|
if (researchesInLvl.length === 0) continue;
|
||||||
|
|
||||||
const data = uData[key];
|
html += `<div class="academy-col">
|
||||||
let text = `${nameGr}`;
|
<div class="academy-col-header">Επίπεδο ${lvl}</div>`;
|
||||||
|
|
||||||
if (data) {
|
for (const [key, data] of researchesInLvl) {
|
||||||
const w = window.fmt(data.wood || 0);
|
const isResearched = !!townResearches[key];
|
||||||
const st = window.fmt(data.stone || 0);
|
const isSelected = key === window.selectedResearchId;
|
||||||
const i = window.fmt(data.iron || 0);
|
const isLocked = academyLvl < lvl;
|
||||||
const pop = data.pop || 0;
|
const noResources = townResources.wood < data.wood || townResources.stone < data.stone || townResources.iron < data.iron;
|
||||||
|
|
||||||
// Unit build_time is usually raw seconds in GameData
|
let statusClass, statusLabel, cardClass = '';
|
||||||
let t = data.build_time || 0;
|
if (isResearched) {
|
||||||
let tStr = `${t}s`;
|
statusClass = 'maxed'; statusLabel = '✓ Ερευνήθηκε'; cardClass = 'bld-maxed';
|
||||||
if (t > 60) {
|
} else if (isLocked) {
|
||||||
let m = Math.floor(t / 60);
|
statusClass = 'locked'; statusLabel = '🔒 Κλειδωμένο'; cardClass = 'bld-locked';
|
||||||
let s = t % 60;
|
} else if (noResources) {
|
||||||
tStr = `${m}m ${s}s`;
|
statusClass = 'no-resources'; statusLabel = '❌ Λείπουν Πόροι';
|
||||||
}
|
|
||||||
|
|
||||||
const costStr = `Ξ:${w} Π:${st} Α:${i} 🧔:${pop} · ⏱ ${tStr}`;
|
|
||||||
|
|
||||||
const missingKeys = data.missing_dependencies ? Object.keys(data.missing_dependencies) : [];
|
|
||||||
const isLocked = missingKeys.length > 0;
|
|
||||||
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = key;
|
|
||||||
|
|
||||||
if (isLocked) {
|
|
||||||
option.textContent = `${text} — 🔒 Κλειδωμένο`;
|
|
||||||
option.style.color = '#ff4444';
|
|
||||||
} else if (data.enough_resources === false) {
|
|
||||||
option.textContent = `${text} — ❌ ${costStr} (Λείπουν Πόροι 1x)`;
|
|
||||||
option.style.color = '#aa5555';
|
|
||||||
} else {
|
} else {
|
||||||
option.textContent = `${text} — ✅ ${costStr}`;
|
statusClass = 'can-build'; statusLabel = '✅ Έρευνα';
|
||||||
}
|
}
|
||||||
|
|
||||||
uSelect.appendChild(option);
|
const clickable = !isResearched && !isLocked;
|
||||||
} else {
|
const onclick = clickable ? `onclick="window.selectResearch('${key}', '${data.name}')"` : '';
|
||||||
const option = document.createElement('option');
|
const costStr = `Ξ:${window.fmt(data.wood)} Π:${window.fmt(data.stone)} Α:${window.fmt(data.iron)}`;
|
||||||
option.value = key;
|
|
||||||
option.textContent = text;
|
html += `<div class="bld-card ${cardClass}${isSelected ? ' bld-selected' : ''}" ${onclick}>
|
||||||
uSelect.appendChild(option);
|
<span class="bld-name" style="margin-top:6px; font-size:0.8rem;">${data.name}</span>
|
||||||
|
<span class="bld-status ${statusClass}">${statusLabel}</span>
|
||||||
|
<span class="bld-cost" style="color:#a88; margin-top:4px;">Πόντοι: ${data.points}</span>
|
||||||
|
<span class="bld-cost">${costStr}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
html += `</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.innerHTML = html;
|
||||||
|
document.getElementById('academy-modal-overlay').classList.add('open');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.closeAcademyModal = function(e) {
|
||||||
|
if (e && e.target !== document.getElementById('academy-modal-overlay') && e.target !== document.getElementById('academy-modal-close')) return;
|
||||||
|
document.getElementById('academy-modal-overlay').classList.remove('open');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.selectResearch = function(key, name) {
|
||||||
|
window.selectedResearchId = key;
|
||||||
|
window.updateSelectionDisplay();
|
||||||
|
window.openAcademyModal();
|
||||||
|
setTimeout(() => document.getElementById('academy-modal-overlay').classList.remove('open'), 180);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
window.selectedUnitId = null;
|
||||||
|
|
||||||
|
window.UNIT_ICONS = {
|
||||||
|
sword: '⚔️', slinger: '🪨', archer: '🏹', hoplite: '🛡️',
|
||||||
|
rider: '🐎', chariot: '🛷', catapult: '☄️', godsent: '👼',
|
||||||
|
big_transporter: '⛴️', small_transporter: '🚤', bireme: '🛶',
|
||||||
|
attack_ship: '🔥', trireme: '🔱', colonize_ship: '⚓',
|
||||||
|
medusa: '🐍', zyklop: '👁️', harpy: '🦅', pegasus: '🐴',
|
||||||
|
minotaur: '🐂', manticore: '🦁', cerberus: '🐕',
|
||||||
|
hydra: '🐉', sea_monster: '🦑', militia: '🧑🌾'
|
||||||
|
};
|
||||||
|
|
||||||
|
window.renderUnitDropdown = function() {
|
||||||
|
// No-op - selection now happens via modal
|
||||||
|
};
|
||||||
|
|
||||||
|
window.openUnitModal = function() {
|
||||||
|
const town = window.getSelectedTown();
|
||||||
|
if (!town) return;
|
||||||
|
const grid = document.getElementById('unit-grid');
|
||||||
|
const uData = town.unit_data || {};
|
||||||
|
|
||||||
|
// Group units into categories for better display
|
||||||
|
const categories = {
|
||||||
|
'Ξηρά': ['sword', 'slinger', 'archer', 'hoplite', 'rider', 'chariot', 'catapult', 'godsent'],
|
||||||
|
'Ναυτικές': ['big_transporter', 'small_transporter', 'bireme', 'attack_ship', 'trireme', 'colonize_ship'],
|
||||||
|
'Μυθικές': ['medusa', 'zyklop', 'harpy', 'pegasus', 'minotaur', 'manticore', 'cerberus', 'hydra', 'sea_monster']
|
||||||
|
};
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
for (const [catName, units] of Object.entries(categories)) {
|
||||||
|
let catHtml = '';
|
||||||
|
|
||||||
|
for (const key of units) {
|
||||||
|
if (key === 'militia') continue;
|
||||||
|
|
||||||
|
const nameGr = window.UNIT_NAMES_GR[key] || key;
|
||||||
|
const data = uData[key];
|
||||||
|
const icon = window.UNIT_ICONS[key] || '💂';
|
||||||
|
const isSelected = key === window.selectedUnitId;
|
||||||
|
|
||||||
|
let statusClass, statusLabel, cardClass = '';
|
||||||
|
let costStr = '';
|
||||||
|
let clickable = false;
|
||||||
|
|
||||||
|
const requiredGod = window.UNIT_GODS ? window.UNIT_GODS[key] : null;
|
||||||
|
const isWrongGod = requiredGod && town.god !== requiredGod;
|
||||||
|
const isNoGod = key === 'godsent' && !town.god;
|
||||||
|
|
||||||
|
if (isWrongGod) {
|
||||||
|
statusClass = 'locked'; statusLabel = '🔒 Άλλος Θεός'; cardClass = 'bld-locked';
|
||||||
|
const greekGods = { zeus: 'Δία', poseidon: 'Ποσειδώνα', hera: 'Ήρα', athena: 'Αθηνά', hades: 'Άδη', artemis: 'Άρτεμις', aphrodite: 'Αφροδίτη', ares: 'Άρη' };
|
||||||
|
costStr = `Απαιτεί: ${greekGods[requiredGod] || requiredGod}`;
|
||||||
|
} else if (isNoGod) {
|
||||||
|
statusClass = 'locked'; statusLabel = '🔒 Χωρίς Θεό'; cardClass = 'bld-locked';
|
||||||
|
costStr = `Απαιτείται Ναός`;
|
||||||
|
} else if (data) {
|
||||||
|
const missingKeys = data.missing_dependencies ? Object.keys(data.missing_dependencies) : [];
|
||||||
|
const isLocked = missingKeys.length > 0;
|
||||||
|
|
||||||
|
const w = window.fmt(data.wood || 0);
|
||||||
|
const st = window.fmt(data.stone || 0);
|
||||||
|
const i = window.fmt(data.iron || 0);
|
||||||
|
const pop = data.pop || 0;
|
||||||
|
|
||||||
|
let t = data.build_time || 0;
|
||||||
|
let tStr = `${t}s`;
|
||||||
|
if (t > 60) {
|
||||||
|
let m = Math.floor(t / 60);
|
||||||
|
let s = t % 60;
|
||||||
|
tStr = `${m}m ${s}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show favor if it's a mythical unit or godsent
|
||||||
|
const favorStr = (data.favor && data.favor > 0) ? ` ⚡:${data.favor}` : '';
|
||||||
|
costStr = `Ξ:${w} Π:${st} Α:${i}${favorStr} 🧔:${pop} · ⏱ ${tStr}`;
|
||||||
|
|
||||||
|
if (isLocked) {
|
||||||
|
statusClass = 'locked'; statusLabel = '🔒 Κλειδωμένο'; cardClass = 'bld-locked';
|
||||||
|
} else if (data.enough_resources === false) {
|
||||||
|
statusClass = 'no-resources'; statusLabel = '❌ Λείπουν Πόροι';
|
||||||
|
} else {
|
||||||
|
statusClass = 'can-build'; statusLabel = '✅ Διαθέσιμο';
|
||||||
|
clickable = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If no data is available for this unit, treat it as locked/unknown
|
||||||
|
statusClass = 'locked'; statusLabel = '🔒 Άγνωστο'; cardClass = 'bld-locked';
|
||||||
|
}
|
||||||
|
|
||||||
|
const onclick = clickable ? `onclick="window.selectUnit('${key}', '${nameGr}')"` : '';
|
||||||
|
|
||||||
|
catHtml += `<div class="bld-card ${cardClass}${isSelected ? ' bld-selected' : ''}" ${onclick} style="width:140px; justify-content:flex-start;">
|
||||||
|
<span class="bld-icon">${icon}</span>
|
||||||
|
<span class="bld-name" style="margin-top:6px; font-size:0.85rem;">${nameGr}</span>
|
||||||
|
<span class="bld-status ${statusClass}">${statusLabel}</span>
|
||||||
|
<span class="bld-cost" style="font-size:0.65rem;">${costStr}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (catHtml) {
|
||||||
|
html += `<div style="width:100%; margin-top:10px;"><h4 style="color:#c8a44a; margin-bottom:10px; border-bottom:1px solid #2a4a6a; padding-bottom:4px;">${catName}</h4>
|
||||||
|
<div style="display:flex; flex-wrap:wrap; gap:10px;">${catHtml}</div>
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentVal && Array.from(uSelect.options).some(o => o.value === currentVal)) {
|
grid.innerHTML = html;
|
||||||
uSelect.value = currentVal;
|
document.getElementById('unit-modal-overlay').classList.add('open');
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.closeUnitModal = function(e) {
|
||||||
|
if (e && e.target !== document.getElementById('unit-modal-overlay') && e.target !== document.getElementById('unit-modal-close')) return;
|
||||||
|
document.getElementById('unit-modal-overlay').classList.remove('open');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.selectUnit = function(key, nameGr) {
|
||||||
|
window.selectedUnitId = key;
|
||||||
|
window.updateSelectionDisplay();
|
||||||
|
window.openUnitModal();
|
||||||
|
setTimeout(() => document.getElementById('unit-modal-overlay').classList.remove('open'), 180);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
|
||||||
window.renderBuildQueuePreview = function() {
|
window.renderBuildQueuePreview = function() {
|
||||||
const town = window.getSelectedTown();
|
const town = window.getSelectedTown();
|
||||||
const el = document.getElementById('build-queue-preview');
|
const el = document.getElementById('build-queue-preview');
|
||||||
|
|||||||
@@ -1,11 +1,171 @@
|
|||||||
// ================================================================
|
// ================================================================
|
||||||
// Command Log Component
|
// Command Log & Build Queue Component
|
||||||
// ================================================================
|
// ================================================================
|
||||||
|
|
||||||
|
// -- Panel state: 'queue' | 'log' ----------------------------
|
||||||
|
window._logPanelMode = 'queue';
|
||||||
|
|
||||||
|
// ---- Toggle buttons -------------------------------------------
|
||||||
|
window.switchToQueueMode = function() {
|
||||||
|
window._logPanelMode = 'queue';
|
||||||
|
document.getElementById('tab-queue').classList.add('tab-active');
|
||||||
|
document.getElementById('tab-log').classList.remove('tab-active');
|
||||||
|
window.refreshLogPanel();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.switchToLogMode = function() {
|
||||||
|
window._logPanelMode = 'log';
|
||||||
|
document.getElementById('tab-log').classList.add('tab-active');
|
||||||
|
document.getElementById('tab-queue').classList.remove('tab-active');
|
||||||
|
window.fetchLog();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- Main dispatcher ------------------------------------------
|
||||||
|
window.refreshLogPanel = function() {
|
||||||
|
if (window._logPanelMode === 'queue') {
|
||||||
|
const town = window.getSelectedTown();
|
||||||
|
if (town) {
|
||||||
|
window.fetchBuildQueue(town.town_id);
|
||||||
|
} else {
|
||||||
|
document.getElementById('log-content').innerHTML =
|
||||||
|
'<p style="color:#555;font-size:0.85rem;padding:12px 0;">← Επιλέξτε πόλη για να δείτε την ουρά.</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// BUILD QUEUE (per-town, draggable)
|
||||||
|
// ================================================================
|
||||||
|
window.fetchBuildQueue = async function(townId) {
|
||||||
|
if (window._logPanelMode !== 'queue') return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/dashboard/commands/queue?player_id=${window.PLAYER_ID}&town_id=${encodeURIComponent(townId)}`);
|
||||||
|
const cmds = await res.json();
|
||||||
|
window.renderBuildQueue(cmds, townId);
|
||||||
|
} catch(e) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Drag state
|
||||||
|
let _dragSrcIdx = null;
|
||||||
|
|
||||||
|
window.renderBuildQueue = function(cmds, townId) {
|
||||||
|
const el = document.getElementById('log-content');
|
||||||
|
|
||||||
|
if (!cmds || cmds.length === 0) {
|
||||||
|
el.innerHTML = `
|
||||||
|
<div style="text-align:center;padding:2rem 1rem;color:#444;">
|
||||||
|
<div style="font-size:2rem;margin-bottom:0.5rem;">🏗️</div>
|
||||||
|
<p style="font-size:0.85rem;">Η ουρά κατασκευών είναι κενή.</p>
|
||||||
|
<p style="font-size:0.75rem;color:#333;margin-top:0.3rem;">Χρησιμοποιήστε την φόρμα για να προσθέσετε κατασκευές.</p>
|
||||||
|
</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = cmds.map((cmd, idx) => {
|
||||||
|
const p = typeof cmd.payload === 'string' ? JSON.parse(cmd.payload) : cmd.payload;
|
||||||
|
const nameGr = window.BUILDING_NAMES_GR?.[p.building_id] || p.building_id || '?';
|
||||||
|
const icon = window.BUILDING_ICONS?.[p.building_id] || '🏗️';
|
||||||
|
const isExec = cmd.status === 'executing';
|
||||||
|
|
||||||
|
const statusDot = isExec
|
||||||
|
? `<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#4acc64;box-shadow:0 0 5px #4acc64;flex-shrink:0;" title="Εκτελείται"></span>`
|
||||||
|
: `<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#555;flex-shrink:0;" title="Σε αναμονή"></span>`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="bq-row" draggable="true"
|
||||||
|
data-idx="${idx}" data-id="${cmd.id}" data-town="${townId}"
|
||||||
|
ondragstart="window._bqDragStart(event,${idx})"
|
||||||
|
ondragover="window._bqDragOver(event)"
|
||||||
|
ondrop="window._bqDrop(event,${idx},'${townId}')"
|
||||||
|
ondragend="window._bqDragEnd(event)">
|
||||||
|
<span class="bq-handle" title="Σύρε για αναδιάταξη">⠿</span>
|
||||||
|
<span class="bq-pos">${idx + 1}</span>
|
||||||
|
${statusDot}
|
||||||
|
<span class="bq-icon">${icon}</span>
|
||||||
|
<span class="bq-name">${nameGr}</span>
|
||||||
|
<button class="bq-cancel-btn" onclick="window._bqCancel(${cmd.id})" title="Ακύρωση">✕</button>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
el.innerHTML = `<div id="bq-list">${rows}</div>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- Drag-and-drop handlers -----------------------------------
|
||||||
|
window._bqDragStart = function(e, idx) {
|
||||||
|
_dragSrcIdx = idx;
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
setTimeout(() => {
|
||||||
|
const rows = document.querySelectorAll('.bq-row');
|
||||||
|
if (rows[idx]) rows[idx].style.opacity = '0.4';
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
window._bqDragOver = function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
// Highlight target row
|
||||||
|
document.querySelectorAll('.bq-row').forEach(r => r.classList.remove('bq-drag-over'));
|
||||||
|
const row = e.currentTarget;
|
||||||
|
if (row) row.classList.add('bq-drag-over');
|
||||||
|
};
|
||||||
|
|
||||||
|
window._bqDrop = function(e, targetIdx, townId) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (_dragSrcIdx === null || _dragSrcIdx === targetIdx) return;
|
||||||
|
|
||||||
|
// Re-order the DOM
|
||||||
|
const list = document.getElementById('bq-list');
|
||||||
|
const rows = Array.from(list.querySelectorAll('.bq-row'));
|
||||||
|
const movedRow = rows.splice(_dragSrcIdx, 1)[0];
|
||||||
|
rows.splice(targetIdx, 0, movedRow);
|
||||||
|
|
||||||
|
// Update numbering & opacity
|
||||||
|
rows.forEach((r, i) => {
|
||||||
|
r.style.opacity = '1';
|
||||||
|
r.classList.remove('bq-drag-over');
|
||||||
|
r.dataset.idx = i;
|
||||||
|
r.querySelector('.bq-pos').textContent = i + 1;
|
||||||
|
r.setAttribute('ondragstart', `window._bqDragStart(event,${i})`);
|
||||||
|
r.setAttribute('ondrop', `window._bqDrop(event,${i},'${townId}')`);
|
||||||
|
});
|
||||||
|
list.innerHTML = '';
|
||||||
|
rows.forEach(r => list.appendChild(r));
|
||||||
|
|
||||||
|
// Persist new order to server
|
||||||
|
const orderedIds = rows.map(r => parseInt(r.dataset.id));
|
||||||
|
fetch('/dashboard/commands/reorder', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ player_id: window.PLAYER_ID, town_id: townId, order: orderedIds })
|
||||||
|
});
|
||||||
|
|
||||||
|
_dragSrcIdx = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
window._bqDragEnd = function(e) {
|
||||||
|
document.querySelectorAll('.bq-row').forEach(r => {
|
||||||
|
r.style.opacity = '1';
|
||||||
|
r.classList.remove('bq-drag-over');
|
||||||
|
});
|
||||||
|
_dragSrcIdx = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
window._bqCancel = async function(id) {
|
||||||
|
await fetch(`/dashboard/commands/${id}`, { method: 'DELETE' });
|
||||||
|
// Refresh the queue for the currently selected town
|
||||||
|
const town = window.getSelectedTown();
|
||||||
|
if (town) window.fetchBuildQueue(town.town_id);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// COMMAND LOG (full history, existing behaviour)
|
||||||
|
// ================================================================
|
||||||
window.renderLog = function(cmds) {
|
window.renderLog = function(cmds) {
|
||||||
|
if (window._logPanelMode !== 'log') return;
|
||||||
const el = document.getElementById('log-content');
|
const el = document.getElementById('log-content');
|
||||||
if (!cmds.length) {
|
if (!cmds.length) {
|
||||||
el.innerHTML = '<p id="empty-log">No commands sent yet.</p>';
|
el.innerHTML = '<p id="empty-log" style="color:#555;font-size:0.85rem;padding:12px 0;">No commands sent yet.</p>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,10 +180,11 @@ window.renderLog = function(cmds) {
|
|||||||
desc = `Recruit: ${p.amount}x ${nameGr}`;
|
desc = `Recruit: ${p.amount}x ${nameGr}`;
|
||||||
} else if (cmd.type === 'market_offer') {
|
} else if (cmd.type === 'market_offer') {
|
||||||
desc = `Market: ${p.offer} ${p.offer_type} ➞ ${p.demand} ${p.demand_type}`;
|
desc = `Market: ${p.offer} ${p.offer_type} ➞ ${p.demand} ${p.demand_type}`;
|
||||||
|
} else {
|
||||||
|
desc = cmd.type;
|
||||||
}
|
}
|
||||||
const statusClass = `status-${cmd.status}`;
|
const statusClass = `status-${cmd.status}`;
|
||||||
const cancelBtn = `<button class="btn btn-danger btn-sm" onclick="cancelCommand(${cmd.id})">✕</button>`;
|
const cancelBtn = `<button class="btn btn-danger btn-sm" onclick="cancelCommand(${cmd.id})">✕</button>`;
|
||||||
|
|
||||||
const timeStr = new Date(cmd.created_at + 'Z').toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
|
const timeStr = new Date(cmd.created_at + 'Z').toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
|
||||||
|
|
||||||
return `<tr>
|
return `<tr>
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ window.renderTowns = function() {
|
|||||||
// Get active filters
|
// Get active filters
|
||||||
const searchTerm = (document.getElementById('town-search')?.value || '').toLowerCase();
|
const searchTerm = (document.getElementById('town-search')?.value || '').toLowerCase();
|
||||||
const reqFullWh = document.getElementById('filter-full-wh')?.checked;
|
const reqFullWh = document.getElementById('filter-full-wh')?.checked;
|
||||||
const reqFestival = document.getElementById('filter-festival')?.checked;
|
const reqNotBuilding = document.getElementById('filter-not-building')?.checked;
|
||||||
const reqPoints = document.getElementById('filter-points')?.checked;
|
|
||||||
|
|
||||||
const filteredTowns = window.towns.filter(t => {
|
const filteredTowns = window.towns.filter(t => {
|
||||||
// 1. Search by name
|
// 1. Search by name
|
||||||
@@ -29,13 +28,8 @@ window.renderTowns = function() {
|
|||||||
// 2. Full WH (>95%)
|
// 2. Full WH (>95%)
|
||||||
if (reqFullWh && Math.max(wPct, sPct, iPct) < 0.95) return false;
|
if (reqFullWh && Math.max(wPct, sPct, iPct) < 0.95) return false;
|
||||||
|
|
||||||
// 3. Festival (Wood>15k, Stone>18k, Iron>15k)
|
// 3. Not Building (no items in build_queue)
|
||||||
// City Festival exact costs = 15000, 18000, 15000
|
if (reqNotBuilding && t.build_queue && t.build_queue.length > 0) return false;
|
||||||
if (reqFestival && ((res.wood || 0) < 15000 || (res.stone || 0) < 18000 || (res.iron || 0) < 15000)) return false;
|
|
||||||
|
|
||||||
// 4. Large Points
|
|
||||||
const pts = typeof t.points === 'number' ? t.points : parseInt(t.points) || 0;
|
|
||||||
if (reqPoints && pts < 10000) return false;
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@@ -62,9 +56,6 @@ window.renderTowns = function() {
|
|||||||
if (wPct >= 0.95 || sPct >= 0.95 || iPct >= 0.95) {
|
if (wPct >= 0.95 || sPct >= 0.95 || iPct >= 0.95) {
|
||||||
markers += '<span title="Γεμάτη Αποθήκη!" style="margin-right:4px;">⚠️</span>';
|
markers += '<span title="Γεμάτη Αποθήκη!" style="margin-right:4px;">⚠️</span>';
|
||||||
}
|
}
|
||||||
if ((res.wood || 0) >= 15000 && (res.stone || 0) >= 18000 && (res.iron || 0) >= 15000) {
|
|
||||||
markers += '<span title="Αρκετοί πόροι για Φεστιβάλ!" style="margin-right:4px;">🎭</span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
const getC = (pct) => pct >= 0.95 ? 'color:#ff4a4a;font-weight:bold;' : '';
|
const getC = (pct) => pct >= 0.95 ? 'color:#ff4a4a;font-weight:bold;' : '';
|
||||||
|
|
||||||
@@ -99,6 +90,10 @@ window.selectTown = function(id) {
|
|||||||
window.renderBuildingDropdown();
|
window.renderBuildingDropdown();
|
||||||
window.renderUnitDropdown();
|
window.renderUnitDropdown();
|
||||||
window.renderTownDetails();
|
window.renderTownDetails();
|
||||||
|
// Refresh build queue panel for the newly selected town
|
||||||
|
if (window._logPanelMode === 'queue') {
|
||||||
|
window.fetchBuildQueue(id);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.getSelectedTown = function() {
|
window.getSelectedTown = function() {
|
||||||
@@ -169,6 +164,8 @@ window.renderTownDetails = function() {
|
|||||||
document.getElementById('td-market').innerHTML = mCap > 0
|
document.getElementById('td-market').innerHTML = mCap > 0
|
||||||
? `📦 Εμπορική Χωρητικότητα: <strong>${window.fmt(mCap)}</strong>`
|
? `📦 Εμπορική Χωρητικότητα: <strong>${window.fmt(mCap)}</strong>`
|
||||||
: '';
|
: '';
|
||||||
|
const mCapLabel = document.getElementById('market-capacity-label');
|
||||||
|
if (mCapLabel) mCapLabel.textContent = `Χωρητικότητα: ${window.fmt(mCap)}`;
|
||||||
|
|
||||||
const godName = t.god ? t.god.charAt(0).toUpperCase() + t.god.slice(1) : 'Κανένας';
|
const godName = t.god ? t.god.charAt(0).toUpperCase() + t.god.slice(1) : 'Κανένας';
|
||||||
const seaStr = t.sea != null ? `Θ${t.sea}` : '—';
|
const seaStr = t.sea != null ? `Θ${t.sea}` : '—';
|
||||||
@@ -215,4 +212,43 @@ window.renderTownDetails = function() {
|
|||||||
if(unitsHtml === '') unitsHtml = '<div style="color:#666">Κανένα στράτευμα</div>';
|
if(unitsHtml === '') unitsHtml = '<div style="color:#666">Κανένα στράτευμα</div>';
|
||||||
|
|
||||||
document.getElementById('td-units').innerHTML = unitsHtml;
|
document.getElementById('td-units').innerHTML = unitsHtml;
|
||||||
|
|
||||||
|
// ---- Blueprint Lock & Indicator ----
|
||||||
|
const isBlueprintActive = !!t.blueprint_active;
|
||||||
|
const bpName = t.blueprint_name || 'Standard Growth';
|
||||||
|
|
||||||
|
if (isBlueprintActive) {
|
||||||
|
document.getElementById('td-general').innerHTML += `
|
||||||
|
<div style="margin-top:10px; padding: 4px 8px; background: rgba(200,164,74,0.15); border-left: 3px solid #c8a44a; border-radius:4px; font-size:0.8rem;">
|
||||||
|
🤖 Blueprint: <strong style="color:#c8a44a">${bpName}</strong> <span style="color:#2ecc71">(ΕΝΕΡΓΟ)</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const btnBuild = document.getElementById('seg-build');
|
||||||
|
const btnResearch = document.getElementById('seg-research');
|
||||||
|
|
||||||
|
if (btnBuild && btnResearch) {
|
||||||
|
if (isBlueprintActive) {
|
||||||
|
btnBuild.style.opacity = '0.3';
|
||||||
|
btnBuild.style.pointerEvents = 'none';
|
||||||
|
btnBuild.title = 'Απενεργοποιημένο λόγω Blueprint';
|
||||||
|
|
||||||
|
btnResearch.style.opacity = '0.3';
|
||||||
|
btnResearch.style.pointerEvents = 'none';
|
||||||
|
btnResearch.title = 'Απενεργοποιημένο λόγω Blueprint';
|
||||||
|
|
||||||
|
if (window.currentCmdType === 'build' || window.currentCmdType === 'research') {
|
||||||
|
window.setCmdType('recruit');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
btnBuild.style.opacity = '1';
|
||||||
|
btnBuild.style.pointerEvents = 'auto';
|
||||||
|
btnBuild.title = '';
|
||||||
|
|
||||||
|
btnResearch.style.opacity = '1';
|
||||||
|
btnResearch.style.pointerEvents = 'auto';
|
||||||
|
btnResearch.title = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ window.BUILDING_NAMES_GR = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.UNIT_NAMES_GR = {
|
window.UNIT_NAMES_GR = {
|
||||||
sword: "Ξιφομάχος", slinger: "Σφενδονήτης", archer: "Τοξότης", hoplite: "Οπλίτης",
|
sword: "Ξιφομάχος", slinger: "Εκσφενδονιστής", archer: "Τοξότης", hoplite: "Οπλίτης",
|
||||||
rider: "Ιππέας", chariot: "Άρμα", catapult: "Καταπέλτης",
|
rider: "Ιππέας", chariot: "Άρμα", catapult: "Καταπέλτης", godsent: "Θεόσταλτος",
|
||||||
big_transporter: "Μεταφορικό", small_transporter: "Γρήγ. Μεταφορικό", bireme: "Διήρης",
|
big_transporter: "Μεταφορικό", small_transporter: "Γρήγ. Μεταφορικό", bireme: "Διήρης",
|
||||||
attack_ship: "Πλοίο Φάρος", trireme: "Τριήρης", colonize_ship: "Αποικιακό",
|
attack_ship: "Πλοίο Φάρος", trireme: "Τριήρης", colonize_ship: "Αποικιακό",
|
||||||
medusa: "Μέδουσα", zyklop: "Κύκλωπας", harpy: "Άρπυια", pegasus: "Πήγασος",
|
medusa: "Μέδουσα", zyklop: "Κύκλωπας", harpy: "Άρπυια", pegasus: "Πήγασος",
|
||||||
@@ -33,6 +33,17 @@ window.UNIT_NAMES_GR = {
|
|||||||
hydra: "Ύδρα", sea_monster: "Τέρας Θάλασσας", militia: "Εθνοφρουρά"
|
hydra: "Ύδρα", sea_monster: "Τέρας Θάλασσας", militia: "Εθνοφρουρά"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.UNIT_GODS = {
|
||||||
|
minotaur: 'zeus', manticore: 'zeus',
|
||||||
|
zyklop: 'poseidon', hydra: 'poseidon',
|
||||||
|
harpy: 'hera', medusa: 'hera',
|
||||||
|
pegasus: 'athena', centaur: 'athena',
|
||||||
|
cerberus: 'hades', erinys: 'hades',
|
||||||
|
griffon: 'artemis', calydonian_boar: 'artemis',
|
||||||
|
siren: 'aphrodite', satyr: 'aphrodite',
|
||||||
|
spartoi: 'ares', ladon: 'ares'
|
||||||
|
};
|
||||||
|
|
||||||
window.RES_ICONS = {
|
window.RES_ICONS = {
|
||||||
wood: '<span class="res-icon res-wood" style="display:inline-block; margin-right:4px;"></span>',
|
wood: '<span class="res-icon res-wood" style="display:inline-block; margin-right:4px;"></span>',
|
||||||
stone: '<span class="res-icon res-stone" style="display:inline-block; margin-right:4px;"></span>',
|
stone: '<span class="res-icon res-stone" style="display:inline-block; margin-right:4px;"></span>',
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
|
|
||||||
<header>
|
<header>
|
||||||
<h1><a href="/" style="text-decoration: none; margin-right: 15px; cursor: pointer;" title="Back to Players">⬅️</a> ⚔️ Grepolis Remote</h1>
|
<h1><a href="/" style="text-decoration: none; margin-right: 15px; cursor: pointer;" title="Back to Players">⬅️</a> ⚔️ Grepolis Remote</h1>
|
||||||
<div class="status-indicator">
|
<div class="status-indicator" style="display: flex; align-items: center; gap: 10px;">
|
||||||
|
<button class="btn btn-gold btn-sm" id="live-btn" onclick="window.requestLiveSync()" title="Request immediate data update from game" style="padding: 4px 8px; font-size: 0.72rem; border-radius: 4px; border: 1px solid #c8a44a;">⚡ Live Sync</button>
|
||||||
<div id="server-status" class="conn-badge">Server…</div>
|
<div id="server-status" class="conn-badge">Server…</div>
|
||||||
<div id="client-status" class="conn-badge">Client…</div>
|
<div id="client-status" class="conn-badge">Client…</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,7 +30,6 @@
|
|||||||
<div id="town-panel">
|
<div id="town-panel">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||||
<h2 style="margin: 0;">Towns</h2>
|
<h2 style="margin: 0;">Towns</h2>
|
||||||
<button class="btn btn-gold btn-sm" id="live-btn" onclick="window.requestLiveSync()" title="Request immediate data update from game" style="padding: 4px 8px; font-size: 0.72rem;">⚡ Live Sync</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="town-filters" style="margin-bottom: 15px; padding: 10px; background: #0f3460; border-radius: 6px; border: 1px solid #2a4a6a;">
|
<div id="town-filters" style="margin-bottom: 15px; padding: 10px; background: #0f3460; border-radius: 6px; border: 1px solid #2a4a6a;">
|
||||||
@@ -41,11 +41,7 @@
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label style="font-size: 0.8rem; cursor: pointer; display: flex; align-items: center; gap: 4px; background: rgba(0,0,0,0.2); padding: 4px 8px; border-radius: 12px; border: 1px solid #2a4a6a;">
|
<label style="font-size: 0.8rem; cursor: pointer; display: flex; align-items: center; gap: 4px; background: rgba(0,0,0,0.2); padding: 4px 8px; border-radius: 12px; border: 1px solid #2a4a6a;">
|
||||||
<input type="checkbox" id="filter-festival" onchange="window.renderTowns()"> 🎭 Ελεύθεροι Πόροι
|
<input type="checkbox" id="filter-not-building" onchange="window.renderTowns()"> 🏗️ Δεν χτίζει
|
||||||
</label>
|
|
||||||
|
|
||||||
<label style="font-size: 0.8rem; cursor: pointer; display: flex; align-items: center; gap: 4px; background: rgba(0,0,0,0.2); padding: 4px 8px; border-radius: 12px; border: 1px solid #2a4a6a;">
|
|
||||||
<input type="checkbox" id="filter-points" onchange="window.renderTowns()"> 📈 10k+ Πόντοι
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,71 +83,92 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="command-form-wrap" style="display:none">
|
<div id="command-form-wrap" style="display:none">
|
||||||
<div class="command-form">
|
<div class="command-form" style="display: flex; flex-direction: column; gap: 15px; align-items: flex-start;">
|
||||||
|
|
||||||
<div class="form-group">
|
<!-- Segmented Control Row -->
|
||||||
<label>Command Type</label>
|
<div class="segmented-control" id="cmd-type-buttons" style="display: flex; gap: 5px; background: #16213e; padding: 4px; border-radius: 8px; border: 1px solid #2a4a6a;">
|
||||||
<select id="cmd-type" onchange="onCmdTypeChange()">
|
<button class="seg-btn" id="seg-build" onclick="window.setCmdType('build', true)" style="flex: 1; padding: 10px; border: none; background: transparent; color: #888; border-radius: 6px; cursor: pointer; transition: 0.2s;">🏗️ Κατασκευές</button>
|
||||||
<option value="" disabled selected>-- Επιλέξτε --</option>
|
<button class="seg-btn" id="seg-recruit" onclick="window.setCmdType('recruit', true)" style="flex: 1; padding: 10px; border: none; background: transparent; color: #888; border-radius: 6px; cursor: pointer; transition: 0.2s;">⚔️ Στρατός</button>
|
||||||
<option value="build">Build / Upgrade</option>
|
<button class="seg-btn" id="seg-market" onclick="window.setCmdType('market_offer', true)" style="flex: 1; padding: 10px; border: none; background: transparent; color: #888; border-radius: 6px; cursor: pointer; transition: 0.2s;">🛒 Παζάρι</button>
|
||||||
<option value="recruit">Recruit Troops</option>
|
<button class="seg-btn" id="seg-research" onclick="window.setCmdType('research', true)" style="flex: 1; padding: 10px; border: none; background: transparent; color: #888; border-radius: 6px; cursor: pointer; transition: 0.2s;">🦉 Έρευνα</button>
|
||||||
<option value="market_offer">Παζάρι - Προσφορά</option>
|
<button class="seg-btn" id="seg-blueprints" onclick="window.setCmdType('blueprints', true)" style="flex: 1; padding: 10px; border: none; background: transparent; color: #888; border-radius: 6px; cursor: pointer; transition: 0.2s;">📜 Blueprints</button>
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Build options - now a button that opens the visual picker -->
|
<!-- Dynamic Selection Area -->
|
||||||
<div class="form-group" id="build-options">
|
<div id="selection-area" style="display:none; align-items: center; gap: 10px; flex-wrap: wrap;">
|
||||||
<label>Building</label>
|
<button class="btn btn-gold" id="active-selection-display" onclick="window.reopenActiveModal()" style="min-width: 250px; padding: 10px 15px; border-radius: 6px; display: flex; justify-content: space-between; align-items: center; font-weight: bold;">
|
||||||
<button class="btn btn-gold" id="open-building-modal" onclick="window.openBuildingModal()" style="text-align:left; min-width:200px;">
|
<span id="selection-label">-- Επιλέξτε --</span>
|
||||||
<span id="selected-building-label">-- Επιλέξτε Κατασκευή --</span>
|
</button>
|
||||||
</button>
|
|
||||||
|
<div id="recruit-amount-wrap" style="display:none; align-items: center; gap: 5px; background: #16213e; padding: 8px 12px; border-radius: 6px; border: 1px solid #2a4a6a;">
|
||||||
|
<label style="font-size:0.85rem; color:#ccc; margin:0;">Ποσότητα:</label>
|
||||||
|
<input type="number" id="recruit-amount" value="1" min="1" max="9999" style="width: 70px; background: #0f3460; color: #fff; border: 1px solid #c8a44a; border-radius: 4px; padding: 4px 8px;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="btn-send" class="btn btn-gold" onclick="window.sendCommand()" style="margin-left: auto; padding: 10px 20px; font-size: 1rem;">Send ⚡</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Recruit options -->
|
</div>
|
||||||
<div class="form-group" id="recruit-options" style="display:none">
|
|
||||||
<label>Unit</label>
|
|
||||||
<select id="unit-select">
|
|
||||||
<optgroup label="Ξηρά">
|
|
||||||
<option value="sword">Ξιφομάχος</option>
|
|
||||||
<option value="slinger">Σφενδονήτης</option>
|
|
||||||
<option value="archer">Τοξότης</option>
|
|
||||||
<option value="hoplite">Οπλίτης</option>
|
|
||||||
<option value="rider">Ιππέας</option>
|
|
||||||
<option value="chariot">Άρμα</option>
|
|
||||||
<option value="catapult">Καταπέλτης</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="Ναυτικές">
|
|
||||||
<option value="big_transporter">Μεταφορικό Πλοίο</option>
|
|
||||||
<option value="small_transporter">Γρήγορο Μεταφορικό Πλοίο</option>
|
|
||||||
<option value="bireme">Διήρης</option>
|
|
||||||
<option value="attack_ship">Πλοίο Φάρος</option>
|
|
||||||
<option value="trireme">Τριήρης</option>
|
|
||||||
<option value="colonize_ship">Αποικιακό Πλοίο</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="Μυθικές">
|
|
||||||
<option value="medusa">Μέδουσα</option>
|
|
||||||
<option value="zyklop">Κύκλωπας</option>
|
|
||||||
<option value="harpy">Άρπυια</option>
|
|
||||||
<option value="pegasus">Πήγασος</option>
|
|
||||||
<option value="minotaur">Μινώταυρος</option>
|
|
||||||
<option value="manticore">Μαντιχώρας</option>
|
|
||||||
<option value="cerberus">Κέρβερος</option>
|
|
||||||
<option value="hydra">Ύδρα</option>
|
|
||||||
<option value="sea_monster">Τέρας της Θάλασσας</option>
|
|
||||||
</optgroup>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Market options -->
|
<div id="build-queue-preview"></div>
|
||||||
<div class="form-group" id="market-options" style="display:none">
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom right: Build Queue / Command Log (tabbed) -->
|
||||||
|
<div id="log-panel">
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; margin-bottom:12px; border-bottom:1px solid #2a3a5a; padding-bottom:10px;">
|
||||||
|
<h2 style="margin:0; flex:1;">Ουρά Κατασκευών</h2>
|
||||||
|
<button id="tab-queue" class="log-tab-btn tab-active" onclick="window.switchToQueueMode()">🏗️ Ουρά</button>
|
||||||
|
<button id="tab-log" class="log-tab-btn" onclick="window.switchToLogMode()">📋 Ιστορικό</button>
|
||||||
|
</div>
|
||||||
|
<div id="log-content">
|
||||||
|
<p style="color:#555;font-size:0.85rem;padding:12px 0;">← Επιλέξτε πόλη για να δείτε την ουρά.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ====== Blueprints Modal ====== -->
|
||||||
|
<div class="modal-overlay" id="blueprints-modal-overlay" onclick="window.closeBlueprintsModal(event)">
|
||||||
|
<div class="custom-modal" id="blueprints-modal" style="max-width: 400px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>📜 Επιλογή Blueprint</h3>
|
||||||
|
<button class="modal-close" onclick="window.closeBlueprintsModal()">✕</button>
|
||||||
|
</div>
|
||||||
|
<div style="padding: 15px;">
|
||||||
|
<p style="color:#ccc; font-size:0.85rem; margin-bottom:15px;">Επιλέξτε ένα Blueprint για να αναλάβει το Python την αυτόματη κατασκευή της πόλης.</p>
|
||||||
|
|
||||||
|
<div id="blueprint-list" style="display:flex; flex-direction:column; gap:10px; margin-bottom:20px;">
|
||||||
|
<!-- Currently just one blueprint -->
|
||||||
|
<div class="bld-card" id="bp-card-standard" onclick="window.selectBlueprint('Standard Growth')" style="width:100%; justify-content:flex-start; cursor:pointer;">
|
||||||
|
<span class="bld-icon" style="font-size:2rem;">🏙️</span>
|
||||||
|
<div style="display:flex; flex-direction:column; align-items:flex-start;">
|
||||||
|
<span class="bld-name" style="margin-top:0; font-size:1rem; font-weight:bold;">Standard Growth</span>
|
||||||
|
<span style="font-size:0.75rem; color:#888;">Αυτόματη ανάπτυξη κτιρίων & ακαδημίας</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ====== Market Modal ====== -->
|
||||||
|
<div class="modal-overlay" id="market-modal-overlay" onclick="window.closeMarketModal(event)">
|
||||||
|
<div class="custom-modal" id="market-modal" style="max-width: 500px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>🛒 Ρυθμίσεις Παζαριού</h3>
|
||||||
|
<button class="modal-close" onclick="window.closeMarketModal()">✕</button>
|
||||||
|
</div>
|
||||||
|
<div style="padding: 15px;">
|
||||||
<div style="display:flex; gap:10px; margin-bottom:10px;">
|
<div style="display:flex; gap:10px; margin-bottom:10px;">
|
||||||
<div style="flex:1;">
|
<div style="flex:1;">
|
||||||
<label>Προσφορά</label>
|
<label style="color:#ccc; font-size:0.85rem;">Προσφορά</label>
|
||||||
<input type="number" id="market-offer-amount" value="1000" min="1" max="99999" style="width:100%;">
|
<input type="number" id="market-offer-amount" value="1000" min="1" max="99999" style="width:100%; background:#0f3460; color:#fff; border:1px solid #2a4a6a; border-radius:4px; padding:6px;">
|
||||||
</div>
|
</div>
|
||||||
<div style="flex:1;">
|
<div style="flex:1;">
|
||||||
<label>Πόρος Προσφοράς</label>
|
<label style="color:#ccc; font-size:0.85rem;">Πόρος Προσφοράς</label>
|
||||||
<select id="market-offer-type">
|
<select id="market-offer-type" style="width:100%; background:#0f3460; color:#fff; border:1px solid #2a4a6a; border-radius:4px; padding:6px;">
|
||||||
<option value="wood">Ξύλο</option>
|
<option value="wood">Ξύλο</option>
|
||||||
<option value="stone">Πέτρα</option>
|
<option value="stone">Πέτρα</option>
|
||||||
<option value="iron">Ασήμι</option>
|
<option value="iron">Ασήμι</option>
|
||||||
@@ -160,22 +177,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="display:flex; gap:10px; margin-bottom:10px;">
|
<div style="display:flex; gap:10px; margin-bottom:10px;">
|
||||||
<div style="flex:1;">
|
<div style="flex:1;">
|
||||||
<label>Ζήτηση</label>
|
<label style="color:#ccc; font-size:0.85rem;">Ζήτηση</label>
|
||||||
<input type="number" id="market-demand-amount" value="1000" min="1" max="99999" style="width:100%;">
|
<input type="number" id="market-demand-amount" value="1000" min="1" max="99999" style="width:100%; background:#0f3460; color:#fff; border:1px solid #2a4a6a; border-radius:4px; padding:6px;">
|
||||||
</div>
|
</div>
|
||||||
<div style="flex:1;">
|
<div style="flex:1;">
|
||||||
<label>Πόρος Ζήτησης</label>
|
<label style="color:#ccc; font-size:0.85rem;">Πόρος Ζήτησης</label>
|
||||||
<select id="market-demand-type">
|
<select id="market-demand-type" style="width:100%; background:#0f3460; color:#fff; border:1px solid #2a4a6a; border-radius:4px; padding:6px;">
|
||||||
<option value="iron">Ασήμι</option>
|
<option value="iron">Ασήμι</option>
|
||||||
<option value="stone">Πέτρα</option>
|
<option value="stone">Πέτρα</option>
|
||||||
<option value="wood">Ξύλο</option>
|
<option value="wood">Ξύλο</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex; gap:10px;">
|
<div style="display:flex; gap:10px; margin-bottom:20px;">
|
||||||
<div style="flex:1;">
|
<div style="flex:1;">
|
||||||
<label>Χρόνος (Ώρες)</label>
|
<label style="color:#ccc; font-size:0.85rem;">Χρόνος (Ώρες)</label>
|
||||||
<select id="market-max-time">
|
<select id="market-max-time" style="width:100%; background:#0f3460; color:#fff; border:1px solid #2a4a6a; border-radius:4px; padding:6px;">
|
||||||
<option value="1800">0.5</option>
|
<option value="1800">0.5</option>
|
||||||
<option value="3600">1</option>
|
<option value="3600">1</option>
|
||||||
<option value="7200">2</option>
|
<option value="7200">2</option>
|
||||||
@@ -187,46 +204,18 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex:1;">
|
<div style="flex:1;">
|
||||||
<label>Ορατότητα</label>
|
<label style="color:#ccc; font-size:0.85rem;">Ορατότητα</label>
|
||||||
<select id="market-visibility">
|
<select id="market-visibility" style="width:100%; background:#0f3460; color:#fff; border:1px solid #2a4a6a; border-radius:4px; padding:6px;">
|
||||||
|
<option value="allies">Συμμαχία Μόνο</option>
|
||||||
<option value="all">Όλοι</option>
|
<option value="all">Όλοι</option>
|
||||||
<option value="alliance">Συμμαχία</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<button class="btn btn-gold" onclick="window.saveMarketModal()" style="width:100%; padding:10px; font-weight:bold;">✅ Αποθήκευση Προσφοράς</button>
|
||||||
|
|
||||||
<div class="form-group" id="amount-group" style="display:none">
|
|
||||||
<label>Amount</label>
|
|
||||||
<input type="number" id="recruit-amount" value="1" min="1" max="9999" style="width:80px;">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="btn btn-gold" onclick="sendCommand()">Send ⚡</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="build-queue-preview"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bottom right: Command log -->
|
|
||||||
<div id="log-panel">
|
|
||||||
<h2>Command Log</h2>
|
|
||||||
<div id="log-content">
|
|
||||||
<p id="empty-log">No commands sent yet.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
window.PLAYER_ID = "{{ player_id }}";
|
|
||||||
</script>
|
|
||||||
<script src="/static/js/state.js"></script>
|
|
||||||
<script src="/static/js/components/townViewer.js"></script>
|
|
||||||
<script src="/static/js/components/commandForm.js"></script>
|
|
||||||
<script src="/static/js/components/commandLog.js"></script>
|
|
||||||
<script src="/static/js/api.js"></script>
|
|
||||||
<script src="/static/js/app.js"></script>
|
|
||||||
<!-- ====== Building Picker Modal ====== -->
|
<!-- ====== Building Picker Modal ====== -->
|
||||||
<div id="building-modal-overlay" onclick="window.closeBuildingModal(event)">
|
<div id="building-modal-overlay" onclick="window.closeBuildingModal(event)">
|
||||||
<div id="building-modal">
|
<div id="building-modal">
|
||||||
@@ -241,5 +230,101 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- ====== Unit Picker Modal ====== -->
|
||||||
|
<div id="unit-modal-overlay" class="modal-overlay" onclick="window.closeUnitModal(event)">
|
||||||
|
<div id="unit-modal" class="custom-modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>⚔️ Επιλογή Μονάδας</h3>
|
||||||
|
<button id="unit-modal-close" onclick="window.closeUnitModal()">✕</button>
|
||||||
|
</div>
|
||||||
|
<div id="unit-grid" style="display:flex; flex-wrap:wrap; gap:10px; max-height:70vh; overflow-y:auto; padding:10px 5px;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ====== Academy Picker Modal ====== -->
|
||||||
|
<div id="academy-modal-overlay" onclick="window.closeAcademyModal(event)">
|
||||||
|
<div id="academy-modal">
|
||||||
|
<div id="academy-modal-header">
|
||||||
|
<h3>🦉 Ακαδημία</h3>
|
||||||
|
<button id="academy-modal-close" onclick="window.closeAcademyModal()">✕</button>
|
||||||
|
</div>
|
||||||
|
<div id="academy-grid"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Tab buttons for queue / log toggle */
|
||||||
|
.log-tab-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #2a3a5a;
|
||||||
|
color: #666;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
.log-tab-btn:hover { border-color: #4a7aaa; color: #aaa; }
|
||||||
|
.log-tab-btn.tab-active { border-color: #c8a44a; color: #c8a44a; background: rgba(200,164,74,0.1); }
|
||||||
|
|
||||||
|
/* Draggable build queue row */
|
||||||
|
.bq-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #1a2a3a;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
background: #0d1e30;
|
||||||
|
cursor: default;
|
||||||
|
transition: background 0.15s, border-color 0.15s;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.bq-row:hover { background: #112038; border-color: #2a4a6a; }
|
||||||
|
.bq-row.bq-drag-over { border-color: #c8a44a; background: rgba(200,164,74,0.08); }
|
||||||
|
.bq-handle {
|
||||||
|
cursor: grab;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #3a5a7a;
|
||||||
|
line-height: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
.bq-handle:hover { color: #c8a44a; }
|
||||||
|
.bq-pos {
|
||||||
|
width: 18px;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: #3a5a7a;
|
||||||
|
font-weight: 700;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.bq-icon { font-size: 1.1rem; flex-shrink: 0; }
|
||||||
|
.bq-name { flex: 1; font-size: 0.88rem; color: #d0d0d0; }
|
||||||
|
.bq-cancel-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #3a2a2a;
|
||||||
|
color: #884444;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.bq-cancel-btn:hover { background: rgba(200,80,80,0.15); border-color: #cc4444; color: #ff6666; }
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
window.PLAYER_ID = "{{ player_id }}";
|
||||||
|
window.WORLD_ID = "{{ world_id }}";
|
||||||
|
</script>
|
||||||
|
<script src="/static/js/state.js?v=6"></script>
|
||||||
|
<script src="/static/js/components/townViewer.js?v=6"></script>
|
||||||
|
<script src="/static/js/components/commandForm.js?v=6"></script>
|
||||||
|
<script src="/static/js/components/commandLog.js?v=6"></script>
|
||||||
|
<script src="/static/js/api.js?v=6"></script>
|
||||||
|
<script src="/static/js/app.js?v=6"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -233,7 +233,7 @@
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="topbar">
|
<div class="topbar">
|
||||||
<a href="/player/{{ player_id }}">← Πίσω</a>
|
<a href="/player/{{ player_id }}/{{ world_id }}">← Πίσω</a>
|
||||||
<h1>🌾 Farm Manager</h1>
|
<h1>🌾 Farm Manager</h1>
|
||||||
<span class="online-dot" id="online-dot" title="Κατάσταση Script"></span>
|
<span class="online-dot" id="online-dot" title="Κατάσταση Script"></span>
|
||||||
<button class="sync-btn" onclick="requestSync()">Live Sync</button>
|
<button class="sync-btn" onclick="requestSync()">Live Sync</button>
|
||||||
@@ -242,6 +242,15 @@
|
|||||||
<!-- Status banner -->
|
<!-- Status banner -->
|
||||||
<div class="status-bar" id="status-bar"></div>
|
<div class="status-bar" id="status-bar"></div>
|
||||||
|
|
||||||
|
<!-- Warehouse-full notice (hidden by default) -->
|
||||||
|
<div id="warehouse-full-banner" style="display:none; background: linear-gradient(90deg, #5a1a00, #8b2500); border: 1px solid #ff6600; border-radius: 8px; padding: 12px 18px; margin-bottom: 1rem; align-items: center; gap: 12px; font-weight: 600;">
|
||||||
|
<span style="font-size: 1.4rem;">📦</span>
|
||||||
|
<span>
|
||||||
|
<strong style="color:#ff9933;">Αποθήκη Γεμάτη!</strong>
|
||||||
|
Όλες οι αποθήκες είναι >95% — το bot παρακάμπτει τη λεηλασία μέχρι να αδειάσει χώρος.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Control Panel -->
|
<!-- Control Panel -->
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>⚙️ Ρυθμίσεις</h2>
|
<h2>⚙️ Ρυθμίσεις</h2>
|
||||||
@@ -255,15 +264,6 @@
|
|||||||
<span style="color:#888; font-size:0.85rem;" id="toggle-hint">Ανενεργό</span>
|
<span style="color:#888; font-size:0.85rem;" id="toggle-hint">Ανενεργό</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toggle-row">
|
|
||||||
<span class="toggle-label">Στρατόπεδο Ληστών (Auto)</span>
|
|
||||||
<label class="toggle">
|
|
||||||
<input type="checkbox" id="bandit-camp-enabled">
|
|
||||||
<span class="slider"></span>
|
|
||||||
</label>
|
|
||||||
<span style="color:#888; font-size:0.85rem;" id="bandit-toggle-hint">Ανενεργό</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-bottom: 0.75rem; font-size: 0.85rem; color: #888;">Επίπεδο Λεηλασίας:</div>
|
<div style="margin-bottom: 0.75rem; font-size: 0.85rem; color: #888;">Επίπεδο Λεηλασίας:</div>
|
||||||
<div class="option-grid">
|
<div class="option-grid">
|
||||||
<button class="option-btn selected" data-option="1">
|
<button class="option-btn selected" data-option="1">
|
||||||
@@ -284,8 +284,11 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="save-btn" id="save-btn" onclick="saveSettings()">💾 Αποθήκευση</button>
|
<div style="display: flex; gap: 1rem; align-items: center;">
|
||||||
<span class="save-status" id="save-status">✓ Αποθηκεύτηκε</span>
|
<button class="save-btn" id="save-btn" onclick="saveSettings()">💾 Αποθήκευση</button>
|
||||||
|
<button class="save-btn" id="manual-loot-btn" onclick="triggerManualLoot()" style="background: linear-gradient(135deg, #2a5a7a, #4a9ccc);">🌾 Λεηλασία Τώρα</button>
|
||||||
|
<span class="save-status" id="save-status">✓ Στάλθηκε</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Farm Upgrade Panel -->
|
<!-- Farm Upgrade Panel -->
|
||||||
@@ -310,6 +313,80 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Bandit Camp Panel -->
|
||||||
|
<div class="panel">
|
||||||
|
<h2>🏕️ Αυτόματο Bandit Camp</h2>
|
||||||
|
<p style="font-size:0.85rem;color:#888;margin-bottom:1rem;">
|
||||||
|
Το bot επιτίθεται αυτόματα στο στρατόπεδο ληστών και διεκδικεί αμοιβές.<br>
|
||||||
|
Ελέγχει κάθε <strong>12–22 λεπτά</strong> (τυχαίο) — ανθρώπινος ρυθμός.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="toggle-row" style="margin-bottom:1rem;">
|
||||||
|
<span class="toggle-label">Αυτόματη Επίθεση</span>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" id="bootcamp-enabled">
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
<span style="color:#888;font-size:0.85rem;" id="bootcamp-hint">Ανενεργό</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toggle-row" style="margin-bottom:1.5rem;">
|
||||||
|
<span class="toggle-label">Συμπ. Αμυντικές Μονάδες (Σπαθ/Τοξ)</span>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" id="bootcamp-use-def">
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="save-btn" onclick="saveBotSettings()">💾 Αποθήκευση</button>
|
||||||
|
<span class="save-status" id="bot-save-status">✓ Αποθηκεύτηκε</span>
|
||||||
|
|
||||||
|
<div style="margin-top: 1rem; border-top: 1px solid #1a3040; padding-top: 1rem;">
|
||||||
|
<button class="save-btn" id="bootcamp-attack-btn" onclick="attackBootcampNow()" style="background: linear-gradient(135deg, #7a2a2a, #cc4a4a); width: 100%;">⚔️ Επίθεση Τώρα</button>
|
||||||
|
<div style="text-align: center; margin-top: 5px;"><span class="save-status" id="bootcamp-attack-status">Εντολή εστάλη!</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="margin-top:1.5rem;font-size:0.9rem;color:#aaa;">📋 Ιστορικό</h3>
|
||||||
|
<div id="bootcamp-log" style="background:#0a1520;border:1px solid #1a3040;border-radius:8px;padding:10px;max-height:180px;overflow-y:auto;font-size:0.78rem;font-family:monospace;color:#8ab4d0;">
|
||||||
|
<span style="color:#444;">Αναμονή δεδομένων...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rural Trade Panel -->
|
||||||
|
<div class="panel">
|
||||||
|
<h2>🔄 Αυτόματο Trade Χωριών</h2>
|
||||||
|
<p style="font-size:0.85rem;color:#888;margin-bottom:1rem;">
|
||||||
|
Ενεργοποιείται <strong>μόνο όταν μια κατασκευή κολλάει λόγω πόρων</strong>.<br>
|
||||||
|
Ψάχνει χωριά στο νησί που προσφέρουν τον ελλείποντα πόρο και κάνει trade.<br>
|
||||||
|
Ελέγχει κάθε <strong>25–45 λεπτά</strong> (τυχαίο).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="toggle-row" style="margin-bottom:1rem;">
|
||||||
|
<span class="toggle-label">Αυτόματο Trade</span>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" id="rural-trade-enabled">
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
<span style="color:#888;font-size:0.85rem;" id="rural-trade-hint">Ανενεργό</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom:0.75rem;font-size:0.85rem;color:#888;">Ελάχιστο Ratio Trade (τιμή χωριού):</div>
|
||||||
|
<div class="option-grid" style="margin-bottom:1.5rem;">
|
||||||
|
<button class="option-btn" data-ratio="1" onclick="selectRatio(1)"><span class="opt-time">0.25</span><span class="opt-name">Ελάχ.</span></button>
|
||||||
|
<button class="option-btn" data-ratio="2" onclick="selectRatio(2)"><span class="opt-time">0.50</span><span class="opt-name">Χαμηλό</span></button>
|
||||||
|
<button class="option-btn selected" data-ratio="3" onclick="selectRatio(3)"><span class="opt-time">0.75</span><span class="opt-name">Κανον.</span></button>
|
||||||
|
<button class="option-btn" data-ratio="4" onclick="selectRatio(4)"><span class="opt-time">1.00</span><span class="opt-name">Υψηλό</span></button>
|
||||||
|
<button class="option-btn" data-ratio="5" onclick="selectRatio(5)"><span class="opt-time">1.25</span><span class="opt-name">Μέγιστο</span></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="save-btn" onclick="saveBotSettings()">💾 Αποθήκευση</button>
|
||||||
|
|
||||||
|
<h3 style="margin-top:1.5rem;font-size:0.9rem;color:#aaa;">📋 Ιστορικό</h3>
|
||||||
|
<div id="rural-trade-log" style="background:#0a1520;border:1px solid #1a3040;border-radius:8px;padding:10px;max-height:180px;overflow-y:auto;font-size:0.78rem;font-family:monospace;color:#8ab4d0;">
|
||||||
|
<span style="color:#444;">Αναμονή δεδομένων...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Farm Status Table -->
|
<!-- Farm Status Table -->
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>🏘️ Κατάσταση Χωριών</h2>
|
<h2>🏘️ Κατάσταση Χωριών</h2>
|
||||||
@@ -321,17 +398,20 @@
|
|||||||
<th>Έτοιμα</th>
|
<th>Έτοιμα</th>
|
||||||
<th>Σύνολο</th>
|
<th>Σύνολο</th>
|
||||||
<th>Επόμενο</th>
|
<th>Επόμενο</th>
|
||||||
|
<th>Τελευταία Λεηλασία</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="farm-table-body">
|
<tbody id="farm-table-body">
|
||||||
<tr><td colspan="4"><div class="empty-state">⏳ <p>Φόρτωση δεδομένων...</p></div></td></tr>
|
<tr><td colspan="5"><div class="empty-state">⏳ <p>Φόρτωση δεδομένων...</p></div></td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const PLAYER_ID = '{{ player_id }}';
|
const PLAYER_ID = '{{ player_id }}';
|
||||||
|
const WORLD_ID = '{{ world_id }}';
|
||||||
let selectedOption = 1;
|
let selectedOption = 1;
|
||||||
|
|
||||||
// -- Loot option buttons --
|
// -- Loot option buttons --
|
||||||
@@ -346,36 +426,27 @@
|
|||||||
// -- Toggle hint text --
|
// -- Toggle hint text --
|
||||||
document.getElementById('farm-enabled').addEventListener('change', function () {
|
document.getElementById('farm-enabled').addEventListener('change', function () {
|
||||||
document.getElementById('toggle-hint').textContent = this.checked ? '🟢 Ενεργό' : 'Ανενεργό';
|
document.getElementById('toggle-hint').textContent = this.checked ? '🟢 Ενεργό' : 'Ανενεργό';
|
||||||
updateStatusBar(this.checked, document.getElementById('bandit-camp-enabled').checked);
|
updateStatusBar(this.checked);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('bandit-camp-enabled').addEventListener('change', function () {
|
function updateStatusBar(enabled) {
|
||||||
document.getElementById('bandit-toggle-hint').textContent = this.checked ? '🟢 Ενεργό' : 'Ανενεργό';
|
|
||||||
updateStatusBar(document.getElementById('farm-enabled').checked, this.checked);
|
|
||||||
});
|
|
||||||
|
|
||||||
function updateStatusBar(farmEnabled, banditEnabled) {
|
|
||||||
const bar = document.getElementById('status-bar');
|
const bar = document.getElementById('status-bar');
|
||||||
if (farmEnabled || banditEnabled) {
|
if (enabled) {
|
||||||
bar.className = 'status-bar visible';
|
bar.className = 'status-bar visible';
|
||||||
let msg = [];
|
bar.textContent = '🤖 Ο αυτόματος farmer είναι ενεργός. Το script θα λεηλατεί χωριά με τυχαίες καθυστερήσεις.';
|
||||||
if (farmEnabled) msg.push('Ο αυτόματος farmer είναι ενεργός.');
|
|
||||||
if (banditEnabled) msg.push('Το στρατόπεδο ληστών είναι ενεργό.');
|
|
||||||
bar.textContent = '🤖 ' + msg.join(' ') + ' Το script θα εκτελεί δράσεις με τυχαίες καθυστερήσεις.';
|
|
||||||
} else {
|
} else {
|
||||||
bar.className = 'status-bar visible off';
|
bar.className = 'status-bar visible off';
|
||||||
bar.textContent = '⏸ Οι αυτόματες ενέργειες είναι ανενεργές.';
|
bar.textContent = '⏸ Η αυτόματη λεηλασία είναι ανενεργή.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Save settings --
|
// -- Save settings --
|
||||||
function saveSettings() {
|
function saveSettings() {
|
||||||
const enabled = document.getElementById('farm-enabled').checked;
|
const enabled = document.getElementById('farm-enabled').checked;
|
||||||
const bandit_camp_enabled = document.getElementById('bandit-camp-enabled').checked;
|
|
||||||
fetch('/dashboard/farm-settings', {
|
fetch('/dashboard/farm-settings', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ player_id: PLAYER_ID, enabled, bandit_camp_enabled, loot_option: selectedOption })
|
body: JSON.stringify({ player_id: PLAYER_ID, world_id: WORLD_ID, enabled, loot_option: selectedOption })
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -387,17 +458,12 @@
|
|||||||
|
|
||||||
// -- Load current settings --
|
// -- Load current settings --
|
||||||
function loadSettings() {
|
function loadSettings() {
|
||||||
fetch(`/dashboard/farm-settings?player_id=${PLAYER_ID}`)
|
fetch(`/dashboard/farm-settings?player_id=${PLAYER_ID}&world_id=${WORLD_ID}`)
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(cfg => {
|
.then(cfg => {
|
||||||
document.getElementById('farm-enabled').checked = cfg.enabled;
|
document.getElementById('farm-enabled').checked = cfg.enabled;
|
||||||
document.getElementById('bandit-camp-enabled').checked = cfg.bandit_camp_enabled || false;
|
|
||||||
|
|
||||||
document.getElementById('toggle-hint').textContent = cfg.enabled ? '🟢 Ενεργό' : 'Ανενεργό';
|
document.getElementById('toggle-hint').textContent = cfg.enabled ? '🟢 Ενεργό' : 'Ανενεργό';
|
||||||
document.getElementById('bandit-toggle-hint').textContent = cfg.bandit_camp_enabled ? '🟢 Ενεργό' : 'Ανενεργό';
|
if (cfg.enabled) updateStatusBar(true);
|
||||||
|
|
||||||
updateStatusBar(cfg.enabled, cfg.bandit_camp_enabled);
|
|
||||||
|
|
||||||
selectedOption = cfg.loot_option || 1;
|
selectedOption = cfg.loot_option || 1;
|
||||||
document.querySelectorAll('.option-btn').forEach(b => {
|
document.querySelectorAll('.option-btn').forEach(b => {
|
||||||
b.classList.toggle('selected', parseInt(b.dataset.option) === selectedOption);
|
b.classList.toggle('selected', parseInt(b.dataset.option) === selectedOption);
|
||||||
@@ -406,17 +472,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -- Load farm data table --
|
// -- Load farm data table --
|
||||||
|
function timeAgo(isoStr) {
|
||||||
|
if (!isoStr) return '—';
|
||||||
|
const diff = Math.floor((Date.now() - new Date(isoStr + (isoStr.endsWith('Z') ? '' : 'Z'))) / 1000);
|
||||||
|
if (diff < 60) return `${diff}δ πριν`;
|
||||||
|
if (diff < 3600) return `${Math.floor(diff / 60)}λ πριν`;
|
||||||
|
return `${Math.floor(diff / 3600)}ω πριν`;
|
||||||
|
}
|
||||||
|
|
||||||
function loadFarmData() {
|
function loadFarmData() {
|
||||||
fetch(`/dashboard/farm-data?player_id=${PLAYER_ID}`)
|
fetch(`/dashboard/farm-data?player_id=${PLAYER_ID}&world_id=${WORLD_ID}`)
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(resp => {
|
||||||
|
const data = resp.towns || [];
|
||||||
|
const lastFarmed = resp.last_farmed_at ? timeAgo(resp.last_farmed_at) : '—';
|
||||||
const tbody = document.getElementById('farm-table-body');
|
const tbody = document.getElementById('farm-table-body');
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
tbody.innerHTML = '<tr><td colspan="4"><div class="empty-state">🌱 <p>Δεν υπάρχουν δεδομένα χωριών ακόμη.<br>Βεβαιώσου ότι το script v3.3+ τρέχει στο παιχνίδι.</p></div></td></tr>';
|
tbody.innerHTML = '<tr><td colspan="5"><div class="empty-state">🌱 <p>Δεν υπάρχουν δεδομένα χωριών ακόμη.<br>Βεβαιώσου ότι το script v3.3+ τρέχει στο παιχνίδι.</p></div></td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
tbody.innerHTML = data.map(t => {
|
tbody.innerHTML = data.map((t, idx) => {
|
||||||
const readyBadge = t.ready_farms > 0
|
const readyBadge = t.ready_farms > 0
|
||||||
? `<span class="badge ready">✓ ${t.ready_farms} Έτοιμα</span>`
|
? `<span class="badge ready">✓ ${t.ready_farms} Έτοιμα</span>`
|
||||||
: `<span class="badge waiting">Αναμονή</span>`;
|
: `<span class="badge waiting">Αναμονή</span>`;
|
||||||
@@ -431,11 +507,16 @@
|
|||||||
nextStr = '<span class="countdown">Τώρα</span>';
|
nextStr = '<span class="countdown">Τώρα</span>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Show last farmed only in first row — same value for all rows
|
||||||
|
const lastFarmedCell = idx === 0
|
||||||
|
? `<td rowspan="${data.length}" style="color:#4acc64;font-size:0.82rem;vertical-align:middle;">${lastFarmed}</td>`
|
||||||
|
: '';
|
||||||
return `<tr>
|
return `<tr>
|
||||||
<td><strong>${t.town_name}</strong></td>
|
<td><strong>${t.town_name}</strong></td>
|
||||||
<td>${readyBadge}</td>
|
<td>${readyBadge}</td>
|
||||||
<td><span style="color:#888">${t.total_farms}</span></td>
|
<td><span style="color:#888">${t.total_farms}</span></td>
|
||||||
<td>${nextStr}</td>
|
<td>${nextStr}</td>
|
||||||
|
${lastFarmedCell}
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
});
|
});
|
||||||
@@ -483,7 +564,7 @@
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
player_id: PLAYER_ID,
|
player_id: PLAYER_ID,
|
||||||
town_id: 0,
|
town_id: WORLD_ID ? `0_${WORLD_ID}` : "0",
|
||||||
type: 'farm_upgrade',
|
type: 'farm_upgrade',
|
||||||
payload: { threshold: threshold, action_type: actionType }
|
payload: { threshold: threshold, action_type: actionType }
|
||||||
})
|
})
|
||||||
@@ -498,12 +579,154 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- Trigger Manual Loot --
|
||||||
|
function triggerManualLoot() {
|
||||||
|
const btn = document.getElementById('manual-loot-btn');
|
||||||
|
const originalText = btn.innerText;
|
||||||
|
btn.innerText = '⏳ Αποστολή...';
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
fetch('/dashboard/commands', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
player_id: PLAYER_ID,
|
||||||
|
town_id: WORLD_ID ? `0_${WORLD_ID}` : "0",
|
||||||
|
type: 'farm_loot',
|
||||||
|
payload: { loot_option: selectedOption }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(() => {
|
||||||
|
btn.innerText = '✓ Εστάλη!';
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.innerText = originalText;
|
||||||
|
btn.disabled = false;
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function attackBootcampNow() {
|
||||||
|
const btn = document.getElementById('bootcamp-attack-btn');
|
||||||
|
const status = document.getElementById('bootcamp-attack-status');
|
||||||
|
const originalText = btn.innerText;
|
||||||
|
|
||||||
|
btn.innerText = '⏳ Αποστολή...';
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
fetch('/dashboard/bootcamp-attack-now', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ player_id: PLAYER_ID, world_id: WORLD_ID })
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(() => {
|
||||||
|
btn.innerText = '✓ Εστάλη!';
|
||||||
|
status.style.opacity = '1';
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.innerText = originalText;
|
||||||
|
btn.disabled = false;
|
||||||
|
status.style.opacity = '0';
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkWarehouseStatus() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/farm_status?player_id=${PLAYER_ID}&world_id=${WORLD_ID}`);
|
||||||
|
const data = await res.json();
|
||||||
|
const banner = document.getElementById('warehouse-full-banner');
|
||||||
|
if (banner) banner.style.display = data.warehouse_full ? 'flex' : 'none';
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Bot Settings (Bootcamp + Rural Trade) ─────────────────────
|
||||||
|
let selectedRatio = 3;
|
||||||
|
|
||||||
|
function selectRatio(n) {
|
||||||
|
selectedRatio = n;
|
||||||
|
document.querySelectorAll('[data-ratio]').forEach(b => {
|
||||||
|
b.classList.toggle('selected', parseInt(b.dataset.ratio) === n);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadBotSettings() {
|
||||||
|
fetch(`/dashboard/bot-settings?player_id=${PLAYER_ID}&world_id=${WORLD_ID}`)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(cfg => {
|
||||||
|
document.getElementById('bootcamp-enabled').checked = !!cfg.bootcamp_enabled;
|
||||||
|
document.getElementById('bootcamp-hint').textContent = cfg.bootcamp_enabled ? '🟢 Ενεργό' : 'Ανενεργό';
|
||||||
|
document.getElementById('bootcamp-use-def').checked = !!cfg.bootcamp_use_def;
|
||||||
|
document.getElementById('rural-trade-enabled').checked = !!cfg.rural_trade_enabled;
|
||||||
|
document.getElementById('rural-trade-hint').textContent = cfg.rural_trade_enabled ? '🟢 Ενεργό' : 'Ανενεργό';
|
||||||
|
selectRatio(cfg.rural_trade_ratio || 3);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveBotSettings() {
|
||||||
|
fetch('/dashboard/bot-settings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
player_id: PLAYER_ID,
|
||||||
|
world_id: WORLD_ID,
|
||||||
|
bootcamp_enabled: document.getElementById('bootcamp-enabled').checked,
|
||||||
|
bootcamp_use_def: document.getElementById('bootcamp-use-def').checked,
|
||||||
|
rural_trade_enabled: document.getElementById('rural-trade-enabled').checked,
|
||||||
|
rural_trade_ratio: selectedRatio
|
||||||
|
})
|
||||||
|
}).then(r => r.json()).then(() => {
|
||||||
|
// Update hints
|
||||||
|
document.getElementById('bootcamp-hint').textContent =
|
||||||
|
document.getElementById('bootcamp-enabled').checked ? '🟢 Ενεργό' : 'Ανενεργό';
|
||||||
|
document.getElementById('rural-trade-hint').textContent =
|
||||||
|
document.getElementById('rural-trade-enabled').checked ? '🟢 Ενεργό' : 'Ανενεργό';
|
||||||
|
const s = document.getElementById('bot-save-status');
|
||||||
|
s.classList.add('visible');
|
||||||
|
setTimeout(() => s.classList.remove('visible'), 2500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wire toggle hints live
|
||||||
|
document.getElementById('bootcamp-enabled').addEventListener('change', function() {
|
||||||
|
document.getElementById('bootcamp-hint').textContent = this.checked ? '🟢 Ενεργό' : 'Ανενεργό';
|
||||||
|
});
|
||||||
|
document.getElementById('rural-trade-enabled').addEventListener('change', function() {
|
||||||
|
document.getElementById('rural-trade-hint').textContent = this.checked ? '🟢 Ενεργό' : 'Ανενεργό';
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Bot Logs ───────────────────────────────────────────────────
|
||||||
|
function renderBotLog(containerId, entries) {
|
||||||
|
const el = document.getElementById(containerId);
|
||||||
|
if (!entries || entries.length === 0) {
|
||||||
|
el.innerHTML = '<span style="color:#444;">Δεν υπάρχουν εγγραφές ακόμη.</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.innerHTML = entries.map(e => {
|
||||||
|
const t = new Date(e.created_at + 'Z').toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
|
||||||
|
return `<div><span style="color:#3a6a8a;">[${t}]</span> ${e.message}</div>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadBotLogs() {
|
||||||
|
fetch(`/dashboard/bot-logs?player_id=${PLAYER_ID}&world_id=${WORLD_ID}&feature=bootcamp`)
|
||||||
|
.then(r => r.json()).then(data => renderBotLog('bootcamp-log', data));
|
||||||
|
fetch(`/dashboard/bot-logs?player_id=${PLAYER_ID}&world_id=${WORLD_ID}&feature=rural_trade`)
|
||||||
|
.then(r => r.json()).then(data => renderBotLog('rural-trade-log', data));
|
||||||
|
}
|
||||||
|
|
||||||
// -- Boot --
|
// -- Boot --
|
||||||
loadSettings();
|
loadSettings();
|
||||||
|
loadBotSettings();
|
||||||
loadFarmData();
|
loadFarmData();
|
||||||
|
loadBotLogs();
|
||||||
checkOnline();
|
checkOnline();
|
||||||
setInterval(loadFarmData, 15000);
|
checkWarehouseStatus();
|
||||||
setInterval(checkOnline, 20000);
|
setInterval(loadFarmData, 15000);
|
||||||
|
setInterval(loadBotLogs, 15000);
|
||||||
|
setInterval(checkOnline, 20000);
|
||||||
|
setInterval(checkWarehouseStatus, 20000);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -154,13 +154,13 @@
|
|||||||
|
|
||||||
<div class="hub-grid">
|
<div class="hub-grid">
|
||||||
|
|
||||||
<a href="/player/{{ player_id }}/admin" class="hub-card admin">
|
<a href="/player/{{ player_id }}/{{ world_id }}/admin" class="hub-card admin">
|
||||||
<span class="card-icon">🏛️</span>
|
<span class="card-icon">🏛️</span>
|
||||||
<div class="card-title">Admin Mode</div>
|
<div class="card-title">Admin Mode</div>
|
||||||
<div class="card-desc">Κτίρια, στρατολόγηση, αγορά, ουρά κατασκευών και πλήρης έλεγχος πόλεων.</div>
|
<div class="card-desc">Κτίρια, στρατολόγηση, αγορά, ουρά κατασκευών και πλήρης έλεγχος πόλεων.</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="/player/{{ player_id }}/farm" class="hub-card farm">
|
<a href="/player/{{ player_id }}/{{ world_id }}/farm" class="hub-card farm">
|
||||||
<span class="card-icon">🌾</span>
|
<span class="card-icon">🌾</span>
|
||||||
<div class="card-title">Farm Manager</div>
|
<div class="card-title">Farm Manager</div>
|
||||||
<div class="card-desc">Αυτόματη συλλογή πόρων από χωριά. Ρυθμίσεις χρόνου λεηλασίας και έλεγχος με ένα κλικ.</div>
|
<div class="card-desc">Αυτόματη συλλογή πόρων από χωριά. Ρυθμίσεις χρόνου λεηλασίας και έλεγχος με ένα κλικ.</div>
|
||||||
@@ -180,7 +180,8 @@
|
|||||||
<script>
|
<script>
|
||||||
// Fetch player name to show in the badge
|
// Fetch player name to show in the badge
|
||||||
const playerId = '{{ player_id }}';
|
const playerId = '{{ player_id }}';
|
||||||
fetch(`/dashboard/towns?player_id=${playerId}`)
|
const worldId = '{{ world_id }}';
|
||||||
|
fetch(`/dashboard/towns?player_id=${playerId}&world_id=${worldId}`)
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(towns => {
|
.then(towns => {
|
||||||
if (towns && towns.length > 0) {
|
if (towns && towns.length > 0) {
|
||||||
|
|||||||
@@ -5,26 +5,64 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Grepolis Remote Dashboard - Select Player</title>
|
<title>Grepolis Remote Dashboard - Select Player</title>
|
||||||
<link rel="stylesheet" href="/static/css/styles.css">
|
<link rel="stylesheet" href="/static/css/styles.css">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
body {
|
body {
|
||||||
|
font-family: 'Inter', 'Segoe UI', sans-serif;
|
||||||
|
background-color: #0d1117;
|
||||||
|
min-height: 100vh;
|
||||||
|
color: #e6edf3;
|
||||||
|
}
|
||||||
|
/* --- Top nav --- */
|
||||||
|
.topbar {
|
||||||
|
background: #161b22;
|
||||||
|
border-bottom: 1px solid #30363d;
|
||||||
|
padding: 14px 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.topbar-logo { font-size: 1.1rem; font-weight: 700; color: #c8a44a; }
|
||||||
|
.topbar-nav { display: flex; gap: 16px; align-items: center; }
|
||||||
|
.topbar-nav a {
|
||||||
|
color: #8b949e; font-size: 0.875rem; text-decoration: none;
|
||||||
|
padding: 6px 12px; border-radius: 6px; transition: background 0.2s, color 0.2s;
|
||||||
|
}
|
||||||
|
.topbar-nav a:hover { background: #21262d; color: #e6edf3; }
|
||||||
|
.topbar-nav .user-badge { color: #c8a44a; font-size: 0.875rem; font-weight: 600; }
|
||||||
|
.btn-logout {
|
||||||
|
background: rgba(248,81,73,0.1); color: #f85149;
|
||||||
|
border: 1px solid rgba(248,81,73,0.3); padding: 6px 14px; border-radius: 6px;
|
||||||
|
font-size: 0.875rem; font-family: inherit; text-decoration: none;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.btn-logout:hover { background: rgba(248,81,73,0.2) !important; }
|
||||||
|
|
||||||
|
/* --- Main content --- */
|
||||||
|
.main-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
min-height: 100vh;
|
padding: 60px 20px;
|
||||||
background-color: #1a1a24;
|
min-height: calc(100vh - 57px);
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
}
|
}
|
||||||
.landing-container {
|
.landing-container {
|
||||||
background: #2a2a36;
|
background: #161b22;
|
||||||
|
border: 1px solid #30363d;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
border-radius: 8px;
|
border-radius: 10px;
|
||||||
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
|
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
max-width: 500px;
|
max-width: 520px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.landing-container h1 { color: #c8a44a; margin-bottom: 5px; font-size: 1.5rem; }
|
||||||
|
.landing-container > p { color: #8b949e; margin-bottom: 20px; font-size: 0.9rem; }
|
||||||
|
|
||||||
.player-card {
|
.player-card {
|
||||||
background: #3a3a46;
|
background: #21262d;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -33,66 +71,86 @@
|
|||||||
color: white;
|
color: white;
|
||||||
display: block;
|
display: block;
|
||||||
transition: background 0.2s, transform 0.1s;
|
transition: background 0.2s, transform 0.1s;
|
||||||
font-size: 1.1rem;
|
font-size: 1rem;
|
||||||
border: 1px solid #4a4a56;
|
border: 1px solid #30363d;
|
||||||
}
|
}
|
||||||
.player-card:hover {
|
.player-card:hover { background: #30363d; transform: translateY(-2px); border-color: #c8a44a; }
|
||||||
background: #5a5a66;
|
.player-card span { color: #8b949e; font-size: 0.8rem; margin-left: 8px; }
|
||||||
transform: translateY(-2px);
|
|
||||||
border-color: #c8a44a;
|
.no-clan-box {
|
||||||
|
background: rgba(200,164,74,0.1);
|
||||||
|
border: 1px solid rgba(200,164,74,0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
.player-card span {
|
.no-clan-box p { color: #c8a44a; margin-bottom: 12px; }
|
||||||
color: #888;
|
.btn-create {
|
||||||
font-size: 0.8rem;
|
display: inline-block;
|
||||||
margin-left: 10px;
|
background: #c8a44a; color: #0d1117;
|
||||||
}
|
padding: 9px 20px; border-radius: 6px;
|
||||||
h1 {
|
font-weight: 700; font-size: 0.875rem; text-decoration: none;
|
||||||
color: #c8a44a;
|
transition: background 0.2s;
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
color: #aaa;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
}
|
||||||
|
.btn-create:hover { background: #e0b85a; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="landing-container">
|
|
||||||
<h1>⚔️ Grepolis Remote</h1>
|
|
||||||
<p>Select an active account to manage</p>
|
|
||||||
|
|
||||||
{% if not players %}
|
<div class="topbar">
|
||||||
<p style="color: #ffaa55;">No players found! Install the Tampermonkey script and log into the game first.</p>
|
<span class="topbar-logo">⚔️ Grepolis Remote</span>
|
||||||
{% endif %}
|
<div class="topbar-nav">
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
{% for p in players %}
|
<span class="user-badge">{{ current_user.username }}</span>
|
||||||
<a href="/player/{{ p.player_id }}" class="player-card">
|
<a href="/auth/options">Ρυθμίσεις</a>
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
<a href="/auth/logout" class="btn-logout">Αποσύνδεση</a>
|
||||||
<div>
|
{% endif %}
|
||||||
<strong>{{ p.player }}</strong> <span style="color: #6fcfcf;">[{{ p.world_id }}]</span> <span>(ID: {{ p.player_id }})</span>
|
</div>
|
||||||
</div>
|
|
||||||
<div style="display: flex; gap: 8px;">
|
|
||||||
{% if p.captcha_active %}
|
|
||||||
<span style="display: flex; align-items: center; gap: 6px; background: rgba(255, 100, 100, 0.2); padding: 5px 12px; border-radius: 20px; font-size: 0.8rem; color: #ff6464; font-weight: bold; border: 1px solid rgba(255, 100, 100, 0.5); box-shadow: 0 0 8px rgba(255, 100, 100, 0.4);">
|
|
||||||
⚠️ Captcha
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if p.is_online %}
|
|
||||||
<span style="display: flex; align-items: center; gap: 6px; background: rgba(50, 150, 50, 0.2); padding: 5px 12px; border-radius: 20px; font-size: 0.8rem; color: #7bcc7b; font-weight: bold; border: 1px solid rgba(123, 204, 123, 0.3);">
|
|
||||||
<span style="display: inline-block; width: 8px; height: 8px; background: #7bcc7b; border-radius: 50%; box-shadow: 0 0 6px #7bcc7b;"></span>
|
|
||||||
Online
|
|
||||||
</span>
|
|
||||||
{% else %}
|
|
||||||
<span style="display: flex; align-items: center; gap: 6px; background: rgba(150, 50, 50, 0.2); padding: 5px 12px; border-radius: 20px; font-size: 0.8rem; color: #cc7b7b; font-weight: bold; border: 1px solid rgba(204, 123, 123, 0.3);">
|
|
||||||
<span style="display: inline-block; width: 8px; height: 8px; background: #cc7b7b; border-radius: 50%;"></span>
|
|
||||||
Offline
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="main-content">
|
||||||
|
<div class="landing-container">
|
||||||
|
<h1>⚔️ Grepolis Remote</h1>
|
||||||
|
<p>Select an active account to manage</p>
|
||||||
|
|
||||||
|
{% if no_clan %}
|
||||||
|
<div class="no-clan-box">
|
||||||
|
<p>Δεν έχετε δημιουργήσει clan ακόμη. Πηγαίνετε στις Ρυθμίσεις για να ξεκινήσετε.</p>
|
||||||
|
<a href="/auth/options" class="btn-create">🏰 Δημιουργία Clan →</a>
|
||||||
|
</div>
|
||||||
|
{% elif not players %}
|
||||||
|
<p style="color:#ffaa55;">Κανένας παίκτης δεν έχει συνδεθεί ακόμη. Βεβαιωθείτε ότι το Loader script τρέχει με το σωστό Clan Key.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for p in players %}
|
||||||
|
<a href="/player/{{ p.player_id }}/{{ p.world_id }}" class="player-card">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
|
<div>
|
||||||
|
<strong>{{ p.player }}</strong>
|
||||||
|
<span style="color:#6fcfcf;">[{{ p.world_id }}]</span>
|
||||||
|
<span>(ID: {{ p.player_id }})</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 8px;">
|
||||||
|
{% if p.captcha_active %}
|
||||||
|
<span style="display:flex;align-items:center;gap:6px;background:rgba(255,100,100,0.2);padding:5px 12px;border-radius:20px;font-size:0.8rem;color:#ff6464;font-weight:bold;border:1px solid rgba(255,100,100,0.5);">
|
||||||
|
⚠️ Captcha
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if p.is_online %}
|
||||||
|
<span style="display:flex;align-items:center;gap:6px;background:rgba(50,150,50,0.2);padding:5px 12px;border-radius:20px;font-size:0.8rem;color:#7bcc7b;font-weight:bold;border:1px solid rgba(123,204,123,0.3);">
|
||||||
|
<span style="display:inline-block;width:8px;height:8px;background:#7bcc7b;border-radius:50%;box-shadow:0 0 6px #7bcc7b;"></span>Online
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span style="display:flex;align-items:center;gap:6px;background:rgba(150,50,50,0.2);padding:5px 12px;border-radius:20px;font-size:0.8rem;color:#cc7b7b;font-weight:bold;border:1px solid rgba(204,123,123,0.3);">
|
||||||
|
<span style="display:inline-block;width:8px;height:8px;background:#cc7b7b;border-radius:50%;"></span>Offline
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
115
templates/login.html
Normal file
115
templates/login.html
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="el">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Grepolis Remote — Σύνδεση</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
background: #0d1117;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #e6edf3;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: #161b22;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 40px 36px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
.logo h1 {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #c8a44a;
|
||||||
|
}
|
||||||
|
.logo p { color: #8b949e; font-size: 0.9rem; margin-top: 6px; }
|
||||||
|
.form-group { margin-bottom: 18px; }
|
||||||
|
label { display: block; font-size: 0.85rem; font-weight: 500; color: #8b949e; margin-bottom: 6px; }
|
||||||
|
input[type="text"], input[type="password"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: #0d1117;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #e6edf3;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
input:focus { outline: none; border-color: #c8a44a; }
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 11px;
|
||||||
|
background: #c8a44a;
|
||||||
|
color: #0d1117;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s, transform 0.1s;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.btn:hover { background: #e0b85a; transform: translateY(-1px); }
|
||||||
|
.error {
|
||||||
|
background: rgba(248,81,73,0.12);
|
||||||
|
border: 1px solid rgba(248,81,73,0.4);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
color: #f85149;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.footer-link {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 22px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
.footer-link a { color: #c8a44a; text-decoration: none; }
|
||||||
|
.footer-link a:hover { text-decoration: underline; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<div class="logo">
|
||||||
|
<h1>⚔️ Grepolis Remote</h1>
|
||||||
|
<p>Συνδεθείτε στον λογαριασμό σας</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<div class="error">{{ error }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="POST" action="/auth/login">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Όνομα Χρήστη</label>
|
||||||
|
<input type="text" id="username" name="username" autocomplete="username" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Κωδικός</label>
|
||||||
|
<input type="password" id="password" name="password" autocomplete="current-password" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn">Σύνδεση →</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="footer-link">
|
||||||
|
Δεν έχετε λογαριασμό; <a href="/auth/register">Εγγραφή</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
363
templates/options.html
Normal file
363
templates/options.html
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="el">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Grepolis Remote — Ρυθμίσεις Clan</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: 'Inter', sans-serif; background: #0d1117; color: #e6edf3; min-height: 100vh; }
|
||||||
|
|
||||||
|
/* --- Top nav --- */
|
||||||
|
.topbar {
|
||||||
|
background: #161b22;
|
||||||
|
border-bottom: 1px solid #30363d;
|
||||||
|
padding: 14px 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.topbar-logo { font-size: 1.1rem; font-weight: 700; color: #c8a44a; text-decoration: none; }
|
||||||
|
.topbar-nav { display: flex; gap: 16px; align-items: center; }
|
||||||
|
.topbar-nav a {
|
||||||
|
color: #8b949e; font-size: 0.875rem; text-decoration: none;
|
||||||
|
padding: 6px 12px; border-radius: 6px; transition: background 0.2s, color 0.2s;
|
||||||
|
}
|
||||||
|
.topbar-nav a:hover { background: #21262d; color: #e6edf3; }
|
||||||
|
.topbar-nav a.active { color: #c8a44a; }
|
||||||
|
.topbar-nav .btn-logout {
|
||||||
|
background: rgba(248,81,73,0.1); color: #f85149;
|
||||||
|
border: 1px solid rgba(248,81,73,0.3); padding: 6px 14px; border-radius: 6px;
|
||||||
|
cursor: pointer; font-size: 0.875rem; font-family: inherit; text-decoration: none;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.topbar-nav .btn-logout:hover { background: rgba(248,81,73,0.2); }
|
||||||
|
|
||||||
|
/* --- Page layout --- */
|
||||||
|
.page { max-width: 760px; margin: 40px auto; padding: 0 20px; }
|
||||||
|
.page-title { font-size: 1.5rem; font-weight: 700; margin-bottom: 6px; }
|
||||||
|
.page-subtitle { color: #8b949e; font-size: 0.9rem; margin-bottom: 32px; }
|
||||||
|
|
||||||
|
/* --- Cards --- */
|
||||||
|
.card {
|
||||||
|
background: #161b22;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 24px 28px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.card-title {
|
||||||
|
font-size: 1rem; font-weight: 600;
|
||||||
|
margin-bottom: 18px; padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #30363d;
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Key display --- */
|
||||||
|
.key-box {
|
||||||
|
background: #0d1117;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
gap: 12px; margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.key-value {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #c8a44a;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
}
|
||||||
|
.btn-copy {
|
||||||
|
background: #21262d; color: #e6edf3;
|
||||||
|
border: 1px solid #30363d; border-radius: 6px;
|
||||||
|
padding: 6px 14px; font-size: 0.8rem; cursor: pointer;
|
||||||
|
font-family: inherit; transition: background 0.2s; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.btn-copy:hover { background: #30363d; }
|
||||||
|
|
||||||
|
/* --- Create clan form --- */
|
||||||
|
.inline-form { display: flex; gap: 10px; }
|
||||||
|
.inline-form input[type="text"] {
|
||||||
|
flex: 1; padding: 9px 14px;
|
||||||
|
background: #0d1117; border: 1px solid #30363d; border-radius: 6px;
|
||||||
|
color: #e6edf3; font-size: 0.9rem; font-family: inherit;
|
||||||
|
}
|
||||||
|
.inline-form input:focus { outline: none; border-color: #c8a44a; }
|
||||||
|
|
||||||
|
/* --- Buttons --- */
|
||||||
|
.btn-primary {
|
||||||
|
background: #c8a44a; color: #0d1117;
|
||||||
|
border: none; border-radius: 6px; padding: 9px 18px;
|
||||||
|
font-weight: 700; font-size: 0.875rem; font-family: inherit;
|
||||||
|
cursor: pointer; transition: background 0.2s, transform 0.1s; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.btn-primary:hover { background: #e0b85a; transform: translateY(-1px); }
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: rgba(248,81,73,0.1); color: #f85149;
|
||||||
|
border: 1px solid rgba(248,81,73,0.3); border-radius: 6px;
|
||||||
|
padding: 5px 12px; font-size: 0.8rem; font-family: inherit;
|
||||||
|
cursor: pointer; transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.btn-danger:hover { background: rgba(248,81,73,0.2); }
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
background: rgba(210,153,34,0.1); color: #d99512;
|
||||||
|
border: 1px solid rgba(210,153,34,0.3); border-radius: 6px;
|
||||||
|
padding: 7px 14px; font-size: 0.8rem; font-family: inherit;
|
||||||
|
cursor: pointer; transition: background 0.2s; margin-top: 6px;
|
||||||
|
}
|
||||||
|
.btn-warning:hover { background: rgba(210,153,34,0.2); }
|
||||||
|
|
||||||
|
/* --- Members table --- */
|
||||||
|
.members-table { width: 100%; border-collapse: collapse; }
|
||||||
|
.members-table th {
|
||||||
|
text-align: left; font-size: 0.75rem; color: #8b949e;
|
||||||
|
text-transform: uppercase; letter-spacing: 0.5px;
|
||||||
|
padding: 0 0 10px 0; border-bottom: 1px solid #30363d;
|
||||||
|
}
|
||||||
|
.members-table td {
|
||||||
|
padding: 12px 0; border-bottom: 1px solid #21262d;
|
||||||
|
font-size: 0.875rem; vertical-align: middle;
|
||||||
|
}
|
||||||
|
.members-table tr:last-child td { border-bottom: none; }
|
||||||
|
.player-name { font-weight: 600; }
|
||||||
|
.player-id { color: #8b949e; font-size: 0.78rem; font-family: monospace; }
|
||||||
|
.status-online { color: #3fb950; font-size: 0.78rem; font-weight: 600; }
|
||||||
|
.status-offline { color: #8b949e; font-size: 0.78rem; }
|
||||||
|
.empty-state {
|
||||||
|
text-align: center; padding: 32px 0;
|
||||||
|
color: #8b949e; font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-group { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
||||||
|
.toggle-label {
|
||||||
|
display: flex; align-items: center; gap: 6px;
|
||||||
|
font-size: 0.8rem; color: #8b949e; cursor: pointer;
|
||||||
|
background: #0d1117; border: 1px solid #30363d;
|
||||||
|
padding: 4px 10px; border-radius: 20px;
|
||||||
|
transition: border-color 0.2s, color 0.2s;
|
||||||
|
}
|
||||||
|
.toggle-label:has(input:checked) { border-color: #3fb950; color: #3fb950; }
|
||||||
|
.toggle-label input[type=checkbox] { display: none; }
|
||||||
|
.btn-apply {
|
||||||
|
background: #21262d; color: #8b949e;
|
||||||
|
border: 1px solid #30363d; border-radius: 6px;
|
||||||
|
padding: 4px 10px; font-size: 0.78rem; font-family: inherit;
|
||||||
|
cursor: pointer; transition: background 0.2s, color 0.2s;
|
||||||
|
}
|
||||||
|
.btn-apply:hover { background: #30363d; color: #e6edf3; }
|
||||||
|
|
||||||
|
.warn-box {
|
||||||
|
background: rgba(210,153,34,0.1);
|
||||||
|
border: 1px solid rgba(210,153,34,0.3);
|
||||||
|
border-radius: 6px; padding: 12px 16px;
|
||||||
|
color: #d99512; font-size: 0.85rem; margin-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="topbar">
|
||||||
|
<a class="topbar-logo" href="/">⚔️ Grepolis Remote</a>
|
||||||
|
<div class="topbar-nav">
|
||||||
|
<a href="/">Clients</a>
|
||||||
|
<a href="/auth/options" class="active">Ρυθμίσεις</a>
|
||||||
|
<a href="/auth/logout" class="btn-logout">Αποσύνδεση</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
<div class="page-title">Ρυθμίσεις Clan</div>
|
||||||
|
<div class="page-subtitle">Διαχείριση της ομάδας σας και του κλειδιού πρόσβασης</div>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="warn-box" style="margin-bottom: 20px; color: {% if category == 'error' %}#f85149{% else %}#3fb950{% endif %}; border-color: {% if category == 'error' %}rgba(248,81,73,0.3){% else %}rgba(63,185,80,0.3){% endif %}; background: {% if category == 'error' %}rgba(248,81,73,0.1){% else %}rgba(63,185,80,0.1){% endif %};">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% if clan %}
|
||||||
|
{% if clan.owner_id == current_user.id %}
|
||||||
|
<!-- ===================== Clan Key Section ===================== -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">🔑 Clan Key</div>
|
||||||
|
<p style="color:#8b949e; font-size:0.875rem; margin-bottom:14px;">
|
||||||
|
Μοιραστείτε αυτό το κλειδί με τους παίκτες σας. Πρέπει να το προσθέσουν στο Loader script τους για να συνδεθούν στην ομάδα σας.
|
||||||
|
</p>
|
||||||
|
<div class="key-box">
|
||||||
|
<span class="key-value" id="clanKeyDisplay">{{ clan.clan_key }}</span>
|
||||||
|
<button class="btn-copy" onclick="copyKey()">📋 Αντιγραφή</button>
|
||||||
|
</div>
|
||||||
|
<div class="warn-box">
|
||||||
|
⚠️ Εάν αναγεννήσετε το κλειδί, οι παίκτες σας θα πρέπει να ενημερώσουν το script τους με το νέο κλειδί.
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="/auth/clan/regenerate-key"
|
||||||
|
onsubmit="return confirm('Σίγουρα; Οι παίκτες σου θα πρέπει να ανανεώσουν το κλειδί τους.');">
|
||||||
|
<button type="submit" class="btn-warning">🔄 Αναγέννηση Κλειδιού</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===================== Website Admins Section ===================== -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">👨💻 Website Admins</div>
|
||||||
|
<p style="color:#8b949e; font-size:0.875rem; margin-bottom:14px;">
|
||||||
|
Προσθέστε το username άλλων παικτών που έχουν ήδη εγγραφεί στο site για να τους δώσετε πρόσβαση στο dashboard σας.
|
||||||
|
</p>
|
||||||
|
<form method="POST" action="/auth/clan/add-admin" class="inline-form" style="margin-bottom: 20px;">
|
||||||
|
<input type="text" name="admin_username" placeholder="π.χ. player123" required>
|
||||||
|
<button type="submit" class="btn-primary">Προσθήκη Admin</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if admins %}
|
||||||
|
<table class="members-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Admin Username</th>
|
||||||
|
<th>Ημερομηνία Προσθήκης</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for a in admins %}
|
||||||
|
<tr>
|
||||||
|
<td><div class="player-name">{{ a.username }}</div></td>
|
||||||
|
<td style="color:#8b949e; font-size:0.8rem;">{{ a.created_at[:10] }}</td>
|
||||||
|
<td style="text-align:right;">
|
||||||
|
<form method="POST" action="/auth/clan/remove-admin/{{ a.id }}" onsubmit="return confirm('Αφαίρεση του admin {{ a.username }}?');">
|
||||||
|
<button type="submit" class="btn-danger">Αφαίρεση</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="empty-state" style="padding: 16px 0;">
|
||||||
|
Δεν έχετε προσθέσει κανέναν website admin ακόμη.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ===================== Members Section ===================== -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">👥 Μέλη Clan — {{ clan.name }}</div>
|
||||||
|
{% if members %}
|
||||||
|
<table class="members-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Παίκτης</th>
|
||||||
|
<th>Κατάσταση</th>
|
||||||
|
<th>Δυνατότητες</th>
|
||||||
|
<th>Προστέθηκε</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for m in members %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="player-name">{{ m.player_name }}</div>
|
||||||
|
<div class="player-id">ID: {{ m.player_id }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if m.is_online %}
|
||||||
|
<span class="status-online">● Online</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="status-offline">● Offline</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if clan.owner_id == current_user.id %}
|
||||||
|
<form method="POST" action="/auth/clan/update-features/{{ m.player_id }}" style="display:inline;">
|
||||||
|
<div class="toggle-group">
|
||||||
|
<label class="toggle-label">
|
||||||
|
<input type="checkbox" name="farm" onchange="this.form.submit()" {{ 'checked' if m.feat_farm }}> 🌾 Farm
|
||||||
|
</label>
|
||||||
|
<label class="toggle-label">
|
||||||
|
<input type="checkbox" name="admin" onchange="this.form.submit()" {{ 'checked' if m.feat_admin }}> 🏛 Admin
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<div class="toggle-group" style="opacity: 0.8;">
|
||||||
|
<span class="toggle-label" style="cursor:default; border-color: {{ '#3fb950' if m.feat_farm else '#30363d' }}; color: {{ '#3fb950' if m.feat_farm else '#8b949e' }};">🌾 Farm</span>
|
||||||
|
<span class="toggle-label" style="cursor:default; border-color: {{ '#3fb950' if m.feat_admin else '#30363d' }}; color: {{ '#3fb950' if m.feat_admin else '#8b949e' }};">🏛 Admin</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td style="color:#8b949e; font-size:0.8rem;">{{ m.joined_at }}</td>
|
||||||
|
<td style="text-align:right;">
|
||||||
|
{% if clan.owner_id == current_user.id %}
|
||||||
|
<form method="POST" action="/auth/clan/remove-member/{{ m.player_id }}"
|
||||||
|
onsubmit="return confirm('Αφαίρεση παίκτη {{ m.player_name }}?');">
|
||||||
|
<button type="submit" class="btn-danger">Αφαίρεση</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="empty-state">
|
||||||
|
Δεν υπάρχουν μέλη ακόμη.<br>
|
||||||
|
<span style="font-size:0.8rem; margin-top:6px; display:block;">
|
||||||
|
Μοιραστείτε το Clan Key με τους παίκτες σας για να συνδεθούν.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if clan.owner_id != current_user.id %}
|
||||||
|
<!-- ===================== Leave Clan Section ===================== -->
|
||||||
|
<div class="card" style="border-color: rgba(248,81,73,0.3);">
|
||||||
|
<div class="card-title" style="color: #f85149; border-bottom-color: rgba(248,81,73,0.3);">🚪 Αποχώρηση από Clan</div>
|
||||||
|
<p style="color:#8b949e; font-size:0.875rem; margin-bottom:18px;">
|
||||||
|
Εάν αποχωρήσετε, δεν θα έχετε πλέον πρόσβαση στους παίκτες αυτής της ομάδας.
|
||||||
|
</p>
|
||||||
|
<form method="POST" action="/auth/clan/leave" onsubmit="return confirm('Είστε βέβαιοι ότι θέλετε να αποχωρήσετε από το Clan;');">
|
||||||
|
<button type="submit" class="btn-danger">Αποχώρηση</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<!-- ===================== Create Clan Section ===================== -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">🏰 Δημιουργία Clan</div>
|
||||||
|
<p style="color:#8b949e; font-size:0.875rem; margin-bottom:18px;">
|
||||||
|
Δεν έχετε δημιουργήσει clan ακόμη. Δώστε ένα όνομα για να ξεκινήσετε.
|
||||||
|
</p>
|
||||||
|
<form method="POST" action="/auth/clan/create" class="inline-form">
|
||||||
|
<input type="text" name="clan_name" placeholder="π.χ. Alpha Squad" required>
|
||||||
|
<button type="submit" class="btn-primary">Δημιουργία →</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function copyKey() {
|
||||||
|
const key = document.getElementById('clanKeyDisplay').textContent;
|
||||||
|
navigator.clipboard.writeText(key).then(() => {
|
||||||
|
const btn = document.querySelector('.btn-copy');
|
||||||
|
btn.textContent = '✅ Αντιγράφηκε!';
|
||||||
|
setTimeout(() => btn.textContent = '📋 Αντιγραφή', 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
112
templates/register.html
Normal file
112
templates/register.html
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="el">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Grepolis Remote — Εγγραφή</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
background: #0d1117;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #e6edf3;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: #161b22;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 40px 36px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
.logo { text-align: center; margin-bottom: 28px; }
|
||||||
|
.logo h1 { font-size: 1.6rem; font-weight: 700; color: #c8a44a; }
|
||||||
|
.logo p { color: #8b949e; font-size: 0.9rem; margin-top: 6px; }
|
||||||
|
.form-group { margin-bottom: 18px; }
|
||||||
|
label { display: block; font-size: 0.85rem; font-weight: 500; color: #8b949e; margin-bottom: 6px; }
|
||||||
|
input[type="text"], input[type="password"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: #0d1117;
|
||||||
|
border: 1px solid #30363d;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #e6edf3;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
input:focus { outline: none; border-color: #c8a44a; }
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 11px;
|
||||||
|
background: #c8a44a;
|
||||||
|
color: #0d1117;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s, transform 0.1s;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.btn:hover { background: #e0b85a; transform: translateY(-1px); }
|
||||||
|
.error {
|
||||||
|
background: rgba(248,81,73,0.12);
|
||||||
|
border: 1px solid rgba(248,81,73,0.4);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
color: #f85149;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.footer-link {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 22px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
.footer-link a { color: #c8a44a; text-decoration: none; }
|
||||||
|
.footer-link a:hover { text-decoration: underline; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<div class="logo">
|
||||||
|
<h1>⚔️ Grepolis Remote</h1>
|
||||||
|
<p>Δημιουργία νέου λογαριασμού</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<div class="error">{{ error }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="POST" action="/auth/register">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Όνομα Χρήστη</label>
|
||||||
|
<input type="text" id="username" name="username" autocomplete="username" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Κωδικός</label>
|
||||||
|
<input type="password" id="password" name="password" autocomplete="new-password" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="confirm">Επαλήθευση Κωδικού</label>
|
||||||
|
<input type="password" id="confirm" name="confirm" autocomplete="new-password" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn">Εγγραφή →</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="footer-link">
|
||||||
|
Έχετε ήδη λογαριασμό; <a href="/auth/login">Σύνδεση</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user