From 8bd365b1fcf47417d6d9299258b96ebfa8cde53c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 07:55:25 +0000 Subject: [PATCH] =?UTF-8?q?Hustler-Pixelwelt=20T1+T2:=20Import=20der=20Zei?= =?UTF-8?q?chenfl=C3=A4che=20(Bilder,=20CSS=20mit=20hk-Vorsatz,=20Geometri?= =?UTF-8?q?e),=20helles=20Haus=20mit=20grauen=20Raum-Ebenen,=20Press=20Sta?= =?UTF-8?q?rt=202P=20selbst=20gehostet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/hustler-import.mjs liest pixelwelt-v2 (+ pixelwelt-woms/haus) und schreibt src/assets/hustler, src/styles/hustler/{hochhaus,keller,pult}.css und src/lib/hustlerWelt.js; idempotent - haus.png mit allen Räumen hell, acht graue Ebenen für die Such-Räume (GRAUE_RAEUME, .hk-raum-grau-) - Press Start 2P lateinisch aus @fontsource/press-start-2p@5.2.7 (OFL), noch nirgends importiert - 40 neue Tests (Assets, Geometrie, CSS-Umformung) Co-Authored-By: Claude Opus 5.5 --- public/fonts/OFL-PressStart2P.txt | 93 ++ public/fonts/PressStart2P-Regular.woff2 | Bin 0 -> 12512 bytes scripts/hustler-import.mjs | 954 ++++++++++++++++++ scripts/hustler-import/quellen.json | 188 ++++ src/assets/hustler/detail/dampf.png | Bin 0 -> 419 bytes .../hustler/detail/gross-kappe-arbeiten.png | Bin 0 -> 2326 bytes .../hustler/detail/gross-kappe-nicht.png | Bin 0 -> 1897 bytes .../hustler/detail/gross-kappe-schlafen.png | Bin 0 -> 2332 bytes .../hustler/detail/gross-kappe-warten.png | Bin 0 -> 3348 bytes .../hustler/detail/gross-krone-arbeiten.png | Bin 0 -> 2479 bytes .../hustler/detail/gross-krone-nicht.png | Bin 0 -> 1990 bytes .../hustler/detail/gross-krone-schlafen.png | Bin 0 -> 2560 bytes .../hustler/detail/gross-krone-warten.png | Bin 0 -> 3462 bytes .../hustler/detail/gross-muetze-arbeiten.png | Bin 0 -> 2425 bytes .../hustler/detail/gross-muetze-nicht.png | Bin 0 -> 1968 bytes .../hustler/detail/gross-muetze-schlafen.png | Bin 0 -> 2543 bytes .../hustler/detail/gross-muetze-warten.png | Bin 0 -> 3514 bytes src/assets/hustler/detail/monitor-gross.png | Bin 0 -> 1259 bytes src/assets/hustler/detail/overseer-gross.png | Bin 0 -> 23145 bytes src/assets/hustler/detail/szene-bruecke.png | Bin 0 -> 19839 bytes src/assets/hustler/detail/szene-buero.png | Bin 0 -> 19201 bytes src/assets/hustler/detail/szene-nicht.png | Bin 0 -> 19057 bytes src/assets/hustler/detail/wirbel.png | Bin 0 -> 2461 bytes src/assets/hustler/detail/zzz-gross.png | Bin 0 -> 461 bytes src/assets/hustler/figuren/bildschirm.png | Bin 0 -> 243 bytes src/assets/hustler/figuren/kappe-arbeiten.png | Bin 0 -> 894 bytes src/assets/hustler/figuren/kappe-nicht.png | Bin 0 -> 564 bytes src/assets/hustler/figuren/kappe-schlafen.png | Bin 0 -> 790 bytes src/assets/hustler/figuren/kappe-warten.png | Bin 0 -> 1105 bytes src/assets/hustler/figuren/krone-arbeiten.png | Bin 0 -> 873 bytes src/assets/hustler/figuren/krone-nicht.png | Bin 0 -> 548 bytes src/assets/hustler/figuren/krone-schlafen.png | Bin 0 -> 773 bytes src/assets/hustler/figuren/krone-warten.png | Bin 0 -> 1094 bytes .../hustler/figuren/muetze-arbeiten.png | Bin 0 -> 904 bytes src/assets/hustler/figuren/muetze-nicht.png | Bin 0 -> 561 bytes .../hustler/figuren/muetze-schlafen.png | Bin 0 -> 816 bytes src/assets/hustler/figuren/muetze-warten.png | Bin 0 -> 1156 bytes src/assets/hustler/figuren/overseer-klein.png | Bin 0 -> 3269 bytes src/assets/hustler/figuren/punkte.png | Bin 0 -> 174 bytes src/assets/hustler/figuren/radar.png | Bin 0 -> 1466 bytes src/assets/hustler/figuren/zzz.png | Bin 0 -> 249 bytes src/assets/hustler/haus/aufzug-seil.png | Bin 0 -> 111 bytes src/assets/hustler/haus/aufzug.png | Bin 0 -> 563 bytes src/assets/hustler/haus/haus-fenster-a.png | Bin 0 -> 3097 bytes src/assets/hustler/haus/haus-fenster-b.png | Bin 0 -> 3096 bytes src/assets/hustler/haus/haus-fenster-c.png | Bin 0 -> 3054 bytes src/assets/hustler/haus/haus.png | Bin 0 -> 131007 bytes src/assets/hustler/haus/raum-grau-aerzte.png | Bin 0 -> 1865 bytes src/assets/hustler/haus/raum-grau-foren.png | Bin 0 -> 1979 bytes .../hustler/haus/raum-grau-handwerk.png | Bin 0 -> 1945 bytes .../hustler/haus/raum-grau-kleinanzeigen.png | Bin 0 -> 1873 bytes src/assets/hustler/haus/raum-grau-laden.png | Bin 0 -> 1807 bytes src/assets/hustler/haus/raum-grau-netz.png | Bin 0 -> 2038 bytes src/assets/hustler/haus/raum-grau-portale.png | Bin 0 -> 1962 bytes src/assets/hustler/haus/raum-grau-selbst.png | Bin 0 -> 1979 bytes src/assets/hustler/haus/schild-a.png | Bin 0 -> 2197 bytes src/assets/hustler/haus/schild-b.png | Bin 0 -> 2196 bytes .../hustler/keller/figuren/arne-ablauf.png | Bin 0 -> 9892 bytes .../hustler/keller/figuren/buch-stempel.png | Bin 0 -> 726 bytes src/assets/hustler/keller/figuren/maus.png | Bin 0 -> 338 bytes .../hustler/keller/figuren/neu-funke.png | Bin 0 -> 409 bytes .../hustler/keller/figuren/rohrpost.png | Bin 0 -> 966 bytes .../hustler/keller/figuren/schublade.png | Bin 0 -> 1143 bytes .../hustler/keller/szene/dampf-tasse.png | Bin 0 -> 334 bytes .../hustler/keller/szene/fenster-kreuz.png | Bin 0 -> 204 bytes .../hustler/keller/szene/fenster-passant.png | Bin 0 -> 366 bytes .../hustler/keller/szene/fensterschnee.png | Bin 0 -> 349 bytes .../hustler/keller/szene/keller-licht.png | Bin 0 -> 67293 bytes src/assets/hustler/keller/szene/keller.png | Bin 0 -> 165866 bytes .../hustler/keller/szene/licht-halle.png | Bin 0 -> 38513 bytes .../hustler/keller/szene/licht-lampe-236.png | Bin 0 -> 13171 bytes .../hustler/keller/szene/licht-lampe-92.png | Bin 0 -> 13046 bytes .../keller/szene/licht-schreibtisch.png | Bin 0 -> 2357 bytes src/assets/hustler/keller/szene/staub.png | Bin 0 -> 424 bytes src/assets/hustler/keller/ui/akte-icon.png | Bin 0 -> 225 bytes src/assets/hustler/keller/ui/akte.png | Bin 0 -> 577 bytes src/assets/hustler/keller/ui/alle.png | Bin 0 -> 319 bytes src/assets/hustler/keller/ui/brief-x.png | Bin 0 -> 308 bytes .../hustler/keller/ui/brief-zurueck.png | Bin 0 -> 306 bytes src/assets/hustler/keller/ui/buch.png | Bin 0 -> 540 bytes src/assets/hustler/keller/ui/doppelt.png | Bin 0 -> 288 bytes src/assets/hustler/keller/ui/etikett-leer.png | Bin 0 -> 296 bytes src/assets/hustler/keller/ui/fremd.png | Bin 0 -> 202 bytes src/assets/hustler/keller/ui/haken.png | Bin 0 -> 216 bytes src/assets/hustler/keller/ui/kalender.png | Bin 0 -> 273 bytes src/assets/hustler/keller/ui/kette.png | Bin 0 -> 302 bytes src/assets/hustler/keller/ui/lupe.png | Bin 0 -> 487 bytes src/assets/hustler/keller/ui/messing.png | Bin 0 -> 381 bytes src/assets/hustler/keller/ui/pause.png | Bin 0 -> 152 bytes src/assets/hustler/keller/ui/pfeil-links.png | Bin 0 -> 167 bytes src/assets/hustler/keller/ui/pfeil-rechts.png | Bin 0 -> 155 bytes src/assets/hustler/keller/ui/pfeil-runter.png | Bin 0 -> 226 bytes src/assets/hustler/keller/ui/quelle.png | Bin 0 -> 359 bytes src/assets/hustler/keller/ui/raum-aerzte.png | Bin 0 -> 239 bytes .../hustler/keller/ui/raum-handwerk.png | Bin 0 -> 306 bytes src/assets/hustler/keller/ui/raum-laden.png | Bin 0 -> 324 bytes src/assets/hustler/keller/ui/raum-selbst.png | Bin 0 -> 260 bytes src/assets/hustler/keller/ui/robo.png | Bin 0 -> 387 bytes src/assets/hustler/keller/ui/schere.png | Bin 0 -> 328 bytes src/assets/hustler/keller/ui/schliessen.png | Bin 0 -> 170 bytes src/assets/hustler/keller/ui/schloss.png | Bin 0 -> 261 bytes src/assets/hustler/keller/ui/seite-kaputt.png | Bin 0 -> 243 bytes src/assets/hustler/keller/ui/stempel.png | Bin 0 -> 293 bytes src/assets/hustler/keller/ui/stopp.png | Bin 0 -> 267 bytes src/assets/hustler/keller/ui/telefon.png | Bin 0 -> 300 bytes src/assets/hustler/keller/ui/uhr.png | Bin 0 -> 335 bytes src/assets/hustler/keller/ui/waage-kaputt.png | Bin 0 -> 420 bytes src/assets/hustler/keller/ui/zurueck.png | Bin 0 -> 169 bytes .../hustler/pult/eigen/lichterkette-0.png | Bin 0 -> 1211 bytes .../hustler/pult/eigen/lichterkette-1.png | Bin 0 -> 1203 bytes .../hustler/pult/eigen/lichterkette-2.png | Bin 0 -> 1210 bytes .../hustler/pult/figuren/aufzug-ding.png | Bin 0 -> 477 bytes .../hustler/pult/figuren/aufzug-innen.png | Bin 0 -> 1103 bytes .../hustler/pult/figuren/aufzug-licht.png | Bin 0 -> 1028 bytes .../hustler/pult/figuren/aufzug-ruftaste.png | Bin 0 -> 131 bytes .../hustler/pult/figuren/aufzug-tuer.png | Bin 0 -> 4498 bytes .../hustler/pult/figuren/boten-ablauf.png | Bin 0 -> 10646 bytes .../pult/figuren/briefkasten-funke.png | Bin 0 -> 225 bytes .../hustler/pult/figuren/briefkasten.png | Bin 0 -> 1189 bytes src/assets/hustler/pult/figuren/monitor.png | Bin 0 -> 367 bytes src/assets/hustler/pult/szene/dampf-tasse.png | Bin 0 -> 380 bytes .../hustler/pult/szene/fenster-sprossen-1.png | Bin 0 -> 228 bytes .../hustler/pult/szene/fenster-sprossen-2.png | Bin 0 -> 449 bytes .../hustler/pult/szene/fenster2-maske.png | Bin 0 -> 294 bytes .../hustler/pult/szene/fensterschnee.png | Bin 0 -> 349 bytes src/assets/hustler/pult/szene/licht-mond.png | Bin 0 -> 4353 bytes .../hustler/pult/szene/licht-pendel-232.png | Bin 0 -> 10051 bytes .../hustler/pult/szene/licht-pendel-55.png | Bin 0 -> 10312 bytes .../hustler/pult/szene/licht-rechts.png | Bin 0 -> 18436 bytes .../hustler/pult/szene/licht-schreibtisch.png | Bin 0 -> 2854 bytes src/assets/hustler/pult/szene/pult.png | Bin 0 -> 173959 bytes src/assets/hustler/pult/ui/ansehen.png | Bin 0 -> 280 bytes src/assets/hustler/pult/ui/antworten.png | Bin 0 -> 319 bytes src/assets/hustler/pult/ui/art-bau-kaputt.png | Bin 0 -> 328 bytes src/assets/hustler/pult/ui/art-beschwerde.png | Bin 0 -> 249 bytes src/assets/hustler/pult/ui/art-mail.png | Bin 0 -> 251 bytes src/assets/hustler/pult/ui/art-mensch.png | Bin 0 -> 272 bytes src/assets/hustler/pult/ui/art-plattform.png | Bin 0 -> 364 bytes src/assets/hustler/pult/ui/art-preisfrage.png | Bin 0 -> 269 bytes src/assets/hustler/pult/ui/art-unsicher.png | Bin 0 -> 233 bytes src/assets/hustler/pult/ui/art-vorschau.png | Bin 0 -> 340 bytes src/assets/hustler/pult/ui/bauen.png | Bin 0 -> 368 bytes src/assets/hustler/pult/ui/brief.png | Bin 0 -> 514 bytes src/assets/hustler/pult/ui/briefkasten.png | Bin 0 -> 729 bytes src/assets/hustler/pult/ui/einfuegen.png | Bin 0 -> 334 bytes src/assets/hustler/pult/ui/erledigt.png | Bin 0 -> 318 bytes src/assets/hustler/pult/ui/gesendet.png | Bin 0 -> 274 bytes src/assets/hustler/pult/ui/klammer.png | Bin 0 -> 498 bytes src/assets/hustler/pult/ui/klemmbrett.png | Bin 0 -> 445 bytes src/assets/hustler/pult/ui/kopf-kappe.png | Bin 0 -> 346 bytes src/assets/hustler/pult/ui/kopf-krone.png | Bin 0 -> 367 bytes src/assets/hustler/pult/ui/kopf-muetze.png | Bin 0 -> 368 bytes src/assets/hustler/pult/ui/kopieren.png | Bin 0 -> 271 bytes src/assets/hustler/pult/ui/oeffnen.png | Bin 0 -> 367 bytes src/assets/hustler/pult/ui/person.png | Bin 0 -> 364 bytes src/assets/hustler/pult/ui/sanduhr.png | Bin 0 -> 368 bytes src/assets/hustler/pult/ui/uebernehmen.png | Bin 0 -> 323 bytes src/assets/hustler/pult/ui/umschlag-offen.png | Bin 0 -> 275 bytes src/assets/hustler/pult/ui/umschlag.png | Bin 0 -> 254 bytes src/assets/hustler/pult/ui/veraltet.png | Bin 0 -> 429 bytes src/assets/hustler/pult/ui/verwerfen.png | Bin 0 -> 214 bytes src/assets/hustler/ui/blase.png | Bin 0 -> 354 bytes src/assets/hustler/ui/glocke.png | Bin 0 -> 448 bytes src/assets/hustler/ui/knopf.png | Bin 0 -> 220 bytes src/assets/hustler/ui/muenze-gruen.png | Bin 0 -> 970 bytes src/assets/hustler/ui/muenze-rot.png | Bin 0 -> 974 bytes src/assets/hustler/ui/panel.png | Bin 0 -> 206 bytes src/assets/hustler/ui/pause.png | Bin 0 -> 140 bytes src/assets/hustler/ui/pfeil-links.png | Bin 0 -> 167 bytes src/assets/hustler/ui/pfeil-rechts.png | Bin 0 -> 155 bytes src/assets/hustler/ui/pokal.png | Bin 0 -> 452 bytes src/assets/hustler/ui/robo.png | Bin 0 -> 381 bytes src/assets/hustler/ui/schliessen.png | Bin 0 -> 170 bytes src/assets/hustler/ui/stern.png | Bin 0 -> 485 bytes src/assets/hustler/ui/zahnrad.png | Bin 0 -> 782 bytes src/assets/hustler/welt/laternen-licht.png | Bin 0 -> 17494 bytes src/assets/hustler/welt/mond.png | Bin 0 -> 1885 bytes src/assets/hustler/welt/schnee-1.png | Bin 0 -> 682 bytes src/assets/hustler/welt/schnee-2.png | Bin 0 -> 629 bytes src/assets/hustler/welt/schnee-3.png | Bin 0 -> 554 bytes src/assets/hustler/welt/sterne-funkeln.png | Bin 0 -> 2449 bytes src/assets/hustler/welt/sterne-verteilt.png | Bin 0 -> 15162 bytes src/assets/hustler/welt/welt-licht-1.png | Bin 0 -> 18481 bytes src/assets/hustler/welt/welt-licht-2.png | Bin 0 -> 18609 bytes src/assets/hustler/welt/welt-licht-3.png | Bin 0 -> 18386 bytes src/assets/hustler/welt/welt-licht-4.png | Bin 0 -> 18637 bytes src/assets/hustler/welt/welt.png | Bin 0 -> 213333 bytes src/lib/hustlerWelt.js | 369 +++++++ src/styles/hustler/hochhaus.css | 161 +++ src/styles/hustler/keller.css | 119 +++ src/styles/hustler/pult.css | 122 +++ src/styles/hustler/schrift.css | 9 + tests/unit/hustlerAssets.test.mjs | 221 ++++ tests/unit/hustlerImportCss.test.mjs | 294 ++++++ tests/unit/hustlerWelt.test.mjs | 167 +++ 195 files changed, 2697 insertions(+) create mode 100644 public/fonts/OFL-PressStart2P.txt create mode 100644 public/fonts/PressStart2P-Regular.woff2 create mode 100644 scripts/hustler-import.mjs create mode 100644 scripts/hustler-import/quellen.json create mode 100644 src/assets/hustler/detail/dampf.png create mode 100644 src/assets/hustler/detail/gross-kappe-arbeiten.png create mode 100644 src/assets/hustler/detail/gross-kappe-nicht.png create mode 100644 src/assets/hustler/detail/gross-kappe-schlafen.png create mode 100644 src/assets/hustler/detail/gross-kappe-warten.png create mode 100644 src/assets/hustler/detail/gross-krone-arbeiten.png create mode 100644 src/assets/hustler/detail/gross-krone-nicht.png create mode 100644 src/assets/hustler/detail/gross-krone-schlafen.png create mode 100644 src/assets/hustler/detail/gross-krone-warten.png create mode 100644 src/assets/hustler/detail/gross-muetze-arbeiten.png create mode 100644 src/assets/hustler/detail/gross-muetze-nicht.png create mode 100644 src/assets/hustler/detail/gross-muetze-schlafen.png create mode 100644 src/assets/hustler/detail/gross-muetze-warten.png create mode 100644 src/assets/hustler/detail/monitor-gross.png create mode 100644 src/assets/hustler/detail/overseer-gross.png create mode 100644 src/assets/hustler/detail/szene-bruecke.png create mode 100644 src/assets/hustler/detail/szene-buero.png create mode 100644 src/assets/hustler/detail/szene-nicht.png create mode 100644 src/assets/hustler/detail/wirbel.png create mode 100644 src/assets/hustler/detail/zzz-gross.png create mode 100644 src/assets/hustler/figuren/bildschirm.png create mode 100644 src/assets/hustler/figuren/kappe-arbeiten.png create mode 100644 src/assets/hustler/figuren/kappe-nicht.png create mode 100644 src/assets/hustler/figuren/kappe-schlafen.png create mode 100644 src/assets/hustler/figuren/kappe-warten.png create mode 100644 src/assets/hustler/figuren/krone-arbeiten.png create mode 100644 src/assets/hustler/figuren/krone-nicht.png create mode 100644 src/assets/hustler/figuren/krone-schlafen.png create mode 100644 src/assets/hustler/figuren/krone-warten.png create mode 100644 src/assets/hustler/figuren/muetze-arbeiten.png create mode 100644 src/assets/hustler/figuren/muetze-nicht.png create mode 100644 src/assets/hustler/figuren/muetze-schlafen.png create mode 100644 src/assets/hustler/figuren/muetze-warten.png create mode 100644 src/assets/hustler/figuren/overseer-klein.png create mode 100644 src/assets/hustler/figuren/punkte.png create mode 100644 src/assets/hustler/figuren/radar.png create mode 100644 src/assets/hustler/figuren/zzz.png create mode 100644 src/assets/hustler/haus/aufzug-seil.png create mode 100644 src/assets/hustler/haus/aufzug.png create mode 100644 src/assets/hustler/haus/haus-fenster-a.png create mode 100644 src/assets/hustler/haus/haus-fenster-b.png create mode 100644 src/assets/hustler/haus/haus-fenster-c.png create mode 100644 src/assets/hustler/haus/haus.png create mode 100644 src/assets/hustler/haus/raum-grau-aerzte.png create mode 100644 src/assets/hustler/haus/raum-grau-foren.png create mode 100644 src/assets/hustler/haus/raum-grau-handwerk.png create mode 100644 src/assets/hustler/haus/raum-grau-kleinanzeigen.png create mode 100644 src/assets/hustler/haus/raum-grau-laden.png create mode 100644 src/assets/hustler/haus/raum-grau-netz.png create mode 100644 src/assets/hustler/haus/raum-grau-portale.png create mode 100644 src/assets/hustler/haus/raum-grau-selbst.png create mode 100644 src/assets/hustler/haus/schild-a.png create mode 100644 src/assets/hustler/haus/schild-b.png create mode 100644 src/assets/hustler/keller/figuren/arne-ablauf.png create mode 100644 src/assets/hustler/keller/figuren/buch-stempel.png create mode 100644 src/assets/hustler/keller/figuren/maus.png create mode 100644 src/assets/hustler/keller/figuren/neu-funke.png create mode 100644 src/assets/hustler/keller/figuren/rohrpost.png create mode 100644 src/assets/hustler/keller/figuren/schublade.png create mode 100644 src/assets/hustler/keller/szene/dampf-tasse.png create mode 100644 src/assets/hustler/keller/szene/fenster-kreuz.png create mode 100644 src/assets/hustler/keller/szene/fenster-passant.png create mode 100644 src/assets/hustler/keller/szene/fensterschnee.png create mode 100644 src/assets/hustler/keller/szene/keller-licht.png create mode 100644 src/assets/hustler/keller/szene/keller.png create mode 100644 src/assets/hustler/keller/szene/licht-halle.png create mode 100644 src/assets/hustler/keller/szene/licht-lampe-236.png create mode 100644 src/assets/hustler/keller/szene/licht-lampe-92.png create mode 100644 src/assets/hustler/keller/szene/licht-schreibtisch.png create mode 100644 src/assets/hustler/keller/szene/staub.png create mode 100644 src/assets/hustler/keller/ui/akte-icon.png create mode 100644 src/assets/hustler/keller/ui/akte.png create mode 100644 src/assets/hustler/keller/ui/alle.png create mode 100644 src/assets/hustler/keller/ui/brief-x.png create mode 100644 src/assets/hustler/keller/ui/brief-zurueck.png create mode 100644 src/assets/hustler/keller/ui/buch.png create mode 100644 src/assets/hustler/keller/ui/doppelt.png create mode 100644 src/assets/hustler/keller/ui/etikett-leer.png create mode 100644 src/assets/hustler/keller/ui/fremd.png create mode 100644 src/assets/hustler/keller/ui/haken.png create mode 100644 src/assets/hustler/keller/ui/kalender.png create mode 100644 src/assets/hustler/keller/ui/kette.png create mode 100644 src/assets/hustler/keller/ui/lupe.png create mode 100644 src/assets/hustler/keller/ui/messing.png create mode 100644 src/assets/hustler/keller/ui/pause.png create mode 100644 src/assets/hustler/keller/ui/pfeil-links.png create mode 100644 src/assets/hustler/keller/ui/pfeil-rechts.png create mode 100644 src/assets/hustler/keller/ui/pfeil-runter.png create mode 100644 src/assets/hustler/keller/ui/quelle.png create mode 100644 src/assets/hustler/keller/ui/raum-aerzte.png create mode 100644 src/assets/hustler/keller/ui/raum-handwerk.png create mode 100644 src/assets/hustler/keller/ui/raum-laden.png create mode 100644 src/assets/hustler/keller/ui/raum-selbst.png create mode 100644 src/assets/hustler/keller/ui/robo.png create mode 100644 src/assets/hustler/keller/ui/schere.png create mode 100644 src/assets/hustler/keller/ui/schliessen.png create mode 100644 src/assets/hustler/keller/ui/schloss.png create mode 100644 src/assets/hustler/keller/ui/seite-kaputt.png create mode 100644 src/assets/hustler/keller/ui/stempel.png create mode 100644 src/assets/hustler/keller/ui/stopp.png create mode 100644 src/assets/hustler/keller/ui/telefon.png create mode 100644 src/assets/hustler/keller/ui/uhr.png create mode 100644 src/assets/hustler/keller/ui/waage-kaputt.png create mode 100644 src/assets/hustler/keller/ui/zurueck.png create mode 100644 src/assets/hustler/pult/eigen/lichterkette-0.png create mode 100644 src/assets/hustler/pult/eigen/lichterkette-1.png create mode 100644 src/assets/hustler/pult/eigen/lichterkette-2.png create mode 100644 src/assets/hustler/pult/figuren/aufzug-ding.png create mode 100644 src/assets/hustler/pult/figuren/aufzug-innen.png create mode 100644 src/assets/hustler/pult/figuren/aufzug-licht.png create mode 100644 src/assets/hustler/pult/figuren/aufzug-ruftaste.png create mode 100644 src/assets/hustler/pult/figuren/aufzug-tuer.png create mode 100644 src/assets/hustler/pult/figuren/boten-ablauf.png create mode 100644 src/assets/hustler/pult/figuren/briefkasten-funke.png create mode 100644 src/assets/hustler/pult/figuren/briefkasten.png create mode 100644 src/assets/hustler/pult/figuren/monitor.png create mode 100644 src/assets/hustler/pult/szene/dampf-tasse.png create mode 100644 src/assets/hustler/pult/szene/fenster-sprossen-1.png create mode 100644 src/assets/hustler/pult/szene/fenster-sprossen-2.png create mode 100644 src/assets/hustler/pult/szene/fenster2-maske.png create mode 100644 src/assets/hustler/pult/szene/fensterschnee.png create mode 100644 src/assets/hustler/pult/szene/licht-mond.png create mode 100644 src/assets/hustler/pult/szene/licht-pendel-232.png create mode 100644 src/assets/hustler/pult/szene/licht-pendel-55.png create mode 100644 src/assets/hustler/pult/szene/licht-rechts.png create mode 100644 src/assets/hustler/pult/szene/licht-schreibtisch.png create mode 100644 src/assets/hustler/pult/szene/pult.png create mode 100644 src/assets/hustler/pult/ui/ansehen.png create mode 100644 src/assets/hustler/pult/ui/antworten.png create mode 100644 src/assets/hustler/pult/ui/art-bau-kaputt.png create mode 100644 src/assets/hustler/pult/ui/art-beschwerde.png create mode 100644 src/assets/hustler/pult/ui/art-mail.png create mode 100644 src/assets/hustler/pult/ui/art-mensch.png create mode 100644 src/assets/hustler/pult/ui/art-plattform.png create mode 100644 src/assets/hustler/pult/ui/art-preisfrage.png create mode 100644 src/assets/hustler/pult/ui/art-unsicher.png create mode 100644 src/assets/hustler/pult/ui/art-vorschau.png create mode 100644 src/assets/hustler/pult/ui/bauen.png create mode 100644 src/assets/hustler/pult/ui/brief.png create mode 100644 src/assets/hustler/pult/ui/briefkasten.png create mode 100644 src/assets/hustler/pult/ui/einfuegen.png create mode 100644 src/assets/hustler/pult/ui/erledigt.png create mode 100644 src/assets/hustler/pult/ui/gesendet.png create mode 100644 src/assets/hustler/pult/ui/klammer.png create mode 100644 src/assets/hustler/pult/ui/klemmbrett.png create mode 100644 src/assets/hustler/pult/ui/kopf-kappe.png create mode 100644 src/assets/hustler/pult/ui/kopf-krone.png create mode 100644 src/assets/hustler/pult/ui/kopf-muetze.png create mode 100644 src/assets/hustler/pult/ui/kopieren.png create mode 100644 src/assets/hustler/pult/ui/oeffnen.png create mode 100644 src/assets/hustler/pult/ui/person.png create mode 100644 src/assets/hustler/pult/ui/sanduhr.png create mode 100644 src/assets/hustler/pult/ui/uebernehmen.png create mode 100644 src/assets/hustler/pult/ui/umschlag-offen.png create mode 100644 src/assets/hustler/pult/ui/umschlag.png create mode 100644 src/assets/hustler/pult/ui/veraltet.png create mode 100644 src/assets/hustler/pult/ui/verwerfen.png create mode 100644 src/assets/hustler/ui/blase.png create mode 100644 src/assets/hustler/ui/glocke.png create mode 100644 src/assets/hustler/ui/knopf.png create mode 100644 src/assets/hustler/ui/muenze-gruen.png create mode 100644 src/assets/hustler/ui/muenze-rot.png create mode 100644 src/assets/hustler/ui/panel.png create mode 100644 src/assets/hustler/ui/pause.png create mode 100644 src/assets/hustler/ui/pfeil-links.png create mode 100644 src/assets/hustler/ui/pfeil-rechts.png create mode 100644 src/assets/hustler/ui/pokal.png create mode 100644 src/assets/hustler/ui/robo.png create mode 100644 src/assets/hustler/ui/schliessen.png create mode 100644 src/assets/hustler/ui/stern.png create mode 100644 src/assets/hustler/ui/zahnrad.png create mode 100644 src/assets/hustler/welt/laternen-licht.png create mode 100644 src/assets/hustler/welt/mond.png create mode 100644 src/assets/hustler/welt/schnee-1.png create mode 100644 src/assets/hustler/welt/schnee-2.png create mode 100644 src/assets/hustler/welt/schnee-3.png create mode 100644 src/assets/hustler/welt/sterne-funkeln.png create mode 100644 src/assets/hustler/welt/sterne-verteilt.png create mode 100644 src/assets/hustler/welt/welt-licht-1.png create mode 100644 src/assets/hustler/welt/welt-licht-2.png create mode 100644 src/assets/hustler/welt/welt-licht-3.png create mode 100644 src/assets/hustler/welt/welt-licht-4.png create mode 100644 src/assets/hustler/welt/welt.png create mode 100644 src/lib/hustlerWelt.js create mode 100644 src/styles/hustler/hochhaus.css create mode 100644 src/styles/hustler/keller.css create mode 100644 src/styles/hustler/pult.css create mode 100644 src/styles/hustler/schrift.css create mode 100644 tests/unit/hustlerAssets.test.mjs create mode 100644 tests/unit/hustlerImportCss.test.mjs create mode 100644 tests/unit/hustlerWelt.test.mjs diff --git a/public/fonts/OFL-PressStart2P.txt b/public/fonts/OFL-PressStart2P.txt new file mode 100644 index 0000000..5891186 --- /dev/null +++ b/public/fonts/OFL-PressStart2P.txt @@ -0,0 +1,93 @@ +Copyright 2012 The Press Start 2P Project Authors (cody@zone38.net), with Reserved Font Name "Press Start 2P" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/public/fonts/PressStart2P-Regular.woff2 b/public/fonts/PressStart2P-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..947a9792df962367ce85b81182fdb07be1eaced1 GIT binary patch literal 12512 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 literal 0 HcmV?d00001 diff --git a/scripts/hustler-import.mjs b/scripts/hustler-import.mjs new file mode 100644 index 0000000..a720fa7 --- /dev/null +++ b/scripts/hustler-import.mjs @@ -0,0 +1,954 @@ +#!/usr/bin/env node +// Spielt die Pixel-Oberfläche „Hustler“ aus der Claude-Design-Zeichenfläche (pixelwelt-v2) in WOMS ein. +// +// Aufruf: node scripts/hustler-import.mjs [] +// +// Erzeugt (alles deterministisch, ein zweiter Lauf ändert keine Datei): +// src/assets/hustler/ die benutzten PNGs (Vite hängt beim Bauen einen Hash an) +// src/styles/hustler/{hochhaus,keller,pult}.css das CSS der drei Artboards, auf WOMS zugeschnitten +// src/lib/hustlerWelt.js Geometrie des Hochhauses (reine Daten, eingefroren) +// +// Der optionale zweite Ordner (pixelwelt-woms) enthält das neu gezeichnete Haus. Eine Datei dort unter +// haus/ ersetzt die gleichnamige aus pixelwelt-v2/haus/. Nennt dort haus/manifest.json „graueRaeume“, +// entstehen zusätzlich die Ebenen für abgedunkelte Räume (CSS-Klassen hk-raum-grau- und GRAUE_RAEUME). +// Erwartetes Format: graueRaeume = { "": { "file": "raum-grau-.png", "x": …, "y": …, "w": …, "h": … } } +// (x/y in Weltkoordinaten wie alle Werte der Haus-Manifeste; file, w und h sind optional – ohne Angabe gilt +// raum-grau-.png in seiner Pixelgröße). Statt eines Objekts geht auch eine Liste mit „raum“ je Eintrag. +// +// Nur Node-Bordmittel. Schreibt ausschließlich in die drei Zielorte oben; gelöscht werden höchstens alte +// Ebenen src/assets/hustler/haus/raum-grau-*.png, die der aktuelle Lauf nicht mehr erzeugt. + +import { createHash } from 'node:crypto' +import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const HIER = path.dirname(fileURLToPath(import.meta.url)) +const REPO = path.resolve(HIER, '..') +const ROSTER_DATEI = path.join(REPO, 'src/lib/workersRoster.js') +const QUELLEN_DATEI = path.join(HIER, 'hustler-import/quellen.json') + +// Ausgabeorte (relativ zur Repo-Wurzel, mit /) +const ASSETS = 'src/assets/hustler' +const STYLES = 'src/styles/hustler' +const WELT_DATEI = 'src/lib/hustlerWelt.js' + +// Die drei Bildschirme: Artboard, Blob-Zuordnung, Wurzelklasse, Keyframe-Vorsatz, Ausgabedatei +export const BILDSCHIRME = Object.freeze({ + hochhaus: { artboard: 'Main.dc.html', titel: 'Hochhaus', html: 'entwurf/Main.dc.html', blobs: 'entwurf/blobs.json', wurzel: 'hk-hochhaus', kfVorsatz: 'hk-h-', css: 'hochhaus.css' }, + keller: { artboard: 'BotLeads.dc.html', titel: 'Keller', html: 'keller/entwurf/BotLeads.dc.html', blobs: 'keller/entwurf/blobs.json', wurzel: 'hk-keller', kfVorsatz: 'hk-k-', css: 'keller.css' }, + pult: { artboard: 'Freigabe.dc.html', titel: 'Pult', html: 'pult/entwurf/Freigabe.dc.html', blobs: 'pult/entwurf/blobs.json', wurzel: 'hk-pult', kfVorsatz: 'hk-p-', css: 'pult.css' }, +}) + +// Raum-Schlüssel im Haus-Manifest -> Raum-Schlüssel in ROOM_TITLES (workersRoster.js) +const RAUM_NACH_ROSTER = Object.freeze({ + bruecke: 'bruecke', radar: 'netz', klein: 'kleinanzeigen', portale: 'portale', foren: 'foren', + aerzte: 'aerzte', handwerk: 'handwerk', selbst: 'selbst', laeden: 'laden', waage: 'waage', + mail: 'mail', forum: 'forum', post: 'posteingang', abschluss: 'abschluss', + tisch1: 'tisch1', tisch2: 'tisch2', tisch3: 'tisch3', pruef: 'pruefung', + freigabe: 'pult', tresor: 'tresor', keller: 'archiv', +}) + +export class ImportFehler extends Error { + constructor(text) { + super(text) + this.name = 'ImportFehler' + } +} +const fehler = (text) => new ImportFehler(text) + +const sha256 = (daten) => createHash('sha256').update(daten).digest('hex') +const kurz = (text, n = 70) => { + const t = String(text).replace(/\s+/g, ' ').trim() + return t.length > n ? `${t.slice(0, n)}…` : t +} + +// ───────────────────────────── CSS-Tokenizer ───────────────────────────── +// Kleiner, strenger Zerleger: Kommentare, Zeichenketten, url(…), verschachtelte @media/@supports, @keyframes. + +function stringEnde(s, i) { + const q = s[i] + let j = i + 1 + while (j < s.length) { + const c = s[j] + if (c === '\\') { j += 2; continue } + if (c === q) return j + 1 + if (c === '\n') break + j += 1 + } + throw fehler(`CSS: Zeichenkette ohne Ende bei „${kurz(s.slice(i, i + 40))}“.`) +} + +// Entfernt /* … */ (Zeichenketten und ungequotetes url(…) bleiben unberührt) +function ohneKommentare(s) { + let aus = '' + let i = 0 + while (i < s.length) { + const c = s[i] + if (c === '"' || c === "'") { const e = stringEnde(s, i); aus += s.slice(i, e); i = e; continue } + if (c === '\\') { aus += s.slice(i, i + 2); i += 2; continue } + if (c === '/' && s[i + 1] === '*') { + const e = s.indexOf('*/', i + 2) + if (e < 0) throw fehler('CSS: Kommentar ohne Ende.') + aus += ' ' + i = e + 2 + continue + } + if ((c === 'u' || c === 'U') && /^url\(\s*[^\s"')]/i.test(s.slice(i, i + 12)) && !/[\w-]/.test(s[i - 1] ?? ' ')) { + const e = s.indexOf(')', i) + if (e < 0) throw fehler('CSS: url( ohne schließende Klammer.') + aus += s.slice(i, e + 1) + i = e + 1 + continue + } + aus += c + i += 1 + } + return aus +} + +// Erste Stelle ab i, an der ein Zeichen aus `stopps` auf oberster Ebene steht (außerhalb von (), [] und Zeichenketten) +function sucheOben(s, i, stopps) { + let klammer = 0 + let eckig = 0 + while (i < s.length) { + const c = s[i] + if (c === '"' || c === "'") { i = stringEnde(s, i); continue } + if (c === '\\') { i += 2; continue } + if (c === '(') klammer += 1 + else if (c === ')') klammer = Math.max(0, klammer - 1) + else if (c === '[') eckig += 1 + else if (c === ']') eckig = Math.max(0, eckig - 1) + else if (klammer === 0 && eckig === 0 && stopps.includes(c)) return i + i += 1 + } + return -1 +} + +function teileOben(s, trenner) { + const teile = [] + let start = 0 + for (;;) { + const e = sucheOben(s, start, trenner) + if (e < 0) { teile.push(s.slice(start)); return teile } + teile.push(s.slice(start, e)) + start = e + 1 + } +} + +// Index der Klammer, die zu s[offen] ('(' oder '[') gehört; Zeichenketten und Escapes zählen nicht mit +function passende(s, offen) { + const auf = s[offen] + const zu = auf === '(' ? ')' : ']' + let tiefe = 0 + for (let i = offen; i < s.length; i += 1) { + const c = s[i] + if (c === '"' || c === "'") { i = stringEnde(s, i) - 1; continue } + if (c === '\\') { i += 1; continue } + if (c === auf) tiefe += 1 + else if (c === zu) { + tiefe -= 1 + if (tiefe === 0) return i + } + } + throw fehler(`CSS: „${auf}“ ohne passende schließende Klammer in „${kurz(s.slice(offen))}“.`) +} + +const GRUPPEN_REGELN = new Set(['media', 'supports', 'layer', 'container']) + +// → { knoten, ende }; knoten: { art: 'regel'|'gruppe'|'keyframes'|'weg', … } +function parseListe(s, start, innen) { + const knoten = [] + let i = start + for (;;) { + while (i < s.length && /\s/.test(s[i])) i += 1 + if (i >= s.length) { + if (innen) throw fehler('CSS: Block ohne schließende Klammer.') + return { knoten, ende: i } + } + if (s[i] === '}') { + if (!innen) throw fehler('CSS: unerwartete schließende Klammer.') + return { knoten, ende: i + 1 } + } + const stopp = sucheOben(s, i, ';{}') + if (stopp < 0) throw fehler(`CSS: Regel „${kurz(s.slice(i))}“ endet nicht.`) + const kopf = s.slice(i, stopp).trim() + if (s[stopp] === '}') throw fehler(`CSS: „${kurz(kopf)}“ steht ohne Block.`) + if (s[stopp] === ';') { + // @import/@charset ohne Block entfallen, andere Anweisungen kennen wir nicht + if (/^@(import|charset)\b/i.test(kopf)) { knoten.push({ art: 'weg' }); i = stopp + 1; continue } + throw fehler(`CSS: nicht unterstützte Anweisung „${kurz(kopf)}“.`) + } + // Block + if (kopf.startsWith('@')) { + const name = (kopf.match(/^@([A-Za-z-]+)/)?.[1] ?? '').toLowerCase() + if (name === 'font-face' || name === 'import') { + const e = blockEnde(s, stopp) + knoten.push({ art: 'weg' }) + i = e + 1 + continue + } + if (/(^|-)keyframes$/.test(name)) { + const kfName = kopf.slice(kopf.indexOf(name) + name.length).trim() + if (!/^[A-Za-z_][\w-]*$/.test(kfName)) throw fehler(`CSS: ungewöhnlicher Keyframe-Name „${kurz(kfName)}“.`) + const { knoten: bilder, ende } = parseListe(s, stopp + 1, true) + knoten.push({ art: 'keyframes', at: `@${name}`, name: kfName, bilder }) + i = ende + continue + } + if (GRUPPEN_REGELN.has(name)) { + const { knoten: kinder, ende } = parseListe(s, stopp + 1, true) + knoten.push({ art: 'gruppe', kopf, kinder }) + i = ende + continue + } + throw fehler(`CSS: nicht unterstützte At-Regel „${kurz(kopf)}“.`) + } + const e = blockEnde(s, stopp) + knoten.push({ art: 'regel', selektor: kopf, koerper: s.slice(stopp + 1, e) }) + i = e + 1 + } +} + +// Index der schließenden Klammer zum Block, der bei s[offen] === '{' beginnt (ohne Verschachtelung) +function blockEnde(s, offen) { + const e = sucheOben(s, offen + 1, '{}') + if (e < 0) throw fehler('CSS: Block ohne schließende Klammer.') + if (s[e] === '{') throw fehler(`CSS: verschachtelte Regel in „${kurz(s.slice(offen + 1, e))}“ wird nicht unterstützt.`) + return e +} + +// ───────────────────────────── Selektoren ───────────────────────────── + +function istIdentStart(s, j) { + const c = s[j] + if (c === undefined) return false + if (/[A-Za-z_\\]/.test(c) || c.charCodeAt(0) > 127) return true + return c === '-' && /[A-Za-z_\\-]/.test(s[j + 1] ?? '') +} + +// Jede Klasse (.name) bekommt hk- davor; Zeichenketten und [attribute] bleiben unberührt +function praefixKlassen(s) { + let aus = '' + let i = 0 + while (i < s.length) { + const c = s[i] + if (c === '"' || c === "'") { const e = stringEnde(s, i); aus += s.slice(i, e); i = e; continue } + if (c === '\\') { aus += s.slice(i, i + 2); i += 2; continue } + if (c === '[') { + const ende = passende(s, i) + aus += s.slice(i, ende + 1) + i = ende + 1 + continue + } + if (c === '.' && istIdentStart(s, i + 1)) { aus += '.hk-'; i += 1; continue } + aus += c + i += 1 + } + return aus +} + +const WURZEL_ANFANG = /^(?:html|body|:root)(?![\w-])/i +const WURZEL_FOLGE = /^\s*(?:>\s*)?(?=(?:html|body|:root)(?![\w-]))/i + +export function wandleSelektor(selektor, wurzel) { + let rest = selektor.trim() + if (!rest) throw fehler('CSS: leerer Selektor.') + let istWurzel = false + while (WURZEL_ANFANG.test(rest)) { + istWurzel = true + rest = rest.replace(WURZEL_ANFANG, '') + const folge = rest.match(WURZEL_FOLGE) + if (!folge) break + rest = rest.slice(folge[0].length) + } + if (/(^|[\s>+~(,])(?:html|body|:root)(?![\w-])/i.test(rest)) { + throw fehler(`CSS: html/body/:root mitten im Selektor „${kurz(selektor)}“ lässt sich nicht sicher umformen.`) + } + const mitKlassen = praefixKlassen(rest) + return istWurzel ? `.${wurzel}${mitKlassen}` : `.${wurzel} ${mitKlassen}` +} + +function wandleSelektorListe(liste, wurzel) { + const neu = [] + for (const teil of teileOben(liste, ',')) { + const s = wandleSelektor(teil, wurzel) + if (!neu.includes(s)) neu.push(s) + } + return neu.join(',') +} + +// ───────────────────────────── Werte ───────────────────────────── + +const BLOB_ADRESSE = /^\/_blob\/([0-9A-Za-z]+)$/ + +function blobInhalt(inhalt, blobPfad) { + const m = inhalt.trim().match(BLOB_ADRESSE) + if (m) return blobPfad(m[1]) + if (inhalt.includes('/_blob/')) throw fehler(`CSS: unerwartete Blob-Adresse „${kurz(inhalt)}“.`) + return inhalt +} + +// url(/_blob/…) und Zeichenketten mit Blob-Adresse -> Asset-Pfad +function ersetzeBlobs(wert, blobPfad) { + let aus = '' + let i = 0 + while (i < wert.length) { + const c = wert[i] + if (c === '"' || c === "'") { + const e = stringEnde(wert, i) + aus += c + blobInhalt(wert.slice(i + 1, e - 1), blobPfad) + c + i = e + continue + } + if (c === '\\') { aus += wert.slice(i, i + 2); i += 2; continue } + if ((c === 'u' || c === 'U') && /^url\(/i.test(wert.slice(i, i + 4)) && !/[\w-]/.test(wert[i - 1] ?? ' ')) { + let j = i + 4 + while (j < wert.length && /\s/.test(wert[j])) j += 1 + if (wert[j] === '"' || wert[j] === "'") { aus += wert.slice(i, j); i = j; continue } // die Zeichenkette folgt in der Schleife + const e = wert.indexOf(')', j) + if (e < 0) throw fehler(`CSS: url( ohne schließende Klammer in „${kurz(wert)}“.`) + aus += `url(${blobInhalt(wert.slice(j, e), blobPfad)})` + i = e + 1 + continue + } + aus += c + i += 1 + } + return aus +} + +const ZAHL = /[+-]?(?:\d*\.\d+|\d+\.?)(?:[eE][+-]?\d+)?[A-Za-z%]*/y +const IDENT = /(?:[A-Za-z_]|[^\u0000-\u007f]|\\.)(?:[\w-]|[^\u0000-\u007f]|\\.)*|-(?:[A-Za-z_-]|[^\u0000-\u007f]|\\.)(?:[\w-]|[^\u0000-\u007f]|\\.)*/y + +// Bekannte Keyframe-Namen in animation/animation-name umbenennen; Schlüsselwörter, Zahlen mit Einheit +// (3200ms, -1.3s, .5s) und Funktionen (steps(1,end)) bleiben unberührt +function benenneAnimationen(wert, umbenennen) { + let aus = '' + let i = 0 + while (i < wert.length) { + const c = wert[i] + if (c === '"' || c === "'") { const e = stringEnde(wert, i); aus += wert.slice(i, e); i = e; continue } + ZAHL.lastIndex = i + const zahl = ZAHL.exec(wert) + if (zahl) { + aus += zahl[0] + i += zahl[0].length + continue + } + IDENT.lastIndex = i + const ident = IDENT.exec(wert) + if (ident) { + const name = ident[0] + i += name.length + if (wert[i] === '(') { + // Funktion (steps, cubic-bezier, var …): unverändert bis zur passenden Klammer + const ende = passende(wert, i) + aus += name + wert.slice(i, ende + 1) + i = ende + 1 + } else { + aus += umbenennen.has(name) ? umbenennen.get(name) : name + } + continue + } + aus += c + i += 1 + } + return aus +} + +function wandleDeklarationen(koerper, ctx) { + const teile = [] + for (const roh of teileOben(koerper, ';')) { + const d = roh.trim() + if (!d) continue + const k = sucheOben(d, 0, ':') + if (k < 0) throw fehler(`CSS: Deklaration ohne „:“ in „${kurz(d)}“.`) + const eigenschaft = d.slice(0, k).trim() + let wert = d.slice(k + 1).trim().replace(/\s*\n\s*/g, ' ') + wert = ersetzeBlobs(wert, ctx.blobPfad) + if (/^(?:-webkit-)?animation(?:-name)?$/i.test(eigenschaft)) wert = benenneAnimationen(wert, ctx.kfNamen) + teile.push(`${eigenschaft}:${wert}`) + } + return teile.join('; ') +} + +function sammleKeyframes(knoten, namen) { + for (const k of knoten) { + if (k.art === 'keyframes') namen.add(k.name) + else if (k.art === 'gruppe') sammleKeyframes(k.kinder, namen) + } +} + +function schreibeKnoten(knoten, ctx, zeilen) { + for (const k of knoten) { + if (k.art === 'weg') continue + if (k.art === 'regel') { + zeilen.push(`${wandleSelektorListe(k.selektor, ctx.wurzel)}{${wandleDeklarationen(k.koerper, ctx)}}`) + } else if (k.art === 'keyframes') { + const bilder = k.bilder.map((b) => { + if (b.art !== 'regel') throw fehler(`CSS: unerwarteter Inhalt in @keyframes ${k.name}.`) + return `${b.selektor.replace(/\s+/g, ' ')}{${wandleDeklarationen(b.koerper, ctx)}}` + }) + zeilen.push(`${k.at} ${ctx.kfNamen.get(k.name)}{${bilder.join('')}}`) + } else if (k.art === 'gruppe') { + const innen = [] + schreibeKnoten(k.kinder, ctx, innen) + zeilen.push(`${k.kopf.replace(/\s+/g, ' ')}{`, ...innen, '}') + } + } +} + +/** + * Formt das CSS eines Artboards für WOMS um. + * @param {string} css zusammengesetzter Inhalt der ', '.neu{background:url(/_blob/ffffffffffffffffffffffffffffffff)}')) + await assert.rejects(baue({ v2: tmp }), (e) => e instanceof ImportFehler && /BotLeads\.dc\.html steht \/_blob\/ffffffffffffffffffffffffffffffff, aber .*blobs\.json kennt diese Adresse nicht/.test(e.message)) + } finally { + loesche(tmp) + } +}) + +test('baue mit pixelwelt-woms: gleichnamige Haus-Dateien ersetzen die aus v2, graue Räume werden Klassen und Daten, alte Ebenen verschwinden wieder', opt, async () => { + const tmp = mkdtempSync(path.join(os.tmpdir(), 'hustler-woms-')) + const wurzel = mkdtempSync(path.join(os.tmpdir(), 'hustler-wurzel-')) + try { + mkdirSync(path.join(tmp, 'haus')) + cpSync(path.join(V2, 'haus/schild-a.png'), path.join(tmp, 'haus/haus.png')) + cpSync(path.join(V2, 'ui/panel.png'), path.join(tmp, 'haus/raum-grau-netz.png')) + cpSync(path.join(V2, 'ui/knopf.png'), path.join(tmp, 'haus/raum-grau-kleinanzeigen.png')) + writeFileSync(path.join(tmp, 'haus/manifest.json'), JSON.stringify({ graueRaeume: { netz: { x: 1040, y: 558, w: 24, h: 24 }, kleinanzeigen: { file: 'raum-grau-kleinanzeigen.png', x: 1140, y: 558 } } })) + const ohne = await baue({ v2: V2 }) + const mit = await baue({ v2: V2, woms: tmp }) + assert.notEqual(pruefsumme(ohne.dateien), pruefsumme(mit.dateien)) + assert.ok(mit.dateien.get('src/assets/hustler/haus/haus.png').equals(readFileSync(path.join(V2, 'haus/schild-a.png'))), 'haus.png stammt aus pixelwelt-woms') + assert.ok(mit.dateien.get('src/assets/hustler/haus/haus-fenster-a.png').equals(ohne.dateien.get('src/assets/hustler/haus/haus-fenster-a.png')), 'andere Dateien bleiben aus v2') + assert.deepEqual(mit.bericht.graueRaeume, ['netz', 'kleinanzeigen']) + const css = mit.dateien.get('src/styles/hustler/hochhaus.css') + assert.match(css, /\.hk-hochhaus \.hk-raum-grau-netz\{position:absolute; left:1040px; top:558px; width:24px; height:24px; background-image:url\(\.\.\/\.\.\/assets\/hustler\/haus\/raum-grau-netz\.png\); image-rendering:pixelated; pointer-events:none\}/) + assert.match(css, /\.hk-hochhaus \.hk-raum-grau-kleinanzeigen\{position:absolute; left:1140px; top:558px; width:24px; height:24px;/) + const welt = mit.dateien.get('src/lib/hustlerWelt.js') + assert.match(welt, /export const GRAUE_RAEUME = Object\.freeze\(\{\s*netz: Object\.freeze\(\{ left: 1040, top: 558, width: 24, height: 24 \}\),\s*kleinanzeigen:/) + // schreiben, nochmal schreiben (nichts mehr zu tun), dann ohne woms: die grauen Ebenen werden entfernt + const z1 = schreibe(mit, wurzel) + assert.equal(z1.geschrieben, mit.dateien.size) + assert.equal(schreibe(mit, wurzel).geschrieben, 0) + assert.ok(existsSync(path.join(wurzel, 'src/assets/hustler/haus/raum-grau-netz.png'))) + const z2 = schreibe(ohne, wurzel) + assert.deepEqual(z2.entfernt.sort(), ['raum-grau-kleinanzeigen.png', 'raum-grau-netz.png']) + assert.ok(!existsSync(path.join(wurzel, 'src/assets/hustler/haus/raum-grau-netz.png'))) + // Fehler: kein Such-Raum, falsche Größe + writeFileSync(path.join(tmp, 'haus/manifest.json'), JSON.stringify({ graueRaeume: { bruecke: { x: 1, y: 1 } } })) + await assert.rejects(baue({ v2: V2, woms: tmp }), /„bruecke“ ist kein Such-Raum/) + writeFileSync(path.join(tmp, 'haus/manifest.json'), JSON.stringify({ graueRaeume: { netz: { x: 1040, y: 558, w: 99, h: 24 } } })) + await assert.rejects(baue({ v2: V2, woms: tmp }), /99×24 im Manifest, aber raum-grau-netz\.png ist 24×24/) + // ein Ordner ohne graueRaeume ändert an den Räumen nichts + writeFileSync(path.join(tmp, 'haus/manifest.json'), JSON.stringify({})) + assert.deepEqual((await baue({ v2: V2, woms: tmp })).bericht.graueRaeume, []) + } finally { + loesche(tmp) + loesche(wurzel) + } +}) diff --git a/tests/unit/hustlerWelt.test.mjs b/tests/unit/hustlerWelt.test.mjs new file mode 100644 index 0000000..ac0697d --- /dev/null +++ b/tests/unit/hustlerWelt.test.mjs @@ -0,0 +1,167 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { loadModule } from './load.mjs' + +// Geometrie des Hochhauses (src/lib/hustlerWelt.js, vom Import-Skript erzeugt) gegen das Roster + +const W = await loadModule('src/lib/hustlerWelt.js') +const R = await loadModule('src/lib/workersRoster.js') +const { WELT, HAUS, HAUS_BILD, ETAGEN, ETAGEN_SPALTE, RAEUME, HUSTLER, SCHILDER, AUFZUG, DETAIL, GRAUE_RAEUME } = W +const { ROLES, FLOORS, ROOM_TITLES, SEARCH_ROOMS, roleOf } = R + +function tiefEingefroren(wert, pfad = 'wert') { + if (wert === null || typeof wert !== 'object') return + assert.ok(Object.isFrozen(wert), `${pfad} ist nicht eingefroren`) + for (const [k, v] of Object.entries(wert)) tiefEingefroren(v, `${pfad}.${k}`) +} + +test('Welt und Haus: Größen wie in der Zeichenfläche', () => { + assert.deepEqual({ ...WELT }, { w: 2400, h: 1200, hud: 84 }) + assert.deepEqual({ ...HAUS }, { x: 930, y: 366, w: 570, h: 706 }) + assert.deepEqual({ ...HAUS_BILD }, { x: 930, y: 370, w: 570, h: 700 }) + assert.ok(HAUS.x >= 0 && HAUS.x + HAUS.w <= WELT.w && HAUS.y >= 0 && HAUS.y + HAUS.h <= WELT.h) + assert.ok(HAUS_BILD.x >= HAUS.x && HAUS_BILD.y >= HAUS.y && HAUS_BILD.x + HAUS_BILD.w <= HAUS.x + HAUS.w && HAUS_BILD.y + HAUS_BILD.h <= HAUS.y + HAUS.h) +}) + +test('alle Geometrie-Daten sind tief eingefroren', () => { + for (const [name, wert] of Object.entries(W)) tiefEingefroren(wert, name) +}) + +test('ETAGEN: Ids gleich FLOORS in Reihenfolge, Klickbereiche lückenlos von oben nach unten', () => { + assert.deepEqual(ETAGEN.map((e) => e.id), [...FLOORS]) + for (let i = 0; i < ETAGEN.length; i += 1) { + const e = ETAGEN[i] + assert.ok(e.oben < e.unten, `${e.id}: Klickbereich leer`) + if (i > 0) assert.equal(ETAGEN[i - 1].unten, e.oben, `${ETAGEN[i - 1].id} und ${e.id} schließen nicht lückenlos an`) + const [o, u] = e.oeffnung + assert.ok(e.oben <= o && o < u && u <= e.unten, `${e.id}: Öffnung ${o}-${u} nicht im Klickbereich ${e.oben}-${e.unten}`) + } + assert.equal(ETAGEN[0].oben, 476) + assert.equal(ETAGEN.at(-1).unten, 1062) + assert.deepEqual({ ...ETAGEN_SPALTE }, { x: 960, w: 512 }) + // Etagen links vom Raum-Bereich, Spalte deckt ihn ab + assert.ok(ETAGEN_SPALTE.x <= 1040 && ETAGEN_SPALTE.x + ETAGEN_SPALTE.w >= 1440) +}) + +test('RAEUME: jeder Raum aus ROOM_TITLES hat ein Rechteck innerhalb des Hauses', () => { + assert.deepEqual(Object.keys(RAEUME).sort(), Object.keys(ROOM_TITLES).sort()) + for (const [schluessel, r] of Object.entries(RAEUME)) { + assert.ok(r.x0 < r.x1 && r.y0 < r.y1, `${schluessel}: leeres Rechteck`) + assert.ok(r.x0 >= HAUS.x && r.x1 <= HAUS.x + HAUS.w && r.y0 >= HAUS.y && r.y1 <= HAUS.y + HAUS.h, `${schluessel}: außerhalb von HAUS`) + const etage = ETAGEN.find((e) => e.id === r.etage) + assert.ok(etage, `${schluessel}: unbekannte Etage ${r.etage}`) + assert.deepEqual([r.y0, r.y1], [...etage.oeffnung], `${schluessel}: y-Bereich ungleich Öffnung der Etage`) + // Etiketten-Punkt: x0 + 20, Öffnung oben + 3 + assert.deepEqual({ ...r.etikett }, { x: r.x0 + 20, y: r.y0 + 3 }, `${schluessel}: Etikett`) + } +}) + +test('RAEUME: die Räume einer Etage füllen 1040 bis 1440 ohne Lücke und ohne Überlappung', () => { + for (const etage of FLOORS) { + const raeume = Object.values(RAEUME).filter((r) => r.etage === etage).sort((a, b) => a.x0 - b.x0) + assert.ok(raeume.length >= 1, `${etage}: kein Raum`) + assert.equal(raeume[0].x0, 1040, `${etage}: Anfang`) + assert.equal(raeume.at(-1).x1, 1440, `${etage}: Ende`) + for (let i = 1; i < raeume.length; i += 1) assert.equal(raeume[i - 1].x1, raeume[i].x0, `${etage}: Lücke oder Überlappung`) + } + // Worker-Räume je Etage wie im Roster + for (const rolle of ROLES) assert.equal(RAEUME[rolle.room].etage, rolle.floor, `${rolle.workerId}: Raum ${rolle.room} liegt auf der falschen Etage`) + assert.equal(RAEUME.pult.etage, 'freigabe') + assert.equal(RAEUME.tresor.etage, 'tresor') +}) + +test('HUSTLER: jede Rolle hat einen Eintrag, nicht mehr und nicht weniger', () => { + assert.deepEqual(Object.keys(HUSTLER).sort(), ROLES.map((r) => r.workerId).sort()) + assert.equal(Object.keys(HUSTLER).length, 19) +}) + +test('HUSTLER: Anker liegt im Rechteck des Raums (otto auf der Brücke)', () => { + for (const [id, h] of Object.entries(HUSTLER)) { + const raum = RAEUME[roleOf(id).room] + assert.ok(raum, `${id}: Raum fehlt`) + assert.ok(h.a >= raum.x0 && h.a <= raum.x1, `${id}: a=${h.a} nicht in ${raum.x0}-${raum.x1}`) + assert.ok(h.f >= raum.y0 && h.f <= raum.y1, `${id}: f=${h.f} nicht in ${raum.y0}-${raum.y1}`) + // Fußpunkt der Hustler am Schreibtisch: 4 px über der Unterkante der Öffnung (Spec der Zeichenfläche); + // der Overseer schwebt über der Holo-Konsole und steht höher + if (id === 'otto') assert.ok(h.f < raum.y1 - 4, 'Overseer schwebt über dem Boden') + else assert.equal(h.f, raum.y1 - 4, `${id}: Fuß nicht 4 px über dem Boden`) + } + assert.equal(roleOf('otto').room, 'bruecke') + assert.equal(RAEUME[roleOf('otto').room].etage, 'bruecke') +}) + +test('HUSTLER: Knopf-Rechteck = Anker (left a-12, top f-31, 24x32), Overseer eigene Größe', () => { + for (const [id, h] of Object.entries(HUSTLER)) { + if (id === 'otto') continue + assert.deepEqual({ ...h.knopf }, { left: h.a - 12, top: h.f - 31, w: 24, h: 32 }, id) + } + const o = HUSTLER.otto + assert.deepEqual({ ...o.knopf }, { left: 1236, top: 497, w: 28, h: 40 }) + // Sprite-Anker (14, 39) sitzt auf dem Anker + assert.equal(o.knopf.left + 14, o.a) + assert.equal(o.knopf.top + 39, o.f) + assert.deepEqual([o.a, o.f], [1250, 536]) +}) + +test('HUSTLER: Bildschirm bei a-40/f-26 für alle außer otto, Radar nur bei Rita', () => { + for (const [id, h] of Object.entries(HUSTLER)) { + if (id === 'otto') { + assert.equal(h.bildschirm, undefined, 'Overseer hat keinen Schreibtisch-Bildschirm') + } else { + assert.deepEqual({ ...h.bildschirm }, { left: h.a - 40, top: h.f - 26 }, id) + } + if (id === 'rita') assert.deepEqual({ ...h.radar }, { left: 1118, top: 566 }) + else assert.equal(h.radar, undefined, `${id}: unerwartetes Radar`) + } + const rita = RAEUME.netz + assert.ok(HUSTLER.rita.radar.left >= rita.x0 && HUSTLER.rita.radar.left + 20 <= rita.x1) +}) + +test('HUSTLER: Anker verschiedener Hustler im selben Raum liegen nicht aufeinander', () => { + const gesehen = new Map() + for (const [id, h] of Object.entries(HUSTLER)) { + const schluessel = `${h.a}/${h.f}` + assert.ok(!gesehen.has(schluessel), `${id} und ${gesehen.get(schluessel)} stehen auf demselben Punkt`) + gesehen.set(schluessel, id) + } +}) + +test('SCHILDER, AUFZUG und DETAIL: Positionen vorhanden und im erwarteten Bereich', () => { + const zahl = (v, was) => assert.ok(Number.isFinite(v), `${was} ist keine Zahl`) + // Briefkasten-Zähler im Pult, Tresor-/Keller-Text in ihren Räumen + assert.ok(SCHILDER.briefkasten.left >= RAEUME.pult.x0 && SCHILDER.briefkasten.top >= RAEUME.pult.y0 - 10 && SCHILDER.briefkasten.top < RAEUME.pult.y1) + assert.ok(SCHILDER.tresor.left >= RAEUME.tresor.x0 && SCHILDER.tresor.left < RAEUME.tresor.x1 && SCHILDER.tresor.top >= RAEUME.tresor.y0 && SCHILDER.tresor.top < RAEUME.tresor.y1) + assert.ok(SCHILDER.keller.left >= RAEUME.archiv.x0 && SCHILDER.keller.left < RAEUME.archiv.x1 && SCHILDER.keller.top >= RAEUME.archiv.y0 && SCHILDER.keller.top < RAEUME.archiv.y1) + // Neon-Fläche und Antenne über dem Dach (oberhalb der Brücke) + assert.ok(SCHILDER.neon.top + SCHILDER.neon.h <= RAEUME.bruecke.y0 && SCHILDER.antenne.top + SCHILDER.antenne.h <= RAEUME.bruecke.y0) + assert.deepEqual({ ...SCHILDER.neon }, { left: 1088, top: 394, w: 224, h: 32, schrift: 22 }) + assert.deepEqual({ ...SCHILDER.antenne }, { left: 1408, top: 394, w: 8, h: 6 }) + for (const [name, s] of Object.entries(SCHILDER)) for (const [k, v] of Object.entries(s)) if (typeof v === 'number') zahl(v, `SCHILDER.${name}.${k}`) + // „nicht angeschlossen“-Zettel gehören zu Such-Räumen + assert.ok(SCHILDER.nichtAngeschlossen.breite > 0) + for (const raum of Object.keys(SCHILDER.nichtAngeschlossen.raeume)) assert.ok(SEARCH_ROOMS.includes(raum), `${raum} ist kein Such-Raum`) + // Aufzug: ein Halt je Etage, Kabine startet am ersten Halt, Seil hängt über der Kabine + assert.equal(AUFZUG.halte.length, FLOORS.length) + assert.deepEqual([...AUFZUG.halte], [...AUFZUG.halte].sort((a, b) => a - b)) + assert.equal(AUFZUG.kabine.top, AUFZUG.halte[0]) + assert.ok(AUFZUG.seil.top + AUFZUG.seil.h <= AUFZUG.kabine.top + 1 && AUFZUG.seil.top < AUFZUG.kabine.top) + ETAGEN.forEach((e, i) => assert.equal(AUFZUG.halte[i] + AUFZUG.kabine.h, e.oeffnung[1], `Halt ${e.id}: Kabinenboden bündig mit dem Boden der Öffnung`)) + // Detailansicht + assert.deepEqual({ ...DETAIL.szene }, { w: 640, h: 400 }) + assert.deepEqual(Object.keys(DETAIL.figur).sort(), ['bruecke', 'buero', 'nicht']) + for (const p of [...Object.values(DETAIL.figur), DETAIL.monitor, DETAIL.dampf, DETAIL.zzz, DETAIL.wirbel, DETAIL.schild]) { + assert.ok(p.left >= 0 && p.top >= 0 && p.left < DETAIL.szene.w && p.top < DETAIL.szene.h, `Overlay außerhalb der Szene: ${JSON.stringify(p)}`) + } + assert.ok(DETAIL.schild.w > 0 && DETAIL.schild.h > 0) +}) + +test('GRAUE_RAEUME: nur Such-Räume mit sinnvoller Lage (leer, solange das neue Haus fehlt)', () => { + assert.equal(typeof GRAUE_RAEUME, 'object') + for (const [raum, g] of Object.entries(GRAUE_RAEUME)) { + assert.ok(SEARCH_ROOMS.includes(raum), `${raum} ist kein Such-Raum`) + for (const k of ['left', 'top', 'width', 'height']) assert.ok(Number.isInteger(g[k]), `${raum}.${k}`) + const r = RAEUME[raum] + assert.ok(g.left >= HAUS_BILD.x && g.left + g.width <= HAUS_BILD.x + HAUS_BILD.w && g.top >= HAUS_BILD.y && g.top + g.height <= HAUS_BILD.y + HAUS_BILD.h, `${raum}: außerhalb des Hausbilds`) + assert.ok(g.left < r.x1 && g.left + g.width > r.x0 && g.top < r.y1 && g.top + g.height > r.y0, `${raum}: Ebene trifft den Raum nicht`) + } +})