// ==UserScript== // @name [MWI]Talent Market // @namespace http://tampermonkey.net/ // @version 1.5.2 // @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') { if (window.self !== window.top) { initPapiyasChatFeatures(); } return; } 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); } 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 apiUrls = [ 'https://api.milkywayidle.com/v1/users/me', 'https://api-test.milkywayidle.com/v1/users/me', 'https://api.milkywayidlecn.com/v1/users/me', 'https://api-test.milkywayidlecn.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 zs = zone.includes('/') ? zone.split('/').pop() : zone; if (!ADZ.includes(zs) && difficulty) { const t = parseInt(difficulty.replace(/^T/i, ''), 10); if (!isNaN(t) && t < 2) return { error: "This zone only accepts difficulty T2 or higher" }; } return { zone, difficulty, members }; } 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 = `

${t.title}

${t.loading}

`; 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 = `
Generating...
`; 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 += `
Lv.${roomLevel}
`; } 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 += `
Lv.${roomLevel}
`; } 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 += `
Lv.${ability.level}
`; } 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 += `
Lv.${aura.level}
`; } 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' ? '名片上传中...' : '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) + '' + ''; 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 = '

' + self._t('title') + '

'; 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 = '
' + '
' + '
' + '
' + ''; 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') + '' : '-') + '' + '' + ''; } 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 = 'Card'; 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') + '
' + '
' + '
' + '
' + '
' + '' + '' + '
'; 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 = `

${isZH ? '模拟器数据自动更新' : 'Simulator Data Auto-Update'}

${isZH ? '启用并保存简历密码后,会定时自动获取模拟器数据上传更新到人才市场

注:名片图片仍需手动重新导入生成提交
如果没有提交过简历请先投递简历此设置才生效' : 'After enabling and saving resume password, simulator data will be automatically uploaded to talent market

Note: Card images still need to be manually re-imported and submitted
If you have not submitted a resume yet, please submit one first for this setting to take effect'}
`; 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(); } })();