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 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();
|
||||
|
||||
Reference in New Issue
Block a user