Files
tickte-system/src/lib/hustlerKeller.js
T

148 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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}`,
}
}