Workers P2: Engine-Requisiten und Branchen-Icons

Möbel und Requisiten aus der Vorlage (Schreibtisch, Monitor mit Schalter an/aus, Aktenschrank, Pflanze, Münze, Pokal, Stempel, Radar, Briefkasten, Tresor, Waage), neu Ablagekorb, Schild und die acht 7x7-Branchen-Icons mit drawIcon.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-18 01:58:15 +00:00
1 parent d44b28368d
commit 514469d93c
2 files changed
+337

No files matched your search

+255
View File
@@ -0,0 +1,255 @@
// Möbel, Requisiten und Branchen-Icons des Hustler-Hochhauses.
// Portiert aus entwuerfe/sprites.js (Möbel und Requisiten, ohne drawConveyor und drawZone). Neu: drawTray, drawSign,
// drawIcon samt ICONS und der Schalter `on` an drawMonitor.
// Alle Maße in logischen Einheiten; gezeichnet wird über px() aus sprites.js, es gibt keinen eigenen Zustand.
// Kein React, kein Appwrite, kein window/document.
import { px, label, PAL, TEAM, HAT } from './sprites.js'
// ---------- Möbel ----------
export function drawDesk(ctx, x, y, w, accent) {
px(ctx, x, y, w, 1, PAL.wood2)
px(ctx, x, y + 1, w, 4, PAL.wood)
px(ctx, x, y + 5, w, 1, PAL.wood2)
if (accent) px(ctx, x, y + 5, w, 1, accent)
px(ctx, x + 1, y + 6, 1, 5, PAL.wood2)
px(ctx, x + w - 2, y + 6, 1, 5, PAL.wood2)
px(ctx, x + 1, y + 11, w - 2, 1, PAL.shadow)
}
// on = false: nur Rahmen, Schirm und Fuß, keine Zeilen und kein Blinken
export function drawMonitor(ctx, x, y, t, color, on = true) {
const c = color || '#7fb2ff'
px(ctx, x, y, 10, 7, PAL.o)
px(ctx, x + 1, y + 1, 8, 5, PAL.screen)
if (on) {
const long = Math.floor((t || 0) * 2) % 2 === 0
px(ctx, x + 2, y + 2, 5, 1, c)
px(ctx, x + 2, y + 4, long ? 4 : 2, 1, c)
}
px(ctx, x + 4, y + 7, 2, 2, PAL.o)
px(ctx, x + 2, y + 9, 6, 1, PAL.o)
}
export function drawCabinet(ctx, x, y, drawers) {
const n = drawers || 3
px(ctx, x, y, 12, n * 6 + 1, PAL.metal2)
for (let i = 0; i < n; i++) {
px(ctx, x + 1, y + 1 + i * 6, 10, 5, PAL.metal)
px(ctx, x + 4, y + 3 + i * 6, 4, 1, PAL.metal2)
}
px(ctx, x, y + n * 6 + 1, 12, 1, PAL.shadow)
}
export function drawPlant(ctx, x, y) {
px(ctx, x + 2, y, 2, 2, '#5cbf8a')
px(ctx, x, y + 2, 2, 2, '#5cbf8a')
px(ctx, x + 4, y + 1, 2, 3, '#3a9466')
px(ctx, x + 2, y + 2, 2, 3, '#3a9466')
px(ctx, x + 1, y + 5, 4, 4, '#c9714f')
px(ctx, x + 1, y + 5, 4, 1, '#a85a3c')
}
// Ablagekorb / Eingangskorb, 10 × 4: voll = Papierstapel ragt über den Rand
export function drawTray(ctx, x, y, filled) {
px(ctx, x, y + 1, 10, 1, PAL.metal2) // Rand
px(ctx, x, y + 2, 10, 1, PAL.metal) // Vorderwand
px(ctx, x, y + 3, 10, 1, PAL.metal2) // Boden
if (filled) {
px(ctx, x + 1, y, 8, 1, PAL.g) // Oberkante des Stapels
px(ctx, x + 1, y + 1, 8, 1, PAL.w) // oberstes Blatt in der Öffnung
px(ctx, x + 3, y + 1, 4, 1, PAL.p) // eine Textzeile darauf
}
}
// ---------- Requisiten ----------
export function drawCoin(ctx, x, y, t) {
const f = Math.floor((t || 0) * 6) % 4
const w = [5, 3, 1, 3][f]
const ox = (5 - w) / 2
px(ctx, x + ox, y, w, 5, PAL.gold2)
if (w > 2) px(ctx, x + ox + 1, y + 1, w - 2, 3, PAL.gold)
}
export function drawTrophy(ctx, x, y) {
px(ctx, x, y, 7, 1, PAL.gold2)
px(ctx, x, y + 1, 7, 3, PAL.gold)
px(ctx, x + 1, y + 4, 5, 1, PAL.gold2)
px(ctx, x + 3, y + 5, 1, 2, PAL.gold2)
px(ctx, x + 1, y + 7, 5, 2, PAL.wood2)
px(ctx, x + 1, y + 1, 1, 2, '#fff3c4')
}
export function drawStamp(ctx, x, y, ok) {
const c = ok ? PAL.green : PAL.red
px(ctx, x, y, 7, 1, c)
px(ctx, x, y + 4, 7, 1, c)
px(ctx, x, y, 1, 5, c)
px(ctx, x + 6, y, 1, 5, c)
if (ok) {
// Haken
px(ctx, x + 2, y + 2, 1, 1, c)
px(ctx, x + 3, y + 3, 1, 1, c)
px(ctx, x + 4, y + 1, 1, 2, c)
} else {
// Kreuz
px(ctx, x + 2, y + 1, 1, 1, c)
px(ctx, x + 4, y + 1, 1, 1, c)
px(ctx, x + 3, y + 2, 1, 1, c)
px(ctx, x + 2, y + 3, 1, 1, c)
px(ctx, x + 4, y + 3, 1, 1, c)
}
}
export function drawRadar(ctx, x, y, t) {
px(ctx, x + 2, y, 10, 1, PAL.o)
px(ctx, x, y + 1, 14, 10, PAL.o)
px(ctx, x + 1, y + 2, 12, 8, '#e8f2fc')
px(ctx, x + 6, y + 5, 2, 2, TEAM.radar)
const a = (t || 0) * 2.2
for (let i = 1; i < 6; i++) px(ctx, x + 7 + Math.round(Math.cos(a) * i), y + 6 + Math.round(Math.sin(a) * i * 0.7), 1, 1, TEAM.radar)
const blink = Math.floor((t || 0) * 3) % 3
if (blink === 0) px(ctx, x + 3, y + 3, 1, 1, PAL.red)
if (blink === 1) px(ctx, x + 10, y + 7, 1, 1, PAL.red)
px(ctx, x + 6, y + 11, 2, 3, PAL.o)
px(ctx, x + 3, y + 14, 8, 1, PAL.o)
}
// Briefkasten des Pults; das rote Abzeichen mit „!“ steht nur bei count > 0
export function drawMailbox(ctx, x, y, count) {
px(ctx, x, y + 2, 12, 8, TEAM.freigabe)
px(ctx, x + 1, y, 10, 2, TEAM.freigabe)
px(ctx, x + 2, y + 4, 8, 2, HAT.overseer[1])
px(ctx, x + 5, y + 10, 2, 5, PAL.o)
if (count > 0) {
px(ctx, x + 9, y - 3, 5, 5, PAL.red)
px(ctx, x + 11, y - 2, 1, 3, PAL.w)
}
}
export function drawVault(ctx, x, y, t) {
px(ctx, x, y, 22, 20, PAL.metal2)
px(ctx, x + 1, y + 1, 20, 18, PAL.metal)
px(ctx, x + 7, y + 6, 8, 8, PAL.metal2)
px(ctx, x + 8, y + 7, 6, 6, PAL.metal)
const a = (t || 0) * 0.8
px(ctx, x + 11 + Math.round(Math.cos(a) * 2), y + 10 + Math.round(Math.sin(a) * 2), 1, 1, PAL.o)
px(ctx, x + 18, y + 9, 2, 3, PAL.metal2)
px(ctx, x, y + 20, 22, 1, PAL.shadow)
}
export function drawScale(ctx, x, y, tilt) {
const d = Math.round(tilt || 0)
px(ctx, x + 7, y + 2, 2, 12, PAL.o)
px(ctx, x + 3, y + 14, 10, 2, PAL.o)
px(ctx, x, y + 2 - d, 16, 1, PAL.o)
px(ctx, x - 1, y + 3 - d, 5, 2, TEAM.bewertung)
px(ctx, x + 12, y + 3 + d, 5, 2, TEAM.bewertung)
}
// Weißes Schild mit dunklem Rand, 8 hoch; (x, y) = linke obere Ecke, Text zentriert (z. B. „noch nicht angeschlossen“)
export function drawSign(ctx, x, y, w, text) {
px(ctx, x, y, w, 8, PAL.o)
px(ctx, x + 1, y + 1, w - 2, 6, PAL.w)
label(ctx, text, x + w / 2, y + 2.5, { size: 9, color: PAL.ink, align: 'center' })
}
// ---------- Branchen-Icons (7 × 7) ----------
// '.' leer, 'x' Linie in der Teamfarbe, 'a' Akzent in Gold. Schlüssel = die acht Such-Räume (SEARCH_ROOMS)
const icon = (...rows) => Object.freeze(rows)
export const ICONS = Object.freeze({
netz: icon( // Funkwellen
'.xxxxx.',
'x.....x',
'..xxx..',
'.x...x.',
'.......',
'...a...',
'...a...',
),
kleinanzeigen: icon( // Umschlag mit @-Punkt
'xxxxxxx',
'xx...xx',
'x.x.x.x',
'x..x..x',
'x.....x',
'x..a..x',
'xxxxxxx',
),
portale: icon( // Zeilenliste
'xxxxxxx',
'.......',
'a.xxxx.',
'.......',
'a.xxxx.',
'.......',
'a.xxxx.',
),
foren: icon( // Sprechblase
'.xxxxx.',
'x.....x',
'x.a.a.x',
'x.....x',
'.xxxxx.',
'..xx...',
'.x.....',
),
aerzte: icon( // Kreuz
'..xxx..',
'..xxx..',
'xxxxxxx',
'xxxxxxx',
'xxxxxxx',
'..xxx..',
'..xxx..',
),
handwerk: icon( // Hammer
'.xxxxx.',
'xxxxxxx',
'.xxxxx.',
'...a...',
'...a...',
'...a...',
'...a...',
),
selbst: icon( // Person
'..xxx..',
'.xxxxx.',
'..xxx..',
'.......',
'.xxxxx.',
'xxxxxxx',
'xxxxxxx',
),
laden: icon( // Markise
'.xxxxx.',
'xaxaxax',
'xaxaxax',
'x.x.x.x',
'.......',
'x.....x',
'xxxxxxx',
),
})
// Zeichnet das Icon `name` mit der linken oberen Ecke bei (x, y): 'x' in `color`, 'a' in Gold.
// Gleiche Zeichen einer Zeile werden zu einem Rechteck zusammengefasst; ein unbekannter Name zeichnet nichts.
export function drawIcon(ctx, name, x, y, color) {
const rows = typeof name === 'string' ? ICONS[name] : null
if (!Array.isArray(rows)) return
for (let r = 0; r < rows.length; r++) {
const row = rows[r]
let c = 0
while (c < row.length) {
const ch = row[c]
let end = c + 1
while (end < row.length && row[end] === ch) end++
if (ch === 'x') px(ctx, x + c, y + r, end - c, 1, color)
else if (ch === 'a') px(ctx, x + c, y + r, end - c, 1, PAL.gold)
c = end
}
}
}
+82
View File
@@ -0,0 +1,82 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
import { makeCtx } from './helpers/fakeCtx.mjs'
// Engine-Requisiten: Möbel, Briefkasten, Tresor, Waage, Schild und die 7×7-Branchen-Icons
const P = await loadModule('src/components/workers/engine/props.js')
const { PAL } = await loadModule('src/components/workers/engine/sprites.js')
const { SEARCH_ROOMS } = await loadModule('src/lib/workersRoster.js')
test('alle Requisiten zeichnen ohne Wurf und mindestens ein Rechteck', () => {
const calls = { desk: (c) => P.drawDesk(c, 0, 0, 18, '#f00'), monitor: (c) => P.drawMonitor(c, 0, 0, 1, '#7fb2ff'), cabinet: (c) => P.drawCabinet(c, 0, 0, 3), plant: (c) => P.drawPlant(c, 0, 0),
coin: (c) => P.drawCoin(c, 0, 0, 0.3), trophy: (c) => P.drawTrophy(c, 0, 0), stampOk: (c) => P.drawStamp(c, 0, 0, true), stampNo: (c) => P.drawStamp(c, 0, 0, false),
radar: (c) => P.drawRadar(c, 0, 0, 1), mailbox: (c) => P.drawMailbox(c, 0, 0, 2), vault: (c) => P.drawVault(c, 0, 0, 1), scale: (c) => P.drawScale(c, 0, 0, 2), tray: (c) => P.drawTray(c, 0, 0, true) }
for (const [name, fn] of Object.entries(calls)) { const { ctx, rects } = makeCtx(); fn(ctx); assert.ok(rects.length >= 1, name) }
})
test('Briefkasten-Abzeichen nur bei count > 0; Monitor aus zeigt keine Inhaltszeilen', () => {
const red = (n) => { const { ctx, rects } = makeCtx(); P.drawMailbox(ctx, 0, 0, n); return rects.filter((r) => r.fill === PAL.red).length }
assert.equal(red(0), 0); assert.ok(red(3) >= 1)
const lines = (on) => { const { ctx, rects } = makeCtx(); P.drawMonitor(ctx, 0, 0, 1, '#abcdef', on); return rects.filter((r) => r.fill === '#abcdef').length }
assert.ok(lines(true) >= 2); assert.equal(lines(false), 0)
})
test('ICONS: genau die 8 Suchräume, je 7 Zeilen à 7 Zeichen; drawSign schreibt den Text', () => {
assert.deepEqual(Object.keys(P.ICONS).sort(), ['aerzte', 'foren', 'handwerk', 'kleinanzeigen', 'laden', 'netz', 'portale', 'selbst'])
for (const [k, rows] of Object.entries(P.ICONS)) { assert.equal(rows.length, 7, k); for (const r of rows) assert.match(r, /^[.xa]{7}$/, k) }
const { ctx, texts, rects } = makeCtx(); P.drawSign(ctx, 0, 0, 40, 'noch nicht angeschlossen'); assert.equal(texts[0].text, 'noch nicht angeschlossen'); assert.ok(rects.length >= 2)
const t = makeCtx(); P.drawIcon(t.ctx, 'aerzte', 0, 0, '#3fb6b2'); assert.ok(t.rects.length >= 5)
})
// ---- Ergänzungen: Verhalten, das die drei Tests oben nicht berühren ----
test('ICONS: Schlüssel gleich SEARCH_ROOMS, eingefroren, jedes Icon hat Linien und die Zeichen stehen nur in .xa', () => {
assert.deepEqual([...SEARCH_ROOMS].sort(), Object.keys(P.ICONS).sort())
assert.ok(Object.isFrozen(P.ICONS))
for (const [k, rows] of Object.entries(P.ICONS)) {
assert.ok(Object.isFrozen(rows), k)
assert.ok(rows.join('').includes('x'), k)
}
})
test('drawIcon: x in der Farbe, a in Gold, nichts außerhalb von 7 × 7; unbekannter Name zeichnet nichts und wirft nicht', () => {
const { ctx, rects } = makeCtx(); P.drawIcon(ctx, 'handwerk', 10, 20, '#3fb6b2')
assert.ok(rects.some((r) => r.fill === '#3fb6b2')); assert.ok(rects.some((r) => r.fill === PAL.gold))
assert.ok(rects.every((r) => r.fill === '#3fb6b2' || r.fill === PAL.gold))
assert.ok(rects.every((r) => r.x >= 30 && r.x + r.w <= 30 + 21 && r.y >= 60 && r.y + r.h <= 60 + 21))
for (const name of ['gibt-es-nicht', 'constructor', '__proto__', 'toString', undefined, null]) {
const e = makeCtx(); assert.doesNotThrow(() => P.drawIcon(e.ctx, name, 0, 0, '#000'), String(name)); assert.equal(e.rects.length, 0, String(name))
}
})
test('drawIcon: jedes der acht Icons zeichnet sichtbar (mindestens 5 Rechtecke)', () => {
for (const name of Object.keys(P.ICONS)) { const { ctx, rects } = makeCtx(); P.drawIcon(ctx, name, 0, 0, '#123456'); assert.ok(rects.length >= 5, name) }
})
test('drawMonitor: Standard ist an; aus hat kein Blinken, an blinkt mit t', () => {
const draw = (t, on) => { const { ctx, rects } = makeCtx(); if (on === undefined) P.drawMonitor(ctx, 0, 0, t, '#abcdef'); else P.drawMonitor(ctx, 0, 0, t, '#abcdef', on); return JSON.stringify(rects) }
assert.equal(draw(0.2), draw(0.2, true))
assert.notEqual(draw(0.2, true), draw(0.7, true))
assert.equal(draw(0.2, false), draw(0.7, false))
const off = makeCtx(); P.drawMonitor(off.ctx, 0, 0, 1, '#abcdef', false); assert.ok(off.rects.length >= 2) // Rahmen und Schirm bleiben
})
test('drawTray: voll zeigt Papier über dem Rand, beide bleiben in 10 × 4', () => {
const draw = (filled) => { const { ctx, rects } = makeCtx(); P.drawTray(ctx, 5, 7, filled); return rects }
const full = draw(true); const empty = draw(false)
assert.ok(full.some((r) => r.fill === PAL.w)); assert.ok(!empty.some((r) => r.fill === PAL.w))
for (const rs of [full, empty]) assert.ok(rs.every((r) => r.x >= 15 && r.x + r.w <= 15 + 30 && r.y >= 21 && r.y + r.h <= 21 + 12))
assert.equal(Math.max(...full.map((r) => r.x + r.w)) - Math.min(...full.map((r) => r.x)), 30)
})
test('drawSign: dunkler Rand, weiße Fläche, Text mittig im Schild', () => {
const { ctx, rects, texts } = makeCtx(); P.drawSign(ctx, 4, 6, 40, 'Ablage')
assert.equal(rects[0].fill, PAL.o); assert.ok(rects.some((r) => r.fill === PAL.w))
const border = rects[0]; assert.deepEqual([border.x, border.y, border.w], [12, 18, 120])
assert.ok(rects.slice(1).every((r) => r.x > border.x && r.x + r.w < border.x + border.w && r.y > border.y && r.y + r.h < border.y + border.h))
assert.equal(texts.length, 1); assert.equal(texts[0].x, (4 + 20) * 3)
assert.ok(texts[0].y > border.y && texts[0].y < border.y + border.h)
})
test('drawStamp: ok grün, nein rot; drawCoin dreht sich mit t; drawScale kippt mit tilt', () => {
const fills = (fn) => { const { ctx, rects } = makeCtx(); fn(ctx); return new Set(rects.map((r) => r.fill)) }
assert.ok(fills((c) => P.drawStamp(c, 0, 0, true)).has(PAL.green)); assert.ok(!fills((c) => P.drawStamp(c, 0, 0, true)).has(PAL.red))
assert.ok(fills((c) => P.drawStamp(c, 0, 0, false)).has(PAL.red)); assert.ok(!fills((c) => P.drawStamp(c, 0, 0, false)).has(PAL.green))
const coinW = (t) => { const { ctx, rects } = makeCtx(); P.drawCoin(ctx, 0, 0, t); return rects[0].w }
assert.notEqual(coinW(0), coinW(1 / 6))
const pans = (tilt) => { const { ctx, rects } = makeCtx(); P.drawScale(ctx, 0, 0, tilt); return rects.filter((r) => r.w === 15 && r.h === 6).map((r) => r.y) }
assert.equal(pans(0).length, 2); assert.notDeepEqual(pans(2), pans(-2))
})