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:
1 parent
fb331de1a6
commit
8555453a74
3 files changed
+660
No files matched your search
@@ -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 })
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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()))
|
||||
})
|
||||
Reference in new issue
Block a user