// ==UserScript==
// @name DarkGold Rating
// @namespace http://tampermonkey.net/
// @version 1.6.6
// @description 自动识别暗金装备流派、计算评分,支持面板缩放和部位动态关键词匹配。
// @author Lunaris
// @match https://aring.cc/awakening-of-war-soul-ol/
// @icon https://aring.cc/awakening-of-war-soul-ol/favicon.ico
// @grant none
// @license MIT
// ==/UserScript==
// =======================================================================
// 【用户自定义设置】
// =======================================================================
/**
* ★ 面板缩放比例系数 (Scale Factor)
* 默认值 1.0 为原始大小。
* 设为 0.5 则面板和字体大小变为原来的 50%。
* 缩小化的面板大小不会受此系数影响。
*/
const SCALE_FACTOR = 0.7;
// =======================================================================
// 【计分说明】
// - 词条命中流派推荐关键词: 满分三分
// - 词条未命中流派推荐关键词时,仅得0.5倍分数
// 例:72.4% 未命中 => 0.724 × 3 × 0.5 = 1.09
// - 非流派核心属性:参与计分,按 0.5 × 完美度。
//
// ★ 新增:用户通过设置面板可自定义以下比例(默认值如下):
// =======================================================================
// 非流派核心属性得分基础权重(占词条基础权重的比例)
let NON_CORE_PROPS_WEIGHT = 0.5;
// 词条未命中流派关键词时的得分惩罚比例(占满分的比例)
let TRAIT_MISMATCH_PENALTY = 0.5;
(function() {
'use strict';
// 全局变量和状态
let analysisPanel = null;
let settingsPanel = null; // 新增:设置面板
let isDragging = false;
let dragOffsetX, dragOffsetY;
// 记录用户是否主动关闭了面板。默认为 false (未关闭)。
let isUserClosed = false;
// 记录上一次触发评分的装备 DOM 元素
let lastEquipWrap = null;
let lastEquipSignature = null;
// 1. 加载和保存设置的工具函数
function loadSettings() {
try {
const storedSettings = localStorage.getItem('darkGoldRating_settings');
if (storedSettings) {
const settings = JSON.parse(storedSettings);
// 确保加载的值是数字且在合理范围
if (typeof settings.NON_CORE_PROPS_WEIGHT === 'number' && settings.NON_CORE_PROPS_WEIGHT >= 0 && settings.NON_CORE_PROPS_WEIGHT <= 1) {
NON_CORE_PROPS_WEIGHT = settings.NON_CORE_PROPS_WEIGHT;
}
if (typeof settings.TRAIT_MISMATCH_PENALTY === 'number' && settings.TRAIT_MISMATCH_PENALTY >= 0 && settings.TRAIT_MISMATCH_PENALTY <= 1) {
TRAIT_MISMATCH_PENALTY = settings.TRAIT_MISMATCH_PENALTY;
}
}
} catch (e) {
console.error('加载设置失败:', e);
}
}
function saveSettings(coreWeight, penalty) {
try {
NON_CORE_PROPS_WEIGHT = coreWeight;
TRAIT_MISMATCH_PENALTY = penalty;
const settings = {
NON_CORE_PROPS_WEIGHT: coreWeight,
TRAIT_MISMATCH_PENALTY: penalty
};
localStorage.setItem('darkGoldRating_settings', JSON.stringify(settings));
console.log('设置已保存:', settings);
} catch (e) {
console.error('保存设置失败:', e);
}
}
// 在脚本启动时立即加载设置
loadSettings();
// =======================================================================
// 1. 流派数据、最大值与通用属性定义 (Stream/Build Data Definition)
// =======================================================================
/**
* 流派数据定义。
* ★ 部位关键词字段统一为 'keywords'。
*/
const BUILD_DATA = {
// 分裂流派
'分裂-追击': {
core: '攻击速度、破防、命中',
keywords: {
'武器': '分裂, 轻灵',
'头盔': '追击, 碎骨或影刃',
'衣服': '分裂, 追击',
'鞋子': '分裂, 追击',
'戒指': '分裂, 追击',
},
priority: 3
},
'分裂-裂创': {
core: '暴击率、攻击速度、命中',
keywords: {
'武器': '分裂, 裂创',
'头盔': '裂创, 击溃',
'衣服': '分裂, 轻灵, 重创',
'鞋子': '分裂, 裂创',
'戒指': '分裂, 裂创',
},
priority: 3
},
'分裂-重创': {
core: '暴击率、破防、攻击速度、命中',
keywords: {
'武器': '分裂, 残忍',
'头盔': '重创, 残忍/击溃',
'衣服': '分裂, 重创',
'鞋子': '分裂, 重创',
'戒指': '分裂, 重创',
},
priority: 2
},
// 破阵流派
'破阵-荣耀': { // 对应 "菊花荣耀"
core: '暴击率、暴击伤害、破防、攻击速度',
keywords: {
'武器': '破阵',
'头盔': '破阵, 爆发',
'衣服': '破阵, 轻灵',
'鞋子': '轻灵, 爆发',
'戒指': '轻灵',
},
priority: 2
},
'破阵-冲锋/收割': {
core: '暴击率、暴击伤害、破防、攻击速度、攻击',
keywords: {
'武器': '破阵, 冲锋或收割',
'头盔': '破阵, 冲锋或收割',
'衣服': '破阵, 冲锋或收割',
'鞋子': '轻灵, 收尾',
'戒指': '轻灵, 收尾',
},
priority: 1
},
// 特殊装备流派 (菊花)
'菊花-命运': {
core: '暴击率、暴击伤害、攻击速度、破防',
keywords: {
'武器': '破阵',
'头盔': '破阵, 冲锋或收割',
'衣服': '破阵, 轻灵',
'鞋子': '轻灵, 冲击',
'戒指': '轻灵',
},
priority: 4
},
'命运-冲锋': {
core: '暴击率、暴击伤害、破防、攻击速度、攻击',
keywords: {
'武器': '破阵',
'头盔': '破阵, 冲锋',
'衣服': '破阵, 冲锋',
'鞋子': '轻灵, 冲击百分比',
'戒指': '轻灵',
},
priority: 4
},
};
const EQUIP_ICONS = {
'武器': '🔪',
'头盔': '🧢',
'衣服': '🥋',
'鞋子': '🥾',
'指环': '💍',
'戒指': '💍',
'符': '💍', // 符使用戒指的逻辑
};
// --- 特殊装备名称列表 (这些装备通常没有词条一,只有刻印词条) ---
const SPECIAL_EQUIP_NAMES = ['秘 · 菊一文字', '命运', '荣耀'];
// --- 各种暗金属性的理论最大值 (Max Roll) ---
const MAX_ROLLS = {
'破防': 25.5,
'暴击率': 8.5,
'全伤害加成': 4.5,
'暴击伤害': 25.5,
'攻击速度': 8.5,
'命中率': 8.5,
'攻击': 17,
};
// --- 核心属性最大分数定义 ---
const MAX_CORE_SCORE = 4.0;
// =======================================================================
// 2. 评分逻辑 (Scoring Logic)
// =======================================================================
/**
* 计算核心属性评分 (Core Props Score)
*/
function calculateCorePropsScore(equipData, buildInfo) {
const EXCLUDED_PROPS = [];
// ★ 使用全局变量
const NON_CORE_BASE = NON_CORE_PROPS_WEIGHT;
const corePropNames = String(buildInfo.core || '')
.split('、')
.map(s => s.trim())
.filter(Boolean);
const FORCED_CORE_PROPS = new Set(['全伤害加成']);
const isCoreProp = (name) => corePropNames.includes(name) || FORCED_CORE_PROPS.has(name);
if (!equipData.extraProps || equipData.extraProps.length === 0) {
return {
score: 0,
coreBaseScore: 0,
quality: 0,
ignoredProps: [],
bonusScore: 0,
combinedProps: []
};
}
const combinedPropsList = [];
(equipData.extraProps || []).forEach((prop, idx) => {
combinedPropsList.push({
// 记录一个顺序 id,方便调试
id: `extra#${idx + 1}`,
name: prop.name,
value: prop.value,
baseValue: prop.value,
enhancedValue: 0,
isEnhanced: false,
maxRoll: MAX_ROLLS[prop.name]
});
});
// 2) 处理强化属性:按“同名→就近未加成的一条”匹配一次;没匹配到则单独入列
(equipData.enhancedProps || []).forEach((prop, eidx) => {
const target = combinedPropsList.find(p => p.name === prop.name && !p.isEnhanced);
if (target) {
target.value += prop.value;
target.enhancedValue += prop.value;
target.isEnhanced = true;
} else {
combinedPropsList.push({
id: `enhanced#${eidx + 1}`,
name: prop.name,
value: prop.value,
baseValue: 0,
enhancedValue: prop.value,
isEnhanced: true,
maxRoll: MAX_ROLLS[prop.name]
});
}
});
// 3) 词条权重按“实际词条条数”来均分(包含同名的多条)
const totalPropsCount = combinedPropsList.length || 1;
const maxCoreScore = MAX_CORE_SCORE;
const propBaseWeight = maxCoreScore / totalPropsCount;
// 4) 遍历评分:不再遍历 Map,而是遍历数组
let score = 0;
let coreBaseScore = 0;
let bonusScore = 0;
let matchingPropsCount = 0;
let totalCoreRoll = 0;
const ignoredProps = [];
for (const prop of combinedPropsList) {
const propName = prop.name;
const maxRoll = prop.maxRoll;
prop.isCore = false;
prop.isIgnored = false;
prop.isOverRoll = false;
if (EXCLUDED_PROPS.includes(propName)) {
prop.isIgnored = true;
ignoredProps.push({ name: propName, reason: '基础属性,已排除评分' });
continue;
}
if (!maxRoll) {
prop.isIgnored = true;
ignoredProps.push({ name: propName, reason: '未定义最大值' });
continue;
}
const roll = Math.min(Math.max(prop.value / maxRoll, 0), 1);
if (isCoreProp(propName)) {
const add = propBaseWeight * roll;
score += add;
coreBaseScore += add;
totalCoreRoll += roll;
matchingPropsCount++;
if (prop.value / maxRoll > 1.0) {
const over = propBaseWeight * 0.5;
bonusScore += over;
score += over;
prop.isOverRoll = true;
}
prop.isCore = true;
} else {
const add = NON_CORE_BASE * roll * propBaseWeight;
score += add;
coreBaseScore += add;
}
}
const coreRollQuality = matchingPropsCount > 0
? (totalCoreRoll / matchingPropsCount)
: 0;
return {
score,
coreBaseScore,
quality: coreRollQuality,
ignoredProps,
bonusScore,
combinedProps: combinedPropsList
};
}
/**
* 计算装备评分 (10分制)
*/
function calculateScore(equipData, buildInfo) {
const TRAIT_MATCH_MAX = 3.0; // 命中流派关键词
// ★ 使用全局变量
const MISMATCH_PENALTY = TRAIT_MISMATCH_PENALTY;
const coreScoreResult = calculateCorePropsScore(equipData, buildInfo);
let score = coreScoreResult.score;
// buildInfo.primaryKeywords 此时已经是动态生成的 effectiveKeywords
const keywords = Array.isArray(buildInfo.primaryKeywords) ? buildInfo.primaryKeywords : [];
const traitScoreAndExplain = (traitObj, isSpecialTraitOne = false) => {
if (!traitObj || !traitObj.name) {
if (isSpecialTraitOne && SPECIAL_EQUIP_NAMES.includes(equipData.name)) {
return { v: TRAIT_MATCH_MAX, exp: `特殊装备无第一词条:默认满分 ${TRAIT_MATCH_MAX}` };
}
return { v: 0, exp: '无' };
}
const prob = Number(traitObj.probability || 0) / 100;
if (isSpecialTraitOne && SPECIAL_EQUIP_NAMES.includes(equipData.name)) {
return { v: TRAIT_MATCH_MAX, exp: `特殊装备:固定满分 ${TRAIT_MATCH_MAX}` };
}
// ★ 新增 mandatory 校验逻辑
const isGrouped = !!(buildInfo && buildInfo.isGrouped && buildInfo.groupMeta);
const mandatoryOk = !isGrouped
|| [equipData.traitOne?.name, equipData.traitTwo?.name].includes(buildInfo.groupMeta.mandatory);
const isMatch = keywords.includes(traitObj.name) && mandatoryOk;
if (isMatch) {
const v = prob * TRAIT_MATCH_MAX;
return {
v,
exp: `${(prob * 100).toFixed(1)}% × ${TRAIT_MATCH_MAX}(命中部位关键词) = ${v.toFixed(2)}`
};
} else {
const base = TRAIT_MATCH_MAX * MISMATCH_PENALTY;
const v = prob * base;
return {
v,
exp: `${(prob * 100).toFixed(1)}% × ${TRAIT_MATCH_MAX} × ${MISMATCH_PENALTY.toFixed(2)}(未命中部位关键词) = ${v.toFixed(2)}`
};
}
};
const t1 = traitScoreAndExplain(equipData.traitOne, true);
const t2 = traitScoreAndExplain(equipData.traitTwo, false);
score += t1.v + t2.v;
return {
totalScore: score.toFixed(2),
coreBaseScore: coreScoreResult.coreBaseScore.toFixed(2),
coreBonusScore: coreScoreResult.bonusScore.toFixed(2),
coreQuality: (coreScoreResult.quality * 100).toFixed(1),
traitOneScore: t1.v.toFixed(2),
traitTwoScore: t2.v.toFixed(2),
traitOneExplain: t1.exp,
traitTwoExplain: t2.exp,
coreIgnoredProps: coreScoreResult.ignoredProps,
combinedProps: coreScoreResult.combinedProps
};
}
// =======================================================================
// 3. DOM 操作和数据提取 (更精确的属性提取)
// =======================================================================
// ... (parseEquipData, extractProperty, quickEquipSignature, identifyBuilds 保持不变) ...
/**
* 辅助函数:从 P 标签中提取属性名称和数值
*/
function extractProperty(p, isPercentage = false) {
const textContent = p.textContent.trim();
const property = { name: '', value: 0 };
let match;
// 尝试匹配中文名称 + 数值 + % (如果存在)
if (isPercentage) {
match = textContent.match(/([\u4e00-\u9fa5]+)\s*[+\-\*]*\s*([\d\.]+)\s*\%/);
} else {
match = textContent.match(/([\u4e00-\u9fa5]+)\s*[+\-\*]*\s*([\d\.]+)/);
}
if (match) {
property.name = match[1].trim();
property.value = parseFloat(match[2]);
return property;
}
// 尝试匹配 span 结构
const nameSpan = p.querySelector('span:first-child:not(.special)');
const valueSpan = p.querySelector('.grow');
if (nameSpan && valueSpan) {
property.name = nameSpan.textContent.trim();
let valueText = valueSpan.textContent.trim().replace('%', '');
property.value = parseFloat(valueText);
return property;
}
return null;
}
/**
* 解析装备详情面板,提取所需数据
*/
function parseEquipData(wrap) {
const data = {
name: '',
type: '',
quality: '',
price: '',
mainProps: [],
extraProps: [], // 暗金属性 (核心属性)
traitOne: { name: '', probability: 0 },
traitTwo: { name: '', probability: 0 },
enhancedProps: [] // 强化属性 (精造)
};
const equipInfo = wrap;
if (!equipInfo) return data;
const infoPs = Array.from(equipInfo.querySelectorAll('p'));
// --- 1. 提取名称和类型 ---
const titleP = equipInfo.querySelector('p:first-child');
if (titleP) {
/**
* 根据 class (.weapon, .helmet, .armor, .shoes, .jewelry) 来识别装备类型。
*/
const classList = titleP.classList;
if (classList.contains('weapon')) {
data.type = '武器';
} else if (classList.contains('helmet')) {
data.type = '头盔';
} else if (classList.contains('armor')) {
data.type = '衣服';
} else if (classList.contains('shoes')) {
data.type = '鞋子';
} else if (classList.contains('jewelry')) {
data.type = '戒指'; // 涵盖戒指、符等饰品
}
// —— DOM 清洗法:不提取 emoji,不做整体字符删除 ——
// 1) 找到标题的第一个 <span>
const nameSpan = titleP.querySelector('span:first-child');
// 2) 克隆一份,用于安全地移除不需要的子元素
const nameClone = nameSpan.cloneNode(true);
// 3) 移除会“污染”名字展示的子元素(星标、强化星串、流派标签等)
nameClone.querySelectorAll('b, .refine-wrap, .dark-gold-spec-info, .equip-lock').forEach(el => el.remove());
// 4) 读取纯文本,并去掉末尾的 “+数字” 部分(例如 +18)
let nameText = nameClone.textContent;
// 去掉末尾的“+18 / +20”等(仅裁掉从第一个“ +数字”开始的后缀)
nameText = nameText.replace(/\s*\+\d+\s*.*$/, '');
// 移除名字开头的所有 emoji 与空白
nameText = nameText.replace(/^[\p{Emoji_Presentation}\p{Extended_Pictographic}\s]+/u, '');
// 规范空白
nameText = nameText.replace(/\s+/g, ' ').trim();
// 5) 设置最终名称(此时会自然包含最前面的 emoji)
data.name = nameText;
}
// 兜底逻辑:如果 class 未能识别类型,则根据名称判断
if (!data.type && (data.name.includes('指环') || data.name.includes('戒指'))) {
data.type = '戒指';
} else if (!data.type && data.name.includes('符')) {
data.type = '符'; // 确保“符”被识别
}
let currentSection = 'start';
for (const p of infoPs) {
const text = p.textContent.replace(/\s+/g, '').trim();
if (text.includes('属性:') && !text.includes('暗金属性')) {
currentSection = 'mainProps';
continue;
} else if (text.includes('暗金属性:')) {
currentSection = 'extraProps';
continue;
} else if (text.includes('刻印属性:')) {
currentSection = 'traitTwo';
continue;
} else if (text.includes('强化属性:')) {
currentSection = 'enhancedProps';
continue;
} else if (text.includes('品质:')) {
data.quality = p.querySelector('.darkGold, .mythicalGold, .gold')?.textContent.trim() || '暗金';
} else if (text.includes('售价:')) {
data.price = p.querySelector('.gold, .mythicalGold')?.textContent.trim() || '未知';
}
// --- 核心词条提取 (词条一和词条二) ---
const specialSpan = p.querySelector('.special');
const probSpan = p.querySelector('.darkGold');
if (specialSpan && probSpan) {
const potentialTraitName = specialSpan.textContent.trim();
const probText = probSpan.textContent.trim();
const probMatch = probText.match(/(\d+\.?\d*)\%/);
const probability = probMatch ? parseFloat(probMatch[1]) : 0;
// 特殊装备:直接当刻印(词条二)
if (SPECIAL_EQUIP_NAMES.includes(data.name)) {
data.traitTwo.name = potentialTraitName;
data.traitTwo.probability = probability;
} else {
// 普通装备:先占词条一,再占词条二
if (!data.traitOne.name) {
data.traitOne.name = potentialTraitName;
data.traitOne.probability = probability;
} else if (potentialTraitName !== data.traitOne.name && !data.traitTwo.name) {
data.traitTwo.name = potentialTraitName;
data.traitTwo.probability = probability;
}
}
continue; // 跳过属性提取
}
// --- 属性数值提取 ---
if (currentSection === 'mainProps') {
const prop = extractProperty(p, p.textContent.includes('%'));
if (prop) data.mainProps.push(prop);
} else if (currentSection === 'extraProps') {
const prop = extractProperty(p, p.textContent.includes('%'));
if (prop) data.extraProps.push(prop);
} else if (currentSection === 'enhancedProps') {
// 强化属性通常不显示百分号,但为了兼容性,检查一下
const prop = extractProperty(p, p.textContent.includes('%'));
if (prop) data.enhancedProps.push(prop);
}
}
return data;
}
function quickEquipSignature(wrap) {
const titleP = wrap.querySelector('p:first-child');
let name = '', type = '';
if (titleP) {
const cl = titleP.classList;
if (cl.contains('weapon')) type = '武器';
else if (cl.contains('helmet')) type = '头盔';
else if (cl.contains('armor')) type = '衣服';
else if (cl.contains('shoes')) type = '鞋子';
else if (cl.contains('jewelry')) type = '戒指';
const nameSpan = titleP.querySelector('span:first-child');
name = (nameSpan?.textContent || '').replace(/\s*\+\d+\s*.*$/, '').trim();
}
let t1 = '', t2 = '';
const specials = wrap.querySelectorAll('p .special');
if (specials[0]) t1 = specials[0].textContent.trim();
if (specials[1]) t2 = specials[1].textContent.trim();
return [name || '', type || '', t1, t2].join('|');
}
/**
* 识别适用流派 【返回推荐流派(用于评分)和备用流派(用于显示)】
* ★ 基于 equipData.type 从流派的 keywords 字段中动态提取关键词进行匹配和评分。
*/
function identifyBuilds(equipData) {
const traitOneName = equipData.traitOne.name;
const traitTwoName = equipData.traitTwo.name;
const traits = [traitOneName, traitTwoName].filter(Boolean);
// 1. 确定用于关键词查询的部位类型 ('符' 映射到 '戒指')
let typeForLookup = equipData.type;
if (typeForLookup === '符') typeForLookup = '戒指';
// 2. 特殊装备处理
if (SPECIAL_EQUIP_NAMES.includes(equipData.name)) {
const preferName = (equipData.traitOne.name === '冲锋' || equipData.traitTwo.name === '冲锋')
&& BUILD_DATA['命运-冲锋'] ? '命运-冲锋' : '菊花-命运';
const info = BUILD_DATA[preferName];
// 提取特殊装备的有效关键词(使用 keywords 字段)
const keywordsStr = info.keywords ? info.keywords[typeForLookup] : '';
const effectiveKeywords = keywordsStr
.split(/,|或|\/|,/)
.map(s => s.trim())
.filter(Boolean);
return {
builds: preferName,
backupBuilds: '无',
core: info.core,
keywords: info.keywords, // 使用新的字段名
primaryKeywords: effectiveKeywords, // 使用动态有效关键词
isSpecial: true,
strategy: 'special'
};
}
// 普通装备:基于部位关键词 (keywords) 进行匹配
const perfectMatches = [];
const partialMatches = [];
for (const [buildName, info] of Object.entries(BUILD_DATA)) {
// 3. 动态提取该流派在该部位的“有效关键词”
const keywordsStr = info.keywords ? info.keywords[typeForLookup] : '';
if (!keywordsStr) continue;
// —— 新增:识别“成对逻辑” ——
// 规则:如果同一串里同时出现 “,”/“,”(表示 AND) 和 “或/ /”(表示 OR)
// 则按 “A, B或C” => 需要同时命中 A 且 (B 或 C) 的成对逻辑。
// 仅在同一串内同时出现 AND 和 OR 时启用该逻辑;否则保持原有“扁平或”逻辑。
const hasAnd = /,|,/.test(keywordsStr);
const hasOr = /或|\//.test(keywordsStr);
const isGrouped = hasAnd && hasOr;
if (isGrouped) {
// 仅取第一个逗号前后的两段:A, (B 或 C 或 …)
// 若写法更复杂,保持向后兼容:只按第一段作为“必选”,第二段作为“候选或集合”
const parts = keywordsStr.split(/,|,/).map(s => s.trim()).filter(Boolean);
const mandatory = parts[0]; // A
const orPool = (parts[1] || '')
.split(/或|\//)
.map(s => s.trim())
.filter(Boolean); // [B, C, …]
// 命中情况
const hasMandatory = traits.includes(mandatory);
const orHits = orPool.filter(k => traits.includes(k));
const hasAnyOr = orHits.length > 0;
// perfect:命中 A 且命中 (B 或 C)
if (hasMandatory && hasAnyOr) {
// 为了后续显示,仍保留 effectiveKeywords(仅用于展示)
const effectiveKeywords = [mandatory, ...orPool];
perfectMatches.push({ buildName, info: { ...info, effectiveKeywords, isGrouped: true } });
}
// partial:只命中 A 或 只命中 (B/C)
else if (hasMandatory || hasAnyOr) {
const hitCount = (hasMandatory ? 1 : 0) + orHits.length;
const effectiveKeywords = [mandatory, ...orPool];
partialMatches.push({ buildName, info: { ...info, effectiveKeywords, isGrouped: true }, hitCount });
}
} else {
// —— 保持原有“扁平或”逻辑(与之前完全一致) ——
const effectiveKeywords = keywordsStr
.split(/,|或|\/|,/)
.map(s => s.trim())
.filter(Boolean);
if (effectiveKeywords.length === 0) continue;
const hitCount = effectiveKeywords.filter(k => traits.includes(k)).length;
if (hitCount === effectiveKeywords.length && effectiveKeywords.length >= 1) {
perfectMatches.push({ buildName, info: { ...info, effectiveKeywords } });
} else if (hitCount >= 1) {
partialMatches.push({ buildName, info: { ...info, effectiveKeywords }, hitCount });
}
}
}
// 6. 处理完美匹配 (优先级最高的作为主推荐)
if (perfectMatches.length > 0) {
perfectMatches.sort((a, b) => b.info.priority - a.info.priority);
const primary = perfectMatches[0];
const backup = partialMatches
.filter(p => p.buildName !== primary.buildName)
.map(p => p.buildName);
return {
builds: perfectMatches.map(p => p.buildName).join('、'),
backupBuilds: backup.join('、') || '无',
core: primary.info.core,
keywords: primary.info.keywords, // ★ 使用 keywords
primaryKeywords: primary.info.effectiveKeywords, // 使用部位有效的关键词
isSpecial: false,
strategy: 'perfect'
};
}
// 7. 处理部分匹配 (进入候选集评分)
if (partialMatches.length > 0) {
// 优先按 priority 排序,次要按 hitCount 排序
partialMatches.sort((a, b) => (b.info.priority - a.info.priority) || (b.hitCount - a.hitCount));
const backup = partialMatches.map(p => p.buildName);
return {
builds: '(单关键词匹配,待候选集评分决出)',
backupBuilds: backup.join('、') || '无',
core: '待候选集评分后确定',
keywords: {}, // ★ 使用 keywords
primaryKeywords: [],
isSpecial: false,
strategy: 'need_candidates',
candidates: partialMatches // ★ 关键:返回所有部分匹配的流派数据
};
}
// —— 无匹配:只提示,不评分 ——
updatePanelForNoBuild(equipData);
return {
builds: '无流派',
backupBuilds: '无流派',
core: '无明确核心属性',
keywords: {}, // ★ 使用 keywords
primaryKeywords: [],
isSpecial: false,
strategy: 'none'
};
}
// =======================================================================
// 4. 浮动面板逻辑 (CSS, 创建, 交互)
// =======================================================================
function injectStyles() {
const scaledWidth = 340 * SCALE_FACTOR;
const scaledPadding = 15 * SCALE_FACTOR;
const scaledHeaderPadding = 12 * SCALE_FACTOR;
const scaledBaseFontSize = 14 * SCALE_FACTOR;
const scaledHeaderFontSize = 15 * SCALE_FACTOR;
const scaledControlFontSize = 16 * SCALE_FACTOR;
const scaledPropFontSize = 13 * SCALE_FACTOR;
const style = document.createElement('style');
style.textContent = `
/* Analysis Panel Base Styles */
#equip-analysis-panel, #settings-panel { /* 添加设置面板 */
position: fixed;
top: 150px;
right: 20px;
width: ${scaledWidth}px; /* 缩放宽度 */
background: #2c3e50; /* Dark background */
border: 2px solid #f39c12; /* Dark gold border */
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
z-index: 99999;
color: #ecf0f1;
font-family: 'Inter', sans-serif;
transition: all 0.3s ease-in-out;
overflow: hidden;
display: none; /* 默认隐藏 */
}
/* Panel Header (Drag Handle) */
.analysis-header, .settings-header {
background: #f39c12;
color: #2c3e50;
padding: ${scaledPadding * 0.5}px ${scaledHeaderPadding}px; /* 缩放内边距 */
cursor: move;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: bold;
font-size: ${scaledHeaderFontSize}px; /* 缩放字体 */
}
/* Control Buttons */
.header-controls button {
background: transparent;
border: none;
color: #2c3e50;
font-weight: bold;
font-size: ${scaledControlFontSize}px; /* 缩放字体 */
margin-left: ${8 * SCALE_FACTOR}px;
cursor: pointer;
padding: ${2 * SCALE_FACTOR}px ${5 * SCALE_FACTOR}px;
border-radius: 4px;
transition: background 0.2s;
}
.header-controls button:hover {
background: rgba(0, 0, 0, 0.1);
}
#settings-btn { /* 新增设置按钮的样式 */
font-size: ${scaledControlFontSize * 0.8}px;
margin-left: ${4 * SCALE_FACTOR}px;
padding: ${4 * SCALE_FACTOR}px ${8 * SCALE_FACTOR}px;
border: 1px solid #2c3e50;
border-radius: 4px;
}
#settings-btn:hover {
background: #e67e22;
}
/* Panel Content */
.analysis-content, .settings-content {
padding: ${10 * SCALE_FACTOR}px ${scaledPadding}px; /* 缩放内边距 */
line-height: 1.5;
font-size: ${scaledBaseFontSize}px; /* 缩放字体 */
display: block; /* Default is visible */
}
/* Settings Panel Specific Styles */
#settings-panel label {
display: block;
margin-top: ${10 * SCALE_FACTOR}px;
}
#settings-panel input[type="range"] {
width: 100%;
}
#settings-panel .rule-box {
border: 1px solid #7f8c8d;
padding: ${8 * SCALE_FACTOR}px;
margin-top: ${10 * SCALE_FACTOR}px;
border-radius: 4px;
font-size: ${scaledPropFontSize}px;
color: #bdc3c7;
}
#settings-panel .rule-box strong {
color: #ecf0f1;
}
/* Content Colors/Emphasis */
.analysis-content .darkGold, .settings-content .darkGold {
color: #ffd700; /* Gold color for emphasis */
font-weight: bold;
}
.analysis-content .grow, .settings-content .grow {
color: #2ecc71; /* Green color for positive/growth values */
font-weight: bold;
font-size: inherit;
}
.analysis-content .warning, .settings-content .warning {
color: #e74c3c;
}
.analysis-content p, .settings-content p {
margin: ${4 * SCALE_FACTOR}px 0;
padding: 0;
}
.stat-title {
margin-top: ${15 * SCALE_FACTOR}px;
font-weight: bold;
color: #bdc3c7;
}
.stat-list {
margin: ${5 * SCALE_FACTOR}px 0 ${10 * SCALE_FACTOR}px 0;
padding: 0;
list-style: none;
border-left: ${3 * SCALE_FACTOR}px solid #3498db;
padding-left: ${10 * SCALE_FACTOR}px;
font-size: ${scaledPropFontSize}px; /* 缩放字体 */
color: #bdc3c7;
}
.stat-list li {
margin: ${2 * SCALE_FACTOR}px 0;
}
.stat-list .ignored {
color: #e74c3c;
text-decoration: line-through;
}
.stat-list .scored {
color: #2ecc71;
}
.stat-list .overroll {
color: #3498db;
font-weight: bold;
border-bottom: 1px dotted #3498db;
}
/* Minimized State (保持原始大小,不进行缩放) */
.minimized .analysis-content {
display: none;
}
.minimized {
width: 150px !important; /* 原始值 */
height: auto !important; /* 原始值 */
opacity: 0.95;
}
/* Minimized Title Override (确保最小化标题可见,不受SCALE_FACTOR影响) */
.minimized .analysis-header {
font-size: 15px; /* 原始值 */
padding: 8px 12px; /* 原始值 */
}
.minimized .header-controls button {
font-size: 16px; /* 原始值 */
margin-left: 8px; /* 原始值 */
padding: 2px 5px; /* 原始值 */
}
`;
document.head.appendChild(style);
}
/**
* 设置面板的拖动、最小化和关闭监听器 (支持鼠标和触摸)
*/
function setupPanelListeners(panel) {
const header = panel.querySelector('.analysis-header, .settings-header'); // 兼容两个面板
const minimizeBtn = panel.querySelector('#minimize-btn');
const closeBtn = panel.querySelector('#close-btn, #settings-close-btn'); // 兼容两个面板
const settingsBtn = panel.querySelector('#settings-btn');
// --- 拖动功能 (通用逻辑) ---
const startDrag = (clientX, clientY) => {
isDragging = true;
dragOffsetX = clientX - panel.offsetLeft;
dragOffsetY = clientY - panel.offsetTop;
panel.style.transition = 'none'; // 拖动时禁用动画
};
const moveDrag = (clientX, clientY) => {
if (!isDragging) return;
let newX = clientX - dragOffsetX;
let newY = clientY - dragOffsetY;
// 限制拖动范围在屏幕内
newX = Math.max(0, Math.min(newX, window.innerWidth - panel.offsetWidth));
newY = Math.max(0, Math.min(newY, window.innerHeight - panel.offsetHeight));
panel.style.left = newX + 'px';
panel.style.top = newY + 'px';
};
const endDrag = () => {
if (isDragging) {
isDragging = false;
panel.style.transition = 'all 0.3s ease-in-out'; // 恢复动画
}
};
header.addEventListener('mousedown', (e) => {
if (e.target.closest('.header-controls')) return; // 点击按钮不进入拖拽
startDrag(e.clientX, e.clientY);
e.preventDefault();
});
document.addEventListener('mousemove', (e) => moveDrag(e.clientX, e.clientY));
document.addEventListener('mouseup', endDrag);
// --- 2. 触摸事件监听 (移动端) ---
header.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
if (e.target.closest('.header-controls')) return; // 点击按钮不拖拽
const touch = e.touches[0];
startDrag(touch.clientX, touch.clientY);
e.preventDefault();
}
}, { passive: false });
document.addEventListener('touchmove', (e) => {
if (!isDragging) return; // 只有拖拽中才处理
if (e.touches.length === 1) {
const touch = e.touches[0];
moveDrag(touch.clientX, touch.clientY);
e.preventDefault(); // 仅拖拽时阻止页面滚动
}
}, { passive: false });
document.addEventListener('touchend', endDrag);
document.addEventListener('touchcancel', endDrag); // 处理触摸中断情况
// --- 最小化/恢复功能 (仅分析面板) ---
if (minimizeBtn) {
minimizeBtn.addEventListener('click', () => {
panel.classList.toggle('minimized');
const minimized = panel.classList.contains('minimized');
minimizeBtn.textContent = minimized ? '🗗' : '―';
// 切换标题:最小化显示“得分”,还原显示“装备分析”
const headerTitle = panel.querySelector('.analysis-header span');
headerTitle.textContent = minimized
? `得分 ${panel.dataset.latestScore || '—'} `
: '装备分析';
});
}
// --- 设置按钮功能 (仅分析面板) ---
if (settingsBtn) {
settingsBtn.addEventListener('click', () => {
createSettingsPanel();
});
}
// --- 关闭功能 ---
closeBtn.addEventListener('click', () => {
panel.remove();
if (panel.id === 'equip-analysis-panel') {
analysisPanel = null;
// 记录用户已主动关闭,阻止下次自动弹出
isUserClosed = true;
} else if (panel.id === 'settings-panel') {
settingsPanel = null;
}
});
}
/**
* 创建或获取浮动分析面板
*/
function getOrCreatePanel() {
// 【新增检查】如果用户已主动关闭,则阻止面板创建和显示
if (isUserClosed) {
return null;
}
if (analysisPanel) {
return analysisPanel;
}
const panel = document.createElement('div');
panel.id = 'equip-analysis-panel';
// 默认位置根据 SCALE_FACTOR 调整
const defaultWidth = 340;
const scaledWidth = defaultWidth * SCALE_FACTOR;
panel.style.left = (window.innerWidth - 20 - scaledWidth) + 'px';
panel.style.top = '150px';
panel.innerHTML = `
<div class="analysis-header">
<span>装备分析</span>
<div class="header-controls">
<button id="settings-btn" title="设置">⚙️</button>
<button id="minimize-btn" title="最小化/恢复">―</button>
<button id="close-btn" title="关闭">✕</button>
</div>
</div>
<div class="analysis-content">
<p>等待暗金装备数据...</p>
</div>
`;
document.body.appendChild(panel);
setupPanelListeners(panel);
analysisPanel = panel;
return analysisPanel;
}
/**
* 创建并显示设置面板
*/
function createSettingsPanel() {
if (settingsPanel) {
settingsPanel.style.display = 'block';
return;
}
const panel = document.createElement('div');
panel.id = 'settings-panel';
// 默认位置在分析面板旁边
const defaultWidth = 340;
const scaledWidth = defaultWidth * SCALE_FACTOR;
panel.style.left = (window.innerWidth - 40 - scaledWidth * 2) + 'px';
panel.style.top = '150px';
panel.style.display = 'block';
const nonCoreWeightPercent = (NON_CORE_PROPS_WEIGHT * 100).toFixed(0);
const traitPenaltyPercent = (TRAIT_MISMATCH_PENALTY * 100).toFixed(0);
panel.innerHTML = `
<div class="settings-header">
<span>评分设置与规则</span>
<div class="header-controls">
<button id="settings-close-btn" title="关闭">✕</button>
</div>
</div>
<div class="settings-content">
<p class="stat-title">评分权重设置 (百分比):</p>
<label for="nonCoreWeight">
非核心属性得分比例:
<span id="nonCoreWeightDisplay" class="grow">${nonCoreWeightPercent}%</span>
</label>
<input type="range" id="nonCoreWeight" min="0" max="100" step="5" value="${nonCoreWeightPercent}">
<p style="margin-top: ${5 * SCALE_FACTOR}px; color:#bdc3c7; font-size: ${12 * SCALE_FACTOR}px;">
(非核心属性满分 = 核心属性基础权重 × 此比例)
</p>
<label for="traitPenalty">
词条未命中惩罚比例:
<span id="traitPenaltyDisplay" class="grow">${traitPenaltyPercent}%</span>
</label>
<input type="range" id="traitPenalty" min="0" max="100" step="5" value="${traitPenaltyPercent}">
<p style="margin-top: ${5 * SCALE_FACTOR}px; color:#bdc3c7; font-size: ${12 * SCALE_FACTOR}px;">
(未命中关键词得分 = 词条概率 × 3分 × 此比例)
</p>
<button id="saveSettingsBtn" class="darkGold" style="margin-top: ${15 * SCALE_FACTOR}px; padding: ${6 * SCALE_FACTOR}px ${12 * SCALE_FACTOR}px; border: 1px solid #f39c12; background: #e67e22; color: #2c3e50; border-radius: 4px; cursor: pointer;">
保存设置并应用 (需重开装备面板)
</button>
<p class="stat-title" style="margin-top: ${25 * SCALE_FACTOR}px; color: #f39c12;">当前计分规则:</p>
<div class="rule-box">
<p><strong>基础属性满分:</strong> ${MAX_CORE_SCORE.toFixed(1)}分 (均分给所有暗金属性)</p>
<p><strong>核心属性得分:</strong> 基础权重 × 属性完美度 (Roll) [0-100%]</p>
<p><strong>非核心属性得分:</strong> 基础权重 × 属性完美度 × <span id="ruleNonCoreWeight">${NON_CORE_PROPS_WEIGHT.toFixed(2)}</span></p>
<p><strong>属性超限加分:</strong> 额外 +0.5 基础权重 (仅核心属性)</p>
<p style="margin-top: ${8 * SCALE_FACTOR}px;"><strong>词条满分:</strong> ${3.0.toFixed(1)}分 (命中关键词)</p>
<p><strong>词条命中关键词得分:</strong> 词条概率 × ${3.0.toFixed(1)}分</p>
<p><strong>词条未命中关键词得分:</strong> 词条概率 × ${3.0.toFixed(1)}分 × <span id="ruleTraitPenalty">${TRAIT_MISMATCH_PENALTY.toFixed(2)}</span></p>
</div>
</div>
`;
document.body.appendChild(panel);
settingsPanel = panel;
setupPanelListeners(panel);
const nonCoreInput = panel.querySelector('#nonCoreWeight');
const nonCoreDisplay = panel.querySelector('#nonCoreWeightDisplay');
const traitPenaltyInput = panel.querySelector('#traitPenalty');
const traitPenaltyDisplay = panel.querySelector('#traitPenaltyDisplay');
const saveBtn = panel.querySelector('#saveSettingsBtn');
const ruleNonCoreWeight = panel.querySelector('#ruleNonCoreWeight');
const ruleTraitPenalty = panel.querySelector('#ruleTraitPenalty');
const updateDisplays = () => {
const ncVal = parseInt(nonCoreInput.value) / 100;
const tpVal = parseInt(traitPenaltyInput.value) / 100;
nonCoreDisplay.textContent = `${nonCoreInput.value}%`;
traitPenaltyDisplay.textContent = `${traitPenaltyInput.value}%`;
ruleNonCoreWeight.textContent = ncVal.toFixed(2);
ruleTraitPenalty.textContent = tpVal.toFixed(2);
};
nonCoreInput.addEventListener('input', updateDisplays);
traitPenaltyInput.addEventListener('input', updateDisplays);
saveBtn.addEventListener('click', () => {
const newNonCoreWeight = parseInt(nonCoreInput.value) / 100;
const newTraitPenalty = parseInt(traitPenaltyInput.value) / 100;
saveSettings(newNonCoreWeight, newTraitPenalty);
// 立即更新规则显示
updateDisplays();
});
// 确保初次加载时规则显示正确
updateDisplays();
}
/**
* 渲染合并后的属性 HTML (使用内联样式进行字体缩放)
*/
function renderCombinedPropsHTML(combinedProps) {
if (!Array.isArray(combinedProps)) return '';
const scaledSmallFontSize = 11 * SCALE_FACTOR;
return combinedProps.map(prop => {
const isIgnored = !!prop.isIgnored; // 只对真正忽略的属性划线
const baseStyle = isIgnored ? 'text-decoration: line-through; opacity: 0.6;' : '';
// ★ 非核心属性:很暗的灰色 + 轻微透明
const nonCoreStyle = (!prop.isCore && !isIgnored) ? 'color:#8b9099; opacity:0.7;' : '';
const nameClass = prop.isCore ? 'grow' : ''; // 核心属性保留绿色,非核心走灰色样式
const quality = prop.maxRoll
? `(属性完美度 ${(Math.min(prop.value / prop.maxRoll, 1) * 100).toFixed(1)}%)`
: '';
return `
<p style="margin:${2 * SCALE_FACTOR}px 0; ${baseStyle} ${nonCoreStyle}">
<span class="${nameClass}">${prop.name}</span>:<span>${prop.value}</span>
<span style="font-size: ${scaledSmallFontSize}px; color: #bdc3c7;">${quality}</span>
${prop.isOverRoll ? `<span style="margin-left:${6 * SCALE_FACTOR}px;color:#2ecc71; font-size: ${scaledSmallFontSize}px;">(超限加分)</span>` : ''}
</p>
`;
}).join('');
}
/**
* 更新面板内容
*/
function updatePanelContent(equipData, buildInfo, scoreResult) {
const traitOneMatched = buildInfo.primaryKeywords?.includes(equipData.traitOne?.name);
const traitTwoMatched = buildInfo.primaryKeywords?.includes(equipData.traitTwo?.name);
const traitOneClass = traitOneMatched ? 'grow' : '';
const traitTwoClass = traitTwoMatched ? 'grow' : '';
const panel = getOrCreatePanel();
if (!panel) return;
const contentDiv = panel.querySelector('.analysis-content');
const corePropsScoreMax = MAX_CORE_SCORE;
const traitScoreMax = 3.0;
// 1) 确保面板可见
panel.style.display = 'block';
const minimized = panel.classList.contains('minimized');
panel.querySelector('#minimize-btn').textContent = minimized ? '🗗' : '―';
// 2) 基础属性列表 HTML
const combinedPropsHTML = renderCombinedPropsHTML(scoreResult.combinedProps);
// 3) 部位关键词(“符”按“戒指”处理)
let recommendationType = equipData.type;
if (recommendationType === '符') recommendationType = '戒指';
// ★ 使用 buildInfo.keywords
const keywordsBySlot = (buildInfo.keywords && buildInfo.keywords[recommendationType])
? buildInfo.keywords[recommendationType]
: '该部位无特定关键词';
// 4) 常用变量
const icon = EQUIP_ICONS[equipData.type] || '✨';
const coreBaseScore = parseFloat(scoreResult.coreBaseScore);
const coreBonusScore = parseFloat(scoreResult.coreBonusScore);
const totalScore = parseFloat(scoreResult.totalScore);
const coreRatio = (coreBaseScore / corePropsScoreMax) || 0;
// 原始字符串
const recBuildsTextRaw = buildInfo.builds || '';
const backupBuildsTextRaw = buildInfo.backupBuilds || '';
// 规范化为数组
const primaryList = recBuildsTextRaw.split('、').map(s => s.trim()).filter(Boolean);
const backupListRaw = backupBuildsTextRaw.split('、').map(s => s.trim()).filter(Boolean);
// 过滤:去掉与推荐重复的、去掉空串、去重
const backupListFiltered = Array.from(new Set(
backupListRaw.filter(b => !primaryList.includes(b))
));
const recBuildsText = primaryList.length ? primaryList.join('、') : '无';
const backupBuildsText = backupListFiltered.length ? backupListFiltered.join('、') : '无';
// 字体大小调整
const scaledTitleFontSize = 16 * SCALE_FACTOR;
const scaledInfoMargin = 5 * SCALE_FACTOR;
// 5) 面板内容
const contentHTML = `
<p style="font-size: ${scaledTitleFontSize}px; margin-bottom: ${10 * SCALE_FACTOR}px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); padding-bottom: ${5 * SCALE_FACTOR}px;">
${icon} <span class="darkGold">${equipData.name}</span>
</p>
<p style="margin-top: ${10 * SCALE_FACTOR}px; font-weight: bold; color: #ff6600;">
总评分:<span class="grow">${totalScore.toFixed(2)}</span>
</p>
<p style="margin: ${scaledInfoMargin}px 0 0 0;">
<span style="font-weight: bold; color: #bdc3c7;">基础属性得分:</span>
<span class="${coreRatio > 0.6 ? 'grow' : 'warning'}">${coreBaseScore.toFixed(2)}</span>
<span style="color:#bdc3c7;"> / ${corePropsScoreMax.toFixed(0)}</span>
</p>
<p class="stat-title">基础属性:</p>
${combinedPropsHTML}
${coreBonusScore > 0.01 ? `
<p style="margin: ${scaledInfoMargin}px 0 0 0;">
<span style="font-weight: bold; color: #2ecc71;">超限精造加分:</span>
<span class="grow">+${coreBonusScore.toFixed(2)}</span>
</p>
` : ''}
<p class="stat-title" style="color: #f39c12;">流派词条得分:</p>
<p style="color: #bdc3c7; margin-top: ${scaledInfoMargin}px;">
自带词条 (<span class="${traitOneClass}">${(equipData.traitOne && equipData.traitOne.name) || '无'}</span>) 概率:
<span class="darkGold">${(equipData.traitOne && equipData.traitOne.probability) ?? 0}%</span>
得分: ${scoreResult.traitOneScore} / ${traitScoreMax.toFixed(1)}
</p>
<p style="color: #bdc3c7; margin-top: ${6 * SCALE_FACTOR}px;">
刻印词条 (<span class="${traitTwoClass}">${(equipData.traitTwo && equipData.traitTwo.name) || '无'}</span>) 概率:
<span class="darkGold">${(equipData.traitTwo && equipData.traitTwo.probability) ?? 0}%</span>
得分: ${scoreResult.traitTwoScore} / ${traitScoreMax.toFixed(1)}
</p>
<p style="margin-top:${8 * SCALE_FACTOR}px;">推荐流派:<span class="grow">${recBuildsText}</span></p>
<p>备用流派:<span class="grow">${backupBuildsText}</span></p>
<p>核心属性:<span class="grow">${buildInfo.core}</span></p>
<p style="margin-bottom: 0;">
部位关键词 (<span class="darkGold">${equipData.type || '未知部位'}</span>):
<span class="grow">${keywordsBySlot}</span>
</p>
`;
contentDiv.innerHTML = contentHTML;
// 6) 更新标题(最小化时显示分数)
panel.dataset.latestScore = totalScore.toFixed(2);
const headerTitle = panel.querySelector('.analysis-header span');
headerTitle.textContent = panel.classList.contains('minimized')
? `得分 ${panel.dataset.latestScore}`
: '装备分析';
}
/**
* 当未匹配到流派时,更新面板内容为提示信息。
*/
function updatePanelForNoBuild(equipData) {
const panel = getOrCreatePanel();
if (!panel) return;
const contentDiv = panel.querySelector('.analysis-content');
// 1) 确保面板可见
panel.style.display = 'block';
const icon = EQUIP_ICONS[equipData.type] || '✨';
const scaledTitleFontSize = 16 * SCALE_FACTOR;
const scaledWarningFontSize = 15 * SCALE_FACTOR;
const scaledMargin = 10 * SCALE_FACTOR;
// 2) 更新内容
const contentHTML = `
<p style="font-size: ${scaledTitleFontSize}px; margin-bottom: ${scaledMargin}px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); padding-bottom: ${5 * SCALE_FACTOR}px;">
${icon} <span class="darkGold">${equipData.name || '未知装备'}</span>
</p>
<p class="warning" style="font-weight: bold; font-size: ${scaledWarningFontSize}px;">
❌ 未匹配到任何流派
</p>
<p style="margin-top: ${scaledMargin}px; color: #bdc3c7;">
当前装备词条:
<span class="darkGold">${(equipData.traitOne && equipData.traitOne.name) || '无'}</span>
、
<span class="darkGold">${(equipData.traitTwo && equipData.traitTwo.name) || '无'}</span>
</p>
<p style="color: #bdc3c7;">
根据部位关键词,没有找到匹配的流派。
</p>
`;
contentDiv.innerHTML = contentHTML;
// 3) 更新标题
panel.dataset.latestScore = '—';
const headerTitle = panel.querySelector('.analysis-header span');
headerTitle.textContent = panel.classList.contains('minimized')
? `得分 —`
: '装备分析(无匹配流派)';
}
/**
* 主函数:处理装备面板并触发浮动面板显示
*/
function showAnalysisPanel(wrap) {
// 1) 只处理暗金/神话(标题 p.darkGold / p.myth)
if (!isDarkOrMythPanel(wrap)) {
if (analysisPanel) analysisPanel.style.display = 'none';
lastEquipWrap = null;
return;
}
// ★ 用“快速指纹”判定是否是另一件暗金(零拷贝、零解析)
const curSignature = quickEquipSignature(wrap);
// 如果还是同一件且用户曾主动关闭,则不再弹出
if (isUserClosed && curSignature === lastEquipSignature) {
if (analysisPanel) analysisPanel.style.display = 'none';
return;
}
// 只要换了“另一件暗金”,才允许重新弹出
if (curSignature !== lastEquipSignature) {
isUserClosed = false;
lastEquipSignature = curSignature;
lastEquipWrap = wrap; // 兼容保留
}
// ——从这里开始,继续使用 v1.6 原有流程:解析 → 识别流派 → 打分 → 渲染——
const equipData = parseEquipData(wrap);
if (!equipData.name || equipData.name.includes('...')) {
if (analysisPanel) analysisPanel.style.display = 'none';
return;
}
const buildInfo = identifyBuilds(equipData);
if (buildInfo && buildInfo.strategy === 'none') {
updatePanelForNoBuild(equipData);
return;
}
if (buildInfo.strategy === 'need_candidates' && buildInfo.candidates) {
const candidateBuilds = buildInfo.candidates;
if (candidateBuilds.length > 0) {
let best = { name: '', info: null, score: -Infinity, scoreResult: null };
for (const candidate of candidateBuilds) {
const name = candidate.buildName;
const info = candidate.info;
const tmpInfo = {
core: info.core,
keywords: info.keywords,
primaryKeywords: info.effectiveKeywords
};
const s = calculateScore(equipData, tmpInfo);
const total = parseFloat(s.totalScore);
if (total > best.score) best = { name, info, score: total, scoreResult: s };
}
const finalBuildInfo = {
builds: best.name,
backupBuilds: buildInfo.backupBuilds,
core: best.info.core,
keywords: best.info.keywords,
primaryKeywords: best.info.effectiveKeywords,
isSpecial: false,
strategy: 'resolved_by_candidates'
};
updatePanelContent(equipData, finalBuildInfo, best.scoreResult);
return;
} else {
updatePanelForNoBuild(equipData);
return;
}
}
const scoreResult = calculateScore(equipData, buildInfo);
updatePanelContent(equipData, buildInfo, scoreResult);
}
// =======================================================================
// 5. 观察者逻辑 (MutationObserver)
// =======================================================================
/**
* 观察者:监听页面装备面板的出现
*/
const observer = new MutationObserver(function(mutations) {
let isDarkGoldPanelVisible = false;
mutations.forEach(function(mutation) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1) {
// 查找 .equip-info.affix
const equipWrap = node.closest?.('.equip-info.affix') || node.querySelector?.('.equip-info.affix');
if (equipWrap && isDarkOrMythPanel(equipWrap)) {
showAnalysisPanel(equipWrap);
isDarkGoldPanelVisible = true;
}
}
});
}
});
// 兜底逻辑:如果本轮没检测到暗金面板,且页面上也没有“真可见”的暗金/神话面板,则隐藏浮动面板
if (!isDarkGoldPanelVisible && analysisPanel && analysisPanel.style.display !== 'none') {
const allEquipPanels = document.querySelectorAll('.equip-info.affix');
const hasActiveDarkGoldPanel = Array.from(allEquipPanels).some((p) => {
const popper = p.closest('.el-popper');
const visible = popper ? isVisible(popper) : isVisible(p);
return visible && isDarkOrMythPanel(p);
});
if (!hasActiveDarkGoldPanel) {
analysisPanel.style.display = 'none';
lastEquipWrap = null;
}
}
});
// =======================================================================
// 6. 初始化
//=======================================================================
// 1. 注入 CSS 样式
injectStyles();
// --- 可见性判断 ---
function isVisible(el) {
if (!el) return false;
const style = window.getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
// offsetParent 为 null 通常意味着元素或其祖先被 display:none / position:fixed 且无尺寸等
if (el.offsetParent === null && style.position !== 'fixed') return false;
const rect = el.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) return false;
// 至少有一部分在视口内(避免离屏/被移走也算“可见”)
const inViewport = rect.bottom > 0 && rect.right > 0 && rect.top < (window.innerHeight || document.documentElement.clientHeight) && rect.left < (window.innerWidth || document.documentElement.clientWidth);
return inViewport;
}
function isDarkOrMythPanel(wrap) {
// 标题 <p> 可能是 p.darkGold 或 p.myth
const titleP = wrap.querySelector('p:first-child');
if (!titleP) return false;
const cl = titleP.classList;
// 1) 直接在 p 上的类
const hitOnP = cl.contains('darkGold') || cl.contains('myth') || cl.contains('mythicalGold');
if (hitOnP) return true;
// 2) 兼容:某些主题会把标色类挂在内部元素
return !!wrap.querySelector('p:first-child .darkGold, p:first-child .myth, p:first-child .mythicalGold');
}
// --- 兜底扫描:对所有可见的装备面板触发评分 ---
function scanAllEquipPanels() {
const all = Array.from(document.querySelectorAll('.equip-info.affix'));
// 过滤出“真可见”的候选:popper 看 popper,可嵌入的看自身
const visibleCandidates = all.filter((wrap) => {
const popper = wrap.closest('.el-popper');
if (popper) return isVisible(popper);
return isVisible(wrap);
});
// 优先级:当前交互来源 > 可见 popper > 其它可见
let target = null;
if (currentSourceWrap && visibleCandidates.includes(currentSourceWrap)) {
target = currentSourceWrap;
} else {
const visiblePoppers = visibleCandidates.filter(w => w.closest('.el-popper'));
if (visiblePoppers.length) {
target = visiblePoppers[visiblePoppers.length - 1]; // DOM 末尾更可能是最新弹出
} else if (visibleCandidates.length) {
target = visibleCandidates[visibleCandidates.length - 1];
}
}
if (target) {
// 防抖:如果目标本体/其 popper 不再真可见则不展示
const popper = target.closest('.el-popper');
if ((popper && !isVisible(popper)) || (!popper && !isVisible(target))) {
return;
}
showAnalysisPanel(target);
} else {
// 没有任何真可见候选 → 关闭评分面板并重置
if (analysisPanel) analysisPanel.style.display = 'none';
lastEquipWrap = null;
lastEquipSignature = null;
}
}
// --- 监听 el-popper 显隐(style/class/aria-hidden 变化)---
const popperAttributesObserver = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.type === 'attributes' && ['style', 'class', 'aria-hidden'].includes(m.attributeName)) {
scanAllEquipPanels();
break;
}
}
});
// --- 给当前页所有 el-popper 装上属性监听 ---
function attachPopperObservers() {
document.querySelectorAll('.el-popper').forEach((el) => {
popperAttributesObserver.observe(el, {
attributes: true,
attributeFilter: ['style', 'class', 'aria-hidden'],
subtree: true
});
});
}
// --- 监听后续新创建的 el-popper(例如打开新弹层时)---
const popperCreationObserver = new MutationObserver((mutations) => {
let foundNewPopper = false;
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) {
if (node.classList.contains('el-popper') || node.querySelector?.('.el-popper')) {
foundNewPopper = true;
}
}
});
});
if (foundNewPopper) {
attachPopperObservers(); // 给新弹层挂监听
scanAllEquipPanels(); // 立即扫描评分
}
});
popperCreationObserver.observe(document.body, { childList: true, subtree: true });
// 【优化】为所有平台加延迟扫描,确保装备面板加载完成后再识别
const DELAY_MS = 120; // 延迟毫秒数,可自行调整
let currentSourceWrap = null; // 记录最近一次交互命中的 equip 面板
function markSourceFromEvent(e) {
const t = e.target;
if (!t || !t.closest) return;
const wrap = t.closest('.equip-info.affix');
if (wrap) currentSourceWrap = wrap;
}
document.addEventListener('click', (e) => {
markSourceFromEvent(e);
setTimeout(scanAllEquipPanels, DELAY_MS);
}, true);
document.addEventListener('touchstart', (e) => {
markSourceFromEvent(e);
setTimeout(scanAllEquipPanels, DELAY_MS);
}, true);
// --- 启动 ---\
window.addEventListener('load', () => {
// 保留原有主体观察(新增节点仍可触发评分)
observer.observe(document.body, { childList: true, subtree: true });
// 初始化:给现有 el-popper 安装显隐监听,并做一次全量扫描
attachPopperObservers();
scanAllEquipPanels();
console.log('暗金装备评分脚本已启用:含 el-popper 显隐监听与兜底扫描。');
});
})();