Improve clarity in text content across templates, enhance weapon matching logic in filterWeapons.js, and update designs for better consistency.

This commit is contained in:
2026-02-18 01:12:12 +01:00
parent 71e3a888e9
commit b804379bbb
3 changed files with 92 additions and 36 deletions
+77 -22
View File
@@ -29,6 +29,16 @@ let allWeapons = [];
let currentWeaponTypes = []; let currentWeaponTypes = [];
let currentOperativeCategory = null; 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 numberOrNull = (value) => {
const parsed = Number.parseFloat(value); const parsed = Number.parseFloat(value);
return Number.isFinite(parsed) ? parsed : null; return Number.isFinite(parsed) ? parsed : null;
@@ -43,23 +53,39 @@ const formatValue = (value, suffix = '') => {
}; };
const matchesOperative = (modifierOperative) => { const matchesOperative = (modifierOperative) => {
if (!modifierOperative) { const required = normalizeToArray(modifierOperative);
if (!required.length) {
return true; return true;
} }
if (!currentOperativeCategory) { if (!currentOperativeCategory) {
return false; return false;
} }
return modifierOperative.toLowerCase() === currentOperativeCategory.toLowerCase(); const operative = currentOperativeCategory.toLowerCase();
return required.some((entry) => entry.toLowerCase() === operative);
}; };
const matchesWeaponType = (appliesTo) => { const matchesWeaponType = (appliesTo) => {
if (!appliesTo) { const required = normalizeToArray(appliesTo);
if (!required.length) {
return true; return true;
} }
if (!currentWeaponTypes.length) { if (!currentWeaponTypes.length) {
return false; 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) => { const applyBuffValue = (current, value, buffType) => {
@@ -79,7 +105,11 @@ const applyModifierToStats = (modifier, stats) => {
if (!modifier) { if (!modifier) {
return stats; return stats;
} }
if (!matchesOperative(modifier.operatives) || !matchesWeaponType(modifier.appliesTo)) { if (
!matchesOperative(modifier.operatives)
|| !matchesWeaponType(modifier.appliesTo)
|| !matchesWeaponExclusion(modifier.appliesNotTo)
) {
return stats; return stats;
} }
@@ -141,17 +171,18 @@ const updateActiveModifierList = (modifiers) => {
const calculate = () => { const calculate = () => {
const data = new FormData(form); const data = new FormData(form);
const baseDamage = numberOrNull(data.get('baseDamage')); 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 rpm = numberOrNull(data.get('rpm'));
const magazine = numberOrNull(data.get('magazine')); const magazine = numberOrNull(data.get('magazine'));
const reload = numberOrNull(data.get('reload')); const reload = numberOrNull(data.get('reload'));
const targetHealth = numberOrNull(data.get('targetHealth')); const targetHealth = numberOrNull(data.get('targetHealth'));
if (!baseDamage || !rpm) { if (baseDamage === null || rpm === null || baseDamage < 0 || rpm <= 0) {
Object.values(outputs).forEach((node) => { Object.values(outputs).forEach((node) => {
node.textContent = '--'; node.textContent = '--';
}); });
updateActiveModifierList([]); updateActiveModifierList(getSelectedModifiers());
return; return;
} }
@@ -165,19 +196,31 @@ const calculate = () => {
} }
); );
const shotDamage = stats.damage * projectiles; const safeStats = {
const shotsPerSecond = stats.rpm / 60; 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 burstDps = shotDamage * shotsPerSecond;
const magDamage = magazine ? shotDamage * magazine : null; const magDamage = magazineSize ? shotDamage * magazineSize : null;
const timeToEmpty = magazine ? magazine / shotsPerSecond : null; const timeToEmpty = magazineSize ? magazineSize / shotsPerSecond : null;
let sustainedDps = null; let sustainedDps = null;
if (magDamage !== null && stats.reload !== null && timeToEmpty !== null && (timeToEmpty + stats.reload) > 0) { if (
sustainedDps = magDamage / (timeToEmpty + stats.reload); magDamage !== null
&& Number.isFinite(safeStats.reload)
&& timeToEmpty !== null
&& (timeToEmpty + safeStats.reload) > 0
) {
sustainedDps = magDamage / (timeToEmpty + safeStats.reload);
} }
let ttk = null; let ttk = null;
if (targetHealth && burstDps > 0) { if (targetHealth !== null && targetHealth >= 0 && burstDps > 0) {
ttk = targetHealth / burstDps; ttk = targetHealth / burstDps;
} }
@@ -220,7 +263,8 @@ const normalizeModifier = (modifier, fallbackLabel) => {
id: modifier.id, id: modifier.id,
label: modifier.label ?? fallbackLabel ?? 'Unnamed modifier', label: modifier.label ?? fallbackLabel ?? 'Unnamed modifier',
operatives: modifier.operatives, operatives: modifier.operatives,
appliesTo: modifier.appliesTo appliesTo: modifier.appliesTo,
appliesNotTo: modifier.appliesNotTo
}; };
if (Number.isFinite(modifier.value) && modifier.type) { if (Number.isFinite(modifier.value) && modifier.type) {
@@ -241,7 +285,7 @@ const inferSlot = (weapon) => {
return directSlot; return directSlot;
} }
const types = weapon.types ?? weapon.guntype ?? []; const types = normalizeToArray(weapon.types ?? weapon.guntype ?? []);
if (types.includes('SECONDARY')) { if (types.includes('SECONDARY')) {
return 'SECONDARY'; return 'SECONDARY';
} }
@@ -251,7 +295,9 @@ const inferSlot = (weapon) => {
return 'PRIMARY'; return 'PRIMARY';
}; };
const normalizeWeapon = (weapon) => ({ const normalizeWeapon = (weapon) => {
const types = normalizeToArray(weapon.types ?? weapon.guntype ?? []);
return {
id: weapon.id ?? weapon.name, id: weapon.id ?? weapon.name,
name: weapon.name, name: weapon.name,
damage: weapon.damage, damage: weapon.damage,
@@ -259,10 +305,11 @@ const normalizeWeapon = (weapon) => ({
rpm: weapon.rpm, rpm: weapon.rpm,
magazine: weapon.magazine ?? weapon.mag_size, magazine: weapon.magazine ?? weapon.mag_size,
reload: weapon.reload ?? weapon.reload_time, reload: weapon.reload ?? weapon.reload_time,
types: weapon.types ?? weapon.guntype ?? [], types,
slot: inferSlot(weapon), slot: inferSlot(weapon),
operatives: Array.isArray(weapon.operatives) ? weapon.operatives : [] operatives: Array.isArray(weapon.operatives) ? weapon.operatives : []
}); };
};
const selectedOperativeId = () => operativeSelect?.value ?? ''; const selectedOperativeId = () => operativeSelect?.value ?? '';
const selectedSlot = () => (loadoutSlotSelect?.value ?? 'primary').toUpperCase(); const selectedSlot = () => (loadoutSlotSelect?.value ?? 'primary').toUpperCase();
@@ -355,7 +402,10 @@ const createModifierOption = (modifier, container) => {
if (!modifier || !container) { if (!modifier || !container) {
return; 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); modifiersById.set(id, modifier);
const wrapper = document.createElement('label'); const wrapper = document.createElement('label');
@@ -370,6 +420,9 @@ const createModifierOption = (modifier, container) => {
if (modifier.appliesTo) { if (modifier.appliesTo) {
checkbox.dataset.appliesTo = modifier.appliesTo; checkbox.dataset.appliesTo = modifier.appliesTo;
} }
if (modifier.appliesNotTo) {
checkbox.dataset.appliesNotTo = modifier.appliesNotTo;
}
const label = document.createElement('span'); const label = document.createElement('span');
label.textContent = modifier.label ?? 'Unnamed modifier'; label.textContent = modifier.label ?? 'Unnamed modifier';
@@ -479,10 +532,12 @@ const updateModifierAvailability = () => {
checkboxes.forEach((checkbox) => { checkboxes.forEach((checkbox) => {
const requiredOperative = checkbox.dataset.operatives; const requiredOperative = checkbox.dataset.operatives;
const requiredType = checkbox.dataset.appliesTo; const requiredType = checkbox.dataset.appliesTo;
const excludedType = checkbox.dataset.appliesNotTo;
const wrapper = checkbox.closest('.af-checkbox'); const wrapper = checkbox.closest('.af-checkbox');
const operativeMatch = requiredOperative ? matchesOperative(requiredOperative) : true; const operativeMatch = requiredOperative ? matchesOperative(requiredOperative) : true;
const typeMatch = requiredType ? matchesWeaponType(requiredType) : 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; checkbox.disabled = !isEnabled;
if (wrapper) { if (wrapper) {
wrapper.classList.toggle('is-disabled', !isEnabled); wrapper.classList.toggle('is-disabled', !isEnabled);
+1 -1
View File
@@ -862,7 +862,7 @@ body.menu-open {
} }
.af-details summary::before { .af-details summary::before {
content: "▸"; content: ">";
transition: transform 0.2s ease; transition: transform 0.2s ease;
} }
+5 -4
View File
@@ -58,12 +58,12 @@
<div class="af-card af-card-builder"> <div class="af-card af-card-builder">
<div class="af-card-header"> <div class="af-card-header">
<h2>Loadout builder</h2> <h2>Loadout builder</h2>
<p>Follow this flow: operative → slot → weapon → buffs. Results update right below.</p> <p>Follow this flow: operative -> slot -> weapon -> buffs. Results update right below.</p>
</div> </div>
<form class="af-form" id="af-calculator"> <form class="af-form" id="af-calculator">
<div class="af-flow-grid"> <div class="af-flow-grid">
<label> <label>
Step 1 · Operative Step 1 - Operative
<select id="af-operative-select" name="operativeSelect" <select id="af-operative-select" name="operativeSelect"
data-source="{{ url_for('static', filename='json/operative.json') }}"> data-source="{{ url_for('static', filename='json/operative.json') }}">
<option value="">No operative selected</option> <option value="">No operative selected</option>
@@ -72,7 +72,7 @@
</label> </label>
<label> <label>
Step 2 · Loadout slot Step 2 - Loadout slot
<select id="af-loadout-slot" name="loadoutSlot" disabled> <select id="af-loadout-slot" name="loadoutSlot" disabled>
<option value="primary">Primary</option> <option value="primary">Primary</option>
<option value="secondary">Secondary</option> <option value="secondary">Secondary</option>
@@ -82,7 +82,7 @@
</label> </label>
<label> <label>
Step 3 · Weapon Step 3 - Weapon
<select name="weaponSelect" id="af-weapon-select" <select name="weaponSelect" id="af-weapon-select"
data-source="{{ url_for('static', filename='json/data.json') }}"> data-source="{{ url_for('static', filename='json/data.json') }}">
<option value="">Custom entry</option> <option value="">Custom entry</option>
@@ -253,3 +253,4 @@
</script> </script>
</body> </body>
</html> </html>