From 3f46c84530738c7991804a3c19190f096bc705dc Mon Sep 17 00:00:00 2001 From: Map Date: Mon, 8 Jun 2026 12:12:34 +0100 Subject: [PATCH 1/8] Fix browser tests for Phaser 4 HUD container (.list) integration Replace all scene.children.list.find/filter with a collectFromSceneAndHud helper that recursively walks Phaser 4's .list property on Containers and the hudContainer. Phaser 4 uses .list (not .children) for container children, causing overlay/button tests to miss objects parented into the HUD container. - HelpPanel.browser.test.ts: add collectFromSceneAndHud, find ? button - GolfOverlay.browser.test.ts: fix collect helper and findButtonContainer - SushiGoOverlay.browser.test.ts: fix collectFromSceneAndHud - TheMindOverlay.browser.test.ts: fix collect helper and newScene.walk - BeleagueredCastleOverlay.browser.test.ts: rewrite with collect helper All unit tests (3006) and browser tests pass. Build succeeds. --- .../BeleagueredCastleOverlay.browser.test.ts | 71 +++++++++++++------ tests/golf/GolfOverlay.browser.test.ts | 59 +++++++++++---- tests/sushi-go/SushiGoOverlay.browser.test.ts | 57 ++++++++++----- tests/the-mind/TheMindOverlay.browser.test.ts | 51 ++++++++----- tests/ui/HelpPanel.browser.test.ts | 54 +++++++++++--- 5 files changed, 211 insertions(+), 81 deletions(-) diff --git a/tests/beleaguered-castle/BeleagueredCastleOverlay.browser.test.ts b/tests/beleaguered-castle/BeleagueredCastleOverlay.browser.test.ts index ca538c04..99102d92 100644 --- a/tests/beleaguered-castle/BeleagueredCastleOverlay.browser.test.ts +++ b/tests/beleaguered-castle/BeleagueredCastleOverlay.browser.test.ts @@ -40,6 +40,29 @@ function getOverlayManager(scene: Phaser.Scene): any { return (scene as any).overlayManager; } +/** + * Collect display objects from scene children and the HUD container. + * Phaser 4 containers store children in .list (not .children). + */ +function collectFromSceneAndHud( + scene: Phaser.Scene, + predicate: (obj: Phaser.GameObjects.GameObject) => obj is T, +): T[] { + const result: T[] = []; + const walk = (parent: Phaser.GameObjects.GameObject[]) => { + for (const child of parent) { + if (predicate(child)) result.push(child); + if (child instanceof Phaser.GameObjects.Container && (child as any).list) { + walk((child as any).list); + } + } + }; + walk(scene.children.list); + const hud = (scene as any).hudContainer as { list: Phaser.GameObjects.GameObject[] } | undefined; + if (hud && hud.list) walk(hud.list); + return result; +} + describe('Beleaguered Castle help panel', () => { let game: Phaser.Game | null = null; @@ -122,29 +145,31 @@ describe('Beleaguered Castle overlays', () => { const scene = game.scene.getScene('BeleagueredCastleScene') as any; await waitFrames(8); - getOverlayManager(scene).showWinOverlay(0); + (scene as any).showWinOverlay(0); await waitFrames(5); - // Check blocker - const rects = scene.children.list.filter( - (child: any) => child instanceof Phaser.GameObjects.Rectangle && child.depth === 2000, - ) as Phaser.GameObjects.Rectangle[]; - expect(rects.length).toBeGreaterThanOrEqual(1); - const blocker = rects.find((r: any) => r.width === 1280 && r.height === 720 && r.input?.enabled); + // Check blocker - objects are in HUD container + const allRects = collectFromSceneAndHud(scene, (child): child is Phaser.GameObjects.Rectangle => + child instanceof Phaser.GameObjects.Rectangle && child.depth === 2000, + ); + expect(allRects.length).toBeGreaterThanOrEqual(1); + const blocker = allRects.find((r) => r.width === 1280 && r.height === 720 && r.input?.enabled); expect(blocker).toBeDefined(); // Check buttons at depth 2001 const labels = ['[ New Game ]', '[ Restart ]', '[ Menu ]']; - const btns = scene.children.list.filter( - (child: any) => child instanceof Phaser.GameObjects.Text && labels.includes(child.text) && child.depth === 2001, - ) as Phaser.GameObjects.Text[]; + const btns = collectFromSceneAndHud(scene, (child): child is Phaser.GameObjects.Text => + child instanceof Phaser.GameObjects.Text && labels.includes(child.text) && child.depth === 2001, + ); expect(btns.length).toBeGreaterThanOrEqual(2); for (const btn of btns) expect(btn.input?.enabled).toBe(true); // Dismiss getOverlayManager(scene).dismiss(); await waitFrames(3); - const winText = scene.children.list.filter((child: any) => child instanceof Phaser.GameObjects.Text && child.text === 'You Win!'); + const winText = collectFromSceneAndHud(scene, (child): child is Phaser.GameObjects.Text => + child instanceof Phaser.GameObjects.Text && child.text === 'You Win!', + ); expect(winText.length).toBe(0); }); @@ -153,30 +178,30 @@ describe('Beleaguered Castle overlays', () => { const scene = game.scene.getScene('BeleagueredCastleScene') as any; await waitFrames(8); - getOverlayManager(scene).showNoMovesOverlay(); + (scene as any).showNoMovesOverlay(); await waitFrames(5); - // Check blocker - const rects = scene.children.list.filter( - (child: any) => child instanceof Phaser.GameObjects.Rectangle && child.depth === 2000, - ) as Phaser.GameObjects.Rectangle[]; - expect(rects.length).toBeGreaterThanOrEqual(1); - const blocker = rects.find((r: any) => r.width === 1280 && r.height === 720 && r.input?.enabled); + // Check blocker - objects are in HUD container + const allRects = collectFromSceneAndHud(scene, (child): child is Phaser.GameObjects.Rectangle => + child instanceof Phaser.GameObjects.Rectangle && child.depth === 2000, + ); + expect(allRects.length).toBeGreaterThanOrEqual(1); + const blocker = allRects.find((r) => r.width === 1280 && r.height === 720 && r.input?.enabled); expect(blocker).toBeDefined(); // Check buttons const labels = ['[ Undo Last ]', '[ New Game ]', '[ Restart ]', '[ Menu ]']; - const btns = scene.children.list.filter( - (child: any) => child instanceof Phaser.GameObjects.Text && labels.includes(child.text) && child.depth === 2001, - ) as Phaser.GameObjects.Text[]; + const btns = collectFromSceneAndHud(scene, (child): child is Phaser.GameObjects.Text => + child instanceof Phaser.GameObjects.Text && labels.includes(child.text) && child.depth === 2001, + ); expect(btns.length).toBeGreaterThanOrEqual(3); for (const btn of btns) expect(btn.input?.enabled).toBe(true); // Dismiss getOverlayManager(scene).dismiss(); await waitFrames(3); - const noMoveText = scene.children.list.filter( - (child: any) => child instanceof Phaser.GameObjects.Text && child.text === 'No Productive Moves Available', + const noMoveText = collectFromSceneAndHud(scene, (child): child is Phaser.GameObjects.Text => + child instanceof Phaser.GameObjects.Text && child.text === 'No Productive Moves Available', ); expect(noMoveText.length).toBe(0); }); diff --git a/tests/golf/GolfOverlay.browser.test.ts b/tests/golf/GolfOverlay.browser.test.ts index d21da845..9dfa62ec 100644 --- a/tests/golf/GolfOverlay.browser.test.ts +++ b/tests/golf/GolfOverlay.browser.test.ts @@ -88,10 +88,32 @@ async function waitForCondition( * Get scene private properties via type-safe cast. */ function getSceneInternals(scene: Phaser.Scene) { - return scene as any; } +/** + * Collect display objects from scene children and the HUD container. + * Phaser 4 containers store children in .list. + */ +function collectFromSceneAndHud( + scene: Phaser.Scene, + predicate: (obj: Phaser.GameObjects.GameObject) => obj is T, +): T[] { + const result: T[] = []; + const walk = (parent: Phaser.GameObjects.GameObject[]) => { + for (const child of parent) { + if (predicate(child)) result.push(child); + if (child instanceof Phaser.GameObjects.Container && (child as any).list) { + walk((child as any).list); + } + } + }; + walk(scene.children.list); + const hud = (scene as any).hudContainer as { list: Phaser.GameObjects.GameObject[] } | undefined; + if (hud && hud.list) walk(hud.list); + return result; +} + /** * Dispatch a real DOM MouseEvent on the game canvas at the given * game-world coordinates. This routes through Phaser's full input @@ -179,17 +201,26 @@ describe('Golf overlay button tests', () => { await waitFrames(3); // Helper: find a container that contains a Text child with the given label. + // Search both scene children and HUD container (Phaser 4 uses .list) const findContainerByText = ( label: string, ): Phaser.GameObjects.Container | undefined => { - return scene.children.list.find( - (child: Phaser.GameObjects.GameObject) => - child instanceof Phaser.GameObjects.Container && - (child as Phaser.GameObjects.Container).list.some( - (c: Phaser.GameObjects.GameObject) => - c instanceof Phaser.GameObjects.Text && c.text === label, - ), - ) as Phaser.GameObjects.Container | undefined; + const findInList = (items: Phaser.GameObjects.GameObject[]) => { + const found = items.find( + (child: Phaser.GameObjects.GameObject) => + child instanceof Phaser.GameObjects.Container && + (child as any).list.some( + (c: Phaser.GameObjects.GameObject) => + c instanceof Phaser.GameObjects.Text && c.text === label, + ), + ); + return found as Phaser.GameObjects.Container | undefined; + }; + const found = findInList(scene.children.list); + if (found) return found; + const hud = (scene as any).hudContainer as { list: Phaser.GameObjects.GameObject[] } | undefined; + if (hud && hud.list) return findInList(hud.list); + return undefined; }; const playAgainBtn = findContainerByText('[ Play Again ]'); @@ -280,11 +311,11 @@ describe('Golf overlay button tests', () => { await waitFrames(3); // Find interactive rectangles at depth 10 (the input blocker) - const rects = scene.children.list.filter( - (child: Phaser.GameObjects.GameObject) => - child instanceof Phaser.GameObjects.Rectangle && - (child as Phaser.GameObjects.Rectangle).depth === 10, - ) as Phaser.GameObjects.Rectangle[]; + // The overlay system parents objects into the HUD container in Phaser 4 + const allRects = collectFromSceneAndHud(scene, (child): child is Phaser.GameObjects.Rectangle => + child instanceof Phaser.GameObjects.Rectangle, + ); + const rects = allRects.filter((r) => r.depth === 10); // Should have at least 2 rectangles at depth 10: the full-screen blocker and the visible overlay expect(rects.length).toBeGreaterThanOrEqual(2); diff --git a/tests/sushi-go/SushiGoOverlay.browser.test.ts b/tests/sushi-go/SushiGoOverlay.browser.test.ts index c0ebf084..bb30862c 100644 --- a/tests/sushi-go/SushiGoOverlay.browser.test.ts +++ b/tests/sushi-go/SushiGoOverlay.browser.test.ts @@ -36,6 +36,29 @@ function waitFrames(n: number): Promise { }); } +/** + * Collect display objects from scene children and the HUD container. + * Phaser 4 containers store children in .list (not .children). + */ +function collectFromSceneAndHud( + scene: Phaser.Scene, + predicate: (obj: Phaser.GameObjects.GameObject) => obj is T, +): T[] { + const result: T[] = []; + const walk = (parent: Phaser.GameObjects.GameObject[]) => { + for (const child of parent) { + if (predicate(child)) result.push(child); + if (child instanceof Phaser.GameObjects.Container && (child as any).list) { + walk((child as any).list); + } + } + }; + walk(scene.children.list); + const hud = (scene as any).hudContainer as { list: Phaser.GameObjects.GameObject[] } | undefined; + if (hud && hud.list) walk(hud.list); + return result; +} + describe('Sushi Go round-score overlay', () => { let game: Phaser.Game | null = null; @@ -66,9 +89,9 @@ describe('Sushi Go round-score overlay', () => { await waitFrames(3); // Find the "Next Round" button container - const containers = scene.children.list.filter( - (child: Phaser.GameObjects.GameObject) => child instanceof Phaser.GameObjects.Container, - ) as Phaser.GameObjects.Container[]; + const containers = collectFromSceneAndHud(scene, (child): child is Phaser.GameObjects.Container => + child instanceof Phaser.GameObjects.Container, + ); const findButtonLabel = (container: Phaser.GameObjects.Container, label: string): boolean => { return (container as any).list?.some( @@ -89,9 +112,9 @@ describe('Sushi Go round-score overlay', () => { expect(bg?.input?.enabled).toBe(true); // Verify the full-screen input blocker exists and is interactive - const rects = scene.children.list.filter( - (child: any) => child instanceof Phaser.GameObjects.Rectangle && child.depth === 10, - ) as Phaser.GameObjects.Rectangle[]; + const rects = collectFromSceneAndHud(scene, (child): child is Phaser.GameObjects.Rectangle => + child instanceof Phaser.GameObjects.Rectangle && child.depth === 10, + ); expect(rects.length).toBeGreaterThanOrEqual(2); // blocker + visible box const blocker = rects.find((r: any) => r.width === 1280 && r.height === 720 && r.input?.enabled); expect(blocker).toBeDefined(); @@ -131,9 +154,9 @@ describe('Sushi Go game-over overlay', () => { await waitFrames(3); // Action buttons are Containers with Text children (migrated to shared Renderer API). - const containers = scene.children.list.filter( - (child: Phaser.GameObjects.GameObject) => child instanceof Phaser.GameObjects.Container, - ) as Phaser.GameObjects.Container[]; + const containers = collectFromSceneAndHud(scene, (child): child is Phaser.GameObjects.Container => + child instanceof Phaser.GameObjects.Container, + ); const findButtonLabel = (container: Phaser.GameObjects.Container, label: string): boolean => { return (container as any).list?.some( @@ -157,9 +180,9 @@ describe('Sushi Go game-over overlay', () => { expect(menuBg?.input?.enabled).toBe(true); // Verify the full-screen input blocker exists and is interactive - const rects = scene.children.list.filter( - (child: any) => child instanceof Phaser.GameObjects.Rectangle && child.depth === 10, - ) as Phaser.GameObjects.Rectangle[]; + const rects = collectFromSceneAndHud(scene, (child): child is Phaser.GameObjects.Rectangle => + child instanceof Phaser.GameObjects.Rectangle && child.depth === 10, + ); expect(rects.length).toBeGreaterThanOrEqual(2); // blocker + visible box const blocker = rects.find((r: any) => r.width === 1280 && r.height === 720 && r.input?.enabled); expect(blocker).toBeDefined(); @@ -167,8 +190,8 @@ describe('Sushi Go game-over overlay', () => { // Verify dismissal cleans up overlay scene.overlayManager.dismiss(); await waitFrames(2); - const textsAfterDismiss = scene.children.list.filter( - (child: any) => child instanceof Phaser.GameObjects.Text && + const textsAfterDismiss = collectFromSceneAndHud(scene, (child): child is Phaser.GameObjects.Text => + child instanceof Phaser.GameObjects.Text && (child as Phaser.GameObjects.Text).text.includes('You Win!'), ); expect(textsAfterDismiss.length).toBe(0); @@ -202,9 +225,9 @@ describe('Sushi Go game-over overlay', () => { await waitFrames(3); - const texts = scene.children.list.filter( - (child: Phaser.GameObjects.GameObject) => child instanceof Phaser.GameObjects.Text, - ) as Phaser.GameObjects.Text[]; + const texts = collectFromSceneAndHud(scene, (child): child is Phaser.GameObjects.Text => + child instanceof Phaser.GameObjects.Text, + ); const finalTextObj = texts.find((t) => (t.text as string).includes('Final: You')); diff --git a/tests/the-mind/TheMindOverlay.browser.test.ts b/tests/the-mind/TheMindOverlay.browser.test.ts index f8f25d30..365e1cc4 100644 --- a/tests/the-mind/TheMindOverlay.browser.test.ts +++ b/tests/the-mind/TheMindOverlay.browser.test.ts @@ -60,10 +60,32 @@ function waitFrames(n: number): Promise { * Get scene private properties via type-safe cast. */ function getSceneInternals(scene: Phaser.Scene) { - return scene as any; } +/** + * Collect display objects from scene children and the HUD container. + * Phaser 4 containers store children in .list (not .children). + */ +function collectFromSceneAndHud( + scene: Phaser.Scene, + predicate: (obj: Phaser.GameObjects.GameObject) => obj is T, +): T[] { + const result: T[] = []; + const walk = (parent: Phaser.GameObjects.GameObject[]) => { + for (const child of parent) { + if (predicate(child)) result.push(child); + if (child instanceof Phaser.GameObjects.Container && (child as any).list) { + walk((child as any).list); + } + } + }; + walk(scene.children.list); + const hud = (scene as any).hudContainer as { list: Phaser.GameObjects.GameObject[] } | undefined; + if (hud && hud.list) walk(hud.list); + return result; +} + /** * Dispatch a real DOM MouseEvent on the game canvas at the given * game-world coordinates. This routes through Phaser's full input @@ -157,10 +179,9 @@ describe('The Mind overlay button tests', () => { await waitFrames(3); // Find text objects with overlay button labels - const texts = scene.children.list.filter( - (child: Phaser.GameObjects.GameObject) => + const texts = collectFromSceneAndHud(scene, (child): child is Phaser.GameObjects.Text => child instanceof Phaser.GameObjects.Text, - ) as Phaser.GameObjects.Text[]; + ); const tryAgainBtn = texts.find((t) => t.text === '[ Try Again ]'); const menuBtn = texts.find((t) => t.text === '[ Menu ]'); @@ -183,10 +204,9 @@ describe('The Mind overlay button tests', () => { await waitFrames(5); // Find the "Try Again" button to get its coordinates - const texts = scene.children.list.filter( - (child: Phaser.GameObjects.GameObject) => + const texts = collectFromSceneAndHud(scene, (child): child is Phaser.GameObjects.Text => child instanceof Phaser.GameObjects.Text, - ) as Phaser.GameObjects.Text[]; + ); const tryAgainBtn = texts.find((t) => t.text === '[ Try Again ]'); expect(tryAgainBtn).toBeDefined(); @@ -210,10 +230,9 @@ describe('The Mind overlay button tests', () => { expect(newPhase).not.toBe('game-won'); // Verify: overlay buttons no longer exist - const newTexts = newScene.children.list.filter( - (child: Phaser.GameObjects.GameObject) => - child instanceof Phaser.GameObjects.Text, - ) as Phaser.GameObjects.Text[]; + const newTexts = collectFromSceneAndHud(newScene, (child): child is Phaser.GameObjects.Text => + child instanceof Phaser.GameObjects.Text, + ); const tryAgainAfterRestart = newTexts.find( (t) => t.text === '[ Try Again ]', ); @@ -230,10 +249,9 @@ describe('The Mind overlay button tests', () => { await waitFrames(5); // Find the "Play Again" button - const texts = scene.children.list.filter( - (child: Phaser.GameObjects.GameObject) => + const texts = collectFromSceneAndHud(scene, (child): child is Phaser.GameObjects.Text => child instanceof Phaser.GameObjects.Text, - ) as Phaser.GameObjects.Text[]; + ); const playAgainBtn = texts.find((t) => t.text === '[ Play Again ]'); expect(playAgainBtn).toBeDefined(); expect(playAgainBtn!.input?.enabled).toBe(true); @@ -260,11 +278,10 @@ describe('The Mind overlay button tests', () => { await waitFrames(3); // Find interactive rectangles at depth 2000 (the overlay background) - const rects = scene.children.list.filter( - (child: Phaser.GameObjects.GameObject) => + const rects = collectFromSceneAndHud(scene, (child): child is Phaser.GameObjects.Rectangle => child instanceof Phaser.GameObjects.Rectangle && (child as Phaser.GameObjects.Rectangle).depth === 2000, - ) as Phaser.GameObjects.Rectangle[]; + ); // Should have at least 2 rectangles: the full-screen blocker and the visible overlay box expect(rects.length).toBeGreaterThanOrEqual(2); diff --git a/tests/ui/HelpPanel.browser.test.ts b/tests/ui/HelpPanel.browser.test.ts index b6b9a343..bdc2ebc1 100644 --- a/tests/ui/HelpPanel.browser.test.ts +++ b/tests/ui/HelpPanel.browser.test.ts @@ -46,6 +46,38 @@ function waitFrames(n: number): Promise { }); } +/** + * Collect display objects from scene children and the HUD container. + * In Phaser 4, containers store children in .list (not .children). + */ +function collectFromSceneAndHud( + scene: Phaser.Scene, + predicate: (obj: Phaser.GameObjects.GameObject) => obj is T, +): T[] { + const result: T[] = []; + + // Walk scene children recursively + const walk = (parent: Phaser.GameObjects.GameObject[]) => { + for (const child of parent) { + if (predicate(child)) { + result.push(child); + } + if (child instanceof Phaser.GameObjects.Container && (child as any).list) { + walk((child as any).list); + } + } + }; + walk(scene.children.list); + + // Also walk the HUD container + const hud = (scene as any).hudContainer as { list: Phaser.GameObjects.GameObject[] } | undefined; + if (hud && hud.list) { + walk(hud.list); + } + + return result; +} + // ── Tests ─────────────────────────────────────────────────── describe('UI module exports (browser)', () => { @@ -69,11 +101,12 @@ describe('HelpPanel browser tests', () => { game = await bootGame(); const scene = game.scene.getScene('GolfScene') as Phaser.Scene; - const texts = scene.children.list.filter( - (child) => child instanceof Phaser.GameObjects.Text, - ) as Phaser.GameObjects.Text[]; + // Collect Text objects from scene and HUD container (Phaser 4 uses .list) + const allTexts = collectFromSceneAndHud(scene, (child): child is Phaser.GameObjects.Text => + child instanceof Phaser.GameObjects.Text + ); - const helpButtonText = texts.find((t) => t.text === '?'); + const helpButtonText = allTexts.find((t) => t.text === '?'); expect(helpButtonText).toBeDefined(); }); @@ -81,16 +114,17 @@ describe('HelpPanel browser tests', () => { game = await bootGame(); const scene = game.scene.getScene('GolfScene') as Phaser.Scene; - // The HelpPanel container should exist but not be visible - const containers = scene.children.list.filter( - (child) => child instanceof Phaser.GameObjects.Container, - ) as Phaser.GameObjects.Container[]; + // Collect Containers from scene and HUD container + const allContainers = collectFromSceneAndHud(scene, (child): child is Phaser.GameObjects.Container => + child instanceof Phaser.GameObjects.Container + ); // At least one container should exist (the help panel) - expect(containers.length).toBeGreaterThanOrEqual(1); + expect(allContainers.length).toBeGreaterThanOrEqual(1); // The help panel container should be hidden (not visible or off-screen) - const panelContainer = containers.find((c) => c.x < 0 || !c.visible); + // HelpPanel creates its container at x = -panelWidth + const panelContainer = allContainers.find((c) => c.x < 0 || !c.visible); expect(panelContainer).toBeDefined(); }); From 37f2810e19fbc988247ee9a13efb4d87581a8fc9 Mon Sep 17 00:00:00 2001 From: Map Date: Mon, 8 Jun 2026 13:04:31 +0100 Subject: [PATCH 2/8] CG-0MQ54PYAJ001XCUG: Fix Sushi Go overlay text not visible by parenting to hudContainer - Add text elements in showRoundScoreOverlay() to hudContainer so they render above the overlay background box. - Add text elements in showGameOverOverlay() to hudContainer so they render above the overlay background box. - Add two new browser tests verifying text elements are in hudContainer for both round-score and game-over overlays. This mirrors the Main Street fix (CG-0MQ2IL40S0060FUG, commit d7446dc). --- .../sushi-go/scenes/SushiGoOverlayContent.ts | 6 ++ tests/sushi-go/SushiGoOverlay.browser.test.ts | 81 +++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/example-games/sushi-go/scenes/SushiGoOverlayContent.ts b/example-games/sushi-go/scenes/SushiGoOverlayContent.ts index da23a96c..4815e473 100644 --- a/example-games/sushi-go/scenes/SushiGoOverlayContent.ts +++ b/example-games/sushi-go/scenes/SushiGoOverlayContent.ts @@ -93,6 +93,9 @@ export class SushiGoOverlayContent { }) .setOrigin(0.5, 0) .setDepth(11); + if ((this.scene as any).hudContainer) { + (this.scene as any).hudContainer.add(text); + } this.overlayManager.add(text); const btn = createActionButton( @@ -204,6 +207,9 @@ export class SushiGoOverlayContent { }) .setOrigin(0.5, 0) .setDepth(11); + if ((this.scene as any).hudContainer) { + (this.scene as any).hudContainer.add(text); + } this.overlayManager.add(text); const playBtn = createActionButton( diff --git a/tests/sushi-go/SushiGoOverlay.browser.test.ts b/tests/sushi-go/SushiGoOverlay.browser.test.ts index bb30862c..d35dcac8 100644 --- a/tests/sushi-go/SushiGoOverlay.browser.test.ts +++ b/tests/sushi-go/SushiGoOverlay.browser.test.ts @@ -197,6 +197,87 @@ describe('Sushi Go game-over overlay', () => { expect(textsAfterDismiss.length).toBe(0); }); + it('renders round-score text inside hudContainer for correct z-ordering', async () => { + game = await bootGame(); + const scene = game.scene.getScene('SushiGoScene') as any; + + const fakeRoundResult = { + round: 1, + tableauScores: [9, 8], + tableauBreakdowns: [ + { tempura: 0, sashimi: 0, dumpling: 0, nigiri: 0, chopsticks: 0, puddingCount: 0 }, + { tempura: 0, sashimi: 0, dumpling: 0, nigiri: 0, chopsticks: 0, puddingCount: 0 }, + ], + makiCounts: [0, 0], + makiBonuses: [0, 0], + roundScores: [9, 8], + puddingCounts: [0, 0], + puddingBonuses: [0, 0], + }; + + scene.overlayManager.showRoundScoreOverlay(fakeRoundResult, () => {}); + await waitFrames(3); + + // Verify hudContainer exists + expect(scene.hudContainer).toBeDefined(); + expect(scene.hudContainer).toBeInstanceOf(Phaser.GameObjects.Container); + + // Collect text from hudContainer specifically + const hud = scene.hudContainer as { list: Phaser.GameObjects.GameObject[] }; + const hudTexts = hud.list?.filter( + (child) => child instanceof Phaser.GameObjects.Text, + ) as Phaser.GameObjects.Text[]; + + // The round-score overlay text should be in hudContainer so it renders above the overlay box + const roundScoreText = hudTexts.find((t) => (t.text as string).includes('Round') && (t.text as string).includes('Complete')); + expect(roundScoreText).toBeDefined(); + }); + + it('renders game-over text inside hudContainer for correct z-ordering', async () => { + game = await bootGame(); + const scene = game.scene.getScene('SushiGoScene') as any; + + // Prepare session roundScores so computeDisplayedTotal can sum them + scene.session.players[0].roundScores = [9]; + scene.session.players[1].roundScores = [8]; + + const fakeRoundResult = { + round: 2, + tableauScores: [9, 8], + tableauBreakdowns: [ + { tempura: 0, sashimi: 0, dumpling: 0, nigiri: 0, chopsticks: 0, puddingCount: 0 }, + { tempura: 0, sashimi: 0, dumpling: 0, nigiri: 0, chopsticks: 0, puddingCount: 0 }, + ], + makiCounts: [0, 0], + makiBonuses: [0, 0], + roundScores: [9, 8], + puddingCounts: [0, 0], + puddingBonuses: [0, 0], + }; + + scene.overlayManager.showGameOverOverlay(fakeRoundResult, null, () => { + scene.scene.restart(); + }); + + await waitFrames(3); + + // Verify hudContainer exists + expect(scene.hudContainer).toBeDefined(); + expect(scene.hudContainer).toBeInstanceOf(Phaser.GameObjects.Container); + + // Collect text from hudContainer specifically + const hud = scene.hudContainer as { list: Phaser.GameObjects.GameObject[] }; + const hudTexts = hud.list?.filter( + (child) => child instanceof Phaser.GameObjects.Text, + ) as Phaser.GameObjects.Text[]; + + // The game-over text should be in hudContainer so it renders above the overlay box + const winnerText = hudTexts.find((t) => (t.text as string).includes('You Win!') || (t.text as string).includes('AI Wins!')); + const finalText = hudTexts.find((t) => (t.text as string).includes('Final:')); + expect(winnerText).toBeDefined(); + expect(finalText).toBeDefined(); + }); + it('displays correct final totals including pudding bonuses when provided', async () => { game = await bootGame(); const scene = game.scene.getScene('SushiGoScene') as any; From 28e46855d78cf50fcc4724d94e9cc9f19791eec9 Mon Sep 17 00:00:00 2001 From: Map Date: Mon, 8 Jun 2026 13:32:49 +0100 Subject: [PATCH 3/8] CG-0MQ54PYAJ001XCUG: Also parent overlay buttons to hudContainer - Parent Next Round button (round-score overlay) to hudContainer. - Parent Play Again and Menu buttons (game-over overlay) to hudContainer. - Added 2 new browser tests verifying buttons are in hudContainer for correct z-ordering. This ensures buttons are rendered above the overlay background box. --- .../sushi-go/scenes/SushiGoOverlayContent.ts | 9 ++ tests/sushi-go/SushiGoOverlay.browser.test.ts | 85 +++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/example-games/sushi-go/scenes/SushiGoOverlayContent.ts b/example-games/sushi-go/scenes/SushiGoOverlayContent.ts index 4815e473..e73e9757 100644 --- a/example-games/sushi-go/scenes/SushiGoOverlayContent.ts +++ b/example-games/sushi-go/scenes/SushiGoOverlayContent.ts @@ -111,6 +111,9 @@ export class SushiGoOverlayContent { }, { depth: 11 }, ); + if ((this.scene as any).hudContainer) { + (this.scene as any).hudContainer.add(btn); + } this.overlayManager.add(btn); } @@ -224,9 +227,15 @@ export class SushiGoOverlayContent { }, { depth: 11 }, ); + if ((this.scene as any).hudContainer) { + (this.scene as any).hudContainer.add(playBtn); + } this.overlayManager.add(playBtn); const menuBtn = createSushiGoMenuButton(this.scene, GAME_W / 2 + 20, buttonY - 16, 120, { depth: 11 }); + if ((this.scene as any).hudContainer) { + (this.scene as any).hudContainer.add(menuBtn); + } this.overlayManager.add(menuBtn); } diff --git a/tests/sushi-go/SushiGoOverlay.browser.test.ts b/tests/sushi-go/SushiGoOverlay.browser.test.ts index d35dcac8..8a428960 100644 --- a/tests/sushi-go/SushiGoOverlay.browser.test.ts +++ b/tests/sushi-go/SushiGoOverlay.browser.test.ts @@ -233,6 +233,44 @@ describe('Sushi Go game-over overlay', () => { expect(roundScoreText).toBeDefined(); }); + it('renders Next Round button inside hudContainer for correct z-ordering', async () => { + game = await bootGame(); + const scene = game.scene.getScene('SushiGoScene') as any; + + const fakeRoundResult = { + round: 1, + tableauScores: [9, 8], + tableauBreakdowns: [ + { tempura: 0, sashimi: 0, dumpling: 0, nigiri: 0, chopsticks: 0, puddingCount: 0 }, + { tempura: 0, sashimi: 0, dumpling: 0, nigiri: 0, chopsticks: 0, puddingCount: 0 }, + ], + makiCounts: [0, 0], + makiBonuses: [0, 0], + roundScores: [9, 8], + puddingCounts: [0, 0], + puddingBonuses: [0, 0], + }; + + scene.overlayManager.showRoundScoreOverlay(fakeRoundResult, () => {}); + await waitFrames(3); + + // Collect containers from hudContainer + const hud = scene.hudContainer as { list: Phaser.GameObjects.GameObject[] }; + const hudContainers = hud.list?.filter( + (child) => child instanceof Phaser.GameObjects.Container, + ) as Phaser.GameObjects.Container[]; + + const findButtonLabel = (container: Phaser.GameObjects.Container, label: string): boolean => { + return (container as any).list?.some( + (child: any) => child instanceof Phaser.GameObjects.Text && child.text === label, + ); + }; + + // The Next Round button container should be in hudContainer so it renders above the overlay box + const nextRoundBtn = hudContainers.find((c) => findButtonLabel(c, 'Next Round')); + expect(nextRoundBtn).toBeDefined(); + }); + it('renders game-over text inside hudContainer for correct z-ordering', async () => { game = await bootGame(); const scene = game.scene.getScene('SushiGoScene') as any; @@ -278,6 +316,53 @@ describe('Sushi Go game-over overlay', () => { expect(finalText).toBeDefined(); }); + it('renders game-over buttons inside hudContainer for correct z-ordering', async () => { + game = await bootGame(); + const scene = game.scene.getScene('SushiGoScene') as any; + + // Prepare session roundScores so computeDisplayedTotal can sum them + scene.session.players[0].roundScores = [9]; + scene.session.players[1].roundScores = [8]; + + const fakeRoundResult = { + round: 2, + tableauScores: [9, 8], + tableauBreakdowns: [ + { tempura: 0, sashimi: 0, dumpling: 0, nigiri: 0, chopsticks: 0, puddingCount: 0 }, + { tempura: 0, sashimi: 0, dumpling: 0, nigiri: 0, chopsticks: 0, puddingCount: 0 }, + ], + makiCounts: [0, 0], + makiBonuses: [0, 0], + roundScores: [9, 8], + puddingCounts: [0, 0], + puddingBonuses: [0, 0], + }; + + scene.overlayManager.showGameOverOverlay(fakeRoundResult, null, () => { + scene.scene.restart(); + }); + + await waitFrames(3); + + // Collect containers from hudContainer + const hud = scene.hudContainer as { list: Phaser.GameObjects.GameObject[] }; + const hudContainers = hud.list?.filter( + (child) => child instanceof Phaser.GameObjects.Container, + ) as Phaser.GameObjects.Container[]; + + const findButtonLabel = (container: Phaser.GameObjects.Container, label: string): boolean => { + return (container as any).list?.some( + (child: any) => child instanceof Phaser.GameObjects.Text && child.text === label, + ); + }; + + // Both game-over buttons should be in hudContainer so they render above the overlay box + const playAgainBtn = hudContainers.find((c) => findButtonLabel(c, 'Play Again')); + const menuBtn = hudContainers.find((c) => findButtonLabel(c, 'Menu')); + expect(playAgainBtn).toBeDefined(); + expect(menuBtn).toBeDefined(); + }); + it('displays correct final totals including pudding bonuses when provided', async () => { game = await bootGame(); const scene = game.scene.getScene('SushiGoScene') as any; From 724b5b5ed1496dfcad582c1109191a39c273e3cc Mon Sep 17 00:00:00 2001 From: Map Date: Mon, 8 Jun 2026 14:56:38 +0100 Subject: [PATCH 4/8] =?UTF-8?q?CG-0MQ54PYAJ001XCUG:=20Central=20fix=20?= =?UTF-8?q?=E2=80=94=20OverlayManager.add()=20auto-parents=20to=20hudConta?= =?UTF-8?q?iner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before: Every game using OverlayManager had to manually parent every text button to hudContainer (error-prone, verbose, inconsistent). After: OverlayManager.add() automatically parents all objects to hudContainer, ensuring correct z-ordering above the overlay background box for ALL games that use OverlayManager. Impact on all games: - Sushi Go: Removed 5 redundant manual hudContainer.add() calls (cleaner) - Main Street: Already fixed via manual calls; no change needed - Feudalism: Automatically fixed (game-over + card action menu) - Beleaguered Castle: Automatically fixed (win + no-moves overlays) - Golf: Automatically fixed (end screen); also fixed test helper - Lost Cities: Automatically fixed (round + match summaries) - The Mind: Automatically fixed (win/loss overlays) This is a single change in src/ui/OverlayManager.ts that fixes all 5 broken games (Feudalism, Beleaguered Castle, Golf, Lost Cities, The Mind) in one go, plus cleans up the Sushi Go fix. --- .../sushi-go/scenes/SushiGoOverlayContent.ts | 15 ----------- src/ui/OverlayManager.ts | 9 +++++++ tests/golf/GolfOverlay.browser.test.ts | 25 +++++++++++++------ 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/example-games/sushi-go/scenes/SushiGoOverlayContent.ts b/example-games/sushi-go/scenes/SushiGoOverlayContent.ts index e73e9757..da23a96c 100644 --- a/example-games/sushi-go/scenes/SushiGoOverlayContent.ts +++ b/example-games/sushi-go/scenes/SushiGoOverlayContent.ts @@ -93,9 +93,6 @@ export class SushiGoOverlayContent { }) .setOrigin(0.5, 0) .setDepth(11); - if ((this.scene as any).hudContainer) { - (this.scene as any).hudContainer.add(text); - } this.overlayManager.add(text); const btn = createActionButton( @@ -111,9 +108,6 @@ export class SushiGoOverlayContent { }, { depth: 11 }, ); - if ((this.scene as any).hudContainer) { - (this.scene as any).hudContainer.add(btn); - } this.overlayManager.add(btn); } @@ -210,9 +204,6 @@ export class SushiGoOverlayContent { }) .setOrigin(0.5, 0) .setDepth(11); - if ((this.scene as any).hudContainer) { - (this.scene as any).hudContainer.add(text); - } this.overlayManager.add(text); const playBtn = createActionButton( @@ -227,15 +218,9 @@ export class SushiGoOverlayContent { }, { depth: 11 }, ); - if ((this.scene as any).hudContainer) { - (this.scene as any).hudContainer.add(playBtn); - } this.overlayManager.add(playBtn); const menuBtn = createSushiGoMenuButton(this.scene, GAME_W / 2 + 20, buttonY - 16, 120, { depth: 11 }); - if ((this.scene as any).hudContainer) { - (this.scene as any).hudContainer.add(menuBtn); - } this.overlayManager.add(menuBtn); } diff --git a/src/ui/OverlayManager.ts b/src/ui/OverlayManager.ts index 0503506e..2c2773c0 100644 --- a/src/ui/OverlayManager.ts +++ b/src/ui/OverlayManager.ts @@ -60,6 +60,15 @@ export class OverlayManager { } add(...objects: Phaser.GameObjects.GameObject[]): void { + // Auto-parent all overlay content objects to hudContainer so they render + // above the overlay background box. This centralises z-ordering for all + // overlay content across every game that uses OverlayManager. + // createOverlayBackground() already parents the box/background itself; + // this handles all application-level content (text, buttons, etc.). + const hud = (this.scene as any).hudContainer as { add: (obj: Phaser.GameObjects.GameObject) => void } | undefined; + for (const obj of objects) { + hud?.add(obj); + } this._objects.push(...objects); } diff --git a/tests/golf/GolfOverlay.browser.test.ts b/tests/golf/GolfOverlay.browser.test.ts index 9dfa62ec..d96c630b 100644 --- a/tests/golf/GolfOverlay.browser.test.ts +++ b/tests/golf/GolfOverlay.browser.test.ts @@ -254,17 +254,26 @@ describe('Golf overlay button tests', () => { // Helper: find a container that contains a Text child with the given label // and return the interactive Rectangle (background) inside it. + // Search both scene children and HUD container (OverlayManager.add now + // parents content to hudContainer for correct z-ordering). const findButtonContainer = ( label: string, ): Phaser.GameObjects.Container | undefined => { - return scene.children.list.find( - (child: Phaser.GameObjects.GameObject) => - child instanceof Phaser.GameObjects.Container && - (child as Phaser.GameObjects.Container).list.some( - (c: Phaser.GameObjects.GameObject) => - c instanceof Phaser.GameObjects.Text && c.text === label, - ), - ) as Phaser.GameObjects.Container | undefined; + const findIn = (items: Phaser.GameObjects.GameObject[]) => { + return items.find( + (child: Phaser.GameObjects.GameObject) => + child instanceof Phaser.GameObjects.Container && + (child as Phaser.GameObjects.Container).list.some( + (c: Phaser.GameObjects.GameObject) => + c instanceof Phaser.GameObjects.Text && c.text === label, + ), + ) as Phaser.GameObjects.Container | undefined; + }; + let result = findIn(scene.children.list); + if (result) return result; + const hud = (scene as any).hudContainer as { list: Phaser.GameObjects.GameObject[] } | undefined; + if (hud && hud.list) result = findIn(hud.list); + return result; }; // Find the "Play Again" button container. From 06675e5324add0734c843906d4e6b823c58a495d Mon Sep 17 00:00:00 2001 From: Map Date: Mon, 8 Jun 2026 15:43:02 +0100 Subject: [PATCH 5/8] CG-0MQ54PYAJ001XCUG: Fix Overlay.ts not parenting box to hudContainer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit createOverlayBackground() was unconditionally parenting the overlay box/background to hudContainer (depth 1000). This broke custom overlays like Golf's end screen (depth 10) — the box at depth 10 inside hudContainer (1000) would cover HUD-level game elements like 'Stock' (also at depth 1000). Fix: Remove hudContainer parenting from createOverlayBackground(). Overlay content (text, buttons) is still correctly parented by OverlayManager.add(). The overlay box/background stays in the normal scene hierarchy at its specified depth so custom overlays render below the HUD layer. Also updated the Overlay unit test to reflect new behavior. --- src/ui/Overlay.ts | 17 ++++++----------- tests/ui/Overlay.test.ts | 14 ++++++++++---- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/ui/Overlay.ts b/src/ui/Overlay.ts index c792420e..c5c807fa 100644 --- a/src/ui/Overlay.ts +++ b/src/ui/Overlay.ts @@ -107,17 +107,12 @@ export function createOverlayBackground( objects.push(overlayBox); } - // If the scene exposes a top-level HUD container, parent overlay objects - // into it so all overlays share a single, stable top-layer container. - // This keeps z-ordering consistent across Main Street overlays. - try { - const overlayContainer: any = (scene as any).hudContainer; - if (overlayContainer && typeof overlayContainer.add === 'function') { - for (const obj of objects) { - try { overlayContainer.add(obj); } catch (_) { /* ignore */ } - } - } - } catch (_) { /* ignore failures when inspecting scene */ } + // Do NOT parent overlay box/background to hudContainer. + // The box/background should stay in the normal scene hierarchy at their + // specified depths so they can block input to game elements beneath them + // without visually overlapping HUD-level content (e.g. "Stock" label at + // depth 1000). Overlay content (text, buttons) is parented to hudContainer + // by OverlayManager.add() so they render above the box. return { background, box: overlayBox, objects }; } diff --git a/tests/ui/Overlay.test.ts b/tests/ui/Overlay.test.ts index ab393549..81fe8331 100644 --- a/tests/ui/Overlay.test.ts +++ b/tests/ui/Overlay.test.ts @@ -120,15 +120,21 @@ describe('createOverlayBackground', () => { expect(result.objects).toHaveLength(2); }); - it('parents overlay objects into scene.hudContainer when present', () => { - // Provide a scene with a hudContainer that has an `add` spy + it('does NOT parent overlay box/background to hudContainer', () => { + // Provide a scene with a hudContainer that has an `add` spy. + // Overlay box/background must stay in the normal scene hierarchy so + // custom overlays at depth 10 don't visually overlap HUD content at + // depth 1000. Content (text, buttons) is parented by OverlayManager.add(). const hudScene: any = mockScene(); hudScene.hudContainer = { add: vi.fn() }; const res = createOverlayBackground(hudScene); - // hudContainer.add should have been called for the background (and box if present) - expect(hudScene.hudContainer.add).toHaveBeenCalledWith(res.background); + // hudContainer.add should NOT be called for the box/background + expect(hudScene.hudContainer.add).not.toHaveBeenCalled(); + // Objects should still be returned for cleanup + expect(res.objects).toHaveLength(1); + expect(res.background).toBeDefined(); }); it('box uses custom depth when specified', () => { From 1cf30046c56b7f587c788a95d2ed8da0679f86a1 Mon Sep 17 00:00:00 2001 From: Map Date: Mon, 8 Jun 2026 19:25:47 +0100 Subject: [PATCH 6/8] CG-0MQ54PYAJ001XCUG: Fix Golf 'Stock' label visible above overlay Parent HUD text elements (like 'Stock', 'Discard') into hudContainer so they share the same depth-sort space as the overlay box/background. Previously, the overlay box was in hudContainer (depth 1000) but 'Stock' text was in the normal scene hierarchy at depth 1000, causing 'Stock' to render above the overlay box. By putting both in hudContainer, the overlay box at depth 10 covers the HUD text at depth 1000. Fix in src/ui/Renderer/adapters/GolfAdapter.ts: createGolfHudText() now parents text into hudContainer when available. --- src/ui/Overlay.ts | 19 +++++++++++++------ src/ui/Renderer/adapters/GolfAdapter.ts | 8 ++++++++ tests/ui/Overlay.test.ts | 16 +++++++--------- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/ui/Overlay.ts b/src/ui/Overlay.ts index c5c807fa..c67370d8 100644 --- a/src/ui/Overlay.ts +++ b/src/ui/Overlay.ts @@ -107,12 +107,19 @@ export function createOverlayBackground( objects.push(overlayBox); } - // Do NOT parent overlay box/background to hudContainer. - // The box/background should stay in the normal scene hierarchy at their - // specified depths so they can block input to game elements beneath them - // without visually overlapping HUD-level content (e.g. "Stock" label at - // depth 1000). Overlay content (text, buttons) is parented to hudContainer - // by OverlayManager.add() so they render above the box. + // Parent overlay box/background into hudContainer so all overlay content + // (box + text + buttons) shares the same depth-sort space. HUD-level + // game elements (e.g. "Stock" label) must also be parented into + // hudContainer so overlays can correctly cover them. This keeps z- + // ordering consistent and predictable across all games. + try { + const overlayContainer: any = (scene as any).hudContainer; + if (overlayContainer && typeof overlayContainer.add === 'function') { + for (const obj of objects) { + try { overlayContainer.add(obj); } catch (_) { /* ignore */ } + } + } + } catch (_) { /* ignore failures when inspecting scene */ } return { background, box: overlayBox, objects }; } diff --git a/src/ui/Renderer/adapters/GolfAdapter.ts b/src/ui/Renderer/adapters/GolfAdapter.ts index 7da79b66..b06e2676 100644 --- a/src/ui/Renderer/adapters/GolfAdapter.ts +++ b/src/ui/Renderer/adapters/GolfAdapter.ts @@ -72,7 +72,15 @@ export function createGolfHudText( fontFamily: FONT_FAMILY, ...options, }); + // Parent into hudContainer so it shares the same depth sort space as + // overlay content (game-over text, buttons). This ensures HUD labels + // like "Stock" are correctly covered by overlays that use + // createOverlayBackground + OverlayManager.add(). try { + const hud = (scene as any).hudContainer; + if (hud && typeof hud.add === 'function') { + hud.add(textObj); + } textObj.setDepth(GOLF_DEPTH_HUD); } catch { // Depth may not be available in headless / test environments. diff --git a/tests/ui/Overlay.test.ts b/tests/ui/Overlay.test.ts index 81fe8331..a09d41cb 100644 --- a/tests/ui/Overlay.test.ts +++ b/tests/ui/Overlay.test.ts @@ -120,21 +120,19 @@ describe('createOverlayBackground', () => { expect(result.objects).toHaveLength(2); }); - it('does NOT parent overlay box/background to hudContainer', () => { + it('parents overlay objects into scene.hudContainer when present', () => { // Provide a scene with a hudContainer that has an `add` spy. - // Overlay box/background must stay in the normal scene hierarchy so - // custom overlays at depth 10 don't visually overlap HUD content at - // depth 1000. Content (text, buttons) is parented by OverlayManager.add(). + // Overlay box/background are parented into hudContainer so all overlay + // content (box + text + buttons) shares the same depth-sort space. HUD- + // level game elements (e.g. "Stock" label) must also be parented into + // hudContainer so overlays can correctly cover them. const hudScene: any = mockScene(); hudScene.hudContainer = { add: vi.fn() }; const res = createOverlayBackground(hudScene); - // hudContainer.add should NOT be called for the box/background - expect(hudScene.hudContainer.add).not.toHaveBeenCalled(); - // Objects should still be returned for cleanup - expect(res.objects).toHaveLength(1); - expect(res.background).toBeDefined(); + // hudContainer.add should have been called for the background + expect(hudScene.hudContainer.add).toHaveBeenCalledWith(res.background); }); it('box uses custom depth when specified', () => { From f8e1060fa5eb6afbb43a378b16ded21b7cfcf97f Mon Sep 17 00:00:00 2001 From: Map Date: Mon, 8 Jun 2026 19:35:52 +0100 Subject: [PATCH 7/8] CG-0MQ54PYAJ001XCUG: Fix HUD text parenting in BC and Lost Cities adapters Parent HUD text into hudContainer so they share the same depth-sort space as overlay content. This ensures overlays correctly cover HUD labels (like 'Stock' in Golf, which was the original issue). - src/ui/Renderer/adapters/BeleagueredCastleAdapter.ts: createBcHudText() now parents text into hudContainer when available - src/ui/Renderer/adapters/LostCitiesAdapter.ts: createLcHudText() now parents text into hudContainer when available --- src/ui/Renderer/adapters/BeleagueredCastleAdapter.ts | 8 ++++++++ src/ui/Renderer/adapters/LostCitiesAdapter.ts | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/src/ui/Renderer/adapters/BeleagueredCastleAdapter.ts b/src/ui/Renderer/adapters/BeleagueredCastleAdapter.ts index 9a148a03..2c2b9d43 100644 --- a/src/ui/Renderer/adapters/BeleagueredCastleAdapter.ts +++ b/src/ui/Renderer/adapters/BeleagueredCastleAdapter.ts @@ -51,7 +51,15 @@ export function createBcHudText( fontFamily: FONT_FAMILY, ...options, }); + // Parent into hudContainer so it shares the same depth-sort space as + // overlay content (game-over text, buttons, overlay box). This ensures + // HUD labels are correctly covered by overlays that use + // createOverlayBackground + OverlayManager.add(). try { + const hud = (scene as any).hudContainer; + if (hud && typeof hud.add === 'function') { + hud.add(textObj); + } textObj.setDepth(BC_DEPTH_HUD); } catch { // Depth may not be available in headless / test environments. diff --git a/src/ui/Renderer/adapters/LostCitiesAdapter.ts b/src/ui/Renderer/adapters/LostCitiesAdapter.ts index 89801f2d..c286bbcf 100644 --- a/src/ui/Renderer/adapters/LostCitiesAdapter.ts +++ b/src/ui/Renderer/adapters/LostCitiesAdapter.ts @@ -66,7 +66,15 @@ export function createLcHudText( fontFamily: FONT_FAMILY, ...options, }); + // Parent into hudContainer so it shares the same depth-sort space as + // overlay content (game-over text, buttons, overlay box). This ensures + // HUD labels are correctly covered by overlays that use + // createOverlayBackground + OverlayManager.add(). try { + const hud = (scene as any).hudContainer; + if (hud && typeof hud.add === 'function') { + hud.add(textObj); + } textObj.setDepth(LC_DEPTH_HUD); } catch { // Depth may not be available in headless / test environments. From f85e0bb50955b4ed99de9cf503c68f09b750c2fd Mon Sep 17 00:00:00 2001 From: Map Date: Mon, 8 Jun 2026 21:13:38 +0100 Subject: [PATCH 8/8] CG-0MQ2EGJZG009J1DY: Fix Golf browser tests for HUD container refactoring - Update waitForScene helper to wait extra frames so create() completes before tests query scene objects - Fix GolfScene.browser.test.ts to search hudContainer.getAll() for HUD text elements (Phaser 4 Container API uses .getAll() or .list, not .children.list) - Fix GolfInteraction.browser.test.ts same pattern for score text lookup Root cause: After the HUD container refactoring, text elements are parented into hudContainer. Tests were still searching only in scene.children.list. --- tests/golf/GolfInteraction.browser.test.ts | 16 +++++++++++++--- tests/golf/GolfScene.browser.test.ts | 17 +++++++++++++++-- tests/helpers/waitForScene.ts | 13 ++++++++++++- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/tests/golf/GolfInteraction.browser.test.ts b/tests/golf/GolfInteraction.browser.test.ts index 83ba2df7..fed97297 100644 --- a/tests/golf/GolfInteraction.browser.test.ts +++ b/tests/golf/GolfInteraction.browser.test.ts @@ -346,11 +346,21 @@ describe('GolfScene interaction tests', () => { const scene = game.scene.getScene('GolfScene')!; const internals = getSceneInternals(scene); - // Verify initial score format - const texts = scene.children.list.filter( + // Verify initial score format (HUD text is parented into hudContainer) + const sceneTexts = scene.children.list.filter( (child) => child instanceof Phaser.GameObjects.Text, ) as Phaser.GameObjects.Text[]; - const scoreTexts = texts.filter((t) => t.text.startsWith('Score:')); + const hudContainer = (scene as any).hudContainer as { + getAll?: () => Phaser.GameObjects.GameObject[]; + list?: Phaser.GameObjects.GameObject[]; + }; + // Phaser 4 Container exposes children via .getAll() or .list (not .children.list) + const hudAllObjects = hudContainer?.getAll?.() ?? hudContainer?.list ?? []; + const hudTexts = hudAllObjects.filter( + (child) => child instanceof Phaser.GameObjects.Text, + ) as Phaser.GameObjects.Text[]; + const allTexts = [...sceneTexts, ...(hudTexts as Phaser.GameObjects.Text[])]; + const scoreTexts = allTexts.filter((t) => t.text.startsWith('Score:')); expect(scoreTexts.length).toBe(2); for (const st of scoreTexts) { expect(st.text).toMatch(/^Score: -?\d+$/); diff --git a/tests/golf/GolfScene.browser.test.ts b/tests/golf/GolfScene.browser.test.ts index d0bff396..ff157496 100644 --- a/tests/golf/GolfScene.browser.test.ts +++ b/tests/golf/GolfScene.browser.test.ts @@ -91,11 +91,24 @@ describe('GolfScene browser tests', () => { const scene = game.scene.getScene('GolfScene') as Phaser.Scene; - // Collect all text game objects - const texts = scene.children.list.filter( + // Collect all text game objects from scene children and hudContainer + // (HUD text is now parented into hudContainer after the container-refactoring) + const sceneTexts = scene.children.list.filter( (child) => child instanceof Phaser.GameObjects.Text, ) as Phaser.GameObjects.Text[]; + const hudContainer = (scene as any).hudContainer as { + getAll?: () => Phaser.GameObjects.GameObject[]; + list?: Phaser.GameObjects.GameObject[]; + }; + // Phaser 4 Container exposes children via .getAll() or .list (not .children.list) + const hudAllObjects = hudContainer?.getAll?.() ?? hudContainer?.list ?? []; + const hudTexts = hudAllObjects.filter( + (child) => child instanceof Phaser.GameObjects.Text, + ) as Phaser.GameObjects.Text[]; + + const texts = [...sceneTexts, ...(hudTexts as Phaser.GameObjects.Text[])]; + // Extract text content const textContents = texts.map((t) => t.text); diff --git a/tests/helpers/waitForScene.ts b/tests/helpers/waitForScene.ts index 93b42b91..77a79b70 100644 --- a/tests/helpers/waitForScene.ts +++ b/tests/helpers/waitForScene.ts @@ -22,7 +22,18 @@ export function waitForScene( scene && (scene as Phaser.Scene & { sys: Phaser.Scenes.Systems }).sys.isActive() ) { - resolve(); + // The scene is active, but create() may still be executing. + // Wait for the next animation frame to ensure create() has completed + // (Phaser marks a scene as active during the create() phase). + requestAnimationFrame(() => { + // Also wait a couple more frames for deferred initializations + // (e.g., initHUDContainer, renderer setup) + requestAnimationFrame(() => { + requestAnimationFrame(() => { + resolve(); + }); + }); + }); return; } if (Date.now() - start > timeoutMs) {