MWI角色名片插件 - 一键生成角色名片
当前为
// ==UserScript==
// @name MWI角色名片插件
// @namespace http://tampermonkey.net/
// @version 1.3.1
// @license MIT
// @description MWI角色名片插件 - 一键生成角色名片
// @author Windoge
// @match https://www.milkywayidle.com/*
// @grant GM_getValue
// @grant GM_setValue
// @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 isZH = navigator.language.includes('zh');
// 检测移动端
function isMobile() {
return window.innerWidth <= 768 || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
// 简化的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 modal = document.querySelector('.skill-selector-modal');
if (modal) {
document.body.removeChild(modal);
}
}
// 全局版本号
const VERSION = '1.3.1';
// 使用闭包管理状态,避免全局变量
const state = {
svgTool: new CharacterCardSVGTool(),
debugInfo: {
firstSvgPath: null,
iconCount: 0
},
observer: null,
timer: null,
isInitialized: false,
// 用户自定义技能展示状态
customSkills: {
selectedSkills: [], // 用户选择的技能列表
maxSkills: 8 // 最大技能数量
}
};
// 简化的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 ? '装备' : 'Equipment'}</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 ? '属性等级' : 'Abilities'}</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) {
let abilities = data.abilities || data.characterSkills || [];
let combatSkills;
if (isMyCharacter) {
// 场景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 ? '技能等级' : 'Skills'}</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/"));
let html = '<div class="skill-panel">';
html += `<div class="panel-title">${isZH ? '技能等级' : 'Skills'}</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) {
let characterObj = data.player || data;
const equipmentPanel = generateEquipmentPanel(characterObj);
// 创建标题栏内容
let headerContent = '';
if (characterNameElement) {
// 使用从页面复制的角色信息元素
headerContent = characterNameElement;
} else {
// 后备方案:使用简单的角色名
headerContent = `<h2>${characterName}</h2>`;
}
return `
<div id="character-card" class="character-card">
<div class="card-header">${headerContent}</div>
<div class="card-content">
${equipmentPanel}
${generateAbilityPanel(characterObj)}
${generateSkillPanel(data, isMyCharacter)}
${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;
}
.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; grid-template-columns: 1fr 0.7fr; grid-template-rows: auto 1fr; gap: 20px;
}
.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;
}
.equipment-panel { grid-column: 1; grid-row: 1; }
/* 只为模态框内的装备面板添加网格布局,不影响游戏原生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;
}
/* 移动端单列布局 */
.card-content {
grid-template-columns: 1fr;
grid-template-rows: auto auto auto auto;
gap: 12px;
}
.equipment-panel { grid-column: 1; grid-row: 1; }
.ability-panel { grid-column: 1; grid-row: 2; }
.house-panel { grid-column: 1; grid-row: 3; }
.skill-panel { grid-column: 1; grid-row: 4; }
/* 移动端面板样式调整 */
.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 {
background: #28a745; color: white; border: none; padding: 8px 16px;
border-radius: 5px; font-size: 14px; font-weight: bold; cursor: pointer;
transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.download-card-btn:hover:not(:disabled) {
background: #218838; transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.download-card-btn:disabled {
background: #6c757d; cursor: not-allowed; transform: none;
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);
}
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);
// 如果是场景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 isMobileDevice = isMobile();
// 内联关键样式(避免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;
}
.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);
// 创建临时容器
const tempContainer = document.createElement('div');
tempContainer.style.position = 'absolute';
tempContainer.style.left = '-9999px';
tempContainer.style.top = '-9999px';
tempContainer.appendChild(clonedCard);
document.body.appendChild(tempContainer);
// 配置html2canvas选项(根据设备类型调整)
const options = {
backgroundColor: '#1a1a2e', // 使用纯色背景代替渐变
scale: isMobileDevice ? 1.5 : 2, // 移动端使用较小的缩放比例
useCORS: true,
allowTaint: true,
foreignObjectRendering: false,
width: isMobileDevice ? 350 : cardElement.offsetWidth, // 移动端使用固定宽度
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';
}
// 确保所有文本都是白色
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 ? '下载名片(Beta)' : 'Download Card(Beta)';
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 = "剪贴板数据";
// 先尝试自动点击导出按钮
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;
}
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">
<button class="download-card-btn">${isZH ? '下载名片(Beta)' : 'Download Card(Beta)'}</button>
</div>
${generateCharacterCard(characterData, characterName, characterNameElement, false)}
</div>
`;
modal.querySelector('.close-modal').onclick = () => document.body.removeChild(modal);
modal.querySelector('.download-card-btn').onclick = downloadCharacterCard;
modal.onclick = (e) => { if (e.target === modal) document.body.removeChild(modal); };
// 添加技能槽点击事件监听器(仅场景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 = "WS数据";
// 检查是否有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 ? '下载名片(Beta)' : 'Download Card(Beta)'}</button>
<button class="save-skill-config-btn">${isZH ? '保存技能配置' : 'Save Skill Config'}</button>
<button class="load-skill-config-btn">${isZH ? '读取技能配置' : 'Load Skill Config'}</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.onclick = (e) => { if (e.target === modal) document.body.removeChild(modal); };
// 添加技能配置按钮事件监听器
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秒后停止检查
}
async function init() {
console.log(`MWI角色名片插件 v${VERSION}`);
console.log('使用说明:');
console.log('1. 在角色信息界面点击"查看角色名片"按钮 - 使用剪贴板数据');
console.log('2. 在右上角点击"我的角色名片"按钮 - 使用WebSocket数据');
console.log('3. 下载选项:');
console.log(' • "下载名片(Beta)" - 自动转换为PNG图片下载');
console.log('4. 调试工具:');
console.log(' • 安装 websocket_debug.js 查看WebSocket通信');
console.log(' • 安装 test_data_sharing.js 测试数据共享');
createModalStyles();
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();
// 创建一个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('=== 角色数据处理完成 ===');
}
} 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;
}
})(); // 结束立即执行函数
})();