diff --git a/src/render/garden/tile-renderer.test.ts b/src/render/garden/tile-renderer.test.ts new file mode 100644 index 0000000..78bf14b --- /dev/null +++ b/src/render/garden/tile-renderer.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect, vi } from 'vitest'; + +/** + * G3 (gap closure 02-06) — assert tile-renderer uses the brightened + * outline colors and the hover fill bump. + * + * Phaser cannot import under happy-dom — its boot probe `checkInverseAlpha` + * calls `canvas.getContext('2d')` which returns null and the call into + * `context.fillStyle = '...'` then crashes (Plan 02-02 SUMMARY documents + * this). We mock the `phaser` module entirely so importing tile-renderer.ts + * does not pull the real Phaser bundle. + * + * The rest of the test mocks the Scene API surface that drawTiles uses. + */ +vi.mock('phaser', () => ({ + default: {}, + // No named exports needed — tile-renderer uses only Phaser types and + // the runtime call surface comes from the mocked scene argument below. +})); + +const { drawTiles, OUTLINE_COLOR, OUTLINE_HOVER } = await import('./tile-renderer'); +type Scene = Parameters[0]; + +describe('tile-renderer (Plan 02-06 G3 closure)', () => { + it('exports OUTLINE_COLOR=0x5a5a60 (brightened from 0x4d4d52)', () => { + expect(OUTLINE_COLOR).toBe(0x5a5a60); + }); + + it('exports OUTLINE_HOVER=0x7a7a82 (brightened from 0x6e6e75)', () => { + expect(OUTLINE_HOVER).toBe(0x7a7a82); + }); + + function makeMocks(): { + graphics: { clear: ReturnType; lineStyle: ReturnType; strokeRoundedRect: ReturnType }; + rectangle: { + setInteractive: ReturnType; + on: ReturnType; + setData: ReturnType; + setFillStyle: ReturnType; + }; + scene: Scene; + pointerOverHandlers: Array<() => void>; + } { + const graphics = { + clear: vi.fn(), + lineStyle: vi.fn(), + strokeRoundedRect: vi.fn(), + }; + const pointerOverHandlers: Array<() => void> = []; + const rectangle = { + setInteractive: vi.fn().mockReturnThis(), + on: vi.fn().mockReturnThis(), + setData: vi.fn().mockReturnThis(), + setFillStyle: vi.fn().mockReturnThis(), + }; + rectangle.on.mockImplementation((evt: string, fn: () => void) => { + if (evt === 'pointerover') pointerOverHandlers.push(fn); + return rectangle; + }); + const scene = { + add: { + graphics: vi.fn(() => graphics), + rectangle: vi.fn(() => rectangle), + }, + } as unknown as Scene; + return { graphics, rectangle, scene, pointerOverHandlers }; + } + + it('drawTiles creates 16 tile groups with outline graphics + hit rectangles', () => { + const { scene } = makeMocks(); + const tiles = drawTiles(scene); + expect(tiles).toHaveLength(16); + expect((scene.add.graphics as ReturnType)).toHaveBeenCalledTimes(16); + expect((scene.add.rectangle as ReturnType)).toHaveBeenCalledTimes(16); + }); + + it('initial draw uses OUTLINE_COLOR (resting state)', () => { + const { graphics, scene } = makeMocks(); + drawTiles(scene); + const calls = graphics.lineStyle.mock.calls; + expect(calls.length).toBeGreaterThan(0); + // Every initial call uses OUTLINE_COLOR; assert the first. + expect(calls[0][1]).toBe(OUTLINE_COLOR); + }); + + it('pointerover handler swaps to OUTLINE_HOVER and adds fill alpha bump', () => { + const { graphics, rectangle, scene, pointerOverHandlers } = makeMocks(); + drawTiles(scene); + expect(pointerOverHandlers.length).toBeGreaterThan(0); + + // Fire the first tile's pointerover handler. + pointerOverHandlers[0]!(); + + // After pointerover, the most recent lineStyle call uses OUTLINE_HOVER. + const lineStyleCalls = graphics.lineStyle.mock.calls; + const lastLineCall = lineStyleCalls[lineStyleCalls.length - 1]; + expect(lastLineCall[1]).toBe(OUTLINE_HOVER); + + // setFillStyle was called with the hover alpha bump (>0, ≤0.1). + const fillCalls = rectangle.setFillStyle.mock.calls; + const fillBumpCall = fillCalls.find((c) => c[1] && c[1] > 0); + expect(fillBumpCall).toBeDefined(); + expect(fillBumpCall![1]).toBeGreaterThan(0); + expect(fillBumpCall![1]).toBeLessThanOrEqual(0.1); // sanity: subtle bump, not a flash + }); +}); diff --git a/src/render/garden/tile-renderer.ts b/src/render/garden/tile-renderer.ts index ef9eb72..2cbab0b 100644 --- a/src/render/garden/tile-renderer.ts +++ b/src/render/garden/tile-renderer.ts @@ -3,13 +3,18 @@ import { GRID_SIZE } from '../../sim/garden/types'; import { tileTopLeftCanvas, GRID_LAYOUT } from './tile-coords'; /** - * Empty-tile look: faint outlined rounded rectangle with subtle hover. + * Empty-tile look: outlined rounded rectangle with subtle hover. * Per CONTEXT D-06; Phase 3 paints the watercolor treatment over this * primitive without changing the function signature. + * + * Plan 02-06 G3 — outline + hover values brightened so the 4×4 grid + * reads as legible interactive surfaces against the #1a1a1a canvas + * background. No painted assets (Phase 3 deferral preserved). */ -const OUTLINE_COLOR = 0x4d4d52; -const OUTLINE_HOVER = 0x6e6e75; +export const OUTLINE_COLOR = 0x5a5a60; // ← Plan 02-06 G3 (was 0x4d4d52 — too dim against #1a1a1a) +export const OUTLINE_HOVER = 0x7a7a82; // ← Plan 02-06 G3 (was 0x6e6e75 — needed clearer contrast in resting vs hover) const OUTLINE_ALPHA = 0.6; +const HOVER_FILL_ALPHA = 0.06; // ← Plan 02-06 G3 — slight fill on hover to reinforce the affordance (no animation noise, reduced-motion-safe) export interface TileGameObjects { /** Hit-area rectangle (interactive). */ @@ -34,11 +39,18 @@ export function drawTiles(scene: Phaser.Scene): TileGameObjects[] { const g = scene.add.graphics(); drawOutline(g, tl.x, tl.y, OUTLINE_COLOR); - // Hit rectangle (transparent, interactive). + // Hit rectangle (interactive). Holds a faint hover fill to reinforce + // the click affordance — Plan 02-06 G3. const hit = scene.add.rectangle(cx, cy, GRID_LAYOUT.tileSize, GRID_LAYOUT.tileSize, 0xffffff, 0); hit.setInteractive({ useHandCursor: true }); - hit.on('pointerover', () => drawOutline(g, tl.x, tl.y, OUTLINE_HOVER)); - hit.on('pointerout', () => drawOutline(g, tl.x, tl.y, OUTLINE_COLOR)); + hit.on('pointerover', () => { + drawOutline(g, tl.x, tl.y, OUTLINE_HOVER); + hit.setFillStyle(0xffffff, HOVER_FILL_ALPHA); // ← Plan 02-06 G3 — slight fill bump + }); + hit.on('pointerout', () => { + drawOutline(g, tl.x, tl.y, OUTLINE_COLOR); + hit.setFillStyle(0xffffff, 0); // ← Plan 02-06 G3 — reset + }); // Tag the hit object with its index for handler dispatch. hit.setData('tileIdx', i);