Files
helldivers/public/index.html
T
Jeremy Brandenburger 2d27d9fe4d 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
2026-03-31 08:48:56 +02:00

587 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HELLDIVERS 2 Stratagem Trainer</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Exo+2:wght@400;600;700&family=Rajdhani:wght@600;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<!-- ── 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>
</div>
<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>
</div>
<div class="nav-user">
<span class="nav-username" id="nav-username"></span>
<button class="btn btn-muted btn-sm" id="btn-logout">Logout</button>
</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">
<span class="login-logo"></span>
<h1>HELLDIVERS 2</h1>
<p class="login-sub">STRATAGEM TRAINER — SUPER EARTH AUTHORIZED</p>
</div>
<form id="login-form" class="login-form" autocomplete="off">
<div class="field">
<label for="login-username">Helldiver ID</label>
<input id="login-username" type="text" placeholder="Username" autocomplete="username" required>
</div>
<div class="field">
<label for="login-password">Access Code</label>
<input id="login-password" type="password" placeholder="Password" autocomplete="current-password" required>
</div>
<p id="login-error" class="error hidden"></p>
<button type="submit" class="btn btn-accent btn-full">AUTHENTICATE</button>
</form>
</div>
</div>
<!-- ── CHANGE PASSWORD ───────────────────────────────────────── -->
<div id="view-change-password" class="view view-centered hidden">
<div class="login-box">
<div class="login-header">
<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">
<div class="field">
<label for="cp-old">Current Password</label>
<input id="cp-old" type="password" required autocomplete="current-password">
</div>
<div class="field">
<label for="cp-new">New Password (min 8 chars)</label>
<input id="cp-new" type="password" required minlength="8" autocomplete="new-password">
</div>
<div class="field">
<label for="cp-confirm">Confirm New Password</label>
<input id="cp-confirm" type="password" required autocomplete="new-password">
</div>
<p id="cp-error" class="error hidden"></p>
<button type="submit" class="btn btn-accent btn-full">SET NEW PASSWORD</button>
</form>
</div>
</div>
<!-- ── DASHBOARD ─────────────────────────────────────────────── -->
<div id="view-dashboard" class="view hidden">
<div class="dashboard-grid">
<!-- 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>
<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="hero-stat-item">
<div class="hero-stat-val" id="dash-rank"></div>
<div class="hero-stat-label">Global Rank</div>
</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>
<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-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 players -->
<div class="card">
<h3 class="card-title">Online Helldivers</h3>
<div id="dash-online" class="online-list">
<span class="muted">Loading...</span>
</div>
</div>
<!-- Recent sessions -->
<div class="card">
<h3 class="card-title">Recent Sessions</h3>
<table class="data-table">
<thead>
<tr><th>Stratagem</th><th>Mode</th><th>Score</th><th>Time</th></tr>
</thead>
<tbody id="dash-recent">
<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="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 -->
<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">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">
<!-- 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" 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 -->
<div class="dpad" id="practice-dpad">
<div class="dpad-row">
<button class="dpad-btn" data-dir="up" aria-label="Arrow up"></button>
</div>
<div class="dpad-row">
<button class="dpad-btn" data-dir="left" aria-label="Arrow left"></button>
<div class="dpad-center"></div>
<button class="dpad-btn" data-dir="right" aria-label="Arrow right"></button>
</div>
<div class="dpad-row">
<button class="dpad-btn" data-dir="down" aria-label="Arrow down"></button>
</div>
</div>
<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>
<!-- ── LOBBY ──────────────────────────────────────────────────── -->
<div id="view-lobby" class="view hidden">
<div class="page-header">
<h2 class="page-title">1v1 ARENA</h2>
<p class="page-sub">Challenge a fellow Helldiver to a stratagem duel</p>
</div>
<div class="lobby-layout">
<div class="card">
<h3 class="card-title">Online Helldivers</h3>
<div id="lobby-players" class="player-list">
<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>
</div>
<!-- ── MATCH ──────────────────────────────────────────────────── -->
<div id="view-match" class="view hidden">
<div class="match-header">
<div class="match-status-text" id="match-status">Waiting...</div>
<div class="match-category" id="match-category"></div>
</div>
<div class="match-scoreboard">
<div class="match-player me">
<div class="match-player-name" id="match-me-name"></div>
<div class="match-wins" id="match-me-wins">0</div>
</div>
<div class="match-vs">VS</div>
<div class="match-player opp">
<div class="match-player-name" id="match-opp-name"></div>
<div class="match-wins" id="match-opp-wins">0</div>
</div>
</div>
<!-- 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>
<div class="arrow-sequence" id="match-me-sequence"></div>
</div>
<div class="match-seq-col">
<div class="match-seq-label">OPPONENT</div>
<div class="arrow-sequence" id="match-opp-sequence"></div>
</div>
</div>
<div class="dpad" id="match-dpad">
<div class="dpad-row">
<button class="dpad-btn" data-dir="up" aria-label="Arrow up"></button>
</div>
<div class="dpad-row">
<button class="dpad-btn" data-dir="left" aria-label="Arrow left"></button>
<div class="dpad-center"></div>
<button class="dpad-btn" data-dir="right" aria-label="Arrow right"></button>
</div>
<div class="dpad-row">
<button class="dpad-btn" data-dir="down" aria-label="Arrow down"></button>
</div>
</div>
</div>
<div class="match-actions">
<button class="btn btn-accent hidden" id="match-ready-btn">READY</button>
<button class="btn btn-muted btn-sm" id="btn-leave-match">Leave Match</button>
</div>
</div>
<!-- ── LEADERBOARD ────────────────────────────────────────────── -->
<div id="view-leaderboard" class="view hidden">
<div class="page-header">
<h2 class="page-title">HALL OF HEROES</h2>
<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">
<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="6" class="muted">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
<!-- ── ADMIN ──────────────────────────────────────────────────── -->
<div id="view-admin" class="view hidden">
<div class="page-header">
<h2 class="page-title">ADMIN PANEL</h2>
</div>
<div class="admin-layout">
<div class="card">
<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}">
</div>
<div class="field">
<label for="new-role">Role</label>
<select id="new-role">
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</div>
<p id="admin-error" class="error hidden"></p>
<button class="btn btn-accent" id="btn-create-user">Create User</button>
<div id="new-pw-display" class="pw-display hidden"></div>
</div>
<div class="card">
<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>
<script src="app.js"></script>
</body>
</html>