fix(02-06,G3): brighten tile outline and hover state — close dim-grid gap
Brightens OUTLINE_COLOR 0x4d4d52 → 0x5a5a60 and OUTLINE_HOVER
0x6e6e75 → 0x7a7a82, plus adds a subtle HOVER_FILL_ALPHA=0.06 fill bump
on the hit rectangle. Closes G3 first-impression UX gap from 2026-05-09
live UAT — the 4×4 grid now reads as legible interactive surfaces
against the #1a1a1a canvas background.
The hover state is pointer-driven steady-state (color + fill alpha
swap), not animation — reduced-motion-safe per Phase 8 ownership of
global motion preferences. Phase 3 watercolor deferral preserved: this
is color values + a fill alpha; no painted assets, no new sprites.
Constants OUTLINE_COLOR / OUTLINE_HOVER are exported so the test pins
the brightened values directly. drawTiles function signature unchanged —
Garden.ts continues to work without modification.
Vitest: 5 new cases green via Phaser-Scene-mock pattern (vi.mock('phaser')
short-circuits the Phaser 4 / happy-dom checkInverseAlpha boot crash;
the mocked scene's add.graphics / add.rectangle capture the call args).
npm run ci exits 0; 329/329 total green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<typeof drawTiles>[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<typeof vi.fn>; lineStyle: ReturnType<typeof vi.fn>; strokeRoundedRect: ReturnType<typeof vi.fn> };
|
||||||
|
rectangle: {
|
||||||
|
setInteractive: ReturnType<typeof vi.fn>;
|
||||||
|
on: ReturnType<typeof vi.fn>;
|
||||||
|
setData: ReturnType<typeof vi.fn>;
|
||||||
|
setFillStyle: ReturnType<typeof vi.fn>;
|
||||||
|
};
|
||||||
|
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<typeof vi.fn>)).toHaveBeenCalledTimes(16);
|
||||||
|
expect((scene.add.rectangle as ReturnType<typeof vi.fn>)).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
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,13 +3,18 @@ import { GRID_SIZE } from '../../sim/garden/types';
|
|||||||
import { tileTopLeftCanvas, GRID_LAYOUT } from './tile-coords';
|
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
|
* Per CONTEXT D-06; Phase 3 paints the watercolor treatment over this
|
||||||
* primitive without changing the function signature.
|
* 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;
|
export const OUTLINE_COLOR = 0x5a5a60; // ← Plan 02-06 G3 (was 0x4d4d52 — too dim against #1a1a1a)
|
||||||
const OUTLINE_HOVER = 0x6e6e75;
|
export const OUTLINE_HOVER = 0x7a7a82; // ← Plan 02-06 G3 (was 0x6e6e75 — needed clearer contrast in resting vs hover)
|
||||||
const OUTLINE_ALPHA = 0.6;
|
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 {
|
export interface TileGameObjects {
|
||||||
/** Hit-area rectangle (interactive). */
|
/** Hit-area rectangle (interactive). */
|
||||||
@@ -34,11 +39,18 @@ export function drawTiles(scene: Phaser.Scene): TileGameObjects[] {
|
|||||||
const g = scene.add.graphics();
|
const g = scene.add.graphics();
|
||||||
drawOutline(g, tl.x, tl.y, OUTLINE_COLOR);
|
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);
|
const hit = scene.add.rectangle(cx, cy, GRID_LAYOUT.tileSize, GRID_LAYOUT.tileSize, 0xffffff, 0);
|
||||||
hit.setInteractive({ useHandCursor: true });
|
hit.setInteractive({ useHandCursor: true });
|
||||||
hit.on('pointerover', () => drawOutline(g, tl.x, tl.y, OUTLINE_HOVER));
|
hit.on('pointerover', () => {
|
||||||
hit.on('pointerout', () => drawOutline(g, tl.x, tl.y, OUTLINE_COLOR));
|
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.
|
// Tag the hit object with its index for handler dispatch.
|
||||||
hit.setData('tileIdx', i);
|
hit.setData('tileIdx', i);
|
||||||
|
|||||||
Reference in New Issue
Block a user