#!/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 = 96, ROW_GAP = 6, ROW2 = 32, LAMP_H = 23; const PW = PAD_X * 2 + DIAL * 2 + GAUGE + GAP * 2; const PH = PAD_Y * 2 + ROW1 + ROW_GAP + ROW2; 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 + 2 + GAUGE / 2; const ROW2_Y = PY + PAD_Y + ROW1 + ROW_GAP; // Werte und Lampe auf einer Linie 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, zoneFrom, zoneTo, needleColour }) { // 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 += ``; return s; } /** Beschriftung und Wert unter der Uhr - eine Zeile mit der Lampe. */ function readout(cx, label, value, unit, colour) { return `${label} ${value}${unit}`; } 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 = ` ${dial(LEFT_CX, DIAL_CY, { value: pressure, max: 300, zoneFrom: 0, zoneTo: reservePressure, needleColour: pressureColour })} ${readout(LEFT_CX, 'DRUCK', Math.round(pressure), 'bar', pressureColour)} ${fmt(oxygen)} LUFT ${dial(RIGHT_CX, DIAL_CY, { value: depth, max: 100, zoneFrom: depthLimit, zoneTo: 100, needleColour: depthColour })} ${readout(RIGHT_CX, 'TIEFE', depth.toFixed(1), 'm', depthColour)} LAMPE `; 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 });