import { describe, expect, it } from 'vitest'; import { ROOM } from '../../src/scenes/floor/layouts/room'; import { isWalkable, tileAt, worldToTile } from '../../src/scenes/floor/venueMap'; import { assertLayout } from './layoutInvariants'; const tiles = (predicate: (kind: ReturnType) => boolean): number => { let n = 0; for (let ty = 0; ty < ROOM.map.height; ty++) { for (let tx = 0; tx < ROOM.map.width; tx++) if (predicate(tileAt(ROOM.map, tx, ty))) n++; } return n; }; describe('ROOM layout', () => { it('satisfies the shared venue invariants', () => { assertLayout(ROOM, { bar: 4, dance: 6, booth: 1, toilet: 3, smoke: 2, entry: 1, exit: 1 }); }); it('matches the venue id the ladder looks it up by', () => { // data/venues.ts: a night finds its room by VenueDef.id, so a typo here is // a venue that silently falls back to Voltage. expect(ROOM.id).toBe('room'); }); // ---- the pillars: this venue's movement character --------------------------- it('scatters concrete pillars through the main floor as real obstacles', () => { const pillars = ROOM.props.filter((p) => p.kind === 'pillar'); expect(pillars.length).toBeGreaterThanOrEqual(8); for (const p of pillars) { // A pillar prop over walkable tiles is decoration; the whole point is // that you path AROUND these. for (let ty = p.ty; ty < p.ty + p.th; ty++) { for (let tx = p.tx; tx < p.tx + p.tw; tx++) { expect(isWalkable(ROOM.map, tx, ty), `pillar '${p.id}' is walkable at ${tx},${ty}`).toBe(false); } } } }); // ---- the DJ: on the floor, not in a booth ------------------------------------ it('puts the DJ low and central with no barrier round the desk', () => { const decks = ROOM.posts.decks!; // Central-ish: within a quarter of the grid of the middle, in both axes. expect(Math.abs(decks.tx - ROOM.map.width / 2)).toBeLessThan(ROOM.map.width / 4); expect(Math.abs(decks.ty - ROOM.map.height / 2)).toBeLessThan(ROOM.map.height / 4); // The DJ stands on the same tiles the crowd does — a `djbooth` or `djfloor` // tile under the post means somebody has walled the booth back in. expect(tileAt(ROOM.map, decks.tx, decks.ty)).toBe('dance'); // ...and the desk is genuinely open: three of the four sides of the post // are walkable, so you can be shoved off it. const sides: readonly (readonly [number, number])[] = [[1, 0], [-1, 0], [0, -1]]; const open = sides.filter(([dx, dy]) => isWalkable(ROOM.map, decks.tx + dx, decks.ty + dy)); expect(open).toHaveLength(3); }); it('runs CDJs, not turntables', () => { expect(ROOM.props.some((p) => p.kind === 'cdj')).toBe(true); expect(ROOM.props.some((p) => p.kind === 'deck')).toBe(false); }); // ---- the dunnies ------------------------------------------------------------- it('has three cubicles, a cracked basin and no mirror', () => { expect(ROOM.map.stalls).toHaveLength(3); expect(ROOM.props.filter((p) => p.kind === 'brokenBasin')).toHaveLength(1); // Only one of the three still has a door on it; the other two tell their // story with what is on the floor outside them. expect(ROOM.props.filter((p) => p.kind === 'cubicleDoor')).toHaveLength(1); expect(ROOM.props.filter((p) => p.kind === 'wetFloor').length).toBeGreaterThanOrEqual(2); expect(ROOM.props.some((p) => p.kind === 'mopBucket')).toBe(true); }); // ---- the loading dock -------------------------------------------------------- it('smokes on the loading dock: open-sky yard behind a fence and a roller door', () => { for (const a of ROOM.map.anchors.smoke) { const t = worldToTile(a.x, a.y); expect(tileAt(ROOM.map, t.tx, t.ty), `smoke anchor ${t.tx},${t.ty} is not open sky`).toBe('yard'); } expect(tiles((k) => k === 'yard')).toBeGreaterThan(80); expect(tiles((k) => k === 'fence')).toBeGreaterThan(0); expect(ROOM.props.some((p) => p.kind === 'rollerDoor')).toBe(true); expect(ROOM.props.some((p) => p.kind === 'wheelieBin')).toBe(true); expect(ROOM.props.filter((p) => p.kind === 'pallet').length).toBeGreaterThanOrEqual(2); }); // ---- the bar ----------------------------------------------------------------- it('serves cans off a plywood plank, with no taps and no till', () => { expect(ROOM.props.some((p) => p.kind === 'plywoodBar')).toBe(true); expect(ROOM.props.some((p) => p.kind === 'canStack')).toBe(true); expect(ROOM.props.some((p) => p.kind === 'taps' || p.kind === 'till')).toBe(false); }); // ---- the board --------------------------------------------------------------- it('hangs THE BOARD by the entry, where shuffleDressCode can be read', () => { const board = ROOM.props.find((p) => p.kind === 'ruleBoard'); expect(board).toBeDefined(); const entry = worldToTile(ROOM.map.anchors.entry[0]!.x, ROOM.map.anchors.entry[0]!.y); expect(Math.abs(board!.tx - entry.tx)).toBeLessThanOrEqual(12); expect(Math.abs(board!.ty - entry.ty)).toBeLessThanOrEqual(12); }); // ---- no branding, and no light ------------------------------------------------ it('paints no neon anywhere, because branding is how you get found', () => { expect(tiles((k) => k === 'neon')).toBe(0); }); it('is darker than Voltage: fewer lights, tighter radii, sodium over the dock', () => { // Voltage runs 19 zone lights with dance pools at radius 26. If ROOM ever // catches up with that, the torch has stopped being the mechanic. expect(ROOM.lights.length).toBeLessThanOrEqual(14); for (const l of ROOM.lights) expect(l.radius, `light '${l.id}' is too big for ROOM`).toBeLessThanOrEqual(40); const dance = ROOM.lights.filter((l) => l.colour === 0xd03470 && l.pulse === 'beat'); expect(dance.length).toBeGreaterThanOrEqual(4); for (const l of dance) expect(l.radius).toBeLessThan(26); // The one signature colour ROOM is allowed, and it belongs to the dock. const sodium = ROOM.lights.find((l) => l.colour === 0xe08420); expect(sodium).toBeDefined(); expect(tileAt(ROOM.map, sodium!.tx, sodium!.ty)).toBe('yard'); }); it('runs the rig on the floor, taped down', () => { for (const kind of ['strobe', 'smokeMachine', 'cableSnake', 'stackF1', 'subBass'] as const) { expect(ROOM.props.some((p) => p.kind === kind), `no '${kind}' props`).toBe(true); } }); it('is bare concrete, kept inside the darkness sheet brightness band', () => { const luma = (c: number): number => (((c >> 16) & 0xff) + ((c >> 8) & 0xff) + (c & 0xff)) / 3; const palette = ROOM.palette!; // Voltage's floor sits at 0x221e2b (~36). ROOM must be darker but not black: // a palette that reads right in isolation renders as nothing in the room. expect(luma(palette.floor!)).toBeLessThan(36); expect(luma(palette.floor!)).toBeGreaterThan(16); expect(luma(palette.dance!)).toBeLessThan(luma(palette.floor!) + 12); }); });