diff --git a/static/filterWeapons.js b/static/filterWeapons.js
index 03d469f..4658696 100644
--- a/static/filterWeapons.js
+++ b/static/filterWeapons.js
@@ -1,644 +1,102 @@
-let selectedWeapon = null;
-let selectedOperative = null;
-let weapons = [];
-let globalModifiers = [];
-let selectedGunTypes = [];
-let headshotMultiplier = 0;
-let headshotBaseApplied = false;
-let ignoreHeadshots = false;
-let isEditing = false;
+const form = document.getElementById('af-calculator');
+const outputs = {
+ shotDamage: document.querySelector('[data-output="shotDamage"]'),
+ burstDps: document.querySelector('[data-output="burstDps"]'),
+ sustainedDps: document.querySelector('[data-output="sustainedDps"]'),
+ timeToEmpty: document.querySelector('[data-output="timeToEmpty"]'),
+ magDamage: document.querySelector('[data-output="magDamage"]'),
+ ttk: document.querySelector('[data-output="ttk"]')
+};
-//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));
-}
+const resetButton = document.getElementById('af-reset');
+const copyButton = document.getElementById('af-copy');
-//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);
- }
- });
-}
+const numberOrNull = (value) => {
+ const parsed = Number.parseFloat(value);
+ return Number.isFinite(parsed) ? parsed : null;
+};
-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'));
- });
+const formatValue = (value, suffix = '') => {
+ if (value === null) {
+ return '--';
}
- operativeSection.addEventListener('change', applyModifiers);
- applyModifiers();
-}
+ const formatted = Number.isFinite(value) ? value.toFixed(2) : '--';
+ return `${formatted}${suffix}`;
+};
-
-//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 calculate = () => {
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.');
+ const baseDamage = numberOrNull(data.get('baseDamage'));
+ const projectiles = numberOrNull(data.get('projectiles')) ?? 1;
+ const rpm = numberOrNull(data.get('rpm'));
+ const magazine = numberOrNull(data.get('magazine'));
+ const reload = numberOrNull(data.get('reload'));
+ const targetHealth = numberOrNull(data.get('targetHealth'));
+
+ if (!baseDamage || !rpm) {
+ Object.values(outputs).forEach((node) => {
+ node.textContent = '--';
+ });
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
- });
+
+ const shotDamage = baseDamage * projectiles;
+ const shotsPerSecond = rpm / 60;
+ const burstDps = shotDamage * shotsPerSecond;
+ const magDamage = magazine ? shotDamage * magazine : null;
+ const timeToEmpty = magazine ? magazine / shotsPerSecond : null;
+
+ let sustainedDps = null;
+ if (magDamage !== null && reload !== null && timeToEmpty !== null && (timeToEmpty + reload) > 0) {
+ sustainedDps = magDamage / (timeToEmpty + reload);
}
- populateWeapons();
- const weaponSelect = document.getElementById('weapon-select');
- weaponSelect.value = selectedWeapon.id;
- weaponSelect.dispatchEvent(new Event('change'));
- modal.style.display = 'none';
+
+ let ttk = null;
+ if (targetHealth && burstDps > 0) {
+ ttk = targetHealth / burstDps;
+ }
+
+ outputs.shotDamage.textContent = formatValue(shotDamage);
+ outputs.burstDps.textContent = formatValue(burstDps);
+ outputs.sustainedDps.textContent = formatValue(sustainedDps);
+ outputs.timeToEmpty.textContent = formatValue(timeToEmpty, 's');
+ outputs.magDamage.textContent = formatValue(magDamage);
+ outputs.ttk.textContent = formatValue(ttk, 's');
+};
+
+form.addEventListener('input', calculate);
+
+resetButton.addEventListener('click', () => {
form.reset();
- isEditing = false;
- editBtn.disabled = true;
+ calculate();
});
-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';
+copyButton.addEventListener('click', async () => {
+ const data = new FormData(form);
+ const name = data.get('weaponName') || 'Unnamed loadout';
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}`
+ `Loadout: ${name}`,
+ `Damage per shot: ${outputs.shotDamage.textContent}`,
+ `Burst DPS: ${outputs.burstDps.textContent}`,
+ `Sustained DPS: ${outputs.sustainedDps.textContent}`,
+ `Time to empty: ${outputs.timeToEmpty.textContent}`,
+ `Magazine damage: ${outputs.magDamage.textContent}`,
+ `Time to eliminate: ${outputs.ttk.textContent}`
].join('\n');
+
try {
await navigator.clipboard.writeText(payload);
- showCopyMessage('Stats copied to clipboard!', 'success');
- } catch (err) {
- console.error(err);
- showCopyMessage('Failed to copy.', 'error');
+ copyButton.textContent = 'Copied!';
+ setTimeout(() => {
+ copyButton.textContent = 'Copy summary';
+ }, 2000);
+ } catch (error) {
+ console.error('Clipboard copy failed', error);
+ copyButton.textContent = 'Copy failed';
+ setTimeout(() => {
+ copyButton.textContent = 'Copy summary';
+ }, 2000);
}
});
-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()
\ No newline at end of file
+calculate();
diff --git a/static/img/TRL_Logo.jpg b/static/img/TRL_Logo.jpg
deleted file mode 100644
index 4836f40..0000000
Binary files a/static/img/TRL_Logo.jpg and /dev/null differ
diff --git a/static/img/TRL_Logo_Custom.jpg b/static/img/TRL_Logo_Custom.jpg
deleted file mode 100644
index 174746e..0000000
Binary files a/static/img/TRL_Logo_Custom.jpg and /dev/null differ
diff --git a/static/img/menu-list-icon-app-navigation-option-page-interface-dropdown-three-lines-button-dots-sign-symbol-black-artwork-graphic-illustration-clipart-eps-vector.jpg b/static/img/menu-list-icon-app-navigation-option-page-interface-dropdown-three-lines-button-dots-sign-symbol-black-artwork-graphic-illustration-clipart-eps-vector.jpg
deleted file mode 100644
index 02c1bd4..0000000
Binary files a/static/img/menu-list-icon-app-navigation-option-page-interface-dropdown-three-lines-button-dots-sign-symbol-black-artwork-graphic-illustration-clipart-eps-vector.jpg and /dev/null differ
diff --git a/static/img/trlbg1.jpg b/static/img/trlbg1.jpg
deleted file mode 100644
index d858c66..0000000
Binary files a/static/img/trlbg1.jpg and /dev/null differ
diff --git a/static/style.css b/static/style.css
index 7fc3e07..fd1c274 100644
--- a/static/style.css
+++ b/static/style.css
@@ -27,14 +27,13 @@ input[type="text"] {
}
body {
- background-image: url("/trlstats/static/img/trlbg1.jpg");
- background-repeat: no-repeat;
+ background: radial-gradient(circle at top, #0f172a 0%, #0b1120 45%, #020617 100%);
background-attachment: fixed;
- background-size: cover;
position: relative;
z-index: auto;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif;
margin: 20px;
+ color: #e2e8f0;
}
header nav {
@@ -784,7 +783,7 @@ body::before {
content: "";
position: fixed;
inset: 0;
- background: linear-gradient(180deg, rgba(2, 6, 23, 0.35), rgba(2, 6, 23, 0.25)), var(--bg-tint);
+ background: linear-gradient(180deg, rgba(2, 6, 23, 0.45), rgba(2, 6, 23, 0.25));
pointer-events: none;
z-index: 0;
}
@@ -1060,3 +1059,284 @@ th, td {
flex: 1;
}
}
+
+/* --------------------------
+ Anomaly Front calculator
+ -------------------------- */
+.af-body {
+ margin: 0;
+ padding: 32px;
+}
+
+.af-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 24px;
+ background: rgba(15, 23, 42, 0.7);
+ border: 1px solid rgba(148, 163, 184, 0.2);
+ border-radius: 20px;
+ padding: 20px 28px;
+ margin-bottom: 24px;
+ position: relative;
+ z-index: 2;
+ backdrop-filter: blur(12px);
+}
+
+.af-brand {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.af-badge {
+ font-size: 0.9rem;
+ font-weight: 600;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ background: rgba(59, 130, 246, 0.2);
+ color: #bae6fd;
+ padding: 6px 12px;
+ border-radius: 999px;
+ border: 1px solid rgba(59, 130, 246, 0.35);
+}
+
+.af-title {
+ font-size: 1.5rem;
+ margin: 0;
+ color: #f8fafc;
+ font-weight: 600;
+}
+
+.af-subtitle {
+ margin: 4px 0 0;
+ color: #cbd5f5;
+ font-size: 0.95rem;
+}
+
+.af-nav {
+ display: flex;
+ gap: 16px;
+ flex-wrap: wrap;
+}
+
+.af-nav a {
+ color: #e2e8f0;
+ font-weight: 500;
+ padding: 8px 14px;
+ border-radius: 999px;
+ border: 1px solid rgba(148, 163, 184, 0.2);
+ background: rgba(15, 23, 42, 0.4);
+}
+
+.af-nav a:hover {
+ color: #fff;
+ background: rgba(59, 130, 246, 0.3);
+ text-decoration: none;
+}
+
+.af-shell {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ position: relative;
+ z-index: 2;
+}
+
+.af-hero {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 24px;
+ align-items: center;
+ justify-content: space-between;
+ background: rgba(15, 23, 42, 0.65);
+ border-radius: 24px;
+ padding: 28px;
+ border: 1px solid rgba(148, 163, 184, 0.2);
+ box-shadow: 0 20px 40px rgba(2, 6, 23, 0.4);
+}
+
+.af-hero h1 {
+ margin-top: 0;
+ color: #f8fafc;
+}
+
+.af-hero p {
+ color: #cbd5f5;
+ max-width: 560px;
+}
+
+.af-status {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
+ gap: 16px;
+ width: min(520px, 100%);
+}
+
+.af-status-label {
+ text-transform: uppercase;
+ font-size: 0.75rem;
+ letter-spacing: 0.08em;
+ color: #94a3b8;
+ margin-bottom: 6px;
+}
+
+.af-status-value {
+ margin: 0;
+ font-weight: 600;
+ color: #e2e8f0;
+}
+
+.af-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 24px;
+}
+
+.af-card {
+ background: rgba(15, 23, 42, 0.65);
+ border-radius: 20px;
+ padding: 24px;
+ border: 1px solid rgba(148, 163, 184, 0.2);
+ box-shadow: 0 16px 30px rgba(2, 6, 23, 0.35);
+}
+
+.af-card-header h2 {
+ margin-top: 0;
+ color: #f8fafc;
+}
+
+.af-card-header p {
+ color: #cbd5f5;
+ margin-bottom: 20px;
+}
+
+.af-form {
+ display: grid;
+ gap: 14px;
+}
+
+.af-form label {
+ display: grid;
+ gap: 6px;
+ color: #e2e8f0;
+ font-weight: 500;
+}
+
+.af-form input {
+ background: rgba(15, 23, 42, 0.8);
+ border: 1px solid rgba(148, 163, 184, 0.3);
+ color: #f8fafc;
+}
+
+.af-form-actions {
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+ margin-top: 6px;
+}
+
+.af-form-actions button {
+ background: rgba(59, 130, 246, 0.9);
+ color: #f8fafc;
+ border: none;
+ border-radius: 999px;
+ padding: 10px 18px;
+ font-weight: 600;
+}
+
+#af-reset {
+ background: rgba(148, 163, 184, 0.4);
+}
+
+.af-metrics {
+ display: grid;
+ gap: 16px;
+}
+
+.af-metric {
+ background: rgba(2, 6, 23, 0.35);
+ border-radius: 16px;
+ padding: 16px;
+ border: 1px solid rgba(148, 163, 184, 0.15);
+}
+
+.af-metric-label {
+ text-transform: uppercase;
+ font-size: 0.75rem;
+ letter-spacing: 0.08em;
+ color: #94a3b8;
+ margin-bottom: 8px;
+}
+
+.af-metric-value {
+ margin: 0;
+ font-size: 1.4rem;
+ font-weight: 600;
+ color: #f8fafc;
+}
+
+.af-callout {
+ margin-top: 20px;
+ background: rgba(59, 130, 246, 0.15);
+ border-radius: 16px;
+ padding: 16px;
+ border: 1px solid rgba(59, 130, 246, 0.35);
+}
+
+.af-callout h3 {
+ margin-top: 0;
+ color: #e0f2fe;
+}
+
+.af-callout ul {
+ margin: 0;
+ padding-left: 18px;
+ color: #cbd5f5;
+}
+
+.af-empty {
+ display: grid;
+ gap: 12px;
+ color: #cbd5f5;
+ background: rgba(2, 6, 23, 0.3);
+ border-radius: 16px;
+ padding: 18px;
+ border: 1px dashed rgba(148, 163, 184, 0.3);
+}
+
+.af-muted-button {
+ background: rgba(148, 163, 184, 0.2);
+ border-radius: 999px;
+ padding: 10px 16px;
+ color: #e2e8f0;
+ border: none;
+ width: fit-content;
+}
+
+.af-notes {
+ width: 100%;
+ min-height: 180px;
+ border-radius: 16px;
+ padding: 16px;
+ background: rgba(15, 23, 42, 0.8);
+ border: 1px solid rgba(148, 163, 184, 0.3);
+ color: #f8fafc;
+ resize: vertical;
+}
+
+@media (max-width: 720px) {
+ .af-body {
+ padding: 20px;
+ }
+
+ .af-header {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .af-nav {
+ width: 100%;
+ justify-content: flex-start;
+ }
+}
diff --git a/templates/about_us.html b/templates/about_us.html
index 71f5b27..c0391de 100644
--- a/templates/about_us.html
+++ b/templates/about_us.html
@@ -4,7 +4,6 @@
About Us - TRL Stats
-
@@ -114,4 +113,4 @@
);