/** * 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/.svg (flat directory, slug = name) const slug = name.replace(/[^a-z0-9]/gi, '_').toLowerCase(); const outPath = path.join(ICONS_DIR, slug + '.svg'); // 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);