diff --git a/CHANGELOG.md b/CHANGELOG.md
index f18a884..d9c251d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,9 @@
### Changed
- Release-Automation ergänzt: `npm run release:sync`, `npm run release:verify`, `.githooks/pre-commit` und versionsbasiertes Cache-Busting aus `package.json` – helldivers-trainer
+- Practice- und Match-Flow wirken jetzt deutlich spielerischer mit Deployment-Countdown, Fokusbereich, direkter Feedback-Anzeige und staerkeren Mobile-HUDs
+- Stratagem-Icons wurden breit ueberarbeitet und koennen ueber das neue Wiki-Download-Skript einfacher aktualisiert werden
+- Admin-Bereich zeigt mehr Nutzer- und Aktivitaetsdaten, inklusive Sessions, letzter Aktivitaet, Passwort-Reset und Rollenpflege
## [2.1.3] – 2026-03-31
diff --git a/PROJECT_MAP.md b/PROJECT_MAP.md
index 5183e97..a74b002 100644
--- a/PROJECT_MAP.md
+++ b/PROJECT_MAP.md
@@ -48,6 +48,9 @@ state = {
| `loadSettings() / saveSettings()` | localStorage-Persistenz für Settings |
| `applySettingsToUI()` | Settings auf UI-Elemente anwenden |
| `showView(name)` | Ansicht wechseln (login/dashboard/practice/leaderboard/history/admin) |
+| `focusGameplayArea(id)` | Scrollt den aktiven Gameplay-Bereich gezielt in den Viewport |
+| `runGameplayCountdown(label, steps)` | Kurzer Start-Countdown vor Practice- und Match-Runden |
+| `showGameplayFeedback(id, text, tone, duration)` | Inline-Feedback fuer Treffer, Fehler und Startphasen |
| `connectWS() / wsSend(type, payload)` | WebSocket-Verbindung + Nachrichten senden |
| `handleWSMessage({ type, payload })` | Eingehende WS-Nachrichten dispatchen |
@@ -63,6 +66,7 @@ state = {
| `startSpeedrunTimer()` | Timer für Speedrun-Modus |
| `renderPracticeStratagem()` | Aktuelles Stratagem + Pfeile rendern |
| `renderArrows(containerId, sequence, progress)` | Pfeil-Sequenz rendern |
+| `trackPracticeMistake()` | Fehler je Stratagem fuer die Session-Statistik erfassen |
| `updateScoreDisplay() / updateStreakDisplay() / updateLivesDisplay()` | Score-UI |
| `openSessionSummary() / closeSessionSummary()` | Sitzungs-Zusammenfassung Modal |
diff --git a/public/app.js b/public/app.js
index f73d037..a04a7ef 100644
--- a/public/app.js
+++ b/public/app.js
@@ -56,7 +56,7 @@ const state = {
speedrunElapsed: 0,
// Session stats
- sessionStats: { completed: 0, missed: 0, bestTime: Infinity, stratagems: {} },
+ sessionStats: { completed: 0, missed: 0, bestTime: Infinity, stratagems: {}, mistakes: {}, maxStreak: 0 },
},
lobby: {
@@ -144,6 +144,46 @@ function showView(name) {
if (name === 'practice') initPracticeView();
if (name === 'lobby') updateLobbyView();
if (name === 'history') loadHistory();
+
+ if (name !== 'practice') document.body.classList.remove('in-practice-session');
+ if (name !== 'match') document.body.classList.remove('in-match-round');
+}
+
+function focusGameplayArea(id) {
+ const el = document.getElementById(id);
+ if (!el) return;
+ requestAnimationFrame(() => {
+ el.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ });
+}
+
+function sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+async function runGameplayCountdown(label = 'Deploying', steps = ['3', '2', '1', 'GO']) {
+ const el = document.getElementById('gameplay-countdown');
+ if (!el) return;
+ el.classList.remove('hidden');
+ for (const step of steps) {
+ el.innerHTML = `${esc(label)}${esc(step)}`;
+ el.classList.remove('countdown-pop');
+ void el.offsetWidth;
+ el.classList.add('countdown-pop');
+ await sleep(step === 'GO' ? 380 : 520);
+ }
+ el.classList.add('hidden');
+}
+
+function showGameplayFeedback(id, text, tone = 'info', duration = 900) {
+ const el = document.getElementById(id);
+ if (!el) return;
+ el.textContent = text;
+ el.className = `gameplay-feedback gameplay-feedback-${tone}`;
+ clearTimeout(el._hideTimer);
+ el._hideTimer = setTimeout(() => {
+ el.className = 'gameplay-feedback hidden';
+ }, duration);
}
// ── Auth ──────────────────────────────────────────────────────────────────────
@@ -239,6 +279,9 @@ document.getElementById('change-password-form').addEventListener('submit', async
document.querySelectorAll('.nav-btn[data-view]').forEach(btn => {
btn.addEventListener('click', () => showView(btn.dataset.view));
});
+document.getElementById('btn-briefing-practice')?.addEventListener('click', () => showView('practice'));
+document.getElementById('btn-briefing-lobby')?.addEventListener('click', () => showView('lobby'));
+document.getElementById('btn-briefing-leaderboard')?.addEventListener('click', () => showView('leaderboard'));
// ── Hamburger nav ─────────────────────────────────────────────────────────────
function openDrawer() {
@@ -317,8 +360,9 @@ function handleWSMessage({ type, payload }) {
state.match.current = payload.stratagem;
state.match.myProgress = 0;
state.match.oppProgress = 0;
- state.match.roundActive = true;
+ state.match.roundActive = false;
renderMatchRound();
+ beginMatchRound();
break;
case 'input-result':
@@ -365,6 +409,7 @@ async function loadDashboard() {
function renderDashboard({ stats, rank, elo, eloRank: rankLabel, online, recent, daily }) {
const myRank = eloRankFor(elo || 1000);
+ const onlineOthers = (online || []).filter(u => u.name !== state.user?.user);
setText('dash-hero-name', state.user.user);
setText('dash-rank-label', rankLabel || myRank.label);
setText('dash-elo', elo || 1000);
@@ -375,14 +420,22 @@ function renderDashboard({ stats, rank, elo, eloRank: rankLabel, online, recent,
setText('dash-sessions', stats.sessions || 0);
const wr = stats.matches > 0 ? Math.round(((stats.wins || 0) / stats.matches) * 100) + '%' : '—';
setText('dash-win-rate', wr);
+ setText('dash-online-count', String(onlineOthers.length));
if (daily) {
setText('dash-daily-name', daily.stratagem.name);
setText('dash-daily-category', daily.stratagem.category);
setText('dash-daily-best', daily.bestTime ? (daily.bestTime / 1000).toFixed(2) + 's' : 'No record yet');
+ setText('dash-daily-focus', daily.stratagem.category);
+ setText('dash-status-line', daily.bestTime
+ ? `Daily focus is ${daily.stratagem.name}. Your best run is ${(daily.bestTime / 1000).toFixed(2)}s, so a cleaner sequence could move you up fast.`
+ : `Daily focus is ${daily.stratagem.name}. No record logged yet, so this is a clean chance to set the pace for today.`);
state.practice.dailyTarget = daily.stratagem.name;
renderDailySequencePreview(daily.stratagem.sequence);
setIcon(document.getElementById('dash-daily-icon'), daily.stratagem.icon);
+ } else {
+ setText('dash-daily-focus', 'Stand By');
+ setText('dash-status-line', 'Systems are online. Review recent runs, sharpen your execution, and push your rank before heading into the arena.');
}
const tbody = document.getElementById('dash-recent');
@@ -469,6 +522,7 @@ function showPracticeIdle() {
document.getElementById('hud-lives-wrap').classList.add('hidden');
document.getElementById('hud-timer-wrap').classList.remove('hidden');
document.getElementById('danger-vignette').classList.add('hidden');
+ document.body.classList.remove('in-practice-session');
state.practice.active = false;
}
@@ -479,10 +533,10 @@ function getPool() {
}
function resetSessionStats() {
- state.practice.sessionStats = { completed: 0, missed: 0, bestTime: Infinity, stratagems: {} };
+ state.practice.sessionStats = { completed: 0, missed: 0, bestTime: Infinity, stratagems: {}, mistakes: {}, maxStreak: 0 };
}
-function startPractice() {
+async function startPractice() {
const pool = getPool();
if (!pool.length) { showToast('No stratagems match the selected filters'); return; }
@@ -496,6 +550,7 @@ function startPractice() {
document.getElementById('practice-idle').classList.add('hidden');
document.getElementById('practice-active').classList.remove('hidden');
+ document.body.classList.add('in-practice-session');
if (mode === 'drill') {
state.practice.drillPool = shuffleArray([...pool]);
@@ -526,6 +581,9 @@ function startPractice() {
setText('hud-timer-label', 'TIME');
}
+ focusGameplayArea('practice-active');
+ showGameplayFeedback('practice-feedback', 'Stand by. Deployment starting.', 'info', 1500);
+ await runGameplayCountdown('Deployment');
nextStratagem();
}
@@ -642,8 +700,10 @@ function startPracticeTimer() {
document.getElementById('danger-vignette').classList.add('hidden');
state.practice.streak = 0;
state.practice.sessionStats.missed++;
+ trackPracticeMistake();
updateStreakDisplay();
shakeIcon();
+ showGameplayFeedback('practice-feedback', 'Timer expired. Next stratagem.', 'danger', 1000);
setTimeout(nextStratagem, 500);
}
}, 1000);
@@ -687,6 +747,7 @@ function renderPracticeStratagem() {
if (iconEl) iconEl.style.display = 'none';
if (fallbackEl) fallbackEl.style.display = '';
}
+ showGameplayFeedback('practice-feedback', `${s.name} ready. Execute the sequence.`, 'info', 900);
}
function setIcon(imgEl, src) {
@@ -698,17 +759,25 @@ function setIcon(imgEl, src) {
}
function renderArrows(containerId, sequence, progress) {
- const ARROW = { up: '↑', down: '↓', left: '←', right: '→' };
const el = document.getElementById(containerId);
if (!el) return;
el.innerHTML = sequence.map((dir, i) => {
let cls = 'arrow-key';
if (i < progress) cls += ' completed';
if (i === progress) cls += ' active';
- return `
${ARROW[dir]}
`;
+ return `${renderDirGlyph(dir)}
`;
}).join('');
}
+function renderDirGlyph(dir) {
+ return `
+
+ `;
+}
+
function updateTimerDisplay(total) {
if (state.practice.mode === 'speedrun') return;
const el = document.getElementById('practice-timer');
@@ -754,6 +823,13 @@ function updateLivesDisplay() {
).join('');
}
+function trackPracticeMistake() {
+ const current = state.practice.current;
+ if (!current) return;
+ const mistakes = state.practice.sessionStats.mistakes;
+ mistakes[current.name] = (mistakes[current.name] || 0) + 1;
+}
+
function updateDrillProgress() {
const p = state.practice;
setText('drill-progress-text', p.drillCompleted + ' / ' + p.drillTotal);
@@ -801,6 +877,7 @@ function handlePracticeInput(dir) {
p.score += pts;
p.streak++;
+ p.sessionStats.maxStreak = Math.max(p.sessionStats.maxStreak, p.streak);
p.sessionStats.completed++;
if (elapsed < p.sessionStats.bestTime) p.sessionStats.bestTime = elapsed;
@@ -825,6 +902,7 @@ function handlePracticeInput(dir) {
// Score popup
showScorePopup('+' + pts);
+ showGameplayFeedback('practice-feedback', `${p.streak >= 5 ? 'Perfect chain' : 'Confirmed'} +${pts}`, 'success', 1100);
api('POST', '/scores/practice', {
stratagem: p.current.name,
@@ -852,6 +930,8 @@ function handlePracticeInput(dir) {
cur?.classList.add('flash-wrong');
p.progress = 0;
shakeIcon();
+ trackPracticeMistake();
+ showGameplayFeedback('practice-feedback', 'Wrong input. Sequence reset.', 'danger', 1000);
if (mode === 'endless') {
p.lives--;
@@ -931,7 +1011,7 @@ function openSessionSummary() {
grid.innerHTML = [
{ label: 'Score', val: p.score },
{ label: 'Completed', val: s.completed },
- { label: 'Streak Max',val: p.streak },
+ { label: 'Streak Max',val: s.maxStreak },
{ label: 'Accuracy', val: accuracy },
{ label: 'Best Time', val: bestTimeStr },
{ label: 'Mode', val: p.mode },
@@ -941,6 +1021,20 @@ function openSessionSummary() {
`).join('');
}
+ const insightsEl = document.getElementById('summary-insights');
+ if (insightsEl) {
+ const mistakeEntries = Object.entries(s.mistakes).sort((a, b) => b[1] - a[1]);
+ const stratEntries = Object.entries(s.stratagems)
+ .map(([name, stat]) => ({ name, avg: stat.totalMs / stat.count, count: stat.count }))
+ .sort((a, b) => b.avg - a.avg);
+ const insights = [
+ `Accuracy landed at ${accuracy}${s.maxStreak >= 5 ? ` with a peak streak of ${s.maxStreak}.` : '.'}`,
+ stratEntries[0] ? `Slowest repeated stratagem was ${stratEntries[0].name} at ${(stratEntries[0].avg / 1000).toFixed(2)}s average.` : 'Run more sessions to identify your slowest stratagems.',
+ mistakeEntries[0] ? `Most common reset came from ${mistakeEntries[0][0]} with ${mistakeEntries[0][1]} mistake${mistakeEntries[0][1] > 1 ? 's' : ''}.` : 'No input resets recorded in this session.',
+ ];
+ insightsEl.innerHTML = insights.map(line => `${esc(line)}
`).join('');
+ }
+
// Top stratagems by count
const topEl = document.getElementById('summary-top-stratagems');
if (topEl) {
@@ -1087,6 +1181,9 @@ function renderMatchWaiting() {
btn.textContent = 'READY';
btn.disabled = false;
btn.classList.remove('hidden');
+ document.body.classList.remove('in-match-round');
+ showGameplayFeedback('match-feedback', 'Waiting for both divers to ready up.', 'info', 1200);
+ updateMatchProgressUI();
// Hide match icon
const matchIcon = document.getElementById('match-icon');
if (matchIcon) matchIcon.style.display = 'none';
@@ -1111,27 +1208,35 @@ function renderMatchRound() {
setText('match-category', m.current.category);
document.getElementById('match-round-area').classList.remove('hidden');
document.getElementById('match-ready-btn').classList.add('hidden');
+ document.body.classList.add('in-match-round');
renderArrows('match-me-sequence', m.current.sequence, 0);
renderArrows('match-opp-sequence', m.current.sequence, 0);
+ updateMatchProgressUI();
// Show stratagem icon in match
const strat = state.stratagems.find(s => s.name === m.current.name);
const matchIcon = document.getElementById('match-icon');
if (strat?.icon) setIcon(matchIcon, strat.icon);
else if (matchIcon) matchIcon.style.display = 'none';
+ focusGameplayArea('match-round-area');
}
function updateMyArrows(correct) {
renderArrows('match-me-sequence', state.match.current.sequence, state.match.myProgress);
+ updateMatchProgressUI();
if (!correct) {
const el = document.getElementById('match-me-sequence');
el?.classList.add('flash-wrong-seq');
setTimeout(() => el?.classList.remove('flash-wrong-seq'), 350);
+ showGameplayFeedback('match-feedback', 'Input rejected. Recover now.', 'danger', 900);
+ } else {
+ showGameplayFeedback('match-feedback', 'Confirmed. Keep pushing.', 'success', 550);
}
}
function updateOppArrows() {
renderArrows('match-opp-sequence', state.match.current.sequence, state.match.oppProgress);
+ updateMatchProgressUI();
}
function handleMatchInput(dir) {
@@ -1139,14 +1244,36 @@ function handleMatchInput(dir) {
wsSend('input-arrow', { direction: dir });
}
+function updateMatchProgressUI() {
+ const total = state.match.current?.sequence?.length || 0;
+ const myFill = document.getElementById('match-me-progress-fill');
+ const oppFill = document.getElementById('match-opp-progress-fill');
+ const myText = document.getElementById('match-me-progress-text');
+ const oppText = document.getElementById('match-opp-progress-text');
+ if (myFill) myFill.style.width = total ? `${(state.match.myProgress / total) * 100}%` : '0%';
+ if (oppFill) oppFill.style.width = total ? `${(state.match.oppProgress / total) * 100}%` : '0%';
+ if (myText) myText.textContent = `${state.match.myProgress} / ${total}`;
+ if (oppText) oppText.textContent = `${state.match.oppProgress} / ${total}`;
+}
+
+async function beginMatchRound() {
+ focusGameplayArea('match-round-area');
+ showGameplayFeedback('match-feedback', 'Round locked. Prepare to input.', 'info', 1200);
+ await runGameplayCountdown('Round Start');
+ state.match.roundActive = true;
+ showGameplayFeedback('match-feedback', 'Go go go.', 'success', 700);
+}
+
function renderRoundResult(winner) {
const won = winner === state.user.user;
setText('match-status', won ? '✓ ROUND WON' : '✗ ROUND LOST');
renderMatchScores();
+ showGameplayFeedback('match-feedback', won ? 'Round secured.' : 'Opponent took the round.', won ? 'success' : 'danger', 1200);
const matchIcon = document.getElementById('match-icon');
if (matchIcon) matchIcon.style.display = 'none';
setTimeout(() => {
document.getElementById('match-round-area').classList.add('hidden');
+ document.body.classList.remove('in-match-round');
const btn = document.getElementById('match-ready-btn');
btn.textContent = 'Ready for next round';
btn.disabled = false;
@@ -1388,6 +1515,9 @@ document.getElementById('history-pagination')?.addEventListener('click', (e) =>
document.getElementById('history-filter-mode')?.addEventListener('change', () => { state.history.page = 1; loadHistory(); });
document.getElementById('history-filter-cat')?.addEventListener('change', () => { state.history.page = 1; loadHistory(); });
+document.getElementById('admin-user-search')?.addEventListener('input', () => {
+ renderAdminUsers(state.adminUsers || []);
+});
async function loadStratagemStats() {
try {
@@ -1416,25 +1546,143 @@ async function loadStratagemStats() {
async function loadAdmin() {
if (state.user?.role !== 'admin') { showView('dashboard'); return; }
try {
- const users = await api('GET', '/users');
+ const [users, overview, activity] = await Promise.all([
+ api('GET', '/users'),
+ api('GET', '/admin/overview'),
+ api('GET', '/admin/activity'),
+ ]);
+ state.adminUsers = users;
+ renderAdminOverview(overview);
renderAdminUsers(users);
+ renderAdminActivity(activity);
} catch {
document.getElementById('admin-users').innerHTML = 'Error loading users';
}
}
+function renderAdminOverview(data = {}) {
+ setText('admin-total-users', String(data.totals?.users ?? 0));
+ setText('admin-total-admins', String(data.totals?.admins ?? 0));
+ setText('admin-temp-passwords', String(data.totals?.tempPasswords ?? 0));
+ setText('admin-practice-sessions', String(data.activity?.practiceSessions ?? 0));
+
+ const topUser = data.topUser?.username || 'No data yet';
+ const topMeta = data.topUser
+ ? `Score ${Number(data.topUser.totalScore || 0).toLocaleString()} across ${data.topUser.sessions || 0} sessions`
+ : 'Waiting for enough runs to identify a standout Helldiver.';
+
+ setText('admin-top-user', topUser);
+ setText('admin-top-user-meta', topMeta);
+}
+
function renderAdminUsers(users) {
const el = document.getElementById('admin-users');
- el.innerHTML = users.map(u =>
- `
- ${esc(u.username)}
- ${u.role}
- ${u.mustChange ? 'temp pw' : ''}
- ${u.username !== state.user.user
- ? ``
- : ''}
-
`
- ).join('');
+ const search = document.getElementById('admin-user-search')?.value.trim().toLowerCase() || '';
+ const filtered = users.filter((u) => {
+ if (!search) return true;
+ return [
+ u.username,
+ u.role,
+ String(u.elo ?? ''),
+ String(u.sessions ?? ''),
+ ].join(' ').toLowerCase().includes(search);
+ });
+
+ if (!filtered.length) {
+ el.innerHTML = 'No matching users found.
';
+ return;
+ }
+
+ el.innerHTML = filtered.map((u) => {
+ const isSelf = u.username === state.user.user;
+ const nextRole = u.role === 'admin' ? 'user' : 'admin';
+ const lastPlayed = u.lastPlayed ? new Date(u.lastPlayed).toLocaleString() : 'No activity yet';
+ return `
+
+
+ ${esc(u.username)}
+ ${u.role}
+ ${u.mustChange ? 'temp pw' : ''}
+ ${isSelf ? 'you' : ''}
+
+
+ ELO ${Number(u.elo ?? 1000)}
+ ${Number(u.sessions ?? 0)} sessions
+ ${esc(lastPlayed)}
+
+
+
+
+ ${isSelf ? '' : ``}
+ ${isSelf ? '' : ``}
+
+
`;
+ }).join('');
+}
+
+function renderAdminActivity(data = {}) {
+ const practiceEl = document.getElementById('admin-recent-practice');
+ const matchesEl = document.getElementById('admin-recent-matches');
+
+ const practiceRows = data.practice || [];
+ const matchRows = data.matches || [];
+
+ practiceEl.innerHTML = practiceRows.length
+ ? practiceRows.map((row) => `
+
+
+ ${esc(row.username)}
+ ${esc(row.mode || 'practice')}
+
+
${esc(row.stratagem || 'Unknown stratagem')}
+
+ ${Number(row.score || 0)} pts
+ ${row.created_at ? esc(new Date(row.created_at).toLocaleString()) : '—'}
+
+
+ `).join('')
+ : 'No recent practice activity.
';
+
+ matchesEl.innerHTML = matchRows.length
+ ? matchRows.map((row) => `
+
+
+ ${esc(row.winner || 'Pending')}
+ ${esc(row.winner || 'Pending')} vs ${esc(row.loser || 'Unknown')}
+
+
Scoreline: ${row.winner_rounds ?? 0} : ${row.loser_rounds ?? 0}
+
+ ${row.created_at ? esc(new Date(row.created_at).toLocaleString()) : '—'}
+
+
+ `).join('')
+ : 'No recent match activity.
';
+}
+
+async function resetUserPassword(username) {
+ if (!confirm(`Reset password for "${username}" and require a password change on next login?`)) return;
+ try {
+ const result = await api('POST', `/users/${encodeURIComponent(username)}/reset-password`);
+ const pwEl = document.getElementById('new-pw-display');
+ pwEl.textContent = `Temp password for ${username}: ${result.tempPassword}`;
+ pwEl.classList.remove('hidden');
+ showToast(`Password reset for ${username}`);
+ loadAdmin();
+ } catch (err) {
+ showToast('Error: ' + err.message);
+ }
+}
+
+async function updateUserRole(username, role) {
+ const label = role === 'admin' ? 'promote' : 'demote';
+ if (!confirm(`Really ${label} "${username}"?`)) return;
+ try {
+ await api('PATCH', `/users/${encodeURIComponent(username)}`, { role });
+ showToast(`Role updated for ${username}`);
+ loadAdmin();
+ } catch (err) {
+ showToast('Error: ' + err.message);
+ }
}
async function createUser() {
@@ -1478,6 +1726,8 @@ document.addEventListener('click', (e) => {
if (action === 'challenge' && user) sendChallenge(user);
if (action === 'accept' && user) acceptChallenge(user);
if (action === 'decline' && user) declineChallenge(user);
+ if (action === 'reset-password' && user) resetUserPassword(user);
+ if (action === 'toggle-role' && user) updateUserRole(user, btn.dataset.role);
if (action === 'delete-user' && user) deleteUser(user);
if (action === 'toggle-cat' && cat) toggleCategory(cat);
diff --git a/public/icons/airburst_rocket_launcher.svg b/public/icons/airburst_rocket_launcher.svg
index 2056ef4..ec0d381 100644
--- a/public/icons/airburst_rocket_launcher.svg
+++ b/public/icons/airburst_rocket_launcher.svg
@@ -1 +1,15 @@
-
\ No newline at end of file
+
diff --git a/public/icons/anti_materiel_rifle.svg b/public/icons/anti_materiel_rifle.svg
index 39ed438..4a292d7 100644
--- a/public/icons/anti_materiel_rifle.svg
+++ b/public/icons/anti_materiel_rifle.svg
@@ -1 +1,15 @@
-
\ No newline at end of file
+
diff --git a/public/icons/anti_personnel_minefield.svg b/public/icons/anti_personnel_minefield.svg
index b01f92c..786b792 100644
--- a/public/icons/anti_personnel_minefield.svg
+++ b/public/icons/anti_personnel_minefield.svg
@@ -1,5 +1,15 @@
-
\ No newline at end of file
+
diff --git a/public/icons/anti_tank_emplacement.svg b/public/icons/anti_tank_emplacement.svg
index c00222a..88170b4 100644
--- a/public/icons/anti_tank_emplacement.svg
+++ b/public/icons/anti_tank_emplacement.svg
@@ -1 +1,16 @@
-
\ No newline at end of file
+
diff --git a/public/icons/anti_tank_mines.svg b/public/icons/anti_tank_mines.svg
index 1843037..6d4ee78 100644
--- a/public/icons/anti_tank_mines.svg
+++ b/public/icons/anti_tank_mines.svg
@@ -1 +1,15 @@
-
+
diff --git a/public/icons/arc_thrower.svg b/public/icons/arc_thrower.svg
index 8ced9b2..a7b2a01 100644
--- a/public/icons/arc_thrower.svg
+++ b/public/icons/arc_thrower.svg
@@ -1,4 +1,15 @@
-
\ No newline at end of file
+
diff --git a/public/icons/autocannon.svg b/public/icons/autocannon.svg
index ec09df6..d78e48c 100644
--- a/public/icons/autocannon.svg
+++ b/public/icons/autocannon.svg
@@ -1,4 +1,15 @@
-
\ No newline at end of file
+
diff --git a/public/icons/autocannon_sentry.svg b/public/icons/autocannon_sentry.svg
index a01e7d9..938d29f 100644
--- a/public/icons/autocannon_sentry.svg
+++ b/public/icons/autocannon_sentry.svg
@@ -1,6 +1,15 @@
-
\ No newline at end of file
+
diff --git a/public/icons/ballistic_shield_backpack.svg b/public/icons/ballistic_shield_backpack.svg
index 0dcd29d..ed59d2a 100644
--- a/public/icons/ballistic_shield_backpack.svg
+++ b/public/icons/ballistic_shield_backpack.svg
@@ -1,8 +1,15 @@
-
\ No newline at end of file
+
diff --git a/public/icons/commando.svg b/public/icons/commando.svg
index 40117f4..1d89165 100644
--- a/public/icons/commando.svg
+++ b/public/icons/commando.svg
@@ -1,7 +1,15 @@
-
\ No newline at end of file
+
diff --git a/public/icons/directional_shield.svg b/public/icons/directional_shield.svg
index bbbe779..b8cb38e 100644
--- a/public/icons/directional_shield.svg
+++ b/public/icons/directional_shield.svg
@@ -1 +1,15 @@
-
\ No newline at end of file
+
diff --git a/public/icons/eagle_110mm_rocket_pods.svg b/public/icons/eagle_110mm_rocket_pods.svg
index d89638e..5913399 100644
--- a/public/icons/eagle_110mm_rocket_pods.svg
+++ b/public/icons/eagle_110mm_rocket_pods.svg
@@ -1,9 +1,15 @@
-
\ No newline at end of file
+
diff --git a/public/icons/eagle_500kg_bomb.svg b/public/icons/eagle_500kg_bomb.svg
index 1c5ebb2..5e40113 100644
--- a/public/icons/eagle_500kg_bomb.svg
+++ b/public/icons/eagle_500kg_bomb.svg
@@ -1,7 +1,15 @@
-
\ No newline at end of file
+
diff --git a/public/icons/eagle_airstrike.svg b/public/icons/eagle_airstrike.svg
index 185eb3c..fa8d2e1 100644
--- a/public/icons/eagle_airstrike.svg
+++ b/public/icons/eagle_airstrike.svg
@@ -1,7 +1,15 @@
-
\ No newline at end of file
+
diff --git a/public/icons/eagle_cluster_bomb.svg b/public/icons/eagle_cluster_bomb.svg
index c1d1cc6..23141d7 100644
--- a/public/icons/eagle_cluster_bomb.svg
+++ b/public/icons/eagle_cluster_bomb.svg
@@ -1,9 +1,20 @@
-