182 lines
5.9 KiB
JavaScript
182 lines
5.9 KiB
JavaScript
const form = document.getElementById('af-calculator');
|
|
const weaponSelect = document.getElementById('af-weapon-select');
|
|
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"]')
|
|
};
|
|
|
|
const resetButton = document.getElementById('af-reset');
|
|
const copyButton = document.getElementById('af-copy');
|
|
const weaponsById = new Map();
|
|
|
|
const numberOrNull = (value) => {
|
|
const parsed = Number.parseFloat(value);
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
};
|
|
|
|
const formatValue = (value, suffix = '') => {
|
|
if (value === null) {
|
|
return '--';
|
|
}
|
|
const formatted = Number.isFinite(value) ? value.toFixed(2) : '--';
|
|
return `${formatted}${suffix}`;
|
|
};
|
|
|
|
const calculate = () => {
|
|
const data = new FormData(form);
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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');
|
|
};
|
|
|
|
const setFormValue = (name, value) => {
|
|
const field = form.elements[name];
|
|
if (!field) {
|
|
return;
|
|
}
|
|
field.value = value ?? '';
|
|
};
|
|
|
|
const applyWeapon = (weapon) => {
|
|
if (!weapon) {
|
|
return;
|
|
}
|
|
setFormValue('weaponName', weapon.name ?? '');
|
|
setFormValue('baseDamage', weapon.damage ?? '');
|
|
setFormValue('projectiles', weapon.damage_multiplier ?? 1);
|
|
setFormValue('rpm', weapon.rpm ?? '');
|
|
setFormValue('magazine', weapon.mag_size ?? '');
|
|
setFormValue('reload', weapon.reload_time ?? '');
|
|
calculate();
|
|
};
|
|
|
|
const loadWeaponLibrary = async () => {
|
|
if (!weaponSelect) {
|
|
return;
|
|
}
|
|
const source = weaponSelect.dataset.source;
|
|
if (!source) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(source);
|
|
if (!response.ok) {
|
|
throw new Error('Failed to load weapons');
|
|
}
|
|
const weapons = await response.json();
|
|
const normalized = weapons
|
|
.filter((weapon) => weapon && weapon.name)
|
|
.map((weapon) => ({
|
|
id: weapon.id ?? weapon.name,
|
|
name: weapon.name,
|
|
damage: weapon.damage,
|
|
damage_multiplier: weapon.damage_multiplier ?? 1,
|
|
rpm: weapon.rpm,
|
|
mag_size: weapon.mag_size,
|
|
reload_time: weapon.reload_time
|
|
}))
|
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
normalized.forEach((weapon) => {
|
|
const key = String(weapon.id);
|
|
weaponsById.set(key, weapon);
|
|
const option = document.createElement('option');
|
|
option.value = key;
|
|
option.textContent = weapon.name;
|
|
weaponSelect.appendChild(option);
|
|
});
|
|
} catch (error) {
|
|
console.error('Weapon library load failed', error);
|
|
const option = document.createElement('option');
|
|
option.textContent = 'Weapon library unavailable';
|
|
option.disabled = true;
|
|
weaponSelect.appendChild(option);
|
|
}
|
|
};
|
|
|
|
form.addEventListener('input', calculate);
|
|
|
|
resetButton.addEventListener('click', () => {
|
|
form.reset();
|
|
calculate();
|
|
});
|
|
|
|
if (weaponSelect) {
|
|
weaponSelect.addEventListener('change', (event) => {
|
|
const selected = weaponsById.get(event.target.value);
|
|
if (!selected) {
|
|
return;
|
|
}
|
|
applyWeapon(selected);
|
|
});
|
|
}
|
|
|
|
copyButton.addEventListener('click', async () => {
|
|
const data = new FormData(form);
|
|
const name = data.get('weaponName') || 'Unnamed loadout';
|
|
const payload = [
|
|
`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);
|
|
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);
|
|
}
|
|
});
|
|
|
|
calculate();
|
|
loadWeaponLibrary();
|