forked from phenom/healer-man
143 lines
12 KiB
JavaScript
143 lines
12 KiB
JavaScript
import { writeFile } from "node:fs/promises";
|
|
import { fileURLToPath } from "node:url";
|
|
import { dirname, join } from "node:path";
|
|
import sharp from "sharp";
|
|
|
|
const outputDir = dirname(fileURLToPath(import.meta.url));
|
|
const bosses = [
|
|
{ name: "Lady Anacondra", copies: "3 copies owned", owned: true, accent: "#69b4ff", shape: "serpent" },
|
|
{ name: "Lord Cobrahn", copies: "Card not collected", owned: false, accent: "#a5a99b", shape: "serpent" },
|
|
{ name: "Lord Pythas", copies: "Card not collected", owned: false, accent: "#a5a99b", shape: "serpent" },
|
|
{ name: "Verdan the Everliving", copies: "5 copies owned", owned: true, accent: "#c493ff", shape: "guardian" },
|
|
{ name: "Lord Serpentis", copies: "Card not collected", owned: false, accent: "#a5a99b", shape: "serpent" },
|
|
{ name: "Mutanus the Devourer", copies: "Card not collected", owned: false, accent: "#a5a99b", shape: "mutant" },
|
|
];
|
|
|
|
const defs = `<defs>
|
|
<linearGradient id="backdrop" x2="1" y2="1"><stop stop-color="#193628"/><stop offset="1" stop-color="#06100e"/></linearGradient>
|
|
<linearGradient id="panel" x2="1" y2="1"><stop stop-color="#27312a"/><stop offset="1" stop-color="#0c1714"/></linearGradient>
|
|
<radialGradient id="portrait"><stop stop-color="#435549"/><stop offset="1" stop-color="#111d1a"/></radialGradient>
|
|
<linearGradient id="scale" x2=".8" y2="1"><stop stop-color="#aac49b"/><stop offset=".45" stop-color="#4d8067"/><stop offset="1" stop-color="#1d4b42"/></linearGradient>
|
|
<linearGradient id="belly" x2="1" y2=".8"><stop stop-color="#eee5a5"/><stop offset="1" stop-color="#a7a474"/></linearGradient>
|
|
<linearGradient id="guardian-fill" x2=".8" y2="1"><stop stop-color="#aad36f"/><stop offset=".48" stop-color="#568d49"/><stop offset="1" stop-color="#274f37"/></linearGradient>
|
|
<filter id="shadow"><feDropShadow dx="0" dy="12" stdDeviation="17" flood-opacity=".7"/></filter>
|
|
<filter id="creature-shadow"><feDropShadow dx="0" dy="5" stdDeviation="4" flood-opacity=".7"/></filter>
|
|
<g id="serpent" filter="url(#creature-shadow)">
|
|
<path d="M24 92 C8 75 30 61 53 69 C84 79 89 55 69 45 C53 37 54 23 67 18" fill="none" stroke="#193f37" stroke-width="26" stroke-linecap="round"/>
|
|
<path d="M24 91 C12 77 36 65 55 73 C82 82 92 54 70 44 C56 36 57 23 68 18" fill="none" stroke="url(#scale)" stroke-width="19" stroke-linecap="round"/>
|
|
<path d="M27 84 C35 76 43 75 52 78 M67 73 C75 70 79 62 78 56 M67 41 C59 32 62 22 68 18" fill="none" stroke="url(#belly)" stroke-width="6" stroke-linecap="round" opacity=".85"/>
|
|
<path d="M62 25 L56 15 L64 18 L67 9 L74 17 L82 14 L79 25 Q78 35 70 36 Q61 35 62 25" fill="url(#scale)" stroke="#173d34" stroke-width="2"/>
|
|
<path d="M62 33 Q69 43 77 33 L73 48 L69 40 L64 48Z" fill="#e2cf9c"/>
|
|
<circle cx="65" cy="25" r="2.5" fill="#f8d378"/><circle cx="75" cy="25" r="2.5" fill="#f8d378"/>
|
|
</g>
|
|
<g id="guardian" filter="url(#creature-shadow)">
|
|
<path d="M40 92 L22 76 L17 47 L29 28 L43 22 L63 26 L77 43 L82 75 L65 91Z" fill="url(#guardian-fill)" stroke="#214c30" stroke-width="3"/>
|
|
<path d="M24 41 L11 20 L31 31 M72 40 L90 18 L75 50" fill="url(#guardian-fill)" stroke="#214c30" stroke-width="3"/>
|
|
<path d="M38 30 L40 15 L50 25 L60 14 L66 33 M27 58 L10 53 L25 72 M76 59 L91 54 L80 76" fill="#427d3d" stroke="#1b472b" stroke-width="3"/>
|
|
<path d="M40 43 Q50 51 61 43 L65 71 Q50 80 35 69Z" fill="#9cc372" opacity=".8"/>
|
|
<path d="M35 38 L45 42 L41 46 M65 38 L55 42 L59 46" fill="#eae99e"/>
|
|
<path d="M40 58 Q50 63 61 58" fill="none" stroke="#173e29" stroke-width="4"/>
|
|
</g>
|
|
<g id="mutant" filter="url(#creature-shadow)">
|
|
<path d="M30 90 L20 65 L30 37 L45 29 L58 31 L73 45 L80 69 L68 91Z" fill="url(#scale)" stroke="#214b3e" stroke-width="3"/>
|
|
<path d="M34 35 L21 19 L24 42 M62 36 L75 18 L75 46" fill="url(#scale)" stroke="#214b3e" stroke-width="3"/>
|
|
<path d="M21 61 L5 53 L18 80 M76 58 L92 48 L84 80" fill="none" stroke="#376b56" stroke-width="12" stroke-linecap="round"/>
|
|
<path d="M36 51 L46 54 M62 51 L55 54" stroke="#e7dc89" stroke-width="5" stroke-linecap="round"/>
|
|
<path d="M38 66 Q49 80 62 66 L58 75 L54 69 L50 79 L46 69 L42 75Z" fill="#eadfb3"/>
|
|
</g>
|
|
</defs>`;
|
|
|
|
function rarityMarks(boss, x, y, compact = false) {
|
|
const size = compact ? 15 : 21;
|
|
const gap = compact ? 4 : 6;
|
|
return ["N", "R", "E", "L"].map((label, index) => {
|
|
const owned = boss.owned && (boss.name.startsWith("Verdan") ? index < 3 : index < 2);
|
|
const color = ["#ddd8ce", "#69b4ff", "#c493ff", "#e5a757"][index];
|
|
return `<g transform="translate(${x + index * (size + gap)} ${y})"><rect width="${size}" height="${size}" rx="3" fill="${owned ? "#2a3a3c" : "#252f2c"}" stroke="${owned ? color : "#5c6a60"}"/><text x="${size / 2}" y="${compact ? 11 : 15}" text-anchor="middle" font-family="Arial,sans-serif" font-size="${compact ? 9 : 12}" fill="${owned ? color : "#79877d"}">${label}</text></g>`;
|
|
}).join("");
|
|
}
|
|
|
|
function largeCard(boss, index) {
|
|
const x = 88 + index % 3 * 352;
|
|
const y = 166 + Math.floor(index / 3) * 226;
|
|
const owned = boss.owned;
|
|
return `<g transform="translate(${x} ${y})">
|
|
<rect width="320" height="210" rx="9" fill="#151f1b" stroke="${owned ? boss.accent : "#69756b"}" stroke-width="${owned ? 2 : 1}"/>
|
|
<rect x="1" y="1" width="318" height="28" rx="8" fill="#0a1410"/>
|
|
<text x="12" y="19" font-family="Arial,sans-serif" font-size="11" letter-spacing="2" fill="#efe8d7">BOSS ${String(index + 1).padStart(2, "0")}</text>
|
|
<text x="307" y="19" text-anchor="end" font-family="Arial,sans-serif" font-size="10" fill="${boss.accent}">${owned ? "COLLECTED" : "UNCOLLECTED"}</text>
|
|
<rect x="7" y="31" width="306" height="113" rx="3" fill="url(#portrait)"/>
|
|
<ellipse cx="160" cy="137" rx="53" ry="5" fill="#030a08" opacity=".7"/>
|
|
<use href="#${boss.shape}" transform="translate(105 30) scale(1.08)"/>
|
|
<text x="13" y="164" font-family="Georgia,serif" font-size="${boss.name.length > 20 ? 18 : 20}" fill="#f3ead1">${boss.name}</text>
|
|
<text x="13" y="181" font-family="Arial,sans-serif" font-size="11" fill="#b8b5a7">${boss.copies}</text>
|
|
<path d="M0 187H320" stroke="#536357" opacity=".6"/>
|
|
${rarityMarks(boss, 12, 191)}
|
|
</g>`;
|
|
}
|
|
|
|
const countdown = `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="700" viewBox="0 0 1200 700">
|
|
${defs}
|
|
<rect width="1200" height="700" fill="url(#backdrop)"/>
|
|
<path d="M0 660Q130 310 300 490T760 365T1200 470V700H0" fill="#2c5037" opacity=".24"/>
|
|
<rect x="58" y="34" width="1084" height="632" rx="16" fill="url(#panel)" stroke="#8d7d56" stroke-width="2" filter="url(#shadow)"/>
|
|
<text x="88" y="102" font-family="Georgia,serif" font-size="38" fill="#f3ead1">Wailing Caverns</text>
|
|
<text x="88" y="132" font-family="Arial,sans-serif" font-size="14" fill="#b8b5a7">6 bosses ahead · 2 collected</text>
|
|
<path d="M978 56V140" stroke="#88764f"/>
|
|
<text x="1050" y="78" text-anchor="middle" font-family="Arial,sans-serif" font-size="11" letter-spacing="2" fill="#b8b5a7">ENTERING IN</text>
|
|
<text x="1050" y="134" text-anchor="middle" font-family="Georgia,serif" font-size="60" fill="#d5b875">05</text>
|
|
${bosses.map(largeCard).join("\n ")}
|
|
</svg>\n`;
|
|
|
|
function compactCard(boss, index) {
|
|
const x = 180 + index * 169;
|
|
return `<g transform="translate(${x} 145)">
|
|
<rect width="155" height="171" rx="7" fill="#17231e" stroke="${boss.owned ? boss.accent : "#6e796d"}"/>
|
|
<rect x="1" y="1" width="153" height="20" rx="6" fill="#0a1410"/>
|
|
<text x="8" y="14" font-family="Arial,sans-serif" font-size="8" letter-spacing="1" fill="#e9e2d2">BOSS ${String(index + 1).padStart(2, "0")}</text>
|
|
<text x="147" y="14" text-anchor="end" font-family="Arial,sans-serif" font-size="8" fill="${boss.accent}">${boss.owned ? "OWNED" : "MISSING"}</text>
|
|
<rect x="4" y="23" width="147" height="79" fill="url(#portrait)"/>
|
|
<ellipse cx="78" cy="100" rx="37" ry="4" fill="#030a08" opacity=".6"/>
|
|
<use href="#${boss.shape}" transform="translate(46 22) scale(.67)"/>
|
|
<text x="7" y="120" font-family="Georgia,serif" font-size="${boss.name.length > 19 ? 10 : 12}" fill="#f3ead1">${boss.name}</text>
|
|
<text x="7" y="136" font-family="Arial,sans-serif" font-size="9" fill="#b8b5a7">${boss.owned ? boss.copies : "Not collected"}</text>
|
|
<path d="M0 143H155" stroke="#536357" opacity=".6"/>
|
|
${rarityMarks(boss, 8, 148, true)}
|
|
</g>`;
|
|
}
|
|
|
|
const pause = `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="700" viewBox="0 0 1200 700">
|
|
${defs}
|
|
<rect width="1200" height="700" fill="url(#backdrop)"/>
|
|
<path d="M0 680Q130 370 320 520T700 390T1200 540V700H0" fill="#275137" opacity=".32"/>
|
|
<rect width="1200" height="700" fill="#020705" opacity=".58"/>
|
|
<rect x="150" y="30" width="900" height="640" rx="15" fill="url(#panel)" stroke="#8d7d56" stroke-width="2" filter="url(#shadow)"/>
|
|
<text x="180" y="80" font-family="Arial,sans-serif" font-size="24" font-weight="bold" letter-spacing="4" fill="#f3ead1">PAUSED</text>
|
|
<circle cx="779" cy="70" r="22" fill="#253a31" stroke="#b7b381"/>
|
|
<text x="779" y="78" text-anchor="middle" font-family="Georgia,serif" font-size="23" fill="#d5b875">+</text>
|
|
<text x="814" y="67" font-family="Arial,sans-serif" font-size="17" font-weight="bold" fill="#f3ead1">Aria</text>
|
|
<text x="814" y="86" font-family="Arial,sans-serif" font-size="12" fill="#b8b5a7">Level 24 · Priest / Discipline</text>
|
|
<path d="M178 105H1022" stroke="#8d7d56" opacity=".6"/>
|
|
<text x="180" y="131" font-family="Arial,sans-serif" font-size="12" font-weight="bold" letter-spacing="1" fill="#d5b875">BOSS CARDS IN THIS DUNGEON</text>
|
|
<text x="1018" y="131" text-anchor="end" font-family="Arial,sans-serif" font-size="12" fill="#b8b5a7">2 / 6 COLLECTED</text>
|
|
${bosses.slice(0, 5).map(compactCard).join("\n ")}
|
|
<text x="1018" y="335" text-anchor="end" font-family="Arial,sans-serif" font-size="11" fill="#9fa994">SCROLL →</text>
|
|
<path d="M178 347H1022" stroke="#8d7d56" opacity=".6"/>
|
|
<rect x="440" y="364" width="320" height="45" rx="8" fill="#587348" stroke="#9bb477"/>
|
|
<text x="600" y="393" text-anchor="middle" font-family="Arial,sans-serif" font-size="16" font-weight="bold" fill="#fbfee9">Resume game</text>
|
|
<g font-family="Arial,sans-serif">
|
|
<rect x="180" y="427" width="384" height="101" rx="8" fill="#1a2621" stroke="#526153"/>
|
|
<rect x="636" y="427" width="384" height="101" rx="8" fill="#1a2621" stroke="#526153"/>
|
|
<rect x="180" y="544" width="384" height="101" rx="8" fill="#1a2621" stroke="#526153"/>
|
|
<rect x="636" y="544" width="384" height="101" rx="8" fill="#1a2621" stroke="#526153"/>
|
|
<g fill="#b8b5a7" font-size="12" font-weight="bold" letter-spacing="2"><text x="195" y="451">ADVENTURE</text><text x="651" y="451">CHARACTER</text><text x="195" y="568">SETTINGS</text><text x="651" y="568">SESSION</text></g>
|
|
<g fill="#314136" stroke="#64795d"><rect x="195" y="466" width="169" height="43" rx="6"/><rect x="379" y="466" width="169" height="43" rx="6"/><rect x="651" y="466" width="169" height="43" rx="6"/><rect x="835" y="466" width="169" height="43" rx="6"/><rect x="195" y="583" width="169" height="43" rx="6"/><rect x="379" y="583" width="169" height="43" rx="6"/><rect x="651" y="583" width="169" height="43" rx="6"/><rect x="835" y="583" width="169" height="43" rx="6"/></g>
|
|
<g fill="#eee9d9" font-size="12" text-anchor="middle"><text x="279" y="493">Party management</text><text x="463" y="493">Dungeon map</text><text x="735" y="493">Inventory</text><text x="919" y="493">Spellbook</text><text x="279" y="610">Options</text><text x="463" y="610">Keybinds</text><text x="735" y="610">Return to entrance</text><text x="919" y="610">Main menu</text></g>
|
|
</g>
|
|
</svg>\n`;
|
|
|
|
for (const [name, svg] of [["dungeon-boss-countdown", countdown], ["pause-boss-card-strip", pause]]) {
|
|
await writeFile(join(outputDir, `${name}.svg`), svg);
|
|
await sharp(Buffer.from(svg)).png().toFile(join(outputDir, `${name}.png`));
|
|
}
|