From d6ad232e29c8ed1d12dee00aab9635eaf96482b9 Mon Sep 17 00:00:00 2001 From: Fretux Date: Tue, 17 Feb 2026 22:07:56 +0100 Subject: [PATCH] Redesign calculator flow and split operative armor/ability buffs --- static/filterWeapons.js | 238 ++++++++-- static/json/data.json | 877 ++++++++++++++++++++++++++++++++----- static/json/operative.json | 72 ++- static/style.css | 27 ++ templates/calculator.html | 90 ++-- 5 files changed, 1124 insertions(+), 180 deletions(-) diff --git a/static/filterWeapons.js b/static/filterWeapons.js index 733882a..8fe50bc 100644 --- a/static/filterWeapons.js +++ b/static/filterWeapons.js @@ -15,12 +15,17 @@ const weaponsById = new Map(); const modifiersById = new Map(); const operativesById = new Map(); const operativeSelect = document.getElementById('af-operative-select'); +const loadoutSlotSelect = document.getElementById('af-loadout-slot'); 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 operativeAbilityModifierList = document.getElementById('af-operative-ability-modifiers'); +const operativeArmorModifierList = document.getElementById('af-operative-armor-modifiers'); const activeModifierList = document.getElementById('af-active-modifiers'); + +let allWeapons = []; let currentWeaponTypes = []; let currentOperativeCategory = null; @@ -78,7 +83,7 @@ const applyModifierToStats = (modifier, stats) => { return stats; } - if (modifier.value && modifier.type) { + if (Number.isFinite(modifier.value) && modifier.type) { if (modifier.type === 'additive') { stats.damage += modifier.value; } @@ -155,8 +160,8 @@ const calculate = () => { (acc, modifier) => applyModifierToStats(modifier, acc), { damage: baseDamage, - rpm: rpm, - reload: reload + rpm, + reload } ); @@ -199,14 +204,124 @@ const applyWeapon = (weapon) => { } setFormValue('weaponName', weapon.name ?? ''); setFormValue('baseDamage', weapon.damage ?? ''); - setFormValue('projectiles', weapon.damage_multiplier ?? 1); + setFormValue('projectiles', weapon.projectiles ?? weapon.damage_multiplier ?? 1); setFormValue('rpm', weapon.rpm ?? ''); - setFormValue('magazine', weapon.mag_size ?? ''); - setFormValue('reload', weapon.reload_time ?? ''); - currentWeaponTypes = weapon.guntype ?? []; + setFormValue('magazine', weapon.magazine ?? weapon.mag_size ?? ''); + setFormValue('reload', weapon.reload ?? weapon.reload_time ?? ''); + currentWeaponTypes = weapon.types ?? weapon.guntype ?? []; calculate(); }; +const normalizeModifier = (modifier, fallbackLabel) => { + if (!modifier) { + return null; + } + const normalized = { + id: modifier.id, + label: modifier.label ?? fallbackLabel ?? 'Unnamed modifier', + operatives: modifier.operatives, + appliesTo: modifier.appliesTo + }; + + if (Number.isFinite(modifier.value) && modifier.type) { + normalized.value = modifier.value; + normalized.type = modifier.type; + } + + if (Array.isArray(modifier.buffs)) { + normalized.buffs = modifier.buffs; + } + + return normalized; +}; + +const inferSlot = (weapon) => { + const directSlot = weapon.slot?.toUpperCase(); + if (directSlot === 'PRIMARY' || directSlot === 'SECONDARY' || directSlot === 'SPECIAL') { + return directSlot; + } + + const types = weapon.types ?? weapon.guntype ?? []; + if (types.includes('SECONDARY')) { + return 'SECONDARY'; + } + if (types.includes('SPECIAL')) { + return 'SPECIAL'; + } + 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 selectedOperativeId = () => operativeSelect?.value ?? ''; +const selectedSlot = () => (loadoutSlotSelect?.value ?? 'primary').toUpperCase(); + +const weaponAllowedForOperative = (weapon, operativeId) => { + if (!operativeId) { + return false; + } + if (!weapon.operatives.length) { + return false; + } + return weapon.operatives.includes('*') || weapon.operatives.includes(operativeId); +}; + +const refreshWeaponSelect = () => { + if (!weaponSelect) { + return; + } + + const operativeId = selectedOperativeId(); + const slot = selectedSlot(); + const previousValue = weaponSelect.value; + + weaponSelect.innerHTML = ''; + const defaultOption = document.createElement('option'); + defaultOption.value = ''; + defaultOption.textContent = operativeId ? 'Custom entry' : 'Select operative first'; + weaponSelect.appendChild(defaultOption); + + weaponsById.clear(); + + if (!operativeId) { + weaponSelect.disabled = true; + return; + } + + weaponSelect.disabled = false; + + const filtered = allWeapons + .filter((weapon) => weapon.slot === slot) + .filter((weapon) => weaponAllowedForOperative(weapon, operativeId)) + .sort((a, b) => a.name.localeCompare(b.name)); + + filtered.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); + }); + + if (previousValue && weaponsById.has(previousValue)) { + weaponSelect.value = previousValue; + } else { + weaponSelect.value = ''; + } +}; + const loadWeaponLibrary = async () => { if (!weaponSelect) { return; @@ -222,34 +337,17 @@ const loadWeaponLibrary = async () => { throw new Error('Failed to load weapons'); } const weapons = await response.json(); - const normalized = weapons + allWeapons = 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, - guntype: weapon.guntype ?? [] - })) - .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); - }); + .map((weapon) => normalizeWeapon(weapon)); + refreshWeaponSelect(); } 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); + weaponSelect.disabled = true; } }; @@ -310,19 +408,39 @@ const loadModifierLibrary = async () => { } }; -const renderOperativeModifiers = (operative) => { - if (!operativeModifierList) { +const renderModifierBucket = (container, modifiers, emptyText) => { + if (!container) { return; } - operativeModifierList.innerHTML = ''; - if (!operative || !Array.isArray(operative.modifiers) || !operative.modifiers.length) { + container.innerHTML = ''; + if (!modifiers.length) { const placeholder = document.createElement('p'); placeholder.className = 'af-empty-hint'; - placeholder.textContent = 'No operative-specific modifiers available.'; - operativeModifierList.appendChild(placeholder); + placeholder.textContent = emptyText; + container.appendChild(placeholder); return; } - operative.modifiers.forEach((modifier) => createModifierOption(modifier, operativeModifierList)); + modifiers.forEach((modifier) => createModifierOption(modifier, container)); +}; + +const renderOperativeModifiers = (operative) => { + const operativePerks = Array.isArray(operative?.modifiers) + ? operative.modifiers.map((mod) => normalizeModifier(mod)).filter(Boolean) + : []; + + const operativeAbilities = Array.isArray(operative?.abilities) + ? operative.abilities + .map((ability, index) => normalizeModifier(ability?.modifier, ability?.label ?? `Ability ${index + 1}`)) + .filter(Boolean) + : []; + + const armorBuffs = operative?.armor?.modifier + ? [normalizeModifier(operative.armor.modifier, operative.armor.label)].filter(Boolean) + : []; + + renderModifierBucket(operativeModifierList, operativePerks, 'No operative perk modifiers available.'); + renderModifierBucket(operativeAbilityModifierList, operativeAbilities, 'No operative ability buffs available.'); + renderModifierBucket(operativeArmorModifierList, armorBuffs, 'No operative armor buffs available.'); }; const loadOperatives = async () => { @@ -342,8 +460,8 @@ const loadOperatives = async () => { operativesById.set(operative.id, operative); const option = document.createElement('option'); option.value = operative.id; - option.textContent = operative.name; - option.dataset.category = operative.category ?? ''; + option.textContent = operative.class ? `${operative.name} (${operative.class})` : operative.name; + option.dataset.category = operative.category ?? operative.class ?? ''; operativeSelect.appendChild(option); }); updateModifierAvailability(); @@ -379,12 +497,24 @@ form.addEventListener('input', calculate); resetButton.addEventListener('click', () => { form.reset(); + document.querySelectorAll('[data-modifier-id]:checked').forEach((checkbox) => { checkbox.checked = false; }); + renderOperativeModifiers(null); currentWeaponTypes = []; + currentOperativeCategory = null; + if (loadoutSlotSelect) { + loadoutSlotSelect.value = 'primary'; + loadoutSlotSelect.disabled = true; + } + if (weaponSelect) { + weaponSelect.disabled = true; + } + refreshWeaponSelect(); updateModifierAvailability(); calculate(); }); if (weaponSelect) { + weaponSelect.disabled = true; weaponSelect.addEventListener('change', (event) => { const selected = weaponsById.get(event.target.value); if (!selected) { @@ -400,8 +530,39 @@ if (weaponSelect) { if (operativeSelect) { operativeSelect.addEventListener('change', (event) => { const selected = operativesById.get(event.target.value); - currentOperativeCategory = selected?.category ?? null; + currentOperativeCategory = selected?.category ?? selected?.class ?? null; renderOperativeModifiers(selected); + + if (loadoutSlotSelect) { + loadoutSlotSelect.disabled = !selected; + } + + refreshWeaponSelect(); + setFormValue('weaponName', ''); + setFormValue('baseDamage', ''); + setFormValue('projectiles', 1); + setFormValue('rpm', ''); + setFormValue('magazine', ''); + setFormValue('reload', ''); + currentWeaponTypes = []; + + updateModifierAvailability(); + calculate(); + }); +} + +if (loadoutSlotSelect) { + loadoutSlotSelect.disabled = true; + loadoutSlotSelect.addEventListener('change', () => { + refreshWeaponSelect(); + setFormValue('weaponSelect', ''); + setFormValue('weaponName', ''); + setFormValue('baseDamage', ''); + setFormValue('projectiles', 1); + setFormValue('rpm', ''); + setFormValue('magazine', ''); + setFormValue('reload', ''); + currentWeaponTypes = []; updateModifierAvailability(); calculate(); }); @@ -442,6 +603,7 @@ copyButton.addEventListener('click', async () => { }); calculate(); +renderOperativeModifiers(null); loadWeaponLibrary(); loadModifierLibrary(); loadOperatives(); diff --git a/static/json/data.json b/static/json/data.json index 23f644d..b733835 100644 --- a/static/json/data.json +++ b/static/json/data.json @@ -7,7 +7,14 @@ "rpm": 800, "mag_size": 17, "reload_time": 2, - "guntype": ["PISTOL", "SECONDARY"] + "guntype": [ + "PISTOL", + "SECONDARY" + ], + "slot": "SECONDARY", + "operatives": [ + "*" + ] }, { "id": 2, @@ -17,7 +24,15 @@ "rpm": 580, "mag_size": 8, "reload_time": 1.15, - "guntype": ["PISTOL", "SECONDARY", "WW2"] + "guntype": [ + "PISTOL", + "SECONDARY", + "WW2" + ], + "slot": "SECONDARY", + "operatives": [ + "*" + ] }, { "id": 3, @@ -27,7 +42,14 @@ "rpm": 300, "mag_size": 15, "reload_time": 2, - "guntype": ["PISTOL", "SECONDARY"] + "guntype": [ + "PISTOL", + "SECONDARY" + ], + "slot": "SECONDARY", + "operatives": [ + "*" + ] }, { "id": 4, @@ -37,7 +59,14 @@ "rpm": 720, "mag_size": 16, "reload_time": 3, - "guntype": ["PISTOL", "SECONDARY"] + "guntype": [ + "PISTOL", + "SECONDARY" + ], + "slot": "SECONDARY", + "operatives": [ + "*" + ] }, { "id": 5, @@ -47,7 +76,15 @@ "rpm": 116, "mag_size": 8, "reload_time": 1.95, - "guntype": ["PISTOL", "WW2", "SECONDARY"] + "guntype": [ + "PISTOL", + "WW2", + "SECONDARY" + ], + "slot": "SECONDARY", + "operatives": [ + "*" + ] }, { "id": 6, @@ -57,7 +94,15 @@ "rpm": 600, "mag_size": 30, "reload_time": 2.1, - "guntype": ["RIFLE", "PRIMARY","SOVIET"] + "guntype": [ + "RIFLE", + "PRIMARY", + "SOVIET" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 7, @@ -67,7 +112,14 @@ "rpm": 700, "mag_size": 30, "reload_time": 2.4, - "guntype": ["RIFLE", "PRIMARY"] + "guntype": [ + "RIFLE", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 8, @@ -77,7 +129,15 @@ "rpm": 400, "mag_size": 24, "reload_time": 2.6, - "guntype": ["RIFLE", "SEMI-AUTO", "PRIMARY"] + "guntype": [ + "RIFLE", + "SEMI-AUTO", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 9, @@ -87,7 +147,14 @@ "rpm": 670, "mag_size": 30, "reload_time": 3.25, - "guntype": ["RIFLE", "PRIMARY"] + "guntype": [ + "RIFLE", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 10, @@ -97,7 +164,14 @@ "rpm": 700, "mag_size": 30, "reload_time": 2.35, - "guntype": ["RIFLE", "PRIMARY"] + "guntype": [ + "RIFLE", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 11, @@ -107,7 +181,16 @@ "rpm": 60, "mag_size": 8, "reload_time": 2.8, - "guntype": ["RIFLE", "SEMI-AUTO", "PRIMARY", "WW2"] + "guntype": [ + "RIFLE", + "SEMI-AUTO", + "PRIMARY", + "WW2" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 12, @@ -117,7 +200,14 @@ "rpm": 70, "mag_size": 12, "reload_time": 6, - "guntype": ["SHOTGUN", "PRIMARY"] + "guntype": [ + "SHOTGUN", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 87, @@ -127,7 +217,14 @@ "rpm": 70, "mag_size": 12, "reload_time": 6, - "guntype": ["SHOTGUN", "PRIMARY"] + "guntype": [ + "SHOTGUN", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 14, @@ -137,7 +234,14 @@ "rpm": 300, "mag_size": 7, "reload_time": 4.5, - "guntype": ["SHOTGUN", "PRIMARY"] + "guntype": [ + "SHOTGUN", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 16, @@ -147,7 +251,14 @@ "rpm": 150, "mag_size": 8, "reload_time": 3.5, - "guntype": ["SHOTGUN", "SECONDARY"] + "guntype": [ + "SHOTGUN", + "SECONDARY" + ], + "slot": "SECONDARY", + "operatives": [ + "*" + ] }, { "id": 18, @@ -157,7 +268,14 @@ "rpm": 300, "mag_size": 20, "reload_time": 2.2, - "guntype": ["SHOTGUN", "PRIMARY"] + "guntype": [ + "SHOTGUN", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 20, @@ -167,7 +285,14 @@ "rpm": 900, "mag_size": 50, "reload_time": 3, - "guntype": ["SMG", "PRIMARY"] + "guntype": [ + "SMG", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 21, @@ -177,7 +302,15 @@ "rpm": 750, "mag_size": 50, "reload_time": 2.5, - "guntype": ["SMG", "PRIMARY", "WW2"] + "guntype": [ + "SMG", + "PRIMARY", + "WW2" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 22, @@ -187,7 +320,14 @@ "rpm": 950, "mag_size": 20, "reload_time": 2, - "guntype": ["SMG", "SECONDARY"] + "guntype": [ + "SMG", + "SECONDARY" + ], + "slot": "SECONDARY", + "operatives": [ + "*" + ] }, { "id": 23, @@ -197,7 +337,14 @@ "rpm": 1050, "mag_size": 33, "reload_time": 2, - "guntype": ["SMG", "PRIMARY"] + "guntype": [ + "SMG", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 24, @@ -207,7 +354,14 @@ "rpm": 800, "mag_size": 30, "reload_time": 2.1, - "guntype": ["SMG", "SECONDARY"] + "guntype": [ + "SMG", + "SECONDARY" + ], + "slot": "SECONDARY", + "operatives": [ + "*" + ] }, { "id": 25, @@ -217,7 +371,14 @@ "rpm": 750, "mag_size": 65, "reload_time": 2, - "guntype": ["SMG", "SECONDARY"] + "guntype": [ + "SMG", + "SECONDARY" + ], + "slot": "SECONDARY", + "operatives": [ + "*" + ] }, { "id": 26, @@ -227,7 +388,14 @@ "rpm": 780, "mag_size": 30, "reload_time": 2.65, - "guntype": ["SMG", "SECONDARY"] + "guntype": [ + "SMG", + "SECONDARY" + ], + "slot": "SECONDARY", + "operatives": [ + "*" + ] }, { "id": 27, @@ -237,7 +405,14 @@ "rpm": 600, "mag_size": 25, "reload_time": 2, - "guntype": ["SMG", "SECONDARY"] + "guntype": [ + "SMG", + "SECONDARY" + ], + "slot": "SECONDARY", + "operatives": [ + "*" + ] }, { "id": 28, @@ -247,7 +422,14 @@ "rpm": 800, "mag_size": 25, "reload_time": 2.5, - "guntype": ["SMG", "SECONDARY"] + "guntype": [ + "SMG", + "SECONDARY" + ], + "slot": "SECONDARY", + "operatives": [ + "*" + ] }, { "id": 29, @@ -257,7 +439,14 @@ "rpm": 800, "mag_size": 25, "reload_time": 2.5, - "guntype": ["SMG", "SECONDARY"] + "guntype": [ + "SMG", + "SECONDARY" + ], + "slot": "SECONDARY", + "operatives": [ + "*" + ] }, { "id": 30, @@ -267,7 +456,15 @@ "rpm": 550, "mag_size": 32, "reload_time": 3, - "guntype": ["SMG", "PRIMARY", "WW2"] + "guntype": [ + "SMG", + "PRIMARY", + "WW2" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 31, @@ -277,7 +474,15 @@ "rpm": 750, "mag_size": 100, "reload_time": 5.5, - "guntype": ["LMG", "PRIMARY","SOVIET"] + "guntype": [ + "LMG", + "PRIMARY", + "SOVIET" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 32, @@ -287,7 +492,14 @@ "rpm": 850, "mag_size": 100, "reload_time": 6, - "guntype": ["LMG", "PRIMARY"] + "guntype": [ + "LMG", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 33, @@ -297,7 +509,14 @@ "rpm": 780, "mag_size": 125, "reload_time": 5.5, - "guntype": ["LMG", "PRIMARY"] + "guntype": [ + "LMG", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 34, @@ -307,7 +526,14 @@ "rpm": 700, "mag_size": 50, "reload_time": 3.25, - "guntype": ["RIFLE", "PRIMARY"] + "guntype": [ + "RIFLE", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 35, @@ -317,7 +543,14 @@ "rpm": 850, "mag_size": 125, "reload_time": 5.5, - "guntype": ["LMG", "PRIMARY"] + "guntype": [ + "LMG", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 36, @@ -327,7 +560,14 @@ "rpm": 750, "mag_size": 50, "reload_time": 3.25, - "guntype": ["RIFLE", "PRIMARY"] + "guntype": [ + "RIFLE", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 37, @@ -337,7 +577,15 @@ "rpm": 30, "mag_size": 5, "reload_time": 3.5, - "guntype": ["RIFLE", "SEMI-AUTO", "PRIMARY"] + "guntype": [ + "RIFLE", + "SEMI-AUTO", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 38, @@ -347,7 +595,14 @@ "rpm": 800, "mag_size": 50, "reload_time": 2.5, - "guntype": ["SMG", "PRIMARY"] + "guntype": [ + "SMG", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 39, @@ -357,7 +612,14 @@ "rpm": 900, "mag_size": 64, "reload_time": 4, - "guntype": ["SMG", "PRIMARY"] + "guntype": [ + "SMG", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 40, @@ -367,7 +629,14 @@ "rpm": 1000, "mag_size": 50, "reload_time": 2.45, - "guntype": ["RIFLE", "PRIMARY"] + "guntype": [ + "RIFLE", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 41, @@ -377,7 +646,13 @@ "rpm": 3000, "mag_size": 10000000000000000000, "reload_time": 0, - "guntype": ["SPECIAL"] + "guntype": [ + "SPECIAL" + ], + "slot": "SPECIAL", + "operatives": [ + "*" + ] }, { "id": 42, @@ -387,7 +662,13 @@ "rpm": 500, "mag_size": 650, "reload_time": 0, - "guntype": ["SPECIAL"] + "guntype": [ + "SPECIAL" + ], + "slot": "SPECIAL", + "operatives": [ + "*" + ] }, { "id": 43, @@ -397,7 +678,13 @@ "rpm": 300, "mag_size": 650, "reload_time": 0, - "guntype": ["SPECIAL"] + "guntype": [ + "SPECIAL" + ], + "slot": "SPECIAL", + "operatives": [ + "*" + ] }, { "id": 44, @@ -407,7 +694,13 @@ "rpm": 650, "mag_size": 18, "reload_time": 2, - "guntype": ["PISTOL"] + "guntype": [ + "PISTOL" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 89, @@ -417,7 +710,13 @@ "rpm": 650, "mag_size": 23, "reload_time": 2, - "guntype": ["PISTOL"] + "guntype": [ + "PISTOL" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 45, @@ -427,7 +726,13 @@ "rpm": 550, "mag_size": 18, "reload_time": 2, - "guntype": ["PISTOL"] + "guntype": [ + "PISTOL" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 46, @@ -437,7 +742,14 @@ "rpm": 225, "mag_size": 10, "reload_time": 5, - "guntype": ["SHOTGUN", "PRIMARY"] + "guntype": [ + "SHOTGUN", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 48, @@ -447,7 +759,14 @@ "rpm": 550, "mag_size": 30, "reload_time": 2.35, - "guntype": ["RIFLE", "PRIMARY"] + "guntype": [ + "RIFLE", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 49, @@ -457,7 +776,14 @@ "rpm": 600, "mag_size": 30, "reload_time": 2.35, - "guntype": ["RIFLE", "PRIMARY"] + "guntype": [ + "RIFLE", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 50, @@ -467,7 +793,14 @@ "rpm": 600, "mag_size": 32, "reload_time": 2.45, - "guntype": ["RIFLE", "PRIMARY"] + "guntype": [ + "RIFLE", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 51, @@ -477,7 +810,14 @@ "rpm": 650, "mag_size": 32, "reload_time": 2.45, - "guntype": ["RIFLE", "PRIMARY"] + "guntype": [ + "RIFLE", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 52, @@ -487,7 +827,14 @@ "rpm": 625, "mag_size": 100, "reload_time": 4.5, - "guntype": ["LMG", "PRIMARY"] + "guntype": [ + "LMG", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 53, @@ -497,7 +844,14 @@ "rpm": 700, "mag_size": 100, "reload_time": 4.5, - "guntype": ["LMG", "PRIMARY"] + "guntype": [ + "LMG", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 54, @@ -507,7 +861,14 @@ "rpm": 800, "mag_size": 50, "reload_time": 2.6, - "guntype": ["SMG", "PRIMARY"] + "guntype": [ + "SMG", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 55, @@ -517,7 +878,15 @@ "rpm": 550, "mag_size": 32, "reload_time": 2.5, - "guntype": ["SMG", "PRIMARY", "WW2"] + "guntype": [ + "SMG", + "PRIMARY", + "WW2" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 56, @@ -527,7 +896,13 @@ "rpm": 700, "mag_size": 34, "reload_time": 3.5, - "guntype": ["PISTOL"] + "guntype": [ + "PISTOL" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 57, @@ -537,7 +912,15 @@ "rpm": 440, "mag_size": 18, "reload_time": 3.5, - "guntype": ["RIFLE", "SEMI-AUTO", "PRIMARY"] + "guntype": [ + "RIFLE", + "SEMI-AUTO", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 58, @@ -547,7 +930,15 @@ "rpm": 540, "mag_size": 18, "reload_time": 3.5, - "guntype": ["RIFLE", "SEMI-AUTO", "PRIMARY"] + "guntype": [ + "RIFLE", + "SEMI-AUTO", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 59, @@ -557,7 +948,14 @@ "rpm": 780, "mag_size": 30, "reload_time": 2.25, - "guntype": ["RIFLE", "PRIMARY"] + "guntype": [ + "RIFLE", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 60, @@ -567,7 +965,13 @@ "rpm": 900, "mag_size": 26, "reload_time": 2.5, - "guntype": ["PISTOL"] + "guntype": [ + "PISTOL" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 61, @@ -577,7 +981,13 @@ "rpm": 1200, "mag_size": 26, "reload_time": 2, - "guntype": ["PISTOL"] + "guntype": [ + "PISTOL" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 62, @@ -587,7 +997,14 @@ "rpm": 800, "mag_size": 30, "reload_time": 1.5, - "guntype": ["SMG", "PRIMARY"] + "guntype": [ + "SMG", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 63, @@ -597,7 +1014,11 @@ "rpm": 200, "mag_size": 2, "reload_time": 3, - "guntype": "SPECIAL" + "guntype": "SPECIAL", + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 64, @@ -607,7 +1028,14 @@ "rpm": 1050, "mag_size": 30, "reload_time": 2, - "guntype": ["SMG", "PRIMARY"] + "guntype": [ + "SMG", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 88, @@ -617,7 +1045,14 @@ "rpm": 1050, "mag_size": 33, "reload_time": 2, - "guntype": ["SMG", "PRIMARY"] + "guntype": [ + "SMG", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 65, @@ -627,7 +1062,14 @@ "rpm": 850, "mag_size": 30, "reload_time": 2, - "guntype": ["SMG", "PRIMARY"] + "guntype": [ + "SMG", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 66, @@ -637,7 +1079,14 @@ "rpm": 850, "mag_size": 30, "reload_time": 2, - "guntype": ["SMG", "PRIMARY"] + "guntype": [ + "SMG", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 67, @@ -647,7 +1096,11 @@ "rpm": 600, "mag_size": 20, "reload_time": 2, - "guntype": "SPECIAL" + "guntype": "SPECIAL", + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 68, @@ -657,7 +1110,11 @@ "rpm": 650, "mag_size": 30, "reload_time": 2.35, - "guntype": "SPECIAL" + "guntype": "SPECIAL", + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 69, @@ -667,7 +1124,14 @@ "rpm": 830, "mag_size": 30, "reload_time": 2.5, - "guntype": ["RIFLE", "PRIMARY"] + "guntype": [ + "RIFLE", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 70, @@ -677,7 +1141,14 @@ "rpm": 900, "mag_size": 30, "reload_time": 2.5, - "guntype": ["RIFLE", "PRIMARY"] + "guntype": [ + "RIFLE", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 71, @@ -687,7 +1158,11 @@ "rpm": 480, "mag_size": 1, "reload_time": 1, - "guntype": "SPECIAL" + "guntype": "SPECIAL", + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 72, @@ -697,7 +1172,14 @@ "rpm": 700, "mag_size": 30, "reload_time": 3.25, - "guntype": ["RIFLE", "PRIMARY"] + "guntype": [ + "RIFLE", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 73, @@ -707,7 +1189,14 @@ "rpm": 400, "mag_size": 18, "reload_time": 2.1, - "guntype": ["PISTOL", "DEAGLE"] + "guntype": [ + "PISTOL", + "DEAGLE" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 74, @@ -717,7 +1206,14 @@ "rpm": 400, "mag_size": 36, "reload_time": 3.5, - "guntype": ["PISTOL", "DEAGLE"] + "guntype": [ + "PISTOL", + "DEAGLE" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 75, @@ -727,7 +1223,15 @@ "rpm": 300, "mag_size": 10, "reload_time": 3, - "guntype": ["RIFLE", "PRIMARY", "SEMI-AUTO"] + "guntype": [ + "RIFLE", + "PRIMARY", + "SEMI-AUTO" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 76, @@ -737,7 +1241,11 @@ "rpm": 700, "mag_size": 10, "reload_time": 2, - "guntype": "RIFLE" + "guntype": "RIFLE", + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 77, @@ -747,7 +1255,15 @@ "rpm": 1000, "mag_size": 71, "reload_time": 2.5, - "guntype": ["SMG", "PRIMARY", "SOVIET"] + "guntype": [ + "SMG", + "PRIMARY", + "SOVIET" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 78, @@ -757,7 +1273,15 @@ "rpm": 690, "mag_size": 30, "reload_time": 2.5, - "guntype": ["RIFLE", "PRIMARY", "SOVIET"] + "guntype": [ + "RIFLE", + "PRIMARY", + "SOVIET" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 79, @@ -767,7 +1291,13 @@ "rpm": 800, "mag_size": 64, "reload_time": 4, - "guntype": ["SMG"] + "guntype": [ + "SMG" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 80, @@ -777,7 +1307,13 @@ "rpm": 800, "mag_size": 60, "reload_time": 4, - "guntype": ["SMG"] + "guntype": [ + "SMG" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 81, @@ -787,7 +1323,14 @@ "rpm": 800, "mag_size": 50, "reload_time": 2.5, - "guntype": ["SMG", "PRIMARY"] + "guntype": [ + "SMG", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 82, @@ -797,7 +1340,13 @@ "rpm": 400, "mag_size": 12, "reload_time": 2.6, - "guntype": ["RIFLE"] + "guntype": [ + "RIFLE" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 83, @@ -807,7 +1356,14 @@ "rpm": 150, "mag_size": 8, "reload_time": 2.2, - "guntype": ["SHOTGUN", "PRIMARY"] + "guntype": [ + "SHOTGUN", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 84, @@ -817,7 +1373,14 @@ "rpm": 200, "mag_size": 1000000000000000000000000000000000000000000000000000000000000000, "reload_time": 0, - "guntype": ["SHOTGUN", "PRIMARY"] + "guntype": [ + "SHOTGUN", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 85, @@ -827,7 +1390,14 @@ "rpm": 900, "mag_size": 30, "reload_time": 4, - "guntype": ["RIFLE", "PRIMARY"] + "guntype": [ + "RIFLE", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 86, @@ -837,27 +1407,50 @@ "rpm": 9999, "mag_size": 999, "reload_time": 1, - "guntype": ["RIFLE", "PRIMARY"] + "guntype": [ + "RIFLE", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { - "id": 90, - "name": "M1915", - "damage": 65, - "damage_multiplier": 1, - "rpm": 400, - "mag_size": 30, - "reload_time": 3, - "guntype": ["RIFLE", "PRIMARY", "SOVIET"] + "id": 90, + "name": "M1915", + "damage": 65, + "damage_multiplier": 1, + "rpm": 400, + "mag_size": 30, + "reload_time": 3, + "guntype": [ + "RIFLE", + "PRIMARY", + "SOVIET" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { - "id": 91, - "name": "M1985", - "damage": 250, - "damage_multiplier": 1, - "rpm": 40, - "mag_size": 6, - "reload_time": 2, - "guntype": ["PISTOL", "SECONDARY", "SOVIET"] + "id": 91, + "name": "M1985", + "damage": 250, + "damage_multiplier": 1, + "rpm": 40, + "mag_size": 6, + "reload_time": 2, + "guntype": [ + "PISTOL", + "SECONDARY", + "SOVIET" + ], + "slot": "SECONDARY", + "operatives": [ + "*" + ] }, { "id": 92, @@ -867,7 +1460,14 @@ "rpm": 850, "mag_size": 150, "reload_time": 6, - "guntype": ["LMG", "PRIMARY"] + "guntype": [ + "LMG", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 93, @@ -877,7 +1477,14 @@ "rpm": 120, "mag_size": 12, "reload_time": 12, - "guntype": ["SHOTGUN", "PRIMARY"] + "guntype": [ + "SHOTGUN", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] }, { "id": 94, @@ -887,6 +1494,68 @@ "rpm": 770, "mag_size": 50, "reload_time": 2.4, - "guntype": ["RIFLE", "PRIMARY"] + "guntype": [ + "RIFLE", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "*" + ] + }, + { + "id": "heavy-primary-bulldog-shotgun", + "name": "Bulldog Shotgun", + "damage": 22, + "projectiles": 8, + "rpm": 96, + "magazine": 8, + "reload": 3.1, + "types": [ + "SHOTGUN", + "PRIMARY" + ], + "slot": "PRIMARY", + "operatives": [ + "heavy-breacher", + "Classic-Demolitionist", + "Classic-Juggernaut" + ] + }, + { + "id": "heavy-secondary-deagle", + "name": "Deagle (Heavy Loadout)", + "damage": 145, + "projectiles": 1, + "rpm": 170, + "magazine": 7, + "reload": 2.0, + "types": [ + "PISTOL", + "SECONDARY", + "DEAGLE" + ], + "slot": "SECONDARY", + "operatives": [ + "heavy-breacher", + "Classic-Scout" + ] + }, + { + "id": "heavy-special-sledgehammer", + "name": "Sledgehammer", + "damage": 260, + "projectiles": 1, + "rpm": 52, + "magazine": 1, + "reload": 1.2, + "types": [ + "MELEE", + "SPECIAL" + ], + "slot": "SPECIAL", + "operatives": [ + "heavy-breacher" + ] } -] \ No newline at end of file +] diff --git a/static/json/operative.json b/static/json/operative.json index 94fb6f1..56abf28 100644 --- a/static/json/operative.json +++ b/static/json/operative.json @@ -64,7 +64,7 @@ { "id": "max-corruption-damage-perk", "label": "Max Corruption (+30% Damage)", - "value": 1.30, + "value": 1.3, "type": "multiplicative", "appliesTo": "" }, @@ -102,7 +102,7 @@ { "id": "chaos-insurgency-damage-perk", "label": "Chaos Insurgency (+50% Damage)", - "value": 1.50, + "value": 1.5, "type": "multiplicative", "appliesTo": "" } @@ -162,7 +162,7 @@ { "id": "hold-the-line-damage-perk", "label": "Hold The Line (+90% Damage)", - "value": 1.90, + "value": 1.9, "type": "multiplicative", "appliesTo": "" } @@ -515,7 +515,7 @@ { "id": "coinflip-damage-perk", "label": "Coinflip (+20% Damage)", - "value": 1.20, + "value": 1.2, "type": "multiplicative", "appliesTo": "" }, @@ -561,7 +561,7 @@ "buffs": [ { "type": "damage", - "value": 1.20, + "value": 1.2, "buffType": "multiplicative" } ], @@ -1472,7 +1472,7 @@ "buffs": [ { "type": "damage", - "value": 1.20, + "value": 1.2, "buffType": "multiplicative" } ], @@ -1992,6 +1992,64 @@ "appliesTo": "SOVIET" } ] + }, + { + "id": "heavy-breacher", + "name": "Atlas", + "class": "Heavy", + "category": "Heavy", + "abilities": [ + { + "id": "heavy-ability-braced-stance", + "label": "Braced Stance (+12% damage, -15% reload time)", + "modifier": { + "id": "heavy-ability-braced-stance-mod", + "label": "Braced Stance", + "buffs": [ + { + "type": "damage", + "value": 1.12, + "buffType": "multiplicative" + }, + { + "type": "reloadTime", + "value": 0.85, + "buffType": "multiplicative" + } + ], + "operatives": "Heavy" + } + }, + { + "id": "heavy-ability-overrun", + "label": "Overrun (+18% RPM for shotguns)", + "modifier": { + "id": "heavy-ability-overrun-mod", + "label": "Overrun", + "buffs": [ + { + "type": "rpm", + "value": 1.18, + "buffType": "multiplicative" + } + ], + "operatives": "Heavy", + "appliesTo": "SHOTGUN" + } + } + ], + "armor": { + "id": "heavy-armor-aegis", + "label": "Aegis Heavy Armor (+20% damage resistance protocol)", + "modifier": { + "id": "heavy-armor-aegis-mod", + "label": "Aegis Protocol (+8% damage)", + "value": 1.08, + "type": "multiplicative", + "operatives": "Heavy" + } + }, + "modifiers": [] } ] -} \ No newline at end of file +} diff --git a/static/style.css b/static/style.css index 986a9e5..2c0d48a 100644 --- a/static/style.css +++ b/static/style.css @@ -740,6 +740,29 @@ body.menu-open { margin-bottom: 32px; } + +.af-grid-calculator { + grid-template-columns: minmax(0, 1.45fr) minmax(300px, 1fr); + align-items: start; +} + +.af-card-builder .af-form { + margin-top: 18px; +} + +.af-flow-grid { + display: grid; + gap: 14px; + grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); + padding: 14px; + border-radius: 14px; + background: var(--input-bg); + border: 1px solid var(--border); +} + +.af-results-header { + margin-top: 24px; +} .af-card { background: var(--surface); border-radius: 20px; @@ -1088,6 +1111,10 @@ body.menu-open { } @media (max-width: 820px) { + + .af-grid-calculator { + grid-template-columns: 1fr; + } .af-header { flex-wrap: wrap; } diff --git a/templates/calculator.html b/templates/calculator.html index 2156554..c86cffe 100644 --- a/templates/calculator.html +++ b/templates/calculator.html @@ -54,21 +54,43 @@ -
-
+
+
-

Base inputs

-

Enter a prototype loadout and tweak values to see combat pacing.

+

Loadout builder

+

Follow this flow: operative → slot → weapon → buffs. Results update right below.

- +
+ + + + + +
+
-
-
-
+

Calculated outputs

-

Instantly generated from the values you enter.

+

Instantly generated from your current loadout and active buffs.

@@ -136,25 +156,33 @@
+
-

Modifiers & operatives

-

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

+

Buff stack

+

Class buffs are separated so armor and operative abilities are easy to toggle.

- +
+ Operative armor buffs +
+
+

Select an operative to see armor buffs.

+
+
+
-

Expand each section to keep the page compact while you tweak modifiers.

+
+ Operative abilities +
+
+

Select an operative to see ability buffs.

+
+
+
- Operative modifiers + Operative perks

Select an operative to see perk modifiers.

@@ -163,21 +191,21 @@
- Item buffs + Global item buffs
- Operative perks + Global operative perks
- Ability perks + Global ability perks