Skip to content
8 changes: 5 additions & 3 deletions src/ui/Overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
9 changes: 9 additions & 0 deletions src/ui/OverlayManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
8 changes: 8 additions & 0 deletions src/ui/Renderer/adapters/BeleagueredCastleAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions src/ui/Renderer/adapters/GolfAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions src/ui/Renderer/adapters/LostCitiesAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
71 changes: 48 additions & 23 deletions tests/beleaguered-castle/BeleagueredCastleOverlay.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends Phaser.GameObjects.GameObject>(
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;

Expand Down Expand Up @@ -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);
});

Expand All @@ -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);
});
Expand Down
84 changes: 62 additions & 22 deletions tests/golf/GolfOverlay.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends Phaser.GameObjects.GameObject>(
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
Expand Down Expand Up @@ -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 ]');
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading