Greasy Fork

来自缓存

Greasy Fork is available in English.

WOD AFK Helper

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

(function() {
    'use strict';

    /**
     * 解析时间字符串为时间戳
     * @param {string} text - 支持格式:
     *   1. "明天 02:34"
     *   2. "03:22"
     *   3. "今天 11:22"
     *   4. "你可以再次获得 5 : 明天 17:14"
     * @returns {number} 时间戳(毫秒)
     */
    function parseTime(text) {
        if ((/每日|立刻/).test(text)) return 0;
        // 匹配时间部分:可选前缀(今天/明天+空格) + 时间(HH:mm)
        const match = text.match(/(明天 |今天 )?(\d{2}:\d{2})$/);
        if (!match) throw new Error(`不支持的时间格式: '${text}'`);

        // 提取时间部分(如"02:34")
        const [_, prefix, timeStr] = match;

        // 拆分小时和分钟
        const [hours, minutes] = timeStr.split(':');

        const date = new Date();
        date.setHours(hours, minutes, 0, 0);

        // 处理"明天"的情况
        if (prefix === "明天 ") {
            date.setDate(date.getDate() + 1);
        }

        return date.getTime();
    }

    function formatTime(microSeconds) {
        const seconds = Math.floor(microSeconds / 1000);
        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 Status {
        static DEPOSIT_SUCCESS = '入库成功';
        static DEPOSIT_PACKAGE_FULL = '背包满了';
        static DEPOSIT_PROCESSING = '入库中';

        static VOTE_QUERYING = '查询中';
        static VOTE_READY = '已准备好';
    }

    class ScriptStorageManager {

        static getDefaultValues() {
            const defaultValues = {
                scriptVersion: '1.1.2',
                actionName: HeroesPageManager.ACTION_DEFAULT,
                lastDepositDate: 0, // 最后入库的日期
                currentHeroIndex: 0, // 入库操作,当前操作的英雄列表索引
                checkReportTimestamp: 0, // 检查战报的时间
                overburdenedHeroes: [] // 战利品超载的英雄列表 {heroId, timestamp}
            };
            return defaultValues;
        }

        static set(key, value) {
            GM_setValue(key, value);
        }

        static get(key) {
            const currentVersion = GM_getValue('scriptVersion');
            const defaultValues = this.getDefaultValues();

            if (currentVersion !== defaultValues.scriptVersion) {
                const honoreeHeroId = GM_getValue('honoreeHeroId');
                const allKeys = GM_listValues().filter(k => k !== 'honoreeHeroId');
                allKeys.forEach(k => GM_deleteValue(k));
                if (honoreeHeroId !== undefined) {
                    GM_setValue('honoreeHeroId', honoreeHeroId);
                }
                GM_setValue('scriptVersion', defaultValues.scriptVersion);
            }
            return GM_getValue(key, defaultValues[key]);
        }

        static getAll() {
            const storedKeys = GM_listValues();
            const allValues = {};

            storedKeys.forEach(key => {
                allValues[key] = GM_getValue(key);
            });

            return allValues;
        }

        static deleteAll() {
            Object.keys(GM_listValues()).forEach(key => {
                GM_deleteValue(key);
            });
        }

        static getVersion() {
            return this.get('scriptVersion');
        }

        static getActionName() {
            return this.get('actionName');
        }

        static setActionName(value) {
            return this.set('actionName', value);
        }

        static getCheckReportTimestamp() {
            return this.get('checkReportTimestamp');
        }

        static setCheckReportTimestamp(value) {
            this.set('checkReportTimestamp', value);
        }

        static getHonoreeHeroId() {
            return this.get('honoreeHeroId');
        }

        static setHonoreeHeroId(value) {
            this.set('honoreeHeroId', value);
        }

        static getCurrentHeroIndex() {
            return this.get('currentHeroIndex');
        }

        static setCurrentHeroIndex(value) {
            this.set('currentHeroIndex', value);
        }

        static getLastDepositDate() {
            return this.get('lastDepositDate');
        }

        // 记录入库时间
        static recordDepositDate() {
            this.set('lastDepositDate', new Date().getDate());
        }

        static getOverburdenedHeroes() {
            return this.get('overburdenedHeroes');
        }

        // 添加超载英雄
        static markHeroAsOverburdened(heroId) {
            const heroes = this.get('overburdenedHeroes');
            const existingHero = heroes.find(h => h.heroId === heroId);
            if (existingHero) {
                existingHero.timestamp = Date.now();
            } else {
                heroes.push({
                    heroId,
                    timestamp: Date.now()
                });
            }
        }

        // 清除超载英雄标记(入库成功后、清理包裹后调用)
        static clearOverburdenedStatus(heroId) {
            const heroes = this.get('overburdenedHeroes').filter(
                h => h.heroId !== heroId
            );
            this.set('overburdenedHeroes', heroes);
        }
    }

    class HeroesPageManager {

        static ACTION_DEFAULT = 'default';
        static ACTION_DEPOSIT = 'deposit';
        static ACTION_VOTE = 'vote';

        static get ACTIONS() {
            return [
                HeroesPageManager.ACTION_DEPOSIT,
                HeroesPageManager.ACTION_VOTE,
            ];
        }

        constructor() {
            this.heroTable = new HeroTable(document.querySelector('table.content_table'));
            this.reduceBtn = document.querySelector('input[name="reduce_dungeon_time"]');
            this.submitBtn = document.querySelector('input[name="ok"]');
            this.actionName = ScriptStorageManager.getActionName();
        }

        execute() {
            this.runAction(this.actionName);
        }

        runAction(name) {
            switch (name) {
                case HeroesPageManager.ACTION_DEPOSIT:
                    return this.runDeposit();
                case HeroesPageManager.ACTION_VOTE:
                    return this.runVote();
                default:
                    return this.runDefault();
            }
        }

        runDefault() {
            if (!this.isHeroTablePage()) return;
            if (this.dungeonReduce()) return;
            this.dungeonMonitor();
            this.depositMonitor();
            this.voteMonitor();
        }

        dungeonMonitor() {
            const heroTable = this.heroTable;
            const minDungeonEndTimeRows = heroTable.find().whereClassIsNotMentor().whereMinDungeonEndTime().get();
            if (!this.areHeroesOnline(minDungeonEndTimeRows)) {
                this.setHeroesOnline(minDungeonEndTimeRows);
                return;
            }
            if (minDungeonEndTimeRows.length === 1 && !minDungeonEndTimeRows[0].isActive) {
                this.activeHero(minDungeonEndTimeRows[0]);
            }

            heroTable.addColumn(HeroTable.TH_DUNGEON_COUNTDOWN);
            const countdownColumnIndex = heroTable.indexOfTableHead(HeroTable.TH_DUNGEON_COUNTDOWN);
            heroTable.find().whereNextDungeonIsNull().get().forEach(
                row => row.showOn(countdownColumnIndex, HeroTable.DUNGEON_REQUIRE_TEXT)
            );
            const checkTimeout = () => {
                let countdown = minDungeonEndTimeRows[0].nextDungeon.endTimeCountdown();
                let countdownTimer = formatTime(countdown);
                if (countdown > 0) {
                    minDungeonEndTimeRows.forEach(row => row.showOn(countdownColumnIndex, countdownTimer));
                    setTimeout(checkTimeout, 1000);
                    return;
                }

                countdown = ScriptStorageManager.getCheckReportTimestamp() - Date.now();
                countdownTimer = formatTime(countdown);
                if (countdown > 0) {
                    minDungeonEndTimeRows.forEach(row => row.showOn(countdownColumnIndex, `${HeroTable.DUNGEON_STATUS_GENERATING_REPORT} ${countdownTimer}`));
                    setTimeout(checkTimeout, 1000);
                    return;
                }

                ScriptStorageManager.setCheckReportTimestamp(Date.now() + 60 * 1000);
                window.location.reload();
            }
            checkTimeout();
        }

        depositMonitor() {
            const hasDepositedToday = () => {
                return new Date().getDate() === ScriptStorageManager.getLastDepositDate();
            }
            const isTimeEnough = () => {
                const hero = this.heroTable.find().whereClassIsNotMentor().first();
                const countdown = hero.nextDungeon.endTimeCountdown();
                const timeCostPerHero = 5 * 60 * 1000;
                return countdown > timeCostPerHero * this.heroTable.findAll().length;
            }
            const canDeposit = () => {
                return !hasDepositedToday() && isTimeEnough();
            }
            if (canDeposit()) {
                ScriptStorageManager.setActionName(HeroesPageManager.ACTION_DEPOSIT);
                window.location.reload();
            }
        }

        voteMonitor() {
            const heroTable = this.heroTable;
            heroTable.addVoteCoumn(HeroTable.TH_VOTE);
            const voteColumnsIndex = heroTable.indexOfTableHead(HeroTable.TH_VOTE);
            const rows = heroTable.findAll();
            heroTable.dom.addEventListener('change', e => {
                if (e.target?.matches('input[name=vote]')) {
                    ScriptStorageManager.setHonoreeHeroId(e.target.value);
                    rows.forEach(row => row.showOn(voteColumnsIndex, '', {
                        selector: '.vote.countdown'
                    }));
                    window.location.reload();
                }
            })

            const honoreeHeroId = ScriptStorageManager.getHonoreeHeroId();
            const honoree = heroTable.find().byId(honoreeHeroId);
            if (honoree === undefined) {
                rows.forEach(row => row.showOn(voteColumnsIndex, Status.VOTE_QUERYING, {
                    selector: '.vote.countdown'
                }));
            } else {
                honoree.showOn(voteColumnsIndex, Status.VOTE_QUERYING, {
                    selector: '.vote.countdown'
                });
            }

            GM_xmlhttpRequest({
                method: 'GET',
                url: '/wod/spiel/rewards/vote.php',
                timeout: 2 * 60 * 1000,
                onload: (response) => {
                    try {
                        const votePage = this.getVoteServiceFromResponse(response);
                        const canVote = () => {
                            const hero = heroTable.find().whereClassIsNotMentor().whereNextDungeonIsNotNull().first();
                            const countdown = hero.nextDungeon.endTimeCountdown();
                            const timeCostPerUrl = 5 * 60 * 1000;
                            return countdown > timeCostPerUrl * votePage.findAll().length;
                        }
                        // 轮询检查倒计时
                        const checkTimeout = () => {
                            const event = votePage.findOneByMaxRewardTime();
                            const countdown = event.nextRewardTimeCountdown();

                            if (countdown > 0) {
                                // 更新倒计时显示
                                const displayText = formatTime(countdown);
                                if (honoree === undefined) {
                                    rows.forEach(row =>
                                        row.showOn(voteColumnsIndex, displayText, {
                                            selector: '.vote.countdown'
                                        })
                                    );
                                } else {
                                    honoree.showOn(voteColumnsIndex, displayText, {
                                        selector: '.vote.countdown'
                                    });
                                }

                                // 继续轮询
                                setTimeout(checkTimeout, 1000);
                                return;
                            }

                            // 倒计时结束,投票就绪
                            console.log('投票已准备好');
                            if (honoree === undefined) {
                                rows.forEach(row =>
                                    row.showOn(voteColumnsIndex, Status.VOTE_READY, {
                                        selector: '.vote.countdown'
                                    })
                                );
                            } else {

                                if (canVote()) {
                                    ScriptStorageManager.setActionName(HeroesPageManager.ACTION_VOTE);
                                    window.location.reload();
                                }
                            }
                        }

                        // 启动轮询
                        checkTimeout();

                    } catch (error) {
                        console.error("解析投票页面失败:", error);
                    }
                },
                onerror: (error) => {
                    console.error("请求投票页面失败:", error);
                },
                ontimeout: () => {
                    console.error("请求投票页面超时");
                }
            });
        }

        runDeposit() {
            const heroTable = this.heroTable;
            heroTable.addColumn(HeroTable.TH_DEPOSITE_STATUS);
            const rows = heroTable.find().whereClassIsNotMentor().get();
            const depositColIndex = heroTable.indexOfTableHead(HeroTable.TH_DEPOSITE_STATUS);

            console.group('开始入库战利品');
            const depositProcess = () => {
                const currentHeroindex = ScriptStorageManager.getCurrentHeroIndex();
                const currentHero = rows[currentHeroindex];
                console.log('当前英雄:', currentHero);
                console.log(`当前进度:${currentHeroindex}/${rows.length}`);

                // 检查是否完成所有英雄
                if (currentHeroindex >= rows.length) {
                    ScriptStorageManager.recordDepositDate();
                    ScriptStorageManager.setCurrentHeroIndex(0);
                    ScriptStorageManager.setActionName(HeroesPageManager.ACTION_DEFAULT);
                    window.location.reload();
                    return;
                }

                // 激活当前英雄
                if (!currentHero.isActive) {
                    this.activeHero(currentHero);
                    return;
                }

                // 更新已完成英雄的存储状态
                if (currentHeroindex > 0) {
                    rows.slice(0, currentHeroindex).forEach((row) => {
                        const status = ScriptStorageManager.getOverburdenedHeroes().some(obj => obj.heroId === row.id) ?
                            Status.DEPOSIT_PACKAGE_FULL : Status.DEPOSIT_SUCCESS;
                        row.showOn(depositColIndex, status);
                    });
                }

                // 显示存储状态
                currentHero.showOn(depositColIndex, Status.DEPOSIT_PROCESSING);

                GM_xmlhttpRequest({
                    method: 'GET',
                    url: '/wod/spiel/hero/items.php',
                    timeout: 5 * 60 * 1000,
                    onload: (response) => {
                        response.responseText;
                        const docItem = new DOMParser().parseFromString(response.responseText, 'text/html');
                        const lastDepositTime = docItem.querySelectorAll('#main_content table.content_table tr')[2].querySelectorAll('td')[5].textContent.trim();
                        console.log('最后物品入库时间: ', lastDepositTime);
                        try {
                            const isPackageFull = response.responseText.includes(ItemsPageManager.PACKAGE_FULL_DESCRIPTION);

                            if (isPackageFull) {
                                currentHero.showOn(depositColIndex, Status.DEPOSIT_PACKAGE_FULL);
                                ScriptStorageManager.markHeroAsOverburdened(currentHero.sessionId);
                            } else {
                                currentHero.showOn(depositColIndex, Status.DEPOSIT_SUCCESS);
                                ScriptStorageManager.clearOverburdenedStatus(currentHero.sessionId);
                            }

                            ScriptStorageManager.setCurrentHeroIndex(currentHeroindex + 1);
                            depositProcess();
                        } catch (error) {
                            console.error("响应处理失败:", error);
                            currentHero.showOn(depositColIndex, "❌ 处理错误");
                            window.location.reload();
                        }
                    },
                    onerror: (error) => {
                        console.error("请求失败:", error);
                        currentHero.showOn(depositColIndex, "❌ 网络错误");
                        setTimeout(window.location.reload(), 3000);
                    },
                    ontimeout: () => {
                        console.error("请求超时");
                        currentHero.showOn(depositColIndex, "⏱️ 请求超时");
                        setTimeout(window.location.reload(), 3000);
                    }
                });
            };

            depositProcess();
        }

        runVote() {
            const heroTable = this.heroTable;
            heroTable.addTableHead(HeroTable.TH_VOTE);
            heroTable.findAll().forEach(row => row.addTd());
            const voteColumnsIndex = heroTable.indexOfTableHead(HeroTable.TH_VOTE);
            const honoree = heroTable.find().byId(ScriptStorageManager.getHonoreeHeroId());

            if (!honoree.isActive) {
                this.activeHero(honoree);
                return;
            }

            let totalFame = 0;
            let completed = 0;
            let success = 0;
            let failed = 0;
            let totalUrls = 0;

            const updateStatus = () => {
                honoree.showOn(
                    voteColumnsIndex,
                    `投票中 (${completed}/${totalUrls}) ${totalFame}<img src="/wod/css/skins/skin-4/images/icons/lang/cn/fame.gif" alt="荣誉">`, {
                        mode: 'html'
                    }
                );
            };

            const handleVoteResponse = (url, response) => {
                try {
                    const votePage = this.getVoteServiceFromResponse(response);
                    const event = votePage.findOneByTrackedRedirectionUrl(url);
                    console.log('handleVoteResponse', event);

                    if (event && event.fame) {
                        totalFame += event.fame;
                        console.log(`✅ 投票成功: ${url}, 获得 ${event.fame} 荣誉`);
                        success++;
                    } else {
                        console.warn(`⚠️ 未找到荣誉值: ${url}`, event);
                        failed++;
                    }
                } catch (e) {
                    console.error(`❌ 解析响应失败: ${url}`, e);
                    failed++;
                } finally {
                    completed++;
                    updateStatus();
                }
            };

            const handleVoteError = (url, error) => {
                console.error(`❌ 投票失败: ${url}`, error);
                failed++;
                completed++;
                updateStatus();
            };

            const checkCompletion = () => {
                if (completed >= totalUrls) {
                    const resultText = `完成 (成功:${success} 失败:${failed}) ${totalFame}<img src="/wod/css/skins/skin-4/images/icons/lang/cn/fame.gif" alt="荣誉">`;
                    honoree.showOn(voteColumnsIndex, resultText, {
                        mode: 'html'
                    });
                    ScriptStorageManager.setActionName(HeroesPageManager.ACTION_DEFAULT);

                    console.log('所有投票完成,3秒后刷新');
                    setTimeout(() => window.location.reload(), 3000);
                }
            };

            // 显示初始状态
            honoree.showOn(voteColumnsIndex, Status.VOTE_QUERYING);

            // 发起请求
            GM_xmlhttpRequest({
                method: 'GET',
                url: '/wod/spiel/rewards/vote.php',
                timeout: 1 * 60 * 1000,
                onload: (response) => {
                    try {
                        const votePage = this.getVoteServiceFromResponse(response);
                        const urls = votePage.findAll().map(event => event.trackedRedirectionUrl);
                        totalUrls = urls.length;
                        console.log(urls);
                        // 初始状态
                        updateStatus();

                        // 发起所有投票请求
                        urls.forEach(url => {
                            GM_xmlhttpRequest({
                                method: 'GET',
                                url: url,
                                timeout: 1 * 60 * 1000,
                                onload: (resp) => {
                                    handleVoteResponse(url, resp);
                                    checkCompletion();
                                },
                                onerror: (error) => {
                                    handleVoteError(url, error);
                                    checkCompletion();
                                },
                                ontimeout: () => {
                                    handleVoteError(url, new Error(`请求超时 (120s)`));
                                    checkCompletion();
                                }
                            });
                        });
                    } catch (e) {
                        console.error('解析投票页面失败:', e);
                        honoree.showOn(voteColumnsIndex, `❌ 解析错误: ${e.message}`);
                        setTimeout(() => window.location.reload(), 10 * 1000);
                    }
                },
                onerror: (error) => {
                    console.error('获取投票页面失败:', error);
                    honoree.showOn(voteColumnsIndex, `❌ 网络错误: ${error.statusText || error.message}`);
                    setTimeout(() => window.location.reload(), 10 * 1000);
                },
                ontimeout: () => {
                    console.error('获取投票页面超时');
                    honoree.showOn(voteColumnsIndex, '❌ 获取投票超时');
                    setTimeout(() => window.location.reload(), 10 * 1000);
                }
            });
        }

        getVoteServiceFromResponse(response) {
            const doc = new DOMParser().parseFromString(response.responseText, 'text/html');
            return new VoteService(doc);
        }

        activeHero(row) {
            row.radio.checked = true;
            this.submitBtn.click();
        }

        areHeroesOnline(rows) {
            return !rows.some(row => row.isOnline === false);
        }

        setHeroesOnline(rows) {
            this.heroTable.findAll().forEach(row => {
                row.deselected();
            });

            let lastHero = null;
            rows.forEach(row => {
                row.selected();
                if (row.isOwnerShipDirect) {
                    lastHero = row;
                }
            });

            ScriptStorageManager.checkReportTimestamp = 0;
            this.activeHero(lastHero);
        }

        isHeroTablePage() {
            return document.querySelector('input[name=uv_start]') ? true : false;
        }

        dungeonReduce(reload = true) {
            if (reload) {
                this._reduceBtnMonitor();
            }
            if (this.reduceBtn) {
                this.reduceBtn.click();
                return true;
            }
            return false;
        }

        _reduceBtnMonitor() {
            const reduce_URL = /\/wod\/ajax\/setPlayerSetting\.php?.+/;

            const originalXHROpen = XMLHttpRequest.prototype.open;
            const originalXHRSend = XMLHttpRequest.prototype.send;

            XMLHttpRequest.prototype.open = function(method, url) {
                this._xhrId = Math.random().toString(36).slice(2, 9);
                this._requestUrl = url;
                console.log(`XHR初始化 [${this._xhrId}]`, method, url);
                return originalXHROpen.apply(this, arguments);
            };

            XMLHttpRequest.prototype.send = function(body) {
                const xhr = this;
                const url = xhr._requestUrl;

                console.log(`XHR发送请求 [${xhr._xhrId}]`, url);

                if (reduce_URL.test(url)) {
                    console.log(`匹配到目标请求 [${xhr._xhrId}]`, url);

                    xhr.addEventListener('readystatechange', function() {
                        console.log(`状态变化 [${xhr._xhrId}]`, {
                            readyState: xhr.readyState,
                            status: xhr.status,
                            headers: xhr.getAllResponseHeaders()
                        });

                        if (xhr.readyState === 4) {
                            console.log(`请求完成 [${xhr._xhrId}]`, {
                                status: xhr.status,
                                response: xhr.responseText,
                                headers: xhr.getAllResponseHeaders()
                            });

                            if (xhr.responseText.includes('END_OF_HEADER')) {
                                console.log(`检测到特殊标记 [${xhr._xhrId}]`);
                                setTimeout(() => {
                                    console.log(`执行页面刷新 [${xhr._xhrId}]`);
                                    location.reload();
                                }, 300);
                            }
                        }
                    });

                    xhr.addEventListener('error', function(e) {
                        console.log(`请求错误 [${xhr._xhrId}]`, e);
                    });

                    xhr.addEventListener('timeout', function(e) {
                        console.log(`请求超时 [${xhr._xhrId}]`, e);
                    });
                }

                return originalXHRSend.apply(this, arguments);
            };

            window.addEventListener('beforeunload', function() {
                console.log('页面即将刷新/关闭');
            });
        }
    }

    class HeroTable {

        static TH_DUNGEON_COUNTDOWN = '倒计时';
        static TH_VOTE = '投票奖励';
        static TH_DEPOSITE_STATUS = '入库状态';
        static DUNGEON_STATUS_GENERATING_REPORT = '等待结算';
        static DUNGEON_REQUIRE_TEXT = '未选择地城';

        constructor(dom) {
            this.dom = dom;
        }

        findAll() {
            return Array.from(this.dom.querySelectorAll('tr:not(.header)')).map(
                (row, index) => new HeroRow(row, index)
            );
        }

        find() {
            return new HeroTableQueryBuilder(this.findAll());
        }

        addTableHead(tableHead) {
            const headRow = this.dom.querySelector('tr.header');
            const th = document.createElement('th');
            th.textContent = tableHead;
            headRow.appendChild(th);
        }

        indexOfTableHead(name) {
            const headerRow = this.dom.querySelectorAll('tr.header th');
            if (!headerRow) return -1;
            const headers = Array.from(headerRow);
            const index = headers.findIndex(th => th.textContent.trim() === name);
            return index;
        }

        addColumn(tableHead) {
            this.addTableHead(tableHead);
            this.findAll().forEach(row => row.addTd());
        }

        addVoteCoumn(tableHead) {
            this.addTableHead(HeroTable.TH_VOTE);
            const rows = this.findAll();
            const nodes = (row) => {
                const input = document.createElement('input');
                input.type = 'radio';
                input.name = 'vote';
                input.value = row.id;
                if (ScriptStorageManager.getHonoreeHeroId() === input.value) {
                    input.checked = true;
                }
                const span = document.createElement('span');
                span.className = 'vote countdown';
                return [input, span];
            }
            rows.forEach(row => row.addTd(nodes(row)));
        }
    }

    class HeroTableQueryBuilder {

        constructor(rows) {
            this.rows = rows;
        }

        byId(id) {
            return this.rows.find(row => row.id === id);
        }

        whereNextDungeonIsNull() {
            this.rows = this.rows.filter(row => !row.nextDungeon?.name);
            return this;
        }

        whereNextDungeonIsNotNull() {
            this.rows = this.rows.filter(row => row.nextDungeon?.name);
            return this;
        }

        whereMinDungeonEndTime() {
            if (this.rows.length === 0) return this;

            const minTime = Math.min(
                ...this.rows.map(row => row.nextDungeon?.estimatedTimestamp)
            );
            this.rows = this.rows.filter(
                row => row.nextDungeon?.estimatedTimestamp === minTime
            );
            return this;
        }

        whereClassIsNotMentor() {
            this.rows = this.rows.filter(row => row.class !== '导师');
            return this;
        }

        get() {
            return this.rows;
        }

        first() {
            return this.rows[0];
        }
    }

    class HeroRow {

        constructor(row, index) {
            this.dom = row;
            this.index = index;
            this.id = (() => {
                const aTag = this.dom.querySelector('td:first-child a');
                return new URL(aTag?.href).searchParams.get('id');
            })();
            this.radio = this.dom.querySelector('input[type=radio][name=FIGUR]');
            this.name = this.dom.querySelector('td:nth-child(1)').innerText.trim();
            this.class = this.dom.querySelector('td:nth-child(2)').innerText.trim();
            this.level = this.dom.querySelector('td:nth-child(3)').innerText.trim();
            const fourthTd = this.dom.querySelector('td:nth-child(4)');
            this.checkbox = fourthTd?.querySelector('input[type="checkbox"]');
            this.isActive = this.id === document.querySelector('input[type=hidden][name=session_hero_id]').value;
            this.isOwnerShipDirect = fourthTd?.querySelector('input[type="submit"]') ? false : true;
            this.isOnline = (() => {
                if (!this.isOwnerShipDirect) return true;
                return this.dom.querySelector('.hero_inactive') ? false : true;
            })();
            this.nextDungeon = new NextDungeon(this.dom.querySelector('td:nth-child(5)'));
        }

        get cells() {
            return this.dom.querySelectorAll('td');
        }

        selected() {
            if (this.checkbox) {
                this.checkbox.checked = true;
            }
        }

        deselected() {
            if (this.checkbox) {
                this.checkbox.checked = false;
            }
        }

        addTd(content = null, options = {}) {
            const td = document.createElement('td');

            if (options.className) {
                td.className = options.className;
            }

            if (options.attributes) {
                Object.entries(options.attributes).forEach(([key, value]) => {
                    td.setAttribute(key, value);
                });
            }

            if (content !== null && content !== undefined) {
                const processContent = (item) => {
                    if (item instanceof Node) {
                        return item;
                    }
                    if (typeof item === 'string' && item.startsWith('<')) {
                        const wrapper = document.createElement('div');
                        wrapper.innerHTML = item;
                        return wrapper.firstChild || document.createTextNode('');
                    }
                    return document.createTextNode(String(item));
                };

                if (Array.isArray(content)) {
                    content.forEach(item => {
                        const node = processContent(item);
                        td.appendChild(node);
                    });
                } else {
                    const node = processContent(content);
                    td.appendChild(node);
                }
            }

            this.dom.appendChild(td);
            return td;
        }


        /**
         * 在表格单元格或其子元素上显示内容
         *
         * @param {number} tdIndex - 目标单元格在 tdList 中的索引
         * @param {string|Node|null} content - 要显示的内容(支持HTML字符串或DOM节点)
         * @param {Object} [options={}] - 配置选项
         * @param {string} [options.selector=null] - CSS选择器,用于定位单元格内的子元素
         * @param {'text'|'html'|'replace'|'append'|'prepend'} [options.mode='html'] - 显示模式:
         *   - 'text': 作为纯文本插入(自动转义HTML标签)
         *   - 'html': 作为HTML解析插入(渲染标签)
         *   - 'replace': 替换整个目标元素
         *   - 'append': 在目标元素末尾插入
         *   - 'prepend': 在目标元素开头插入
         *
         * @example
         * // 案例1:在单元格内显示纯文本
         * showOn(0, 'Hello World', { mode: 'text' });
         *
         * @example
         * // 案例2:在特定子元素中渲染HTML
         * showOn(1, '<b>Strong</b> Text', {
         *   selector: '.content-area',
         *   mode: 'html'
         * });
         *
         * @example
         * // 案例3:替换整个子元素
         * const newNode = document.createElement('div');
         * newNode.textContent = 'Replaced';
         * showOn(2, newNode, {
         *   selector: '.old-element',
         *   mode: 'replace'
         * });
         */
        showOn(tdIndex, content, options = {}) {
            const td = this.cells[tdIndex];
            if (!td) {
                console.warn(`表格单元格索引 ${tdIndex} 不存在`);
                return;
            }

            const {
                selector = null,
                    mode = 'html'
            } = options;

            const target = selector ? td.querySelector(selector) : td;
            if (!target) {
                console.warn(`未找到匹配元素: ${selector || '单元格'}`);
                return;
            }

            if (content == null || content === '') {
                target.innerHTML = '';
                return;
            }

            const processContent = () => {
                // 5.1 已经是DOM节点直接返回
                if (content instanceof Node) return content;

                // 5.2 文本模式直接返回字符串
                if (mode === 'text') return String(content);

                // 5.3 HTML模式创建临时容器解析
                const container = document.createElement('div');
                container.innerHTML = content;

                // 返回解析后的DOM节点(多个节点时包裹在div中)
                return container.childNodes.length > 1 ?
                    container :
                    container.firstChild || "";
            };

            // 6. 安全执行DOM操作
            try {
                switch (mode) {
                    case 'text':
                        target.textContent = String(content);
                        break;

                    case 'html':
                        target.innerHTML = typeof content === 'string' ?
                            content :
                            "";
                        break;

                    case 'replace':
                        target.replaceWith(processContent());
                        break;

                    case 'append':
                        target.append(processContent());
                        break;

                    case 'prepend':
                        target.prepend(processContent());
                        break;

                    default:
                        throw new Error(`不支持的mode参数: ${mode}`);
                }
            } catch (error) {
                console.error('内容渲染失败:', error);
                target.innerHTML = '<span style="color:red">渲染错误</span>';
            }
        }
    }

    class NextDungeon {

        constructor(td) {
            this.dom = td;
            this.name = this.dom.getAttribute('onmouseover')?.match(/wodToolTip\(.*?,\s*'([^']*)'/)?.[1] || null;
            this.estimatedEndTime = this.dom.textContent.trim();
            this.estimatedTimestamp = parseTime(this.estimatedEndTime);
        }

        endTimeCountdown() {
            return this.estimatedTimestamp - Date.now();
        }
    }

    class ItemsPageManager {

        static PACKAGE_FULL_DESCRIPTION = '无法分出手来干别的事情';

        constructor() {
            this.sessionHeroId = document.querySelector('input[type=hidden][name=session_hero_id]');
            this.submitBtn = document.querySelectorAll('input[type=submit][name=ok]');
        }

        execute() {
            if (!document.body.textContent.includes(ItemsPageManager.PACKAGE_FULL_DESCRIPTION)) return;

            document.querySelector('#gadgettable-center-td').addEventListener('click', e => {
                if (e.target?.matches('input[name="ok"]')) {
                    ScriptStorageManager.clearOverburdenedStatus(this.sessionHeroId);
                }
            });
        }
    }

    class VoteService {

        constructor(doc) {
            if (!(doc instanceof Document)) {
                throw new Error('doc is not the global document object');
            }
            this.doc = doc;
        }

        findAll() {
            return Array.from(this.doc.querySelectorAll('div.vote.reward img')).map(img => new VoteEvent(img));
        }

        findOneByMaxRewardTime() {
            const maxRewardTime = Math.max(...this.findAll().map(event => event.nextRewardTimestamp));
            return this.findAll().find(event => event.nextRewardTimestamp === maxRewardTime);
        }

        findOneByTrackedRedirectionUrl(url) {
            return this.findAll().find(event => event.trackedRedirectionUrl === url);
        }
    }

    class VoteEvent {

        constructor(img) {
            this.dom = img;
            this.aTag = this.dom.closest('div.vote.reward').previousElementSibling.querySelector('a');
            this.banner = this.aTag.querySelector('img');
            this.trackedRedirectionUrl = this.extractJsUrls();
            this.nextRewardTimeStr = this.dom.closest('div.vote.reward').textContent;
            this.nextRewardTimestamp = parseTime(this.nextRewardTimeStr);
            this.fame = Number(this.dom.parentNode.innerHTML.match(/(\d+)<img/)[1]);
        }

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

        nextRewardTimeCountdown() {
            return this.nextRewardTimestamp - Date.now();
        }
    }

    class WOD {

        static #pageName;

        static get pageName() {
            if (!this.#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');
                this.#pageName = pageName;
            }
            return this.#pageName;
        }

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

        static AFK() {
            const className = this.pageName[0].toUpperCase() + this.pageName.slice(1) + 'PageManager';
            const DynamicClass = this.classMap[className];
            return new DynamicClass().execute();
        }
    }

    WOD.AFK();
})();