// ==UserScript==
// @name [MWI]Talent Market
// @namespace http://tampermonkey.net/
// @version 1.5.6
// @description MWI Talent Market(www.papiyas.chat),游戏页面内嵌网站弹窗,支持一键导入角色信息生成名片上传
// @author SHIIN
// @match https://www.milkywayidle.com/*
// @match https://www.milkywayidlecn.com/*
// @match https://test.milkywayidle.com/*
// @match https://test.milkywayidlecn.com/*
// @match https://papiyas.chat/*
// @match https://shykai.github.io/MWICombatSimulatorTest/*
// @match https://amvoidguy.github.io/MWICombatSimulatorTest/*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_info
// @grant GM_xmlhttpRequest
// @grant GM_getResourceText
// @grant unsafeWindow
// @icon https://www.papiyas.chat/img/favicon.ico
// @license CC-BY-NC-SA-4.0
// @connect papiyas.chat
// @connect tupian.li
// @connect www.milkywayidle.com
// @connect www.milkywayidlecn.com
// @require https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.5.0/lz-string.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/mathjs/12.4.2/math.js
// @resource cardStyles https://papiyas.chat/static/js/mwi-card-styles.css?v=1.5.2
// @downloadURL none
// ==/UserScript==
(function(global) {
'use strict';
const SnapDOM = (function() {
function initSnapDOM() {
if (typeof global.snapdom !== 'undefined') return;
global.snapdom = async function(node, options = {}) {
const scale = options.scale || 2;
const width = options.width || node.offsetWidth;
const height = options.height || node.offsetHeight;
const bgcolor = options.backgroundColor || 'transparent';
async function cloneNode(node, filter) {
if (node.nodeType === Node.TEXT_NODE) return document.createTextNode(node.nodeValue);
if (node.nodeType !== Node.ELEMENT_NODE) return null;
const clone = node.cloneNode(false);
const style = window.getComputedStyle(node);
for (let i = 0; i < style.length; i++) {
const name = style[i];
if (name !== 'backgroundImage') clone.style[name] = style.getPropertyValue(name);
}
const classNameForFix = typeof node.className === 'string' ? node.className : (node.className?.baseVal || '');
if (classNameForFix.includes('mwi-stat-value') || classNameForFix.includes('mwi-stat-label') || classNameForFix.includes('mwi-character-id')) {
clone.style.setProperty('overflow', 'visible', 'important');
clone.style.setProperty('text-overflow', 'clip', 'important');
clone.style.setProperty('white-space', 'nowrap', 'important');
clone.style.setProperty('max-width', 'none', 'important');
}
if (classNameForFix.includes('mwi-stat-item') || classNameForFix.includes('mwi-character-id-wrapper') || classNameForFix.includes('mwi-character-info-top')) {
clone.style.setProperty('overflow', 'visible', 'important');
clone.style.setProperty('max-width', 'none', 'important');
}
if (classNameForFix.includes('CharacterName_name') || classNameForFix.includes('CharacterName_characterName')) {
clone.style.setProperty('overflow', 'visible', 'important');
clone.style.setProperty('height', 'auto', 'important');
clone.style.setProperty('max-height', 'none', 'important');
clone.style.setProperty('line-height', 'normal', 'important');
}
if (node.style) {
for (let i = 0; i < node.style.length; i++) {
const propName = node.style[i];
if (propName.startsWith('--')) clone.style.setProperty(propName, node.style.getPropertyValue(propName));
}
}
if (node.tagName === 'use' || node.tagName === 'USE') {
const href = node.getAttribute('href') || node.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
if (href) { clone.setAttribute('href', href); clone.setAttributeNS('http://www.w3.org/1999/xlink', 'href', href); }
}
let backgroundApplied = false;
if (node.style?.backgroundImage && node.style.backgroundImage !== 'none') {
clone.style.backgroundImage = node.style.backgroundImage;
const inlineMatch = node.style.backgroundImage.match(/url\(["']?([^"']*?)["']?\)/);
backgroundApplied = !!(inlineMatch?.[1] || node.style.backgroundImage.includes('gradient'));
}
const bgProps = ['backgroundImage', 'backgroundColor', 'backgroundSize', 'backgroundPosition', 'backgroundRepeat', 'backgroundAttachment', 'backgroundClip', 'backgroundOrigin'];
bgProps.forEach(prop => {
const computedValue = style.getPropertyValue(prop);
if (computedValue && computedValue !== 'none' && computedValue !== 'initial') {
clone.style.setProperty(prop, computedValue);
if (prop === 'backgroundImage' && computedValue !== 'none') backgroundApplied = true;
}
});
if (backgroundApplied) {
['backgroundSize', 'backgroundPosition', 'backgroundRepeat', 'backgroundAttachment'].forEach(prop => {
const value = node.style?.[prop] || style.getPropertyValue(prop);
if (value && value !== 'initial') clone.style[prop] = value;
});
}
const supportsMask = CSS.supports('mask', 'linear-gradient(#fff 0 0)') || CSS.supports('-webkit-mask', 'linear-gradient(#fff 0 0)');
['::before', '::after'].forEach(pseudo => {
const pseudoStyle = window.getComputedStyle(node, pseudo);
const content = pseudoStyle.getPropertyValue('content');
const background = pseudoStyle.getPropertyValue('background');
if ((content && content !== 'none' && content !== 'normal') || (background && background.includes('gradient'))) {
const classNameStr = typeof node.className === 'string' ? node.className : (node.className?.baseVal || node.className?.toString() || '');
if (classNameStr.includes('mwi-character-info-top') && pseudo === '::before') return;
if (classNameStr.includes('mwi-character-card') && pseudo === '::before' && !supportsMask) return;
const pseudoElement = document.createElement('div');
pseudoElement.className = `pseudo-${pseudo.replace('::', '')}`;
['position', 'top', 'right', 'bottom', 'left', 'inset', 'width', 'height', 'background', 'background-color', 'background-image', 'background-size', 'background-position', 'background-repeat', 'background-attachment', 'background-clip', 'border-radius', 'padding', 'margin', 'z-index', 'pointer-events', 'backdrop-filter', '-webkit-backdrop-filter', 'mask', '-webkit-mask', 'mask-composite', '-webkit-mask-composite'].forEach(prop => {
const value = pseudoStyle.getPropertyValue(prop);
if (value && value !== 'initial' && value !== 'auto' && value !== 'normal') pseudoElement.style.setProperty(prop, value);
});
const zIndex = pseudoStyle.getPropertyValue('z-index');
pseudoElement.style.setProperty('z-index', (zIndex && zIndex !== 'auto') ? zIndex : '-1', 'important');
if (pseudo === '::before') clone.insertBefore(pseudoElement, clone.firstChild || null);
else clone.appendChild(pseudoElement);
}
});
for (let i = 0; i < node.childNodes.length; i++) {
const child = await cloneNode(node.childNodes[i], filter);
if (child) clone.appendChild(child);
}
return clone;
}
async function embedSVG(clone) {
const spriteMap = {};
const uses = clone.querySelectorAll('use');
for (const use of uses) {
const href = use.getAttribute('href') || use.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
if (!href) continue;
if (href.includes('.svg#')) {
const [spriteUrl, symbolId] = href.split('#');
if (!spriteMap[spriteUrl]) spriteMap[spriteUrl] = new Set();
spriteMap[spriteUrl].add(symbolId);
} else if (href.startsWith('#')) {
const symbolId = href.substring(1);
const symbol = document.getElementById(symbolId);
if (!symbol) continue;
const svg = use.closest('svg');
if (!svg) continue;
let defs = svg.querySelector('defs');
if (!defs) { defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs'); svg.insertBefore(defs, svg.firstChild); }
if (!defs.querySelector('#' + symbolId)) { const clonedSymbol = symbol.cloneNode(true); clonedSymbol.querySelectorAll('text, tspan').forEach(t => t.remove()); defs.appendChild(clonedSymbol); }
}
}
for (const [spriteUrl, symbolIds] of Object.entries(spriteMap)) {
try {
const response = await fetch(spriteUrl);
const svgText = await response.text();
const svgDoc = new DOMParser().parseFromString(svgText, 'image/svg+xml');
for (const symbolId of symbolIds) {
const symbol = svgDoc.getElementById(symbolId);
if (!symbol) continue;
const relevantUses = clone.querySelectorAll(`use[href="${spriteUrl}#${symbolId}"]`);
for (const use of relevantUses) {
const svg = use.closest('svg');
if (!svg) continue;
let defs = svg.querySelector('defs');
if (!defs) { defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs'); svg.insertBefore(defs, svg.firstChild); }
const spriteType = spriteUrl.match(/\/([^\/]+)_sprite/)?.[1] || 'unknown';
const uniqueId = `${spriteType}_${symbolId}`;
if (!defs.querySelector('#' + uniqueId)) { const clonedSymbol = symbol.cloneNode(true); clonedSymbol.id = uniqueId; clonedSymbol.querySelectorAll('text, tspan').forEach(t => t.remove()); defs.appendChild(clonedSymbol); }
use.setAttribute('href', '#' + uniqueId);
use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', '#' + uniqueId);
}
}
} catch (error) { console.warn('[SnapDOM] embedSVG fetch failed for sprite:', error.message); }
}
}
const clonedNode = await cloneNode(node, options.filter);
await embedSVG(clonedNode);
[{ selector: '.mwi-equipment-slot.filled', bgColor: 'rgba(255, 255, 255, 0.08)' }, { selector: '.mwi-ability-item:not(.empty)', bgColor: 'rgba(255, 255, 255, 0.08)' }, { selector: '.mwi-consumable-item:not(.empty)', bgColor: 'rgba(255, 255, 255, 0.08)' }, { selector: '.mwi-house-item:not(.empty)', bgColor: 'rgba(255, 255, 255, 0.08)' }, { selector: '.mwi-stat-item', bgColor: 'rgba(255, 255, 255, 0.03)' }, { selector: '.mwi-consumables-container, .mwi-abilities-grid, .mwi-equipment-grid, .mwi-house-grid', bgColor: 'rgba(0, 0, 0, 0.2)' }, { selector: '.mwi-stats-grid', bgColor: 'rgba(0, 0, 0, 0.06)' }].forEach(fix => {
clonedNode.querySelectorAll(fix.selector).forEach(el => {
if (!el.style.backgroundColor || el.style.backgroundColor === 'transparent') { el.style.backgroundColor = fix.bgColor; el.style.backdropFilter = 'blur(20px) saturate(180%)'; el.style.webkitBackdropFilter = 'blur(20px) saturate(180%)'; }
});
});
const xmlns = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(xmlns, 'svg');
svg.setAttribute('width', width * scale); svg.setAttribute('height', height * scale);
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
svg.setAttribute('color-interpolation', 'sRGB'); svg.setAttribute('color-interpolation-filters', 'sRGB');
svg.style.backgroundColor = bgcolor;
const foreignObject = document.createElementNS(xmlns, 'foreignObject');
foreignObject.setAttribute('width', '100%'); foreignObject.setAttribute('height', '100%');
foreignObject.setAttribute('x', '0'); foreignObject.setAttribute('y', '0');
let bgImageUrl = null;
const cardElement = clonedNode.querySelector('.mwi-character-card') || clonedNode;
if (cardElement && cardElement.style.backgroundImage) {
const match = cardElement.style.backgroundImage.match(/url\(["']?([^"')]+)["']?\)/);
if (match && match[1]) bgImageUrl = match[1];
}
foreignObject.appendChild(clonedNode); svg.appendChild(foreignObject);
const dataUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(new XMLSerializer().serializeToString(svg));
return {
async toCanvas() {
return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas');
canvas.width = width * scale; canvas.height = height * scale;
const ctx = canvas.getContext('2d', { alpha: true, colorSpace: 'srgb' });
ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high';
const drawSvgContent = () => {
const svgImg = new Image();
svgImg.crossOrigin = 'anonymous'; svgImg.decoding = 'sync';
svgImg.onload = () => { ctx.drawImage(svgImg, 0, 0); resolve(canvas); };
svgImg.onerror = () => reject(new Error('Failed to load SVG image'));
svgImg.src = dataUrl;
};
if (bgImageUrl) {
const bgImg = new Image();
bgImg.crossOrigin = 'anonymous';
bgImg.onload = () => { ctx.drawImage(bgImg, 0, 0, canvas.width, canvas.height); drawSvgContent(); };
bgImg.onerror = () => { drawSvgContent(); };
bgImg.src = bgImageUrl;
} else {
if (bgcolor && bgcolor !== 'transparent') { ctx.fillStyle = bgcolor; ctx.fillRect(0, 0, canvas.width, canvas.height); }
drawSvgContent();
}
});
}
};
};
}
return { async toCanvas(element, options = {}) { initSnapDOM(); const result = await global.snapdom(element, options); return result.toCanvas(); } };
})();
global.SnapDOM = SnapDOM;
})(typeof window !== 'undefined' ? window : this);
(function(global) {
'use strict';
const SVGTool = {
sprites: { items: null, skills: null, abilities: null, misc: null, chat_icons: null, avatars: null, avatar_outfits: null },
initialized: false,
init() {
if (this.initialized) return;
try {
const rawAssets = localStorage.getItem('preloadedAssets');
if (rawAssets) {
const preloadedAssets = JSON.parse(rawAssets);
['items', 'skills', 'abilities', 'misc', 'chat_icons', 'avatars', 'avatar_outfits'].forEach(type => {
const key = `${type}_sprite`;
if (preloadedAssets[key]) this.sprites[type] = preloadedAssets[key].split('?')[0];
});
}
} catch (e) {}
const missingSprites = Object.keys(this.sprites).filter(k => !this.sprites[k]);
if (missingSprites.length > 0) {
document.querySelectorAll("svg use[href*='sprite']").forEach(useEl => {
const href = useEl.getAttribute("href") || useEl.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
if (!href) return;
const [filePath] = href.split('#');
missingSprites.forEach(type => { if (filePath.includes(`${type}_sprite`) && !this.sprites[type]) this.sprites[type] = filePath; });
});
}
const defaults = { items: '/static/media/items_sprite.6d12eb9d.svg', skills: '/static/media/skills_sprite.3bb4d936.svg', abilities: '/static/media/abilities_sprite.fdd1b4de.svg', misc: '/static/media/misc_sprite.6fa5e97c.svg', chat_icons: '/static/media/chat_icons_sprite.f870cd32.svg', avatars: '/static/media/avatars_sprite.23c6df2d.svg', avatar_outfits: '/static/media/avatar_outfits_sprite.fe228a76.svg' };
Object.keys(defaults).forEach(key => { if (!this.sprites[key]) this.sprites[key] = defaults[key]; });
this.initialized = true;
},
getSpriteURL(type = 'items') { if (!this.initialized) this.init(); return this.sprites[type] || this.sprites.items; },
refreshFromDOM() {
try {
const rawAssets = localStorage.getItem('preloadedAssets');
if (rawAssets) {
const preloadedAssets = JSON.parse(rawAssets);
['items', 'skills', 'abilities', 'misc', 'chat_icons', 'avatars', 'avatar_outfits'].forEach(type => {
const key = `${type}_sprite`;
if (preloadedAssets[key]) {
const url = preloadedAssets[key].split('?')[0];
if (url && this.sprites[type] !== url) this.sprites[type] = url;
}
});
}
} catch (e) {}
document.querySelectorAll("svg use[href*='sprite']").forEach(useEl => {
const href = useEl.getAttribute("href") || useEl.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
if (!href) return;
const [filePath] = href.split('#');
['items', 'skills', 'abilities', 'misc', 'chat_icons', 'avatars', 'avatar_outfits'].forEach(type => {
if (filePath.includes(`${type}_sprite`) && this.sprites[type] !== filePath) this.sprites[type] = filePath;
});
});
},
createIcon(hrid, width = 40, height = 40, type = 'items') {
const iconId = hrid.split('/').pop();
const spriteURL = this.getSpriteURL(type);
if (!spriteURL) return `
?
`;
return ` `;
}
};
global.SVGTool = SVGTool;
})(typeof window !== 'undefined' ? window : this);
(function() {
'use strict';
try {
const cardStyles = GM_getResourceText('cardStyles');
if (cardStyles) {
GM_addStyle(cardStyles);
}
} catch (e) {
}
const SITE_URL = 'https://papiyas.chat';
const IntervalManager = {
intervals: new Map(),
isPageVisible: true,
visibilityHandler: null,
init() {
if (this.visibilityHandler) return;
this.visibilityHandler = () => {
this.isPageVisible = !document.hidden;
if (this.isPageVisible) {
this.resumeAll();
} else {
this.pauseAll();
}
};
document.addEventListener('visibilitychange', this.visibilityHandler);
},
register(name, callback, interval) {
this.init();
if (this.intervals.has(name)) {
this.clear(name);
}
const id = setInterval(() => {
if (this.isPageVisible) {
callback();
}
}, interval);
this.intervals.set(name, { id, callback, interval });
return id;
},
clear(name) {
const entry = this.intervals.get(name);
if (entry) {
clearInterval(entry.id);
this.intervals.delete(name);
}
},
pauseAll() {
},
resumeAll() {
},
clearAll() {
this.intervals.forEach((entry, name) => {
clearInterval(entry.id);
});
this.intervals.clear();
if (this.visibilityHandler) {
document.removeEventListener('visibilitychange', this.visibilityHandler);
this.visibilityHandler = null;
}
}
};
const currentHostname = window.location.hostname;
const isSimulatorPage = document.URL.includes("shykai.github.io/MWICombatSimulatorTest") ||
document.URL.includes("amvoidguy.github.io/MWICombatSimulatorTest");
const cardLinkFillStatus = {
submitFormFilled: false,
editFormFilled: false,
submitFormExists: false,
editFormExists: false,
currentTimestamp: 0,
checkFormExistence: function() {
this.submitFormExists = !!document.querySelector('#cardLink');
this.editFormExists = !!document.querySelector('#editCardLink');
},
markFilled: function(formType, timestamp) {
if (timestamp !== this.currentTimestamp) {
this.submitFormFilled = false;
this.editFormFilled = false;
this.currentTimestamp = timestamp;
}
this.checkFormExistence();
if (formType === 'submit') {
this.submitFormFilled = true;
} else if (formType === 'edit') {
this.editFormFilled = true;
}
const submitDone = !this.submitFormExists || this.submitFormFilled;
const editDone = !this.editFormExists || this.editFormFilled;
if (submitDone && editDone) {
GM_setValue('mwi_card_image_url', '');
GM_setValue('mwi_card_image_timestamp', 0);
GM_setValue('mwi_upload_progress', 0);
GM_setValue('mwi_upload_status', '');
this.submitFormFilled = false;
this.editFormFilled = false;
this.currentTimestamp = 0;
}
},
reset: function() {
this.submitFormFilled = false;
this.editFormFilled = false;
this.submitFormExists = false;
this.editFormExists = false;
this.currentTimestamp = 0;
}
};
function enableClipboardPermissionsIfSupported(iframeElement) {
if (!iframeElement) {
return;
}
try {
const userAgent = (navigator.userAgent || '').toLowerCase();
const isChromiumFamily = /chrome|edg|opr|brave|vivaldi/.test(userAgent) && !userAgent.includes('firefox');
if (!isChromiumFamily) {
return;
}
iframeElement.setAttribute('allow', 'clipboard-read; clipboard-write');
} catch (error) {
}
}
if (currentHostname === 'papiyas.chat') {
try {
localStorage.setItem('mwi_tm_version', GM_info.script.version);
} catch (e) {}
if (window.self !== window.top) {
initPapiyasChatFeatures();
}
return;
}
try {
GM_setValue('mwi_tm_version', GM_info.script.version);
} catch (e) {}
if (!isSimulatorPage) {
(function initNavigationLink() {
window.detectLanguage = function() {
const lang = localStorage.getItem("i18nextLng") || document.documentElement.lang || navigator.language || 'en';
return lang.toLowerCase().startsWith('zh') ? 'zh' : 'en';
};
function findSocko(container) {
return [...container.children].find(el =>
el.textContent.includes('socko') || el.textContent.includes('战斗榜')
);
}
function createLink() {
const lang = window.detectLanguage();
const div = document.createElement('div');
div.className = 'NavigationBar_minorNavigationLink__31K7Y mwi-talent-market-link';
div.style.cssText = 'cursor: pointer; color: #FFA500;';
div.textContent = lang === 'zh' ? '人才市场 SHIIN' : 'Talent Market SHIIN';
div.addEventListener('click', () => {
if (typeof window.toggleTalentMarketModal === 'function') {
window.toggleTalentMarketModal();
}
});
return div;
}
function insertOrRepositionLink(forceFallback = false) {
const container = document.querySelector('.NavigationBar_minorNavigationLinks__dbxh7');
if (!container) return false;
const socko = findSocko(container);
let link = document.querySelector('.mwi-talent-market-link');
if (link) {
if (socko && socko.nextElementSibling !== link) {
socko.insertAdjacentElement('afterend', link);
}
return true;
}
if (socko) {
socko.insertAdjacentElement('afterend', createLink());
return true;
}
if (forceFallback) {
container.insertAdjacentElement('afterbegin', createLink());
return true;
}
return false;
}
let attempts = 0;
const maxAttempts = 15;
const checkInterval = setInterval(() => {
attempts++;
const isLastAttempt = attempts >= maxAttempts;
if (insertOrRepositionLink(isLastAttempt) || isLastAttempt) {
clearInterval(checkInterval);
}
}, 200);
let throttleTimer = null;
let initialCheckDone = false;
const observer = new MutationObserver(() => {
if (throttleTimer) return;
throttleTimer = setTimeout(() => {
throttleTimer = null;
const linkExists = !!document.querySelector('.mwi-talent-market-link');
insertOrRepositionLink(!linkExists && initialCheckDone);
}, 200);
});
setTimeout(() => { initialCheckDone = true; }, 3000);
if (document.body) {
observer.observe(document.body, { childList: true, subtree: true });
} else {
document.addEventListener('DOMContentLoaded', () => {
observer.observe(document.body, { childList: true, subtree: true });
});
}
})();
} // end if (!isSimulatorPage)
const CARD_BACKGROUND_IMAGE = 'https://tupian.li/images/2026/01/06/695c6aa763f87.png';
const BackgroundImagePreloader = {
cachedDataURL: null,
isLoading: false,
loadPromise: null,
async preload() {
if (this.cachedDataURL) {
return this.cachedDataURL;
}
if (this.isLoading) {
return this.loadPromise;
}
this.isLoading = true;
this.loadPromise = this._loadImage();
try {
this.cachedDataURL = await this.loadPromise;
return this.cachedDataURL;
} catch (error) {
this.cachedDataURL = null;
throw error;
} finally {
this.isLoading = false;
this.loadPromise = null;
}
},
async _loadImage() {
if (!CARD_BACKGROUND_IMAGE || CARD_BACKGROUND_IMAGE.trim() === '') {
throw new Error('Background image URL not configured');
}
const blob = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: CARD_BACKGROUND_IMAGE,
responseType: 'blob',
onload: (response) => {
if (response.status >= 200 && response.status < 300) {
resolve(response.response);
} else {
reject(new Error(`HTTP ${response.status}`));
}
},
onerror: () => reject(new Error('Network error loading background image')),
ontimeout: () => reject(new Error('Timeout loading background image'))
});
});
const img = await new Promise((resolve, reject) => {
const imgEl = new Image();
imgEl.crossOrigin = 'anonymous';
imgEl.decoding = 'sync';
const blobUrl = URL.createObjectURL(blob);
imgEl.onload = () => { URL.revokeObjectURL(blobUrl); resolve(imgEl); };
imgEl.onerror = () => { URL.revokeObjectURL(blobUrl); reject(new Error('Failed to decode background image')); };
imgEl.src = blobUrl;
});
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth || img.width;
canvas.height = img.naturalHeight || img.height;
const ctx = canvas.getContext('2d', { alpha: true, colorSpace: 'srgb', willReadFrequently: false });
ctx.drawImage(img, 0, 0);
URL.revokeObjectURL(img.src);
return canvas.toDataURL('image/png', 1.0);
},
getCached() {
return this.cachedDataURL;
},
clear() {
this.cachedDataURL = null;
this.isLoading = false;
this.loadPromise = null;
}
};
if (currentHostname !== 'papiyas.chat' && !isSimulatorPage) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
BackgroundImagePreloader.preload().catch(() => {});
});
} else {
BackgroundImagePreloader.preload().catch(() => {});
}
}
function initCardUploadMonitor() {
let lastProcessedTimestamp = 0;
let isUploading = false;
const monitorCallback = async () => {
if (isUploading) {
return;
}
const requestTimestamp = GM_getValue('mwi_card_upload_request', 0);
if (requestTimestamp && requestTimestamp !== lastProcessedTimestamp) {
lastProcessedTimestamp = requestTimestamp;
isUploading = true;
GM_setValue('mwi_card_upload_request', 0);
try {
let cardElement = document.getElementById('mwi-card-content');
if (!cardElement) {
if (typeof generateCharacterCard === 'function') {
await generateCharacterCard();
await new Promise(resolve => setTimeout(resolve, 3000));
cardElement = document.getElementById('mwi-card-content');
if (!cardElement) {
throw new Error('名片生成后仍未找到DOM元素');
}
} else {
throw new Error('generateCharacterCard函数不存在');
}
} else {
await new Promise(resolve => setTimeout(resolve, 3000));
}
const imageUrl = await generateAndUploadCard();
const responseData = {
type: 'MWI_UPLOAD_CARD_RESPONSE',
success: true,
imageUrl: imageUrl
};
window.postMessage(responseData, '*');
const iframes = document.querySelectorAll('iframe');
iframes.forEach((iframe, index) => {
try {
iframe.contentWindow.postMessage(responseData, '*');
} catch (e) {}
});
if (window.parent && window.parent !== window) {
try {
window.parent.postMessage(responseData, '*');
} catch (e) {}
}
} catch (error) {
const errorData = {
type: 'MWI_UPLOAD_CARD_RESPONSE',
success: false,
error: error.message
};
window.postMessage(errorData, '*');
const iframes = document.querySelectorAll('iframe');
iframes.forEach((iframe) => {
try {
iframe.contentWindow.postMessage(errorData, '*');
} catch (e) {}
});
} finally {
isUploading = false;
}
}
};
IntervalManager.register('cardUploadMonitor', monitorCallback, 500);
}
function initPapiyasChatFeatures() {
try {
const btName = GM_getValue('bt_auth_name', '');
if (btName) localStorage.setItem('bt_auth_name', btName);
localStorage.setItem('mwi_tm_version', GM_info.script.version);
} catch (e) { /* silent */ }
initFormImportListener();
initSimulatorDataAutoFill();
initCardImageUrlAutoFill();
initEditFormAutoFill();
initEditCardImageUrlAutoFill();
initGuildFormAutoFill();
initEditGuildFormAutoFill();
}
function initCardImageUrlAutoFill() {
let lastProcessedTimestamp = 0;
function fillCardLink(cardImageUrl, timestamp) {
if (typeof window.__fillCardLink__ === 'function') {
window.__fillCardLink__(cardImageUrl);
}
lastProcessedTimestamp = timestamp;
cardLinkFillStatus.markFilled('submit', timestamp);
GM_setValue('mwi_upload_progress', 0);
GM_setValue('mwi_upload_status', '');
}
function checkAndFillCardLink() {
try {
const cardImageUrl = GM_getValue('mwi_card_image_url', '');
const timestamp = GM_getValue('mwi_card_image_timestamp', 0);
if (cardImageUrl && timestamp && timestamp !== lastProcessedTimestamp) {
fillCardLink(cardImageUrl, timestamp);
return;
}
} catch (error) {
}
}
window.addEventListener('message', (event) => {
if (event.data && event.data.type === 'MWI_UPLOAD_CARD_RESPONSE' && event.data.success && event.data.imageUrl) {
const timestamp = Date.now();
if (timestamp !== lastProcessedTimestamp) {
fillCardLink(event.data.imageUrl, timestamp);
}
}
});
IntervalManager.register('cardImageUrlAutoFill', checkAndFillCardLink, 500);
setTimeout(checkAndFillCardLink, 100);
}
function initFormImportListener() {
function getUserIdFromPage() {
const pageType = detectPageType();
let userId = '';
if (pageType === 'guild') {
const submitInput = document.querySelector('#guildLeaderId');
const editInput = document.querySelector('#editGuildLeaderId');
userId = submitInput?.value?.trim() || editInput?.value?.trim() || '';
} else {
const submitInput = document.querySelector('#playerId');
const editInput = document.querySelector('#editPlayerId');
userId = submitInput?.value?.trim() || editInput?.value?.trim() || '';
}
return userId;
}
function detectPageType() {
const url = window.location.pathname;
if (url.includes('/guild')) return 'guild';
if (url.includes('/IC')) return 'ic';
return 'standard';
}
async function getGameData(pageType) {
const simulatorDataStr = GM_getValue('mwi_simulator_data', null);
if (simulatorDataStr) {
try {
const simulatorData = JSON.parse(simulatorDataStr);
return simulatorData;
} catch (e) {}
}
const baseData = {
battleLevel: '123.456',
combatLevel: '78.901',
mainAttributeLevel: '45.678',
characterTotalLevel: '567.890'
};
if (pageType === 'guild') {
return {
battleLevel: baseData.battleLevel,
combatLevel: baseData.combatLevel,
mainAttributeLevel: baseData.mainAttributeLevel,
characterTotalLevel: baseData.characterTotalLevel
};
} else {
return baseData;
}
}
async function fillFormData(data, pageType) {
const commonFields = {
'battle-level-input': data.battleLevel,
'combat-level-input': data.combatLevel,
'main-attribute-level-input': data.mainAttributeLevel,
'character-total-level-input': data.characterTotalLevel
};
const guildFields = {
'guild-battle-level': data.battleLevel,
'guild-combat-level': data.combatLevel,
'guild-main-attribute-level': data.mainAttributeLevel,
'guild-character-total-level': data.characterTotalLevel
};
const fieldsToFill = pageType === 'guild' ?
{ ...commonFields, ...guildFields } :
commonFields;
for (const [id, value] of Object.entries(fieldsToFill)) {
const selectors = [
`#${id}`,
`input[id="${id}"]`,
`input[name="${id}"]`,
`[data-field="${id}"]`
];
for (const selector of selectors) {
const input = document.querySelector(selector);
if (input) {
input.removeAttribute('readonly');
input.removeAttribute('disabled');
input.value = value;
const events = ['input', 'change', 'blur', 'keyup'];
events.forEach(eventType => {
input.dispatchEvent(new Event(eventType, { bubbles: true }));
});
break;
}
}
}
}
function listenImportButton() {
if (window._MWI_IMPORT_LISTENER_ATTACHED) {
return;
}
const importHandler = async (e) => {
try {
const pageType = detectPageType();
const gameData = await getGameData(pageType);
await fillFormData(gameData, pageType);
document.dispatchEvent(new CustomEvent('plugin-import-response', {
detail: {
success: true,
data: gameData,
pageType: pageType
}
}));
const isInGuildModal = !!(document.querySelector('.guild-submit-modal-content') || document.querySelector('.edit-guild-modal-content'));
if (!isInGuildModal) {
GM_setValue('mwi_card_upload_request', Date.now());
}
} catch (error) {
document.dispatchEvent(new CustomEvent('plugin-import-response', {
detail: {
success: false,
error: error.message
}
}));
}
};
document.addEventListener('plugin-import-request', importHandler);
window._MWI_IMPORT_LISTENER_ATTACHED = true;
}
listenImportButton();
}
async function generateAndUploadCard() {
return await autoUploadCard();
}
window.addEventListener('message', async (event) => {
if (event.data && event.data.type === 'MWI_GENERATE_CARD_REQUEST') {
try {
if (typeof generateCharacterCard === 'function') {
await generateCharacterCard();
} else {
throw new Error('generateCharacterCard函数未找到');
}
} catch (error) {
}
}
if (event.data && event.data.type === 'MWI_UPLOAD_CARD_REQUEST') {
try {
const imageUrl = await generateAndUploadCard();
const responseData = {
type: 'MWI_UPLOAD_CARD_RESPONSE',
success: true,
imageUrl: imageUrl
};
if (event.source) {
event.source.postMessage(responseData, event.origin);
}
window.postMessage(responseData, '*');
} catch (error) {
const errorData = {
type: 'MWI_UPLOAD_CARD_RESPONSE',
success: false,
error: error.message
};
if (event.source) {
event.source.postMessage(errorData, event.origin);
}
window.postMessage(errorData, '*');
}
}
if (event.data && event.data.type === 'MODAL_OPENED') {
GM_setValue('mwi_upload_progress', 0);
GM_setValue('mwi_upload_status', '');
}
if (event.data && event.data.type === 'MODAL_CLOSED') {
GM_setValue('mwi_upload_progress', 0);
GM_setValue('mwi_upload_status', '');
GM_setValue('mwi_card_upload_request', 0);
}
});
function initSimulatorDataAutoFill() {
function attachImportListener() {
const importBtnSelectors = [
'#submitForm > div:nth-child(1) > button:nth-child(1)',
'#submitForm button:first-child'
];
let importBtn = null;
for (const selector of importBtnSelectors) {
importBtn = document.querySelector(selector);
if (importBtn) {
break;
}
}
if (!importBtn) {
const allButtons = document.querySelectorAll('#submitForm button');
for (const btn of allButtons) {
const text = btn.textContent?.trim() || '';
if (text.includes('导入数据') || text.includes('Import Data')) {
importBtn = btn;
break;
}
}
}
if (!importBtn) {
return false;
}
if (importBtn.dataset.simulatorListenerAttached) {
importBtn.removeEventListener('click', importBtn._simulatorClickHandler);
}
const clickHandler = async function(e) {
try {
await new Promise(resolve => setTimeout(resolve, 100));
const simDataInput = document.querySelector('#simData');
if (!simDataInput) {
return;
}
let simulatorData = null;
try {
const storedData = GM_getValue('mwi_simulator_data', '');
if (storedData) {
simulatorData = JSON.parse(storedData);
}
} catch (e) {
}
if (!simulatorData && window.opener && window.opener.MWI_INTEGRATED) {
const getSimData = window.opener.MWI_INTEGRATED.getSimulatorData;
if (typeof getSimData === 'function') {
simulatorData = getSimData();
}
}
if (!simulatorData && window.MWI_INTEGRATED && typeof window.MWI_INTEGRATED.getSimulatorData === 'function') {
simulatorData = window.MWI_INTEGRATED.getSimulatorData();
}
if (!simulatorData) {
const lang = typeof window.detectLanguage === 'function' ? window.detectLanguage() : 'zh';
const message = lang === 'zh' ? '无法获取模拟器数据\n\n请确保:\n1. 已在游戏页面加载完整数据\n2. 游戏页面已接收到WebSocket数据\n3. 刷新游戏页面后再试' : 'Unable to get simulator data\n\nPlease ensure:\n1. Data is fully loaded on game page\n2. Game page has received WebSocket data\n3. Refresh game page and try again';
alert(message);
return;
}
const dataString = JSON.stringify(simulatorData, null, 2);
simDataInput.value = dataString;
simDataInput.dispatchEvent(new Event('input', { bubbles: true }));
simDataInput.dispatchEvent(new Event('change', { bubbles: true }));
await new Promise(resolve => setTimeout(resolve, 500));
let targetWindow = null;
if (window.opener && !window.opener.closed) {
targetWindow = window.opener;
} else if (window.parent && window.parent !== window) {
targetWindow = window.parent;
} else if (window.top && window.top !== window) {
targetWindow = window.top;
}
if (targetWindow) {
targetWindow.postMessage({
type: 'MWI_GENERATE_CARD_REQUEST',
timestamp: Date.now()
}, '*');
} else if (window.MWI_INTEGRATED && typeof window.MWI_INTEGRATED.generateCard === 'function') {
await window.MWI_INTEGRATED.generateCard();
}
} catch (error) {
const lang = typeof window.detectLanguage === 'function' ? window.detectLanguage() : 'zh';
const message = lang === 'zh' ? '填写数据失败: ' + error.message : 'Failed to fill data: ' + error.message;
alert(message);
}
};
importBtn._simulatorClickHandler = clickHandler;
importBtn.addEventListener('click', clickHandler);
importBtn.dataset.simulatorListenerAttached = 'true';
return true;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
if (!attachImportListener()) {
const observer = new MutationObserver(() => {
if (attachImportListener()) {
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
}, 500);
});
} else {
setTimeout(() => {
if (!attachImportListener()) {
const observer = new MutationObserver(() => {
if (attachImportListener()) {
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
}, 500);
}
}
function initEditFormAutoFill() {
function attachEditImportListener() {
const importBtnSelectors = [
'#editForm > div:nth-child(1) > button:nth-child(1)',
'#editForm button:first-child',
'#editForm .import-data-btn'
];
let importBtn = null;
for (const selector of importBtnSelectors) {
importBtn = document.querySelector(selector);
if (importBtn) {
break;
}
}
if (!importBtn) {
return false;
}
if (importBtn.dataset.editSimulatorListenerAttached) {
importBtn.removeEventListener('click', importBtn._editSimulatorClickHandler);
}
const clickHandler = async function(e) {
try {
await new Promise(resolve => setTimeout(resolve, 100));
const simDataInput = document.querySelector('#editSimData');
if (!simDataInput) {
return;
}
let simulatorData = null;
try {
const storedData = GM_getValue('mwi_simulator_data', '');
if (storedData) {
simulatorData = JSON.parse(storedData);
}
} catch (e) {
}
if (!simulatorData && window.opener && window.opener.MWI_INTEGRATED) {
const getSimData = window.opener.MWI_INTEGRATED.getSimulatorData;
if (typeof getSimData === 'function') {
simulatorData = getSimData();
}
}
if (!simulatorData && window.MWI_INTEGRATED && typeof window.MWI_INTEGRATED.getSimulatorData === 'function') {
simulatorData = window.MWI_INTEGRATED.getSimulatorData();
}
if (!simulatorData) {
const lang = typeof window.detectLanguage === 'function' ? window.detectLanguage() : 'zh';
const message = lang === 'zh' ? '无法获取模拟器数据\n\n请确保:\n1. 已在游戏页面加载完整数据\n2. 游戏页面已接收到WebSocket数据\n3. 刷新游戏页面后再试' : 'Unable to get simulator data\n\nPlease ensure:\n1. Data is fully loaded on game page\n2. Game page has received WebSocket data\n3. Refresh game page and try again';
alert(message);
return;
}
const dataString = JSON.stringify(simulatorData, null, 2);
simDataInput.value = dataString;
simDataInput.dispatchEvent(new Event('input', { bubbles: true }));
simDataInput.dispatchEvent(new Event('change', { bubbles: true }));
await new Promise(resolve => setTimeout(resolve, 500));
let targetWindow = null;
if (window.opener && !window.opener.closed) {
targetWindow = window.opener;
} else if (window.parent && window.parent !== window) {
targetWindow = window.parent;
} else if (window.top && window.top !== window) {
targetWindow = window.top;
}
if (targetWindow) {
targetWindow.postMessage({
type: 'MWI_GENERATE_CARD_REQUEST',
timestamp: Date.now()
}, '*');
} else if (window.MWI_INTEGRATED && typeof window.MWI_INTEGRATED.generateCard === 'function') {
await window.MWI_INTEGRATED.generateCard();
}
} catch (error) {
const lang = typeof window.detectLanguage === 'function' ? window.detectLanguage() : 'zh';
const message = lang === 'zh' ? '填写数据失败: ' + error.message : 'Failed to fill data: ' + error.message;
alert(message);
}
};
importBtn._editSimulatorClickHandler = clickHandler;
importBtn.addEventListener('click', clickHandler);
importBtn.dataset.editSimulatorListenerAttached = 'true';
return true;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
if (!attachEditImportListener()) {
const observer = new MutationObserver(() => {
if (attachEditImportListener()) {
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
}, 500);
});
} else {
setTimeout(() => {
if (!attachEditImportListener()) {
const observer = new MutationObserver(() => {
if (attachEditImportListener()) {
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
}, 500);
}
}
function initGuildFormAutoFill() {
function attachGuildImportListener() {
const importBtnSelectors = [
'#guildForm > div:nth-child(1) > button:nth-child(1)',
'#guildForm button:first-child',
'#guildForm .import-data-btn'
];
let importBtn = null;
for (const selector of importBtnSelectors) {
importBtn = document.querySelector(selector);
if (importBtn) {
break;
}
}
if (!importBtn) {
return false;
}
if (importBtn.dataset.guildSimulatorListenerAttached) {
importBtn.removeEventListener('click', importBtn._guildSimulatorClickHandler);
}
const clickHandler = async function(e) {
try {
await new Promise(resolve => setTimeout(resolve, 100));
const simDataInput = document.querySelector('#guildSimData');
if (!simDataInput) {
return;
}
let simulatorData = null;
try {
const storedData = GM_getValue('mwi_simulator_data', '');
if (storedData) {
simulatorData = JSON.parse(storedData);
}
} catch (e) {
}
if (!simulatorData && window.opener && window.opener.MWI_INTEGRATED) {
const getSimData = window.opener.MWI_INTEGRATED.getSimulatorData;
if (typeof getSimData === 'function') {
simulatorData = getSimData();
}
}
if (!simulatorData && window.MWI_INTEGRATED && typeof window.MWI_INTEGRATED.getSimulatorData === 'function') {
simulatorData = window.MWI_INTEGRATED.getSimulatorData();
}
if (!simulatorData) {
const lang = typeof window.detectLanguage === 'function' ? window.detectLanguage() : 'zh';
const message = lang === 'zh' ? '无法获取模拟器数据\n\n请确保:\n1. 已在游戏页面加载完整数据\n2. 游戏页面已接收到WebSocket数据\n3. 刷新游戏页面后再试' : 'Unable to get simulator data\n\nPlease ensure:\n1. Data is fully loaded on game page\n2. Game page has received WebSocket data\n3. Refresh game page and try again';
alert(message);
return;
}
const dataString = JSON.stringify(simulatorData, null, 2);
simDataInput.value = dataString;
simDataInput.dispatchEvent(new Event('input', { bubbles: true }));
simDataInput.dispatchEvent(new Event('change', { bubbles: true }));
} catch (error) {
alert('填写数据失败: ' + error.message);
}
};
importBtn._guildSimulatorClickHandler = clickHandler;
importBtn.addEventListener('click', clickHandler);
importBtn.dataset.guildSimulatorListenerAttached = 'true';
return true;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
if (!attachGuildImportListener()) {
const observer = new MutationObserver(() => {
if (attachGuildImportListener()) {
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
}, 500);
});
} else {
setTimeout(() => {
if (!attachGuildImportListener()) {
const observer = new MutationObserver(() => {
if (attachGuildImportListener()) {
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
}, 500);
}
}
function initEditGuildFormAutoFill() {
function attachEditGuildImportListener() {
const importBtnSelectors = [
'#editGuildForm > div:nth-child(1) > button:nth-child(1)',
'#editGuildForm button:first-child',
'#editGuildForm .import-data-btn'
];
let importBtn = null;
for (const selector of importBtnSelectors) {
importBtn = document.querySelector(selector);
if (importBtn) {
break;
}
}
if (!importBtn) {
return false;
}
if (importBtn.dataset.editGuildSimulatorListenerAttached) {
importBtn.removeEventListener('click', importBtn._editGuildSimulatorClickHandler);
}
const clickHandler = async function(e) {
try {
await new Promise(resolve => setTimeout(resolve, 100));
const simDataInput = document.querySelector('#editGuildSimData');
if (!simDataInput) {
return;
}
let simulatorData = null;
try {
const storedData = GM_getValue('mwi_simulator_data', '');
if (storedData) {
simulatorData = JSON.parse(storedData);
}
} catch (e) {
}
if (!simulatorData && window.opener && window.opener.MWI_INTEGRATED) {
const getSimData = window.opener.MWI_INTEGRATED.getSimulatorData;
if (typeof getSimData === 'function') {
simulatorData = getSimData();
}
}
if (!simulatorData && window.MWI_INTEGRATED && typeof window.MWI_INTEGRATED.getSimulatorData === 'function') {
simulatorData = window.MWI_INTEGRATED.getSimulatorData();
}
if (!simulatorData) {
const lang = typeof window.detectLanguage === 'function' ? window.detectLanguage() : 'zh';
const message = lang === 'zh' ? '无法获取模拟器数据\n\n请确保:\n1. 已在游戏页面加载完整数据\n2. 游戏页面已接收到WebSocket数据\n3. 刷新游戏页面后再试' : 'Unable to get simulator data\n\nPlease ensure:\n1. Data is fully loaded on game page\n2. Game page has received WebSocket data\n3. Refresh game page and try again';
alert(message);
return;
}
const dataString = JSON.stringify(simulatorData, null, 2);
simDataInput.value = dataString;
simDataInput.dispatchEvent(new Event('input', { bubbles: true }));
simDataInput.dispatchEvent(new Event('change', { bubbles: true }));
} catch (error) {
alert('填写数据失败: ' + error.message);
}
};
importBtn._editGuildSimulatorClickHandler = clickHandler;
importBtn.addEventListener('click', clickHandler);
importBtn.dataset.editGuildSimulatorListenerAttached = 'true';
return true;
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
if (!attachEditGuildImportListener()) {
const observer = new MutationObserver(() => {
if (attachEditGuildImportListener()) {
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
}, 500);
});
} else {
setTimeout(() => {
if (!attachEditGuildImportListener()) {
const observer = new MutationObserver(() => {
if (attachEditGuildImportListener()) {
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
}, 500);
}
}
function initEditCardImageUrlAutoFill() {
let lastProcessedTimestamp = 0;
function fillEditCardLink(cardImageUrl, timestamp) {
if (typeof window.__fillCardLink__ === 'function') {
window.__fillCardLink__(cardImageUrl);
}
lastProcessedTimestamp = timestamp;
cardLinkFillStatus.markFilled('edit', timestamp);
}
function checkAndFillEditCardLink() {
try {
const cardImageUrl = GM_getValue('mwi_card_image_url', '');
const timestamp = GM_getValue('mwi_card_image_timestamp', 0);
if (cardImageUrl && timestamp && timestamp !== lastProcessedTimestamp) {
fillEditCardLink(cardImageUrl, timestamp);
return;
}
} catch (error) {
}
}
window.addEventListener('message', (event) => {
if (event.data && event.data.type === 'MWI_UPLOAD_CARD_RESPONSE' && event.data.success && event.data.imageUrl) {
const timestamp = Date.now();
if (timestamp !== lastProcessedTimestamp) {
fillEditCardLink(event.data.imageUrl, timestamp);
}
}
});
IntervalManager.register('editCardImageUrlAutoFill', checkAndFillEditCardLink, 500);
setTimeout(checkAndFillEditCardLink, 100);
}
window.MWI_INTEGRATED = {
generateCard: null,
closeCard: null,
getData: null,
getSimulatorData: null,
isDataLoaded: null,
websocketData: null
};
const BuildScoreModule = (function() {
function getMarketApiUrl() {
if (window.location.href.includes("milkywayidle.com")) return "https://www.milkywayidle.com/game_data/marketplace.json";
if (window.location.href.includes("milkywayidlecn.com")) return "https://www.milkywayidlecn.com/game_data/marketplace.json";
var server = GM_getValue('mwi_game_server', 'en');
return server === 'cn' ? "https://www.milkywayidlecn.com/game_data/marketplace.json" : "https://www.milkywayidle.com/game_data/marketplace.json";
}
let cachedMarketData = null;
let marketDataTimestamp = 0;
let levelExperienceTable = null;
let itemDetailMap = null;
let actionDetailMap = null;
let houseRoomDetailMap = null;
const enhanceParams = {
enhancing_level: 125,
laboratory_level: 6,
enhancer_bonus: 5.42,
glove_bonus: 12.9,
tea_enhancing: false,
tea_super_enhancing: false,
tea_ultra_enhancing: true,
tea_blessed: true,
priceAskBidRatio: 1,
};
async function fetchMarketData(forceFetch = false) {
const now = Date.now();
if (!forceFetch && cachedMarketData && (now - marketDataTimestamp) < 3600000) {
return cachedMarketData;
}
try {
var gmMktJson = GM_getValue('mwi_market_data_for_sim', '');
var gmMktTs = GM_getValue('mwi_market_data_timestamp', '');
if (!forceFetch && gmMktJson && gmMktTs && (now - parseInt(gmMktTs)) < 3600000) {
cachedMarketData = JSON.parse(gmMktJson);
marketDataTimestamp = parseInt(gmMktTs);
return cachedMarketData;
}
} catch (e) {}
const cachedTimestamp = localStorage.getItem("MWITools_marketAPI_timestamp");
const cachedJson = localStorage.getItem("MWITools_marketAPI_json");
if (!forceFetch && cachedTimestamp && cachedJson && (now - parseInt(cachedTimestamp)) < 3600000) {
try {
cachedMarketData = JSON.parse(cachedJson);
marketDataTimestamp = parseInt(cachedTimestamp);
return cachedMarketData;
} catch (e) {}
}
const sendRequest = typeof GM_xmlhttpRequest === "function" ? GM_xmlhttpRequest : null;
if (!sendRequest) return cachedMarketData;
const response = await new Promise((resolve) => {
sendRequest({
url: getMarketApiUrl(),
method: "GET",
timeout: 5000,
onload: resolve,
onerror: () => resolve(null),
ontimeout: () => resolve(null)
});
});
if (response && response.status === 200) {
const jsonObj = JSON.parse(response.responseText);
if (jsonObj && jsonObj.marketData) {
jsonObj.marketData["/items/coin"] = { 0: { a: 1, b: 1 } };
jsonObj.marketData["/items/task_token"] = { 0: { a: 0, b: 0 } };
jsonObj.marketData["/items/cowbell"] = { 0: { a: 0, b: 0 } };
jsonObj.marketData["/items/small_treasure_chest"] = { 0: { a: 0, b: 0 } };
jsonObj.marketData["/items/medium_treasure_chest"] = { 0: { a: 0, b: 0 } };
jsonObj.marketData["/items/large_treasure_chest"] = { 0: { a: 0, b: 0 } };
jsonObj.marketData["/items/basic_task_badge"] = { 0: { a: 0, b: 0 } };
jsonObj.marketData["/items/advanced_task_badge"] = { 0: { a: 0, b: 0 } };
jsonObj.marketData["/items/expert_task_badge"] = { 0: { a: 0, b: 0 } };
cachedMarketData = jsonObj;
marketDataTimestamp = now;
localStorage.setItem("MWITools_marketAPI_timestamp", now.toString());
localStorage.setItem("MWITools_marketAPI_json", JSON.stringify(jsonObj));
return jsonObj;
}
}
return cachedMarketData;
}
function initClientData(clientData) {
if (clientData) {
levelExperienceTable = clientData.levelExperienceTable;
itemDetailMap = clientData.itemDetailMap;
actionDetailMap = clientData.actionDetailMap;
houseRoomDetailMap = clientData.houseRoomDetailMap;
}
}
function getWeightedMarketPrice(marketPrices, ratio = 0.5) {
if (!marketPrices || !marketPrices[0]) return 0;
let ask = marketPrices[0].a || 0;
let bid = marketPrices[0].b || 0;
if (ask > 0 && bid < 0) bid = ask;
if (bid > 0 && ask < 0) ask = bid;
return ask * ratio + bid * (1 - ratio);
}
async function getHouseFullBuildPrice(house) {
const marketData = await fetchMarketData();
if (!marketData || !houseRoomDetailMap) return 0;
const roomDetail = houseRoomDetailMap[house.houseRoomHrid];
if (!roomDetail || !roomDetail.upgradeCostsMap) return 0;
let cost = 0;
for (let i = 1; i <= house.level; i++) {
const levelCosts = roomDetail.upgradeCostsMap[i];
if (levelCosts) {
for (const item of levelCosts) {
const prices = marketData.marketData[item.itemHrid];
if (prices && prices[0]) {
cost += item.count * getWeightedMarketPrice(prices);
}
}
}
}
return cost;
}
async function calculateAbilityScore(combatAbilities) {
const marketData = await fetchMarketData();
if (!marketData || !combatAbilities || !levelExperienceTable) return 0;
const exp50Skills = ["poke", "scratch", "smack", "quick_shot", "water_strike", "fireball", "entangle", "minor_heal"];
const getNeedBooksToLevel = (targetLevel, expPerBook) => {
const needExp = levelExperienceTable[targetLevel] || 0;
return parseFloat(((needExp / expPerBook) + 1).toFixed(1));
};
let totalPrice = 0;
for (const ability of combatAbilities) {
if (!ability || !ability.abilityHrid) continue;
const expPerBook = exp50Skills.some(s => ability.abilityHrid.includes(s)) ? 50 : 500;
const numBooks = getNeedBooksToLevel(ability.level, expPerBook);
const itemHrid = ability.abilityHrid.replace("/abilities/", "/items/");
const prices = marketData.marketData[itemHrid];
if (prices && prices[0]) {
totalPrice += numBooks * getWeightedMarketPrice(prices);
}
}
return totalPrice / 1000000;
}
function Enhancelate(itemHrid, stopAt, protectAt) {
const successRate = [50, 45, 45, 40, 40, 40, 35, 35, 35, 35, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30];
const itemLevel = itemDetailMap?.[itemHrid]?.itemLevel || 1;
const effectiveLevel = enhanceParams.enhancing_level +
(enhanceParams.tea_enhancing ? 3 : 0) +
(enhanceParams.tea_super_enhancing ? 6 : 0) +
(enhanceParams.tea_ultra_enhancing ? 8 : 0);
let totalBonus;
if (effectiveLevel >= itemLevel) {
totalBonus = 1 + (0.05 * (effectiveLevel + enhanceParams.laboratory_level - itemLevel) + enhanceParams.enhancer_bonus) / 100;
} else {
totalBonus = 1 - 0.5 * (1 - effectiveLevel / itemLevel) + (0.05 * enhanceParams.laboratory_level + enhanceParams.enhancer_bonus) / 100;
}
let markov = math.zeros(20, 20);
for (let i = 0; i < stopAt; i++) {
const successChance = (successRate[i] / 100.0) * totalBonus;
const destination = i >= protectAt ? i - 1 : 0;
if (enhanceParams.tea_blessed) {
markov.set([i, i + 2], successChance * 0.01);
markov.set([i, i + 1], successChance * 0.99);
markov.set([i, destination], 1 - successChance);
} else {
markov.set([i, i + 1], successChance);
markov.set([i, destination], 1.0 - successChance);
}
}
markov.set([stopAt, stopAt], 1.0);
const Q = markov.subset(math.index(math.range(0, stopAt), math.range(0, stopAt)));
const M = math.inv(math.subtract(math.identity(stopAt), Q));
const attemptsArray = M.subset(math.index(math.range(0, 1), math.range(0, stopAt)));
const attempts = math.flatten(math.row(attemptsArray, 0).valueOf()).reduce((a, b) => a + b, 0);
const protectAttempts = M.subset(math.index(math.range(0, 1), math.range(protectAt, stopAt)));
const protectArray = typeof protectAttempts === "number" ? [protectAttempts] : math.flatten(math.row(protectAttempts, 0).valueOf());
const protects = protectArray.map((a, i) => a * markov.get([i + protectAt, i + protectAt - 1])).reduce((a, b) => a + b, 0);
return { actions: attempts, protectCount: protects };
}
function getItemMarketPrice(hrid, marketData) {
const priceData = marketData?.marketData?.[hrid];
if (!priceData || !priceData[0]) return 0;
let ask = priceData[0].a || 0;
let bid = priceData[0].b || 0;
if (ask > 0 && bid < 0) bid = ask;
if (bid > 0 && ask < 0) ask = bid;
return ask * enhanceParams.priceAskBidRatio + bid * (1 - enhanceParams.priceAskBidRatio);
}
function getActionHridFromItemName(name) {
let newName = name.replace("Milk", "Cow");
newName = newName.replace("Log", "Tree");
newName = newName.replace("Cowing", "Milking");
newName = newName.replace("Rainbow Cow", "Unicow");
newName = newName.replace("Collector's Boots", "Collectors Boots");
newName = newName.replace("Knight's Aegis", "Knights Aegis");
if (!actionDetailMap) return null;
for (const action of Object.values(actionDetailMap)) {
if (action.name === newName) {
return action.hrid;
}
}
return null;
}
function getBaseItemProductionCost(itemName, marketData) {
const actionHrid = getActionHridFromItemName(itemName);
if (!actionHrid || !actionDetailMap || !actionDetailMap[actionHrid]) {
return -1;
}
let totalPrice = 0;
const inputItems = JSON.parse(JSON.stringify(actionDetailMap[actionHrid].inputItems || []));
for (let item of inputItems) {
totalPrice += getItemMarketPrice(item.itemHrid, marketData) * item.count;
}
totalPrice *= 0.9;
const upgradedFromItemHrid = actionDetailMap[actionHrid]?.upgradeItemHrid;
if (upgradedFromItemHrid) {
totalPrice += getItemMarketPrice(upgradedFromItemHrid, marketData) * 1;
}
return totalPrice;
}
function getRealisticBaseItemPrice(hrid, marketData) {
const itemDetail = itemDetailMap?.[hrid];
if (!itemDetail) return 0;
const productionCost = getBaseItemProductionCost(itemDetail.name, marketData);
const priceData = marketData?.marketData?.[hrid];
const ask = priceData?.[0]?.a || 0;
const bid = priceData?.[0]?.b || 0;
let result = 0;
if (ask > 0) {
if (bid > 0) {
if (ask / bid > 1.3) {
result = Math.max(bid, productionCost);
} else {
result = ask;
}
} else {
if (productionCost > 0 && ask / productionCost > 1.3) {
result = productionCost;
} else {
result = Math.max(ask, productionCost);
}
}
} else {
if (bid > 0) {
result = Math.max(bid, productionCost);
} else {
result = productionCost > 0 ? productionCost : 0;
}
}
return result;
}
function getCosts(hrid, marketData) {
const detail = itemDetailMap?.[hrid];
if (!detail) return null;
const baseCost = getRealisticBaseItemPrice(hrid, marketData);
const protectHrids = detail.protectionItemHrids
? [hrid, "/items/mirror_of_protection", ...detail.protectionItemHrids]
: [hrid, "/items/mirror_of_protection"];
let minProtectCost = getRealisticBaseItemPrice(protectHrids[0], marketData);
for (let i = 1; i < protectHrids.length; i++) {
const cost = getRealisticBaseItemPrice(protectHrids[i], marketData);
if (cost > 0 && (minProtectCost <= 0 || cost < minProtectCost)) {
minProtectCost = cost;
}
}
let perActionCost = 0;
if (detail.enhancementCosts) {
for (const need of detail.enhancementCosts) {
const price = need.itemHrid.startsWith("/items/trainee_") ? 250000 : getRealisticBaseItemPrice(need.itemHrid, marketData);
perActionCost += price * need.count;
}
}
return { baseCost, minProtectCost, perActionCost };
}
async function findBestEnhanceStrat(itemHrid, stopAt) {
const marketData = await fetchMarketData();
if (!marketData || !itemDetailMap) return null;
let best = null;
for (let protectAt = 2; protectAt <= stopAt; protectAt++) {
const sim = Enhancelate(itemHrid, stopAt, protectAt);
const costs = getCosts(itemHrid, marketData);
if (!costs) continue;
const totalCost = costs.baseCost + costs.minProtectCost * sim.protectCount + costs.perActionCost * sim.actions;
if (!best || totalCost < best.totalCost) {
best = { protectAt, totalCost };
}
}
return best;
}
async function findBestEnhanceStratWithPhiMirror(itemHrid, stopAt) {
const marketData = await fetchMarketData();
if (!marketData || !itemDetailMap) return null;
let best = await findBestEnhanceStrat(itemHrid, stopAt);
if (!best) return best;
const pMirrorHrid = "/items/philosophers_mirror";
const pMirrorCost = getItemMarketPrice(pMirrorHrid, marketData);
if (pMirrorCost <= 0) return best;
if (stopAt <= 3) return best;
const keyRefined = "_refined";
const isRefined = itemHrid.includes(keyRefined);
const baseItemHrid = isRefined ? itemHrid.replace(keyRefined, "") : itemHrid;
const lowerBest = {};
const lowestAt = 9;
for (let i = lowestAt; i < stopAt; i++) {
lowerBest[i] = await findBestEnhanceStrat(baseItemHrid, i);
}
let refinedCost = 0;
if (isRefined && actionDetailMap) {
const itemDetail = itemDetailMap[itemHrid];
if (itemDetail) {
const actionHrid = getActionHridFromItemName(itemDetail.name);
if (actionHrid && actionDetailMap[actionHrid]?.inputItems) {
for (const item of actionDetailMap[actionHrid].inputItems) {
refinedCost += getItemMarketPrice(item.itemHrid, marketData) * item.count;
}
}
}
}
const fibonacci = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181];
for (let protectAt = lowestAt + 1; protectAt < stopAt; protectAt++) {
if (!lowerBest[protectAt] || !lowerBest[protectAt - 1]) continue;
const baseCount = fibonacci[stopAt - protectAt + 1];
const inputCount = fibonacci[stopAt - protectAt];
const protectCount = baseCount + inputCount - 1;
const totalCost = baseCount * lowerBest[protectAt].totalCost + inputCount * lowerBest[protectAt - 1].totalCost + pMirrorCost * protectCount + refinedCost;
if (totalCost < best.totalCost) {
best = { protectAt, totalCost };
}
}
return best;
}
async function calculateEquipmentScore(characterItems) {
const marketData = await fetchMarketData();
if (!marketData || !characterItems) return 0;
let totalValue = 0;
for (const item of characterItems) {
if (item.itemLocationHrid === "/item_locations/inventory") continue;
const enhanceLevel = item.enhancementLevel || 0;
const prices = marketData.marketData[item.itemHrid];
if (enhanceLevel > 1) {
const best = await findBestEnhanceStratWithPhiMirror(item.itemHrid, enhanceLevel);
if (best) {
var roundedCost = best.totalCost ? Math.round(best.totalCost) : 0;
totalValue += item.count * roundedCost;
}
} else if (prices && prices[0]) {
const ask = prices[0].a > 0 ? prices[0].a : 0;
const bid = prices[0].b > 0 ? prices[0].b : 0;
totalValue += item.count * (ask * 0.5 + bid * 0.5);
}
}
return totalValue / 1000000;
}
async function calculateBuildScore(characterData) {
if (!characterData) return null;
const battleHouses = ["dining_room", "library", "dojo", "gym", "armory", "archery_range", "mystical_study"];
let houseScore = 0;
if (characterData.characterHouseRoomMap) {
for (const key in characterData.characterHouseRoomMap) {
const house = characterData.characterHouseRoomMap[key];
if (battleHouses.some(h => house.houseRoomHrid.includes(h))) {
houseScore += await getHouseFullBuildPrice(house) / 1000000;
}
}
}
const combatAbilities = characterData.combatUnit?.combatAbilities || [];
const abilityScore = await calculateAbilityScore(combatAbilities);
const equipmentScore = await calculateEquipmentScore(characterData.characterItems || []);
const totalScore = houseScore + abilityScore + equipmentScore;
return {
total: parseFloat(totalScore.toFixed(1)),
house: parseFloat(houseScore.toFixed(1)),
ability: parseFloat(abilityScore.toFixed(1)),
equipment: parseFloat(equipmentScore.toFixed(1))
};
}
return {
initClientData,
fetchMarketData,
calculateBuildScore
};
})();
const DataStore = {
raw: null,
characterSkills: null,
characterItems: null,
characterAbilities: null,
combatUnit: null,
characterHouseRoomMap: null,
currentEquipmentMap: {},
currentActions: [],
combatSetup: null,
actionTypeFoodSlotsMap: null,
actionTypeDrinkSlotsMap: null,
abilityCombatTriggersMap: null,
consumableCombatTriggersMap: null,
characterName: null,
totalLevel: null,
buildScore: null,
weaponType: null,
weaponTypeEN: null,
combatLevel: null,
guildName: null,
guildLevel: null,
guildMembers: null,
characterId: null,
nameColor: null,
nameColorHrid: null,
nameElementHTML: null,
chatIconHrid: null,
supporterBadgeHrid: null,
profileIconHrid: null,
avatarHrid: null,
outfitHrid: null,
itemDetailMap: null,
actionDetailMap: null,
abilityDetailMap: null,
achievementDetailMap: null,
achievementTierDetailMap: null,
characterAchievements: null,
isLoaded: false,
hookInstalled: false,
dataLoadedOnce: false,
gameMode: null,
partyInfo: null,
sharableCharacterMap: null,
wsConnection: null,
pendingSentMessages: [],
profileMap: {},
profileRequestTemplate: null,
suppressProfileUI: false
};
const WebSocketHook = {
install() {
if (DataStore.hookInstalled) {
return false;
}
this.hookSend();
window.addEventListener('mwi_websocket_data', (e) => {
if (e.detail && e.detail.rawMessage) {
this.handleMessage(e.detail.rawMessage);
}
});
try {
const descriptor = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data");
if (!descriptor || !descriptor.get) {
DataStore.hookInstalled = true;
return false;
}
const originalGetter = descriptor.get;
if (originalGetter._MWI_HOOKED || originalGetter._MWI_INTEGRATED_HOOKED) {
DataStore.hookInstalled = true;
return false;
}
descriptor.get = function interceptData() {
const ws = this.currentTarget;
if (!(ws instanceof WebSocket)) {
return originalGetter.call(this);
}
const isMWIWebSocket = ws.url && (
ws.url.includes("api.milkywayidle.com/ws") ||
ws.url.includes("api-test.milkywayidle.com/ws") ||
ws.url.includes("api.milkywayidlecn.com/ws") ||
ws.url.includes("api-test.milkywayidlecn.com/ws")
);
if (!isMWIWebSocket) {
return originalGetter.call(this);
}
if (!DataStore.wsConnection || DataStore.wsConnection.readyState !== WebSocket.OPEN) {
DataStore.wsConnection = ws;
}
const rawMessage = originalGetter.call(this);
try {
Object.defineProperty(this, "data", {
value: rawMessage,
writable: false,
configurable: true
});
} catch (e) {}
try {
window.dispatchEvent(new CustomEvent('mwi_websocket_data', {
detail: { rawMessage: rawMessage }
}));
} catch (e) {}
WebSocketHook.handleMessage(rawMessage);
return rawMessage;
};
descriptor.get._MWI_HOOKED = true;
descriptor.get._MWI_INTEGRATED_HOOKED = true;
try {
Object.defineProperty(MessageEvent.prototype, "data", descriptor);
} catch (e) {
DataStore.hookInstalled = true;
return false;
}
DataStore.hookInstalled = true;
return true;
} catch (error) {
DataStore.hookInstalled = true;
return false;
}
},
hookSend() {
const origSend = WebSocket.prototype.send;
if (origSend._MWI_TM_SEND_HOOKED) return;
WebSocket.prototype.send = function (data) {
const isMWI = this.url && (
this.url.includes("api.milkywayidle.com/ws") ||
this.url.includes("api-test.milkywayidle.com/ws") ||
this.url.includes("api.milkywayidlecn.com/ws") ||
this.url.includes("api-test.milkywayidlecn.com/ws")
);
if (isMWI) {
if (!DataStore.wsConnection || DataStore.wsConnection.readyState !== WebSocket.OPEN) {
DataStore.wsConnection = this;
}
try {
const msg = JSON.parse(data);
DataStore.pendingSentMessages.push({ msg, timestamp: Date.now() });
if (DataStore.pendingSentMessages.length > 20) {
DataStore.pendingSentMessages.shift();
}
} catch (e) { }
}
return origSend.call(this, data);
};
WebSocket.prototype.send._MWI_TM_SEND_HOOKED = true;
},
_simDataSaveTimer: null,
_debouncedSaveSimData(delay) {
const self = this;
if (self._simDataSaveTimer) clearTimeout(self._simDataSaveTimer);
self._simDataSaveTimer = setTimeout(() => {
self._simDataSaveTimer = null;
try {
const simulatorData = self.generateSimulatorData();
if (simulatorData) {
GM_setValue('mwi_simulator_data', JSON.stringify(simulatorData));
}
} catch (e) {}
}, delay || 300);
},
handleMessage(rawMessage) {
try {
const data = JSON.parse(rawMessage);
switch (data.type) {
case "init_character_data":
this.processCharacterData(data);
DataStore.dataLoadedOnce = true;
break;
case "init_client_data":
if (!DataStore.dataLoadedOnce) this.processClientData(data);
break;
case "items_updated":
this.updateItems(data);
break;
case "profile_shared":
this.storeProfile(data);
break;
case "actions_updated":
if (DataStore.isLoaded && data.characterActions) {
DataStore.currentActions = data.characterActions;
}
break;
case "party_updated":
if (DataStore.isLoaded) {
DataStore.partyInfo = data.partyInfo || DataStore.partyInfo;
if (data.partyInfo?.sharableCharacterMap) {
DataStore.sharableCharacterMap = data.partyInfo.sharableCharacterMap;
}
}
break;
default:
break;
}
} catch (error) {
}
},
processCharacterData(data) {
DataStore.raw = data;
DataStore.characterSkills = data.characterSkills || [];
DataStore.characterItems = data.characterItems || [];
DataStore.characterAbilities = data.characterAbilities || [];
DataStore.combatUnit = data.combatUnit || null;
DataStore.characterHouseRoomMap = data.characterHouseRoomMap || {};
DataStore.currentActions = data.characterActions || [];
DataStore.combatSetup = data.characterCombatSetup || null;
DataStore.actionTypeFoodSlotsMap = data.actionTypeFoodSlotsMap || {};
DataStore.actionTypeDrinkSlotsMap = data.actionTypeDrinkSlotsMap || {};
DataStore.abilityCombatTriggersMap = data.abilityCombatTriggersMap || {};
DataStore.consumableCombatTriggersMap = data.consumableCombatTriggersMap || {};
DataStore.characterName = data.characterName ||
data.character?.name ||
data.combatUnit?.name ||
this.getNameFromDOM() ||
"Unknown Adventurer";
DataStore.characterId = data.character?.id || null;
DataStore.gameMode = data.gameMode || data.character?.gameMode || null;
DataStore.nameColor = null;
DataStore.nameColorHrid = data.character?.nameColorHrid || null;
DataStore.chatIconHrid = data.character?.chatIconHrid || null;
DataStore.supporterBadgeHrid = data.character?.specialChatIconHrid || null;
DataStore.profileIconHrid = data.character?.profileIconHrid || null;
DataStore.avatarHrid = data.character?.avatarHrid || null;
DataStore.outfitHrid = data.character?.avatarOutfitHrid || null;
DataStore.characterAchievements = data.characterAchievements || [];
const host = location.hostname;
const apiUrls = host.includes('test.milkywayidlecn') ? ['https://api-test.milkywayidlecn.com/v1/users/me']
: host.includes('milkywayidlecn') ? ['https://api.milkywayidlecn.com/v1/users/me']
: host.includes('test.milkywayidle') ? ['https://api-test.milkywayidle.com/v1/users/me']
: ['https://api.milkywayidle.com/v1/users/me'];
const tryFetch = (index = 0) => {
if (index >= apiUrls.length) return;
fetch(apiUrls[index], { credentials: 'include' })
.then(r => {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(apiData => {
if (apiData?.characters) {
const char = apiData.characters.find(c => c.name === DataStore.characterName);
if (char) {
DataStore.chatIconHrid = char.chatIconHrid || DataStore.chatIconHrid;
DataStore.supporterBadgeHrid = char.specialChatIconHrid || DataStore.supporterBadgeHrid;
DataStore.avatarHrid = char.avatarHrid || DataStore.avatarHrid;
DataStore.outfitHrid = char.avatarOutfitHrid || DataStore.outfitHrid;
DataStore.nameColorHrid = char.nameColorHrid || DataStore.nameColorHrid;
DataStore.gameMode = char.gameMode || DataStore.gameMode;
this._debouncedSaveSimData();
}
}
})
.catch(() => tryFetch(index + 1));
};
tryFetch();
DataStore.guildName = data.guild?.name || null;
DataStore.guildLevel = data.guild?.level || null;
DataStore.guildMembers = data.guildCharacterMap ? Object.keys(data.guildCharacterMap).length : null;
DataStore.totalLevel = null;
DataStore.buildScore = null;
const self = this;
const checkTotalLevel = (attempt = 0, maxAttempts = 10) => {
const totalLevelElem = document.querySelector('.Header_totalLevel__8LY3Q');
if (totalLevelElem) {
const match = totalLevelElem.textContent.match(/(\d+)/);
if (match) {
DataStore.totalLevel = parseInt(match[1]);
window.dispatchEvent(new CustomEvent('mwi_totallevel_updated', { detail: { totalLevel: DataStore.totalLevel } }));
self._debouncedSaveSimData();
return;
}
}
if (attempt < maxAttempts) {
setTimeout(() => checkTotalLevel(attempt + 1, maxAttempts), 500);
}
};
setTimeout(() => checkTotalLevel(), 1500);
const nameColorHrid = data.character?.nameColorHrid || null;
DataStore.nameElementHTML = null;
if (nameColorHrid || DataStore.characterName) {
setTimeout(() => {
const nameContainer = document.querySelector('.CharacterName_characterName__2FqyZ') ||
document.querySelector('[class*="CharacterName_characterName"]');
if (nameContainer) {
const nameLink = nameContainer.querySelector('a') || nameContainer;
const computedStyle = window.getComputedStyle(nameLink);
const extractedColor = computedStyle.color;
if (extractedColor && extractedColor !== 'rgb(255, 255, 255)' && extractedColor !== 'rgb(0, 0, 0)') {
DataStore.nameColor = extractedColor;
self._debouncedSaveSimData();
}
const clonedContainer = nameContainer.cloneNode(true);
const chatIcon = clonedContainer.querySelector('.CharacterName_chatIcon__22lxV') ||
clonedContainer.querySelector('[class*="CharacterName_chatIcon"]');
if (chatIcon) chatIcon.remove();
const clickableElements = clonedContainer.querySelectorAll('a, [href]');
clickableElements.forEach(el => {
if (el.tagName.toLowerCase() === 'a') {
const span = document.createElement('span');
span.innerHTML = el.innerHTML;
if (el.className) span.className = el.className;
if (el.style.cssText) span.style.cssText = el.style.cssText;
el.replaceWith(span);
} else {
el.removeAttribute('href');
}
});
DataStore.nameElementHTML = clonedContainer.outerHTML;
}
}, 2000);
}
DataStore.partyInfo = data.partyInfo || null;
DataStore.sharableCharacterMap = data.partyInfo?.sharableCharacterMap || null;
DataStore.currentEquipmentMap = {};
DataStore.characterItems.forEach(item => {
if (item.itemLocationHrid && item.itemLocationHrid !== "/item_locations/inventory") {
DataStore.currentEquipmentMap[item.itemLocationHrid] = item;
}
});
const weaponInfo = getWeapon(DataStore.currentEquipmentMap);
DataStore.weaponType = weaponInfo.zh;
DataStore.weaponTypeEN = weaponInfo.en;
DataStore.combatLevel = DataStore.combatUnit?.combatLevel || null;
if (DataStore.combatLevel === null && DataStore.characterSkills) {
const stamina = DataStore.characterSkills.find(s => s.skillHrid === '/skills/stamina')?.level || 0;
const intelligence = DataStore.characterSkills.find(s => s.skillHrid === '/skills/intelligence')?.level || 0;
const attack = DataStore.characterSkills.find(s => s.skillHrid === '/skills/attack')?.level || 0;
const defense = DataStore.characterSkills.find(s => s.skillHrid === '/skills/defense')?.level || 0;
const melee = DataStore.characterSkills.find(s => s.skillHrid === '/skills/melee')?.level || 0;
const ranged = DataStore.characterSkills.find(s => s.skillHrid === '/skills/ranged')?.level || 0;
const magic = DataStore.characterSkills.find(s => s.skillHrid === '/skills/magic')?.level || 0;
const maxCombatSkill = Math.max(melee, ranged, magic);
const maxAllCombat = Math.max(attack, defense, melee, ranged, magic);
DataStore.combatLevel = Math.floor(0.1 * (stamina + intelligence + attack + defense + maxCombatSkill) + 0.5 * maxAllCombat);
}
DataStore.isLoaded = true;
try { GM_setValue("bt_auth_name", DataStore.characterName); } catch (e) { }
btRestoreAutoUploadState();
window.MWI_INTEGRATED.websocketData = data;
this._debouncedSaveSimData(0);
window.dispatchEvent(new CustomEvent("mwi_data_ready", {
detail: { characterName: DataStore.characterName, dataLoaded: true }
}));
if (DataStore.itemDetailMap) {
const self = this;
(async () => {
try {
const scoreResult = await BuildScoreModule.calculateBuildScore(DataStore.raw);
if (scoreResult) {
DataStore.buildScore = scoreResult.total;
window.dispatchEvent(new CustomEvent('mwi_buildscore_updated', { detail: { buildScore: DataStore.buildScore } }));
self._debouncedSaveSimData();
}
} catch (e) {}
})();
}
},
processClientData(data) {
DataStore.itemDetailMap = data.itemDetailMap || null;
DataStore.actionDetailMap = data.actionDetailMap || null;
DataStore.abilityDetailMap = data.abilityDetailMap || null;
DataStore.achievementDetailMap = data.achievementDetailMap || null;
DataStore.achievementTierDetailMap = data.achievementTierDetailMap || null;
BuildScoreModule.initClientData(data);
try {
const clientDataForSim = {
levelExperienceTable: data.levelExperienceTable || null,
itemDetailMap: data.itemDetailMap || null,
actionDetailMap: data.actionDetailMap || null,
houseRoomDetailMap: data.houseRoomDetailMap || null
};
GM_setValue('mwi_client_data_for_sim', LZString.compressToUTF16(JSON.stringify(clientDataForSim)));
} catch (e) { /* silent */ }
if (DataStore.raw) {
const self = this;
(async () => {
try {
const scoreResult = await BuildScoreModule.calculateBuildScore(DataStore.raw);
if (scoreResult) {
DataStore.buildScore = scoreResult.total;
window.dispatchEvent(new CustomEvent('mwi_buildscore_updated', { detail: { buildScore: DataStore.buildScore } }));
self._debouncedSaveSimData();
}
} catch (e) {}
})();
}
},
updateItems(data) {
if (!DataStore.isLoaded || !data.characterItems) return;
DataStore.characterItems = data.characterItems;
DataStore.currentEquipmentMap = {};
DataStore.characterItems.forEach(item => {
if (item.itemLocationHrid && item.itemLocationHrid !== "/item_locations/inventory") {
DataStore.currentEquipmentMap[item.itemLocationHrid] = item;
}
});
const weaponInfo = getWeapon(DataStore.currentEquipmentMap);
DataStore.weaponType = weaponInfo.zh;
DataStore.weaponTypeEN = weaponInfo.en;
},
getNameFromDOM() {
const selectors = [
'.CharacterName_characterName__2FqyZ',
'.Header_name__227rJ',
'.character-name',
'.player-name'
];
for (const selector of selectors) {
const elem = document.querySelector(selector);
if (elem?.textContent?.trim()) return elem.textContent.trim();
}
return null;
},
generateSimulatorData() {
if (!DataStore.characterSkills) {
return null;
}
const foodSlots = (DataStore.actionTypeFoodSlotsMap || {})["/action_types/combat"] || [];
const drinkSlots = (DataStore.actionTypeDrinkSlotsMap || {})["/action_types/combat"] || [];
const mapSlots = (slots) => slots.map(slot => ({ itemHrid: slot?.itemHrid || "" }));
const food = { "/action_types/combat": mapSlots(foodSlots) };
const drinks = { "/action_types/combat": mapSlots(drinkSlots) };
const abilities = Array(5).fill({ abilityHrid: "", level: "1" });
const combatAbilities = DataStore.combatUnit?.combatAbilities || [];
let normalIdx = 1;
combatAbilities.forEach(ability => {
if (!ability) return;
const detail = DataStore.abilityDetailMap?.[ability.abilityHrid];
const idx = detail?.isSpecialAbility ? 0 : (normalIdx < abilities.length ? normalIdx++ : -1);
if (idx >= 0) {
abilities[idx] = { abilityHrid: ability.abilityHrid, level: ability.level };
}
});
const triggerMap = Object.assign(
{},
DataStore.abilityCombatTriggersMap || {},
DataStore.consumableCombatTriggersMap || {}
);
const productionTools = [
"/item_locations/woodcutting_tool",
"/item_locations/foraging_tool",
"/item_locations/milking_tool",
"/item_locations/cheesesmithing_tool",
"/item_locations/crafting_tool",
"/item_locations/tailoring_tool",
"/item_locations/cooking_tool",
"/item_locations/brewing_tool",
"/item_locations/alchemy_tool",
"/item_locations/enhancing_tool"
];
const getSkillLevel = (skillHrid) => {
return DataStore.characterSkills?.find(s => s.skillHrid === skillHrid)?.level || 0;
};
const meleeLevel = getSkillLevel("/skills/melee");
const rangedLevel = getSkillLevel("/skills/ranged");
const magicLevel = getSkillLevel("/skills/magic");
return {
player: {
defenseLevel: getSkillLevel("/skills/defense"),
magicLevel: magicLevel,
attackLevel: getSkillLevel("/skills/attack"),
intelligenceLevel: getSkillLevel("/skills/intelligence"),
staminaLevel: getSkillLevel("/skills/stamina"),
meleeLevel: meleeLevel,
rangedLevel: rangedLevel,
equipment: Object.entries(DataStore.currentEquipmentMap)
.filter(([location]) => !productionTools.includes(location))
.map(([location, item]) => ({
itemLocationHrid: location,
itemHrid: item.itemHrid,
enhancementLevel: item.enhancementLevel || 0
}))
},
food: food,
drinks: drinks,
abilities: abilities,
triggerMap: triggerMap,
houseRooms: Object.fromEntries(
Object.entries(DataStore.characterHouseRoomMap || {}).map(([hrid, data]) => [hrid, data.level || 0])
),
characterName: DataStore.characterName || "Unknown Adventurer",
totalLevel: DataStore.totalLevel || null,
buildScore: DataStore.buildScore || null,
combatLevel: DataStore.combatLevel || null,
weaponType: DataStore.weaponTypeEN || null,
guildName: DataStore.guildName || null,
guildLevel: DataStore.guildLevel || null,
guildMembers: DataStore.guildMembers || null,
nameColorHrid: DataStore.nameColorHrid || null,
chatIconHrid: DataStore.chatIconHrid || null,
supporterBadgeHrid: DataStore.supporterBadgeHrid || null,
gameMode: DataStore.gameMode || null
};
},
storeProfile(data) {
if (!data.profile?.characterSkills?.[0]?.characterID) return;
const charID = data.profile.characterSkills[0].characterID;
const charName = data.profile.sharableCharacter?.name || "Unknown";
DataStore.profileMap[charID] = { characterID: charID, characterName: charName, profile: data.profile, timestamp: Date.now() };
const wt = getWeapon(data.profile?.wearableItemMap);
btCacheWeaponType(charID, charName, wt?.en || null);
try {
GM_xmlhttpRequest({
method: "POST",
url: SITE_URL + "/api/teams/player",
headers: { "Content-Type": "application/json" },
data: JSON.stringify({ characterId: charID, playerName: charName, weaponType: wt?.en || '' }),
onload(r) { },
onerror(e) { }
});
} catch (e) { }
}
};
const BT_WEAPON_LISTS = {
bow: ["gobo_shooter", "cursed_bow", "cursed_bow_refined"],
water: ["rippling_trident", "rippling_trident_refined", "frost_staff", "frost_staff_refined"],
fire: ["gobo_boomstick", "blazing_trident", "blazing_trident_refined", "infernal_battlestaff", "infernal_battlestaff_refined"],
nature: ["jackalope_staff", "jackalope_staff_refined", "blooming_trident", "blooming_trident_refined"],
sword: ["gobo_slasher", "werewolf_slasher", "werewolf_slasher_refined"],
mace: ["gobo_smasher", "granite_bludgeon", "granite_bludgeon_refined", "chaotic_flail", "chaotic_flail_refined"],
spear: ["gobo_stabber"],
bulwark: ["griffin_bulwark", "griffin_bulwark_refined", "spiked_bulwark", "spiked_bulwark_refined"]
};
function btGetItemHridBySlot(map, slot) {
const d = map[slot]?.itemHrid;
if (d) return d;
for (const k in map) { if (map[k].itemLocationHrid === slot) return map[k].itemHrid; }
return null;
}
function getWeapon(wearableItemMap) {
if (!wearableItemMap) return { zh: null, en: null };
const weaponTypeMap = {
"水法": "Water",
"火法": "Fire",
"自然法": "Nature",
"弓": "Bow",
"弩": "Crossbow",
"盾": "Bulwark",
"枪": "Spear",
"锤": "Flail",
"剑": "Sword"
};
const oh = btGetItemHridBySlot(wearableItemMap, '/item_locations/off_hand');
if (oh) {
const offWeapon = oh.includes('/') ? oh.split('/').pop() : oh;
if (offWeapon && (offWeapon.includes("_bulwark") || BT_WEAPON_LISTS.bulwark.includes(offWeapon))) {
return { zh: "盾", en: "Bulwark" };
}
}
const h = btGetItemHridBySlot(wearableItemMap, '/item_locations/main_hand') || btGetItemHridBySlot(wearableItemMap, '/item_locations/two_hand');
if (!h) return { zh: null, en: null };
const weapon = h.includes('/') ? h.split('/').pop() : h;
if (!weapon) return { zh: null, en: null };
let typeZH = null;
if (weapon.includes("_bow") || BT_WEAPON_LISTS.bow.includes(weapon)) {
typeZH = "弓";
} else if (weapon.includes("_crossbow")) {
typeZH = "弩";
} else if (weapon.includes("_water_staff") || BT_WEAPON_LISTS.water.includes(weapon)) {
typeZH = "水法";
} else if (weapon.includes("_fire_staff") || BT_WEAPON_LISTS.fire.includes(weapon)) {
typeZH = "火法";
} else if (weapon.includes("_nature_staff") || BT_WEAPON_LISTS.nature.includes(weapon)) {
typeZH = "自然法";
} else if (weapon.includes("_sword") || BT_WEAPON_LISTS.sword.includes(weapon)) {
if (weapon === "cheese_sword") return { zh: null, en: null };
typeZH = "剑";
} else if (weapon.includes("_mace") || BT_WEAPON_LISTS.mace.includes(weapon)) {
typeZH = "锤";
} else if (weapon.includes("_spear") || BT_WEAPON_LISTS.spear.includes(weapon)) {
typeZH = "枪";
} else if (weapon.includes("_bulwark") || BT_WEAPON_LISTS.bulwark.includes(weapon)) {
typeZH = "盾";
}
return {
zh: typeZH,
en: typeZH ? weaponTypeMap[typeZH] : null
};
}
// ==================== Persistent Weapon Cache ====================
const BT_WEAPON_CACHE_KEY = 'mwi_bt_weapon_cache';
let btWeaponCache = {};
try { btWeaponCache = JSON.parse(GM_getValue(BT_WEAPON_CACHE_KEY, '{}')); } catch (e) { btWeaponCache = {}; }
function btSaveWeaponCache() {
try { GM_setValue(BT_WEAPON_CACHE_KEY, JSON.stringify(btWeaponCache)); } catch (e) {}
}
function btCacheWeaponType(characterID, characterName, weaponType) {
btWeaponCache[characterID] = { name: characterName, weapon: weaponType, ts: Date.now() };
btSaveWeaponCache();
}
function btGetCachedWeaponType(characterID) {
return btWeaponCache[characterID]?.weapon || null;
}
function btGetMissingMembers() {
const psm = DataStore.partyInfo?.partySlotMap;
if (!psm) return [];
const missing = [];
for (const m of Object.values(psm)) {
if (!m.characterID || m.characterID === DataStore.characterId) continue;
if (DataStore.profileMap[m.characterID] || btWeaponCache[m.characterID]) continue;
const sc = DataStore.sharableCharacterMap?.[m.characterID] || DataStore.sharableCharacterMap?.[String(m.characterID)];
missing.push(sc?.name || 'Unknown');
}
return missing;
}
function btRestoreAutoUploadState() {
if (GM_getValue('mwi_upload_team_disabled', false)) return;
BattleTeamsModule.startAutoUpload();
}
const BattleTeamsModule = (() => {
const UPLOAD_URL = SITE_URL + "/api/teams/upload";
const AUTO_INTERVAL_MS = 90 * 60 * 1000;
const ADZ = ['pirate_cove', 'enchanted_fortress', 'sinister_circus', 'chimerical_den'];
let autoUploadTimer = null;
function getMemberName(characterID) {
const p = DataStore.profileMap[characterID];
if (p) return p.characterName;
const sm = DataStore.sharableCharacterMap;
const sc = sm?.[characterID] || sm?.[String(characterID)];
return sc?.name || "Unknown";
}
function buildTeamExportData() {
if (!DataStore.isLoaded) return { error: "Game data not loaded" };
const psm = DataStore.partyInfo?.partySlotMap;
const members = [];
let zone = "", difficulty = "";
if (psm) {
zone = DataStore.partyInfo?.party?.actionHrid || "";
const dt = DataStore.partyInfo?.party?.difficultyTier;
difficulty = (dt != null) ? "T" + dt : "";
}
if (!zone) {
const combatAction = (DataStore.currentActions || []).find(a => a?.actionHrid?.includes("/actions/combat/"));
if (combatAction) {
zone = combatAction.actionHrid;
difficulty = (combatAction.difficultyTier != null) ? "T" + combatAction.difficultyTier : "";
}
}
const selfWeapon = getWeapon(DataStore.currentEquipmentMap).en;
if (!psm) {
if (!zone) zone = 'solo';
members.push({ slot: 1, player_name: DataStore.characterName, weapon_type: selfWeapon, weapon_ts: Date.now() });
} else {
if (!zone) return { error: "Not in combat" };
let si = 1;
for (const m of Object.values(psm)) {
if (!m.characterID) { si++; continue; }
if (String(m.characterID) === String(DataStore.characterId)) {
members.push({ slot: si, player_name: DataStore.characterName, weapon_type: selfWeapon, weapon_ts: Date.now() });
} else {
const p = DataStore.profileMap[m.characterID];
const mwt = p ? getWeapon(p.profile?.wearableItemMap).en : btGetCachedWeaponType(m.characterID);
const mts = p ? (p.timestamp || 0) : (btWeaponCache[m.characterID]?.ts || 0);
members.push({ slot: si, player_name: getMemberName(m.characterID), weapon_type: mwt, weapon_ts: mts });
}
si++;
}
}
if (members.length < 1) return { error: "No team members detected" };
const isBattling = !!(DataStore.partyInfo?.party?.status === "battling" || (DataStore.currentActions || []).some(a => a?.actionHrid?.includes("/actions/combat/")));
return { zone, difficulty, members, isBattling };
}
function uploadTeamData(data) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "POST",
url: UPLOAD_URL,
headers: { "Content-Type": "application/json" },
data: JSON.stringify(data),
onload(r) { try { resolve(JSON.parse(r.responseText)); } catch (e) { reject(e); } },
onerror(e) { reject(e); }
});
});
}
async function silentUpload() {
if (!DataStore.isLoaded) return;
if (DataStore.gameMode && DataStore.gameMode !== 'standard') return;
const result = buildTeamExportData();
if (result.error) return;
result.characterType = 'standard';
result.gameMode = DataStore.gameMode || null;
result.reporter = DataStore.characterName || '';
try { await uploadTeamData(result); } catch (e) {}
}
return {
startAutoUpload() {
if (autoUploadTimer) clearInterval(autoUploadTimer);
setTimeout(() => silentUpload(), 5000);
autoUploadTimer = setInterval(() => silentUpload(), AUTO_INTERVAL_MS);
},
stopAutoUpload() {
if (autoUploadTimer) {
clearInterval(autoUploadTimer);
autoUploadTimer = null;
}
},
isRunning() { return autoUploadTimer !== null; }
};
})();
const isZH = !['en'].some(lang => localStorage.getItem("i18nextLng")?.toLowerCase()?.startsWith(lang));
const currentLang = isZH ? 'zh' : 'en';
const i18n = {
combat: { zh: '战斗', en: 'Combat' },
intelligence: { zh: '智力', en: 'Intelligence' },
stamina: { zh: '耐力', en: 'Stamina' },
attack: { zh: '攻击', en: 'Attack' },
defense: { zh: '防御', en: 'Defense' },
melee: { zh: '近战', en: 'Melee' },
ranged: { zh: '远程', en: 'Ranged' },
magic: { zh: '魔法', en: 'Magic' },
house: { zh: '房屋', en: 'House' },
dojo: { zh: '道场', en: 'Dojo' },
library: { zh: '图书馆', en: 'Library' },
dining_room: { zh: '餐厅', en: 'Dining Room' },
mystical_study: { zh: '神秘研究室', en: 'Mystical Study' },
armory: { zh: '军械库', en: 'Armory' },
gym: { zh: '健身房', en: 'Gym' },
archery_range: { zh: '射箭场', en: 'Archery Range' },
back: { zh: '背部', en: 'Back' },
head: { zh: '头部', en: 'Head' },
trinket: { zh: '饰品', en: 'Trinket' },
neck: { zh: '项链', en: 'Neck' },
main_hand: { zh: '主手', en: 'Main Hand' },
body: { zh: '身体', en: 'Body' },
off_hand: { zh: '副手', en: 'Off Hand' },
earrings: { zh: '耳环', en: 'Earrings' },
hands: { zh: '手套', en: 'Hands' },
legs: { zh: '腿部', en: 'Legs' },
pouch: { zh: '腰包', en: 'Pouch' },
ring: { zh: '戒指', en: 'Ring' },
feet: { zh: '鞋子', en: 'Feet' },
charm: { zh: '护符', en: 'Charm' },
two_hand: { zh: '双手', en: 'Two Hand' },
food: { zh: '食物', en: 'Food' },
drink: { zh: '饮料', en: 'Drink' },
abilities: { zh: '技能', en: 'Abilities' },
level: { zh: 'Lv', en: 'Lv' },
unknown: { zh: '未知', en: 'Unknown' }
};
function t(key) {
return i18n[key]?.[currentLang] || key;
}
const ClientData = new class {
#data = null;
#hrid2name = {};
#name2hrid = {};
#loaded = false;
init() {
if (this.#loaded) return;
const compressed = localStorage.getItem("initClientData");
if (!compressed) return;
try {
this.#data = JSON.parse(LZString.decompressFromUTF16(compressed));
this.#buildMappings();
this.#loaded = true;
DataStore.itemDetailMap = this.#data.itemDetailMap;
DataStore.actionDetailMap = this.#data.actionDetailMap;
DataStore.abilityDetailMap = this.#data.abilityDetailMap;
DataStore.achievementDetailMap = this.#data.achievementDetailMap;
DataStore.achievementTierDetailMap = this.#data.achievementTierDetailMap;
} catch (e) { /* silent */ }
}
get() {
if (!this.#loaded) this.init();
return this.#data;
}
#buildMappings() {
if (!this.#data) return;
const maps = [
this.#data.itemDetailMap,
this.#data.abilityDetailMap,
this.#data.actionDetailMap,
this.#data.skillDetailMap
];
for (const detailMap of maps) {
if (!detailMap) continue;
for (const hrid in detailMap) {
const detail = detailMap[hrid];
if (detail && detail.name) {
this.#hrid2name[hrid] = detail.name;
this.#name2hrid[detail.name] = hrid;
}
}
}
}
hrid2name(hrid) {
if (!hrid) return hrid;
if (!this.#loaded) this.init();
return this.#hrid2name[hrid] || hrid.split('/').pop();
}
name2hrid(name) {
if (!name) return name;
if (!this.#loaded) this.init();
return this.#name2hrid[name] || name;
}
getItemDetail(hrid) {
if (!this.#loaded) this.init();
return this.#data?.itemDetailMap?.[hrid] || null;
}
};
function getAllData() {
if (!DataStore.isLoaded) {
return null;
}
return {
characterName: DataStore.characterName,
characterId: DataStore.characterId,
totalLevel: DataStore.totalLevel,
buildScore: DataStore.buildScore,
combatLevel: DataStore.combatLevel,
weaponType: DataStore.weaponType,
weaponTypeEN: DataStore.weaponTypeEN,
guildName: DataStore.guildName,
guildLevel: DataStore.guildLevel,
guildMembers: DataStore.guildMembers,
displayName: DataStore.characterName || 'Player 1',
nameColor: DataStore.nameColor,
chatIconHrid: DataStore.chatIconHrid,
profileIconHrid: DataStore.profileIconHrid,
avatarHrid: DataStore.avatarHrid,
outfitHrid: DataStore.outfitHrid,
characterSkills: DataStore.characterSkills,
characterItems: DataStore.characterItems,
characterAbilities: DataStore.characterAbilities,
equipment: DataStore.currentEquipmentMap,
combatUnit: DataStore.combatUnit,
houseRooms: DataStore.characterHouseRoomMap,
foodSlots: DataStore.actionTypeFoodSlotsMap,
drinkSlots: DataStore.actionTypeDrinkSlotsMap,
combatSetup: DataStore.combatSetup
};
}
function getSimulatorData() {
return WebSocketHook.generateSimulatorData();
}
function isDataReady() {
return DataStore.isLoaded;
}
function waitForData(timeout = 10000) {
return new Promise((resolve, reject) => {
if (DataStore.isLoaded) {
resolve(getAllData());
return;
}
const timer = setTimeout(() => {
window.removeEventListener('mwi_data_ready', handler);
reject(new Error('Data loading timeout'));
}, timeout);
const handler = () => {
clearTimeout(timer);
resolve(getAllData());
};
window.addEventListener('mwi_data_ready', handler, { once: true });
});
}
function toggleTalentMarketModal() {
const existing = document.getElementById('talent-market-modal');
if (existing) {
if (existing.style.display === 'none') {
existing.style.display = '';
if (existing._onShow) existing._onShow();
} else {
if (existing._onHide) existing._onHide();
existing.style.display = 'none';
}
return;
}
createTalentMarketModal();
}
window.toggleTalentMarketModal = toggleTalentMarketModal;
function createTalentMarketModal() {
const isZH = localStorage.getItem("i18nextLng")?.toLowerCase()?.startsWith("zh");
const I18N = {
zh: {
title: 'Talent Market',
back: '返回',
refresh: '刷新',
close: '关闭',
loading: '加载中...',
loadingSlow: '加载时间较长,请稍候...',
loadFailed: '加载失败,请检查网络连接',
loadTimeout: '加载超时,请检查网络连接或刷新页面',
disableUpload: '不上传队伍信息',
},
en: {
title: 'Talent Market',
back: 'Back',
refresh: 'Refresh',
close: 'Close',
loading: 'Loading...',
loadingSlow: 'Loading is taking longer...',
loadFailed: 'Failed to load, check connection',
loadTimeout: 'Loading timeout, please check network connection or refresh the page',
disableUpload: 'Disable Upload',
}
};
const t = I18N[isZH ? 'zh' : 'en'];
const LAYOUT_CONFIG = {
MIN_WIDTH: 540,
MIN_HEIGHT: 787,
NARROW_THRESHOLD: 970,
MOBILE_THRESHOLD: 550,
MARGIN_RATIO: 0.05,
VIEWPORT_RATIO: 0.9,
LOAD_TIMEOUT: 10000,
MESSAGING_DELAY: 200,
RESIZE_DEBOUNCE: 150
};
const existing = document.getElementById('talent-market-modal');
if (existing) {
return existing;
}
const modal = document.createElement('div');
modal.id = 'talent-market-modal';
modal._currentUrl = SITE_URL;
modal.innerHTML = `
`;
const container = modal.querySelector('.tm-container');
initCardUploadMonitor();
bindTalentMarketEvents(modal, t, LAYOUT_CONFIG);
requestAnimationFrame(() => {
document.body.appendChild(modal);
updateScrollbarState(container, LAYOUT_CONFIG);
});
const iframe = modal.querySelector('#tm-iframe');
enableClipboardPermissionsIfSupported(iframe);
const loadingEl = modal.querySelector('.tm-loading');
let loadTimeout = null;
const handleResize = () => {
updateScrollbarState(container, LAYOUT_CONFIG);
updateIframeViewportInfo(iframe, container.offsetWidth, container.offsetHeight, LAYOUT_CONFIG);
};
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.target === container) {
handleResize();
}
}
});
resizeObserver.observe(container);
modal._onHide = () => {
IntervalManager.clear('cardUploadMonitor');
window._MWI_UPLOAD_IN_PROGRESS = false;
GM_setValue('mwi_card_upload_request', 0);
GM_setValue('mwi_card_image_url', '');
GM_setValue('mwi_card_image_timestamp', 0);
GM_setValue('mwi_upload_progress', 0);
GM_setValue('mwi_upload_status', '');
cardLinkFillStatus.reset();
closeCard();
};
modal._onShow = () => {
initCardUploadMonitor();
updateScrollbarState(container, LAYOUT_CONFIG);
updateIframeViewportInfo(iframe, container.offsetWidth, container.offsetHeight, LAYOUT_CONFIG);
};
modal._cleanup = () => {
resizeObserver.disconnect();
document.removeEventListener('keydown', handleEscape);
if (loadTimeout) clearTimeout(loadTimeout);
modal._onHide();
};
const handleEscape = (e) => {
if (e.key === 'Escape') {
toggleTalentMarketModal();
}
};
document.addEventListener('keydown', handleEscape);
iframe.addEventListener('load', () => {
if (loadTimeout) clearTimeout(loadTimeout);
loadingEl.style.display = 'none';
iframe._loaded = true;
setupIframeMessaging(iframe, LAYOUT_CONFIG);
updateIframeViewportInfo(iframe, container.offsetWidth, container.offsetHeight, LAYOUT_CONFIG);
}, { once: true });
loadTimeout = setTimeout(() => {
if (loadingEl.style.display !== 'none') {
loadingEl.innerHTML = `${t.loadingSlow}
`;
}
}, LAYOUT_CONFIG.LOAD_TIMEOUT);
iframe.addEventListener('error', () => {
if (loadTimeout) clearTimeout(loadTimeout);
loadingEl.innerHTML = `${t.loadFailed}
`;
}, { once: true });
return modal;
}
function bindTalentMarketEvents(modal, t, LAYOUT_CONFIG) {
const loadingEl = modal.querySelector('.tm-loading');
const refreshIframe = () => {
const iframe = modal.querySelector('#tm-iframe');
if (iframe) {
if (loadingEl) {
loadingEl.style.display = 'flex';
loadingEl.innerHTML = `
${t.loading}
`;
}
const hideLoadingOnLoad = () => {
if (loadingEl) {
loadingEl.style.display = 'none';
}
};
iframe.addEventListener('load', hideLoadingOnLoad, { once: true });
const currentSrc = iframe.src;
iframe.src = 'about:blank';
setTimeout(() => {
iframe.src = currentSrc;
}, 50);
}
};
modal.querySelector('.tm-btn-close').addEventListener('click', () => {
if (modal._onHide) modal._onHide();
modal.style.display = 'none';
});
modal.querySelector('.tm-btn-refresh').addEventListener('click', () => {
refreshIframe();
});
modal.querySelector('.tm-btn-settings').addEventListener('click', () => {
if (typeof BuildScoreAutoUpdater !== 'undefined') {
BuildScoreAutoUpdater.showSettingsDialog();
}
});
const uploadTeamCheckbox = modal.querySelector('.tm-upload-team-checkbox');
if (uploadTeamCheckbox) {
uploadTeamCheckbox.addEventListener('change', (e) => {
if (e.target.checked) {
GM_setValue('mwi_upload_team_disabled', true);
BattleTeamsModule.stopAutoUpload();
} else {
GM_setValue('mwi_upload_team_disabled', false);
BattleTeamsModule.startAutoUpload();
}
});
}
}
function updateIframeViewportInfo(iframe, width, height, LAYOUT_CONFIG) {
if (!iframe || !iframe.contentWindow || !iframe._loaded) return;
if (!iframe.src || !iframe.src.startsWith(SITE_URL)) return;
const isMobile = width < LAYOUT_CONFIG.MOBILE_THRESHOLD;
const isNarrow = width < LAYOUT_CONFIG.NARROW_THRESHOLD;
try {
iframe.contentWindow.postMessage({
type: 'VIEWPORT_UPDATE',
data: { width, height, isMobile, isNarrow }
}, SITE_URL);
} catch (e) {}
}
function setupIframeMessaging(iframe, LAYOUT_CONFIG) {
const container = iframe.closest('.tm-container');
if (container) {
setTimeout(() => {
updateIframeViewportInfo(iframe, container.offsetWidth, container.offsetHeight, LAYOUT_CONFIG);
}, LAYOUT_CONFIG.MESSAGING_DELAY);
}
if (window._talentMarketMessageListenerAdded) return;
window._talentMarketMessageListenerAdded = true;
window.addEventListener('message', (event) => {
if (event.origin !== SITE_URL) return;
const { type, data } = event.data;
switch(type) {
case 'CLOSE_MODAL':
const modal = document.getElementById('talent-market-modal');
if (modal) {
if (modal._cleanup) modal._cleanup();
modal.remove();
}
break;
}
});
}
function updateScrollbarState(container, LAYOUT_CONFIG) {
const currentWidth = container.offsetWidth;
const currentHeight = container.offsetHeight;
if (currentWidth < LAYOUT_CONFIG.NARROW_THRESHOLD) {
container.setAttribute('data-narrow', 'true');
} else {
container.removeAttribute('data-narrow');
}
if (currentHeight < LAYOUT_CONFIG.MIN_HEIGHT) {
container.setAttribute('data-short', 'true');
} else {
container.removeAttribute('data-short');
}
if (currentWidth < LAYOUT_CONFIG.MOBILE_THRESHOLD) {
container.setAttribute('data-mobile', 'true');
} else {
container.removeAttribute('data-mobile');
}
}
function generateRandomGradientBorder() {
let colorPool, angles;
try {
const style = getComputedStyle(document.documentElement);
const poolStr = style.getPropertyValue('--card-gradient-colors').trim().replace(/^'|'$/g, '');
const anglesStr = style.getPropertyValue('--card-gradient-angles').trim().replace(/^'|'$/g, '');
if (poolStr) colorPool = JSON.parse(poolStr);
if (anglesStr) angles = JSON.parse(anglesStr);
} catch (e) {}
if (!colorPool || !colorPool.length) {
colorPool = ['#8B5CF6','#EC4899','#F59E0B','#A855F7','#3B82F6','#9333EA','#DB2777','#C084FC','#26C6DA','#F472B6','#7C3AED','#A78BFA','#06B6D4','#9D4EDD','#C77DFF','#E0AAFF','#BF40BF','#DA70D6','#EE82EE','#E879F9','#BA55D3','#9370DB','#34D399','#10B981','#14B8A6','#22C55E','#6366F1','#0EA5E9','#FF006E','#FF499E','#F43F5E','#FB7185','#E91E63','#FF1744','#FF4081','#FF69B4','#FF1493','#F59E0B','#EF4444','#FBBF24','#F97316','#DC2626','#FFD700','#FFA500','#FF8C00','#00CED1','#48D1CC','#20B2AA','#4169E1','#6495ED','#7B68EE','#536DFE','#FF6347','#FF4500','#1E90FF','#00BFFF','#87CEEB','#4FC3F7','#29B6F6','#64B5F6','#00E676','#1DE9B6','#18FFFF','#64FFDA','#69F0AE','#EEFF41','#FFD740','#FFAB40','#FF6E40','#9890E3','#B721FF','#FBC2EB','#A18CD1'];
}
if (!angles || !angles.length) {
angles = [45, 90, 135, 180, 225, 270, 315, 360];
}
const count = 3 + Math.floor(Math.random() * 2);
const shuffled = colorPool.slice().sort(() => Math.random() - 0.5);
const picked = shuffled.slice(0, count);
const angle = angles[Math.floor(Math.random() * angles.length)];
return `linear-gradient(${angle}deg, ${picked.join(', ')})`;
}
function getCompletedAchievementTiers() {
const achievements = DataStore.characterAchievements || [];
const achievementDetailMap = DataStore.achievementDetailMap || {};
const tierCounts = {
'/achievement_tiers/beginner': { completed: 0, total: 0 },
'/achievement_tiers/novice': { completed: 0, total: 0 },
'/achievement_tiers/adept': { completed: 0, total: 0 },
'/achievement_tiers/veteran': { completed: 0, total: 0 },
'/achievement_tiers/elite': { completed: 0, total: 0 },
'/achievement_tiers/champion': { completed: 0, total: 0 }
};
for (const hrid in achievementDetailMap) {
const detail = achievementDetailMap[hrid];
if (detail && detail.tierHrid && tierCounts[detail.tierHrid]) {
tierCounts[detail.tierHrid].total++;
}
}
achievements.forEach(ach => {
if (ach.isCompleted) {
const detail = achievementDetailMap[ach.achievementHrid];
if (detail && detail.tierHrid && tierCounts[detail.tierHrid]) {
tierCounts[detail.tierHrid].completed++;
}
}
});
const completedTiers = [];
const tierOrder = ['beginner', 'novice', 'adept', 'veteran', 'elite', 'champion'];
tierOrder.forEach(tier => {
const tierHrid = `/achievement_tiers/${tier}`;
const data = tierCounts[tierHrid];
if (data && data.completed > 0 && data.completed === data.total) {
completedTiers.push(tier);
}
});
return completedTiers;
}
function generateAchievementIconsHTML() {
const completedTiers = getCompletedAchievementTiers();
const iconConfig = {
beginner: { sprite: 'buffs', id: 'gathering' },
novice: { sprite: 'buffs', id: 'wisdom' },
adept: { sprite: 'buffs', id: 'efficiency' },
veteran: { sprite: 'items', id: 'butter_of_proficiency' },
elite: { sprite: 'misc', id: 'combat' },
champion: { sprite: 'skills', id: 'enhancing' }
};
const spriteURLs = {
buffs: '/static/media/buffs_sprite.cd54d85e.svg',
items: SVGTool.getSpriteURL('items'),
misc: SVGTool.getSpriteURL('misc'),
skills: SVGTool.getSpriteURL('skills')
};
let html = '';
html += `
`;
completedTiers.forEach(tier => {
const config = iconConfig[tier];
if (config) {
const spriteURL = spriteURLs[config.sprite];
html += `
`;
}
});
html += '
';
return html;
}
async function generateCharacterCard() {
const existingContainer = document.querySelector('.mwi-card-container');
if (existingContainer) return;
if (!DataStore.characterSkills || DataStore.characterSkills.length === 0) {
alert('Data not loaded!' + String.fromCharCode(10) + String.fromCharCode(10) + 'Please refresh the page and wait for data to auto-load (about 3 seconds)');
return;
}
const container = document.createElement('div');
container.className = 'mwi-card-container';
let bgImageDataURL = '';
if (CARD_BACKGROUND_IMAGE && CARD_BACKGROUND_IMAGE.trim() !== '') {
try {
bgImageDataURL = BackgroundImagePreloader.getCached();
if (!bgImageDataURL) {
bgImageDataURL = await BackgroundImagePreloader.preload();
}
if (!bgImageDataURL) {
throw new Error('Failed to load background image');
}
} catch (error) {
bgImageDataURL = '';
}
} else {
const lang = typeof window.detectLanguage === 'function' ? window.detectLanguage() : 'zh';
const message = lang === 'zh'
? '错误:未配置名片背景图!' + String.fromCharCode(10) + String.fromCharCode(10) + '请在脚本中设置CARD_BACKGROUND_IMAGE变量'
: 'Error: Card background image not configured!' + String.fromCharCode(10) + String.fromCharCode(10) + 'Please set CARD_BACKGROUND_IMAGE variable in the script';
alert(message);
return;
}
const bgImageStyle = bgImageDataURL ? `background-image: url(${bgImageDataURL});` : '';
const borderGradient = generateRandomGradientBorder();
container.innerHTML = `
`;
document.body.appendChild(container);
setTimeout(() => renderCard(), 100);
}
function renderCard() {
const cardContent = document.getElementById('mwi-card-content');
if (!cardContent) return;
const container = cardContent.parentElement;
if (!container) return;
const borderGradient = cardContent.getAttribute('data-border-gradient') || generateRandomGradientBorder();
SVGTool.refreshFromDOM();
ClientData.init();
const isZH = localStorage.getItem("i18nextLng")?.toLowerCase()?.startsWith("zh");
const nameElementHTML = DataStore.nameElementHTML;
const chatIconSpriteURL = SVGTool.getSpriteURL('chat_icons');
const avatarsSpriteURL = SVGTool.getSpriteURL('avatars');
const avatarOutfitsSpriteURL = SVGTool.getSpriteURL('avatar_outfits');
const chatIconId = DataStore.chatIconHrid ? DataStore.chatIconHrid.split('/').pop() : null;
const supporterBadgeId = DataStore.supporterBadgeHrid ? DataStore.supporterBadgeHrid.split('/').pop() : null;
const avatarId = DataStore.avatarHrid ? DataStore.avatarHrid.split('/').pop() : null;
const outfitId = DataStore.outfitHrid ? DataStore.outfitHrid.split('/').pop() : null;
const now = new Date();
const timestamp = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
const achievementIconsHTML = generateAchievementIconsHTML();
let html = `
${achievementIconsHTML}
${timestamp}
`;
html += `
`;
html += `
`;
if (supporterBadgeId || chatIconId || nameElementHTML || DataStore.characterName) {
html += `
`;
if (supporterBadgeId) {
html += `
`;
}
if (chatIconId) {
html += `
`;
}
if (nameElementHTML) {
const _iconCount = (supporterBadgeId ? 1 : 0) + (chatIconId ? 1 : 0);
const _wrapperStyle = _iconCount >= 2 ? 'transform: translate(-18px, 0px);' : '';
html += `
${nameElementHTML}
`;
} else if (DataStore.characterName) {
html += `
${DataStore.characterName}
`;
}
html += `
`;
}
if (avatarId || outfitId) {
html += `
`;
if (avatarId) {
html += `
`;
}
if (outfitId) {
html += `
`;
}
html += `
`;
}
html += `
`;
html += `
`;
html += `
`;
html += `
`;
const equipmentSlots = {
'/item_locations/back': { row: 1, col: 1, name: t('back') },
'/item_locations/head': { row: 1, col: 2, name: t('head') },
'/item_locations/trinket': { row: 1, col: 3, name: t('trinket') },
'/item_locations/neck': { row: 1, col: 4, name: t('neck') },
'/item_locations/main_hand': { row: 2, col: 1, name: t('main_hand') },
'/item_locations/body': { row: 2, col: 2, name: t('body') },
'/item_locations/off_hand': { row: 2, col: 3, name: t('off_hand') },
'/item_locations/earrings': { row: 2, col: 4, name: t('earrings') },
'/item_locations/hands': { row: 3, col: 1, name: t('hands') },
'/item_locations/legs': { row: 3, col: 2, name: t('legs') },
'/item_locations/pouch': { row: 3, col: 3, name: t('pouch') },
'/item_locations/ring': { row: 3, col: 4, name: t('ring') },
'/item_locations/feet': { row: 4, col: 2, name: t('feet') },
'/item_locations/charm': { row: 4, col: 4, name: t('charm') },
'/item_locations/two_hand': { row: 2, col: 1, name: t('two_hand'), colspan: 2 }
};
const hasTwoHand = !!DataStore.currentEquipmentMap['/item_locations/two_hand'];
for (let row = 1; row <= 4; row++) {
for (let col = 1; col <= 4; col++) {
if ((row === 4 && col === 1) || (row === 4 && col === 3)) {
html += `
`;
continue;
}
let slotEntry = Object.entries(equipmentSlots).find(([_, slot]) =>
slot.row === row && slot.col === col
);
if (row === 2 && col === 1 && hasTwoHand) {
slotEntry = ['/item_locations/two_hand', equipmentSlots['/item_locations/two_hand']];
}
if (row === 2 && col === 3 && hasTwoHand) {
slotEntry = null;
}
if (slotEntry) {
const [slotHrid, slotInfo] = slotEntry;
const item = DataStore.currentEquipmentMap[slotHrid];
if (item) {
const itemDetail = DataStore.itemDetailMap?.[item.itemHrid];
const itemName = itemDetail?.name || item.itemHrid.split('/').pop();
const enhance = item.enhancementLevel || 0;
const itemLevel = itemDetail?.itemLevel || 0;
const iconId = item.itemHrid.split('/').pop();
const spriteURL = SVGTool.getSpriteURL('items');
html += `
${enhance > 0 ? `
+${enhance}
` : ''}
${itemLevel > 0 ? `
${itemLevel}
` : ''}
`;
} else {
html += `
`;
}
} else {
html += `
`;
}
}
}
html += `
`;
const stamina = DataStore.characterSkills.find(s => s.skillHrid === '/skills/stamina')?.level || 0;
const intelligence = DataStore.characterSkills.find(s => s.skillHrid === '/skills/intelligence')?.level || 0;
const attack = DataStore.characterSkills.find(s => s.skillHrid === '/skills/attack')?.level || 0;
const defense = DataStore.characterSkills.find(s => s.skillHrid === '/skills/defense')?.level || 0;
const melee = DataStore.characterSkills.find(s => s.skillHrid === '/skills/melee')?.level || 0;
const ranged = DataStore.characterSkills.find(s => s.skillHrid === '/skills/ranged')?.level || 0;
const magic = DataStore.characterSkills.find(s => s.skillHrid === '/skills/magic')?.level || 0;
const maxCombatSkill = Math.max(melee, ranged, magic);
const maxAllCombat = Math.max(attack, defense, melee, ranged, magic);
const combatLevel = Math.floor(0.1 * (stamina + intelligence + attack + defense + maxCombatSkill) + 0.5 * maxAllCombat);
const firstRowStats = [
{ hrid: '/skills/intelligence', name: t('intelligence'), level: intelligence },
{ hrid: '/skills/stamina', name: t('stamina'), level: stamina },
{ hrid: '/skills/attack', name: t('attack'), level: attack }
].sort((a, b) => b.level - a.level);
const secondRowStats = [
{ hrid: '/skills/magic', name: t('magic'), level: magic },
{ hrid: '/skills/melee', name: t('melee'), level: melee },
{ hrid: '/skills/ranged', name: t('ranged'), level: ranged },
{ hrid: '/skills/defense', name: t('defense'), level: defense }
].sort((a, b) => b.level - a.level);
html += `
`;
const miscSpriteURL = SVGTool.getSpriteURL('misc');
const skillsSpriteURL = SVGTool.getSpriteURL('skills');
const combatLevelClass = combatLevel >= 140 ? 'highlight' : '';
html += `
${t('combat')}
Lv.${combatLevel}
`;
firstRowStats.forEach(stat => {
const name = stat.hrid.split('/').pop();
const levelClass = stat.level >= 140 ? 'highlight' : '';
html += `
${stat.name}
Lv.${stat.level}
`;
});
secondRowStats.forEach(stat => {
const name = stat.hrid.split('/').pop();
const levelClass = stat.level >= 140 ? 'highlight' : '';
html += `
${stat.name}
Lv.${stat.level}
`;
});
html += `
`;
html += `
`;
html += `
`;
const productionToolSlots = [
"/item_locations/woodcutting_tool",
"/item_locations/foraging_tool",
"/item_locations/milking_tool",
"/item_locations/cheesesmithing_tool",
"/item_locations/crafting_tool",
"/item_locations/tailoring_tool",
"/item_locations/cooking_tool",
"/item_locations/brewing_tool",
"/item_locations/alchemy_tool",
"/item_locations/enhancing_tool"
];
html += `
`;
for (const slot of productionToolSlots) {
const item = DataStore.currentEquipmentMap[slot];
if (item) {
const itemDetail = DataStore.itemDetailMap?.[item.itemHrid];
const itemName = itemDetail?.name || item.itemHrid.split('/').pop();
const enhance = item.enhancementLevel || 0;
const iconId = item.itemHrid.split('/').pop();
const spriteURL = SVGTool.getSpriteURL('items');
html += `
${enhance > 0 ? `
+${enhance}
` : ''}
`;
} else {
html += `
`;
}
}
html += `
`;
const houseRooms = DataStore.characterHouseRoomMap || {};
const firstRowRooms = [
{ hrid: '/house_rooms/dojo', icon: 'attack', name: t('dojo') },
{ hrid: '/house_rooms/library', icon: 'intelligence', name: t('library') },
{ hrid: '/house_rooms/dining_room', icon: 'stamina', name: t('dining_room') }
];
const secondRowRooms = [
{ hrid: '/house_rooms/mystical_study', icon: 'magic', name: t('mystical_study') },
{ hrid: '/house_rooms/armory', icon: 'defense', name: t('armory') },
{ hrid: '/house_rooms/gym', icon: 'melee', name: t('gym') },
{ hrid: '/house_rooms/archery_range', icon: 'ranged', name: t('archery_range') }
];
const existingFirstRow = firstRowRooms
.filter(room => houseRooms[room.hrid])
.sort((a, b) => {
const levelA = houseRooms[a.hrid]?.level || 0;
const levelB = houseRooms[b.hrid]?.level || 0;
return levelB - levelA;
});
const existingSecondRow = secondRowRooms
.filter(room => houseRooms[room.hrid])
.sort((a, b) => {
const levelA = houseRooms[a.hrid]?.level || 0;
const levelB = houseRooms[b.hrid]?.level || 0;
return levelB - levelA;
});
html += `
`;
for (let i = 0; i < 3; i++) {
const room = existingFirstRow[i];
if (room) {
const roomData = houseRooms[room.hrid];
const roomLevel = roomData.level || 0;
const spriteURL = SVGTool.getSpriteURL('skills');
const levelClass = roomLevel === 8 ? 'max' : 'normal';
html += `
`;
} else {
html += `
`;
}
}
html += `
`;
for (let i = 0; i < 4; i++) {
const room = existingSecondRow[i];
if (room) {
const roomData = houseRooms[room.hrid];
const roomLevel = roomData.level || 0;
const spriteURL = SVGTool.getSpriteURL('skills');
const levelClass = roomLevel === 8 ? 'max' : 'normal';
html += `
`;
} else {
html += `
`;
}
}
html += `
`;
const combatAbilities = DataStore.combatUnit?.combatAbilities || [];
const auraAbilities = [
'/abilities/insanity',
'/abilities/mystic_aura',
'/abilities/critical_aura',
'/abilities/speed_aura',
'/abilities/fierce_aura',
'/abilities/guardian_aura',
'/abilities/revive',
'/abilities/invincible'
];
const equippedNormalAbilities = [];
const equippedAuras = [];
combatAbilities.forEach(ability => {
if (auraAbilities.includes(ability.abilityHrid)) {
equippedAuras.push(ability);
} else {
equippedNormalAbilities.push(ability);
}
});
const equippedAbilityHrids = new Set(combatAbilities.map(a => a.abilityHrid));
const unequippedAuras = [];
auraAbilities.forEach(auraHrid => {
const learnedAura = DataStore.characterAbilities.find(a => a.abilityHrid === auraHrid);
if (learnedAura && !equippedAbilityHrids.has(auraHrid)) {
unequippedAuras.push(learnedAura);
}
});
unequippedAuras.sort((a, b) => b.level - a.level);
const firstRowAbilities = equippedNormalAbilities.slice(0, 4);
const auraRowDisplay = [];
if (equippedAuras.length > 0) {
auraRowDisplay.push(equippedAuras[0]);
}
const remainingSlots = 4 - auraRowDisplay.length;
const topUnequippedAuras = unequippedAuras.slice(0, remainingSlots);
auraRowDisplay.push(...topUnequippedAuras);
html += `
`;
for (let i = 0; i < 4; i++) {
const ability = firstRowAbilities[i];
if (ability && ability.abilityHrid) {
const abilityDetail = DataStore.abilityDetailMap?.[ability.abilityHrid];
const abilityName = abilityDetail?.name || ability.abilityHrid.split('/').pop();
const combatActionHrid = abilityDetail?.combatActionHrid || ability.abilityHrid;
const iconId = combatActionHrid.split('/').pop();
const spriteURL = SVGTool.getSpriteURL('abilities');
html += `
`;
} else {
html += `
`;
}
}
for (let i = 0; i < 4; i++) {
const aura = auraRowDisplay[i];
if (aura && aura.abilityHrid) {
const abilityDetail = DataStore.abilityDetailMap?.[aura.abilityHrid];
const abilityName = abilityDetail?.name || aura.abilityHrid.split('/').pop();
const combatActionHrid = abilityDetail?.combatActionHrid || aura.abilityHrid;
const iconId = combatActionHrid.split('/').pop();
const spriteURL = SVGTool.getSpriteURL('abilities');
html += `
`;
} else {
html += `
`;
}
}
html += `
`;
html += `
`;
cardContent.innerHTML = html;
}
function closeCard() {
const container = document.querySelector('.mwi-card-container');
if (container) {
container.remove();
window._MWI_UPLOAD_IN_PROGRESS = false;
GM_setValue('mwi_card_upload_request', 0);
GM_setValue('mwi_card_image_url', '');
GM_setValue('mwi_card_image_timestamp', 0);
GM_setValue('mwi_upload_progress', 0);
GM_setValue('mwi_upload_status', '');
cardLinkFillStatus.reset();
}
}
async function autoUploadCard() {
if (window._MWI_UPLOAD_IN_PROGRESS) {
return;
}
const cardElement = document.getElementById('mwi-card-content');
if (!cardElement) {
const lang = typeof window.detectLanguage === 'function' ? window.detectLanguage() : 'zh';
throw new Error(lang === 'zh' ? '未找到名片元素' : 'Card element not found');
}
window._MWI_UPLOAD_IN_PROGRESS = true;
const lang = typeof window.detectLanguage === 'function' ? window.detectLanguage() : 'zh';
const existingOverlay = document.getElementById('mwi-upload-progress-overlay');
if (existingOverlay) {
existingOverlay.remove();
}
const abortController = new AbortController();
let isCancelled = false;
const progressOverlay = document.createElement('div');
progressOverlay.id = 'mwi-upload-progress-overlay';
progressOverlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.85);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 999999999;
backdrop-filter: blur(4px);
`;
progressOverlay.innerHTML = `
${lang === 'zh' ? '中止上传' : 'Cancel'}
${lang === 'zh' ? '名片上传中...' : 'Uploading card...'}
${lang === 'zh' ? '准备中...' : 'Preparing...'}
`;
document.body.appendChild(progressOverlay);
const closeBtn = document.getElementById('mwi-upload-close-btn');
closeBtn.addEventListener('mouseover', () => { closeBtn.style.background = 'rgba(255, 255, 255, 0.25)'; });
closeBtn.addEventListener('mouseout', () => { closeBtn.style.background = 'rgba(255, 255, 255, 0.15)'; });
closeBtn.addEventListener('click', () => {
isCancelled = true;
abortController.abort();
progressOverlay.remove();
closeCard();
window._MWI_UPLOAD_IN_PROGRESS = false;
GM_setValue('mwi_card_upload_request', 0);
GM_setValue('mwi_card_image_url', '');
GM_setValue('mwi_card_image_timestamp', 0);
GM_setValue('mwi_upload_progress', 0);
GM_setValue('mwi_upload_status', '');
cardLinkFillStatus.reset();
});
const progressBar = document.getElementById('mwi-progress-bar');
const progressText = document.getElementById('mwi-progress-text');
function updateProgress(percent, text) {
if (isCancelled) return;
if (progressBar) progressBar.style.width = percent + '%';
if (progressText) progressText.textContent = text;
}
try {
if (isCancelled) return;
updateProgress(10, lang === 'zh' ? '生成截图中...' : 'Generating screenshot...');
const cardContainer = cardElement.closest('.mwi-card-container');
const savedContainerCss = cardContainer ? cardContainer.style.cssText : '';
const savedCardCss = cardElement.style.cssText;
if (cardContainer) cardContainer.style.cssText += ';max-width:none!important;width:940px!important;overflow:visible!important;';
cardElement.style.cssText += ';width:940px!important;height:520px!important;';
await new Promise(r => setTimeout(r, 50));
const canvas = await SnapDOM.toCanvas(cardElement, {
width: 940,
height: 520,
backgroundColor: '#1a1a2e',
scale: 2,
logging: false
});
if (cardContainer) cardContainer.style.cssText = savedContainerCss;
cardElement.style.cssText = savedCardCss;
if (isCancelled) throw new Error('cancelled');
updateProgress(30, lang === 'zh' ? '转换图片中...' : 'Converting image...');
const blob = await new Promise((resolve, reject) => {
canvas.toBlob(blob => {
if (!blob) reject(new Error(lang === 'zh' ? 'Canvas转换Blob失败' : 'Canvas to Blob conversion failed'));
else resolve(blob);
}, 'image/png', 1.0);
});
if (isCancelled) throw new Error('cancelled');
updateProgress(50, lang === 'zh' ? '截图完成,开始上传...' : 'Screenshot complete, uploading...');
const UPLOAD_ENDPOINT = 'https://tupian.li/api/v1/upload';
updateProgress(80, lang === 'zh' ? '正在上传图片...需要2分钟' : 'Uploading image... may take 2 minutes');
const formData = new FormData();
formData.append('file', blob, 'character-card.png');
let uploadResp;
let lastError;
const maxRetries = 3;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
if (isCancelled) throw new Error('cancelled');
if (attempt > 1) {
const retryText = lang === 'zh'
? `重试上传 (${attempt}/${maxRetries})...`
: `Retrying upload (${attempt}/${maxRetries})...`;
updateProgress(80 + (attempt - 1) * 3, retryText);
await new Promise(resolve => setTimeout(resolve, 2000 * attempt));
}
uploadResp = await fetch(UPLOAD_ENDPOINT, {
method: 'POST',
body: formData,
signal: abortController.signal
});
if (uploadResp.ok) {
break;
}
lastError = new Error(lang === 'zh' ? `状态码: ${uploadResp.status}` : `Status code: ${uploadResp.status}`);
if (attempt === maxRetries) {
throw lastError;
}
} catch (error) {
if (isCancelled || error.name === 'AbortError' || error.message === 'cancelled') {
throw new Error('cancelled');
}
lastError = error;
if (attempt === maxRetries) {
const errorMsg = lang === 'zh'
? `上传失败 (已重试${maxRetries}次): ${error.message}`
: `Upload failed (retried ${maxRetries} times): ${error.message}`;
throw new Error(errorMsg);
}
}
}
if (!uploadResp || !uploadResp.ok) {
throw lastError || new Error(lang === 'zh' ? '上传失败' : 'Upload failed');
}
if (isCancelled) throw new Error('cancelled');
updateProgress(90, lang === 'zh' ? '处理响应...' : 'Processing response...');
const data = await uploadResp.json();
let imageUrl = null;
if (data && typeof data === 'object' && data.status === true && data.data && data.data.pathname) {
imageUrl = 'https://tupian.li/images/' + data.data.pathname;
} else if (data && typeof data === 'object' && data.success && data.data && data.data.url) {
imageUrl = data.data.url;
} else if (data && typeof data === 'object' && data.url) {
imageUrl = data.url;
} else if (data && typeof data === 'object' && data.link) {
imageUrl = data.link;
}
if (!imageUrl) {
throw new Error(lang === 'zh' ? '无法解析API返回的图片链接' : 'Unable to parse image link from API response');
}
if (isCancelled) throw new Error('cancelled');
updateProgress(100, lang === 'zh' ? '名片上传成功!' : 'Card upload successful!');
const timestamp = Date.now();
GM_setValue('mwi_card_image_url', imageUrl);
GM_setValue('mwi_card_image_timestamp', timestamp);
if (typeof window.__fillCardLink__ === 'function') {
window.__fillCardLink__(imageUrl);
}
await new Promise(resolve => setTimeout(resolve, 100));
const savedUrl = GM_getValue('mwi_card_image_url', '');
const savedTimestamp = GM_getValue('mwi_card_image_timestamp', 0);
if (savedUrl !== imageUrl || savedTimestamp !== timestamp) {
GM_setValue('mwi_card_image_url', imageUrl);
GM_setValue('mwi_card_image_timestamp', timestamp);
await new Promise(resolve => setTimeout(resolve, 100));
}
setTimeout(() => {
progressOverlay.remove();
closeCard();
window._MWI_UPLOAD_IN_PROGRESS = false;
}, 800);
return imageUrl;
} catch (error) {
if (isCancelled || error.message === 'cancelled') {
return;
}
progressOverlay.innerHTML = `
${lang === 'zh' ? '上传失败' : 'Upload failed'}
${error.message}
`;
setTimeout(() => {
progressOverlay.remove();
}, 3000);
throw error;
}
}
if (!isSimulatorPage) {
WebSocketHook.install();
}
const EQUIP_SLOTS = ['head', 'body', 'legs', 'feet', 'hands', 'weapon', 'off_hand', 'pouch', 'neck', 'earrings', 'ring', 'back', 'charm'];
const LEVEL_TYPES = ['stamina', 'intelligence', 'attack', 'melee', 'defense', 'ranged', 'magic'];
const ABILITY_SLOT_COUNT = 5;
const PLAYER_COUNT = 5;
const SimulatorBuildScore = {
playerData: {},
clientDataLoaded: false,
calculating: {},
extraItems: {},
extraItemsCaptured: {},
init() {
if (!isSimulatorPage) return;
this.loadClientData();
this.injectStyles();
this.waitForDOM();
},
loadClientData() {
if (this.clientDataLoaded) return true;
try {
const compressed = GM_getValue('mwi_client_data_for_sim', '');
if (!compressed) return false;
const json = LZString.decompressFromUTF16(compressed);
if (!json) return false;
BuildScoreModule.initClientData(JSON.parse(json));
this.clientDataLoaded = true;
return true;
} catch (e) {
return false;
}
},
injectStyles() {
const style = document.createElement('style');
style.textContent = '.tm-bs-badge { display:inline-block; font-size:14px; font-weight:700; color:#fbbf24; background:rgba(251,191,36,0.15); border:1px solid rgba(251,191,36,0.3); border-radius:4px; padding:1px 6px; margin-left:6px; line-height:22px; vertical-align:middle; white-space:nowrap; transition:all .3s ease; }' +
'.tm-bs-badge.calculating { color:#94a3b8; background:rgba(148,163,184,0.15); border-color:rgba(148,163,184,0.3); }' +
'.tm-bs-summary { display:flex; gap:8px; padding:6px 12px; background:rgba(30,41,59,0.8); border:1px solid rgba(148,163,184,0.2); border-radius:6px; margin:8px 0; font-size:12px; color:#e2e8f0; flex-wrap:wrap; align-items:center; }' +
'.tm-bs-summary-item { display:flex; align-items:center; gap:4px; }' +
'.tm-bs-summary-label { color:#94a3b8; }' +
'.tm-bs-summary-value { color:#fbbf24; font-weight:700; font-size:15px; }' +
'.tm-bs-summary-total { color:#f59e0b; font-weight:700; font-size:20px; }' +
'.tm-bs-recalc { cursor:pointer; color:#94a3b8; font-size:11px; margin-left:auto; padding:2px 8px; border:1px solid rgba(148,163,184,0.3); border-radius:3px; background:transparent; transition:all .2s; }' +
'.tm-bs-recalc:hover { color:#fbbf24; border-color:rgba(251,191,36,0.4); }' +
'a[id^="player"][id$="-tab"]:focus, a[id^="player"][id$="-tab"]:focus-visible { outline:none !important; box-shadow:none !important; }';
document.head.appendChild(style);
},
_getActivePlayerIdx() {
const tab = document.querySelector('a.nav-link.active[id^="player"][id$="-tab"]');
return tab ? parseInt(tab.id.replace('player', '').replace('-tab', '')) : 0;
},
_interceptInputValue(input, onValueSet) {
if (!input || input._tmBsHooked) return;
input._tmBsHooked = true;
const desc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
if (desc && desc.set) {
const origSet = desc.set;
Object.defineProperty(input, 'value', {
get() { return desc.get.call(this); },
set(val) { origSet.call(this, val); if (val) onValueSet(val); },
configurable: true
});
}
input.addEventListener('input', () => { if (input.value) onValueSet(input.value); });
},
_listenChange(el, handler) {
if (!el) return;
el.addEventListener('change', handler);
if (el.tagName === 'INPUT') el.addEventListener('input', handler);
},
waitForDOM() {
var self = this;
var check = function() {
var firstTab = document.querySelector('a#player1-tab');
if (firstTab) {
for (var t = 1; t <= PLAYER_COUNT; t++) {
var pt = document.querySelector('a#player' + t + '-tab');
if (pt) pt.spellcheck = false;
}
self.hookImportButton();
self.observeTabChanges();
self.observeFormChanges();
self.observeModalCheckboxes();
self.tryReadExistingData();
return;
}
setTimeout(check, 300);
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', check);
} else {
check();
}
},
hookImportButton() {
var self = this;
var check = function() {
var importBtn = document.querySelector('button#buttonImportSet');
if (!importBtn) {
setTimeout(check, 300);
return;
}
if (importBtn._tmBsHooked) return;
importBtn._tmBsHooked = true;
importBtn.addEventListener('click', function() {
setTimeout(function() {
self.readImportedData();
for (var pp = 1; pp <= PLAYER_COUNT; pp++) {
self.readPerPlayerImportedData(pp);
}
}, 200);
});
var inputElem = document.querySelector('input#inputSetGroupCombatAll');
if (inputElem) {
var desc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
if (desc && desc.set) {
var origSet = desc.set;
Object.defineProperty(inputElem, 'value', {
get: function() { return desc.get.call(this); },
set: function(val) {
origSet.call(this, val);
if (val) {
self._pendingImportData = val;
setTimeout(function() { self.readImportedData(); }, 300);
}
},
configurable: true
});
}
inputElem.addEventListener('input', function() {
if (inputElem.value) {
setTimeout(function() { self.readImportedData(); }, 300);
}
});
}
for (var pi = 1; pi <= PLAYER_COUNT; pi++) {
(function(idx) {
var perPlayerInput = document.querySelector('input#inputSetGroupCombatplayer' + idx);
if (perPlayerInput && !perPlayerInput._tmBsHooked) {
perPlayerInput._tmBsHooked = true;
var ppDesc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
if (ppDesc && ppDesc.set) {
var origPPSet = ppDesc.set;
Object.defineProperty(perPlayerInput, 'value', {
get: function() { return ppDesc.get.call(this); },
set: function(val) {
origPPSet.call(this, val);
if (val) setTimeout(function() { self.readPerPlayerImportedData(idx); }, 300);
},
configurable: true
});
}
perPlayerInput.addEventListener('input', function() {
if (perPlayerInput.value) setTimeout(function() { self.readPerPlayerImportedData(idx); }, 300);
});
}
})(pi);
}
var soloImportBtn = document.querySelector('button#buttonImportSolo');
if (soloImportBtn && !soloImportBtn._tmBsHooked) {
soloImportBtn._tmBsHooked = true;
soloImportBtn.addEventListener('click', function() {
setTimeout(function() { self.readSoloImportedData(); }, 200);
});
}
var soloInput = document.querySelector('input#inputSetSolo');
if (soloInput && !soloInput._tmBsHooked) {
soloInput._tmBsHooked = true;
var soloDesc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
if (soloDesc && soloDesc.set) {
var origSoloSet = soloDesc.set;
Object.defineProperty(soloInput, 'value', {
get: function() { return soloDesc.get.call(this); },
set: function(val) {
origSoloSet.call(this, val);
if (val) {
setTimeout(function() { self.readSoloImportedData(); }, 300);
}
},
configurable: true
});
}
soloInput.addEventListener('input', function() {
if (soloInput.value) {
setTimeout(function() { self.readSoloImportedData(); }, 300);
}
});
}
};
check();
},
readImportedData() {
try {
var rawData = this._pendingImportData || null;
this._pendingImportData = null;
if (!rawData) {
var inputElem = document.querySelector('input#inputSetGroupCombatAll');
if (inputElem) rawData = inputElem.value;
}
if (!rawData) return;
var exportObj = JSON.parse(rawData);
for (var i = 1; i <= PLAYER_COUNT; i++) {
if (exportObj[i]) {
var pds = typeof exportObj[i] === 'string' ? exportObj[i] : JSON.stringify(exportObj[i]);
this.playerData[i] = JSON.parse(pds);
if (this.playerData[i].characterName) {
this.playerData[i]._characterName = this.playerData[i].characterName;
}
}
}
this.extraItems = {};
this.extraItemsCaptured = {};
this.readPlayerNamesFromTabs();
this.recalculateAll();
} catch (e) { /* silent */ }
},
readSoloImportedData() {
try {
var inputElem = document.querySelector('input#inputSetSolo');
if (!inputElem || !inputElem.value) return;
var playerData = JSON.parse(inputElem.value);
var playerIdx = this._getActivePlayerIdx() || 1;
this.playerData[playerIdx] = playerData;
if (playerData.characterName) {
this.playerData[playerIdx]._characterName = playerData.characterName;
}
delete this.extraItems[playerIdx];
delete this.extraItemsCaptured[playerIdx];
this.readPlayerNamesFromTabs();
this.recalculatePlayer(playerIdx);
} catch (e) { /* silent */ }
},
readPerPlayerImportedData(playerIdx) {
try {
var inputElem = document.querySelector('input#inputSetGroupCombatplayer' + playerIdx);
if (!inputElem || !inputElem.value) return;
var playerData = JSON.parse(inputElem.value);
this.playerData[playerIdx] = playerData;
if (playerData.characterName) {
this.playerData[playerIdx]._characterName = playerData.characterName;
}
delete this.extraItems[playerIdx];
delete this.extraItemsCaptured[playerIdx];
this.readPlayerNamesFromTabs();
this.recalculatePlayer(playerIdx);
} catch (e) { /* silent */ }
},
readPlayerNamesFromTabs() {
for (var i = 1; i <= PLAYER_COUNT; i++) {
var tab = document.querySelector('a#player' + i + '-tab');
if (!tab || !this.playerData[i]) continue;
var badge = tab.querySelector('.tm-bs-badge');
var name = tab.textContent.trim();
if (badge) name = name.replace(badge.textContent, '').trim();
this.playerData[i]._characterName = this.playerData[i].characterName
|| (name && name !== 'Player ' + i ? name : null)
|| 'Player ' + i;
}
},
tryReadExistingData() {
var self = this;
setTimeout(function() {
var inputElem = document.querySelector('input#inputSetGroupCombatAll');
if (inputElem && inputElem.value) {
self.readImportedData();
return;
}
var hasNamedTabs = false;
for (var i = 1; i <= PLAYER_COUNT; i++) {
var tab = document.querySelector('a#player' + i + '-tab');
if (tab) {
var name = tab.textContent.trim();
if (name && name !== 'Player ' + i) {
hasNamedTabs = true;
break;
}
}
}
if (hasNamedTabs) {
setTimeout(function() {
var inp = document.querySelector('input#inputSetGroupCombatAll');
if (inp && inp.value) {
self.readImportedData();
}
}, 2000);
}
}, 500);
},
observeTabChanges() {
var tabList = document.querySelector('#playerTab');
if (!tabList) return;
var self = this;
var observer = new MutationObserver(function(mutations) {
for (var k = 0; k < mutations.length; k++) {
var m = mutations[k];
if (m.type === 'characterData' || m.type === 'childList') {
var target = m.target.closest ? m.target.closest('.nav-link') :
(m.target.parentElement && m.target.parentElement.closest ? m.target.parentElement.closest('.nav-link') : null);
if (target && target.id && /^player\d+-tab$/.test(target.id)) {
var idx = parseInt(target.id.replace('player', '').replace('-tab', ''));
if (self.playerData[idx]) {
var badge = target.querySelector('.tm-bs-badge');
var nm = target.textContent.trim();
if (badge) nm = nm.replace(badge.textContent, '').trim();
if (nm && nm !== 'Player ' + idx) {
self.playerData[idx]._characterName = nm;
}
}
}
}
}
});
observer.observe(tabList, { childList: true, subtree: true, characterData: true });
},
observeFormChanges() {
var self = this;
var debounceTimer = null;
var handleChange = function() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(function() {
var idx = self._getActivePlayerIdx();
if (idx) self.readPlayerStateFromDOM(idx);
}, 800);
};
for (var i = 0; i < EQUIP_SLOTS.length; i++) {
this._listenChange(document.getElementById('selectEquipment_' + EQUIP_SLOTS[i]), handleChange);
this._listenChange(document.getElementById('inputEquipmentEnhancementLevel_' + EQUIP_SLOTS[i]), handleChange);
}
for (var j = 0; j < ABILITY_SLOT_COUNT; j++) {
this._listenChange(document.getElementById('selectAbility_' + j), handleChange);
this._listenChange(document.getElementById('inputAbilityLevel_' + j), handleChange);
}
for (var k = 0; k < LEVEL_TYPES.length; k++) {
this._listenChange(document.getElementById('inputLevel_' + LEVEL_TYPES[k]), handleChange);
}
var houseInputs = document.querySelectorAll('input[data-house-hrid]');
for (var h = 0; h < houseInputs.length; h++) {
this._listenChange(houseInputs[h], handleChange);
}
},
captureExtraItems(playerIdx) {
var data = this.playerData[playerIdx];
this.extraItems[playerIdx] = [];
if (!data || !data.player || !data.player.equipment) return;
var visibleHrids = {};
for (var i = 0; i < EQUIP_SLOTS.length; i++) {
var sel = document.getElementById('selectEquipment_' + EQUIP_SLOTS[i]);
if (sel && sel.value) visibleHrids[sel.value] = true;
}
for (var j = 0; j < data.player.equipment.length; j++) {
var eq = data.player.equipment[j];
if (eq && eq.itemHrid && !visibleHrids[eq.itemHrid]) {
this.extraItems[playerIdx].push({
itemLocationHrid: eq.itemLocationHrid || '/item_locations/unknown',
itemHrid: eq.itemHrid,
enhancementLevel: eq.enhancementLevel || 0,
count: 1
});
}
}
},
buildCharacterDataFromDOM() {
var items = [];
for (var i = 0; i < EQUIP_SLOTS.length; i++) {
var sel = document.getElementById('selectEquipment_' + EQUIP_SLOTS[i]);
if (!sel || !sel.value) continue;
var enh = document.getElementById('inputEquipmentEnhancementLevel_' + EQUIP_SLOTS[i]);
items.push({
itemLocationHrid: '/item_locations/equipment',
itemHrid: sel.value,
enhancementLevel: enh ? (parseInt(enh.value) || 0) : 0,
count: 1
});
}
var abilities = [];
for (var j = 0; j < ABILITY_SLOT_COUNT; j++) {
var abSel = document.getElementById('selectAbility_' + j);
if (!abSel || !abSel.value) continue;
var abLvl = document.getElementById('inputAbilityLevel_' + j);
abilities.push({ abilityHrid: abSel.value, level: parseInt(abLvl ? abLvl.value : '1') || 1 });
}
var rooms = {};
var houseInputs = document.querySelectorAll('input[data-house-hrid]');
for (var h = 0; h < houseInputs.length; h++) {
var hrid = houseInputs[h].dataset.houseHrid;
var level = parseInt(houseInputs[h].value) || 0;
if (hrid && level > 0) rooms[hrid] = { houseRoomHrid: hrid, level: level };
}
return { characterHouseRoomMap: rooms, combatUnit: { combatAbilities: abilities }, characterItems: items };
},
readPlayerStateFromDOM(playerIdx) {
if (!this.extraItemsCaptured[playerIdx] && this.playerData[playerIdx]) {
this.captureExtraItems(playerIdx);
this.extraItemsCaptured[playerIdx] = true;
}
var characterData = this.buildCharacterDataFromDOM();
if (this.extraItems[playerIdx]) {
for (var x = 0; x < this.extraItems[playerIdx].length; x++) {
characterData.characterItems.push(this.extraItems[playerIdx][x]);
}
}
this._calculate(playerIdx, characterData);
},
async _calculate(playerIdx, characterData) {
if (!this.loadClientData()) return;
if (this.calculating[playerIdx]) return;
this.calculating[playerIdx] = true;
this.updateBadge(playerIdx, null, true);
try {
var result = await BuildScoreModule.calculateBuildScore(characterData);
if (result) {
this.updateBadge(playerIdx, result, false);
this.updateDetailPanel(playerIdx, result);
}
} catch (e) {
this.updateBadge(playerIdx, null, false);
} finally {
this.calculating[playerIdx] = false;
}
},
async recalculateAll() {
if (!this.loadClientData()) return;
for (var i = 1; i <= PLAYER_COUNT; i++) {
if (this.playerData[i]) await this.recalculatePlayer(i);
}
},
recalculatePlayer(playerIdx) {
var data = this.playerData[playerIdx];
if (!data) return;
return this._calculate(playerIdx, this.convertToCharacterData(data));
},
convertToCharacterData(pd) {
var rooms = {};
if (pd.houseRooms) {
var hrids = Object.keys(pd.houseRooms);
for (var h = 0; h < hrids.length; h++) {
rooms[hrids[h]] = { houseRoomHrid: hrids[h], level: pd.houseRooms[hrids[h]] };
}
}
var abilities = [];
if (pd.abilities) {
for (var a = 0; a < pd.abilities.length; a++) {
var ab = pd.abilities[a];
if (ab && ab.abilityHrid) abilities.push({ abilityHrid: ab.abilityHrid, level: parseInt(ab.level) || 1 });
}
}
var items = [];
if (pd.player && pd.player.equipment) {
for (var e = 0; e < pd.player.equipment.length; e++) {
var eq = pd.player.equipment[e];
if (eq && eq.itemHrid) {
items.push({ itemLocationHrid: eq.itemLocationHrid || '/item_locations/unknown', itemHrid: eq.itemHrid, enhancementLevel: eq.enhancementLevel || 0, count: 1 });
}
}
}
return { characterHouseRoomMap: rooms, combatUnit: { combatAbilities: abilities }, characterItems: items };
},
updateBadge(playerIdx, scoreResult, isCalculating) {
var tab = document.querySelector('a#player' + playerIdx + '-tab');
if (!tab) return;
var charName = this.playerData[playerIdx] && this.playerData[playerIdx]._characterName;
if (charName) {
var found = false;
for (var c = 0; c < tab.childNodes.length; c++) {
if (tab.childNodes[c].nodeType === 3) {
tab.childNodes[c].textContent = charName;
found = true;
break;
}
}
if (!found) tab.insertBefore(document.createTextNode(charName), tab.firstChild);
}
var badge = tab.querySelector('.tm-bs-badge');
if (!badge) {
badge = document.createElement('span');
badge.className = 'tm-bs-badge';
tab.appendChild(badge);
}
if (isCalculating) {
badge.className = 'tm-bs-badge calculating';
badge.textContent = '...';
} else if (scoreResult) {
badge.className = 'tm-bs-badge';
badge.textContent = scoreResult.total.toFixed(1);
} else {
badge.className = 'tm-bs-badge calculating';
badge.textContent = 'N/A';
}
},
updateDetailPanel(playerIdx, scoreResult) {
var tabContent = document.querySelector('#player' + playerIdx);
if (!tabContent) return;
var panel = tabContent.querySelector('.tm-bs-summary');
if (!panel) {
panel = document.createElement('div');
panel.className = 'tm-bs-summary';
tabContent.insertBefore(panel, tabContent.firstChild);
}
var name = (this.playerData[playerIdx] && this.playerData[playerIdx]._characterName)
? this.playerData[playerIdx]._characterName : 'Player ' + playerIdx;
var self = this;
var _isZH = (localStorage.getItem('i18nextLng') || '').toLowerCase().startsWith('zh');
var _lbl = _isZH
? { total: '\u6218\u529b\u6253\u9020\u5206:', house: '\u623f\u5b50\u5206:', ability: '\u6280\u80fd\u5206:', equip: '\u88c5\u5907\u5206:' }
: { total: 'Build Score:', house: 'House:', ability: 'Ability:', equip: 'Equipment:' };
panel.innerHTML =
'' + name + ' ' +
'' + _lbl.total + ' ' + scoreResult.total.toFixed(1) + ' ' +
'' + _lbl.house + ' ' + scoreResult.house.toFixed(1) + ' ' +
'' + _lbl.ability + ' ' + scoreResult.ability.toFixed(1) + ' ' +
'' + _lbl.equip + ' ' + scoreResult.equipment.toFixed(1) + ' ' +
'Recalc ';
panel.querySelector('.tm-bs-recalc').addEventListener('click', function() {
self.readImportedData();
});
},
styleModalPlayerCheckboxes() {
var playerInfo = [];
for (var i = 1; i <= PLAYER_COUNT; i++) {
var tab = document.querySelector('a#player' + i + '-tab');
if (!tab) continue;
var badge = tab.querySelector('.tm-bs-badge');
if (!badge) continue;
var scoreText = badge.textContent.trim();
if (!scoreText) continue;
var fullText = tab.textContent.trim();
var name = fullText.replace(scoreText, '').trim();
if (!name) continue;
playerInfo.push({ name: name, score: scoreText, concat: name + scoreText });
}
if (playerInfo.length === 0) return;
var labels = document.querySelectorAll('.modal-body .form-check-label');
for (var k = 0; k < labels.length; k++) {
var label = labels[k];
if (label.querySelector('.tm-bs-badge')) continue;
var txt = label.textContent.trim();
for (var j = 0; j < playerInfo.length; j++) {
if (txt === playerInfo[j].concat) {
var input = label.querySelector('input');
if (input) {
label.textContent = '';
label.appendChild(input);
label.appendChild(document.createTextNode(playerInfo[j].name));
} else {
label.textContent = playerInfo[j].name;
}
var b = document.createElement('span');
b.className = 'tm-bs-badge';
b.textContent = playerInfo[j].score;
label.appendChild(b);
break;
}
}
}
},
observeModalCheckboxes() {
var self = this;
var debounceTimer = null;
var observer = new MutationObserver(function() {
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(function() {
self.styleModalPlayerCheckboxes();
}, 200);
});
var target = document.body;
if (target) {
observer.observe(target, { childList: true, subtree: true });
}
}
};
SimulatorBuildScore.init();
const TM_BROWSER_API = 'https://papiyas.chat/api/user-data-all';
const TMB_BLANK_PLAYER = '{"player":{"attackLevel":1,"magicLevel":1,"meleeLevel":1,"rangedLevel":1,"defenseLevel":1,"staminaLevel":1,"intelligenceLevel":1,"equipment":[]},"food":{"/action_types/combat":[{"itemHrid":""},{"itemHrid":""},{"itemHrid":""}]},"drinks":{"/action_types/combat":[{"itemHrid":""},{"itemHrid":""},{"itemHrid":""}]},"abilities":[{"abilityHrid":"","level":"1"},{"abilityHrid":"","level":"1"},{"abilityHrid":"","level":"1"},{"abilityHrid":"","level":"1"},{"abilityHrid":"","level":"1"}],"triggerMap":{},"zone":"/actions/combat/fly","simulationTime":"100","houseRooms":{"/house_rooms/dairy_barn":0,"/house_rooms/garden":0,"/house_rooms/log_shed":0,"/house_rooms/forge":0,"/house_rooms/workshop":0,"/house_rooms/sewing_parlor":0,"/house_rooms/kitchen":0,"/house_rooms/brewery":0,"/house_rooms/laboratory":0,"/house_rooms/observatory":0,"/house_rooms/dining_room":0,"/house_rooms/library":0,"/house_rooms/dojo":0,"/house_rooms/gym":0,"/house_rooms/armory":0,"/house_rooms/archery_range":0,"/house_rooms/mystical_study":0}}';
const TMB_I18N = {
zh: {
title: 'Talent Market', btn: '\u4eba\u624d\u5e02\u573a',
job: '\u804c\u4e1a', minPower: '\u6700\u4f4e\u6218\u529b', minLevel: '\u6700\u4f4e\u7b49\u7ea7', idSearch: '\u641c\u7d22ID',
clear: '\u6e05\u9664', all: '\u5168\u90e8',
colId: 'ID', colJob: '\u804c\u4e1a', colPower: '\u6218\u529b\u5206', colLevel: '\u6218\u7b49', colMainAttr: '\u4e3b\u5c5e\u6027', colCard: '\u540d\u7247', colAction: '\u64cd\u4f5c',
view: '\u67e5\u770b\u540d\u7247', add: '\u5bfc\u5165\u81f3', loading: '\u52a0\u8f7d\u4e2d...', noData: '\u672a\u627e\u5230\u7528\u6237',
loadFail: '\u52a0\u8f7d\u5931\u8d25', parseFail: '\u89e3\u6790\u5931\u8d25', usersFound: '\u4e2a\u7528\u6237',
tabStd: '\u6807\u51c6', tabIC: 'IC',
loginId: '\u89d2\u8272ID', loginPwd: '\u7b80\u5386\u5bc6\u7801',
loginBtn: '\u767b\u5f55', loginCancel: '\u53d6\u6d88', loginFail: '\u9a8c\u8bc1\u5931\u8d25',
loginSuccess: '\u9a8c\u8bc1\u6210\u529f', loginNoPwd: '\u8be5ID\u672a\u8bbe\u7f6e\u5bc6\u7801',
loginLocked: '\u5c1d\u8bd5\u6b21\u6570\u8fc7\u591a\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5', loginVerifying: '\u9a8c\u8bc1\u4e2d...',
authVerified: '\u5df2\u9a8c\u8bc1', authUnverified: '\u672a\u767b\u5f55',
loginDesc: '\u8bf7\u8f93\u5165\u4eba\u624d\u5e02\u573a\u7684\u7528\u6237\u540d\u548c\u5bc6\u7801\u8fdb\u884c\u767b\u5f55\uff0c\u767b\u5f55\u540e\u624d\u53ef\u8c03\u7528\u6570\u636e\n\u5982\u679c\u6ca1\u6709\u7528\u6237\u540d\u548c\u5bc6\u7801\uff0c\u8bf7\u5148\u524d\u5f80\u4eba\u624d\u5e02\u573a\u63d0\u4ea4\u7b80\u5386\u4ee5\u83b7\u53d6\u7528\u6237\u540d\u548c\u5bc6\u7801'
},
en: {
title: 'Talent Market', btn: 'Talent Market',
job: 'Job', minPower: 'Min Power', minLevel: 'Min Level', idSearch: 'Search ID',
clear: 'Clear', all: 'All',
colId: 'ID', colJob: 'Job', colPower: 'Power', colLevel: 'Level', colMainAttr: 'Main Attr', colCard: 'Card', colAction: 'Action',
view: 'View Card', add: 'Import', loading: 'Loading...', noData: 'No users found',
loadFail: 'Failed to load', parseFail: 'Failed to parse', usersFound: 'users found',
tabStd: 'Standard', tabIC: 'IC',
loginId: 'Character ID', loginPwd: 'Resume Password',
loginBtn: 'Login', loginCancel: 'Cancel', loginFail: 'Verification failed',
loginSuccess: 'Verified', loginNoPwd: 'No password set for this ID',
loginLocked: 'Too many attempts, try later', loginVerifying: 'Verifying...',
authVerified: 'Verified', authUnverified: 'Not logged in',
loginDesc: 'Please enter your Talent Market username and password to log in. You need to log in before importing data.\nIf you don\'t have an account, please submit a resume on the Talent Market website first.'
}
};
const TalentMarketBrowser = {
data: { standard: [], ic: [] },
activeTab: 'standard',
loading: false,
panelVisible: false,
filters: { job: '', minPower: '', minLevel: '', idSearch: '' },
sortField: 'updateTime',
sortDir: 'desc',
_t(key) {
var lang = (localStorage.getItem('i18nextLng') || '').toLowerCase().startsWith('zh') ? 'zh' : 'en';
return (TMB_I18N[lang] && TMB_I18N[lang][key]) || TMB_I18N.en[key] || key;
},
_getMainAttr(u) {
if (u._cachedMainAttr !== undefined) return u._cachedMainAttr;
try {
var sd = u.simdata;
if (!sd) { u._cachedMainAttr = ''; return ''; }
var parsed = typeof sd === 'string' ? JSON.parse(sd) : sd;
var p = parsed.player || parsed;
var isZH = (localStorage.getItem('i18nextLng') || '').toLowerCase().startsWith('zh');
var attrs = [
{ key: 'meleeLevel', zh: '\u8fd1\u6218', en: 'Melee' },
{ key: 'rangedLevel', zh: '\u8fdc\u7a0b', en: 'Ranged' },
{ key: 'magicLevel', zh: '\u9b54\u6cd5', en: 'Magic' },
{ key: 'attackLevel', zh: '\u653b\u51fb', en: 'Attack' }
];
var best = null;
for (var i = 0; i < attrs.length; i++) {
var val = parseInt(p[attrs[i].key]) || 0;
if (!best || val > best.val) best = { val: val, attr: attrs[i] };
}
var result = (!best || best.val <= 0) ? '' : best.val + ' ' + (isZH ? best.attr.zh : best.attr.en);
u._cachedMainAttr = result;
return result;
} catch (e) { u._cachedMainAttr = ''; return ''; }
},
init() {
if (!isSimulatorPage) return;
this._injectStyles();
this._waitForDOM();
},
_injectStyles() {
var s = document.createElement('style');
s.textContent =
'#tmBrowserBtn { display:inline-flex; align-items:center; gap:4px; padding:6px 16px; margin-left:8px; background:#0F95B0; color:#fff; border:none; border-radius:8px; font-size:15px; font-weight:600; cursor:pointer; vertical-align:middle; transition:all .2s; }' +
'#tmBrowserBtn:hover { background:#0D7A8E; transform:translateY(-1px); }' +
'#tmBrowserBtn.active { box-shadow:0 0 0 2px #0F95B0; }' +
'#tmBrowserOverlay { display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.7); z-index:10000; justify-content:center; align-items:center; }' +
'#tmBrowserOverlay.visible { display:flex; }' +
'#tmBrowserPanel { background:linear-gradient(135deg,hsl(190,87%,12%) 0%,hsl(190,70%,14%) 25%,hsl(190,59%,16%) 50%,hsl(190,50%,18%) 75%,hsl(190,45%,20%) 100%); border:1px solid rgba(255,255,255,0.15); border-radius:16px; width:900px; max-width:95vw; height:80vh; max-height:90vh; display:flex; flex-direction:column; overflow:hidden; box-shadow:0 8px 32px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.1); }' +
'.tmb-header { display:flex; justify-content:center; align-items:center; padding:20px 16px; background:rgba(255,255,255,0.08); border-bottom:1px solid rgba(255,255,255,0.1); flex-shrink:0; position:relative; }' +
'.tmb-header .tm-title { text-align:center; }' +
'.tmb-header .tm-title h2 { display:block; color:#e8eaed; font-size:1.8em; font-weight:600; margin:0; border:none; padding:0; line-height:1.2; }' +
'.tmb-close-btn { position:absolute; right:12px; top:50%; transform:translateY(-50%); background:none; border:none; color:#e8eaed; font-size:28px; font-weight:bold; cursor:pointer; padding:0; line-height:1; opacity:1; }' +
'.tmb-close-btn:hover { color:#64b5f6; }' +
'.tmb-auth-btn { position:absolute; left:12px; top:50%; transform:translateY(-50%); padding:4px 12px; font-size:13px; border-radius:6px; cursor:pointer; border:1px solid rgba(255,255,255,0.2); background:rgba(255,255,255,0.08); color:rgba(255,255,255,0.7); transition:all .15s; }' +
'.tmb-auth-btn:hover { background:rgba(255,255,255,0.15); }' +
'.tmb-auth-btn.verified { background:rgba(76,175,80,0.15); border-color:rgba(76,175,80,0.4); color:#66bb6a; }' +
'.tmb-tabs { display:flex; border-bottom:1px solid rgba(255,255,255,0.1); flex-shrink:0; }' +
'.tmb-tab { flex:1; padding:8px 0; text-align:center; font-size:15px; font-weight:600; color:rgba(255,255,255,0.6); background:transparent; border:none; cursor:pointer; transition:all .2s; border-bottom:2px solid transparent; }' +
'.tmb-tab.active { color:#e8eaed; border-bottom-color:rgba(85,155,135,0.6); background:rgba(255,255,255,0.05); }' +
'.tmb-tab:hover { color:rgba(255,255,255,0.85); }' +
'.tmb-filters { display:flex; gap:10px; padding:12px 16px; flex-wrap:wrap; align-items:center; background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.1); border-radius:12px; margin:6px; flex-shrink:0; }' +
'.tmb-filter-item { display:flex; flex-direction:column; gap:2px; justify-content:center; }' +
'.tmb-filter-item label { font-size:15px; color:rgba(255,255,255,0.8); font-weight:500; }' +
'.tmb-filter-item select, .tmb-filter-item input { padding:6px 10px; font-size:15px; background:rgba(255,255,255,0.1); color:#fff; border:1px solid rgba(255,255,255,0.2); border-radius:6px; outline:none; min-width:120px; height:32px; }' +
'.tmb-filter-item select:focus, .tmb-filter-item input:focus { outline:none; border-color:rgba(255,255,255,0.4); }' +
'.tmb-filter-item select option { background:rgba(255,255,255,0.95); color:#333; }' +
'.tmb-filter-item input::placeholder { color:rgba(255,255,255,0.5); font-size:13px; }' +
'.tmb-clear-btn { padding:6px 28px; font-size:15px; background:rgba(255,255,255,0.1); color:#fff; border:1px solid rgba(255,255,255,0.3); border-radius:6px; cursor:pointer; margin-left:auto; transition:all .15s; align-self:stretch; display:flex; align-items:center; }' +
'.tmb-clear-btn:hover { background:rgba(255,255,255,0.2); border-color:rgba(255,255,255,0.4); }' +
'.tmb-table-wrap { flex:1; overflow-y:auto; overflow-x:auto; min-height:0; background:rgba(255,255,255,0.03); border:1px solid rgba(255,255,255,0.1); border-radius:12px; margin:0 6px 6px; scrollbar-width:thin; scrollbar-color:rgb(85,155,135) transparent; }' +
'.tmb-table-wrap::-webkit-scrollbar { width:1px; height:1px; }' +
'.tmb-table-wrap::-webkit-scrollbar-track { background:transparent; }' +
'.tmb-table-wrap::-webkit-scrollbar-thumb { background:rgb(20,54,60); border-radius:1px; }' +
'.tmb-table { width:100%; border-collapse:collapse; font-size:16px; }' +
'.tmb-table thead { position:sticky; top:0; z-index:2; background:rgba(20,54,60,0.95); backdrop-filter:blur(10px); }' +
'.tmb-table th { padding:10px 10px; color:#e8eaed; font-weight:600; text-align:center; border-bottom:2px solid rgba(85,155,135,0.6); border-right:1px solid rgba(255,255,255,0.1); white-space:nowrap; font-size:16px; }' +
'.tmb-table th:last-child { border-right:none; }' +
'.tmb-table th.sortable { cursor:pointer; user-select:none; }' +
'.tmb-table th.sortable:hover { background:rgba(255,255,255,0.08); }' +
'.tmb-sort-indicator { display:inline-block; margin-left:2px; font-size:12px; color:rgba(255,255,255,0.3); width:12px; height:12px; vertical-align:middle; }' +
'.tmb-sort-indicator::after { content:"\\25BC"; }' +
'.tmb-sort-indicator.sort-desc { color:#4fc3f7; }' +
'.tmb-table td { padding:8px 10px; color:#e8eaed; border-bottom:1px solid rgba(255,255,255,0.1); border-right:1px solid rgba(255,255,255,0.1); text-align:center; font-size:16px; }' +
'.tmb-table td:last-child { border-right:none; }' +
'.tmb-table tbody tr { height:44px; border-bottom:1px solid rgba(255,255,255,0.1); transition:background-color 0.2s ease; background:rgba(255,255,255,0.02); }' +
'.tmb-table tbody tr:nth-child(even) { background:rgba(255,255,255,0.06); }' +
'.tmb-table tbody tr:hover { background:rgba(85,155,135,0.2); }' +
'.tmb-card-link { color:#64b5f6; cursor:pointer; text-decoration:none; font-size:15px; }' +
'.tmb-card-link:hover { text-decoration:underline; }' +
'.tmb-add-btn { padding:8px 18px; font-size:15px; background:#0F95B0; color:#fff; border:none; border-radius:6px; cursor:pointer; white-space:nowrap; position:relative; transition:all .2s; }' +
'.tmb-add-btn:hover { background:#0D7A8E; }' +
'.tmb-add-menu { position:absolute; right:0; top:100%; background:rgba(20,54,60,0.98); border:1px solid rgba(255,255,255,0.2); border-radius:8px; padding:4px; z-index:100; box-shadow:0 4px 16px rgba(0,0,0,0.4); display:flex; flex-direction:column; gap:3px; backdrop-filter:blur(10px); }' +
'.tmb-add-menu button { padding:8px 24px; font-size:15px; background:rgba(255,255,255,0.1); color:#e8eaed; border:1px solid rgba(255,255,255,0.2); border-radius:6px; cursor:pointer; white-space:nowrap; transition:all .15s; }' +
'.tmb-add-menu button:hover { background:rgba(255,255,255,0.2); border-color:rgba(255,255,255,0.3); }' +
'.tmb-loading { text-align:center; padding:20px; color:rgba(255,255,255,0.7); font-size:13px; }' +
'.tmb-empty { text-align:center; padding:20px; color:rgba(255,255,255,0.5); font-size:13px; }' +
'.tmb-count { font-size:14px; color:rgba(255,255,255,0.6); padding:6px 12px; flex-shrink:0; }' +
'#tmbCardPreview { display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.8); z-index:10001; justify-content:center; align-items:center; cursor:pointer; }' +
'#tmbCardPreview.visible { display:flex; }' +
'#tmbCardPreview img { max-width:90vw; max-height:90vh; border-radius:0; box-shadow:0 8px 32px rgba(0,0,0,0.5); object-fit:contain; }' +
'#tmbCardPreview .tmb-preview-close { position:absolute; top:20px; right:20px; background:rgba(255,255,255,0.15); border:1px solid rgba(255,255,255,0.3); color:#fff; font-size:24px; width:40px; height:40px; border-radius:50%; cursor:pointer; display:flex; align-items:center; justify-content:center; }' +
'#tmbCardPreview .tmb-preview-close:hover { background:rgba(255,255,255,0.3); }' +
'#tmbLoginOverlay { display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.7); z-index:10002; justify-content:center; align-items:center; }' +
'#tmbLoginOverlay.visible { display:flex; }' +
'.tmb-login-panel { background:linear-gradient(135deg,hsl(190,87%,12%),hsl(190,50%,18%)); border:1px solid rgba(255,255,255,0.2); border-radius:12px; padding:24px; min-width:320px; max-width:90vw; box-shadow:0 8px 32px rgba(0,0,0,0.4); }' +
'.tmb-login-desc { color:#ffffff; font-size:18px; line-height:1.6; margin-bottom:14px; white-space:pre-line; text-align:center; }' +
'.tmb-login-field { display:flex; flex-direction:column; gap:4px; margin-bottom:12px; align-items:center; }' +
'.tmb-login-field label { color:rgba(255,255,255,0.8); font-size:14px; }' +
'.tmb-login-field input { padding:8px 12px; font-size:15px; background:rgba(255,255,255,0.1); color:#fff; border:1px solid rgba(255,255,255,0.2); border-radius:6px; outline:none; width:50%; box-sizing:border-box; }' +
'.tmb-login-field input:focus { border-color:rgba(255,255,255,0.4); }' +
'.tmb-login-msg { text-align:center; font-size:13px; min-height:20px; margin-bottom:8px; }' +
'.tmb-login-msg.error { color:#ef5350; }' +
'.tmb-login-msg.success { color:#66bb6a; }' +
'.tmb-login-btns { display:flex; gap:10px; justify-content:center; }' +
'.tmb-login-btns button { padding:8px 24px; font-size:15px; border-radius:6px; cursor:pointer; border:1px solid rgba(255,255,255,0.2); transition:all .15s; }' +
'.tmb-login-submit { background:#0F95B0; color:#fff; border-color:#0F95B0 !important; }' +
'.tmb-login-submit:hover { background:#0D7A8E; }' +
'.tmb-login-submit:disabled { opacity:0.6; cursor:not-allowed; }' +
'.tmb-login-cancel { background:rgba(255,255,255,0.1); color:#e8eaed; }' +
'.tmb-login-cancel:hover { background:rgba(255,255,255,0.2); }';
document.head.appendChild(s);
},
_waitForDOM() {
var self = this;
var check = function() {
var tabList = document.querySelector('#playerTab');
if (tabList) {
self._createUI(tabList);
return;
}
setTimeout(check, 500);
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', check);
} else {
check();
}
},
_createUI(tabList) {
var self = this;
var li = document.createElement('li');
li.className = 'nav-item';
li.setAttribute('role', 'presentation');
li.style.display = 'flex';
li.style.alignItems = 'center';
var btn = document.createElement('button');
btn.id = 'tmBrowserBtn';
btn.innerHTML = self._t('btn');
btn.addEventListener('click', function() { self._toggle(); });
li.appendChild(btn);
tabList.appendChild(li);
var overlay = document.createElement('div');
overlay.id = 'tmBrowserOverlay';
var panel = document.createElement('div');
panel.id = 'tmBrowserPanel';
var header = document.createElement('div');
header.className = 'tmb-header';
var isAuthed = GM_getValue('tmb_auth_id', '') && GM_getValue('tmb_auth_pwd', '');
var authCls = isAuthed ? 'tmb-auth-btn verified' : 'tmb-auth-btn';
var authTxt = isAuthed ? self._t('authVerified') : self._t('authUnverified');
header.innerHTML = '' + authTxt + '
' + self._t('title') + ' \u00d7 ';
header.querySelector('.tmb-close-btn').addEventListener('click', function() { self._toggle(); });
header.querySelector('#tmbAuthBtn').addEventListener('click', function() {
self._showLoginDialog(function() { self._updateAuthBtn(); });
});
panel.appendChild(header);
var tabs = document.createElement('div');
tabs.className = 'tmb-tabs';
var tabStd = document.createElement('button');
tabStd.className = 'tmb-tab active';
tabStd.textContent = self._t('tabStd');
tabStd.dataset.tab = 'standard';
var tabIC = document.createElement('button');
tabIC.className = 'tmb-tab';
tabIC.textContent = self._t('tabIC');
tabIC.dataset.tab = 'ic';
tabs.appendChild(tabStd);
tabs.appendChild(tabIC);
tabs.addEventListener('click', function(e) {
if (e.target.dataset.tab) {
self.activeTab = e.target.dataset.tab;
tabs.querySelectorAll('.tmb-tab').forEach(function(t) { t.classList.remove('active'); });
e.target.classList.add('active');
self._applyFilters();
}
});
panel.appendChild(tabs);
var filters = document.createElement('div');
filters.className = 'tmb-filters';
filters.innerHTML =
'' + self._t('minPower') + '
' +
'' + self._t('minLevel') + '
' +
'' + self._t('idSearch') + '
' +
'' + self._t('job') + ' ' +
'' + self._t('all') + ' ' +
'\u6c34\u6cd5 ' +
'\u706b\u6cd5 ' +
'\u81ea\u7136\u6cd5 ' +
'\u5f13 ' +
'\u5f29 ' +
'\u76fe ' +
'\u67aa ' +
'\u9524 ' +
'\u5251 ' +
'
' +
'' + self._t('clear') + ' ';
panel.appendChild(filters);
var countRow = document.createElement('div');
countRow.className = 'tmb-count';
countRow.id = 'tmbCount';
panel.appendChild(countRow);
var tableWrap = document.createElement('div');
tableWrap.className = 'tmb-table-wrap';
tableWrap.innerHTML =
'' +
'' + self._t('colId') + ' ' +
'' + self._t('colJob') + ' ' +
'' + self._t('colPower') + ' ' +
'' + self._t('colLevel') + ' ' +
'' + self._t('colMainAttr') + ' ' +
'' + self._t('colCard') + ' ' +
'' + self._t('colAction') + ' ' +
'
';
tableWrap.querySelector('thead').addEventListener('click', function(e) {
var th = e.target.closest('th.sortable');
if (!th) return;
var field = th.dataset.sort;
if (self.sortField === field) {
self.sortField = 'updateTime';
self.sortDir = 'desc';
} else {
self.sortField = field;
self.sortDir = 'desc';
}
tableWrap.querySelectorAll('.tmb-sort-indicator').forEach(function(s) { s.className = 'tmb-sort-indicator'; });
if (self.sortField === field) {
var indicator = th.querySelector('.tmb-sort-indicator');
if (indicator) indicator.className = 'tmb-sort-indicator sort-desc';
}
self._applyFilters();
});
panel.appendChild(tableWrap);
overlay.appendChild(panel);
document.body.appendChild(overlay);
overlay.addEventListener('click', function(e) {
if (e.target === overlay) self._toggle();
});
var debounce = null;
var onFilter = function() {
clearTimeout(debounce);
debounce = setTimeout(function() {
self.filters.job = document.getElementById('tmbJobFilter').value;
self.filters.minPower = document.getElementById('tmbPowerFilter').value;
self.filters.minLevel = document.getElementById('tmbLevelFilter').value;
self.filters.idSearch = document.getElementById('tmbIdFilter').value;
self._applyFilters();
}, 250);
};
['tmbJobFilter', 'tmbPowerFilter', 'tmbLevelFilter', 'tmbIdFilter'].forEach(function(id) {
var el = document.getElementById(id);
if (el) {
el.addEventListener('input', onFilter);
el.addEventListener('change', onFilter);
}
});
document.getElementById('tmbClearBtn').addEventListener('click', function() {
self.filters = { job: '', minPower: '', minLevel: '', idSearch: '' };
document.getElementById('tmbJobFilter').value = '';
document.getElementById('tmbPowerFilter').value = '';
document.getElementById('tmbLevelFilter').value = '';
document.getElementById('tmbIdFilter').value = '';
self._applyFilters();
});
document.addEventListener('click', function(e) {
if (!e.target.closest('.tmb-add-btn') && !e.target.closest('.tmb-add-menu')) {
var menus = document.querySelectorAll('.tmb-add-menu');
menus.forEach(function(m) { m.remove(); });
}
});
},
_toggle() {
var overlay = document.getElementById('tmBrowserOverlay');
var btn = document.getElementById('tmBrowserBtn');
if (!overlay) return;
this.panelVisible = !this.panelVisible;
overlay.classList.toggle('visible', this.panelVisible);
btn.classList.toggle('active', this.panelVisible);
if (this.panelVisible && this.data.standard.length === 0 && this.data.ic.length === 0) {
this._fetchData();
}
},
_fetchData() {
if (this.loading) return;
this.loading = true;
var self = this;
var body = document.getElementById('tmbBody');
var count = document.getElementById('tmbCount');
if (body) body.innerHTML = '' + self._t('loading') + ' ';
if (count) count.textContent = '';
GM_xmlhttpRequest({
method: 'GET',
url: TM_BROWSER_API,
timeout: 15000,
onload: function(response) {
try {
var json = JSON.parse(response.responseText);
var rawStd = (json.standard && json.standard.data) ? json.standard.data : [];
var rawIc = (json.ic && json.ic.data) ? json.ic.data : [];
var hasSimdata = function(u) { return u.simdata && u.simdata.trim().length > 0; };
self.data.standard = rawStd.filter(hasSimdata);
self.data.ic = rawIc.filter(hasSimdata);
self._applyFilters();
} catch (e) {
if (body) body.innerHTML = '' + self._t('parseFail') + ' ';
} finally {
self.loading = false;
}
},
onerror: function() {
if (body) body.innerHTML = '' + self._t('loadFail') + ' ';
self.loading = false;
},
ontimeout: function() {
if (body) body.innerHTML = '' + self._t('loadFail') + ' ';
self.loading = false;
}
});
},
_applyFilters() {
var src = this.activeTab === 'ic' ? this.data.ic : this.data.standard;
var f = this.filters;
var filtered = src.filter(function(u) {
if (f.job && (u.job || '') !== f.job) return false;
if (f.minPower && parseFloat(u.mainAttrLevel || 0) < parseFloat(f.minPower)) return false;
if (f.minLevel) {
var lvl = 0;
if (u.simdataDisplay) lvl = parseInt(u.simdataDisplay) || 0;
else if (u.battleLevel) lvl = parseInt(u.battleLevel) || 0;
if (lvl < parseInt(f.minLevel)) return false;
}
if (f.idSearch && (u.id || '').toLowerCase().indexOf(f.idSearch.toLowerCase()) === -1) return false;
return true;
});
var self = this;
var sf = this.sortField;
filtered.sort(function(a, b) {
var va, vb;
if (sf === 'power') {
va = parseFloat(a.mainAttrLevel) || 0;
vb = parseFloat(b.mainAttrLevel) || 0;
} else if (sf === 'level') {
va = parseInt(a.simdataDisplay || a.battleLevel) || 0;
vb = parseInt(b.simdataDisplay || b.battleLevel) || 0;
} else if (sf === 'mainAttr') {
var ma = self._getMainAttr(a), mb = self._getMainAttr(b);
va = ma ? parseInt(ma) || 0 : 0;
vb = mb ? parseInt(mb) || 0 : 0;
} else {
va = a.updateTime ? new Date(a.updateTime).getTime() : 0;
vb = b.updateTime ? new Date(b.updateTime).getTime() : 0;
}
return vb - va;
});
this._renderTable(filtered);
},
_renderTable(users) {
var body = document.getElementById('tmbBody');
var count = document.getElementById('tmbCount');
if (!body) return;
if (count) count.textContent = users.length + ' ' + this._t('usersFound');
if (users.length === 0) {
body.innerHTML = '' + this._t('noData') + ' ';
return;
}
var self = this;
var html = '';
var max = Math.min(users.length, 200);
for (var i = 0; i < max; i++) {
var u = users[i];
var uid = (u.id || '').replace(//g, '>');
var job = (u.job || '').replace(/' +
'' + uid + ' ' +
'' + job + ' ' +
'' + power + ' ' +
'' + level + ' ' +
'' + mainAttr + ' ' +
'' + (hasCard ? '' + self._t('view') + ' ' : '-') + ' ' +
'' + self._t('add') + ' ' +
'';
}
body.innerHTML = html;
body._tmbUsers = users;
body.onclick = function(e) {
var cardLink = e.target.closest('[data-card]');
if (cardLink) {
var ci = parseInt(cardLink.dataset.card);
if (users[ci] && users[ci].cardLink) {
self._viewCard(users[ci].cardLink);
}
return;
}
var menuBtn = e.target.closest('.tmb-add-menu button');
if (menuBtn) {
e.stopPropagation();
var pi = parseInt(menuBtn.dataset.player);
var rowIdx = parseInt(menuBtn.dataset.useridx);
var menuEl = menuBtn.closest('.tmb-add-menu');
if (menuEl) menuEl.remove();
if (users[rowIdx]) {
self._requireAuth(function() { self._addToPlayer(pi, users[rowIdx]); });
}
return;
}
var addBtn = e.target.closest('[data-add]');
if (addBtn) {
e.stopPropagation();
var ai = parseInt(addBtn.dataset.add);
self._showAddMenu(addBtn, users[ai]);
return;
}
};
},
_showAddMenu(btn) {
document.querySelectorAll('.tmb-add-menu').forEach(function(m) { m.remove(); });
var menu = document.createElement('div');
menu.className = 'tmb-add-menu';
var idx = parseInt(btn.dataset.add);
for (var p = 1; p <= PLAYER_COUNT; p++) {
var b = document.createElement('button');
b.textContent = 'P' + p;
b.dataset.player = p;
b.dataset.useridx = idx;
menu.appendChild(b);
}
btn.appendChild(menu);
},
_viewCard(cardUrl) {
var isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent) || window.innerWidth <= 768;
if (isMobile) {
window.open(cardUrl, '_blank');
return;
}
var preview = document.getElementById('tmbCardPreview');
if (!preview) {
preview = document.createElement('div');
preview.id = 'tmbCardPreview';
preview.innerHTML = '\u00d7 ';
preview.addEventListener('click', function(e) {
if (e.target === preview || e.target.classList.contains('tmb-preview-close')) {
preview.classList.remove('visible');
}
});
document.body.appendChild(preview);
}
preview.querySelector('img').src = cardUrl;
preview.classList.add('visible');
},
_updateAuthBtn() {
var btn = document.getElementById('tmbAuthBtn');
if (!btn) return;
var isAuthed = GM_getValue('tmb_auth_id', '') && GM_getValue('tmb_auth_pwd', '');
btn.className = isAuthed ? 'tmb-auth-btn verified' : 'tmb-auth-btn';
btn.textContent = isAuthed ? this._t('authVerified') : this._t('authUnverified');
},
_requireAuth(callback) {
var savedId = GM_getValue('tmb_auth_id', '');
var savedPwd = GM_getValue('tmb_auth_pwd', '');
if (savedId && savedPwd) {
callback();
return;
}
this._showLoginDialog(callback);
},
_showLoginDialog(callback) {
var self = this;
var existing = document.getElementById('tmbLoginOverlay');
if (existing) existing.remove();
var overlay = document.createElement('div');
overlay.id = 'tmbLoginOverlay';
var savedId = GM_getValue('tmb_auth_id', '');
var escapedId = savedId.replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>');
overlay.innerHTML =
'' +
'
' + self._t('loginDesc') + '
' +
'
' + self._t('loginId') + '
' +
'
' + self._t('loginPwd') + '
' +
'
' +
'
' +
'' + self._t('loginCancel') + ' ' +
'' + self._t('loginBtn') + ' ' +
'
';
document.body.appendChild(overlay);
overlay.classList.add('visible');
var idInput = document.getElementById('tmbLoginId');
var pwdInput = document.getElementById('tmbLoginPwd');
var msgEl = document.getElementById('tmbLoginMsg');
var submitBtn = document.getElementById('tmbLoginSubmit');
var cancelBtn = document.getElementById('tmbLoginCancel');
if (!savedId) idInput.focus(); else pwdInput.focus();
cancelBtn.addEventListener('click', function() { overlay.remove(); });
overlay.addEventListener('click', function(e) { if (e.target === overlay) overlay.remove(); });
submitBtn.addEventListener('click', function() {
var id = idInput.value.trim();
var pwd = pwdInput.value;
if (!id || !pwd) {
msgEl.className = 'tmb-login-msg error';
msgEl.textContent = self._t('loginFail');
return;
}
submitBtn.disabled = true;
submitBtn.textContent = self._t('loginVerifying');
msgEl.className = 'tmb-login-msg';
msgEl.textContent = '';
self._verifyCredentials(id, pwd, function(success, errMsg) {
if (success) {
GM_setValue('tmb_auth_id', id);
GM_setValue('tmb_auth_pwd', pwd);
msgEl.className = 'tmb-login-msg success';
msgEl.textContent = self._t('loginSuccess');
self._updateAuthBtn();
setTimeout(function() {
overlay.remove();
callback();
}, 500);
} else {
submitBtn.disabled = false;
submitBtn.textContent = self._t('loginBtn');
msgEl.className = 'tmb-login-msg error';
msgEl.textContent = errMsg || self._t('loginFail');
}
});
});
pwdInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') submitBtn.click();
});
},
_verifyCredentials(id, pwd, callback) {
var self = this;
GM_xmlhttpRequest({
method: 'POST',
url: 'https://papiyas.chat/api/verify-password',
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({ id: id, password: pwd }),
onload: function(response) {
try {
var json = JSON.parse(response.responseText);
if (json.success) {
callback(true);
} else {
var msg = self._t('loginFail');
if (json.code === 'ERR-2') msg = self._t('loginNoPwd');
else if (json.code === 'ERR-LOCKED') msg = self._t('loginLocked');
else if (json.message) msg = json.message;
callback(false, msg);
}
} catch (e) {
callback(false, self._t('loginFail'));
}
},
onerror: function() {
callback(false, self._t('loginFail'));
}
});
},
_addToPlayer(playerIdx, userData) {
if (!userData || !userData.simdata) return;
var simdata = userData.simdata;
if (typeof simdata === 'object') simdata = JSON.stringify(simdata);
var groupObj = {};
for (var s = 1; s <= 5; s++) {
if (s === playerIdx) {
groupObj[s] = simdata;
} else if (typeof SimulatorBuildScore !== 'undefined' && SimulatorBuildScore.playerData && SimulatorBuildScore.playerData[s]) {
groupObj[s] = JSON.stringify(SimulatorBuildScore.playerData[s]);
} else {
groupObj[s] = TMB_BLANK_PLAYER;
}
}
var groupJson = JSON.stringify(groupObj);
var groupTab = document.querySelector('a#group-combat-tab');
if (groupTab) groupTab.click();
var groupInput = document.querySelector('input#inputSetGroupCombatAll');
if (groupInput) {
groupInput.value = groupJson;
}
var importBtn = document.querySelector('button#buttonImportSet');
if (importBtn) {
importBtn.click();
}
var playerTab = document.querySelector('a#player' + playerIdx + '-tab');
if (playerTab && userData.id) {
playerTab.textContent = userData.id;
}
}
};
TalentMarketBrowser.init();
const BuildScoreAutoUpdater = {
INTERVAL: 60 * 60 * 1000,
timer: null,
lastUpdate: 0,
enabled: false,
password: null,
initTimeout: null,
init() {
this.enabled = GM_getValue('mwi_buildscore_auto_enabled', false);
this.password = GM_getValue('mwi_buildscore_password', null);
if (this.enabled && this.password) {
this.start();
if (this.initTimeout) clearTimeout(this.initTimeout);
this.initTimeout = setTimeout(() => {
if (DataStore.isLoaded && DataStore.buildScore) {
this.sendBuildScoreUpdate();
}
}, 5000);
}
},
setPassword(pwd) {
this.password = pwd;
GM_setValue('mwi_buildscore_password', pwd);
},
enable() {
this.enabled = true;
GM_setValue('mwi_buildscore_auto_enabled', true);
this.start();
},
disable() {
this.enabled = false;
GM_setValue('mwi_buildscore_auto_enabled', false);
this.stop();
},
sendBuildScoreUpdate() {
if (!this.enabled || !this.password) {
return false;
}
if (!DataStore.isLoaded || !DataStore.characterName || !DataStore.buildScore) {
return false;
}
const gameMode = DataStore.gameMode;
const isIronman = gameMode && (gameMode === 'IRONMAN' || gameMode.toLowerCase().includes('iron'));
const apiEndpoint = isIronman
? `${SITE_URL}/api/ic/ranking/${encodeURIComponent(DataStore.characterName)}/buildscore`
: `${SITE_URL}/api/ranking/${encodeURIComponent(DataStore.characterName)}/buildscore`;
const simdata = WebSocketHook.generateSimulatorData();
const self = this;
GM_xmlhttpRequest({
method: 'PATCH',
url: apiEndpoint,
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({
buildScore: DataStore.buildScore,
password: this.password,
simdata: simdata ? JSON.stringify(simdata) : null
}),
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
self.lastUpdate = Date.now();
} else {
try {
const result = JSON.parse(response.responseText);
if (result.code === 'ERR-WRONG-PWD') {
self.disable();
}
} catch (e) {}
}
},
onerror: function() {}
});
return true;
},
start() {
if (this.timer) return;
if (!this.enabled || !this.password) return;
this.timer = setInterval(() => {
if (DataStore.isLoaded && DataStore.buildScore) {
this.sendBuildScoreUpdate();
}
}, this.INTERVAL);
},
stop() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
},
showSettingsDialog() {
const isZH = !['en'].some(lang => localStorage.getItem("i18nextLng")?.toLowerCase()?.startsWith(lang));
const existing = document.getElementById('buildscore-settings-dialog');
if (existing) existing.remove();
const currentEnabled = GM_getValue('mwi_buildscore_auto_enabled', false);
this.enabled = currentEnabled;
const dialog = document.createElement('div');
dialog.id = 'buildscore-settings-dialog';
dialog.innerHTML = `
`;
document.body.appendChild(dialog);
const self = this;
dialog.querySelector('#bs-cancel').onclick = () => dialog.remove();
dialog.querySelector('#bs-save').onclick = async () => {
const enabled = dialog.querySelector('#bs-auto-enabled').checked;
const pwd = dialog.querySelector('#bs-password').value;
const saveBtn = dialog.querySelector('#bs-save');
if (enabled) {
const passwordToUse = pwd || self.password;
if (!passwordToUse) {
self.showToast(isZH ? '请先输入密码' : 'Please enter password', 'warning');
return;
}
let charName = DataStore.characterName;
if (!charName) {
const nameEl = document.querySelector('.CharacterName_characterName__2FqyZ') ||
document.querySelector('[class*="CharacterName_characterName"]');
if (nameEl) {
charName = nameEl.textContent?.trim();
if (charName) DataStore.characterName = charName;
}
}
if (!charName) {
self.showToast(isZH ? '无法获取角色名,请刷新页面重试' : 'Cannot get character name, please refresh', 'warning');
return;
}
if (!DataStore.buildScore || DataStore.buildScore <= 0) {
self.showToast(isZH ? '数据未加载,请刷新页面再尝试' : 'BuildScore not loaded, please wait for game data', 'warning');
return;
}
saveBtn.disabled = true;
saveBtn.textContent = isZH ? '验证中...' : 'Verifying...';
const gameMode = DataStore.gameMode;
const isIronman = gameMode && (gameMode === 'IRONMAN' || gameMode.toLowerCase().includes('iron'));
const apiEndpoint = isIronman
? `${SITE_URL}/api/ic/ranking/${encodeURIComponent(charName)}/buildscore`
: `${SITE_URL}/api/ranking/${encodeURIComponent(charName)}/buildscore`;
GM_xmlhttpRequest({
method: 'PATCH',
url: apiEndpoint,
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({ buildScore: DataStore.buildScore || 0, password: passwordToUse }),
onload: function(response) {
try {
const result = JSON.parse(response.responseText);
if (response.status >= 200 && response.status < 300) {
self.setPassword(passwordToUse);
self.enable();
self.updateButtonText();
self.showToast(isZH ? '自动更新已启用' : 'Auto-update enabled', 'success');
dialog.remove();
} else if (result.code === 'ERR-WRONG-PWD') {
self.showToast(isZH ? '密码不正确' : 'Incorrect password', 'error');
saveBtn.disabled = false;
saveBtn.textContent = isZH ? '保存' : 'Save';
} else if (result.code === 'ERR-RATE-LIMIT') {
self.setPassword(passwordToUse);
self.enable();
self.updateButtonText();
self.showToast(isZH ? '自动更新已启用' : 'Auto-update enabled (rate limited, will update later)', 'success');
dialog.remove();
} else if (result.code === 'ERR-NO-PWD-SET') {
self.showToast(isZH ? '该用户未设置简历密码,请先确定已提交过简历' : 'No password set, please set one on website first', 'error');
saveBtn.disabled = false;
saveBtn.textContent = isZH ? '保存' : 'Save';
} else if (response.status === 404) {
self.showToast(isZH ? '用户记录不存在,请先提交简历' : 'User not found, please submit resume first', 'error');
saveBtn.disabled = false;
saveBtn.textContent = isZH ? '保存' : 'Save';
} else {
self.showToast(isZH ? '验证失败: ' + result.message : 'Verification failed: ' + result.message, 'error');
saveBtn.disabled = false;
saveBtn.textContent = isZH ? '保存' : 'Save';
}
} catch (e) {
self.showToast(isZH ? '响应解析错误' : 'Response parse error', 'error');
saveBtn.disabled = false;
saveBtn.textContent = isZH ? '保存' : 'Save';
}
},
onerror: function() {
self.showToast(isZH ? '网络错误,请重试' : 'Network error, please try again', 'error');
saveBtn.disabled = false;
saveBtn.textContent = isZH ? '保存' : 'Save';
}
});
} else {
self.disable();
self.updateButtonText();
dialog.remove();
}
};
dialog.onclick = (e) => { if (e.target === dialog) dialog.remove(); };
},
updateButtonText() {
const btn = document.querySelector('.tm-btn-settings');
if (!btn) return;
const isZH = !['en'].some(lang => localStorage.getItem("i18nextLng")?.toLowerCase()?.startsWith(lang));
if (this.enabled && this.password) {
btn.textContent = isZH ? '已启用自动更新' : 'Auto: ON';
btn.style.background = '#166534';
btn.style.color = '#86efac';
} else {
btn.textContent = isZH ? '未启用自动更新' : 'Auto: OFF';
btn.style.background = '#334155';
btn.style.color = '#94a3b8';
}
},
showToast(message, type = 'success') {
const existing = document.getElementById('bs-toast');
if (existing) existing.remove();
const toast = document.createElement('div');
toast.id = 'bs-toast';
toast.className = type;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translate(-50%,-50%) scale(0.9)';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
};
window.MWI_INTEGRATED.showBuildScoreSettings = () => BuildScoreAutoUpdater.showSettingsDialog();
window.addEventListener('mwi_buildscore_updated', () => {
BuildScoreAutoUpdater.init();
});
const tryGetCharacterNameFromDOM = () => {
if (DataStore.characterName) return DataStore.characterName;
const nameEl = document.querySelector('.CharacterName_characterName__2FqyZ') ||
document.querySelector('[class*="CharacterName_characterName"]');
if (nameEl) {
const name = nameEl.textContent?.trim();
if (name) {
DataStore.characterName = name;
return name;
}
}
return null;
};
let initRetryCount = 0;
const initAutoUpdater = () => {
if (DataStore.isLoaded && DataStore.characterName) {
BuildScoreAutoUpdater.init();
} else {
tryGetCharacterNameFromDOM();
initRetryCount++;
if (initRetryCount < 30) {
setTimeout(initAutoUpdater, 2000);
}
}
};
if (!isSimulatorPage) {
setTimeout(initAutoUpdater, 3000);
}
(function loadCachedClientData() {
const cachedData = localStorage.getItem("initClientData");
if (cachedData) {
try {
const decompressed = LZString.decompressFromUTF16(cachedData);
if (decompressed) {
const clientData = JSON.parse(decompressed);
BuildScoreModule.initClientData(clientData);
DataStore.itemDetailMap = clientData.itemDetailMap || null;
DataStore.actionDetailMap = clientData.actionDetailMap || null;
DataStore.abilityDetailMap = clientData.abilityDetailMap || null;
try {
GM_setValue('mwi_client_data_for_sim', cachedData);
GM_setValue('mwi_game_server', window.location.href.includes('milkywayidlecn.com') ? 'cn' : 'en');
var mktJson = localStorage.getItem('MWITools_marketAPI_json');
var mktTs = localStorage.getItem('MWITools_marketAPI_timestamp');
if (mktJson && mktTs) {
GM_setValue('mwi_market_data_for_sim', mktJson);
GM_setValue('mwi_market_data_timestamp', mktTs);
}
} catch (e2) { /* silent */ }
}
} catch (e) {}
}
})();
ClientData.init();
SVGTool.init();
function waitForDOMAndCreateUI() {
if (document.body) {
window.MWI_INTEGRATED.generateCard = generateCharacterCard;
window.MWI_INTEGRATED.closeCard = closeCard;
window.MWI_INTEGRATED.getData = getAllData;
window.MWI_INTEGRATED.getSimulatorData = getSimulatorData;
window.MWI_INTEGRATED.isDataLoaded = isDataReady;
window.MWI_INTEGRATED.waitForData = waitForData;
window.MWI_INTEGRATED.ClientData = ClientData;
} else {
setTimeout(waitForDOMAndCreateUI, 50);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', waitForDOMAndCreateUI);
window.addEventListener('load', waitForDOMAndCreateUI);
} else {
waitForDOMAndCreateUI();
}
})();