Greasy Fork is available in English.
None
当前为 
        此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.icu/scripts/552155/1675349/Volvo%27s%20SVS%20Stage%201.js
      
// ==UserScript==
// @name         Volvo's SVS Stage 1
// @namespace    None
// @version      1.17
// @description  None
// @match        https://*.tribalwars.net/game.php?*screen=info_village*
// @match        https://*.tribalwars.net/game.php?*screen=place*
// @grant        none
// ==/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 autoOpen() {
        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(autoOpen, 500);
}
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', start, { once: true });
} else {
  start();
}
})();