148 lines
7.6 KiB
JavaScript
148 lines
7.6 KiB
JavaScript
// 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}`,
|
||
}
|
||
}
|