Hustler-Pixelwelt T10: Keller (Szene mit Arne nach Status, Register-Buch, Akte, Gründe)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-22 08:53:56 +00:00
1 parent ec63e5a43d
commit 1b3fdf5d97
14 files changed
+2247 -312

No files matched your search

+147
View File
@@ -0,0 +1,147 @@
// 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}`,
}
}