- 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>
168 lines
9.7 KiB
JavaScript
168 lines
9.7 KiB
JavaScript
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`)
|
|
}
|
|
})
|