Hustler-Pixelwelt T1+T2: Import der Zeichenfläche (Bilder, CSS mit hk-Vorsatz, Geometrie), helles Haus mit grauen Raum-Ebenen, Press Start 2P selbst gehostet
- 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-<raum>)
- 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 <noreply@anthropic.com>
No files matched your search
@@ -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.
|
||||
@@ -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 <pfad-zu-pixelwelt-v2> [<pfad-zu-pixelwelt-woms>]
|
||||
//
|
||||
// Erzeugt (alles deterministisch, ein zweiter Lauf ändert keine Datei):
|
||||
// src/assets/hustler/<quellpfad ohne "entwurf/"> 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-<raum> und GRAUE_RAEUME).
|
||||
// Erwartetes Format: graueRaeume = { "<raum>": { "file": "raum-grau-<raum>.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-<raum>.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 <style>-Blöcke
|
||||
* @param {{ wurzel: string, kfVorsatz: string, blobPfad: (id: string) => string }} opt
|
||||
* @returns {{ css: string, keyframes: string[] }}
|
||||
*/
|
||||
export function wandleCss(css, { wurzel, kfVorsatz, blobPfad }) {
|
||||
const { knoten } = parseListe(ohneKommentare(css), 0, false)
|
||||
const namen = new Set()
|
||||
sammleKeyframes(knoten, namen)
|
||||
const kfNamen = new Map([...namen].map((n) => [n, `${kfVorsatz}${n}`]))
|
||||
const zeilen = []
|
||||
schreibeKnoten(knoten, { wurzel, kfNamen, blobPfad }, zeilen)
|
||||
const aus = zeilen.join('\n')
|
||||
if (aus.includes('/_blob/')) throw fehler('CSS: nach der Umformung steht noch eine Blob-Adresse im Ergebnis.')
|
||||
return { css: aus, keyframes: [...kfNamen.values()] }
|
||||
}
|
||||
|
||||
// ───────────────────────────── HTML der Artboards ─────────────────────────────
|
||||
|
||||
function styleBloecke(html, artboard) {
|
||||
const teile = [...html.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi)].map((m) => m[1])
|
||||
if (teile.length === 0) throw fehler(`${artboard}: kein <style>-Block gefunden.`)
|
||||
return teile.join('\n')
|
||||
}
|
||||
|
||||
const TAG = /<([A-Za-z][\w-]*)((?:\s+[^\s"'<>/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'<>]+))?)*)\s*\/?>/g
|
||||
const ATTR = /([^\s"'<>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'<>]+)))?/g
|
||||
|
||||
function entschluessele(t) {
|
||||
return t.replace(/ /g, '\u00a0').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/&/g, '&')
|
||||
}
|
||||
|
||||
function elemente(bereich) {
|
||||
const liste = []
|
||||
for (const m of bereich.matchAll(TAG)) {
|
||||
const attr = {}
|
||||
for (const a of m[2].matchAll(ATTR)) attr[a[1]] = a[2] ?? a[3] ?? a[4] ?? ''
|
||||
const ende = m.index + m[0].length
|
||||
const naechstes = bereich.indexOf('<', ende)
|
||||
liste.push({
|
||||
tag: m[1].toLowerCase(),
|
||||
attr,
|
||||
klassen: new Set((attr.class ?? '').split(/\s+/).filter(Boolean)),
|
||||
stil: stilMap(attr.style),
|
||||
text: entschluessele(bereich.slice(ende, naechstes < 0 ? undefined : naechstes)).trim(),
|
||||
})
|
||||
}
|
||||
return liste
|
||||
}
|
||||
|
||||
function stilMap(s = '') {
|
||||
const o = {}
|
||||
for (const d of teileOben(s, ';')) {
|
||||
const k = d.indexOf(':')
|
||||
if (k > 0) o[d.slice(0, k).trim().toLowerCase()] = d.slice(k + 1).trim()
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
function px(wert, was) {
|
||||
const m = typeof wert === 'string' ? wert.match(/^(-?\d+(?:\.\d+)?)px$/) : null
|
||||
if (!m) throw fehler(`Hochhaus: ${was} ist keine px-Angabe (${wert === undefined ? 'fehlt' : `„${wert}“`}).`)
|
||||
return Number(m[1])
|
||||
}
|
||||
|
||||
const rechteck = (e, was) => ({ left: px(e.stil.left, `${was}: left`), top: px(e.stil.top, `${was}: top`), w: px(e.stil.width, `${was}: width`), h: px(e.stil.height, `${was}: height`) })
|
||||
const punkt = (e, was) => ({ left: px(e.stil.left, `${was}: left`), top: px(e.stil.top, `${was}: top`) })
|
||||
|
||||
function genau(liste, was, bedingung) {
|
||||
const treffer = liste.filter(bedingung)
|
||||
if (treffer.length !== 1) throw fehler(`Hochhaus: ${was} kommt ${treffer.length}-mal in Main.dc.html vor, erwartet genau 1.`)
|
||||
return treffer[0]
|
||||
}
|
||||
|
||||
// ───────────────────────────── Geometrie des Hochhauses ─────────────────────────────
|
||||
|
||||
function leseHochhaus({ html, hausManifest, roster }) {
|
||||
const { FLOORS, FLOOR_TITLES, ROLES, ROOM_TITLES } = roster
|
||||
const kopfEnde = html.indexOf('</helmet>')
|
||||
const skriptStart = html.search(/<script\b[^>]*type="text\/x-dc"/i)
|
||||
if (kopfEnde < 0 || skriptStart < 0 || skriptStart < kopfEnde) throw fehler('Main.dc.html: Aufbau unbekannt (</helmet> oder das Logik-Skript fehlt).')
|
||||
const markup = elemente(html.slice(kopfEnde, skriptStart))
|
||||
const skript = html.slice(skriptStart)
|
||||
const css = styleBloecke(html, 'Main.dc.html')
|
||||
|
||||
// Welt und Haus (Kamerarahmen) wie welt() in der Logik
|
||||
const w = skript.match(/welt\(\)\s*\{\s*return\s*\{\s*w:\s*(\d+),\s*h:\s*(\d+),\s*haus:\s*\{\s*x:\s*(\d+),\s*y:\s*(\d+),\s*w:\s*(\d+),\s*h:\s*(\d+)\s*\},\s*hud:\s*(\d+)\s*\}/)
|
||||
if (!w) throw fehler('Main.dc.html: welt() in der Logik nicht gefunden.')
|
||||
const WELT = { w: Number(w[1]), h: Number(w[2]), hud: Number(w[7]) }
|
||||
const HAUS = { x: Number(w[3]), y: Number(w[4]), w: Number(w[5]), h: Number(w[6]) }
|
||||
const pl = hausManifest.placement
|
||||
const HAUS_BILD = { x: pl.x, y: pl.y, w: pl.w, h: pl.h }
|
||||
|
||||
// Etagen: Klickbereiche aus den Etagen-Knöpfen, Öffnung aus dem Haus-Manifest
|
||||
const knoepfe = markup.filter((e) => e.klassen.has('etage'))
|
||||
if (knoepfe.length !== FLOORS.length) throw fehler(`Hochhaus: ${knoepfe.length} Etagen-Knöpfe in Main.dc.html, erwartet ${FLOORS.length}.`)
|
||||
if (hausManifest.floors.length !== FLOORS.length) throw fehler(`Hochhaus: ${hausManifest.floors.length} Etagen im Haus-Manifest, erwartet ${FLOORS.length}.`)
|
||||
const spalten = new Set()
|
||||
const ETAGEN = FLOORS.map((id, i) => {
|
||||
const b = rechteck(knoepfe[i], `Etagen-Knopf ${i + 1}`)
|
||||
const beschriftung = knoepfe[i].attr['aria-label'] ?? ''
|
||||
if (!beschriftung.startsWith(FLOOR_TITLES[id])) throw fehler(`Hochhaus: Etagen-Knopf ${i + 1} heißt „${beschriftung}“, erwartet ${FLOOR_TITLES[id]} (id ${id}).`)
|
||||
spalten.add(`${b.left}/${b.w}`)
|
||||
const f = hausManifest.floors[i]
|
||||
if (f.name !== FLOOR_TITLES[id]) throw fehler(`Hochhaus: Etage ${i} heißt im Haus-Manifest „${f.name}“, erwartet ${FLOOR_TITLES[id]}.`)
|
||||
const oeffnung = [f.openingY0, f.openingY1]
|
||||
if (!(b.top <= oeffnung[0] && oeffnung[1] <= b.top + b.h)) throw fehler(`Hochhaus: Öffnung ${oeffnung.join('–')} von ${id} liegt nicht im Klickbereich.`)
|
||||
return { id, oben: b.top, unten: b.top + b.h, oeffnung }
|
||||
})
|
||||
if (spalten.size !== 1) throw fehler('Hochhaus: Die Etagen-Knöpfe stehen nicht alle in derselben Spalte.')
|
||||
const [spalteLinks, spalteBreite] = [...spalten][0].split('/').map(Number)
|
||||
ETAGEN.forEach((e, i) => {
|
||||
if (e.oben >= e.unten) throw fehler(`Hochhaus: Klickbereich von ${e.id} ist leer.`)
|
||||
if (i > 0 && ETAGEN[i - 1].unten !== e.oben) throw fehler(`Hochhaus: Klickbereiche von ${ETAGEN[i - 1].id} und ${e.id} schließen nicht lückenlos an.`)
|
||||
})
|
||||
const ETAGEN_SPALTE = { x: spalteLinks, w: spalteBreite }
|
||||
|
||||
// Räume aus dem Haus-Manifest; Etiketten-Punkt = x0 + 20 / Öffnung oben + 3, gegen die Schilder in Main.dc.html geprüft
|
||||
const raumReihe = Object.keys(ROOM_TITLES)
|
||||
const nachRoster = new Map()
|
||||
for (const r of hausManifest.rooms) {
|
||||
const schluessel = RAUM_NACH_ROSTER[r.key]
|
||||
if (!schluessel) throw fehler(`Hochhaus: Raum „${r.key}“ aus dem Haus-Manifest ist in RAUM_NACH_ROSTER unbekannt.`)
|
||||
if (nachRoster.has(schluessel)) throw fehler(`Hochhaus: Raum ${schluessel} steht doppelt im Haus-Manifest.`)
|
||||
nachRoster.set(schluessel, r)
|
||||
}
|
||||
const beschriftungen = markup.filter((e) => e.tag === 'span' && e.stil['font-size'] === '8px' && e.stil.color === '#4a5266')
|
||||
// Die drei Räume ohne Anschluss tragen in der Vorlage statt des Etiketts einen Zettel „<Raum> / nicht angeschlossen“
|
||||
const suchRaeume = roster.SEARCH_ROOMS
|
||||
const zettel = markup.filter((e) => e.tag === 'span' && e.stil.background === '#f1ede4' && suchRaeume.some((r) => ROOM_TITLES[r] === e.text))
|
||||
const zettelRaeume = {}
|
||||
let zettelBreite = null
|
||||
for (const z of zettel) {
|
||||
const raumSchluessel = suchRaeume.find((r) => ROOM_TITLES[r] === z.text)
|
||||
const breite = px(z.stil.width, `Zettel ${raumSchluessel}: width`)
|
||||
if (zettelBreite !== null && zettelBreite !== breite) throw fehler('Hochhaus: Die „nicht angeschlossen“-Zettel sind nicht gleich breit.')
|
||||
zettelBreite = breite
|
||||
zettelRaeume[raumSchluessel] = punkt(z, `Zettel ${raumSchluessel}`)
|
||||
}
|
||||
const RAEUME = {}
|
||||
for (const schluessel of raumReihe) {
|
||||
const r = nachRoster.get(schluessel)
|
||||
if (!r) throw fehler(`Hochhaus: Raum ${schluessel} fehlt im Haus-Manifest.`)
|
||||
const etage = FLOORS[r.floor]
|
||||
if (!etage) throw fehler(`Hochhaus: Raum ${schluessel} liegt auf unbekannter Etage ${r.floor}.`)
|
||||
const raum = { etage, x0: r.x, x1: r.x + r.w, y0: r.y, y1: r.y + r.h, etikett: { x: r.x + 20, y: r.y + 3 } }
|
||||
const schilder = beschriftungen.filter((e) => e.text === ROOM_TITLES[schluessel])
|
||||
if (schilder.length > 1) throw fehler(`Hochhaus: Raumschild „${ROOM_TITLES[schluessel]}“ kommt ${schilder.length}-mal in Main.dc.html vor.`)
|
||||
if (schilder.length === 1) {
|
||||
const sp = punkt(schilder[0], `Raumschild ${schluessel}`)
|
||||
if (sp.left !== raum.etikett.x || sp.top !== raum.etikett.y) {
|
||||
throw fehler(`Hochhaus: Raumschild „${ROOM_TITLES[schluessel]}“ steht in Main.dc.html bei ${sp.left}/${sp.top}, nach dem Haus-Manifest bei ${raum.etikett.x}/${raum.etikett.y}.`)
|
||||
}
|
||||
} else if (!zettelRaeume[schluessel]) {
|
||||
throw fehler(`Hochhaus: Für Raum „${ROOM_TITLES[schluessel]}“ gibt es in Main.dc.html weder ein Raumschild noch einen „nicht angeschlossen“-Zettel.`)
|
||||
}
|
||||
const eta = ETAGEN.find((e) => e.id === etage)
|
||||
if (raum.y0 !== eta.oeffnung[0] || raum.y1 !== eta.oeffnung[1]) throw fehler(`Hochhaus: Raum ${schluessel} passt nicht zur Öffnung der Etage ${etage}.`)
|
||||
RAEUME[schluessel] = raum
|
||||
}
|
||||
|
||||
// Hustler: Anker aus dem Haus-Manifest, Knopf-Rechtecke und Bildschirme aus Main.dc.html
|
||||
const hustlerKnoepfe = markup.filter((e) => e.klassen.has('hustler'))
|
||||
const bildschirme = markup.filter((e) => e.klassen.has('bildschirm') && e.klassen.has('spr')).map((e) => punkt(e, 'Bildschirm'))
|
||||
const HUSTLER = {}
|
||||
const gesehen = new Set()
|
||||
for (const rolle of ROLES) {
|
||||
const anzeigeName = rolle.workerId === 'otto' ? 'Overseer' : rolle.name
|
||||
const knopf = genau(hustlerKnoepfe, `Hustler-Knopf ${anzeigeName}`, (e) => (e.attr['aria-label'] ?? '').split(',')[0].trim() === anzeigeName)
|
||||
const k = rechteck(knopf, `Knopf ${anzeigeName}`)
|
||||
const arbeiter = hausManifest.workers.find((x) => x.name === anzeigeName)
|
||||
if (!arbeiter) throw fehler(`Hochhaus: ${anzeigeName} fehlt unter workers im Haus-Manifest.`)
|
||||
const [a, f] = arbeiter.anchor
|
||||
const eintrag = { a, f, knopf: k }
|
||||
if (rolle.workerId === 'otto') {
|
||||
const sb = arbeiter.spriteBox
|
||||
if (k.left !== sb.x || k.top !== sb.y || k.w !== sb.w || k.h !== sb.h) throw fehler('Hochhaus: Knopf des Overseers passt nicht zur Sprite-Box im Haus-Manifest.')
|
||||
} else {
|
||||
if (k.left !== a - 12 || k.top !== f - 31 || k.w !== 24 || k.h !== 32) throw fehler(`Hochhaus: Knopf von ${anzeigeName} (${k.left}/${k.top}) passt nicht zum Anker ${a}/${f}.`)
|
||||
eintrag.bildschirm = { left: a - 40, top: f - 26 }
|
||||
const s = arbeiter.screen
|
||||
if (s && (s.x !== eintrag.bildschirm.left || s.y !== eintrag.bildschirm.top)) throw fehler(`Hochhaus: Bildschirm von ${anzeigeName} passt nicht zum Haus-Manifest.`)
|
||||
}
|
||||
const raum = RAEUME[rolle.room]
|
||||
if (!(raum.x0 <= a && a <= raum.x1 && raum.y0 <= f && f <= raum.y1)) throw fehler(`Hochhaus: Anker von ${anzeigeName} liegt nicht in Raum ${rolle.room}.`)
|
||||
HUSTLER[rolle.workerId] = eintrag
|
||||
gesehen.add(knopf)
|
||||
}
|
||||
if (gesehen.size !== hustlerKnoepfe.length) throw fehler(`Hochhaus: Main.dc.html hat ${hustlerKnoepfe.length} Hustler-Knöpfe, das Roster ${ROLES.length} Rollen.`)
|
||||
for (const b of bildschirme) {
|
||||
const passt = Object.values(HUSTLER).some((h) => h.bildschirm && h.bildschirm.left === b.left && h.bildschirm.top === b.top)
|
||||
if (!passt) throw fehler(`Hochhaus: Bildschirm bei ${b.left}/${b.top} in Main.dc.html gehört zu keinem Hustler.`)
|
||||
}
|
||||
const radar = punkt(genau(markup, 'Radar', (e) => e.klassen.has('radar') && e.klassen.has('spr')), 'Radar')
|
||||
const ritaRaum = RAEUME[ROLES.find((r) => r.workerId === 'rita').room]
|
||||
if (!(ritaRaum.x0 <= radar.left && radar.left <= ritaRaum.x1 && ritaRaum.y0 <= radar.top && radar.top <= ritaRaum.y1)) throw fehler('Hochhaus: Das Radar liegt nicht in Ritas Raum.')
|
||||
HUSTLER.rita.radar = radar
|
||||
|
||||
// Schilder
|
||||
const zaehler = genau(markup, 'Briefkasten-Zähler', (e) => e.klassen.has('huepf'))
|
||||
const tresor = genau(markup, 'Tresor-Text', (e) => e.tag === 'span' && /^\d+ Deals$/.test(e.text))
|
||||
const keller = genau(markup, 'Keller-Text', (e) => e.tag === 'span' && /^\d+ Bot-Leads$/.test(e.text))
|
||||
const neon = genau(markup, 'Neon-Fläche', (e) => e.klassen.has('neon'))
|
||||
const antenne = genau(markup, 'Antenne', (e) => e.klassen.has('antenne'))
|
||||
const schrift = (e, was) => ({ schrift: px(e.stil['font-size'], `${was}: font-size`), zeile: px(e.stil['line-height'], `${was}: line-height`) })
|
||||
const SCHILDER = {
|
||||
briefkasten: { left: px(zaehler.stil.left, 'Zähler: left'), top: px(zaehler.stil.top, 'Zähler: top'), minW: px(zaehler.stil['min-width'], 'Zähler: min-width'), h: px(zaehler.stil.height, 'Zähler: height'), schrift: px(zaehler.stil['font-size'], 'Zähler: font-size') },
|
||||
tresor: { ...punkt(tresor, 'Tresor-Text'), ...schrift(tresor, 'Tresor-Text') },
|
||||
keller: { ...punkt(keller, 'Keller-Text'), ...schrift(keller, 'Keller-Text') },
|
||||
neon: { ...rechteck(neon, 'Neon'), schrift: px(neon.stil['font-size'], 'Neon: font-size') },
|
||||
antenne: rechteck(antenne, 'Antenne'),
|
||||
nichtAngeschlossen: { breite: zettelBreite, raeume: zettelRaeume },
|
||||
}
|
||||
|
||||
// Aufzug: Seil und Kabine aus Main.dc.html, Halte (Oberkante der Kabine je Etage) aus dem Haus-Manifest
|
||||
const seil = genau(markup, 'Aufzug-Seil', (e) => e.klassen.has('seil') && e.klassen.has('haus'))
|
||||
const kabine = genau(markup, 'Aufzug-Kabine', (e) => e.klassen.has('aufzug') && e.klassen.has('haus'))
|
||||
const halte = hausManifest.aufzug.stopsY
|
||||
if (!Array.isArray(halte) || halte.length !== FLOORS.length) throw fehler('Hochhaus: Aufzug-Halte im Haus-Manifest passen nicht zur Etagenzahl.')
|
||||
const AUFZUG = { seil: rechteck(seil, 'Seil'), kabine: rechteck(kabine, 'Kabine'), halte }
|
||||
if (AUFZUG.kabine.top !== halte[0]) throw fehler('Hochhaus: Die Kabine startet nicht am ersten Halt.')
|
||||
|
||||
// Detailansicht: Figurposition (Logik) und Overlays (Markup)
|
||||
const fx = skript.match(/fx:\s*p\.hut\s*===\s*'overseer'\s*\?\s*(\d+)\s*:\s*\(p\.st\s*===\s*'nicht'\s*\?\s*(\d+)\s*:\s*(\d+)\)/)
|
||||
const fy = skript.match(/fy:\s*p\.hut\s*===\s*'overseer'\s*\?\s*(\d+)\s*:\s*\(p\.st\s*===\s*'nicht'\s*\?\s*(\d+)\s*:\s*(\d+)\)/)
|
||||
if (!fx || !fy) throw fehler('Main.dc.html: Figurposition (fx/fy) in der Logik nicht gefunden.')
|
||||
const szene = css.match(/\.sz\{[^}]*?width:\s*(\d+)px;\s*height:\s*(\d+)px/)
|
||||
if (!szene) throw fehler('Main.dc.html: CSS-Regel .sz (Größe der Detail-Szene) nicht gefunden.')
|
||||
const overlay = (klasse) => punkt(genau(markup, `Overlay ${klasse}`, (e) => e.klassen.has(klasse) && e.klassen.has('spr')), `Overlay ${klasse}`)
|
||||
const schild = genau(markup, 'Detail-Schild', (e) => e.text === 'noch nicht angeschlossen')
|
||||
const DETAIL = {
|
||||
szene: { w: Number(szene[1]), h: Number(szene[2]) },
|
||||
figur: {
|
||||
bruecke: { left: Number(fx[1]), top: Number(fy[1]) },
|
||||
nicht: { left: Number(fx[2]), top: Number(fy[2]) },
|
||||
buero: { left: Number(fx[3]), top: Number(fy[3]) },
|
||||
},
|
||||
monitor: overlay('monitor-gross'),
|
||||
dampf: overlay('dampf'),
|
||||
zzz: overlay('zzz-gross'),
|
||||
wirbel: overlay('wirbel'),
|
||||
schild: rechteck(schild, 'Detail-Schild'),
|
||||
}
|
||||
|
||||
return { WELT, HAUS, HAUS_BILD, ETAGEN, ETAGEN_SPALTE, RAEUME, HUSTLER, SCHILDER, AUFZUG, DETAIL }
|
||||
}
|
||||
|
||||
// ───────────────────────────── JS-Ausgabe (reine Daten, eingefroren) ─────────────────────────────
|
||||
|
||||
const SCHLUESSEL = /^[A-Za-z_$][\w$]*$/
|
||||
const blatt = (x) => x === null || typeof x !== 'object'
|
||||
const einfach = (x) => blatt(x) || (Array.isArray(x) && x.every(blatt))
|
||||
function jsLiteral(v, ein = 0) {
|
||||
const pad = ' '.repeat(ein)
|
||||
if (Array.isArray(v)) {
|
||||
const teile = v.map((x) => jsLiteral(x, ein + 1))
|
||||
if (v.every(blatt)) return `Object.freeze([${teile.join(', ')}])`
|
||||
return `Object.freeze([\n${teile.map((t) => `${pad} ${t}`).join(',\n')},\n${pad}])`
|
||||
}
|
||||
if (v && typeof v === 'object') {
|
||||
const eintraege = Object.entries(v)
|
||||
if (eintraege.length === 0) return 'Object.freeze({})'
|
||||
const teile = eintraege.map(([k, x]) => `${SCHLUESSEL.test(k) ? k : jsLiteral(k)}: ${jsLiteral(x, ein + 1)}`)
|
||||
const flach = eintraege.every(([, x]) => einfach(x))
|
||||
if (flach && teile.join(', ').length <= 100) return `Object.freeze({ ${teile.join(', ')} })`
|
||||
return `Object.freeze({\n${teile.map((t) => `${pad} ${t}`).join(',\n')},\n${pad}})`
|
||||
}
|
||||
if (typeof v === 'string') return `'${v.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n')}'`
|
||||
if (typeof v === 'number' && !Number.isFinite(v)) throw fehler('Geometrie: keine endliche Zahl.')
|
||||
return String(v)
|
||||
}
|
||||
|
||||
function weltDatei(d, kopf) {
|
||||
const abschnitte = [
|
||||
['WELT', 'Weltgröße in Kunstpixeln; hud = Höhe der Kopfzeile, die beim Einpassen frei bleibt', d.WELT],
|
||||
['HAUS', 'Kamerarahmen um das Haus (Einpassen, „ganzes Haus“)', d.HAUS],
|
||||
['HAUS_BILD', 'Lage von haus.png und den Ebenen gleicher Größe (Fenster, Schild, graue Räume)', d.HAUS_BILD],
|
||||
['ETAGEN_SPALTE', 'Spalte der Etagen-Knöpfe (links, Breite)', d.ETAGEN_SPALTE],
|
||||
['ETAGEN', 'Etagen von oben nach unten: id wie FLOORS; oben/unten = Klickbereich (lückenlos); oeffnung = [oben, unten] des sichtbaren Innenraums', d.ETAGEN],
|
||||
['RAEUME', 'Räume nach ROOM_TITLES-Schlüssel: Rechteck x0/x1/y0/y1, Etage, Etiketten-Punkt (x0 + 20, Öffnung oben + 3)', d.RAEUME],
|
||||
['HUSTLER', 'Hustler nach workerId: Anker a/f (Fußpunkt), Knopf-Rechteck, Bildschirm-Overlay, Radar (nur Rita); otto = Overseer', d.HUSTLER],
|
||||
['SCHILDER', 'Briefkasten-Zähler, Tresor-/Keller-Text, Neon-Fläche, Antenne, „nicht angeschlossen“-Zettel', d.SCHILDER],
|
||||
['AUFZUG', 'Seil, Kabine und Halte (Oberkante der Kabine je Etage, Reihenfolge wie ETAGEN)', d.AUFZUG],
|
||||
['DETAIL', 'Detailansicht: Szenengröße, Figurposition je Szene (Büro, nicht angeschlossen, Brücke) und Overlays', d.DETAIL],
|
||||
['GRAUE_RAEUME', 'Abgedunkelte Raum-Ebenen (nur wenn das neue Haus welche liefert): Raum -> left/top/width/height', d.GRAUE_RAEUME],
|
||||
]
|
||||
const teile = [`// ${kopf}`, '// Geometrie des Hochhauses in Weltkoordinaten. Reine Daten, tief eingefroren, keine Importe.', '']
|
||||
for (const [name, hinweis, wert] of abschnitte) {
|
||||
teile.push(`// ${hinweis}`, `export const ${name} = ${jsLiteral(wert)}`, '')
|
||||
}
|
||||
return teile.join('\n')
|
||||
}
|
||||
|
||||
// ───────────────────────────── Eingaben lesen ─────────────────────────────
|
||||
|
||||
function lies(pfad, was) {
|
||||
if (!existsSync(pfad)) throw fehler(`${was} fehlt: ${pfad}`)
|
||||
return readFileSync(pfad)
|
||||
}
|
||||
|
||||
function liesJson(pfad, was) {
|
||||
const text = lies(pfad, was).toString('utf8')
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch (e) {
|
||||
throw fehler(`${was} ist kein gültiges JSON (${pfad}): ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function ladeRoster() {
|
||||
const quelle = readFileSync(ROSTER_DATEI)
|
||||
// als Daten-URL laden: die Datei hat keine Importe, und es gibt keine „Modultyp“-Warnung wegen package.json
|
||||
return import(`data:text/javascript;base64,${quelle.toString('base64')}`)
|
||||
}
|
||||
|
||||
function pngMasse(inhalt, was) {
|
||||
const kopf = inhalt.subarray(0, 24)
|
||||
if (kopf.length < 24 || kopf.toString('latin1', 1, 4) !== 'PNG' || kopf.readUInt32BE(0) !== 0x89504e47) throw fehler(`Keine gültige PNG-Datei: ${was}`)
|
||||
return { w: kopf.readUInt32BE(16), h: kopf.readUInt32BE(20) }
|
||||
}
|
||||
|
||||
function ladeHashes(v2) {
|
||||
const datei = path.join(v2, 'entwurf/hochgeladen.sha256')
|
||||
const karte = new Map()
|
||||
if (!existsSync(datei)) return karte
|
||||
for (const zeile of readFileSync(datei, 'utf8').split('\n')) {
|
||||
const m = zeile.trim().match(/^([0-9a-f]{64})\s+\*?(.+)$/)
|
||||
if (m) karte.set(m[2], m[1])
|
||||
}
|
||||
return karte
|
||||
}
|
||||
|
||||
function leseGraueRaeume({ woms, roster }) {
|
||||
if (!woms) return []
|
||||
const manifestPfad = path.join(woms, 'haus/manifest.json')
|
||||
if (!existsSync(manifestPfad)) return []
|
||||
const g = liesJson(manifestPfad, 'haus/manifest.json im Ordner pixelwelt-woms').graueRaeume
|
||||
if (g === undefined || g === null) return []
|
||||
const eintraege = Array.isArray(g) ? g.map((e) => [e?.raum ?? e?.room ?? e?.key, e]) : Object.entries(g)
|
||||
const liste = []
|
||||
for (const [raum, e] of eintraege) {
|
||||
if (!roster.SEARCH_ROOMS.includes(raum)) throw fehler(`graueRaeume: „${raum}“ ist kein Such-Raum (erlaubt: ${roster.SEARCH_ROOMS.join(', ')}).`)
|
||||
if (liste.some((x) => x.raum === raum)) throw fehler(`graueRaeume: Raum „${raum}“ steht doppelt.`)
|
||||
const datei = e.datei ?? e.file ?? `raum-grau-${raum}.png`
|
||||
if (path.basename(datei) !== datei || !/\.png$/i.test(datei)) throw fehler(`graueRaeume: ungültiger Dateiname „${datei}“ für ${raum}.`)
|
||||
const inhalt = lies(path.join(woms, 'haus', datei), `Ebene ${datei} (graueRaeume.${raum})`)
|
||||
const masse = pngMasse(inhalt, `haus/${datei}`)
|
||||
const x = e.x ?? e.left
|
||||
const y = e.y ?? e.top
|
||||
const w = e.w ?? e.width ?? masse.w
|
||||
const h = e.h ?? e.height ?? masse.h
|
||||
for (const [n, v] of [['x', x], ['y', y], ['w', w], ['h', h]]) {
|
||||
if (!Number.isInteger(v)) throw fehler(`graueRaeume.${raum}: „${n}“ fehlt oder ist keine ganze Zahl.`)
|
||||
}
|
||||
if (w !== masse.w || h !== masse.h) throw fehler(`graueRaeume.${raum}: ${w}×${h} im Manifest, aber ${datei} ist ${masse.w}×${masse.h}.`)
|
||||
liste.push({ raum, datei, x, y, w, h, inhalt })
|
||||
}
|
||||
// feste Reihenfolge wie SEARCH_ROOMS
|
||||
return roster.SEARCH_ROOMS.map((r) => liste.find((x) => x.raum === r)).filter(Boolean)
|
||||
}
|
||||
|
||||
// ───────────────────────────── Planen ─────────────────────────────
|
||||
|
||||
/**
|
||||
* Berechnet alle Ausgaben im Speicher (schreibt nichts).
|
||||
* @returns {Promise<{ dateien: Map<string, Buffer|string>, bericht: object }>} Schlüssel = Pfad relativ zur Repo-Wurzel
|
||||
*/
|
||||
export async function baue({ v2, woms = null, quellen = null }) {
|
||||
if (!existsSync(v2) || !statSync(v2).isDirectory()) throw fehler(`Der Ordner pixelwelt-v2 existiert nicht: ${v2}`)
|
||||
const tabellen = quellen ?? liesJson(QUELLEN_DATEI, 'scripts/hustler-import/quellen.json')
|
||||
const roster = await ladeRoster()
|
||||
const hashes = ladeHashes(v2)
|
||||
const wombsHaus = woms && existsSync(path.join(woms, 'haus')) ? path.join(woms, 'haus') : null
|
||||
|
||||
const dateien = new Map()
|
||||
const bericht = { bildschirme: {}, warnungen: [] }
|
||||
const quellInfo = new Map() // Quellpfad (relativ) -> { ziel, inhalt, ueberschrieben }
|
||||
|
||||
function holeQuelle(rel, wo) {
|
||||
if (quellInfo.has(rel)) return quellInfo.get(rel)
|
||||
if (typeof rel !== 'string' || !rel || path.isAbsolute(rel) || rel.split('/').includes('..') || !/\.png$/i.test(rel)) {
|
||||
throw fehler(`quellen.json: ungültiger Quellpfad „${rel}“ (${wo}).`)
|
||||
}
|
||||
let pfad = path.join(v2, rel)
|
||||
let ueberschrieben = false
|
||||
if (wombsHaus && rel.startsWith('haus/') && existsSync(path.join(wombsHaus, rel.slice('haus/'.length)))) {
|
||||
pfad = path.join(wombsHaus, rel.slice('haus/'.length))
|
||||
ueberschrieben = true
|
||||
}
|
||||
if (!existsSync(pfad) || !statSync(pfad).isFile()) throw fehler(`Quelldatei fehlt: ${rel} (${wo}); erwartet unter ${pfad}`)
|
||||
const inhalt = readFileSync(pfad)
|
||||
pngMasse(inhalt, rel)
|
||||
if (!ueberschrieben && hashes.has(rel) && sha256(inhalt) !== hashes.get(rel)) {
|
||||
throw fehler(`Prüfsumme stimmt nicht: ${rel} (entwurf/hochgeladen.sha256 nennt ${hashes.get(rel).slice(0, 12)}…, die Datei hat ${sha256(inhalt).slice(0, 12)}…).`)
|
||||
}
|
||||
const ziel = `${ASSETS}/${rel.replace(/(^|\/)entwurf\//g, '$1')}`
|
||||
const vorhanden = dateien.get(ziel)
|
||||
if (vorhanden && !vorhanden.equals(inhalt)) throw fehler(`Zwei verschiedene Quellen würden nach ${ziel} kopiert (${rel}).`)
|
||||
dateien.set(ziel, inhalt)
|
||||
const info = { ziel, inhalt, ueberschrieben }
|
||||
quellInfo.set(rel, info)
|
||||
return info
|
||||
}
|
||||
|
||||
const namen = Object.keys(BILDSCHIRME)
|
||||
for (const k of Object.keys(tabellen)) if (!namen.includes(k)) throw fehler(`quellen.json: unbekannter Bildschirm „${k}“.`)
|
||||
|
||||
const htmls = {}
|
||||
const css = {}
|
||||
for (const name of namen) {
|
||||
const cfg = BILDSCHIRME[name]
|
||||
const tabelle = tabellen[name]
|
||||
if (!tabelle) throw fehler(`quellen.json: Bildschirm „${name}“ fehlt.`)
|
||||
htmls[name] = lies(path.join(v2, cfg.html), `Artboard ${cfg.artboard}`).toString('utf8')
|
||||
const blobs = liesJson(path.join(v2, cfg.blobs), `Blob-Zuordnung ${cfg.blobs}`)
|
||||
for (const k of Object.keys(tabelle)) {
|
||||
if (!(k in blobs)) throw fehler(`quellen.json (${name}): Schlüssel „${k}“ steht nicht in ${cfg.blobs}.`)
|
||||
}
|
||||
const idZuSchluessel = new Map()
|
||||
for (const [k, v] of Object.entries(blobs)) {
|
||||
const id = String(v).replace(/^\/_blob\//, '')
|
||||
if (idZuSchluessel.has(id)) throw fehler(`${cfg.blobs}: die Adresse ${v} gehört zu zwei Schlüsseln (${idZuSchluessel.get(id)}, ${k}).`)
|
||||
idZuSchluessel.set(id, k)
|
||||
}
|
||||
const benutzt = new Set()
|
||||
const umgeformt = wandleCss(styleBloecke(htmls[name], cfg.artboard), {
|
||||
wurzel: cfg.wurzel,
|
||||
kfVorsatz: cfg.kfVorsatz,
|
||||
blobPfad: (id) => {
|
||||
const schluessel = idZuSchluessel.get(id)
|
||||
if (!schluessel) throw fehler(`Im CSS von ${cfg.artboard} steht /_blob/${id}, aber ${cfg.blobs} kennt diese Adresse nicht.`)
|
||||
const rel = tabelle[schluessel]
|
||||
if (!rel) throw fehler(`Der Schlüssel „${schluessel}“ (${cfg.blobs}, Bildschirm ${name}) fehlt in scripts/hustler-import/quellen.json.`)
|
||||
const info = holeQuelle(rel, `Schlüssel „${schluessel}“, Bildschirm ${name}`)
|
||||
benutzt.add(rel)
|
||||
return `../../${info.ziel.slice('src/'.length)}`
|
||||
},
|
||||
})
|
||||
css[name] = { text: umgeformt.css, keyframes: umgeformt.keyframes }
|
||||
bericht.bildschirme[name] = {
|
||||
dateien: benutzt.size,
|
||||
bytes: [...benutzt].reduce((s, rel) => s + quellInfo.get(rel).inhalt.length, 0),
|
||||
keyframes: umgeformt.keyframes.length,
|
||||
ueberschrieben: [...benutzt].filter((rel) => quellInfo.get(rel).ueberschrieben),
|
||||
}
|
||||
}
|
||||
|
||||
// graue Räume (optional, nur mit pixelwelt-woms)
|
||||
const graue = leseGraueRaeume({ woms, roster })
|
||||
const grauZeilen = []
|
||||
const GRAUE_RAEUME = {}
|
||||
for (const g of graue) {
|
||||
const ziel = `${ASSETS}/haus/${g.datei}`
|
||||
dateien.set(ziel, g.inhalt)
|
||||
grauZeilen.push(`.${BILDSCHIRME.hochhaus.wurzel} .hk-raum-grau-${g.raum}{position:absolute; left:${g.x}px; top:${g.y}px; width:${g.w}px; height:${g.h}px; background-image:url(../../${ziel.slice('src/'.length)}); image-rendering:pixelated; pointer-events:none}`)
|
||||
GRAUE_RAEUME[g.raum] = { left: g.x, top: g.y, width: g.w, height: g.h }
|
||||
}
|
||||
bericht.graueRaeume = graue.map((g) => g.raum)
|
||||
|
||||
// Geometrie
|
||||
const hausManifest = liesJson(path.join(v2, 'haus/manifest.json'), 'haus/manifest.json im Ordner pixelwelt-v2')
|
||||
const geometrie = leseHochhaus({ html: htmls.hochhaus, hausManifest, roster })
|
||||
geometrie.GRAUE_RAEUME = GRAUE_RAEUME
|
||||
|
||||
for (const name of namen) {
|
||||
const cfg = BILDSCHIRME[name]
|
||||
const kopf = `Erzeugt von scripts/hustler-import.mjs aus ${cfg.artboard} (${cfg.titel}), nicht von Hand ändern.`
|
||||
const zeilen = [`/* ${kopf} */`, css[name].text]
|
||||
if (name === 'hochhaus' && grauZeilen.length) zeilen.push('/* Abgedunkelte Räume (nur sichtbar, wenn der Dienst „nicht angeschlossen“ meldet) */', ...grauZeilen)
|
||||
dateien.set(`${STYLES}/${cfg.css}`, `${zeilen.join('\n')}\n`)
|
||||
}
|
||||
dateien.set(WELT_DATEI, weltDatei(geometrie, `Erzeugt von scripts/hustler-import.mjs aus ${BILDSCHIRME.hochhaus.artboard} (${BILDSCHIRME.hochhaus.titel}), nicht von Hand ändern.`))
|
||||
|
||||
bericht.einzigartig = {
|
||||
dateien: [...quellInfo.keys()].length + graue.length,
|
||||
bytes: [...quellInfo.values()].reduce((s, i) => s + i.inhalt.length, 0) + graue.reduce((s, g) => s + g.inhalt.length, 0),
|
||||
}
|
||||
return { dateien, bericht }
|
||||
}
|
||||
|
||||
// Prüfsumme über alle Ausgaben (sortierte Pfade + Inhalte): zwei Läufe mit gleichen Eingaben müssen sie gleich liefern
|
||||
export function pruefsumme(dateien) {
|
||||
const h = createHash('sha256')
|
||||
for (const pfad of [...dateien.keys()].sort()) h.update(`${pfad}\0${sha256(dateien.get(pfad))}\n`)
|
||||
return h.digest('hex')
|
||||
}
|
||||
|
||||
// ───────────────────────────── Schreiben ─────────────────────────────
|
||||
|
||||
export function schreibe(plan, wurzel = REPO) {
|
||||
const erlaubt = [ASSETS, STYLES].map((p) => path.join(wurzel, p) + path.sep)
|
||||
const zaehler = { geschrieben: 0, unveraendert: 0, entfernt: [] }
|
||||
for (const [rel, inhalt] of plan.dateien) {
|
||||
const ziel = path.join(wurzel, rel)
|
||||
if (!(rel === WELT_DATEI || erlaubt.some((e) => ziel.startsWith(e)))) throw fehler(`Interner Fehler: ${rel} liegt außerhalb der erlaubten Zielorte.`)
|
||||
const neu = typeof inhalt === 'string' ? Buffer.from(inhalt, 'utf8') : inhalt
|
||||
if (existsSync(ziel) && readFileSync(ziel).equals(neu)) { zaehler.unveraendert += 1; continue }
|
||||
mkdirSync(path.dirname(ziel), { recursive: true })
|
||||
writeFileSync(ziel, neu)
|
||||
zaehler.geschrieben += 1
|
||||
}
|
||||
// alte graue Ebenen, die dieser Lauf nicht mehr erzeugt (nur diese Dateinamen, nur dieser eine Ordner)
|
||||
const hausOrdner = path.join(wurzel, ASSETS, 'haus')
|
||||
if (existsSync(hausOrdner)) {
|
||||
for (const name of readdirSync(hausOrdner)) {
|
||||
if (/^raum-grau-[a-z0-9_-]+\.png$/i.test(name) && !plan.dateien.has(`${ASSETS}/haus/${name}`)) {
|
||||
unlinkSync(path.join(hausOrdner, name))
|
||||
zaehler.entfernt.push(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return zaehler
|
||||
}
|
||||
|
||||
// ───────────────────────────── Programm ─────────────────────────────
|
||||
|
||||
const mb = (b) => `${(b / 1024 / 1024).toFixed(2).replace('.', ',')} MB`
|
||||
|
||||
export async function main(argumente) {
|
||||
if (argumente.includes('--help') || argumente.includes('-h') || argumente.length < 1 || argumente.length > 2) {
|
||||
console.log('Aufruf: node scripts/hustler-import.mjs <pfad-zu-pixelwelt-v2> [<pfad-zu-pixelwelt-woms>]')
|
||||
return argumente.length >= 1 && argumente.length <= 2 ? 0 : 2
|
||||
}
|
||||
const v2 = path.resolve(argumente[0])
|
||||
let woms = argumente[1] ? path.resolve(argumente[1]) : null
|
||||
const hinweise = []
|
||||
if (woms && !existsSync(woms)) {
|
||||
hinweise.push(`Hinweis: Der Ordner ${woms} gibt es noch nicht; es wird nur mit pixelwelt-v2 gearbeitet.`)
|
||||
woms = null
|
||||
}
|
||||
const plan = await baue({ v2, woms })
|
||||
const z = schreibe(plan)
|
||||
for (const h of hinweise) console.log(h)
|
||||
for (const [name, b] of Object.entries(plan.bericht.bildschirme)) {
|
||||
console.log(`${name.padEnd(9)} ${String(b.dateien).padStart(3)} PNG (${mb(b.bytes)}), ${b.keyframes} Keyframes${b.ueberschrieben.length ? `, ${b.ueberschrieben.length} Datei(en) aus pixelwelt-woms` : ''}`)
|
||||
}
|
||||
console.log(`zusammen ${String(plan.bericht.einzigartig.dateien).padStart(3)} verschiedene PNG (${mb(plan.bericht.einzigartig.bytes)})`)
|
||||
if (plan.bericht.graueRaeume.length) console.log(`graue Räume: ${plan.bericht.graueRaeume.join(', ')}`)
|
||||
console.log(`geschrieben: ${z.geschrieben}, unverändert: ${z.unveraendert}${z.entfernt.length ? `, entfernt: ${z.entfernt.join(', ')}` : ''}`)
|
||||
console.log(`Prüfsumme aller Ausgaben: ${pruefsumme(plan.dateien)}`)
|
||||
return 0
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
||||
main(process.argv.slice(2)).then(
|
||||
(code) => { process.exitCode = code },
|
||||
(e) => {
|
||||
console.error(e instanceof ImportFehler ? `Abbruch: ${e.message}` : e)
|
||||
process.exitCode = 1
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"hochhaus": {
|
||||
"haus": "haus/haus.png",
|
||||
"haus-fenster-a": "haus/haus-fenster-a.png",
|
||||
"haus-fenster-b": "haus/haus-fenster-b.png",
|
||||
"haus-fenster-c": "haus/haus-fenster-c.png",
|
||||
"schild-a": "haus/schild-a.png",
|
||||
"schild-b": "haus/schild-b.png",
|
||||
"aufzug": "haus/aufzug.png",
|
||||
"aufzug-seil": "haus/aufzug-seil.png",
|
||||
"welt": "welt/welt.png",
|
||||
"welt-licht-1": "welt/welt-licht-1.png",
|
||||
"welt-licht-2": "welt/welt-licht-2.png",
|
||||
"welt-licht-3": "welt/welt-licht-3.png",
|
||||
"welt-licht-4": "welt/welt-licht-4.png",
|
||||
"laternen-licht": "welt/laternen-licht.png",
|
||||
"schnee-1": "welt/schnee-1.png",
|
||||
"schnee-2": "welt/schnee-2.png",
|
||||
"schnee-3": "welt/schnee-3.png",
|
||||
"sterne-funkeln": "welt/sterne-funkeln.png",
|
||||
"sterne-verteilt": "welt/sterne-verteilt.png",
|
||||
"mond": "welt/mond.png",
|
||||
"muetze-arbeiten": "figuren/muetze-arbeiten.png",
|
||||
"muetze-warten": "figuren/muetze-warten.png",
|
||||
"muetze-schlafen": "figuren/muetze-schlafen.png",
|
||||
"muetze-nicht": "figuren/muetze-nicht.png",
|
||||
"kappe-arbeiten": "figuren/kappe-arbeiten.png",
|
||||
"kappe-warten": "figuren/kappe-warten.png",
|
||||
"kappe-schlafen": "figuren/kappe-schlafen.png",
|
||||
"kappe-nicht": "figuren/kappe-nicht.png",
|
||||
"krone-arbeiten": "figuren/krone-arbeiten.png",
|
||||
"krone-warten": "figuren/krone-warten.png",
|
||||
"krone-schlafen": "figuren/krone-schlafen.png",
|
||||
"krone-nicht": "figuren/krone-nicht.png",
|
||||
"zzz": "figuren/zzz.png",
|
||||
"bildschirm": "figuren/bildschirm.png",
|
||||
"radar": "figuren/radar.png",
|
||||
"punkte": "figuren/punkte.png",
|
||||
"panel": "ui/panel.png",
|
||||
"knopf": "ui/knopf.png",
|
||||
"muenze-gruen": "ui/muenze-gruen.png",
|
||||
"muenze-rot": "ui/muenze-rot.png",
|
||||
"stern": "ui/stern.png",
|
||||
"pokal": "ui/pokal.png",
|
||||
"blase": "ui/blase.png",
|
||||
"glocke": "ui/glocke.png",
|
||||
"robo": "ui/robo.png",
|
||||
"zahnrad": "ui/zahnrad.png",
|
||||
"pause": "ui/pause.png",
|
||||
"schliessen": "ui/schliessen.png",
|
||||
"pfeil-links": "ui/pfeil-links.png",
|
||||
"pfeil-rechts": "ui/pfeil-rechts.png",
|
||||
"szene-buero": "detail/szene-buero.png",
|
||||
"szene-nicht": "detail/szene-nicht.png",
|
||||
"szene-bruecke": "detail/szene-bruecke.png",
|
||||
"gross-muetze-arbeiten": "detail/gross-muetze-arbeiten.png",
|
||||
"gross-muetze-warten": "detail/gross-muetze-warten.png",
|
||||
"gross-muetze-schlafen": "detail/gross-muetze-schlafen.png",
|
||||
"gross-muetze-nicht": "detail/gross-muetze-nicht.png",
|
||||
"gross-kappe-arbeiten": "detail/gross-kappe-arbeiten.png",
|
||||
"gross-kappe-warten": "detail/gross-kappe-warten.png",
|
||||
"gross-kappe-schlafen": "detail/gross-kappe-schlafen.png",
|
||||
"gross-kappe-nicht": "detail/gross-kappe-nicht.png",
|
||||
"gross-krone-arbeiten": "detail/gross-krone-arbeiten.png",
|
||||
"gross-krone-warten": "detail/gross-krone-warten.png",
|
||||
"gross-krone-schlafen": "detail/gross-krone-schlafen.png",
|
||||
"gross-krone-nicht": "detail/gross-krone-nicht.png",
|
||||
"wirbel": "detail/wirbel.png",
|
||||
"monitor-gross": "detail/monitor-gross.png",
|
||||
"dampf": "detail/dampf.png",
|
||||
"zzz-gross": "detail/zzz-gross.png",
|
||||
"overseer-klein": "figuren/overseer-klein.png",
|
||||
"overseer-gross": "detail/overseer-gross.png"
|
||||
},
|
||||
"keller": {
|
||||
"keller": "keller/szene/keller.png",
|
||||
"keller-licht": "keller/szene/keller-licht.png",
|
||||
"licht-lampe-92": "keller/szene/licht-lampe-92.png",
|
||||
"licht-lampe-236": "keller/szene/licht-lampe-236.png",
|
||||
"licht-schreibtisch": "keller/szene/licht-schreibtisch.png",
|
||||
"licht-halle": "keller/szene/licht-halle.png",
|
||||
"staub": "keller/szene/staub.png",
|
||||
"fensterschnee": "keller/szene/fensterschnee.png",
|
||||
"fenster-kreuz": "keller/szene/fenster-kreuz.png",
|
||||
"fenster-passant": "keller/szene/fenster-passant.png",
|
||||
"dampf-tasse": "keller/szene/dampf-tasse.png",
|
||||
"arne-ablauf": "keller/figuren/arne-ablauf.png",
|
||||
"buch-stempel": "keller/figuren/buch-stempel.png",
|
||||
"rohrpost": "keller/figuren/rohrpost.png",
|
||||
"schublade": "keller/figuren/schublade.png",
|
||||
"maus": "keller/figuren/maus.png",
|
||||
"neu-funke": "keller/figuren/neu-funke.png",
|
||||
"buch": "keller/ui/buch.png",
|
||||
"akte": "keller/ui/akte.png",
|
||||
"messing": "keller/ui/messing.png",
|
||||
"akte-icon": "keller/ui/akte-icon.png",
|
||||
"alle": "keller/ui/alle.png",
|
||||
"brief-x": "keller/ui/brief-x.png",
|
||||
"brief-zurueck": "keller/ui/brief-zurueck.png",
|
||||
"doppelt": "keller/ui/doppelt.png",
|
||||
"knopf": "ui/knopf.png",
|
||||
"etikett-leer": "keller/ui/etikett-leer.png",
|
||||
"fremd": "keller/ui/fremd.png",
|
||||
"haken": "keller/ui/haken.png",
|
||||
"kalender": "keller/ui/kalender.png",
|
||||
"kette": "keller/ui/kette.png",
|
||||
"lupe": "keller/ui/lupe.png",
|
||||
"pause": "keller/ui/pause.png",
|
||||
"pfeil-links": "keller/ui/pfeil-links.png",
|
||||
"pfeil-rechts": "keller/ui/pfeil-rechts.png",
|
||||
"pfeil-runter": "keller/ui/pfeil-runter.png",
|
||||
"quelle": "keller/ui/quelle.png",
|
||||
"raum-aerzte": "keller/ui/raum-aerzte.png",
|
||||
"raum-handwerk": "keller/ui/raum-handwerk.png",
|
||||
"raum-laden": "keller/ui/raum-laden.png",
|
||||
"raum-selbst": "keller/ui/raum-selbst.png",
|
||||
"robo": "keller/ui/robo.png",
|
||||
"schere": "keller/ui/schere.png",
|
||||
"schliessen": "keller/ui/schliessen.png",
|
||||
"schloss": "keller/ui/schloss.png",
|
||||
"seite-kaputt": "keller/ui/seite-kaputt.png",
|
||||
"stempel": "keller/ui/stempel.png",
|
||||
"stopp": "keller/ui/stopp.png",
|
||||
"telefon": "keller/ui/telefon.png",
|
||||
"uhr": "keller/ui/uhr.png",
|
||||
"waage-kaputt": "keller/ui/waage-kaputt.png",
|
||||
"zurueck": "keller/ui/zurueck.png"
|
||||
},
|
||||
"pult": {
|
||||
"pult": "pult/szene/pult.png",
|
||||
"licht-pendel-55": "pult/szene/licht-pendel-55.png",
|
||||
"licht-pendel-232": "pult/szene/licht-pendel-232.png",
|
||||
"licht-schreibtisch": "pult/szene/licht-schreibtisch.png",
|
||||
"licht-rechts": "pult/szene/licht-rechts.png",
|
||||
"licht-mond": "pult/szene/licht-mond.png",
|
||||
"fensterschnee": "pult/szene/fensterschnee.png",
|
||||
"fenster2-maske": "pult/szene/fenster2-maske.png",
|
||||
"fenster-sprossen-1": "pult/szene/fenster-sprossen-1.png",
|
||||
"fenster-sprossen-2": "pult/szene/fenster-sprossen-2.png",
|
||||
"dampf-tasse": "pult/szene/dampf-tasse.png",
|
||||
"boten-ablauf": "pult/figuren/boten-ablauf.png",
|
||||
"aufzug-tuer": "pult/figuren/aufzug-tuer.png",
|
||||
"aufzug-licht": "pult/figuren/aufzug-licht.png",
|
||||
"aufzug-ruftaste": "pult/figuren/aufzug-ruftaste.png",
|
||||
"briefkasten-funke": "pult/figuren/briefkasten-funke.png",
|
||||
"aufzug-innen": "pult/figuren/aufzug-innen.png",
|
||||
"aufzug-ding": "pult/figuren/aufzug-ding.png",
|
||||
"fig-briefkasten": "pult/figuren/briefkasten.png",
|
||||
"monitor": "pult/figuren/monitor.png",
|
||||
"klemmbrett": "pult/ui/klemmbrett.png",
|
||||
"klammer": "pult/ui/klammer.png",
|
||||
"umschlag": "pult/ui/umschlag.png",
|
||||
"umschlag-offen": "pult/ui/umschlag-offen.png",
|
||||
"brief": "pult/ui/brief.png",
|
||||
"knopf": "ui/knopf.png",
|
||||
"zurueck": "keller/ui/zurueck.png",
|
||||
"schliessen": "keller/ui/schliessen.png",
|
||||
"messing": "keller/ui/messing.png",
|
||||
"ansehen": "pult/ui/ansehen.png",
|
||||
"antworten": "pult/ui/antworten.png",
|
||||
"art-bau-kaputt": "pult/ui/art-bau-kaputt.png",
|
||||
"art-beschwerde": "pult/ui/art-beschwerde.png",
|
||||
"art-mail": "pult/ui/art-mail.png",
|
||||
"art-mensch": "pult/ui/art-mensch.png",
|
||||
"art-plattform": "pult/ui/art-plattform.png",
|
||||
"art-preisfrage": "pult/ui/art-preisfrage.png",
|
||||
"art-unsicher": "pult/ui/art-unsicher.png",
|
||||
"art-vorschau": "pult/ui/art-vorschau.png",
|
||||
"bauen": "pult/ui/bauen.png",
|
||||
"briefkasten": "pult/ui/briefkasten.png",
|
||||
"einfuegen": "pult/ui/einfuegen.png",
|
||||
"erledigt": "pult/ui/erledigt.png",
|
||||
"gesendet": "pult/ui/gesendet.png",
|
||||
"kopf-kappe": "pult/ui/kopf-kappe.png",
|
||||
"kopf-krone": "pult/ui/kopf-krone.png",
|
||||
"kopf-muetze": "pult/ui/kopf-muetze.png",
|
||||
"kopieren": "pult/ui/kopieren.png",
|
||||
"oeffnen": "pult/ui/oeffnen.png",
|
||||
"person": "pult/ui/person.png",
|
||||
"sanduhr": "pult/ui/sanduhr.png",
|
||||
"uebernehmen": "pult/ui/uebernehmen.png",
|
||||
"veraltet": "pult/ui/veraltet.png",
|
||||
"verwerfen": "pult/ui/verwerfen.png",
|
||||
"lichterkette-0": "pult/entwurf/eigen/lichterkette-0.png",
|
||||
"lichterkette-1": "pult/entwurf/eigen/lichterkette-1.png",
|
||||
"lichterkette-2": "pult/entwurf/eigen/lichterkette-2.png"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 419 B |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 461 B |
|
After Width: | Height: | Size: 243 B |
|
After Width: | Height: | Size: 894 B |
|
After Width: | Height: | Size: 564 B |
|
After Width: | Height: | Size: 790 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 873 B |
|
After Width: | Height: | Size: 548 B |
|
After Width: | Height: | Size: 773 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 904 B |
|
After Width: | Height: | Size: 561 B |
|
After Width: | Height: | Size: 816 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 174 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 249 B |
|
After Width: | Height: | Size: 111 B |
|
After Width: | Height: | Size: 563 B |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 726 B |
|
After Width: | Height: | Size: 338 B |
|
After Width: | Height: | Size: 409 B |
|
After Width: | Height: | Size: 966 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 334 B |
|
After Width: | Height: | Size: 204 B |
|
After Width: | Height: | Size: 366 B |
|
After Width: | Height: | Size: 349 B |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 424 B |
|
After Width: | Height: | Size: 225 B |
|
After Width: | Height: | Size: 577 B |
|
After Width: | Height: | Size: 319 B |
|
After Width: | Height: | Size: 308 B |
|
After Width: | Height: | Size: 306 B |
|
After Width: | Height: | Size: 540 B |
|
After Width: | Height: | Size: 288 B |
|
After Width: | Height: | Size: 296 B |
|
After Width: | Height: | Size: 202 B |
|
After Width: | Height: | Size: 216 B |
|
After Width: | Height: | Size: 273 B |
|
After Width: | Height: | Size: 302 B |
|
After Width: | Height: | Size: 487 B |
|
After Width: | Height: | Size: 381 B |
|
After Width: | Height: | Size: 152 B |
|
After Width: | Height: | Size: 167 B |
|
After Width: | Height: | Size: 155 B |
|
After Width: | Height: | Size: 226 B |
|
After Width: | Height: | Size: 359 B |
|
After Width: | Height: | Size: 239 B |
|
After Width: | Height: | Size: 306 B |
|
After Width: | Height: | Size: 324 B |
|
After Width: | Height: | Size: 260 B |
|
After Width: | Height: | Size: 387 B |
|
After Width: | Height: | Size: 328 B |
|
After Width: | Height: | Size: 170 B |