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 = `
Name Damage Per-bullet (or per-pellet) damage after the weapon’s internal multiplier is applied. RPM Mag Size Reload Time DPS Damage per second while continuously firing. Does not include reload downtime. DPM Damage per minute including reload time (sustained output). Average DPS Sustained DPS including reload time and magazine downtime (DPM ÷ 60).
${weapon.name} ${weapon.mag_size}
`; 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()