Volvo's SVS

Single Village Snipe

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.gf.qytechs.cn/scripts/552155/1676640/Volvo%27s%20SVS.js

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Volvo's SVS
// @namespace    https://greasyfork.org/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});
})();