diff --git a/src/ui/Overlay.ts b/src/ui/Overlay.ts index c792420e..c67370d8 100644 --- a/src/ui/Overlay.ts +++ b/src/ui/Overlay.ts @@ -107,9 +107,11 @@ 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. + // 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') { 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/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/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/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. 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..d96c630b 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 ]'); @@ -223,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. @@ -280,11 +320,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..8a428960 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,13 +190,179 @@ 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); }); + 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 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; + + // 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('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; @@ -202,9 +391,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(); }); diff --git a/tests/ui/Overlay.test.ts b/tests/ui/Overlay.test.ts index ab393549..a09d41cb 100644 --- a/tests/ui/Overlay.test.ts +++ b/tests/ui/Overlay.test.ts @@ -121,13 +121,17 @@ describe('createOverlayBackground', () => { }); it('parents overlay objects into scene.hudContainer when present', () => { - // Provide a scene with a hudContainer that has an `add` spy + // Provide a scene with a hudContainer that has an `add` spy. + // 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 have been called for the background (and box if present) + // hudContainer.add should have been called for the background expect(hudScene.hudContainer.add).toHaveBeenCalledWith(res.background); });