Greasy Fork is available in English.
To display trigger, MWIT must be installed (https://greasyfork.org/zh-CN/scripts/494467-mwitools).
// ==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();
})();