您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
MWI角色名片插件 - 一键生成角色名片
// ==UserScript== // @name MWI角色名片插件 // @name:en MWI Character Card // @namespace http://tampermonkey.net/ // @version 1.6.0 // @license MIT // @description MWI角色名片插件 - 一键生成角色名片 // @description:en MWI Character Card Plugin - Generate character cards with a single click // @author Windoge // @match https://www.milkywayidle.com/* // @icon https://www.milkywayidle.com/favicon.svg // @run-at document-idle // @require https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js // ==/UserScript== (function() { 'use strict'; // 使用立即执行函数避免全局变量污染 const MWICharacterCard = (function() { const isZHInGameSetting = localStorage.getItem("i18nextLng")?.toLowerCase()?.startsWith("zh"); // 获取游戏内设置语言 let isZH = isZHInGameSetting; // MWITools 本身显示的语言默认由游戏内设置语言决定 // 检测移动端 function isMobile() { return window.innerWidth <= 768 || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); } // 进入队伍编辑模式 function enterTeamEditMode(modal) { try { if (!modal) return; // 仅在首次进入编辑时记录原始数据,后续编辑中的重渲染不覆盖 if (!state.teamCard.editMode || !state.teamCard.originalMembers) { try { state.teamCard.originalMembers = JSON.parse(JSON.stringify(state.teamCard.members)); } catch (e) { state.teamCard.originalMembers = state.teamCard.members.slice(); } } state.teamCard.editMode = true; const container = modal.querySelector('#team-character-card'); const maxMembers = 5; // 在按钮行后面插入编辑按钮(保存/取消/添加) const buttonRow = modal.querySelector('.button-row'); const editBtn = modal.querySelector('.edit-team-card-btn'); const downloadBtn = modal.querySelector('.download-team-card-btn'); const refreshBtn = modal.querySelector('.refresh-team-card-btn'); if (editBtn) editBtn.style.display = 'none'; if (downloadBtn) downloadBtn.disabled = true; if (refreshBtn) refreshBtn.disabled = true; const saveBtn = document.createElement('button'); saveBtn.className = 'save-team-card-btn'; saveBtn.textContent = isZH ? '保存' : 'Save'; const cancelBtn = document.createElement('button'); cancelBtn.className = 'cancel-team-card-btn'; cancelBtn.textContent = isZH ? '取消' : 'Cancel'; const addBtn = document.createElement('button'); addBtn.className = 'add-team-card-btn'; addBtn.textContent = isZH ? '添加角色' : 'Add Member'; buttonRow.appendChild(saveBtn); buttonRow.appendChild(cancelBtn); buttonRow.appendChild(addBtn); const refreshAddBtnState = () => { const disabled = state.teamCard.members.length >= maxMembers; addBtn.disabled = disabled; }; // 编辑时禁止卡片拦截点击,便于点击删除按钮 container.querySelectorAll('.team-card-wrap .character-card').forEach(card => { card.style.pointerEvents = 'none'; }); // 为每个非自身卡添加删除按钮 const wraps = container.querySelectorAll('.team-card-wrap'); wraps.forEach((wrap, idx) => { const m = state.teamCard.members[idx]; if (!m || m.isSelf) return; const del = document.createElement('button'); del.textContent = '×'; del.title = isZH ? '删除该队友' : 'Remove'; del.style.cssText = 'position:absolute; right:4px; top:4px; background:#dc3545; color:#fff; border:none; width:22px; height:22px; border-radius:50%; cursor:pointer; z-index:9999; pointer-events:auto;'; del.addEventListener('click', (e) => { e.stopPropagation(); try { state.teamCard.members.splice(idx, 1); } catch(err) {} saveTeamCardToStorage(state.teamCard.teamName, state.teamCard.members); // 重渲染,强制使用 state 数据,且保持编辑模式 try { document.body.removeChild(modal); } catch(err) {} showPartyCharacterCard({ forceState: true, openEditMode: true }); }, { capture: true }); wrap.style.position = 'relative'; wrap.appendChild(del); }); // 添加角色 addBtn.onclick = () => { if (state.teamCard.members.length >= maxMembers) return; const promptDiv = document.createElement('div'); promptDiv.className = 'character-card-modal'; promptDiv.innerHTML = ` <div class="modal-content" style="max-width:95vw;width:1800px;background:#1a1a2e;border:2px solid #4a90e2;border-radius:15px;color:#fff;"> <button class="close-modal">×</button> <div class="instruction-banner">${isZH ? '请输入已导出的角色数据' : 'Paste exported character data'}</div> <div style="margin-bottom:10px;"> <label style="display:block;margin-bottom:6px;color:#4a90e2;">${isZH ? '角色名(选填)' : 'Character Name (optional)'}:</label> <input class="add-member-name" placeholder="${isZH ? '如不填写,使用数据内/默认的名称' : 'Leave empty to use name from data or default'}" style="width:100%;padding:10px;background:rgba(0,0,0,0.3);border:1px solid #4a90e2;border-radius:8px;color:#fff;font-size:14px;" /> </div> <div style="margin-bottom:8px;"> <label style="display:block;margin-bottom:6px;color:#4a90e2;">${isZH ? '角色数据JSON' : 'Character Data JSON'}:</label> <textarea class="add-member-json" style="width:100%;height:300px;background:rgba(0,0,0,0.3);color:#fff;border:1px solid #4a90e2;border-radius:8px;padding:10px;font-family:Courier New, monospace;"></textarea> </div> <div class="button-row" style="margin-top:10px;"> <button class="import-member-btn">${isZH ? '导入' : 'Import'}</button> <button class="cancel-import-btn">${isZH ? '取消' : 'Cancel'}</button> </div> </div>`; document.body.appendChild(promptDiv); const close = () => document.body.removeChild(promptDiv); promptDiv.querySelector('.close-modal').onclick = close; promptDiv.querySelector('.cancel-import-btn').onclick = close; promptDiv.onclick = (e) => { if (e.target === promptDiv) close(); }; promptDiv.querySelector('.import-member-btn').onclick = () => { try { const txt = promptDiv.querySelector('.add-member-json').value.trim(); const nameInput = promptDiv.querySelector('.add-member-name').value.trim(); const obj = JSON.parse(txt); if (!isValidCharacterData(obj)) { showToastNotice(isZH ? 'JSON无效,未检测到角色数据' : 'Invalid JSON', 'error'); return; } const name = nameInput || obj.player?.name || obj.character?.name || (isZH ? '角色' : 'Character'); // 统一格式到 { player, abilities, characterSkills, characterHouseRoomMap, houseRooms } let normalized; if (obj.player) { normalized = obj; } else if (obj.character || obj.characterItems || obj.characterSkills) { normalized = { player: { name: name, equipment: obj.characterItems || [], characterItems: obj.characterItems || [] }, abilities: obj.abilities || [], characterSkills: obj.characterSkills || [], characterHouseRoomMap: obj.characterHouseRoomMap || {}, houseRooms: obj.houseRooms || {} }; } else { normalized = { player: obj }; } state.teamCard.members.push({ name, data: normalized, isSelf: false }); saveTeamCardToStorage(state.teamCard.teamName, state.teamCard.members); close(); try { document.body.removeChild(modal); } catch(err) {} showPartyCharacterCard({ forceState: true, openEditMode: true }); showToastNotice(isZH ? '已导入角色' : 'Member imported', 'success'); } catch (err) { showToastNotice(isZH ? 'JSON解析失败' : 'JSON parse error', 'error'); } }; }; // 保存 saveBtn.onclick = () => { state.teamCard.editMode = false; state.teamCard.originalMembers = null; saveTeamCardToStorage(state.teamCard.teamName, state.teamCard.members); showToastNotice(isZH ? '已保存' : 'Saved', 'success'); // 恢复按钮与交互 if (editBtn) editBtn.style.display = ''; if (downloadBtn) downloadBtn.disabled = false; if (refreshBtn) refreshBtn.disabled = false; container.querySelectorAll('.team-card-wrap .character-card').forEach(card => { card.style.pointerEvents = ''; }); // 关闭当前编辑视图并以非编辑模式重渲染,清理删除/保存/取消按钮 try { document.body.removeChild(modal); } catch (e) {} showPartyCharacterCard({ forceState: true }); }; // 取消 cancelBtn.onclick = () => { if (state.teamCard.originalMembers) { // 深拷贝恢复 try { state.teamCard.members = JSON.parse(JSON.stringify(state.teamCard.originalMembers)); } catch (e) { state.teamCard.members = state.teamCard.originalMembers; } state.teamCard.editMode = false; state.teamCard.originalMembers = null; // 尝试写入缓存(忽略配额错误) saveTeamCardToStorage(state.teamCard.teamName, state.teamCard.members); // 恢复按钮与交互 if (editBtn) editBtn.style.display = ''; if (downloadBtn) downloadBtn.disabled = false; if (refreshBtn) refreshBtn.disabled = false; try { document.body.removeChild(modal); } catch (e) {} // 强制使用内存状态渲染,避免缓存配额失败导致无法回滚 showPartyCharacterCard({ forceState: true }); } }; refreshAddBtnState(); } catch (e) { console.warn('进入编辑模式失败', e); } } // 获取当前有效的布局模式 function getEffectiveLayoutMode() { return state.layoutMode.getCurrentMode(); } // 切换布局模式 function toggleLayoutMode() { const currentMode = getEffectiveLayoutMode(); const newMode = currentMode === 'mobile' ? 'desktop' : 'mobile'; state.layoutMode.forcedMode = newMode; // 重新生成名片并应用新布局 refreshCharacterCard(); } // 刷新角色名片布局 function refreshCharacterCard() { const characterCard = document.querySelector('#character-card'); if (!characterCard) return; const modal = characterCard.closest('.character-card-modal'); if (!modal) return; // 获取当前数据 // 通过检查技能槽是否有data-skill-index属性来判断是否为我的角色名片(可编辑技能) const isMyCharacterCard = characterCard.querySelector('.skill-panel .skill-slot[data-skill-index]') !== null; let characterData, characterName, characterNameElement; if (isMyCharacterCard) { // 我的角色名片 characterData = { player: { name: window.characterCardWebSocketData?.characterName || (isZH ? '角色' : 'Character'), equipment: window.characterCardWebSocketData?.characterItems || [], characterItems: window.characterCardWebSocketData?.characterItems || [], staminaLevel: window.characterCardWebSocketData?.characterSkills?.find(s => s.skillHrid.includes('/skills/stamina'))?.level || 0, intelligenceLevel: window.characterCardWebSocketData?.characterSkills?.find(s => s.skillHrid.includes('/skills/intelligence'))?.level || 0, attackLevel: window.characterCardWebSocketData?.characterSkills?.find(s => s.skillHrid.includes('/skills/attack'))?.level || 0, powerLevel: window.characterCardWebSocketData?.characterSkills?.find(s => s.skillHrid.includes('/skills/power'))?.level || 0, defenseLevel: window.characterCardWebSocketData?.characterSkills?.find(s => s.skillHrid.includes('/skills/defense'))?.level || 0, rangedLevel: window.characterCardWebSocketData?.characterSkills?.find(s => s.skillHrid.includes('/skills/ranged'))?.level || 0, magicLevel: window.characterCardWebSocketData?.characterSkills?.find(s => s.skillHrid.includes('/skills/magic'))?.level || 0 }, abilities: window.characterCardWebSocketData?.characterAbilities || [], characterSkills: window.characterCardWebSocketData?.characterSkills || [], houseRooms: window.characterCardWebSocketData?.characterHouseRoomMap || {}, characterHouseRoomMap: window.characterCardWebSocketData?.characterHouseRoomMap || {} }; characterName = characterData.player.name; // 获取第一个角色名元素(我的角色) const characterNameDivs = document.querySelectorAll('.CharacterName_characterName__2FqyZ'); if (characterNameDivs.length > 0) { characterNameElement = characterNameDivs[0].outerHTML; } } else { // 他人角色名片(从剪贴板)- 使用缓存的数据 if (!state.clipboardCharacterData) { console.warn('剪贴板数据缓存为空,无法刷新布局'); return; } characterData = state.clipboardCharacterData.data; characterName = state.clipboardCharacterData.name; characterNameElement = state.clipboardCharacterData.nameElement; } // 重新生成名片HTML const newCardHTML = generateCharacterCard(characterData, characterName, characterNameElement, isMyCharacterCard); characterCard.outerHTML = newCardHTML; // 重新绑定事件监听器 const newCharacterCard = document.querySelector('#character-card'); if (isMyCharacterCard) { // 重新绑定技能槽点击事件 const skillSlots = newCharacterCard.querySelectorAll('.skill-slot, .empty-skill-slot'); skillSlots.forEach(slot => { slot.addEventListener('click', function() { const skillIndex = parseInt(this.getAttribute('data-skill-index')); showSkillSelector(skillIndex); }); }); } // 重新绑定布局切换按钮事件 const layoutToggleBtn = modal.querySelector('.layout-toggle-btn'); if (layoutToggleBtn) { layoutToggleBtn.onclick = toggleLayoutMode; // 更新按钮文本 updateLayoutToggleButton(); } // 更新模态框容器的布局类名 updateModalLayoutClass(); } // 获取布局切换按钮的文本 function getLayoutToggleText() { const currentMode = getEffectiveLayoutMode(); const currentIcon = currentMode === 'mobile' ? '📱' : '🖥'; const nextIcon = currentMode === 'mobile' ? '🖥' : '📱'; return `${currentIcon} → ${nextIcon}`; } // 更新布局切换按钮的显示 function updateLayoutToggleButton() { const layoutToggleBtn = document.querySelector('.layout-toggle-btn'); if (!layoutToggleBtn) return; const currentMode = getEffectiveLayoutMode(); const currentIcon = currentMode === 'mobile' ? '📱' : '🖥'; const nextIcon = currentMode === 'mobile' ? '🖥' : '📱'; layoutToggleBtn.textContent = `${currentIcon} → ${nextIcon}`; layoutToggleBtn.title = isZH ? `当前: ${currentMode === 'mobile' ? '移动端' : 'PC端'}布局,点击切换到${currentMode === 'mobile' ? 'PC端' : '移动端'}布局` : `Current: ${currentMode === 'mobile' ? 'Mobile' : 'Desktop'} layout, click to switch to ${currentMode === 'mobile' ? 'Desktop' : 'Mobile'} layout`; } // 更新模态框容器的布局类名 function updateModalLayoutClass() { const modalContent = document.querySelector('.character-card-modal .modal-content'); if (!modalContent) return; const currentMode = getEffectiveLayoutMode(); // 移除之前的布局类名 modalContent.classList.remove('desktop-layout', 'mobile-layout'); // 添加当前布局对应的类名 if (currentMode === 'desktop') { modalContent.classList.add('desktop-layout'); } else { modalContent.classList.add('mobile-layout'); } } // 简化的SVG创建工具 class CharacterCardSVGTool { constructor() { this.isLoaded = true; // 简化:直接设为true this.spriteSheets = { items: '/static/media/items_sprite.6d12eb9d.svg', skills: '/static/media/skills_sprite.57eb3a30.svg', abilities: '/static/media/abilities_sprite.38932ac3.svg', misc: '/static/media/misc_sprite.6b3198dc.svg' }; } async loadSpriteSheets() { console.log('SVG sprite系统已初始化'); console.log('Sprite文件路径:', this.spriteSheets); this.isLoaded = true; return true; } // 创建MWI风格的SVG图标 - 直接返回HTML字符串 createSVGIcon(itemId, options = {}) { const { className = 'Icon_icon__2LtL_', title = itemId, type = 'items' } = options; const svgHref = `${this.spriteSheets[type]}#${itemId}`; // 收集调试信息 if (!state.debugInfo.firstSvgPath) { state.debugInfo.firstSvgPath = svgHref; } state.debugInfo.iconCount++; return `<svg role="img" aria-label="${title}" class="${className}" width="100%" height="100%"> <use href="${svgHref}"></use> </svg>`; } // 后备图标 createFallbackIcon(itemId, className, title) { const text = itemId.length > 6 ? itemId.substring(0, 6) : itemId; return `<div class="${className}" title="${title}" style=" width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #4a90e2; color: white; font-size: 10px; border-radius: 4px; ">${text}</div>`; } hasIcon() { return this.isLoaded; } } // 技能选择器相关函数 function showSkillSelector(skillIndex) { // 获取所有可用技能(包括未装备的) const allSkills = window.characterCardWebSocketData?.characterAbilities || []; const availableSkills = allSkills .filter(ability => ability.abilityHrid && ability.abilityHrid.startsWith("/abilities/")) .sort((a, b) => (a.slotNumber || 0) - (b.slotNumber || 0)); // 创建技能选择器模态框 const modal = document.createElement('div'); modal.className = 'skill-selector-modal'; modal.innerHTML = ` <div class="skill-selector-content"> <div class="skill-selector-header"> <h3>${isZH ? '选择技能' : 'Select Skill'}</h3> <button class="close-skill-selector">×</button> </div> <div class="skill-selector-grid"> <!-- 空按钮 --> <div class="skill-option empty-skill-option" data-skill-index="${skillIndex}" data-ability-hrid="" data-level="0"> <div class="skill-option-icon"> <div class="empty-skill-icon">-</div> </div> <div class="skill-option-level">${isZH ? '空' : 'Empty'}</div> </div> ${availableSkills.map(skill => ` <div class="skill-option" data-skill-index="${skillIndex}" data-ability-hrid="${skill.abilityHrid}" data-level="${skill.level}"> <div class="skill-option-icon"> ${createSvgIcon(skill.abilityHrid, 'abilities')} </div> <div class="skill-option-level">Lv.${skill.level}</div> </div> `).join('')} </div> </div> `; // 添加事件监听器 modal.querySelector('.close-skill-selector').onclick = () => { document.body.removeChild(modal); }; modal.onclick = (e) => { if (e.target === modal) { document.body.removeChild(modal); } }; // 添加技能选项点击事件监听器 const skillOptions = modal.querySelectorAll('.skill-option'); skillOptions.forEach(option => { option.addEventListener('click', function() { const skillIndex = parseInt(this.getAttribute('data-skill-index')); const abilityHrid = this.getAttribute('data-ability-hrid'); const level = parseInt(this.getAttribute('data-level')); selectSkill(skillIndex, abilityHrid, level); }); }); document.body.appendChild(modal); } function selectSkill(skillIndex, abilityHrid, level) { // 更新用户选择的技能 if (abilityHrid === "") { // 选择"空"选项,删除该位置的技能 delete state.customSkills.selectedSkills[skillIndex]; } else { // 选择具体技能 state.customSkills.selectedSkills[skillIndex] = { abilityHrid: abilityHrid, level: level, slotNumber: skillIndex + 1 }; } // 重新生成技能面板 const characterCard = document.querySelector('#character-card'); if (characterCard) { const skillPanel = characterCard.querySelector('.skill-panel'); if (skillPanel) { // 重新生成技能面板内容 const characterData = { abilities: window.characterCardWebSocketData?.characterAbilities || [], characterSkills: window.characterCardWebSocketData?.characterSkills || [] }; const newSkillPanel = generateSkillPanel(characterData, true); skillPanel.innerHTML = newSkillPanel.replace(/<div class="skill-panel">([\s\S]*?)<\/div>$/, '$1'); // 重新添加事件监听器 const skillSlots = skillPanel.querySelectorAll('.skill-slot, .empty-skill-slot'); skillSlots.forEach(slot => { slot.addEventListener('click', function() { const skillIndex = parseInt(this.getAttribute('data-skill-index')); showSkillSelector(skillIndex); }); }); } } // 确保布局切换按钮的事件监听器仍然有效 const characterCardModal = characterCard.closest('.character-card-modal'); if (characterCardModal) { const layoutToggleBtn = characterCardModal.querySelector('.layout-toggle-btn'); if (layoutToggleBtn) { // 重新绑定布局切换按钮事件 layoutToggleBtn.onclick = toggleLayoutMode; // 更新按钮文本 updateLayoutToggleButton(); } } // 关闭技能选择器 const modal = document.querySelector('.skill-selector-modal'); if (modal) { document.body.removeChild(modal); } } // 全局版本号 const VERSION = '1.6.0'; // 使用闭包管理状态,避免全局变量 const state = { svgTool: new CharacterCardSVGTool(), debugInfo: { firstSvgPath: null, iconCount: 0 }, observer: null, timer: null, isInitialized: false, // 用户自定义技能展示状态 customSkills: { selectedSkills: [], // 用户选择的技能列表 maxSkills: 8 // 最大技能数量 }, // 布局模式控制 layoutMode: { forcedMode: null, // null=自动检测, 'desktop'=强制PC端, 'mobile'=强制移动端 getCurrentMode: function() { if (this.forcedMode) return this.forcedMode; return isMobile() ? 'mobile' : 'desktop'; } }, // 缓存剪贴板角色数据,用于布局切换 clipboardCharacterData: null, // 队伍名片数据 teamCard: { members: [], // [{ name, data, isSelf }] teamName: '', editMode: false, originalMembers: null } }; // 简化的SVG图标创建函数 function createSvgIcon(itemHrid, iconType = null, className = 'Icon_icon__2LtL_') { // 自动检测图标类型和提取itemId let type = 'items'; let itemId = itemHrid; if (itemHrid.startsWith('/items/')) { type = 'items'; itemId = itemHrid.replace('/items/', ''); } else if (itemHrid.startsWith('/abilities/')) { type = 'abilities'; itemId = itemHrid.replace('/abilities/', ''); } else if (itemHrid.startsWith('/skills/')) { type = 'skills'; itemId = itemHrid.replace('/skills/', ''); } else if (itemHrid.startsWith('/misc/')) { type = 'misc'; itemId = itemHrid.replace('/misc/', ''); } else { // 对于基础属性图标 if (['stamina', 'intelligence', 'attack', 'power', 'defense', 'ranged', 'magic'].includes(itemHrid)) { type = 'skills'; itemId = itemHrid; } else { itemId = itemHrid.replace("/items/", "").replace("/abilities/", "").replace("/skills/", "").replace("/misc/", ""); } } // 如果手动指定了类型,使用指定的类型 if (iconType) { type = iconType; } // 使用SVG工具创建图标 if (state.svgTool && state.svgTool.isLoaded) { return state.svgTool.createSVGIcon(itemId, { className: className, title: itemId, type: type }); } // 后备方案 return state.svgTool.createFallbackIcon(itemId, className, itemId); } function generateEquipmentPanel(characterObj) { // MWI装备槽位映射 - 使用grid位置 const equipmentSlots = { "/item_locations/back": { row: 1, col: 1, name: "背部" }, "/item_locations/head": { row: 1, col: 2, name: "头部" }, "/item_locations/main_hand": { row: 2, col: 1, name: "主手" }, "/item_locations/body": { row: 2, col: 2, name: "身体" }, "/item_locations/off_hand": { row: 2, col: 3, name: "副手" }, "/item_locations/hands": { row: 3, col: 1, name: "手部" }, "/item_locations/legs": { row: 3, col: 2, name: "腿部" }, "/item_locations/pouch": { row: 3, col: 3, name: "口袋" }, "/item_locations/feet": { row: 4, col: 2, name: "脚部" }, "/item_locations/neck": { row: 1, col: 5, name: "项链" }, "/item_locations/earrings": { row: 2, col: 5, name: "耳环" }, "/item_locations/ring": { row: 3, col: 5, name: "戒指" }, "/item_locations/trinket": { row: 4, col: 5, name: "饰品" }, "/item_locations/two_hand": { row: 2, col: 1, name: "双手" } }; let items = characterObj.equipment || characterObj.characterItems || []; const equipmentMap = {}; let hasTwoHandWeapon = false; // 构建装备映射 items.forEach(item => { const slotInfo = equipmentSlots[item.itemLocationHrid]; if (slotInfo) { equipmentMap[item.itemLocationHrid] = item; if (item.itemLocationHrid === "/item_locations/two_hand") hasTwoHandWeapon = true; } }); // 创建MWI风格的装备面板 let html = '<div class="equipment-panel">'; html += `<div class="panel-title">${isZH ? '装备' : 'Equipments'}</div>`; html += '<div class="EquipmentPanel_playerModel__3LRB6" style="margin-top:40px">'; // 遍历所有装备槽位 Object.entries(equipmentSlots).forEach(([slotHrid, slotInfo]) => { // 如果有双手武器,跳过单手主手槽 if (hasTwoHandWeapon && slotHrid === "/item_locations/main_hand") { return; } // 如果没有双手武器,跳过双手槽 if (!hasTwoHandWeapon && slotHrid === "/item_locations/two_hand") { return; } const item = equipmentMap[slotHrid]; html += `<div style="grid-row-start: ${slotInfo.row}; grid-column-start: ${slotInfo.col};">`; html += '<div class="ItemSelector_itemSelector__2eTV6">'; html += '<div class="ItemSelector_itemContainer__3olqe">'; html += '<div class="Item_itemContainer__x7kH1">'; html += '<div>'; if (item) { // 有装备的槽位 const itemName = item.itemHrid.replace('/items/', ''); const enhancementLevel = item.enhancementLevel || 0; html += '<div class="Item_item__2De2O Item_clickable__3viV6" style="position: relative;">'; html += '<div class="Item_iconContainer__5z7j4">'; html += createSvgIcon(item.itemHrid, 'items'); // 使用MWI的Icon类 html += '</div>'; // 强化等级 - 完全按照MWI原生格式 if (enhancementLevel > 0) { html += `<div class="Item_enhancementLevel__19g-e enhancementProcessed enhancementLevel_${enhancementLevel}" style="z-index: 9">+${enhancementLevel}</div>`; } html += '</div>'; } else { // 空装备槽 html += '<div class="Item_item__2De2O" style="position: relative; opacity: 0.3;">'; html += '<div class="Item_iconContainer__5z7j4">'; html += `<div style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; color: #999; font-size: 10px;">${isZH ? '空' : 'Empty'}</div>`; html += '</div>'; html += '</div>'; } html += '</div>'; html += '</div>'; html += '</div>'; html += '</div>'; html += '</div>'; }); html += '</div>'; // EquipmentPanel_playerModel__3LRB6 html += '</div>'; // equipment-panel return html; } // 从页面获取战斗等级 function calculateCombatLevel(characterObj) { try { // 获取各项属性等级 const stamina = characterObj.staminaLevel || 0; const intelligence = characterObj.intelligenceLevel || 0; const defense = characterObj.defenseLevel || 0; const attack = characterObj.attackLevel || 0; const power = characterObj.powerLevel || 0; const ranged = characterObj.rangedLevel || 0; const magic = characterObj.magicLevel || 0; // 计算公式:战斗等级 = 0.2 * (耐力 + 智力 + 防御) + 0.4 * MAX(0.5 * (攻击 + 力量), 远程, 魔法) const baseCombat = 0.2 * (stamina + intelligence + defense); const attackPower = 0.5 * (attack + power); const maxCombat = Math.max(attackPower, ranged, magic); const combatLevel = Math.floor(baseCombat + 0.4 * maxCombat); return combatLevel; } catch (error) { console.log('计算战斗等级失败:', error); return 0; } } function getCombatLevelFromPage() { try { // 查找包含战斗等级信息的元素 const overviewTab = document.querySelector('.SharableProfile_overviewTab__W4dCV'); if (overviewTab) { // 查找包含"战斗等级:"文本的div元素 const combatLevelDiv = Array.from(overviewTab.querySelectorAll('div')).find(div => div.textContent && div.textContent.includes('战斗等级:') ); if (combatLevelDiv) { // 提取数字 const match = combatLevelDiv.textContent.match(/战斗等级:\s*(\d+)/); if (match && match[1]) { return parseInt(match[1]); } } } } catch (error) { console.log('获取战斗等级失败:', error); } return 0; } function generateAbilityPanel(characterObj) { const abilityMapping = [ { key: "staminaLevel", name: isZH ? "耐力" : "Stamina", icon: "stamina" }, { key: "intelligenceLevel", name: isZH ? "智力" : "Intelligence", icon: "intelligence" }, { key: "attackLevel", name: isZH ? "攻击" : "Attack", icon: "attack" }, { key: "powerLevel", name: isZH ? "力量" : "Power", icon: "power" }, { key: "defenseLevel", name: isZH ? "防御" : "Defense", icon: "defense" }, { key: "rangedLevel", name: isZH ? "远程" : "Ranged", icon: "ranged" }, { key: "magicLevel", name: isZH ? "魔法" : "Magic", icon: "magic" } ]; let html = '<div class="ability-panel">'; html += `<div class="panel-title">${isZH ? '属性等级' : 'Skills'}</div><div class="ability-list">`; // 添加战斗等级作为第一行 const combatLevel = calculateCombatLevel(characterObj); html += `<div class="ability-row"> <div class="ability-icon"> <svg role="img" aria-label="combat" class="Icon_icon__2LtL_" width="100%" height="100%"> <use href="/static/media/misc_sprite.6b3198dc.svg#combat"></use> </svg> </div> <span style="flex: 1;">${isZH ? '战斗' : 'Combat'}</span> <span class="level">Lv.${combatLevel}</span> </div>`; abilityMapping.forEach(ability => { let level = 0; if (characterObj[ability.key]) { level = characterObj[ability.key]; } else if (characterObj.characterSkills) { const skillKey = ability.key.replace('Level', ''); const skill = characterObj.characterSkills.find(skill => skill.skillHrid.includes(`/skills/${skillKey}`)); level = skill ? skill.level : 0; } html += `<div class="ability-row"> <div class="ability-icon">${createSvgIcon(ability.icon, 'skills')}</div> <span style="flex: 1;">${ability.name}</span> <span class="level">Lv.${level}</span> </div>`; }); return html + '</div></div>'; } function generateSkillPanel(data, isMyCharacter = false, options = {}) { const teamMode = options && options.teamMode; let abilities = data.abilities || data.characterSkills || []; let combatSkills; if (isMyCharacter) { // 团队模式:仅显示已装备技能(slotNumber>0),不显示空槽,不可编辑 if (teamMode) { combatSkills = abilities .filter(ability => ability.abilityHrid && ability.abilityHrid.startsWith("/abilities/")) .filter(ability => ability.slotNumber && ability.slotNumber > 0) .sort((a, b) => a.slotNumber - b.slotNumber); let html = '<div class="skill-panel">'; html += `<div class="panel-title">${isZH ? '技能等级' : 'Abilities'}</div>`; html += '<div class="AbilitiesPanel_abilityGrid__-p-VF">'; combatSkills.forEach(selectedSkill => { html += '<div>'; html += `<div class="Ability_ability__1njrh">`; html += '<div class="Ability_iconContainer__3syNQ">'; html += createSvgIcon(selectedSkill.abilityHrid, 'abilities'); html += '</div>'; html += `<div class="Ability_level__1L-do">Lv.${selectedSkill.level}</div>`; html += '</div>'; html += '</div>'; }); html += '</div>'; html += '</div>'; return html; } // 场景2:根据slotNumber筛选和排序 combatSkills = abilities .filter(ability => ability.abilityHrid && ability.abilityHrid.startsWith("/abilities/")) .filter(ability => ability.slotNumber && ability.slotNumber > 0) .sort((a, b) => a.slotNumber - b.slotNumber); // 按slotNumber升序排列 // 初始化用户选择的技能(如果为空) if (state.customSkills.selectedSkills.length === 0) { // 默认显示前5个技能 state.customSkills.selectedSkills = combatSkills.slice(0, 5).map(skill => ({ abilityHrid: skill.abilityHrid, level: skill.level, slotNumber: skill.slotNumber })); } let html = '<div class="skill-panel">'; html += `<div class="panel-title">${isZH ? '技能等级' : 'Abilities'}</div>`; // 使用MWI原生的技能网格容器 html += '<div class="AbilitiesPanel_abilityGrid__-p-VF">'; // 渲染用户选择的技能(最多8个) for (let i = 0; i < state.customSkills.maxSkills; i++) { const selectedSkill = state.customSkills.selectedSkills[i]; if (selectedSkill) { // 显示已选择的技能 html += '<div>'; html += `<div class="Ability_ability__1njrh Ability_clickable__w9HcM skill-slot" data-skill-index="${i}">`; html += '<div class="Ability_iconContainer__3syNQ">'; html += createSvgIcon(selectedSkill.abilityHrid, 'abilities'); html += '</div>'; html += `<div class="Ability_level__1L-do">Lv.${selectedSkill.level}</div>`; html += '</div>'; html += '</div>'; } else { // 显示空白位置(鼠标悬停时显示虚线边框) html += '<div>'; html += `<div class="Ability_ability__1njrh Ability_clickable__w9HcM empty-skill-slot" data-skill-index="${i}">`; html += '</div>'; html += '</div>'; } } html += '</div>'; // AbilitiesPanel_abilityGrid__-p-VF html += '</div>'; // skill-panel return html; } else { // 场景1:保持原始顺序,不排序 combatSkills = abilities .filter(ability => ability.abilityHrid && ability.abilityHrid.startsWith("/abilities/")); // 团队模式下,如果包含 slotNumber 字段,则仅展示已装备技能 if (teamMode) { const hasSlot = combatSkills.some(a => a.slotNumber && a.slotNumber > 0); if (hasSlot) { combatSkills = combatSkills .filter(a => a.slotNumber && a.slotNumber > 0) .sort((a, b) => a.slotNumber - b.slotNumber); } } let html = '<div class="skill-panel">'; html += `<div class="panel-title">${isZH ? '技能等级' : 'Abilities'}</div>`; // 使用MWI原生的技能网格容器 html += '<div class="AbilitiesPanel_abilityGrid__-p-VF">'; // 渲染每个技能 combatSkills.forEach(ability => { const abilityId = ability.abilityHrid.replace('/abilities/', ''); html += '<div>'; html += '<div class="Ability_ability__1njrh Ability_clickable__w9HcM">'; html += '<div class="Ability_iconContainer__3syNQ">'; html += createSvgIcon(ability.abilityHrid, 'abilities'); // 使用完整的hrid html += '</div>'; html += `<div class="Ability_level__1L-do">Lv.${ability.level}</div>`; html += '</div>'; html += '</div>'; }); html += '</div>'; // AbilitiesPanel_abilityGrid__-p-VF html += '</div>'; // skill-panel return html; } } function generateHousePanel(data) { const houseRoomsMapping = [ { hrid: "/house_rooms/dining_room", icon: "stamina", name: isZH ? "餐厅" : "Dining Room" }, { hrid: "/house_rooms/library", icon: "intelligence", name: isZH ? "图书馆" : "Library" }, { hrid: "/house_rooms/dojo", icon: "attack", name: isZH ? "道场" : "Dojo" }, { hrid: "/house_rooms/gym", icon: "power", name: isZH ? "健身房" : "Gym" }, { hrid: "/house_rooms/armory", icon: "defense", name: isZH ? "军械库" : "Armory" }, { hrid: "/house_rooms/archery_range", icon: "ranged", name: isZH ? "射箭场" : "Archery Range" }, { hrid: "/house_rooms/mystical_study", icon: "magic", name: isZH ? "神秘研究室" : "Mystical Study" } ]; let houseRoomMap = data.houseRooms || data.characterHouseRoomMap || {}; let html = '<div class="house-panel">'; html += `<div class="panel-title">${isZH ? '房屋等级' : 'House Rooms'}</div>`; // 使用和技能面板相同的MWI原生结构 html += '<div class="AbilitiesPanel_abilityGrid__-p-VF">'; // 遍历所有房屋类型 houseRoomsMapping.forEach(houseRoom => { let level = 0; if (houseRoomMap[houseRoom.hrid]) { level = typeof houseRoomMap[houseRoom.hrid] === 'object' ? houseRoomMap[houseRoom.hrid].level || 0 : houseRoomMap[houseRoom.hrid]; } // 使用和技能相同的MWI原生结构 html += '<div>'; html += '<div class="Ability_ability__1njrh Ability_clickable__w9HcM">'; html += '<div class="Ability_iconContainer__3syNQ">'; html += createSvgIcon(houseRoom.icon, 'skills'); // 使用标准的Icon类 html += '</div>'; // 为8级房屋添加特殊显示 let levelText = ''; let levelClass = 'Ability_level__1L-do'; if (level === 8) { levelText = `Lv.8`; levelClass += ' house-max-level'; } else if (level > 0) { levelText = `Lv.${level}`; } else { levelText = isZH ? '未建造' : 'Lv.0'; } html += `<div class="${levelClass}">${levelText}</div>`; html += '</div>'; html += '</div>'; }); html += '</div>'; // AbilitiesPanel_abilityGrid__-p-VF html += '</div>'; // house-panel return html; } function generateCharacterCard(data, characterName, characterNameElement = null, isMyCharacter = false, options = {}) { let characterObj = data.player || data; const equipmentPanel = generateEquipmentPanel(characterObj); // 创建标题栏内容 let headerContent = ''; if (characterNameElement) { // 使用从页面复制的角色信息元素 headerContent = characterNameElement; } else { // 后备方案:使用简单的角色名 headerContent = `<h2>${characterName}</h2>`; } // 根据当前布局模式添加相应的类名 const currentLayoutMode = getEffectiveLayoutMode(); const layoutClass = `layout-${currentLayoutMode}`; return ` <div id="character-card" class="character-card ${layoutClass}"> <div class="card-header">${headerContent}</div> <div class="card-content"> ${equipmentPanel} ${generateAbilityPanel(characterObj)} ${generateSkillPanel(data, isMyCharacter, options)} ${generateHousePanel(data)} </div> </div> `; } function createModalStyles() { const style = document.createElement('style'); style.textContent = ` .character-card-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); z-index: 10000; display: flex; justify-content: center; align-items: center; padding: 16px; box-sizing: border-box; } .modal-content { background: white; border-radius: 15px; padding: 20px; max-width: 90vw; max-height: 90vh; overflow: auto; position: relative; transition: max-width 0.3s ease; } /* 当强制使用桌面布局时,扩大容器尺寸 */ .modal-content.desktop-layout { max-width: 95vw; } /* 当强制使用桌面布局时,使用桌面端的完整尺寸 */ .modal-content.desktop-layout .character-card { max-width: 1000px; width: auto; } /* 当强制使用移动端布局时,使用移动端的紧凑尺寸 */ .modal-content.mobile-layout .character-card { max-width: 500px; width: auto; } .close-modal { position: absolute; top: 10px; right: 15px; background: none; border: none; font-size: 24px; cursor: pointer; color: #666; z-index: 1; } .close-modal:hover { color: #000; } .character-card { background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); border: 2px solid #4a90e2; border-radius: 15px; padding: 20px; color: white; font-family: 'Arial', sans-serif; max-width: 800px; margin: 0 auto; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); } .card-header { text-align: center; margin-bottom: 20px; border-bottom: 2px solid #4a90e2; padding-bottom: 10px; } .card-header h2 { margin: 0; color: #4a90e2; font-size: 24px; text-shadow: 0 0 10px rgba(74, 144, 226, 0.5); } /* 角色信息元素在名片中的样式 */ .card-header .CharacterName_characterName__2FqyZ { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 8px; } .card-header .CharacterName_chatIcon__22lxV { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; } .card-header .CharacterName_name__1amXp { font-size: 20px; font-weight: bold; text-shadow: 0 0 10px rgba(74, 144, 226, 0.5); } .card-header .CharacterName_gameMode__2Pvw8 { font-size: 14px; opacity: 0.8; } .card-content { display: grid; gap: 20px; } /* PC端布局 */ .character-card.layout-desktop .card-content { grid-template-columns: 1fr 0.7fr; grid-template-rows: auto 1fr; } /* 移动端布局 */ .character-card.layout-mobile .card-content { grid-template-columns: 1fr; grid-template-rows: auto auto auto auto; } .equipment-panel, .house-panel, .ability-panel, .skill-panel { background: rgba(255, 255, 255, 0.1); border-radius: 10px; padding: 6px; border: 1px solid rgba(74, 144, 226, 0.3); } .panel-title { margin: 0 0 15px 0; color: #4a90e2; font-size: 16px; border-bottom: 1px solid rgba(74, 144, 226, 0.3); padding-bottom: 5px; text-align: center; } /* PC端面板位置 */ .character-card.layout-desktop .equipment-panel { grid-column: 1; grid-row: 1; } .character-card.layout-desktop .ability-panel { grid-column: 2; grid-row: 1; } .character-card.layout-desktop .house-panel { grid-column: 1; grid-row: 2; } .character-card.layout-desktop .skill-panel { grid-column: 2; grid-row: 2; } /* 移动端面板位置 */ .character-card.layout-mobile .equipment-panel { grid-column: 1; grid-row: 1; } .character-card.layout-mobile .ability-panel { grid-column: 1; grid-row: 2; } .character-card.layout-mobile .house-panel { grid-column: 1; grid-row: 3; } .character-card.layout-mobile .skill-panel { grid-column: 1; grid-row: 4; } /* 只为模态框内的装备面板添加网格布局,不影响游戏原生UI */ .character-card .EquipmentPanel_playerModel__3LRB6 { display: grid; grid-template-columns: repeat(5, 1fr); grid-template-rows: repeat(4, auto); gap: 8px; padding: 10px; max-width: 350px; margin: 0 auto; } /* 确保装备槽的基本布局 */ .character-card .ItemSelector_itemSelector__2eTV6 { display: flex; align-items: center; justify-content: center; min-height: 60px; } /* 技能面板样式 - 仅作用于角色名片内 */ .character-card .AbilitiesPanel_abilityGrid__-p-VF { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; padding: 10px; max-height: 180px; overflow-y: auto; } /* 技能项容器 */ .character-card .Ability_ability__1njrh { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 70px; border-radius: 8px; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(74, 144, 226, 0.3); transition: all 0.2s ease; } .character-card .Ability_ability__1njrh.Ability_clickable__w9HcM:hover { background: rgba(74, 144, 226, 0.1); border-color: #4a90e2; transform: scale(1.05); } /* 技能图标容器 */ .character-card .Ability_iconContainer__3syNQ { width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; margin-bottom: 4px; } /* 房屋等级图标容器 - 调整垂直居中 */ .character-card .house-panel .Ability_iconContainer__3syNQ { width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; margin-bottom: 4px; transform: translateY(2px); } /* 技能等级文字 */ .character-card .Ability_level__1L-do { font-size: 12px; font-weight: bold; color: #fff; text-align: center; } /* 房屋最高等级特殊样式 */ .character-card .house-max-level { color: #ff8c00 !important; font-weight: bold; text-shadow: 0 0 4px rgba(255, 140, 0, 0.5); } .ability-panel { grid-column: 2; grid-row: 1; } .ability-list { flex: 1; } .ability-row { display: flex; align-items: center; margin-bottom: 8px; padding: 4px; border-radius: 4px; } .ability-icon { width: 30px; height: 30px; margin-right: 10px; display: flex; align-items: center; justify-content: center; } .house-panel { grid-column: 1; grid-row: 2; } .skill-panel { grid-column: 2; grid-row: 2; } .level { color: #fff; font-weight: bold; } @media (max-width: 768px) { /* 移动端模态框调整 */ .character-card-modal { padding: 8px; } .modal-content { max-width: 95vw; max-height: 95vh; padding: 12px; overflow-y: auto; } /* 移动端布局覆盖 - 当在移动设备上且没有强制模式时 */ .character-card:not(.layout-desktop) .card-content { grid-template-columns: 1fr !important; grid-template-rows: auto auto auto auto !important; gap: 12px; } .character-card:not(.layout-desktop) .equipment-panel { grid-column: 1 !important; grid-row: 1 !important; } .character-card:not(.layout-desktop) .ability-panel { grid-column: 1 !important; grid-row: 2 !important; } .character-card:not(.layout-desktop) .house-panel { grid-column: 1 !important; grid-row: 3 !important; } .character-card:not(.layout-desktop) .skill-panel { grid-column: 1 !important; grid-row: 4 !important; } /* 移动端面板样式调整 */ .equipment-panel, .house-panel, .ability-panel, .skill-panel { padding: 10px; margin-bottom: 4px; } /* 移动端装备面板调整 - 保持游戏原始布局 */ .character-card .EquipmentPanel_playerModel__3LRB6 { gap: 6px; padding: 8px; max-width: 100%; } /* 移动端技能面板调整 - 每行4个 */ .character-card .ability-panel .AbilitiesPanel_abilityGrid__-p-VF { grid-template-columns: repeat(4, 1fr); gap: 10px; padding: 12px; max-height: 180px; } /* 移动端房屋面板调整 - 每行4个 */ .character-card .house-panel .AbilitiesPanel_abilityGrid__-p-VF { grid-template-columns: repeat(4, 1fr); gap: 8px; padding: 10px; max-height: 180px; } /* 移动端技能卡片间距调整 */ .character-card .ability-panel .Ability_ability__1njrh { margin: 2px; min-height: 75px; } /* 移动端房屋卡片间距调整 - 4列布局 */ .character-card .house-panel .Ability_ability__1njrh { margin: 1px; min-height: 65px; font-size: 11px; } /* 移动端房屋等级图标容器 - 调整垂直居中 */ .character-card .house-panel .Ability_iconContainer__3syNQ { transform: translateY(1px); } /* 移动端面板标题调整 */ .panel-title { font-size: 14px; margin-bottom: 8px; padding-bottom: 4px; } /* 移动端字体调整 */ .character-card { font-size: 12px; } /* 移动端指示横幅调整 */ .instruction-banner { padding: 8px; font-size: 14px; } } .instruction-banner { background: #17a2b8; color: white; padding: 10px; border-radius: 5px; margin-bottom: 10px; font-weight: bold; text-align: center; } .download-section { text-align: center; margin-bottom: 15px; } /* 统一按钮外观:下载 / 刷新 / 编辑 / 添加 */ .download-card-btn, .download-team-card-btn, .refresh-team-card-btn, .edit-team-card-btn, .add-team-card-btn { background: #17a2b8; color: white; border: none; padding: 6px 12px; border-radius: 4px; font-size: 12px; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .download-card-btn:hover:not(:disabled), .download-team-card-btn:hover:not(:disabled), .refresh-team-card-btn:hover:not(:disabled), .edit-team-card-btn:hover:not(:disabled), .add-team-card-btn:hover:not(:disabled) { background: #138496; transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.15); } .download-card-btn:disabled, .download-team-card-btn:disabled, .refresh-team-card-btn:disabled, .edit-team-card-btn:disabled, .add-team-card-btn:disabled { background: #6c757d; cursor: not-allowed; transform: none; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } /* 保存(绿色) */ .save-team-card-btn { background: #28a745; color: #fff; border: none; padding: 6px 12px; border-radius: 4px; font-size: 12px; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .save-team-card-btn:hover:not(:disabled) { background: #218838; transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.15); } .save-team-card-btn:disabled { background: #6c757d; cursor: not-allowed; transform: none; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } /* 取消(红色) */ .cancel-team-card-btn { background: #dc3545; color: #fff; border: none; padding: 6px 12px; border-radius: 4px; font-size: 12px; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .cancel-team-card-btn:hover:not(:disabled) { background: #c82333; transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.15); } .cancel-team-card-btn:disabled { background: #6c757d; cursor: not-allowed; transform: none; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } /* 布局切换按钮样式 */ .layout-toggle-btn { background: #17a2b8; color: white; border: none; padding: 6px 10px; border-radius: 4px; font-size: 14px; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-weight: bold; } .layout-toggle-btn:hover { background: #138496; transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.15); } .layout-toggle-btn:active { transform: translateY(0); box-shadow: 0 2px 4px rgba(0,0,0,0.1); } /* 技能提示样式 */ .skill-hint { margin-top: 8px; text-align: center; } .skill-hint span { font-size: 12px; color: #17a2b8; font-style: italic; background: rgba(23, 162, 184, 0.1); padding: 4px 8px; border-radius: 4px; border: 1px solid rgba(23, 162, 184, 0.3); } /* 按钮行样式 */ .button-row { display: flex; gap: 8px; justify-content: center; align-items: center; margin-bottom: 8px; } .save-skill-config-btn, .load-skill-config-btn { background: #17a2b8; color: white; border: none; padding: 6px 12px; border-radius: 4px; font-size: 12px; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .save-skill-config-btn:hover, .load-skill-config-btn:hover { background: #138496; transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.15); } /* 仅为角色名片内的SVG图标添加优化,不影响游戏原生UI */ .character-card .Icon_icon__2LtL_ { width: 100%; height: 100%; filter: drop-shadow(0 0 2px rgba(0,0,0,0.3)); image-rendering: -webkit-optimize-contrast; image-rendering: -moz-crisp-edges; image-rendering: pixelated; } /* 空白技能槽样式 */ .character-card .empty-skill-slot { cursor: pointer; border: 1px dashed rgba(74, 144, 226, 0.3); background: transparent; min-height: 70px; display: flex; align-items: center; justify-content: center; } .character-card .empty-skill-slot:hover { border: 1px dashed #4a90e2; background: rgba(74, 144, 226, 0.1); } /* 技能选择器模态框样式 */ .skill-selector-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); z-index: 20000; display: flex; justify-content: center; align-items: center; padding: 16px; box-sizing: border-box; } .skill-selector-content { background: #1a1a2e; border-radius: 15px; padding: 20px; max-width: 80vw; max-height: 80vh; overflow: auto; position: relative; min-width: 400px; border: 2px solid #4a90e2; } .skill-selector-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; border-bottom: 1px solid #4a90e2; padding-bottom: 10px; } .skill-selector-header h3 { margin: 0; color: #fff; font-size: 18px; } .close-skill-selector { background: none; border: none; font-size: 24px; cursor: pointer; color: #ccc; padding: 0; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; } .close-skill-selector:hover { color: #fff; } .skill-selector-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; max-height: 400px; overflow-y: auto; } .skill-option { display: flex; flex-direction: column; align-items: center; padding: 8px; border: 1px solid #4a90e2; border-radius: 8px; cursor: pointer; transition: all 0.2s ease; background: rgba(255, 255, 255, 0.05); } .skill-option:hover { border-color: #4a90e2; background: rgba(74, 144, 226, 0.2); transform: scale(1.05); } .skill-option-icon { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; margin-bottom: 4px; } .skill-option-level { font-size: 11px; font-weight: bold; color: #fff; text-align: center; } /* 空技能选项样式 */ .skill-option.empty-skill-option { border: 1px dashed #4a90e2; background: rgba(255, 255, 255, 0.02); } .skill-option.empty-skill-option:hover { border-color: #4a90e2; background: rgba(74, 144, 226, 0.1); } .empty-skill-icon { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; border: 1px dashed #4a90e2; border-radius: 4px; color: #4a90e2; font-size: 16px; font-weight: bold; } /* 移动端技能选择器调整 */ @media (max-width: 768px) { .skill-selector-content { max-width: 95vw; min-width: 300px; padding: 15px; } .skill-selector-grid { grid-template-columns: repeat(4, 1fr); gap: 8px; } .skill-option { padding: 6px; } .skill-option-icon { width: 32px; height: 32px; } .skill-option-level { font-size: 10px; } .empty-skill-icon { width: 32px; height: 32px; font-size: 14px; } } `; document.head.appendChild(style); } // 队伍名片样式(单独注入,避免干扰已有样式) function createTeamStyles() { const style = document.createElement('style'); style.textContent = ` .team-card-modal .modal-content { max-width: 98vw; } .team-name { text-align: center; color: #4a90e2; font-weight: bold; margin: 6px 0 12px 0; } .team-cards-container { display: flex; gap: 6px; flex-wrap: nowrap; align-items: flex-start; overflow-x: auto; padding-bottom: 8px; } .team-card-wrap { width: 320px; position: relative; } .team-card-wrap .character-card { position: absolute; top: 0; left: 0; transform: scale(0.8); transform-origin: top left; width: 390px; } .team-mode .card-header { margin-bottom: 12px; } .team-mode .panel-title { font-size: 14px; margin-bottom: 10px; } .team-mode .character-card .EquipmentPanel_playerModel__3LRB6 { gap: 6px; padding: 8px; } .team-hint { text-align: center; color: #4a90e2; font-size: 12px; margin: -4px 0 10px 0; opacity: 0.9; } /* 轻量全局提示条 */ .toast-notice { position: fixed; top: 16px; right: 16px; padding: 8px 12px; border-radius: 4px; color: #fff; font-size: 12px; z-index: 20001; box-shadow: 0 2px 8px rgba(0,0,0,0.2); opacity: 0; transform: translateY(-6px); transition: opacity 0.2s ease, transform 0.2s ease; } .toast-notice.show { opacity: 1; transform: translateY(0); } .toast-success { background: #344386; } .toast-error { background: #4f171f; } .toast-info { background: #344386; } `; document.head.appendChild(style); } function adjustTeamCardWrapHeights() { try { const scale = 0.8; const wraps = document.querySelectorAll('.team-card-wrap'); wraps.forEach(w => { const card = w.querySelector('.character-card'); if (!card) return; const unscaledHeight = card.offsetHeight; // layout height const scaledHeight = Math.round(unscaledHeight * scale); w.style.height = scaledHeight + 'px'; }); } catch (e) { /* ignore */ } } // 轻量提示条 function showToastNotice(text, variant = 'success', durationMs = 1800) { try { const div = document.createElement('div'); div.className = `toast-notice toast-${variant}`; div.textContent = text; document.body.appendChild(div); // 触发过渡 requestAnimationFrame(() => div.classList.add('show')); setTimeout(() => { try { div.classList.remove('show'); setTimeout(() => document.body.removeChild(div), 250); } catch (e) {} }, durationMs); } catch (e) {} } // 转换 WS 的 init_character_data 为名片数据 function transformInitCharacterDataToCardData(parsedData) { return { player: { name: parsedData.character?.name || parsedData.characterName || parsedData.name || (isZH ? '角色' : 'Character'), equipment: parsedData.characterItems || [], characterItems: parsedData.characterItems || [], staminaLevel: parsedData.characterSkills?.find(s => s.skillHrid.includes('/skills/stamina'))?.level || 0, intelligenceLevel: parsedData.characterSkills?.find(s => s.skillHrid.includes('/skills/intelligence'))?.level || 0, attackLevel: parsedData.characterSkills?.find(s => s.skillHrid.includes('/skills/attack'))?.level || 0, powerLevel: parsedData.characterSkills?.find(s => s.skillHrid.includes('/skills/power'))?.level || 0, defenseLevel: parsedData.characterSkills?.find(s => s.skillHrid.includes('/skills/defense'))?.level || 0, rangedLevel: parsedData.characterSkills?.find(s => s.skillHrid.includes('/skills/ranged'))?.level || 0, magicLevel: parsedData.characterSkills?.find(s => s.skillHrid.includes('/skills/magic'))?.level || 0 }, abilities: parsedData.characterAbilities || [], characterSkills: parsedData.characterSkills || [], houseRooms: parsedData.characterHouseRoomMap || {}, characterHouseRoomMap: parsedData.characterHouseRoomMap || {} }; } // 将 profile_shared 存档对象转换为名片数据 function transformProfileSharedToCardData(profileStoredObj) { try { const profile = profileStoredObj.profile; const characterName = profileStoredObj.characterName || profile?.sharableCharacter?.name || (isZH ? '角色' : 'Character'); const wearableMap = profile?.wearableItemMap || {}; const equipment = Object.values(wearableMap || {}).map(item => ({ itemLocationHrid: item.itemLocationHrid, itemHrid: item.itemHrid, enhancementLevel: item.enhancementLevel || 0 })); const characterSkills = (profile?.characterSkills || []).map(s => ({ skillHrid: s.skillHrid, level: s.level })); const levels = { staminaLevel: characterSkills.find(s => s.skillHrid.includes('/skills/stamina'))?.level || 0, intelligenceLevel: characterSkills.find(s => s.skillHrid.includes('/skills/intelligence'))?.level || 0, attackLevel: characterSkills.find(s => s.skillHrid.includes('/skills/attack'))?.level || 0, powerLevel: characterSkills.find(s => s.skillHrid.includes('/skills/power'))?.level || 0, defenseLevel: characterSkills.find(s => s.skillHrid.includes('/skills/defense'))?.level || 0, rangedLevel: characterSkills.find(s => s.skillHrid.includes('/skills/ranged'))?.level || 0, magicLevel: characterSkills.find(s => s.skillHrid.includes('/skills/magic'))?.level || 0 }; const abilities = (profile?.equippedAbilities || []).map(a => ({ abilityHrid: a?.abilityHrid || '', level: a?.level || 1 })); const houseMapRaw = profile?.characterHouseRoomMap || {}; const houseRooms = {}; try { Object.values(houseMapRaw).forEach(h => { if (h?.houseRoomHrid) houseRooms[h.houseRoomHrid] = h.level || 0; }); } catch {} return { player: { name: characterName, equipment, characterItems: equipment, ...levels }, abilities, characterSkills, houseRooms, characterHouseRoomMap: houseMapRaw }; } catch (e) { console.warn('transformProfileSharedToCardData 失败:', e); return null; } } function getTeamNameFromPage() { const nameEl = document.querySelector('.Party_partyName__3XL5z'); return nameEl ? nameEl.textContent.trim() : (isZH ? '队伍' : 'Party'); } // 构建队伍成员名片数据列表 function buildPartyCharacterDataList() { const list = []; const wsData = window.characterCardWebSocketData; if (!wsData || !wsData.partyInfo) { console.log('[队伍名片] 未检测到 partyInfo,无法构建队伍数据'); return list; } const myId = wsData?.character?.id; const slotMap = wsData.partyInfo.partySlotMap || {}; const storedProfilesStr = localStorage.getItem('profile_export_list'); let storedProfiles = []; try { storedProfiles = storedProfilesStr ? JSON.parse(storedProfilesStr) : []; } catch {} console.log('[队伍名片] 检测到队伍成员槽位:', Object.keys(slotMap).length); let idx = 0; for (const member of Object.values(slotMap)) { if (!member?.characterID) continue; idx++; if (member.characterID === myId) { const selfName = wsData?.character?.name || wsData.characterName || (isZH ? '角色' : 'Character'); console.log(`[队伍名片] 成员${idx}: 自己 (${selfName}) 使用WS数据`); list.push({ name: selfName, data: transformInitCharacterDataToCardData(wsData), isSelf: true }); } else { const match = storedProfiles.find(p => p.characterID === member.characterID); if (match) { const cardData = transformProfileSharedToCardData(match); if (cardData) { console.log(`[队伍名片] 成员${idx}: ${match.characterName} 使用profile_shared存档`); list.push({ name: match.characterName, data: cardData, isSelf: false }); } else { console.log(`[队伍名片] 成员${idx}: ${member.characterID} 转换失败`); } } else { console.log(`[队伍名片] 成员${idx}: ${member.characterID} 未找到profile_shared记录(请先在游戏中打开其资料页)`); list.push({ name: isZH ? '未知成员' : 'Unknown Member', data: { player: { name: isZH ? '未知' : 'Unknown', equipment: [] }, abilities: [], characterSkills: [], houseRooms: {}, characterHouseRoomMap: {} }, isSelf: false }); } } } return list; } // 本地存储键 const TEAM_CARD_STORAGE_KEY = 'mwi_team_card_cache_v1'; function saveTeamCardToStorage(teamName, members) { try { const data = { teamName, members }; localStorage.setItem(TEAM_CARD_STORAGE_KEY, JSON.stringify(data)); console.log('[队伍名片] 已保存队伍名片数据'); } catch (e) { console.warn('保存队伍名片失败', e); } } function loadTeamCardFromStorage() { try { const str = localStorage.getItem(TEAM_CARD_STORAGE_KEY); if (!str) return null; const obj = JSON.parse(str); if (!obj || !Array.isArray(obj.members)) return null; return obj; } catch (e) { return null; } } // 下载队伍名片 async function downloadTeamCharacterCard() { try { const wrapper = document.getElementById('team-character-card'); if (!wrapper) { alert(isZH ? '未找到队伍名片元素' : 'Team card element not found'); return; } const btn = document.querySelector('.download-team-card-btn'); const originalText = btn ? btn.textContent : ''; if (btn) { btn.textContent = isZH ? '生成中...' : 'Generating...'; btn.disabled = true; } // 保持与预览一致的结构,直接克隆容器 const cloned = wrapper.cloneNode(true); const renderRoot = cloned; const spriteContents = {}; const spriteUrls = Object.values(state.svgTool.spriteSheets); const needsChatSprite = renderRoot.querySelector('svg use[href*="chat_icons_sprite"]'); if (needsChatSprite) spriteUrls.push('/static/media/chat_icons_sprite.2a8f0be2.svg'); for (const url of spriteUrls) { const content = await loadSpriteContent(url); if (content) spriteContents[url] = content; } const useElements = renderRoot.querySelectorAll('svg use'); useElements.forEach(useElement => { try { const href = useElement.getAttribute('href'); const svg = useElement.closest('svg'); if (!href || !href.includes('#') || !svg) return; const [spriteUrl, symbolId] = href.split('#'); const spriteContent = spriteContents[spriteUrl]; if (!spriteContent || !symbolId) return; const symbol = spriteContent.querySelector(`#${symbolId}`); if (!symbol) return; const symbolClone = symbol.cloneNode(true); svg.innerHTML = ''; svg.setAttribute('fill', 'none'); const viewBox = symbol.getAttribute('viewBox'); if (viewBox) svg.setAttribute('viewBox', viewBox); while (symbolClone.firstChild) svg.appendChild(symbolClone.firstChild); } catch (e) {} }); const temp = document.createElement('div'); temp.style.position = 'absolute'; temp.style.left = '-9999px'; temp.style.top = '-9999px'; temp.appendChild(renderRoot); document.body.appendChild(temp); const canvas = await html2canvas(renderRoot, { backgroundColor: '#1a1a2e', scale: 2, useCORS: true, logging: false }); document.body.removeChild(temp); const a = document.createElement('a'); a.download = `MWI_Party_Card_${Date.now()}.png`; a.href = canvas.toDataURL('image/png', 1.0); document.body.appendChild(a); a.click(); document.body.removeChild(a); if (btn) { btn.textContent = originalText; btn.disabled = false; } console.log('队伍名片图片已生成并下载'); } catch (e) { console.error('下载队伍名片失败:', e); alert(isZH ? '下载队伍名片失败' : 'Failed to download team card'); const btn = document.querySelector('.download-team-card-btn'); if (btn) { btn.textContent = isZH ? '下载队伍名片' : 'Download Team Card'; btn.disabled = false; } } } // 展示队伍名片 function showPartyCharacterCard(options = {}) { try { const { forceState = false, openEditMode = false } = options; let teamName = getTeamNameFromPage(); console.log(`[队伍名片] 队伍名称: ${teamName}`); let members; if (forceState && state.teamCard.members && state.teamCard.members.length) { members = state.teamCard.members; teamName = state.teamCard.teamName || teamName; } else { const cached = loadTeamCardFromStorage(); if (cached && cached.members && cached.members.length) { teamName = cached.teamName || teamName; members = cached.members; console.log('[队伍名片] 已从缓存加载队伍数据'); } else if (state.teamCard.members && state.teamCard.members.length) { members = state.teamCard.members; teamName = state.teamCard.teamName || teamName; } else { // 最后兜底:如果没有缓存也没有内存状态,才从当前队伍构建 members = buildPartyCharacterDataList(); } } if (!members.length) { alert(isZH ? '未找到队伍成员数据\n\n提示:请先确保处于队伍中,并至少打开过队友的资料页' : 'No party member data found'); return; } state.teamCard.members = members; state.teamCard.teamName = teamName; const cardsHTML = members.map((m, idx) => { const name = m.name || (isZH ? '角色' : 'Character'); const cardHtml = generateCharacterCard(m.data, name, null, false, { teamMode: true }); // 强制纵向布局:将 desktop 替换为 mobile,并缩放以适配队伍并排 const forcedMobile = cardHtml.replace('layout-desktop', 'layout-mobile'); return `<div class="team-card-wrap" data-index="${idx}"><div class="team-mode">${forcedMobile}</div></div>`; }).join(''); const modal = document.createElement('div'); modal.className = 'character-card-modal team-card-modal'; modal.innerHTML = ` <div class="modal-content"> <button class="close-modal">×</button> <div class="instruction-banner">${isZH ? `MWI队伍名片 (该功能目前不支持移动端)` : `MWI Party Cards (This feature is not supported on mobile devices)`}</div> <div class="team-hint">${isZH ? '请先查看队友资料并刷新页面,才能正常使用队伍名片' : 'Please open teammates\' profiles in-game and refresh the page before using Party Cards.'}</div> <div class="download-section"> <div class="button-row"> <button class="refresh-team-card-btn">${isZH ? '重新获取数据' : 'Refresh Data'}</button> <button class="download-team-card-btn">${isZH ? '下载队伍名片' : 'Download Team Card'}</button> <button class="edit-team-card-btn">${isZH ? '编辑名片' : 'Edit Cards'}</button> </div> </div> <div class="team-name">${teamName}</div> <div id="team-character-card" class="team-cards-container">${cardsHTML}</div> </div>`; modal.querySelector('.close-modal').onclick = () => document.body.removeChild(modal); modal.querySelector('.download-team-card-btn').onclick = async () => { try { const refreshBtn = modal.querySelector('.refresh-team-card-btn'); const editBtn = modal.querySelector('.edit-team-card-btn'); if (refreshBtn) refreshBtn.disabled = true; if (editBtn) editBtn.disabled = true; await downloadTeamCharacterCard(); } finally { const refreshBtn = modal.querySelector('.refresh-team-card-btn'); const editBtn = modal.querySelector('.edit-team-card-btn'); if (refreshBtn) refreshBtn.disabled = false; if (editBtn) editBtn.disabled = false; } }; modal.querySelector('.refresh-team-card-btn').onclick = () => { const newMembers = buildPartyCharacterDataList(); if (newMembers && newMembers.length) { state.teamCard.members = newMembers; state.teamCard.teamName = getTeamNameFromPage(); saveTeamCardToStorage(state.teamCard.teamName, newMembers); try { document.body.removeChild(modal); } catch(err) {} showPartyCharacterCard({ forceState: true }); showToastNotice(isZH ? '已重新获取队伍数据' : 'Party data refreshed', 'info'); } else { showToastNotice(isZH ? '未获取到任何队伍数据' : 'No party data fetched', 'error'); } }; modal.querySelector('.edit-team-card-btn').onclick = () => enterTeamEditMode(modal); modal.onclick = (e) => { if (e.target === modal) document.body.removeChild(modal); }; // 监听尺寸变化,动态更新高度,避免窗口尺寸变化导致空白 let resizeTimer; const onResize = () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(adjustTeamCardWrapHeights, 50); }; window.addEventListener('resize', onResize); // 关闭时移除监听 const removeModal = () => { try { window.removeEventListener('resize', onResize); } catch(e) {} try { document.body.removeChild(modal); } catch(e) {} }; modal.querySelector('.close-modal').onclick = removeModal; modal.onclick = (e) => { if (e.target === modal) removeModal(); }; document.body.appendChild(modal); // 修正队伍卡包裹高度,去掉预览底部空白 adjustTeamCardWrapHeights(); if (openEditMode) { enterTeamEditMode(modal); } } catch (e) { console.error('生成队伍名片失败:', e); alert(isZH ? '生成队伍名片失败' : 'Failed to show party card'); } } async function readClipboardData() { try { const text = await navigator.clipboard.readText(); return text; } catch (error) { console.log('无法读取剪贴板:', error); return null; } } function isValidCharacterData(data) { if (!data || typeof data !== 'object') return false; // 检查新格式 (player对象) if (data.player && ( data.player.equipment || data.player.characterItems || data.player.staminaLevel !== undefined || data.player.name )) { return true; } // 检查旧格式 if (data.character && (data.characterSkills || data.characterItems)) { return true; } // 检查是否直接包含关键字段 if (data.equipment || data.characterItems || data.characterSkills) { return true; } // 检查是否包含技能等级字段 if (data.staminaLevel !== undefined || data.intelligenceLevel !== undefined || data.attackLevel !== undefined || data.powerLevel !== undefined) { return true; } // 检查是否包含房屋数据 if (data.houseRooms || data.characterHouseRoomMap) { return true; } // 检查是否包含能力数据 if (data.abilities && Array.isArray(data.abilities)) { return true; } return false; } // 获取SVG sprite内容 async function loadSpriteContent(spriteUrl) { try { const response = await fetch(spriteUrl); const svgText = await response.text(); const parser = new DOMParser(); const svgDoc = parser.parseFromString(svgText, 'image/svg+xml'); return svgDoc.documentElement; } catch (error) { console.warn('无法加载sprite:', spriteUrl, error); return null; } } // 下载名片功能 async function downloadCharacterCard() { try { // 获取名片元素 const cardElement = document.getElementById('character-card'); if (!cardElement) { alert(isZH ? '未找到名片元素' : 'Character card element not found'); return; } // 显示下载提示 const downloadBtn = document.querySelector('.download-card-btn'); const originalText = downloadBtn.textContent; downloadBtn.textContent = isZH ? '生成中...' : 'Generating...'; downloadBtn.disabled = true; // 克隆名片元素用于处理 const clonedCard = cardElement.cloneNode(true); // 确保克隆的元素有正确的布局类名 const currentLayoutMode = getEffectiveLayoutMode(); clonedCard.className = clonedCard.className.replace(/layout-(mobile|desktop)/g, ''); clonedCard.classList.add(`layout-${currentLayoutMode}`); // 如果是场景2(我的角色名片),重新生成技能面板以保持自定义技能状态 const isMyCharacterCard = cardElement.querySelector('.skill-panel .empty-skill-slot') !== null; if (isMyCharacterCard && state.customSkills.selectedSkills.length > 0) { const skillPanel = clonedCard.querySelector('.skill-panel'); if (skillPanel) { const characterData = { abilities: window.characterCardWebSocketData?.characterAbilities || [], characterSkills: window.characterCardWebSocketData?.characterSkills || [] }; const newSkillPanel = generateSkillPanel(characterData, true); skillPanel.innerHTML = newSkillPanel.replace(/<div class="skill-panel">([\s\S]*?)<\/div>$/, '$1'); } } // 预加载所有sprite内容 const spriteContents = {}; const spriteUrls = Object.values(state.svgTool.spriteSheets); // 检查是否需要加载聊天图标sprite const needsChatSprite = clonedCard.querySelector('svg use[href*="chat_icons_sprite"]'); if (needsChatSprite) { spriteUrls.push('/static/media/chat_icons_sprite.2a8f0be2.svg'); } for (const url of spriteUrls) { const content = await loadSpriteContent(url); if (content) { spriteContents[url] = content; } } // 替换所有使用<use>的SVG为实际内容 const useElements = clonedCard.querySelectorAll('svg use'); useElements.forEach((useElement, index) => { try { const href = useElement.getAttribute('href'); const svg = useElement.closest('svg'); if (href && href.includes('#')) { const [spriteUrl, symbolId] = href.split('#'); const spriteContent = spriteContents[spriteUrl]; if (spriteContent && symbolId) { const symbol = spriteContent.querySelector(`#${symbolId}`); if (symbol) { // 创建新的SVG内容 const svg = useElement.closest('svg'); if (svg) { const symbolClone = symbol.cloneNode(true); // 清空原SVG内容并添加symbol内容 svg.innerHTML = ''; // 添加fill="none"属性解决填充问题 svg.setAttribute('fill', 'none'); // 如果symbol有viewBox,应用到svg const viewBox = symbol.getAttribute('viewBox'); if (viewBox) { svg.setAttribute('viewBox', viewBox); } // 复制symbol的所有子元素到svg while (symbolClone.firstChild) { svg.appendChild(symbolClone.firstChild); } } } else { // 如果找不到symbol,创建文字替代 const svg = useElement.closest('svg'); if (svg) { svg.innerHTML = `<text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle" fill="white" font-size="10">${symbolId.substring(0, 3)}</text>`; } } } else { // 如果找不到spriteContent,创建简单替代 const svg = useElement.closest('svg'); if (svg && symbolId) { const shortText = symbolId.length > 2 ? symbolId.substring(0, 2) : symbolId; svg.innerHTML = `<text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle" fill="white" font-size="8">${shortText}</text>`; } } } } catch (error) { console.warn('处理SVG元素时出错:', error); } }); // 简单处理角色名 - 移除上级div的class使其显示为白色 const characterNameDiv = clonedCard.querySelector('.CharacterName_name__1amXp'); if (characterNameDiv) { characterNameDiv.className = ''; // 清除所有class,使角色名显示为白色 } // 检测是否为移动端设备 - 考虑用户的强制布局设置 const finalLayoutMode = getEffectiveLayoutMode(); const isMobileDevice = finalLayoutMode === 'mobile'; // 内联关键样式(避免linear-gradient问题) const styleElement = document.createElement('style'); // 根据有效布局模式选择不同的样式 if (isMobileDevice) { // 移动端样式 - 单列布局 styleElement.textContent = ` .character-card { background: #1a1a2e !important; border: 2px solid #4a90e2 !important; border-radius: 15px !important; padding: 15px !important; color: white !important; font-family: Arial, sans-serif !important; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5) !important; max-width: 100% !important; width: 350px !important; } .card-header { text-align: center !important; margin-bottom: 15px !important; border-bottom: 2px solid #4a90e2 !important; padding-bottom: 8px !important; } .card-content { display: grid !important; grid-template-columns: 1fr !important; grid-template-rows: auto auto auto auto !important; gap: 15px !important; } .equipment-panel { grid-column: 1 !important; grid-row: 1 !important; } .ability-panel { grid-column: 1 !important; grid-row: 2 !important; } .house-panel { grid-column: 1 !important; grid-row: 3 !important; } .skill-panel { grid-column: 1 !important; grid-row: 4 !important; } .equipment-panel, .house-panel, .ability-panel, .skill-panel { background: rgba(255, 255, 255, 0.1) !important; border-radius: 10px !important; padding: 10px !important; margin-bottom: 4px !important; border: 1px solid rgba(74, 144, 226, 0.3) !important; } .panel-title { margin: 0 0 10px 0 !important; color: #4a90e2 !important; font-size: 14px !important; border-bottom: 1px solid rgba(74, 144, 226, 0.3) !important; padding-bottom: 4px !important; text-align: center !important; } .EquipmentPanel_playerModel__3LRB6 { display: grid !important; grid-template-columns: repeat(5, 1fr) !important; grid-template-rows: repeat(4, auto) !important; gap: 6px !important; padding: 8px !important; max-width: 100% !important; margin: 0 auto !important; } /* 技能面板 - 每行4个 */ .ability-panel .AbilitiesPanel_abilityGrid__-p-VF { display: grid !important; grid-template-columns: repeat(4, 1fr) !important; gap: 10px !important; padding: 12px !important; max-height: 180px !important; overflow-y: auto !important; } /* 房屋面板 - 每行4个 */ .house-panel .AbilitiesPanel_abilityGrid__-p-VF { display: grid !important; grid-template-columns: repeat(4, 1fr) !important; gap: 8px !important; padding: 10px !important; max-height: 180px !important; overflow-y: auto !important; } /* 技能卡片样式 */ .ability-panel .Ability_ability__1njrh { margin: 2px !important; min-height: 75px !important; } /* 房屋卡片样式 - 4列布局 */ .house-panel .Ability_ability__1njrh { margin: 1px !important; min-height: 65px !important; font-size: 11px !important; } .level { color: #fff !important; font-weight: bold !important; font-size: 12px !important; } svg { width: 100% !important; height: 100% !important; } `; } else { // 桌面端样式 - 双列布局 styleElement.textContent = ` .character-card { background: #1a1a2e !important; border: 2px solid #4a90e2 !important; border-radius: 15px !important; padding: 20px !important; color: white !important; font-family: Arial, sans-serif !important; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5) !important; min-width: 700px !important; width: auto !important; } .card-header { text-align: center !important; margin-bottom: 20px !important; border-bottom: 2px solid #4a90e2 !important; padding-bottom: 10px !important; } .card-content { display: grid !important; grid-template-columns: 1fr 0.7fr !important; grid-template-rows: auto 1fr !important; gap: 20px !important; } .equipment-panel { grid-column: 1 !important; grid-row: 1 !important; } .ability-panel { grid-column: 2 !important; grid-row: 1 !important; } .house-panel { grid-column: 1 !important; grid-row: 2 !important; } .skill-panel { grid-column: 2 !important; grid-row: 2 !important; } .equipment-panel, .house-panel, .ability-panel, .skill-panel { background: rgba(255, 255, 255, 0.1) !important; border-radius: 10px !important; padding: 15px !important; border: 1px solid rgba(74, 144, 226, 0.3) !important; } .panel-title { margin: 0 0 15px 0 !important; color: #4a90e2 !important; font-size: 16px !important; border-bottom: 1px solid rgba(74, 144, 226, 0.3) !important; padding-bottom: 5px !important; text-align: center !important; } .EquipmentPanel_playerModel__3LRB6 { display: grid !important; grid-template-columns: repeat(5, 1fr) !important; grid-template-rows: repeat(4, auto) !important; gap: 8px !important; padding: 10px !important; max-width: 350px !important; margin: 0 auto !important; } .AbilitiesPanel_abilityGrid__-p-VF { display: grid !important; grid-template-columns: repeat(4, 1fr) !important; gap: 8px !important; padding: 10px !important; max-height: 180px !important; overflow-y: auto !important; } .level { color: #fff !important; font-weight: bold !important; } svg { width: 100% !important; height: 100% !important; } `; } clonedCard.insertBefore(styleElement, clonedCard.firstChild); // 配置尺寸参数(在创建容器之前) // 为PC端布局确保最小宽度,避免在移动设备上展示不全 const minWidth = isMobileDevice ? 350 : 700; // PC端布局至少需要700px宽度 const actualWidth = Math.max(cardElement.offsetWidth, minWidth); // 创建临时容器 const tempContainer = document.createElement('div'); tempContainer.style.position = 'absolute'; tempContainer.style.left = '-9999px'; tempContainer.style.top = '-9999px'; tempContainer.style.width = actualWidth + 'px'; // 确保容器有足够宽度 tempContainer.appendChild(clonedCard); document.body.appendChild(tempContainer); // 确保克隆的名片有足够宽度来完整展示PC端布局 if (!isMobileDevice) { clonedCard.style.width = actualWidth + 'px'; clonedCard.style.minWidth = minWidth + 'px'; } const options = { backgroundColor: '#1a1a2e', // 使用纯色背景代替渐变 scale: isMobileDevice ? 1.5 : 2, // 移动端布局使用较小的缩放比例 useCORS: true, allowTaint: true, foreignObjectRendering: false, width: actualWidth, // 使用计算出的实际宽度 height: isMobileDevice ? undefined : cardElement.offsetHeight, // 移动端布局自动计算高度 logging: false, // 关闭日志减少干扰 onclone: function(clonedDoc) { try { // 在克隆的文档中应用样式修复 const clonedCard = clonedDoc.querySelector('#character-card'); if (clonedCard) { if (isMobileDevice) { // 移动端布局样式修复 clonedCard.style.background = '#1a1a2e'; clonedCard.style.border = '2px solid #4a90e2'; clonedCard.style.borderRadius = '15px'; clonedCard.style.padding = '15px'; clonedCard.style.color = 'white'; clonedCard.style.fontFamily = 'Arial, sans-serif'; clonedCard.style.width = '350px'; clonedCard.style.maxWidth = '100%'; } else { // 桌面端布局样式修复 clonedCard.style.background = '#1a1a2e'; clonedCard.style.border = '2px solid #4a90e2'; clonedCard.style.borderRadius = '15px'; clonedCard.style.padding = '20px'; clonedCard.style.color = 'white'; clonedCard.style.fontFamily = 'Arial, sans-serif'; clonedCard.style.minWidth = minWidth + 'px'; clonedCard.style.width = actualWidth + 'px'; } // 确保所有文本都是白色 const allText = clonedCard.querySelectorAll('*'); allText.forEach(el => { if (el.tagName !== 'SVG' && el.tagName !== 'USE') { el.style.color = 'white'; } }); } } catch (error) { console.warn('处理克隆文档时出错:', error); } } }; // 生成画布 const canvas = await html2canvas(clonedCard, options); // 清理临时容器 document.body.removeChild(tempContainer); // 检查画布是否有内容 const ctx = canvas.getContext('2d'); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; let hasContent = false; // 检查是否有非透明像素 for (let i = 3; i < data.length; i += 4) { if (data[i] > 0) { hasContent = true; break; } } if (!hasContent) { console.warn('主要下载方法生成的图片为空,尝试备用方法...'); // 使用更简单的方法重试 const simpleOptions = { backgroundColor: '#1a1a2e', scale: 1, useCORS: false, allowTaint: true, logging: false, width: cardElement.offsetWidth, height: cardElement.offsetHeight }; const simpleCanvas = await html2canvas(cardElement, simpleOptions); const simpleCtx = simpleCanvas.getContext('2d'); const simpleImageData = simpleCtx.getImageData(0, 0, simpleCanvas.width, simpleCanvas.height); const simpleData = simpleImageData.data; let simpleHasContent = false; // 检查备用方法是否有内容 for (let i = 3; i < simpleData.length; i += 4) { if (simpleData[i] > 0) { simpleHasContent = true; break; } } if (simpleHasContent) { // 备用方法成功,使用备用画布 const link = document.createElement('a'); link.download = `MWI_Character_Card_${new Date().getTime()}.png`; link.href = simpleCanvas.toDataURL('image/png', 1.0); document.body.appendChild(link); link.click(); document.body.removeChild(link); // 清理并恢复按钮状态 document.body.removeChild(tempContainer); downloadBtn.textContent = originalText; downloadBtn.disabled = false; console.log('使用备用方法成功生成名片图片'); return; } else { throw new Error('生成的图片没有内容(主要方法和备用方法都失败)'); } } // 创建下载链接 const link = document.createElement('a'); link.download = `MWI_Character_Card_${new Date().getTime()}.png`; link.href = canvas.toDataURL('image/png', 1.0); // 触发下载 document.body.appendChild(link); link.click(); document.body.removeChild(link); // 恢复按钮状态 downloadBtn.textContent = originalText; downloadBtn.disabled = false; console.log('名片图片已生成并下载'); } catch (error) { console.error('下载名片失败:', error); alert(isZH ? '下载名片失败\n\n错误信息: ' + error.message + '\n\n建议:请确保网络连接正常,并允许浏览器下载文件' : 'Failed to download character card\n\nError: ' + error.message + '\n\nSuggestion: Please ensure network connection and allow browser downloads'); // 恢复按钮状态 const downloadBtn = document.querySelector('.download-card-btn'); if (downloadBtn) { downloadBtn.textContent = isZH ? '下载名片' : 'Download Card'; downloadBtn.disabled = false; } } } // 自动点击导出按钮的辅助函数 async function autoClickExportButton() { try { console.log('尝试自动点击导出按钮...'); // 查找导出按钮的多种可能选择器 const exportButtonSelectors = [ // 中文版本的按钮文本 'button:contains("导出人物到剪贴板")', // 英文版本的按钮文本 'button:contains("Export to clipboard")', ]; let exportButton = null; // 尝试通过按钮文本查找(中文和英文) const allButtons = document.querySelectorAll('button'); for (const button of allButtons) { const buttonText = button.textContent.trim(); if (buttonText.includes('导出人物到剪贴板') || buttonText.includes('Export to clipboard')) { exportButton = button; break; } } // 如果通过文本没找到,尝试其他属性 if (!exportButton) { for (const selector of exportButtonSelectors.slice(4)) { // 跳过contains选择器 try { exportButton = document.querySelector(selector); if (exportButton) { console.log('通过选择器找到导出按钮:', selector); break; } } catch (e) { // 忽略选择器错误 } } } if (!exportButton) { console.log('未找到导出按钮,将直接尝试读取剪贴板'); return false; } // 检查按钮是否可点击 if (exportButton.disabled || exportButton.style.display === 'none') { console.log('导出按钮不可用,将直接尝试读取剪贴板'); return false; } // 点击按钮 exportButton.click(); console.log('已点击导出按钮,等待数据导出...'); // 等待一段时间让数据导出到剪贴板 await new Promise(resolve => setTimeout(resolve, 500)); return true; } catch (error) { console.log('自动点击导出按钮失败:', error); return false; } } // 使用剪贴板数据生成名片(用于查看其他角色) async function showCharacterCard() { try { let characterData = null; let dataSource = isZH ? `剪贴板数据` : `Clipboard Data`; // 先尝试自动点击导出按钮 const autoExportSuccess = await autoClickExportButton(); const clipboardText = await readClipboardData(); if (!clipboardText) { const errorMessage = autoExportSuccess ? (isZH ? '已尝试自动导出,但无法读取剪贴板数据\n\n请确保:\n1. 允许浏览器访问剪贴板\n2. 等待导出完成后重试' : 'Auto export attempted, but cannot read clipboard data\n\nPlease ensure:\n1. Allow browser to access clipboard\n2. Wait for export to complete and retry' ) : (isZH ? '无法读取剪贴板数据\n\n请确保:\n1. 先点击"导出人物到剪贴板"按钮\n2. 允许浏览器访问剪贴板\n3. 剪贴板中有有效的角色数据' : 'Cannot read clipboard data\n\nPlease ensure:\n1. Click "Export to clipboard" button first\n2. Allow browser to access clipboard\n3. Valid character data in clipboard' ); alert(errorMessage); return; } try { characterData = JSON.parse(clipboardText); } catch (error) { alert(isZH ? '剪贴板中的数据不是有效的JSON格式\n\n请确保先点击"导出人物到剪贴板"按钮' : 'Data in clipboard is not valid JSON\n\nPlease ensure you clicked "Export to clipboard" button first'); return; } if (!isValidCharacterData(characterData)) { alert(isZH ? '剪贴板中的数据不包含有效的角色信息\n\n请确保使用MWI Tools的"导出人物到剪贴板"功能' : 'Data in clipboard does not contain valid character information\n\nPlease ensure you use MWI Tools "Export to clipboard" feature'); return; } // 重置调试信息 state.debugInfo.firstSvgPath = null; state.debugInfo.iconCount = 0; const characterName = characterData.player?.name || characterData.character?.name || (isZH ? '角色' : 'Character'); // 查找页面中的角色信息元素 - 获取最后一个(用于查看其他角色) let characterNameElement = null; const characterNameDivs = document.querySelectorAll('.CharacterName_characterName__2FqyZ'); if (characterNameDivs.length > 0) { // 取最后一个元素(用于查看其他角色) const lastCharacterNameDiv = characterNameDivs[characterNameDivs.length - 1]; characterNameElement = lastCharacterNameDiv.outerHTML; } // 缓存剪贴板数据,用于布局切换 state.clipboardCharacterData = { data: characterData, name: characterName, nameElement: characterNameElement }; const modal = document.createElement('div'); modal.className = 'character-card-modal'; modal.innerHTML = ` <div class="modal-content"> <button class="close-modal">×</button> <div class="instruction-banner"> ${isZH ? `MWI角色名片插件 v${VERSION} (数据来源: ${dataSource})` : `MWI Character Card Plugin v${VERSION} (Data Source: ${dataSource})` } </div> <div class="download-section"> <div class="button-row"> <button class="download-card-btn">${isZH ? '下载名片' : 'Download Card'}</button> <button class="layout-toggle-btn">${getLayoutToggleText()}</button> </div> </div> ${generateCharacterCard(characterData, characterName, characterNameElement, false)} </div> `; modal.querySelector('.close-modal').onclick = () => document.body.removeChild(modal); modal.querySelector('.download-card-btn').onclick = downloadCharacterCard; modal.querySelector('.layout-toggle-btn').onclick = toggleLayoutMode; modal.onclick = (e) => { if (e.target === modal) document.body.removeChild(modal); }; // 初始化布局切换按钮显示 updateLayoutToggleButton(); // 初始化模态框容器布局类名 updateModalLayoutClass(); // 添加技能槽点击事件监听器(仅场景2需要) const skillSlots = modal.querySelectorAll('.skill-slot, .empty-skill-slot'); skillSlots.forEach(slot => { slot.addEventListener('click', function() { const skillIndex = parseInt(this.getAttribute('data-skill-index')); showSkillSelector(skillIndex); }); }); document.body.appendChild(modal); } catch (error) { console.error('生成角色名片失败:', error); alert(isZH ? '生成角色名片时发生错误\n\n错误信息: ' + error.message : 'Error occurred while generating character card\n\nError: ' + error.message); } } // 使用WebSocket数据生成名片(用于查看当前角色) async function showMyCharacterCard() { try { // 获取当前角色名 const currentCharacterName = window.characterCardWebSocketData?.characterName || window.characterCardWebSocketData?.name || (isZH ? '角色' : 'Character'); // 检查是否需要重置技能配置(角色切换) const configKey = `mwi_skill_config_${currentCharacterName}`; const savedConfig = localStorage.getItem(configKey); if (savedConfig) { // 有保存的配置,检查是否匹配当前角色 try { const configData = JSON.parse(savedConfig); if (configData.characterName === currentCharacterName) { // 角色匹配,保持现有配置 console.log(`使用保存的技能配置: ${currentCharacterName}`); } else { // 角色不匹配,重置配置 state.customSkills.selectedSkills = []; console.log(`角色切换,重置技能配置: ${currentCharacterName}`); } } catch (error) { // 配置数据错误,重置 state.customSkills.selectedSkills = []; console.log('配置数据错误,重置技能配置'); } } else { // 没有保存的配置,重置 state.customSkills.selectedSkills = []; } let characterData = null; let dataSource = isZH ? `WS数据` : `WebSocket Data`; // 检查是否有WebSocket数据 if (!window.characterCardWebSocketData) { alert(isZH ? '未找到当前角色数据\n\n请确保:\n1. 已登录游戏\n2. 等待游戏数据加载完成\n3. 刷新页面后重试' : 'No current character data found\n\nPlease ensure:\n1. You are logged into the game\n2. Wait for game data to load\n3. Refresh the page and try again'); return; } const parsedData = window.characterCardWebSocketData; if (parsedData && parsedData.type === "init_character_data") { // 将WebSocket数据格式转换为角色名片插件需要的格式 characterData = { player: { name: parsedData.characterName || parsedData.name || (isZH ? '角色' : 'Character'), equipment: parsedData.characterItems || [], characterItems: parsedData.characterItems || [], staminaLevel: parsedData.characterSkills?.find(s => s.skillHrid.includes('/skills/stamina'))?.level || 0, intelligenceLevel: parsedData.characterSkills?.find(s => s.skillHrid.includes('/skills/intelligence'))?.level || 0, attackLevel: parsedData.characterSkills?.find(s => s.skillHrid.includes('/skills/attack'))?.level || 0, powerLevel: parsedData.characterSkills?.find(s => s.skillHrid.includes('/skills/power'))?.level || 0, defenseLevel: parsedData.characterSkills?.find(s => s.skillHrid.includes('/skills/defense'))?.level || 0, rangedLevel: parsedData.characterSkills?.find(s => s.skillHrid.includes('/skills/ranged'))?.level || 0, magicLevel: parsedData.characterSkills?.find(s => s.skillHrid.includes('/skills/magic'))?.level || 0 }, abilities: parsedData.characterAbilities || [], characterSkills: parsedData.characterSkills || [], houseRooms: parsedData.characterHouseRoomMap || {}, characterHouseRoomMap: parsedData.characterHouseRoomMap || {} }; } else { alert(isZH ? 'WebSocket数据格式不正确\n\n请刷新页面后重试' : 'WebSocket data format is incorrect\n\nPlease refresh the page and try again'); return; } if (!isValidCharacterData(characterData)) { alert(isZH ? 'WebSocket数据不包含有效的角色信息\n\n请刷新页面后重试' : 'WebSocket data does not contain valid character information\n\nPlease refresh the page and try again'); return; } // 重置调试信息 state.debugInfo.firstSvgPath = null; state.debugInfo.iconCount = 0; const characterName = characterData.player?.name || characterData.character?.name || (isZH ? '角色' : 'Character'); // 查找页面中的角色信息元素 - 获取第一个(右上角的当前用户) let characterNameElement = null; const characterNameDivs = document.querySelectorAll('.CharacterName_characterName__2FqyZ'); if (characterNameDivs.length > 0) { // 取第一个元素(右上角的当前用户) const firstCharacterNameDiv = characterNameDivs[0]; characterNameElement = firstCharacterNameDiv.outerHTML; } const modal = document.createElement('div'); modal.className = 'character-card-modal'; modal.innerHTML = ` <div class="modal-content"> <button class="close-modal">×</button> <div class="instruction-banner"> ${isZH ? `MWI角色名片插件 v${VERSION} (数据来源: ${dataSource})` : `MWI Character Card Plugin v${VERSION} (Data Source: ${dataSource})` } </div> <div class="download-section"> <div class="button-row"> <button class="download-card-btn">${isZH ? '下载名片' : 'Download Card'}</button> <button class="save-skill-config-btn">${isZH ? '保存技能配置' : 'Save Skill Config'}</button> <button class="load-skill-config-btn">${isZH ? '读取技能配置' : 'Load Skill Config'}</button> <button class="layout-toggle-btn">${getLayoutToggleText()}</button> </div> <div class="skill-hint"> <span>${isZH ? '💡 点击技能图标可更换/添加展示的技能' : '💡 Click skill icons to change/add displayed skills'}</span> </div> </div> ${generateCharacterCard(characterData, characterName, characterNameElement, true)} </div> `; modal.querySelector('.close-modal').onclick = () => document.body.removeChild(modal); modal.querySelector('.download-card-btn').onclick = downloadCharacterCard; modal.querySelector('.layout-toggle-btn').onclick = toggleLayoutMode; modal.onclick = (e) => { if (e.target === modal) document.body.removeChild(modal); }; // 初始化布局切换按钮显示 updateLayoutToggleButton(); // 初始化模态框容器布局类名 updateModalLayoutClass(); // 添加技能配置按钮事件监听器 modal.querySelector('.save-skill-config-btn').onclick = () => { saveSkillConfig(characterName); }; modal.querySelector('.load-skill-config-btn').onclick = () => { loadSkillConfig(characterName); }; // 添加技能槽点击事件监听器 const skillSlots = modal.querySelectorAll('.skill-slot, .empty-skill-slot'); skillSlots.forEach(slot => { slot.addEventListener('click', function() { const skillIndex = parseInt(this.getAttribute('data-skill-index')); showSkillSelector(skillIndex); }); }); document.body.appendChild(modal); } catch (error) { console.error('生成我的角色名片失败:', error); alert(isZH ? '生成我的角色名片时发生错误\n\n错误信息: ' + error.message : 'Error occurred while generating my character card\n\nError: ' + error.message); } } function addCharacterCardButton() { const checkElem = () => { const selectedElement = document.querySelector(`div.SharableProfile_overviewTab__W4dCV`); if (selectedElement) { clearInterval(state.timer); if (selectedElement.querySelector('.character-card-btn')) return; const button = document.createElement("button"); button.className = 'character-card-btn'; button.textContent = isZH ? "查看角色名片" : "View Character Card"; button.style.cssText = ` border-radius: 6px; height: 24px; background-color: #17a2b8; color: white; box-shadow: 0 2px 4px rgba(0,0,0,0.1); border: 0px; margin: 10px auto; display: inline-block; padding: 0 16px; min-width: 140px; max-width: 180px; font-size: 13px; cursor: pointer; transition: all 0.2s ease; `; // 添加hover效果 button.addEventListener('mouseenter', () => { button.style.backgroundColor = '#138496'; button.style.transform = 'translateY(-1px)'; }); button.addEventListener('mouseleave', () => { button.style.backgroundColor = '#17a2b8'; button.style.transform = 'translateY(0)'; }); button.onclick = () => { showCharacterCard(); return false; }; // 创建按钮容器并居中 const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = 'text-align: center; margin-top: 10px;'; buttonContainer.appendChild(button); // 插入按钮容器 selectedElement.appendChild(buttonContainer); console.log('角色名片按钮已添加'); return false; } }; state.timer = setInterval(checkElem, 1000); } // 在右上角角色信息区域添加"我的角色名片"按钮 function addMyCharacterCardButton() { const checkMyButton = () => { const headerNameElements = document.querySelectorAll('.Header_name__227rJ'); if (headerNameElements.length > 0) { // 找到右上角的角色信息容器 const headerNameElement = headerNameElements[0]; // 检查是否已经添加过按钮 if (headerNameElement.querySelector('.my-character-card-btn')) { return; } // 创建按钮 const myButton = document.createElement("button"); myButton.className = 'my-character-card-btn'; myButton.textContent = isZH ? "我的角色名片" : "My Character Card"; myButton.style.cssText = ` border-radius: 4px; height: 20px; background-color: #28a745; color: white; box-shadow: 0 1px 3px rgba(0,0,0,0.2); border: 0px; margin-left: 8px; display: inline-block; padding: 0 8px; font-size: 11px; cursor: pointer; transition: all 0.2s ease; vertical-align: middle; `; // 添加hover效果 myButton.addEventListener('mouseenter', () => { myButton.style.backgroundColor = '#218838'; myButton.style.transform = 'translateY(-1px)'; }); myButton.addEventListener('mouseleave', () => { myButton.style.backgroundColor = '#28a745'; myButton.style.transform = 'translateY(0)'; }); myButton.onclick = () => { showMyCharacterCard(); return false; }; // 将按钮插入到Header_name容器中 headerNameElement.appendChild(myButton); console.log('我的角色名片按钮已添加到右上角'); return false; } }; // 使用定时器检查并添加按钮 const myButtonTimer = setInterval(checkMyButton, 1000); // 清理定时器(当按钮添加成功后) setTimeout(() => { clearInterval(myButtonTimer); }, 10000); // 10秒后停止检查 } // 在队伍信息区域添加“查看队伍名片”按钮 function addPartyCardButton() { const checkParty = () => { const optionsEl = document.querySelector('.Party_partyOptions__3HGXK'); if (!optionsEl) return; if (optionsEl.querySelector('.party-card-btn')) return; const btn = document.createElement('button'); btn.className = 'party-card-btn'; btn.textContent = isZH ? '查看队伍名片(仅限桌面端)' : 'View Party Cards (Desktop Only)'; btn.style.cssText = ` border-radius: 4px; height: 20px; background-color: #28a745; color: white; box-shadow: 0 1px 3px rgba(0,0,0,0.2); border: 0px; margin-left: 8px; display: inline-block; padding: 0 8px; font-size: 14px; cursor: pointer; transition: all 0.2s ease; vertical-align: middle; `; btn.addEventListener('mouseenter', () => { btn.style.backgroundColor = '#218838'; btn.style.transform = 'translateY(-1px)'; }); btn.addEventListener('mouseleave', () => { btn.style.backgroundColor = '#28a745'; btn.style.transform = 'translateY(0)'; }); btn.onclick = () => { showPartyCharacterCard(); return false; }; // 包装成 div 与其他 div 同级显示 const wrapperDiv = document.createElement('div'); wrapperDiv.style.display = 'inline-block'; wrapperDiv.appendChild(btn); optionsEl.appendChild(wrapperDiv); console.log('队伍名片按钮已添加'); }; // 初次尝试与后续监听 const timer = setInterval(() => { if (document.querySelector('.Party_partyOptions__3HGXK')) { clearInterval(timer); checkParty(); } }, 1000); // DOM 变化时重试插入 const partyObserver = new MutationObserver(() => checkParty()); partyObserver.observe(document.body, { childList: true, subtree: true }); } async function init() { console.log(`MWI角色名片插件 v${VERSION}`); console.log('使用说明:'); console.log('1. 在角色信息界面点击"查看角色名片"按钮 - 使用剪贴板数据'); console.log('2. 在右上角点击"我的角色名片"按钮 - 使用WebSocket数据'); createModalStyles(); createTeamStyles(); const spritesLoaded = await state.svgTool.loadSpriteSheets(); console.log(`图标系统初始化${spritesLoaded ? '成功' : '失败'},将使用${spritesLoaded ? 'MWI原版SVG图标' : '后备图标显示'}`); if (spritesLoaded) { console.log('SVG Sprite文件:', state.svgTool.spriteSheets); } // 设置WebSocket Hook hookWebSocket(); // 监听角色数据可用事件 window.addEventListener('characterDataAvailable', function(event) { // 静默处理事件 }); addCharacterCardButton(); addMyCharacterCardButton(); addPartyCardButton(); // 创建一个MutationObserver来监听body的子节点变化 state.observer = new MutationObserver((mutationsList, observer) => { for(const mutation of mutationsList) { if (mutation.type === 'childList') { // 检查是否是SharableProfile_overviewTab__W4dCV的子节点变化 if (mutation.target.classList.contains('SharableProfile_overviewTab__W4dCV')) { // 延迟执行,确保DOM更新完成 setTimeout(addCharacterCardButton, 100); } } } }); state.observer.observe(document.body, { childList: true, subtree: true }); } // 清理函数 function cleanup() { if (state.observer) { state.observer.disconnect(); state.observer = null; } if (state.timer) { clearInterval(state.timer); state.timer = null; } } // 暴露数据给其他脚本的函数 function exposeDataToOtherScripts() { // 创建一个全局函数,让MWI Tools可以调用 window.exposeMWIToolsData = function(data) { window.mwiToolsData = data; }; // 监听来自MWI Tools的消息 window.addEventListener('message', function(event) { if (event.source === window && event.data && event.data.type === 'MWI_TOOLS_DATA') { window.mwiToolsData = event.data.data; } }); // 监听localStorage变化 window.addEventListener('storage', function(event) { if (event.key === 'init_character_data' && event.newValue) { try { const data = JSON.parse(event.newValue); window.mwiToolsData = data; } catch (error) { // 静默处理错误 } } }); } // WebSocket Hook函数 - 参考MWI Tools的实现 function hookWebSocket() { // 检查是否已经hook过 if (window.characterCardWebSocketHooked) { return; } try { // 获取MessageEvent.prototype.data的属性描述符 const dataProperty = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data"); if (!dataProperty) { return; } const oriGet = dataProperty.get; // 重写getter dataProperty.get = function() { const socket = this.currentTarget; // 检查是否是WebSocket连接 if (!(socket instanceof WebSocket)) { return oriGet.call(this); } // 检查是否是MWI的WebSocket连接 if (socket.url.indexOf("api.milkywayidle.com/ws") <= -1 && socket.url.indexOf("api-test.milkywayidle.com/ws") <= -1) { return oriGet.call(this); } // 获取原始消息 const message = oriGet.call(this); // 防止循环调用 Object.defineProperty(this, "data", { value: message }); // 处理消息 handleWebSocketMessage(message); return message; }; // 重新定义属性 Object.defineProperty(MessageEvent.prototype, "data", dataProperty); // 标记已hook window.characterCardWebSocketHooked = true; } catch (error) { // 静默处理错误 } } // 处理WebSocket消息 function handleWebSocketMessage(message) { try { const obj = JSON.parse(message); // 处理角色数据 if (obj && obj.type === "init_character_data") { console.log('=== 检测到角色数据 ==='); console.log('角色名称:', obj.characterName); console.log('装备数量:', obj.characterItems?.length || 0); console.log('技能数量:', obj.characterSkills?.length || 0); console.log('能力数量:', obj.characterAbilities?.length || 0); console.log('房屋数据:', obj.characterHouseRoomMap); console.log('完整数据:', obj); // 存储到全局变量 window.mwiToolsData = obj; window.characterCardWebSocketData = obj; // 存储到localStorage try { localStorage.setItem('init_character_data', message); console.log('已存储到localStorage'); } catch (error) { console.log('localStorage存储失败:', error); } // 触发数据可用事件 window.dispatchEvent(new CustomEvent('characterDataAvailable', { detail: obj })); console.log('=== 角色数据处理完成 ==='); } else if (obj && obj.type === 'profile_shared') { // 存储队友 profile_shared 以便队伍名片使用 try { let listStr = localStorage.getItem('profile_export_list'); let list = []; try { list = listStr ? JSON.parse(listStr) : []; } catch {} obj.characterID = obj.profile?.characterSkills?.[0]?.characterID; obj.characterName = obj.profile?.sharableCharacter?.name; obj.timestamp = Date.now(); list = (list || []).filter(it => it.characterID !== obj.characterID); list.unshift(obj); if (list.length > 20) list.pop(); localStorage.setItem('profile_export_list', JSON.stringify(list)); console.log('[队伍名片] 已保存队友资料 profile_shared: ', obj.characterName); } catch (e) { // 静默 } } else if (obj && obj.type === 'new_battle') { // 可用于后续扩展(例如消耗品等) try { localStorage.setItem('new_battle', message); } catch {} } } catch (error) { // 静默处理解析错误,不打印日志 } } // 在脚本卸载时清理 window.addEventListener('unload', cleanup); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } // 初始化数据暴露机制 exposeDataToOtherScripts(); // 保存技能配置函数 function saveSkillConfig(characterName) { try { const configKey = `mwi_skill_config_${characterName}`; // 只保存技能ID和位置,不保存等级 const simplifiedSkills = state.customSkills.selectedSkills.map((skill, index) => ({ abilityHrid: skill.abilityHrid, position: index })); const configData = { characterName: characterName, selectedSkills: simplifiedSkills, timestamp: Date.now() }; localStorage.setItem(configKey, JSON.stringify(configData)); // 显示成功提示 const saveBtn = document.querySelector('.save-skill-config-btn'); const originalText = saveBtn.textContent; saveBtn.textContent = isZH ? '保存成功!' : 'Saved!'; saveBtn.style.backgroundColor = '#28a745'; setTimeout(() => { saveBtn.textContent = originalText; saveBtn.style.backgroundColor = '#17a2b8'; }, 2000); console.log(`技能配置已保存: ${characterName}`); } catch (error) { console.error('保存技能配置失败:', error); alert(isZH ? '保存技能配置失败' : 'Failed to save skill config'); } } // 读取技能配置函数 function loadSkillConfig(characterName) { try { const configKey = `mwi_skill_config_${characterName}`; const savedConfig = localStorage.getItem(configKey); if (!savedConfig) { alert(isZH ? `未找到角色 "${characterName}" 的技能配置\n\n请先保存技能配置` : `No skill config found for character "${characterName}"\n\nPlease save skill config first`); return false; } const configData = JSON.parse(savedConfig); // 验证配置数据 if (!configData.selectedSkills || !Array.isArray(configData.selectedSkills)) { alert(isZH ? '技能配置数据格式错误' : 'Invalid skill config data format'); return false; } // 从WebSocket数据获取最新技能信息并应用配置 const allSkills = window.characterCardWebSocketData?.characterAbilities || []; const restoredSkills = []; configData.selectedSkills.forEach(savedSkill => { if (savedSkill.abilityHrid) { // 从WebSocket数据中找到对应的技能 const currentSkill = allSkills.find(skill => skill.abilityHrid === savedSkill.abilityHrid ); if (currentSkill) { // 使用最新的等级信息 restoredSkills[savedSkill.position] = { abilityHrid: currentSkill.abilityHrid, level: currentSkill.level, slotNumber: currentSkill.slotNumber }; } } }); // 应用恢复的技能配置 state.customSkills.selectedSkills = restoredSkills; // 重新生成技能面板 const characterCard = document.querySelector('#character-card'); if (characterCard) { const skillPanel = characterCard.querySelector('.skill-panel'); if (skillPanel) { const characterData = { abilities: window.characterCardWebSocketData?.characterAbilities || [], characterSkills: window.characterCardWebSocketData?.characterSkills || [] }; const newSkillPanel = generateSkillPanel(characterData, true); skillPanel.innerHTML = newSkillPanel.replace(/<div class="skill-panel">([\s\S]*?)<\/div>$/, '$1'); // 重新添加事件监听器 const skillSlots = skillPanel.querySelectorAll('.skill-slot, .empty-skill-slot'); skillSlots.forEach(slot => { slot.addEventListener('click', function() { const skillIndex = parseInt(this.getAttribute('data-skill-index')); showSkillSelector(skillIndex); }); }); } } // 显示成功提示 const loadBtn = document.querySelector('.load-skill-config-btn'); const originalText = loadBtn.textContent; loadBtn.textContent = isZH ? '读取成功!' : 'Loaded!'; loadBtn.style.backgroundColor = '#28a745'; setTimeout(() => { loadBtn.textContent = originalText; loadBtn.style.backgroundColor = '#17a2b8'; }, 2000); console.log(`技能配置已读取: ${characterName}`); return true; } catch (error) { console.error('读取技能配置失败:', error); alert(isZH ? '读取技能配置失败' : 'Failed to load skill config'); return false; } } // 将函数暴露到全局作用域 if (typeof window !== 'undefined') { window.showSkillSelector = showSkillSelector; window.selectSkill = selectSkill; } })(); // 结束立即执行函数 })();