Redesign calculator flow and split operative armor/ability buffs
This commit is contained in:
+200
-38
@@ -15,12 +15,17 @@ const weaponsById = new Map();
|
|||||||
const modifiersById = new Map();
|
const modifiersById = new Map();
|
||||||
const operativesById = new Map();
|
const operativesById = new Map();
|
||||||
const operativeSelect = document.getElementById('af-operative-select');
|
const operativeSelect = document.getElementById('af-operative-select');
|
||||||
|
const loadoutSlotSelect = document.getElementById('af-loadout-slot');
|
||||||
const modifierStack = document.querySelector('.af-modifier-stack');
|
const modifierStack = document.querySelector('.af-modifier-stack');
|
||||||
const itemModifierList = document.getElementById('af-item-modifiers');
|
const itemModifierList = document.getElementById('af-item-modifiers');
|
||||||
const perkModifierList = document.getElementById('af-perk-modifiers');
|
const perkModifierList = document.getElementById('af-perk-modifiers');
|
||||||
const abilityModifierList = document.getElementById('af-ability-modifiers');
|
const abilityModifierList = document.getElementById('af-ability-modifiers');
|
||||||
const operativeModifierList = document.getElementById('af-operative-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');
|
const activeModifierList = document.getElementById('af-active-modifiers');
|
||||||
|
|
||||||
|
let allWeapons = [];
|
||||||
let currentWeaponTypes = [];
|
let currentWeaponTypes = [];
|
||||||
let currentOperativeCategory = null;
|
let currentOperativeCategory = null;
|
||||||
|
|
||||||
@@ -78,7 +83,7 @@ const applyModifierToStats = (modifier, stats) => {
|
|||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modifier.value && modifier.type) {
|
if (Number.isFinite(modifier.value) && modifier.type) {
|
||||||
if (modifier.type === 'additive') {
|
if (modifier.type === 'additive') {
|
||||||
stats.damage += modifier.value;
|
stats.damage += modifier.value;
|
||||||
}
|
}
|
||||||
@@ -155,8 +160,8 @@ const calculate = () => {
|
|||||||
(acc, modifier) => applyModifierToStats(modifier, acc),
|
(acc, modifier) => applyModifierToStats(modifier, acc),
|
||||||
{
|
{
|
||||||
damage: baseDamage,
|
damage: baseDamage,
|
||||||
rpm: rpm,
|
rpm,
|
||||||
reload: reload
|
reload
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -199,14 +204,124 @@ const applyWeapon = (weapon) => {
|
|||||||
}
|
}
|
||||||
setFormValue('weaponName', weapon.name ?? '');
|
setFormValue('weaponName', weapon.name ?? '');
|
||||||
setFormValue('baseDamage', weapon.damage ?? '');
|
setFormValue('baseDamage', weapon.damage ?? '');
|
||||||
setFormValue('projectiles', weapon.damage_multiplier ?? 1);
|
setFormValue('projectiles', weapon.projectiles ?? weapon.damage_multiplier ?? 1);
|
||||||
setFormValue('rpm', weapon.rpm ?? '');
|
setFormValue('rpm', weapon.rpm ?? '');
|
||||||
setFormValue('magazine', weapon.mag_size ?? '');
|
setFormValue('magazine', weapon.magazine ?? weapon.mag_size ?? '');
|
||||||
setFormValue('reload', weapon.reload_time ?? '');
|
setFormValue('reload', weapon.reload ?? weapon.reload_time ?? '');
|
||||||
currentWeaponTypes = weapon.guntype ?? [];
|
currentWeaponTypes = weapon.types ?? weapon.guntype ?? [];
|
||||||
calculate();
|
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 () => {
|
const loadWeaponLibrary = async () => {
|
||||||
if (!weaponSelect) {
|
if (!weaponSelect) {
|
||||||
return;
|
return;
|
||||||
@@ -222,34 +337,17 @@ const loadWeaponLibrary = async () => {
|
|||||||
throw new Error('Failed to load weapons');
|
throw new Error('Failed to load weapons');
|
||||||
}
|
}
|
||||||
const weapons = await response.json();
|
const weapons = await response.json();
|
||||||
const normalized = weapons
|
allWeapons = weapons
|
||||||
.filter((weapon) => weapon && weapon.name)
|
.filter((weapon) => weapon && weapon.name)
|
||||||
.map((weapon) => ({
|
.map((weapon) => normalizeWeapon(weapon));
|
||||||
id: weapon.id ?? weapon.name,
|
refreshWeaponSelect();
|
||||||
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);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Weapon library load failed', error);
|
console.error('Weapon library load failed', error);
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.textContent = 'Weapon library unavailable';
|
option.textContent = 'Weapon library unavailable';
|
||||||
option.disabled = true;
|
option.disabled = true;
|
||||||
weaponSelect.appendChild(option);
|
weaponSelect.appendChild(option);
|
||||||
|
weaponSelect.disabled = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -310,19 +408,39 @@ const loadModifierLibrary = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderOperativeModifiers = (operative) => {
|
const renderModifierBucket = (container, modifiers, emptyText) => {
|
||||||
if (!operativeModifierList) {
|
if (!container) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
operativeModifierList.innerHTML = '';
|
container.innerHTML = '';
|
||||||
if (!operative || !Array.isArray(operative.modifiers) || !operative.modifiers.length) {
|
if (!modifiers.length) {
|
||||||
const placeholder = document.createElement('p');
|
const placeholder = document.createElement('p');
|
||||||
placeholder.className = 'af-empty-hint';
|
placeholder.className = 'af-empty-hint';
|
||||||
placeholder.textContent = 'No operative-specific modifiers available.';
|
placeholder.textContent = emptyText;
|
||||||
operativeModifierList.appendChild(placeholder);
|
container.appendChild(placeholder);
|
||||||
return;
|
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 () => {
|
const loadOperatives = async () => {
|
||||||
@@ -342,8 +460,8 @@ const loadOperatives = async () => {
|
|||||||
operativesById.set(operative.id, operative);
|
operativesById.set(operative.id, operative);
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = operative.id;
|
option.value = operative.id;
|
||||||
option.textContent = operative.name;
|
option.textContent = operative.class ? `${operative.name} (${operative.class})` : operative.name;
|
||||||
option.dataset.category = operative.category ?? '';
|
option.dataset.category = operative.category ?? operative.class ?? '';
|
||||||
operativeSelect.appendChild(option);
|
operativeSelect.appendChild(option);
|
||||||
});
|
});
|
||||||
updateModifierAvailability();
|
updateModifierAvailability();
|
||||||
@@ -379,12 +497,24 @@ form.addEventListener('input', calculate);
|
|||||||
|
|
||||||
resetButton.addEventListener('click', () => {
|
resetButton.addEventListener('click', () => {
|
||||||
form.reset();
|
form.reset();
|
||||||
|
document.querySelectorAll('[data-modifier-id]:checked').forEach((checkbox) => { checkbox.checked = false; });
|
||||||
|
renderOperativeModifiers(null);
|
||||||
currentWeaponTypes = [];
|
currentWeaponTypes = [];
|
||||||
|
currentOperativeCategory = null;
|
||||||
|
if (loadoutSlotSelect) {
|
||||||
|
loadoutSlotSelect.value = 'primary';
|
||||||
|
loadoutSlotSelect.disabled = true;
|
||||||
|
}
|
||||||
|
if (weaponSelect) {
|
||||||
|
weaponSelect.disabled = true;
|
||||||
|
}
|
||||||
|
refreshWeaponSelect();
|
||||||
updateModifierAvailability();
|
updateModifierAvailability();
|
||||||
calculate();
|
calculate();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (weaponSelect) {
|
if (weaponSelect) {
|
||||||
|
weaponSelect.disabled = true;
|
||||||
weaponSelect.addEventListener('change', (event) => {
|
weaponSelect.addEventListener('change', (event) => {
|
||||||
const selected = weaponsById.get(event.target.value);
|
const selected = weaponsById.get(event.target.value);
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
@@ -400,8 +530,39 @@ if (weaponSelect) {
|
|||||||
if (operativeSelect) {
|
if (operativeSelect) {
|
||||||
operativeSelect.addEventListener('change', (event) => {
|
operativeSelect.addEventListener('change', (event) => {
|
||||||
const selected = operativesById.get(event.target.value);
|
const selected = operativesById.get(event.target.value);
|
||||||
currentOperativeCategory = selected?.category ?? null;
|
currentOperativeCategory = selected?.category ?? selected?.class ?? null;
|
||||||
renderOperativeModifiers(selected);
|
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();
|
updateModifierAvailability();
|
||||||
calculate();
|
calculate();
|
||||||
});
|
});
|
||||||
@@ -442,6 +603,7 @@ copyButton.addEventListener('click', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
calculate();
|
calculate();
|
||||||
|
renderOperativeModifiers(null);
|
||||||
loadWeaponLibrary();
|
loadWeaponLibrary();
|
||||||
loadModifierLibrary();
|
loadModifierLibrary();
|
||||||
loadOperatives();
|
loadOperatives();
|
||||||
|
|||||||
+772
-103
File diff suppressed because it is too large
Load Diff
@@ -64,7 +64,7 @@
|
|||||||
{
|
{
|
||||||
"id": "max-corruption-damage-perk",
|
"id": "max-corruption-damage-perk",
|
||||||
"label": "Max Corruption (+30% Damage)",
|
"label": "Max Corruption (+30% Damage)",
|
||||||
"value": 1.30,
|
"value": 1.3,
|
||||||
"type": "multiplicative",
|
"type": "multiplicative",
|
||||||
"appliesTo": ""
|
"appliesTo": ""
|
||||||
},
|
},
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
{
|
{
|
||||||
"id": "chaos-insurgency-damage-perk",
|
"id": "chaos-insurgency-damage-perk",
|
||||||
"label": "Chaos Insurgency (+50% Damage)",
|
"label": "Chaos Insurgency (+50% Damage)",
|
||||||
"value": 1.50,
|
"value": 1.5,
|
||||||
"type": "multiplicative",
|
"type": "multiplicative",
|
||||||
"appliesTo": ""
|
"appliesTo": ""
|
||||||
}
|
}
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
{
|
{
|
||||||
"id": "hold-the-line-damage-perk",
|
"id": "hold-the-line-damage-perk",
|
||||||
"label": "Hold The Line (+90% Damage)",
|
"label": "Hold The Line (+90% Damage)",
|
||||||
"value": 1.90,
|
"value": 1.9,
|
||||||
"type": "multiplicative",
|
"type": "multiplicative",
|
||||||
"appliesTo": ""
|
"appliesTo": ""
|
||||||
}
|
}
|
||||||
@@ -515,7 +515,7 @@
|
|||||||
{
|
{
|
||||||
"id": "coinflip-damage-perk",
|
"id": "coinflip-damage-perk",
|
||||||
"label": "Coinflip (+20% Damage)",
|
"label": "Coinflip (+20% Damage)",
|
||||||
"value": 1.20,
|
"value": 1.2,
|
||||||
"type": "multiplicative",
|
"type": "multiplicative",
|
||||||
"appliesTo": ""
|
"appliesTo": ""
|
||||||
},
|
},
|
||||||
@@ -561,7 +561,7 @@
|
|||||||
"buffs": [
|
"buffs": [
|
||||||
{
|
{
|
||||||
"type": "damage",
|
"type": "damage",
|
||||||
"value": 1.20,
|
"value": 1.2,
|
||||||
"buffType": "multiplicative"
|
"buffType": "multiplicative"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -1472,7 +1472,7 @@
|
|||||||
"buffs": [
|
"buffs": [
|
||||||
{
|
{
|
||||||
"type": "damage",
|
"type": "damage",
|
||||||
"value": 1.20,
|
"value": 1.2,
|
||||||
"buffType": "multiplicative"
|
"buffType": "multiplicative"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -1992,6 +1992,64 @@
|
|||||||
"appliesTo": "SOVIET"
|
"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": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -740,6 +740,29 @@ body.menu-open {
|
|||||||
margin-bottom: 32px;
|
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 {
|
.af-card {
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
@@ -1088,6 +1111,10 @@ body.menu-open {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 820px) {
|
@media (max-width: 820px) {
|
||||||
|
|
||||||
|
.af-grid-calculator {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
.af-header {
|
.af-header {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-31
@@ -54,21 +54,43 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="af-grid">
|
<section class="af-grid af-grid-calculator">
|
||||||
<div class="af-card">
|
<div class="af-card af-card-builder">
|
||||||
<div class="af-card-header">
|
<div class="af-card-header">
|
||||||
<h2>Base inputs</h2>
|
<h2>Loadout builder</h2>
|
||||||
<p>Enter a prototype loadout and tweak values to see combat pacing.</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">
|
||||||
<label>
|
<div class="af-flow-grid">
|
||||||
Weapon library
|
<label>
|
||||||
<select name="weaponSelect" id="af-weapon-select"
|
Step 1 · Operative
|
||||||
data-source="{{ url_for('static', filename='json/data.json') }}">
|
<select id="af-operative-select" name="operativeSelect"
|
||||||
<option value="">Custom entry</option>
|
data-source="{{ url_for('static', filename='json/operative.json') }}">
|
||||||
</select>
|
<option value="">No operative selected</option>
|
||||||
<span class="af-field-hint">Select a weapon to prefill values. You can still edit every field.</span>
|
</select>
|
||||||
</label>
|
<span class="af-field-hint">Pick an operative first. Weapon choices unlock after this step.</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Step 2 · Loadout slot
|
||||||
|
<select id="af-loadout-slot" name="loadoutSlot" disabled>
|
||||||
|
<option value="primary">Primary</option>
|
||||||
|
<option value="secondary">Secondary</option>
|
||||||
|
<option value="special">Special</option>
|
||||||
|
</select>
|
||||||
|
<span class="af-field-hint">Choose which slot to browse (primary / secondary / special).</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Step 3 · Weapon
|
||||||
|
<select name="weaponSelect" id="af-weapon-select"
|
||||||
|
data-source="{{ url_for('static', filename='json/data.json') }}">
|
||||||
|
<option value="">Custom entry</option>
|
||||||
|
</select>
|
||||||
|
<span class="af-field-hint">Shows only weapons available for the selected operative + slot.</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
Weapon name
|
Weapon name
|
||||||
<input type="text" name="weaponName" placeholder="Prototype AR" autocomplete="off">
|
<input type="text" name="weaponName" placeholder="Prototype AR" autocomplete="off">
|
||||||
@@ -102,12 +124,10 @@
|
|||||||
<button type="button" id="af-copy">Copy summary</button>
|
<button type="button" id="af-copy">Copy summary</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="af-card">
|
<div class="af-card-header af-results-header">
|
||||||
<div class="af-card-header">
|
|
||||||
<h2>Calculated outputs</h2>
|
<h2>Calculated outputs</h2>
|
||||||
<p>Instantly generated from the values you enter.</p>
|
<p>Instantly generated from your current loadout and active buffs.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="af-metrics" id="af-metrics">
|
<div class="af-metrics" id="af-metrics">
|
||||||
<div class="af-metric">
|
<div class="af-metric">
|
||||||
@@ -136,25 +156,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="af-card">
|
<div class="af-card">
|
||||||
<div class="af-card-header">
|
<div class="af-card-header">
|
||||||
<h2>Modifiers & operatives</h2>
|
<h2>Buff stack</h2>
|
||||||
<p>Stack items, perks, and abilities with operative-specific bonuses.</p>
|
<p>Class buffs are separated so armor and operative abilities are easy to toggle.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="af-modifier-stack" data-source="{{ url_for('static', filename='json/modifiers.json') }}">
|
<div class="af-modifier-stack" data-source="{{ url_for('static', filename='json/modifiers.json') }}">
|
||||||
<label>
|
<details class="af-details" open>
|
||||||
Operative
|
<summary>Operative armor buffs</summary>
|
||||||
<select id="af-operative-select" name="operativeSelect"
|
<div class="af-modifier-subset">
|
||||||
data-source="{{ url_for('static', filename='json/operative.json') }}">
|
<div class="af-checkbox-grid" id="af-operative-armor-modifiers">
|
||||||
<option value="">No operative selected</option>
|
<p class="af-empty-hint">Select an operative to see armor buffs.</p>
|
||||||
</select>
|
</div>
|
||||||
<span class="af-field-hint">Pick an operative to load their unique modifiers.</span>
|
</div>
|
||||||
</label>
|
</details>
|
||||||
|
|
||||||
<p class="af-field-hint">Expand each section to keep the page compact while you tweak modifiers.</p>
|
<details class="af-details" open>
|
||||||
|
<summary>Operative abilities</summary>
|
||||||
|
<div class="af-modifier-subset">
|
||||||
|
<div class="af-checkbox-grid" id="af-operative-ability-modifiers">
|
||||||
|
<p class="af-empty-hint">Select an operative to see ability buffs.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<details class="af-details">
|
<details class="af-details">
|
||||||
<summary>Operative modifiers</summary>
|
<summary>Operative perks</summary>
|
||||||
<div class="af-modifier-subset">
|
<div class="af-modifier-subset">
|
||||||
<div class="af-checkbox-grid" id="af-operative-modifiers">
|
<div class="af-checkbox-grid" id="af-operative-modifiers">
|
||||||
<p class="af-empty-hint">Select an operative to see perk modifiers.</p>
|
<p class="af-empty-hint">Select an operative to see perk modifiers.</p>
|
||||||
@@ -163,21 +191,21 @@
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="af-details">
|
<details class="af-details">
|
||||||
<summary>Item buffs</summary>
|
<summary>Global item buffs</summary>
|
||||||
<div class="af-modifier-subset">
|
<div class="af-modifier-subset">
|
||||||
<div class="af-checkbox-grid" id="af-item-modifiers"></div>
|
<div class="af-checkbox-grid" id="af-item-modifiers"></div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="af-details">
|
<details class="af-details">
|
||||||
<summary>Operative perks</summary>
|
<summary>Global operative perks</summary>
|
||||||
<div class="af-modifier-subset">
|
<div class="af-modifier-subset">
|
||||||
<div class="af-checkbox-grid" id="af-perk-modifiers"></div>
|
<div class="af-checkbox-grid" id="af-perk-modifiers"></div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="af-details">
|
<details class="af-details">
|
||||||
<summary>Ability perks</summary>
|
<summary>Global ability perks</summary>
|
||||||
<div class="af-modifier-subset">
|
<div class="af-modifier-subset">
|
||||||
<div class="af-checkbox-grid" id="af-ability-modifiers"></div>
|
<div class="af-checkbox-grid" id="af-ability-modifiers"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user