Workers P2: Layout als Daten (10 Etagen, 19 Plätze) und Kamera mit Umkehr-Transformation

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-18 02:04:07 +00:00
1 parent 514469d93c
commit c27f0b600b
4 files changed
+451

No files matched your search

+79
View File
@@ -0,0 +1,79 @@
// Kamera des Hochhauses: Ausschnitt der Welt 320 × 256, Zoom 1 bis 3, Verschieben, Umkehr-Transformation, Treffer-Auswahl.
// Das Sichtfeld ist Welt / zoom. Orte in der Ansicht sind Bruchteile 0..1 (links oben = 0, 0), Orte in der Welt logische Pixel.
// Die Funktionen zoomAt, panBy, centerOn und clampCamera ändern die übergebene Kamera und geben dieselbe Kamera zurück.
// Kein React, kein window/document.
import { WORLD } from './layout.js'
export const ZOOM_MIN = 1
export const ZOOM_MAX = 3
const FIT_WIDTH = 560 // ab dieser Breite (CSS-Pixel) passt das ganze Hochhaus bei Zoom 1 lesbar auf die Bühne
const FIT_MAX = 2.2
const clamp = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v)
// Endliche Zahl oder Ersatzwert: NaN/Infinity aus Zeigerereignissen dürfen die Kamera nie verderben
const num = (v, fallback) => (Number.isFinite(v) ? v : fallback)
// Startzoom für eine Bühne der Breite cssWidth: ab 560 px 1, darunter wachsend bis 2,2; ungültige Breite -> 1
export function fitZoom(cssWidth) {
if (!Number.isFinite(cssWidth) || cssWidth <= 0 || cssWidth >= FIT_WIDTH) return 1
return Math.min(FIT_MAX, FIT_WIDTH / cssWidth)
}
export function createCamera() {
return { zoom: 1, x: 0, y: 0 }
}
// Zoom in 1..3 und Lage so, dass der Ausschnitt in der Welt bleibt
export function clampCamera(cam, world = WORLD) {
cam.zoom = clamp(num(cam.zoom, ZOOM_MIN), ZOOM_MIN, ZOOM_MAX)
cam.x = clamp(num(cam.x, 0), 0, world.w - world.w / cam.zoom)
cam.y = clamp(num(cam.y, 0), 0, world.h - world.h / cam.zoom)
return cam
}
// Ansicht (Bruchteile) -> Welt (logische Pixel)
export function viewToWorld(cam, vx, vy) {
return { x: cam.x + vx * (WORLD.w / cam.zoom), y: cam.y + vy * (WORLD.h / cam.zoom) }
}
// Welt -> Ansicht; Gegenstück zu viewToWorld
export function worldToView(cam, x, y) {
return { vx: (x - cam.x) / (WORLD.w / cam.zoom), vy: (y - cam.y) / (WORLD.h / cam.zoom) }
}
// Zoom um factor; der Weltpunkt unter (fx, fy) bleibt stehen, solange der Rand nicht klemmt
export function zoomAt(cam, factor, fx, fy) {
const f = factor > 0 ? num(factor, 1) : 1
const px = num(fx, 0.5)
const py = num(fy, 0.5)
const w = viewToWorld(cam, px, py)
cam.zoom = clamp(cam.zoom * f, ZOOM_MIN, ZOOM_MAX)
cam.x = w.x - px * (WORLD.w / cam.zoom)
cam.y = w.y - py * (WORLD.h / cam.zoom)
return clampCamera(cam)
}
// Verschieben um Bruchteile der Ansicht (0,5 = halbe Bildbreite)
export function panBy(cam, dxFrac, dyFrac) {
cam.x += num(dxFrac, 0) * (WORLD.w / cam.zoom)
cam.y += num(dyFrac, 0) * (WORLD.h / cam.zoom)
return clampCamera(cam)
}
// Weltpunkt (x, y) in die Mitte der Ansicht
export function centerOn(cam, x, y) {
cam.x = x - WORLD.w / cam.zoom / 2
cam.y = y - WORLD.h / cam.zoom / 2
return clampCamera(cam)
}
// Oberste Figur an (x, y) in Weltkoordinaten; boxes in Zeichenreihenfolge (die letzte liegt oben), pad vergrößert jeden Kasten
export function pickAgent(boxes, x, y, pad = 0) {
if (!Array.isArray(boxes)) return null
for (let i = boxes.length - 1; i >= 0; i--) {
const b = boxes[i]?.box
if (b && x >= b.x - pad && x <= b.x + b.w + pad && y >= b.y - pad && y <= b.y + b.h + pad) return boxes[i].id
}
return null
}
+133
View File
@@ -0,0 +1,133 @@
// Hochhaus-Layout als Daten: Etagen, Räume und Heimatplätze der Figuren in der Welt 320 × 256 (Übersicht §3).
// Zeichnen (rooms, building) und Regie (director) lesen nur diese Tabelle; ein neuer Raum ist eine Zeile in
// ROOM_META plus eine Rolle. Kein React, kein Appwrite, kein window/document.
import { ROLES, FLOORS, FLOOR_TITLES, ROOM_TITLES } from '../../../lib/workersRoster.js'
export const WORLD = Object.freeze({ w: 320, h: 256 })
export const FLOOR_PITCH = 23 // Etage 22 hoch plus eine Bodenlinie
export const BRIDGE_H = 34 // die Kommandobrücke (Etage 0) ist höher
const TOP0 = 6 // Oberkante der Brücke
const FLOOR_H = 22
const ROOM_X0 = 42 // Räume stehen zwischen x 42 und 228 (Schacht und Etagenschilder liegen daneben)
const ROOM_W = 186
const HOME_DY = 14 // Figurenhöhe: Platz y = Boden - 14
const OTTO_DY = 27 // Otto steht höher (Kommandomütze, 24 breit)
const OTTO_W = 24
const WORKER_W = 12
const CAB_H = 21 // Kabinenhöhe des Aufzugs (lift.js importiert layout.js, nicht umgekehrt)
const meta = (kind, team) => Object.freeze({ kind, team })
// Raum -> Art der Zeichnung und Teamfarbe (Schlüssel aus TEAM in sprites.js)
export const ROOM_META = Object.freeze({
bruecke: meta('bruecke', 'freigabe'),
netz: meta('radar', 'radar'),
kleinanzeigen: meta('inbox', 'radar'),
portale: meta('feed', 'radar'),
foren: meta('board', 'radar'),
aerzte: meta('scout', 'aerzte'),
handwerk: meta('scout', 'handwerk'),
selbst: meta('scout', 'selbst'),
laden: meta('scout', 'laden'),
waage: meta('waage', 'bewertung'),
mail: meta('mail', 'freigabe'),
forum: meta('schreiber', 'freigabe'),
posteingang: meta('sortier', 'freigabe'),
abschluss: meta('vertrag', 'freigabe'),
tisch1: meta('tisch', 'werkstatt'),
tisch2: meta('tisch', 'werkstatt'),
tisch3: meta('tisch', 'werkstatt'),
pruefung: meta('pruefung', 'pruefung'),
pult: meta('pult', 'freigabe'),
tresor: meta('tresor', 'tresor'),
archiv: meta('archiv', 'archiv'),
})
// Räume ohne Worker: stehen in ihrer Etage hinter den Räumen der Rollen
const WORKERLESS = Object.freeze({ freigabe: ['pult'], tresor: ['tresor'] })
// Etage i: Brücke oben, danach im Raster 23 (Oberkante 41, 64, 87, ...); floorY ist die Bodenlinie
function floorBox(i) {
const h = i === 0 ? BRIDGE_H : FLOOR_H
const top = i === 0 ? TOP0 : TOP0 + BRIDGE_H + 1 + FLOOR_PITCH * (i - 1)
return { top, floorY: top + h, h }
}
// n Räume teilen die Breite gleichmäßig; der letzte nimmt den Rest (4 Räume: 46, 46, 46, 48)
function splitRooms(n) {
const w = Math.floor(ROOM_W / n)
return Array.from({ length: n }, (_, i) => ({ x: ROOM_X0 + i * w, w: i === n - 1 ? ROOM_W - w * (n - 1) : w }))
}
// Orte für Effekte: Briefkasten am Pult, Tresor, Schublade im Archiv und ein Punkt rechts außerhalb des Turms.
// Fehlt die Etage im Layout (nur bei eigenen Etagenlisten), ist der Ort null.
function buildSpots(floors) {
const at = (floorId, dx, dy) => {
const f = floors.find((fl) => fl.id === floorId)
return f ? { x: ROOM_X0 + dx, y: f.floorY + dy } : null
}
const tresor = at('tresor', 121, -11)
return {
pult: at('freigabe', 93, -15),
tresor,
aussen: tresor && { x: WORLD.w + 4, y: tresor.y - 1 },
archiv: at('keller', 88, -16),
}
}
// Layout aus Rollen und Etagenliste (Standard: das Rollen-Register). Wirft bei unbekanntem Raum oder unbekannter Etage.
export function buildLayout(roles = ROLES, floors = FLOORS) {
for (const r of roles) {
if (!Object.hasOwn(ROOM_META, r.room)) throw new Error(`Unbekannter Raum: ${r.room}`)
if (!floors.includes(r.floor)) throw new Error(`Unbekannte Etage: ${r.floor}`)
}
const homes = {}
const list = floors.map((id, index) => {
const box = floorBox(index)
const entries = roles.filter((r) => r.floor === id).map((r) => ({ id: r.room, workerId: r.workerId }))
for (const room of WORKERLESS[id] ?? []) entries.push({ id: room, workerId: null })
const slots = splitRooms(entries.length)
const rooms = entries.map((e, k) => {
const { kind, team } = ROOM_META[e.id]
return { id: e.id, kind, title: ROOM_TITLES[e.id], team, x: slots[k].x, w: slots[k].w, workerId: e.workerId }
})
for (const room of rooms) {
if (!room.workerId) continue
const otto = room.workerId === 'otto'
const x = room.x + Math.floor((room.w - (otto ? OTTO_W : WORKER_W)) / 2)
homes[room.workerId] = { x, y: box.floorY - (otto ? OTTO_DY : HOME_DY), floor: index, room: room.id }
}
return { id, index, title: FLOOR_TITLES[id] ?? id, top: box.top, floorY: box.floorY, h: box.h, rooms }
})
return { floors: list, homes, spots: buildSpots(list) }
}
// Etagenindex zur Kennung; unbekannt -> -1
export function floorIndex(layout, id) {
return layout.floors.findIndex((f) => f.id === id)
}
// Raum zur Kennung; unbekannt -> null
export function roomById(layout, roomId) {
for (const f of layout.floors) {
const room = f.rooms.find((r) => r.id === roomId)
if (room) return room
}
return null
}
// Heimatplatz des Workers; unbekannte Kennung (auch Prototyp-Namen) -> null
export function homeOf(layout, workerId) {
return typeof workerId === 'string' && Object.hasOwn(layout.homes, workerId) ? layout.homes[workerId] : null
}
// Standhöhe einer Figur auf Etage i (obere Kante des 14 hohen Körpers)
export function fy(layout, i) {
return layout.floors[i].floorY - HOME_DY
}
// Oberkante der Aufzugkabine, wenn sie auf Etage i hält
export function cabTop(layout, i) {
return layout.floors[i].floorY - CAB_H
}