WOD AFK Helper

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

// ==UserScript==
// @name         WOD AFK Helper
// @version      1.0.4
// @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*
// @match        *://*.world-of-dungeons.org/wod/spiel/hero/items.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 StateManager {

        static STORAGE_KEY = 'WOD_HELPER_STATE';

        static DEFAULT_STATE = {
            _version: '1.0.4',
            lastStoredDate: 0,
            currentHeroIndex: 0,
            reportCheckTimeout: 0,
            carryingMaxLootHeroes: []
        };

        static get state() {
            const storedData = GM_getValue(this.STORAGE_KEY, {});
            const mergedState = {
                ...this.DEFAULT_STATE,
                ...storedData
            };
            if (mergedState._version !== this.DEFAULT_STATE._version) {
                this.delete();
                return this.DEFAULT_STATE;
            }

            return mergedState;
        }

        static update(value) {
            const newState = {
                ...this.state,
                ...value
            };
            GM_setValue(this.STORAGE_KEY, newState);
        }

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

    class HeroesPageManager {

        constructor() {
            this.handstuffedText = '';
            this.heroRows = null;
            this.nextDungeonDisabledHeroRows = null;
            this.nextDungeonAvailableHeroRows = null;
            this.nextDungeonAvailableHeroDetails = null;
            this.firstCompletedDungeonTime = null;
            this.firstCompletedheroDetails = null;
            this.nextDungeonReducibleHeroRows = null;
            this.submitBtn = document.querySelector('input[type="submit"][name="ok"]');
            this.reduceBtn = document.querySelector('input[name="reduce_dungeon_time"]');
            this.storingText = '入库中';
            this.emptyHandsText = '入库完成';
            this.carryingMaxLootText = '手里拿满了';
            this.unselectedText = '未选择地城';
            this.init();
        }

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

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

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

        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,
            });
        }

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

        storeLoot() {
            const didntStoredToday = () => {
                return new Date().getDate() === StateManager.state.lastStoredDate
            };
            const isEnoughTime = () => {
                return 1000 * 60 * this.heroRows.length > this.calculateTimeRemaining();
            };

            if (!didntStoredToday() && isEnoughTime()) {
                this.storeLootProcess();
                return true;
            }

            return false;
        }

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

            if (this.reduceBtn?.style.display == '') {
                this.reduceBtn.click();
            }

            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, index) => {
                    const newTd = document.createElement('td');
                    console.log({
                        carryingMaxLootHeroes: StateManager.state.carryingMaxLootHeroes,
                        index: index,
                    });

                    if (StateManager.state.carryingMaxLootHeroes.some(obj => obj.heroTableIndex === index)) {
                        newTd.textContent = this.carryingMaxLootText;
                    } else {
                        newTd.textContent = this.emptyHandsText;
                    }
                    tr.appendChild(newTd);
                });
            }

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

            GM_xmlhttpRequest({
                method: 'GET',
                url: '/wod/spiel/hero/items.php',
                onload: (response) => {
                    if (response.status >= 200 && response.status < 300) {
                        if (response.responseText.includes(WOD.carryingMaxLootText)) {
                            const aTag = this.heroRows[currentIndex].querySelector('td:first-child a');
                            const sessionHeroId = new URL(aTag?.href, window.location.href)
                                .searchParams
                                .get('session_hero_id');

                            let carryingMaxLootHeroes = StateManager.state.carryingMaxLootHeroes;
                            carryingMaxLootHeroes.push({
                                sessionHeroId: sessionHeroId,
                                heroTableIndex: currentIndex,
                            });
                            StateManager.update({
                                carryingMaxLootHeroes: carryingMaxLootHeroes
                            });
                            newTd.textContent = this.carryingMaxLootText;
                        } else {
                            newTd.textContent = this.emptyHandsText;
                        }
                        StateManager.update({
                            currentHeroIndex: ++currentIndex
                        });

                        this.storeLootProcess();
                    } else {
                        console.error(`请求失败,状态码: ${response.status}`);
                    }
                },
                onerror: (error) => {
                    console.error('请求发生错误:', error);
                }
            });
        }

        startDungeonsCountDown() {
            if (!this.activeHeroes()) return;
            this.sendReminder();

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

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

                    const newTimeout = StateManager.state.reportCheckTimeout + 5000;
                    StateManager.update({
                        reportCheckTimeout: newTimeout
                    });
                    console.log({
                        newTimeout: newTimeout
                    });
                    this.firstCompletedheroDetails.forEach(row => {
                        row.newTd.textContent = `⏱️等待生成战报,${Math.floor(newTimeout/1000)}秒后重试`;
                    });
                    setTimeout(() => {
                        window.location.reload()
                    }, newTimeout);
                }
            };
            checkTimeout();
        }

        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;
                            StateManager.update({
                                reportCheckTimeout: 0
                            });
                            this.submitBtn.click();
                            return false;
                        }
                    }
                } else if (lastUvHeroIndex == index && !radio.checked) {
                    radio.checked = true;
                    StateManager.update({
                        reportCheckTimeout: 0
                    });
                    this.submitBtn.click();
                    return false;
                }
            });
            return true;
        }

        sendReminder() {
            this.nextDungeonDisabledHeroRows.forEach(row => {
                const newTd = document.createElement('td');
                newTd.className = 'warning';
                newTd.textContent = this.unselectedText;
                row.appendChild(newTd);
            });

            StateManager.state.carryingMaxLootHeroes.forEach(item => {
                const row = this.heroRows[item.heroTableIndex];
                const newTd = document.createElement('td');
                newTd.className = 'warning';
                newTd.textContent = this.carryingMaxLootText;
                row.appendChild(newTd);
            });
        }
    }


    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 ItemsPageManager {

        constructor() {
            this.init();
        }

        init() {
            if (!this.hasText(WOD.carryingMaxLootText)) return;

            console.log('carryingMaxLoot');
            const submitBtn = document.querySelectorAll('input[type=submit][name=ok]');
            const markAsEmptyHands = () => {
                const input = document.querySelector('input[type=hidden][name=session_hero_id]');
                const seesionHeroId = input.value;

                const carryingMaxLootHeroes = StateManager.state.carryingMaxLootHeroes;
                const newCarryingMaxLootHeroes = carryingMaxLootHeroes.filter(item => item.sessionHeroId != seesionHeroId);
                StateManager.update({
                    carryingMaxLootHeroes: newCarryingMaxLootHeroes,
                });
            };

            submitBtn.forEach(btn => {
                btn.addEventListener('click', markAsEmptyHands);
            });

        }

        hasText(text) {
            return document.body.textContent.includes(text);
        }
    }

    class WOD {

        static carryingMaxLootText = '满了'; // TODO: 替换正确文本

        static get pageName() {
            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');
            return pageName;
        }

        static get classMap() {
            return {
                HeroesPageManager,
                VotePageManager,
                ItemsPageManager,
            };
        }

        static AFK() {
            const className = this.pageName[0].toUpperCase() + this.pageName.slice(1) + 'PageManager';
            const DynamicClass = this.classMap[className];
            console.log('Route: ', {
                currentPageName: this.pageName,
                className: className,
                DynamicClass: DynamicClass
            });
            new DynamicClass();
        }
    }

    WOD.AFK();
})();