The venue ladder has always listed four venues in data/venues.ts, but they shared ONE floor map and differed only by difficulty knobs — so a promotion you survived a whole week for looked exactly like the room you had just left. - venueMap.ts grows a FloorLayout contract (map + props + lights + posts + probes + palette) and the newGrid() primitives every room is painted with. Voltage stays IN venueMap rather than moving to layouts/, so the new rooms can import primitives from it without closing an import cycle. - Three new rooms in src/scenes/floor/layouts/: The Royal (horseshoe public bar, pool room, pokies corner, trough + two cubicles, a beer garden that is plainly the nicest room in the pub, and a DJ "corner" that is a folding table because this pub never built a booth), Elevate (open-air rooftop — most of the grid is sky, the DJ is on an unwalled plinth, you arrive by lift), ROOM (concrete warehouse, pillars you path around, central booth, loading-dock smoking area, twelve lights in the whole venue). - Nothing about a room is a module singleton any more. FloorView, sweep and FloorDemoScene take the layout; the door picks its street plate by venue id. - FloorDemoScene's six hardcoded station spots (TAPS_SPOT, DECKS_SPOT, ...) were Voltage's tile coordinates. At four venues a hardcoded (47,3) puts the bar shift inside The Royal's pool room, so each layout now names its own stations and the scene reads them from there. - tests/floor/layoutInvariants.ts is an executable rulebook: perimeter holds, every walkable tile reachable from the entry, anchors on walkable ground, posts not sealed (a post on a sealed tile freezes the player for the night — that has shipped twice), probes reachable, props on-grid. Proved against Voltage BEFORE the new rooms were authored against it. Dev route #floor:<venueId> boots the floor straight into one room, because otherwise seeing ROOM means surviving three weeks of the ladder. Gate: lint clean, build clean, 821 tests passing (was 784). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
158 lines
7.1 KiB
TypeScript
158 lines
7.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { THE_ROYAL } from '../../src/scenes/floor/layouts/theRoyal';
|
|
import { isWalkable, tileAt, worldToTile } from '../../src/scenes/floor/venueMap';
|
|
import { assertLayout } from './layoutInvariants';
|
|
|
|
const map = THE_ROYAL.map;
|
|
|
|
const countTiles = (predicate: (kind: ReturnType<typeof tileAt>) => boolean): number => {
|
|
let n = 0;
|
|
for (let ty = 0; ty < map.height; ty++) {
|
|
for (let tx = 0; tx < map.width; tx++) if (predicate(tileAt(map, tx, ty))) n++;
|
|
}
|
|
return n;
|
|
};
|
|
|
|
const propsOf = (kind: string) => THE_ROYAL.props.filter((p) => p.kind === kind);
|
|
|
|
describe('THE_ROYAL layout', () => {
|
|
it('satisfies the shared venue invariants', () => {
|
|
assertLayout(THE_ROYAL, { bar: 6, dance: 4, booth: 2, toilet: 2, smoke: 2, entry: 1, exit: 1 });
|
|
});
|
|
|
|
it('matches the venue id the ladder looks it up by', () => {
|
|
// data/venues.ts finds a night's room by VenueDef.id — a typo here is a
|
|
// venue that silently falls back to Voltage.
|
|
expect(THE_ROYAL.id).toBe('theRoyal');
|
|
});
|
|
|
|
// ---- the horseshoe ---------------------------------------------------------
|
|
it('wraps the public bar in counter on three sides with a workable pit inside', () => {
|
|
expect(tileAt(map, 22, 17)).toBe('bar'); // north run
|
|
expect(tileAt(map, 22, 25)).toBe('bar'); // south run
|
|
expect(tileAt(map, 28, 21)).toBe('bar'); // the bend
|
|
// The open west end is the whole point: the taps post lives in the pit, so
|
|
// walling this in freezes the bar shift for the night.
|
|
expect(isWalkable(map, 15, 21)).toBe(true);
|
|
const taps = THE_ROYAL.posts.taps;
|
|
expect(taps).toBeDefined();
|
|
expect(tileAt(map, taps!.tx, taps!.ty)).toBe('floor');
|
|
});
|
|
|
|
// ---- the DJ corner ---------------------------------------------------------
|
|
it('gives the DJ a folding table on the carpet, not a booth', () => {
|
|
expect(countTiles((k) => k === 'djbooth' || k === 'djfloor')).toBe(0);
|
|
const decks = THE_ROYAL.posts.decks;
|
|
expect(decks).toBeDefined();
|
|
expect(isWalkable(map, decks!.tx, decks!.ty)).toBe(true);
|
|
for (const p of [...propsOf('deck'), ...propsOf('mixer'), ...propsOf('highTable')]) {
|
|
if (p.tx < 50 || p.ty < 33) continue; // the pool-room leaner is not DJ gear
|
|
expect(isWalkable(map, p.tx, p.ty), `'${p.id}' is not on plain floor`).toBe(true);
|
|
}
|
|
});
|
|
|
|
// ---- the dunnies -----------------------------------------------------------
|
|
it('has two cubicles, one of them plainly out of order', () => {
|
|
expect(map.stalls).toHaveLength(2);
|
|
expect(map.anchors.toilet).toHaveLength(2);
|
|
for (const stall of map.stalls) {
|
|
const inside = worldToTile(stall.inside.x, stall.inside.y);
|
|
expect(tileAt(map, inside.tx, inside.ty)).toBe('stall');
|
|
}
|
|
// The story prop: a wet floor sign and a mop bucket parked at cubicle two.
|
|
const wet = propsOf('wetFloor');
|
|
expect(wet).toHaveLength(1);
|
|
expect(propsOf('mopBucket')).toHaveLength(1);
|
|
const secondDoor = worldToTile(map.stalls[1]!.door.x, map.stalls[1]!.door.y);
|
|
expect(Math.abs(wet[0]!.tx - secondDoor.tx)).toBeLessThanOrEqual(2);
|
|
});
|
|
|
|
it('plumbs the dunnies with a trough and a single basin', () => {
|
|
expect(propsOf('trough')).toHaveLength(1);
|
|
expect(propsOf('sinkRow')).toHaveLength(1);
|
|
// Both stand on 'sink' tiles, which are furniture: you cannot walk the trough.
|
|
for (const p of [...propsOf('trough'), ...propsOf('sinkRow')]) {
|
|
expect(tileAt(map, p.tx, p.ty), `'${p.id}' is not plumbed in`).toBe('sink');
|
|
}
|
|
});
|
|
|
|
// ---- the beer garden -------------------------------------------------------
|
|
it('opens the beer garden to the sky inside a fence, with one gate', () => {
|
|
const yard = countTiles((k) => k === 'yard');
|
|
expect(yard).toBeGreaterThan(200);
|
|
// Every yard tile must sit inside the fence rectangle, or the rain falls in
|
|
// the pub (the renderer keys the weather off 'yard', not off a room id).
|
|
for (let ty = 0; ty < map.height; ty++) {
|
|
for (let tx = 0; tx < map.width; tx++) {
|
|
if (tileAt(map, tx, ty) !== 'yard') continue;
|
|
expect(tx).toBeGreaterThan(12);
|
|
expect(tx).toBeLessThan(45);
|
|
expect(ty).toBeGreaterThan(33);
|
|
expect(ty).toBeLessThan(43);
|
|
}
|
|
}
|
|
expect(countTiles((k) => k === 'smokeDoor')).toBe(1);
|
|
expect(tileAt(map, 29, 33)).toBe('smokeDoor');
|
|
expect(tileAt(map, 29, 32)).toBe('floor');
|
|
expect(tileAt(map, 29, 34)).toBe('yard');
|
|
// It is the nicest room in the pub, so it is furnished like one.
|
|
expect(propsOf('picnicTable').length).toBeGreaterThanOrEqual(4);
|
|
expect(propsOf('beerUmbrella').length).toBeGreaterThanOrEqual(2);
|
|
expect(propsOf('patioHeater')).toHaveLength(1);
|
|
expect(propsOf('buttBin')).toHaveLength(1);
|
|
});
|
|
|
|
// ---- the rooms that are not the dance floor ---------------------------------
|
|
it('keeps the dance floor smaller than the pool room it plays second fiddle to', () => {
|
|
const dance = countTiles((k) => k === 'dance');
|
|
expect(dance).toBeGreaterThan(0);
|
|
// Voltage's floor is 375 tiles. This one is an afterthought bolted on in 2004.
|
|
expect(dance).toBeLessThan(150);
|
|
expect(propsOf('poolTable')).toHaveLength(2);
|
|
expect(propsOf('cueRack')).toHaveLength(1);
|
|
expect(propsOf('dartboard')).toHaveLength(1);
|
|
expect(propsOf('pokie')).toHaveLength(6);
|
|
expect(propsOf('tabBoard').length).toBeGreaterThanOrEqual(2);
|
|
expect(propsOf('bistroTable').length).toBeGreaterThanOrEqual(4);
|
|
expect(propsOf('bainMarie')).toHaveLength(1);
|
|
expect(propsOf('raffleDrum')).toHaveLength(1);
|
|
expect(propsOf('jukebox')).toHaveLength(1);
|
|
expect(propsOf('discoball')).toHaveLength(1);
|
|
});
|
|
|
|
// ---- light signature -------------------------------------------------------
|
|
it('lights the pub amber and spends its one signature colour on the pokies', () => {
|
|
const AMBER = 0xd8a020;
|
|
const POKIE_BLUE = 0x4aa0c0;
|
|
const allowed = new Set([
|
|
AMBER, POKIE_BLUE,
|
|
0x30a050, // pool-table cone
|
|
0x50c060, // dunnies fluoro
|
|
0xd03470, // dance + entry pink
|
|
0x3060c0, // DJ
|
|
0x6080a0, // beer garden
|
|
]);
|
|
for (const l of THE_ROYAL.lights) {
|
|
expect(allowed.has(l.colour), `light '${l.id}' invents a colour`).toBe(true);
|
|
}
|
|
expect(THE_ROYAL.lights.filter((l) => l.colour === AMBER).length).toBeGreaterThanOrEqual(6);
|
|
expect(THE_ROYAL.lights.filter((l) => l.colour === POKIE_BLUE).length).toBeGreaterThanOrEqual(2);
|
|
expect(THE_ROYAL.lights.filter((l) => l.colour === 0x30a050)).toHaveLength(2);
|
|
|
|
// No pink anywhere but the dance corner and the way out (VENUES.md §2).
|
|
for (const l of THE_ROYAL.lights) {
|
|
if (l.colour !== 0xd03470) continue;
|
|
const onDance = tileAt(map, l.tx, l.ty) === 'dance';
|
|
expect(onDance || l.id === 'entry', `pink light '${l.id}' is loose in the pub`).toBe(true);
|
|
if (onDance) expect(l.pulse).toBe('beat');
|
|
}
|
|
});
|
|
|
|
it('lays pub carpet rather than club floor', () => {
|
|
expect(THE_ROYAL.palette?.floor).toBeDefined();
|
|
// Warm: more red than blue, which is the whole difference from Voltage.
|
|
const floor = THE_ROYAL.palette!.floor!;
|
|
expect((floor >> 16) & 0xff).toBeGreaterThan(floor & 0xff);
|
|
});
|
|
});
|