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