// ==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;
}
})(); // 结束立即执行函数
})();