import test from 'node:test' import assert from 'node:assert/strict' import { loadModule } from './load.mjs' import { makeCtx } from './helpers/fakeCtx.mjs' // Engine-Grundlagen: Raster, Figuren mit Posen, Text, Sprechblasen const S = await loadModule('src/components/workers/engine/sprites.js') test('px: gerundete Kanten ohne Naht, Standardraster 3, Versatz, Mindestgröße', () => { let { ctx, rects } = makeCtx(); S.setGrid(ctx, { k: 3.4, ox: 0, oy: 0 }) S.px(ctx, 0, 0, 1, 1, '#000'); S.px(ctx, 1, 0, 1, 1, '#111'); S.px(ctx, 2, 0, 1, 1, '#222') assert.equal(rects[0].x + rects[0].w, rects[1].x); assert.equal(rects[1].x + rects[1].w, rects[2].x) ;({ ctx, rects } = makeCtx()); S.px(ctx, 1, 2, 3, 4, '#fff'); assert.deepEqual([rects[0].x, rects[0].y, rects[0].w, rects[0].h], [3, 6, 9, 12]) ;({ ctx, rects } = makeCtx()); S.setGrid(ctx, { k: 2, ox: -10, oy: 0 }); S.px(ctx, 5, 0, 0.1, 1, '#fff'); assert.equal(rects[0].x, 0); assert.ok(rects[0].w >= 1) }) test('label: Schriftgröße wächst mit dem Raster', () => { const { ctx, texts } = makeCtx(); S.setGrid(ctx, { k: 6 }); S.label(ctx, 'Hallo', 1, 1, { size: 11 }); assert.match(texts[0].font, /22px/) }) test('drawWorker: Trefferfläche', () => { const { ctx } = makeCtx() assert.deepEqual(S.drawWorker(ctx, 10, 20, { hat: 'haiku' }), { x: 10, y: 18, w: 12, h: 16 }) assert.equal(S.drawWorker(ctx, 10, 20, { hat: 'haiku', carrying: true }).w, 14) assert.deepEqual(S.drawWorker(ctx, 10, 20, { hat: 'overseer', scale: 2 }), { x: 10, y: 16, w: 24, h: 32 }) }) test('drawWorker: Hutfarbe zeigt das Modell, Augen je Pose, Grau mit Alpha', () => { for (const hat of ['opus', 'sonnet', 'haiku', 'overseer']) { const { ctx, rects } = makeCtx(); S.drawWorker(ctx, 0, 0, { hat }); assert.ok(rects.some((r) => r.fill === S.HAT[hat][0]), hat) } const eyes = (pose) => { const { ctx, rects } = makeCtx(); S.drawWorker(ctx, 0, 0, { hat: 'haiku', pose }); return rects.filter((r) => r.fill === S.PAL.e).length } assert.equal(eyes('stand'), 4); assert.equal(eyes('sleep'), 2) for (const pose of ['stand', 'type', 'coffee', 'sleep']) { const { ctx, rects } = makeCtx(); S.drawWorker(ctx, 0, 0, { hat: 'sonnet', pose, t: 1.3 }); assert.ok(rects.length > 20, pose) } const g = makeCtx(); S.drawWorker(g.ctx, 0, 0, { hat: 'opus', grey: true }); assert.ok(g.alphas.includes(0.55)); assert.equal(g.counts.save, g.counts.restore) }) test('drawBubble bleibt im Bild (Spitze nahe am linken und rechten Rand, Raster 3, Breite 960)', () => { const l = makeCtx(); S.drawBubble(l.ctx, 2, 30, 'Hallo'); assert.ok(Math.min(...l.rects.map((r) => r.x)) >= 0) const r = makeCtx(); S.drawBubble(r.ctx, 318, 30, 'Hallo'); assert.ok(Math.max(...r.rects.map((q) => q.x + q.w)) <= 960) }) // ---- Ergänzungen: Verhalten, das die fünf Tests oben nicht berühren ---- test('Raster gilt je Kontext (WeakMap, kein Feld am Kontext); ohne setGrid gilt k = 3', () => { const a = makeCtx() const b = makeCtx() S.setGrid(a.ctx, { k: 6, ox: 1, oy: 2 }) S.px(a.ctx, 1, 1, 1, 1, '#000') S.px(b.ctx, 1, 1, 1, 1, '#000') assert.deepEqual([a.rects[0].x, a.rects[0].y, a.rects[0].w, a.rects[0].h], [7, 8, 6, 6]) assert.deepEqual([b.rects[0].x, b.rects[0].y, b.rects[0].w, b.rects[0].h], [3, 3, 3, 3]) assert.equal('wkGrid' in a.ctx, false) }) test('tint: Alpha nur um die Fläche, save und restore ausgeglichen', () => { const { ctx, rects, alphas, counts } = makeCtx() S.tint(ctx, 0, 0, 2, 2, '#ff0000', 0.25) assert.deepEqual([rects.length, rects[0].fill, rects[0].w], [1, '#ff0000', 6]) assert.deepEqual(alphas, [0.25]) assert.deepEqual(counts, { save: 1, restore: 1 }) }) test('label: Zahl wird zu Text, Hintergrund folgt der Ausrichtung', () => { const n = makeCtx() S.label(n.ctx, 42, 1, 1) assert.equal(n.texts[0].text, '42') const c = makeCtx() S.label(c.ctx, 'abcd', 10, 5, { size: 12, align: 'center', bg: '#ffffff' }) // Breite 4 * 7 = 28, Mitte bei 30: Kasten von 30 - 14 - 4 bis + 28 + 8, Höhe 12 + 5 assert.deepEqual([c.rects[0].x, c.rects[0].y, c.rects[0].w, c.rects[0].h, c.rects[0].fill], [12, 13, 36, 17, '#ffffff']) assert.equal(c.texts[0].x, 30) }) test('label: halo zeichnet erst einen hellen Rand (strokeText), dann den Text darüber; ohne halo kein Rand', () => { const h = makeCtx() S.setGrid(h.ctx, { k: 3 }) S.label(h.ctx, 'Mail-Stube', 2, 1, { size: 10, color: '#445566', halo: '#ffffff' }) assert.equal(h.strokes.length, 1) assert.deepEqual([h.strokes[0].text, h.strokes[0].style], ['Mail-Stube', '#ffffff']) assert.deepEqual([h.strokes[0].x, h.strokes[0].y], [h.texts[0].x, h.texts[0].y], 'Rand und Text an derselben Stelle') assert.equal(h.strokes[0].texts, 0, 'der Rand kommt vor dem Text') assert.ok(h.strokes[0].width >= 2, 'mindestens ein Geräte-Pixel Rand rundum') const big = makeCtx() S.setGrid(big.ctx, { k: 6 }) S.label(big.ctx, 'Mail-Stube', 2, 1, { size: 10, halo: '#ffffff' }) assert.ok(big.strokes[0].width > h.strokes[0].width, 'der Rand wächst mit dem Raster') const plain = makeCtx() S.label(plain.ctx, 'Mail-Stube', 2, 1, { size: 10 }) assert.equal(plain.strokes.length, 0) }) test('drawWorker: Schlafen zeigt ein z über dem Hut, das mit t steigt; stehen nicht', () => { const zs = (pose, t) => { const { ctx, rects } = makeCtx() S.drawWorker(ctx, 20, 40, { hat: 'haiku', pose, t }) return rects.filter((r) => r.fill === S.PAL.mute) } const z0 = zs('sleep', 0) const z1 = zs('sleep', 0.5) assert.equal(z0.length, 3) assert.equal(zs('stand', 0).length, 0) assert.ok(z1[0].y < z0[0].y, 'steigt') assert.ok(Math.max(...z0.map((r) => r.y + r.h)) <= (40 - 2) * 3, 'über dem Hut') }) test('drawWorker: Kaffee zeichnet eine 3x3-Tasse und einen flackernden Dampfpixel', () => { const run = (pose, t) => { const { ctx, rects } = makeCtx() S.drawWorker(ctx, 0, 0, { hat: 'sonnet', pose, t }) return rects } const cups = (rects) => rects.filter((r) => r.fill === S.PAL.metal2 && r.w === 9 && r.h === 9).length const steam = (rects) => rects.filter((r) => r.fill === S.PAL.g).length assert.equal(cups(run('coffee', 0)), 1) assert.equal(cups(run('stand', 0)), 0) assert.deepEqual([steam(run('coffee', 0)), steam(run('coffee', 0.4)), steam(run('stand', 0))], [1, 0, 0]) }) test('drawWorker: Grau tauscht Körper- und Hutfarben, Trefferfläche bleibt', () => { const grey = makeCtx() const hit = S.drawWorker(grey.ctx, 10, 20, { hat: 'opus', grey: true, carrying: true }) assert.deepEqual(hit, S.drawWorker(makeCtx().ctx, 10, 20, { hat: 'opus', carrying: true })) const own = [S.PAL.b, S.PAL.s, S.PAL.h, S.HAT.opus[0], S.HAT.opus[1]] assert.ok(!grey.rects.some((r) => own.includes(r.fill))) assert.deepEqual([grey.alphas, grey.counts], [[0.55], { save: 1, restore: 1 }]) }) test('drawWorker: Auswahlpfeil nur bei selected, außerhalb der Graustufe', () => { const ink = (opts) => { const { ctx, rects, alphas } = makeCtx() S.drawWorker(ctx, 0, 0, { hat: 'haiku', ...opts }) return [rects.filter((r) => r.fill === S.PAL.ink).length, alphas.length] } assert.deepEqual(ink({}), [0, 0]) assert.deepEqual(ink({ selected: true }), [9, 0]) assert.deepEqual(ink({ selected: true, grey: true }), [9, 1]) }) test('drawAlertMark: rotes 3x7-Ausrufezeichen mit Umriss, unten mittig bei (x, y)', () => { const { ctx, rects } = makeCtx() S.drawAlertMark(ctx, 50, 30) assert.deepEqual([...new Set(rects.map((r) => r.fill))].sort(), [S.PAL.o, S.PAL.red].sort()) const red = rects.filter((r) => r.fill === S.PAL.red) const left = Math.min(...red.map((r) => r.x)) const right = Math.max(...red.map((r) => r.x + r.w)) const top = Math.min(...red.map((r) => r.y)) const bottom = Math.max(...red.map((r) => r.y + r.h)) assert.deepEqual([right - left, bottom - top], [9, 21]) assert.ok(Math.abs((left + right) / 2 - 150) <= 1, 'mittig') assert.equal(Math.max(...rects.map((r) => r.y + r.h)), 90) }) test('drawBubble: Text und Maße folgen dem Raster, leerer Text zeichnet nichts, Spitze außerhalb bleibt im Bild', () => { const a = makeCtx() S.drawBubble(a.ctx, 50, 40, 'Hallo') assert.deepEqual([a.texts.length, a.texts[0].text], [1, 'Hallo']) assert.match(a.texts[0].font, /12px/) const b = makeCtx() S.setGrid(b.ctx, { k: 6 }) S.drawBubble(b.ctx, 50, 40, 'Hallo') assert.match(b.texts[0].font, /24px/) const e = makeCtx() S.drawBubble(e.ctx, 50, 40, '') assert.equal(e.rects.length, 0) const o = makeCtx() S.drawBubble(o.ctx, 330, 30, 'Hallo') assert.ok(Math.max(...o.rects.map((q) => q.x + q.w)) <= 960) const small = makeCtx({ width: 100 }) S.drawBubble(small.ctx, 20, 30, 'Hallo') assert.ok(Math.max(...small.rects.map((q) => q.x + q.w)) <= 100) }) test('drawSideBubble: links neben dem Körper, Spitze nach rechts; am linken Rand rechts daneben, Spitze nach links', () => { const l = makeCtx() S.drawSideBubble(l.ctx, 'Hallo', { leftX: 100, rightX: 130, y: 10 }) assert.deepEqual(l.texts.map((t) => t.text), ['Hallo']) assert.ok(Math.max(...l.rects.map((r) => r.x + r.w)) <= 300, 'endet vor der linken Körperkante') assert.ok(Math.max(...l.rects.map((r) => r.x + r.w)) >= 290, 'Spitze reicht bis an den Körper') assert.equal(l.rects.length, 4) const r = makeCtx() S.drawSideBubble(r.ctx, 'Hallo', { leftX: 10, rightX: 40, y: 10 }) assert.ok(Math.min(...r.rects.map((q) => q.x)) >= 120, 'beginnt hinter der rechten Körperkante') assert.ok(Math.min(...r.rects.map((q) => q.x)) <= 124, 'Spitze reicht bis an den Körper') }) test('drawSideBubble: Oberkante bei y, Maße und Schrift folgen dem Raster, rechts in die Bildbreite geklemmt, leer zeichnet nichts', () => { const a = makeCtx() S.drawSideBubble(a.ctx, 'Hallo', { leftX: 100, rightX: 130, y: 10 }) const white = a.rects.find((q) => q.fill === S.PAL.w) assert.deepEqual([white.y, white.h], [30, 22]) assert.match(a.texts[0].font, /12px/) const b = makeCtx() S.setGrid(b.ctx, { k: 6 }) S.drawSideBubble(b.ctx, 'Hallo', { leftX: 100, rightX: 130, y: 10 }) assert.match(b.texts[0].font, /24px/) assert.deepEqual([b.rects.find((q) => q.fill === S.PAL.w).y, b.rects.find((q) => q.fill === S.PAL.w).h], [60, 44]) const small = makeCtx({ width: 120 }) S.drawSideBubble(small.ctx, 'Hallo', { leftX: 10, rightX: 40, y: 10 }) assert.ok(Math.max(...small.rects.map((q) => q.x + q.w)) <= 120) const e = makeCtx() S.drawSideBubble(e.ctx, '', { leftX: 100, rightX: 130, y: 10 }) S.drawSideBubble(e.ctx, undefined, { leftX: 100, rightX: 130, y: 10 }) assert.equal(e.rects.length, 0) })