This commit is contained in:
2026-05-02 01:40:21 +03:00
parent 4272edf432
commit 6157ae1034
4 changed files with 84 additions and 45 deletions

View File

@@ -17,13 +17,13 @@ let lastKnownBotSettings = {};
// ---------------------------------------------------------------- // ----------------------------------------------------------------
// botLog — sends a log entry to the server // botLog — sends a log entry to the server
// ---------------------------------------------------------------- // ----------------------------------------------------------------
async function botLog(player_id, feature, message) { async function botLog(player_id, world_id, feature, message) {
log(`[${feature}] ${message}`); log(`[${feature}] ${message}`);
try { try {
await apiFetch(`${BASE_URL}/api/bot-logs`, { await apiFetch(`${BASE_URL}/api/bot-logs`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ player_id, feature, message }) body: JSON.stringify({ player_id, world_id, feature, message })
}); });
} catch (e) { /* non-critical */ } } catch (e) { /* non-critical */ }
} }
@@ -38,7 +38,8 @@ async function autoBootcampLoop() {
if (!settings.bootcamp_enabled) return; if (!settings.bootcamp_enabled) return;
const player_id = uw.Game?.player_id; const player_id = uw.Game?.player_id;
if (!player_id) return; const world_id = uw.Game?.world_id;
if (!player_id || !world_id) return;
let model; let model;
try { try {
@@ -67,7 +68,7 @@ async function autoBootcampLoop() {
action_name: 'useReward', action_name: 'useReward',
arguments: {} arguments: {}
}); });
await botLog(player_id, 'bootcamp', `Reward used: ${reward.power_id}`); await botLog(player_id, world_id, 'bootcamp', 'Διεκδίκηση αμοιβής ληστών...');
} else if (stashable) { } else if (stashable) {
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', { uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
model_url: `PlayerAttackSpot/${player_id}`, model_url: `PlayerAttackSpot/${player_id}`,
@@ -75,7 +76,7 @@ async function autoBootcampLoop() {
arguments: {} arguments: {}
}, 0, { }, 0, {
success: () => { success: () => {
botLog(player_id, 'bootcamp', `Reward stashed: ${reward.power_id}`); botLog(player_id, world_id, 'bootcamp', `Reward stashed: ${reward.power_id}`);
}, },
error: () => { error: () => {
uw.gpAjax.ajaxPost('frontend_bridge', 'execute', { uw.gpAjax.ajaxPost('frontend_bridge', 'execute', {
@@ -83,7 +84,7 @@ async function autoBootcampLoop() {
action_name: 'useReward', action_name: 'useReward',
arguments: {} arguments: {}
}); });
botLog(player_id, 'bootcamp', `Reward used (inventory full fallback): ${reward.power_id}`); botLog(player_id, world_id, 'bootcamp', 'Αποτυχία διεκδίκησης αμοιβής.');
} }
}); });
} else { } else {
@@ -92,7 +93,7 @@ async function autoBootcampLoop() {
action_name: 'useReward', action_name: 'useReward',
arguments: {} arguments: {}
}); });
await botLog(player_id, 'bootcamp', `Reward used (fallback): ${reward.power_id}`); await botLog(player_id, world_id, 'bootcamp', `Reward used (fallback): ${reward.power_id}`);
} }
await sleep(randInt(3000, 7000)); await sleep(randInt(3000, 7000));
return; // Wait for next cycle to attack return; // Wait for next cycle to attack
@@ -110,7 +111,7 @@ async function autoBootcampLoop() {
const cooldown = model.getCooldownDuration(); const cooldown = model.getCooldownDuration();
if (cooldown > 0) { if (cooldown > 0) {
const minRemaining = Math.round(cooldown / 60); const minRemaining = Math.round(cooldown / 60);
await botLog(player_id, 'bootcamp', `Camp on cooldown — ${minRemaining} min remaining`); await botLog(player_id, world_id, 'bootcamp', `Camp on cooldown — ${minRemaining} min remaining`);
return; return;
} }
@@ -119,7 +120,7 @@ async function autoBootcampLoop() {
if (movements) { if (movements) {
for (const mv of Object.values(movements)) { for (const mv of Object.values(movements)) {
if (mv.attributes.destination_is_attack_spot || mv.attributes.origin_is_attack_spot) { if (mv.attributes.destination_is_attack_spot || mv.attributes.origin_is_attack_spot) {
await botLog(player_id, 'bootcamp', 'Attack already in flight — skipping'); await botLog(player_id, world_id, 'bootcamp', 'Attack already in flight — skipping');
return; return;
} }
} }
@@ -152,7 +153,7 @@ async function autoBootcampLoop() {
} }
if (Object.keys(units).length === 0) { if (Object.keys(units).length === 0) {
await botLog(player_id, 'bootcamp', 'No available units — skipping attack'); await botLog(player_id, world_id, 'bootcamp', 'No available units — skipping attack. (Χωρίς αμυντικά)');
return; return;
} }
@@ -163,10 +164,10 @@ async function autoBootcampLoop() {
}); });
const unitSummary = Object.entries(units).map(([u, n]) => `${n}x${u}`).join(', '); const unitSummary = Object.entries(units).map(([u, n]) => `${n}x${u}`).join(', ');
await botLog(player_id, 'bootcamp', `Attack sent — ${unitSummary}`); await botLog(player_id, world_id, 'bootcamp', `Στέλνω ${JSON.stringify(units)} στο camp...`);
} catch (e) { } catch (e) {
await botLog(player_id, 'bootcamp', `Error during attack: ${e}`); await botLog(player_id, world_id, 'bootcamp', `Error during attack: ${e}`);
} }
} }
@@ -187,7 +188,8 @@ async function autoRuralTradeLoop() {
if (!settings.rural_trade_enabled) return; if (!settings.rural_trade_enabled) return;
const player_id = uw.Game?.player_id; const player_id = uw.Game?.player_id;
if (!player_id) return; const world_id = uw.Game?.world_id;
if (!player_id || !world_id) return;
const minRatio = RATIO_MAP[settings.rural_trade_ratio] ?? 0.75; const minRatio = RATIO_MAP[settings.rural_trade_ratio] ?? 0.75;
@@ -285,12 +287,12 @@ async function autoRuralTradeLoop() {
arguments: { farm_town_id: farm.attributes.id, amount }, arguments: { farm_town_id: farm.attributes.id, amount },
town_id: parseInt(town_id_str) town_id: parseInt(town_id_str)
}); });
await botLog(player_id, 'rural_trade', await botLog(player_id, world_id, 'rural_trade',
`Traded ${amount} ${missingResource}${farm.attributes.name} via ${town_obj.getName?.() ?? town_id_str}`); `Traded ${amount} ${missingResource}${farm.attributes.name} via ${town_obj.getName?.() ?? town_id_str}`);
tradesTotal++; tradesTotal++;
tradeMade = true; tradeMade = true;
} catch (e) { } catch (e) {
await botLog(player_id, 'rural_trade', `Trade error: ${e}`); await botLog(player_id, world_id, 'rural_trade', `Trade error: ${e}`);
} }
await sleep(randInt(800, 1800)); await sleep(randInt(800, 1800));
@@ -300,7 +302,7 @@ async function autoRuralTradeLoop() {
} }
if (!tradeMade && missingResource) { if (!tradeMade && missingResource) {
await botLog(player_id, 'rural_trade', await botLog(player_id, world_id, 'rural_trade',
`${town_obj.getName?.() ?? town_id_str} needs ${missingResource} but no suitable village found`); `${town_obj.getName?.() ?? town_id_str} needs ${missingResource} but no suitable village found`);
} }

View File

@@ -117,13 +117,28 @@ def receive_state():
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def _fetch_pending_of_type(c, cmd_type, player_id, world_id): def _fetch_pending_of_type(c, cmd_type, player_id, world_id):
"""Fetch a single oldest pending command of a given type (recruit, market, etc.).""" """Fetch a single oldest pending command of a given type (recruit, market, etc.)."""
# We use LEFT JOIN because global commands (like farm_loot) use a pseudo town_id like "0_gr121"
# which does not exist in town_state.
global_town_id = f"0_{world_id}" if world_id else "0"
if world_id:
row = c.execute(''' row = c.execute('''
SELECT c.* FROM commands c SELECT c.* FROM commands c
JOIN town_state ts ON c.town_id = ts.town_id 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 = ? 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 ORDER BY c.updated_at ASC, c.id ASC
LIMIT 1 LIMIT 1
''', (cmd_type, player_id, world_id)).fetchone() ''', (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('''
@@ -242,9 +257,12 @@ def get_pending_command():
research_cmd = _fetch_pending_of_type(c, 'research', 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) 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 settings
farm_row = c.execute( farm_row = c.execute(
'SELECT 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,
@@ -253,7 +271,7 @@ def get_pending_command():
# Bot settings (bootcamp + rural trade) # Bot settings (bootcamp + rural trade)
bot_row = c.execute( bot_row = c.execute(
'SELECT * FROM bot_settings WHERE player_id = ?', (str(player_id),) 'SELECT * FROM bot_settings WHERE player_id = ?', (player_key,)
).fetchone() ).fetchone()
bot_settings = { bot_settings = {
'bootcamp_enabled': bool(bot_row['bootcamp_enabled']) if bot_row else False, 'bootcamp_enabled': bool(bot_row['bootcamp_enabled']) if bot_row else False,
@@ -263,7 +281,7 @@ def get_pending_command():
} }
# One-shot manual attack flag # One-shot manual attack flag
attack_now_key = f'bootcamp_attack_now_{player_id}' attack_now_key = f'bootcamp_attack_now_{player_key}'
flag_row = c.execute('SELECT value FROM kv_store WHERE key = ?', (attack_now_key,)).fetchone() flag_row = c.execute('SELECT value FROM kv_store WHERE key = ?', (attack_now_key,)).fetchone()
if flag_row and flag_row['value'] == '1': if flag_row and flag_row['value'] == '1':
bot_settings['attack_now'] = True bot_settings['attack_now'] = True
@@ -457,16 +475,19 @@ def api_bot_logs():
data = request.get_json(silent=True) or {} data = request.get_json(silent=True) or {}
player_id = str(data.get('player_id', '')) player_id = str(data.get('player_id', ''))
world_id = str(data.get('world_id', ''))
feature = data.get('feature', '') feature = data.get('feature', '')
message = data.get('message', '') message = data.get('message', '')
if not player_id or not feature or not message: if not player_id or not feature or not message:
return jsonify({'error': 'missing fields'}), 400 return jsonify({'error': 'missing fields'}), 400
player_key = f"{player_id}_{world_id}" if world_id else player_id
conn = get_db() conn = get_db()
conn.execute( conn.execute(
'INSERT INTO bot_logs (player_id, feature, message) VALUES (?, ?, ?)', 'INSERT INTO bot_logs (player_id, feature, message) VALUES (?, ?, ?)',
(player_id, feature, message) (player_key, feature, message)
) )
# Keep only latest 50 per player/feature # Keep only latest 50 per player/feature
conn.execute(''' conn.execute('''
@@ -477,7 +498,7 @@ def api_bot_logs():
WHERE player_id = ? AND feature = ? WHERE player_id = ? AND feature = ?
ORDER BY id DESC LIMIT 50 ORDER BY id DESC LIMIT 50
) )
''', (player_id, feature, player_id, feature)) ''', (player_key, feature, player_key, feature))
conn.commit() conn.commit()
conn.close() conn.close()
return jsonify({'ok': True}) return jsonify({'ok': True})

View File

@@ -84,9 +84,12 @@ def player_farm(player_id, world_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, 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:
@@ -99,6 +102,9 @@ 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
loot_option = int(data.get('loot_option', 1)) loot_option = int(data.get('loot_option', 1))
conn = get_db() conn = get_db()
@@ -109,7 +115,7 @@ def set_farm_settings():
enabled = excluded.enabled, enabled = excluded.enabled,
loot_option = excluded.loot_option, loot_option = excluded.loot_option,
updated_at = excluded.updated_at updated_at = excluded.updated_at
''', (player_id, 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})
@@ -521,15 +527,18 @@ def fail_stale_commands():
@dashboard.route('/dashboard/bot-settings', methods=['GET', 'POST']) @dashboard.route('/dashboard/bot-settings', methods=['GET', 'POST'])
def bot_settings(): def bot_settings():
player_id = request.args.get('player_id') or (request.json or {}).get('player_id') 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: if not player_id:
return jsonify({'error': 'missing player_id'}), 400 return jsonify({'error': 'missing player_id'}), 400
player_key = f"{player_id}_{world_id}" if world_id else player_id
conn = get_db() conn = get_db()
c = conn.cursor() c = conn.cursor()
if request.method == 'GET': if request.method == 'GET':
row = c.execute( row = c.execute(
'SELECT * FROM bot_settings WHERE player_id = ?', (player_id,) 'SELECT * FROM bot_settings WHERE player_id = ?', (player_key,)
).fetchone() ).fetchone()
conn.close() conn.close()
if row: if row:
@@ -555,7 +564,7 @@ def bot_settings():
rural_trade_ratio = excluded.rural_trade_ratio, rural_trade_ratio = excluded.rural_trade_ratio,
updated_at = excluded.updated_at updated_at = excluded.updated_at
''', ( ''', (
player_id, player_key,
int(bool(data.get('bootcamp_enabled', 0))), int(bool(data.get('bootcamp_enabled', 0))),
int(bool(data.get('bootcamp_use_def', 0))), int(bool(data.get('bootcamp_use_def', 0))),
int(bool(data.get('rural_trade_enabled', 0))), int(bool(data.get('rural_trade_enabled', 0))),
@@ -574,16 +583,19 @@ def bot_settings():
@dashboard.route('/dashboard/bot-logs', methods=['GET', 'POST']) @dashboard.route('/dashboard/bot-logs', methods=['GET', 'POST'])
def bot_logs(): def bot_logs():
player_id = request.args.get('player_id') or (request.json or {}).get('player_id') 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: if not player_id:
return jsonify({'error': 'missing player_id'}), 400 return jsonify({'error': 'missing player_id'}), 400
player_key = f"{player_id}_{world_id}" if world_id else player_id
conn = get_db() conn = get_db()
c = conn.cursor() c = conn.cursor()
if request.method == 'GET': if request.method == 'GET':
feature = request.args.get('feature', '') feature = request.args.get('feature', '')
query = 'SELECT * FROM bot_logs WHERE player_id = ?' query = 'SELECT * FROM bot_logs WHERE player_id = ?'
params = [player_id] params = [player_key]
if feature: if feature:
query += ' AND feature = ?' query += ' AND feature = ?'
params.append(feature) params.append(feature)
@@ -598,7 +610,7 @@ def bot_logs():
message = data.get('message', '') message = data.get('message', '')
c.execute( c.execute(
'INSERT INTO bot_logs (player_id, feature, message) VALUES (?, ?, ?)', 'INSERT INTO bot_logs (player_id, feature, message) VALUES (?, ?, ?)',
(player_id, feature, message) (player_key, feature, message)
) )
# Prune: keep only the latest 50 per player/feature # Prune: keep only the latest 50 per player/feature
c.execute(''' c.execute('''
@@ -609,7 +621,7 @@ def bot_logs():
WHERE player_id = ? AND feature = ? WHERE player_id = ? AND feature = ?
ORDER BY id DESC LIMIT 50 ORDER BY id DESC LIMIT 50
) )
''', (player_id, feature, player_id, feature)) ''', (player_key, feature, player_key, feature))
conn.commit() conn.commit()
conn.close() conn.close()
return jsonify({'ok': True}) return jsonify({'ok': True})
@@ -623,9 +635,12 @@ def bot_logs():
@dashboard.route('/dashboard/bootcamp-attack-now', methods=['POST']) @dashboard.route('/dashboard/bootcamp-attack-now', methods=['POST'])
def bootcamp_attack_now(): def bootcamp_attack_now():
player_id = (request.json or {}).get('player_id') player_id = (request.json or {}).get('player_id')
world_id = (request.json or {}).get('world_id', '')
if not player_id: if not player_id:
return jsonify({'error': 'missing player_id'}), 400 return jsonify({'error': 'missing player_id'}), 400
key = f'bootcamp_attack_now_{player_id}'
player_key = f"{player_id}_{world_id}" if world_id else player_id
key = f'bootcamp_attack_now_{player_key}'
conn = get_db() conn = get_db()
conn.execute(''' conn.execute('''
INSERT INTO kv_store (key, value, updated_at) VALUES (?, '1', ?) INSERT INTO kv_store (key, value, updated_at) VALUES (?, '1', ?)

View File

@@ -446,7 +446,7 @@
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, 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(() => {
@@ -458,7 +458,7 @@
// -- 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;
@@ -564,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 }
}) })
@@ -591,7 +591,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_loot', type: 'farm_loot',
payload: { loot_option: selectedOption } payload: { loot_option: selectedOption }
}) })
@@ -617,7 +617,7 @@
fetch('/dashboard/bootcamp-attack-now', { fetch('/dashboard/bootcamp-attack-now', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ player_id: PLAYER_ID }) body: JSON.stringify({ player_id: PLAYER_ID, world_id: WORLD_ID })
}) })
.then(r => r.json()) .then(r => r.json())
.then(() => { .then(() => {
@@ -633,7 +633,7 @@
async function checkWarehouseStatus() { async function checkWarehouseStatus() {
try { try {
const res = await fetch(`/api/farm_status?player_id=${PLAYER_ID}`); const res = await fetch(`/api/farm_status?player_id=${PLAYER_ID}&world_id=${WORLD_ID}`);
const data = await res.json(); const data = await res.json();
const banner = document.getElementById('warehouse-full-banner'); const banner = document.getElementById('warehouse-full-banner');
if (banner) banner.style.display = data.warehouse_full ? 'flex' : 'none'; if (banner) banner.style.display = data.warehouse_full ? 'flex' : 'none';
@@ -651,7 +651,7 @@
} }
function loadBotSettings() { function loadBotSettings() {
fetch(`/dashboard/bot-settings?player_id=${PLAYER_ID}`) fetch(`/dashboard/bot-settings?player_id=${PLAYER_ID}&world_id=${WORLD_ID}`)
.then(r => r.json()) .then(r => r.json())
.then(cfg => { .then(cfg => {
document.getElementById('bootcamp-enabled').checked = !!cfg.bootcamp_enabled; document.getElementById('bootcamp-enabled').checked = !!cfg.bootcamp_enabled;
@@ -669,6 +669,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,
world_id: WORLD_ID,
bootcamp_enabled: document.getElementById('bootcamp-enabled').checked, bootcamp_enabled: document.getElementById('bootcamp-enabled').checked,
bootcamp_use_def: document.getElementById('bootcamp-use-def').checked, bootcamp_use_def: document.getElementById('bootcamp-use-def').checked,
rural_trade_enabled: document.getElementById('rural-trade-enabled').checked, rural_trade_enabled: document.getElementById('rural-trade-enabled').checked,
@@ -708,9 +709,9 @@
} }
function loadBotLogs() { function loadBotLogs() {
fetch(`/dashboard/bot-logs?player_id=${PLAYER_ID}&feature=bootcamp`) fetch(`/dashboard/bot-logs?player_id=${PLAYER_ID}&world_id=${WORLD_ID}&feature=bootcamp`)
.then(r => r.json()).then(data => renderBotLog('bootcamp-log', data)); .then(r => r.json()).then(data => renderBotLog('bootcamp-log', data));
fetch(`/dashboard/bot-logs?player_id=${PLAYER_ID}&feature=rural_trade`) 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)); .then(r => r.json()).then(data => renderBotLog('rural-trade-log', data));
} }