Files
anomaly-front-stats/static/filterWeapons.js
T

644 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
let selectedWeapon = null;
let selectedOperative = null;
let weapons = [];
let globalModifiers = [];
let selectedGunTypes = [];
let headshotMultiplier = 0;
let headshotBaseApplied = false;
let ignoreHeadshots = false;
let isEditing = false;
//fetch operatives data from JSON
function fetchOperativesData() {
fetch('static/json/operative.json')
.then(response => response.json())
.then(data => {
populateOperatives(data.operatives);
})
.catch(error => console.error('Error fetching operatives data:', error));
}
//populate the select element with operatives
function populateOperatives(operatives) {
const select = document.getElementById('operative-select');
select.innerHTML = "";
const placeholderOption = document.createElement('option');
placeholderOption.value = ""; // Empty value for the placeholder
placeholderOption.text = "Select an operative";
select.appendChild(placeholderOption);
operatives.forEach(operative => {
const option = document.createElement('option');
option.value = operative.id;
option.text = operative.name;
select.appendChild(option);
});
select.addEventListener('change', (event) => {
const selectedOperativeId = event.target.value;
if (!selectedOperativeId) {
document.getElementById('operative-modifiers').innerHTML = '';
return;
}
selectedOperative = operatives.find(op => op.id === selectedOperativeId);
if (selectedOperative) {
updateOperativeBuffs(selectedOperative);
}
});
}
function updateOperativeBuffs(operative) {
const operativeModifiers = operative.modifiers || [];
const operativeSection = document.getElementById('operative-modifiers');
operativeSection.innerHTML = '';
if (operativeModifiers.length > 0) {
operativeModifiers.forEach(mod => {
const label = document.createElement('label');
const input = document.createElement('input');
input.type = 'checkbox';
input.id = mod.id;
input.value = mod.value;
input.dataset.type = mod.type;
input.dataset.buffs = mod.buffs ? JSON.stringify(mod.buffs) : '';
input.checked = false;
label.appendChild(input);
label.appendChild(document.createTextNode(mod.label));
operativeSection.appendChild(label);
operativeSection.appendChild(document.createElement('br'));
});
}
operativeSection.addEventListener('change', applyModifiers);
applyModifiers();
}
//fetch weapons data from JSON
function fetchWeaponsData() {
fetch('static/json/data.json')
.then(response => response.json())
.then(data => {
weapons = data;
populateWeapons();
createGunTypeCheckboxes();
})
.catch(error => console.error('Error fetching weapons data:', error));
}
//populate the weapon dropdown
function populateWeapons() {
const select = document.getElementById('weapon-select');
select.innerHTML = "";
const placeholderOption = document.createElement('option');
placeholderOption.value = "";
placeholderOption.text = "Select a Weapon";
select.appendChild(placeholderOption);
weapons.forEach(weapon => {
const option = document.createElement('option');
option.value = weapon.id;
option.text = weapon.name;
select.appendChild(option);
});
select.addEventListener('change', (event) => {
const id = parseInt(event.target.value, 10);
if (!id) {
document.getElementById('weapon-stats').innerHTML = '';
editBtn.disabled = true;
return;
}
selectedWeapon = weapons.find(w => w.id === id);
if (selectedWeapon) {
updateWeaponStats(selectedWeapon);
applyModifiers();
editBtn.disabled = false;
}
});
}
//display the selected weapon's stats in table
function updateWeaponStats(weapon) {
const statsElement = document.getElementById('weapon-stats');
statsElement.innerHTML = `
<table class="weapon-stats-table horizontal-table">
<tr>
<th>Name</th>
<th>
<span class="tooltip-wrap">
<span>Damage</span>
<button type="button" class="tooltip-trigger" aria-label="Damage definition">?</button>
<span class="tooltip" role="tooltip">Per-bullet (or per-pellet) damage after the weapon’s internal multiplier is applied.</span>
</span>
</th>
<th>RPM</th>
<th>Mag Size</th>
<th>Reload Time</th>
<th>
<span class="tooltip-wrap">
<span>DPS</span>
<button type="button" class="tooltip-trigger" aria-label="DPS definition">?</button>
<span class="tooltip" role="tooltip">Damage per second while continuously firing. Does not include reload downtime.</span>
</span>
</th>
<th>
<span class="tooltip-wrap">
<span>DPM</span>
<button type="button" class="tooltip-trigger" aria-label="DPM definition">?</button>
<span class="tooltip" role="tooltip">Damage per minute including reload time (sustained output).</span>
</span>
</th>
<th>
<span class="tooltip-wrap">
<span>Average DPS</span>
<button type="button" class="tooltip-trigger" aria-label="Average DPS definition">?</button>
<span class="tooltip" role="tooltip">Sustained DPS including reload time and magazine downtime (DPM ÷ 60).</span>
</span>
</th>
</tr>
<tr>
<td data-label="Name" id="name-value">${weapon.name}</td>
<td data-label="Damage" id="damage-value"></td>
<td data-label="RPM" id="rpm-value"></td>
<td data-label="Mag Size" id="mag-value">${weapon.mag_size}</td>
<td data-label="Reload Time" id="reload-value"></td>
<td data-label="DPS" id="dps-value"></td>
<td data-label="DPM" id="dpm-value"></td>
<td data-label="Average DPS" id="average-dps-value"></td>
</tr>
</table>
`;
const damage = weapon.damage * weapon.damage_multiplier;
const rpm = weapon.rpm;
const reloadTime = weapon.reload_time;
document.getElementById('damage-value').innerText = damage.toFixed(2);
document.getElementById('rpm-value').innerText = rpm.toFixed(2);
document.getElementById('reload-value').innerText = reloadTime.toFixed(2) + ' seconds';
calculateDPSAndDPM(damage, rpm, weapon.mag_size, reloadTime);
}
//calculate DPS, DPM, and Average DPS, and update the values in the table
function calculateDPSAndDPM(damage, rpm, magSize, reloadTime) {
const rps = rpm / 60;
const dps = damage * rps;
const reloadTimeMin = reloadTime / 60;
const dpm = (magSize * damage) / (reloadTimeMin + (magSize / rpm));
const averageDPS = dpm / 60;
document.getElementById('dps-value').innerText = dps.toFixed(2);
document.getElementById('dpm-value').innerText = dpm.toFixed(2);
document.getElementById('average-dps-value').innerText = averageDPS.toFixed(2);
const announcer = document.getElementById("stats-announcer");
if (announcer) {
announcer.textContent = `Stats updated. DPS ${dps.toFixed(2)}. Average DPS ${averageDPS.toFixed(2)}. Reload time ${reloadTime.toFixed(2)} seconds.`;
}
}
//fetch and populate modifiers from the JSON file
function fetchModifiers() {
fetch('static/json/modifiers.json')
.then(response => response.json())
.then(data => {
globalModifiers = data;
populateModifiers(data);
})
.catch(error => console.error('Error fetching modifiers:', error));
}
//populate modifiers into the HTML
function populateModifiers(modifiers) {
const modifiersContainer = document.getElementById('modifiers-container');
modifiersContainer.innerHTML = '';
const createModifierSection = (title, modifierList, sectionId) => {
const section = document.createElement('div');
section.id = sectionId;
const header = document.createElement('h3');
header.textContent = title;
section.appendChild(header);
modifierList.forEach(mod => {
const label = document.createElement('label');
const input = document.createElement('input');
input.type = 'checkbox';
input.id = mod.id;
if (mod.buffs) {
input.dataset.buffs = JSON.stringify(mod.buffs);
} else {
input.value = mod.value;
input.dataset.type = mod.type;
}
if (mod.operatives) {
input.dataset.operatives = mod.operatives;
}
if (mod.appliesTo) {
input.dataset.appliesTo = mod.appliesTo;
}
if (mod.appliesNotTo) {
input.dataset.appliesNotTo = mod.appliesNotTo;
}
label.appendChild(input);
label.appendChild(document.createTextNode(mod.label));
const modifierItem = document.createElement('div');
modifierItem.className = 'modifier-item';
modifierItem.appendChild(label);
section.appendChild(modifierItem);
});
return section;
};
const container = document.createElement('div');
container.id = 'modifiers';
container.appendChild(createModifierSection('Item Buffs', modifiers.itemBuffs, 'items'));
container.appendChild(createModifierSection('Perks', modifiers.operativePerks, 'perks'));
modifiersContainer.appendChild(container);
const abilitiesSection = createModifierSection('Abilities', modifiers.abilityPerks, 'abilities');
modifiersContainer.appendChild(abilitiesSection);
modifiersContainer.addEventListener('change', applyModifiers);
}
function applyModifiers() {
if (!selectedWeapon) return;
const hasNVG = !!document.querySelector(
"#modifiers input[id='damage-item-multiplicative-nightvision']:checked"
);
let baseDamage = selectedWeapon.damage;
let baseRpm = selectedWeapon.rpm;
let baseReloadTime = selectedWeapon.reload_time;
let damageModifier = {additive: 0, multiplicative: 1};
let rpmModifier = {additive: 0, multiplicative: 1};
let reloadTimeModifier = {additive: 0, multiplicative: 1};
headshotMultiplier = 0;
headshotBaseApplied = false;
ignoreHeadshots = hasNVG;
function processModifier(mod) {
if (mod.dataset.operatives && mod.dataset.operatives.trim() !== "" && (!selectedOperative || selectedOperative.category !== mod.dataset.operatives)) {
return;
}
const weaponTags = selectedWeapon.guntype;
const appliesNotTo = mod.dataset.appliesNotTo;
const appliesTo = mod.dataset.appliesTo;
if (appliesNotTo && weaponTags.includes(appliesNotTo)) {
return;
}
if (appliesTo && !weaponTags.includes(appliesTo)) {
return;
}
if (mod.appliesNotTo && weaponTags.includes(mod.appliesNotTo)) {
return;
}
if (mod.appliesTo && !weaponTags.includes(mod.appliesTo)) {
return;
}
if (mod.dataset.type === 'multiplicative-headshot') {
// Only apply headshot multipliers if the base headshot modifier is active
if (headshotBaseApplied) {
headshotMultiplier += parseFloat(mod.value) - 1;
}
} else if (mod.dataset.type === 'multiplicative-headshot-base') {
if (!headshotBaseApplied) {
headshotMultiplier += parseFloat(mod.value) - 1;
headshotBaseApplied = true;
}
} else {
applyBuff(mod, rpmModifier, reloadTimeModifier, damageModifier);
}
if (mod.dataset.buffs) {
const buffs = JSON.parse(mod.dataset.buffs);
buffs.forEach(buff => {
applyBuff(buff, rpmModifier, reloadTimeModifier, damageModifier);
});
} else {
const modType = mod.dataset.type;
const modValue = parseFloat(mod.value);
const singleBuff = {
type: modType === 'multiplicative' ? 'multiplicative' : 'additive',
buffType: modType,
value: modValue
};
if (mod.id.includes('damage')) {
singleBuff.type = 'damage';
} else if (mod.id.includes('rpm')) {
singleBuff.type = 'rpm';
} else if (mod.id.includes('reload')) {
singleBuff.type = 'reloadTime';
}
applyBuff(singleBuff, rpmModifier, reloadTimeModifier, damageModifier);
}
}
//process operative-specific modifiers
const processOperativeModifier = (mod) => {
const weaponTags = selectedWeapon.guntype; //this is now an array
if (mod.appliesNotTo && weaponTags.includes(mod.appliesNotTo)) {
return;
}
if (mod.appliesTo && !weaponTags.includes(mod.appliesTo)) {
return;
}
if (mod.buffs) {
mod.buffs.forEach(buff => {
applyBuff(buff, rpmModifier, reloadTimeModifier, damageModifier);
});
} else {
const modValue = parseFloat(mod.value);
const modType = mod.type || 'additive';
const singleBuff = {
type: mod.id.includes('damage') ? 'damage' : mod.id.includes('rpm') ? 'rpm' : 'reloadTime',
buffType: modType === 'multiplicative' ? 'multiplicative' : 'additive',
value: modValue
};
applyBuff(singleBuff, rpmModifier, reloadTimeModifier, damageModifier);
}
};
const abilitiesSection = document.getElementById('abilities');
abilitiesSection.querySelectorAll('input:checked').forEach(mod => {
processModifier(mod); // Process the ability modifier the same way as global modifiers
});
const modifiersContainer = document.getElementById('modifiers');
modifiersContainer.removeEventListener('change', applyModifiers);
document.querySelectorAll('#modifiers input:checked').forEach(mod => {
processModifier(mod);
});
modifiersContainer.addEventListener('change', applyModifiers);
const operativeSection = document.getElementById('operative-modifiers');
operativeSection.querySelectorAll('input:checked').forEach(mod => {
if (selectedOperative && selectedOperative.modifiers) {
const modObj = selectedOperative.modifiers.find(m => m.id === mod.id);
if (modObj) {
processOperativeModifier(modObj);
}
}
});
let damage = baseDamage;
damage *= damageModifier.multiplicative;
if (headshotMultiplier > 0) {
damage *= (1 + headshotMultiplier);
}
damage += damageModifier.additive;
damage *= selectedWeapon.damage_multiplier;
let rpm = baseRpm * rpmModifier.multiplicative + rpmModifier.additive;
let reloadTime = baseReloadTime * reloadTimeModifier.multiplicative + reloadTimeModifier.additive;
if (reloadTime < 0.1) {
reloadTime = 0.1;
}
document.getElementById('damage-value').innerText = damage.toFixed(2);
document.getElementById('rpm-value').innerText = rpm.toFixed(2);
document.getElementById('reload-value').innerText =
reloadTime.toFixed(2) + ' seconds';
calculateDPSAndDPM(damage, rpm, selectedWeapon.mag_size, reloadTime);
}
function applyBuff(buff, rpmModifier, reloadTimeModifier, damageModifier) {
if (ignoreHeadshots &&
(buff.buffType === 'multiplicative-headshot' ||
buff.buffType === 'multiplicative-headshot-base')) {
return;
}
const {type, buffType, value} = buff;
if (buffType === 'multiplicative-headshot-base' && !headshotBaseApplied) {
headshotMultiplier = value - 1;
headshotBaseApplied = true;
return;
}
if (buffType === 'multiplicative-headshot') {
// Only apply if base headshot modifier is active
if (headshotBaseApplied) {
headshotMultiplier += value - 1;
}
return;
}
if (type === 'rpm') {
if (buffType === 'multiplicative') rpmModifier.multiplicative *= value;
else if (buffType === 'additive') rpmModifier.additive += value;
} else if (type === 'reloadTime') {
if (buffType === 'multiplicative') reloadTimeModifier.multiplicative *= value;
else if (buffType === 'additive') reloadTimeModifier.additive += value;
} else if (type === 'damage' && damageModifier) {
if (buffType === 'multiplicative') damageModifier.multiplicative *= value;
else if (buffType === 'additive') damageModifier.additive += value;
}
}
//consider gun type checkboxes
function filterWeapons() {
const input = document.getElementById('weapon-search').value.toLowerCase();
const weaponSelect = document.getElementById('weapon-select');
const options = weaponSelect.getElementsByTagName('option');
const selectedGuntypes = Array.from(document.querySelectorAll('#guntype-filters input:checked'))
.map(checkbox => checkbox.value);
let firstVisibleOption = null;
for (let i = 0; i < options.length; i++) {
const optionText = options[i].textContent || options[i].innerText;
const weaponId = parseInt(options[i].value);
const weapon = weapons.find(w => w.id === weaponId);
if (!weapon) continue;
const matchesSearch = optionText.toLowerCase().indexOf(input) > -1;
const matchesGuntypes = selectedGuntypes.length === 0 || selectedGuntypes.every(type => weapon.guntype.includes(type));
if (matchesSearch && matchesGuntypes) {
options[i].style.display = '';
if (!firstVisibleOption) {
firstVisibleOption = options[i];
}
} else {
options[i].style.display = 'none';
}
}
if (firstVisibleOption) {
weaponSelect.value = firstVisibleOption.value;
weaponSelect.dispatchEvent(new Event('change'));
}
}
//filter operatives in the dropdown based on search input
function filterOperatives() {
const input = document.getElementById('operative-search').value.toLowerCase();
const operativeSelect = document.getElementById('operative-select');
const options = operativeSelect.getElementsByTagName('option');
let firstVisibleOption = null;
for (let i = 0; i < options.length; i++) {
const optionText = options[i].textContent || options[i].innerText;
if (optionText.toLowerCase().indexOf(input) > -1) {
options[i].style.display = '';
if (!firstVisibleOption) {
firstVisibleOption = options[i];
}
} else {
options[i].style.display = 'none';
}
}
if (firstVisibleOption) {
operativeSelect.value = firstVisibleOption.value;
operativeSelect.dispatchEvent(new Event('change'));
}
}
//create gun type checkboxes based on available gun types in the weapon data
function createGunTypeCheckboxes() {
const gunTypeFiltersContainer = document.getElementById('gun-type-filters');
gunTypeFiltersContainer.innerHTML = '';
const gunTypesSet = new Set();
weapons.forEach(weapon => {
weapon.guntype.forEach(type => gunTypesSet.add(type));
});
gunTypesSet.forEach(gunType => {
const label = document.createElement('label');
const input = document.createElement('input');
input.type = 'checkbox';
input.value = gunType;
input.className = 'gun-type-checkbox';
input.addEventListener('change', () => {
const selectedCheckboxes = Array.from(document.querySelectorAll('.gun-type-checkbox:checked'));
selectedGunTypes = selectedCheckboxes.map(checkbox => checkbox.value);
filterWeapons();
});
label.appendChild(input);
label.appendChild(document.createTextNode(gunType));
gunTypeFiltersContainer.appendChild(label);
gunTypeFiltersContainer.appendChild(document.createElement('br'));
});
}
document.getElementById('weapon-search').addEventListener('keyup', filterWeapons);
document.getElementById('operative-search').addEventListener('keyup', filterOperatives);
document.querySelectorAll('#guntype-filters input').forEach(checkbox => {
checkbox.addEventListener('change', filterWeapons);
});
//Create/Edit a Weapon
const openBtn = document.getElementById('weapon-create');
const closeBtn = document.getElementById('weapon-cancel');
const modal = document.getElementById('weapon-form');
const form = document.getElementById('weaponForm');
const editBtn = document.getElementById('weapon-edit');
const editError = document.getElementById('edit-error');
const submitBtn = document.getElementById('submit-btn')
//Show the modal
openBtn.addEventListener('click', () => {
modal.style.display = 'flex';
});
closeBtn.addEventListener('click', () => {
modal.style.display = 'none';
form.reset();
isEditing = false;
});
modal.addEventListener('click', e => {
if (e.target === modal) {
modal.style.display = 'none';
form.reset();
isEditing = false;
}
});
//Handle form submission
form.addEventListener('submit', e => {
e.preventDefault();
const data = new FormData(form);
const types = Array.from(form.querySelectorAll('input[name="gun-type"]:checked'))
.map(cb => cb.value);
if (types.length === 0) {
alert('Please select at least one weapon type.');
return;
}
if (isEditing) {
selectedWeapon.name = data.get('name');
selectedWeapon.damage = parseFloat(data.get('damage'));
selectedWeapon.rpm = parseFloat(data.get('rpm'));
selectedWeapon.mag_size = parseFloat(data.get('magazin'));
selectedWeapon.reload_time = parseFloat(data.get('reload-time'));
selectedWeapon.damage_multiplier = parseFloat(data.get('multiplier'));
selectedWeapon.guntype = types;
} else {
const newId = weapons.length
? Math.max(...weapons.map(w => w.id)) + 1
: 1;
weapons.push({
id: newId,
name: data.get('name'),
damage: parseFloat(data.get('damage')),
rpm: parseFloat(data.get('rpm')),
mag_size: parseFloat(data.get('magazin')),
reload_time: parseFloat(data.get('reload-time')),
damage_multiplier: parseFloat(data.get('multiplier')),
guntype: types
});
}
populateWeapons();
const weaponSelect = document.getElementById('weapon-select');
weaponSelect.value = selectedWeapon.id;
weaponSelect.dispatchEvent(new Event('change'));
modal.style.display = 'none';
form.reset();
isEditing = false;
editBtn.disabled = true;
});
document.getElementById('copy-stats').addEventListener('click', async () => {
const statsTable = document.querySelector('#weapon-stats table tr + tr');
if (!statsTable) {
showCopyMessage('No weapon selected to copy.', 'error');
return;
}
const cells = Array.from(statsTable.querySelectorAll('td')).map(td => td.innerText.trim());
const [name, dmg, rpm, mag, reload, dps, dpm, avgDps] = cells;
const globalMods = Array.from(
document.querySelectorAll('#modifiers label input:checked')
).map(cb => cb.parentNode.textContent.trim());
const opMods = Array.from(
document.querySelectorAll('#operative-modifiers label input:checked')
).map(cb => cb.parentNode.textContent.trim());
const allMods = globalMods.concat(opMods);
const modsString = allMods.length
? allMods.join(', ')
: 'None';
const payload = [
`Weapon: ${name}`,
`Damage: ${dmg}`,
`RPM: ${rpm}`,
`Mag Size: ${mag}`,
`Reload Time: ${reload}`,
`DPS: ${dps}`,
`DPM: ${dpm}`,
`Average DPS: ${avgDps}`,
`Modifiers used for this calculation: ${modsString}`
].join('\n');
try {
await navigator.clipboard.writeText(payload);
showCopyMessage('Stats copied to clipboard!', 'success');
} catch (err) {
console.error(err);
showCopyMessage('Failed to copy.', 'error');
}
});
editBtn.addEventListener('click', () => {
if (!selectedWeapon) {
editError.textContent = 'You must select a weapon first.';
return;
}
editError.textContent = '';
isEditing = true;
isEditing = true;
form.elements['name'].value = selectedWeapon.name;
form.elements['damage'].value = selectedWeapon.damage;
form.elements['rpm'].value = selectedWeapon.rpm;
form.elements['magazin'].value = selectedWeapon.mag_size;
form.elements['reload-time'].value = selectedWeapon.reload_time;
form.elements['multiplier'].value = selectedWeapon.damage_multiplier;
form.querySelectorAll('input[name="gun-type"]').forEach(cb => {
cb.checked = selectedWeapon.guntype.includes(cb.value);
});
modal.style.display = 'flex';
});
function showCopyMessage(text, type) {
const msg = document.getElementById('copy-message');
msg.textContent = text;
msg.className = type;
setTimeout(() => {
msg.textContent = '';
msg.className = '';
}, 3000);
}
// Initialize
fetchWeaponsData();
fetchModifiers();
fetchOperativesData()