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
+86 -31
View File
@@ -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);
+1 -1
View File
@@ -862,7 +862,7 @@ body.menu-open {
}
.af-details summary::before {
content: "▸";
content: ">";
transition: transform 0.2s ease;
}
+5 -4
View File
@@ -58,12 +58,12 @@
<div class="af-card af-card-builder">
<div class="af-card-header">
<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>
<form class="af-form" id="af-calculator">
<div class="af-flow-grid">
<label>
Step 1 · Operative
Step 1 - Operative
<select id="af-operative-select" name="operativeSelect"
data-source="{{ url_for('static', filename='json/operative.json') }}">
<option value="">No operative selected</option>
@@ -72,7 +72,7 @@
</label>
<label>
Step 2 · Loadout slot
Step 2 - Loadout slot
<select id="af-loadout-slot" name="loadoutSlot" disabled>
<option value="primary">Primary</option>
<option value="secondary">Secondary</option>
@@ -82,7 +82,7 @@
</label>
<label>
Step 3 · Weapon
Step 3 - Weapon
<select name="weaponSelect" id="af-weapon-select"
data-source="{{ url_for('static', filename='json/data.json') }}">
<option value="">Custom entry</option>
@@ -253,3 +253,4 @@
</script>
</body>
</html>