fix(02-06,G4): add wall band primitive in gate-renderer — close floating-gate gap
Adds a 4th Phaser primitive to drawGate: a faint vertical wall band at the gate's column (x=880) spanning the full 768px canvas height with alpha=0.18 (mid of the 0.15-0.20 fix_shape range). Closes G4 first-impression UX gap from 2026-05-09 live UAT — the gate now reads as part of a wall rather than a floating gray rectangle, honoring the bible's "walled garden" framing. Z-order: wall (behind) → body → glow → hit. The wall band shares the GATE_COLOR hue (0x6e6e75); the low alpha is what visually distinguishes the wall (structural context) from the body (the load-bearing element). WALL_BAND_WIDTH = GATE_HIT_W * 0.55 = 44px — narrower than the gate so the gate body still reads as the focal point. The wall does NOT pulse; updateGateIndicator continues to manage only the glow's alpha tween. Phase 3 watercolor deferral preserved: this is a single Phaser primitive, no painted texture, no animation. GateGameObjects interface gains a `wall` field — additive, so the existing Garden.ts consumer continues to work unchanged (it stores the whole returned object in this.gate). Vitest: 4 new cases green via Phaser-Scene-mock pattern (constants in range, first rectangle call has wall geometry, 4 total rectangles, gate exposes the wall handle). npm run ci exits 0; 333/333 total green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* G4 (gap closure 02-06) — assert gate-renderer adds a faint vertical
|
||||||
|
* wall band primitive at the gate's column.
|
||||||
|
*
|
||||||
|
* Phaser-Scene-mock pattern from Plan 02-06 Task 3 (avoids the Phaser 4 /
|
||||||
|
* happy-dom canvas.getContext incompatibility per Plan 02-02 SUMMARY).
|
||||||
|
* vi.mock('phaser') short-circuits the Phaser bundle import so this test
|
||||||
|
* can exercise drawGate's call surface in isolation; BlendModes.ADD is
|
||||||
|
* mocked as a sentinel value.
|
||||||
|
*/
|
||||||
|
vi.mock('phaser', () => ({
|
||||||
|
default: { BlendModes: { ADD: 1 } },
|
||||||
|
BlendModes: { ADD: 1 },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const {
|
||||||
|
drawGate,
|
||||||
|
WALL_BAND_X,
|
||||||
|
WALL_BAND_WIDTH,
|
||||||
|
WALL_BAND_HEIGHT,
|
||||||
|
WALL_BAND_ALPHA,
|
||||||
|
WALL_BAND_COLOR,
|
||||||
|
} = await import('./gate-renderer');
|
||||||
|
type Scene = Parameters<typeof drawGate>[0];
|
||||||
|
|
||||||
|
function makeRectangleMock(): {
|
||||||
|
setInteractive: ReturnType<typeof vi.fn>;
|
||||||
|
on: ReturnType<typeof vi.fn>;
|
||||||
|
setData: ReturnType<typeof vi.fn>;
|
||||||
|
setBlendMode: ReturnType<typeof vi.fn>;
|
||||||
|
setAlpha: ReturnType<typeof vi.fn>;
|
||||||
|
} {
|
||||||
|
const r = {
|
||||||
|
setInteractive: vi.fn().mockReturnThis(),
|
||||||
|
on: vi.fn().mockReturnThis(),
|
||||||
|
setData: vi.fn().mockReturnThis(),
|
||||||
|
setBlendMode: vi.fn().mockReturnThis(),
|
||||||
|
setAlpha: vi.fn().mockReturnThis(),
|
||||||
|
};
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeScene(): Scene {
|
||||||
|
const rectangle = makeRectangleMock();
|
||||||
|
return {
|
||||||
|
add: {
|
||||||
|
rectangle: vi.fn(() => rectangle),
|
||||||
|
},
|
||||||
|
tweens: { add: vi.fn() },
|
||||||
|
} as unknown as Scene;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('gate-renderer (Plan 02-06 G4 closure)', () => {
|
||||||
|
it('exports the wall band geometry constants with expected values', () => {
|
||||||
|
expect(WALL_BAND_X).toBe(880); // matches GATE_X
|
||||||
|
expect(WALL_BAND_HEIGHT).toBe(768); // matches Phaser canvas height
|
||||||
|
expect(WALL_BAND_ALPHA).toBeGreaterThanOrEqual(0.15); // fix_shape range
|
||||||
|
expect(WALL_BAND_ALPHA).toBeLessThanOrEqual(0.2); // fix_shape range
|
||||||
|
expect(WALL_BAND_COLOR).toBe(0x6e6e75); // same hue as GATE_COLOR
|
||||||
|
expect(WALL_BAND_WIDTH).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('drawGate adds the wall primitive at the gate column with low alpha', () => {
|
||||||
|
const scene = makeScene();
|
||||||
|
drawGate(scene);
|
||||||
|
|
||||||
|
// First scene.add.rectangle call is the wall band (per drawGate
|
||||||
|
// implementation order — wall is drawn behind everything else).
|
||||||
|
const firstCall = (scene.add.rectangle as ReturnType<typeof vi.fn>).mock.calls[0];
|
||||||
|
// Signature: (x, y, width, height, fillColor, fillAlpha)
|
||||||
|
expect(firstCall[0]).toBe(WALL_BAND_X); // x
|
||||||
|
expect(firstCall[1]).toBe(WALL_BAND_HEIGHT / 2); // y-centered
|
||||||
|
expect(firstCall[2]).toBe(WALL_BAND_WIDTH); // width
|
||||||
|
expect(firstCall[3]).toBe(WALL_BAND_HEIGHT); // height = canvas height (full vertical span)
|
||||||
|
expect(firstCall[4]).toBe(WALL_BAND_COLOR); // color
|
||||||
|
expect(firstCall[5]).toBe(WALL_BAND_ALPHA); // alpha — low (0.18)
|
||||||
|
});
|
||||||
|
|
||||||
|
it('drawGate creates 4 rectangles total (wall + body + glow + hit)', () => {
|
||||||
|
const scene = makeScene();
|
||||||
|
drawGate(scene);
|
||||||
|
expect(scene.add.rectangle as ReturnType<typeof vi.fn>).toHaveBeenCalledTimes(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returned GateGameObjects exposes the wall handle', () => {
|
||||||
|
const scene = makeScene();
|
||||||
|
const gate = drawGate(scene);
|
||||||
|
expect(gate.wall).toBeDefined();
|
||||||
|
expect(gate.body).toBeDefined();
|
||||||
|
expect(gate.glow).toBeDefined();
|
||||||
|
expect(gate.hit).toBeDefined();
|
||||||
|
expect(gate.glowTween).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as Phaser from 'phaser';
|
import * as Phaser from 'phaser';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Phaser primitive gate visual + indicator (D-15).
|
* Phaser primitive gate visual + indicator (D-15) + wall context (G4).
|
||||||
*
|
*
|
||||||
* The gate sits at the right edge of the 4×4 garden (canvas pixel
|
* The gate sits at the right edge of the 4×4 garden (canvas pixel
|
||||||
* coordinates). When a Lura beat is pending — luraBeatProgress.pending
|
* coordinates). When a Lura beat is pending — luraBeatProgress.pending
|
||||||
@@ -9,8 +9,12 @@ import * as Phaser from 'phaser';
|
|||||||
* When the player clicks the gate's hit rectangle, the Garden scene
|
* When the player clicks the gate's hit rectangle, the Garden scene
|
||||||
* dispatches setDialogueOverlayOpen(true), which mounts LuraDialogue.
|
* dispatches setDialogueOverlayOpen(true), which mounts LuraDialogue.
|
||||||
*
|
*
|
||||||
* Phase 3 paints over with the watercolor gate. The hit + glow shapes
|
* Plan 02-06 G4 — additionally renders a faint vertical wall band at
|
||||||
* stay in place; only the body's primitive draw is replaced.
|
* the gate's column connecting top-to-bottom of the canvas. Phaser
|
||||||
|
* primitive only (no painted texture, no animation). Phase 3 paints
|
||||||
|
* the watercolor wall over this band without changing the structural
|
||||||
|
* intent. The bible's "walled garden" framing requires the gate to
|
||||||
|
* read as part of a wall, not a free-floating element.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Canvas-space coordinates. The garden's 4×4 grid is centered at
|
// Canvas-space coordinates. The garden's 4×4 grid is centered at
|
||||||
@@ -24,7 +28,18 @@ const GATE_GLOW_COLOR = 0xe8d8b6;
|
|||||||
const GATE_HIT_W = 80;
|
const GATE_HIT_W = 80;
|
||||||
const GATE_HIT_H = 120;
|
const GATE_HIT_H = 120;
|
||||||
|
|
||||||
|
// Plan 02-06 G4 — wall band geometry. Spans the canvas height vertically
|
||||||
|
// and is centered on the gate's column. Faint alpha so the gate body
|
||||||
|
// reads as the load-bearing element; the wall is structural context only.
|
||||||
|
export const WALL_BAND_X = GATE_X;
|
||||||
|
export const WALL_BAND_WIDTH = GATE_HIT_W * 0.55; // narrower than the gate hit so the gate body still reads
|
||||||
|
export const WALL_BAND_HEIGHT = 768; // matches Phaser canvas height in src/game/main.ts
|
||||||
|
export const WALL_BAND_ALPHA = 0.18; // mid of the 0.15-0.20 fix_shape range
|
||||||
|
export const WALL_BAND_COLOR = GATE_COLOR; // same hue as gate body, low alpha distinguishes them
|
||||||
|
|
||||||
export interface GateGameObjects {
|
export interface GateGameObjects {
|
||||||
|
/** Plan 02-06 G4 — faint vertical wall band primitive (no animation). */
|
||||||
|
wall: Phaser.GameObjects.Rectangle;
|
||||||
hit: Phaser.GameObjects.Rectangle;
|
hit: Phaser.GameObjects.Rectangle;
|
||||||
body: Phaser.GameObjects.Rectangle;
|
body: Phaser.GameObjects.Rectangle;
|
||||||
glow: Phaser.GameObjects.Rectangle;
|
glow: Phaser.GameObjects.Rectangle;
|
||||||
@@ -32,11 +47,22 @@ export interface GateGameObjects {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* drawGate — adds the three rectangles (body / glow / hit) to the
|
* drawGate — adds the four rectangles (wall / body / glow / hit) to the
|
||||||
* scene and returns handles. The glow is initially fully transparent
|
* scene and returns handles. Z-order: wall (behind) → body → glow → hit.
|
||||||
* (alpha=0); updateGateIndicator manages its visibility.
|
* The glow is initially fully transparent (alpha=0); updateGateIndicator
|
||||||
|
* manages its visibility.
|
||||||
*/
|
*/
|
||||||
export function drawGate(scene: Phaser.Scene): GateGameObjects {
|
export function drawGate(scene: Phaser.Scene): GateGameObjects {
|
||||||
|
// Plan 02-06 G4 — wall band first (drawn behind everything else).
|
||||||
|
const wall = scene.add.rectangle(
|
||||||
|
WALL_BAND_X,
|
||||||
|
WALL_BAND_HEIGHT / 2, // y-centered on the canvas
|
||||||
|
WALL_BAND_WIDTH,
|
||||||
|
WALL_BAND_HEIGHT,
|
||||||
|
WALL_BAND_COLOR,
|
||||||
|
WALL_BAND_ALPHA,
|
||||||
|
);
|
||||||
|
|
||||||
const body = scene.add.rectangle(
|
const body = scene.add.rectangle(
|
||||||
GATE_X,
|
GATE_X,
|
||||||
GATE_Y,
|
GATE_Y,
|
||||||
@@ -65,13 +91,14 @@ export function drawGate(scene: Phaser.Scene): GateGameObjects {
|
|||||||
);
|
);
|
||||||
hit.setInteractive({ useHandCursor: true });
|
hit.setInteractive({ useHandCursor: true });
|
||||||
hit.setData('isGate', true);
|
hit.setData('isGate', true);
|
||||||
return { hit, body, glow, glowTween: null };
|
return { wall, hit, body, glow, glowTween: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* updateGateIndicator — start/stop the soft alpha pulse based on
|
* updateGateIndicator — start/stop the soft alpha pulse based on
|
||||||
* whether a beat is pending. Idempotent: calling it twice with the
|
* whether a beat is pending. Idempotent: calling it twice with the
|
||||||
* same isPending value is a no-op.
|
* same isPending value is a no-op. Plan 02-06 G4 leaves this function
|
||||||
|
* untouched — the wall band does NOT pulse.
|
||||||
*/
|
*/
|
||||||
export function updateGateIndicator(
|
export function updateGateIndicator(
|
||||||
scene: Phaser.Scene,
|
scene: Phaser.Scene,
|
||||||
|
|||||||
Reference in New Issue
Block a user