// ==UserScript== // @name 诡镇奇谈 - 可互动提示 // @namespace http://tampermonkey.net/ // @license MIT // @version 5.6 // @description 无 // @author You // @match *://*/* // @grant none // @run-at document-start // @downloadURL https://update.greasyfork.icu/scripts/575905/%E8%AF%A1%E9%95%87%E5%A5%87%E8%B0%88%20-%20%E5%8F%AF%E4%BA%92%E5%8A%A8%E6%8F%90%E7%A4%BA.user.js // @updateURL https://update.greasyfork.icu/scripts/575905/%E8%AF%A1%E9%95%87%E5%A5%87%E8%B0%88%20-%20%E5%8F%AF%E4%BA%92%E5%8A%A8%E6%8F%90%E7%A4%BA.meta.js // ==/UserScript== (function() { 'use strict'; // --- 配置项 --- const CONFIG = { interactClassRegex: /^(?:([a-zA-Z0-9_-]+)--)?can-interact$/, checkEveryNFrames: 2 }; // --- 全局状态 & 本地存储 --- let $panel, $content; let frameCount = 0; let lastFingerprint = ""; let lastAutoClickTime = 0; let isMinimized = false; // 熔断保护与暂停状态 let isPaused = false; let clickHistory = []; // 记录最近的点击时间戳 // 记录用户打钩的卡牌 ID (刷新失效) const autoSkipCards = new Map(); // 防止重复请求API的锁 const fetchingIds = new Set(); // --- 数据持久化 (LocalStorage) --- const LOCAL_NAMES_KEY = 'tm-arkham-card-names'; const SETTINGS_KEY = 'tm-arkham-settings'; let cardNames = {}; let settings = { myInvestigator: '', neverSkipMyTurn: true }; try { cardNames = JSON.parse(localStorage.getItem(LOCAL_NAMES_KEY) || '{}'); } catch(e) {} try { settings = Object.assign(settings, JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}')); } catch(e) {} /** * 调用 ArkhamDB API 获取卡牌中文名 */ async function fetchCardNameFromAPI(baseId) { if (!baseId || fetchingIds.has(baseId) || cardNames[baseId]) return; fetchingIds.add(baseId); // 核心修改 1:处理带有后缀的特殊ID (例如 03006_Rogue_Mutated20) // 尝试提取最前面的 5 位数字作为真实的 ArkhamDB 卡牌编号 let apiId = baseId; const prefixMatch = baseId.match(/^(\d{5})/); if (prefixMatch) { apiId = prefixMatch[1]; } else { apiId = baseId.replace(/[a-zA-Z]$/, ''); } try { const response = await fetch(`https://zh.arkhamdb.com/api/public/card/${apiId}`); if (response.ok) { const data = await response.json(); if (data && data.name) { cardNames[baseId] = data.name; localStorage.setItem(LOCAL_NAMES_KEY, JSON.stringify(cardNames)); lastFingerprint = ""; return; } } throw new Error("无效返回或未找到卡牌"); } catch (error) { cardNames[baseId] = baseId; localStorage.setItem(LOCAL_NAMES_KEY, JSON.stringify(cardNames)); } finally { fetchingIds.delete(baseId); } } /** * 切换暂停状态 */ function setPauseState(paused) { isPaused = paused; const $pauseBtn = $panel.querySelector('.tm-pause-btn'); if ($pauseBtn) { if (paused) { $pauseBtn.innerHTML = ``; $pauseBtn.title = "已暂停 (点击恢复)"; } else { $pauseBtn.innerHTML = ``; $pauseBtn.title = "点击暂停自动跳过"; } } lastFingerprint = ""; // 强制刷新界面以显示或隐藏警告条 } /** * 注入专属CSS */ function injectStyles() { const style = document.createElement('style'); style.textContent = ` #tm-interact-panel { transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s; } #tm-interact-panel.tm-minimized { left: auto !important; right: 0 !important; transform: translateX(calc(100% - 24px)) !important; width: auto !important; min-width: 0 !important; cursor: pointer; } #tm-interact-panel.tm-minimized .tm-header-normal, #tm-interact-panel.tm-minimized .tm-content { display: none !important; } #tm-interact-panel.tm-minimized .tm-restore-tab { display: flex !important; } .tm-restore-tab { display: none; padding: 12px 6px; align-items: center; justify-content: center; color: #fff; opacity: 0.8; } .tm-restore-tab:hover { opacity: 1; background: rgba(255, 255, 255, 0.1); } .tm-skip-checkbox { appearance: none; width: 16px; height: 16px; background: rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.5); border-radius: 4px; cursor: pointer; position: relative; flex-shrink: 0; } .tm-skip-checkbox:checked { background: #4ade80; border-color: #4ade80; } .tm-skip-checkbox:checked::after { content: ''; position: absolute; left: 4px; top: 1px; width: 4px; height: 8px; border: solid white; border-width: 0 2px 2px 0; transform: rotate(45deg); } .tm-content { max-height: 50vh; overflow-y: auto; } .tm-content::-webkit-scrollbar { width: 4px; } .tm-content::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); border-radius: 2px; } `; document.head.appendChild(style); } /** * 初始化面板 */ function createPanel() { if ($panel) return; injectStyles(); $panel = document.createElement('div'); $panel.id = 'tm-interact-panel'; $panel.style.cssText = ` position: fixed; top: 20px; right: 20px; z-index: 999999; background: rgba(94, 123, 115, 0.9); border-radius: 16px; box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2); border: 1px solid rgba(255, 255, 255, 0.3); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); overflow: hidden; width: clamp(220px, 25vw, 320px); max-width: fit-content; display: none; flex-direction: column; color: #ffffff; font-family: system-ui, sans-serif; font-size: 13px; `; const $headerNormal = document.createElement('div'); $headerNormal.className = 'tm-header-normal'; $headerNormal.style.cssText = ` padding: 10px 16px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); background: rgba(0, 0, 0, 0.25); display: flex; justify-content: space-between; align-items: center; cursor: grab; user-select: none; `; $headerNormal.innerHTML = ` 可互动提示
`; const $restoreTab = document.createElement('div'); $restoreTab.className = 'tm-restore-tab'; $restoreTab.innerHTML = ``; $content = document.createElement('div'); $content.className = 'tm-content'; $content.style.cssText = `padding: 12px 16px; display: flex; flex-direction: column; gap: 8px;`; $panel.appendChild($headerNormal); $panel.appendChild($restoreTab); $panel.appendChild($content); document.body.appendChild($panel); // 按钮事件绑定 const $pauseBtn = $headerNormal.querySelector('.tm-pause-btn'); $pauseBtn.addEventListener('click', (e) => { e.stopPropagation(); setPauseState(!isPaused); }); $pauseBtn.addEventListener('mouseenter', () => $pauseBtn.style.opacity = '1'); $pauseBtn.addEventListener('mouseleave', () => $pauseBtn.style.opacity = '0.8'); // 拖拽与最小化逻辑 let isDragging = false; $headerNormal.addEventListener('mousedown', (e) => { if (e.target.closest('button')) return; isDragging = true; $headerNormal.style.cursor = 'grabbing'; const rect = $panel.getBoundingClientRect(); $panel.style.right = 'auto'; $panel.style.bottom = 'auto'; $panel.style.left = rect.left + 'px'; $panel.style.top = rect.top + 'px'; const startX = e.clientX, startY = e.clientY, startLeft = rect.left, startTop = rect.top; const onMouseMove = (moveEvent) => { if (!isDragging) return; $panel.style.left = (startLeft + moveEvent.clientX - startX) + 'px'; $panel.style.top = (startTop + moveEvent.clientY - startY) + 'px'; }; const onMouseUp = () => { isDragging = false; $headerNormal.style.cursor = 'grab'; document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); }; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }); const $minBtn = $headerNormal.querySelector('.tm-min-btn'); $minBtn.addEventListener('click', (e) => { e.stopPropagation(); isMinimized = true; $panel.classList.add('tm-minimized'); }); $minBtn.addEventListener('mouseenter', () => $minBtn.style.opacity = '1'); $minBtn.addEventListener('mouseleave', () => $minBtn.style.opacity = '0.8'); $restoreTab.addEventListener('click', (e) => { e.stopPropagation(); isMinimized = false; $panel.classList.remove('tm-minimized'); }); } /** * 抓取游戏局内的玩家信息和当前行动回合 */ function getPlayersInfo() { const playerTabs = document.querySelectorAll('ul.tabs__header li'); let currentActivePlayer = ''; const availablePlayers = []; playerTabs.forEach(li => { const span = li.querySelector('span'); if (span) { const name = span.textContent.trim(); availablePlayers.push(name); if (li.classList.contains('tab--active-player')) { currentActivePlayer = name; } if (!settings.myInvestigator && li.classList.contains('tab--selected')) { settings.myInvestigator = name; localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); } } }); return { currentActivePlayer, availablePlayers }; } function getCardDetails(el) { let baseId = ""; const img = el.tagName.toLowerCase() === 'img' ? el : el.querySelector('img'); if (img && img.src) { // 核心修改 2:兼容任意非 / 和 . 的文件名字符,以支持类似 03006_Rogue_Mutated20 的命名 const match = img.src.match(/\/cards\/([^/.]+)\.[a-zA-Z0-9]+$/); if (match) baseId = match[1]; } let uniqueId = ""; const asset = el.closest('.asset') || el.closest('.asset--outer'); const container = el.closest('.card-container'); if (asset && asset.dataset.index) { uniqueId = asset.dataset.index; } else if (container && container.dataset.index) { uniqueId = container.dataset.index; } else { uniqueId = el.dataset.playabilityCardId || el.dataset.index || (img && img.dataset.playabilityCardId) || (img && img.dataset.id) || (img && img.dataset.index) || baseId || ('tm-' + Math.random().toString(36).substring(2)); } let posLabel = "场上"; const hand = document.querySelector('section.hand'); if (hand) { const handContainer = el.closest('.card-container'); if (handContainer && hand.contains(handContainer)) { const containers = Array.from(hand.querySelectorAll('.card-container')); const idx = containers.indexOf(handContainer); if (idx >= 0) posLabel = `手牌区第${idx + 1}张`; } } if (posLabel === "场上") { const inPlay = document.querySelector('section.in-play'); if ((inPlay && inPlay.contains(el)) || el.closest('section.in-play')) { posLabel = "装备区"; } } return { uniqueId, baseId, posLabel }; } function createCheckboxItem(cardData, isChecked, isInactive = false) { const { baseId, posLabel } = cardData; let customName = baseId; if (baseId) { if (cardNames[baseId]) customName = cardNames[baseId]; else if (fetchingIds.has(baseId)) customName = `${baseId} (获取中...)`; } else { customName = "未知"; } const displayText = `${posLabel} - ${customName}`; const $label = document.createElement('label'); $label.style.cssText = `display: flex; justify-content: space-between; align-items: center; cursor: pointer; padding: 4px 0; opacity: ${isInactive ? '0.6' : '1'};`; const $text = document.createElement('span'); $text.textContent = displayText; $text.title = "点击可持久化自定义卡牌名称"; $text.style.cssText = 'white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 75%; border-bottom: 1px dashed rgba(255,255,255,0.4);'; $text.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); if (!baseId) { alert("未能提取到卡牌基础编号,无法命名。"); return; } const newName = prompt(`正在为卡牌 [${baseId}] 命名\n(输入留空则删除自定义):`, cardNames[baseId] || ""); if (newName !== null) { if (newName.trim() === "") delete cardNames[baseId]; else cardNames[baseId] = newName.trim(); localStorage.setItem(LOCAL_NAMES_KEY, JSON.stringify(cardNames)); lastFingerprint = ""; } }); const $checkbox = document.createElement('input'); $checkbox.type = 'checkbox'; $checkbox.className = 'tm-skip-checkbox'; $checkbox.checked = isChecked; $checkbox.addEventListener('change', (e) => { if (e.target.checked) autoSkipCards.set(cardData.uniqueId, cardData); else autoSkipCards.delete(cardData.uniqueId); lastFingerprint = ""; }); $label.appendChild($text); $label.appendChild($checkbox); return $label; } /** * 统一的 UI 渲染函数 */ function renderUnifiedPanel(isSkipMode, currentCounts, activeSkipCards, playerInfo, preventSkip) { if (!$content) return; $content.innerHTML = ''; let hasContent = false; // --- 0. 顶部回合与设置模块 --- if (playerInfo.availablePlayers.length > 0) { hasContent = true; const $turnHeader = document.createElement('div'); $turnHeader.style.cssText = 'font-size: 12px; border-bottom: 1px solid rgba(255,255,255,0.2); margin-bottom: 6px; padding-bottom: 6px;'; $turnHeader.innerHTML = `
行动阶段: ${playerInfo.currentActivePlayer || '未知'}
本命角色:
`; const $select = $turnHeader.querySelector('.tm-my-player-select'); $select.addEventListener('change', (e) => { settings.myInvestigator = e.target.value; localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); lastFingerprint = ""; }); const $cb = $turnHeader.querySelector('.tm-no-skip-checkbox'); $cb.addEventListener('change', (e) => { settings.neverSkipMyTurn = e.target.checked; localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); lastFingerprint = ""; }); $content.appendChild($turnHeader); } // --- 1. 警告条展示 (暂停状态 / 回合保护) --- if (isPaused) { hasContent = true; const $pausedAlert = document.createElement('div'); $pausedAlert.innerHTML = `⏸️ 自动跳过已暂停
(点击右上角 ▶️ 恢复)`; $pausedAlert.style.cssText = 'color: #fca5a5; font-size: 12px; margin-bottom: 6px; padding: 6px 4px; background: rgba(239, 68, 68, 0.2); border-radius: 4px; text-align: center; line-height: 1.2;'; $content.appendChild($pausedAlert); } else if (isSkipMode && preventSkip) { hasContent = true; const $warning = document.createElement('div'); $warning.textContent = "当前是你的回合,自动跳过暂时挂起"; $warning.style.cssText = 'color: #93c5fd; font-size: 11px; margin-bottom: 4px; padding: 4px; background: rgba(59, 130, 246, 0.2); border-radius: 4px; text-align: center;'; $content.appendChild($warning); } // --- 2. 普通提示模式 --- if (!isSkipMode && Object.keys(currentCounts).length > 0) { hasContent = true; Object.keys(currentCounts).forEach(key => { const $item = document.createElement('div'); $item.style.cssText = `display: flex; justify-content: space-between; align-items: center;`; $item.innerHTML = `${key}可以互动x${currentCounts[key]}`; $content.appendChild($item); }); } // --- 3. 触发跳过模式 (当前亮起的卡牌) --- if (isSkipMode && activeSkipCards.length > 0) { hasContent = true; const $title = document.createElement('div'); $title.textContent = '请勾选要跳过的卡牌 (点击改名):'; $title.style.cssText = 'font-size: 12px; color: #fbbf24; margin-bottom: 4px; padding-bottom: 4px; border-bottom: 1px solid rgba(255,255,255,0.2);'; $content.appendChild($title); activeSkipCards.forEach(cardData => { const isChecked = autoSkipCards.has(cardData.uniqueId); $content.appendChild(createCheckboxItem(cardData, isChecked)); }); } // --- 4. 常驻的已勾选列表 --- const inactiveSavedCards = Array.from(autoSkipCards.entries()).filter(([uniqueId]) => { if (!isSkipMode) return true; return !activeSkipCards.some(c => c.uniqueId === uniqueId); }); if (inactiveSavedCards.length > 0) { if (hasContent) { const $divider = document.createElement('div'); $divider.style.cssText = 'height: 1px; background: rgba(255,255,255,0.2); margin: 6px 0 2px 0;'; $content.appendChild($divider); } hasContent = true; const $titleRow = document.createElement('div'); $titleRow.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; margin-top: 4px;'; $titleRow.innerHTML = `
已设置自动跳过:
`; const $clearBtn = document.createElement('button'); $clearBtn.textContent = '清空名单'; $clearBtn.style.cssText = 'background: rgba(239, 68, 68, 0.2); border: 1px solid rgba(239, 68, 68, 0.5); color: #fca5a5; font-size: 11px; border-radius: 4px; cursor: pointer; padding: 2px 6px;'; $clearBtn.addEventListener('click', () => { autoSkipCards.clear(); lastFingerprint = ""; }); $titleRow.appendChild($clearBtn); $content.appendChild($titleRow); inactiveSavedCards.forEach(([uniqueId, cardData]) => { $content.appendChild(createCheckboxItem(cardData, true, true)); }); } $panel.style.display = hasContent ? 'flex' : 'none'; } /** * 帧循环主函数 */ function frameTick() { frameCount++; if (frameCount % CONFIG.checkEveryNFrames !== 0) { requestAnimationFrame(frameTick); return; } const skipBtns = Array.from(document.querySelectorAll('button.skip-triggers-button')); const activeSkipBtn = skipBtns.find(btn => { if (btn.disabled) return false; const rect = btn.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; }); const targetElements = document.querySelectorAll('[class*="can-interact"]'); const interactEls = []; targetElements.forEach(el => { for (const cls of el.classList) { if (CONFIG.interactClassRegex.test(cls)) { interactEls.push(el); break; } } }); const playerInfo = getPlayersInfo(); let preventSkip = false; if (settings.neverSkipMyTurn && playerInfo.currentActivePlayer && playerInfo.currentActivePlayer === settings.myInvestigator) { preventSkip = true; } const isSkipMode = !!activeSkipBtn && interactEls.length > 0; const currentCounts = {}; const activeSkipCards = []; if (isSkipMode) { interactEls.forEach(el => { const details = getCardDetails(el); activeSkipCards.push(details); if (details.baseId && !cardNames[details.baseId] && !fetchingIds.has(details.baseId)) { fetchCardNameFromAPI(details.baseId); } }); const allChecked = activeSkipCards.length > 0 && activeSkipCards.every(c => autoSkipCards.has(c.uniqueId)); // 触发自动跳过的判定 if (allChecked && !preventSkip && !isPaused) { const now = Date.now(); // 基础防抖 (200ms) 确保不会在一瞬间点两次 if (now - lastAutoClickTime > 200) { clickHistory.push(now); // 清理 1 秒之前的记录 clickHistory = clickHistory.filter(t => now - t <= 1000); // 熔断机制:如果在1秒内出现了第3次点击,强制暂停 if (clickHistory.length >= 3) { setPauseState(true); clickHistory = []; // 清空记录,防止解禁后立即再次触发 } else { activeSkipBtn.click(); lastAutoClickTime = now; } } } } else { interactEls.forEach(el => { for (const cls of el.classList) { const match = cls.match(CONFIG.interactClassRegex); if (match) { const key = match[1] || '通用'; currentCounts[key] = (currentCounts[key] || 0) + 1; break; } } }); } const fingerprintData = { skipMode: isSkipMode, counts: currentCounts, active: activeSkipCards.map(c => c.uniqueId).join(','), saved: Array.from(autoSkipCards.keys()).join(','), names: JSON.stringify(cardNames), fetching: Array.from(fetchingIds).join(','), playerData: JSON.stringify(playerInfo), mySetting: settings.myInvestigator, noSkip: settings.neverSkipMyTurn, paused: isPaused // 暂停状态变更也强制刷新指纹 }; const currentFingerprint = JSON.stringify(fingerprintData); if (currentFingerprint !== lastFingerprint) { renderUnifiedPanel(isSkipMode, currentCounts, activeSkipCards, playerInfo, preventSkip); lastFingerprint = currentFingerprint; } requestAnimationFrame(frameTick); } function init() { createPanel(); frameTick(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();