// ==UserScript==
// @name Dead Frontier Tooltip Details
// @author ils94
// @namespace http://tampermonkey.net/
// @version 1.7
// @description Add information from the wiki in the weapons infobox with bonus calculations
// @match https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=24
// @match https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=25
// @match https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=28*
// @match https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=35
// @match https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=50
// @match https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=59
// @match https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=82*
// @match https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=84
// @match https://fairview.deadfrontier.com/onlinezombiemmo/DF3D/DF3D_InventoryPage.php?page=31*
// @match https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=32*
// @license MIT
// @grant none
// ==/UserScript==
(function() {
'use strict';
let dpsData = {};
let userStats = {
totalDamage: 0,
attackSpeed: 0,
meleeBonuses: 0,
chainsawBonuses: 0,
pistolBonuses: 0,
rifleBonuses: 0,
shotgunBonuses: 0,
smgBonuses: 0,
machineGunBonuses: 0,
explosiveBonuses: 0
};
let isShiftPressed = false;
let presets = JSON.parse(localStorage.getItem('deadFrontierPresets') || '{}');
let lastSelectedPreset = localStorage.getItem('deadFrontierLastPreset') || '';
let tooltipVisibility = JSON.parse(localStorage.getItem('deadFrontierTooltipVisibility') || '{}');
const defaultVisibility = {
avgDPS: true,
avgDPSTheoretical: true,
criticalDPS: true,
criticalDPSTheoretical: true,
damagePerHit: true,
criticalDamagePerHit: true,
hitsPerSecond: true,
hitsPerSecondTheoretical: true
};
tooltipVisibility = {
...defaultVisibility,
...tooltipVisibility
};
let isTooltipContainerVisible = localStorage.getItem('deadFrontierTooltipContainerVisible') !== 'false';
document.addEventListener('keydown', (e) => {
if (e.key === 'Shift') {
isShiftPressed = true;
}
});
document.addEventListener('keyup', (e) => {
if (e.key === 'Shift') {
isShiftPressed = false;
}
});
function loadSavedStats() {
const saved = localStorage.getItem('deadFrontierUserStats');
if (saved) {
try {
const parsed = JSON.parse(saved);
Object.assign(userStats, parsed);
} catch (e) {
console.error('[DPS] Failed to parse saved stats:', e);
}
}
if (lastSelectedPreset && presets[lastSelectedPreset]) {
loadPreset(lastSelectedPreset);
}
}
function savePreset(name, stats, overwrite = false) {
if (presets[name] && !overwrite) {
const confirmOverwrite = confirm(`Preset "${name}" already exists. Do you want to overwrite it?`);
if (!confirmOverwrite) return false;
}
presets[name] = {
...stats
};
localStorage.setItem('deadFrontierPresets', JSON.stringify(presets));
updateAllPresetDropdowns();
return true;
}
function loadPreset(name, inputElements = []) {
if (!presets[name]) return;
Object.assign(userStats, presets[name]);
inputElements.forEach(({
key,
inputEl
}) => {
inputEl.value = userStats[key] || 0;
});
localStorage.setItem('deadFrontierUserStats', JSON.stringify(userStats));
localStorage.setItem('deadFrontierLastPreset', name);
lastSelectedPreset = name;
updateAllPresetDropdowns();
const staticInfoboxPages = ['page=25', 'page=28', 'page=50', 'page=59', 'page=84', 'page=31', 'page=32'];
if (staticInfoboxPages.some(page => window.location.href.includes(page))) {
injectDPSIntoStaticBoxes();
}
}
function deletePreset(name) {
if (!presets[name]) return;
if (confirm(`Are you sure you want to delete preset "${name}"?`)) {
delete presets[name];
localStorage.setItem('deadFrontierPresets', JSON.stringify(presets));
if (lastSelectedPreset === name) {
lastSelectedPreset = '';
localStorage.removeItem('deadFrontierLastPreset');
}
updateAllPresetDropdowns();
}
}
function updatePresetDropdown(dropdown) {
dropdown.innerHTML = '<option value="">Select Preset</option>';
Object.keys(presets).forEach(name => {
const option = document.createElement('option');
option.value = name;
option.textContent = name;
dropdown.appendChild(option);
});
if (lastSelectedPreset && presets[lastSelectedPreset]) {
dropdown.value = lastSelectedPreset;
}
}
function updateAllPresetDropdowns() {
const dropdowns = document.querySelectorAll('.presetDropdown');
dropdowns.forEach(dropdown => updatePresetDropdown(dropdown));
}
function createPresetDropdown() {
if (window.location.href === 'https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=25') {
return;
}
const presetContainer = document.createElement('div');
presetContainer.style.position = 'fixed';
presetContainer.style.top = '50px';
presetContainer.style.left = '5px';
presetContainer.style.backgroundColor = '#1a1a1a';
presetContainer.style.padding = '10px';
presetContainer.style.border = '2px solid #00FF00';
presetContainer.style.borderRadius = '8px';
presetContainer.style.zIndex = '1000';
presetContainer.style.color = '#00FF00';
presetContainer.style.fontFamily = 'Arial, sans-serif';
presetContainer.style.fontSize = '14px';
presetContainer.style.boxShadow = '0 4px 8px rgba(0, 255, 0, 0.3)';
presetContainer.style.display = 'flex';
presetContainer.style.flexDirection = 'column';
presetContainer.style.gap = '8px';
const dropdownContainer = document.createElement('div');
dropdownContainer.style.display = 'flex';
dropdownContainer.style.alignItems = 'center';
dropdownContainer.style.gap = '8px';
const presetLabel = document.createElement('label');
presetLabel.textContent = 'Presets:';
presetLabel.style.fontSize = '12px';
dropdownContainer.appendChild(presetLabel);
const presetDropdown = document.createElement('select');
presetDropdown.className = 'presetDropdown';
presetDropdown.style.width = '120px';
presetDropdown.style.backgroundColor = '#2a2a2a';
presetDropdown.style.color = '#00FF00';
presetDropdown.style.border = '1px solid #00FF00';
presetDropdown.style.borderRadius = '4px';
presetDropdown.style.padding = '4px';
presetDropdown.style.fontSize = '12px';
updatePresetDropdown(presetDropdown);
dropdownContainer.appendChild(presetDropdown);
presetContainer.appendChild(dropdownContainer);
const toggleButton = document.createElement('button');
toggleButton.textContent = isTooltipContainerVisible ? 'Hide Options' : 'Show Options';
toggleButton.style.backgroundColor = '#00FF00';
toggleButton.style.color = '#000';
toggleButton.style.border = 'none';
toggleButton.style.borderRadius = '5px';
toggleButton.style.padding = '10px 10px';
toggleButton.style.cursor = 'pointer';
toggleButton.style.fontSize = '12px';
toggleButton.style.fontWeight = 'bold';
toggleButton.style.minHeight = '25px';
toggleButton.style.transition = 'background-color 0.2s';
toggleButton.style.alignCenter = 'flex-start';
toggleButton.addEventListener('mouseover', () => {
toggleButton.style.backgroundColor = '#00CC00';
});
toggleButton.addEventListener('mouseout', () => {
toggleButton.style.backgroundColor = '#00FF00';
});
toggleButton.addEventListener('click', () => {
isTooltipContainerVisible = !isTooltipContainerVisible;
const tooltipContainer = document.querySelector('.tooltipVisibilityContainer');
if (tooltipContainer) {
tooltipContainer.style.display = isTooltipContainerVisible ? 'block' : 'none';
}
toggleButton.textContent = isTooltipContainerVisible ? 'Hide Options' : 'Show Options';
localStorage.setItem('deadFrontierTooltipContainerVisible', isTooltipContainerVisible);
});
presetContainer.appendChild(toggleButton);
presetDropdown.addEventListener('change', () => {
const selected = presetDropdown.value;
if (selected) {
loadPreset(selected);
}
});
document.body.appendChild(presetContainer);
}
function createTooltipVisibilityContainer() {
const container = document.createElement('div');
container.className = 'tooltipVisibilityContainer';
container.style.position = 'fixed';
container.style.top = '140px';
container.style.left = '5px';
container.style.backgroundColor = '#1a1a1a';
container.style.padding = '15px';
container.style.border = '2px solid #00FF00';
container.style.borderRadius = '8px';
container.style.zIndex = '1000';
container.style.color = '#00FF00';
container.style.fontFamily = 'Arial, sans-serif';
container.style.fontSize = '14px';
container.style.boxShadow = '0 4px 8px rgba(0, 255, 0, 0.3)';
container.style.width = '250px';
if (window.location.href === 'https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=25') {
container.style.display = 'block';
} else {
container.style.display = isTooltipContainerVisible ? 'block' : 'none';
}
const title = document.createElement('div');
title.textContent = 'Tooltip Display Options';
title.style.fontSize = '16px';
title.style.fontWeight = 'bold';
title.style.textAlign = 'center';
title.style.marginBottom = '12px';
container.appendChild(title);
const checkboxes = [{
label: 'Avg. DPS',
key: 'avgDPS'
},
{
label: 'Avg. DPS Theoretical',
key: 'avgDPSTheoretical'
},
{
label: 'Critical/AoE DPS',
key: 'criticalDPS'
},
{
label: 'Critical/AoE DPS Theoretical',
key: 'criticalDPSTheoretical'
},
{
label: 'Damage per Hit',
key: 'damagePerHit'
},
{
label: 'Critical/AoE Damage per Hit',
key: 'criticalDamagePerHit'
},
{
label: 'Hit(s) per Second',
key: 'hitsPerSecond'
},
{
label: 'Hit(s) per Second Theoretical',
key: 'hitsPerSecondTheoretical'
}
];
checkboxes.forEach(({
label,
key
}) => {
const labelEl = document.createElement('label');
labelEl.style.display = 'flex';
labelEl.style.alignItems = 'center';
labelEl.style.marginBottom = '8px';
labelEl.style.fontSize = '12px';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = tooltipVisibility[key];
checkbox.style.marginRight = '8px';
checkbox.style.cursor = 'pointer';
checkbox.addEventListener('change', () => {
tooltipVisibility[key] = checkbox.checked;
localStorage.setItem('deadFrontierTooltipVisibility', JSON.stringify(tooltipVisibility));
const staticInfoboxPages = ['page=25', 'page=28', 'page=50', 'page=59', 'page=84', 'page=31', 'page=32'];
if (staticInfoboxPages.some(page => window.location.href.includes(page))) {
injectDPSIntoStaticBoxes();
}
});
labelEl.appendChild(checkbox);
labelEl.appendChild(document.createTextNode(label));
container.appendChild(labelEl);
});
document.body.appendChild(container);
}
function createInputContainer() {
if (window.location.href !== 'https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=25') {
return;
}
const container = document.createElement('div');
container.style.position = 'fixed';
container.style.bottom = '10px';
container.style.left = '10px';
container.style.backgroundColor = '#1a1a1a';
container.style.padding = '15px';
container.style.border = '2px solid #00FF00';
container.style.borderRadius = '8px';
container.style.zIndex = '1000';
container.style.color = '#00FF00';
container.style.fontFamily = 'Arial, sans-serif';
container.style.fontSize = '14px';
container.style.boxShadow = '0 4px 8px rgba(0, 255, 0, 0.3)';
container.style.width = '250px';
const title = document.createElement('div');
title.textContent = 'Implant and Weapons Bonuses';
title.style.fontSize = '16px';
title.style.fontWeight = 'bold';
title.style.textAlign = 'center';
title.style.marginBottom = '12px';
container.appendChild(title);
const presetContainer = document.createElement('div');
presetContainer.style.display = 'flex';
presetContainer.style.alignItems = 'center';
presetContainer.style.marginBottom = '12px';
const presetLabel = document.createElement('label');
presetLabel.textContent = 'Presets:';
presetLabel.style.fontSize = '12px';
presetContainer.appendChild(presetLabel);
const presetDropdown = document.createElement('select');
presetDropdown.className = 'presetDropdown';
presetDropdown.style.marginLeft = 'auto';
presetDropdown.style.width = '120px';
presetDropdown.style.backgroundColor = '#2a2a2a';
presetDropdown.style.color = '#00FF00';
presetDropdown.style.border = '1px solid #00FF00';
presetDropdown.style.borderRadius = '4px';
presetDropdown.style.padding = '4px';
presetDropdown.style.fontSize = '12px';
updatePresetDropdown(presetDropdown);
presetContainer.appendChild(presetDropdown);
const deletePresetButton = document.createElement('button');
deletePresetButton.textContent = 'X';
deletePresetButton.style.marginLeft = '5px';
deletePresetButton.style.backgroundColor = '#FF3333';
deletePresetButton.style.color = '#000';
deletePresetButton.style.border = 'none';
deletePresetButton.style.borderRadius = '3px';
deletePresetButton.style.padding = '5px 5px';
deletePresetButton.style.cursor = 'pointer';
deletePresetButton.style.fontSize = '20px';
deletePresetButton.style.minWidth = '25px';
deletePresetButton.style.minHeight = '12px';
deletePresetButton.addEventListener('click', () => {
const selected = presetDropdown.value;
if (selected) {
deletePreset(selected);
}
});
presetContainer.appendChild(deletePresetButton);
container.appendChild(presetContainer);
const inputs = [{
label: 'Total Inflicted Damage:',
key: 'totalDamage'
},
{
label: 'Total Attack Speed:',
key: 'attackSpeed'
},
{
label: 'Melee Bonuses:',
key: 'meleeBonuses'
},
{
label: 'Chainsaw Bonuses:',
key: 'chainsawBonuses'
},
{
label: 'Pistol Bonuses:',
key: 'pistolBonuses'
},
{
label: 'Rifle Bonuses:',
key: 'rifleBonuses'
},
{
label: 'Shotgun Bonuses:',
key: 'shotgunBonuses'
},
{
label: 'SMG Bonuses:',
key: 'smgBonuses'
},
{
label: 'Machine Gun Bonuses:',
key: 'machineGunBonuses'
},
{
label: 'Explosive Bonuses:',
key: 'explosiveBonuses'
}
];
const inputElements = [];
inputs.forEach(input => {
const label = document.createElement('label');
label.textContent = input.label;
label.style.display = 'flex';
label.style.alignItems = 'center';
label.style.marginBottom = '8px';
label.style.fontSize = '12px';
const inputEl = document.createElement('input');
inputEl.type = 'text';
inputEl.inputMode = 'decimal';
inputEl.value = userStats[input.key];
inputEl.style.width = '80px';
inputEl.style.marginLeft = 'auto';
inputEl.style.backgroundColor = '#2a2a2a';
inputEl.style.color = '#00FF00';
inputEl.style.border = '1px solid #00FF00';
inputEl.style.borderRadius = '4px';
inputEl.style.padding = '4px';
inputEl.style.fontSize = '12px';
inputEl.style.outline = 'none';
inputEl.addEventListener('input', () => {
let cleaned = inputEl.value.replace(/[^0-9.]/g, '');
const parts = cleaned.split('.');
if (parts.length > 2) {
cleaned = parts[0] + '.' + parts.slice(1).join('');
}
inputEl.value = cleaned;
userStats[input.key] = parseFloat(inputEl.value) || 0;
});
label.appendChild(inputEl);
container.appendChild(label);
inputElements.push({
key: input.key,
inputEl
});
});
presetDropdown.addEventListener('change', () => {
const selected = presetDropdown.value;
if (selected) {
loadPreset(selected, inputElements);
}
});
const saveButton = document.createElement('button');
saveButton.textContent = 'Save';
saveButton.style.display = 'block';
saveButton.style.margin = '10px auto 0';
saveButton.style.backgroundColor = '#00FF00';
saveButton.style.color = '#000';
saveButton.style.border = 'none';
saveButton.style.borderRadius = '5px';
saveButton.style.padding = '15px 15px';
saveButton.style.cursor = 'pointer';
saveButton.style.fontSize = '16px';
saveButton.style.fontWeight = 'bold';
saveButton.style.transition = 'background-color 0.2s, transform 0.1s';
saveButton.style.minWidth = '50px';
saveButton.style.minHeight = '25px';
saveButton.addEventListener('mouseover', () => {
saveButton.style.backgroundColor = '#00CC00';
});
saveButton.addEventListener('mouseout', () => {
saveButton.style.backgroundColor = '#00FF00';
});
saveButton.addEventListener('click', () => {
const presetName = prompt('Enter a name for the preset:');
if (presetName && presetName.trim()) {
if (savePreset(presetName.trim(), userStats)) {
localStorage.setItem('deadFrontierUserStats', JSON.stringify(userStats));
localStorage.setItem('deadFrontierLastPreset', presetName.trim());
lastSelectedPreset = presetName.trim();
injectDPSIntoStaticBoxes();
console.log('[DPS] User stats saved:', userStats);
}
}
});
container.appendChild(saveButton);
document.body.appendChild(container);
}
function loadDPS() {
if (typeof window.weaponData === 'undefined') {
console.error('[DPS] External weapon data not loaded');
return;
}
window.weaponData.weapons.forEach(weapon => {
const key = weapon.name.toLowerCase();
dpsData[key] = {
name: weapon.name,
category: weapon.category,
dps: weapon.stats.DPS || {},
dph: weapon.stats.DPH || {},
hps: weapon.stats.HPS || {}
};
});
console.log('[DPS] Loaded', Object.keys(dpsData).length, 'entries from external JSON');
loadSavedStats();
createPresetDropdown();
createTooltipVisibilityContainer();
createInputContainer();
startWatcher();
injectDPSIntoStaticBoxes();
}
function parseSumExpression(expr) {
if (!expr || typeof expr !== 'string') return {
terms: [],
total: null,
base: null,
multiplier: null,
innerMultiplier: null
};
// Remove whitespace and normalize brackets
expr = expr.replace(/\s+/g, '').replace(/[\[\]]/g, '');
// Pattern 1: [a + b + c] x m = t (e.g., "[42 + 12 + 6] x 5 = 300")
let match = expr.match(/^(\d+\.?\d*)\+(\d+\.?\d*)\+(\d+\.?\d*)x(\d+\.?\d*)=(\d+\.?\d*)$/);
if (match) {
const sumTerms = [parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3])];
const multiplier = parseFloat(match[4]);
const total = parseFloat(match[5]);
return {
terms: sumTerms,
total,
base: null,
multiplier,
innerMultiplier: null
};
}
// Pattern 2: [a + b] x m = t (e.g., "[14.4 + 3.6] x 16 = 288")
match = expr.match(/^(\d+\.?\d*)\+(\d+\.?\d*)x(\d+\.?\d*)=(\d+\.?\d*)$/);
if (match) {
const sumTerms = [parseFloat(match[1]), parseFloat(match[2])];
const multiplier = parseFloat(match[3]);
const total = parseFloat(match[4]);
return {
terms: sumTerms,
total,
base: null,
multiplier,
innerMultiplier: null
};
}
// Pattern 3: [a x b] x m = t (e.g., "[12 x 9] x 2 = 216")
match = expr.match(/^(\d+\.?\d*)x(\d+\.?\d*)x(\d+\.?\d*)=(\d+\.?\d*)$/);
if (match) {
const base = parseFloat(match[1]);
const innerMultiplier = parseFloat(match[2]);
const multiplier = parseFloat(match[3]);
const total = parseFloat(match[4]);
return {
terms: [base],
total,
base,
multiplier,
innerMultiplier
};
}
// Pattern 4: a x m = t (e.g., "10 x 4 = 40")
match = expr.match(/^(\d+\.?\d*)x(\d+\.?\d*)=(\d+\.?\d*)$/);
if (match) {
const base = parseFloat(match[1]);
const multiplier = parseFloat(match[2]);
const total = parseFloat(match[3]);
return {
terms: [base],
total,
base,
multiplier,
innerMultiplier: null
};
}
// Pattern 5: a + b + c = t (e.g., "10 + 20 + 30 = 60")
match = expr.match(/^(\d+\.?\d*)\+(\d+\.?\d*)\+(\d+\.?\d*)=(\d+\.?\d*)$/);
if (match) {
const terms = [parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3])];
const total = parseFloat(match[4]);
return {
terms,
total,
base: null,
multiplier: null,
innerMultiplier: null
};
}
// Pattern 6: a + b = t (e.g., "10 + 20 = 30")
match = expr.match(/^(\d+\.?\d*)\+(\d+\.?\d*)=(\d+\.?\d*)$/);
if (match) {
const terms = [parseFloat(match[1]), parseFloat(match[2])];
const total = parseFloat(match[3]);
return {
terms,
total,
base: null,
multiplier: null,
innerMultiplier: null
};
}
// Pattern 7: a + b (e.g., "10 + 20")
match = expr.match(/^(\d+\.?\d*)\+(\d+\.?\d*)$/);
if (match) {
const terms = [parseFloat(match[1]), parseFloat(match[2])];
const total = terms.reduce((sum, term) => sum + term, 0);
return {
terms,
total,
base: null,
multiplier: null,
innerMultiplier: null
};
}
// Pattern 8: Single number (e.g., "50")
const singleNumber = parseFloat(expr);
if (!isNaN(singleNumber)) {
return {
terms: [singleNumber],
total: singleNumber,
base: null,
multiplier: null,
innerMultiplier: null
};
}
return {
terms: [],
total: null,
base: null,
multiplier: null,
innerMultiplier: null
};
}
function calculateBonuses(entry) {
const category = entry.category.toLowerCase();
let masteryBonus = 0;
if (category.includes('melee')) masteryBonus = userStats.meleeBonuses;
if (category.includes('chainsaw')) masteryBonus = userStats.chainsawBonuses;
if (category.includes('pistol')) masteryBonus = userStats.pistolBonuses;
if (category.includes('rifle')) masteryBonus = userStats.rifleBonuses;
if (category.includes('shotgun')) masteryBonus = userStats.shotgunBonuses;
if (category.includes('smg')) masteryBonus = userStats.smgBonuses;
if (category.includes('machine gun')) masteryBonus = userStats.machineGunBonuses;
if (category.includes('grenade launchers') || category.includes('flamethrowers')) masteryBonus = userStats.explosiveBonuses;
const damageMultiplier = 1 + (userStats.totalDamage + masteryBonus) / 100;
const speedMultiplier = 1 + userStats.attackSpeed / 100;
const dphTotalParsed = parseSumExpression(entry.dph.total);
const dphCriticalParsed = parseSumExpression(entry.dph.critical);
let dphTotal;
let dphTotalTerms = [];
// Handle complex patterns
if (dphTotalParsed.multiplier !== null) {
if (dphTotalParsed.innerMultiplier !== null) {
// Pattern: [base x innerMultiplier] x multiplier = total
const adjustedBase = (dphTotalParsed.terms[0] * damageMultiplier).toFixed(2);
dphTotal = (dphTotalParsed.terms[0] * damageMultiplier * dphTotalParsed.innerMultiplier * dphTotalParsed.multiplier).toFixed(2);
dphTotalTerms = [adjustedBase];
} else {
// Pattern: [a + b + c] x multiplier or [a + b] x multiplier = total
dphTotalTerms = dphTotalParsed.terms.map(term => (term * damageMultiplier).toFixed(2));
dphTotal = (dphTotalParsed.terms.reduce((sum, term) => sum + term, 0) * damageMultiplier * dphTotalParsed.multiplier).toFixed(2);
}
} else if (dphTotalParsed.base !== null && dphTotalParsed.multiplier !== null) {
// Pattern: base x multiplier = total
const adjustedBase = (dphTotalParsed.base * damageMultiplier).toFixed(2);
dphTotal = (dphTotalParsed.base * damageMultiplier * dphTotalParsed.multiplier).toFixed(2);
dphTotalTerms = [adjustedBase];
} else {
// Simple sum or single value
dphTotalTerms = dphTotalParsed.terms.map(term => (term * damageMultiplier).toFixed(2));
dphTotal = dphTotalParsed.total !== null ? (dphTotalParsed.total * damageMultiplier).toFixed(2) : 'N/A';
}
return {
dps: {
real: entry.dps.real ? (entry.dps.real * damageMultiplier * speedMultiplier).toFixed(2) : 'N/A',
theoretical: entry.dps.theoretical ? (entry.dps.theoretical * damageMultiplier * speedMultiplier).toFixed(2) : 'N/A',
critical: entry.dps.critical ? (entry.dps.critical * damageMultiplier * speedMultiplier).toFixed(2) : 'N/A',
theoretical_critical: entry.dps.theoretical_critical ? (entry.dps.theoretical_critical * damageMultiplier * speedMultiplier).toFixed(2) : 'N/A'
},
dph: {
total: dphTotal,
critical: dphCriticalParsed.total !== null ? (dphCriticalParsed.total * damageMultiplier).toFixed(2) : 'N/A',
totalTerms: dphTotalTerms,
criticalTerms: dphCriticalParsed.terms.map(term => (term * damageMultiplier).toFixed(2)),
base: dphTotalParsed.base,
multiplier: dphTotalParsed.multiplier,
innerMultiplier: dphTotalParsed.innerMultiplier
},
hps: {
real: entry.hps.real ? (entry.hps.real * speedMultiplier).toFixed(2) : 'N/A',
theoretical: entry.hps.theoretical ? (entry.hps.theoretical * speedMultiplier).toFixed(2) : 'N/A'
}
};
}
function generateStatsHTML(entry) {
const bonuses = calculateBonuses(entry);
const dphTotalParsed = parseSumExpression(entry.dph.total);
let dphDisplay;
let dphBonusDisplay;
if (dphTotalParsed.innerMultiplier !== null && dphTotalParsed.multiplier !== null) {
// Pattern: [base x innerMultiplier] x multiplier = total
dphDisplay = `[${dphTotalParsed.terms[0]} x ${dphTotalParsed.innerMultiplier}] x ${dphTotalParsed.multiplier} = ${dphTotalParsed.total}`;
dphBonusDisplay = `[${bonuses.dph.totalTerms[0]} x ${dphTotalParsed.innerMultiplier}] x ${dphTotalParsed.multiplier} = ${bonuses.dph.total}`;
} else if (dphTotalParsed.terms.length >= 2 && dphTotalParsed.multiplier !== null) {
// Pattern: [a + b + c] x multiplier or [a + b] x multiplier = total
dphDisplay = `[${dphTotalParsed.terms.join(' + ')}] x ${dphTotalParsed.multiplier} = ${dphTotalParsed.total}`;
dphBonusDisplay = `[${bonuses.dph.totalTerms.join(' + ')}] x ${dphTotalParsed.multiplier} = ${bonuses.dph.total}`;
} else if (dphTotalParsed.base !== null && dphTotalParsed.multiplier !== null) {
// Pattern: base x multiplier = total
dphDisplay = `${dphTotalParsed.base} x ${dphTotalParsed.multiplier} = ${dphTotalParsed.total}`;
dphBonusDisplay = `${bonuses.dph.totalTerms[0]} x ${dphTotalParsed.multiplier} = ${bonuses.dph.total}`;
} else {
// Simple sum or single value
dphDisplay = dphTotalParsed.terms.length > 1 ? dphTotalParsed.terms.join(' + ') + (dphTotalParsed.total ? ` = ${dphTotalParsed.total}` : '') : (dphTotalParsed.total || 'N/A');
dphBonusDisplay = dphTotalParsed.terms.length > 1 ? `${bonuses.dph.totalTerms.join(' + ')} = ${bonuses.dph.total}` : bonuses.dph.total;
}
const dphCriticalParsed = parseSumExpression(entry.dph.critical);
const dphCriticalDisplay = dphCriticalParsed.terms.length > 1 ? dphCriticalParsed.terms.join(' + ') + (dphCriticalParsed.total ? ` = ${dphCriticalParsed.total}` : '') : (entry.dph.critical || 'N/A');
const dphCriticalBonusDisplay = dphCriticalParsed.terms.length > 1 ? `${bonuses.dph.criticalTerms.join(' + ')} = ${bonuses.dph.critical}` : bonuses.dph.critical;
const isExplosive = entry.category === 'Grenade Launchers' || entry.category === 'Flamethrowers';
const isGrenadeLauncher = entry.category === 'Grenade Launchers';
const dpsCriticalLabel = isExplosive ? 'Avg. DPS AoE' : 'Avg. DPS Critical';
const dpsCriticalTheoreticalLabel = isExplosive ? 'Avg. DPS AoE Theoretical' : 'Avg. DPS Critical Theoretical';
const dphCriticalLabel = isExplosive ? 'Damage per AoE' : 'Damage per Hit Critical';
const baseStats = [];
const bonusStats = [];
if (tooltipVisibility.avgDPS) {
baseStats.push(`Avg. DPS: ${entry.dps.real || 'N/A'}`);
bonusStats.push(`Avg. DPS: ${bonuses.dps.real}`);
}
if (tooltipVisibility.avgDPSTheoretical) {
baseStats.push(`Avg. DPS Theoretical: ${entry.dps.theoretical || 'N/A'}`);
bonusStats.push(`Avg. DPS Theoretical: ${bonuses.dps.theoretical}`);
}
if (tooltipVisibility.criticalDPS) {
baseStats.push(`${dpsCriticalLabel}: ${entry.dps.critical || 'N/A'}`);
bonusStats.push(`${dpsCriticalLabel}: ${bonuses.dps.critical}`);
}
if (tooltipVisibility.criticalDPSTheoretical) {
baseStats.push(`${dpsCriticalTheoreticalLabel}: ${entry.dps.theoretical_critical || 'N/A'}`);
bonusStats.push(`${dpsCriticalTheoreticalLabel}: ${bonuses.dps.theoretical_critical}`);
}
if (isGrenadeLauncher && tooltipVisibility.damagePerHit) {
const targetMultipliers = [{
count: 1,
multiplier: 3.5
},
{
count: 2,
multiplier: 2.0
},
{
count: 3,
multiplier: 1.5
},
{
count: 4,
multiplier: 1.25
},
{
count: 5,
multiplier: 1.0
}
];
const baseDamages = targetMultipliers.map(({
count,
multiplier
}) => {
const baseDamage = (dphTotalParsed.total * multiplier).toFixed(2);
return `${count}: ${baseDamage}`;
}).join('<br>');
const bonusDamages = targetMultipliers.map(({
count,
multiplier
}) => {
const bonusDamage = (bonuses.dph.total * multiplier).toFixed(2);
return `${count}: ${bonusDamage}`;
}).join('<br>');
baseStats.push(`Damage per Target:<br>${baseDamages}`);
bonusStats.push(`Damage per Target:<br>${bonusDamages}`);
} else {
if (tooltipVisibility.damagePerHit) {
baseStats.push(`Damage per Hit: ${dphDisplay}`);
bonusStats.push(`Damage per Hit: ${dphBonusDisplay}`);
}
if (tooltipVisibility.criticalDamagePerHit && !isGrenadeLauncher) {
baseStats.push(`${dphCriticalLabel}: ${dphCriticalDisplay}`);
bonusStats.push(`${dphCriticalLabel}: ${dphCriticalBonusDisplay}`);
}
}
if (tooltipVisibility.hitsPerSecond) {
baseStats.push(`Hit(s) per Second: ${entry.hps.real || 'N/A'}`);
bonusStats.push(`Hit(s) per Second: ${bonuses.hps.real}`);
}
if (tooltipVisibility.hitsPerSecondTheoretical) {
baseStats.push(`Hit(s) per Second Theoretical: ${entry.hps.theoretical || 'N/A'}`);
bonusStats.push(`Hit(s) per Second Theoretical: ${bonuses.hps.theoretical}`);
}
const statsHTML = [];
if (baseStats.length > 0) {
statsHTML.push('<strong>Base Stats:</strong>', ...baseStats);
}
if (bonusStats.length > 0) {
statsHTML.push('<strong>With Bonuses:</strong>', ...bonusStats);
}
return statsHTML.map(line => {
if (line.startsWith('<strong>')) {
return `<br>${line}<br>`;
}
return line;
}).join('<br>');
}
function startWatcher() {
let tooltipWindow = null;
setInterval(() => {
const box = document.getElementById('infoBox');
if (!box || box.style.visibility === 'hidden') {
if (tooltipWindow) {
tooltipWindow.remove();
tooltipWindow = null;
}
return;
}
if (!isShiftPressed) {
if (tooltipWindow) {
tooltipWindow.remove();
tooltipWindow = null;
}
return;
}
// Extract background-image URL
const bgImage = box.style.backgroundImage;
if (!bgImage) {
if (tooltipWindow) {
tooltipWindow.remove();
tooltipWindow = null;
}
return;
}
// Parse the URL to get the file name (e.g., "xdusksaw.png")
const urlMatch = bgImage.match(/url\(["']?(.+?)["']?\)/);
if (!urlMatch || !urlMatch[1]) {
console.log('[DPS] ✗ No valid background-image URL found');
if (tooltipWindow) {
tooltipWindow.remove();
tooltipWindow = null;
}
return;
}
// Extract file name and clean it
const url = urlMatch[1];
const fileName = url.split('/').pop(); // Get the last part (e.g., "xdusksaw.png")
const weaponKey = fileName
.replace(/\.[^/.]+$/, '') // Remove extension (e.g., ".png")
.replace(/[^a-zA-Z0-9]/g, '') // Remove special characters
.toLowerCase(); // Convert to lowercase
const entry = dpsData[weaponKey];
if (!entry) {
console.log(`[DPS] ✗ ${weaponKey} (hover, no exact match)`);
if (tooltipWindow) {
tooltipWindow.remove();
tooltipWindow = null;
}
return;
}
if (!tooltipWindow) {
tooltipWindow = document.createElement('div');
tooltipWindow.className = 'dpsTooltip';
tooltipWindow.style.position = 'absolute';
tooltipWindow.style.backgroundColor = '#1a1a1a';
tooltipWindow.style.border = '1px solid #00FF00';
tooltipWindow.style.padding = '10px';
tooltipWindow.style.color = '#00FF00';
tooltipWindow.style.fontSize = '12px';
tooltipWindow.style.zIndex = '1001';
tooltipWindow.style.borderRadius = '4px';
tooltipWindow.style.boxShadow = '0 2px 4px rgba(0, 255, 0, 0.3)';
document.body.appendChild(tooltipWindow);
}
const boxRect = box.getBoundingClientRect();
tooltipWindow.style.left = `${boxRect.right + 10}px`;
tooltipWindow.style.top = `${boxRect.top}px`;
tooltipWindow.innerHTML = generateStatsHTML(entry);
console.log(`[DPS] ✔ ${entry.name} (hover, matched via ${weaponKey})`);
}, 100);
}
function injectDPSIntoStaticBoxes() {
const staticBoxes = document.querySelectorAll('.itemName');
staticBoxes.forEach(nameEl => {
const parent = nameEl.parentElement;
if (!parent) return;
const existing = parent.querySelector('.dpsInjected');
if (existing) existing.remove();
// Extract background-image from parent
const bgImage = parent.style.backgroundImage;
if (!bgImage) {
console.log('[DPS] ✗ No background-image found for static infobox');
return;
}
// Parse the URL to get the file name
const urlMatch = bgImage.match(/url\(["']?(.+?)["']?\)/);
if (!urlMatch || !urlMatch[1]) {
console.log('[DPS] ✗ No valid background-image URL found for static infobox');
return;
}
const url = urlMatch[1];
const fileName = url.split('/').pop();
const weaponKey = fileName
.replace(/\.[^/.]+$/, '')
.replace(/[^a-zA-Z0-9]/g, '')
.toLowerCase();
const entry = dpsData[weaponKey];
if (!entry) {
console.log(`[DPS] ✗ ${weaponKey} (static, no exact match)`);
return;
}
const statsDiv = document.createElement('div');
statsDiv.className = 'itemData dpsInjected';
statsDiv.style.color = '#00FF00';
statsDiv.style.fontSize = '12px';
statsDiv.innerHTML = generateStatsHTML(entry);
parent.appendChild(statsDiv);
console.log(`[DPS] ✔ ${entry.name} (static, matched via ${weaponKey})`);
});
}
function loadExternalScript() {
const script = document.createElement('script');
script.src = 'dead_frontier_weapons.js';
script.onload = () => {
console.log('[DPS] External JSON script loaded');
loadDPS();
};
script.onerror = () => {
console.error('[DPS] Failed to load external JSON script');
};
document.head.appendChild(script);
}
loadExternalScript();
})();