#!/usr/bin/env node
// Erzeugt die HUD-Renderings für die README aus denselben Werten wie nui/style.css.
// Keine In-Game-Screenshots - eine massstabsgetreue Nachbildung der Konsole.
//
// Nutzung: node docs/gen-hud-svg.mjs [ziel-ordner] (default: docs)
import { writeFileSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';
const OUT = process.argv[2] ?? 'docs';
mkdirSync(OUT, { recursive: true });
const C = {
accent: '#38bdf8',
warn: '#fbbf24',
danger: '#f87171',
text: '#e2e8f0',
muted: '#94a3b8',
panel: '#09121c',
face: '#0b1622'
};
const F = "'Segoe UI',Roboto,system-ui,sans-serif";
// Masse spiegeln das Grid aus style.css: padding 14/20, column-gap 20,
// Uhr 96, Ring 92, Beschriftung darunter, Lampe in Zeile 2.
const PAD_X = 20, PAD_Y = 14, DIAL = 96, GAUGE = 92, GAP = 20;
const ROW1 = 112, ROW_GAP = 6, LAMP_H = 23;
const PW = PAD_X * 2 + DIAL * 2 + GAUGE + GAP * 2;
const PH = PAD_Y * 2 + ROW1 + ROW_GAP + LAMP_H;
const W = 560, H = PH + 70;
const PX = (W - PW) / 2, PY = (H - PH) / 2;
const LEFT_CX = PX + PAD_X + DIAL / 2;
const RIGHT_CX = PX + PW - PAD_X - DIAL / 2;
const MID_CX = PX + PW / 2;
const DIAL_CY = PY + PAD_Y + DIAL / 2;
const GAUGE_CY = PY + PAD_Y + ROW1 / 2;
const n = (v) => Number(v.toFixed(2));
const rad = (d) => ((d - 90) * Math.PI) / 180;
const pt = (cx, cy, r, d) => [cx + r * Math.cos(rad(d)), cy + r * Math.sin(rad(d))];
const fmt = (s) => `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(Math.floor(s % 60)).padStart(2, '0')}`;
/** Zeigerinstrument. 270-Grad-Skala, unten offen - wie ein echtes Finimeter. */
function dial(cx, cy, { value, max, unit, label, zoneFrom, zoneTo, needleColour, valueColour, decimals = 0 }) {
// 96px Uhr, Geometrie aus dem viewBox 0 0 100 100 skaliert
const k = DIAL / 100;
const R = 42 * k, A0 = -135, SWEEP = 270;
const ang = (v) => A0 + SWEEP * Math.min(1, Math.max(0, v / (max || 1)));
let s = ``;
if (zoneFrom !== undefined && zoneFrom < max) {
const a = ang(zoneFrom), b = ang(Math.min(zoneTo, max));
const [x1, y1] = pt(cx, cy, R - 2 * k, a);
const [x2, y2] = pt(cx, cy, R - 2 * k, b);
s += ``;
}
for (let i = 0; i <= 10; i++) {
const a = A0 + (SWEEP * i) / 10, major = i % 2 === 0;
const [x1, y1] = pt(cx, cy, R - 6 * k, a);
const [x2, y2] = pt(cx, cy, (major ? R - 13 * k : R - 10 * k), a);
s += ``;
}
const [tipX, tipY] = pt(cx, cy, 33 * k, ang(value));
const [tailX, tailY] = pt(cx, cy, -8 * k, ang(value));
s += ``;
s += ``;
s += `${value.toFixed(decimals)}${unit}`;
s += `${label}`;
return s;
}
function build({ file, title, oxygen, capacity, level, depth, depthLimit, pressure, reservePressure, lamp, deep }) {
const ratio = Math.max(0, Math.min(1, oxygen / capacity));
const ringColour = level === 'danger' ? C.danger : level === 'warn' ? C.warn : C.accent;
const R = 52 * (GAUGE / 120), SW = 8 * (GAUGE / 120), CIRC = 2 * Math.PI * R;
const overLimit = depth >= depthLimit;
const depthColour = overLimit ? C.danger : deep ? C.warn : C.text;
const inReserve = pressure <= reservePressure;
const pressureColour = inReserve ? C.danger : C.text;
const lampColour = lamp ? C.accent : C.muted;
const svg = `
`;
writeFileSync(join(OUT, file), svg);
console.log(file);
}
build({
file: 'hud-normal.svg', title: 'Tauch-HUD: normaler Zustand',
oxygen: 545, capacity: 1200, level: 'ok',
depth: 12.4, depthLimit: 45, pressure: 136, reservePressure: 50, lamp: true
});
build({
file: 'hud-warnung.svg', title: 'Tauch-HUD: Warnstufe und Reservedruck',
oxygen: 92, capacity: 900, level: 'warn',
depth: 27.5, depthLimit: 45, deep: true, pressure: 31, reservePressure: 50, lamp: false
});
build({
file: 'hud-kritisch.svg', title: 'Tauch-HUD: unter der Tiefengrenze des Atemgases',
oxygen: 38, capacity: 900, level: 'danger',
depth: 52.4, depthLimit: 45, deep: true, pressure: 13, reservePressure: 50, lamp: true
});