// Reine Daten und Ableitungen des Kellers (Hustler-Pixelwelt T10, Spec E12/E13): Lage der Dinge in der 640 x 360 großen Szene (aus dem Markup von // keller/entwurf/BotLeads.dc.html), Einpassen der Szene („cover“), Arne nach Status, Raum-Reiter, Zeilenmodell der Bot-Leads, Scout je Raum. // Kein React, kein DOM, kein Appwrite: unter Node testbar (loadModule). Beispieldaten gibt es hier nicht; alle Werte kommen aus dem Dienst. import { ROOM_FILTERS, botleadRow } from './botleadsView.js' import { grundInfo } from './botleadGruende.js' import { figurZustand } from './hustlerSicht.js' import { ROLES } from './workersRoster.js' // --------------------------------------------------------------------------------------------------------------- // Szene /** Größe der Szene in Kunstpixeln (keller.png) */ export const SZENE = Object.freeze({ breite: 640, hoehe: 360 }) /** * Szene in den Rahmen einpassen wie die Vorlage (object-fit „cover“, links verankert): s = Maßstab, x/y = Verschiebung in Bildschirmpixeln. * Ohne brauchbare Größe (Rahmen noch nicht gemessen) gilt der Standard der Vorlage: Maßstab 2, keine Verschiebung. */ export function kellerEinpassen(breite, hoehe) { if (!(Number.isFinite(breite) && Number.isFinite(hoehe) && breite > 0 && hoehe > 0)) return { s: 2, x: 0, y: 0 } const s = Math.max(breite / SZENE.breite, hoehe / SZENE.hoehe) const ueber = hoehe - SZENE.hoehe * s return { s, x: Math.min(0, (breite - SZENE.breite * s) * 0.15), y: Math.min(ueber / 2, ueber + 36 * s) } } const px = (n) => `${n}px` /** Fenster mit Schnee, Passant und Kreuz (Glas 46 x 29 bei 23/34) */ export const FENSTER = Object.freeze({ position: 'absolute', left: px(23), top: px(34), width: px(46), height: px(29), overflow: 'hidden' }) /** Dampf über der Tasse */ export const DAMPF = Object.freeze({ position: 'absolute', left: px(26), top: px(247) }) /** Stempelabdruck im Register */ export const BUCH_STEMPEL = Object.freeze({ position: 'absolute', left: px(60), top: px(252) }) /** Rohrpost (Mündung bei 150/232, Sprite 40 x 48 links oben bei 130/224) */ export const ROHRPOST = Object.freeze({ position: 'absolute', left: px(130), top: px(224) }) /** Schublade des Aktenschranks (dritte, unterste breite Schublade) */ export const SCHUBLADE = Object.freeze({ position: 'absolute', left: px(214), top: px(279) }) /** Bodenstreifen, in dem die Maus läuft (Ausschnitt 238 x 24) */ export const MAUS_STREIFEN = Object.freeze({ position: 'absolute', left: px(30), top: px(336), width: px(238), height: px(24), overflow: 'hidden' }) /** Arnes Platz am Pult, wenn er nicht arbeitet: Füße auf der Laufline (318) beim Halt am Pult (128), Sprite 24 x 32 */ export const ARNE_RUHE = Object.freeze({ position: 'absolute', left: px(128 - 12), top: px(318 - 32) }) // Blinkpunkte (Lage, Größe, Aus-Farbe, Animation): Keyframes hk-k-k-led-… stehen in keller.css const led = (name, left, top, breite, aus, dauer) => Object.freeze({ name, stil: Object.freeze({ left: px(left), top: px(top), width: px(breite), height: px(2), background: aus, animation: `hk-k-k-led-${name} ${dauer}ms steps(1,end) infinite` }), }) export const LEDS = Object.freeze([ led('weiche-gruen', 146, 57, 2, '#1f5a3a', 1800), led('weiche-bernstein', 153, 57, 2, '#6a4a1a', 2600), led('sicherung-1', 161, 125, 1, '#1f5a3a', 3000), led('sicherung-2', 163, 125, 1, '#6a4a1a', 2200), led('sicherung-3', 165, 125, 1, '#1f5a3a', 3400), led('verteiler', 203, 33, 2, '#6a2a1a', 2000), ]) // --------------------------------------------------------------------------------------------------------------- // Arne /** * Was die Szene von Arne zeigt (Spec E12): bei „arbeitet“ die Ablauf-Schleife mit Rohrpost, Stempel und Schublade, sonst steht oder sitzt er als Haus-Figur der * passenden Pose (warten, schlafen, nicht) am Pult und die drei ruhen; bei „fehler“ trägt er den roten Marker. workers = Hustler-Tabelle der Seite. */ export function arneAnsicht(workers) { const f = figurZustand('arne', workers && typeof workers === 'object' ? workers.arne : undefined) return { status: f.status, arbeitet: f.status === 'arbeitet', pose: f.pose, alarm: f.alarm, punkte: f.punkte, zzz: f.zzz, label: f.label, } } // --------------------------------------------------------------------------------------------------------------- // Register-Buch const RAUM_KURZ = Object.freeze({ '': 'Alle', aerzte: 'Ärzte', handwerk: 'Handwerk', selbst: 'Selbstständige', laden: 'Läden' }) const RAUM_SYMBOL = Object.freeze({ '': 'alle', aerzte: 'raum-aerzte', handwerk: 'raum-handwerk', selbst: 'raum-selbst', laden: 'raum-laden' }) /** Raum-Reiter in der Reihenfolge von ROOM_FILTERS: value, label (voll, für Screenreader), kurz (sichtbar), icon und klasse (Farbe des Reiters). */ export const REITER = Object.freeze(ROOM_FILTERS.map((f) => Object.freeze({ value: f.value, label: f.label, kurz: RAUM_KURZ[f.value] ?? f.label, icon: `hk-ic hk-ic-${RAUM_SYMBOL[f.value] ?? 'alle'}`, klasse: `hk-reiter hk-reiter-${f.value === '' ? 'alle' : f.value}`, }))) /** Symbol-Klassen des Raums einer Zeile; ein unbekannter Raum zeigt den Aktenstapel. */ export function raumIcon(room) { const symbol = typeof room === 'string' && Object.hasOwn(RAUM_SYMBOL, room) && room !== '' ? RAUM_SYMBOL[room] : 'alle' return `hk-ic hk-ic-${symbol}` } /** Hustler, der den Raum scoutet (Roster: Etage „Ohne Website“, Raum aerzte → Ali, handwerk → Hanna, selbst → Sven, laden → Lea), sonst ''. */ export function scoutFuer(room) { const rolle = ROLES.find((r) => r.floor === 'ohne_website' && r.room === room) return rolle ? rolle.name : '' } // Punkte-Balken: Farbe nach Punktzahl (ab 75 grün, ab 55 gelb, darunter rot), ohne Punktzahl grau const farbeVon = (punkte) => (punkte === null ? 'leer' : punkte >= 75 ? 'gut' : punkte >= 55 ? 'mittel' : 'schlecht') /** Punkte-Anzeige: text (Zahl oder „–“), breite (Füllung in px bei 48 px Balken), breiteGross (96 px, Akte), farbe (gut | mittel | schlecht | leer). */ export function punkteInfo(score) { const punkte = Number.isFinite(score) ? Math.round(score) : null const balken = punkte === null ? 0 : Math.min(100, Math.max(0, punkte)) // die Füllung bleibt im Balken, der Text nennt die Zahl des Dienstes return { text: punkte === null ? '–' : String(punkte), breite: Math.round(balken * 0.48), breiteGross: Math.round(balken * 0.96), farbe: farbeVon(punkte), } } /** Symbol-Klassen der Telefon-Zeile: Schloss bei Heilberufen („bewusst nicht gespeichert“), Schere bei gekürzten Einträgen, sonst Telefon. */ function telefonIcon(item, gekuerzt) { const heil = Boolean(item && typeof item === 'object' && item.isHealth) return `hk-ic hk-ic-${heil ? 'schloss' : gekuerzt ? 'schere' : 'telefon'}` } /** * Alles, was eine Zeile des Buchs und die Akte zu einem Eintrag des Dienstes brauchen: das Zeilenmodell aus botleadRow (Telefonregel, sichere Quelle, Datum) * plus Raum-Symbol, Scout, Punkte-Anzeige, Grund mit Symbol und Erklärung und die Beschriftung für Screenreader. */ export function eintragModell(item) { const it = item && typeof item === 'object' ? item : {} const row = botleadRow(it) const raum = typeof it.room === 'string' ? it.room : '' const grund = grundInfo(row.reason) const punkte = punkteInfo(it.score) return { ...row, raum, raumIcon: raumIcon(raum), scout: scoutFuer(raum), punkte, grundIcon: grund.icon, erklaerung: grund.erklaerung, telefonIcon: telefonIcon(it, row.gekuerzt), label: [row.title + (row.gekuerzt ? ' (gekürzt)' : ''), row.place, row.scoreLabel, row.reason].filter((t) => t !== '').join(', ') + `, gefunden am ${row.found}`, } }