diff --git a/bot_modules/04_execute.js b/bot_modules/04a_execute_farm.js similarity index 61% rename from bot_modules/04_execute.js rename to bot_modules/04a_execute_farm.js index 41a3ead..ad3aa10 100644 --- a/bot_modules/04_execute.js +++ b/bot_modules/04a_execute_farm.js @@ -1,6 +1,6 @@ // ================================================================ -// 04_execute.js — All command executors -// Depends on: uw, BASE_URL, log, sleep, randInt, paused, pushState +// 04a_execute_farm.js — Farm command executors +// Depends on: uw, log, sleep, randInt, paused, pushState // ================================================================ // ---------------------------------------------------------------- @@ -129,8 +129,8 @@ async function executeFarmLoot(cmd) { const island_id = islandList[i]; const townIds = islandTownsMap[island_id]; - let selected_town_id = null; - let lowest_total_res = Infinity; + let selected_town_id = null; + let lowest_total_res = Infinity; for (const t_id of townIds) { const t = uw.ITowns?.towns?.[t_id]; @@ -152,8 +152,8 @@ async function executeFarmLoot(cmd) { const total_res = w + s + ir; if (total_res < lowest_total_res) { - lowest_total_res = total_res; - selected_town_id = t_id; + lowest_total_res = total_res; + selected_town_id = t_id; } } @@ -219,138 +219,3 @@ async function executeFarmLoot(cmd) { return { ok: true, msg: `Farm done: ${claimed} claimed, ${skipped} islands skipped, ${errors} errors` }; } - -// ---------------------------------------------------------------- -// 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` }; -} diff --git a/bot_modules/04b_execute_admin.js b/bot_modules/04b_execute_admin.js new file mode 100644 index 0000000..a15ee57 --- /dev/null +++ b/bot_modules/04b_execute_admin.js @@ -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` }; +} diff --git a/bot_modules/05_main.js b/bot_modules/05_main.js index d18b3cd..7b9c313 100644 --- a/bot_modules/05_main.js +++ b/bot_modules/05_main.js @@ -17,12 +17,17 @@ async function pollAndExecute() { return; } - const buildCmd = cmdData.build; - const recruitCmd = cmdData.recruit; - const marketCmd = cmdData.market; - const researchCmd = cmdData.research; - const farmCmd = cmdData.farm; - const farmUpgradeCmd = cmdData.farm_upgrade; + // 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'); + + const buildCmd = adminOn ? cmdData.build : null; + 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'); @@ -63,9 +68,9 @@ async function pollAndExecute() { await execute(farmCmd); await execute(farmUpgradeCmd); - // Auto-farm: if enabled, claim all ready farms (no explicit command needed) + // Auto-farm: only if farm feature is enabled const farmSettings = cmdData.farm_settings || {}; - if (farmSettings.enabled && !farmCmd) { + if (farmOn && farmSettings.enabled && !farmCmd) { const nowTs = Math.floor(Date.now() / 1000); let readyFarms = []; try { diff --git a/db.py b/db.py index 4bfcbea..697c4a0 100644 --- a/db.py +++ b/db.py @@ -76,6 +76,7 @@ def init_db(): 'ALTER TABLE town_state ADD COLUMN sea INTEGER', 'ALTER TABLE commands ADD COLUMN player_id TEXT', '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'", ]: try: c.execute(_col) diff --git a/routes/api.py b/routes/api.py index 8a595a3..ac72e0d 100644 --- a/routes/api.py +++ b/routes/api.py @@ -134,37 +134,48 @@ def get_pending_command(): conn = get_db() c = conn.cursor() - build_cmd = _fetch_pending_of_type(c, 'build', player_id) - recruit_cmd = _fetch_pending_of_type(c, 'recruit', player_id) - market_cmd = _fetch_pending_of_type(c, 'market_offer', player_id) - farm_cmd = _fetch_pending_of_type(c, 'farm_loot', player_id) + build_cmd = _fetch_pending_of_type(c, 'build', player_id) + recruit_cmd = _fetch_pending_of_type(c, 'recruit', player_id) + market_cmd = _fetch_pending_of_type(c, 'market_offer', player_id) + farm_cmd = _fetch_pending_of_type(c, 'farm_loot', player_id) farm_upgrade_cmd = _fetch_pending_of_type(c, 'farm_upgrade', player_id) - research_cmd = _fetch_pending_of_type(c, 'research', player_id) - sync_req = _check_and_reset_sync(c, player_id) + research_cmd = _fetch_pending_of_type(c, 'research', player_id) + sync_req = _check_and_reset_sync(c, player_id) - # Also return current farm settings so TM knows loot_option + # Farm settings farm_row = c.execute( 'SELECT enabled, loot_option FROM farm_settings WHERE player_id = ?', (player_id,) ).fetchone() farm_settings = { - 'enabled': bool(farm_row['enabled']) if farm_row else False, - 'loot_option': farm_row['loot_option'] if farm_row else 1 + 'enabled': bool(farm_row['enabled']) if farm_row else False, + 'loot_option': farm_row['loot_option'] if farm_row else 1 } + # 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.close() return jsonify({ - 'build': build_cmd, - 'recruit': recruit_cmd, - 'market': market_cmd, - 'research': research_cmd, - 'farm': farm_cmd, - 'farm_upgrade': farm_upgrade_cmd, - 'farm_settings': farm_settings, - 'sync_requested': sync_req + 'build': build_cmd, + 'recruit': recruit_cmd, + 'market': market_cmd, + 'research': research_cmd, + 'farm': farm_cmd, + 'farm_upgrade': farm_upgrade_cmd, + 'farm_settings': farm_settings, + 'enabled_features': enabled_features, + 'sync_requested': sync_req }) + def _check_and_reset_sync(c, player_id): key = f'sync_request_{player_id}' row = c.execute("SELECT value FROM kv_store WHERE key = ?", (key,)).fetchone() diff --git a/routes/auth.py b/routes/auth.py index 2c33b28..47e60b7 100644 --- a/routes/auth.py +++ b/routes/auth.py @@ -140,7 +140,9 @@ def options(): '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 + 'is_online': is_online, + 'feat_farm': 'farm' in (row['features'] or 'farm,admin'), + 'feat_admin': 'admin' in (row['features'] or 'farm,admin'), }) conn.close() @@ -191,9 +193,6 @@ def regenerate_key(): return redirect(url_for('auth.options')) -# ------------------------------------------------------------------ -# POST /auth/clan/remove-member/ -# ------------------------------------------------------------------ @auth.route('/auth/clan/remove-member/', methods=['POST']) @login_required def remove_member(player_id): @@ -209,3 +208,28 @@ def remove_member(player_id): conn.commit() conn.close() return redirect(url_for('auth.options')) + + +# ------------------------------------------------------------------ +# POST /auth/clan/update-features/ +# ------------------------------------------------------------------ +@auth.route('/auth/clan/update-features/', 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')) + diff --git a/templates/options.html b/templates/options.html index b6830ae..80e2a43 100644 --- a/templates/options.html +++ b/templates/options.html @@ -134,6 +134,24 @@ 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); @@ -186,6 +204,7 @@ Παίκτης Κατάσταση + Δυνατότητες Προστέθηκε @@ -204,6 +223,18 @@ ● Offline {% endif %} + +
+
+ + +
+
+ {{ m.joined_at }}