Hustler-Pixelwelt T13: Korrekturen Hochhaus (Fokus, Tastatur, weniger Bewegung, Handy-Tippflächen, Schnee per transform, Pause überall, Seite ohne Leerlauf-Blitz)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-22 09:47:57 +00:00
1 parent 0924a8d2ae
commit 5e6698f5e0
14 files changed
+994 -52

No files matched your search

+140 -3
View File
@@ -12,11 +12,11 @@ const W = await loadModule('src/lib/hustlerWelt.js')
const {
HUT, STATUS_ANZEIGE, POSE_NACH_STATUS, AUFGABEN, ZOOM_MAX, SICHT_STANDARD,
anzeigeName, modellKurz, modellName, statusVon, figurZustand, detailModell, blaettern, hudWerte, schilder,
kameraMinS, kameraGrenzen, kameraPassend, kameraZoomAuf, kameraEtage,
kameraMinS, kameraGrenzen, kameraPassend, kameraZoomAuf, kameraEtage, kameraZeige, tippRand,
} = S
const { ROLES, SEARCH_ROOMS, FLOOR_TITLES, ROOM_TITLES } = R
const { formatEur, formatUsd, formatCount, kpiTiles, traceRows } = V
const { ETAGEN } = W
const { ETAGEN, HUSTLER } = W
const NBSP = String.fromCharCode(160)
const STATUS = ['arbeitet', 'wartet', 'leerlauf', 'grau', 'fehler']
@@ -691,8 +691,18 @@ const VL = (() => {
const FENSTER = [
{ w: 1280, h: 720 }, { w: 1920, h: 1080 }, { w: 390, h: 700 }, { w: 390, h: 844 }, { w: 2560, h: 1440 },
{ w: 800, h: 400 }, { w: 3840, h: 2160 }, { w: 2400, h: 1200 }, { w: 1024, h: 1366 }, { w: 1500, h: 500 },
{ w: 844, h: 390 }, { w: 360, h: 740 }, { w: 768, h: 1024 },
]
// Einzige gewollte Abweichung von der Vorlage (M8): „Etage heranzoomen“ vergrößert mindestens das 1,6-fache von „ganzes Haus“ (höchstens 5-fach).
// Wo die Vorlage ohnehin mehr zoomt (Desktop, Handy quer), bleibt es bei ihrer Zahl; sonst gilt der Boden, gerechnet mit den Grenzen der Vorlage.
function etageErwartet(oben, unten, g) {
const vorlage = VL.etage(oben, unten, g)
const boden = Math.min(5, VL.passend(g).s * 1.6)
if (vorlage.s >= boden) return vorlage
return VL.grenzen({ s: boden, x: g.w / 2 - 1216 * boden, y: 42 + g.h / 2 - ((oben + unten) / 2) * boden }, g)
}
// Gleichmäßige Zufallszahlen ohne Math.random, damit der Test immer dasselbe prüft
function zufall(seed) {
let z = seed >>> 0
@@ -704,7 +714,7 @@ test('Kamera: gleiche Zahlen wie die Formeln der Zeichenfläche', () => {
for (const g of FENSTER) {
assert.equal(kameraMinS(g), VL.minS(g), `minS ${g.w}x${g.h}`)
assert.deepEqual(kameraPassend(g), VL.passend(g), `passend ${g.w}x${g.h}`)
for (const e of ETAGEN) assert.deepEqual(kameraEtage(e.oben, e.unten, g), VL.etage(e.oben, e.unten, g), `etage ${e.id} ${g.w}x${g.h}`)
for (const e of ETAGEN) assert.deepEqual(kameraEtage(e.oben, e.unten, g), etageErwartet(e.oben, e.unten, g), `etage ${e.id} ${g.w}x${g.h}`)
for (let i = 0; i < 40; i += 1) {
const st = { s: rnd() * 8, x: (rnd() - 0.7) * 6000, y: (rnd() - 0.6) * 3000 }
assert.deepEqual(kameraGrenzen(st, g), VL.grenzen(st, g), `grenzen ${JSON.stringify(st)}`)
@@ -846,6 +856,133 @@ test('kameraEtage: die Etage liegt mittig im Bild, höchstens 3-fach, nie kleine
assert.equal(kameraEtage(720, 776, { w: 1920, h: 1080 }).s, 3)
})
test('kameraEtage (M8): mindestens 1,6 x „ganzes Haus“ (höchstens ZOOM_MAX); Handy hochkant zoomt jetzt sichtbar, Desktop und Handy quer bleiben wie die Vorlage', () => {
for (const g of FENSTER) {
const ganz = kameraPassend(g).s
for (const e of ETAGEN) {
const k = kameraEtage(e.oben, e.unten, g)
const wo = `${e.id} ${g.w}x${g.h} ${JSON.stringify(k)}`
assert.ok(k.s >= Math.min(ZOOM_MAX, ganz * 1.6) - 1e-9, `mindestens 1,6 x ganz: ${wo}`)
assert.ok(k.s <= ZOOM_MAX, `höchstens ZOOM_MAX: ${wo}`)
}
}
const nahe = (a, b, wo) => assert.ok(Math.abs(a - b) < 1e-9, `${wo}: ${a} ≈ ${b}`)
// 390 x 844: ganzes Haus = cover = 844 / 1200; vorher änderte „heranzoomen“ den Zoom gar nicht (Breite begrenzte auf unter cover), jetzt 1,6 x
const handy = { w: 390, h: 844 }
nahe(kameraPassend(handy).s, 844 / 1200, 'ganz 390x844')
nahe(kameraEtage(552, 608, handy).s, (844 / 1200) * 1.6, 'Etage 390x844')
assert.ok(kameraEtage(552, 608, handy).s > kameraPassend(handy).s * 1.5)
// Desktop: die Breite begrenzt, das ist schon mehr als 1,6 x
nahe(kameraEtage(720, 776, { w: 1280, h: 720 }).s, (1280 - 96) / 560, 'Etage 1280x720')
assert.equal(kameraEtage(720, 776, { w: 1920, h: 1080 }).s, 3)
// Handy quer 844 x 390: 1,336 (Breite) bleibt, ganz = 0,41
nahe(kameraEtage(776, 832, { w: 844, h: 390 }).s, (844 - 96) / 560, 'Etage 844x390')
// Tablet hochkant: vorher kleiner als „ganzes Haus“ (1,2 gegen 1,26)
assert.ok(kameraEtage(776, 832, { w: 768, h: 1024 }).s > kameraPassend({ w: 768, h: 1024 }).s * 1.59)
// Die Etage bleibt mittig und die Kamera in den Grenzen
for (const e of ETAGEN) {
const k = kameraEtage(e.oben, e.unten, handy)
assert.deepEqual(kameraGrenzen(k, handy), k, e.id)
}
})
test('kameraZeige (M1): holt ein Rechteck der Welt ins Bild (unter der Kopfzeile, mit Rand), Zoom bleibt; schon Sichtbares und Unmögliches geben null', () => {
const g = { w: 1280, h: 720 }
const k = { s: 1, x: -600, y: -300 } // sichtbar: Welt x 600..1880, y 300+kopf..1020
const figur = { left: 1000, top: 500, width: 24, height: 32 }
assert.equal(kameraZeige(k, figur, g), null)
// rechts außerhalb: rechter Rand der Figur kommt auf Breite - 24
const rechts = kameraZeige(k, { ...figur, left: 2000 }, g)
assert.deepEqual(rechts, { s: 1, x: 1280 - 24 - (2000 + 24), y: -300 })
// links außerhalb: linker Rand auf 24
const links = kameraZeige(k, { ...figur, left: 400 }, g)
assert.deepEqual(links, { s: 1, x: 24 - 400, y: -300 })
// oben: unter der Kopfzeile (Standard 84 px) plus Rand 24
const oben = kameraZeige({ s: 1, x: -600, y: -300 }, { ...figur, top: 340 }, g)
assert.deepEqual(oben, { s: 1, x: -600, y: 84 + 24 - 340 })
// eine höhere Kopfzeile (2 Reihen am Handy) verschiebt die Grenze
assert.deepEqual(kameraZeige({ s: 1, x: -600, y: -300 }, { ...figur, top: 340 }, g, { oben: 150 }), { s: 1, x: -600, y: 150 + 24 - 340 })
// ein Rechteck dicht unter der Kopfzeile, das die Standard-Zeile nicht stört, aber eine hohe: wird weggeholt
assert.equal(kameraZeige({ s: 1, x: -600, y: -300 }, { ...figur, top: 420 }, g), null)
assert.notEqual(kameraZeige({ s: 1, x: -600, y: -300 }, { ...figur, top: 420 }, g, { oben: 150 }), null)
// unten
assert.deepEqual(kameraZeige(k, { ...figur, top: 1100 }, g), { s: 1, x: -600, y: 720 - 24 - (1100 + 32) })
// Zoom wirkt: bei s = 2 zählt das Rechteck doppelt
assert.deepEqual(kameraZeige({ s: 2, x: 0, y: 0 }, { left: 700, top: 200, width: 24, height: 32 }, g), { s: 2, x: 1280 - 24 - (700 + 24) * 2, y: 0 })
// größer als der sichtbare Bereich: die Anfangskante kommt an den Rand
const breit = kameraZeige({ s: 3, x: -2000, y: -300 }, { left: 960, top: 888, width: 512, height: 56 }, g)
assert.equal(breit.x, 24 - 960 * 3 - 0) // 960*3 - 2000 = 880 > 24 (sichtbar), Rechteck 1536 breit > 1232 -> linke Kante auf 24
// Ergebnis liegt in den Grenzen (die Welt endet links bei 0, rechts bei Breite - Welt)
const ganzRechts = kameraZeige({ s: 1, x: 0, y: 0 }, { left: 2390, top: 500, width: 24, height: 32 }, g)
assert.deepEqual(kameraGrenzen(ganzRechts, g), ganzRechts)
assert.equal(ganzRechts.x, 1280 - 2400)
// Eingabe unverändert, kaputte Eingaben geben null
const eingabe = Object.freeze({ s: 1, x: -600, y: -300 })
kameraZeige(eingabe, { ...figur, left: 2000 }, g)
assert.deepEqual({ ...eingabe }, { s: 1, x: -600, y: -300 })
for (const kaputt of [null, undefined, {}, { left: 1 }, { left: NaN, top: 0, width: 1, height: 1 }, 'x']) assert.equal(kameraZeige(k, kaputt, g), null)
assert.equal(kameraZeige(null, figur, g), null)
assert.equal(kameraZeige({ s: 0, x: 0, y: 0 }, figur, g), null)
// kaputtes Fenster fällt auf die Entwurfsgröße zurück
assert.deepEqual(kameraZeige(k, { ...figur, left: 2000 }, null), kameraZeige(k, { ...figur, left: 2000 }, SICHT_STANDARD))
})
test('tippRand (M8): Tippfläche der Figuren wächst mit dem Zoom auf 40 Bildschirm-Pixel, gedeckelt gegen Überlappung; bei starkem Zoom die alten 12/8/6', () => {
const nahe = (a, b, wo) => assert.ok(Math.abs(a - b) < 1e-9, `${wo}: ${a} ≈ ${b}`)
// starker Zoom: bisherige Überstände von hochhaus.css (-12/-12/-8/-6)
for (const z of [1.7, 2, 3, 5]) assert.deepEqual(tippRand(z), { x: 12, oben: 8, unten: 6 }, `Zoom ${z}`)
// Handy hochkant, ganzes Haus (0,7033): 40 px = 56,9 Weltpixel -> 24 + 2 x 16,4; senkrecht auf 12 gedeckelt (32 + 2 x 12 = 56 = Etagenabstand)
const z = 844 / 1200
const t = tippRand(z)
nahe(t.x, (40 / z - 24) / 2, 'x 0,7033')
assert.equal(t.oben, 12); assert.equal(t.unten, 12)
nahe((24 + 2 * t.x) * z, 40, 'Breite am Bildschirm')
nahe((32 + t.oben + t.unten) * z, 56 * z, 'Höhe am Bildschirm')
assert.ok((32 + t.oben + t.unten) * z > 39 && (32 + t.oben + t.unten) * z < 40)
// sehr klein (Handy quer, ganzes Haus 0,41; cover 0,35): seitlich auf 36 gedeckelt (24 + 72 = 96 < 100 Weltpixel Figurenabstand)
assert.equal(tippRand(0.41).x, 36); assert.equal(tippRand(0.3517).x, 36)
assert.ok((24 + 2 * 36) * 0.3517 >= 32, 'auch beim kleinsten Zoom mindestens 32 px breit')
// nie kleiner als die bisherigen Werte, immer endlich, auch bei kaputtem Zoom
for (const kaputt of [0, -1, NaN, Infinity, undefined, null, 'x']) {
const k = tippRand(kaputt)
for (const v of [k.x, k.oben, k.unten]) assert.ok(Number.isFinite(v) && v > 0, String(kaputt))
}
for (let zoom = 0.3; zoom <= 5; zoom += 0.05) {
const k = tippRand(zoom)
assert.ok(k.x >= 12 && k.oben >= 8 && k.unten >= 6 && k.x <= 36 && k.oben <= 12 && k.unten <= 12, `Zoom ${zoom}`)
}
})
test('tippRand (M8): die gewachsenen Tippflächen überdecken sich bei keinem Zoom (alle 19 Figuren, echte Lage), und am Handy sind sie breit genug', () => {
const figuren = Object.entries(HUSTLER).map(([id, h]) => ({ id, ...h.knopf }))
assert.equal(figuren.length, 19)
for (let zoom = 0.35; zoom <= 5; zoom += 0.01) {
const t = tippRand(zoom)
const flaechen = figuren.map((f) => ({ id: f.id, l: f.left - t.x, r: f.left + f.w + t.x, o: f.top - t.oben, u: f.top + f.h + t.unten }))
for (let i = 0; i < flaechen.length; i += 1) {
for (let j = i + 1; j < flaechen.length; j += 1) {
const a = flaechen[i]; const b = flaechen[j]
const ueberlappt = a.l < b.r - 1e-9 && b.l < a.r - 1e-9 && a.o < b.u - 1e-9 && b.o < a.u - 1e-9
assert.ok(!ueberlappt, `${a.id} und ${b.id} überdecken sich bei Zoom ${zoom.toFixed(2)}`)
}
}
}
// 390 x 844 (ganzes Haus 0,7033): jede Figur ≥ 40 px breit, ≥ 32 px hoch (senkrecht begrenzt der Etagenabstand 56 Weltpixel)
const z = kameraPassend({ w: 390, h: 844 }).s
const t = tippRand(z)
for (const f of figuren) {
assert.ok((f.w + 2 * t.x) * z >= 40 - 1e-9, `${f.id} Breite`)
assert.ok((f.h + t.oben + t.unten) * z >= 32, `${f.id} Höhe`)
}
// nach „Etage heranzoomen“ (1,125): ≥ 40 in beide Richtungen
const nah = kameraEtage(552, 608, { w: 390, h: 844 }).s
const tn = tippRand(nah)
for (const f of figuren) {
assert.ok((f.w + 2 * tn.x) * nah >= 40 - 1e-9, `${f.id} Breite nah`)
assert.ok((f.h + tn.oben + tn.unten) * nah >= 40 - 1e-9, `${f.id} Höhe nah`)
}
})
// ---------------------------------------------------------------------------------------------------------------
// Aufbau des Moduls