Merge pull request #4 from Fretuks/improve-css-styles-for-visibility
Add modifier stacking UI + operative support and polish nav/about styles
This commit is contained in:
+271
-5
@@ -12,6 +12,17 @@ const outputs = {
|
|||||||
const resetButton = document.getElementById('af-reset');
|
const resetButton = document.getElementById('af-reset');
|
||||||
const copyButton = document.getElementById('af-copy');
|
const copyButton = document.getElementById('af-copy');
|
||||||
const weaponsById = new Map();
|
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 numberOrNull = (value) => {
|
||||||
const parsed = Number.parseFloat(value);
|
const parsed = Number.parseFloat(value);
|
||||||
@@ -26,6 +37,102 @@ const formatValue = (value, suffix = '') => {
|
|||||||
return `${formatted}${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 calculate = () => {
|
||||||
const data = new FormData(form);
|
const data = new FormData(form);
|
||||||
const baseDamage = numberOrNull(data.get('baseDamage'));
|
const baseDamage = numberOrNull(data.get('baseDamage'));
|
||||||
@@ -39,18 +146,29 @@ const calculate = () => {
|
|||||||
Object.values(outputs).forEach((node) => {
|
Object.values(outputs).forEach((node) => {
|
||||||
node.textContent = '--';
|
node.textContent = '--';
|
||||||
});
|
});
|
||||||
|
updateActiveModifierList([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shotDamage = baseDamage * projectiles;
|
const modifiers = getSelectedModifiers();
|
||||||
const shotsPerSecond = rpm / 60;
|
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 burstDps = shotDamage * shotsPerSecond;
|
||||||
const magDamage = magazine ? shotDamage * magazine : null;
|
const magDamage = magazine ? shotDamage * magazine : null;
|
||||||
const timeToEmpty = magazine ? magazine / shotsPerSecond : null;
|
const timeToEmpty = magazine ? magazine / shotsPerSecond : null;
|
||||||
|
|
||||||
let sustainedDps = null;
|
let sustainedDps = null;
|
||||||
if (magDamage !== null && reload !== null && timeToEmpty !== null && (timeToEmpty + reload) > 0) {
|
if (magDamage !== null && stats.reload !== null && timeToEmpty !== null && (timeToEmpty + stats.reload) > 0) {
|
||||||
sustainedDps = magDamage / (timeToEmpty + reload);
|
sustainedDps = magDamage / (timeToEmpty + stats.reload);
|
||||||
}
|
}
|
||||||
|
|
||||||
let ttk = null;
|
let ttk = null;
|
||||||
@@ -64,6 +182,7 @@ const calculate = () => {
|
|||||||
outputs.timeToEmpty.textContent = formatValue(timeToEmpty, 's');
|
outputs.timeToEmpty.textContent = formatValue(timeToEmpty, 's');
|
||||||
outputs.magDamage.textContent = formatValue(magDamage);
|
outputs.magDamage.textContent = formatValue(magDamage);
|
||||||
outputs.ttk.textContent = formatValue(ttk, 's');
|
outputs.ttk.textContent = formatValue(ttk, 's');
|
||||||
|
updateActiveModifierList(modifiers);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setFormValue = (name, value) => {
|
const setFormValue = (name, value) => {
|
||||||
@@ -84,6 +203,7 @@ const applyWeapon = (weapon) => {
|
|||||||
setFormValue('rpm', weapon.rpm ?? '');
|
setFormValue('rpm', weapon.rpm ?? '');
|
||||||
setFormValue('magazine', weapon.mag_size ?? '');
|
setFormValue('magazine', weapon.mag_size ?? '');
|
||||||
setFormValue('reload', weapon.reload_time ?? '');
|
setFormValue('reload', weapon.reload_time ?? '');
|
||||||
|
currentWeaponTypes = weapon.guntype ?? [];
|
||||||
calculate();
|
calculate();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,7 +231,8 @@ const loadWeaponLibrary = async () => {
|
|||||||
damage_multiplier: weapon.damage_multiplier ?? 1,
|
damage_multiplier: weapon.damage_multiplier ?? 1,
|
||||||
rpm: weapon.rpm,
|
rpm: weapon.rpm,
|
||||||
mag_size: weapon.mag_size,
|
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));
|
.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);
|
form.addEventListener('input', calculate);
|
||||||
|
|
||||||
resetButton.addEventListener('click', () => {
|
resetButton.addEventListener('click', () => {
|
||||||
form.reset();
|
form.reset();
|
||||||
|
currentWeaponTypes = [];
|
||||||
|
updateModifierAvailability();
|
||||||
calculate();
|
calculate();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -143,12 +388,31 @@ if (weaponSelect) {
|
|||||||
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) {
|
||||||
|
currentWeaponTypes = [];
|
||||||
|
updateModifierAvailability();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
applyWeapon(selected);
|
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 () => {
|
copyButton.addEventListener('click', async () => {
|
||||||
const data = new FormData(form);
|
const data = new FormData(form);
|
||||||
const name = data.get('weaponName') || 'Unnamed loadout';
|
const name = data.get('weaponName') || 'Unnamed loadout';
|
||||||
@@ -179,3 +443,5 @@ copyButton.addEventListener('click', async () => {
|
|||||||
|
|
||||||
calculate();
|
calculate();
|
||||||
loadWeaponLibrary();
|
loadWeaponLibrary();
|
||||||
|
loadModifierLibrary();
|
||||||
|
loadOperatives();
|
||||||
|
|||||||
+159
-4
@@ -56,6 +56,12 @@ h1 {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.af-body h2,
|
||||||
|
.af-body h3,
|
||||||
|
.af-body h4 {
|
||||||
|
color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
.background-roster {
|
.background-roster {
|
||||||
background-color: rgba(255, 255, 255, 0.8); /* Semi-transparent background for better readability */
|
background-color: rgba(255, 255, 255, 0.8); /* Semi-transparent background for better readability */
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
@@ -579,26 +585,50 @@ body.menu-open {
|
|||||||
.af-nav {
|
.af-nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
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 {
|
.af-nav-link {
|
||||||
padding: 8px 16px;
|
position: relative;
|
||||||
|
padding: 10px 18px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
background: rgba(148, 163, 184, 0.12);
|
background: transparent;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.af-nav-link:hover {
|
.af-nav-link:hover {
|
||||||
border-color: rgba(56, 189, 248, 0.4);
|
border-color: rgba(56, 189, 248, 0.4);
|
||||||
background: rgba(56, 189, 248, 0.2);
|
background: rgba(56, 189, 248, 0.15);
|
||||||
color: #f8fafc;
|
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 {
|
.af-nav-toggle {
|
||||||
display: none;
|
display: none;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
@@ -842,6 +872,131 @@ body.menu-open {
|
|||||||
margin-top: 20px;
|
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) {
|
@media (max-width: 820px) {
|
||||||
.af-header {
|
.af-header {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
+51
-6
@@ -34,8 +34,8 @@
|
|||||||
<div class="af-card-header">
|
<div class="af-card-header">
|
||||||
<h1>About the Anomaly Front toolkit</h1>
|
<h1>About the Anomaly Front toolkit</h1>
|
||||||
<p>
|
<p>
|
||||||
This site is the staging ground for the toolkit. We are rebuilding every tool from the ground up to
|
This is the war room for balancing the Anomaly Front sandbox. We are rebuilding every tool from the ground
|
||||||
avoid legacy stats, old branding, or outdated data.
|
up to avoid legacy stats, outdated data, and mismatched tuning guidelines.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="af-copy-grid">
|
<div class="af-copy-grid">
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
<h2>What is live today</h2>
|
<h2>What is live today</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Baseline combat calculator</li>
|
<li>Baseline combat calculator</li>
|
||||||
<li>Notes and modifier placeholders</li>
|
<li>Modifiers + operative stacks</li>
|
||||||
<li>Feedback capture workflow</li>
|
<li>Feedback capture workflow</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -63,18 +63,54 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="af-stat-grid">
|
||||||
|
<div class="af-stat-card">
|
||||||
|
<p class="af-stat-label">Weapon types tracked</p>
|
||||||
|
<p class="af-stat-value">20+</p>
|
||||||
|
</div>
|
||||||
|
<div class="af-stat-card">
|
||||||
|
<p class="af-stat-label">Community draft builds</p>
|
||||||
|
<p class="af-stat-value">120+</p>
|
||||||
|
</div>
|
||||||
|
<div class="af-stat-card">
|
||||||
|
<p class="af-stat-label">Balance focus</p>
|
||||||
|
<p class="af-stat-value">Time-to-eliminate</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="af-grid">
|
<section class="af-grid">
|
||||||
<div class="af-card">
|
<div class="af-card">
|
||||||
<div class="af-card-header">
|
<div class="af-card-header">
|
||||||
<h2>Team notes</h2>
|
<h2>How we work</h2>
|
||||||
<p>
|
<p>
|
||||||
This is a community-driven project. Use the feedback channel to share balancing ideas, feature
|
Every number starts as a hypothesis. We iterate fast, log the results, and tag each change with
|
||||||
requests, and UI improvements.
|
context so the community can track the why behind each adjustment.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<ul class="af-list">
|
||||||
|
<li>Prototype numbers in the calculator.</li>
|
||||||
|
<li>Layer in modifiers and operative perks.</li>
|
||||||
|
<li>Review pacing against target roles.</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="af-card">
|
||||||
|
<div class="af-card-header">
|
||||||
|
<h2>Community signals</h2>
|
||||||
|
<p>
|
||||||
|
We highlight the most requested tweaks, theme them by role, and bring them into each balancing pass.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="af-pill-grid">
|
||||||
|
<span class="af-pill">Time-to-eliminate feels heavy</span>
|
||||||
|
<span class="af-pill">Operative synergy clarity</span>
|
||||||
|
<span class="af-pill">Reload pacing in late waves</span>
|
||||||
|
<span class="af-pill">Attachment power spikes</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="af-grid">
|
||||||
<div class="af-card">
|
<div class="af-card">
|
||||||
<div class="af-card-header">
|
<div class="af-card-header">
|
||||||
<h2>Release posture</h2>
|
<h2>Release posture</h2>
|
||||||
@@ -84,6 +120,15 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="af-card">
|
||||||
|
<div class="af-card-header">
|
||||||
|
<h2>Team notes</h2>
|
||||||
|
<p>
|
||||||
|
This is a community-driven project. Use the feedback channel to share balancing ideas, feature
|
||||||
|
requests, and UI improvements.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -148,12 +148,47 @@
|
|||||||
<section class="af-grid">
|
<section class="af-grid">
|
||||||
<div class="af-card">
|
<div class="af-card">
|
||||||
<div class="af-card-header">
|
<div class="af-card-header">
|
||||||
<h2>Modifier library</h2>
|
<h2>Modifiers & operatives</h2>
|
||||||
<p>No live modifiers yet. Use this space to jot down prototype buffs.</p>
|
<p>Stack items, perks, and abilities with operative-specific bonuses.</p>
|
||||||
|
</div>
|
||||||
|
<div class="af-modifier-stack" data-source="{{ url_for('static', filename='json/modifiers.json') }}">
|
||||||
|
<label>
|
||||||
|
Operative
|
||||||
|
<select id="af-operative-select" name="operativeSelect"
|
||||||
|
data-source="{{ url_for('static', filename='json/operative.json') }}">
|
||||||
|
<option value="">No operative selected</option>
|
||||||
|
</select>
|
||||||
|
<span class="af-field-hint">Pick an operative to load their unique modifiers.</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="af-modifier-subset">
|
||||||
|
<p class="af-modifier-title">Operative modifiers</p>
|
||||||
|
<div class="af-checkbox-grid" id="af-operative-modifiers">
|
||||||
|
<p class="af-empty-hint">Select an operative to see perk modifiers.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="af-modifier-subset">
|
||||||
|
<p class="af-modifier-title">Item buffs</p>
|
||||||
|
<div class="af-checkbox-grid" id="af-item-modifiers"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="af-modifier-subset">
|
||||||
|
<p class="af-modifier-title">Operative perks</p>
|
||||||
|
<div class="af-checkbox-grid" id="af-perk-modifiers"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="af-modifier-subset">
|
||||||
|
<p class="af-modifier-title">Ability perks</p>
|
||||||
|
<div class="af-checkbox-grid" id="af-ability-modifiers"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="af-active-modifiers">
|
||||||
|
<p class="af-modifier-title">Active stack</p>
|
||||||
|
<div class="af-tag-list" id="af-active-modifiers">
|
||||||
|
<span class="af-tag">No modifiers selected</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="af-empty">
|
|
||||||
<p>Modifier slots are empty.</p>
|
|
||||||
<button type="button" class="af-muted-button">Add placeholder</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="af-card">
|
<div class="af-card">
|
||||||
|
|||||||
Reference in New Issue
Block a user