Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
16 changes: 13 additions & 3 deletions tests/golf/GolfInteraction.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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+$/);
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
17 changes: 15 additions & 2 deletions tests/golf/GolfScene.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
13 changes: 12 additions & 1 deletion tests/helpers/waitForScene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading
Loading