Greasy Fork is available in English.
Single Village Snipe
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.icu/scripts/552155/1676640/Volvo%27s%20SVS.js
// ==UserScript==
// @name Volvo's SVS
// @namespace http://greasyfork.icu/scripts/552157-volvos-svs
// @version 1.20, 이게 1.20 으로 보이면 자동 업데이트 된거임
// @description Single Village Snipe UI + auto sender: info_village UI patch, place auto distribution/input, timed send, auto tab close
// @author Volvo
// @license Volvo
// @match https://*.tribalwars.net/game.php*
// ==/UserScript==
(function () {
'use strict';
const SEL_TABLE = '#possibleCombinationsTable table.ra-table';
const K = (id, sfx) => `Volvo[${id}]${sfx}`;
const S = {
get: (id, s) => localStorage.getItem(K(id, s)),
set: (id, s, v) => localStorage.setItem(K(id, s), v),
remove: (id, s) => localStorage.removeItem(K(id, s))
};
const t2s = str => {
const m = String(str || '').trim().match(/^(\d{1,2}):(\d{2}):(\d{2})$/);
return m ? (+m[1]) * 3600 + (+m[2]) * 60 + (+m[3]) : 0;
};
const randThreshold9to10 = () => 9 + Math.random();
const OPENED = new Set();
const THRESH = new Map();
function getVillageIdFromRow(row) {
const link = row?.querySelector('a[href*="screen=info_village"][href*="id="]');
if (!link) return null;
try {
return new URL(link.href, location.origin).searchParams.get('id');
} catch {
const m = link.href.match(/id=(\d+)/);
return m ? m[1] : null;
}
}
function getLaunchTimeFromRow(row) {
const cell = row?.cells?.[4];
if (!cell) return null;
const full = (cell.textContent || '').trim();
if (!full) return null;
const parts = full.split(' ');
const t = parts.length > 1 ? parts[1] : full;
return /^\d{1,2}:\d{2}:\d{2}$/.test(t) ? t : null;
}
function getUnitTypeFromRow(row) {
const img = row?.querySelector('img[src*="/graphic/unit/unit_"]');
if (!img) return null;
const m = (img.getAttribute('src') || '').match(/unit_([a-z]+)\./);
return m ? m[1] : null;
}
function findSendAnchor(row) {
return row.querySelector('a.btn[href*="screen=place"]') ||
Array.from(row.querySelectorAll('a[href]')).find(a => a.href.includes('screen=place')) ||
null;
}
function getRowURL(row) {
const a = findSendAnchor(row);
if (!a) return '';
try {
return new URL(a.href, location.origin).href;
} catch {
return a.getAttribute('href') || '';
}
}
(function injectStyle() {
if (document.getElementById('svs-ui-style')) return;
const style = document.createElement('style');
style.id = 'svs-ui-style';
style.textContent = `
.svs-hidden{display:none!important}
.svs-divide-wrap{display:flex;gap:8px;align-items:center;justify-content:center}
.svs-inputs{display:flex;flex-wrap:wrap;gap:4px;max-width:180px;justify-content:center}
.svs-input{width:42px;text-align:center;padding:2px 4px;font-size:12px;border:1px solid #c7c7c7;border-radius:3px}
.svs-input::-webkit-outer-spin-button,
.svs-input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
.svs-input[type=number]{-moz-appearance:textfield}
.svs-attack,.svs-support{margin-left:4px}
.btn.svs-btn-active{background:linear-gradient(to bottom,#0bac00 0%,#0e7a1e 100%)!important;color:#fff!important;border-color:#006712!important}
#TabAutoCloseBtn{margin-left:8px}
`;
document.head.appendChild(style);
})();
function ensureDivideHeader(tbl) {
const bodyRow = tbl.querySelector('tbody tr'),
thead = tbl.querySelector('thead');
if (!bodyRow || !thead) return;
const idx = bodyRow.lastElementChild?.cellIndex;
if (idx == null) return;
const th = thead.querySelectorAll('th')[idx];
if (th && th.textContent.trim() !== 'Divide / Custom')
th.textContent = 'Divide / Custom';
}
function percentDefaults(n) {
const base = Math.floor(100 / n),
rem = 100 - base * n;
return Array.from({ length: n }, (_, i) => String(base + (i < rem ? 1 : 0)));
}
function buildPercentInputs(td, wrap, count, reset) {
let vals = reset ? percentDefaults(count) : JSON.parse(td.dataset.svsPValues || '[]');
if (!Array.isArray(vals) || vals.length !== count) vals = percentDefaults(count);
td.dataset.svsPValues = JSON.stringify(vals);
wrap.textContent = '';
wrap.style.display = 'flex';
vals.forEach((val, idx) => {
const input = document.createElement('input');
input.type = 'number';
input.min = '0';
input.max = '100';
input.step = '1';
input.className = 'svs-input';
input.value = val;
const commit = () => {
const v = String(input.value || '');
const cur = JSON.parse(td.dataset.svsPValues || '[]');
cur[idx] = v;
td.dataset.svsPValues = JSON.stringify(cur);
input.value = v;
};
input.addEventListener('input', commit);
input.addEventListener('blur', commit);
wrap.appendChild(input);
});
}
function buildCustomInputs(td, wrap, initDefaults) {
const val = initDefaults ? '1000' : JSON.parse(td.dataset.svsCValues || '["1000"]')[0];
td.dataset.svsCValues = JSON.stringify([val]);
wrap.textContent = '';
wrap.style.display = 'flex';
const input = document.createElement('input');
input.type = 'number';
input.step = '1';
input.className = 'svs-input';
input.style.width = '56px';
input.value = val;
const commit = () => {
const v = String(input.value || '');
td.dataset.svsCValues = JSON.stringify([v]);
input.value = v;
};
input.addEventListener('input', commit);
input.addEventListener('blur', commit);
wrap.appendChild(input);
}
function showDivide(row, show) {
row.querySelector('td:last-child')?.classList.toggle('svs-hidden', !show);
}
function setDividePercent(row, n, reset = true) {
const td = row.querySelector('td:last-child');
if (!td) return;
const wrap = td.querySelector('.svs-inputs');
const btn = td.querySelector('.svs-divide');
td.dataset.svsMode = 'percent';
td.dataset.svsDivideN = String(n);
if (btn) btn.textContent = `Divide: ${n}`;
buildPercentInputs(td, wrap, n, reset);
}
function setDivideCustom(row, init = true) {
const td = row.querySelector('td:last-child');
if (!td) return;
const wrap = td.querySelector('.svs-inputs');
const btn = td.querySelector('.svs-divide');
td.dataset.svsMode = 'custom';
if (btn) btn.textContent = 'Custom';
buildCustomInputs(td, wrap, init);
}
function makeDivideCell(td) {
if (!td || td.closest('thead') || td.querySelector('.svs-divide')) return;
td.dataset.svsDivideReady = '1';
td.textContent = '';
const wrap = document.createElement('div');
wrap.className = 'svs-divide-wrap';
const btn = document.createElement('a');
btn.href = 'javascript:void(0)';
btn.className = 'btn svs-divide';
btn.textContent = 'Divide: 1';
btn.title = 'Attack: 1→2→3→4→Custom→1 / Support: 1↔Custom';
const inputs = document.createElement('div');
inputs.className = 'svs-inputs';
td.dataset.svsMode = 'percent';
td.dataset.svsDivideN = '1';
buildPercentInputs(td, inputs, 1, true);
btn.addEventListener('click', () => {
const row = td.closest('tr');
const mode = td.dataset.svsMode;
const n = parseInt(td.dataset.svsDivideN || '1', 10);
const sendType = row.querySelector('.svs-btn-active')?.textContent || '';
if (!sendType) return;
if (sendType === 'Support') {
if (mode === 'percent') setDivideCustom(row, true);
else setDividePercent(row, 1, false);
} else {
if (mode === 'percent') {
if (n < 4) {
td.dataset.svsDivideN = String(n + 1);
setDividePercent(row, n + 1, true);
} else setDivideCustom(row, true);
} else setDividePercent(row, 1, true);
}
});
wrap.appendChild(btn);
wrap.appendChild(inputs);
td.appendChild(wrap);
td.classList.add('svs-hidden');
}
function addAttackSupport(sendTd) {
if (!sendTd || sendTd.querySelector('.svs-attack')) return;
const handle = (e) => {
e.preventDefault();
const btn = e.currentTarget;
const row = btn.closest('tr');
const wasActive = btn.classList.contains('svs-btn-active');
const allBtns = row.querySelectorAll('.svs-attack, .svs-support');
if (wasActive) {
btn.classList.remove('svs-btn-active');
showDivide(row, false);
} else {
allBtns.forEach(b => b.classList.remove('svs-btn-active'));
btn.classList.add('svs-btn-active');
showDivide(row, true);
setDividePercent(row, 1, true);
}
};
const mk = (t, c) => {
const a = document.createElement('a');
a.href = 'javascript:void(0)';
a.className = `btn ${c}`;
a.textContent = t;
a.addEventListener('click', handle);
return a;
};
const sendA = sendTd.querySelector('a.btn[href*="screen=place"]');
if (!sendA) return;
const atk = mk('Attack', 'svs-attack');
const sup = mk('Support', 'svs-support');
sendA.insertAdjacentElement('afterend', atk);
atk.insertAdjacentElement('afterend', sup);
}
function addTabAutoCloseButton() {
if (document.getElementById('TabAutoCloseBtn')) return;
const btn = document.createElement('a');
btn.href = 'javascript:void(0)';
btn.id = 'TabAutoCloseBtn';
btn.className = 'btn';
btn.textContent = 'Tab Auto Close';
let state = localStorage.getItem('VolvoTabAutoClose');
if (state === null) {
state = 'Off';
localStorage.setItem('VolvoTabAutoClose', state);
}
if (state === 'On') btn.classList.add('svs-btn-active');
btn.addEventListener('click', () => {
const cur = localStorage.getItem('VolvoTabAutoClose') || 'Off';
const ns = cur === 'On' ? 'Off' : 'On';
localStorage.setItem('VolvoTabAutoClose', ns);
btn.classList.toggle('svs-btn-active', ns === 'On');
});
const resetBtn = document.getElementById('resetScriptBtn');
if (resetBtn) resetBtn.insertAdjacentElement('afterend', btn);
else(document.getElementById('content_value') || document.body).appendChild(btn);
}
function patchTableAndUI() {
const tbl = document.querySelector(SEL_TABLE);
if (!tbl) return;
ensureDivideHeader(tbl);
tbl.querySelectorAll('tbody tr').forEach(tr => {
if (!tr.dataset.svsUiPatched) {
const tds = [...tr.children];
const sendTd = tds.find(td => td.querySelector('a.btn[href*="screen=place"]'));
addAttackSupport(sendTd);
const last = tr.querySelector('td:last-child');
if (last) makeDivideCell(last);
tr.dataset.svsUiPatched = '1';
}
});
addTabAutoCloseButton();
}
function extractTimeWithOptionalMs(val) {
const str = String(val || '');
const m = str.match(/(\d{1,2}:\d{2}:\d{2})(?::\d{1,3})?/);
return m ? m[0] : '';
}
function readServerNowSec() {
const el = document.getElementById('serverTime');
return el ? t2s((el.textContent || '').trim()) : 0;
}
function snapshotAndOpen(row) {
const id = getVillageIdFromRow(row);
if (!id) return;
const url = getRowURL(row);
if (!url) return;
const type = row.querySelector('.svs-attack.svs-btn-active,.svs-support.svs-btn-active')?.textContent || '';
const unit = getUnitTypeFromRow(row) || '';
if (type || unit) S.set(id, 'Send', JSON.stringify({ type, unit }));
S.set(id, 'URL', url);
const td = row.querySelector('td:last-child[data-svs-divide-ready="1"]');
if (!td) return;
const mode = td.dataset.svsMode || 'percent';
if (mode === 'custom') {
const input = td.querySelector('.svs-input');
const raw = String(input?.value ?? '1000');
S.set(id, 'Divide', JSON.stringify({ mode: 'custom', count: '1', '#1': raw }));
} else {
const count = Math.max(1, Math.min(4, parseInt(td.dataset.svsDivideN || '1', 10) || 1));
const inputs = Array.from(td.querySelectorAll('.svs-input') || []);
const obj = { mode: 'percent', count: String(count) };
for (let i = 1; i <= count; i++) {
obj['#' + i] = String(inputs[i - 1]?.value ?? '');
}
S.set(id, 'Divide', JSON.stringify(obj));
}
const ltEl = document.getElementById('raLandingTime');
if (ltEl) {
const t = extractTimeWithOptionalMs(ltEl.value);
if (t) S.set(id, 'LandingTime', t);
}
const launchTime = getLaunchTimeFromRow(row);
if (launchTime) S.set(id, 'LaunchTime', launchTime);
try {
window.open(url, '_blank', 'noopener,noreferrer');
} catch {}
}
function autoOpenLoop() {
const nowSec = readServerNowSec();
if (!nowSec) return;
const tbl = document.querySelector(SEL_TABLE);
if (!tbl) return;
let best = { row: null, key: null, diff: 1e9 };
tbl.querySelectorAll('tbody tr').forEach(tr => {
const activeBtn = tr.querySelector('.svs-attack.svs-btn-active, .svs-support.svs-btn-active');
if (!activeBtn) return;
const id = getVillageIdFromRow(tr);
if (!id) return;
const lt = getLaunchTimeFromRow(tr);
if (!lt) return;
const url = getRowURL(tr);
if (!url) return;
let diff = t2s(lt) - nowSec;
if (diff < -43200) diff += 86400;
const key = `${id}|${lt}|${url}`;
if (OPENED.has(key)) return;
if (diff > 0 && diff < best.diff) best = { row: tr, key, diff };
});
if (!best.row) return;
if (!THRESH.has(best.key)) THRESH.set(best.key, randThreshold9to10());
if (best.diff <= THRESH.get(best.key)) {
OPENED.add(best.key);
snapshotAndOpen(best.row);
}
}
function start() {
patchTableAndUI();
const root = document.querySelector('#possibleCombinationsTable') || document.body;
const mo = new MutationObserver(() => patchTableAndUI());
mo.observe(root, { childList: true, subtree: true });
setInterval(autoOpenLoop, 500);
setInterval(updateServerTime, 500);
setInterval(autoCloseAndCleanup, 1000);
}
function updateServerTime() {
const serverTime = readServerTime();
if (serverTime && /^\d{1,2}:\d{2}:\d{2}$/.test(serverTime)) {
const dateEl = document.getElementById('serverDate');
if (dateEl) {
const dateText = (dateEl.textContent || '').trim();
const dateMatch = dateText.match(/(\d{2})\/(\d{2})\/(\d{4})/);
if (dateMatch) {
const [, mm, dd, yyyy] = dateMatch;
const formattedDateTime = `${yyyy}-${mm}-${dd} ${serverTime}`;
localStorage.setItem('VolvoServerTime', formattedDateTime);
return;
}
}
localStorage.setItem('VolvoServerTime', serverTime);
}
}
function getVillageIdFromURL() {
try {
const params = new URLSearchParams(window.location.search);
return params.get('village') || null;
} catch {
const m = window.location.href.match(/village=(\d+)/);
return m ? m[1] : null;
}
}
function cleanupStorage(id) {
const keys = ['LandingTime', 'Divide', 'Send', 'URL', 'LaunchTime', 'UnitCount'];
keys.forEach(k => S.remove(id, k));
}
function readServerTime() {
const el = document.getElementById('serverTime');
if (!el) {
const dateEl = document.getElementById('serverDate');
if (dateEl) {
const text = (dateEl.textContent || '').trim();
const match = text.match(/(\d{1,2}:\d{2}:\d{2})/);
if (match) return match[1];
}
return null;
}
const text = (el.textContent || el.innerText || '').trim();
const match = text.match(/(\d{1,2}:\d{2}:\d{2})/);
return match ? match[1] : null;
}
function autoCloseAndCleanup() {
const serverTimeRaw = localStorage.getItem('VolvoServerTime');
if (!serverTimeRaw) return;
let serverTime;
const fullMatch = serverTimeRaw.match(/\d{4}-\d{2}-\d{2} (\d{1,2}:\d{2}:\d{2})/);
if (fullMatch) {
serverTime = fullMatch[1];
} else if (/^\d{1,2}:\d{2}:\d{2}$/.test(serverTimeRaw)) {
serverTime = serverTimeRaw;
} else {
return;
}
const serverSec = t2s(serverTime);
const qs = new URLSearchParams(window.location.search);
const isPlacePage = qs.get('screen') === 'place';
if (isPlacePage) {
const id = getVillageIdFromURL();
if (!id) return;
const launchTime = S.get(id, 'LaunchTime');
if (!launchTime) return;
const launchSec = t2s(launchTime);
let diff = serverSec - launchSec;
if (diff < -43200) diff += 86400;
if (diff >= 3) {
const autoClose = localStorage.getItem('VolvoTabAutoClose');
cleanupStorage(id);
if (autoClose === 'On') {
window.close();
}
}
} else {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (!key || !key.startsWith('Volvo[') || !key.includes(']LaunchTime')) continue;
const match = key.match(/^Volvo\[(\d+)\]LaunchTime$/);
if (!match) continue;
const id = match[1];
const launchTime = S.get(id, 'LaunchTime');
if (!launchTime) continue;
const launchSec = t2s(launchTime);
let diff = serverSec - launchSec;
if (diff < -43200) diff += 86400;
if (diff >= 3) {
cleanupStorage(id);
}
}
}
}
start();
const randDelay = () => 200 + Math.floor(Math.random() * 201);
const shortDelay = () => 20 + Math.floor(Math.random() * 21);
const UNIT_MAP = {
spear: ['spear', 'spy', 'heavy'],
sword: ['spear', 'sword', 'spy', 'heavy'],
axe: ['axe', 'spy', 'light'],
light: ['light'],
heavy: ['heavy'],
ram: ['spear', 'sword', 'axe', 'spy', 'light', 'heavy', 'ram', 'catapult'],
catapult: ['spear', 'sword', 'axe', 'spy', 'light', 'heavy', 'ram', 'catapult'],
snob: ['snob', 'spear', 'sword', 'axe', 'spy', 'light', 'heavy', 'ram', 'catapult'],
};
function getAllUnitInputs() {
return {
spear: document.getElementById('unit_input_spear'),
sword: document.getElementById('unit_input_sword'),
axe: document.getElementById('unit_input_axe'),
spy: document.getElementById('unit_input_spy'),
light: document.getElementById('unit_input_light'),
heavy: document.getElementById('unit_input_heavy'),
ram: document.getElementById('unit_input_ram'),
catapult: document.getElementById('unit_input_catapult'),
snob: document.getElementById('unit_input_snob'),
};
}
function readAllCounts(unitInputs) {
const counts = {};
for (const [unit, input] of Object.entries(unitInputs)) {
if (!input) continue;
const allCount = input.getAttribute('data-all-count');
counts[unit] = parseInt(allCount || '0', 10) || 0;
}
return counts;
}
function setAmount(input, value) {
if (!input) return;
input.value = value > 0 ? String(value) : '';
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
}
function applyDivideMode(unitType, percent, unitCounts, unitInputs) {
const group = UNIT_MAP[unitType] || (unitInputs[unitType] ? [unitType] : []);
if (!group.length) return;
const p = Math.max(0, Math.min(100, percent));
for (const u of group) {
if (unitType === 'snob' && u === 'snob') {
setAmount(unitInputs[u], 1);
} else {
setAmount(unitInputs[u], Math.floor((unitCounts[u] || 0) * p / 100));
}
}
}
function applyMultiAttackMode(unitType, attackCount, percent, unitCounts, unitInputs) {
const group = UNIT_MAP[unitType] || (unitInputs[unitType] ? [unitType] : []);
if (!group.length) return;
const hasRam = (unitCounts.ram || 0) > 0;
const hasCat = (unitCounts.catapult || 0) > 0;
const reserve = attackCount - 1;
const p = Math.max(0, Math.min(100, percent));
for (const t of group) {
const n = unitCounts[t] || 0;
const inp = unitInputs[t];
if (!inp) continue;
let cnt = 0;
if (t === 'ram' || t === 'catapult') {
if (unitType === 'snob') {
if (t === 'ram') cnt = hasRam ? Math.max(n - reserve, 0) : 0;
else if (t === 'catapult') cnt = (!hasRam && hasCat) ? Math.max(n - reserve, 0) : 0;
} else if (unitType === 'ram') {
if (t === 'ram' && hasRam) cnt = Math.max(n - reserve, 0);
else if (t === 'catapult' && hasRam) cnt = n;
else if (t === 'catapult' && !hasRam) cnt = Math.max(n - reserve, 0);
} else if (unitType === 'catapult') {
if (t === 'catapult') cnt = Math.max(n - reserve, 0);
else if (t === 'ram') cnt = n;
}
} else if (t === 'snob') {
cnt = unitType === 'snob' ? 1 : 0;
} else {
cnt = Math.floor(n * p / 100);
}
setAmount(inp, cnt);
}
}
function applyCustomMode(unitType, customTotal, unitCounts, unitInputs) {
const cap = Math.max(0, customTotal | 0);
const group = (UNIT_MAP[unitType] || (unitInputs[unitType] ? [unitType] : []))
.map(u => ({ unit: u, input: unitInputs[u], available: unitCounts[u] || 0 }))
.filter(e => e.input);
const total = group.reduce((s, e) => s + e.available, 0);
if (total <= 0) { group.forEach(e => setAmount(e.input, 0)); return; }
if (cap >= total) { group.forEach(e => setAmount(e.input, e.available)); return; }
const scale = cap / total;
let floorSum = 0;
const dist = group.map(e => {
const exact = e.available * scale;
const base = Math.floor(exact);
floorSum += base;
return { ...e, base, fraction: exact - base };
});
let rem = cap - floorSum;
dist.sort((a, b) => b.fraction - a.fraction);
for (let i = 0; i < dist.length && rem > 0; i++) {
if (dist[i].base < dist[i].available) { dist[i].base++; rem--; }
}
for (const d of dist) setAmount(d.input, d.base);
}
function setTrainValue(rowIdx, unitKey, val) {
const inp = document.querySelector(`input[type="number"][name="train[${rowIdx}][${unitKey}]"]`);
if (!inp) return;
inp.value = val > 0 ? String(val) : '';
inp.setAttribute('value', inp.value);
inp.dispatchEvent(new Event('input', { bubbles: true }));
inp.dispatchEvent(new Event('change', { bubbles: true }));
}
function getTrainRows() {
const rows = new Set();
document.querySelectorAll('input[type="number"][name^="train["]').forEach(el => {
const m = el.name.match(/^train\[(\d+)\]\[/);
if (m) rows.add(parseInt(m[1], 10));
});
return Array.from(rows).sort((a, b) => a - b);
}
function handleTimedClick(submitBtn, villageId) {
const landingTime = localStorage.getItem(K(villageId, 'LandingTime'));
if (!landingTime || landingTime.trim() === '') {
submitBtn.click();
return;
}
const parts = landingTime.split(':');
if (parts.length < 3) {
submitBtn.click();
return;
}
const [hh, mm, ss, ms] = parts;
const targetHms = `${hh}:${mm}:${ss}`;
const targetMs = parseInt(ms || '0', 10);
const relativeTimeEl = document.querySelector('.relative_time');
if (!relativeTimeEl) {
submitBtn.click();
return;
}
const readTime = () => {
const text = relativeTimeEl.textContent || relativeTimeEl.innerText || '';
const match = text.match(/\d{2}:\d{2}:\d{2}/);
return match ? match[0] : null;
};
const currentTime = readTime();
const parseHMS = (hms) => {
if (!hms) return null;
const [h, m, s] = hms.split(':').map(Number);
return h * 3600 + m * 60 + s;
};
const currentSec = parseHMS(currentTime);
const targetSec = parseHMS(targetHms);
if (currentSec !== null && targetSec !== null && currentSec > targetSec) {
submitBtn.click();
return;
}
const interval = setInterval(() => {
if (readTime() === targetHms) {
setTimeout(() => submitBtn.click(), targetMs);
clearInterval(interval);
}
}, 5);
}
async function handleConfirmScreen() {
const qs = new URLSearchParams(location.search);
const villageId = qs.get('village');
if (!villageId) return;
const sendRaw = localStorage.getItem(K(villageId, 'Send'));
const divideRaw = localStorage.getItem(K(villageId, 'Divide')) || localStorage.getItem(K(villageId, 'Divde'));
const unitCountRaw = localStorage.getItem(K(villageId, 'UnitCount'));
if (!sendRaw || !divideRaw || !unitCountRaw) return;
const sendConfig = JSON.parse(sendRaw);
const divideConfig = JSON.parse(divideRaw);
const unitCounts = JSON.parse(unitCountRaw);
const unitType = sendConfig.unit;
const attackCount = parseInt(String(divideConfig.count || '1'), 10);
if (attackCount <= 1) {
await new Promise(r => setTimeout(r, randDelay()));
const submitBtn = document.querySelector('.troop_confirm_go, #troop_confirm_submit');
if (!submitBtn) return;
const flagKey = K(villageId, 'ConfirmClicked');
if (sessionStorage.getItem(flagKey) === '1') return;
sessionStorage.setItem(flagKey, '1');
handleTimedClick(submitBtn, villageId);
return;
}
const trainBtn = document.getElementById('troop_confirm_train');
if (!trainBtn) return;
const toAdd = attackCount - 1;
for (let i = 0; i < toAdd; i++) {
if (i === 0) await new Promise(r => setTimeout(r, shortDelay()));
trainBtn.click();
await new Promise(r => setTimeout(r, shortDelay()));
}
await new Promise(r => setTimeout(r, shortDelay()));
const rows = getTrainRows();
if (rows.length === 0) return;
const allUnits = ['spear', 'sword', 'axe', 'spy', 'light', 'heavy', 'ram', 'catapult', 'snob'];
rows.forEach(rowIdx => {
allUnits.forEach(unit => setTrainValue(rowIdx, unit, 0));
});
await new Promise(r => setTimeout(r, shortDelay()));
const group = UNIT_MAP[unitType] || [unitType];
rows.forEach(rowIdx => {
const percent = parseInt(String(divideConfig[`#${rowIdx}`] || '0'), 10);
group.forEach(unit => {
let amount = 0;
if ((unitType === 'snob' && unit === 'snob') ||
(unitType === 'ram' && unit === 'ram') ||
(unitType === 'catapult' && unit === 'catapult')) {
amount = 1;
} else if (unit !== 'ram' && unit !== 'catapult' && unit !== 'snob') {
amount = Math.floor((unitCounts[unit] || 0) * percent / 100);
}
setTrainValue(rowIdx, unit, amount);
});
});
await new Promise(r => setTimeout(r, randDelay()));
const submitBtn = document.querySelector('.troop_confirm_go, #troop_confirm_submit');
if (!submitBtn) return;
const flagKey = K(villageId, 'ConfirmClicked');
if (sessionStorage.getItem(flagKey) === '1') return;
sessionStorage.setItem(flagKey, '1');
handleTimedClick(submitBtn, villageId);
}
function handlePlaceScreen() {
const qs = new URLSearchParams(location.search);
const villageId = qs.get('village');
if (!villageId) return;
const hasSend = !!localStorage.getItem(K(villageId, 'Send'));
const hasDivide = !!localStorage.getItem(K(villageId, 'Divide')) || !!localStorage.getItem(K(villageId, 'Divde'));
const hasCustom = !!localStorage.getItem(K(villageId, 'Custom'));
if (!hasSend && !hasDivide && !hasCustom) return;
const isReady = () => {
const any = document.querySelector(
'#unit_input_spear, #unit_input_sword, #unit_input_axe, ' +
'#unit_input_spy, #unit_input_light, #unit_input_heavy, ' +
'#unit_input_ram, #unit_input_catapult, #unit_input_snob'
);
return !!(any && any.getAttribute('data-all-count') !== null);
};
function parseSend() {
let sendType = null;
let unitType = null;
const raw = localStorage.getItem(K(villageId, 'Send'));
if (raw) {
try {
const j = JSON.parse(raw);
if (j && typeof j === 'object') {
if (j.type === 'Attack' || j.type === 'Support') sendType = j.type;
if (typeof j.unit === 'string' && j.unit) unitType = j.unit;
}
} catch (_) {}
}
if (!unitType) {
const order = ['snob', 'catapult', 'ram', 'axe', 'light', 'heavy', 'spear', 'sword'];
for (const u of order) {
const v = qs.get(u);
if (v && parseInt(v, 10) > 0) { unitType = u; break; }
}
}
return { sendType, unitType };
}
function parseDivide() {
let raw = localStorage.getItem(K(villageId, 'Divide'));
if (!raw) raw = localStorage.getItem(K(villageId, 'Divde'));
if (raw) {
try {
const j = JSON.parse(raw);
if (j && typeof j === 'object') return j;
} catch (_) {}
}
return null;
}
const fillTroops = () => {
const unitInputs = getAllUnitInputs();
const unitCounts = readAllCounts(unitInputs);
localStorage.setItem(K(villageId, 'UnitCount'), JSON.stringify(unitCounts));
const { sendType, unitType } = parseSend();
if (!unitType) return;
const divideConfig = parseDivide();
if (divideConfig && divideConfig.mode === 'custom' && divideConfig['#1']) {
const customTotal = parseInt(String(divideConfig['#1']), 10);
if (!isNaN(customTotal) && customTotal > 0) {
applyCustomMode(unitType, customTotal, unitCounts, unitInputs);
return sendType;
}
}
const customRaw = localStorage.getItem(K(villageId, 'Custom'));
if (customRaw && customRaw.trim() !== '') {
const customTotal = parseInt(customRaw, 10);
if (!isNaN(customTotal) && customTotal > 0) {
applyCustomMode(unitType, customTotal, unitCounts, unitInputs);
return sendType;
}
}
if (divideConfig && parseInt(String(divideConfig.count || '1'), 10) > 1 &&
(unitType === 'ram' || unitType === 'catapult' || unitType === 'snob')) {
const percent = parseInt(String(divideConfig['#1'] || '100'), 10);
applyMultiAttackMode(unitType, parseInt(String(divideConfig.count), 10), percent, unitCounts, unitInputs);
return sendType;
}
if (divideConfig && divideConfig.mode === 'percent' && divideConfig['#1']) {
const percent = parseInt(String(divideConfig['#1']), 10);
if (!isNaN(percent)) {
applyDivideMode(unitType, percent, unitCounts, unitInputs);
return sendType;
}
}
const legacyPercent = parseInt(localStorage.getItem(K(villageId, 'Divide#1')) || '100', 10);
applyDivideMode(unitType, legacyPercent, unitCounts, unitInputs);
return sendType;
};
const clickButton = (sendType) => {
const flagKey = K(villageId, 'PlaceClicked');
if (sessionStorage.getItem(flagKey) === '1') return;
const btn = sendType === 'Attack' ? document.getElementById('target_attack') :
sendType === 'Support' ? document.getElementById('target_support') : null;
if (!btn) return;
sessionStorage.setItem(flagKey, '1');
setTimeout(() => {
const form = btn.closest('form');
if (form && form.requestSubmit) form.requestSubmit(btn);
else btn.click();
}, randDelay());
};
const execute = () => {
setTimeout(() => {
const sendType = fillTroops();
setTimeout(() => clickButton(sendType), randDelay());
}, shortDelay());
};
let executed = false;
const tryExecute = () => {
if (executed) return;
if (isReady()) {
executed = true;
observer.disconnect();
clearInterval(poll);
clearTimeout(hard);
execute();
}
};
const observer = new MutationObserver(tryExecute);
observer.observe(document.body, { childList: true, subtree: true });
const poll = setInterval(tryExecute, 20);
const hard = setTimeout(() => {
if (!executed) {
executed = true;
observer.disconnect();
clearInterval(poll);
execute();
}
}, 1200);
}
const qs = new URLSearchParams(location.search);
if (location.pathname.includes('/game.php') && qs.get('screen') === 'place') {
if (qs.get('try') === 'confirm') {
const checkReady = () => !!document.getElementById('troop_confirm_train');
let executed = false;
const tryExecute = () => {
if (executed) return;
if (checkReady()) {
executed = true;
observer.disconnect();
clearInterval(poll);
clearTimeout(hard);
handleConfirmScreen();
}
};
const observer = new MutationObserver(tryExecute);
observer.observe(document.body, { childList: true, subtree: true });
const poll = setInterval(tryExecute, 20);
const hard = setTimeout(() => {
if (!executed) {
executed = true;
observer.disconnect();
clearInterval(poll);
handleConfirmScreen();
}
}, 1500);
} else {
handlePlaceScreen();
}
}
let lastUrl = location.href;
new MutationObserver(() => {
const url = location.href;
if (url !== lastUrl) {
lastUrl = url;
const qs = new URLSearchParams(location.search);
if (location.pathname.includes('/game.php') &&
qs.get('screen') === 'place' &&
qs.get('try') === 'confirm') {
setTimeout(() => {
if (document.getElementById('troop_confirm_train')) handleConfirmScreen();
}, 500);
}
}
}).observe(document, {subtree: true, childList: true});
})();