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:
2026-05-09 12:16:42 -04:00
parent ab48c7ef30
commit 88adc4f623
2 changed files with 131 additions and 8 deletions
+96
View File
@@ -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();
});
});