- Tauchgeraet bereit = Taucherflasche (rigItem, Slot 31) mit Patrone UND Maske (maskItem, Slot 29) angezogen; Server getWorn statt verifyGear, geprueft bei jeder Inventaraenderung und nach jedem Umziehen. Props, diving_gear und der Anzieh-Balken entfallen. - Die fuenf Flaschen sind Patronen: benutzen = in die getragene Flasche einsetzen, die alte kommt mit Restluft zurueck (patroneTauschen). - Faeden mit Laufnummer, damit ein Tausch sie nicht verdoppelt. - exports.ox_inventory -> exports.d4rk_inventory, Meldungen ueber lib.notify (meldung_shared) und d4rk_bridge:Notify. - Konsole beim HUD angemeldet (client/hud.lua), 366 x 165 bei 1080p gemessen; ohne HUD bleibt die CSS-Position. - Nachfuellen (diving_fill) stillgelegt: kommt spaeter ueber Shop/Crafting. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
216 lines
7.2 KiB
JavaScript
216 lines
7.2 KiB
JavaScript
// Das HUD ist reine Anzeige - es sendet nichts zurueck an den Client.
|
|
// Der Lua-Client schickt den kompletten Zustand samt Skalenendwerten, damit die
|
|
// Locale-Texte und die Config an einer Stelle bleiben.
|
|
|
|
const RING = 327; // 2 * PI * 52, muss zu --ring in style.css passen
|
|
|
|
// Zeigerinstrument: 270-Grad-Skala, unten offen - wie ein echtes Finimeter.
|
|
const DIAL_START = -135;
|
|
const DIAL_SWEEP = 270;
|
|
const DIAL_R = 42;
|
|
|
|
const hud = document.getElementById('hud');
|
|
|
|
// Das HUD ist auf 1080p entworfen. Ohne Nachskalieren waere es auf 1440p und 4K
|
|
// deutlich zu klein, weil FiveM das NUI in der echten Bildschirmaufloesung
|
|
// rendert. Der Faktor aus der Config kommt obendrauf.
|
|
const DESIGN_HEIGHT = 1080;
|
|
let configScale = 1;
|
|
|
|
function applyScale() {
|
|
const scale = Math.min(2.5, Math.max(0.6, (window.innerHeight / DESIGN_HEIGHT) * configScale));
|
|
document.documentElement.style.setProperty('--hud-scale', scale.toFixed(3));
|
|
}
|
|
|
|
applyScale();
|
|
window.addEventListener('resize', applyScale);
|
|
const gaugeFill = document.getElementById('gauge-fill');
|
|
const airValue = document.getElementById('air-value');
|
|
const lamp = document.getElementById('lamp');
|
|
|
|
const dials = {
|
|
pressure: {
|
|
needle: document.getElementById('pressure-needle'),
|
|
zone: document.getElementById('pressure-zone'),
|
|
ticks: document.getElementById('pressure-ticks'),
|
|
value: document.getElementById('pressure-value')
|
|
},
|
|
depth: {
|
|
needle: document.getElementById('depth-needle'),
|
|
zone: document.getElementById('depth-zone'),
|
|
ticks: document.getElementById('depth-ticks'),
|
|
value: document.getElementById('depth-value')
|
|
}
|
|
};
|
|
|
|
const labels = {
|
|
air: document.getElementById('air-label'),
|
|
depth: document.getElementById('depth-label'),
|
|
pressure: document.getElementById('pressure-label'),
|
|
lamp: document.getElementById('lamp-label')
|
|
};
|
|
|
|
const NS = 'http://www.w3.org/2000/svg';
|
|
|
|
const toRad = (deg) => ((deg - 90) * Math.PI) / 180;
|
|
|
|
function pointOn(radius, deg) {
|
|
const r = toRad(deg);
|
|
return [50 + radius * Math.cos(r), 50 + radius * Math.sin(r)];
|
|
}
|
|
|
|
/** Winkel für einen Messwert auf der Skala. */
|
|
function angleFor(value, max) {
|
|
const ratio = Math.min(1, Math.max(0, value / (max || 1)));
|
|
return DIAL_START + DIAL_SWEEP * ratio;
|
|
}
|
|
|
|
/** Striche einmalig zeichnen - die ändern sich nie. */
|
|
function buildTicks(group) {
|
|
for (let i = 0; i <= 10; i++) {
|
|
const deg = DIAL_START + (DIAL_SWEEP * i) / 10;
|
|
const major = i % 2 === 0;
|
|
const [x1, y1] = pointOn(DIAL_R - 6, deg);
|
|
const [x2, y2] = pointOn(major ? DIAL_R - 13 : DIAL_R - 10, deg);
|
|
const line = document.createElementNS(NS, 'line');
|
|
|
|
line.setAttribute('x1', x1.toFixed(2));
|
|
line.setAttribute('y1', y1.toFixed(2));
|
|
line.setAttribute('x2', x2.toFixed(2));
|
|
line.setAttribute('y2', y2.toFixed(2));
|
|
|
|
if (major) line.setAttribute('class', 'major');
|
|
|
|
group.appendChild(line);
|
|
}
|
|
}
|
|
|
|
buildTicks(dials.pressure.ticks);
|
|
buildTicks(dials.depth.ticks);
|
|
|
|
/** Roter Bereich. Beim Finimeter die Reserve, beim Tiefenmesser die Grenze des Atemgases. */
|
|
function setZone(dial, from, to, max) {
|
|
if (!(max > 0) || from === undefined || from >= max) {
|
|
dial.zone.removeAttribute('d');
|
|
return;
|
|
}
|
|
|
|
const a = angleFor(from, max);
|
|
const b = angleFor(Math.min(to, max), max);
|
|
const [x1, y1] = pointOn(DIAL_R - 2, a);
|
|
const [x2, y2] = pointOn(DIAL_R - 2, b);
|
|
const large = b - a > 180 ? 1 : 0;
|
|
|
|
dial.zone.setAttribute(
|
|
'd',
|
|
`M ${x1.toFixed(2)} ${y1.toFixed(2)} A ${DIAL_R - 2} ${DIAL_R - 2} 0 ${large} 1 ${x2.toFixed(2)} ${y2.toFixed(2)}`
|
|
);
|
|
}
|
|
|
|
function setNeedle(dial, value, max) {
|
|
dial.needle.setAttribute('transform', `rotate(${angleFor(value, max).toFixed(2)} 50 50)`);
|
|
}
|
|
|
|
function formatTime(seconds) {
|
|
const total = Math.max(0, Math.floor(seconds));
|
|
const mm = String(Math.floor(total / 60)).padStart(2, '0');
|
|
const ss = String(total % 60).padStart(2, '0');
|
|
return `${mm}:${ss}`;
|
|
}
|
|
|
|
// Lage vom HUD (d4rk_hud, BauteilAnmelden - client/hud.lua). null = keine,
|
|
// dann gilt die Position aus style.css. 28.09.2026.
|
|
let lage = null;
|
|
let letzterStand = null;
|
|
|
|
function lageAnwenden() {
|
|
if (!lage) {
|
|
hud.classList.remove('gelagert');
|
|
hud.style.left = '';
|
|
hud.style.top = '';
|
|
hud.style.removeProperty('--lage-scale');
|
|
return;
|
|
}
|
|
|
|
hud.classList.add('gelagert');
|
|
hud.style.left = `${lage.links}px`;
|
|
hud.style.top = `${lage.oben}px`;
|
|
hud.style.setProperty('--lage-scale', String(lage.skalierung ?? 1));
|
|
}
|
|
|
|
function render(state) {
|
|
letzterStand = state;
|
|
|
|
// Im HUD abgeschaltet, Fotomodus, Auswahl: das HUD sagt "nicht sichtbar".
|
|
const zeigen = state.visible && (!lage || lage.sichtbar !== false);
|
|
|
|
hud.classList.toggle('hidden', !zeigen);
|
|
|
|
if (!zeigen) return;
|
|
|
|
const capacity = state.capacity > 0 ? state.capacity : 1;
|
|
const ratio = Math.min(1, Math.max(0, state.oxygen / capacity));
|
|
|
|
gaugeFill.style.strokeDashoffset = String(RING * (1 - ratio));
|
|
airValue.textContent = formatTime(state.oxygen);
|
|
|
|
// Warnstufen kommen als Restsekunden vom Client, damit sie zu
|
|
// config.warnAtSeconds passen statt hier nochmal definiert zu werden.
|
|
hud.classList.toggle('warn', state.level === 'warn');
|
|
hud.classList.toggle('danger', state.level === 'danger');
|
|
hud.classList.toggle('overlimit', !!state.overLimit);
|
|
|
|
const maxPressure = state.maxPressure || 300;
|
|
dials.pressure.value.textContent = String(Math.round(state.pressure ?? 0));
|
|
setNeedle(dials.pressure, state.pressure ?? 0, maxPressure);
|
|
setZone(dials.pressure, 0, state.reservePressure ?? 0, maxPressure);
|
|
hud.classList.toggle('pressure-alert', !!state.reserve);
|
|
|
|
const depthScale = state.depthScale || 100;
|
|
dials.depth.value.textContent = state.depth.toFixed(1);
|
|
setNeedle(dials.depth, state.depth, depthScale);
|
|
setZone(dials.depth, state.depthLimit, depthScale, depthScale);
|
|
hud.classList.toggle('depth-alert', !!state.overLimit);
|
|
hud.classList.toggle('depth-deep', !state.overLimit && !!state.deep);
|
|
|
|
lamp.classList.toggle('hidden', !state.hasLamp);
|
|
lamp.classList.toggle('on', !!state.lamp);
|
|
}
|
|
|
|
window.addEventListener('message', (event) => {
|
|
const data = event.data;
|
|
|
|
if (!data) return;
|
|
|
|
if (data.action === 'labels') {
|
|
if (typeof data.scale === 'number' && data.scale !== configScale) {
|
|
configScale = data.scale;
|
|
applyScale();
|
|
}
|
|
|
|
labels.air.textContent = data.air;
|
|
labels.depth.textContent = data.depth;
|
|
labels.pressure.textContent = data.pressure;
|
|
labels.lamp.textContent = data.lamp;
|
|
return;
|
|
}
|
|
|
|
if (data.action === 'state') render(data);
|
|
|
|
if (data.action === 'lage') {
|
|
lage = data.lage || null;
|
|
lageAnwenden();
|
|
if (letzterStand) render(letzterStand);
|
|
}
|
|
});
|
|
|
|
// Damit sich das HUD im Browser ohne FiveM anschauen laesst.
|
|
if (typeof GetParentResourceName !== 'function') {
|
|
render({
|
|
visible: true, oxygen: 372, capacity: 600,
|
|
depth: 27.5, depthScale: 100, depthLimit: 45, deep: true,
|
|
pressure: 186, maxPressure: 300, reservePressure: 50,
|
|
level: 'ok', hasLamp: true, lamp: true
|
|
});
|
|
}
|