// ==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(); } })();