// ==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
// @downloadURL https://update.greasyfork.icu/scripts/534756/WOD%20AFK%20Helper.user.js
// @updateURL https://update.greasyfork.icu/scripts/534756/WOD%20AFK%20Helper.meta.js
// ==/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}
`, {
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}
`;
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, 'Strong 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 = '渲染错误';
}
}
}
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+)![]()