Files
d4rk_divegear/icons/gen-icons.mjs
T
D4rkst3randClaude Opus 5 e5f47610a1
Lint / Lint Resource (push) Has been cancelled
feat: Tiefengrenze ueber das Atemgas
Macht ein Tiefenlimit mechanisch statt zur Absprache - und oeffnet die tiefen
Spots fuer den, der die richtige Flasche dabei hat.

Der Grund fuer die realen 40-45 m ist nicht der Druck, sondern der Stickstoff in
der Pressluft: tiefer wirkt er narkotisch, ab ~56 m wird auch der Sauerstoff
giftig. Wer tiefer will, atmet Trimix, wo Helium einen Teil des Stickstoffs
ersetzt. Genau so umgesetzt:

- maxDepth und gas pro Flasche in config/shared.lua; Pressluft 45 m,
  neues diving_tank_trimix 100 m
- Darunter Tiefenrausch in drei Stufen: Bildschirmeffekt, Kamerawackeln,
  hoeherer Luftverbrauch, in der letzten Stufe Schaden. Schwellen, Effekte,
  Staerken und Schaden komplett in config.narcosis
- Vorwarnung 5 m vor der Grenze, Tiefenanzeige im HUD wird rot und pulsiert
- Effekte werden beim Auftauchen, Ablegen, Tod und Resource-Stop aufgeraeumt
- Auswahlmenue zeigt Gas und Tiefengrenze pro Flasche
- Icon fuer die Trimix-Flasche, violett abgesetzt

Keine Dekompression - auf Wunsch bewusst weggelassen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 09:11:12 +02:00

189 lines
8.5 KiB
JavaScript

#!/usr/bin/env node
// Erzeugt die Item-Icons als SVG. Die PNGs macht danach ImageMagick daraus:
//
// node icons/gen-icons.mjs
// for f in icons/src/*.svg; do magick -background none "$f" -resize 128x128 "icons/$(basename "$f" .svg).png"; done
//
// Nur SVG 1.1 mit Presentation-Attributen, keine CSS-Bloecke und keine Filter -
// gerendert wird mit librsvg 2.40, und das ist alt.
//
// librsvg 2.40 wirft bei jedem <text>-Element ein
// WARNING: Invalid UTF-8 string passed to pango_layout_set_text()
// Das ist harmlos und tritt auch beim minimalsten Testfall auf - die Zahlen auf
// den Flaschen rendern korrekt. Nachgeprueft, nicht ignoriert.
//
// Erzeugt nebenbei icons/_check.html: alle Icons auf Schachbrett, in Slot-Groesse
// und klein, zum Gegenschauen nach Aenderungen.
import { writeFileSync, mkdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
const OUT = join(process.argv[2] ?? 'icons', 'src');
mkdirSync(OUT, { recursive: true });
const S = 128; // Canvas, quadratisch
const C = {
steelLight: '#cbd5e1',
steel: '#94a3b8',
steelDark: '#64748b',
valve: '#334155',
valveDark: '#1e293b',
glass: '#38bdf8',
glassDark: '#0284c7',
rubber: '#1e293b',
outline: '#0f172a'
};
/** Farbband pro Groesse, damit die Flaschen im Inventar auf einen Blick unterscheidbar sind. */
const BANDS = {
small: '#4ade80',
medium: '#38bdf8',
large: '#fbbf24',
xl: '#f472b6',
trimix: '#a78bfa'
};
const defs = `<defs>
<linearGradient id="steel" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="${C.steelDark}"/>
<stop offset="28%" stop-color="${C.steelLight}"/>
<stop offset="55%" stop-color="${C.steel}"/>
<stop offset="100%" stop-color="${C.steelDark}"/>
</linearGradient>
<linearGradient id="glass" x1="0" y1="0" x2="0.7" y2="1">
<stop offset="0%" stop-color="${C.glass}"/>
<stop offset="100%" stop-color="${C.glassDark}"/>
</linearGradient>
</defs>`;
const svg = (body) =>
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${S} ${S}" width="${S}" height="${S}">
${defs}
${body}
</svg>
`;
/**
* Eine Tauchflasche. cx = Mittelachse, w = Breite, top/bottom = Koerper in y.
* minutes = Laufzeit aufs Band gedruckt. Ohne die Zahl sind die Einzelflaschen
* bei kleiner Darstellung nur noch an der Bandfarbe auseinanderzuhalten.
*/
function tank(cx, w, top, bottom, band, minutes) {
const x = cx - w / 2;
const h = bottom - top;
const valveW = w * 0.4;
const bandY = top + h * 0.34;
const bandH = 22;
const label = minutes
? `<text x="${cx}" y="${bandY + bandH - 6}" text-anchor="middle" font-family="sans-serif"
font-size="16" font-weight="700" fill="${C.outline}">${minutes}</text>`
: '';
return ` <g>
<rect x="${cx - valveW / 2}" y="${top - 14}" width="${valveW}" height="17" rx="3" fill="${C.valve}"/>
<circle cx="${cx}" cy="${top - 17}" r="8" fill="none" stroke="${C.valveDark}" stroke-width="4.5"/>
<rect x="${x}" y="${top}" width="${w}" height="${h}" rx="${w / 2.4}" fill="url(#steel)" stroke="${C.outline}" stroke-width="2.5"/>
<rect x="${x + w * 0.15}" y="${top + h * 0.08}" width="${w * 0.13}" height="${h * 0.5}" rx="${w * 0.065}" fill="${C.steelLight}" opacity="0.75"/>
<rect x="${x}" y="${bandY}" width="${w}" height="${bandH}" fill="${band}"/>
${label}
</g>`;
}
// --- Flaschen: gleiche Breite, wachsende Hoehe. Die Silhouette traegt die Info,
// --- das Farbband hilft zusaetzlich.
const sizes = {
diving_tank_small: { w: 42, top: 48, bottom: 114, band: BANDS.small, minutes: 5 },
diving_tank_medium: { w: 46, top: 36, bottom: 116, band: BANDS.medium, minutes: 10 },
diving_tank_large: { w: 50, top: 26, bottom: 118, band: BANDS.large, minutes: 15 }
};
for (const [name, cfg] of Object.entries(sizes)) {
writeFileSync(join(OUT, `${name}.svg`), svg(tank(64, cfg.w, cfg.top, cfg.bottom, cfg.band, cfg.minutes)));
}
// --- XL = Doppelflasche, passend zum Label. Die Zahl steht auf einem Band
// --- ueber beide Flaschen, sonst muesste sie zweimal auftauchen.
writeFileSync(join(OUT, 'diving_tank_xl.svg'), svg(
` <rect x="38" y="26" width="52" height="8" rx="4" fill="${C.valveDark}"/>
${tank(46, 38, 38, 118, BANDS.xl)}
${tank(82, 38, 38, 118, BANDS.xl)}
<rect x="27" y="65" width="74" height="23" rx="4" fill="${BANDS.xl}" stroke="${C.outline}" stroke-width="2"/>
<text x="64" y="82" text-anchor="middle" font-family="sans-serif"
font-size="16" font-weight="700" fill="${C.outline}">20</text>`
));
// --- Trimix: Doppelflasche mit eigenem Farbband, deutlich abgesetzt von den
// --- Pressluft-Flaschen, weil die Verwechslung teuer waere.
writeFileSync(join(OUT, 'diving_tank_trimix.svg'), svg(
` <rect x="38" y="26" width="52" height="8" rx="4" fill="${C.valveDark}"/>
${tank(46, 38, 38, 118, BANDS.trimix)}
${tank(82, 38, 38, 118, BANDS.trimix)}
<rect x="20" y="65" width="88" height="23" rx="4" fill="${BANDS.trimix}" stroke="${C.outline}" stroke-width="2"/>
<text x="64" y="82" text-anchor="middle" font-family="sans-serif"
font-size="14" font-weight="700" fill="${C.outline}">TRIMIX</text>`
));
// --- Tauchmaske
writeFileSync(join(OUT, 'diving_gear.svg'), svg(
` <path d="M18 62 L30 52 L30 78 L18 72 Z" fill="${C.rubber}"/>
<path d="M110 62 L98 52 L98 78 L110 72 Z" fill="${C.rubber}"/>
<rect x="22" y="60" width="84" height="9" rx="4.5" fill="${C.rubber}"/>
<path d="M34 40 h60 a12 12 0 0 1 12 12 v22 a16 16 0 0 1 -16 16 h-8 l-8 -7 h-12 l-8 7 h-8 a16 16 0 0 1 -16 -16 v-22 a12 12 0 0 1 12 -12 z"
fill="${C.rubber}" stroke="${C.outline}" stroke-width="2.5"/>
<path d="M42 50 h44 a7 7 0 0 1 7 7 v14 a10 10 0 0 1 -10 10 h-6 l-7 -6 h-12 l-7 6 h-6 a10 10 0 0 1 -10 -10 v-14 a7 7 0 0 1 7 -7 z"
fill="url(#glass)"/>
<path d="M48 55 h13 l-11 22 h-6 a5 5 0 0 1 -5 -5 v-9 z" fill="#ffffff" opacity="0.32"/>
<rect x="58" y="88" width="12" height="16" rx="4" fill="${C.valve}"/>
<circle cx="64" cy="104" r="9" fill="${C.valveDark}" stroke="${C.outline}" stroke-width="2"/>`
));
// --- Pressluft-Kartusche: liegende, gedrungene Form mit Manometer. Bewusst quer
// --- statt hochkant, damit sie sich auf einen Blick von den Flaschen abhebt.
writeFileSync(join(OUT, 'diving_fill.svg'), svg(
` <rect x="18" y="44" width="86" height="44" rx="22" fill="url(#steel)" stroke="${C.outline}" stroke-width="2.5"/>
<rect x="30" y="52" width="44" height="7" rx="3.5" fill="${C.steelLight}" opacity="0.8"/>
<rect x="24" y="44" width="9" height="44" fill="${C.glass}" opacity="0.85"/>
<rect x="88" y="44" width="9" height="44" fill="${C.glass}" opacity="0.85"/>
<rect x="99" y="58" width="14" height="16" rx="4" fill="${C.valve}"/>
<rect x="110" y="62" width="9" height="8" rx="3" fill="${C.valveDark}"/>
<circle cx="62" cy="66" r="17" fill="${C.valveDark}" stroke="${C.outline}" stroke-width="2.5"/>
<circle cx="62" cy="66" r="12" fill="${C.steelLight}"/>
<path d="M62 66 l8 -7" fill="none" stroke="${C.outline}" stroke-width="3" stroke-linecap="round"/>
<circle cx="62" cy="66" r="2.5" fill="${C.outline}"/>`
));
// --- Kontrollblatt
const NAMES = [
'diving_gear', 'diving_fill',
'diving_tank_small', 'diving_tank_medium', 'diving_tank_large', 'diving_tank_xl',
'diving_tank_trimix'
];
const slots = (extra = '') =>
NAMES.map((n) => `<div class="slot${extra}"><img src="${n}.png" alt="${n}" title="${n}"></div>`).join('');
writeFileSync(join(dirname(OUT), '_check.html'), `<style>
body{margin:0;padding:16px;font:12px 'Segoe UI',sans-serif;background:#0f172a;color:#cbd5e1}
h3{margin:18px 0 8px;font-size:11px;letter-spacing:1px;text-transform:uppercase;color:#94a3b8}
.row{display:flex;gap:10px;flex-wrap:wrap;padding:12px;border-radius:10px}
.checker{background-color:#94a3b8;background-size:16px 16px;background-position:0 0,0 8px,8px -8px,-8px 0;
background-image:linear-gradient(45deg,#64748b 25%,transparent 25%),linear-gradient(-45deg,#64748b 25%,transparent 25%),
linear-gradient(45deg,transparent 75%,#64748b 75%),linear-gradient(-45deg,transparent 75%,#64748b 75%)}
.dark{background:#1e293b}
.slot{width:100px;height:100px;display:flex;align-items:center;justify-content:center}
.dark .slot{border:1px solid rgba(148,163,184,.35);border-radius:8px;background:rgba(0,0,0,.25)}
.slot img{max-width:88px;max-height:88px}
.small .slot{width:56px;height:56px}.small .slot img{max-width:48px;max-height:48px}
</style>
<h3>Transparenz</h3>
<div class="row checker">${slots()}</div>
<h3>Inventar-Slot 100px</h3>
<div class="row dark">${slots()}</div>
<h3>Klein 56px</h3>
<div class="row dark small">${slots()}</div>
`);
console.log('SVGs in', OUT, '+ _check.html');