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:
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* Download Helldivers 2 stratagem SVG icons from:
|
||||
* github.com/nvigneux/Helldivers-2-Stratagems-icons-svg
|
||||
*
|
||||
* Run once: node scripts/download-icons.js
|
||||
*/
|
||||
import https from 'https';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ICONS_DIR = path.join(__dirname, '..', 'public', 'icons');
|
||||
const BASE_URL = 'https://raw.githubusercontent.com/nvigneux/Helldivers-2-Stratagems-icons-svg/master';
|
||||
|
||||
// Map: our stratagem name → { repo category folder, repo filename (without .svg) }
|
||||
const ICON_MAP = [
|
||||
// ── General / PAC ────────────────────────────────────────────────────────
|
||||
['Reinforce', 'General Stratagems', 'Reinforce'],
|
||||
['Resupply', 'General Stratagems', 'Resupply'],
|
||||
['SOS Beacon', 'General Stratagems', 'SOS Beacon'],
|
||||
['Hellbomb', 'General Stratagems', 'Hellbomb'],
|
||||
['SEAF Artillery', 'General Stratagems', 'SEAF Artillery'],
|
||||
['Upload Data', 'General Stratagems', 'Upload Data'],
|
||||
['Prospecting Drill', 'General Stratagems', 'Prospecting Drill'],
|
||||
['Orbital Illumination Flare', 'General Stratagems', 'Orbital Illumination Flare'],
|
||||
// ── Orbital Cannons ───────────────────────────────────────────────────────
|
||||
['Orbital Gatling Barrage', 'Orbital Cannons', 'Orbital Gatling Barrage'],
|
||||
['Orbital Airburst Strike', 'Orbital Cannons', 'Orbital Airburst Strike'],
|
||||
['Orbital 120MM HE Barrage', 'Orbital Cannons', 'Orbital 120MM HE Barrage'],
|
||||
['Orbital 380MM HE Barrage', 'Orbital Cannons', 'Orbital 380MM HE Barrage'],
|
||||
['Orbital Walking Barrage', 'Orbital Cannons', 'Orbital Walking Barrage'],
|
||||
['Orbital Laser', 'Orbital Cannons', 'Orbital Laser'],
|
||||
['Orbital Railcannon Strike', 'Orbital Cannons', 'Orbital Railcannon Strike'],
|
||||
// ── Bridge ────────────────────────────────────────────────────────────────
|
||||
['Orbital Precision Strike', 'Bridge', 'Orbital Precision Strike'],
|
||||
['Orbital Gas Strike', 'Bridge', 'Orbital Gas Strike'],
|
||||
['Orbital EMS Strike', 'Bridge', 'Orbital EMS Strike'],
|
||||
['Orbital Smoke Strike', 'Bridge', 'Orbital Smoke Strike'],
|
||||
['Tesla Tower', 'Bridge', 'Tesla Tower'],
|
||||
['Shield Generator Relay', 'Bridge', 'Shield Generator Relay'],
|
||||
['HMG Emplacement', 'Bridge', 'HMG Emplacement'],
|
||||
// ── Hangar ────────────────────────────────────────────────────────────────
|
||||
['Eagle Strafing Run', 'Hangar', 'Eagle Strafing Run'],
|
||||
['Eagle Airstrike', 'Hangar', 'Eagle Airstrike'],
|
||||
['Eagle Cluster Bomb', 'Hangar', 'Eagle Cluster Bomb'],
|
||||
['Eagle Napalm Airstrike', 'Hangar', 'Eagle Napalm Airstrike'],
|
||||
['LIFT-850 Jump Pack', 'Hangar', 'Jump Pack'],
|
||||
['Eagle Smoke Strike', 'Hangar', 'Eagle Smoke Strike'],
|
||||
['Eagle 110MM Rocket Pods', 'Hangar', 'Eagle 110MM Rocket Pods'],
|
||||
['Eagle 500KG Bomb', 'Hangar', 'Eagle 500KG Bomb'],
|
||||
['Eagle Rearm', 'Hangar', 'Eagle Rearm'],
|
||||
// ── PAC – Support Weapons ─────────────────────────────────────────────────
|
||||
['Machine Gun', 'Patriotic Administration Center', 'Machine Gun'],
|
||||
['Anti-Materiel Rifle', 'Patriotic Administration Center', 'Anti-Materiel Rifle'],
|
||||
['Stalwart', 'Patriotic Administration Center', 'Stalwart'],
|
||||
['Expendable Anti-Tank', 'Patriotic Administration Center', 'Expendable Anti-Tank'],
|
||||
['Recoilless Rifle', 'Patriotic Administration Center', 'Recoilless Rifle'],
|
||||
['Flamethrower', 'Patriotic Administration Center', 'Flamethrower'],
|
||||
['Autocannon', 'Patriotic Administration Center', 'Autocannon'],
|
||||
['Heavy Machine Gun', 'Patriotic Administration Center', 'Heavy Machine Gun'],
|
||||
['Airburst Rocket Launcher', 'Patriotic Administration Center', 'Airburst Rocket Launcher'],
|
||||
['Commando', 'Patriotic Administration Center', 'Commando'],
|
||||
['Railgun', 'Patriotic Administration Center', 'Railgun'],
|
||||
['Spear', 'Patriotic Administration Center', 'Spear'],
|
||||
// ── Engineering Bay ───────────────────────────────────────────────────────
|
||||
['Quasar Cannon', 'Engineering Bay', 'Quasar Cannon'],
|
||||
['Arc Thrower', 'Engineering Bay', 'Arc Thrower'],
|
||||
['Laser Cannon', 'Engineering Bay', 'Laser Cannon'],
|
||||
['Grenade Launcher', 'Engineering Bay', 'Grenade Launcher'],
|
||||
['Supply Pack', 'Engineering Bay', 'Supply Pack'],
|
||||
['Guard Dog Rover', 'Engineering Bay', 'Guard Dog Rover'],
|
||||
['Ballistic Shield Backpack', 'Engineering Bay', 'Ballistic Shield Backpack'],
|
||||
['Shield Generator Pack', 'Engineering Bay', 'Shield Generator Pack'],
|
||||
['Anti-Personnel Minefield', 'Engineering Bay', 'Anti-Personnel Minefield'],
|
||||
['Incendiary Mines', 'Engineering Bay', 'Incendiary Mines'],
|
||||
['Anti-Tank Mines', 'Engineering Bay', 'Anti-Tank Mines'],
|
||||
// ── Robotics Workshop ─────────────────────────────────────────────────────
|
||||
['Machine Gun Sentry', 'Robotics Workshop', 'Machine Gun Sentry'],
|
||||
['Gatling Sentry', 'Robotics Workshop', 'Gatling Sentry'],
|
||||
['Mortar Sentry', 'Robotics Workshop', 'Mortar Sentry'],
|
||||
['Guard Dog', 'Robotics Workshop', 'Guard Dog'],
|
||||
['Autocannon Sentry', 'Robotics Workshop', 'Autocannon Sentry'],
|
||||
['Rocket Sentry', 'Robotics Workshop', 'Rocket Sentry'],
|
||||
['EMS Mortar Sentry', 'Robotics Workshop', 'EMS Mortar Sentry'],
|
||||
['Patriot Exosuit', 'Robotics Workshop', 'Patriot Exosuit'],
|
||||
['Emancipator Exosuit', 'Robotics Workshop', 'Emancipator Exosuit'],
|
||||
// ── Urban Legends / Defensive ─────────────────────────────────────────────
|
||||
['Directional Shield', 'Urban Legends', 'Directional Shield'],
|
||||
['Anti-Tank Emplacement', 'Urban Legends', 'Anti-Tank Emplacement'],
|
||||
];
|
||||
|
||||
function fetchURL(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.get(url, (res) => {
|
||||
if (res.statusCode === 301 || res.statusCode === 302) {
|
||||
return fetchURL(res.headers.location).then(resolve).catch(reject);
|
||||
}
|
||||
if (res.statusCode !== 200) {
|
||||
reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
||||
res.resume();
|
||||
return;
|
||||
}
|
||||
const chunks = [];
|
||||
res.on('data', d => chunks.push(d));
|
||||
res.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.setTimeout(10000, () => { req.destroy(); reject(new Error('Timeout: ' + url)); });
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadAll() {
|
||||
let ok = 0, fail = 0;
|
||||
const failed = [];
|
||||
|
||||
for (const [name, folder, file] of ICON_MAP) {
|
||||
// Local output: public/icons/<slug>.svg (flat directory, slug = name)
|
||||
const slug = name.replace(/[^a-z0-9]/gi, '_').toLowerCase();
|
||||
const outPath = path.join(ICONS_DIR, slug + '.svg');
|
||||
|
||||
if (fs.existsSync(outPath)) {
|
||||
console.log(` ✓ skip ${name}`);
|
||||
ok++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build GitHub raw URL (spaces → %20)
|
||||
const encoded = encodeURIComponent(folder) + '/' + encodeURIComponent(file + '.svg');
|
||||
const url = `${BASE_URL}/${encoded}`;
|
||||
|
||||
try {
|
||||
const buf = await fetchURL(url);
|
||||
fs.writeFileSync(outPath, buf);
|
||||
console.log(` ↓ ok ${name}`);
|
||||
ok++;
|
||||
} catch (err) {
|
||||
console.log(` ✗ FAIL ${name} (${err.message})`);
|
||||
fail++;
|
||||
failed.push({ name, url });
|
||||
}
|
||||
|
||||
// Be polite to GitHub CDN
|
||||
await new Promise(r => setTimeout(r, 80));
|
||||
}
|
||||
|
||||
console.log(`\nDone: ${ok} ok, ${fail} failed`);
|
||||
if (failed.length) {
|
||||
console.log('Failed:');
|
||||
failed.forEach(f => console.log(` ${f.name} → ${f.url}`));
|
||||
}
|
||||
|
||||
// Output slug map for server.js
|
||||
const slugMap = {};
|
||||
for (const [name] of ICON_MAP) {
|
||||
const slug = name.replace(/[^a-z0-9]/gi, '_').toLowerCase();
|
||||
if (fs.existsSync(path.join(ICONS_DIR, slug + '.svg'))) {
|
||||
slugMap[name] = '/icons/' + slug + '.svg';
|
||||
}
|
||||
}
|
||||
const mapPath = path.join(ICONS_DIR, '_map.json');
|
||||
fs.writeFileSync(mapPath, JSON.stringify(slugMap, null, 2));
|
||||
console.log(`\nIcon map written to ${mapPath}`);
|
||||
}
|
||||
|
||||
downloadAll().catch(console.error);
|
||||
Reference in New Issue
Block a user