From 5da0e5485a188a68b69913c323640a1b27df8ce1 Mon Sep 17 00:00:00 2001 From: Fretux Date: Wed, 14 Jan 2026 20:44:19 +0100 Subject: [PATCH] Enhance calculator modifiers and UI styling --- static/filterWeapons.js | 276 +++++++++++++++++++++++++++++++++++++- static/style.css | 163 +++++++++++++++++++++- templates/about_us.html | 57 +++++++- templates/calculator.html | 45 ++++++- 4 files changed, 521 insertions(+), 20 deletions(-) diff --git a/static/filterWeapons.js b/static/filterWeapons.js index db191fb..733882a 100644 --- a/static/filterWeapons.js +++ b/static/filterWeapons.js @@ -12,6 +12,17 @@ const outputs = { const resetButton = document.getElementById('af-reset'); const copyButton = document.getElementById('af-copy'); const weaponsById = new Map(); +const modifiersById = new Map(); +const operativesById = new Map(); +const operativeSelect = document.getElementById('af-operative-select'); +const modifierStack = document.querySelector('.af-modifier-stack'); +const itemModifierList = document.getElementById('af-item-modifiers'); +const perkModifierList = document.getElementById('af-perk-modifiers'); +const abilityModifierList = document.getElementById('af-ability-modifiers'); +const operativeModifierList = document.getElementById('af-operative-modifiers'); +const activeModifierList = document.getElementById('af-active-modifiers'); +let currentWeaponTypes = []; +let currentOperativeCategory = null; const numberOrNull = (value) => { const parsed = Number.parseFloat(value); @@ -26,6 +37,102 @@ const formatValue = (value, suffix = '') => { return `${formatted}${suffix}`; }; +const matchesOperative = (modifierOperative) => { + if (!modifierOperative) { + return true; + } + if (!currentOperativeCategory) { + return false; + } + return modifierOperative.toLowerCase() === currentOperativeCategory.toLowerCase(); +}; + +const matchesWeaponType = (appliesTo) => { + if (!appliesTo) { + return true; + } + if (!currentWeaponTypes.length) { + return false; + } + return currentWeaponTypes.map((type) => type.toUpperCase()).includes(appliesTo.toUpperCase()); +}; + +const applyBuffValue = (current, value, buffType) => { + if (!Number.isFinite(current)) { + return current; + } + if (!Number.isFinite(value)) { + return current; + } + if (buffType === 'additive') { + return current + value; + } + return current * value; +}; + +const applyModifierToStats = (modifier, stats) => { + if (!modifier) { + return stats; + } + if (!matchesOperative(modifier.operatives) || !matchesWeaponType(modifier.appliesTo)) { + return stats; + } + + if (modifier.value && modifier.type) { + if (modifier.type === 'additive') { + stats.damage += modifier.value; + } + if (modifier.type === 'multiplicative') { + stats.damage *= modifier.value; + } + } + + if (Array.isArray(modifier.buffs)) { + modifier.buffs.forEach((buff) => { + const buffType = buff.buffType?.startsWith('additive') ? 'additive' : 'multiplicative'; + if (buff.type === 'damage') { + stats.damage = applyBuffValue(stats.damage, buff.value, buffType); + } + if (buff.type === 'rpm') { + stats.rpm = applyBuffValue(stats.rpm, buff.value, buffType); + } + if (buff.type === 'reloadTime') { + stats.reload = applyBuffValue(stats.reload, buff.value, buffType); + } + }); + } + + return stats; +}; + +const getSelectedModifiers = () => { + const checkboxes = document.querySelectorAll('[data-modifier-id]:checked'); + return Array.from(checkboxes) + .map((checkbox) => modifiersById.get(checkbox.dataset.modifierId)) + .filter(Boolean); +}; + +const updateActiveModifierList = (modifiers) => { + if (!activeModifierList) { + return; + } + activeModifierList.innerHTML = ''; + if (!modifiers.length) { + const tag = document.createElement('span'); + tag.className = 'af-tag'; + tag.textContent = 'No modifiers selected'; + activeModifierList.appendChild(tag); + return; + } + + modifiers.forEach((modifier) => { + const tag = document.createElement('span'); + tag.className = 'af-tag'; + tag.textContent = modifier.label ?? 'Unnamed modifier'; + activeModifierList.appendChild(tag); + }); +}; + const calculate = () => { const data = new FormData(form); const baseDamage = numberOrNull(data.get('baseDamage')); @@ -39,18 +146,29 @@ const calculate = () => { Object.values(outputs).forEach((node) => { node.textContent = '--'; }); + updateActiveModifierList([]); return; } - const shotDamage = baseDamage * projectiles; - const shotsPerSecond = rpm / 60; + const modifiers = getSelectedModifiers(); + const stats = modifiers.reduce( + (acc, modifier) => applyModifierToStats(modifier, acc), + { + damage: baseDamage, + rpm: rpm, + reload: reload + } + ); + + const shotDamage = stats.damage * projectiles; + const shotsPerSecond = stats.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); + if (magDamage !== null && stats.reload !== null && timeToEmpty !== null && (timeToEmpty + stats.reload) > 0) { + sustainedDps = magDamage / (timeToEmpty + stats.reload); } let ttk = null; @@ -64,6 +182,7 @@ const calculate = () => { outputs.timeToEmpty.textContent = formatValue(timeToEmpty, 's'); outputs.magDamage.textContent = formatValue(magDamage); outputs.ttk.textContent = formatValue(ttk, 's'); + updateActiveModifierList(modifiers); }; const setFormValue = (name, value) => { @@ -84,6 +203,7 @@ const applyWeapon = (weapon) => { setFormValue('rpm', weapon.rpm ?? ''); setFormValue('magazine', weapon.mag_size ?? ''); setFormValue('reload', weapon.reload_time ?? ''); + currentWeaponTypes = weapon.guntype ?? []; calculate(); }; @@ -111,7 +231,8 @@ const loadWeaponLibrary = async () => { damage_multiplier: weapon.damage_multiplier ?? 1, rpm: weapon.rpm, mag_size: weapon.mag_size, - reload_time: weapon.reload_time + reload_time: weapon.reload_time, + guntype: weapon.guntype ?? [] })) .sort((a, b) => a.name.localeCompare(b.name)); @@ -132,10 +253,134 @@ const loadWeaponLibrary = async () => { } }; +const createModifierOption = (modifier, container) => { + if (!modifier || !container) { + return; + } + const id = modifier.id ?? `${modifier.label}-${Math.random().toString(36).slice(2)}`; + modifiersById.set(id, modifier); + + const wrapper = document.createElement('label'); + wrapper.className = 'af-checkbox'; + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.dataset.modifierId = id; + if (modifier.operatives) { + checkbox.dataset.operatives = modifier.operatives; + } + if (modifier.appliesTo) { + checkbox.dataset.appliesTo = modifier.appliesTo; + } + + const label = document.createElement('span'); + label.textContent = modifier.label ?? 'Unnamed modifier'; + + wrapper.appendChild(checkbox); + wrapper.appendChild(label); + container.appendChild(wrapper); +}; + +const loadModifierLibrary = async () => { + if (!modifierStack || !modifierStack.dataset.source) { + return; + } + try { + const response = await fetch(modifierStack.dataset.source); + if (!response.ok) { + throw new Error('Failed to load modifiers'); + } + const payload = await response.json(); + itemModifierList.innerHTML = ''; + perkModifierList.innerHTML = ''; + abilityModifierList.innerHTML = ''; + + payload.itemBuffs?.forEach((modifier) => createModifierOption(modifier, itemModifierList)); + payload.operativePerks?.forEach((modifier) => createModifierOption(modifier, perkModifierList)); + payload.abilityPerks?.forEach((modifier) => createModifierOption(modifier, abilityModifierList)); + updateModifierAvailability(); + } catch (error) { + console.error('Modifier library load failed', error); + const errorNote = document.createElement('p'); + errorNote.className = 'af-empty-hint'; + errorNote.textContent = 'Modifier library unavailable.'; + itemModifierList.appendChild(errorNote.cloneNode(true)); + perkModifierList.appendChild(errorNote.cloneNode(true)); + abilityModifierList.appendChild(errorNote); + } +}; + +const renderOperativeModifiers = (operative) => { + if (!operativeModifierList) { + return; + } + operativeModifierList.innerHTML = ''; + if (!operative || !Array.isArray(operative.modifiers) || !operative.modifiers.length) { + const placeholder = document.createElement('p'); + placeholder.className = 'af-empty-hint'; + placeholder.textContent = 'No operative-specific modifiers available.'; + operativeModifierList.appendChild(placeholder); + return; + } + operative.modifiers.forEach((modifier) => createModifierOption(modifier, operativeModifierList)); +}; + +const loadOperatives = async () => { + if (!operativeSelect || !operativeSelect.dataset.source) { + return; + } + try { + const response = await fetch(operativeSelect.dataset.source); + if (!response.ok) { + throw new Error('Failed to load operatives'); + } + const payload = await response.json(); + payload.operatives?.forEach((operative) => { + if (!operative || !operative.name) { + return; + } + operativesById.set(operative.id, operative); + const option = document.createElement('option'); + option.value = operative.id; + option.textContent = operative.name; + option.dataset.category = operative.category ?? ''; + operativeSelect.appendChild(option); + }); + updateModifierAvailability(); + } catch (error) { + console.error('Operative load failed', error); + const option = document.createElement('option'); + option.textContent = 'Operatives unavailable'; + option.disabled = true; + operativeSelect.appendChild(option); + } +}; + +const updateModifierAvailability = () => { + const checkboxes = document.querySelectorAll('[data-modifier-id]'); + checkboxes.forEach((checkbox) => { + const requiredOperative = checkbox.dataset.operatives; + const requiredType = checkbox.dataset.appliesTo; + const wrapper = checkbox.closest('.af-checkbox'); + const operativeMatch = requiredOperative ? matchesOperative(requiredOperative) : true; + const typeMatch = requiredType ? matchesWeaponType(requiredType) : true; + const isEnabled = operativeMatch && typeMatch; + checkbox.disabled = !isEnabled; + if (wrapper) { + wrapper.classList.toggle('is-disabled', !isEnabled); + } + if (!isEnabled) { + checkbox.checked = false; + } + }); +}; + form.addEventListener('input', calculate); resetButton.addEventListener('click', () => { form.reset(); + currentWeaponTypes = []; + updateModifierAvailability(); calculate(); }); @@ -143,12 +388,31 @@ if (weaponSelect) { weaponSelect.addEventListener('change', (event) => { const selected = weaponsById.get(event.target.value); if (!selected) { + currentWeaponTypes = []; + updateModifierAvailability(); return; } applyWeapon(selected); + updateModifierAvailability(); }); } +if (operativeSelect) { + operativeSelect.addEventListener('change', (event) => { + const selected = operativesById.get(event.target.value); + currentOperativeCategory = selected?.category ?? null; + renderOperativeModifiers(selected); + updateModifierAvailability(); + calculate(); + }); +} + +document.addEventListener('change', (event) => { + if (event.target && event.target.matches('[data-modifier-id]')) { + calculate(); + } +}); + copyButton.addEventListener('click', async () => { const data = new FormData(form); const name = data.get('weaponName') || 'Unnamed loadout'; @@ -179,3 +443,5 @@ copyButton.addEventListener('click', async () => { calculate(); loadWeaponLibrary(); +loadModifierLibrary(); +loadOperatives(); diff --git a/static/style.css b/static/style.css index 878d9d5..4599e63 100644 --- a/static/style.css +++ b/static/style.css @@ -56,6 +56,12 @@ h1 { margin-bottom: 20px; } +.af-body h2, +.af-body h3, +.af-body h4 { + color: #f8fafc; +} + .background-roster { background-color: rgba(255, 255, 255, 0.8); /* Semi-transparent background for better readability */ padding: 20px; @@ -579,26 +585,50 @@ body.menu-open { .af-nav { display: flex; align-items: center; - gap: 12px; + gap: 10px; + padding: 6px; + border-radius: 999px; + background: rgba(15, 23, 42, 0.65); + border: 1px solid rgba(148, 163, 184, 0.3); + box-shadow: inset 0 0 0 1px rgba(56, 189, 248, 0.1); } .af-nav-link { - padding: 8px 16px; + position: relative; + padding: 10px 18px; border-radius: 999px; text-decoration: none; font-weight: 600; color: #e2e8f0; - background: rgba(148, 163, 184, 0.12); + background: transparent; border: 1px solid transparent; transition: all 0.2s ease; } .af-nav-link:hover { border-color: rgba(56, 189, 248, 0.4); - background: rgba(56, 189, 248, 0.2); + background: rgba(56, 189, 248, 0.15); color: #f8fafc; } +.af-nav-link::after { + content: ""; + position: absolute; + left: 16px; + right: 16px; + bottom: 6px; + height: 2px; + border-radius: 999px; + background: linear-gradient(90deg, rgba(56, 189, 248, 0), rgba(56, 189, 248, 0.9), rgba(34, 197, 94, 0.9), rgba(34, 197, 94, 0)); + opacity: 0; + transition: opacity 0.2s ease; +} + +.af-nav-link:hover::after, +.af-nav-link:focus-visible::after { + opacity: 1; +} + .af-nav-toggle { display: none; padding: 8px 16px; @@ -842,6 +872,131 @@ body.menu-open { margin-top: 20px; } +.af-stat-grid { + display: grid; + gap: 16px; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + margin-top: 24px; +} + +.af-stat-card { + padding: 16px; + border-radius: 16px; + border: 1px solid rgba(148, 163, 184, 0.2); + background: rgba(2, 6, 23, 0.6); +} + +.af-stat-label { + margin: 0 0 6px; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #94a3b8; +} + +.af-stat-value { + margin: 0; + font-size: 1.4rem; + font-weight: 700; + color: #f8fafc; +} + +.af-list { + margin: 16px 0 0; + padding-left: 20px; + color: #cbd5f5; + line-height: 1.7; +} + +.af-pill-grid { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 16px; +} + +.af-pill { + padding: 8px 12px; + border-radius: 999px; + background: rgba(56, 189, 248, 0.15); + border: 1px solid rgba(56, 189, 248, 0.35); + color: #e2e8f0; + font-size: 0.85rem; + font-weight: 600; +} + +.af-modifier-stack { + display: grid; + gap: 18px; + margin-top: 16px; +} + +.af-modifier-subset { + display: grid; + gap: 12px; +} + +.af-modifier-title { + margin: 0; + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #94a3b8; +} + +.af-checkbox-grid { + display: grid; + gap: 10px; +} + +.af-checkbox { + display: flex; + gap: 10px; + align-items: flex-start; + padding: 12px; + border-radius: 12px; + border: 1px solid rgba(148, 163, 184, 0.2); + background: rgba(2, 6, 23, 0.55); + color: #e2e8f0; + font-size: 0.9rem; + line-height: 1.4; +} + +.af-checkbox input[type="checkbox"] { + margin-top: 2px; +} + +.af-checkbox.is-disabled { + opacity: 0.5; +} + +.af-active-modifiers { + margin-top: 20px; + padding-top: 16px; + border-top: 1px solid rgba(148, 163, 184, 0.2); +} + +.af-tag-list { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.af-tag { + padding: 6px 12px; + border-radius: 999px; + background: rgba(15, 23, 42, 0.8); + border: 1px solid rgba(148, 163, 184, 0.25); + color: #e2e8f0; + font-size: 0.8rem; +} + +.af-empty-hint { + margin: 0; + color: #94a3b8; + font-size: 0.9rem; +} + @media (max-width: 820px) { .af-header { flex-wrap: wrap; diff --git a/templates/about_us.html b/templates/about_us.html index 58884d9..9893b2b 100644 --- a/templates/about_us.html +++ b/templates/about_us.html @@ -34,8 +34,8 @@

About the Anomaly Front toolkit

- This site is the staging ground for the toolkit. We are rebuilding every tool from the ground up to - avoid legacy stats, old branding, or outdated data. + This is the war room for balancing the Anomaly Front sandbox. We are rebuilding every tool from the ground + up to avoid legacy stats, outdated data, and mismatched tuning guidelines.

@@ -50,7 +50,7 @@

What is live today

@@ -63,18 +63,54 @@ +
+
+

Weapon types tracked

+

20+

+
+
+

Community draft builds

+

120+

+
+
+

Balance focus

+

Time-to-eliminate

+
+
-

Team notes

+

How we work

- This is a community-driven project. Use the feedback channel to share balancing ideas, feature - requests, and UI improvements. + Every number starts as a hypothesis. We iterate fast, log the results, and tag each change with + context so the community can track the why behind each adjustment.

+
    +
  • Prototype numbers in the calculator.
  • +
  • Layer in modifiers and operative perks.
  • +
  • Review pacing against target roles.
  • +
+
+
+

Community signals

+

+ We highlight the most requested tweaks, theme them by role, and bring them into each balancing pass. +

+
+
+ Time-to-eliminate feels heavy + Operative synergy clarity + Reload pacing in late waves + Attachment power spikes +
+
+
+ +

Release posture

@@ -84,6 +120,15 @@

+
+
+

Team notes

+

+ This is a community-driven project. Use the feedback channel to share balancing ideas, feature + requests, and UI improvements. +

+
+
diff --git a/templates/calculator.html b/templates/calculator.html index 0e9b94c..c189aa4 100644 --- a/templates/calculator.html +++ b/templates/calculator.html @@ -148,12 +148,47 @@
-

Modifier library

-

No live modifiers yet. Use this space to jot down prototype buffs.

+

Modifiers & operatives

+

Stack items, perks, and abilities with operative-specific bonuses.

-
-

Modifier slots are empty.

- +
+ + +
+

Operative modifiers

+
+

Select an operative to see perk modifiers.

+
+
+ +
+

Item buffs

+
+
+ +
+

Operative perks

+
+
+ +
+

Ability perks

+
+
+
+ +
+

Active stack

+
+ No modifiers selected +