From 15c4b597bda51a9ec96059170038f62249cb19e1 Mon Sep 17 00:00:00 2001 From: arudaev Date: Sun, 25 Oct 2026 09:42:06 +0100 Subject: [PATCH] feat(debug): a white backdrop next to the purple one Alexander: check every layer against purple, then white. Debug mode's backdrop can be white instead: dark specks and fringes hide on purple and show on white. docs/09 records the rule and backdrop_check.py. Refs #72 --- docs/02-GAME-DESIGN.md | 2 +- docs/09-ART-PIPELINE.md | 6 ++++++ game/src/core/debug.test.ts | 9 ++++++++- game/src/core/debug.ts | 11 +++++++++-- game/src/stage/debugOverlay.ts | 5 ++--- game/src/ui/menu.ts | 1 + 6 files changed, 27 insertions(+), 7 deletions(-) diff --git a/docs/02-GAME-DESIGN.md b/docs/02-GAME-DESIGN.md index f7b8bb2..d725225 100644 --- a/docs/02-GAME-DESIGN.md +++ b/docs/02-GAME-DESIGN.md @@ -48,7 +48,7 @@ A ☰ button in the top-right corner, Esc or the pad's Start open the **game men - **Controls** – the table above, for keyboard and gamepad. - **Settings** – sound on/off and volume, zoom (the game's own view or 133 / 200 / 286 % wider), full screen, **Hero** (a brighter, cooler outline on the hero, easier to see against the leaves; playtest R1), and **Keys**. In Keys, click an action and press its new key (Esc keeps the old one). The key then belongs to that action alone and is taken from any other. The choice is saved in the browser, and "Reset to the defaults" brings back both laptop layouts. Esc, Tab and the debug keys stay the game's (`player/physicalKeys.ts` `Bindings`). - **Debug** – debug mode and its switches (`game/src/core/debug.ts`); the corner line names every switch that is on, so a playtest screenshot records what was showing: - - **Layers**: every stage layer by id and z, switched off in all chunks at once; with the **purple backdrop** any hole or seam in a plate behind it shows at once (the hot-dog rule, docs/09). + - **Layers**: every stage layer by id and z, switched off in all chunks at once; with the **purple backdrop** any hole or seam in a plate behind it shows at once (the hot-dog rule, docs/09). **White instead** shows what purple hides: dark specks and a dark fringe. Check every layer against both. - **Collision**: walk lines, one-way beams, ladders, walls, water, hazards, triggers (the F2 legend). - **Actors**: the hero's body, speed and ground; every rover, crab and drone with its hitbox, facing arrow, sight box (red when it sees the hero), patrol path and turn points, phase and health; Jenny's arena, box, facing and phase; Dexter's position, state and next stop. - **FPS and position**: frame rate, the hero's position and chunk. diff --git a/docs/09-ART-PIPELINE.md b/docs/09-ART-PIPELINE.md index 405ac03..7b49693 100644 --- a/docs/09-ART-PIPELINE.md +++ b/docs/09-ART-PIPELINE.md @@ -221,6 +221,12 @@ flowchart TB `art/sprites/boards.json`: the hero sheet (locked) comes first, the pose or object photos follow, and the style master goes last. The model draws on a flat magenta key. `crush` keys the magenta at full size, downscales ×2 with premultiplied alpha (so no purple fringes), quantises to 96 colours, and writes a transparent 1× PNG plus a readable preview. Chair photos are split into **block**, **overhead/up-down** and **side** sequences by file name. Every board stays "to do" until the direction is approved; see [07](07-ASSET-MANIFEST.md). +**Every new sprite and layer is checked over purple, then over white (Alexander, 25 Oct), before it ships:** +- offline, `python scripts/backdrop_check.py [--holes]` puts it over both and counts key specks, soft pixels and, for plates, enclosed holes; +- in the game, debug mode → Purple backdrop, and "white instead", with each layer shown alone. + +A glow the model draws as a soft halo crushes into scattered specks; `"despeckle": N` drops islands under N px. A plate cut from a soft source keeps its pinholes, so fill them before it ships. + Every animation board gets an `atlas` entry when it is drawn, because the review preview is cut with it. Export into the game only once its Design review is **Approved**. `rows` lists the board's rows of sprites from top to bottom: the animations in each row, left to right, with their frame counts from [07](07-ASSET-MANIFEST.md), or `"skip"` for a row that isn't exported (close-ups, scale lineups). `height` is the game height in pixels of the tallest frame of the first animation; the hero's is 76, in an 80 px frame. When the first animation has no standing pose (a jump), `"ref": "jump-08"` names the frame that is 76 px instead. An animation may continue at the start of the next row (death 6 + 6: list it again with the rest of its frames). `"even": true` keeps a row's specks and cuts it along seams without the weight check, for frames that break into dust. `"float": true` on an animation drawn in the air (jump, fall) puts each frame's lowest pixel on the baseline, because the game already lifts the body. `fps` defaults to 12 and `loop` to true. A sheet holds at most about 30 frames in four rows; the model miscounts bigger sheets, so split them (`hero-move`, `hero-run`, `hero-idle`). Open every brief with the magenta background and the no-text rule: put later in the brief, the model sometimes draws on paper with captions. ### How a sprite board is made diff --git a/game/src/core/debug.test.ts b/game/src/core/debug.test.ts index dc63f7d..598ac9d 100644 --- a/game/src/core/debug.test.ts +++ b/game/src/core/debug.test.ts @@ -1,5 +1,5 @@ import { describe as suite, expect, it } from 'vitest'; -import { DebugStore, describe } from './debug'; +import { DebugStore, describe, backdropColour } from './debug'; suite('debug mode switches', () => { it('shows nothing until debug mode is on', () => { @@ -27,5 +27,12 @@ suite('debug mode switches', () => { store.set({ on: true, actors: false, backdrop: true, zoom: 0.5 }); store.toggleLayer('01-far'); expect(describe(store.state)).toBe('debug: backdrop collision stats | hidden: 01-far | zoom 0.5'); + store.set({ white: true }); + expect(describe(store.state)).toMatch(/^debug: backdrop-white /); + }); + + it('paints the backdrop purple, or white to check a layer against both', () => { + expect(backdropColour({ white: false })).toBe(0x9b30ff); + expect(backdropColour({ white: true })).toBe(0xffffff); }); }); diff --git a/game/src/core/debug.ts b/game/src/core/debug.ts index abf5da5..3474725 100644 --- a/game/src/core/debug.ts +++ b/game/src/core/debug.ts @@ -8,6 +8,8 @@ export interface DebugState { on: boolean; /** The back of the world painted purple, so a hole in any plate shows at once. */ backdrop: boolean; + /** The backdrop white instead of purple: dark specks and a dark fringe show there, light ones on purple. */ + white: boolean; /** Walk lines, one-way beams, ladders, walls, water, hazards and triggers. */ collision: boolean; /** Hitboxes, facing, sight, patrol paths and state of the hero, the enemies, Jenny and Dexter. */ @@ -25,7 +27,7 @@ type Listener = (state: DebugState) => void; export const ZOOMS = [1, 0.75, 0.5, 0.35] as const; export class DebugStore { - private value: DebugState = { on: false, backdrop: false, collision: true, actors: true, stats: true, hidden: new Set(), zoom: 1 }; + private value: DebugState = { on: false, backdrop: false, white: false, collision: true, actors: true, stats: true, hidden: new Set(), zoom: 1 }; private listeners = new Set(); get state(): DebugState { return this.value; } set(patch: Partial>) { this.value = { ...this.value, ...patch }; this.emit(); } @@ -44,9 +46,14 @@ export class DebugStore { export const debugStore = new DebugStore(); /** One line per switch that is on – printed in the corner so a screenshot records the debug setup. */ +/** The debug backdrop's colour: purple, or white when asked (check every layer against both). */ +export const BACKDROPS = { purple: 0x9b30ff, white: 0xffffff } as const; +export const backdropColour = (state: Pick) => state.white ? BACKDROPS.white : BACKDROPS.purple; + export function describe(state: DebugState): string { if (!state.on) return ''; - const on = (['backdrop', 'collision', 'actors', 'stats'] as const).filter(key => state[key]); + const on = (['backdrop', 'collision', 'actors', 'stats'] as const).filter(key => state[key]) + .map(key => key === 'backdrop' && state.white ? 'backdrop-white' : key); const hidden = [...state.hidden].sort(); return [`debug: ${on.join(' ') || 'nothing'}`, hidden.length ? `hidden: ${hidden.join(' ')}` : '', state.zoom !== 1 ? `zoom ${state.zoom}` : ''].filter(Boolean).join(' | '); diff --git a/game/src/stage/debugOverlay.ts b/game/src/stage/debugOverlay.ts index 9524868..4fd5730 100644 --- a/game/src/stage/debugOverlay.ts +++ b/game/src/stage/debugOverlay.ts @@ -1,5 +1,5 @@ import Phaser from 'phaser'; -import { debugStore, describe, type DebugState } from '../core/debug'; +import { backdropColour, debugStore, describe, type DebugState } from '../core/debug'; import { BODY_HEIGHT, BODY_WIDTH } from '../player/body'; import { SPOT_RANGE } from '../enemies/rover'; import { CRAB_SPOT } from '../enemies/crab'; @@ -36,7 +36,6 @@ export interface DexterLike { x: number; state: string; stop?: { x: number }; ro /** How far each kind sees ahead (px), from its model's constants; the sight box is one body high. */ export const SIGHT: Record = { rover: SPOT_RANGE, crab: CRAB_SPOT, drone: DRONE_SPOT }; -const PURPLE = 0x9b30ff; const ALERT = new Set(['tell', 'charge', 'stab', 'dive']); /** The sight box in front of an enemy: from its centre `range` px the way it faces, one body high. */ @@ -72,7 +71,7 @@ export class DebugOverlay { private apply(state: DebugState) { // Layer visibility: the stage's culling (render.ts) asks debugStore each frame. const camera = this.scene.cameras.main; - camera.setBackgroundColor(debugStore.shows('backdrop') ? PURPLE : this.background); + camera.setBackgroundColor(debugStore.shows('backdrop') ? backdropColour(state) : this.background); camera.setZoom(state.zoom); // a Settings choice: wider views work with debug mode off too this.collision.setVisible(debugStore.shows('collision')); this.stats.setVisible(debugStore.shows('stats')); diff --git a/game/src/ui/menu.ts b/game/src/ui/menu.ts index 082de91..90fcbb5 100644 --- a/game/src/ui/menu.ts +++ b/game/src/ui/menu.ts @@ -210,6 +210,7 @@ export class GameMenu { field('Debug mode', check(s.on, 'On – draws over the game; screenshots record what is on', on => debugStore.set({ on }))), field('Show', row( check(s.backdrop, 'Purple backdrop', v => debugStore.set({ backdrop: v })), + check(s.white, '… white instead (check every layer against both)', v => debugStore.set({ white: v })), check(s.collision, 'Collision', v => debugStore.set({ collision: v })), check(s.actors, 'Actors: boxes, facing, sight, paths', v => debugStore.set({ actors: v })), check(s.stats, 'FPS and position', v => debugStore.set({ stats: v })))),