diff --git a/CHANGELOG.md b/CHANGELOG.md
index 80ba619..2cfbf26 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,28 @@
# Changelog – helldivers-trainer
+## [2.1.0] – 2026-03-31
+
+### Added
+- **Stratagem icons**: 65 SVG icons downloaded from community icon set, served as static files under `/icons/`; icon download script at `scripts/download-icons.js`
+- **Gold CSS filter** on all stratagem icons to match the game's yellow accent theme
+- **Session summary modal**: opens after stopping practice or finishing a drill/speedrun — shows score, completed count, accuracy, best time, and top 5 stratagems
+- **Queue preview strip**: shows next 3 upcoming stratagems (with icons) below the active stratagem
+- **Score popup animation**: floating `+N pts` text appears on every correct completion
+- **Shake animation on wrong input**: stratagem icon shakes on incorrect arrow key
+- **Icon complete pulse**: stratagem icon scales + brightens when sequence is completed correctly
+- **ELO rank icon** in post-match result header (matches rank tier badge)
+- **Inline icons** in history table rows, best-per-stratagem table, and leaderboard
+- `scripts/download-icons.js` — automated icon fetcher from GitHub community SVG repo
+
+### Changed
+- `index.html` fully rewritten: new elements for icon display, queue, score popup, session summary modal
+- `app.js` fully rewritten (~1100 lines): icon helpers, queue builder, popup system, session stats, improved match/lobby flows
+- `server.js` STRATAGEMS array: all 65 entries now have `icon` field (or `null` for missing ones)
+- `broadcastLobbyUpdate()` now sends `[{name, elo, rank}]` objects with CSS-safe rank labels
+- `challenge-received` WS event now includes challenger ELO for display in challenge modal
+
+---
+
## [2.0.0] – 2026-03-30
### Added
diff --git a/public/app.js b/public/app.js
index 6fb7c28..f14958d 100644
--- a/public/app.js
+++ b/public/app.js
@@ -1,44 +1,116 @@
'use strict';
+// ── Constants ─────────────────────────────────────────────────────────────────
+const RING_CIRCUMFERENCE = 219.9; // 2π × r(35)
+const ELO_RANKS = [
+ { label: 'PRIVATE', min: 0, icon: '⚡' },
+ { label: 'SERGEANT', min: 1100, icon: '★' },
+ { label: 'LIEUTENANT', min: 1300, icon: '☆' },
+ { label: 'CAPTAIN', min: 1500, icon: '⚔' },
+ { label: 'GENERAL', min: 1700, icon: '🏆' },
+];
+
+function eloRankFor(elo) {
+ for (let i = ELO_RANKS.length - 1; i >= 0; i--) {
+ if (elo >= ELO_RANKS[i].min) return ELO_RANKS[i];
+ }
+ return ELO_RANKS[0];
+}
+
// ── State ─────────────────────────────────────────────────────────────────────
const state = {
- user: null, // { user, role, mustChange }
+ user: null,
currentView: 'login',
stratagems: [],
+ settings: {
+ timerDuration: 30, // 15 | 30 | 45
+ difficulty: 'normal', // 'easy' | 'normal' | 'hard'
+ },
+
practice: {
- active: false,
- current: null,
- progress: 0,
- timeLeft: 30,
- timerHandle: null,
- startTime: null,
- score: 0,
- streak: 0,
- selectedCats: new Set(), // empty = all categories
- dailyTarget: null, // set when using daily challenge shortcut
+ active: false,
+ mode: 'timed',
+ current: null,
+ queue: [], // upcoming stratagems (for queue preview)
+ progress: 0,
+ timeLeft: 30,
+ timerHandle: null,
+ startTime: null,
+ score: 0,
+ streak: 0,
+ selectedCats: new Set(),
+ dailyTarget: null,
+
+ // Endless mode
+ lives: 3,
+
+ // Drill mode
+ drillPool: [],
+ drillCompleted: 0,
+ drillTotal: 0,
+
+ // Speedrun mode
+ speedrunStart: null,
+ speedrunPool: [],
+ speedrunElapsed: 0,
+
+ // Session stats
+ sessionStats: { completed: 0, missed: 0, bestTime: Infinity, stratagems: {} },
},
lobby: {
- online: [],
- incoming: [], // usernames who challenged me
+ online: [],
+ incoming: [],
+ pendingChallenge: null,
},
match: {
- roomId: null,
- opponent: null,
- matchScores: {},
- current: null,
- myProgress: 0,
- oppProgress: 0,
- roundActive: false,
+ roomId: null,
+ opponent: null,
+ matchScores: {},
+ current: null,
+ myProgress: 0,
+ oppProgress: 0,
+ roundActive: false,
+ roundHistory: [],
},
- ws: null,
+ leaderboard: { activeTab: 'practice' },
+
+ history: { page: 1, total: 0 },
+
+ ws: null,
wsReconnectTimer: null,
};
-// ── API helpers ───────────────────────────────────────────────────────────────
+// ── Settings ──────────────────────────────────────────────────────────────────
+function loadSettings() {
+ try {
+ const raw = localStorage.getItem('hd2-settings');
+ if (raw) {
+ const s = JSON.parse(raw);
+ if ([15, 30, 45].includes(s.timerDuration)) state.settings.timerDuration = s.timerDuration;
+ if (['easy', 'normal', 'hard'].includes(s.difficulty)) state.settings.difficulty = s.difficulty;
+ }
+ } catch { /* ignore */ }
+ applySettingsToUI();
+}
+
+function saveSettings() {
+ localStorage.setItem('hd2-settings', JSON.stringify(state.settings));
+}
+
+function applySettingsToUI() {
+ document.querySelectorAll('[data-setting="timer"]').forEach(btn => {
+ btn.classList.toggle('active', Number(btn.dataset.value) === state.settings.timerDuration);
+ });
+ document.querySelectorAll('[data-setting="difficulty"]').forEach(btn => {
+ btn.classList.toggle('active', btn.dataset.value === state.settings.difficulty);
+ });
+}
+
+// ── API ───────────────────────────────────────────────────────────────────────
async function api(method, endpoint, body) {
const opts = { method, headers: { 'Content-Type': 'application/json' } };
if (body !== undefined) opts.body = JSON.stringify(body);
@@ -50,52 +122,52 @@ async function api(method, endpoint, body) {
// ── View system ───────────────────────────────────────────────────────────────
function showView(name) {
- document.querySelectorAll('.view').forEach(v => v.classList.add('hidden'));
+ document.querySelectorAll('.view').forEach(v => {
+ v.classList.add('hidden');
+ v.classList.remove('view-fade-in');
+ });
const el = document.getElementById('view-' + name);
- if (el) el.classList.remove('hidden');
+ if (el) {
+ el.classList.remove('hidden');
+ requestAnimationFrame(() => el.classList.add('view-fade-in'));
+ }
state.currentView = name;
- // Highlight active nav button
document.querySelectorAll('.nav-btn').forEach(b => {
b.classList.toggle('active', b.dataset.view === name);
});
- // Stop practice timer when navigating away
if (name !== 'practice') stopPracticeTimer();
-
- // View-specific init
- if (name === 'dashboard') loadDashboard();
+ if (name === 'dashboard') loadDashboard();
if (name === 'leaderboard') loadLeaderboard();
- if (name === 'admin') loadAdmin();
- if (name === 'practice') initPracticeView();
- if (name === 'lobby') updateLobbyView();
+ if (name === 'admin') loadAdmin();
+ if (name === 'practice') initPracticeView();
+ if (name === 'lobby') updateLobbyView();
+ if (name === 'history') loadHistory();
}
-// ── Authentication ────────────────────────────────────────────────────────────
+// ── Auth ──────────────────────────────────────────────────────────────────────
async function checkAuth() {
try {
const data = await api('GET', '/me');
if (data.user) {
state.user = data;
- if (data.mustChange) {
- showView('change-password');
- } else {
- onLoggedIn();
- }
+ if (data.mustChange) showView('change-password');
+ else onLoggedIn();
} else {
showView('login');
}
- } catch {
- showView('login');
- }
+ } catch { showView('login'); }
}
async function onLoggedIn() {
document.getElementById('main-nav').classList.remove('hidden');
document.getElementById('nav-username').textContent = state.user.user;
document.getElementById('nav-admin').classList.toggle('hidden', state.user.role !== 'admin');
- // Stratagems are served via authenticated API – not as a public static file
+ document.getElementById('drawer-admin')?.classList.toggle('hidden', state.user.role !== 'admin');
state.stratagems = await api('GET', '/stratagems').catch(() => []);
+ populateCategoryFilter();
+ loadSettings();
connectWS();
showView('dashboard');
}
@@ -110,7 +182,20 @@ async function logout() {
showView('login');
}
-// Login form
+function populateCategoryFilter() {
+ const cats = [...new Set(state.stratagems.map(s => s.category))];
+ const sel = document.getElementById('history-filter-cat');
+ if (!sel) return;
+ // Remove old options except the first
+ while (sel.options.length > 1) sel.remove(1);
+ cats.forEach(cat => {
+ const opt = document.createElement('option');
+ opt.value = cat;
+ opt.textContent = cat;
+ sel.appendChild(opt);
+ });
+}
+
document.getElementById('login-form').addEventListener('submit', async (e) => {
e.preventDefault();
const el = document.getElementById('login-error');
@@ -127,14 +212,12 @@ document.getElementById('login-form').addEventListener('submit', async (e) => {
}
});
-// Change password form
document.getElementById('change-password-form').addEventListener('submit', async (e) => {
e.preventDefault();
const errEl = document.getElementById('cp-error');
const newPw = document.getElementById('cp-new').value;
const confPw = document.getElementById('cp-confirm').value;
errEl.classList.add('hidden');
-
if (newPw !== confPw) {
errEl.textContent = 'Passwords do not match';
errEl.classList.remove('hidden');
@@ -153,25 +236,41 @@ document.getElementById('change-password-form').addEventListener('submit', async
}
});
-// Nav buttons
document.querySelectorAll('.nav-btn[data-view]').forEach(btn => {
btn.addEventListener('click', () => showView(btn.dataset.view));
});
+// ── Hamburger nav ─────────────────────────────────────────────────────────────
+function openDrawer() {
+ document.getElementById('nav-drawer').classList.add('open');
+ document.getElementById('nav-overlay').classList.add('open');
+ document.getElementById('nav-hamburger').setAttribute('aria-expanded', 'true');
+}
+
+function closeDrawer() {
+ document.getElementById('nav-drawer').classList.remove('open');
+ document.getElementById('nav-overlay').classList.remove('open');
+ document.getElementById('nav-hamburger').setAttribute('aria-expanded', 'false');
+}
+
+document.getElementById('nav-hamburger').addEventListener('click', openDrawer);
+document.getElementById('nav-overlay').addEventListener('click', closeDrawer);
+document.querySelectorAll('.drawer-btn[data-view]').forEach(btn => {
+ btn.addEventListener('click', () => { showView(btn.dataset.view); closeDrawer(); });
+});
+document.getElementById('btn-logout-drawer')?.addEventListener('click', () => { closeDrawer(); logout(); });
+
// ── WebSocket ─────────────────────────────────────────────────────────────────
function connectWS() {
if (state.ws) return;
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
state.ws = new WebSocket(proto + '//' + location.host);
-
- state.ws.onopen = () => { clearTimeout(state.wsReconnectTimer); };
+ state.ws.onopen = () => clearTimeout(state.wsReconnectTimer);
state.ws.onmessage = (e) => { try { handleWSMessage(JSON.parse(e.data)); } catch {} };
state.ws.onerror = () => state.ws.close();
state.ws.onclose = () => {
state.ws = null;
- if (state.user) {
- state.wsReconnectTimer = setTimeout(connectWS, 3000);
- }
+ if (state.user) state.wsReconnectTimer = setTimeout(connectWS, 3000);
};
}
@@ -194,8 +293,7 @@ function handleWSMessage({ type, payload }) {
case 'challenge-received':
if (!state.lobby.incoming.includes(payload.from)) state.lobby.incoming.push(payload.from);
updateChallengeBadge();
- if (state.currentView === 'lobby') updateLobbyView();
- showToast(esc(payload.from) + ' challenges you! Go to 1v1 to respond.');
+ openChallengeModal(payload.from, payload.elo ?? '?');
break;
case 'challenge-declined':
@@ -209,6 +307,8 @@ function handleWSMessage({ type, payload }) {
state.match.myProgress = 0;
state.match.oppProgress = 0;
state.match.roundActive = false;
+ state.match.roundHistory = [];
+ closeChallengeModal();
showView('match');
renderMatchWaiting();
break;
@@ -223,23 +323,28 @@ function handleWSMessage({ type, payload }) {
case 'input-result':
if (payload.userId === state.user.user) {
- state.match.myProgress = payload.progress;
+ state.match.myProgress = payload.progress;
updateMyArrows(payload.correct);
} else {
state.match.oppProgress = payload.progress;
- updateOppArrows(payload.correct);
+ updateOppArrows();
}
break;
case 'round-complete':
state.match.roundActive = false;
state.match.matchScores = payload.matchScores;
+ state.match.roundHistory.push({ stratagem: state.match.current?.name, winner: payload.winner });
renderRoundResult(payload.winner);
break;
case 'match-end':
state.match.matchScores = payload.matchScores;
- renderMatchEnd(payload.winner);
+ openMatchResultModal({
+ winner: payload.winner,
+ eloChanges: payload.eloChanges,
+ roundHistory: payload.roundHistory || state.match.roundHistory,
+ });
break;
case 'opponent-left':
@@ -251,56 +356,79 @@ function handleWSMessage({ type, payload }) {
// ── Dashboard ─────────────────────────────────────────────────────────────────
async function loadDashboard() {
+ setText('dash-hero-name', state.user?.user || '—');
try {
const data = await api('GET', '/dashboard');
renderDashboard(data);
- } catch {
- /* silently ignore – dashboard is cosmetic */
- }
+ } catch { /* ignore */ }
}
-function renderDashboard({ stats, rank, online, recent, daily }) {
- setText('dash-total-score', stats.totalScore || 0);
- setText('dash-rank', rank ? '#' + rank.position : 'Unranked');
- setText('dash-sessions', stats.sessions || 0);
+function renderDashboard({ stats, rank, elo, eloRank: rankLabel, online, recent, daily }) {
+ const r = eloRankFor(elo || 1000);
+ setText('dash-hero-name', state.user.user);
+ setText('dash-rank-label', rankLabel || r.label);
+ setText('dash-elo', elo || 1000);
+ setText('dash-rank-icon', r.icon);
- const wr = (stats.matches > 0) ? Math.round((stats.wins / stats.matches) * 100) + '%' : '—';
+ setText('dash-total-score', stats.totalScore || 0);
+ setText('dash-rank', rank ? '#' + rank.position : 'Unranked');
+ setText('dash-sessions', stats.sessions || 0);
+ const wr = stats.matches > 0 ? Math.round(((stats.wins || 0) / stats.matches) * 100) + '%' : '—';
setText('dash-win-rate', wr);
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');
- // Store for the "Practice this" shortcut
state.practice.dailyTarget = daily.stratagem.name;
+ renderDailySequencePreview(daily.stratagem.sequence);
+ setIcon(document.getElementById('dash-daily-icon'), daily.stratagem.icon);
}
const tbody = document.getElementById('dash-recent');
- if (recent.length === 0) {
- tbody.innerHTML = '
No other Helldivers online. Waiting for reinforcements...
';
- } else {
- el.innerHTML = others.map(u =>
- `
-
${esc(u.username)}
-
${u.role}
- ${u.mustChange ? '
temp pw' : ''}
+
${esc(u.username)}
+
${u.role}
+ ${u.mustChange ? '
temp pw' : ''}
${u.username !== state.user.user
? `
`
: ''}
@@ -704,7 +1462,6 @@ async function createUser() {
const pwEl = document.getElementById('new-pw-display');
errEl.classList.add('hidden');
pwEl.classList.add('hidden');
-
if (!username) return;
try {
const result = await api('POST', '/users', { username, role });
@@ -728,11 +1485,10 @@ async function deleteUser(username) {
}
}
-// ── Event delegation (replaces inline onclick for user-data actions) ──────────
+// ── Event delegation ──────────────────────────────────────────────────────────
document.addEventListener('click', (e) => {
const btn = e.target.closest('[data-action]');
if (!btn) return;
-
const action = btn.dataset.action;
const user = btn.dataset.user;
const cat = btn.dataset.cat;
@@ -742,21 +1498,49 @@ document.addEventListener('click', (e) => {
if (action === 'decline' && user) declineChallenge(user);
if (action === 'delete-user' && user) deleteUser(user);
if (action === 'toggle-cat' && cat) toggleCategory(cat);
+
+ // Settings options
+ const settingBtn = e.target.closest('[data-setting]');
+ if (settingBtn) {
+ const setting = settingBtn.dataset.setting;
+ const value = settingBtn.dataset.value;
+ if (setting === 'timer') state.settings.timerDuration = Number(value);
+ else if (setting === 'difficulty') state.settings.difficulty = value;
+ saveSettings();
+ applySettingsToUI();
+ updateModeLabel();
+ }
});
-// ── Keyboard input ────────────────────────────────────────────────────────────
+// ── Keyboard ──────────────────────────────────────────────────────────────────
document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') {
+ closeSettingsModal();
+ closeChallengeModal();
+ closeMatchResultModal();
+ if (document.getElementById('modal-session-summary')?.classList.contains('hidden') === false) {
+ closeSessionSummary();
+ }
+ if (state.currentView === 'practice' && state.practice.active) {
+ stopPracticeUI();
+ }
+ return;
+ }
+ if (e.key === 'Enter' && state.currentView === 'practice' && !state.practice.active) {
+ startPractice();
+ return;
+ }
+
const MAP = { ArrowUp: 'up', ArrowDown: 'down', ArrowLeft: 'left', ArrowRight: 'right' };
const dir = MAP[e.key];
if (!dir) return;
if (state.currentView === 'practice' || state.currentView === 'match') {
- e.preventDefault(); // prevent page scroll
+ e.preventDefault();
dpadInput(dir);
}
});
-// Called by on-screen D-pad and keyboard handler
function dpadInput(dir) {
if (state.currentView === 'practice') handlePracticeInput(dir);
if (state.currentView === 'match') handleMatchInput(dir);
@@ -779,23 +1563,32 @@ function setText(id, value) {
function showToast(msg) {
const container = document.getElementById('toast-container');
- // Limit simultaneous toasts to avoid stacking
if (container.children.length >= 3) container.firstChild?.remove();
-
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = msg;
container.appendChild(toast);
-
requestAnimationFrame(() => requestAnimationFrame(() => toast.classList.add('show')));
-
setTimeout(() => {
toast.classList.remove('show');
toast.addEventListener('transitionend', () => toast.remove(), { once: true });
}, 3200);
}
-// ── Static button bindings (replaces inline onclick – blocked by CSP script-src-attr) ──
+function showScorePopup(text) {
+ const el = document.getElementById('score-popup');
+ if (!el) return;
+ el.textContent = text;
+ el.classList.remove('show', 'hidden');
+ requestAnimationFrame(() => {
+ el.classList.add('show');
+ el.addEventListener('animationend', () => {
+ el.classList.remove('show');
+ }, { once: true });
+ });
+}
+
+// ── Static button bindings ───────────────────────────────────────────────────
document.getElementById('btn-logout') ?.addEventListener('click', logout);
document.getElementById('btn-daily-challenge') ?.addEventListener('click', startDailyChallenge);
document.getElementById('btn-start-practice') ?.addEventListener('click', startPractice);
@@ -804,7 +1597,6 @@ document.getElementById('match-ready-btn') ?.addEventListener('click', setRe
document.getElementById('btn-leave-match') ?.addEventListener('click', leaveMatch);
document.getElementById('btn-create-user') ?.addEventListener('click', createUser);
-// D-pad: practice and match both use data-dir buttons
document.getElementById('practice-dpad')?.addEventListener('click', (e) => {
const dir = e.target.closest('[data-dir]')?.dataset.dir;
if (dir) dpadInput(dir);
diff --git a/public/icons/_map.json b/public/icons/_map.json
new file mode 100644
index 0000000..ff23a0a
--- /dev/null
+++ b/public/icons/_map.json
@@ -0,0 +1,67 @@
+{
+ "Reinforce": "/icons/reinforce.svg",
+ "Resupply": "/icons/resupply.svg",
+ "SOS Beacon": "/icons/sos_beacon.svg",
+ "Hellbomb": "/icons/hellbomb.svg",
+ "SEAF Artillery": "/icons/seaf_artillery.svg",
+ "Upload Data": "/icons/upload_data.svg",
+ "Prospecting Drill": "/icons/prospecting_drill.svg",
+ "Orbital Illumination Flare": "/icons/orbital_illumination_flare.svg",
+ "Orbital Gatling Barrage": "/icons/orbital_gatling_barrage.svg",
+ "Orbital Airburst Strike": "/icons/orbital_airburst_strike.svg",
+ "Orbital 120MM HE Barrage": "/icons/orbital_120mm_he_barrage.svg",
+ "Orbital 380MM HE Barrage": "/icons/orbital_380mm_he_barrage.svg",
+ "Orbital Walking Barrage": "/icons/orbital_walking_barrage.svg",
+ "Orbital Laser": "/icons/orbital_laser.svg",
+ "Orbital Railcannon Strike": "/icons/orbital_railcannon_strike.svg",
+ "Orbital Precision Strike": "/icons/orbital_precision_strike.svg",
+ "Orbital Gas Strike": "/icons/orbital_gas_strike.svg",
+ "Orbital EMS Strike": "/icons/orbital_ems_strike.svg",
+ "Orbital Smoke Strike": "/icons/orbital_smoke_strike.svg",
+ "Tesla Tower": "/icons/tesla_tower.svg",
+ "Shield Generator Relay": "/icons/shield_generator_relay.svg",
+ "HMG Emplacement": "/icons/hmg_emplacement.svg",
+ "Eagle Strafing Run": "/icons/eagle_strafing_run.svg",
+ "Eagle Airstrike": "/icons/eagle_airstrike.svg",
+ "Eagle Cluster Bomb": "/icons/eagle_cluster_bomb.svg",
+ "Eagle Napalm Airstrike": "/icons/eagle_napalm_airstrike.svg",
+ "LIFT-850 Jump Pack": "/icons/lift_850_jump_pack.svg",
+ "Eagle Smoke Strike": "/icons/eagle_smoke_strike.svg",
+ "Eagle 110MM Rocket Pods": "/icons/eagle_110mm_rocket_pods.svg",
+ "Eagle 500KG Bomb": "/icons/eagle_500kg_bomb.svg",
+ "Eagle Rearm": "/icons/eagle_rearm.svg",
+ "Machine Gun": "/icons/machine_gun.svg",
+ "Anti-Materiel Rifle": "/icons/anti_materiel_rifle.svg",
+ "Stalwart": "/icons/stalwart.svg",
+ "Expendable Anti-Tank": "/icons/expendable_anti_tank.svg",
+ "Recoilless Rifle": "/icons/recoilless_rifle.svg",
+ "Flamethrower": "/icons/flamethrower.svg",
+ "Autocannon": "/icons/autocannon.svg",
+ "Heavy Machine Gun": "/icons/heavy_machine_gun.svg",
+ "Airburst Rocket Launcher": "/icons/airburst_rocket_launcher.svg",
+ "Commando": "/icons/commando.svg",
+ "Railgun": "/icons/railgun.svg",
+ "Spear": "/icons/spear.svg",
+ "Quasar Cannon": "/icons/quasar_cannon.svg",
+ "Arc Thrower": "/icons/arc_thrower.svg",
+ "Laser Cannon": "/icons/laser_cannon.svg",
+ "Grenade Launcher": "/icons/grenade_launcher.svg",
+ "Supply Pack": "/icons/supply_pack.svg",
+ "Guard Dog Rover": "/icons/guard_dog_rover.svg",
+ "Ballistic Shield Backpack": "/icons/ballistic_shield_backpack.svg",
+ "Shield Generator Pack": "/icons/shield_generator_pack.svg",
+ "Anti-Personnel Minefield": "/icons/anti_personnel_minefield.svg",
+ "Incendiary Mines": "/icons/incendiary_mines.svg",
+ "Anti-Tank Mines": "/icons/anti_tank_mines.svg",
+ "Machine Gun Sentry": "/icons/machine_gun_sentry.svg",
+ "Gatling Sentry": "/icons/gatling_sentry.svg",
+ "Mortar Sentry": "/icons/mortar_sentry.svg",
+ "Guard Dog": "/icons/guard_dog.svg",
+ "Autocannon Sentry": "/icons/autocannon_sentry.svg",
+ "Rocket Sentry": "/icons/rocket_sentry.svg",
+ "EMS Mortar Sentry": "/icons/ems_mortar_sentry.svg",
+ "Patriot Exosuit": "/icons/patriot_exosuit.svg",
+ "Emancipator Exosuit": "/icons/emancipator_exosuit.svg",
+ "Directional Shield": "/icons/directional_shield.svg",
+ "Anti-Tank Emplacement": "/icons/anti_tank_emplacement.svg"
+}
\ No newline at end of file
diff --git a/public/icons/airburst_rocket_launcher.svg b/public/icons/airburst_rocket_launcher.svg
new file mode 100644
index 0000000..2056ef4
--- /dev/null
+++ b/public/icons/airburst_rocket_launcher.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/icons/anti_materiel_rifle.svg b/public/icons/anti_materiel_rifle.svg
new file mode 100644
index 0000000..39ed438
--- /dev/null
+++ b/public/icons/anti_materiel_rifle.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/icons/anti_personnel_minefield.svg b/public/icons/anti_personnel_minefield.svg
new file mode 100644
index 0000000..b01f92c
--- /dev/null
+++ b/public/icons/anti_personnel_minefield.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/public/icons/anti_tank_emplacement.svg b/public/icons/anti_tank_emplacement.svg
new file mode 100644
index 0000000..c00222a
--- /dev/null
+++ b/public/icons/anti_tank_emplacement.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/icons/anti_tank_mines.svg b/public/icons/anti_tank_mines.svg
new file mode 100644
index 0000000..1843037
--- /dev/null
+++ b/public/icons/anti_tank_mines.svg
@@ -0,0 +1 @@
+
diff --git a/public/icons/arc_thrower.svg b/public/icons/arc_thrower.svg
new file mode 100644
index 0000000..8ced9b2
--- /dev/null
+++ b/public/icons/arc_thrower.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/public/icons/autocannon.svg b/public/icons/autocannon.svg
new file mode 100644
index 0000000..ec09df6
--- /dev/null
+++ b/public/icons/autocannon.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/public/icons/autocannon_sentry.svg b/public/icons/autocannon_sentry.svg
new file mode 100644
index 0000000..a01e7d9
--- /dev/null
+++ b/public/icons/autocannon_sentry.svg
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/public/icons/ballistic_shield_backpack.svg b/public/icons/ballistic_shield_backpack.svg
new file mode 100644
index 0000000..0dcd29d
--- /dev/null
+++ b/public/icons/ballistic_shield_backpack.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/public/icons/commando.svg b/public/icons/commando.svg
new file mode 100644
index 0000000..40117f4
--- /dev/null
+++ b/public/icons/commando.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/public/icons/directional_shield.svg b/public/icons/directional_shield.svg
new file mode 100644
index 0000000..bbbe779
--- /dev/null
+++ b/public/icons/directional_shield.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/icons/eagle_110mm_rocket_pods.svg b/public/icons/eagle_110mm_rocket_pods.svg
new file mode 100644
index 0000000..d89638e
--- /dev/null
+++ b/public/icons/eagle_110mm_rocket_pods.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/public/icons/eagle_500kg_bomb.svg b/public/icons/eagle_500kg_bomb.svg
new file mode 100644
index 0000000..1c5ebb2
--- /dev/null
+++ b/public/icons/eagle_500kg_bomb.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/public/icons/eagle_airstrike.svg b/public/icons/eagle_airstrike.svg
new file mode 100644
index 0000000..185eb3c
--- /dev/null
+++ b/public/icons/eagle_airstrike.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/public/icons/eagle_cluster_bomb.svg b/public/icons/eagle_cluster_bomb.svg
new file mode 100644
index 0000000..c1d1cc6
--- /dev/null
+++ b/public/icons/eagle_cluster_bomb.svg
@@ -0,0 +1,9 @@
+
diff --git a/public/icons/eagle_napalm_airstrike.svg b/public/icons/eagle_napalm_airstrike.svg
new file mode 100644
index 0000000..ad9b890
--- /dev/null
+++ b/public/icons/eagle_napalm_airstrike.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/public/icons/eagle_rearm.svg b/public/icons/eagle_rearm.svg
new file mode 100644
index 0000000..a3551ee
--- /dev/null
+++ b/public/icons/eagle_rearm.svg
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/public/icons/eagle_smoke_strike.svg b/public/icons/eagle_smoke_strike.svg
new file mode 100644
index 0000000..3780320
--- /dev/null
+++ b/public/icons/eagle_smoke_strike.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/public/icons/eagle_strafing_run.svg b/public/icons/eagle_strafing_run.svg
new file mode 100644
index 0000000..e94ec15
--- /dev/null
+++ b/public/icons/eagle_strafing_run.svg
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/public/icons/emancipator_exosuit.svg b/public/icons/emancipator_exosuit.svg
new file mode 100644
index 0000000..41fb9a2
--- /dev/null
+++ b/public/icons/emancipator_exosuit.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/icons/ems_mortar_sentry.svg b/public/icons/ems_mortar_sentry.svg
new file mode 100644
index 0000000..291fee1
--- /dev/null
+++ b/public/icons/ems_mortar_sentry.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/public/icons/expendable_anti_tank.svg b/public/icons/expendable_anti_tank.svg
new file mode 100644
index 0000000..c186324
--- /dev/null
+++ b/public/icons/expendable_anti_tank.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/public/icons/flamethrower.svg b/public/icons/flamethrower.svg
new file mode 100644
index 0000000..c27eb34
--- /dev/null
+++ b/public/icons/flamethrower.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/public/icons/gatling_sentry.svg b/public/icons/gatling_sentry.svg
new file mode 100644
index 0000000..3bcefdf
--- /dev/null
+++ b/public/icons/gatling_sentry.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/public/icons/grenade_launcher.svg b/public/icons/grenade_launcher.svg
new file mode 100644
index 0000000..555fc5f
--- /dev/null
+++ b/public/icons/grenade_launcher.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/icons/guard_dog.svg b/public/icons/guard_dog.svg
new file mode 100644
index 0000000..2d1626c
--- /dev/null
+++ b/public/icons/guard_dog.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/public/icons/guard_dog_rover.svg b/public/icons/guard_dog_rover.svg
new file mode 100644
index 0000000..d0dec92
--- /dev/null
+++ b/public/icons/guard_dog_rover.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/public/icons/heavy_machine_gun.svg b/public/icons/heavy_machine_gun.svg
new file mode 100644
index 0000000..f3fac24
--- /dev/null
+++ b/public/icons/heavy_machine_gun.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/public/icons/hellbomb.svg b/public/icons/hellbomb.svg
new file mode 100644
index 0000000..fe9c084
--- /dev/null
+++ b/public/icons/hellbomb.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/public/icons/hmg_emplacement.svg b/public/icons/hmg_emplacement.svg
new file mode 100644
index 0000000..d5e02ef
--- /dev/null
+++ b/public/icons/hmg_emplacement.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/public/icons/incendiary_mines.svg b/public/icons/incendiary_mines.svg
new file mode 100644
index 0000000..5fbd866
--- /dev/null
+++ b/public/icons/incendiary_mines.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/public/icons/laser_cannon.svg b/public/icons/laser_cannon.svg
new file mode 100644
index 0000000..4091779
--- /dev/null
+++ b/public/icons/laser_cannon.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/public/icons/lift_850_jump_pack.svg b/public/icons/lift_850_jump_pack.svg
new file mode 100644
index 0000000..92eacef
--- /dev/null
+++ b/public/icons/lift_850_jump_pack.svg
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/public/icons/machine_gun.svg b/public/icons/machine_gun.svg
new file mode 100644
index 0000000..b1be61a
--- /dev/null
+++ b/public/icons/machine_gun.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/public/icons/machine_gun_sentry.svg b/public/icons/machine_gun_sentry.svg
new file mode 100644
index 0000000..5c8bfa8
--- /dev/null
+++ b/public/icons/machine_gun_sentry.svg
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/public/icons/mortar_sentry.svg b/public/icons/mortar_sentry.svg
new file mode 100644
index 0000000..1293d06
--- /dev/null
+++ b/public/icons/mortar_sentry.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/public/icons/orbital_120mm_he_barrage.svg b/public/icons/orbital_120mm_he_barrage.svg
new file mode 100644
index 0000000..b0a51a3
--- /dev/null
+++ b/public/icons/orbital_120mm_he_barrage.svg
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/public/icons/orbital_380mm_he_barrage.svg b/public/icons/orbital_380mm_he_barrage.svg
new file mode 100644
index 0000000..7d4c540
--- /dev/null
+++ b/public/icons/orbital_380mm_he_barrage.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/public/icons/orbital_airburst_strike.svg b/public/icons/orbital_airburst_strike.svg
new file mode 100644
index 0000000..170ecff
--- /dev/null
+++ b/public/icons/orbital_airburst_strike.svg
@@ -0,0 +1,17 @@
+
\ No newline at end of file
diff --git a/public/icons/orbital_ems_strike.svg b/public/icons/orbital_ems_strike.svg
new file mode 100644
index 0000000..595b60d
--- /dev/null
+++ b/public/icons/orbital_ems_strike.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/public/icons/orbital_gas_strike.svg b/public/icons/orbital_gas_strike.svg
new file mode 100644
index 0000000..ffc7ac0
--- /dev/null
+++ b/public/icons/orbital_gas_strike.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/public/icons/orbital_gatling_barrage.svg b/public/icons/orbital_gatling_barrage.svg
new file mode 100644
index 0000000..792c237
--- /dev/null
+++ b/public/icons/orbital_gatling_barrage.svg
@@ -0,0 +1,13 @@
+
\ No newline at end of file
diff --git a/public/icons/orbital_illumination_flare.svg b/public/icons/orbital_illumination_flare.svg
new file mode 100644
index 0000000..47b8941
--- /dev/null
+++ b/public/icons/orbital_illumination_flare.svg
@@ -0,0 +1,15 @@
+
\ No newline at end of file
diff --git a/public/icons/orbital_laser.svg b/public/icons/orbital_laser.svg
new file mode 100644
index 0000000..40a43d2
--- /dev/null
+++ b/public/icons/orbital_laser.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/public/icons/orbital_precision_strike.svg b/public/icons/orbital_precision_strike.svg
new file mode 100644
index 0000000..948ea3a
--- /dev/null
+++ b/public/icons/orbital_precision_strike.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/public/icons/orbital_railcannon_strike.svg b/public/icons/orbital_railcannon_strike.svg
new file mode 100644
index 0000000..55b45dc
--- /dev/null
+++ b/public/icons/orbital_railcannon_strike.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/public/icons/orbital_smoke_strike.svg b/public/icons/orbital_smoke_strike.svg
new file mode 100644
index 0000000..f55b6df
--- /dev/null
+++ b/public/icons/orbital_smoke_strike.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/public/icons/orbital_walking_barrage.svg b/public/icons/orbital_walking_barrage.svg
new file mode 100644
index 0000000..c49ebfe
--- /dev/null
+++ b/public/icons/orbital_walking_barrage.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/public/icons/patriot_exosuit.svg b/public/icons/patriot_exosuit.svg
new file mode 100644
index 0000000..7c7562e
--- /dev/null
+++ b/public/icons/patriot_exosuit.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/public/icons/prospecting_drill.svg b/public/icons/prospecting_drill.svg
new file mode 100644
index 0000000..e0a4d92
--- /dev/null
+++ b/public/icons/prospecting_drill.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/public/icons/quasar_cannon.svg b/public/icons/quasar_cannon.svg
new file mode 100644
index 0000000..5dc4cf8
--- /dev/null
+++ b/public/icons/quasar_cannon.svg
@@ -0,0 +1,8 @@
+
diff --git a/public/icons/railgun.svg b/public/icons/railgun.svg
new file mode 100644
index 0000000..61d0569
--- /dev/null
+++ b/public/icons/railgun.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/public/icons/recoilless_rifle.svg b/public/icons/recoilless_rifle.svg
new file mode 100644
index 0000000..62f0c22
--- /dev/null
+++ b/public/icons/recoilless_rifle.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/public/icons/reinforce.svg b/public/icons/reinforce.svg
new file mode 100644
index 0000000..8c90ed5
--- /dev/null
+++ b/public/icons/reinforce.svg
@@ -0,0 +1,4 @@
+
diff --git a/public/icons/resupply.svg b/public/icons/resupply.svg
new file mode 100644
index 0000000..31f543e
--- /dev/null
+++ b/public/icons/resupply.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/public/icons/rocket_sentry.svg b/public/icons/rocket_sentry.svg
new file mode 100644
index 0000000..31df2a3
--- /dev/null
+++ b/public/icons/rocket_sentry.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/public/icons/seaf_artillery.svg b/public/icons/seaf_artillery.svg
new file mode 100644
index 0000000..9fa7308
--- /dev/null
+++ b/public/icons/seaf_artillery.svg
@@ -0,0 +1,4 @@
+
diff --git a/public/icons/shield_generator_pack.svg b/public/icons/shield_generator_pack.svg
new file mode 100644
index 0000000..e59f50b
--- /dev/null
+++ b/public/icons/shield_generator_pack.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/public/icons/shield_generator_relay.svg b/public/icons/shield_generator_relay.svg
new file mode 100644
index 0000000..dd2ac16
--- /dev/null
+++ b/public/icons/shield_generator_relay.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/public/icons/sos_beacon.svg b/public/icons/sos_beacon.svg
new file mode 100644
index 0000000..0ed2911
--- /dev/null
+++ b/public/icons/sos_beacon.svg
@@ -0,0 +1,4 @@
+
diff --git a/public/icons/spear.svg b/public/icons/spear.svg
new file mode 100644
index 0000000..625f1e8
--- /dev/null
+++ b/public/icons/spear.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/public/icons/stalwart.svg b/public/icons/stalwart.svg
new file mode 100644
index 0000000..9f501c3
--- /dev/null
+++ b/public/icons/stalwart.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/icons/supply_pack.svg b/public/icons/supply_pack.svg
new file mode 100644
index 0000000..6d8640c
--- /dev/null
+++ b/public/icons/supply_pack.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/public/icons/tesla_tower.svg b/public/icons/tesla_tower.svg
new file mode 100644
index 0000000..38abc5d
--- /dev/null
+++ b/public/icons/tesla_tower.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/public/icons/upload_data.svg b/public/icons/upload_data.svg
new file mode 100644
index 0000000..67498e2
--- /dev/null
+++ b/public/icons/upload_data.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index 4f97876..0a0cfab 100644
--- a/public/index.html
+++ b/public/index.html
@@ -13,6 +13,7 @@