// Die Spielwelt des Hochhauses (Hustler-Pixelwelt T5, Spec E1/E4/E5/E8/E14): 1:1 aus dem Markup der Zeichenfläche (entwurf/markup.html), Klassen mit hk- davor. // Himmel, Funkeln, Mond, die 2400 x 1200 große Bühne (per transform verschoben und gezoomt) mit Welt-, Licht-, Haus- und Fensterebenen, Neon, Antenne, Aufzug, // Raumschilder, Bildschirme, Radar, Briefkasten-Zähler, Tresor-/Keller-Text, den Etagen (Knöpfe, Freigabe und Keller sind Links) und den 19 Hustlern; davor der Schnee. // Alles hängt am Dienst-Zustand: Figur und Zusätze nach Status (figurZustand), graue Raum-Ebene samt Papierschild nur für nicht angeschlossene Such-Räume. // Stufen-Sperre (hustlerStufe.js): unterhalb der Stufe, ab der der Keller begehbar ist (3), ist die Keller-Etage kein Link auf den Keller, sondern ein Knopf wie jede andere Etage (heranzoomen). // Die Kamera (Rad, Ziehen, Pinch, Tasten) steckt in useKamera. `children` (die Regie-Ebene) liegt in der Bühne über den Figuren, in Weltkoordinaten. // Bedienung: Tastaturfokus (:focus-visible) auf Etage oder Figur außerhalb des Bildes holt sie per Kamera herein (beiFokus, kein Schwenk bei Mausklick); das Sichtfenster ist // per tabindex -1 fokussierbar (ein Klick in die Welt gibt ihm den Fokus, damit + - 0 Esc wirken); die Tippflächen der Figuren wachsen mit dem Zoom (--hk-tipp-*, tippRand); // bei offener Detailansicht (gesperrt) trägt das Sichtfenster hk-verdeckt (Schnee aus). // Kein CSS-Import (die Stile hängt WorkersPage ein: styles/hustler/hochhaus.css, hustler-ui.css, hustler-welt.css), keine Bilder, kein innerHTML. import { forwardRef, useImperativeHandle } from 'react' import { anzeigeName, figurZustand, schilder, tippRand } from '../../../lib/hustlerSicht.js' import { HUSTLER_STUFE, kellerBegehbar } from '../../../lib/hustlerStufe.js' import { AUFZUG, ETAGEN, ETAGEN_SPALTE, HAUS_BILD, HUSTLER, RAEUME, SCHILDER } from '../../../lib/hustlerWelt.js' import { FLOOR_TITLES, ROLES, ROOM_TITLES } from '../../../lib/workersRoster.js' import useKamera from './useKamera.js' // Etagen, die ein Bildschirm sind (Link statt Zoom): Text und Label wörtlich aus der Zeichenfläche const LINK_ETAGEN = Object.freeze({ freigabe: Object.freeze({ tab: 'freigabe', text: 'Freigabe ▸', label: 'Freigabe: Freigabe-Pult öffnen' }), keller: Object.freeze({ tab: 'botleads', text: 'Keller · Bot-Leads ▸', label: 'Keller · Bot-Leads: Bot-Leads öffnen' }), }) // Welche Etagen Links sind: der Keller nur, wenn er begehbar ist (ab Stufe 3, auch wenn sein Register noch leer ist); sonst zoomt er heran wie jede andere Etage const OHNE_KELLER = Object.freeze({ freigabe: LINK_ETAGEN.freigabe }) const linkEtagen = (stufe) => (kellerBegehbar(stufe) ? LINK_ETAGEN : OHNE_KELLER) const HAUS_RECHTECK = Object.freeze({ left: HAUS_BILD.x, top: HAUS_BILD.y, width: HAUS_BILD.w, height: HAUS_BILD.h }) const PX = (n) => `${n}px` // Elemente, die Fokus per Tastatur bekommen können und beim Fokus ins Bild geholt werden (M1) const FOKUS_ZIEL = '.hk-etage, .hk-hustler' // Namensschild einer Figur: so viele Bildschirm-Pixel über dem Knopf sollen beim Heranholen mit sichtbar sein const SCHILD_HOEHE = 34 // Feste, je Figur verschiedene Startverschiebung der Animation (deterministisch aus dem Index: 7 und 19 sind teilerfremd, 19 Figuren -> 19 Werte) const versatz = (index, schritt = 7, anzahl = 19) => `-${(((index * schritt) % anzahl) / 10).toFixed(1)}s` function geraeteDichte() { try { return window.devicePixelRatio || 1 } catch { return 1 } } // Lage des „nicht angeschlossen“-Zettels: die Daten kennen drei Räume, alle acht Such-Räume haben dieselbe Lage relativ zum Raum (x0 + 16, y0 + 1) function zettelLage(raum) { const bekannt = SCHILDER.nichtAngeschlossen.raeume[raum] if (bekannt) return bekannt const r = RAEUME[raum] return { left: r.x0 + 16, top: r.y0 + 1 } } const HochhausWelt = forwardRef(function HochhausWelt({ state, workers, onZeige, onOpen, gesperrt = false, stufe = HUSTLER_STUFE, children }, ref) { const kam = useKamera({ gesperrt }) const links = linkEtagen(stufe) useImperativeHandle(ref, () => ({ ganz: kam.ganz }), [kam.ganz]) const pausiert = state?.paused === true const tafel = schilder(state?.kpis) const figuren = ROLES.map((rolle, index) => ({ id: rolle.workerId, index, f: figurZustand(rolle.workerId, workers?.[rolle.workerId], { paused: pausiert }), })) const grau = new Set(figuren.filter((x) => x.f.raumGrau).map((x) => x.f.raum)) const { s, x, y } = kam const tipp = tippRand(s) const etikett = Math.min(18, Math.max(8, 15 / s)).toFixed(2) const namePx = Math.min(14, Math.max(4, 12 / s)).toFixed(2) const buehneStil = { transform: `translate(${x.toFixed(1)}px, ${y.toFixed(1)}px) scale(${s.toFixed(4)})`, imageRendering: s * geraeteDichte() < 1 ? 'auto' : 'pixelated', '--hk-etikett': `${etikett}px`, '--hk-name': `${namePx}px`, // Tippfläche der Figuren: wächst mit dem Zoom auf 40 Bildschirm-Pixel (M8, hustler-welt.css) '--hk-tipp-x': `${tipp.x.toFixed(2)}px`, '--hk-tipp-oben': `${tipp.oben.toFixed(2)}px`, '--hk-tipp-unten': `${tipp.unten.toFixed(2)}px`, } const zeige = (id) => () => { if (kam.nachZug()) return if (typeof onZeige === 'function') onZeige(id) } // Freigabe und Keller sind echte Links; ein einfacher Klick wechselt den Bildschirm, Strg/Cmd/Umschalt/Mitteltaste öffnen wie jeder Link const linkKlick = (tab) => (e) => { if (kam.nachZug()) { e.preventDefault(); e.stopPropagation(); return } if (e.ctrlKey || e.metaKey || e.shiftKey || e.button === 1) return e.preventDefault() if (typeof onOpen === 'function') onOpen(tab) } // Fokus per Tastatur (:focus-visible) auf Etage oder Figur außerhalb des sichtbaren Bereichs: Kamera holt sie ins Bild (M1). Kein Schwenk bei Mausklick. const beiFokus = (e) => { if (gesperrt) return const knoten = e && e.target if (!knoten || typeof knoten.closest !== 'function') return const ziel = knoten.closest(FOKUS_ZIEL) if (!ziel) return let tastatur = false try { tastatur = ziel.matches(':focus-visible') } catch { tastatur = false } if (!tastatur) return // Der Browser darf das Sichtfenster nicht selbst verschieben (alte Browser ohne overflow: clip scrollen beim Fokus) const sicht = e.currentTarget if (sicht && (sicht.scrollLeft || sicht.scrollTop)) { sicht.scrollLeft = 0; sicht.scrollTop = 0 } const figur = typeof ziel.matches === 'function' && ziel.matches('.hk-hustler') kam.zeige({ left: ziel.offsetLeft, top: ziel.offsetTop, width: ziel.offsetWidth, height: ziel.offsetHeight }, figur ? SCHILD_HOEHE : 0) } return (