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:
1 parent
514469d93c
commit
c27f0b600b
4 files changed
+451
No files matched your search
@@ -0,0 +1,119 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { loadModule } from './load.mjs'
|
||||
|
||||
// Kamera: Zoom, Verschieben, Umkehr-Transformation, Figuren-Treffer (Ansicht = Welt/Zoom, Bruchteile 0..1)
|
||||
const C = await loadModule('src/components/workers/engine/camera.js')
|
||||
const close = (a, b) => Math.abs(a - b) < 1e-9
|
||||
|
||||
test('fitZoom: ab 560 px 1, darunter wachsend bis 2,2', () => {
|
||||
assert.equal(C.fitZoom(1280), 1)
|
||||
assert.equal(C.fitZoom(600), 1)
|
||||
assert.equal(C.fitZoom(560), 1)
|
||||
assert.ok(close(C.fitZoom(390), 560 / 390))
|
||||
assert.equal(C.fitZoom(200), 2.2)
|
||||
})
|
||||
|
||||
test('fitZoom: ungültige Breite gibt 1, nie NaN', () => {
|
||||
for (const w of [0, -5, NaN, undefined, null, Infinity]) assert.equal(C.fitZoom(w), 1, String(w))
|
||||
})
|
||||
|
||||
test('Zoomgrenzen und Startkamera', () => {
|
||||
assert.deepEqual([C.ZOOM_MIN, C.ZOOM_MAX], [1, 3])
|
||||
assert.deepEqual(C.createCamera(), { zoom: 1, x: 0, y: 0 })
|
||||
assert.notEqual(C.createCamera(), C.createCamera())
|
||||
})
|
||||
|
||||
test('zoomAt hält den Punkt unter dem Zeiger fest, klemmt Zoom und Rand', () => {
|
||||
const p0 = C.viewToWorld(C.createCamera(), 0.25, 0.75)
|
||||
const cam = C.zoomAt(C.createCamera(), 2, 0.25, 0.75)
|
||||
assert.equal(cam.zoom, 2)
|
||||
const p1 = C.viewToWorld(cam, 0.25, 0.75)
|
||||
assert.ok(Math.abs(p1.x - p0.x) < 1e-9 && Math.abs(p1.y - p0.y) < 1e-9)
|
||||
assert.equal(C.zoomAt(C.createCamera(), 10, 0.5, 0.5).zoom, 3)
|
||||
assert.equal(C.zoomAt(C.createCamera(), 0.1, 0.5, 0.5).zoom, 1)
|
||||
const edge = C.panBy({ zoom: 2, x: 0, y: 0 }, 5, 5)
|
||||
assert.deepEqual([edge.x, edge.y], [160, 128])
|
||||
const back = C.panBy({ zoom: 2, x: 10, y: 10 }, -5, -5)
|
||||
assert.deepEqual([back.x, back.y], [0, 0])
|
||||
})
|
||||
|
||||
test('viewToWorld und worldToView sind zueinander invers; centerOn', () => {
|
||||
const cam = { zoom: 2.5, x: 40, y: 60 }
|
||||
const w = C.viewToWorld(cam, 0.3, 0.6)
|
||||
const v = C.worldToView(cam, w.x, w.y)
|
||||
assert.ok(Math.abs(v.vx - 0.3) < 1e-9 && Math.abs(v.vy - 0.6) < 1e-9)
|
||||
const c = C.centerOn({ zoom: 2, x: 0, y: 0 }, 160, 128)
|
||||
assert.deepEqual([c.x, c.y], [80, 64])
|
||||
})
|
||||
|
||||
test('pickAgent: oberste Figur gewinnt, Polster vergrößert die Fläche, Treffer nur im Kasten', () => {
|
||||
const boxes = [{ id: 'a', box: { x: 10, y: 10, w: 12, h: 16 } }, { id: 'b', box: { x: 16, y: 10, w: 12, h: 16 } }]
|
||||
assert.equal(C.pickAgent(boxes, 18, 14), 'b')
|
||||
assert.equal(C.pickAgent(boxes, 11, 14), 'a')
|
||||
assert.equal(C.pickAgent(boxes, 40, 14), null)
|
||||
assert.equal(C.pickAgent(boxes, 30, 14, 0), null)
|
||||
assert.equal(C.pickAgent(boxes, 30, 14, 3), 'b')
|
||||
})
|
||||
|
||||
// ---- Ergänzungen: Verhalten, das die Tests oben nicht berühren ----
|
||||
|
||||
test('Sichtfeld ist Welt/Zoom: bei Zoom 1 gibt es nichts zu verschieben', () => {
|
||||
const cam = C.panBy(C.createCamera(), 0.4, -0.3)
|
||||
assert.deepEqual([cam.zoom, cam.x, cam.y], [1, 0, 0])
|
||||
assert.deepEqual(C.viewToWorld(C.createCamera(), 1, 1), { x: 320, y: 256 })
|
||||
assert.deepEqual(C.viewToWorld({ zoom: 2, x: 160, y: 128 }, 1, 1), { x: 320, y: 256 })
|
||||
})
|
||||
|
||||
test('panBy verschiebt in Bruchteilen der Ansicht und klemmt am Rand', () => {
|
||||
const cam = C.panBy({ zoom: 2, x: 40, y: 30 }, 0.25, 0.5)
|
||||
assert.deepEqual([cam.x, cam.y], [80, 94])
|
||||
})
|
||||
|
||||
test('zoomAt zurück auf 1 setzt die Kamera auf den Ursprung zurück (Rand wird geklemmt)', () => {
|
||||
const cam = C.zoomAt({ zoom: 3, x: 100, y: 80 }, 0.01, 0.5, 0.5)
|
||||
assert.deepEqual([cam.zoom, cam.x, cam.y], [1, 0, 0])
|
||||
})
|
||||
|
||||
test('zoomAt am Rand: der Punkt unter dem Zeiger bleibt, solange der Rand nicht klemmt', () => {
|
||||
const start = { zoom: 2, x: 80, y: 64 }
|
||||
const p0 = C.viewToWorld(start, 0.6, 0.4)
|
||||
const cam = C.zoomAt({ ...start }, 1.5, 0.6, 0.4)
|
||||
assert.equal(cam.zoom, 3)
|
||||
const p1 = C.viewToWorld(cam, 0.6, 0.4)
|
||||
assert.ok(close(p1.x, p0.x) && close(p1.y, p0.y))
|
||||
})
|
||||
|
||||
test('clampCamera klemmt Zoom und Lage, auch mit eigener Welt; Aufrufer bekommt dieselbe Kamera zurück', () => {
|
||||
const cam = { zoom: 5, x: -10, y: 900 }
|
||||
assert.equal(C.clampCamera(cam), cam)
|
||||
assert.deepEqual([cam.zoom, cam.x, cam.y], [3, 0, 256 - 256 / 3])
|
||||
const klein = C.clampCamera({ zoom: 2, x: 99, y: 99 }, { w: 100, h: 50 })
|
||||
assert.deepEqual([klein.x, klein.y], [50, 25])
|
||||
})
|
||||
|
||||
test('centerOn klemmt in den Ecken', () => {
|
||||
const c = C.centerOn({ zoom: 2, x: 80, y: 64 }, 0, 0)
|
||||
assert.deepEqual([c.x, c.y], [0, 0])
|
||||
const d = C.centerOn({ zoom: 2, x: 0, y: 0 }, 320, 256)
|
||||
assert.deepEqual([d.x, d.y], [160, 128])
|
||||
})
|
||||
|
||||
test('ungültige Eingaben (NaN, Infinity) machen die Kamera nie kaputt', () => {
|
||||
const a = C.zoomAt({ zoom: 2, x: 20, y: 10 }, NaN, 0.5, 0.5)
|
||||
assert.deepEqual([a.zoom, a.x, a.y], [2, 20, 10])
|
||||
const b = C.panBy({ zoom: 2, x: 20, y: 10 }, NaN, Infinity)
|
||||
assert.ok(Number.isFinite(b.x) && Number.isFinite(b.y))
|
||||
assert.equal(b.x, 20)
|
||||
const c = C.clampCamera({ zoom: NaN, x: NaN, y: NaN })
|
||||
assert.deepEqual([c.zoom, c.x, c.y], [1, 0, 0])
|
||||
})
|
||||
|
||||
test('pickAgent: leere Liste, kaputte Einträge und Rand des Kastens', () => {
|
||||
assert.equal(C.pickAgent([], 5, 5), null)
|
||||
assert.equal(C.pickAgent(undefined, 5, 5), null)
|
||||
const boxes = [{ id: 'a', box: { x: 10, y: 10, w: 12, h: 16 } }, { id: 'kaputt' }, null]
|
||||
assert.equal(C.pickAgent(boxes, 22, 26), 'a')
|
||||
assert.equal(C.pickAgent(boxes, 23, 26), null)
|
||||
assert.equal(C.pickAgent(boxes, 23, 26, 1), 'a')
|
||||
})
|
||||
@@ -0,0 +1,120 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { loadModule } from './load.mjs'
|
||||
|
||||
// Hochhaus-Layout als Daten: 10 Etagen, 19 Plätze, Räume nach Rolle, feste Orte für Effekte
|
||||
const { buildLayout, homeOf, roomById, floorIndex, fy, cabTop, WORLD, ROOM_META, FLOOR_PITCH, BRIDGE_H } = await loadModule('src/components/workers/engine/layout.js')
|
||||
const { ROLES, FLOORS, FLOOR_TITLES, ROOM_TITLES } = await loadModule('src/lib/workersRoster.js')
|
||||
const { TEAM } = await loadModule('src/components/workers/engine/sprites.js')
|
||||
|
||||
const layout = buildLayout()
|
||||
const rooms = (id) => layout.floors.find((f) => f.id === id).rooms.map((r) => [r.id, r.x, r.w])
|
||||
const role = (workerId, floor, room) => ({ workerId, name: workerId, floor, room, model: 'haiku', role: '', caps: [] })
|
||||
|
||||
test('10 Etagen in FLOORS-Reihenfolge mit festen Höhen, Welt 320×256', () => {
|
||||
assert.deepEqual(layout.floors.map((f) => f.id), [...FLOORS])
|
||||
assert.deepEqual([WORLD.w, WORLD.h], [320, 256])
|
||||
assert.deepEqual(layout.floors.map((f) => [f.top, f.floorY]), [[6, 40], [41, 63], [64, 86], [87, 109], [110, 132], [133, 155], [156, 178], [179, 201], [202, 224], [225, 247]])
|
||||
assert.equal(fy(layout, 3), 95)
|
||||
assert.equal(cabTop(layout, 3), 88)
|
||||
})
|
||||
|
||||
test('Etagen tragen Index, Titel und Höhe (Brücke 34, sonst 22 plus Bodenlinie = Raster 23)', () => {
|
||||
assert.equal(BRIDGE_H, 34)
|
||||
assert.equal(FLOOR_PITCH, 23)
|
||||
layout.floors.forEach((f, i) => {
|
||||
assert.equal(f.index, i)
|
||||
assert.equal(f.title, FLOOR_TITLES[f.id])
|
||||
assert.equal(f.h, i === 0 ? 34 : 22, f.id)
|
||||
assert.equal(f.floorY - f.top, f.h, f.id)
|
||||
})
|
||||
assert.equal(layout.floors[1].top - layout.floors[0].floorY, 1)
|
||||
assert.equal(floorIndex(layout, 'tresor'), 8)
|
||||
assert.equal(floorIndex(layout, 'gibtsnicht'), -1)
|
||||
})
|
||||
|
||||
test('Räume je Etage', () => {
|
||||
assert.deepEqual(rooms('anfragen'), [['netz', 42, 46], ['kleinanzeigen', 88, 46], ['portale', 134, 46], ['foren', 180, 48]])
|
||||
assert.deepEqual(rooms('gespraeche').map((r) => r[0]), ['mail', 'forum', 'posteingang', 'abschluss'])
|
||||
assert.deepEqual(rooms('werkstatt'), [['tisch1', 42, 62], ['tisch2', 104, 62], ['tisch3', 166, 62]])
|
||||
for (const id of ['bruecke', 'bewertung', 'pruefung', 'freigabe', 'tresor', 'keller']) {
|
||||
assert.deepEqual(rooms(id).map((r) => [r[1], r[2]]), [[42, 186]], id)
|
||||
}
|
||||
})
|
||||
|
||||
test('alle Räume füllen die Breite 42..228 lückenlos', () => {
|
||||
for (const f of layout.floors) {
|
||||
let x = 42
|
||||
for (const r of f.rooms) {
|
||||
assert.equal(r.x, x, `${f.id}/${r.id}`)
|
||||
x += r.w
|
||||
}
|
||||
assert.equal(x, 228, f.id)
|
||||
}
|
||||
// andere Raumzahlen teilen gleichmäßig, der letzte Raum nimmt den Rest
|
||||
const zwei = buildLayout([role('a', 'anfragen', 'netz'), role('b', 'anfragen', 'portale')], ['anfragen'])
|
||||
assert.deepEqual(zwei.floors[0].rooms.map((r) => [r.x, r.w]), [[42, 93], [135, 93]])
|
||||
const fuenf = buildLayout(['netz', 'kleinanzeigen', 'portale', 'foren', 'aerzte'].map((room, i) => role(`w${i}`, 'anfragen', room)), ['anfragen'])
|
||||
assert.deepEqual(fuenf.floors[0].rooms.map((r) => [r.x, r.w]), [[42, 37], [79, 37], [116, 37], [153, 37], [190, 38]])
|
||||
})
|
||||
|
||||
test('19 Worker haben genau einen Platz im eigenen Raum; Freigabe und Tresor ohne Worker', () => {
|
||||
for (const r of ROLES) {
|
||||
const h = homeOf(layout, r.workerId)
|
||||
const room = roomById(layout, r.room)
|
||||
const wide = r.workerId === 'otto' ? 24 : 12
|
||||
assert.ok(h.x >= room.x && h.x + wide <= room.x + room.w, r.workerId)
|
||||
assert.equal(h.floor, FLOORS.indexOf(r.floor))
|
||||
assert.equal(h.room, r.room)
|
||||
}
|
||||
assert.equal(Object.keys(layout.homes).length, 19)
|
||||
assert.equal(homeOf(layout, 'otto').y, 13)
|
||||
assert.equal(homeOf(layout, 'kim').y, 49)
|
||||
assert.deepEqual(layout.floors.filter((f) => f.rooms.some((r) => !r.workerId)).map((f) => f.id), ['freigabe', 'tresor'])
|
||||
assert.deepEqual(layout.spots, { pult: { x: 135, y: 186 }, tresor: { x: 163, y: 213 }, aussen: { x: 324, y: 212 }, archiv: { x: 130, y: 231 } })
|
||||
})
|
||||
|
||||
test('Platz zentriert in der Raumbreite (Otto 24 breit, sonst 12)', () => {
|
||||
assert.deepEqual(homeOf(layout, 'otto'), { x: 123, y: 13, floor: 0, room: 'bruecke' })
|
||||
assert.deepEqual(homeOf(layout, 'rita'), { x: 59, y: 49, floor: 1, room: 'netz' })
|
||||
assert.equal(homeOf(layout, 'arne').y, 247 - 14)
|
||||
})
|
||||
|
||||
test('Räume tragen Art, Titel, Teamfarbe und Worker aus den Tabellen', () => {
|
||||
assert.deepEqual(roomById(layout, 'kleinanzeigen'), { id: 'kleinanzeigen', kind: 'inbox', title: ROOM_TITLES.kleinanzeigen, team: 'radar', x: 88, w: 46, workerId: 'kim' })
|
||||
assert.equal(roomById(layout, 'pult').workerId, null)
|
||||
assert.equal(roomById(layout, 'tresor').workerId, null)
|
||||
assert.equal(roomById(layout, 'gibtsnicht'), null)
|
||||
assert.equal(roomById(layout, 'constructor'), null)
|
||||
})
|
||||
|
||||
test('ROOM_META: genau die 21 Räume aus ROOM_TITLES, bekannte Arten, Teamfarbe aus TEAM', () => {
|
||||
assert.deepEqual(Object.keys(ROOM_META).sort(), Object.keys(ROOM_TITLES).sort())
|
||||
const kinds = ['bruecke', 'radar', 'inbox', 'feed', 'board', 'scout', 'waage', 'mail', 'schreiber', 'sortier', 'vertrag', 'tisch', 'pruefung', 'pult', 'tresor', 'archiv']
|
||||
assert.deepEqual([...new Set(Object.values(ROOM_META).map((m) => m.kind))].sort(), [...kinds].sort())
|
||||
for (const [id, m] of Object.entries(ROOM_META)) assert.ok(Object.hasOwn(TEAM, m.team), `${id}: ${m.team}`)
|
||||
const kindTeam = (id) => [ROOM_META[id].kind, ROOM_META[id].team]
|
||||
assert.deepEqual(['netz', 'kleinanzeigen', 'portale', 'foren'].map(kindTeam), [['radar', 'radar'], ['inbox', 'radar'], ['feed', 'radar'], ['board', 'radar']])
|
||||
assert.deepEqual(['aerzte', 'handwerk', 'selbst', 'laden'].map(kindTeam), [['scout', 'aerzte'], ['scout', 'handwerk'], ['scout', 'selbst'], ['scout', 'laden']])
|
||||
assert.deepEqual(['mail', 'forum', 'posteingang', 'abschluss'].map(kindTeam), [['mail', 'freigabe'], ['schreiber', 'freigabe'], ['sortier', 'freigabe'], ['vertrag', 'freigabe']])
|
||||
assert.deepEqual(['tisch1', 'tisch2', 'tisch3'].map(kindTeam), [['tisch', 'werkstatt'], ['tisch', 'werkstatt'], ['tisch', 'werkstatt']])
|
||||
assert.deepEqual(['waage', 'pruefung', 'bruecke', 'pult', 'tresor', 'archiv'].map(kindTeam), [['waage', 'bewertung'], ['pruefung', 'pruefung'], ['bruecke', 'freigabe'], ['pult', 'freigabe'], ['tresor', 'tresor'], ['archiv', 'archiv']])
|
||||
})
|
||||
|
||||
test('unbekannte Kennung gibt null, auch bei Namen aus dem Prototyp', () => {
|
||||
assert.equal(homeOf(layout, 'niemand'), null)
|
||||
assert.equal(homeOf(layout, 'constructor'), null)
|
||||
assert.equal(homeOf(layout, '__proto__'), null)
|
||||
})
|
||||
|
||||
test('neue Räume kommen als Daten; unbekannter Raum oder unbekannte Etage wirft', () => {
|
||||
assert.throws(() => buildLayout([role('x', 'anfragen', 'zz')], ['anfragen']), /Unbekannter Raum: zz/)
|
||||
assert.throws(() => buildLayout([role('x', 'oben', 'netz')], ['anfragen']), /Unbekannte Etage: oben/)
|
||||
assert.throws(() => buildLayout([role('x', 'anfragen', 'constructor')], ['anfragen']), /Unbekannter Raum: constructor/)
|
||||
})
|
||||
|
||||
test('Orte für Effekte fehlen (null), wenn die Etage nicht im Layout ist', () => {
|
||||
const klein = buildLayout([role('x', 'anfragen', 'netz')], ['anfragen'])
|
||||
assert.deepEqual(klein.spots, { pult: null, tresor: null, aussen: null, archiv: null })
|
||||
assert.equal(klein.floors.length, 1)
|
||||
})
|
||||
Reference in new issue
Block a user