// ==UserScript==
// @name 裂隙工会-仓库助手
// @namespace https://lx.idlexiuxianzhuan.cn/
// @version 0.5.28
// @description 锁定装备暗色提示;移除已锁定筛选
// @author wwl
// @match https://lx.idlexiuxianzhuan.cn/*
// @match http://lx.idlexiuxianzhuan.cn/*
// @icon https://lx.idlexiuxianzhuan.cn/favicon.ico
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant unsafeWindow
// @run-at document-start
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const FILTER_STORAGE_KEY = 'lx_guild_stat_filter';
const EQUIP_FX_STORAGE_KEY = 'lx_guild_equip_fx';
const CONFIG_VERSION = 4;
const pageWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
const STAT_WEIGHTS = { hp: 0.6, str: 1, dex: 1, con: 1, int: 1, wil: 1, spd: 2.5 };
const RARITY_SCORE = { common: 0, green: 15, blue: 35, purple: 60, gold: 85, orange: 110 };
const PRIMARY_STAT_TIERS = [3.2, 2.5, 2.0, 1.6];
const DEFAULT_STAT_WEIGHTS = { hp: 0.45, str: 0.65, dex: 0.65, con: 0.65, int: 0.65, wil: 0.65, spd: 0.9 };
const PERCENT_AFFIX_CODES = new Set([
'weapon_damage_bonus', 'crit_rate_bonus', 'evasion_rate_bonus',
'fixed_damage_bonus', 'splash_damage_bonus', 'dot_damage_bonus',
'equipment_defense_bonus', 'equipment_magic_defense_bonus',
'shield_bonus', 'fixed_damage_reduction', 'direct_damage_reduction',
'direct_damage_reflect', 'shield_penetration_bonus',
]);
const ROLE_AFFIX_WEIGHTS = {
physical: {
weapon_damage_bonus: 95, weapon_attack_flat: 80, weapon_attack_min_flat: 45,
weapon_attack_max_flat: 45, weapon_attack_per_level: 55, crit_rate_bonus: 85,
fixed_damage_bonus: 35, splash_damage_bonus: 40, dot_damage_bonus: 35,
equipment_defense_bonus: 18, equipment_magic_defense_bonus: 12,
equipment_defense_flat: 22, equipment_magic_defense_flat: 15,
shield_bonus: 20, evasion_rate_bonus: 30,
},
magic: {
weapon_damage_bonus: 70, weapon_attack_flat: 45, weapon_attack_min_flat: 25,
weapon_attack_max_flat: 25, weapon_attack_per_level: 40, crit_rate_bonus: 35,
fixed_damage_bonus: 75, splash_damage_bonus: 45, dot_damage_bonus: 80,
equipment_defense_bonus: 15, equipment_magic_defense_bonus: 25,
equipment_defense_flat: 18, equipment_magic_defense_flat: 28,
shield_bonus: 22, evasion_rate_bonus: 25,
},
tank: {
weapon_damage_bonus: 25, weapon_attack_flat: 20, crit_rate_bonus: 15,
fixed_damage_bonus: 20, splash_damage_bonus: 10, dot_damage_bonus: 10,
equipment_defense_bonus: 90, equipment_magic_defense_bonus: 85,
equipment_defense_flat: 95, equipment_magic_defense_flat: 88,
shield_bonus: 100, fixed_damage_reduction: 80, direct_damage_reduction: 75,
direct_damage_reflect: 55, evasion_rate_bonus: 20,
},
healer: {
weapon_damage_bonus: 20, weapon_attack_flat: 15, crit_rate_bonus: 12,
fixed_damage_bonus: 30, splash_damage_bonus: 15, dot_damage_bonus: 25,
equipment_defense_bonus: 55, equipment_magic_defense_bonus: 70,
equipment_defense_flat: 50, equipment_magic_defense_flat: 65,
shield_bonus: 85, fixed_damage_reduction: 60, direct_damage_reduction: 55,
evasion_rate_bonus: 18,
},
support: {
weapon_damage_bonus: 22, weapon_attack_flat: 18, crit_rate_bonus: 15,
fixed_damage_bonus: 35, splash_damage_bonus: 20, dot_damage_bonus: 30,
equipment_defense_bonus: 50, equipment_magic_defense_bonus: 65,
equipment_defense_flat: 45, equipment_magic_defense_flat: 58,
shield_bonus: 75, fixed_damage_reduction: 55, direct_damage_reduction: 50,
evasion_rate_bonus: 22,
},
balanced: {
weapon_damage_bonus: 55, weapon_attack_flat: 50, weapon_attack_min_flat: 28,
weapon_attack_max_flat: 28, weapon_attack_per_level: 35, crit_rate_bonus: 45,
fixed_damage_bonus: 40, splash_damage_bonus: 30, dot_damage_bonus: 35,
equipment_defense_bonus: 45, equipment_magic_defense_bonus: 45,
equipment_defense_flat: 48, equipment_magic_defense_flat: 48,
shield_bonus: 45, fixed_damage_reduction: 35, direct_damage_reduction: 35,
evasion_rate_bonus: 28,
},
};
const RACE_AFFIX_BONUS = {
dwarf: { shield_bonus: 1.35, equipment_defense_bonus: 1.2, equipment_defense_flat: 1.2 },
drow: { crit_rate_bonus: 1.4, evasion_rate_bonus: 1.15 },
fairy: { fixed_damage_bonus: 1.45, evasion_rate_bonus: 1.35 },
undead: { fixed_damage_reduction: 1.3, direct_damage_reduction: 1.25 },
gnome: { weapon_damage_bonus: 1.25, weapon_attack_flat: 1.15 },
elf: { evasion_rate_bonus: 1.1 },
};
const ARMOR_SLOTS = new Set(['chest', 'shoulder', 'legs', 'head', 'hands', 'feet']);
const ACCESSORY_SLOTS = new Set(['ring', 'necklace', 'belt', 'trinket']);
function averageAttack(min, max, flat) {
const lo = Number(min) || Number(flat) || 0;
const hi = Number(max) || Number(flat) || 0;
return (lo + hi) / 2;
}
function normalizeAffixValue(code, raw) {
const val = Number(raw) || 0;
if (!PERCENT_AFFIX_CODES.has(code)) return val;
if (Math.abs(val) <= 1) return val * 100;
return val;
}
function getBuildStatWeights(profile) {
const weights = { ...DEFAULT_STAT_WEIGHTS };
(profile?.primary || []).forEach((key, index) => {
weights[key] = PRIMARY_STAT_TIERS[index] || 1.35;
});
const raceKey = profile?.raceKey || '';
if (raceKey === 'fairy') {
weights.int = Math.max(weights.int, 2.9);
weights.dex = Math.max(weights.dex, 2.1);
weights.wil = Math.max(weights.wil, 2.0);
} else if (raceKey === 'drow') {
weights.dex = Math.max(weights.dex, 3.1);
weights.str = Math.max(weights.str, 2.2);
} else if (raceKey === 'dwarf') {
weights.con = Math.max(weights.con, 2.9);
weights.wil = Math.max(weights.wil, 2.3);
} else if (raceKey === 'elf') {
weights.spd = Math.max(weights.spd, 2.6);
weights.int = Math.max(weights.int, 2.3);
weights.wil = Math.max(weights.wil, 2.1);
} else if (raceKey === 'undead') {
weights.wil = Math.max(weights.wil, 2.7);
weights.con = Math.max(weights.con, 2.0);
} else if (raceKey === 'gnome') {
weights.int = Math.max(weights.int, 2.3);
weights.dex = Math.max(weights.dex, 2.1);
weights.str = Math.max(weights.str, 2.0);
} else if (raceKey === 'human') {
['str', 'dex', 'con', 'int', 'wil'].forEach((key) => {
weights[key] = Math.max(weights[key], 1.5);
});
}
return weights;
}
function getRoleAffixWeights(role) {
return ROLE_AFFIX_WEIGHTS[role] || ROLE_AFFIX_WEIGHTS.balanced;
}
function getSlotScoreMultiplier(slotKey, profile) {
if (!slotKey || !profile) return 1;
if (slotKey === 'weapon') {
if (profile.role === 'physical' || profile.role === 'magic') return 1.08;
return 0.92;
}
if (ARMOR_SLOTS.has(slotKey) && profile.role === 'tank') return 1.12;
if (ARMOR_SLOTS.has(slotKey) && (profile.role === 'healer' || profile.role === 'support')) return 1.05;
if (ACCESSORY_SLOTS.has(slotKey) && (profile.role === 'physical' || profile.role === 'magic')) return 1.06;
return 1;
}
function scoreAffixForBuild(affix, profile, statWeights, slotKey) {
const code = affix?.effectCode || '';
const raw = Number(affix?.effectValue) || 0;
if (!code || !raw) return 0;
if (STAT_FLAT_AFFIX_CODES.has(code)) {
if (code === 'stat_all_flat') {
const avg = (statWeights.str + statWeights.dex + statWeights.con + statWeights.int + statWeights.wil) / 5;
return raw * avg * 2.4;
}
const statKey = code.replace('stat_', '').replace('_flat', '');
return raw * (statWeights[statKey] || 1) * 1.8;
}
const roleWeights = getRoleAffixWeights(profile.role);
let weight = roleWeights[code] || 28;
const raceBonus = RACE_AFFIX_BONUS[profile.raceKey]?.[code];
if (raceBonus) weight *= raceBonus;
if (slotKey === 'weapon' && /weapon_|crit_|splash_|dot_|fixed_damage/.test(code)) weight *= 1.15;
if (ARMOR_SLOTS.has(slotKey) && /equipment_|shield_|damage_reduction|damage_reflect/.test(code)) weight *= 1.12;
if (ACCESSORY_SLOTS.has(slotKey) && /weapon_damage|crit_|dot_|evasion_/.test(code)) weight *= 1.1;
const val = normalizeAffixValue(code, raw);
return val * weight;
}
function scoreItemAttackForRole(item, profile) {
const phys = averageAttack(item.weaponAttackMin, item.weaponAttackMax, item.weaponAttack);
const mag = averageAttack(item.magicAttackMin, item.magicAttackMax, item.magicAttack);
const def = Number(item.equipmentDefense) || 0;
const mdef = Number(item.equipmentMagicDefense) || 0;
switch (profile.role) {
case 'physical':
return phys * 1.55 + mag * 0.25 + def * 0.35 + mdef * 0.25;
case 'magic':
return mag * 1.55 + phys * 0.25 + def * 0.3 + mdef * 0.45;
case 'tank':
return def * 1.15 + mdef * 1.1 + phys * 0.45 + mag * 0.3;
case 'healer':
return mag * 0.75 + def * 0.55 + mdef * 0.85 + phys * 0.2;
case 'support':
return mag * 0.65 + def * 0.5 + mdef * 0.75 + phys * 0.25;
default:
return Math.max(phys, mag) * 1.1 + Math.min(phys, mag) * 0.35 + def * 0.5 + mdef * 0.5;
}
}
function scoreItemBase(item) {
if (!item) return 0;
let score = (Number(item.requiredLevel) || 0) * 3;
score += RARITY_SCORE[item.rarity] || 0;
const phys = averageAttack(item.weaponAttackMin, item.weaponAttackMax, item.weaponAttack);
const mag = averageAttack(item.magicAttackMin, item.magicAttackMax, item.magicAttack);
score += Math.max(phys, mag) * 1.2 + Math.min(phys, mag) * 0.4;
score += (Number(item.equipmentDefense) || 0) * 0.9;
score += (Number(item.equipmentMagicDefense) || 0) * 0.9;
STAT_DEFS.forEach((def) => {
score += getItemStatValue(item, def.key) * (STAT_WEIGHTS[def.key] || 1);
});
score += (Number(item.affixCount) || 0) * 5;
(item.affixes || []).forEach((affix) => {
score += normalizeAffixValue(affix.effectCode, affix.effectValue) * 0.35;
});
return Math.round(score);
}
function scoreItem(item) {
return scoreItemBase(item);
}
function scoreItemForCharacter(item, character, slotKey) {
if (!item) return 0;
const profile = getCharacterPrimaryProfile(character);
if (!profile) return scoreItemBase(item);
const statWeights = getBuildStatWeights(profile);
let score = (Number(item.requiredLevel) || 0) * 2.2;
score += (RARITY_SCORE[item.rarity] || 0) * 0.65;
score += scoreItemAttackForRole(item, profile);
STAT_DEFS.forEach((def) => {
score += getItemStatValue(item, def.key) * (statWeights[def.key] || 0.6);
});
score += (Number(item.affixCount) || 0) * 3;
(item.affixes || []).forEach((affix) => {
score += scoreAffixForBuild(affix, profile, statWeights, slotKey || item.slotKey);
});
score *= getSlotScoreMultiplier(slotKey || item.slotKey, profile);
return Math.round(score);
}
function scoreCharacterEquipment(character) {
return (character?.equipment || []).reduce((sum, slot) => (
sum + scoreItemForCharacter(slot.item, character, slot.slotKey)
), 0);
}
function formatItemScore(item, character, slotKey) {
if (character) {
return `适配 ${scoreItemForCharacter(item, character, slotKey)}`;
}
return `基础 ${scoreItemBase(item)}`;
}
function formatItemScoreDelta(item, compareItem, character, slotKey) {
if (!character || !compareItem) return '';
const delta = scoreItemForCharacter(item, character, slotKey)
- scoreItemForCharacter(compareItem, character, slotKey);
if (delta === 0) return '持平';
const sign = delta > 0 ? '+' : '';
return `${sign}${delta}`;
}
function formatCharacterEquipScore(character) {
if (!character) return '';
return `适配装备 ${scoreCharacterEquipment(character)}`;
}
const STAT_DEFS = [
{ key: 'hp', label: 'HP' },
{ key: 'str', label: '力量' },
{ key: 'dex', label: '灵巧' },
{ key: 'con', label: '体质' },
{ key: 'int', label: '智力' },
{ key: 'wil', label: '意志' },
{ key: 'spd', label: '速度' },
];
const STAT_KEY_LABEL = Object.fromEntries(STAT_DEFS.map((def) => [def.key, def.label]));
const CLASS_GROWTH = {
adventurer: { str: 3, dex: 3, con: 3, int: 3, wil: 3 },
warrior: { str: 5, dex: 5, con: 6, int: 5, wil: 5 },
forester: { str: 5, dex: 6, con: 5, int: 5, wil: 5 },
arcanist: { str: 5, dex: 5, con: 5, int: 6, wil: 5 },
artisan: { str: 5, dex: 5, con: 5, int: 5, wil: 6 },
striker: { str: 10, dex: 8, con: 8, int: 6, wil: 8 },
defender: { str: 7, dex: 6, con: 11, int: 6, wil: 10 },
ranger: { str: 7, dex: 11, con: 7, int: 7, wil: 8 },
assassin: { str: 8, dex: 11, con: 7, int: 7, wil: 7 },
priest: { str: 6, dex: 6, con: 8, int: 9, wil: 11 },
mage: { str: 6, dex: 7, con: 6, int: 12, wil: 9 },
blacksmith: { str: 9, dex: 6, con: 10, int: 6, wil: 9 },
jeweler: { str: 6, dex: 8, con: 6, int: 10, wil: 10 },
};
const SCHOOL_GROWTH = {
striker_duelist: { str: 16, dex: 15, con: 13, int: 11, wil: 13 },
striker_berserker: { str: 18, dex: 12, con: 16, int: 10, wil: 14 },
striker_weapon_master: { str: 17, dex: 14, con: 13, int: 12, wil: 14 },
defender_bastion: { str: 13, dex: 9, con: 20, int: 10, wil: 18 },
defender_paladin: { str: 14, dex: 10, con: 17, int: 11, wil: 18 },
defender_chakra: { str: 13, dex: 12, con: 17, int: 12, wil: 16 },
ranger_lord: { str: 12, dex: 18, con: 14, int: 10, wil: 16 },
ranger_earth_shooter: { str: 13, dex: 17, con: 15, int: 11, wil: 14 },
ranger_tactician: { str: 11, dex: 16, con: 13, int: 15, wil: 15 },
assassin_stalker: { str: 12, dex: 20, con: 11, int: 13, wil: 13 },
assassin_venom_blade: { str: 11, dex: 19, con: 12, int: 14, wil: 14 },
assassin_executioner: { str: 15, dex: 18, con: 12, int: 11, wil: 14 },
priest_bishop: { str: 9, dex: 10, con: 14, int: 16, wil: 21 },
priest_judicator: { str: 10, dex: 11, con: 13, int: 20, wil: 17 },
priest_dark_templar: { str: 14, dex: 10, con: 17, int: 13, wil: 17 },
mage_arcanist: { str: 9, dex: 11, con: 11, int: 22, wil: 18 },
mage_elementalist: { str: 10, dex: 12, con: 10, int: 23, wil: 17 },
mage_rule_scholar: { str: 9, dex: 10, con: 12, int: 21, wil: 20 },
blacksmith_runesmith: { str: 13, dex: 10, con: 15, int: 16, wil: 16 },
blacksmith_armorer: { str: 15, dex: 9, con: 20, int: 10, wil: 18 },
blacksmith_weaponsmith: { str: 18, dex: 11, con: 15, int: 10, wil: 16 },
jeweler_inlayer: { str: 10, dex: 13, con: 11, int: 18, wil: 18 },
jeweler_cutter: { str: 11, dex: 17, con: 10, int: 17, wil: 15 },
jeweler_enchanter: { str: 9, dex: 12, con: 11, int: 20, wil: 18 },
};
const RACE_PRIMARY_STATS = {
human: ['str', 'dex', 'con', 'int', 'wil'],
elf: ['int', 'wil', 'spd'],
dwarf: ['con', 'wil'],
gnome: ['int', 'dex', 'str'],
drow: ['dex', 'str'],
undead: ['wil', 'con'],
fairy: ['int', 'dex', 'wil'],
};
const RACE_STAT_NOTES = {
human: '全属性均衡',
elf: '智力/意志/速度',
dwarf: '体质/护盾/防御',
gnome: '武器伤害/双倍触发',
drow: '灵巧/会心',
undead: '意志/减伤',
fairy: '智力/固伤/闪避',
};
const ROLE_LABELS = {
physical: '物理输出',
magic: '魔法输出',
healer: '治疗续航',
tank: '承伤防线',
support: '辅助防护',
balanced: '均衡发展',
};
const ROLE_EQUIP_HINTS = {
physical: ['物攻/武器伤害', '力量或灵巧'],
magic: ['魔攻/智力', '魔法词缀'],
healer: ['意志/智力', '治疗与护盾词缀'],
tank: ['体质/意志', '物防/魔防/护盾'],
support: ['意志/智力', '团队增益词缀'],
balanced: ['均衡五维', '按进阶方向调整'],
};
const HEALER_SCHOOLS = new Set(['priest_bishop']);
const TANK_SCHOOLS = new Set([
'defender_bastion', 'defender_paladin', 'defender_chakra',
'priest_dark_templar', 'blacksmith_armorer',
]);
function topGrowthStats(growth, count = 3) {
if (!growth) return [];
return ['str', 'dex', 'con', 'int', 'wil']
.map((key) => ({ key, val: growth[key] || 0 }))
.sort((a, b) => b.val - a.val)
.filter((entry) => entry.val > 0)
.slice(0, count)
.map((entry) => entry.key);
}
function inferBuildRole(schoolKey, t2Key, t1Key) {
if (HEALER_SCHOOLS.has(schoolKey)) return 'healer';
if (schoolKey?.startsWith('mage_') || schoolKey === 'priest_judicator') return 'magic';
if (TANK_SCHOOLS.has(schoolKey) || t2Key === 'defender') return 'tank';
if (schoolKey?.startsWith('priest_')) return 'support';
if (t2Key === 'mage' || t1Key === 'arcanist') return 'magic';
if (['striker', 'assassin', 'ranger', 'forester', 'warrior'].includes(t2Key)) return 'physical';
if (t2Key === 'priest') return 'support';
if (t2Key === 'jeweler') return 'magic';
if (t2Key === 'blacksmith') return 'physical';
return 'balanced';
}
function getCharacterCareerKeys(character) {
return {
schoolKey: character?.career?.primary?.school?.key || character?.schoolKey || '',
t2Key: character?.career?.primary?.t2?.key || character?.classKey || '',
t1Key: character?.career?.primary?.t1?.key || '',
raceKey: character?.raceKey || '',
};
}
function formatCareerSummary(character) {
const parts = [];
const school = character?.career?.primary?.school?.name || character?.schoolName;
const t2 = character?.career?.primary?.t2?.name || character?.className;
const t1 = character?.career?.primary?.t1?.name;
if (school) parts.push(school);
else if (t2) parts.push(t2);
else if (t1) parts.push(t1);
if (character?.raceName) parts.push(character.raceName);
return parts.join(' · ');
}
function buildEquipHints(statKeys, role) {
const hints = [...(ROLE_EQUIP_HINTS[role] || ROLE_EQUIP_HINTS.balanced)];
if (statKeys.includes('dex')) hints.push('会心/闪避');
if (statKeys.includes('con')) hints.push('生存/防具');
if (statKeys.includes('spd')) hints.push('速度词缀');
return [...new Set(hints)].slice(0, 5);
}
function getCharacterPrimaryProfile(character) {
if (!character) return null;
const { schoolKey, t2Key, t1Key, raceKey } = getCharacterCareerKeys(character);
const statKeys = [];
const addKeys = (keys) => {
(keys || []).forEach((key) => {
if (key && !statKeys.includes(key)) statKeys.push(key);
});
};
if (schoolKey && SCHOOL_GROWTH[schoolKey]) addKeys(topGrowthStats(SCHOOL_GROWTH[schoolKey], 3));
else if (t2Key && CLASS_GROWTH[t2Key]) addKeys(topGrowthStats(CLASS_GROWTH[t2Key], 3));
else if (t1Key && CLASS_GROWTH[t1Key]) addKeys(topGrowthStats(CLASS_GROWTH[t1Key], 2));
addKeys(RACE_PRIMARY_STATS[raceKey] || []);
const role = inferBuildRole(schoolKey, t2Key, t1Key);
const primary = statKeys.slice(0, 4);
const labels = primary.map((key) => STAT_KEY_LABEL[key] || key);
const raceNote = RACE_STAT_NOTES[raceKey] || '';
const careerSummary = formatCareerSummary(character);
const equipHints = buildEquipHints(primary, role);
return {
primary,
labels,
role,
roleLabel: ROLE_LABELS[role] || ROLE_LABELS.balanced,
raceKey,
schoolKey,
t2Key,
raceNote,
careerSummary,
equipHints,
summary: labels.length
? `主要属性:${labels.join('、')}${careerSummary ? `(${careerSummary})` : ''}`
: '',
equipSummary: labels.length
? `推荐装备:${labels.join('、')} · ${equipHints.slice(0, 3).join(' · ')}`
: '',
};
}
const STAT_FLAT_AFFIX_CODES = new Set([
'stat_hp_flat', 'stat_str_flat', 'stat_dex_flat', 'stat_con_flat',
'stat_int_flat', 'stat_wil_flat', 'stat_all_flat',
]);
const AFFIX_EFFECT_LABELS = {
weapon_damage_bonus: '伤害',
weapon_attack_flat: '攻击',
weapon_attack_min_flat: '最小伤害',
weapon_attack_max_flat: '最大伤害',
weapon_attack_per_level: '每级攻击',
crit_rate_bonus: '会心',
evasion_rate_bonus: '闪避',
fixed_damage_bonus: '固伤',
splash_damage_bonus: '溅射',
dot_damage_bonus: '持续伤害',
equipment_defense_bonus: '物防',
equipment_magic_defense_bonus: '魔防',
equipment_defense_flat: '物防',
equipment_magic_defense_flat: '魔防',
shield_bonus: '护盾',
fixed_damage_reduction: '固伤减免',
direct_damage_reduction: '直伤减免',
direct_damage_reflect: '直伤反弹',
shield_penetration_bonus: '护盾穿透',
};
const BASE_STAT_EFFECTS = [
{
key: 'hp',
label: 'HP',
lines: [
{ target: '最大生命', formula: '1:1', value: (v) => v },
{ target: '部分被动', formula: '可额外按体质比例提升', value: () => null },
],
},
{
key: 'str',
label: '力量',
lines: [
{ target: '物攻', formula: '×25%', value: (v) => Math.round(v * 0.25) },
{ target: '物理武技', formula: '默认按力量缩放伤害', value: () => null },
],
},
{
key: 'dex',
label: '灵巧',
lines: [
{ target: '会心/闪避', formula: '种族天赋与技能加成', value: () => null },
{ target: '灵巧武技', formula: '部分技能以灵巧计攻', value: () => null, note: '如决斗者疾风连击' },
],
},
{
key: 'con',
label: '体质',
lines: [
{ target: '物防', formula: '1:1', value: (v) => v },
{
target: '回合护盾',
formula: (character) => `体质×${formatShieldCoefficient(character)}`,
value: (v, character) => Math.round(v * getShieldCoefficient(character)),
},
],
},
{
key: 'int',
label: '智力',
lines: [
{ target: '魔攻', formula: '×25%', value: (v) => Math.round(v * 0.25) },
{ target: '魔法武技', formula: '默认按智力(智慧)缩放', value: () => null },
],
},
{
key: 'wil',
label: '意志',
lines: [
{ target: '魔防', formula: '1:1', value: (v) => v },
{ target: '减伤等', formula: '部分种族/技能按意志加成', value: () => null },
],
},
];
const defaultFilterState = () => ({
version: CONFIG_VERSION,
enabled: true,
mode: 'any',
stats: Object.fromEntries(STAT_DEFS.map((s) => [s.key, { active: false }])),
});
function loadFilterState() {
const saved = GM_getValue(FILTER_STORAGE_KEY, null);
const base = defaultFilterState();
if (!saved || (saved.version || 0) < CONFIG_VERSION) return base;
return {
...base,
...saved,
stats: { ...base.stats, ...(saved.stats || {}) },
};
}
function saveFilterState(state) {
state.version = CONFIG_VERSION;
GM_setValue(FILTER_STORAGE_KEY, state);
}
function loadEquipFxState() {
const saved = GM_getValue(EQUIP_FX_STORAGE_KEY, null);
return { enabled: saved?.enabled !== false };
}
function saveEquipFxState(state) {
GM_setValue(EQUIP_FX_STORAGE_KEY, state);
}
let filterState = loadFilterState();
let equipFxState = loadEquipFxState();
let inventoryLookup = new Map();
let inventoryById = new Map();
let inventorySignature = '';
let inventoryFetchPromise = null;
let originalFetchRef = null;
let enhanceTimer = null;
let isEnhancing = false;
let backpackObserver = null;
let charactersCache = [];
let slotNameToKey = new Map();
let skillsCatalogByKey = new Map();
let catalogFetchPromise = null;
let adventurerObserver = null;
let equipBtnTimer = null;
let equipModalEl = null;
let skillModalEl = null;
let sidebarObserver = null;
function escapeHtml(text) {
return String(text ?? '')
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
}
function normalizeItemRarity(rarity) {
return String(rarity || 'common').toLowerCase().replace(/[^a-z_]/g, '') || 'common';
}
function applyEquipFxRootClass() {
document.documentElement.classList.toggle('lx-equip-fx-enabled', equipFxState.enabled);
}
function clearItemFx(container) {
if (!container) return;
container.querySelector('.lx-item-fx-glow')?.remove();
container.classList.remove('lx-item-fx');
[...container.classList].forEach((cls) => {
if (cls.startsWith('rarity-')) container.classList.remove(cls);
});
delete container.dataset.lxRarity;
delete container.dataset.lxItemId;
}
function applyItemFx(container, item) {
if (!container) return;
clearItemFx(container);
if (!item || !equipFxState.enabled) return;
const rarity = normalizeItemRarity(item.rarity);
container.classList.add('lx-item-fx', `rarity-${rarity}`);
container.dataset.lxRarity = rarity;
if (item.id) container.dataset.lxItemId = item.id;
const glow = document.createElement('span');
glow.className = 'lx-item-fx-glow';
glow.setAttribute('aria-hidden', 'true');
container.insertBefore(glow, container.firstChild);
}
function isBackpackCardLocked(card) {
if (!card) return false;
if (card.classList.contains('locked')) return true;
const actions = card.querySelector('.backpack-card-actions');
if (!actions) return false;
return [...actions.querySelectorAll('button')].some((btn) => {
const text = (btn.textContent || '').trim();
return text === '解锁';
});
}
function resolveItemLocked(item, container) {
if (container && isBackpackCardLocked(container)) return true;
return Boolean(item?.locked);
}
function syncItemLockedFlag(item, container) {
if (!item?.id) return item;
const locked = resolveItemLocked(item, container);
if (item.locked === locked) return item;
const next = { ...item, locked };
inventoryById.set(item.id, next);
const key = `${item.name}::${item.rarity || 'common'}`;
const bucket = inventoryLookup.get(key);
if (bucket) {
const idx = bucket.findIndex((entry) => entry.id === item.id);
if (idx >= 0) bucket[idx] = next;
}
return next;
}
function clearItemLockState(container) {
if (!container) return;
container.classList.remove('lx-item-locked');
container.querySelector('.lx-item-lock-badge')?.remove();
container.querySelector('.lx-item-lock-strip')?.remove();
delete container.dataset.lxLocked;
}
function applyItemLockState(container, item) {
if (!container) return;
clearItemLockState(container);
if (!resolveItemLocked(item, container)) return;
container.classList.add('lx-item-locked');
container.dataset.lxLocked = '1';
const strip = document.createElement('span');
strip.className = 'lx-item-lock-strip';
strip.setAttribute('aria-hidden', 'true');
container.appendChild(strip);
const badge = document.createElement('span');
badge.className = 'lx-item-lock-badge';
badge.setAttribute('aria-label', '已锁定');
badge.innerHTML = '🔒已锁定';
container.appendChild(badge);
}
function refreshOpenEquipPickerFx() {
if (!equipModalEl) return;
equipModalEl.querySelectorAll('.lx-equip-option, .lx-equip-current-card, .lx-equip-preview').forEach((el) => {
const item = el.dataset.lxItemId ? inventoryById.get(el.dataset.lxItemId) : null;
if (item) applyItemFx(el, item);
else clearItemFx(el);
if (item) applyItemLockState(el, item);
else clearItemLockState(el);
});
}
function setEquipFxEnabled(enabled) {
equipFxState.enabled = enabled;
saveEquipFxState(equipFxState);
applyEquipFxRootClass();
scheduleEnhanceBackpackItems();
scheduleAdventurerEnhance();
refreshOpenEquipPickerFx();
}
function updateSkillsCatalogCache(skills) {
if (!Array.isArray(skills) || skills.length === 0) return;
skills.forEach((skill) => {
if (skill?.key) skillsCatalogByKey.set(skill.key, skill);
});
}
function extractSkillsCatalogFromResponse(data) {
if (!data || typeof data !== 'object') return null;
if (Array.isArray(data.skills) && data.skills.length > 0) return data.skills;
if (Array.isArray(data.catalog?.skills) && data.catalog.skills.length > 0) return data.catalog.skills;
if (Array.isArray(data.dashboard?.catalog?.skills) && data.dashboard.catalog.skills.length > 0) {
return data.dashboard.catalog.skills;
}
return null;
}
function getSkillDescription(key, character) {
if (!key) return '';
const equipped = (character?.equippedSkills || []).find((skill) => skill.key === key);
if (equipped?.description) return equipped.description;
return skillsCatalogByKey.get(key)?.description || '';
}
function getSkillName(key, character, selectEl) {
if (!key) return '空槽';
const equipped = (character?.equippedSkills || []).find((skill) => skill.key === key);
if (equipped?.name) return equipped.name;
const catalog = skillsCatalogByKey.get(key);
if (catalog?.name) return catalog.name;
const option = selectEl?.selectedOptions?.[0];
return option?.textContent?.trim() || key;
}
function buildPassiveSkillList(character) {
if (!character) return [];
const list = [];
if (character.raceTalentName) {
list.push({
key: `race:${character.raceKey || character.raceName || 'race'}`,
name: character.raceTalentName,
description: character.raceTalentDescription || '',
source: 'race',
});
}
const learned = new Set(character.learnedSkillKeys || []);
[...skillsCatalogByKey.values()]
.filter((skill) => skill.skillType === 'passive' && learned.has(skill.key))
.sort((a, b) => a.name.localeCompare(b.name, 'zh-Hans-CN'))
.forEach((skill) => {
list.push({
key: skill.key,
name: skill.name,
description: skill.description || '',
source: 'skill',
});
});
return list;
}
function findSkillsPanel(view) {
return view?.querySelector('.skills-panel') || null;
}
function getSkillSlotIndex(slot) {
const slots = slot.closest('.skill-slots')?.querySelectorAll('.skill-slot');
if (!slots?.length) return 1;
return [...slots].indexOf(slot) + 1;
}
function updateActiveSlotMeta(slot, character) {
const select = slot.querySelector('select');
const nameEl = slot.querySelector('.lx-skill-slot-name');
const descEl = slot.querySelector('.lx-skill-slot-desc');
if (!select || !nameEl || !descEl) return;
const key = select.value;
if (!key) {
nameEl.textContent = '空槽';
descEl.textContent = '未装配主动技能';
return;
}
nameEl.textContent = getSkillName(key, character, select);
descEl.textContent = getSkillDescription(key, character) || '暂无描述';
}
function updateSkillPickerButton(slot, character) {
const select = slot.querySelector('select');
const picker = slot.querySelector('.lx-skill-picker-btn');
if (!select || !picker) return;
if (select.disabled) {
picker.disabled = true;
return;
}
picker.disabled = false;
const key = select.value;
picker.textContent = key ? getSkillName(key, character, select) : '空槽 · 点击选择';
}
function closeSkillPicker() {
skillModalEl?.remove();
skillModalEl = null;
}
function applySkillSelection(slot, select, key, character) {
select.value = key;
select.dispatchEvent(new Event('change', { bubbles: true }));
updateActiveSlotMeta(slot, character);
updateSkillPickerButton(slot, character);
closeSkillPicker();
}
function openSkillPicker(slot, character) {
const select = slot.querySelector('select');
if (!select || select.disabled) return;
closeSkillPicker();
const slotIndex = getSkillSlotIndex(slot);
const currentKey = select.value;
const backdrop = document.createElement('div');
backdrop.className = 'lx-skill-modal-backdrop modal-backdrop';
backdrop.innerHTML = `
`;
const scroll = backdrop.querySelector('.lx-skill-modal-scroll');
const list = document.createElement('div');
list.className = 'lx-skill-modal-list';
const emptyBtn = document.createElement('button');
emptyBtn.type = 'button';
emptyBtn.className = `lx-skill-option${!currentKey ? ' is-selected' : ''}`;
emptyBtn.innerHTML = `
空槽
不装配主动技能
`;
emptyBtn.addEventListener('click', () => applySkillSelection(slot, select, '', character));
list.appendChild(emptyBtn);
[...select.options].filter((option) => option.value).forEach((option) => {
const key = option.value;
const btn = document.createElement('button');
btn.type = 'button';
btn.className = `lx-skill-option${currentKey === key ? ' is-selected' : ''}`;
btn.disabled = option.disabled;
btn.innerHTML = `
${escapeHtml(option.textContent.trim())}
${escapeHtml(getSkillDescription(key, character) || '暂无描述')}
`;
btn.addEventListener('click', () => {
if (option.disabled) return;
applySkillSelection(slot, select, key, character);
});
list.appendChild(btn);
});
scroll.appendChild(list);
backdrop.addEventListener('click', (event) => {
if (event.target === backdrop) closeSkillPicker();
});
backdrop.querySelector('.lx-skill-modal-close')?.addEventListener('click', closeSkillPicker);
document.body.appendChild(backdrop);
skillModalEl = backdrop;
}
function bindActiveSlotEnhancement(slot, character) {
const select = slot.querySelector('select');
if (!select) return;
select.classList.add('lx-skill-select-hidden');
slot.querySelector('.lx-skill-select-preview')?.remove();
let picker = slot.querySelector('.lx-skill-picker-btn');
if (!picker) {
picker = document.createElement('button');
picker.type = 'button';
picker.className = 'lx-skill-picker-btn';
select.insertAdjacentElement('beforebegin', picker);
}
let meta = slot.querySelector('.lx-skill-slot-meta');
if (!meta) {
meta = document.createElement('div');
meta.className = 'lx-skill-slot-meta';
meta.innerHTML = '
';
select.insertAdjacentElement('afterend', meta);
}
updateSkillPickerButton(slot, character);
updateActiveSlotMeta(slot, character);
if (select.dataset.lxSkillBound === '1') return;
select.dataset.lxSkillBound = '1';
picker.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
if (select.disabled) return;
const current = getSelectedCharacter();
if (!current) return;
openSkillPicker(slot, current);
});
select.addEventListener('change', () => {
const current = getSelectedCharacter();
if (!current) return;
updateActiveSlotMeta(slot, current);
updateSkillPickerButton(slot, current);
});
}
function ensureSkillLayout(panel) {
let layout = panel.querySelector('.lx-skill-layout');
if (!layout) {
layout = document.createElement('div');
layout.className = 'lx-skill-layout';
const activeCol = document.createElement('div');
activeCol.className = 'lx-skill-column lx-skill-column--active';
activeCol.innerHTML = '主动技能
';
const passiveCol = document.createElement('div');
passiveCol.className = 'lx-skill-column lx-skill-column--passive';
passiveCol.innerHTML = `
被动技能
`;
layout.appendChild(activeCol);
layout.appendChild(passiveCol);
const title = panel.querySelector('.skills-panel-title');
if (title) title.insertAdjacentElement('afterend', layout);
else panel.appendChild(layout);
}
const skillSlots = panel.querySelector('.skill-slots');
const activeCol = layout.querySelector('.lx-skill-column--active');
if (skillSlots && activeCol && !activeCol.contains(skillSlots)) {
activeCol.appendChild(skillSlots);
}
return layout;
}
function enhanceSkillEquipment(view, character) {
const panel = findSkillsPanel(view);
if (!panel || !character) return;
ensureSkillLayout(panel);
panel.querySelectorAll('.skill-slot').forEach((slot) => {
bindActiveSlotEnhancement(slot, character);
});
const passives = buildPassiveSkillList(character);
const passiveList = panel.querySelector('.lx-passive-skill-list');
const signature = `${character.id}:${passives.map((item) => `${item.key}:${item.description}`).join('|')}`;
if (passiveList && passiveList.dataset.signature !== signature) {
passiveList.dataset.signature = signature;
passiveList.innerHTML = passives.length
? passives.map((item) => `
${escapeHtml(item.name)}
${escapeHtml(item.description || '暂无描述')}
`).join('')
: '暂无已学习被动
';
}
}
function getItemStatValue(item, statKey) {
if (!item) return 0;
if (statKey === 'spd') return Number(item.bonusSpd) || 0;
return Number(item.bonus?.[statKey]) || 0;
}
function getActiveStatKeys(state = filterState) {
return STAT_DEFS
.filter((def) => state.stats[def.key]?.active)
.map((def) => def.key);
}
function getItemFilterSortValue(item, activeStats) {
return activeStats.reduce((sum, key) => sum + getItemStatValue(item, key), 0);
}
function sortByFilterStats(items, state = filterState, character, slotKey) {
const activeStats = getActiveStatKeys(state);
if (!state.enabled || activeStats.length === 0) return items;
return [...items].sort((a, b) => {
const statDiff = getItemFilterSortValue(b, activeStats) - getItemFilterSortValue(a, activeStats);
if (statDiff !== 0) return statDiff;
if (character) {
const buildDiff = scoreItemForCharacter(b, character, slotKey) - scoreItemForCharacter(a, character, slotKey);
if (buildDiff !== 0) return buildDiff;
}
return scoreItemBase(b) - scoreItemBase(a);
});
}
function itemMatchesFilter(item, state = filterState) {
if (!state.enabled) return true;
const activeStats = getActiveStatKeys(state);
if (activeStats.length === 0) return true;
const hasStat = (key) => getItemStatValue(item, key) > 0;
return state.mode === 'all'
? activeStats.every(hasStat)
: activeStats.some(hasStat);
}
function filterInventory(inventory) {
if (!Array.isArray(inventory) || !filterState.enabled) return inventory;
if (getActiveStatKeys().length === 0) return inventory;
const filtered = inventory.filter((item) => itemMatchesFilter(item));
return sortByFilterStats(filtered);
}
function buildInventorySignature(inventory) {
if (!Array.isArray(inventory)) return '';
return inventory.map((item) => `${item.id}:${item.locked ? 1 : 0}`).sort().join('|');
}
function updateInventoryCache(inventory, { merge = false } = {}) {
if (!Array.isArray(inventory) || inventory.length === 0) return;
let nextInventory = inventory;
if (merge && inventoryById.size > 0) {
const merged = new Map(inventoryById);
inventory.forEach((item) => merged.set(item.id, item));
nextInventory = [...merged.values()];
}
const signature = buildInventorySignature(nextInventory);
if (signature === inventorySignature) return;
inventorySignature = signature;
inventoryById = new Map();
inventoryLookup = new Map();
nextInventory.forEach((item) => {
inventoryById.set(item.id, item);
const key = `${item.name}::${item.rarity || 'common'}`;
if (!inventoryLookup.has(key)) inventoryLookup.set(key, []);
inventoryLookup.get(key).push(item);
});
}
function extractInventoryFromResponse(data) {
if (!data || typeof data !== 'object') return null;
if (Array.isArray(data.inventory)) return data.inventory;
if (Array.isArray(data.dashboard?.inventory)) return data.dashboard.inventory;
return null;
}
function updateCharactersCache(characters) {
if (!Array.isArray(characters)) return;
charactersCache = characters;
}
function mergeCharactersIntoCache(characters) {
if (!Array.isArray(characters) || characters.length === 0) return;
if (!charactersCache.length) {
charactersCache = characters;
return;
}
const byId = new Map(charactersCache.map((entry) => [entry.id, entry]));
characters.forEach((entry) => {
if (entry?.id) byId.set(entry.id, entry);
});
charactersCache = [...byId.values()];
}
function getSlotNameFromKey(slotKey) {
for (const [name, key] of slotNameToKey.entries()) {
if (key === slotKey) return name;
}
return slotKey;
}
function patchCharacterEquipmentSlot(characterId, slotKey, item) {
const index = charactersCache.findIndex((entry) => entry.id === characterId);
if (index < 0) return false;
const character = charactersCache[index];
const equipment = Array.isArray(character.equipment) ? [...character.equipment] : [];
const slotIndex = equipment.findIndex((entry) => entry.slotKey === slotKey);
if (slotIndex >= 0) {
equipment[slotIndex] = { ...equipment[slotIndex], item: item || null };
} else if (item) {
equipment.push({
slotKey,
slotName: getSlotNameFromKey(slotKey),
item,
});
}
const next = [...charactersCache];
next[index] = { ...character, equipment };
charactersCache = next;
return true;
}
function extractCharactersFromResponse(data) {
if (!data || typeof data !== 'object') return null;
if (Array.isArray(data.characters)) return data.characters;
if (Array.isArray(data.dashboard?.characters)) return data.dashboard.characters;
if (data.character && typeof data.character === 'object') return [data.character];
return null;
}
async function ensureCatalogCache() {
if (slotNameToKey.size > 0 && skillsCatalogByKey.size > 0) return;
if (catalogFetchPromise) {
await catalogFetchPromise;
return;
}
const token = pageWindow.localStorage?.getItem('idleforge.token');
if (!token || !originalFetchRef) return;
catalogFetchPromise = originalFetchRef(`${location.origin}/api/catalog`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
}).then(async (response) => {
if (!response.ok) return;
const data = await response.json();
(data.equipmentSlots || []).forEach((slot) => {
slotNameToKey.set(slot.name, slot.key);
});
updateSkillsCatalogCache(data.skills || []);
}).catch(() => {}).finally(() => {
catalogFetchPromise = null;
});
await catalogFetchPromise;
}
function getSlotKeyFromName(slotName) {
return slotNameToKey.get(slotName) || null;
}
function getSelectedCharacter() {
const selected = document.querySelector('.adventurers-view .character-row.selected');
const name = selected?.querySelector('strong')?.textContent?.trim();
if (!name) return charactersCache[0] || null;
return charactersCache.find((character) => character.name === name) || charactersCache[0] || null;
}
function getShieldCoefficient(character) {
const coef = Number(character?.raceShieldCoefficient);
return Number.isFinite(coef) && coef > 0 ? coef : 0.2;
}
function formatShieldCoefficient(character) {
return getShieldCoefficient(character).toFixed(2);
}
function formatAffixEffectValue(effectCode, effectValue) {
const num = Number(effectValue) || 0;
if (!Number.isFinite(num) || num === 0) return '';
const isPercent = /bonus|rate|reduction|reflect|penetration|splash|dot|shield|damage/.test(effectCode || '')
&& !/_flat$/.test(effectCode || '');
if (isPercent) {
const pct = Math.round(num * 1000) / 10;
return `${pct > 0 ? '+' : ''}${pct}%`;
}
if (effectCode === 'weapon_attack_per_level') {
return `${num > 0 ? '+' : ''}${num}/Lv`;
}
return num > 0 ? `+${Math.round(num)}` : `${Math.round(num)}`;
}
function buildItemAffixText(item) {
if (!item) return '';
const parts = [];
(item.affixes || []).forEach((affix) => {
if (STAT_FLAT_AFFIX_CODES.has(affix.effectCode)) return;
const label = AFFIX_EFFECT_LABELS[affix.effectCode] || affix.name;
const value = formatAffixEffectValue(affix.effectCode, affix.effectValue);
if (value) parts.push(`${label} ${value}`);
});
if (item.legendaryEffectName) {
parts.push(`传奇·${item.legendaryEffectName}`);
} else if (item.setName) {
parts.push(`套装·${item.setName}`);
}
return parts.join(' · ');
}
function buildRaceTalentSummary(character) {
if (!character?.raceTalentName) return '';
const race = character.raceName || character.raceKey || '';
const desc = character.raceTalentDescription || '';
return race ? `${race}「${character.raceTalentName}」${desc ? `:${desc}` : ''}` : '';
}
function buildBattleSpeedNote(character) {
const parts = ['战斗「速度」≠灵巧:基础上限 + 装备速度(重武可为负) + 种族天赋等'];
if (character?.raceKey === 'elf') {
parts.push('当前种族:每级 +0.5 速度');
}
return parts.join(';');
}
function formatStatEffectLine(line, statValue, character) {
const formulaText = typeof line.formula === 'function' ? line.formula(character) : line.formula;
const amount = line.value(statValue, character);
if (amount == null) {
return `${line.target}(${formulaText})${line.note ? ` · ${line.note}` : ''}`;
}
return `${line.target} +${amount}(${formulaText})`;
}
function buildStatDerivationRows(character) {
if (!character?.stats) return [];
return BASE_STAT_EFFECTS.map((entry) => {
const statValue = Number(character.stats[entry.key]) || 0;
const effects = entry.lines.map((line) => formatStatEffectLine(line, statValue, character)).join(';');
return { key: entry.key, label: entry.label, value: statValue, effects };
});
}
function findBaseStatsSection(view) {
for (const label of view.querySelectorAll('.mini-label')) {
if (label.textContent.trim() === '基础属性') return label.parentElement;
}
return null;
}
function findEquipmentSection(view) {
for (const heading of view.querySelectorAll('h2')) {
if (heading.textContent.trim() === '装备') return heading.closest('section');
}
const card = view.querySelector('.equipment-slot-card');
return card?.closest('section') || null;
}
function enhancePrimaryStatsBanner(section, profile) {
if (!section) return;
let banner = section.querySelector('.lx-primary-stats-banner');
if (!profile?.primary?.length) {
banner?.remove();
return;
}
const signature = `${profile.primary.join(',')}:${profile.role}:${profile.careerSummary}:${profile.raceNote}`;
if (!banner) {
banner = document.createElement('div');
banner.className = 'lx-primary-stats-banner';
banner.innerHTML = `
主要属性
`;
const derivation = section.querySelector('.lx-stat-derivation');
if (derivation) section.insertBefore(banner, derivation);
else section.appendChild(banner);
}
if (banner.dataset.signature === signature) return;
banner.dataset.signature = signature;
const tagsEl = banner.querySelector('.lx-primary-stats-tags');
if (tagsEl) {
tagsEl.innerHTML = profile.labels.map((label, index) => (
`${label}`
)).join('');
}
const metaEl = banner.querySelector('.lx-primary-stats-meta');
if (metaEl) {
const parts = [];
if (profile.roleLabel) parts.push(profile.roleLabel);
if (profile.careerSummary) parts.push(profile.careerSummary);
if (profile.raceNote) parts.push(`种族侧重:${profile.raceNote}`);
metaEl.textContent = parts.join(' · ');
}
}
function enhanceEquipmentPriority(view, profile) {
const section = findEquipmentSection(view);
if (!section) return;
let panel = section.querySelector('.lx-equip-priority');
if (!profile?.primary?.length) {
panel?.remove();
return;
}
const signature = `${profile.primary.join(',')}:${profile.equipHints.join('|')}:${profile.role}`;
if (!panel) {
panel = document.createElement('div');
panel.className = 'lx-equip-priority';
panel.innerHTML = `
装备推荐属性
`;
const slotsWrap = section.querySelector('.equipment-slot-card')?.parentElement;
if (slotsWrap) section.insertBefore(panel, slotsWrap);
else section.appendChild(panel);
}
if (panel.dataset.signature === signature) return;
panel.dataset.signature = signature;
const tagsEl = panel.querySelector('.lx-equip-priority-tags');
if (tagsEl) {
tagsEl.innerHTML = profile.labels.map((label, index) => (
`${label}`
)).join('');
}
const metaEl = panel.querySelector('.lx-equip-priority-meta');
if (metaEl) {
metaEl.textContent = `${profile.roleLabel} · 优先词缀:${profile.equipHints.join(' · ')} · 装备与更换列表按适配分排序`;
}
}
function enhanceStatDerivation(character) {
const view = document.querySelector('.adventurers-view');
if (!view) return;
const section = findBaseStatsSection(view);
if (!section) return;
const profile = getCharacterPrimaryProfile(character);
enhancePrimaryStatsBanner(section, profile);
section.querySelectorAll('.lx-native-stat-primary').forEach((el) => {
el.classList.remove('lx-native-stat-primary');
});
let panel = section.querySelector('.lx-stat-derivation');
if (!character) {
panel?.remove();
return;
}
const rows = buildStatDerivationRows(character);
const raceText = buildRaceTalentSummary(character);
const speedText = buildBattleSpeedNote(character);
const primaryKey = (profile?.primary || []).join(',');
const signature = `${character.id}:${primaryKey}:${raceText}:${speedText}:${rows.map((row) => `${row.key}=${row.value}`).join('|')}`;
if (!panel) {
panel = document.createElement('div');
panel.className = 'lx-stat-derivation';
panel.innerHTML = `
属性转化
仅统计基础属性对白字战斗属性的贡献,不含装备、词缀与种族百分比天赋。高亮项为当前种族/职业推荐主属性。
`;
section.appendChild(panel);
}
if (panel.dataset.signature === signature) return;
panel.dataset.signature = signature;
const raceEl = panel.querySelector('.lx-stat-derivation-race');
if (raceEl) {
raceEl.textContent = raceText;
raceEl.style.display = raceText ? 'block' : 'none';
}
const speedEl = panel.querySelector('.lx-stat-derivation-speed');
if (speedEl) speedEl.textContent = speedText;
const grid = panel.querySelector('.lx-stat-derivation-grid');
if (!grid) return;
const primarySet = new Set(profile?.primary || []);
grid.innerHTML = rows.map((row) => {
const primaryClass = primarySet.has(row.key) ? ' lx-stat-derivation-row-primary' : '';
const badge = primarySet.has(row.key) ? '主' : '';
return `
${badge}${row.label} ${row.value}
→ ${row.effects}
`;
}).join('');
}
function buildItemDefenseText(item) {
const parts = [];
if (item.equipmentDefense) parts.push(`物防 ${item.equipmentDefense}`);
if (item.equipmentMagicDefense) parts.push(`魔防 ${item.equipmentMagicDefense}`);
return parts.join(' · ');
}
function buildItemInlineText(item) {
const parts = [];
if (item.requiredLevel) parts.push(`Lv.${item.requiredLevel}`);
if (item.subtypeName) parts.push(item.subtypeName);
if (item.armorTypeName) parts.push(item.armorTypeName);
const attack = buildItemAttackText(item);
const defense = buildItemDefenseText(item);
if (attack) parts.push(attack);
if (defense) parts.push(defense);
return parts.join(' · ');
}
function getEquippedItemForSlot(character, slotKey) {
const entry = (character?.equipment || []).find((slot) => slot.slotKey === slotKey);
return entry?.item || null;
}
function renderEquipItemPreview(item, className = 'lx-equip-preview', character, slotKey) {
const profile = character ? getCharacterPrimaryProfile(character) : null;
const wrap = document.createElement('div');
wrap.className = className;
const inline = buildItemInlineText(item);
const bonus = profile ? buildItemBonusHtml(item, profile) : buildItemBonusText(item);
const affixes = buildItemAffixText(item);
const scoreClass = character ? 'lx-item-score lx-item-score--build' : 'lx-item-score lx-item-score--base';
wrap.innerHTML = `
${item.name}
${formatItemScore(item, character, slotKey)}
${inline ? `${inline}` : ''}
${bonus ? `${bonus}
` : ''}
${affixes ? `${affixes}
` : ''}
`;
applyItemFx(wrap, item);
applyItemLockState(wrap, item);
return wrap;
}
function getWarehouseItemsForSlot(slotKey, character) {
const rarityOrder = { orange: 5, gold: 6, purple: 4, blue: 3, green: 2, common: 1 };
const items = [...inventoryById.values()]
.filter((item) => item.slotKey === slotKey && !item.locked);
const activeStats = getActiveStatKeys();
const sortFn = (a, b) => {
if (character) {
const buildDiff = scoreItemForCharacter(b, character, slotKey) - scoreItemForCharacter(a, character, slotKey);
if (buildDiff !== 0) return buildDiff;
}
return (
scoreItemBase(b) - scoreItemBase(a)
|| (b.requiredLevel - a.requiredLevel)
|| ((rarityOrder[b.rarity] || 0) - (rarityOrder[a.rarity] || 0))
|| a.name.localeCompare(b.name, 'zh-Hans-CN')
);
};
if (filterState.enabled && activeStats.length > 0) {
return sortByFilterStats(items.filter((item) => itemMatchesFilter(item)), filterState, character, slotKey);
}
return items.sort(sortFn);
}
function ingestInventoryStatePayload(state) {
if (!state || typeof state !== 'object') return false;
if (Array.isArray(state.inventory)) {
updateInventoryCache(state.inventory, { merge: false });
scheduleEnhanceBackpackItems();
return true;
}
return false;
}
function ingestLootItems(items) {
if (!Array.isArray(items) || items.length === 0) return false;
updateInventoryCache(items, { merge: true });
scheduleEnhanceBackpackItems();
return true;
}
function handleDashboardWsMessage(raw) {
let data;
try {
data = JSON.parse(raw);
} catch {
return;
}
if (!data || typeof data !== 'object') return;
if (data.type === 'inventory_state' && data.inventoryState) {
ingestInventoryStatePayload(data.inventoryState);
return;
}
if (data.type === 'dashboard' && data.dashboard) {
const inventory = extractInventoryFromResponse(data.dashboard);
if (inventory) {
updateInventoryCache(inventory, { merge: false });
scheduleEnhanceBackpackItems();
}
const characters = extractCharactersFromResponse(data.dashboard);
if (characters) updateCharactersCache(characters);
}
}
function extractLootItemsFromPayload(data) {
if (!data || typeof data !== 'object') return [];
const items = [];
if (Array.isArray(data.payload?.items)) items.push(...data.payload.items);
if (Array.isArray(data.items)) items.push(...data.items);
(data.events || []).forEach((event) => {
if (Array.isArray(event?.payload?.items)) items.push(...event.payload.items);
});
return items;
}
function ingestGameResponse(data) {
let updated = false;
const inventory = extractInventoryFromResponse(data);
if (inventory) {
updateInventoryCache(inventory, { merge: false });
scheduleEnhanceBackpackItems();
updated = true;
} else if (Array.isArray(data?.items) && data.items.length > 0) {
updateInventoryCache(data.items, { merge: true });
scheduleEnhanceBackpackItems();
updated = true;
} else if (data?.item?.id) {
updateInventoryCache([data.item], { merge: true });
scheduleEnhanceBackpackItems();
updated = true;
}
const lootItems = extractLootItemsFromPayload(data);
if (lootItems.length > 0 && ingestLootItems(lootItems)) {
updated = true;
}
if (data?.type === 'inventory_state' && ingestInventoryStatePayload(data.inventoryState)) {
updated = true;
}
const characters = extractCharactersFromResponse(data);
if (characters) {
mergeCharactersIntoCache(characters);
scheduleAdventurerEnhance();
updated = true;
}
const skills = extractSkillsCatalogFromResponse(data);
if (skills) {
updateSkillsCatalogCache(skills);
scheduleAdventurerEnhance();
updated = true;
}
return updated;
}
async function refreshInventoryCache() {
if (inventoryFetchPromise) return inventoryFetchPromise;
const token = pageWindow.localStorage?.getItem('idleforge.token');
if (!token || !originalFetchRef) return;
inventoryFetchPromise = originalFetchRef(`${location.origin}/api/me`, {
headers: { Authorization: `Bearer ${token}` },
credentials: 'include',
}).then(async (response) => {
if (!response.ok) return;
const data = await response.json();
if (Array.isArray(data.inventory)) {
updateInventoryCache(data.inventory, { merge: false });
}
if (Array.isArray(data.characters)) {
updateCharactersCache(data.characters);
}
}).catch(() => {}).finally(() => {
inventoryFetchPromise = null;
});
return inventoryFetchPromise;
}
async function ensureInventoryCache() {
if (inventoryById.size > 0) return;
await refreshInventoryCache();
}
function formatStatValue(value) {
const num = Number(value) || 0;
return num > 0 ? `+${num}` : `${num}`;
}
function formatAttackRange(min, max, flat) {
const low = Number(min) || Number(flat) || 0;
const high = Number(max) || Number(flat) || 0;
return low === high ? `${flat || low}` : `${low}-${high}`;
}
function buildItemAttackText(item) {
const parts = [];
if (item.weaponAttack || item.weaponAttackMin || item.weaponAttackMax) {
parts.push(`物攻 ${formatAttackRange(item.weaponAttackMin, item.weaponAttackMax, item.weaponAttack)}`);
}
if (item.magicAttack || item.magicAttackMin || item.magicAttackMax) {
parts.push(`魔攻 ${formatAttackRange(item.magicAttackMin, item.magicAttackMax, item.magicAttack)}`);
}
return parts.join(' · ');
}
function buildItemBonusText(item, profile) {
const primarySet = new Set(profile?.primary || []);
const parts = [];
STAT_DEFS.forEach((def) => {
const value = getItemStatValue(item, def.key);
if (value !== 0) {
const mark = primarySet.has(def.key) ? '★' : '';
parts.push(`${mark}${def.label} ${formatStatValue(value)}`);
}
});
return parts.join(' · ');
}
function buildItemBonusHtml(item, profile) {
const primarySet = new Set(profile?.primary || []);
const parts = [];
STAT_DEFS.forEach((def) => {
const value = getItemStatValue(item, def.key);
if (value !== 0) {
const cls = primarySet.has(def.key) ? 'lx-stat-primary' : '';
parts.push(`${def.label} ${formatStatValue(value)}`);
}
});
return parts.join(' · ');
}
function upsertItemMeta(head, anchorEl, className, text) {
let el = head.querySelector(`.${className}`);
if (!text) {
el?.remove();
return null;
}
if (!el) {
el = document.createElement('span');
el.className = className;
anchorEl.insertAdjacentElement('afterend', el);
} else if (el.previousElementSibling !== anchorEl) {
anchorEl.insertAdjacentElement('afterend', el);
}
if (el.textContent !== text) {
el.textContent = text;
}
return el;
}
function findItemFromCard(nameEl, usedIds, card) {
const name = nameEl.textContent.trim();
const rarityMatch = nameEl.className.match(/rarity-([a-z_]+)/i);
const rarity = rarityMatch ? rarityMatch[1] : 'common';
const candidates = inventoryLookup.get(`${name}::${rarity}`) || [];
const domLocked = card ? isBackpackCardLocked(card) : null;
if (domLocked !== null && candidates.length > 0) {
const lockedMatch = candidates.find((item) => !usedIds.has(item.id) && Boolean(item.locked) === domLocked);
if (lockedMatch) return lockedMatch;
}
return candidates.find((item) => !usedIds.has(item.id)) || null;
}
function closeEquipPicker() {
equipModalEl?.remove();
equipModalEl = null;
}
async function openEquipPicker(slotKey, slotName) {
await ensureInventoryCache();
await ensureCatalogCache();
const character = getSelectedCharacter();
if (!character) {
window.alert('请先选择冒险者');
return;
}
const items = getWarehouseItemsForSlot(slotKey, character);
const currentItem = getEquippedItemForSlot(character, slotKey);
const profile = getCharacterPrimaryProfile(character);
closeEquipPicker();
const backdrop = document.createElement('div');
backdrop.className = 'lx-equip-modal-backdrop modal-backdrop';
backdrop.innerHTML = `
`;
const currentHost = backdrop.querySelector('.lx-equip-modal-current');
const scrollHost = backdrop.querySelector('.lx-equip-modal-scroll');
const currentSection = document.createElement('div');
currentSection.className = 'lx-equip-current-section';
currentSection.innerHTML = '当前装备
';
if (currentItem) {
currentSection.appendChild(renderEquipItemPreview(currentItem, 'lx-equip-current-card', character, slotKey));
} else {
const empty = document.createElement('p');
empty.className = 'lx-equip-empty-current';
empty.textContent = '当前未装备';
currentSection.appendChild(empty);
}
currentHost.appendChild(currentSection);
const warehouseSection = document.createElement('div');
warehouseSection.className = 'lx-equip-warehouse-section';
warehouseSection.innerHTML = '仓库可选
';
if (items.length === 0) {
const empty = document.createElement('p');
empty.className = 'lx-equip-empty';
empty.textContent = '仓库中没有可装备的' + slotName + '。';
warehouseSection.appendChild(empty);
} else {
const list = document.createElement('div');
list.className = 'lx-equip-list';
items.forEach((item) => {
const canEquip = character.level >= item.requiredLevel;
const bonus = profile ? buildItemBonusHtml(item, profile) : buildItemBonusText(item);
const affixes = buildItemAffixText(item);
const delta = formatItemScoreDelta(item, currentItem, character, slotKey);
const deltaClass = !delta ? '' : delta.startsWith('+') ? 'lx-item-score-delta--up' : delta === '持平' ? 'lx-item-score-delta--flat' : 'lx-item-score-delta--down';
const button = document.createElement('button');
button.type = 'button';
button.className = 'lx-equip-option';
button.disabled = !canEquip;
button.innerHTML = `
${item.name}
${formatItemScore(item, character, slotKey)}
${delta ? `${delta}` : ''}
${buildItemInlineText(item)}
${bonus ? `${bonus}` : ''}
${affixes ? `${affixes}` : ''}
${canEquip ? `需求 Lv.${item.requiredLevel}` : `需要 Lv.${item.requiredLevel}`}
`;
button.addEventListener('click', async () => {
if (!canEquip) return;
button.disabled = true;
try {
await apiEquipItem(character.id, slotKey, item.id);
closeEquipPicker();
} catch (error) {
button.disabled = false;
window.alert(error?.message || '装备失败');
}
});
list.appendChild(button);
applyItemFx(button, item);
applyItemLockState(button, item);
});
warehouseSection.appendChild(list);
}
scrollHost.appendChild(warehouseSection);
backdrop.addEventListener('click', (event) => {
if (event.target === backdrop) closeEquipPicker();
});
backdrop.querySelector('.lx-equip-modal-close')?.addEventListener('click', closeEquipPicker);
document.body.appendChild(backdrop);
equipModalEl = backdrop;
}
async function apiEquipItem(characterId, slotKey, itemId) {
const token = pageWindow.localStorage?.getItem('idleforge.token');
if (!token || !originalFetchRef) throw new Error('未登录');
const equippedItem = inventoryById.get(itemId) || null;
const response = await originalFetchRef(`${location.origin}/api/characters/${characterId}/equipment`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({ slotKey, itemId }),
});
const data = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(data.error || '装备失败');
}
ingestGameResponse(data);
const characters = extractCharactersFromResponse(data);
if (characters) {
mergeCharactersIntoCache(characters);
} else if (equippedItem) {
patchCharacterEquipmentSlot(characterId, slotKey, equippedItem);
}
enhanceCharacterRows();
scheduleAdventurerEnhance();
triggerDashboardRefresh();
}
function triggerDashboardRefresh() {
const refreshBtn = document.querySelector('.icon-button[title="刷新"]');
if (refreshBtn && !refreshBtn.disabled) {
refreshBtn.click();
return;
}
location.reload();
}
function scheduleAdventurerEnhance() {
if (equipBtnTimer) clearTimeout(equipBtnTimer);
equipBtnTimer = setTimeout(() => {
equipBtnTimer = null;
void enhanceAdventurerView();
}, 120);
}
function findEquippedItemFromSlotCard(card, character) {
const slotName = card.querySelector('strong')?.textContent?.trim();
const slotKey = getSlotKeyFromName(slotName);
if (!slotKey || !character) return null;
return getEquippedItemForSlot(character, slotKey);
}
function ensureNameRow(head, nameEl) {
if (nameEl.parentElement?.classList.contains('lx-item-name-row')) {
return nameEl.parentElement;
}
const row = document.createElement('div');
row.className = 'lx-item-name-row';
nameEl.parentElement.insertBefore(row, nameEl);
row.appendChild(nameEl);
return row;
}
function clearItemMeta(container) {
if (!container) return;
clearItemFx(container);
clearItemLockState(container);
container.querySelector('.lx-item-extra')?.remove();
container.querySelector('.lx-item-attacks')?.remove();
container.querySelector('.lx-item-score')?.remove();
container.querySelector('.lx-item-stats')?.remove();
const nameRow = container.querySelector('.lx-item-name-row');
if (nameRow) {
const nameEl = nameRow.querySelector('.equipment-name');
if (nameEl) nameRow.parentElement.insertBefore(nameEl, nameRow);
nameRow.remove();
}
}
function applyItemMeta(head, nameEl, item, character, slotKey) {
const profile = character ? getCharacterPrimaryProfile(character) : null;
const inlineText = buildItemInlineText(item);
const bonusText = profile ? buildItemBonusHtml(item, profile) : buildItemBonusText(item);
const affixText = buildItemAffixText(item);
const scoreText = formatItemScore(item, character, slotKey);
const scoreClass = character ? 'lx-item-score lx-item-score--build' : 'lx-item-score lx-item-score--base';
head.querySelector('.lx-item-attacks')?.remove();
head.querySelector('.lx-item-stats:not(.lx-item-extra .lx-item-stats)')?.remove();
const nameRow = ensureNameRow(head, nameEl);
let scoreEl = nameRow.querySelector('.lx-item-score');
if (!scoreText) {
scoreEl?.remove();
} else {
if (!scoreEl) {
scoreEl = document.createElement('span');
nameEl.insertAdjacentElement('afterend', scoreEl);
} else if (scoreEl.previousElementSibling !== nameEl) {
nameEl.insertAdjacentElement('afterend', scoreEl);
}
scoreEl.className = scoreClass;
if (scoreEl.textContent !== scoreText) scoreEl.textContent = scoreText;
}
if (!inlineText && !bonusText && !affixText) {
head.querySelector('.lx-item-extra')?.remove();
return;
}
let extra = head.querySelector('.lx-item-extra');
if (!extra) {
extra = document.createElement('div');
extra.className = 'lx-item-extra';
head.appendChild(extra);
}
let line2 = extra.querySelector('.lx-item-line2');
if (inlineText) {
if (!line2) {
line2 = document.createElement('div');
line2.className = 'lx-item-line2';
extra.appendChild(line2);
}
if (line2.textContent !== inlineText) line2.textContent = inlineText;
} else {
line2?.remove();
}
let statsEl = extra.querySelector('.lx-item-stats');
if (bonusText) {
if (!statsEl) {
statsEl = document.createElement('div');
statsEl.className = 'lx-item-stats';
extra.appendChild(statsEl);
}
if (profile && statsEl.innerHTML !== bonusText) {
statsEl.innerHTML = bonusText;
} else if (!profile && statsEl.textContent !== bonusText) {
statsEl.textContent = bonusText;
}
} else {
statsEl?.remove();
}
let affixEl = extra.querySelector('.lx-item-affixes');
if (affixText) {
if (!affixEl) {
affixEl = document.createElement('div');
affixEl.className = 'lx-item-affixes';
extra.appendChild(affixEl);
}
if (affixEl.textContent !== affixText) affixEl.textContent = affixText;
} else {
affixEl?.remove();
}
}
function enhanceCharacterRows() {
const view = document.querySelector('.adventurers-view');
if (!view) return;
view.querySelectorAll('.character-row').forEach((row) => {
const name = row.querySelector('strong')?.textContent?.trim();
const character = charactersCache.find((entry) => entry.name === name);
const lvEl = row.querySelector(':scope > b');
if (!lvEl) return;
let wrap = lvEl.parentElement;
if (!wrap.classList.contains('lx-char-lv-wrap')) {
wrap = document.createElement('span');
wrap.className = 'lx-char-lv-wrap';
lvEl.parentElement.insertBefore(wrap, lvEl);
wrap.appendChild(lvEl);
}
let scoreEl = wrap.querySelector('.lx-char-equip-score');
if (!scoreEl) {
scoreEl = document.createElement('span');
scoreEl.className = 'lx-char-equip-score';
wrap.appendChild(scoreEl);
}
if (!character) {
scoreEl.textContent = '';
return;
}
const text = formatCharacterEquipScore(character);
if (scoreEl.textContent !== text) scoreEl.textContent = text;
});
}
async function injectEquipSwapButtons() {
const view = document.querySelector('.adventurers-view');
if (!view) return;
await ensureCatalogCache();
view.querySelectorAll('.equipment-slot-card').forEach((card) => {
const strong = card.querySelector('strong');
const header = strong?.parentElement;
if (!strong || !header) return;
header.classList.add('lx-equip-slot-head');
let actions = header.querySelector('.lx-equip-slot-actions');
if (!actions) {
actions = document.createElement('div');
actions.className = 'lx-equip-slot-actions';
header.appendChild(actions);
}
const unequipBtn = header.querySelector('.equipment-unequip-button');
if (unequipBtn && unequipBtn.parentElement !== actions) {
actions.appendChild(unequipBtn);
}
let swapBtn = actions.querySelector('.lx-equip-swap-btn');
if (!swapBtn) {
const slotName = strong.textContent.trim();
const slotKey = getSlotKeyFromName(slotName);
if (!slotKey) return;
swapBtn = document.createElement('button');
swapBtn.type = 'button';
swapBtn.className = 'lx-equip-swap-btn';
swapBtn.textContent = '更换';
swapBtn.dataset.slotKey = slotKey;
swapBtn.dataset.slotName = slotName;
swapBtn.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
openEquipPicker(slotKey, slotName);
});
actions.insertBefore(swapBtn, unequipBtn || null);
}
});
}
async function enhanceAdventurerView() {
const view = document.querySelector('.adventurers-view');
if (!view) return;
await ensureCatalogCache();
await ensureInventoryCache();
await injectEquipSwapButtons();
const character = getSelectedCharacter();
view.querySelectorAll('.equipment-slot-card').forEach((card) => {
if (card.classList.contains('empty')) {
clearItemMeta(card);
return;
}
const nameEl = card.querySelector('.equipment-name');
if (!nameEl) return;
const item = findEquippedItemFromSlotCard(card, character);
if (!item) {
clearItemMeta(card);
return;
}
const slotName = card.querySelector('strong')?.textContent?.trim();
const slotKey = getSlotKeyFromName(slotName);
applyItemMeta(card, nameEl, item, character, slotKey);
applyItemFx(card, item);
applyItemLockState(card, item);
});
enhanceCharacterRows();
const profile = getCharacterPrimaryProfile(character);
enhanceStatDerivation(character);
enhanceEquipmentPriority(view, profile);
enhanceSkillEquipment(view, character);
}
function observeAdventurerView() {
const view = document.querySelector('.adventurers-view');
if (!view) {
if (adventurerObserver) {
adventurerObserver.disconnect();
adventurerObserver = null;
}
return;
}
if (adventurerObserver) return;
adventurerObserver = new MutationObserver((mutations) => {
if (!shouldHandleMutations(mutations)) return;
scheduleAdventurerEnhance();
});
adventurerObserver.observe(view, { childList: true, subtree: true });
}
function onAdventurerReady() {
observeAdventurerView();
scheduleAdventurerEnhance();
}
function scheduleEnhanceBackpackItems() {
if (enhanceTimer) clearTimeout(enhanceTimer);
enhanceTimer = setTimeout(() => {
enhanceTimer = null;
enhanceBackpackItems();
}, 120);
}
function isOwnMutationNode(node) {
if (!node || node.nodeType !== 1) return false;
if (node.classList?.contains('lx-item-stats')) return true;
if (node.classList?.contains('lx-item-attacks')) return true;
if (node.classList?.contains('lx-item-score')) return true;
if (node.classList?.contains('lx-item-score-delta')) return true;
if (node.classList?.contains('lx-item-name-row')) return true;
if (node.classList?.contains('lx-item-extra')) return true;
if (node.classList?.contains('lx-item-line2')) return true;
if (node.classList?.contains('lx-item-affixes')) return true;
if (node.classList?.contains('lx-char-equip-score')) return true;
if (node.classList?.contains('lx-char-lv-wrap')) return true;
if (node.classList?.contains('lx-equip-swap-btn')) return true;
if (node.classList?.contains('lx-equip-slot-head')) return true;
if (node.classList?.contains('lx-equip-slot-actions')) return true;
if (node.classList?.contains('lx-equip-modal-backdrop')) return true;
if (node.classList?.contains('lx-skill-modal-backdrop')) return true;
if (node.classList?.contains('lx-skill-picker-btn')) return true;
if (node.classList?.contains('lx-stat-derivation')) return true;
if (node.classList?.contains('lx-primary-stats-banner')) return true;
if (node.classList?.contains('lx-equip-priority')) return true;
if (node.classList?.contains('lx-skill-layout')) return true;
if (node.classList?.contains('lx-skill-column')) return true;
if (node.classList?.contains('lx-skill-slot-meta')) return true;
if (node.classList?.contains('lx-passive-skill-list')) return true;
if (node.classList?.contains('lx-passive-skill-item')) return true;
if (node.classList?.contains('lx-skill-option')) return true;
if (node.classList?.contains('lx-sidebar-meta')) return true;
if (node.classList?.contains('lx-sidebar-account-bar')) return true;
if (node.classList?.contains('lx-sidebar-prefs')) return true;
if (node.classList?.contains('lx-equip-fx-toggle')) return true;
if (node.classList?.contains('lx-item-fx-glow')) return true;
if (node.classList?.contains('lx-item-lock-badge')) return true;
if (node.classList?.contains('lx-item-lock-strip')) return true;
if (node.classList?.contains('lx-item-lock-icon')) return true;
if (node.classList?.contains('lx-primary-stat-tag')) return true;
if (node.id === 'lx-guild-stat-filter') return true;
return Boolean(node.closest?.('#lx-guild-stat-filter, .lx-item-stats, .lx-item-attacks, .lx-item-score, .lx-item-score-delta, .lx-stat-primary, .lx-item-name-row, .lx-item-extra, .lx-item-line2, .lx-item-affixes, .lx-char-equip-score, .lx-char-lv-wrap, .lx-equip-modal-backdrop, .lx-skill-modal-backdrop, .lx-equip-modal-profile, .lx-equip-swap-btn, .lx-equip-slot-head, .lx-equip-slot-actions, .lx-skill-picker-btn, .lx-stat-derivation, .lx-primary-stats-banner, .lx-equip-priority, .lx-primary-stat-tag, .lx-skill-layout, .lx-skill-column, .lx-skill-slot-meta, .lx-passive-skill-list, .lx-passive-skill-item, .lx-skill-option, .lx-sidebar-prefs, .lx-equip-fx-toggle, .lx-item-lock-badge, .lx-item-lock-strip, .lx-item-lock-icon'));
}
function shouldHandleMutations(mutations) {
return mutations.some((mutation) => {
if (isOwnMutationNode(mutation.target)) return false;
for (const node of mutation.addedNodes) {
if (!isOwnMutationNode(node)) return true;
}
for (const node of mutation.removedNodes) {
if (!isOwnMutationNode(node)) return true;
}
return mutation.type === 'characterData';
});
}
async function enhanceBackpackItems() {
if (isEnhancing) return;
const backpackView = document.querySelector('.backpack-view');
if (!backpackView) return;
isEnhancing = true;
if (backpackObserver) backpackObserver.disconnect();
try {
for (let attempt = 0; attempt < 4; attempt += 1) {
if (attempt > 0) {
inventorySignature = '';
await refreshInventoryCache();
await new Promise((resolve) => {
window.setTimeout(resolve, 350 * attempt);
});
} else {
await ensureInventoryCache();
}
const usedIds = new Set();
let unmatched = 0;
const cards = backpackView.querySelectorAll('.backpack-item:not(.material)');
cards.forEach((card) => {
const head = card.querySelector('.backpack-item-head');
const nameEl = head?.querySelector('.equipment-name');
if (!head || !nameEl) return;
const item = findItemFromCard(nameEl, usedIds, card);
if (!item) {
applyItemLockState(card, null);
unmatched += 1;
return;
}
const syncedItem = syncItemLockedFlag(item, card);
usedIds.add(syncedItem.id);
applyItemMeta(head, nameEl, syncedItem);
applyItemFx(card, syncedItem);
applyItemLockState(card, syncedItem);
});
if (unmatched === 0) break;
}
} finally {
isEnhancing = false;
observeBackpackView();
}
}
function observeBackpackView() {
const backpackView = document.querySelector('.backpack-view');
if (!backpackView) {
if (backpackObserver) {
backpackObserver.disconnect();
backpackObserver = null;
}
return;
}
if (backpackObserver) return;
backpackObserver = new MutationObserver((mutations) => {
if (!shouldHandleMutations(mutations)) return;
scheduleEnhanceBackpackItems();
});
backpackObserver.observe(backpackView, { childList: true, subtree: true, characterData: true, attributes: true });
if (!backpackView.dataset.lxLockWatch) {
backpackView.dataset.lxLockWatch = '1';
backpackView.addEventListener('click', (event) => {
const btn = event.target.closest('.backpack-card-actions button');
if (!btn) return;
const text = (btn.textContent || '').trim();
if (text !== '锁定' && text !== '解锁') return;
window.setTimeout(() => scheduleEnhanceBackpackItems(), 60);
void refreshInventoryCache().then(() => scheduleEnhanceBackpackItems());
}, true);
}
}
function patchMeResponse(data) {
if (!data || typeof data !== 'object' || !Array.isArray(data.inventory)) return data;
updateInventoryCache(data.inventory, { merge: false });
if (Array.isArray(data.characters)) updateCharactersCache(data.characters);
scheduleEnhanceBackpackItems();
scheduleAdventurerEnhance();
const filtered = filterInventory(data.inventory);
if (filtered.length === data.inventory.length) return data;
return { ...data, inventory: filtered };
}
function installNetworkFilter() {
if (pageWindow.__lxGuildFilterInstalled) return;
pageWindow.__lxGuildFilterInstalled = true;
originalFetchRef = pageWindow.fetch.bind(pageWindow);
pageWindow.fetch = async function patchedFetch(input, init) {
const response = await originalFetchRef(input, init);
const url = typeof input === 'string' ? input : input?.url || '';
try {
const contentType = response.headers.get('Content-Type') || '';
if (!contentType.includes('application/json')) return response;
const data = await response.clone().json();
ingestGameResponse(data);
if (!url.includes('/api/me')) return response;
const patched = patchMeResponse(data);
if (patched === data) return response;
return new Response(JSON.stringify(patched), {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
} catch {
return response;
}
};
const OriginalXHR = pageWindow.XMLHttpRequest;
function PatchedXHR() {
const xhr = new OriginalXHR();
let requestUrl = '';
const originalOpen = xhr.open;
xhr.open = function open(method, url, ...rest) {
requestUrl = String(url || '');
return originalOpen.call(this, method, url, ...rest);
};
const originalSend = xhr.send;
xhr.send = function send(...args) {
xhr.addEventListener('load', function onLoad() {
try {
const data = JSON.parse(xhr.responseText);
ingestGameResponse(data);
if (!requestUrl.includes('/api/me')) return;
const patched = patchMeResponse(data);
if (patched !== data) {
Object.defineProperty(xhr, 'responseText', { value: JSON.stringify(patched) });
Object.defineProperty(xhr, 'response', { value: JSON.stringify(patched) });
}
} catch {
// ignore
}
});
return originalSend.apply(this, args);
};
return xhr;
}
PatchedXHR.prototype = OriginalXHR.prototype;
pageWindow.XMLHttpRequest = PatchedXHR;
}
function wrapRealtimeListener(type, listener) {
const invoke = (event) => {
if (typeof listener === 'function') listener.call(this, event);
else if (listener && typeof listener.handleEvent === 'function') listener.handleEvent(event);
};
return function wrappedRealtimeListener(event) {
try {
const url = String(this.url || '');
if (type === 'message' && url.includes('/api/dashboard/ws')) {
handleDashboardWsMessage(event.data);
}
if (type === 'battle' && url.includes('/api/battle/events')) {
const data = JSON.parse(event.data);
ingestLootItems(extractLootItemsFromPayload(data));
}
} catch (_) {}
invoke.call(this, event);
};
}
function installRealtimeHooks() {
if (pageWindow.__lxGuildRealtimeInstalled) return;
pageWindow.__lxGuildRealtimeInstalled = true;
const wsAdd = pageWindow.WebSocket.prototype.addEventListener;
pageWindow.WebSocket.prototype.addEventListener = function lxWsAdd(type, listener, options) {
if (type === 'message' && listener) {
return wsAdd.call(this, type, wrapRealtimeListener.call(this, type, listener), options);
}
return wsAdd.call(this, type, listener, options);
};
const esAdd = pageWindow.EventSource.prototype.addEventListener;
pageWindow.EventSource.prototype.addEventListener = function lxEsAdd(type, listener, options) {
if (type === 'battle' && listener) {
return esAdd.call(this, type, wrapRealtimeListener.call(this, type, listener), options);
}
return esAdd.call(this, type, listener, options);
};
}
installNetworkFilter();
installRealtimeHooks();
function triggerPageRefresh() {
triggerDashboardRefresh();
}
function buildFilterSummary(state = filterState) {
if (!state.enabled) return '未启用,显示全部装备';
const activeStats = getActiveStatKeys(state);
if (activeStats.length === 0) return '已启用,未选属性,显示全部装备';
const modeLabel = state.mode === 'all' ? '同时含有' : '含有任一';
const labels = activeStats.map((key) => {
const def = STAT_DEFS.find((s) => s.key === key);
return def?.label || key;
});
return `${modeLabel}:${labels.join('、')}`;
}
function applyFilterFromUI(root) {
filterState = readFilterFromUI(root);
saveFilterState(filterState);
const summaryEl = root.querySelector('.lx-filter-summary');
if (summaryEl) {
summaryEl.textContent = buildFilterSummary();
summaryEl.classList.toggle('active', filterState.enabled);
}
triggerPageRefresh();
}
function injectStyles() {
GM_addStyle(`
:root,
html[data-rift-theme] {
--lx-surface: var(--panel-2, rgba(248, 241, 229, .9));
--lx-surface-strong: var(--panel, rgba(255, 252, 244, .94));
--lx-border: var(--line-strong, rgba(82, 68, 51, .28));
--lx-border-soft: var(--line, rgba(82, 68, 51, .16));
--lx-fg: var(--text, #2e281f);
--lx-fg-soft: var(--soft, #4d463a);
--lx-fg-muted: var(--muted, #746a5a);
--lx-accent: var(--accent, #4f8068);
--lx-accent-2: var(--accent-2, #b8793f);
--lx-success: var(--green, #5e8d67);
--lx-accent-bg: color-mix(in srgb, var(--lx-accent) 12%, var(--lx-surface));
--lx-accent-bg-strong: color-mix(in srgb, var(--lx-accent) 20%, var(--lx-surface));
--lx-accent-border: color-mix(in srgb, var(--lx-accent) 38%, var(--lx-border));
--lx-tag-bg: color-mix(in srgb, var(--lx-accent) 14%, var(--lx-surface));
--lx-tag-border: color-mix(in srgb, var(--lx-accent) 32%, var(--lx-border));
--lx-highlight-bg: color-mix(in srgb, var(--lx-accent) 10%, transparent);
--lx-highlight-border: color-mix(in srgb, var(--lx-accent) 42%, var(--lx-border-soft));
}
@keyframes lx-rarity-glow {
0%, 100% {
opacity: 0.22;
filter: brightness(1) blur(12px);
transform: scale(0.98);
}
50% {
opacity: 0.42;
filter: brightness(1.12) blur(14px);
transform: scale(1.01);
}
}
@keyframes lx-rarity-shimmer {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 200% 50%; }
}
@keyframes lx-rarity-border-spin {
0% { --lx-fx-angle: 0deg; }
100% { --lx-fx-angle: 360deg; }
}
@keyframes lx-rarity-pulse-shadow {
0%, 100% {
box-shadow:
0 0 0 1px color-mix(in srgb, var(--lx-fx-1) 28%, transparent),
0 0 10px color-mix(in srgb, var(--lx-fx-glow) 22%, transparent),
0 0 22px color-mix(in srgb, var(--lx-fx-glow) 14%, transparent),
0 6px 18px color-mix(in srgb, var(--lx-fx-2) 12%, transparent),
inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
50% {
box-shadow:
0 0 0 1px color-mix(in srgb, var(--lx-fx-2) 36%, transparent),
0 0 14px color-mix(in srgb, var(--lx-fx-glow) 32%, transparent),
0 0 30px color-mix(in srgb, var(--lx-fx-glow) 18%, transparent),
0 8px 24px color-mix(in srgb, var(--lx-fx-2) 16%, transparent),
inset 0 1px 0 rgba(255, 255, 255, 0.12);
}
}
@property --lx-fx-angle {
syntax: '';
initial-value: 0deg;
inherits: false;
}
html.lx-equip-fx-enabled .lx-item-fx {
position: relative;
isolation: isolate;
overflow: hidden;
transition: box-shadow 0.3s ease, transform 0.3s ease, border-color 0.3s ease, filter 0.3s ease;
animation: lx-rarity-pulse-shadow 3.6s ease-in-out infinite;
}
html.lx-equip-fx-enabled .lx-item-fx-glow {
position: absolute;
inset: -3px;
border-radius: inherit;
pointer-events: none;
z-index: 0;
opacity: 0.36;
background: conic-gradient(
from var(--lx-fx-angle),
color-mix(in srgb, var(--lx-fx-1) 72%, transparent),
color-mix(in srgb, var(--lx-fx-2) 72%, transparent),
color-mix(in srgb, var(--lx-fx-3, var(--lx-fx-2)) 72%, transparent),
color-mix(in srgb, var(--lx-fx-1) 72%, transparent)
);
filter: blur(14px);
animation: lx-rarity-glow 3.2s ease-in-out infinite, lx-rarity-border-spin 8s linear infinite;
}
html.lx-equip-fx-enabled .backpack-view .backpack-item.lx-item-fx:hover,
html.lx-equip-fx-enabled .adventurers-view .equipment-slot-card.lx-item-fx:hover,
html.lx-equip-fx-enabled .lx-equip-option.lx-item-fx:hover:not(:disabled) {
transform: translateY(-2px) scale(1.005);
filter: brightness(1.02);
}
html.lx-equip-fx-enabled .lx-item-fx::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
z-index: 0;
background:
radial-gradient(ellipse 85% 65% at 12% 0%, rgba(255, 255, 255, 0.08), transparent 58%),
linear-gradient(145deg, color-mix(in srgb, var(--lx-fx-2) 8%, transparent), transparent 42%),
linear-gradient(320deg, color-mix(in srgb, var(--lx-fx-1) 6%, transparent), transparent 55%);
opacity: 0.32;
}
html.lx-equip-fx-enabled .lx-item-fx::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
z-index: 0;
padding: 2px;
background: linear-gradient(
120deg,
color-mix(in srgb, var(--lx-fx-1) 85%, transparent),
color-mix(in srgb, var(--lx-fx-2) 85%, transparent),
color-mix(in srgb, var(--lx-fx-3, var(--lx-fx-2)) 85%, transparent),
rgba(255, 255, 255, 0.35),
color-mix(in srgb, var(--lx-fx-2) 85%, transparent),
color-mix(in srgb, var(--lx-fx-1) 85%, transparent)
);
background-size: 300% 300%;
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
opacity: 0.62;
animation: lx-rarity-shimmer 4s linear infinite;
}
html.lx-equip-fx-enabled .backpack-view .backpack-item.lx-item-fx .backpack-item-head,
html.lx-equip-fx-enabled .backpack-view .backpack-item.lx-item-fx .backpack-card-actions,
html.lx-equip-fx-enabled .adventurers-view .equipment-slot-card.lx-item-fx > :not(.lx-item-fx-glow),
html.lx-equip-fx-enabled .lx-equip-current-card.lx-item-fx > :not(.lx-item-fx-glow),
html.lx-equip-fx-enabled .lx-equip-preview.lx-item-fx > :not(.lx-item-fx-glow),
html.lx-equip-fx-enabled .lx-equip-option.lx-item-fx > :not(.lx-item-fx-glow) {
position: relative;
z-index: 1;
background: transparent !important;
box-shadow: none !important;
}
html.lx-equip-fx-enabled .lx-item-fx[data-lx-rarity="common"] {
--lx-fx-1: #8a8278;
--lx-fx-2: #c8c0b4;
--lx-fx-3: #6e6860;
--lx-fx-glow: #a09890;
--lx-fx-bg: rgba(138, 130, 120, 0.06);
animation: none;
}
html.lx-equip-fx-enabled .lx-item-fx[data-lx-rarity="common"] .lx-item-fx-glow {
opacity: 0.2;
filter: blur(8px);
animation: none;
}
html.lx-equip-fx-enabled .lx-item-fx[data-lx-rarity="common"]::after {
animation: none;
opacity: 0.45;
padding: 1px;
}
html.lx-equip-fx-enabled .lx-item-fx[data-lx-rarity="green"] {
--lx-fx-1: #2f8f4a;
--lx-fx-2: #7dff9a;
--lx-fx-3: #1f6b38;
--lx-fx-glow: #52d97a;
--lx-fx-bg: rgba(63, 145, 88, 0.08);
}
html.lx-equip-fx-enabled .lx-item-fx[data-lx-rarity="blue"] {
--lx-fx-1: #2f66b8;
--lx-fx-2: #7ec8ff;
--lx-fx-3: #1a4f96;
--lx-fx-glow: #4da3ff;
--lx-fx-bg: rgba(63, 116, 184, 0.08);
}
html.lx-equip-fx-enabled .lx-item-fx[data-lx-rarity="blue"]::after,
html.lx-equip-fx-enabled .lx-item-fx[data-lx-rarity="green"]::after {
animation-duration: 5s;
}
html.lx-equip-fx-enabled .lx-item-fx[data-lx-rarity="purple"] {
--lx-fx-1: #6a3fbf;
--lx-fx-2: #d5a8ff;
--lx-fx-3: #4f2a96;
--lx-fx-glow: #a56cff;
--lx-fx-bg: rgba(123, 82, 196, 0.09);
}
html.lx-equip-fx-enabled .lx-item-fx[data-lx-rarity="purple"] .lx-item-fx-glow {
opacity: 0.42;
animation-duration: 2.8s, 6s;
}
html.lx-equip-fx-enabled .lx-item-fx[data-lx-rarity="gold"] {
--lx-fx-1: #a87218;
--lx-fx-2: #ffe27a;
--lx-fx-3: #ff9f1a;
--lx-fx-glow: #ffc940;
--lx-fx-bg: rgba(184, 134, 47, 0.1);
}
html.lx-equip-fx-enabled .lx-item-fx[data-lx-rarity="gold"] .lx-item-fx-glow {
opacity: 0.48;
filter: blur(18px);
animation-duration: 2.4s, 5s;
}
html.lx-equip-fx-enabled .lx-item-fx[data-lx-rarity="gold"]::after {
animation-duration: 3s;
}
html.lx-equip-fx-enabled .lx-item-fx[data-lx-rarity="orange"] {
--lx-fx-1: #c44a12;
--lx-fx-2: #ffb86c;
--lx-fx-3: #ff5e1a;
--lx-fx-glow: #ff8a3d;
--lx-fx-bg: rgba(200, 90, 36, 0.11);
}
html.lx-equip-fx-enabled .lx-item-fx[data-lx-rarity="orange"] .lx-item-fx-glow {
inset: -5px;
opacity: 0.52;
filter: blur(22px);
animation-duration: 2s, 4s;
}
html.lx-equip-fx-enabled .lx-item-fx[data-lx-rarity="orange"]::after {
animation-duration: 2.2s;
padding: 2px;
}
html.lx-equip-fx-enabled .lx-item-fx[data-lx-rarity="orange"] {
animation-duration: 2.4s;
}
html.lx-equip-fx-enabled .backpack-view .backpack-item.lx-item-fx:not(.material):not(.empty-storage-slot) {
border-color: color-mix(in srgb, var(--lx-fx-2) 32%, var(--lx-border)) !important;
background-image:
linear-gradient(155deg, color-mix(in srgb, var(--lx-fx-2) 10%, transparent), transparent 48%, color-mix(in srgb, var(--lx-fx-1) 6%, transparent)),
linear-gradient(325deg, color-mix(in srgb, var(--lx-fx-glow) 8%, transparent), transparent 55%);
background-color: inherit;
}
html.lx-equip-fx-enabled .adventurers-view .equipment-slot-card.lx-item-fx:not(.empty) {
border-color: color-mix(in srgb, var(--lx-fx-2) 36%, var(--lx-border)) !important;
background-image:
linear-gradient(155deg, color-mix(in srgb, var(--lx-fx-2) 9%, transparent), transparent 50%, color-mix(in srgb, var(--lx-fx-1) 5%, transparent)),
linear-gradient(325deg, color-mix(in srgb, var(--lx-fx-glow) 8%, transparent), transparent 55%);
background-color: inherit;
}
html.lx-equip-fx-enabled .lx-equip-current-card.lx-item-fx,
html.lx-equip-fx-enabled .lx-equip-preview.lx-item-fx,
html.lx-equip-fx-enabled .lx-equip-option.lx-item-fx {
border-color: color-mix(in srgb, var(--lx-fx-2) 38%, var(--lx-border)) !important;
background-image:
linear-gradient(155deg, color-mix(in srgb, var(--lx-fx-2) 9%, transparent), transparent 52%, color-mix(in srgb, var(--lx-fx-1) 5%, transparent)),
linear-gradient(325deg, color-mix(in srgb, var(--lx-fx-glow) 8%, transparent), transparent 55%);
background-color: inherit;
}
#lx-guild-stat-filter {
margin-top: 10px;
padding: 10px 12px;
border: 1px solid var(--lx-border);
border-radius: 10px;
background: var(--lx-surface);
color: var(--lx-fg-soft);
}
#lx-guild-stat-filter .lx-filter-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
#lx-guild-stat-filter .lx-filter-title {
font-size: 13px;
font-weight: 600;
color: var(--lx-fg);
}
#lx-guild-stat-filter .lx-filter-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--lx-fg-soft);
}
#lx-guild-stat-filter .lx-filter-mode {
display: flex;
gap: 8px;
margin-bottom: 8px;
flex-wrap: wrap;
}
#lx-guild-stat-filter .lx-filter-mode label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--lx-fg-soft);
cursor: pointer;
}
#lx-guild-stat-filter .lx-filter-mode input[type="radio"],
#lx-guild-stat-filter .lx-filter-toggle input[type="checkbox"],
#lx-guild-stat-filter .lx-stat-item input[type="checkbox"] {
width: 14px !important;
height: 14px !important;
min-width: 14px !important;
max-width: 14px !important;
min-height: 14px !important;
max-height: 14px !important;
flex: 0 0 14px !important;
margin: 0 !important;
padding: 0 !important;
box-sizing: border-box !important;
transform: none !important;
-webkit-appearance: auto;
appearance: auto;
accent-color: var(--lx-accent);
}
#lx-guild-stat-filter .lx-stat-grid {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
#lx-guild-stat-filter .lx-stat-item {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 5px 8px;
border-radius: 8px;
background: color-mix(in srgb, var(--lx-fg) 4%, var(--lx-surface));
border: 1px solid transparent;
cursor: pointer;
font-size: 12px;
color: var(--lx-fg-soft);
}
#lx-guild-stat-filter .lx-stat-item.active {
border-color: var(--lx-accent-border);
background: var(--lx-accent-bg-strong);
color: var(--lx-fg);
}
#lx-guild-stat-filter .lx-filter-actions {
display: flex;
gap: 8px;
margin-top: 8px;
}
#lx-guild-stat-filter .lx-filter-actions button {
border: 1px solid var(--lx-border);
border-radius: 8px;
background: var(--lx-accent-bg);
color: var(--lx-fg-soft);
font-size: 12px;
padding: 6px 10px;
cursor: pointer;
}
#lx-guild-stat-filter .lx-filter-actions button:hover {
background: var(--lx-accent-bg-strong);
color: var(--lx-fg);
}
#lx-guild-stat-filter .lx-filter-summary {
margin-top: 8px;
font-size: 12px;
line-height: 1.5;
color: var(--lx-fg-muted);
}
#lx-guild-stat-filter .lx-filter-summary.active {
color: var(--lx-success);
}
.backpack-view .backpack-grid {
grid-auto-rows: minmax(140px, auto) !important;
gap: 12px !important;
}
.backpack-view .backpack-item:not(.empty-storage-slot) {
min-height: 140px !important;
height: auto !important;
max-height: none !important;
align-content: start !important;
align-items: start !important;
overflow: visible !important;
box-sizing: border-box !important;
}
.backpack-view .backpack-item.material {
min-height: 72px !important;
}
.backpack-view .backpack-item:not(.material):not(.empty-storage-slot) {
row-gap: 8px !important;
padding: 10px 11px 12px !important;
}
.backpack-view .backpack-item-head {
display: grid !important;
grid-template-columns: auto minmax(0, 1fr) !important;
column-gap: 10px !important;
row-gap: 5px !important;
align-items: start !important;
width: 100%;
min-width: 0;
}
.backpack-view .backpack-item-head > input[type="checkbox"] {
grid-column: 1;
grid-row: 1 / -1;
align-self: start;
margin-top: 3px;
}
.backpack-view .backpack-item-head .lx-item-name-row {
grid-column: 2;
grid-row: 1;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px 8px;
min-width: 0;
}
.backpack-view .backpack-item-head .lx-item-extra {
grid-column: 2;
grid-row: 2;
min-width: 0;
}
.backpack-view .backpack-item-head .equipment-name {
min-width: 0;
white-space: normal !important;
overflow: visible !important;
text-overflow: unset !important;
line-height: 1.45;
}
.backpack-view .backpack-item-head .lx-item-line2 {
font-size: 11px;
line-height: 1.5;
color: var(--lx-fg-muted);
}
.backpack-view .backpack-item-head .lx-item-stats {
margin: 3px 0 0;
font-size: 11px;
line-height: 1.5;
color: var(--lx-fg-soft);
}
.backpack-view .backpack-card-actions {
align-self: start !important;
margin-top: 2px;
}
.backpack-view .backpack-item:not(.material):not(.empty-storage-slot) {
grid-template-columns: minmax(0, 1fr) auto !important;
align-items: start !important;
}
.backpack-view .lx-item-score,
.adventurers-view .lx-item-score,
.lx-equip-preview .lx-item-score,
.lx-equip-option .lx-item-score {
flex: 0 0 auto;
font-size: 11px;
font-weight: 700;
line-height: 1.45;
white-space: nowrap;
}
.lx-item-score--base {
color: var(--lx-fg-muted);
}
.lx-item-score--build {
color: var(--lx-accent-2);
}
.lx-item-score-delta {
flex: 0 0 auto;
font-size: 10px;
font-weight: 700;
line-height: 1.45;
white-space: nowrap;
}
.lx-item-score-delta--up {
color: var(--lx-success);
}
.lx-item-score-delta--down {
color: #e07070;
}
.lx-item-score-delta--flat {
color: var(--lx-fg-muted);
}
.backpack-view .backpack-item,
.adventurers-view .equipment-slot-card,
.lx-equip-current-card,
.lx-equip-preview,
.lx-equip-option {
position: relative;
}
.lx-item-locked {
isolation: isolate;
background: color-mix(in srgb, #000 10%, var(--lx-surface-strong, var(--panel-2, #fff))) !important;
}
.lx-item-locked::before {
content: '';
position: absolute;
inset: 0;
border: 2px dashed color-mix(in srgb, var(--lx-muted, #8b93a3) 72%, var(--lx-border, #5c6575));
border-radius: inherit;
pointer-events: none;
z-index: 4;
box-shadow:
inset 0 0 0 1px color-mix(in srgb, #000 18%, transparent),
0 0 0 1px color-mix(in srgb, var(--lx-muted, #8b93a3) 12%, transparent);
}
.lx-item-locked::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
z-index: 1;
background: repeating-linear-gradient(
-45deg,
transparent,
transparent 8px,
color-mix(in srgb, #000 7%, transparent) 8px,
color-mix(in srgb, #000 7%, transparent) 16px
);
}
.lx-item-lock-strip {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 5px;
z-index: 6;
border-radius: inherit 0 0 inherit;
pointer-events: none;
background: linear-gradient(
180deg,
color-mix(in srgb, var(--lx-muted, #9aa3b2) 55%, #4a5568) 0%,
color-mix(in srgb, var(--lx-muted, #7a8494) 70%, #374151) 100%
);
box-shadow: inset -1px 0 0 color-mix(in srgb, #fff 8%, transparent);
}
.lx-item-lock-badge {
position: absolute;
top: 5px;
right: 5px;
z-index: 7;
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 9px 2px 7px;
border-radius: 999px;
font-size: 10px;
font-weight: 700;
line-height: 1.35;
letter-spacing: 0.03em;
white-space: nowrap;
color: color-mix(in srgb, var(--lx-fg-soft, #c8cdd6) 92%, #fff);
background: color-mix(in srgb, #000 24%, var(--lx-surface-strong, #2a2f38));
border: 1px solid color-mix(in srgb, var(--lx-muted, #8b93a3) 42%, var(--lx-border, #4a5568));
box-shadow: 0 1px 6px color-mix(in srgb, #000 28%, transparent);
pointer-events: none;
}
.lx-item-lock-icon {
font-size: 11px;
line-height: 1;
opacity: 0.88;
}
.backpack-view .backpack-item.lx-item-locked .lx-item-lock-badge {
top: 4px;
left: auto;
right: 4px;
}
.backpack-view .backpack-item.lx-item-locked .lx-item-lock-strip {
width: 7px;
}
.backpack-view .backpack-item.lx-item-locked .backpack-item-head,
.backpack-view .backpack-item.lx-item-locked .backpack-card-actions,
.adventurers-view .equipment-slot-card.lx-item-locked > :not(.lx-item-lock-badge):not(.lx-item-lock-strip):not(.lx-item-fx-glow),
.lx-equip-current-card.lx-item-locked > :not(.lx-item-lock-badge):not(.lx-item-lock-strip):not(.lx-item-fx-glow),
.lx-equip-preview.lx-item-locked > :not(.lx-item-lock-badge):not(.lx-item-lock-strip):not(.lx-item-fx-glow) {
position: relative;
z-index: 2;
}
.lx-stat-primary {
color: var(--lx-accent-2);
font-weight: 700;
}
.lx-stat-sep {
color: var(--lx-fg-muted);
}
.lx-equip-modal-profile {
margin: 6px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--lx-fg-soft);
}
.adventurers-view .lx-equip-slot-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
width: 100%;
flex: 0 0 100%;
min-width: 0;
}
.adventurers-view .lx-equip-slot-actions {
display: inline-flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
margin-left: auto;
}
.adventurers-view .lx-equip-swap-btn,
.adventurers-view .equipment-slot-card .equipment-unequip-button {
position: static;
top: auto;
right: auto;
transform: none;
margin: 0;
min-height: 0;
padding: 2px 8px;
border: 1px solid var(--lx-accent-border);
border-radius: 6px;
background: var(--lx-accent-bg);
color: var(--lx-fg-soft);
font-size: 12px;
font-weight: 600;
line-height: 1.4;
white-space: nowrap;
cursor: pointer;
}
.adventurers-view .equipment-slot-card .equipment-unequip-button:hover,
.adventurers-view .lx-equip-swap-btn:hover {
background: var(--lx-accent-bg-strong);
color: var(--lx-fg);
}
.adventurers-view .equipment-slot-card:not(.empty) {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 4px 8px;
min-height: 72px;
padding: 10px 12px 8px !important;
}
.adventurers-view .equipment-slot-card .equipment-name {
flex: 0 1 auto;
min-width: 0;
}
.adventurers-view .equipment-slot-card .lx-item-name-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px 8px;
width: 100%;
}
.adventurers-view .equipment-slot-card .lx-item-extra {
flex-basis: 100%;
width: 100%;
}
.adventurers-view .equipment-slot-card .lx-item-line2 {
font-size: 11px;
line-height: 1.5;
color: var(--lx-fg-muted);
}
.adventurers-view .equipment-slot-card .lx-item-stats {
flex-basis: 100%;
margin: 0;
font-size: 11px;
line-height: 1.45;
color: var(--lx-fg-soft);
}
.adventurers-view .lx-char-lv-wrap {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2px;
margin-left: auto;
flex-shrink: 0;
}
.adventurers-view .lx-char-equip-score {
font-size: 11px;
font-weight: 600;
line-height: 1.2;
color: var(--lx-success);
white-space: nowrap;
}
.lx-equip-modal-backdrop {
z-index: 100000;
}
.lx-equip-modal {
width: min(560px, calc(100vw - 32px));
max-height: min(72vh, 720px);
display: flex;
flex-direction: column;
}
.lx-equip-modal-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
padding: 16px 18px 10px;
border-bottom: 1px solid var(--lx-border-soft);
}
.lx-equip-modal-head h2 {
margin: 4px 0 0;
font-size: 18px;
color: var(--lx-fg);
}
.lx-equip-modal-head .eyebrow {
margin: 0;
font-size: 12px;
color: var(--lx-fg-muted);
}
.lx-equip-modal-close {
font-size: 22px;
line-height: 1;
}
.lx-equip-modal-current {
flex-shrink: 0;
padding: 12px 14px;
border-bottom: 1px solid var(--lx-border-soft);
background: var(--lx-surface);
}
.lx-equip-modal-scroll {
flex: 1;
min-height: 0;
overflow: auto;
padding: 12px 14px 16px;
}
.lx-equip-empty {
margin: 0;
color: var(--lx-fg-muted);
font-size: 13px;
}
.lx-equip-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.lx-equip-option {
display: grid;
grid-template-columns: 1fr;
gap: 4px;
width: 100%;
padding: 10px 12px;
border: 1px solid var(--lx-border-soft);
border-radius: 10px;
background: var(--lx-surface);
text-align: left;
cursor: pointer;
color: var(--lx-fg-soft);
}
.lx-equip-option:hover:not(:disabled) {
border-color: var(--lx-accent-border);
background: var(--lx-accent-bg);
}
.lx-equip-option:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.lx-equip-section-title {
margin: 0 0 8px;
font-size: 12px;
font-weight: 600;
color: var(--lx-fg-muted);
}
.lx-equip-current-section {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.lx-equip-warehouse-section {
margin-top: 4px;
}
.lx-equip-current-card,
.lx-equip-preview {
padding: 10px 12px;
border: 1px solid var(--lx-border);
border-radius: 10px;
background: var(--lx-accent-bg);
}
.lx-equip-empty-current {
margin: 0;
padding: 10px 12px;
border: 1px dashed var(--lx-border);
border-radius: 10px;
color: var(--lx-fg-muted);
font-size: 13px;
}
.lx-equip-preview-head,
.lx-equip-option-head {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px 8px;
}
.lx-equip-preview-inline,
.lx-equip-option-inline {
font-size: 11px;
line-height: 1.45;
color: var(--lx-fg-muted);
white-space: nowrap;
}
.lx-equip-preview-meta {
margin-top: 4px;
font-size: 11px;
line-height: 1.45;
color: var(--lx-fg-soft);
}
.lx-equip-option .equipment-name {
font-size: 14px;
font-weight: 600;
}
.lx-equip-option-meta {
font-size: 11px;
line-height: 1.45;
color: var(--lx-fg-soft);
}
.lx-equip-option-level {
font-size: 11px;
color: var(--lx-fg-muted);
}
.lx-stat-derivation {
margin-top: 10px;
padding-top: 10px;
border-top: 1px dashed var(--lx-border-soft);
}
.lx-stat-derivation-title {
margin: 0 0 8px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.04em;
color: var(--lx-fg-muted);
}
.lx-stat-derivation-grid {
display: flex;
flex-direction: column;
gap: 6px;
}
.lx-stat-derivation-row {
display: grid;
grid-template-columns: minmax(88px, auto) 1fr;
gap: 6px 10px;
align-items: start;
font-size: 11px;
line-height: 1.45;
}
.lx-stat-derivation-key {
color: var(--lx-fg);
font-weight: 600;
white-space: nowrap;
}
.lx-stat-derivation-effects {
color: var(--lx-fg-soft);
}
.lx-item-affixes,
.lx-equip-preview-affixes,
.lx-equip-option-affixes {
font-size: 11px;
line-height: 1.45;
color: var(--lx-accent);
}
.lx-stat-derivation-race {
margin: 0 0 8px;
font-size: 10px;
line-height: 1.45;
color: var(--lx-fg-soft);
}
.lx-stat-derivation-speed {
margin: 8px 0 0;
font-size: 10px;
line-height: 1.45;
color: var(--lx-fg-muted);
}
.lx-stat-derivation-note {
margin: 8px 0 0;
font-size: 10px;
line-height: 1.4;
color: var(--lx-fg-muted);
}
.lx-primary-stats-banner,
.lx-equip-priority {
margin-top: 10px;
padding: 10px 12px;
border-radius: 8px;
background: var(--lx-accent-bg);
border: 1px solid var(--lx-accent-border);
}
.lx-equip-priority {
margin: 0 0 10px;
}
.lx-primary-stats-title,
.lx-equip-priority-title {
margin: 0 0 8px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.04em;
color: var(--lx-accent);
}
.lx-primary-stats-tags,
.lx-equip-priority-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.lx-primary-stat-tag {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
color: var(--lx-fg);
background: var(--lx-tag-bg);
border: 1px solid var(--lx-tag-border);
}
.lx-primary-stats-meta,
.lx-equip-priority-meta {
margin: 8px 0 0;
font-size: 10px;
line-height: 1.45;
color: var(--lx-fg-muted);
}
.lx-stat-derivation-row-primary {
padding: 4px 6px;
margin: -4px -6px;
border-radius: 6px;
background: var(--lx-highlight-bg);
}
.lx-stat-derivation-row-primary .lx-stat-derivation-key {
color: var(--lx-accent);
}
.lx-stat-primary-badge {
display: inline-block;
margin-right: 4px;
padding: 0 4px;
border-radius: 4px;
font-size: 9px;
line-height: 1.5;
color: var(--lx-surface-strong);
background: var(--lx-accent);
vertical-align: middle;
}
.skills-panel .skills-panel-title .panel-pill-button {
display: none;
}
.lx-skill-layout {
display: grid;
grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
gap: 14px;
margin-top: 10px;
}
@media (max-width: 960px) {
.lx-skill-layout {
grid-template-columns: 1fr;
}
}
.lx-skill-column-title {
margin: 0 0 8px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.04em;
color: var(--lx-accent);
}
.lx-skill-column--active .skill-slots {
display: flex;
flex-direction: column;
gap: 10px;
}
.adventurers-view .skill-slot {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
grid-template-areas:
"primary picker"
"meta meta";
gap: 6px 8px;
padding: 8px 10px;
border: 1px solid var(--lx-border-soft);
border-radius: 8px;
background: var(--lx-surface);
}
.adventurers-view .skill-slot .primary-skill-button {
grid-area: primary;
align-self: center;
}
.adventurers-view .skill-slot select.lx-skill-select-hidden {
display: none !important;
}
.lx-skill-picker-btn {
grid-area: picker;
width: 100%;
min-width: 0;
padding: 6px 10px;
border: 1px solid var(--lx-border);
border-radius: 6px;
background: var(--lx-surface);
color: var(--lx-fg);
font-size: 12px;
font-weight: 600;
line-height: 1.4;
text-align: left;
cursor: pointer;
}
.lx-skill-picker-btn:hover:not(:disabled) {
border-color: var(--lx-accent-border);
background: var(--lx-accent-bg);
}
.lx-skill-picker-btn:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.lx-skill-slot-meta {
grid-area: meta;
min-width: 0;
}
.lx-skill-modal-backdrop {
z-index: 100000;
}
.lx-skill-modal {
width: min(520px, calc(100vw - 32px));
max-height: min(72vh, 680px);
display: flex;
flex-direction: column;
}
.lx-skill-modal-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
padding: 16px 18px 10px;
border-bottom: 1px solid var(--lx-border-soft);
}
.lx-skill-modal-head h2 {
margin: 4px 0 0;
font-size: 18px;
color: var(--lx-fg);
}
.lx-skill-modal-head .eyebrow {
margin: 0;
font-size: 12px;
color: var(--lx-fg-muted);
}
.lx-skill-modal-close {
font-size: 22px;
line-height: 1;
}
.lx-skill-modal-scroll {
flex: 1;
min-height: 0;
overflow: auto;
padding: 12px 14px 16px;
}
.lx-skill-modal-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.lx-skill-slot-name,
.lx-passive-skill-item .lx-skill-name,
.lx-skill-option .lx-skill-name {
font-size: 12px;
font-weight: 600;
line-height: 1.4;
color: var(--lx-fg);
}
.adventurers-view .skill-slot .lx-skill-slot-desc,
.lx-passive-skill-item .lx-skill-desc {
display: none;
margin: 0;
font-size: 11px;
line-height: 1.45;
color: var(--lx-fg-soft);
}
.adventurers-view .skill-slot:hover .lx-skill-slot-desc,
.lx-passive-skill-item:hover .lx-skill-desc {
display: block;
margin-top: 4px;
}
.lx-skill-option .lx-skill-desc {
margin: 4px 0 0;
font-size: 11px;
line-height: 1.45;
color: var(--lx-fg-soft);
}
.lx-skill-option {
display: block;
width: 100%;
padding: 6px 8px;
border: 1px solid var(--lx-border-soft);
border-radius: 6px;
background: var(--lx-accent-bg);
text-align: left;
cursor: pointer;
color: inherit;
}
.lx-skill-option:hover:not(:disabled) {
border-color: var(--lx-accent-border);
background: var(--lx-accent-bg-strong);
}
.lx-skill-option.is-selected {
border-color: var(--lx-accent-border);
background: var(--lx-accent-bg-strong);
}
.lx-skill-option:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.lx-passive-skill-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 520px;
overflow: auto;
padding-right: 2px;
}
.lx-passive-skill-item {
padding: 8px 10px;
border: 1px solid var(--lx-border-soft);
border-radius: 8px;
background: var(--lx-surface);
}
.lx-skill-empty {
margin: 0;
font-size: 11px;
color: var(--lx-fg-muted);
}
.app-sidebar:has(.lx-sidebar-meta) {
grid-template-rows: auto auto auto minmax(0, 1fr) !important;
}
.app-sidebar .brand-lockup {
grid-row: 1;
}
.app-sidebar .lx-sidebar-meta {
grid-row: 2;
}
.app-sidebar .app-nav {
grid-row: 3;
align-self: start;
}
.app-sidebar .sidebar-footer {
grid-row: 4;
align-self: end;
}
.app-sidebar .sidebar-footer[hidden] {
display: none !important;
}
.lx-sidebar-meta {
display: grid;
gap: 10px;
padding: 12px;
margin-bottom: 4px;
border: 1px solid var(--lx-border-soft);
border-radius: 8px;
background: var(--lx-surface);
}
.lx-sidebar-account-bar strong {
display: block;
font-size: 13px;
line-height: 1.4;
color: var(--lx-fg);
word-break: break-all;
}
.lx-sidebar-prefs {
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed var(--lx-border-soft);
}
.lx-equip-fx-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
line-height: 1.4;
color: var(--lx-fg-soft);
cursor: pointer;
user-select: none;
}
.lx-equip-fx-toggle input[type="checkbox"] {
width: 14px !important;
height: 14px !important;
min-width: 14px !important;
max-width: 14px !important;
min-height: 14px !important;
max-height: 14px !important;
flex: 0 0 14px !important;
margin: 0 !important;
padding: 0 !important;
box-sizing: border-box !important;
accent-color: var(--lx-accent);
}
.lx-sidebar-meta .sidebar-ledger {
display: grid;
gap: 6px;
margin: 0;
padding: 0;
border-top: none;
}
.lx-sidebar-meta .sidebar-ledger > div {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
font-size: 12px;
color: var(--lx-fg-soft);
}
.lx-sidebar-meta .sidebar-ledger b {
color: var(--lx-accent-2);
font-weight: 700;
}
.lx-sidebar-meta .sidebar-actions {
display: flex;
gap: 8px;
padding-top: 4px;
border-top: 1px dashed var(--lx-border-soft);
}
.lx-sidebar-meta .sidebar-actions .icon-button {
flex: 1;
min-height: 36px;
}
`);
}
function readFilterFromUI(root) {
const next = defaultFilterState();
next.enabled = root.querySelector('.lx-enable-filter')?.checked ?? true;
next.mode = root.querySelector('input[name="lx-filter-mode"]:checked')?.value || 'any';
STAT_DEFS.forEach((def) => {
const item = root.querySelector(`.lx-stat-item[data-stat="${def.key}"]`);
next.stats[def.key] = {
active: item?.querySelector('input[type="checkbox"]')?.checked ?? false,
};
});
return next;
}
function bindAutoApply(root) {
root.querySelectorAll('input').forEach((input) => {
input.addEventListener('change', () => {
root.querySelectorAll('.lx-stat-item').forEach((item) => {
const checked = item.querySelector('input[type="checkbox"]')?.checked;
item.classList.toggle('active', Boolean(checked));
});
applyFilterFromUI(root);
});
});
}
function renderFilterUI(root) {
root.innerHTML = `
助手 · 属性筛选
${STAT_DEFS.map((def) => {
const active = filterState.stats[def.key]?.active;
return `
`;
}).join('')}
${buildFilterSummary()}
`;
bindAutoApply(root);
root.querySelector('.lx-clear-filter')?.addEventListener('click', () => {
filterState = defaultFilterState();
saveFilterState(filterState);
renderFilterUI(root);
triggerPageRefresh();
});
}
function ensureBackpackFilter() {
const backpackView = document.querySelector('.backpack-view');
if (!backpackView) return;
const controls = backpackView.querySelector('.backpack-controls');
if (!controls || controls.querySelector('#lx-guild-stat-filter')) return;
const filterRoot = document.createElement('section');
filterRoot.id = 'lx-guild-stat-filter';
controls.appendChild(filterRoot);
renderFilterUI(filterRoot);
}
function onBackpackReady() {
ensureBackpackFilter();
observeBackpackView();
scheduleEnhanceBackpackItems();
}
function syncSidebarAccount(accountBar, lockup) {
if (!accountBar || !lockup) return;
const usernameEl = lockup.querySelector('strong');
if (!usernameEl) return;
if (usernameEl.parentElement !== accountBar) {
accountBar.appendChild(usernameEl);
}
}
function ensureEquipFxToggle(accountBar) {
if (!accountBar) return;
let prefs = accountBar.querySelector('.lx-sidebar-prefs');
if (!prefs) {
prefs = document.createElement('div');
prefs.className = 'lx-sidebar-prefs';
accountBar.appendChild(prefs);
}
let toggle = prefs.querySelector('.lx-equip-fx-toggle');
if (!toggle) {
toggle = document.createElement('label');
toggle.className = 'lx-equip-fx-toggle';
toggle.title = '为仓库与装备选择界面启用稀有度边框光效';
toggle.innerHTML = '装备光效';
toggle.querySelector('input').addEventListener('change', (event) => {
setEquipFxEnabled(event.target.checked);
});
prefs.appendChild(toggle);
}
const input = toggle.querySelector('input');
if (input && input.checked !== equipFxState.enabled) {
input.checked = equipFxState.enabled;
}
}
function enhanceSidebar() {
const sidebar = document.querySelector('.app-sidebar');
if (!sidebar) return;
const footer = sidebar.querySelector('.sidebar-footer');
const lockup = sidebar.querySelector('.brand-lockup');
if (!footer || !lockup) return;
let panel = sidebar.querySelector('.lx-sidebar-meta');
if (!panel) {
panel = document.createElement('div');
panel.className = 'lx-sidebar-meta';
const accountBar = document.createElement('div');
accountBar.className = 'lx-sidebar-account-bar';
panel.appendChild(accountBar);
}
let accountBar = panel.querySelector('.lx-sidebar-account-bar');
if (!accountBar) {
accountBar = document.createElement('div');
accountBar.className = 'lx-sidebar-account-bar';
panel.prepend(accountBar);
}
const ledger = panel.querySelector('.sidebar-ledger') || footer.querySelector('.sidebar-ledger');
const actions = panel.querySelector('.sidebar-actions') || footer.querySelector('.sidebar-actions');
if (ledger && ledger.parentElement !== panel) panel.appendChild(ledger);
if (actions && actions.parentElement !== panel) panel.appendChild(actions);
syncSidebarAccount(accountBar, lockup);
ensureEquipFxToggle(accountBar);
if (panel.previousElementSibling !== lockup) {
lockup.insertAdjacentElement('afterend', panel);
}
footer.hidden = footer.childElementCount === 0;
}
function observeSidebar() {
const sidebar = document.querySelector('.app-sidebar');
if (!sidebar) {
if (sidebarObserver) {
sidebarObserver.disconnect();
sidebarObserver = null;
}
return;
}
if (sidebarObserver) return;
enhanceSidebar();
sidebarObserver = new MutationObserver(() => {
enhanceSidebar();
});
sidebarObserver.observe(sidebar, { childList: true, subtree: true });
}
function onShellReady() {
observeSidebar();
enhanceSidebar();
}
function watchRouteChanges() {
let wasOnBackpack = Boolean(document.querySelector('.backpack-view'));
let wasOnAdventurer = Boolean(document.querySelector('.adventurers-view'));
let wasOnShell = Boolean(document.querySelector('.app-sidebar'));
if (wasOnBackpack) onBackpackReady();
if (wasOnAdventurer) onAdventurerReady();
if (wasOnShell) onShellReady();
const routeObserver = new MutationObserver(() => {
const hasBackpack = Boolean(document.querySelector('.backpack-view'));
const hasAdventurer = Boolean(document.querySelector('.adventurers-view'));
if (hasBackpack !== wasOnBackpack) {
wasOnBackpack = hasBackpack;
if (hasBackpack) onBackpackReady();
else if (backpackObserver) {
backpackObserver.disconnect();
backpackObserver = null;
}
}
if (hasAdventurer !== wasOnAdventurer) {
wasOnAdventurer = hasAdventurer;
if (hasAdventurer) onAdventurerReady();
else {
closeEquipPicker();
closeSkillPicker();
if (adventurerObserver) {
adventurerObserver.disconnect();
adventurerObserver = null;
}
}
}
const hasShell = Boolean(document.querySelector('.app-sidebar'));
if (hasShell !== wasOnShell) {
wasOnShell = hasShell;
if (hasShell) onShellReady();
else if (sidebarObserver) {
sidebarObserver.disconnect();
sidebarObserver = null;
}
}
});
routeObserver.observe(document.documentElement, { childList: true, subtree: true });
}
function initDomTools() {
applyEquipFxRootClass();
injectStyles();
pageWindow.__lxGuildEnhanceBackpack = scheduleEnhanceBackpackItems;
watchRouteChanges();
console.info('[仓库助手] 已加载 v0.5.23', {
filterEnabled: filterState.enabled,
equipFxEnabled: equipFxState.enabled,
summary: buildFilterSummary(),
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initDomTools);
} else {
initDomTools();
}
})();