diff --git a/static/filterWeapons.js b/static/filterWeapons.js index 8fe50bc..39c6085 100644 --- a/static/filterWeapons.js +++ b/static/filterWeapons.js @@ -29,6 +29,16 @@ let allWeapons = []; let currentWeaponTypes = []; let currentOperativeCategory = null; +const normalizeToArray = (value) => { + if (!value) { + return []; + } + if (Array.isArray(value)) { + return value.filter(Boolean); + } + return [value].filter(Boolean); +}; + const numberOrNull = (value) => { const parsed = Number.parseFloat(value); return Number.isFinite(parsed) ? parsed : null; @@ -43,23 +53,39 @@ const formatValue = (value, suffix = '') => { }; const matchesOperative = (modifierOperative) => { - if (!modifierOperative) { + const required = normalizeToArray(modifierOperative); + if (!required.length) { return true; } if (!currentOperativeCategory) { return false; } - return modifierOperative.toLowerCase() === currentOperativeCategory.toLowerCase(); + const operative = currentOperativeCategory.toLowerCase(); + return required.some((entry) => entry.toLowerCase() === operative); }; const matchesWeaponType = (appliesTo) => { - if (!appliesTo) { + const required = normalizeToArray(appliesTo); + if (!required.length) { return true; } if (!currentWeaponTypes.length) { return false; } - return currentWeaponTypes.map((type) => type.toUpperCase()).includes(appliesTo.toUpperCase()); + const available = currentWeaponTypes.map((type) => type.toUpperCase()); + return required.some((type) => available.includes(type.toUpperCase())); +}; + +const matchesWeaponExclusion = (appliesNotTo) => { + const excluded = normalizeToArray(appliesNotTo); + if (!excluded.length) { + return true; + } + if (!currentWeaponTypes.length) { + return true; + } + const available = currentWeaponTypes.map((type) => type.toUpperCase()); + return !excluded.some((type) => available.includes(type.toUpperCase())); }; const applyBuffValue = (current, value, buffType) => { @@ -79,7 +105,11 @@ const applyModifierToStats = (modifier, stats) => { if (!modifier) { return stats; } - if (!matchesOperative(modifier.operatives) || !matchesWeaponType(modifier.appliesTo)) { + if ( + !matchesOperative(modifier.operatives) + || !matchesWeaponType(modifier.appliesTo) + || !matchesWeaponExclusion(modifier.appliesNotTo) + ) { return stats; } @@ -141,17 +171,18 @@ const updateActiveModifierList = (modifiers) => { const calculate = () => { const data = new FormData(form); const baseDamage = numberOrNull(data.get('baseDamage')); - const projectiles = numberOrNull(data.get('projectiles')) ?? 1; + const projectilesRaw = numberOrNull(data.get('projectiles')); + const projectiles = projectilesRaw !== null ? Math.max(projectilesRaw, 1) : 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) { + if (baseDamage === null || rpm === null || baseDamage < 0 || rpm <= 0) { Object.values(outputs).forEach((node) => { node.textContent = '--'; }); - updateActiveModifierList([]); + updateActiveModifierList(getSelectedModifiers()); return; } @@ -165,19 +196,31 @@ const calculate = () => { } ); - const shotDamage = stats.damage * projectiles; - const shotsPerSecond = stats.rpm / 60; + const safeStats = { + damage: Math.max(stats.damage, 0), + rpm: Math.max(stats.rpm, 0), + reload: Number.isFinite(stats.reload) ? Math.max(stats.reload, 0) : stats.reload + }; + + const magazineSize = Number.isFinite(magazine) && magazine > 0 ? magazine : null; + const shotDamage = safeStats.damage * projectiles; + const shotsPerSecond = safeStats.rpm / 60; const burstDps = shotDamage * shotsPerSecond; - const magDamage = magazine ? shotDamage * magazine : null; - const timeToEmpty = magazine ? magazine / shotsPerSecond : null; + const magDamage = magazineSize ? shotDamage * magazineSize : null; + const timeToEmpty = magazineSize ? magazineSize / shotsPerSecond : null; let sustainedDps = null; - if (magDamage !== null && stats.reload !== null && timeToEmpty !== null && (timeToEmpty + stats.reload) > 0) { - sustainedDps = magDamage / (timeToEmpty + stats.reload); + if ( + magDamage !== null + && Number.isFinite(safeStats.reload) + && timeToEmpty !== null + && (timeToEmpty + safeStats.reload) > 0 + ) { + sustainedDps = magDamage / (timeToEmpty + safeStats.reload); } let ttk = null; - if (targetHealth && burstDps > 0) { + if (targetHealth !== null && targetHealth >= 0 && burstDps > 0) { ttk = targetHealth / burstDps; } @@ -220,7 +263,8 @@ const normalizeModifier = (modifier, fallbackLabel) => { id: modifier.id, label: modifier.label ?? fallbackLabel ?? 'Unnamed modifier', operatives: modifier.operatives, - appliesTo: modifier.appliesTo + appliesTo: modifier.appliesTo, + appliesNotTo: modifier.appliesNotTo }; if (Number.isFinite(modifier.value) && modifier.type) { @@ -241,7 +285,7 @@ const inferSlot = (weapon) => { return directSlot; } - const types = weapon.types ?? weapon.guntype ?? []; + const types = normalizeToArray(weapon.types ?? weapon.guntype ?? []); if (types.includes('SECONDARY')) { return 'SECONDARY'; } @@ -251,18 +295,21 @@ const inferSlot = (weapon) => { return 'PRIMARY'; }; -const normalizeWeapon = (weapon) => ({ - id: weapon.id ?? weapon.name, - name: weapon.name, - damage: weapon.damage, - projectiles: weapon.projectiles ?? weapon.damage_multiplier ?? 1, - rpm: weapon.rpm, - magazine: weapon.magazine ?? weapon.mag_size, - reload: weapon.reload ?? weapon.reload_time, - types: weapon.types ?? weapon.guntype ?? [], - slot: inferSlot(weapon), - operatives: Array.isArray(weapon.operatives) ? weapon.operatives : [] -}); +const normalizeWeapon = (weapon) => { + const types = normalizeToArray(weapon.types ?? weapon.guntype ?? []); + return { + id: weapon.id ?? weapon.name, + name: weapon.name, + damage: weapon.damage, + projectiles: weapon.projectiles ?? weapon.damage_multiplier ?? 1, + rpm: weapon.rpm, + magazine: weapon.magazine ?? weapon.mag_size, + reload: weapon.reload ?? weapon.reload_time, + types, + slot: inferSlot(weapon), + operatives: Array.isArray(weapon.operatives) ? weapon.operatives : [] + }; +}; const selectedOperativeId = () => operativeSelect?.value ?? ''; const selectedSlot = () => (loadoutSlotSelect?.value ?? 'primary').toUpperCase(); @@ -355,7 +402,10 @@ const createModifierOption = (modifier, container) => { if (!modifier || !container) { return; } - const id = modifier.id ?? `${modifier.label}-${Math.random().toString(36).slice(2)}`; + let id = modifier.id ?? `${modifier.label}-${Math.random().toString(36).slice(2)}`; + while (modifiersById.has(id)) { + id = `${id}-${Math.random().toString(36).slice(2)}`; + } modifiersById.set(id, modifier); const wrapper = document.createElement('label'); @@ -370,6 +420,9 @@ const createModifierOption = (modifier, container) => { if (modifier.appliesTo) { checkbox.dataset.appliesTo = modifier.appliesTo; } + if (modifier.appliesNotTo) { + checkbox.dataset.appliesNotTo = modifier.appliesNotTo; + } const label = document.createElement('span'); label.textContent = modifier.label ?? 'Unnamed modifier'; @@ -479,10 +532,12 @@ const updateModifierAvailability = () => { checkboxes.forEach((checkbox) => { const requiredOperative = checkbox.dataset.operatives; const requiredType = checkbox.dataset.appliesTo; + const excludedType = checkbox.dataset.appliesNotTo; const wrapper = checkbox.closest('.af-checkbox'); const operativeMatch = requiredOperative ? matchesOperative(requiredOperative) : true; const typeMatch = requiredType ? matchesWeaponType(requiredType) : true; - const isEnabled = operativeMatch && typeMatch; + const exclusionMatch = excludedType ? matchesWeaponExclusion(excludedType) : true; + const isEnabled = operativeMatch && typeMatch && exclusionMatch; checkbox.disabled = !isEnabled; if (wrapper) { wrapper.classList.toggle('is-disabled', !isEnabled); diff --git a/static/style.css b/static/style.css index 2c0d48a..4d352be 100644 --- a/static/style.css +++ b/static/style.css @@ -862,7 +862,7 @@ body.menu-open { } .af-details summary::before { - content: "▸"; + content: ">"; transition: transform 0.2s ease; } diff --git a/templates/calculator.html b/templates/calculator.html index c86cffe..543b065 100644 --- a/templates/calculator.html +++ b/templates/calculator.html @@ -58,12 +58,12 @@
Follow this flow: operative → slot → weapon → buffs. Results update right below.
+Follow this flow: operative -> slot -> weapon -> buffs. Results update right below.