[MWI]Trigger display

To display trigger, MWIT must be installed (https://greasyfork.org/zh-CN/scripts/494467-mwitools).

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         [MWI]Trigger display
// @name:zh      [银河奶牛]Trigger展示
// @description  To display trigger, MWIT must be installed (https://greasyfork.org/zh-CN/scripts/494467-mwitools).
// @description:zh  展示trigger,前置需要安装MWIT(https://greasyfork.org/zh-CN/scripts/494467-mwitools)
// @namespace    https://www.milkywayidle.com/
// @version      1.5
// @author       GPT-DiamondMoo
// @icon         https://www.milkywayidle.com/favicon.svg
// @license      MIT
// @match        https://www.milkywayidle.com/*
// @match        https://www.milkywayidlecn.com/*
// @match        https://test.milkywayidle.com/*
// @match        https://test.milkywayidlecn.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

(function () {
    'use strict';

    const STORAGE_KEY = "profile_export_list";
    const CACHE_BASE_KEY = "mw_party_cache_";
    const PANEL_STATE_KEY = "mw_panel_state_";

    let pageUniqueId = null;
    let cache = {};
    let panel = null;
    let panelVisible = false;
    let panelState = {
        left: 120,
        top: 120,
        expanded: true
    };

    // 统一的不可选样式类
    const addNoSelectStyle = () => {
        if (document.getElementById("script_no_select_style")) return;
        const style = document.createElement("style");
        style.id = "script_no_select_style";
        style.innerHTML = `
            .script-no-select {
                user-select: none;
                -webkit-user-select: none;
                -moz-user-select: none;
                -ms-user-select: none;
            }
            .Script_centerRow, .Script_abilityIconsRow {
                display:flex;
                justify-content:center;
                gap:4px;
                flex-wrap:wrap;
            }
            .Script_consumablesRow {
                display:flex;
                gap:4px;
                margin-top:4px;
                flex-wrap:wrap;
                justify-content:center;
            }
            .Script_settingsAfterAbility {
                margin-top:2px;
                display:flex;
                justify-content:center;
            }
            .Button_partyCache__custom {
                background-color: #546ddb;
                color: white;
                border-radius: 5px;
                padding: 5px 10px;
                cursor: pointer;
                transition: background-color 0.3s;
            }
            .Button_partyCache__custom:hover {
                background-color: #6b84ff;
            }
        `;
        document.head.appendChild(style);
    };

    // 生成当前页面的唯一标识
    function getPageUniqueId() {
        if (pageUniqueId) return pageUniqueId;

        try {
            const page = document.querySelector('[class^="GamePage"]');
            if (page) {
                const key = Object.keys(page).find(k => k.startsWith("__reactFiber$"));
                const gameState = page[key]?.return?.stateNode?.state;
                const playerId = gameState?.accountInfo?.playerId || gameState?.characterInfo?.accountID || "unknown";
                pageUniqueId = `${playerId}_${Date.now()}`;
            } else {
                pageUniqueId = `${window.location.href}_${Date.now()}`;
            }
        } catch (e) {
            pageUniqueId = `fallback_${Math.random().toString(36).substr(2, 9)}`;
        }
        return pageUniqueId;
    }

    // 加载面板状态(位置和展开状态)
    function loadPanelState() {
        const stateKey = PANEL_STATE_KEY + getPageUniqueId();
        const savedState = GM_getValue(stateKey, null);
        if (savedState) {
            panelState = {
                left: savedState.left || 120,
                top: savedState.top || 120,
                expanded: savedState.expanded !== undefined ? savedState.expanded : true
            };
        }
    }

    // 保存面板状态
    function savePanelState() {
        const stateKey = PANEL_STATE_KEY + getPageUniqueId();
        panelState.left = parseInt(panel.style.left) || 120;
        panelState.top = parseInt(panel.style.top) || 120;
        GM_setValue(stateKey, panelState);
    }

    // 边界保护和保底重置
    function applyBoundaryProtection() {
        if (!panel) return;
        // 获取窗口尺寸
        const windowWidth = window.innerWidth;
        const windowHeight = window.innerHeight;
        // 获取面板尺寸(添加默认值防止获取失败)
        const panelWidth = panel.offsetWidth || 260;
        const panelHeight = panel.offsetHeight || 100;

        // 保底保护:如果面板完全在窗口外,重置到左上角
        if (panelState.left > windowWidth || panelState.top > windowHeight ||
            panelState.left + panelWidth < 0 || panelState.top + panelHeight < 0) {
            panelState.left = 20;
            panelState.top = 20;
        }

        // 边界保护:确保面板不超出可视区域
        panelState.left = Math.max(0, Math.min(panelState.left, windowWidth - panelWidth - 10));
        panelState.top = Math.max(0, Math.min(panelState.top, windowHeight - panelHeight - 10));

        // 应用位置
        panel.style.left = `${panelState.left}px`;
        panel.style.top = `${panelState.top}px`;
    }

    function getCurrentPageCache() {
        const cacheKey = CACHE_BASE_KEY + getPageUniqueId();
        return GM_getValue(cacheKey, {});
    }

    function saveCurrentPageCache() {
        const cacheKey = CACHE_BASE_KEY + getPageUniqueId();
        GM_setValue(cacheKey, cache);
    }

    function isEnglishSite() {
        const lng = (localStorage.getItem("i18nextLng") || "").toLowerCase();
        return lng.startsWith("en");
    }

    function getI18n() {
        const page = document.querySelector('[class^="GamePage"]');
        if (!page) return null;
        const node = (el => el?.[Object.keys(el).find(k => k.startsWith('__reactFiber$'))]?.return?.stateNode)(page);
        const res = node?.props?.i18n?.options?.resources;
        return isEnglishSite() ? (res?.en?.translation || null) : (res?.zh?.translation || null);
    }

    function saveProfileShared(obj) {
        try {
            let listStr = localStorage.getItem(STORAGE_KEY);
            let list = [];
            try { list = listStr ? JSON.parse(listStr) : []; } catch {}
            obj.characterID = obj?.profile?.characterSkills?.[0]?.characterID;
            obj.characterName = obj?.profile?.sharableCharacter?.name;
            obj.timestamp = Date.now();
            list = (list || []).filter(it => it.characterID !== obj.characterID);
            list.unshift(obj);
            if (list.length > 20) list.pop();
            localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
        } catch (e) {}
    }

    function hookWebSocketForProfiles() {
        if (window.triggerDisplayWebSocketHooked) return;
        try {
            const desc = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data");
            if (!desc) return;
            const oriGet = desc.get;
            desc.get = function () {
                const socket = this.currentTarget;
                if (!(socket instanceof WebSocket)) return oriGet.call(this);
                if (socket.url.indexOf("api.milkywayidle.com/ws") <= -1 &&
                    socket.url.indexOf("api-test.milkywayidle.com/ws") <= -1 &&
                    socket.url.indexOf("api.milkywayidlecn.com/ws") <= -1) {
                    return oriGet.call(this);
                }
                const message = oriGet.call(this);
                Object.defineProperty(this, "data", { value: message });
                try {
                    const obj = JSON.parse(message);
                    if (obj && obj.type === "profile_shared") {
                        saveProfileShared(obj);
                    }
                } catch (e) {}
                return message;
            };
            Object.defineProperty(MessageEvent.prototype, "data", desc);
            window.triggerDisplayWebSocketHooked = true;
        } catch (e) {}
    }

    async function readClipboardData() {
        try {
            const text = await navigator.clipboard.readText();
            return text;
        } catch (e) {
            return null;
        }
    }

    async function autoClickExportButton() {
        try {
            let exportButton = null;
            const allButtons = document.querySelectorAll('button');
            for (const button of allButtons) {
                const txt = (button.textContent || "").trim();
                if (txt.includes('导出人物到剪贴板') || txt.includes('Export to clipboard')) {
                    exportButton = button;
                    break;
                }
            }
            if (!exportButton) return false;
            if (exportButton.disabled || exportButton.style.display === 'none') return false;
            exportButton.click();
            await new Promise(r => setTimeout(r, 500));
            return true;
        } catch (e) {
            return false;
        }
    }

    async function tryCaptureSharableFromClipboard() {
        try {
            await autoClickExportButton();
            const text = await readClipboardData();
            if (!text) return false;
            let obj = null;
            try { obj = JSON.parse(text); } catch {}
            if (obj && obj.profile) {
                if (!obj.type) obj.type = "profile_shared";
                saveProfileShared(obj);
                return true;
            }
            return false;
        } catch (e) {
            return false;
        }
    }

    function translateTrigger(triggerArr) {
        if (!triggerArr || !triggerArr.length)
            return isEnglishSite() ? "Activate as soon as it's off cooldown" : "冷却结束后立即使用";

        const i18n = getI18n();
        const isEN = isEnglishSite();
        const joinParts = (...parts) => {
            const ps = parts.map(p => (p === undefined || p === null) ? "" : String(p).trim()).filter(Boolean);
            return isEN ? ps.join(" ") : ps.join("");
        };

        return triggerArr.map(t => {
            const dep = i18n?.combatTriggerDependencyNames[t.dependencyHrid] || t.dependencyHrid || "";
            const cond = i18n?.combatTriggerConditionNames[t.conditionHrid] || t.conditionHrid || "";
            const comp = i18n?.combatTriggerComparatorNames[t.comparatorHrid] || t.comparatorHrid || "";

            if ((t.comparatorHrid || "").includes("is_active") ||
                (t.comparatorHrid || "").includes("is_inactive")) {
                return joinParts(dep, cond, comp);
            } else {
                return joinParts(dep, cond, comp, t.value);
            }
        }).join("\n");
    }

    let tooltipEl = null;
    function ensureTooltip() {
        if (tooltipEl) return;
        tooltipEl = document.createElement("div");
        tooltipEl.className = "script-no-select";
        tooltipEl.style.cssText = `
            position:fixed;
            max-width:260px;
            background:#222;
            color:#eee;
            border:1px solid #444;
            padding:6px 8px;
            border-radius:4px;
            font-size:12px;
            line-height:1.35;
            z-index:100000;
            white-space:pre-wrap;
            pointer-events:none;
            box-shadow:0 2px 6px rgba(0,0,0,0.4);
            display:none;
        `;
        document.body.appendChild(tooltipEl);
    }
    function showTooltip(text, x, y) {
        ensureTooltip();
        tooltipEl.textContent = text || "";
        tooltipEl.style.left = (x + 12) + "px";
        tooltipEl.style.top = (y + 12) + "px";
        tooltipEl.style.display = "block";
    }
    function moveTooltip(x, y) {
        if (!tooltipEl || tooltipEl.style.display === "none") return;
        tooltipEl.style.left = (x + 12) + "px";
        tooltipEl.style.top = (y + 12) + "px";
    }
    function hideTooltip() {
        if (!tooltipEl) return;
        tooltipEl.style.display = "none";
    }

    function getProfileByName(n) {
        const list = getProfiles();
        if (!n || !list || !list.length) return null;
        const name = String(n).trim().toLowerCase();
        return list.find(p => String(p.characterName || "").trim().toLowerCase() === name) || null;
    }

    function unwrapProfile(p) {
        return p?.profile || p || null;
    }

    function getConsumablesContainer() {
        const all = Array.from(document.querySelectorAll("div[class^='SharableProfile_equippedAbilities__']"));
        return all.length > 0 ? all[all.length - 1] : null;
    }

    function getAbilitiesContainer() {
        const all = Array.from(document.querySelectorAll("div[class^='SharableProfile_equippedAbilities__']"));
        return all.find(el => el.querySelector("div[class*='Ability_ability__']")) || null;
    }

    function getSiblingConsumablesContainer(abilitiesContainer) {
        if (!abilitiesContainer) return null;
        // 查找同级的下一个同类容器
        let sib = abilitiesContainer.nextElementSibling;
        while (sib) {
            if (sib.matches("div[class^='SharableProfile_equippedAbilities__']")) {
                return sib;
            }
            sib = sib.nextElementSibling;
        }
        // 若不存在则创建一个紧随其后的容器
        const newContainer = document.createElement("div");
        newContainer.className = abilitiesContainer.className;
        newContainer.style.cssText = "display:flex;gap:4px;margin-top:4px;flex-wrap:wrap;";
        abilitiesContainer.parentElement?.insertBefore(newContainer, abilitiesContainer.nextSibling);
        return newContainer;
    }

    function renderSharableConsumables(profileEntry) {
        if (!profileEntry) return false;
        const profile = unwrapProfile(profileEntry);
        if (!profile) {
            console.warn("[PartyTrigger Sharable] 未能解析 profile(profile.profile 为空, 渲染消耗品跳过)");
            return false;
        }
        // 消耗品应与技能容器同级,定位到技能容器后取其同级容器
        const abilitiesContainer = getAbilitiesContainer();
        const container = abilitiesContainer ? getSiblingConsumablesContainer(abilitiesContainer) : getConsumablesContainer();
        if (!container) {
            console.warn("[PartyTrigger Sharable] 未找到消耗品容器,跳过渲染");
            return false;
        }
        // 只管理我们自己的行,不清空原容器内容
        let row = container.querySelector(".Script_consumablesRow");
        if (!row) {
            row = document.createElement("div");
            row.className = "Script_consumablesRow";
            container.appendChild(row);
        }
        // 清理我们注入的行内容
        row.innerHTML = "";
        const items = profile.combatConsumables || [];
        let changed = false;
        for (let i = 0; i < 6; i++) {
            const item = items[i];
            const div = document.createElement("div");
            if (item && item.itemHrid) {
                const n = item.itemHrid.replace("/items/", "");
                div.innerHTML = `
<div class="Item_item__2De2O Item_small__1HxwE">
<div class="Item_iconContainer__5z7j4">
<svg width="36" height="36">
<use href="/static/media/items_sprite.328d6606.svg#${n}"></use>
</svg>
</div>
</div>`;
                const tipText = translateTrigger((profile.consumableCombatTriggersMap || {})[item.itemHrid]);
                div.onmouseenter = (e) => showTooltip(tipText, e.clientX, e.clientY);
                div.onmousemove = (e) => moveTooltip(e.clientX, e.clientY);
                div.onmouseleave = hideTooltip;
                changed = true;
            }
            row.appendChild(div);
        }
        return changed;
    }

    function renderSettingsUnderAbilities(profileEntry) {
        if (!profileEntry) return false;
        const profile = unwrapProfile(profileEntry);
        if (!profile) {
            console.warn("[PartyTrigger Sharable] 未能解析 profile(profile.profile 为空, 渲染设置(贴在技能下)跳过)");
            return false;
        }
        const abilitiesSorted = (profile.equippedAbilities || []).slice().sort((a, b) => (a.slotNumber || 0) - (b.slotNumber || 0));
        const container = getAbilitiesContainer();
        if (!container) {
            console.warn("[PartyTrigger Sharable] 未检测到技能容器,跳过“贴在技能下”的设置渲染");
            return false;
        }
        const wrappers = Array.from(container.children).filter(el => el.querySelector("div[class*='Ability_ability__']"));
        let changed = false;
        for (let i = 0; i < abilitiesSorted.length && i < wrappers.length; i++) {
            const a = abilitiesSorted[i];
            const wrap = wrappers[i];
            const abilityBlock = wrap.querySelector("div[class*='Ability_ability__']");
            if (!abilityBlock) continue;
            // 若已插入过,清理并重建
            wrap.querySelector(":scope > .Script_settingsAfterAbility")?.remove();

            const srow = document.createElement("div");
            srow.className = "Script_settingsAfterAbility";
            const block = document.createElement("div");
            block.className = "CombatTriggersSetting_combatTriggersSetting__380iI";
            block.innerHTML = `
<div><div class=""><svg role="img" aria-label="设置" class="Icon_icon__2LtL_ Icon_small__2bxvH" width="100%" height="100%"><use href="/static/media/misc_sprite.354aafcf.svg#settings"></use></svg></div></div>`;
            block.style.cursor = "pointer";
            block.onmouseenter = (e) => {
                const text = a ? translateTrigger((profile.abilityCombatTriggersMap || {})[a.abilityHrid]) : "";
                showTooltip(text, e.clientX, e.clientY);
            };
            block.onmousemove = (e) => moveTooltip(e.clientX, e.clientY);
            block.onmouseleave = hideTooltip;
            srow.appendChild(block);
            // 插在 ability 块之后
            abilityBlock.parentElement?.appendChild(srow);
            changed = true;
        }
        return changed;
    }

    function getSharableNameElement() {
        const candidates = Array.from(document.querySelectorAll("div[class^='CharacterName_name__'][data-name]"));
        if (candidates.length) return candidates[candidates.length - 1];
        return document.querySelector("div[class^='CharacterName_name__'] span, div[class^='CharacterName_name__']");
    }

    async function enhanceSharable() {
        const nameEl = getSharableNameElement();
        if (!nameEl) {
            console.warn("[PartyTrigger Sharable] 未找到角色名节点");
            return;
        }
        const host = nameEl.closest("div[class^='CharacterName_name__']") || nameEl;
        const findName = (host?.getAttribute("data-name")?.trim()) || (nameEl.textContent || "").trim();
        let profile = getProfileByName(findName);
        if (!profile) {
            const ok = await tryCaptureSharableFromClipboard();
            if (!ok) {
                console.warn("[PartyTrigger Sharable] 未在 profile_export_list 匹配到角色");
                return;
            }
            profile = getProfileByName(findName);
            if (!profile) return;
        }
        const changedA = renderSettingsUnderAbilities(profile);
        const changedC = renderSharableConsumables(profile);
        const didChange = !!(changedA || changedC);
        if (host && host.dataset && didChange) {
            host.dataset.scriptEnhanced = "1";
        }
    }

    let sharableWatcherInterval = null;
    let sharableLastTs = null;
    function getFirstProfileTsSafe() {
        try {
            const list = getProfiles();
            return typeof list?.[0]?.timestamp === "number" ? list[0].timestamp : null;
        } catch (e) {
            console.warn("[PartyTrigger Sharable] 读取 profile_export_list[0].timestamp 失败", e);
            return null;
        }
    }
    function hasSharableDom() {
        return !!getSharableNameElement() && !!document.querySelector("div[class^='SharableProfile_equippedAbilities__']");
    }
    function startSharableWatcher() {
        if (sharableWatcherInterval) return;
        sharableLastTs = getFirstProfileTsSafe();
        sharableWatcherInterval = setInterval(() => {
            const current = getFirstProfileTsSafe();
            if (current && sharableLastTs !== current && hasSharableDom()) {
                sharableLastTs = current;
                enhanceSharable();
            }
        }, 1500);
    }

    function createPanel() {
        const el = document.createElement("div");
        el.className = "script-no-select";
        el.style.cssText = `
            position:fixed;
            top:0px;
            left:0px;
            background:#111;
            color:#eee;
            font-size:12px;
            border:1px solid #444;
            padding:6px;
            z-index:99999;
            width: fit-content;
            max-width: 260px;
            display:none;
        `;
        document.body.appendChild(el);
        makeDraggable(el);
        return el;
    }

    function makeDraggable(el) {
        let x = 0, y = 0, drag = false;

        el.onmousedown = e => {
            if (e.target.tagName === "BUTTON") return;
            drag = true;
            x = e.clientX - el.offsetLeft;
            y = e.clientY - el.offsetTop;
            e.preventDefault();
        };

        document.onmousemove = e => {
            if (!drag) return;
            el.style.left = e.clientX - x + "px";
            el.style.top = e.clientY - y + "px";
            e.preventDefault();
        };

        document.onmouseup = () => {
            if (drag) {
                savePanelState();
                applyBoundaryProtection();
            }
            drag = false;
        };

        // 防止双击选中文字
        el.ondblclick = e => e.preventDefault();
    }

    function getProfiles() {
        try {
            const rawData = localStorage.getItem(STORAGE_KEY);
            return rawData ? JSON.parse(rawData) : [];
        } catch (e) {
            console.error("读取profile_export_list失败:", e);
            return [];
        }
    }

    function getPartyMap() {
        const page = document.querySelector('[class^="GamePage"]');
        if (!page) return null;
        const key = Object.keys(page).find(k => k.startsWith("__reactFiber$"));
        return page[key]?.return?.stateNode?.state?.partyInfo?.partySlotMap || null;
    }

    function getReactState() {
        const page = document.querySelector('[class^="GamePage"]');
        if (!page) return null;
        const key = Object.keys(page).find(k => k.startsWith("__reactFiber$"));
        return page[key]?.return?.stateNode?.state || null;
    }

    function getSelfCharacterID() {
        return getReactState()?.characterInfo?.characterID || null;
    }

    function getPartyDisplayOrder() {
        const m = getPartyMap();
        if (!m) return [];
        return Object.entries(m)
            .sort(([ak], [bk]) => {
                const aNum = parseInt(ak.match(/\d+/)?.[0] || ak, 10);
                const bNum = parseInt(bk.match(/\d+/)?.[0] || bk, 10);
                return isNaN(aNum) || isNaN(bNum) ? 0 : aNum - bNum;
            })
            .map(([_, v]) => v?.characterID)
            .filter(Boolean);
    }

    function updateSelfDataNow() {
        const s = getReactState();
        if (!s?.characterInfo?.characterID) return;

        const id = s.characterInfo.characterID;
        const characterName = s?.character?.name || s?.characterInfo?.characterName || "";
        const abilitiesSrc = s?.combatUnit?.combatAbilities || [];
        const consumables = s?.combatUnit?.combatConsumables || [];
        const abilityMap = s?.abilityCombatTriggersDict || {};
        const consumableMap = s?.consumableCombatTriggersDict || {};

        const equippedAbilities = abilitiesSrc.map((a, idx) => {
            const abilityHrid = a?.abilityHrid || a?.hrid || a?.ability?.hrid || a?.hridName;
            const level = a?.level || a?.abilityLevel || a?.lvl || 1;
            const slotNumber = a?.slotNumber || a?.slot || (idx + 1);
            return abilityHrid ? { slotNumber, abilityHrid, level } : null;
        }).filter(Boolean);

        cache = getCurrentPageCache();
        const prevHidden = cache[id]?.nameHidden || false;
        cache[id] = {
            characterName,
            timestamp: Date.now(),
            equippedAbilities,
            combatConsumables: consumables,
            abilityCombatTriggersMap: abilityMap,
            consumableCombatTriggersMap: consumableMap,
            nameHidden: prevHidden
        };
        saveCurrentPageCache();
    }

    function sync() {
        cache = getCurrentPageCache();
        const profiles = getProfiles();
        const party = getPartyMap();

        updateSelfDataNow();

        if (party && profiles.length) {
            const selfId = getSelfCharacterID();
            Object.values(party).forEach(slot => {
                const id = slot?.characterID;
                if (!id || (selfId && id === selfId)) return;

                const profileEntry = profiles.find(p => p.characterID === id);
                if (!profileEntry) return;

                const { timestamp, profile, characterName } = profileEntry;
                if (!cache[id] || cache[id].timestamp !== timestamp) {
                    const prevHidden = cache[id]?.nameHidden || false;
                    cache[id] = {
                        characterName,
                        timestamp,
                        equippedAbilities: profile.equippedAbilities || [],
                        combatConsumables: profile.combatConsumables || [],
                        abilityCombatTriggersMap: profile.abilityCombatTriggersMap || {},
                        consumableCombatTriggersMap: profile.consumableCombatTriggersMap || {},
                        nameHidden: prevHidden
                    };
                    saveCurrentPageCache();
                }
            });
        }

        render();
    }

    function render() {
        if (!panel) return;
        panel.innerHTML = "";

        // 头部关闭按钮
        const header = document.createElement("div");
        header.style.cssText = "display:flex;justify-content:flex-end;align-items:center;margin-bottom:6px;position:relative;";
        const closeBtn = document.createElement("button");
        closeBtn.textContent = "×";
        closeBtn.style.cssText = "padding:0 6px;font-size:14px;border:1px solid #444;background:#333;color:#eee;cursor:pointer;border-radius:2px;line-height:1.2;";
        closeBtn.onclick = hidePanel;
        header.appendChild(closeBtn);
        panel.appendChild(header);

        const order = getPartyDisplayOrder();
        let ids = [];
        if (order.length) {
            const orderIds = order.map(String);
            ids = orderIds.filter(id => cache[id]);
            ids = ids.concat(Object.keys(cache).filter(id => !orderIds.includes(id)));
        } else {
            ids = Object.keys(cache);
        }

        if (ids.length === 0) {
            const emptyTip = document.createElement("div");
            emptyTip.className = "script-no-select";
            emptyTip.textContent = "暂无队伍配装数据";
            emptyTip.style.color = "#999";
            panel.appendChild(emptyTip);
            return;
        }

        ids.forEach(id => {
            const data = cache[id];
            const row = document.createElement("div");
            row.style.cssText = "border-bottom:1px solid #333;margin-bottom:6px;";

            // 名称和时间戳
            const nameContainer = document.createElement("div");
            nameContainer.style.cssText = "display:flex;justify-content:space-between;font-weight:bold;";
            const name = document.createElement("span");
            name.textContent = data.characterName;
            name.style.cssText = "display:inline-block;min-width:80px;cursor:pointer;";
            if (data.nameHidden) {
                name.style.color = "transparent";
                name.style.textShadow = "none";
            }
            name.onclick = () => {
                cache[id].nameHidden = !cache[id].nameHidden;
                saveCurrentPageCache();
                name.style.color = cache[id].nameHidden ? "transparent" : "";
                name.style.textShadow = cache[id].nameHidden ? "none" : "";
            };
            const timestamp = document.createElement("span");
            timestamp.textContent = new Date(data.timestamp).toLocaleString();
            nameContainer.appendChild(name);
            nameContainer.appendChild(timestamp);
            row.appendChild(nameContainer);

            // 技能图标行
            const abilityRow = document.createElement("div");
            abilityRow.style.cssText = "display:flex;gap:4px;flex-wrap:wrap;";
            for (let i = 1; i <= 5; i++) {
                const a = data.equippedAbilities.find(x => x.slotNumber === i);
                const div = document.createElement("div");
                if (a) {
                    const n = a.abilityHrid.replace("/abilities/", "");
                    div.innerHTML = `
<div class="Ability_ability__1njrh Ability_small__1GKAt">
<div class="Ability_iconContainer__3syNQ">
<svg width="36" height="36">
<use href="/static/media/abilities_sprite.fdd1b4de.svg#${n}"></use>
</svg>
</div>
<div class="Ability_level__1L-do">Lv.${a.level}</div>
</div>`;
                    const tipText = translateTrigger(data.abilityCombatTriggersMap[a.abilityHrid]);
                    div.onmouseenter = (e) => showTooltip(tipText, e.clientX, e.clientY);
                    div.onmousemove = (e) => moveTooltip(e.clientX, e.clientY);
                    div.onmouseleave = hideTooltip;
                }
                abilityRow.appendChild(div);
            }
            row.appendChild(abilityRow);

            // 消耗品图标行
            const itemRow = document.createElement("div");
            itemRow.style.cssText = "display:flex;gap:4px;margin-top:4px;flex-wrap:wrap;";
            for (let i = 0; i < 6; i++) {
                const item = data.combatConsumables[i];
                const div = document.createElement("div");
                if (item) {
                    const n = item.itemHrid.replace("/items/", "");
                    div.innerHTML = `
<div class="Item_item__2De2O Item_small__1HxwE">
<div class="Item_iconContainer__5z7j4">
<svg width="36" height="36">
<use href="/static/media/items_sprite.328d6606.svg#${n}"></use>
</svg>
</div>
</div>`;
                    const tipText = translateTrigger(data.consumableCombatTriggersMap[item.itemHrid]);
                    div.onmouseenter = (e) => showTooltip(tipText, e.clientX, e.clientY);
                    div.onmousemove = (e) => moveTooltip(e.clientX, e.clientY);
                    div.onmouseleave = hideTooltip;
                }
                itemRow.appendChild(div);
            }
            row.appendChild(itemRow);
            panel.appendChild(row);
        });
    }

    function waitForGame() {
        const interval = setInterval(() => {
            if (getPartyMap()) {
                clearInterval(interval);
                sync();
                setInterval(sync, 3000);
            }
        }, 1000);
    }

    function centerPanel() {
        if (!panelVisible || !panel) return;
        const w = panel.offsetWidth || 260;
        const h = panel.offsetHeight || 100;
        const l = Math.max(0, (window.innerWidth - w) / 2);
        const t = Math.max(0, (window.innerHeight - h) / 2);
        panel.style.left = `${l}px`;
        panel.style.top = `${t}px`;
        panelState.left = l;
        panelState.top = t;
    }

    function showPanel() {
        panelVisible = true;
        updateSelfDataNow();
        render();
        panel.style.display = "block";
        centerPanel();
    }

    function hidePanel() {
        panelVisible = false;
        panel.style.display = "none";
        hideTooltip();
    }

    function addStartButton() {
        const combatTabs = document.querySelector('[class^="CombatPanel_tabsComponentContainer"] [class^="TabsComponent_tabsContainer__"] > div > div > div');
        if (!combatTabs || combatTabs.querySelector('.Button_partyCache__custom')) return;

        const referenceTab = combatTabs.children[1];
        if (!referenceTab) return;

        const btn = document.createElement('div');
        btn.className = referenceTab.className + ' Button_partyCache__custom';
        btn.setAttribute('script_translatedfrom', 'New Action');
        btn.textContent = "Trigger";
        btn.addEventListener('click', showPanel);
        combatTabs.insertBefore(btn, combatTabs.lastElementChild.nextSibling);
    }

    // 初始化执行
    addNoSelectStyle();
    panel = createPanel();
    loadPanelState();
    applyBoundaryProtection();
    waitForGame();

    // 事件监听
    window.addEventListener('resize', () => {
        if (panelVisible) {
            centerPanel();
        } else {
            applyBoundaryProtection();
        }
    });

    if (typeof ResizeObserver !== 'undefined') {
        const ro = new ResizeObserver(() => {
            if (panelVisible) centerPanel();
        });
        ro.observe(document.body);
    }

    // 监听DOM变化添加按钮
    const mo = new MutationObserver(addStartButton);
    mo.observe(document.body, { childList: true, subtree: true });
    addStartButton();

    // 启动监听
    hookWebSocketForProfiles();
    startSharableWatcher();
})();