import test from 'node:test' import assert from 'node:assert/strict' import { loadModule } from './load.mjs' import { makeCtx } from './helpers/fakeCtx.mjs' // Räume nach kind, Pokal-Seiten, graue Räume und Gebäudehülle (Quelle: entwuerfe/scene-turm.js Z. 99-314) const R = await loadModule('src/components/workers/engine/rooms.js') const B = await loadModule('src/components/workers/engine/building.js') const { buildLayout, ROOM_META } = await loadModule('src/components/workers/engine/layout.js') const { PAL, TEAM } = await loadModule('src/components/workers/engine/sprites.js') const { FLOOR_TITLES, ROOM_TITLES } = await loadModule('src/lib/workersRoster.js') const layout = buildLayout() const draw = (look, t = 1) => { const m = makeCtx() R.drawRooms(m.ctx, layout, t, look) return m } const SIGN = 'noch nicht angeschlossen' const looks = (patch = {}) => Object.assign(R.createLook(), patch) const count = (rects, fill) => rects.filter((r) => r.fill === fill).length // Pokale sind die 7 × 3 großen Goldflächen (k = 3: 21 × 9 Geräte-Pixel) const trophies = (look) => draw(look).rects.filter((r) => r.fill === PAL.gold && r.w === 21 && r.h === 9).length test('trophyPaging: 8 je Seite, Seite wird geklemmt', () => { assert.deepEqual(R.trophyPaging(0, 0), { shown: 0, page: 0, pages: 1, from: 0 }) assert.deepEqual(R.trophyPaging(8, 0), { shown: 8, page: 0, pages: 1, from: 0 }) assert.deepEqual(R.trophyPaging(9, 1), { shown: 1, page: 1, pages: 2, from: 8 }) assert.deepEqual(R.trophyPaging(9, 5), { shown: 1, page: 1, pages: 2, from: 8 }) assert.deepEqual(R.trophyPaging(25, -1), { shown: 8, page: 0, pages: 4, from: 0 }) }) test('alle 21 Räume zeichnen, jeder Titel erscheint', () => { const { texts, rects } = draw(R.createLook()) assert.ok(rects.length > 300) for (const title of Object.values(ROOM_TITLES)) assert.ok(texts.some((x) => x.text === title), title) }) test('graue Räume tragen das Schild, sonst keins (Review Focus 4)', () => { const look = R.createLook() look.greyRooms = new Set(['kleinanzeigen', 'portale']) assert.equal(draw(look).texts.filter((x) => x.text === SIGN).length, 2) assert.equal(draw(R.createLook()).texts.filter((x) => x.text === SIGN).length, 0) }) test('Pult-Zahl, Pokal- und Bot-Lead-Zähler', () => { const look = R.createLook() look.pultOpen = 7 look.dealsMonth = 9 look.botLeads = 112 const t = draw(look).texts.map((x) => x.text) assert.ok(t.includes('7')) assert.ok(t.includes('9 Deals')) assert.ok(t.includes('112 Bot-Leads')) assert.ok(!draw(R.createLook()).texts.some((x) => x.text === '7')) }) test('Gebäude: Etagenschilder, Lampen, Pausenschleier', () => { const m = makeCtx() B.drawTower(m.ctx, layout, 1, { at: (i) => i === 3 }) for (const title of Object.values(FLOOR_TITLES)) assert.ok(m.texts.some((x) => x.text === title), title) const on = makeCtx() const look = R.createLook() look.paused = true B.drawPauseVeil(on.ctx, look) assert.ok(on.texts.some((x) => x.text === 'Pausiert')) const off = makeCtx() B.drawPauseVeil(off.ctx, R.createLook()) assert.equal(off.texts.length, 0) B.drawOutside(makeCtx().ctx) }) // ---- Ergänzungen: Verhalten, das die fünf Tests oben nicht berühren ---- test('createLook: Ausgangszustand ohne gemeinsame Mengen zwischen zwei Aufrufen', () => { const a = R.createLook() const b = R.createLook() assert.ok(a.greyRooms instanceof Set && a.busyRooms instanceof Set) assert.equal(a.greyRooms.size + a.busyRooms.size, 0) assert.deepEqual({ ...a, greyRooms: 0, busyRooms: 0 }, { greyRooms: 0, busyRooms: 0, pultOpen: 0, dealsMonth: 0, botLeads: 0, trophyPage: 0, pageT: 0, trophyPop: 0, vaultFlash: 0, paused: false }) a.busyRooms.add('netz') assert.equal(b.busyRooms.size, 0) }) test('ROOM_DRAWERS: eine Zeichnung für jede Raumart des Layouts, eingefroren', () => { const kinds = new Set(Object.values(ROOM_META).map((m) => m.kind)) assert.equal(kinds.size, 16) for (const kind of kinds) assert.equal(typeof R.ROOM_DRAWERS[kind], 'function', kind) assert.ok(Object.isFrozen(R.ROOM_DRAWERS)) }) test('jeder Raum zeichnet sichtbaren Inhalt über Tint und Titel hinaus (kein leerer Raum)', () => { // Rechtecke je Raum zählen: Raum für Raum in ein eigenes Layout mit nur dieser Etage for (const floor of layout.floors) { for (const room of floor.rooms) { const single = { ...layout, floors: [{ ...floor, rooms: [room] }] } const m = makeCtx() R.drawRooms(m.ctx, single, 1, R.createLook()) assert.ok(m.rects.length >= 5, room.id) } } }) test('trophyPaging: ungültige Werte fallen auf eine leere erste Seite zurück', () => { const empty = { shown: 0, page: 0, pages: 1, from: 0 } assert.deepEqual(R.trophyPaging(NaN, NaN), empty) assert.deepEqual(R.trophyPaging(undefined, undefined), empty) assert.deepEqual(R.trophyPaging(-4, 2), empty) assert.deepEqual(R.trophyPaging(16, 1), { shown: 8, page: 1, pages: 2, from: 8 }) assert.deepEqual(R.trophyPaging(5, 0, 2), { shown: 2, page: 0, pages: 3, from: 0 }) }) test('Pokalbrett zeigt die gewählte Seite; der neueste Pokal blinkt bei trophyPop', () => { assert.equal(trophies(looks()), 0) assert.equal(trophies(looks({ dealsMonth: 3 })), 3) assert.equal(trophies(looks({ dealsMonth: 20 })), 8) assert.equal(trophies(looks({ dealsMonth: 9, trophyPage: 1 })), 1) assert.equal(trophies(looks({ dealsMonth: 9, trophyPage: 7 })), 1) // floor(0.25 * 10) = 2 -> gerade: sichtbar; floor(0.15 * 10) = 1 -> ungerade: der neueste Pokal fehlt assert.equal(trophies(looks({ dealsMonth: 3, trophyPop: 0.25 })), 3) assert.equal(trophies(looks({ dealsMonth: 3, trophyPop: 0.15 })), 2) }) test('Tresor blitzt Gold bei vaultFlash, sonst nicht', () => { const gold = (look) => draw(look).alphas.filter((a) => a > 0.1).length assert.ok(gold(looks({ vaultFlash: 0.7 })) > gold(looks())) }) test('Pult-Zahl: 0 zeigt keine Zahl, ab 100 steht 99+', () => { const nums = (n) => draw(looks({ pultOpen: n })).texts.map((x) => x.text).filter((x) => /^\d+\+?$/.test(x)) assert.deepEqual(nums(0), []) assert.deepEqual(nums(120), ['99+']) assert.deepEqual(nums(99), ['99']) }) test('Zähler zeigen immer ihre Zahl, auch 0', () => { const t = draw(R.createLook()).texts.map((x) => x.text) assert.ok(t.includes('0 Deals')) assert.ok(t.includes('0 Bot-Leads')) }) test('Monitore laufen nur in Räumen aus busyRooms', () => { const lines = (look) => count(draw(look).rects, TEAM.aerzte) const idle = lines(looks()) const busy = lines(looks({ busyRooms: new Set(['aerzte']) })) assert.ok(busy > idle) // ein anderer Raum arbeitet: Ärzte & Praxen bleibt dunkel assert.equal(lines(looks({ busyRooms: new Set(['handwerk']) })), idle) }) test('graue Räume: Monitor aus trotz busyRooms, pult und tresor werden nie grau', () => { const lines = (look) => count(draw(look).rects, TEAM.aerzte) const grey = looks({ greyRooms: new Set(['aerzte']), busyRooms: new Set(['aerzte']) }) const greyIdle = looks({ greyRooms: new Set(['aerzte']) }) assert.equal(lines(grey), lines(greyIdle)) const never = looks({ greyRooms: new Set(['pult', 'tresor', 'gibt-es-nicht']) }) assert.equal(draw(never).texts.filter((x) => x.text === SIGN).length, 0) }) test('Schild steht innerhalb des grauen Raums und in der Etage seiner Figur', () => { const look = looks({ greyRooms: new Set(['kleinanzeigen']) }) const m = draw(look) const sign = m.texts.find((x) => x.text === SIGN) const floor = layout.floors.find((f) => f.id === 'anfragen') const room = floor.rooms.find((r) => r.id === 'kleinanzeigen') // Mitte des Raums (k = 3, align center), Zeile innerhalb der Etage assert.equal(sign.x, Math.round((room.x + room.w / 2) * 3)) assert.ok(sign.y >= floor.top * 3 && sign.y < floor.floorY * 3) }) test('unbekannte Raumart zeichnet nur Titel und Tint, ohne Wurf', () => { const odd = { ...layout, floors: layout.floors.map((f) => ({ ...f, rooms: f.rooms.map((r) => ({ ...r, kind: 'gibt-es-nicht' })) })) } const m = makeCtx() assert.doesNotThrow(() => R.drawRooms(m.ctx, odd, 1, R.createLook())) assert.ok(m.texts.some((x) => x.text === ROOM_TITLES.netz)) }) test('drawRooms hält save/restore im Gleichgewicht', () => { const m = draw(looks({ greyRooms: new Set(['netz']), pultOpen: 3, dealsMonth: 2 })) assert.equal(m.counts.save, m.counts.restore) }) test('Gebäude: Bodenlinie liegt auf dem Boden der Tresor-Etage (y 224)', () => { const tresor = layout.floors.find((f) => f.id === 'tresor') assert.equal(tresor.floorY, 224) const m = makeCtx() B.drawOutside(m.ctx) // k = 3: volle Breite 960, eine logische Einheit hoch, auf y 672 assert.ok(m.rects.some((r) => r.x === 0 && r.y === 224 * 3 && r.w === 960 && r.h === 3)) // Erde unter der Bodenlinie, bis zum Weltrand assert.ok(m.rects.some((r) => r.y === 225 * 3 && r.h === (256 - 225) * 3)) }) test('Gebäude: Lampe der Etage mit Kabine leuchtet grün, keine ohne Kabine', () => { const green = (at) => { const m = makeCtx() B.drawTower(m.ctx, layout, 1, { at }) return count(m.rects, PAL.green) } const none = green(() => false) assert.equal(green((i) => i === 3), none + 1) assert.equal(green((i) => i === 0 || i === 9), none + 2) }) test('Gebäude: Etagenschilder rechts außen bei x 234, in der Zeile ihrer Etage', () => { const m = makeCtx() B.drawTower(m.ctx, layout, 1, { at: () => false }) for (const floor of layout.floors) { const sign = m.texts.find((x) => x.text === floor.title) assert.equal(sign.x, 234 * 3, floor.id) assert.ok(sign.y >= floor.top * 3 && sign.y < floor.floorY * 3, floor.id) } }) test('Gebäude: Hülle reicht bis zum Fundament der letzten Etage', () => { const m = makeCtx() B.drawTower(m.ctx, layout, 1, { at: () => false }) const last = layout.floors[layout.floors.length - 1] const bottom = Math.max(...m.rects.map((r) => (r.y + r.h) / 3)) assert.equal(bottom, last.floorY + 2) assert.ok(m.rects.every((r) => r.x >= 0 && (r.x + r.w) / 3 <= 320)) }) test('Pausenschleier: 45 % Weiß über der ganzen Welt, Aufrufe gleichen sich aus', () => { const m = makeCtx() B.drawPauseVeil(m.ctx, looks({ paused: true })) assert.ok(m.alphas.includes(0.45)) const veil = m.rects.find((r) => r.fill === PAL.w) assert.ok(veil.x <= 0 && veil.y <= 0 && veil.x + veil.w >= 960 && veil.y + veil.h >= 768) assert.equal(m.counts.save, m.counts.restore) const off = makeCtx() B.drawPauseVeil(off.ctx, R.createLook()) assert.equal(off.rects.length, 0) }) test('Gebäude: Layout ohne Etagen zeichnet nichts und wirft nicht', () => { const m = makeCtx() assert.doesNotThrow(() => B.drawTower(m.ctx, { floors: [], homes: {}, spots: {} }, 1, { at: () => false })) assert.doesNotThrow(() => R.drawRooms(m.ctx, { floors: [], homes: {}, spots: {} }, 1, R.createLook())) }) // ---- Fix-Runde 1: Zeichnung und Effektorte, graue Räume ---- // Rechtecke der Raumzeichnung, die den Mittelpunkt der Einheitszelle (x, y) überdecken; die Tönung, die den ganzen Raum füllt, zählt nicht const hitsAt = (floorId, spot, look = R.createLook()) => { const floor = layout.floors.find((f) => f.id === floorId) const full = Math.max(...floor.rooms.map((r) => r.w)) * 3 const m = makeCtx() R.drawRooms(m.ctx, { ...layout, floors: [floor] }, 1, look) const cx = spot.x * 3 + 1 const cy = spot.y * 3 + 1 return m.rects.filter((r) => r.w < full && cx >= r.x && cx < r.x + r.w && cy >= r.y && cy < r.y + r.h) } test('Zeichnung trifft den Spot: Briefkasten am Pult, Spitze genau bei spots.pult', () => { const spot = layout.spots.pult assert.ok(hitsAt('freigabe', spot).some((r) => r.fill === TEAM.freigabe), 'Briefkasten unter dem Spot') // Spitze: eine Zeile darüber ist kein Briefkasten mehr assert.ok(!hitsAt('freigabe', { x: spot.x, y: spot.y - 1 }).some((r) => r.fill === TEAM.freigabe)) }) test('Zeichnung trifft den Spot: Tresortür in der Mitte von spots.tresor', () => { const hit = hitsAt('tresor', layout.spots.tresor) assert.ok(hit.some((r) => r.fill === PAL.metal2 && r.w === 22 * 3 && r.h === 20 * 3), 'Tresorkörper unter dem Spot') // die 6 × 6 große Türplatte (x + 8 ... x + 14) umschließt den Spot assert.ok(hit.some((r) => r.fill === PAL.metal && r.w === 6 * 3 && r.h === 6 * 3), 'Türplatte unter dem Spot') }) test('Zeichnung trifft den Spot: Schublade eines Aktenschranks bei spots.archiv, nicht auf Arne', () => { const spot = layout.spots.archiv const hit = hitsAt('keller', spot) // oberste Schubladenfront (10 × 5) des ersten Aktenschranks assert.ok(hit.some((r) => r.fill === PAL.metal && r.w === 10 * 3 && r.h === 5 * 3), 'Schubladenfront unter dem Spot') assert.ok(hit.some((r) => r.fill === PAL.metal2 && r.w === 12 * 3), 'Schrankkorpus unter dem Spot') // Arnes Platz (Körper 12 breit, 14 hoch, Hut ab y - 2) liegt nicht auf dem Spot const arne = layout.homes.arne assert.ok(!(spot.x >= arne.x && spot.x < arne.x + 12 && spot.y >= arne.y - 2 && spot.y < arne.y + 14)) }) test('grauer Raum: Teamfarbton bleibt, das Schild ersetzt den Titel (kein Platz für beides)', () => { const floor = layout.floors.find((f) => f.id === 'anfragen') const room = floor.rooms.find((r) => r.id === 'kleinanzeigen') assert.equal(room.w, 46) const run = (look) => { const m = makeCtx() R.drawRooms(m.ctx, { ...layout, floors: [{ ...floor, rooms: [room] }] }, 1, look) return m } const normal = run(looks()) const grey = run(looks({ greyRooms: new Set(['kleinanzeigen']) })) const tintRects = (m) => m.rects.filter((r) => r.fill === TEAM[room.team] && r.w === room.w * 3 && r.h === floor.h * 3) assert.equal(tintRects(normal).length, 1) assert.equal(tintRects(grey).length, 1, 'Teamfarbton auch im grauen Raum') assert.ok(grey.alphas.includes(0.1) && grey.alphas.includes(0.45), 'Teamfarbton 0,10, darüber die Grautönung 0,45') assert.ok(normal.texts.some((x) => x.text === room.title)) assert.ok(!grey.texts.some((x) => x.text === room.title), 'im grauen Raum steht das Schild an der Stelle des Titels') assert.equal(grey.texts.filter((x) => x.text === SIGN).length, 1) assert.equal(normal.texts.filter((x) => x.text === SIGN).length, 0) }) test('Schilder: drawRooms mit signs: false lässt sie aus (Grautönung bleibt), drawSigns zeichnet genau diese Schilder an dieselbe Stelle', () => { const look = looks({ greyRooms: new Set(['kleinanzeigen', 'portale']) }) const withSigns = draw(look) const m = makeCtx() R.drawRooms(m.ctx, layout, 1, look, { signs: false }) assert.equal(m.texts.filter((x) => x.text === SIGN).length, 0) assert.equal(m.rects.length < withSigns.rects.length, true) assert.ok(m.alphas.includes(0.45), 'die Grautönung gehört weiter zum Raum') assert.deepEqual(m.texts.filter((x) => x.text !== SIGN).map((x) => x.text), withSigns.texts.filter((x) => x.text !== SIGN).map((x) => x.text)) const s = makeCtx() R.drawSigns(s.ctx, layout, look) assert.deepEqual(s.texts.map((x) => x.text), [SIGN, SIGN]) assert.deepEqual(s.texts.map((x) => [x.x, x.y]), withSigns.texts.filter((x) => x.text === SIGN).map((x) => [x.x, x.y])) const none = makeCtx() R.drawSigns(none.ctx, layout, R.createLook()) assert.equal(none.rects.length + none.texts.length, 0) const noWorker = makeCtx() R.drawSigns(noWorker.ctx, layout, looks({ greyRooms: new Set(['pult', 'tresor']) })) // Räume ohne Worker werden nie grau assert.equal(noWorker.texts.length, 0) }) test('Raumtitel: über den Möbeln gezeichnet (ein Monitor deckt sie nicht zu) und mit hellem Rand', () => { const floor = layout.floors.find((f) => f.id === 'gespraeche') const room = floor.rooms.find((r) => r.id === 'mail') const m = makeCtx() R.drawRooms(m.ctx, { ...layout, floors: [{ ...floor, rooms: [room] }] }, 1, R.createLook()) assert.equal(m.strokes.length, 1) assert.equal(m.strokes[0].text, room.title) assert.ok(m.strokes[0].style.startsWith('rgba(255, 255, 255')) assert.equal(m.strokes[0].rects, m.rects.length, 'nach dem Titel kommt keine Fläche mehr: Möbel, Tisch und Monitor liegen darunter') assert.ok(m.rects.length > 20) assert.equal(m.texts.filter((x) => x.text === room.title).length, 1) const grey = makeCtx() R.drawRooms(grey.ctx, { ...layout, floors: [{ ...floor, rooms: [{ ...room, id: 'kleinanzeigen' }] }] }, 1, looks({ greyRooms: new Set(['kleinanzeigen']) })) assert.equal(grey.strokes.length, 0, 'im grauen Raum steht kein Titel') })