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