Workers P2: Räume nach kind, Pokal-Seiten, graue Räume, Gebäudehülle

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-18 02:29:57 +00:00
1 parent fb331de1a6
commit 8555453a74
3 files changed
+660

No files matched your search

+114
View File
@@ -0,0 +1,114 @@
// Gebäudehülle des Hustler-Hochhauses: Boden und Erde, Dach, Aufzugschacht, Etagenböden und -wände, Schachttüren mit Lampen,
// Etagenschilder rechts außen und der Pausenschleier.
// Portiert aus entwuerfe/scene-turm.js (drawOutside Z. 99-111, drawBuilding Z. 118-172) auf 10 Etagen. Die Stadtsilhouette,
// das Rohrpost-Rohr und die Welle der Vorlage entfallen. Die Räume selbst zeichnet rooms.js (danach, über der Rückwand).
// Etagenmaße kommen aus dem Layout, die Hüllenmaße (Turm x 14..230, Räume x 42..228) stehen hier. Kein React, kein Appwrite,
// kein window/document.
import { px, tint, label, PAL, TEAM } from './sprites.js'
import { WORLD } from './layout.js'
const GROUND_Y = 224 // Bodenlinie = Boden der Tresor-Etage (Erdgeschoss); darunter liegt der Keller
const ROOF_Y = 4 // Oberkante der Dachlinie
const HULL_X0 = 14 // Turm: linke Außenkante
const HULL_W = 216 // Turm: Breite (x 14..230)
const WALL_W = 2 // Wandstärke
const SHAFT_X = 16 // Aufzugschacht x 16..40
const SHAFT_W = 24
const SHAFT_WALL_X = 40 // Trennwand zwischen Schacht und Räumen
const ROOMS_X0 = 42
const ROOMS_W = 186
const DOOR_H = 18 // Schachttür: Sturz 18 über dem Boden
const LAMP_X = 43
const SIGN_DOT_X = 230 // Etagenschild: Punkt rechts neben der Außenwand, Text bei x 234
const SIGN_X = 234
const SIDE_DOOR_X = 225
const C = {
wall: '#aab2c0',
back: '#f3f4f7',
strip: '#e6e9ef',
shaft: '#e4e8ef',
rail: '#d3d8e1',
frame: PAL.metal2,
mute: PAL.mute,
lampOff: '#c3cad6',
ground: PAL.metal,
earth: '#f7f4f0',
speck: '#e8e2da',
post: '#d5dae3',
doorWood: '#a8825f',
}
// Erdflecken: [x, Abstand unter der Bodenlinie]; die mittleren liegen unter dem Keller und sind verdeckt
const SPECKS = [[4, 7], [9, 15], [6, 25], [244, 9], [262, 17], [290, 8], [306, 19], [300, 26], [60, 28], [140, 30], [210, 27]]
// Boden, Erde, Dachlinie mit Fahne und eine Laterne links. Wird vor dem Turm gezeichnet.
export function drawOutside(ctx) {
px(ctx, 0, GROUND_Y + 1, WORLD.w, WORLD.h - GROUND_Y - 1, C.earth)
px(ctx, 0, GROUND_Y, WORLD.w, 1, C.ground)
for (const [x, dy] of SPECKS) px(ctx, x, GROUND_Y + dy, 2, 1, C.speck)
// Laterne
px(ctx, 6, GROUND_Y - 19, 1, 19, C.post)
px(ctx, 4, GROUND_Y - 21, 5, 2, C.post)
px(ctx, 5, GROUND_Y - 19, 3, 1, '#fff3c4')
// Dachlinie und Fahne
px(ctx, HULL_X0, ROOF_Y, HULL_W, 2, C.frame)
px(ctx, 60, 0, 1, 4, C.frame)
px(ctx, 61, 0, 4, 2, TEAM.freigabe)
}
// Hülle, Schacht, Rückwände, Böden, Schachttüren mit Lampen (grün: Kabine steht auf der Etage) und die Etagenschilder.
// t ist im Aufruf vorgesehen (Takt der Szene), die Hülle bewegt sich heute nicht.
export function drawTower(ctx, layout, t, lift) {
const floors = layout.floors
if (floors.length === 0) return
const topY = floors[0].top
const bottom = floors[floors.length - 1].floorY
// Schacht mit Schienen
px(ctx, SHAFT_X, topY, SHAFT_W, bottom - topY, C.shaft)
px(ctx, SHAFT_X, topY, 1, bottom - topY, C.rail)
px(ctx, SHAFT_X + SHAFT_W - 1, topY, 1, bottom - topY, C.rail)
// Rückwand und Bodenstreifen jeder Etage
for (const f of floors) {
px(ctx, ROOMS_X0, f.top, ROOMS_W, f.h, C.back)
px(ctx, ROOMS_X0, f.floorY - 3, ROOMS_W, 3, C.strip)
}
// Trennwand zum Schacht mit Türöffnung je Etage, Sturz und Etagenlampe
px(ctx, SHAFT_WALL_X, topY, WALL_W, bottom - topY, C.back)
floors.forEach((f, i) => {
px(ctx, SHAFT_WALL_X, f.top, WALL_W, f.h - DOOR_H, C.wall)
px(ctx, SHAFT_WALL_X, f.floorY - DOOR_H, WALL_W, 1, C.frame)
px(ctx, LAMP_X, f.floorY - 14, 2, 2, lift.at(i) ? PAL.green : C.lampOff)
})
// Etagenböden, Fundament und Seitenwände
for (const f of floors) px(ctx, HULL_X0, f.floorY, HULL_W, 1, C.wall)
px(ctx, HULL_X0, bottom, HULL_W, 2, C.frame)
px(ctx, HULL_X0, ROOF_Y, WALL_W, bottom + 2 - ROOF_Y, C.wall)
px(ctx, ROOMS_X0 + ROOMS_W, ROOF_Y, WALL_W, bottom + 2 - ROOF_Y, C.wall)
// Seitentür im Erdgeschoss (Tresor-Etage): hier gehen die Scouts in die Stadt
const ground = floors.find((f) => f.id === 'tresor')
if (ground) {
const g = ground.floorY
px(ctx, ROOMS_X0 + ROOMS_W, g - 14, WALL_W, 11, C.back)
px(ctx, ROOMS_X0 + ROOMS_W, g - 3, WALL_W, 3, C.strip)
px(ctx, SIDE_DOOR_X, g - 16, 8, 2, C.doorWood)
px(ctx, SIDE_DOOR_X, g - 14, 2, 14, C.doorWood)
px(ctx, SIDE_DOOR_X + 6, g - 14, 2, 14, C.doorWood)
}
// Schachtkopf mit Rolle
px(ctx, 23, topY, 11, 3, C.frame)
px(ctx, 24, topY + 1, 9, 1, C.ground)
// Etagenschilder rechts außen: Punkt in der Teamfarbe des ersten Raums, Titel aus dem Layout
for (const f of floors) {
px(ctx, SIGN_DOT_X, f.top + 2, 2, 2, TEAM[f.rooms[0]?.team] ?? C.wall)
label(ctx, f.title, SIGN_X, f.top + 1, { size: 11, color: C.mute, weight: 600 })
}
}
// Weißer Schleier mit „Pausiert“ über der ganzen Welt; nur bei look.paused
export function drawPauseVeil(ctx, look) {
if (!look.paused) return
tint(ctx, -2, -2, WORLD.w + 4, WORLD.h + 4, PAL.w, 0.45)
label(ctx, 'Pausiert', WORLD.w / 2, WORLD.h / 2 - 5, { size: 28, color: PAL.ink, weight: 700, align: 'center', bg: PAL.w })
}
+306
View File
@@ -0,0 +1,306 @@
// Räume des Hustler-Hochhauses: eine Zeichnung je Raumart (kind), Pokal-Seiten, graue Räume und die Anzeigewerte `look`.
// Portiert aus entwuerfe/scene-turm.js (drawBridge ... drawArchive, Z. 182-314) auf die Raumtabelle des Layouts: alle Maße
// relativ zu room.x und zum Boden der Etage, Schriften nur über label(). Rückwand und Boden zeichnet building.js davor.
// Kein React, kein Appwrite, kein window/document.
import { px, tint, label, drawPaper, PAL, TEAM } from './sprites.js'
import { drawDesk, drawMonitor, drawCabinet, drawPlant, drawTray, drawTrophy, drawStamp, drawRadar, drawMailbox, drawVault, drawScale, drawSign, drawIcon } from './props.js'
const TROPHIES_PER_PAGE = 8
const TINT_ALPHA = 0.10 // Teamfarbton über jedem Raum
const GREY_ALPHA = 0.45
const GREY_TINT = '#9aa3b2'
const DIVIDER = '#e1e5ec'
const SIGN_TEXT = 'noch nicht angeschlossen'
const SIGN_MAX_W = 60
const STEP_COLORS = [TEAM.radar, TEAM.bewertung, TEAM.werkstatt, TEAM.pruefung, TEAM.freigabe] // Stationen der Pipeline-Tafel
const FEED_ROWS = [8, 5, 9] // Breiten der durchlaufenden Zeilen im Feed-Schirm
const MAX_PAGE_DOTS = 19
// Anzeigewerte der Räume. Die Regie (director.js) schreibt sie, die Zeichnung liest sie nur.
// greyRooms: nicht angeschlossene Räume, busyRooms: Räume, deren Worker gerade arbeitet (dort laufen die Monitore).
export function createLook() {
return {
greyRooms: new Set(),
busyRooms: new Set(),
pultOpen: 0,
dealsMonth: 0,
botLeads: 0,
trophyPage: 0, // gezeigte Pokal-Seite
pageT: 0, // Zeit seit der letzten Blätterung (Sekunden)
trophyPop: 0, // Restzeit des Aufblinkens des neuesten Pokals
vaultFlash: 0, // Restzeit des Tresor-Blitzes
paused: false,
}
}
// Pokal-Seiten: perPage je Seite, die Seite wird auf 0..pages-1 geklemmt; ungültige Werte zählen als 0
export function trophyPaging(total, page, perPage = TROPHIES_PER_PAGE) {
const size = Number.isFinite(perPage) && perPage >= 1 ? Math.floor(perPage) : TROPHIES_PER_PAGE
const n = whole(total)
const pages = Math.max(1, Math.ceil(n / size))
const p = Number.isFinite(page) ? Math.min(pages - 1, Math.max(0, Math.floor(page))) : 0
const from = p * size
return { shown: Math.min(size, n - from), page: p, pages, from }
}
// Zähler als ganze Zahl >= 0
function whole(value) {
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0
}
// ---------- Hilfen für die Raumzeichnungen ----------
// r = { room, x, w, top, g, tp, busy, grey, color, look }: g = Boden der Etage, tp = Zeit mit Versatz je Raum (Monitore blinken nicht im Gleichschritt)
const deskAt = (ctx, r, dx, w) => drawDesk(ctx, r.x + dx, r.g - 11, w, r.color)
const monitorAt = (ctx, r, dx, color = r.color) => drawMonitor(ctx, r.x + dx, r.g - 21, r.tp, color, r.busy)
const hop = (r) => (r.busy && Math.floor(r.tp * 2) % 2 === 0 ? 3 : 0) // Stempel springt im Takt, solange gearbeitet wird
// Monitor mit Schreib-Animation: erste Zeile wächst, dann die zweite, dazu eine blinkende Schreibmarke
function drawTypingMonitor(ctx, r, dx) {
const x = r.x + dx
const y = r.g - 21
drawMonitor(ctx, x, y, r.tp, r.color, false)
if (!r.busy) return
const n = Math.floor(r.tp * 4) % 9
const first = Math.min(6, n + 1)
const second = Math.max(0, n - 5)
px(ctx, x + 2, y + 2, first, 1, r.color)
if (second > 0) px(ctx, x + 2, y + 4, second, 1, r.color)
if (n % 2 === 0) px(ctx, x + 2 + (second > 0 ? second : first), y + (second > 0 ? 4 : 2), 1, 1, PAL.w)
}
// Pappkarton 15 × 8 mit Etikett
function drawBox(ctx, x, y) {
px(ctx, x, y, 15, 8, PAL.wood)
px(ctx, x, y, 15, 1, PAL.wood2)
px(ctx, x + 5, y + 3, 5, 2, '#efe3d2')
}
// ---------- Raumarten ----------
export const ROOM_DRAWERS = Object.freeze({
// Kommandobrücke: Konsole mit 2 Monitoren, Pipeline-Tafel, Fenster in die Nacht, Pflanze
bruecke(ctx, r) {
const { x, top, g, tp } = r
px(ctx, x + 62, top + 3, 104, 22, PAL.metal2)
px(ctx, x + 63, top + 4, 102, 20, '#e8ebf9')
px(ctx, x + 63, top + 16, 102, 8, '#dfe3f5')
for (const [dx, dy, w, h] of [[66, 18, 6, 6], [74, 14, 5, 10], [82, 17, 8, 7], [128, 15, 6, 9], [138, 18, 9, 6], [152, 13, 5, 11]]) px(ctx, x + dx, top + dy, w, h, '#cdd3ee')
const stars = [[70, 6], [86, 9], [99, 5], [130, 7], [144, 5], [156, 10], [108, 8], [78, 16]]
stars.forEach(([dx, dy], i) => {
if ((Math.floor(tp * 1.3) + i) % 3 !== 0) px(ctx, x + dx, top + dy, 1, 1, '#b3bbec')
})
px(ctx, x + 150, top + 7, 5, 5, '#f6dc7a')
px(ctx, x + 152, top + 6, 4, 4, '#e8ebf9')
px(ctx, x + 113, top + 4, 2, 20, PAL.metal2)
px(ctx, x + 60, top + 25, 108, 2, '#aab2c0')
// Konsole
drawDesk(ctx, x + 6, g - 11, 30, r.color)
drawMonitor(ctx, x + 10, g - 21, tp, '#b7a4f0', r.busy)
drawMonitor(ctx, x + 24, g - 21, tp + 1, '#7fb2ff', r.busy)
// Pipeline-Tafel
px(ctx, x + 42, top + 5, 18, 14, PAL.metal2)
px(ctx, x + 43, top + 6, 16, 12, PAL.w)
px(ctx, x + 45, top + 10, 12, 1, PAL.metal)
STEP_COLORS.forEach((color, i) => px(ctx, x + 44 + 3 * i, top + 9, 2, 2, color))
px(ctx, x + 45, top + 13, 3, 3, '#ffe28a')
px(ctx, x + 50, top + 13, 3, 3, '#bde5c9')
px(ctx, x + 55, top + 13, 3, 3, '#f6b9b9')
drawPlant(ctx, x + 174, g - 9)
},
// Netz-Radar: Radar auf dem Boden, Tisch daneben
radar(ctx, r) {
deskAt(ctx, r, 2, 14)
drawRadar(ctx, r.x + 31, r.g - 15, r.busy ? r.tp : 0)
},
// Kleinanzeigen: Tisch mit Monitor, Eingangskorb auf dem Wandbrett (voll, solange gearbeitet wird)
inbox(ctx, r) {
deskAt(ctx, r, 2, 14)
monitorAt(ctx, r, 4)
px(ctx, r.x + 31, r.g - 11, 12, 1, PAL.wood2)
drawTray(ctx, r.x + 32, r.g - 15, r.busy)
},
// Portale: Tisch mit Monitor, Wandschirm mit durchlaufenden Zeilen
feed(ctx, r) {
deskAt(ctx, r, 2, 14)
monitorAt(ctx, r, 4)
const sx = r.x + 31
const sy = r.g - 18
px(ctx, sx, sy, 14, 10, PAL.o)
px(ctx, sx + 1, sy + 1, 12, 8, PAL.screen)
if (!r.busy) return
const shift = Math.floor(r.tp * 3) % FEED_ROWS.length
for (let i = 0; i < FEED_ROWS.length; i++) px(ctx, sx + 2, sy + 2 + i * 2, FEED_ROWS[(i + shift) % FEED_ROWS.length], 1, r.color)
},
// Foren: Tisch mit Monitor, Tafel mit zwei Sprechblasen
board(ctx, r) {
deskAt(ctx, r, 2, 14)
monitorAt(ctx, r, 4)
const bx = r.x + 31
const by = r.g - 19
px(ctx, bx, by, 14, 11, PAL.wood2)
px(ctx, bx + 1, by + 1, 12, 9, '#eadfcf')
px(ctx, bx + 2, by + 2, 6, 3, PAL.w)
px(ctx, bx + 3, by + 5, 1, 1, PAL.w)
px(ctx, bx + 6, by + 6, 6, 3, r.color)
px(ctx, bx + 10, by + 9, 1, 1, r.color)
},
// Branchen-Scout: Tisch in der Teamfarbe, Monitor, Branchen-Icon an der Wand (Icon = Raumkennung)
scout(ctx, r) {
deskAt(ctx, r, 2, 14)
monitorAt(ctx, r, 4)
drawIcon(ctx, r.room.id, r.x + r.w - 10, r.top + 8, r.color)
},
// Waage: Waage links der Figur (schwankt beim Bewerten), Tisch mit Papieren rechts
waage(ctx, r) {
drawScale(ctx, r.x + 62, r.g - 16, r.busy ? Math.round(Math.sin(r.tp * 6) * 2.4) : 0)
deskAt(ctx, r, 104, 22)
drawPaper(ctx, r.x + 109, r.g - 18)
drawPaper(ctx, r.x + 117, r.g - 18)
},
// Mail-Stube: Tisch, Monitor, Briefstapel auf einem Beistelltisch
mail(ctx, r) {
deskAt(ctx, r, 2, 14)
monitorAt(ctx, r, 4)
px(ctx, r.x + 31, r.g - 7, 12, 1, PAL.wood2)
px(ctx, r.x + 32, r.g - 6, 1, 6, PAL.wood2)
px(ctx, r.x + 41, r.g - 6, 1, 6, PAL.wood2)
for (let i = 0; i < 3; i++) {
px(ctx, r.x + 32, r.g - 9 - i * 2, 10, 2, PAL.w)
px(ctx, r.x + 32, r.g - 9 - i * 2, 10, 1, PAL.g)
}
},
// Forum-Stube: Tisch, Monitor mit Schreib-Animation
schreiber(ctx, r) {
deskAt(ctx, r, 2, 14)
drawTypingMonitor(ctx, r, 4)
},
// Posteingang: Tisch, Regal mit drei Ablagekörben (Eingang voll, Mitte beim Sortieren, Ausgang leer)
sortier(ctx, r) {
deskAt(ctx, r, 2, 14)
const rx = r.x + 31
px(ctx, rx, r.g - 20, 1, 18, PAL.wood2)
px(ctx, rx + 11, r.g - 20, 1, 18, PAL.wood2)
const filled = [true, r.busy, false]
filled.forEach((full, i) => drawTray(ctx, rx + 1, r.g - 19 + i * 6, full))
},
// Abschluss: Tisch, Vertrag, Stempel (springt beim Arbeiten)
vertrag(ctx, r) {
deskAt(ctx, r, 2, 14)
drawPaper(ctx, r.x + 3, r.g - 18)
drawStamp(ctx, r.x + 10, r.g - 16 - hop(r), true)
},
// Werkbank: Tisch, Monitor
tisch(ctx, r) {
deskAt(ctx, r, 6, 18)
monitorAt(ctx, r, 10)
},
// Prüftisch: Tisch mit zwei Stempeln, Handy auf einem Sockel (zeigt beim Prüfen wechselnde Bilder)
pruefung(ctx, r) {
deskAt(ctx, r, 60, 22)
drawStamp(ctx, r.x + 66, r.g - 16, false)
drawStamp(ctx, r.x + 73, r.g - 16 - hop(r), true)
const sx = r.x + 108
px(ctx, sx, r.g - 5, 10, 5, PAL.wood)
px(ctx, sx, r.g - 5, 10, 1, PAL.wood2)
px(ctx, sx + 2, r.g - 15, 6, 10, PAL.o)
const shot = Math.floor(r.tp * 2) % 2 === 0 ? '#cfe3ff' : '#bde5c9'
px(ctx, sx + 3, r.g - 14, 4, 7, r.busy ? shot : PAL.screen)
px(ctx, sx + 4, r.g - 7, 2, 1, PAL.mute)
},
// Dein Pult: der Briefkasten in der Mitte; Zahl der offenen Einträge daneben (keine bei 0)
pult(ctx, r) {
const open = whole(r.look.pultOpen)
const cx = r.x + r.w / 2
drawMailbox(ctx, cx - 6, r.g - 15, open)
if (open > 0) label(ctx, open > 99 ? '99+' : String(open), cx + 15, r.g - 19, { size: 11, color: PAL.w, weight: 700, align: 'center', bg: PAL.red })
},
// Tresor: Pokalbrett (eine Seite zu 8), Tresortür, Zähler der Deals dieses Monats
tresor(ctx, r) {
const { look } = r
const total = whole(look.dealsMonth)
const pg = trophyPaging(total, look.trophyPage)
const shelf = r.g - 9
px(ctx, r.x + 24, shelf, 78, 2, PAL.wood2)
px(ctx, r.x + 26, shelf + 2, 2, 3, PAL.wood2)
px(ctx, r.x + 98, shelf + 2, 2, 3, PAL.wood2)
for (let i = 0; i < pg.shown; i++) {
const newest = pg.from + i === total - 1
if (newest && look.trophyPop > 0 && Math.floor(look.trophyPop * 10) % 2) continue
drawTrophy(ctx, r.x + 26 + 9 * i, shelf - 9)
}
// Seitenpunkte, wenn mehr als eine Seite da ist
for (let p = 0; p < Math.min(pg.pages, MAX_PAGE_DOTS); p++) px(ctx, r.x + 24 + p * 4, shelf + 6, 2, 2, p === pg.page ? TEAM.tresor : PAL.metal)
drawVault(ctx, r.x + 110, r.g - 21, r.tp)
if (look.vaultFlash > 0) tint(ctx, r.x + 110, r.g - 21, 22, 20, PAL.gold, 0.3 * Math.min(1, look.vaultFlash * 2))
label(ctx, `${total} Deals`, r.x + 138, r.g - 15, { size: 11, color: PAL.ink, weight: 600 })
},
// Archiv & Register: Kartons links, vier Aktenschränke rechts der Figur (beim Arbeiten springt je eine Schublade auf), Zähler der Bot-Leads
archiv(ctx, r) {
drawBox(ctx, r.x + 14, r.g - 9)
drawBox(ctx, r.x + 31, r.g - 9)
drawBox(ctx, r.x + 22, r.g - 17)
const open = Math.floor(r.tp * 1.5) % 4
for (let i = 0; i < 4; i++) {
const x = r.x + 100 + 14 * i
const y = r.g - 20
drawCabinet(ctx, x, y, 3)
if (r.busy && i === open) {
px(ctx, x + 1, y + 7, 10, 5, '#fafbfd')
px(ctx, x + 2, y + 9, 8, 2, PAL.p)
px(ctx, x + 3, y + 6, 6, 1, PAL.w)
}
}
label(ctx, `${whole(r.look.botLeads)} Bot-Leads`, r.x + r.w - 2, r.g - 15, { size: 11, color: PAL.ink, weight: 600, align: 'right' })
},
})
// ---------- Zeichnen ----------
function drawRoom(ctx, room, floor, index, t, look) {
// Räume ohne Worker (pult, tresor) werden nie grau
const grey = room.workerId != null && look.greyRooms.has(room.id)
const r = {
room,
x: room.x,
w: room.w,
top: floor.top,
g: floor.floorY,
tp: t + room.x * 0.05,
busy: !grey && look.busyRooms.has(room.id),
grey,
color: TEAM[room.team] ?? PAL.metal2,
look,
}
if (!grey) tint(ctx, room.x, floor.top, room.w, floor.h, r.color, TINT_ALPHA)
if (index > 0) px(ctx, room.x, floor.top, 1, floor.h, DIVIDER)
// Das Schild steht oben mittig und verdrängt den Titel; Name und Branche erkennt man an Möbeln und Icon
if (!grey) label(ctx, room.title, room.x + 2, floor.top + 1, { size: 10, color: PAL.mute, weight: 500 })
if (Object.hasOwn(ROOM_DRAWERS, room.kind)) ROOM_DRAWERS[room.kind](ctx, r)
if (grey) {
tint(ctx, room.x, floor.top, room.w, floor.h, GREY_TINT, GREY_ALPHA)
const w = Math.min(room.w - 2, SIGN_MAX_W)
drawSign(ctx, room.x + (room.w - w) / 2, floor.top + 1, w, SIGN_TEXT)
}
}
// Alle Räume aller Etagen. Vorher: drawOutside, drawTower. Nachher: Aufzug hinten, Figuren, Aufzug vorn.
export function drawRooms(ctx, layout, t, look) {
for (const floor of layout.floors) floor.rooms.forEach((room, index) => drawRoom(ctx, room, floor, index, t, look))
}
+240
View File
@@ -0,0 +1,240 @@
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()))
})