Two things, one of them a real bug. THE BUG: bakeTiles baked to a single global 'floor:tiles' and early-returned if that key existed. Textures outlive a scene, so the first venue you played baked the floor for every venue after it — you would survive a week at The Royal, get promoted, walk into Voltage and still be standing on the pub's carpet. It only shows on promotion, which is exactly the moment the venue ladder is supposed to pay off, and never in a dev route because those reload the page. The baked texture is now keyed per venue; verified by switching venues inside one session and watching the concrete arrive. MATERIALS: FloorLayout.materials names a generated plate per tile kind and the baker stamps it TINTED to the palette colour. The plate carries grain, the palette keeps hue — an untinted photo-real plate blows straight past the brightness band the whole room was tuned in, and darkness is the mechanic. Plates are 16x16, the game's own tile, because at that size "carpet" is structured noise, which is also what pub carpet looks like from head height. Ten of them: carpet, concrete, deck, parquet, looTile, paving, plaster, brick, timber, steel. No plate named, or art not landed, falls back to flat colour exactly as before. Wave 2 filler for the two spaces the review found genuinely empty — The Royal's central carpet (four props across ~500 tiles) and ROOM's east hall (~23% of its interior with no lights and three props) — plus the kit every real room has and no game room does: fire extinguishers, exit signs, CCTV, bins, a notice board, ceiling fans. Voltage gets the same pass; being the reference room is how it quietly missed the last one. The invariant caught a duplicate prop id I introduced while placing these, which is what it is for. Also relaxed one ROOM assertion that keyed on array order: it checked that the FIRST sodium light stands over the loading dock, which broke the moment the east hall legitimately burned the same colour. It now checks that one of them does. Gate: lint clean, tsc clean, 821 tests passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
143 lines
7.2 KiB
TypeScript
143 lines
7.2 KiB
TypeScript
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<typeof tileAt>) => 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 20 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 the dock is what it is FOR.
|
|
// Asserted as "at least one sodium light stands over the dock" rather than
|
|
// "the first one does": the east hall legitimately burns the same colour
|
|
// (it is the same building's lighting), and keying on array order made this
|
|
// fail the moment another sodium light was added anywhere earlier.
|
|
const sodium = ROOM.lights.filter((l) => l.colour === 0xe08420);
|
|
expect(sodium.length).toBeGreaterThan(0);
|
|
expect(
|
|
sodium.some((l) => tileAt(ROOM.map, l.tx, l.ty) === 'yard'),
|
|
'no sodium light stands over the loading dock',
|
|
).toBe(true);
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|