Refresh Anomaly Front pages and nav

This commit is contained in:
2026-01-14 20:12:40 +01:00
parent 328f872588
commit a7108bdc20
10 changed files with 875 additions and 1030 deletions
+80 -622
View File
@@ -1,644 +1,102 @@
let selectedWeapon = null; const form = document.getElementById('af-calculator');
let selectedOperative = null; const outputs = {
let weapons = []; shotDamage: document.querySelector('[data-output="shotDamage"]'),
let globalModifiers = []; burstDps: document.querySelector('[data-output="burstDps"]'),
let selectedGunTypes = []; sustainedDps: document.querySelector('[data-output="sustainedDps"]'),
let headshotMultiplier = 0; timeToEmpty: document.querySelector('[data-output="timeToEmpty"]'),
let headshotBaseApplied = false; magDamage: document.querySelector('[data-output="magDamage"]'),
let ignoreHeadshots = false; ttk: document.querySelector('[data-output="ttk"]')
let isEditing = false;
//fetch operatives data from JSON
function fetchOperativesData() {
fetch('static/json/operative.json')
.then(response => response.json())
.then(data => {
populateOperatives(data.operatives);
})
.catch(error => console.error('Error fetching operatives data:', error));
}
//populate the select element with operatives
function populateOperatives(operatives) {
const select = document.getElementById('operative-select');
select.innerHTML = "";
const placeholderOption = document.createElement('option');
placeholderOption.value = ""; // Empty value for the placeholder
placeholderOption.text = "Select an operative";
select.appendChild(placeholderOption);
operatives.forEach(operative => {
const option = document.createElement('option');
option.value = operative.id;
option.text = operative.name;
select.appendChild(option);
});
select.addEventListener('change', (event) => {
const selectedOperativeId = event.target.value;
if (!selectedOperativeId) {
document.getElementById('operative-modifiers').innerHTML = '';
return;
}
selectedOperative = operatives.find(op => op.id === selectedOperativeId);
if (selectedOperative) {
updateOperativeBuffs(selectedOperative);
}
});
}
function updateOperativeBuffs(operative) {
const operativeModifiers = operative.modifiers || [];
const operativeSection = document.getElementById('operative-modifiers');
operativeSection.innerHTML = '';
if (operativeModifiers.length > 0) {
operativeModifiers.forEach(mod => {
const label = document.createElement('label');
const input = document.createElement('input');
input.type = 'checkbox';
input.id = mod.id;
input.value = mod.value;
input.dataset.type = mod.type;
input.dataset.buffs = mod.buffs ? JSON.stringify(mod.buffs) : '';
input.checked = false;
label.appendChild(input);
label.appendChild(document.createTextNode(mod.label));
operativeSection.appendChild(label);
operativeSection.appendChild(document.createElement('br'));
});
}
operativeSection.addEventListener('change', applyModifiers);
applyModifiers();
}
//fetch weapons data from JSON
function fetchWeaponsData() {
fetch('static/json/data.json')
.then(response => response.json())
.then(data => {
weapons = data;
populateWeapons();
createGunTypeCheckboxes();
})
.catch(error => console.error('Error fetching weapons data:', error));
}
//populate the weapon dropdown
function populateWeapons() {
const select = document.getElementById('weapon-select');
select.innerHTML = "";
const placeholderOption = document.createElement('option');
placeholderOption.value = "";
placeholderOption.text = "Select a Weapon";
select.appendChild(placeholderOption);
weapons.forEach(weapon => {
const option = document.createElement('option');
option.value = weapon.id;
option.text = weapon.name;
select.appendChild(option);
});
select.addEventListener('change', (event) => {
const id = parseInt(event.target.value, 10);
if (!id) {
document.getElementById('weapon-stats').innerHTML = '';
editBtn.disabled = true;
return;
}
selectedWeapon = weapons.find(w => w.id === id);
if (selectedWeapon) {
updateWeaponStats(selectedWeapon);
applyModifiers();
editBtn.disabled = false;
}
});
}
//display the selected weapon's stats in table
function updateWeaponStats(weapon) {
const statsElement = document.getElementById('weapon-stats');
statsElement.innerHTML = `
<table class="weapon-stats-table horizontal-table">
<tr>
<th>Name</th>
<th>
<span class="tooltip-wrap">
<span>Damage</span>
<button type="button" class="tooltip-trigger" aria-label="Damage definition">?</button>
<span class="tooltip" role="tooltip">Per-bullet (or per-pellet) damage after the weapon’s internal multiplier is applied.</span>
</span>
</th>
<th>RPM</th>
<th>Mag Size</th>
<th>Reload Time</th>
<th>
<span class="tooltip-wrap">
<span>DPS</span>
<button type="button" class="tooltip-trigger" aria-label="DPS definition">?</button>
<span class="tooltip" role="tooltip">Damage per second while continuously firing. Does not include reload downtime.</span>
</span>
</th>
<th>
<span class="tooltip-wrap">
<span>DPM</span>
<button type="button" class="tooltip-trigger" aria-label="DPM definition">?</button>
<span class="tooltip" role="tooltip">Damage per minute including reload time (sustained output).</span>
</span>
</th>
<th>
<span class="tooltip-wrap">
<span>Average DPS</span>
<button type="button" class="tooltip-trigger" aria-label="Average DPS definition">?</button>
<span class="tooltip" role="tooltip">Sustained DPS including reload time and magazine downtime (DPM ÷ 60).</span>
</span>
</th>
</tr>
<tr>
<td data-label="Name" id="name-value">${weapon.name}</td>
<td data-label="Damage" id="damage-value"></td>
<td data-label="RPM" id="rpm-value"></td>
<td data-label="Mag Size" id="mag-value">${weapon.mag_size}</td>
<td data-label="Reload Time" id="reload-value"></td>
<td data-label="DPS" id="dps-value"></td>
<td data-label="DPM" id="dpm-value"></td>
<td data-label="Average DPS" id="average-dps-value"></td>
</tr>
</table>
`;
const damage = weapon.damage * weapon.damage_multiplier;
const rpm = weapon.rpm;
const reloadTime = weapon.reload_time;
document.getElementById('damage-value').innerText = damage.toFixed(2);
document.getElementById('rpm-value').innerText = rpm.toFixed(2);
document.getElementById('reload-value').innerText = reloadTime.toFixed(2) + ' seconds';
calculateDPSAndDPM(damage, rpm, weapon.mag_size, reloadTime);
}
//calculate DPS, DPM, and Average DPS, and update the values in the table
function calculateDPSAndDPM(damage, rpm, magSize, reloadTime) {
const rps = rpm / 60;
const dps = damage * rps;
const reloadTimeMin = reloadTime / 60;
const dpm = (magSize * damage) / (reloadTimeMin + (magSize / rpm));
const averageDPS = dpm / 60;
document.getElementById('dps-value').innerText = dps.toFixed(2);
document.getElementById('dpm-value').innerText = dpm.toFixed(2);
document.getElementById('average-dps-value').innerText = averageDPS.toFixed(2);
const announcer = document.getElementById("stats-announcer");
if (announcer) {
announcer.textContent = `Stats updated. DPS ${dps.toFixed(2)}. Average DPS ${averageDPS.toFixed(2)}. Reload time ${reloadTime.toFixed(2)} seconds.`;
}
}
//fetch and populate modifiers from the JSON file
function fetchModifiers() {
fetch('static/json/modifiers.json')
.then(response => response.json())
.then(data => {
globalModifiers = data;
populateModifiers(data);
})
.catch(error => console.error('Error fetching modifiers:', error));
}
//populate modifiers into the HTML
function populateModifiers(modifiers) {
const modifiersContainer = document.getElementById('modifiers-container');
modifiersContainer.innerHTML = '';
const createModifierSection = (title, modifierList, sectionId) => {
const section = document.createElement('div');
section.id = sectionId;
const header = document.createElement('h3');
header.textContent = title;
section.appendChild(header);
modifierList.forEach(mod => {
const label = document.createElement('label');
const input = document.createElement('input');
input.type = 'checkbox';
input.id = mod.id;
if (mod.buffs) {
input.dataset.buffs = JSON.stringify(mod.buffs);
} else {
input.value = mod.value;
input.dataset.type = mod.type;
}
if (mod.operatives) {
input.dataset.operatives = mod.operatives;
}
if (mod.appliesTo) {
input.dataset.appliesTo = mod.appliesTo;
}
if (mod.appliesNotTo) {
input.dataset.appliesNotTo = mod.appliesNotTo;
}
label.appendChild(input);
label.appendChild(document.createTextNode(mod.label));
const modifierItem = document.createElement('div');
modifierItem.className = 'modifier-item';
modifierItem.appendChild(label);
section.appendChild(modifierItem);
});
return section;
}; };
const container = document.createElement('div');
container.id = 'modifiers';
container.appendChild(createModifierSection('Item Buffs', modifiers.itemBuffs, 'items'));
container.appendChild(createModifierSection('Perks', modifiers.operativePerks, 'perks'));
modifiersContainer.appendChild(container);
const abilitiesSection = createModifierSection('Abilities', modifiers.abilityPerks, 'abilities');
modifiersContainer.appendChild(abilitiesSection);
modifiersContainer.addEventListener('change', applyModifiers);
}
function applyModifiers() { const resetButton = document.getElementById('af-reset');
if (!selectedWeapon) return; const copyButton = document.getElementById('af-copy');
const hasNVG = !!document.querySelector(
"#modifiers input[id='damage-item-multiplicative-nightvision']:checked"
);
let baseDamage = selectedWeapon.damage;
let baseRpm = selectedWeapon.rpm;
let baseReloadTime = selectedWeapon.reload_time;
let damageModifier = {additive: 0, multiplicative: 1};
let rpmModifier = {additive: 0, multiplicative: 1};
let reloadTimeModifier = {additive: 0, multiplicative: 1};
headshotMultiplier = 0;
headshotBaseApplied = false;
ignoreHeadshots = hasNVG;
function processModifier(mod) { const numberOrNull = (value) => {
if (mod.dataset.operatives && mod.dataset.operatives.trim() !== "" && (!selectedOperative || selectedOperative.category !== mod.dataset.operatives)) { const parsed = Number.parseFloat(value);
return; return Number.isFinite(parsed) ? parsed : null;
}
const weaponTags = selectedWeapon.guntype;
const appliesNotTo = mod.dataset.appliesNotTo;
const appliesTo = mod.dataset.appliesTo;
if (appliesNotTo && weaponTags.includes(appliesNotTo)) {
return;
}
if (appliesTo && !weaponTags.includes(appliesTo)) {
return;
}
if (mod.appliesNotTo && weaponTags.includes(mod.appliesNotTo)) {
return;
}
if (mod.appliesTo && !weaponTags.includes(mod.appliesTo)) {
return;
}
if (mod.dataset.type === 'multiplicative-headshot') {
// Only apply headshot multipliers if the base headshot modifier is active
if (headshotBaseApplied) {
headshotMultiplier += parseFloat(mod.value) - 1;
}
} else if (mod.dataset.type === 'multiplicative-headshot-base') {
if (!headshotBaseApplied) {
headshotMultiplier += parseFloat(mod.value) - 1;
headshotBaseApplied = true;
}
} else {
applyBuff(mod, rpmModifier, reloadTimeModifier, damageModifier);
}
if (mod.dataset.buffs) {
const buffs = JSON.parse(mod.dataset.buffs);
buffs.forEach(buff => {
applyBuff(buff, rpmModifier, reloadTimeModifier, damageModifier);
});
} else {
const modType = mod.dataset.type;
const modValue = parseFloat(mod.value);
const singleBuff = {
type: modType === 'multiplicative' ? 'multiplicative' : 'additive',
buffType: modType,
value: modValue
}; };
if (mod.id.includes('damage')) {
singleBuff.type = 'damage';
} else if (mod.id.includes('rpm')) {
singleBuff.type = 'rpm';
} else if (mod.id.includes('reload')) {
singleBuff.type = 'reloadTime';
}
applyBuff(singleBuff, rpmModifier, reloadTimeModifier, damageModifier);
}
}
//process operative-specific modifiers const formatValue = (value, suffix = '') => {
const processOperativeModifier = (mod) => { if (value === null) {
const weaponTags = selectedWeapon.guntype; //this is now an array return '--';
if (mod.appliesNotTo && weaponTags.includes(mod.appliesNotTo)) {
return;
} }
if (mod.appliesTo && !weaponTags.includes(mod.appliesTo)) { const formatted = Number.isFinite(value) ? value.toFixed(2) : '--';
return; return `${formatted}${suffix}`;
}
if (mod.buffs) {
mod.buffs.forEach(buff => {
applyBuff(buff, rpmModifier, reloadTimeModifier, damageModifier);
});
} else {
const modValue = parseFloat(mod.value);
const modType = mod.type || 'additive';
const singleBuff = {
type: mod.id.includes('damage') ? 'damage' : mod.id.includes('rpm') ? 'rpm' : 'reloadTime',
buffType: modType === 'multiplicative' ? 'multiplicative' : 'additive',
value: modValue
}; };
applyBuff(singleBuff, rpmModifier, reloadTimeModifier, damageModifier);
}
};
const abilitiesSection = document.getElementById('abilities');
abilitiesSection.querySelectorAll('input:checked').forEach(mod => {
processModifier(mod); // Process the ability modifier the same way as global modifiers
});
const modifiersContainer = document.getElementById('modifiers');
modifiersContainer.removeEventListener('change', applyModifiers);
document.querySelectorAll('#modifiers input:checked').forEach(mod => {
processModifier(mod);
});
modifiersContainer.addEventListener('change', applyModifiers);
const operativeSection = document.getElementById('operative-modifiers');
operativeSection.querySelectorAll('input:checked').forEach(mod => {
if (selectedOperative && selectedOperative.modifiers) {
const modObj = selectedOperative.modifiers.find(m => m.id === mod.id);
if (modObj) {
processOperativeModifier(modObj);
}
}
});
let damage = baseDamage;
damage *= damageModifier.multiplicative;
if (headshotMultiplier > 0) {
damage *= (1 + headshotMultiplier);
}
damage += damageModifier.additive;
damage *= selectedWeapon.damage_multiplier;
let rpm = baseRpm * rpmModifier.multiplicative + rpmModifier.additive;
let reloadTime = baseReloadTime * reloadTimeModifier.multiplicative + reloadTimeModifier.additive;
if (reloadTime < 0.1) {
reloadTime = 0.1;
}
document.getElementById('damage-value').innerText = damage.toFixed(2);
document.getElementById('rpm-value').innerText = rpm.toFixed(2);
document.getElementById('reload-value').innerText =
reloadTime.toFixed(2) + ' seconds';
calculateDPSAndDPM(damage, rpm, selectedWeapon.mag_size, reloadTime);
}
function applyBuff(buff, rpmModifier, reloadTimeModifier, damageModifier) { const calculate = () => {
if (ignoreHeadshots &&
(buff.buffType === 'multiplicative-headshot' ||
buff.buffType === 'multiplicative-headshot-base')) {
return;
}
const {type, buffType, value} = buff;
if (buffType === 'multiplicative-headshot-base' && !headshotBaseApplied) {
headshotMultiplier = value - 1;
headshotBaseApplied = true;
return;
}
if (buffType === 'multiplicative-headshot') {
// Only apply if base headshot modifier is active
if (headshotBaseApplied) {
headshotMultiplier += value - 1;
}
return;
}
if (type === 'rpm') {
if (buffType === 'multiplicative') rpmModifier.multiplicative *= value;
else if (buffType === 'additive') rpmModifier.additive += value;
} else if (type === 'reloadTime') {
if (buffType === 'multiplicative') reloadTimeModifier.multiplicative *= value;
else if (buffType === 'additive') reloadTimeModifier.additive += value;
} else if (type === 'damage' && damageModifier) {
if (buffType === 'multiplicative') damageModifier.multiplicative *= value;
else if (buffType === 'additive') damageModifier.additive += value;
}
}
//consider gun type checkboxes
function filterWeapons() {
const input = document.getElementById('weapon-search').value.toLowerCase();
const weaponSelect = document.getElementById('weapon-select');
const options = weaponSelect.getElementsByTagName('option');
const selectedGuntypes = Array.from(document.querySelectorAll('#guntype-filters input:checked'))
.map(checkbox => checkbox.value);
let firstVisibleOption = null;
for (let i = 0; i < options.length; i++) {
const optionText = options[i].textContent || options[i].innerText;
const weaponId = parseInt(options[i].value);
const weapon = weapons.find(w => w.id === weaponId);
if (!weapon) continue;
const matchesSearch = optionText.toLowerCase().indexOf(input) > -1;
const matchesGuntypes = selectedGuntypes.length === 0 || selectedGuntypes.every(type => weapon.guntype.includes(type));
if (matchesSearch && matchesGuntypes) {
options[i].style.display = '';
if (!firstVisibleOption) {
firstVisibleOption = options[i];
}
} else {
options[i].style.display = 'none';
}
}
if (firstVisibleOption) {
weaponSelect.value = firstVisibleOption.value;
weaponSelect.dispatchEvent(new Event('change'));
}
}
//filter operatives in the dropdown based on search input
function filterOperatives() {
const input = document.getElementById('operative-search').value.toLowerCase();
const operativeSelect = document.getElementById('operative-select');
const options = operativeSelect.getElementsByTagName('option');
let firstVisibleOption = null;
for (let i = 0; i < options.length; i++) {
const optionText = options[i].textContent || options[i].innerText;
if (optionText.toLowerCase().indexOf(input) > -1) {
options[i].style.display = '';
if (!firstVisibleOption) {
firstVisibleOption = options[i];
}
} else {
options[i].style.display = 'none';
}
}
if (firstVisibleOption) {
operativeSelect.value = firstVisibleOption.value;
operativeSelect.dispatchEvent(new Event('change'));
}
}
//create gun type checkboxes based on available gun types in the weapon data
function createGunTypeCheckboxes() {
const gunTypeFiltersContainer = document.getElementById('gun-type-filters');
gunTypeFiltersContainer.innerHTML = '';
const gunTypesSet = new Set();
weapons.forEach(weapon => {
weapon.guntype.forEach(type => gunTypesSet.add(type));
});
gunTypesSet.forEach(gunType => {
const label = document.createElement('label');
const input = document.createElement('input');
input.type = 'checkbox';
input.value = gunType;
input.className = 'gun-type-checkbox';
input.addEventListener('change', () => {
const selectedCheckboxes = Array.from(document.querySelectorAll('.gun-type-checkbox:checked'));
selectedGunTypes = selectedCheckboxes.map(checkbox => checkbox.value);
filterWeapons();
});
label.appendChild(input);
label.appendChild(document.createTextNode(gunType));
gunTypeFiltersContainer.appendChild(label);
gunTypeFiltersContainer.appendChild(document.createElement('br'));
});
}
document.getElementById('weapon-search').addEventListener('keyup', filterWeapons);
document.getElementById('operative-search').addEventListener('keyup', filterOperatives);
document.querySelectorAll('#guntype-filters input').forEach(checkbox => {
checkbox.addEventListener('change', filterWeapons);
});
//Create/Edit a Weapon
const openBtn = document.getElementById('weapon-create');
const closeBtn = document.getElementById('weapon-cancel');
const modal = document.getElementById('weapon-form');
const form = document.getElementById('weaponForm');
const editBtn = document.getElementById('weapon-edit');
const editError = document.getElementById('edit-error');
const submitBtn = document.getElementById('submit-btn')
//Show the modal
openBtn.addEventListener('click', () => {
modal.style.display = 'flex';
});
closeBtn.addEventListener('click', () => {
modal.style.display = 'none';
form.reset();
isEditing = false;
});
modal.addEventListener('click', e => {
if (e.target === modal) {
modal.style.display = 'none';
form.reset();
isEditing = false;
}
});
//Handle form submission
form.addEventListener('submit', e => {
e.preventDefault();
const data = new FormData(form); const data = new FormData(form);
const types = Array.from(form.querySelectorAll('input[name="gun-type"]:checked')) const baseDamage = numberOrNull(data.get('baseDamage'));
.map(cb => cb.value); const projectiles = numberOrNull(data.get('projectiles')) ?? 1;
if (types.length === 0) { const rpm = numberOrNull(data.get('rpm'));
alert('Please select at least one weapon type.'); const magazine = numberOrNull(data.get('magazine'));
const reload = numberOrNull(data.get('reload'));
const targetHealth = numberOrNull(data.get('targetHealth'));
if (!baseDamage || !rpm) {
Object.values(outputs).forEach((node) => {
node.textContent = '--';
});
return; return;
} }
if (isEditing) {
selectedWeapon.name = data.get('name'); const shotDamage = baseDamage * projectiles;
selectedWeapon.damage = parseFloat(data.get('damage')); const shotsPerSecond = rpm / 60;
selectedWeapon.rpm = parseFloat(data.get('rpm')); const burstDps = shotDamage * shotsPerSecond;
selectedWeapon.mag_size = parseFloat(data.get('magazin')); const magDamage = magazine ? shotDamage * magazine : null;
selectedWeapon.reload_time = parseFloat(data.get('reload-time')); const timeToEmpty = magazine ? magazine / shotsPerSecond : null;
selectedWeapon.damage_multiplier = parseFloat(data.get('multiplier'));
selectedWeapon.guntype = types; let sustainedDps = null;
} else { if (magDamage !== null && reload !== null && timeToEmpty !== null && (timeToEmpty + reload) > 0) {
const newId = weapons.length sustainedDps = magDamage / (timeToEmpty + reload);
? Math.max(...weapons.map(w => w.id)) + 1
: 1;
weapons.push({
id: newId,
name: data.get('name'),
damage: parseFloat(data.get('damage')),
rpm: parseFloat(data.get('rpm')),
mag_size: parseFloat(data.get('magazin')),
reload_time: parseFloat(data.get('reload-time')),
damage_multiplier: parseFloat(data.get('multiplier')),
guntype: types
});
} }
populateWeapons();
const weaponSelect = document.getElementById('weapon-select'); let ttk = null;
weaponSelect.value = selectedWeapon.id; if (targetHealth && burstDps > 0) {
weaponSelect.dispatchEvent(new Event('change')); ttk = targetHealth / burstDps;
modal.style.display = 'none'; }
outputs.shotDamage.textContent = formatValue(shotDamage);
outputs.burstDps.textContent = formatValue(burstDps);
outputs.sustainedDps.textContent = formatValue(sustainedDps);
outputs.timeToEmpty.textContent = formatValue(timeToEmpty, 's');
outputs.magDamage.textContent = formatValue(magDamage);
outputs.ttk.textContent = formatValue(ttk, 's');
};
form.addEventListener('input', calculate);
resetButton.addEventListener('click', () => {
form.reset(); form.reset();
isEditing = false; calculate();
editBtn.disabled = true;
}); });
document.getElementById('copy-stats').addEventListener('click', async () => { copyButton.addEventListener('click', async () => {
const statsTable = document.querySelector('#weapon-stats table tr + tr'); const data = new FormData(form);
if (!statsTable) { const name = data.get('weaponName') || 'Unnamed loadout';
showCopyMessage('No weapon selected to copy.', 'error');
return;
}
const cells = Array.from(statsTable.querySelectorAll('td')).map(td => td.innerText.trim());
const [name, dmg, rpm, mag, reload, dps, dpm, avgDps] = cells;
const globalMods = Array.from(
document.querySelectorAll('#modifiers label input:checked')
).map(cb => cb.parentNode.textContent.trim());
const opMods = Array.from(
document.querySelectorAll('#operative-modifiers label input:checked')
).map(cb => cb.parentNode.textContent.trim());
const allMods = globalMods.concat(opMods);
const modsString = allMods.length
? allMods.join(', ')
: 'None';
const payload = [ const payload = [
`Weapon: ${name}`, `Loadout: ${name}`,
`Damage: ${dmg}`, `Damage per shot: ${outputs.shotDamage.textContent}`,
`RPM: ${rpm}`, `Burst DPS: ${outputs.burstDps.textContent}`,
`Mag Size: ${mag}`, `Sustained DPS: ${outputs.sustainedDps.textContent}`,
`Reload Time: ${reload}`, `Time to empty: ${outputs.timeToEmpty.textContent}`,
`DPS: ${dps}`, `Magazine damage: ${outputs.magDamage.textContent}`,
`DPM: ${dpm}`, `Time to eliminate: ${outputs.ttk.textContent}`
`Average DPS: ${avgDps}`,
`Modifiers used for this calculation: ${modsString}`
].join('\n'); ].join('\n');
try { try {
await navigator.clipboard.writeText(payload); await navigator.clipboard.writeText(payload);
showCopyMessage('Stats copied to clipboard!', 'success'); copyButton.textContent = 'Copied!';
} catch (err) {
console.error(err);
showCopyMessage('Failed to copy.', 'error');
}
});
editBtn.addEventListener('click', () => {
if (!selectedWeapon) {
editError.textContent = 'You must select a weapon first.';
return;
}
editError.textContent = '';
isEditing = true;
isEditing = true;
form.elements['name'].value = selectedWeapon.name;
form.elements['damage'].value = selectedWeapon.damage;
form.elements['rpm'].value = selectedWeapon.rpm;
form.elements['magazin'].value = selectedWeapon.mag_size;
form.elements['reload-time'].value = selectedWeapon.reload_time;
form.elements['multiplier'].value = selectedWeapon.damage_multiplier;
form.querySelectorAll('input[name="gun-type"]').forEach(cb => {
cb.checked = selectedWeapon.guntype.includes(cb.value);
});
modal.style.display = 'flex';
});
function showCopyMessage(text, type) {
const msg = document.getElementById('copy-message');
msg.textContent = text;
msg.className = type;
setTimeout(() => { setTimeout(() => {
msg.textContent = ''; copyButton.textContent = 'Copy summary';
msg.className = ''; }, 2000);
}, 3000); } catch (error) {
console.error('Clipboard copy failed', error);
copyButton.textContent = 'Copy failed';
setTimeout(() => {
copyButton.textContent = 'Copy summary';
}, 2000);
} }
});
calculate();
// Initialize
fetchWeaponsData();
fetchModifiers();
fetchOperativesData()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

+421 -7
View File
@@ -27,14 +27,13 @@ input[type="text"] {
} }
body { body {
background-image: url("/trlstats/static/img/trlbg1.jpg"); background: radial-gradient(circle at top, #0f172a 0%, #0b1120 45%, #020617 100%);
background-repeat: no-repeat;
background-attachment: fixed; background-attachment: fixed;
background-size: cover;
position: relative; position: relative;
z-index: auto; z-index: auto;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif;
margin: 20px; margin: 20px;
color: #e2e8f0;
} }
header nav { header nav {
@@ -283,9 +282,9 @@ input[type="text"], input[type="number"], textarea {
} }
input[type="text"]:focus, input[type="number"]:focus, textarea:focus { input[type="text"]:focus, input[type="number"]:focus, textarea:focus {
border-color: black; border-color: #60a5fa;
box-shadow: 0 0 5px darkred; box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.35);
background-color: #fff; background-color: #0f172a;
outline: none; outline: none;
} }
@@ -784,7 +783,7 @@ body::before {
content: ""; content: "";
position: fixed; position: fixed;
inset: 0; inset: 0;
background: linear-gradient(180deg, rgba(2, 6, 23, 0.35), rgba(2, 6, 23, 0.25)), var(--bg-tint); background: linear-gradient(180deg, rgba(2, 6, 23, 0.45), rgba(2, 6, 23, 0.25));
pointer-events: none; pointer-events: none;
z-index: 0; z-index: 0;
} }
@@ -1060,3 +1059,418 @@ th, td {
flex: 1; flex: 1;
} }
} }
/* --------------------------
Anomaly Front calculator
-------------------------- */
.af-body {
margin: 0;
padding: 32px;
}
.af-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
background: linear-gradient(135deg, rgba(15, 23, 42, 0.85), rgba(30, 41, 59, 0.6));
border: 1px solid rgba(148, 163, 184, 0.25);
border-radius: 24px;
padding: 20px 28px;
margin-bottom: 24px;
position: relative;
z-index: 2;
backdrop-filter: blur(12px);
}
.af-header::before {
content: "";
position: absolute;
inset: 0;
border-radius: 24px;
padding: 1px;
background: linear-gradient(120deg, rgba(56, 189, 248, 0.6), rgba(59, 130, 246, 0.2), rgba(14, 116, 144, 0.5));
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
.af-brand {
display: flex;
align-items: center;
gap: 16px;
}
.af-logo {
width: 44px;
height: 44px;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
gap: 4px;
padding: 6px;
border-radius: 14px;
background: rgba(15, 23, 42, 0.8);
border: 1px solid rgba(148, 163, 184, 0.35);
box-shadow: 0 8px 20px rgba(2, 6, 23, 0.45);
}
.af-logo span {
border-radius: 6px;
background: linear-gradient(135deg, rgba(56, 189, 248, 0.9), rgba(59, 130, 246, 0.6));
}
.af-logo span:nth-child(2) {
background: linear-gradient(135deg, rgba(14, 116, 144, 0.9), rgba(2, 132, 199, 0.6));
}
.af-logo span:nth-child(3) {
background: linear-gradient(135deg, rgba(37, 99, 235, 0.9), rgba(15, 23, 42, 0.6));
}
.af-title {
font-size: 1.5rem;
margin: 0;
color: #f8fafc;
font-weight: 600;
}
.af-subtitle {
margin: 4px 0 0;
color: #cbd5f5;
font-size: 0.95rem;
}
.af-nav-toggle {
display: none;
align-items: center;
gap: 8px;
padding: 8px 14px;
border-radius: 999px;
background: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(148, 163, 184, 0.35);
color: #e2e8f0;
font-weight: 600;
cursor: pointer;
z-index: 1;
}
.af-nav {
display: flex;
gap: 16px;
flex-wrap: wrap;
z-index: 1;
}
.af-nav-link {
position: relative;
color: #e2e8f0;
font-weight: 500;
padding: 8px 16px;
border-radius: 999px;
border: 1px solid rgba(148, 163, 184, 0.3);
background: rgba(15, 23, 42, 0.4);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.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(56, 189, 248, 0));
opacity: 0;
transition: opacity 0.2s ease;
}
.af-nav-link:hover {
color: #fff;
background: rgba(59, 130, 246, 0.3);
box-shadow: 0 10px 20px rgba(14, 116, 144, 0.2);
transform: translateY(-1px);
text-decoration: none;
}
.af-nav-link:hover::after {
opacity: 1;
}
.af-shell {
display: flex;
flex-direction: column;
gap: 24px;
position: relative;
z-index: 2;
}
.af-hero {
display: flex;
flex-wrap: wrap;
gap: 24px;
align-items: center;
justify-content: space-between;
background: rgba(15, 23, 42, 0.65);
border-radius: 24px;
padding: 28px;
border: 1px solid rgba(148, 163, 184, 0.2);
box-shadow: 0 20px 40px rgba(2, 6, 23, 0.4);
}
.af-hero h1 {
margin-top: 0;
color: #f8fafc;
}
.af-hero p {
color: #cbd5f5;
max-width: 560px;
}
.af-status {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 16px;
width: min(520px, 100%);
}
.af-status-label {
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.08em;
color: #94a3b8;
margin-bottom: 6px;
}
.af-status-value {
margin: 0;
font-weight: 600;
color: #e2e8f0;
}
.af-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 24px;
}
.af-card {
background: rgba(15, 23, 42, 0.65);
border-radius: 20px;
padding: 24px;
border: 1px solid rgba(148, 163, 184, 0.2);
box-shadow: 0 16px 30px rgba(2, 6, 23, 0.35);
}
.af-card-actions {
margin-top: 18px;
}
.af-cta {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 18px;
border-radius: 999px;
background: linear-gradient(120deg, rgba(56, 189, 248, 0.9), rgba(59, 130, 246, 0.85));
color: #f8fafc;
font-weight: 600;
border: 1px solid rgba(56, 189, 248, 0.6);
box-shadow: 0 12px 24px rgba(14, 116, 144, 0.35);
}
.af-cta:hover {
color: #fff;
text-decoration: none;
transform: translateY(-1px);
}
.af-card-header h2 {
margin-top: 0;
color: #f8fafc;
}
.af-card-header p {
color: #cbd5f5;
margin-bottom: 20px;
}
.af-form {
display: grid;
gap: 14px;
}
.af-form label {
display: grid;
gap: 6px;
color: #e2e8f0;
font-weight: 500;
}
.af-form input {
background: rgba(15, 23, 42, 0.8);
border: 1px solid rgba(148, 163, 184, 0.3);
color: #f8fafc;
}
.af-form input:focus,
.af-notes:focus,
.af-input:focus {
border-color: rgba(56, 189, 248, 0.7);
box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.35);
background: rgba(15, 23, 42, 0.92);
color: #f8fafc;
}
.af-field {
display: grid;
gap: 6px;
color: #e2e8f0;
font-weight: 500;
}
.af-input {
width: 100%;
padding: 12px 14px;
border-radius: 12px;
background: rgba(15, 23, 42, 0.8);
border: 1px solid rgba(148, 163, 184, 0.3);
color: #f8fafc;
}
.af-form-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-top: 6px;
}
.af-form-actions button {
background: rgba(59, 130, 246, 0.9);
color: #f8fafc;
border: none;
border-radius: 999px;
padding: 10px 18px;
font-weight: 600;
}
#af-reset {
background: rgba(148, 163, 184, 0.4);
}
.af-metrics {
display: grid;
gap: 16px;
}
.af-metric {
background: rgba(2, 6, 23, 0.35);
border-radius: 16px;
padding: 16px;
border: 1px solid rgba(148, 163, 184, 0.15);
}
.af-metric-label {
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.08em;
color: #94a3b8;
margin-bottom: 8px;
}
.af-metric-value {
margin: 0;
font-size: 1.4rem;
font-weight: 600;
color: #f8fafc;
}
.af-callout {
margin-top: 20px;
background: rgba(59, 130, 246, 0.15);
border-radius: 16px;
padding: 16px;
border: 1px solid rgba(59, 130, 246, 0.35);
}
.af-callout h3 {
margin-top: 0;
color: #e0f2fe;
}
.af-callout ul {
margin: 0;
padding-left: 18px;
color: #cbd5f5;
}
.af-empty {
display: grid;
gap: 12px;
color: #cbd5f5;
background: rgba(2, 6, 23, 0.3);
border-radius: 16px;
padding: 18px;
border: 1px dashed rgba(148, 163, 184, 0.3);
}
.af-muted-button {
background: rgba(148, 163, 184, 0.2);
border-radius: 999px;
padding: 10px 16px;
color: #e2e8f0;
border: none;
width: fit-content;
}
.af-notes {
width: 100%;
min-height: 180px;
border-radius: 16px;
padding: 16px;
background: rgba(15, 23, 42, 0.8);
border: 1px solid rgba(148, 163, 184, 0.3);
color: #f8fafc;
resize: vertical;
}
.af-copy-grid {
display: grid;
gap: 18px;
color: #cbd5f5;
}
.af-copy-grid ul {
padding-left: 18px;
margin: 8px 0 0;
}
@media (max-width: 720px) {
.af-body {
padding: 20px;
}
.af-header {
flex-direction: column;
align-items: flex-start;
}
.af-nav-toggle {
display: inline-flex;
}
.af-nav {
width: 100%;
flex-direction: column;
align-items: flex-start;
gap: 10px;
display: none;
}
.af-nav.is-open {
display: flex;
}
}
+77 -94
View File
@@ -3,115 +3,98 @@
<head> <head>
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>About Us - TRL Stats</title> <title>About | Anomaly Front Toolkit</title>
<link rel="icon" type="image/x-icon" href="/trlstats/static/img/TRL_Logo_Custom.jpg"/>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"/> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"/>
</head> </head>
<body class="af-body">
<body> <header class="af-header">
<header> <div class="af-brand">
<nav> <div class="af-logo" aria-hidden="true">
<button class="hamburger" aria-label="Toggle menu" aria-expanded="false" aria-controls="nav-menu"> <span></span>
<span class="bar"></span> <span></span>
<span class="bar"></span> <span></span>
<span class="bar"></span>
</button>
<div class="nav-menu" id="nav-menu">
<a href="{{ url_for('calculator') }}">DPS/DPM CALCULATOR</a>
<a href="{{ url_for('index') }}">GUN STATS</a>
<a href="{{ url_for('suggestion') }}">SUGGESTIONS & FEEDBACK</a>
<a href="https://www.roblox.com/games/5766084948/SCP-The-Red-Lake-EVENT">PLAY THE RED LAKE</a>
<a href="https://discord.gg/2jDevH8aTd">OUR DISCORD</a>
</div> </div>
<div>
<p class="af-title">Anomaly Front</p>
<p class="af-subtitle">About the toolkit</p>
</div>
</div>
<button class="af-nav-toggle" type="button" aria-expanded="false" aria-controls="af-nav">
Menu
</button>
<nav class="af-nav" id="af-nav" aria-label="Primary">
<a class="af-nav-link" href="{{ url_for('index') }}">Home</a>
<a class="af-nav-link" href="{{ url_for('calculator') }}">Calculator</a>
<a class="af-nav-link" href="{{ url_for('suggestion') }}">Feedback</a>
</nav> </nav>
</header> </header>
<main class="about_us_main"> <main class="af-shell">
<h1 class="about_us_h1">About Us</h1> <section class="af-card">
<div class="af-card-header">
<h3 class="about_us_h3"><b>Hey there!</b> You found the TRL Stats Page by <b>Fretux</b> and <b>Losernoob124</b></h3> <h1>About the Anomaly Front toolkit</h1>
<p>
<h2 class="about_us_h2">Our Mission</h2> This site is the staging ground for an unreleased experience. We are rebuilding every tool from the
<p class="about_us_text"> ground up to avoid legacy stats, old branding, or outdated data.
We created this platform to help the TRL community quickly and easily calculate gun DPS and DPM. No
spreadsheets, no hassle. Whether you're theorycrafting a Deathwish loadout or comparing your sidearms, our
tool’s built to support your grind.
</p>
<p class="about_us_text">
Got ideas or stat suggestions? Hit us up on Discord or submit feedback via our <a
href="{{ url_for('suggestion') }}">Suggestions Page</a>.
</p>
<h2 class="about_us_h2">Our Story</h2>
<p class="about_us_text">
It started with a spreadsheet... and frustration. Calculating DPS manually was tedious and we knew we could do
better. What began as a side hobby by Losernoob124 was reignited by Fretux and evolved into a full web app
through plenty of bug fixing, community help, and caffeine.
</p>
<h2 class="about_us_h2">The Team</h2>
<div class="team-section">
<div class="team-member">
<b>Fretux</b> – Frontend Developer & Data Specialist. Designs the UI and ensures stat accuracy.
</div>
<div class="team-member">
<b>Losernoob124</b> – Backend Developer. Handles everything under the hood and makes sure the calculators
work.
</div>
</div>
<h2 class="about_us_h2">Frequently Asked Questions</h2>
<div class="faq">
<p><b>How do you calculate DPS and DPM?</b></p>
<p class="about_us_text">
We use data from <a
href="https://docs.google.com/spreadsheets/d/1hGGLMOKB5Tj3FSo38HX7WbIvOmMbgfE5ZApTSOZgnS8/edit?gid=0#gid=0"
target="_blank">Lanz1021's Stat Sheet</a>, convert it into a JSON structure, and use it with formulas
similar to what’s on the sheet.
</p>
<p><b>How often are stats updated?</b></p>
<p class="about_us_text">
We update our database every time TRL releases a gameplay patch or changes weapon stats.
</p> </p>
</div> </div>
<div class="af-copy-grid">
<h2 class="about_us_h2">Special Thanks</h2> <div>
<div class="thanks-section"> <h2>Purpose</h2>
<p><b>gameknight90999 (holdenn)</b>: Helped with various stat and weapon data and ideas</p> <p>
<p><b>Kris HD</b>: Motivation + bug hunting</p> Provide a shared workspace for balancing weapons, capturing loadout notes, and testing combat pacing
<p><b>Lanz1021</b>: Feedback & review</p> long before the official numbers land.
<p><b>YOU</b>: For visiting — thank you!</p> </p>
</div> </div>
<div>
<h2>What is live today</h2>
<ul>
<li>Baseline combat calculator</li>
<li>Notes and modifier placeholders</li>
<li>Feedback capture workflow</li>
</ul>
</div>
<div>
<h2>What comes next</h2>
<ul>
<li>Shared weapon library for early drafts</li>
<li>Attachment tuning stacks</li>
<li>Operative perks and passive effects</li>
</ul>
</div>
</div>
</section>
<h3 class="about_us_text" style="text-align: center; margin-top: 2rem;"> <section class="af-grid">
Thanks for using <b>TRL Stats</b> <div class="af-card">
</h3> <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>
<div class="af-card">
<div class="af-card-header">
<h2>Release posture</h2>
<p>
Anomaly Front is unreleased. Everything shown here is a placeholder designed to help us think
through combat pacing before the official data arrives.
</p>
</div>
</div>
</section>
</main> </main>
<script> <script>
const hamburger = document.querySelector('.hamburger'); const navToggle = document.querySelector('.af-nav-toggle');
const navMenu = document.querySelector('.nav-menu'); const nav = document.getElementById('af-nav');
function setMenu(open) { navToggle.addEventListener('click', () => {
hamburger.classList.toggle('active', open); const isOpen = nav.classList.toggle('is-open');
navMenu.classList.toggle('active', open); navToggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
document.body.classList.toggle('menu-open', open);
hamburger.setAttribute('aria-expanded', open ? 'true' : 'false');
}
hamburger.addEventListener('click', () => {
const isOpen = hamburger.classList.contains('active');
setMenu(!isOpen);
}); });
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') setMenu(false);
});
navMenu.querySelectorAll('a').forEach(link =>
link.addEventListener('click', () => setMenu(false))
);
</script> </script>
</body> </body>
</html> </html>
+148 -179
View File
@@ -3,200 +3,169 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="/trlstats/static/img/TRL_Logo_Custom.jpg">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<title>DPM/DPS Calculator</title> <title>Anomaly Front | Combat Calculator</title>
</head> </head>
<header> <body class="af-body">
<nav> <header class="af-header">
<button class="hamburger" aria-label="Toggle menu" aria-expanded="false" aria-controls="nav-menu"> <div class="af-brand">
<span class="bar"></span> <div class="af-logo" aria-hidden="true">
<span class="bar"></span> <span></span>
<span class="bar"></span> <span></span>
</button> <span></span>
<div class="nav-menu" id="nav-menu">
<a href="{{ url_for('index') }}">GUN STATS</a>
<a href="{{ url_for('about_us') }}">ABOUT US</a>
<a href="{{ url_for('suggestion') }}">SUGGESTIONS & FEEDBACK</a>
<a href="https://www.roblox.com/games/5766084948/SCP-The-Red-Lake-EVENT">PLAY THE RED LAKE</a>
<a href="https://discord.gg/2jDevH8aTd">OUR DISCORD</a>
</div> </div>
<div>
<p class="af-title">Anomaly Front</p>
<p class="af-subtitle">Combat Calculator</p>
</div>
</div>
<button class="af-nav-toggle" type="button" aria-expanded="false" aria-controls="af-nav">
Menu
</button>
<nav class="af-nav" id="af-nav" aria-label="Primary">
<a class="af-nav-link" href="{{ url_for('index') }}">Home</a>
<a class="af-nav-link" href="{{ url_for('suggestion') }}">Feedback</a>
<a class="af-nav-link" href="{{ url_for('about_us') }}">About</a>
</nav> </nav>
</header> </header>
<body> <main class="af-shell">
<main> <section class="af-hero">
<div class="select-wrapper">
<label for="weapon-search" class="sr-only">Search weapons</label>
<input type="text" id="weapon-search" placeholder="Search weapons...">
<label for="weapon-select" class="sr-only">Select a weapon</label>
<select id="weapon-select" name="weapons">
<option value="">Select a Weapon</option>
</select>
</div>
<button id="weapon-create">Create New Weapon</button>
<button id="weapon-edit" disabled>Edit Weapon</button>
<div id="edit-error" class="error-message" aria-live="polite"></div>
<br>
<div id="guntype-filters">
<label><input type="checkbox" id="primary" value="PRIMARY"> Primary</label>
<label><input type="checkbox" id="secondary" value="SECONDARY"> Secondary</label>
<label><input type="checkbox" id="ww2" value="WW2"> WW2</label>
<label><input type="checkbox" id="soviet" value="SOVIET"> Soviet</label>
<label><input type="checkbox" id="rifle" value="RIFLE"> Rifle</label>
<label><input type="checkbox" id="smg" value="SMG"> SMG</label>
<label><input type="checkbox" id="shotgun" value="SHOTGUN"> Shotgun</label>
<label><input type="checkbox" id="lmg" value="LMG"> LMG</label>
<label><input type="checkbox" id="pistol" value="PISTOL"> Pistol</label>
<label><input type="checkbox" id="special" value="SPECIAL"> Special</label>
<label><input type="checkbox" id="semi-auto" value="SEMI-AUTO"> Semi-Auto</label>
</div>
<div class="select-wrapper">
<label for="operative-search" class="sr-only">Search operatives</label>
<input type="text" id="operative-search" placeholder="Search operatives...">
<label for="operative-select" class="sr-only">Select an operative</label>
<select id="operative-select">
<option value="">Select an operative</option>
</select>
</div>
<br>
<div id="stats-announcer" class="sr-only" aria-live="polite" aria-atomic="true"></div>
<div id="weapon-stats"></div>
<div id="calculated-stats"></div>
<button id="copy-stats">Copy to Clipboard</button>
<div id="copy-message" aria-live="polite"></div>
<h3>Operative Specific Modifiers</h3>
<div id="operative-modifiers"></div>
<div id="modifiers-container">
<div id="modifiers"></div>
<div id="abilities"></div>
</div>
</main>
<div id="weapon-form">
<div> <div>
<h2>New Weapon</h2> <h1>Build a baseline weapon profile</h1>
<form id="weaponForm"> <p>
<label> Anomaly Front is still unreleased, so this calculator starts from scratch. Add your own
Name:<br> placeholder values to explore pacing, time-to-eliminate, and reload cadence.
<input type="text" name="name" required> </p>
</label><br><br> </div>
<div class="af-status">
<div>
<p class="af-status-label">Stats library</p>
<p class="af-status-value">No official stats yet</p>
</div>
<div>
<p class="af-status-label">Modifier stack</p>
<p class="af-status-value">Coming soon</p>
</div>
<div>
<p class="af-status-label">Version</p>
<p class="af-status-value">Foundation v0.1</p>
</div>
</div>
</section>
<section class="af-grid">
<div class="af-card">
<div class="af-card-header">
<h2>Base inputs</h2>
<p>Enter a prototype loadout and tweak values to see combat pacing.</p>
</div>
<form class="af-form" id="af-calculator">
<label> <label>
Damage:<br> Weapon name
<input type="number" step="0.01" name="damage" min="0" required> <input type="text" name="weaponName" placeholder="Prototype AR" autocomplete="off">
</label><br><br>
<label>
Rounds Per Minute:<br>
<input type="number" step="0.01" name="rpm" min="0" required>
</label><br><br>
<label>
Magazine Size:<br>
<input type="number" name="magazin" min="1" required>
</label><br><br>
<label>
Reload Time:<br>
<input type="number" step="0.01" name="reload-time" min="0" required>
</label><br><br>
<label>
Damage Multiplier (bullets per shot):
<br>
<input type="number" name="multiplier" min="0" required>
</label> </label>
<fieldset class="checkbox-group"> <label>
<legend>Weapon Type<span class="required">*</span></legend> Base damage
<label><input type="checkbox" name="gun-type" value="PRIMARY"> Primary</label> <input type="number" name="baseDamage" step="0.01" min="0" placeholder="32">
<label><input type="checkbox" name="gun-type" value="SECONDARY"> Secondary</label> </label>
<label><input type="checkbox" name="gun-type" value="WW2"> WW2</label> <label>
<label><input type="checkbox" name="gun-type" value="RIFLE"> Rifle</label> Projectiles per shot
<label><input type="checkbox" name="gun-type" value="SOVIET"> Soviet</label> <input type="number" name="projectiles" step="1" min="1" value="1">
<label><input type="checkbox" name="gun-type" value="SMG"> SMG</label> </label>
<label><input type="checkbox" name="gun-type" value="SHOTGUN"> Shotgun</label> <label>
<label><input type="checkbox" name="gun-type" value="LMG"> LMG</label> Rate of fire (RPM)
<label><input type="checkbox" name="gun-type" value="PISTOL"> Pistol</label> <input type="number" name="rpm" step="0.1" min="0" placeholder="720">
<label><input type="checkbox" name="gun-type" value="SPECIAL"> Special</label> </label>
<label><input type="checkbox" name="gun-type" value="SEMI-AUTO"> Semi-Auto</label> <label>
</fieldset> Magazine size
<input type="number" name="magazine" step="1" min="0" placeholder="30">
</label>
<button type="submit" id="submit-btn">Create</button> <label>
<button type="button" id="weapon-cancel">Cancel</button> Reload time (seconds)
<input type="number" name="reload" step="0.1" min="0" placeholder="2.4">
</label>
<label>
Target health (optional)
<input type="number" name="targetHealth" step="1" min="0" placeholder="200">
</label>
<div class="af-form-actions">
<button type="button" id="af-reset">Reset</button>
<button type="button" id="af-copy">Copy summary</button>
</div>
</form> </form>
</div> </div>
<div class="af-card">
<div class="af-card-header">
<h2>Calculated outputs</h2>
<p>Instantly generated from the values you enter.</p>
</div> </div>
</body> <div class="af-metrics" id="af-metrics">
<div class="af-metric">
<p class="af-metric-label">Damage per shot</p>
<p class="af-metric-value" data-output="shotDamage">--</p>
</div>
<div class="af-metric">
<p class="af-metric-label">Burst DPS</p>
<p class="af-metric-value" data-output="burstDps">--</p>
</div>
<div class="af-metric">
<p class="af-metric-label">Sustained DPS</p>
<p class="af-metric-value" data-output="sustainedDps">--</p>
</div>
<div class="af-metric">
<p class="af-metric-label">Time to empty</p>
<p class="af-metric-value" data-output="timeToEmpty">--</p>
</div>
<div class="af-metric">
<p class="af-metric-label">Magazine damage</p>
<p class="af-metric-value" data-output="magDamage">--</p>
</div>
<div class="af-metric">
<p class="af-metric-label">Time to eliminate</p>
<p class="af-metric-value" data-output="ttk">--</p>
</div>
</div>
<div class="af-callout">
<h3>Placeholder roadmap</h3>
<ul>
<li>Attachment tuning stacks</li>
<li>Operative perks &amp; passive bonuses</li>
<li>Armor breakpoints + elemental statuses</li>
</ul>
</div>
</div>
</section>
<section class="af-grid">
<div class="af-card">
<div class="af-card-header">
<h2>Modifier library</h2>
<p>No live modifiers yet. Use this space to jot down prototype buffs.</p>
</div>
<div class="af-empty">
<p>Modifier slots are empty.</p>
<button type="button" class="af-muted-button">Add placeholder</button>
</div>
</div>
<div class="af-card">
<div class="af-card-header">
<h2>Loadout notes</h2>
<p>Capture ideas for upgrades, attachments, or role identity.</p>
</div>
<textarea class="af-notes" placeholder="Example: Assault rifle tuned for mid-range suppression."></textarea>
</div>
</section>
</main>
<script src="{{ url_for('static', filename='filterWeapons.js') }}"></script> <script src="{{ url_for('static', filename='filterWeapons.js') }}"></script>
<script> <script>
const hamburger = document.querySelector('.hamburger'); const navToggle = document.querySelector('.af-nav-toggle');
const navMenu = document.querySelector('.nav-menu'); const nav = document.getElementById('af-nav');
function lockBodyScroll() { navToggle.addEventListener('click', () => {
document.body.style.overflow = 'hidden'; const isOpen = nav.classList.toggle('is-open');
} navToggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
function unlockBodyScroll() {
document.body.style.overflow = '';
}
openBtn.addEventListener('click', () => {
modal.classList.add('show');
lockBodyScroll();
});
closeBtn.addEventListener('click', () => {
modal.classList.remove('show');
form.reset();
isEditing = false;
unlockBodyScroll();
});
modal.addEventListener('click', e => {
if (e.target === modal) {
modal.classList.remove('show');
form.reset();
isEditing = false;
unlockBodyScroll();
}
});
submitBtn.addEventListener('click', () => {
modal.classList.remove('show');
unlockBodyScroll();
})
function setMenu(open) {
hamburger.classList.toggle('active', open);
navMenu.classList.toggle('active', open);
document.body.classList.toggle('menu-open', open);
hamburger.setAttribute('aria-expanded', open ? 'true' : 'false');
}
hamburger.addEventListener('click', () => {
const isOpen = hamburger.classList.contains('active');
setMenu(!isOpen);
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') setMenu(false);
});
navMenu.querySelectorAll('a').forEach(link =>
link.addEventListener('click', () => setMenu(false))
);
document.addEventListener('DOMContentLoaded', function () {
var coll = document.getElementsByClassName("collapsible-header");
for (var i = 0; i < coll.length; i++) {
coll[i].addEventListener("click", function () {
this.classList.toggle("active");
var content = this.nextElementSibling;
if (content.style.display === "block") {
content.style.display = "none";
} else {
content.style.display = "block";
}
});
}
}); });
</script> </script>
</body>
</html> </html>
+81 -63
View File
@@ -3,76 +3,94 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="/trlstats/static/img/TRL_Logo_Custom.jpg"> <title>Anomaly Front | Operations Hub</title>
<title>Gun Stats</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head> </head>
<header> <body class="af-body">
<nav> <header class="af-header">
<button class="hamburger" aria-label="Toggle menu" aria-expanded="false" aria-controls="nav-menu"> <div class="af-brand">
<span class="bar"></span> <div class="af-logo" aria-hidden="true">
<span class="bar"></span> <span></span>
<span class="bar"></span> <span></span>
</button> <span></span>
<div class="nav-menu" id="nav-menu">
<a href="{{ url_for('calculator') }}">DPS/DPM CALCULATOR</a>
<a href="{{ url_for('about_us') }}">ABOUT US</a>
<a href="{{ url_for('suggestion') }}">SUGGESTIONS & FEEDBACK</a>
<a href="https://www.roblox.com/games/5766084948/SCP-The-Red-Lake-EVENT">PLAY THE RED LAKE</a>
<a href="https://discord.gg/2jDevH8aTd">OUR DISCORD</a>
</div> </div>
<div>
<p class="af-title">Anomaly Front</p>
<p class="af-subtitle">Operations hub for the unreleased toolkit.</p>
</div>
</div>
<button class="af-nav-toggle" type="button" aria-expanded="false" aria-controls="af-nav">
Menu
</button>
<nav class="af-nav" id="af-nav" aria-label="Primary">
<a class="af-nav-link" href="{{ url_for('calculator') }}">Calculator</a>
<a class="af-nav-link" href="{{ url_for('suggestion') }}">Feedback</a>
<a class="af-nav-link" href="{{ url_for('about_us') }}">About</a>
</nav> </nav>
</header> </header>
<body class="background-image"> <main class="af-shell">
<main> <section class="af-hero">
<h1>Gun Stats</h1> <div>
<div class="search-container"> <h1>Welcome to the Anomaly Front planning space</h1>
<input type="text" id="search" placeholder="Search by name" onkeyup="filterTable()"> <p>
This hub replaces the legacy stats pages with a clean foundation for a new game. Start with the
calculator, capture balancing ideas, and iterate as official data arrives.
</p>
</div> </div>
<table id="gun-stats" class="background-roster"> <div class="af-status">
<thead> <div>
<tr> <p class="af-status-label">Core modules</p>
<th>Name</th> <p class="af-status-value">Calculator • Notes • Roadmap</p>
<th>Damage</th> </div>
<th>Rounds per minute</th> <div>
<th>Magazine Size</th> <p class="af-status-label">Data state</p>
<th>Reload time</th> <p class="af-status-value">Prototype-only</p>
<th>RPS</th> </div>
<th>DPS</th> <div>
<th>DPM</th> <p class="af-status-label">Next release</p>
<th>Avg DPS</th> <p class="af-status-value">Loadout library</p>
</tr> </div>
</thead> </div>
<tbody> </section>
<!-- Gun stats will be populated here -->
</tbody>
</table>
<script src="{{ url_for('static', filename='populateTable.js') }}"></script>
<script src="{{ url_for('static', filename='filterTable.js') }}"></script>
<script>
const hamburger = document.querySelector('.hamburger');
const navMenu = document.querySelector('.nav-menu');
function setMenu(open) { <section class="af-grid">
hamburger.classList.toggle('active', open); <div class="af-card">
navMenu.classList.toggle('active', open); <div class="af-card-header">
document.body.classList.toggle('menu-open', open); <h2>Start a baseline build</h2>
hamburger.setAttribute('aria-expanded', open ? 'true' : 'false'); <p>Jump into the combat calculator and block out a weapon profile in minutes.</p>
} </div>
<div class="af-card-actions">
hamburger.addEventListener('click', () => { <a class="af-cta" href="{{ url_for('calculator') }}">Open calculator</a>
const isOpen = hamburger.classList.contains('active'); </div>
setMenu(!isOpen); </div>
}); <div class="af-card">
<div class="af-card-header">
document.addEventListener('keydown', (e) => { <h2>Capture early feedback</h2>
if (e.key === 'Escape') setMenu(false); <p>Use the feedback page to log concepts, UI tweaks, and balancing ideas.</p>
}); </div>
<div class="af-card-actions">
navMenu.querySelectorAll('a').forEach(link => <a class="af-cta" href="{{ url_for('suggestion') }}">Leave feedback</a>
link.addEventListener('click', () => setMenu(false)) </div>
); </div>
</script> <div class="af-card">
<div class="af-card-header">
<h2>Align on the vision</h2>
<p>Keep the team on the same page with the updated Anomaly Front background.</p>
</div>
<div class="af-card-actions">
<a class="af-cta" href="{{ url_for('about_us') }}">Read the brief</a>
</div>
</div>
</section>
</main> </main>
<script>
const navToggle = document.querySelector('.af-nav-toggle');
const nav = document.getElementById('af-nav');
navToggle.addEventListener('click', () => {
const isOpen = nav.classList.toggle('is-open');
navToggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
});
</script>
</body> </body>
</html> </html>
+52 -49
View File
@@ -3,70 +3,73 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="/trlstats/static/img/TRL_Logo_Custom.jpg">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<title>Suggestion</title> <title>Feedback | Anomaly Front</title>
</head> </head>
<header> <body class="af-body">
<nav> <header class="af-header">
<button class="hamburger" aria-label="Toggle menu" aria-expanded="false" aria-controls="nav-menu"> <div class="af-brand">
<span class="bar"></span> <div class="af-logo" aria-hidden="true">
<span class="bar"></span> <span></span>
<span class="bar"></span> <span></span>
</button> <span></span>
<div class="nav-menu" id="nav-menu">
<a href="{{ url_for('calculator') }}">DPS/DPM CALCULATOR</a>
<a href="{{ url_for('about_us') }}">ABOUT US</a>
<a href="{{ url_for('index') }}">GUN STATS</a>
<a href="https://www.roblox.com/games/5766084948/SCP-The-Red-Lake-EVENT">PLAY THE RED LAKE</a>
<a href="https://discord.gg/2jDevH8aTd">OUR DISCORD</a>
</div> </div>
<div>
<p class="af-title">Anomaly Front</p>
<p class="af-subtitle">Feedback channel</p>
</div>
</div>
<button class="af-nav-toggle" type="button" aria-expanded="false" aria-controls="af-nav">
Menu
</button>
<nav class="af-nav" id="af-nav" aria-label="Primary">
<a class="af-nav-link" href="{{ url_for('index') }}">Home</a>
<a class="af-nav-link" href="{{ url_for('calculator') }}">Calculator</a>
<a class="af-nav-link" href="{{ url_for('about_us') }}">About</a>
</nav> </nav>
</header> </header>
<body> <main class="af-shell">
<main class="background-image"> <section class="af-card">
<h1>Suggestion</h1> <div class="af-card-header">
<h3 class="suggestion-title"><b>Welcome to Suggestions!</b><br> We encourage you to share your ideas, improvments, <h1>Share your feedback</h1>
or even fun comments with us, so we can develop this website further.</h3> <p>
<article> Help us shape the Anomaly Front toolkit. Share feature requests, balance ideas, or UI tweaks and we
<label for="name">Pseudoname:</label> will log them for the next iteration.
<input type="text" id="name" name="name" placeholder="not required"><br><br> </p>
</div>
<article class="af-form">
<label class="af-field" for="name">
Pseudoname
<input class="af-input" type="text" id="name" name="name" placeholder="Optional">
</label>
<label for="rate">Rating:</label> <label class="af-field" for="rate">
<input type="number" id="rate" name="rating" min="1" max="10" required><br><br> Rating
<input class="af-input" type="number" id="rate" name="rating" min="1" max="10" required>
</label>
<label for="text">Text:</label> <label class="af-field" for="text">
<textarea id="text" name="text" rows="4" cols="50" placeholder="Enter your comments here..." minlength="5" Feedback
maxlength="500" required></textarea><br><br> <textarea class="af-input" id="text" name="text" rows="4" cols="50" placeholder="Enter your comments here..." minlength="5"
maxlength="500" required></textarea>
</label>
</article> </article>
<button class="button1" type="button" onclick="sendMessage()">Send</button> <div class="af-form-actions">
<button class="af-cta" type="button" onclick="sendMessage()">Send feedback</button>
</div>
<p id="successMessage" style="display: none;" aria-live="polite" role="status"></p> <p id="successMessage" style="display: none;" aria-live="polite" role="status"></p>
<p id="errorMessage" style="display: none; color: crimson;" aria-live="polite" role="status"></p> <p id="errorMessage" style="display: none; color: crimson;" aria-live="polite" role="status"></p>
</section>
</main> </main>
<script src="{{ url_for('static', filename='suggestion.js') }}"></script> <script src="{{ url_for('static', filename='suggestion.js') }}"></script>
<script> <script>
const hamburger = document.querySelector('.hamburger'); const navToggle = document.querySelector('.af-nav-toggle');
const navMenu = document.querySelector('.nav-menu'); const nav = document.getElementById('af-nav');
function setMenu(open) { navToggle.addEventListener('click', () => {
hamburger.classList.toggle('active', open); const isOpen = nav.classList.toggle('is-open');
navMenu.classList.toggle('active', open); navToggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
document.body.classList.toggle('menu-open', open);
hamburger.setAttribute('aria-expanded', open ? 'true' : 'false');
}
hamburger.addEventListener('click', () => {
const isOpen = hamburger.classList.contains('active');
setMenu(!isOpen);
}); });
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') setMenu(false);
});
navMenu.querySelectorAll('a').forEach(link =>
link.addEventListener('click', () => setMenu(false))
);
</script> </script>
</body> </body>
</html> </html>