Greasy Fork

Greasy Fork is available in English.

WOD AFK Helper

1.自动激活最先结束地城的英雄;2.自动加速地城;3.每日访问一次仓库存放战利品

当前为 2025-05-03 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         WOD AFK Helper
// @version      1.0.2
// @description  1.自动激活最先结束地城的英雄;2.自动加速地城;3.每日访问一次仓库存放战利品
// @author       purupurupururu
// @namespace    https://github.com/purupurupururu
// @match        *://*.world-of-dungeons.org/wod/spiel/settings/heroes.php*
// @match        *://*.world-of-dungeons.org/wod/spiel/rewards/vote.php*
// @icon         http://info.world-of-dungeons.org/wod/css/WOD.gif
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function() {
    'use strict';

    // 解析字符串里的时间
    function parseTime(text) {
        if ((/每日|立刻/).test(text)) return 0;

        const match = text.match(/(今天|明天)?\s(\d{2}):(\d{2})/);
        if (!match) throw new Error(`not support string:${text}`);
        const [_, dayPart, hours, minutes] = match;

        const date = new Date();
        if (dayPart === '明天') {
            date.setDate(date.getDate() + 1);
        }
        date.setHours(hours, minutes);

        return date.getTime();
    }

    function getOffsetCountdown(baseTime, offsetSeconds = 60) {
        return Math.floor((baseTime - Date.now()) / 1000) + offsetSeconds;
    }

    function formatTime(seconds) {
        const h = Math.floor(seconds / 3600);
        const m = Math.floor((seconds % 3600) / 60);
        const s = seconds % 60;
        return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
    }

    /////////////////////////////////////////////////////////////////////////////////
    class State{

        static STORAGE_KEY = 'WOD_HELPER_STATE';

        static getState() {
            return GM_getValue(this.STORAGE_KEY, {
                lastStoredDate: 0,
                currentHeroIndex: 0,
                checkTime: true,
            });
        }

        static updateState(updater) {
            const newState = {...this.getState(), ...updater};
            GM_setValue(this.STORAGE_KEY, newState);
            return newState;
        }

        static resetState(){
            GM_deleteValue(this.STORAGE_KEY);
        }
    }

    class HeroesPageManager {

        constructor(){
            this.heroRows = null;
            this.nextDungeonDisabledHeroRows = null;
            this.nextDungeonAvailableHeroRows = null;
            this.nextDungeonAvailableHeroDetails = null;
            this.firstCompletedDungeonTime = null;
            this.firstCompletedheroDetails = null;
            this.submitBtn = document.querySelector('input[type="submit"][name="ok"]');
            this.reduceBtn = document.querySelector('input[name="reduce_dungeon_time"]');

            this.init();
        }

        init() {
            if (!this.inHeroListPageContent()) return;
            if (this.handleReduceBtn()) return;
            this.processHeroList();
            this.monitor();
        }

        inHeroListPageContent(){
            if(document.querySelector('input[name=uv_start]')){
                return true;
            }
            return false;
        }

        handleReduceBtn() {
            if(this.reduceBtn?.style.display == '') {
                this.reduceBtn.addEventListener('click', () => {
                    setTimeout(() => {window.location.reload()}, 1000*3);
                    // TODO: 监控AJAX请求,成功请求后刷新页面
                });
                this.reduceBtn.click();
                return true;
            }
            return false;
        }

        calculateTimeRemaining(){
            return getOffsetCountdown(this.firstCompletedDungeonTime);
        }

        processHeroList() {
            this.heroRows = Array.from(
                document.querySelectorAll('table.content_table > tbody > tr:not(.header)')
            );
            this.nextDungeonDisabledHeroRows = Array.from(
                document.querySelectorAll('table.content_table > tbody > tr:not(.header):not(:has(td:nth-child(5) img))')
            );
            this.nextDungeonAvailableHeroRows = Array.from(
                document.querySelectorAll('table.content_table > tbody > tr:not(.header):has(td:nth-child(5) img)')
            );
            this.nextDungeonAvailableHeroDetails = this.nextDungeonAvailableHeroRows.map(row => ({
                dom: row,
                time: parseTime(row.lastElementChild.textContent),
                owned: row.querySelector('input[type="submit"]') ? false : true
            }));
            this.firstCompletedDungeonTime = Math.min(
                ...this.nextDungeonAvailableHeroDetails.map(h => h.time)
            );
            this.firstCompletedheroDetails = this.nextDungeonAvailableHeroDetails.filter(
                h => h.time == this.firstCompletedDungeonTime
            );
            console.log('processHeroList: ',{
                heroRows: this.heroRows,
                nextDungeonDisabledHeroRows: this.nextDungeonDisabledHeroRows,
                nextDungeonAvailableHeroRows: this.nextDungeonAvailableHeroRows,
                nextDungeonAvailableHeroDetails: this.nextDungeonAvailableHeroDetails,
                firstCompletedDungeonTime: this.firstCompletedDungeonTime,
                firstCompletedheroDetails: this.firstCompletedheroDetails,
            });
        }

        sendReminder(){
            this.nextDungeonDisabledHeroRows.forEach(row => {
                const newTd = document.createElement('td');
                newTd.className = 'warning';
                newTd.textContent = '未选择地城';
                row.appendChild(newTd);
            });
        }

        activeHeroes(){
            // deselect all checkbox
            this.heroRows.forEach(tr => {
                const checkbox = tr.querySelector('input[type="checkbox"]');
                if(checkbox && checkbox.checked){
                    checkbox.checked = false;
                    console.log('deselect: ', checkbox);
                }
            });

            let lastOwnedHero = null;
            let lastOwnedHeroIndex = -1;
            for (let i = this.firstCompletedheroDetails.length - 1; i >= 0; i--) {
                if (this.firstCompletedheroDetails[i].owned) {
                    lastOwnedHero = this.firstCompletedheroDetails[i];
                    lastOwnedHeroIndex = i;
                    break;
                }
            }
            console.log({
                lastOwnedHero: lastOwnedHero,
                lastOwnedHeroIndex: lastOwnedHeroIndex,
            });

            let lastUvHero = null;
            let lastUvHeroIndex = -1;
            for (let i = this.firstCompletedheroDetails.length - 1; i >= 0; i--) {
                if (!this.firstCompletedheroDetails[i].owned) {
                    lastUvHero = this.firstCompletedheroDetails[i];
                    lastUvHeroIndex = i;
                    break;
                }
            }
            console.log({
                lastUvHero: lastUvHero,
                lastUvHeroIndex: lastUvHeroIndex,
            });

            this.firstCompletedheroDetails.forEach((row, index) => {
                const checkbox = row.dom.querySelector('input[type="checkbox"]');
                const radio = row.dom.querySelector('input[type="radio"][name="FIGUR"]');
                if(lastOwnedHero){
                    if(row.owned && checkbox) {
                        checkbox.checked = true;
                        console.log('seleted: ', checkbox);
                        let checkboxNotActivated = false;
                        if(row.dom.querySelector('.hero_inactive')){
                            checkboxNotActivated = true;
                        }
                        if(lastOwnedHeroIndex == index && !radio.checked || checkboxNotActivated){
                            radio.checked = true;
                            this.submitBtn.click();
                        }
                    }
                }else{
                    if(lastUvHeroIndex == index && !radio.checked){
                        radio.checked = true;
                        this.submitBtn.click();
                    }
                }
            });
        }

        storeLoot(){
            let currentIndex = State.getState().currentHeroIndex;

            if (currentIndex >= this.heroRows.length) {
                console.log('所有英雄处理完毕');
                State.updateState({
                    lastStoredDate: new Date().getDate(),
                    currentHeroIndex: 0,
                });
                window.location.reload();
                console.log(State.getState());
                return;
            }

            const radio = this.heroRows[currentIndex].querySelector('input[type=radio]');
            if(radio && !radio.checked){
                radio.checked = true;
                this.submitBtn.click();
                return;
            }

            if(currentIndex > 0){
                this.heroRows.slice(0, currentIndex).forEach(tr => {
                    const newTd = document.createElement('td');
                    newTd.textContent = '入库完成';
                    tr.appendChild(newTd);
                });
            }

            const newTd = document.createElement('td');
            newTd.textContent = '入库中';
            this.heroRows[currentIndex].appendChild(newTd);

            GM_xmlhttpRequest({
                method: 'GET',
                url: '/wod/spiel/hero/items.php',
                onload: (response) => {
                    if (response.status >= 200 && response.status < 300) {
                        const buildTextContent = (res) => {
                            // TODO: 正则获取关键字判断手上的战利品是不是满了
                        };
                        State.updateState({currentHeroIndex: ++currentIndex});
                        newTd.textContent = '入库完成';
                        this.storeLoot();
                    } else {
                        console.error(`请求失败,状态码: ${response.status}`);
                    }
                },
                onerror: (error) => {
                    console.error('请求发生错误:', error);
                }
            });
        }

        monitor() {
            // 战利品入库
            const didntStoredToday = () => (new Date().getDate() === State.getState().lastStoredDate);
            const isEnoughTime = () => (1000*60*this.heroRows.length > this.calculateTimeRemaining());
            if(!didntStoredToday() && isEnoughTime()){
                this.storeLoot();
                return;
            }

            // 地城倒计时
            this.sendReminder();
            this.activeHeroes();
            this.startCountdonw();
        }

        startCountdonw(){
            this.firstCompletedheroDetails = this.firstCompletedheroDetails.map(row => ({
                ...row,
                display: document.createElement('td')
            }))
            this.firstCompletedheroDetails.forEach(row => {
                row.dom.appendChild(row.display)
            });

            let timeoutId = null;
            const checkTimeout = () => {
                const countdown = this.calculateTimeRemaining();
                if(countdown > 0){
                    this.firstCompletedheroDetails.forEach(row => {
                        row.display.innerHTML = '⏱️'+ formatTime(countdown);
                    });
                    timeoutId = setTimeout(checkTimeout, 1000);
                }else{
                    clearTimeout(timeoutId);
                    window.location.reload();
                }
            };
            checkTimeout();
        }
    }

    class VotePageManager{
        constructor(){
            this.currentVote = null;
            this.init();
        }

        init(){
            this.processVoteList();
            this.refreshAtMidnight();
            this.monitor();
        }

        extractJsUrls(a){
            const onclickAttr = a?.getAttribute('onclick');
            if(!onclickAttr) return null;
            const match = onclickAttr.match(/js_goto_url\('([^']+)'/);
            return match ? match[1] : null;
        }

        processVoteList(){
            const imgList = Array.from(document.querySelectorAll('div.vote.reward img[title=荣誉]'))
            .map(row =>({
                dom: row,
                url: this.extractJsUrls(row.closest('div.vote.reward').previousElementSibling.querySelector('a')),
                time: parseTime(row.parentElement.textContent)
            }));
            const minItem = imgList.reduce((min, current) => {
                if (!min || current.time < min.time) return current;
                return min;
            }, null);

            this.currentVote = minItem;
        }

        refreshAtMidnight(){
            const midnight = new Date();
            midnight.setHours(24, 0, 0, 0);
            const checkTimeout = () => {
                const remainingtime = getOffsetCountdown(midnight.getTime(), 0);
                remainingtime > 0 ? setTimeout(checkTimeout, 1000) : window.location.reload();
            }
            checkTimeout();
        }

        monitor(){
            const newSpan = document.createElement('span');
            this.currentVote.dom.parentElement.appendChild(newSpan);

            const checkTimeout = () => {
                const remainingtime = getOffsetCountdown(this.currentVote.time);
                if (remainingtime > 0) {
                    setTimeout(checkTimeout, 1000);
                    newSpan.innerHTML = ' ⏱️' + formatTime(remainingtime);
                }else{
                    window.location = this.currentVote.url;
                }
            }
            checkTimeout();
        }
    }

    class WOD{

        static AFK(){
            const path = window.location.pathname;
            const match = path.match(/\/([^\/]+?)\.php$/);
            const pageName = match ? match[1] : '';

            if (typeof pageName !== 'string' || pageName.length === 0) throw new Error('not support current page name');
            const classMap = { HeroesPageManager, VotePageManager };
            const className = pageName[0].toUpperCase() + pageName.slice(1) + 'PageManager';
            const DynamicClass = classMap[className];
            console.log('Route: ',{
                currentPathName: path,
                currentPageName: pageName,
                className: className,
                DynamicClass: DynamicClass
            });
            new DynamicClass();
        }
    }

    WOD.AFK();
})();