feat: stratagem icons, session summary, queue preview, UX polish

- Download 65 SVG icons from community repo (scripts/download-icons.js)
- Gold CSS filter on all icons to match game theme
- Session summary modal with score/accuracy/top stratagems
- Queue preview strip (next 3 stratagems with icons)
- Score popup animation, icon shake on wrong input
- Icons in history, leaderboard, and best-per-stratagem tables
- server.js: icon fields on all stratagems, ELO in lobby-update WS events
This commit is contained in:
Jeremy Brandenburger
2026-03-31 08:48:56 +02:00
parent 0d971745a6
commit 2d27d9fe4d
72 changed files with 2280 additions and 372 deletions
+316 -75
View File
@@ -13,6 +13,7 @@
<!-- ── Navigation ─────────────────────────────────────────────── -->
<nav id="main-nav" class="hidden">
<button id="nav-hamburger" aria-label="Open menu" aria-expanded="false">&#9776;</button>
<div class="nav-brand">
<span class="nav-logo"></span>
<span>HELLDIVERS 2</span>
@@ -20,6 +21,7 @@
<div class="nav-links">
<button class="nav-btn" data-view="dashboard">Dashboard</button>
<button class="nav-btn" data-view="practice">Training</button>
<button class="nav-btn" data-view="history">History</button>
<button class="nav-btn" data-view="lobby">1v1</button>
<button class="nav-btn" data-view="leaderboard">Highscores</button>
<button class="nav-btn nav-btn-admin hidden" id="nav-admin" data-view="admin">Admin</button>
@@ -30,14 +32,32 @@
</div>
</nav>
<!-- ── Mobile drawer ──────────────────────────────────────────── -->
<div id="nav-overlay"></div>
<nav id="nav-drawer" role="navigation" aria-label="Mobile navigation">
<div class="drawer-header">⚡ HELLDIVERS 2</div>
<button class="drawer-btn" data-view="dashboard">Dashboard</button>
<button class="drawer-btn" data-view="practice">Training</button>
<button class="drawer-btn" data-view="history">History</button>
<button class="drawer-btn" data-view="lobby">1v1 Arena</button>
<button class="drawer-btn" data-view="leaderboard">Highscores</button>
<button class="drawer-btn drawer-btn-admin hidden" id="drawer-admin" data-view="admin">Admin</button>
<div class="drawer-footer">
<button class="btn btn-muted btn-full btn-sm" id="btn-logout-drawer">Logout</button>
</div>
</nav>
<!-- Incoming challenge badge (shown anywhere) -->
<div id="challenge-badge" class="challenge-badge hidden"></div>
<!-- Score popup (practice) -->
<div id="score-popup" class="score-popup hidden"></div>
<!-- ── LOGIN ─────────────────────────────────────────────────── -->
<div id="view-login" class="view view-centered">
<div class="login-box">
<div class="login-header">
<div class="login-logo"></div>
<span class="login-logo"></span>
<h1>HELLDIVERS 2</h1>
<p class="login-sub">STRATAGEM TRAINER — SUPER EARTH AUTHORIZED</p>
</div>
@@ -60,7 +80,7 @@
<div id="view-change-password" class="view view-centered hidden">
<div class="login-box">
<div class="login-header">
<h2>CHANGE ACCESS CODE</h2>
<h2 style="font-family:var(--font-heading);font-size:1.4rem;letter-spacing:.1em;color:var(--accent)">CHANGE ACCESS CODE</h2>
<p class="login-sub">Temporary password must be changed before proceeding</p>
</div>
<form id="change-password-form" class="login-form">
@@ -84,126 +104,254 @@
<!-- ── DASHBOARD ─────────────────────────────────────────────── -->
<div id="view-dashboard" class="view hidden">
<div class="page-header">
<h2 class="page-title">COMMAND CENTER</h2>
<p class="page-sub">Welcome back, Helldiver. For Super Earth.</p>
</div>
<div class="dashboard-grid">
<!-- Stats card -->
<div class="card">
<h3 class="card-title">YOUR STATS</h3>
<div class="stat-grid">
<div class="stat-item">
<div class="stat-value" id="dash-total-score"></div>
<div class="stat-label">Total Score</div>
<!-- Hero card -->
<div class="card card-hero dashboard-hero">
<div class="hero-identity">
<div class="hero-rank-icon" id="dash-rank-icon"></div>
<div>
<div class="hero-name" id="dash-hero-name"></div>
<div class="hero-rank-name" id="dash-rank-label">PRIVATE</div>
</div>
<div class="stat-item">
<div class="stat-value accent" id="dash-rank"></div>
<div class="stat-label">Global Rank</div>
</div>
<div class="hero-elo">ELO <span id="dash-elo">1000</span></div>
<div class="hero-stats">
<div class="hero-stat-item">
<div class="hero-stat-val" id="dash-total-score"></div>
<div class="hero-stat-label">Total Score</div>
</div>
<div class="stat-item">
<div class="stat-value" id="dash-sessions"></div>
<div class="stat-label">Sessions</div>
<div class="hero-stat-item">
<div class="hero-stat-val" id="dash-rank"></div>
<div class="hero-stat-label">Global Rank</div>
</div>
<div class="stat-item">
<div class="stat-value" id="dash-win-rate"></div>
<div class="stat-label">Match Win Rate</div>
<div class="hero-stat-item">
<div class="hero-stat-val" id="dash-sessions"></div>
<div class="hero-stat-label">Sessions</div>
</div>
<div class="hero-stat-item">
<div class="hero-stat-val" id="dash-win-rate"></div>
<div class="hero-stat-label">Win Rate</div>
</div>
</div>
</div>
<!-- Daily challenge card -->
<div class="card card-accent">
<h3 class="card-title">⚡ DAILY CHALLENGE</h3>
<h3 class="card-title">Daily Challenge</h3>
<div class="daily-stratagem">
<div class="daily-icon-wrap">
<img id="dash-daily-icon" class="stratagem-icon-md" src="" alt="" style="display:none">
</div>
<div class="daily-name" id="dash-daily-name"></div>
<div class="daily-category" id="dash-daily-category"></div>
<div class="daily-best">
Best time: <span id="dash-daily-best"></span>
</div>
<button class="btn btn-accent" id="btn-daily-challenge">Practice this stratagem</button>
<div class="daily-sequence-preview" id="dash-daily-sequence"></div>
<div class="daily-best">Personal best: <span id="dash-daily-best"></span></div>
<button class="btn btn-accent btn-sm" id="btn-daily-challenge">⚡ Practice Now</button>
</div>
</div>
<!-- Online users card -->
<!-- Online players -->
<div class="card">
<h3 class="card-title">ONLINE HELLDIVERS</h3>
<h3 class="card-title">Online Helldivers</h3>
<div id="dash-online" class="online-list">
<span class="muted">Loading...</span>
</div>
</div>
<!-- Recent sessions card -->
<!-- Recent sessions -->
<div class="card">
<h3 class="card-title">RECENT SESSIONS</h3>
<h3 class="card-title">Recent Sessions</h3>
<table class="data-table">
<thead>
<tr><th>Stratagem</th><th>Score</th><th>Time</th></tr>
<tr><th>Stratagem</th><th>Mode</th><th>Score</th><th>Time</th></tr>
</thead>
<tbody id="dash-recent">
<tr><td colspan="3" class="muted">No sessions yet</td></tr>
<tr><td colspan="4" class="muted">No sessions yet</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ── PRACTICE ───────────────────────────────────────────────── -->
<div id="view-practice" class="view hidden">
<div class="page-header">
<h2 class="page-title">TRAINING PROTOCOL</h2>
<div class="practice-header">
<div>
<h2 class="page-title">TRAINING PROTOCOL</h2>
<p class="page-sub" id="practice-mode-label">Select a mode to begin</p>
</div>
<button class="btn btn-ghost btn-icon" id="btn-practice-settings" aria-label="Practice settings" data-tooltip="Settings"></button>
</div>
<!-- Mode selector -->
<div id="practice-mode-grid" class="mode-grid">
<div class="mode-card active" data-mode="timed">
<div class="mode-icon"></div>
<div class="mode-name">Timed</div>
<div class="mode-desc">30s per stratagem. Score by speed.</div>
</div>
<div class="mode-card" data-mode="endless">
<div class="mode-icon"></div>
<div class="mode-name">Endless</div>
<div class="mode-desc">3 lives. No timer. How far can you go?</div>
</div>
<div class="mode-card" data-mode="drill">
<div class="mode-icon">🎯</div>
<div class="mode-name">Category Drill</div>
<div class="mode-desc">Master one category completely.</div>
</div>
<div class="mode-card" data-mode="speedrun">
<div class="mode-icon"></div>
<div class="mode-name">Speed Run</div>
<div class="mode-desc">All stratagems, fastest total time.</div>
</div>
</div>
<!-- Category filters -->
<div class="category-row" id="practice-categories"></div>
<!-- Idle (start screen) -->
<div id="practice-idle" class="practice-idle">
<div class="idle-hint">Select categories above, then start training</div>
<button class="btn btn-accent btn-lg" id="btn-start-practice">⚡ START TRAINING</button>
<!-- Idle -->
<div id="practice-idle" class="practice-setup">
<div class="idle-hint" id="practice-idle-hint">Select categories above, then start training</div>
<div class="practice-start-row">
<button class="btn btn-accent btn-lg" id="btn-start-practice">⚡ START TRAINING</button>
</div>
</div>
<!-- Active training -->
<div id="practice-active" class="practice-active hidden">
<!-- Drill progress bar -->
<div id="drill-progress-wrap" class="drill-progress hidden">
<div class="drill-label">
<span>Category Progress</span>
<span id="drill-progress-text">0 / 0</span>
</div>
<div class="progress-bar">
<div class="progress-fill" id="drill-progress-fill" style="width:0%"></div>
</div>
</div>
<div class="stratagem-display card">
<!-- Stratagem icon -->
<div class="stratagem-icon-wrap">
<img id="practice-icon" class="stratagem-icon-lg" src="" alt="" style="display:none">
<div id="practice-icon-fallback" class="stratagem-icon-fallback"></div>
</div>
<div class="stratagem-category" id="practice-category"></div>
<div class="stratagem-name" id="practice-name"></div>
<div class="arrow-sequence" id="practice-sequence"></div>
<div class="practice-hint">Use Arrow Keys or D-Pad</div>
<div class="practice-hint">Arrow Keys or D-Pad · <kbd>Esc</kbd> to stop</div>
</div>
<!-- Upcoming queue -->
<div class="stratagem-queue" id="practice-queue"></div>
<div class="practice-hud">
<div class="hud-item">
<div class="hud-label">TIME</div>
<div class="timer" id="practice-timer">30</div>
<!-- Timer ring / lives / elapsed -->
<div class="hud-item" id="hud-timer-wrap">
<div class="hud-label" id="hud-timer-label">TIME</div>
<div class="timer-ring-wrap">
<svg class="timer-ring-svg" viewBox="0 0 80 80">
<circle class="timer-ring-bg" cx="40" cy="40" r="35"/>
<circle class="timer-ring-fill" id="timer-ring-fill" cx="40" cy="40" r="35"
stroke-dasharray="219.9" stroke-dashoffset="0"/>
</svg>
<div class="timer-ring-val" id="practice-timer">30</div>
</div>
</div>
<!-- Lives (endless mode) -->
<div class="hud-item hidden" id="hud-lives-wrap">
<div class="hud-label">LIVES</div>
<div class="lives-display" id="practice-lives">
<span class="life-icon"></span>
<span class="life-icon"></span>
<span class="life-icon"></span>
</div>
</div>
<div class="hud-item">
<div class="hud-label">SCORE</div>
<div class="hud-value" id="practice-score">0</div>
</div>
<div class="hud-item">
<div class="hud-item" id="hud-streak-item">
<div class="hud-label">STREAK</div>
<div class="hud-value accent" id="practice-streak">0</div>
<div class="combo-badge hidden" id="practice-combo">×1.0</div>
</div>
</div>
<!-- D-Pad (mobile) -->
<!-- D-Pad -->
<div class="dpad" id="practice-dpad">
<div class="dpad-row">
<button class="dpad-btn dpad-up" data-dir="up"></button>
<button class="dpad-btn" data-dir="up" aria-label="Arrow up"></button>
</div>
<div class="dpad-row">
<button class="dpad-btn dpad-left" data-dir="left"></button>
<button class="dpad-btn" data-dir="left" aria-label="Arrow left"></button>
<div class="dpad-center"></div>
<button class="dpad-btn dpad-right" data-dir="right"></button>
<button class="dpad-btn" data-dir="right" aria-label="Arrow right"></button>
</div>
<div class="dpad-row">
<button class="dpad-btn dpad-down" data-dir="down"></button>
<button class="dpad-btn" data-dir="down" aria-label="Arrow down"></button>
</div>
</div>
<button class="btn btn-muted" id="btn-stop-practice">Stop Training</button>
<button class="btn btn-muted btn-sm" id="btn-stop-practice">Stop Training</button>
</div>
</div>
<!-- ── HISTORY ────────────────────────────────────────────────── -->
<div id="view-history" class="view hidden">
<div class="page-header">
<h2 class="page-title">MISSION LOGS</h2>
<p class="page-sub">Your practice history and stratagem statistics</p>
</div>
<div class="card" style="margin-bottom:16px">
<h3 class="card-title">Score Progression</h3>
<div class="history-chart" id="history-chart">
<svg id="history-chart-svg" width="100%" height="100%" preserveAspectRatio="none"></svg>
</div>
</div>
<div class="card">
<div class="history-filters">
<select id="history-filter-mode">
<option value="">All Modes</option>
<option value="timed">Timed</option>
<option value="endless">Endless</option>
<option value="drill">Category Drill</option>
<option value="speedrun">Speed Run</option>
</select>
<select id="history-filter-cat">
<option value="">All Categories</option>
</select>
</div>
<h3 class="card-title">Session History</h3>
<table class="data-table">
<thead>
<tr><th>Stratagem</th><th>Category</th><th>Mode</th><th>Score</th><th>Time</th><th>Date</th></tr>
</thead>
<tbody id="history-table-body">
<tr><td colspan="6" class="muted">Loading...</td></tr>
</tbody>
</table>
<div class="pagination" id="history-pagination"></div>
</div>
<div class="card" style="margin-top:16px">
<h3 class="card-title">Best Times per Stratagem</h3>
<table class="data-table">
<thead>
<tr><th></th><th>Stratagem</th><th>Category</th><th>Best Time</th><th>Attempts</th></tr>
</thead>
<tbody id="best-per-stratagem-body">
<tr><td colspan="5" class="muted">No data yet</td></tr>
</tbody>
</table>
</div>
</div>
@@ -216,12 +364,20 @@
<div class="lobby-layout">
<div class="card">
<h3 class="card-title">ONLINE HELLDIVERS</h3>
<h3 class="card-title">Online Helldivers</h3>
<div id="lobby-players" class="player-list">
<p class="muted">Loading...</p>
<div class="lobby-empty">
<div class="lobby-empty-icon">📡</div>
<p>No other Helldivers online.<br>Waiting for reinforcements...</p>
</div>
</div>
</div>
<div class="card">
<h3 class="card-title">Incoming Challenges</h3>
<div id="lobby-challenges" class="challenge-list">
<p class="muted">No incoming challenges</p>
</div>
</div>
<div id="lobby-challenges" class="challenge-list"></div>
</div>
</div>
@@ -246,6 +402,10 @@
<!-- Round area -->
<div id="match-round-area" class="match-round-area hidden">
<!-- Stratagem icon in match -->
<div style="text-align:center;margin-bottom:8px">
<img id="match-icon" class="stratagem-icon-md" src="" alt="" style="display:none">
</div>
<div class="match-sequences">
<div class="match-seq-col">
<div class="match-seq-label">YOU</div>
@@ -256,18 +416,17 @@
<div class="arrow-sequence" id="match-opp-sequence"></div>
</div>
</div>
<!-- D-Pad (mobile) -->
<div class="dpad" id="match-dpad">
<div class="dpad-row">
<button class="dpad-btn dpad-up" data-dir="up"></button>
<button class="dpad-btn" data-dir="up" aria-label="Arrow up"></button>
</div>
<div class="dpad-row">
<button class="dpad-btn dpad-left" data-dir="left"></button>
<button class="dpad-btn" data-dir="left" aria-label="Arrow left"></button>
<div class="dpad-center"></div>
<button class="dpad-btn dpad-right" data-dir="right"></button>
<button class="dpad-btn" data-dir="right" aria-label="Arrow right"></button>
</div>
<div class="dpad-row">
<button class="dpad-btn dpad-down" data-dir="down"></button>
<button class="dpad-btn" data-dir="down" aria-label="Arrow down"></button>
</div>
</div>
</div>
@@ -285,19 +444,19 @@
<p class="page-sub">Top Helldivers ranked by total practice score</p>
</div>
<div class="leaderboard-tabs">
<button class="tab-btn active" data-tab="practice">Practice Score</button>
<button class="tab-btn" data-tab="elo">ELO Rating</button>
<button class="tab-btn" data-tab="speedrun">Speed Run</button>
</div>
<div class="card">
<table class="data-table leaderboard-table">
<thead>
<tr>
<th>#</th>
<th>Helldiver</th>
<th>Total Score</th>
<th>Sessions</th>
<th>Match W/Total</th>
</tr>
<table class="data-table">
<thead id="leaderboard-thead">
<tr><th>#</th><th>Helldiver</th><th>Rank</th><th>Total Score</th><th>Sessions</th><th>Match W/Total</th></tr>
</thead>
<tbody id="leaderboard-table-body">
<tr><td colspan="5" class="muted">Loading...</td></tr>
<tr><td colspan="6" class="muted">Loading...</td></tr>
</tbody>
</table>
</div>
@@ -308,11 +467,9 @@
<div class="page-header">
<h2 class="page-title">ADMIN PANEL</h2>
</div>
<div class="admin-layout">
<!-- Create user -->
<div class="card">
<h3 class="card-title">CREATE HELLDIVER</h3>
<h3 class="card-title">Create Helldiver</h3>
<div class="field">
<label for="new-username">Username</label>
<input id="new-username" type="text" placeholder="helldiver_name" pattern="[a-zA-Z0-9_-]{2,32}">
@@ -328,15 +485,99 @@
<button class="btn btn-accent" id="btn-create-user">Create User</button>
<div id="new-pw-display" class="pw-display hidden"></div>
</div>
<!-- User list -->
<div class="card">
<h3 class="card-title">ACTIVE HELLDIVERS</h3>
<h3 class="card-title">Active Helldivers</h3>
<div id="admin-users" class="admin-user-list">Loading...</div>
</div>
</div>
</div>
<!-- ── PRACTICE SETTINGS MODAL ──────────────────────────────── -->
<div id="modal-settings" class="modal-overlay hidden" role="dialog" aria-modal="true" aria-label="Practice Settings">
<div class="modal">
<div class="modal-header">
<span class="modal-title">⚙ Practice Settings</span>
<button class="modal-close" id="btn-settings-close" aria-label="Close"></button>
</div>
<div class="settings-section">
<span class="settings-label">Timer Duration (Timed mode)</span>
<div class="settings-options">
<button class="settings-option" data-setting="timer" data-value="15">15s</button>
<button class="settings-option active" data-setting="timer" data-value="30">30s</button>
<button class="settings-option" data-setting="timer" data-value="45">45s</button>
</div>
</div>
<div class="settings-section">
<span class="settings-label">Difficulty</span>
<div class="settings-options">
<button class="settings-option" data-setting="difficulty" data-value="easy">Easy<br><small>Shows category</small></button>
<button class="settings-option active" data-setting="difficulty" data-value="normal">Normal<br><small>Name only</small></button>
<button class="settings-option" data-setting="difficulty" data-value="hard">Hard<br><small>No name shown</small></button>
</div>
</div>
</div>
</div>
<!-- ── CHALLENGE MODAL ────────────────────────────────────────── -->
<div id="modal-challenge" class="modal-overlay hidden" role="dialog" aria-modal="true" aria-label="Incoming Challenge">
<div class="modal">
<div class="modal-header">
<span class="modal-title">⚔ Challenge Incoming!</span>
</div>
<p style="color:var(--text-dim);margin-bottom:8px">Challenger:</p>
<p style="font-family:var(--font-mono);font-size:1.2rem;color:var(--accent);margin-bottom:4px" id="modal-challenger-name"></p>
<p style="font-size:0.8rem;color:var(--text-muted);margin-bottom:20px">ELO: <span id="modal-challenger-elo"></span></p>
<div class="modal-actions">
<button class="btn btn-muted" id="btn-decline-challenge">Decline</button>
<button class="btn btn-accent" id="btn-accept-challenge">⚔ Accept</button>
</div>
</div>
</div>
<!-- ── POST-MATCH RESULT MODAL ───────────────────────────────── -->
<div id="modal-match-result" class="modal-overlay hidden" role="dialog" aria-modal="true" aria-label="Match Result">
<div class="modal modal-wide">
<div class="modal-header">
<span class="modal-title">Match Result</span>
</div>
<div style="text-align:center;padding:8px 0 16px">
<div class="result-winner" id="result-winner-text"></div>
</div>
<div class="elo-delta-row">
<span class="elo-delta-old" id="result-elo-old"></span>
<span class="elo-delta-arrow"></span>
<span class="elo-delta-new" id="result-elo-new"></span>
<span class="elo-delta-val" id="result-elo-delta"></span>
</div>
<div style="height:1px;background:var(--border);margin:12px 0"></div>
<div class="round-history" id="result-round-history"></div>
<div class="modal-actions">
<button class="btn btn-muted" id="btn-result-lobby">Back to Lobby</button>
<button class="btn btn-accent" id="btn-result-rematch">⚔ Rematch</button>
</div>
</div>
</div>
<!-- ── SESSION SUMMARY MODAL ─────────────────────────────────── -->
<div id="modal-session-summary" class="modal-overlay hidden" role="dialog" aria-modal="true" aria-label="Session Summary">
<div class="modal modal-wide">
<div class="modal-header">
<span class="modal-title">Session Complete</span>
</div>
<div class="summary-grid" id="summary-grid"></div>
<div style="height:1px;background:var(--border);margin:16px 0"></div>
<h4 style="font-family:var(--font-heading);font-size:0.85rem;letter-spacing:.06em;color:var(--text-muted);margin-bottom:8px">TOP STRATAGEMS</h4>
<div id="summary-top-stratagems"></div>
<div class="modal-actions">
<button class="btn btn-muted" id="btn-summary-dashboard">Dashboard</button>
<button class="btn btn-accent" id="btn-summary-restart">⚡ Play Again</button>
</div>
</div>
</div>
<!-- ── Danger vignette (≤5s) ──────────────────────────────────── -->
<div id="danger-vignette" class="danger-vignette hidden"></div>
<!-- Toast notifications -->
<div id="toast-container"></div>