您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Enhances Torn faction wars with member status highlighting, chain timer overlay, and hospital alerts. Includes built-in settings panel for better user experience.
当前为
// ==UserScript== // @name Torn War Helper (highlights and chain timers) // @namespace http://tampermonkey.net/ // @version 1.1 // @description Enhances Torn faction wars with member status highlighting, chain timer overlay, and hospital alerts. Includes built-in settings panel for better user experience. // @author EagWasTaken [3264609] // @match https://www.torn.com/factions.php* // @grant none // @run-at document-end // @license MIT // ==/UserScript== (function() { 'use strict'; const SETTINGS = { colors: { online: "rgba(134, 179, 0, 0.4)", idle: "rgba(255, 191, 0, 0.4)", offline: "rgba(220, 53, 69, 0.3)" }, darkModeColors: { online: "rgba(87, 105, 58, 0.9)", idle: "rgba(209, 129, 0, 0.3)", offline: "rgba(153, 27, 27, 0.3)" }, highlightOnline: true, highlightIdle: true, highlightOffline: false, refreshRate: 3000, flashHospitalChange: true, flashDuration: 2000, enableChainTimerBox: true, chainTimerRefreshRate: 200, chainTimerFontSize: '20px', chainTimerBoxOpacity: 0.9, chainTimerPosition: 'top-right', chainTimerBoxPadding: { top: 60, right: 10, left: 10, inner: '10px 15px' } }; const SETTINGS_UI = { panelTitle: "Torn War Helper Settings", settingsButtonId: "wh-settings-btn", settingsPanelId: "wh-settings-panel", panelPosition: { bottom: '40px', left: '20px', }, panelDimensions: { width: '300px', maxHeight: '80vh', }, saveMessageDuration: 2000, sections: [ { title: "Highlighting", settings: [ { id: "highlightOnline", label: "Highlight Online Members", type: "checkbox", default: true, tooltip: "Highlight members who are online with green background" }, { id: "highlightIdle", label: "Highlight Idle Members", type: "checkbox", default: true, tooltip: "Highlight members who are idle with amber background" }, { id: "highlightOffline", label: "Highlight Offline Members", type: "checkbox", default: false, tooltip: "Highlight members who are offline with gray background" } ] }, { title: "Chain Timer", settings: [ { id: "enableChainTimerBox", label: "Enable Chain Timer Box", type: "checkbox", default: true, tooltip: "Show the chain timer box overlay" }, { id: "chainTimerPosition", label: "Chain Timer Position", type: "select", options: [ { value: "top-right", label: "Top Right" }, { value: "top-left", label: "Top Left" }, { value: "top-center", label: "Top Center" } ], default: "top-right", tooltip: "Position of the chain timer box on screen" }, { id: "chainTimerBoxOpacity", label: "Chain Timer Opacity", type: "range", min: 0.1, max: 1.0, step: 0.1, default: 0.9, tooltip: "Transparency of the chain timer box (0.1 = mostly transparent, 1.0 = solid)" }, { id: "chainTimerFontSize", label: "Timer Font Size", type: "range", min: 12, max: 30, step: 1, valueConverter: value => `${value}px`, valueParser: value => parseInt(value.replace('px', '')), default: 20, tooltip: "Font size for the chain timer text" } ] }, { title: "Hospital Alert", settings: [ { id: "flashHospitalChange", label: "Flash Hospital Status Change", type: "checkbox", default: true, tooltip: "Flash when a player leaves hospital" }, { id: "flashDuration", label: "Flash Duration (ms)", type: "number", min: 500, max: 5000, step: 500, default: 2000, tooltip: "Duration of hospital status change flash (in milliseconds)" } ] }, { title: "Refresh Rate", settings: [ { id: "refreshRate", label: "Highlighting Refresh Rate (ms)", type: "number", min: 1000, max: 10000, step: 1000, default: 3000, tooltip: "How often to refresh member highlighting (in milliseconds)" } ] } ] }; function saveSettings() { localStorage.setItem('tornWarHelperSettings', JSON.stringify(SETTINGS)); } function loadSettings() { const savedSettings = localStorage.getItem('tornWarHelperSettings'); if (savedSettings) { try { const parsedSettings = JSON.parse(savedSettings); for (const key in parsedSettings) { if (typeof SETTINGS[key] === 'object' && !Array.isArray(SETTINGS[key])) { Object.assign(SETTINGS[key], parsedSettings[key]); } else { SETTINGS[key] = parsedSettings[key]; } } } catch (e) { // Error handled silently } } } const playerStatusHistory = new Map(); function addStyles() { const styleElement = document.createElement('style'); styleElement.textContent = ` /* Status highlighting with pseudo-elements to prevent layout shifts */ #body.tornWarHelper ul.members-list > li.wh-status--online::before, #body.tornWarHelper [class*="membersWrap___"] > li.wh-status--online::before, #body.tornWarHelper [class*="members-list"] > li.wh-status--online::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: ${SETTINGS.colors.online}; z-index: -1; pointer-events: none; } #body.tornWarHelper.dark-mode ul.members-list > li.wh-status--online::before, #body.tornWarHelper.dark-mode [class*="membersWrap___"] > li.wh-status--online::before, #body.tornWarHelper.dark-mode [class*="members-list"] > li.wh-status--online::before { background-color: ${SETTINGS.darkModeColors.online}; } /* Idle status styles */ #body.tornWarHelper ul.members-list > li.wh-status--idle::before, #body.tornWarHelper [class*="membersWrap___"] > li.wh-status--idle::before, #body.tornWarHelper [class*="members-list"] > li.wh-status--idle::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: ${SETTINGS.colors.idle}; z-index: -1; pointer-events: none; } #body.tornWarHelper.dark-mode ul.members-list > li.wh-status--idle::before, #body.tornWarHelper.dark-mode [class*="membersWrap___"] > li.wh-status--idle::before, #body.tornWarHelper.dark-mode [class*="members-list"] > li.wh-status--idle::before { background-color: ${SETTINGS.darkModeColors.idle}; } /* Offline status styles */ #body.tornWarHelper ul.members-list > li.wh-status--offline::before, #body.tornWarHelper [class*="membersWrap___"] > li.wh-status--offline::before, #body.tornWarHelper [class*="members-list"] > li.wh-status--offline::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: ${SETTINGS.colors.offline}; z-index: -1; pointer-events: none; } /* Ensure all rows have position relative for pseudo-element positioning */ #body.tornWarHelper ul.members-list > li, #body.tornWarHelper [class*="membersWrap___"] > li, #body.tornWarHelper [class*="members-list"] > li { position: relative !important; } /* Make sure no styles affect layout */ #body.tornWarHelper li.enemy .attack, #body.tornWarHelper li[class*="enemy___"] .attack, #body.tornWarHelper li[class*="your___"] .attack { position: relative; z-index: 1; } /* Animation for hospital status change flash */ @keyframes hospitalFlash { 0%, 100% { background-color: transparent; } 50% { background-color: rgba(255, 0, 0, 0.7); } } /* Apply the animation to status elements that changed from hospital */ .wh-hospital-status-change { animation: hospitalFlash ${SETTINGS.flashDuration}ms ease-in-out; } /* Chain Timer Box Styles */ #wh-chain-timer-box { position: fixed; top: ${SETTINGS.chainTimerBoxPadding.top}px; right: ${SETTINGS.chainTimerBoxPadding.right}px; left: auto; transform: none; background-color: rgba(0, 0, 0, 0.8); color: white; padding: 6px 10px; border-radius: 5px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); z-index: 9999; font-size: ${SETTINGS.chainTimerFontSize}; font-weight: bold; opacity: ${SETTINGS.chainTimerBoxOpacity}; transition: opacity 0.3s ease, background-color 0.3s ease; border: 1px solid #555; font-family: "Arial", sans-serif; min-width: 140px; display: flex; align-items: center; } /* Container for the timer content */ #wh-chain-timer-content { display: flex; align-items: center; justify-content: flex-start; width: 100%; position: relative; padding-right: 20px; /* Add padding on the right to make room for the gear icon */ } /* Label text */ #wh-chain-timer-label { margin-right: 6px; white-space: nowrap; } /* Timer value styling */ #wh-chain-timer-value { min-width: 60px; text-align: right; font-family: Arial, sans-serif; margin-right: 0; /* Remove margin as we're using container padding instead */ } /* Settings icon styling */ #wh-chain-timer-settings { position: absolute; top: 50%; transform: translateY(-50%); right: 3px; font-size: 15px; cursor: pointer; opacity: 0.8; color: white; font-weight: bold; background-color: transparent; width: 16px; height: 16px; display: flex; justify-content: center; align-items: center; box-shadow: none; padding: 0; margin: 0; z-index: 10; text-shadow: 0 0 2px #000; } #wh-chain-timer-settings:hover { opacity: 1; color: #3f96e0; } #wh-chain-timer-box:hover { opacity: 1; } #wh-chain-timer-box.warning { background-color: rgba(255, 100, 0, 0.9); animation: pulse 1s infinite; } #wh-chain-timer-box.danger { background-color: rgba(255, 0, 0, 0.9); animation: pulse 0.5s infinite; } /* Chain cooldown style */ #wh-chain-timer-box.cooldown { background-color: rgba(0, 0, 128, 0.9); /* Navy blue */ animation: none; /* No pulse animation during cooldown */ min-width: 155px; /* Wider for cooldown text */ } /* Chain reset style */ #wh-chain-timer-box.reset { background-color: rgba(0, 170, 0, 0.9); /* Bright green */ animation: flash 1s ease-in-out 3; /* Flash 3 times */ } @keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.05); } } @keyframes flash { 0%, 100% { background-color: rgba(0, 170, 0, 0.9); } 50% { background-color: rgba(0, 255, 0, 1); box-shadow: 0 0 15px rgba(0, 255, 0, 0.7); } } /* Position variants */ #wh-chain-timer-box.top-left { right: auto; left: ${SETTINGS.chainTimerBoxPadding.left}px; transform: none; } #wh-chain-timer-box.top-center { right: auto; left: 50%; transform: translateX(-50%); } #wh-chain-timer-box.top-right { left: auto; right: ${SETTINGS.chainTimerBoxPadding.right}px; transform: none; } /* Settings button styles - updated for content-title placement */ #${SETTINGS_UI.settingsButtonId} { color: #777; background: transparent; border: none; cursor: pointer; font-size: 14px; font-family: 'Lucida Grande', sans-serif; display: inline-flex; align-items: center; padding: 0 8px; line-height: 24px; vertical-align: middle; transition: color 0.2s; margin-right: 10px; } #${SETTINGS_UI.settingsButtonId}:hover { color: #069; } #${SETTINGS_UI.settingsButtonId} i { font-size: 16px; margin-right: 5px; } /* Hide text on mobile */ @media screen and (max-width: 600px) { #${SETTINGS_UI.settingsButtonId} span { display: none; } #${SETTINGS_UI.settingsButtonId} i { margin-right: 0; font-size: 18px; } } #${SETTINGS_UI.settingsPanelId} { position: fixed; bottom: ${SETTINGS_UI.panelPosition.bottom}; left: ${SETTINGS_UI.panelPosition.left}; width: ${SETTINGS_UI.panelDimensions.width}; max-height: ${SETTINGS_UI.panelDimensions.maxHeight}; background-color: #1c1c1c; border: 1px solid #444; border-radius: 5px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); z-index: 10000; display: none; overflow-y: auto; color: #fff; font-family: Arial, sans-serif; transform: translateY(20px); opacity: 0; transition: transform 0.3s ease, opacity 0.3s ease; } #${SETTINGS_UI.settingsPanelId}.visible { display: block; transform: translateY(0); opacity: 1; } #${SETTINGS_UI.settingsPanelId} .wh-settings-header { background-color: #333; padding: 10px 15px; border-bottom: 1px solid #444; font-weight: bold; display: flex; justify-content: space-between; align-items: center; } #${SETTINGS_UI.settingsPanelId} .wh-settings-close { cursor: pointer; font-size: 18px; color: #ccc; transition: color 0.2s; } #${SETTINGS_UI.settingsPanelId} .wh-settings-close:hover { color: #fff; } #${SETTINGS_UI.settingsPanelId} .wh-settings-section { padding: 10px 15px; border-bottom: 1px solid #444; } #${SETTINGS_UI.settingsPanelId} .wh-section-title { font-weight: bold; margin-bottom: 10px; color: #ccc; } #${SETTINGS_UI.settingsPanelId} .wh-setting-item { margin-bottom: 12px; display: flex; flex-direction: column; } #${SETTINGS_UI.settingsPanelId} .wh-setting-label { margin-bottom: 5px; display: flex; align-items: center; } #${SETTINGS_UI.settingsPanelId} .wh-setting-tooltip { margin-left: 5px; color: #888; font-size: 12px; cursor: help; } #${SETTINGS_UI.settingsPanelId} .wh-setting-tooltip:hover { color: #ddd; } #${SETTINGS_UI.settingsPanelId} input[type="checkbox"] { margin-right: 5px; } #${SETTINGS_UI.settingsPanelId} input[type="number"], #${SETTINGS_UI.settingsPanelId} input[type="text"], #${SETTINGS_UI.settingsPanelId} select { background-color: #333; border: 1px solid #555; color: #fff; padding: 5px; border-radius: 3px; width: 100%; } #${SETTINGS_UI.settingsPanelId} select, #${SETTINGS_UI.settingsPanelId} input[type="number"] { height: 30px; box-sizing: border-box; appearance: none; -webkit-appearance: none; -moz-appearance: none; padding-right: 10px; } #${SETTINGS_UI.settingsPanelId} input[type="range"] { width: 100%; background-color: #333; } #${SETTINGS_UI.settingsPanelId} .wh-range-value { margin-left: 10px; font-size: 12px; color: #aaa; } #${SETTINGS_UI.settingsPanelId} .wh-settings-footer { padding: 10px 15px; display: flex; justify-content: center; align-items: center; gap: 15px; } #${SETTINGS_UI.settingsPanelId} .wh-save-btn { background-color: #5cb85c; color: #fff; border: none; padding: 4px 10px; border-radius: 3px; cursor: pointer; transition: background-color 0.2s; font-size: 12px; min-width: 60px; } #${SETTINGS_UI.settingsPanelId} .wh-save-btn:hover { background-color: #4cae4c; } #${SETTINGS_UI.settingsPanelId} .wh-reset-btn { background-color: #d9534f; color: #fff; border: none; padding: 4px 10px; border-radius: 3px; cursor: pointer; transition: background-color 0.2s; font-size: 12px; min-width: 60px; } #${SETTINGS_UI.settingsPanelId} .wh-reset-btn:hover { background-color: #c9302c; } #${SETTINGS_UI.settingsPanelId} .wh-save-message { color: #5cb85c; font-size: 12px; transition: opacity 0.5s; opacity: 0; position: absolute; bottom: 45px; left: 0; right: 0; text-align: center; } #${SETTINGS_UI.settingsPanelId} .wh-save-message.visible { opacity: 1; } `; document.head.appendChild(styleElement); } function getRows(type) { if (type === 'enemy') { return [ ...document.querySelectorAll('li.enemy'), ...document.querySelectorAll('li[class*="enemy___"]') ]; } else { return [ ...document.querySelectorAll('li[class*="your___"]'), ...document.querySelectorAll('li.your') ]; } } function processRows(rows) { if (window.location.href.includes('chain')) { return; } rows.forEach(row => { const isInChain = row.closest('[class*="chainStatus"]') !== null || row.closest('[id*="chain"]') !== null || row.closest('[class*="chain-"]') !== null; if (isInChain) return; row.classList.remove('wh-status--online', 'wh-status--idle', 'wh-status--offline'); let playerId = extractPlayerId(row); processHospitalStatus(row, playerId); const statusValue = determinePlayerStatus(row); if (statusValue) { if (statusValue === 'online' && SETTINGS.highlightOnline) { row.classList.add('wh-status--online'); } else if (statusValue === 'idle' && SETTINGS.highlightIdle) { row.classList.add('wh-status--idle'); } else if (statusValue === 'offline' && SETTINGS.highlightOffline) { row.classList.add('wh-status--offline'); } } }); } function extractPlayerId(row) { const playerIdElement = row.querySelector('[data-player], [data-id], [data-member]'); if (playerIdElement) { return playerIdElement.getAttribute('data-player') || playerIdElement.getAttribute('data-id') || playerIdElement.getAttribute('data-member'); } const nameElement = row.querySelector('[class*="name"], .playerName, .userName'); if (nameElement) { return nameElement.textContent.trim(); } return 'player_' + row.textContent.trim().slice(0, 20); } function processHospitalStatus(row, playerId) { if (!SETTINGS.flashHospitalChange || !playerId) return; const statusElements = row.querySelectorAll('[class*="status___"], [class*="prevColumn___"], .status, .userStatus'); let isHospital = false; let isOk = false; let statusElement = null; statusElements.forEach(el => { const classNames = el.className; if (classNames.includes('status___') || classNames.includes('prevColumn___')) { statusElement = el; isHospital = classNames.includes('not-ok') || el.textContent.toLowerCase().includes('hospital'); isOk = classNames.includes('ok') || el.textContent.toLowerCase().includes('ok'); } }); if (statusElement) { const previousStatus = playerStatusHistory.get(playerId); const currentStatus = isHospital ? 'hospital' : (isOk ? 'ok' : 'unknown'); if (previousStatus === 'hospital' && currentStatus === 'ok') { statusElement.classList.remove('wh-hospital-status-change'); void statusElement.offsetWidth; statusElement.classList.add('wh-hospital-status-change'); setTimeout(() => { statusElement.classList.remove('wh-hospital-status-change'); }, SETTINGS.flashDuration); } playerStatusHistory.set(playerId, currentStatus); } } function determinePlayerStatus(row) { const statusIcon = row.querySelector('[class*="status___"], [class*="userIcon___"], .icon-status, .player-status, .memberStatusIcon'); if (statusIcon) { if (statusIcon.className.includes('online')) return 'online'; if (statusIcon.className.includes('idle')) return 'idle'; if (statusIcon.className.includes('offline')) return 'offline'; } const svgIcon = row.querySelector('svg[fill]'); if (svgIcon) { const fillAttr = svgIcon.getAttribute('fill'); if (fillAttr) { const match = fillAttr.match(/(online|offline|idle)/i); if (match) return match[0].toLowerCase(); } } const statusText = row.querySelector('[class*="status___"], .status, .userStatus'); if (statusText && statusText.textContent) { const text = statusText.textContent.toLowerCase().trim(); if (text.includes('online')) return 'online'; if (text.includes('idle')) return 'idle'; return 'offline'; } const statusElement = row.querySelector('[data-status]'); if (statusElement) { return statusElement.getAttribute('data-status').toLowerCase(); } return null; } function createChainTimerBox() { const existingTimerBox = document.getElementById('wh-chain-timer-box'); if (existingTimerBox) { existingTimerBox.remove(); } const timerBox = document.createElement('div'); timerBox.id = 'wh-chain-timer-box'; timerBox.className = SETTINGS.chainTimerPosition || 'top-right'; const timerContent = document.createElement('div'); timerContent.id = 'wh-chain-timer-content'; const timerLabel = document.createElement('span'); timerLabel.id = 'wh-chain-timer-label'; timerLabel.textContent = 'Chain: '; const timerValue = document.createElement('span'); timerValue.id = 'wh-chain-timer-value'; timerValue.textContent = '--:--'; const settingsButton = document.createElement('span'); settingsButton.id = 'wh-chain-timer-settings'; settingsButton.title = 'Settings'; settingsButton.textContent = '⚙'; settingsButton.style.lineHeight = '18px'; settingsButton.addEventListener('click', (e) => { e.stopPropagation(); toggleSettingsPanel(e); }); timerContent.appendChild(timerLabel); timerContent.appendChild(timerValue); timerContent.appendChild(settingsButton); timerBox.appendChild(timerContent); if (timerBox.offsetWidth < 140) { timerBox.style.minWidth = '140px'; } document.body.appendChild(timerBox); timerBox.addEventListener('click', (e) => { if (e.target.id === 'wh-chain-timer-settings') { return; } if (timerBox.classList.contains('top-right')) { timerBox.classList.remove('top-right'); timerBox.classList.add('top-center'); SETTINGS.chainTimerPosition = 'top-center'; } else if (timerBox.classList.contains('top-center')) { timerBox.classList.remove('top-center'); timerBox.classList.add('top-left'); SETTINGS.chainTimerPosition = 'top-left'; } else { timerBox.classList.remove('top-left'); timerBox.classList.add('top-right'); SETTINGS.chainTimerPosition = 'top-right'; } saveSettings(); }); return timerBox; } function updateChainTimerBox() { if (!SETTINGS.enableChainTimerBox) return; const timerBox = document.getElementById('wh-chain-timer-box'); if (!timerBox) { return createChainTimerBox(); } const timerLabel = document.getElementById('wh-chain-timer-label'); const timerValue = document.getElementById('wh-chain-timer-value'); if (!timerLabel || !timerValue) return; const chainState = findChainState(); if (chainState.timeText) { if (chainState.timeText.match(/^0*0:0*0$/) || chainState.timeText === '--:--') { timerBox.style.display = 'none'; window.whPreviousChainTime = null; } else { updateTimerDisplay(chainState.timeText, chainState.isCooldown); } } else { timerBox.style.display = 'none'; window.whPreviousChainTime = null; } function findChainState() { const result = { timeText: null, isCooldown: false }; const chainBoxTitle = document.querySelector('.chain-box-title'); if (chainBoxTitle && chainBoxTitle.textContent.toLowerCase().includes('cooldown')) { const timerElement = document.querySelector('.chain-box-timeleft'); if (timerElement && timerElement.textContent) { result.timeText = timerElement.textContent.trim(); result.isCooldown = true; return result; } } const cooldownElement = document.querySelector('.chain-box .chain-cooldown, [class*="chainCooldown"], .cooldown-timer'); if (cooldownElement && cooldownElement.textContent) { result.timeText = cooldownElement.textContent.trim(); result.isCooldown = true; return result; } const chainElements = document.querySelectorAll('.chain-box, [class*="chainBox"], .chain-wrap'); for (const el of chainElements) { if (el.textContent.toLowerCase().includes('cooldown')) { const timerEl = el.querySelector('.chain-box-timeleft, [class*="timerValue"], .chain-time'); if (timerEl && timerEl.textContent) { result.timeText = timerEl.textContent.trim(); result.isCooldown = true; return result; } const timerMatch = el.textContent.match(/(\d+:\d+)/); if (timerMatch && timerMatch[1]) { result.timeText = timerMatch[1]; result.isCooldown = true; return result; } } } const topBarChain = document.querySelector('.chain'); if (topBarChain && topBarChain.textContent) { if (topBarChain.textContent.toLowerCase().includes('cooldown')) { const match = topBarChain.textContent.match(/(\d+:\d+)/); if (match && match[1]) { result.timeText = match[1]; result.isCooldown = true; return result; } } } const headerChainTimer = document.querySelector('.chain-box:not(.tt-modified)'); if (headerChainTimer) { const timerElement = headerChainTimer.querySelector('.chain-box-timeleft'); if (timerElement && timerElement.textContent) { result.timeText = timerElement.textContent.trim(); return result; } } const otherTimerElement = document.querySelector('.chain-box-timeleft, [class*="chainTimerContainer"] [class*="timerValue"], .chain-box .chain-time'); if (otherTimerElement && otherTimerElement.textContent) { result.timeText = otherTimerElement.textContent.trim(); return result; } const headerTimer = document.querySelector('.header-chain-box, .websiteHeaderContent .chain'); if (headerTimer && headerTimer.textContent && headerTimer.textContent.includes('Chain:')) { const match = headerTimer.textContent.match(/Chain:\s*(\d+:\d+)/i); if (match && match[1]) { result.timeText = match[1]; return result; } } const chainNumberFormat = document.querySelector('.chain'); if (chainNumberFormat && chainNumberFormat.textContent) { const match = chainNumberFormat.textContent.match(/Chain:\d+:(\d+)/i); if (match && match[1]) { const seconds = parseInt(match[1]) / 1000; if (!isNaN(seconds)) { const minutes = Math.floor(seconds / 60); const remainingSeconds = Math.floor(seconds % 60); result.timeText = `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`; return result; } } } return result; } function updateTimerDisplay(timeString, isCooldown) { timerBox.style.display = 'flex'; let formattedTime = timeString; if (/^\d+:\d+$/.test(timeString)) { formattedTime = formattedTime.replace(/^(\d+):(\d+)$/, (match, min, sec) => { return min.padStart(2, '0') + ':' + sec.padStart(2, '0'); }); } const timeComponents = formattedTime.split(':'); const minutes = parseInt(timeComponents[0]); const seconds = parseInt(timeComponents[1]); const currentTotalSeconds = (minutes * 60) + seconds; if (isCooldown) { timerLabel.textContent = 'Cooldown: '; timerBox.style.minWidth = '170px'; } else { timerLabel.textContent = 'Chain: '; timerBox.style.minWidth = '140px'; } timerValue.textContent = formattedTime; timerBox.classList.remove('warning', 'danger', 'cooldown', 'reset'); if (!isCooldown && window.whPreviousChainTime !== null && currentTotalSeconds >= 295 && currentTotalSeconds <= 300 && window.whPreviousChainTime < 290) { timerBox.classList.add('reset'); setTimeout(() => { timerBox.classList.remove('reset'); }, 3000); } if (isCooldown) { timerBox.classList.add('cooldown'); timerBox.setAttribute('title', 'Chain Cooldown'); } else { if (minutes === 4 && seconds >= 55 || minutes === 5 && seconds === 0) { timerBox.classList.add('reset'); timerBox.setAttribute('title', 'Chain Timer - Fresh Chain'); } else if (minutes === 0) { timerBox.classList.add('warning'); if (seconds <= 30) { timerBox.classList.remove('warning'); timerBox.classList.add('danger'); } } } window.whPreviousChainTime = currentTotalSeconds; } } function initializeFontAwesome() { if (!document.querySelector('link[href*="font-awesome"]')) { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css'; document.head.appendChild(link); } } function isChatOverlaying() { const button = document.getElementById(SETTINGS_UI.settingsButtonId); const panel = document.getElementById(SETTINGS_UI.settingsPanelId); if (!button) return false; const buttonRect = button.getBoundingClientRect(); const panelRect = panel && panel.classList.contains('visible') ? panel.getBoundingClientRect() : null; const chatElements = document.querySelectorAll('.chat-box, .chatWindow, .chat-active, .chat-box-content, [class*="chatBox"], .chat-box-input-wrap, .chatContainer, .chat-wrap, .dialogue-box, .message-box'); for (const chat of chatElements) { const chatRect = chat.getBoundingClientRect(); const buttonOverlap = !(chatRect.right < buttonRect.left || chatRect.left > buttonRect.right || chatRect.bottom < buttonRect.top || chatRect.top > buttonRect.bottom); let panelOverlap = false; if (panelRect) { panelOverlap = !(chatRect.right < panelRect.left || chatRect.left > panelRect.right || chatRect.bottom < panelRect.top || chatRect.top > panelRect.bottom); } if (buttonOverlap || panelOverlap) { return true; } } return false; } function updateVisibility() { const button = document.getElementById(SETTINGS_UI.settingsButtonId); const panel = document.getElementById(SETTINGS_UI.settingsPanelId); if (!button) return; if (isChatOverlaying()) { button.style.opacity = '0'; button.style.pointerEvents = 'none'; if (panel && panel.classList.contains('visible')) { panel.classList.remove('visible'); } } else { button.style.opacity = '1'; button.style.pointerEvents = 'auto'; } } function toggleSettingsPanel(e) { if (e) { e.stopPropagation(); } const panel = document.getElementById(SETTINGS_UI.settingsPanelId); if (!panel) return; const timerBox = document.getElementById('wh-chain-timer-box'); if (timerBox) { const timerRect = timerBox.getBoundingClientRect(); panel.style.top = (timerRect.bottom + 10) + 'px'; panel.style.left = '50%'; panel.style.transform = 'translateX(-50%)'; panel.style.right = 'auto'; panel.style.bottom = 'auto'; } else { panel.style.top = '100px'; panel.style.left = '50%'; panel.style.transform = 'translateX(-50%)'; panel.style.right = 'auto'; panel.style.bottom = 'auto'; } if (!isChatOverlaying()) { panel.classList.toggle('visible'); setTimeout(updateVisibility, 50); } } function addSettingsPanelStyles() { const styleElement = document.createElement('style'); styleElement.textContent = ` #${SETTINGS_UI.settingsPanelId} { position: fixed; left: auto; right: auto; bottom: auto; top: auto; width: ${SETTINGS_UI.panelDimensions.width}; max-height: ${SETTINGS_UI.panelDimensions.maxHeight}; background-color: #1c1c1c; border: 1px solid #444; border-radius: 5px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); z-index: 10000; display: none; overflow-y: auto; color: #fff; font-family: Arial, sans-serif; transform: translateY(20px); opacity: 0; transition: transform 0.3s ease, opacity 0.3s ease; } #${SETTINGS_UI.settingsPanelId}.visible { display: block; transform: translateY(0); opacity: 1; } /* Style for settings button on chain timer */ #${SETTINGS_UI.settingsButtonId} { display: inline-block; opacity: 0.8; text-shadow: 0 0 3px rgba(0,0,0,0.5); } #${SETTINGS_UI.settingsButtonId}:hover { opacity: 1; text-shadow: 0 0 5px rgba(255,255,255,0.8); } `; document.head.appendChild(styleElement); } function initialize() { if (!window.location.href.includes('factions.php')) return; loadSettings(); initializeFontAwesome(); document.body.classList.add('tornWarHelper'); addStyles(); addSettingsPanelStyles(); setTimeout(createSettingsPanel, 1000); window.whPreviousChainTime = null; if (SETTINGS.enableChainTimerBox) { createChainTimerBox(); setInterval(updateChainTimerBox, SETTINGS.chainTimerRefreshRate); } setTimeout(() => { highlightFactionMembers(); if (SETTINGS.enableChainTimerBox) { updateChainTimerBox(); } }, 1000); setInterval(highlightFactionMembers, SETTINGS.refreshRate); setInterval(updateVisibility, 300); setupMutationObservers(); } function highlightFactionMembers() { processRows(getRows('enemy')); processRows(getRows('your')); } function setupMutationObservers() { const memberObserver = new MutationObserver(mutations => { for (const mutation of mutations) { if (mutation.type === 'childList' && mutation.target.classList && (mutation.target.classList.contains('members-list') || mutation.target.className.includes('membersWrap___') || mutation.target.className.includes('member'))) { highlightFactionMembers(); break; } } }); memberObserver.observe(document.body, { childList: true, subtree: true }); const chatObserver = new MutationObserver(() => { updateVisibility(); }); chatObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'style'] }); document.addEventListener('click', (e) => { const panel = document.getElementById(SETTINGS_UI.settingsPanelId); const settingsBtn = document.getElementById(SETTINGS_UI.settingsButtonId); if (panel && panel.classList.contains('visible') && !panel.contains(e.target) && settingsBtn && !settingsBtn.contains(e.target)) { panel.classList.remove('visible'); } }); let lastUrl = location.href; const urlObserver = new MutationObserver(() => { if (location.href !== lastUrl) { lastUrl = location.href; if (window.location.href.includes('factions.php')) { setTimeout(() => { if (SETTINGS.enableChainTimerBox && !document.getElementById('wh-chain-timer-box')) { createChainTimerBox(); } highlightFactionMembers(); if (SETTINGS.enableChainTimerBox) { updateChainTimerBox(); } }, 500); } } }); urlObserver.observe(document, { childList: true, subtree: true }); } function createSettingsPanel() { if (document.getElementById(SETTINGS_UI.settingsPanelId)) { return; } const panel = document.createElement('div'); panel.id = SETTINGS_UI.settingsPanelId; panel.innerHTML = ` <div class="wh-settings-header"> <span>${SETTINGS_UI.panelTitle}</span> <span class="wh-settings-close">×</span> </div> `; SETTINGS_UI.sections.forEach(section => { const sectionEl = document.createElement('div'); sectionEl.className = 'wh-settings-section'; sectionEl.innerHTML = `<div class="wh-section-title">${section.title}</div>`; section.settings.forEach(setting => { const settingEl = document.createElement('div'); settingEl.className = 'wh-setting-item'; let inputHtml = ''; switch (setting.type) { case 'checkbox': const checked = SETTINGS[setting.id] ? 'checked' : ''; inputHtml = ` <label class="wh-setting-label"> <input type="checkbox" id="wh-setting-${setting.id}" data-setting-id="${setting.id}" ${checked}> ${setting.label} <span class="wh-setting-tooltip" title="${setting.tooltip}">ⓘ</span> </label> `; break; case 'select': const options = setting.options.map(option => { const selected = SETTINGS[setting.id] === option.value ? 'selected' : ''; return `<option value="${option.value}" ${selected}>${option.label}</option>`; }).join(''); inputHtml = ` <label class="wh-setting-label"> ${setting.label} <span class="wh-setting-tooltip" title="${setting.tooltip}">ⓘ</span> </label> <select id="wh-setting-${setting.id}" data-setting-id="${setting.id}"> ${options} </select> `; break; case 'number': const value = SETTINGS[setting.id] || setting.default; inputHtml = ` <label class="wh-setting-label"> ${setting.label} <span class="wh-setting-tooltip" title="${setting.tooltip}">ⓘ</span> </label> <div style="display: flex; align-items: center;"> <input type="number" id="wh-setting-${setting.id}" data-setting-id="${setting.id}" min="${setting.min}" max="${setting.max}" step="${setting.step}" value="${value}" style="flex: 1;"> </div> `; break; case 'range': const rangeValue = setting.valueParser ? setting.valueParser(SETTINGS[setting.id]) : (SETTINGS[setting.id] || setting.default); inputHtml = ` <label class="wh-setting-label"> ${setting.label} <span class="wh-setting-tooltip" title="${setting.tooltip}">ⓘ</span> </label> <div style="display: flex; align-items: center;"> <input type="range" id="wh-setting-${setting.id}" data-setting-id="${setting.id}" min="${setting.min}" max="${setting.max}" step="${setting.step}" value="${rangeValue}"> <span class="wh-range-value" id="wh-range-value-${setting.id}">${rangeValue}</span> </div> `; break; } settingEl.innerHTML = inputHtml; sectionEl.appendChild(settingEl); }); panel.appendChild(sectionEl); }); const footer = document.createElement('div'); footer.className = 'wh-settings-footer'; footer.innerHTML = ` <button class="wh-reset-btn">Reset</button> <button class="wh-save-btn">Save</button> <span class="wh-save-message">Settings saved!</span> `; panel.appendChild(footer); document.body.appendChild(panel); setupSettingsPanelEvents(); } function setupSettingsPanelEvents() { const panel = document.getElementById(SETTINGS_UI.settingsPanelId); if (!panel) return; panel.querySelector('.wh-settings-close').addEventListener('click', toggleSettingsPanel); panel.querySelectorAll('input[type="range"]').forEach(input => { const settingId = input.getAttribute('data-setting-id'); const valueDisplay = document.getElementById(`wh-range-value-${settingId}`); if (valueDisplay) { input.addEventListener('input', () => { valueDisplay.textContent = input.value; }); } }); panel.querySelector('.wh-save-btn').addEventListener('click', () => { saveSettingsFromUI(); showSaveMessage(); }); panel.querySelector('.wh-reset-btn').addEventListener('click', resetSettings); document.addEventListener('click', (e) => { const settingsBtn = document.getElementById(SETTINGS_UI.settingsButtonId); if (panel.classList.contains('visible') && !panel.contains(e.target) && !settingsBtn.contains(e.target)) { toggleSettingsPanel(); } }); } function saveSettingsFromUI() { const panel = document.getElementById(SETTINGS_UI.settingsPanelId); if (!panel) return; panel.querySelectorAll('[data-setting-id]').forEach(input => { const settingId = input.getAttribute('data-setting-id'); let value; if (input.type === 'checkbox') { value = input.checked; } else if (input.type === 'number') { value = parseFloat(input.value); } else if (input.type === 'range') { const section = SETTINGS_UI.sections.find(s => s.settings.some(set => set.id === settingId) ); const setting = section?.settings.find(s => s.id === settingId); if (setting && setting.valueConverter) { value = setting.valueConverter(parseFloat(input.value)); } else { value = parseFloat(input.value); } } else { value = input.value; } SETTINGS[settingId] = value; }); saveSettings(); refreshUIWithNewSettings(); } function showSaveMessage() { const saveMsg = document.querySelector(`#${SETTINGS_UI.settingsPanelId} .wh-save-message`); if (saveMsg) { saveMsg.classList.add('visible'); setTimeout(() => { saveMsg.classList.remove('visible'); }, SETTINGS_UI.saveMessageDuration); } } function resetSettings() { if (confirm('Are you sure you want to reset all settings to defaults?')) { localStorage.removeItem('tornWarHelperSettings'); window.location.reload(); } } function refreshUIWithNewSettings() { const existingTimerBox = document.getElementById('wh-chain-timer-box'); if (existingTimerBox) { existingTimerBox.remove(); } if (SETTINGS.enableChainTimerBox) { createChainTimerBox(); updateChainTimerBox(); } document.head.querySelector('style')?.remove(); addStyles(); highlightFactionMembers(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initialize); } else { initialize(); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址