Fix menu sub-bar, save panel, and confirm dialog input and z-order bugs
- Replace the no-op `ignorePointer` idiom with a new `setInteractiveEnabled` helper that toggles Phaser v4's `input.enabled`, so hidden/inert UI elements (sub-bar buttons, save cards, scrims, confirm buttons) stop swallowing clicks - Set explicit depths for the menu sub-bar (60) and save panel (70) so they paint above the command deck, HUD, and world objects as intended - Add a headless-browser z-order + input-state probe (`dev/zorder-test.*`) that boots the real engine and asserts both paint order and hit-testing behavior across open/close transitions - Add gas giant landing video asset
This commit is contained in:
parent
834be56557
commit
bf43e12a1c
Binary file not shown.
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Orbit — dev z-order + input-state test</title>
|
||||||
|
<!-- This page lives in /dev, but the game's relative asset paths are
|
||||||
|
rooted at the project root — resolve them against it. -->
|
||||||
|
<base href="../" />
|
||||||
|
<style>
|
||||||
|
html, body { margin: 0; height: 100%; background: #04060d; overflow: hidden; }
|
||||||
|
#game { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
window.__CAPTURED_ERRORS__ = [];
|
||||||
|
const __oe = console.error.bind(console);
|
||||||
|
console.error = (...a) => { window.__CAPTURED_ERRORS__.push(a.map(String).join(' ').slice(0, 600)); __oe(...a); };
|
||||||
|
window.addEventListener('error', (e) => window.__CAPTURED_ERRORS__.push('window: ' + e.message + ' @ ' + (e.filename||'') + ':' + (e.lineno||'')));
|
||||||
|
window.addEventListener('unhandledrejection', (e) => window.__CAPTURED_ERRORS__.push('rejection: ' + String(e.reason && e.reason.stack || e.reason)));
|
||||||
|
</script>
|
||||||
|
<script src="lib/phaser.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="game"></div>
|
||||||
|
<script type="module" src="dev/zorder-test.mjs"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
/**
|
||||||
|
* Z-order + input-state probe (headless browser — NOT a Node test).
|
||||||
|
*
|
||||||
|
* Verifies the two menu fixes against the real engine:
|
||||||
|
*
|
||||||
|
* Z-ORDER (paint order — the scene display list is depth-sorted
|
||||||
|
* ascending, and containers expand their children inline at their
|
||||||
|
* slot, so list order IS back-to-front paint order):
|
||||||
|
* stars(0-2) < planet(5) < ship(10) < HUD(30) < compass(40)
|
||||||
|
* < toast(45) < command deck(50) < menu sub-bar(60)
|
||||||
|
* < save pop-up(70)
|
||||||
|
* ... and the confirm dialog is the pop-up's last content child (only
|
||||||
|
* the toast trails it, and it sits outside the dialog's area), so it
|
||||||
|
* paints on top of the pop-up.
|
||||||
|
*
|
||||||
|
* INPUT STATE (v4 gates hit-testing on `input.enabled`):
|
||||||
|
* sub-bar buttons inert while closed, live while open
|
||||||
|
* pop-up scrim / cards / footer inert while hidden, live while shown
|
||||||
|
* confirm scrim + buttons inert while the dialog is hidden, live
|
||||||
|
* while it is up
|
||||||
|
*
|
||||||
|
* Served by dev/zorder-test.html; results land in `window.__ZORDER__`:
|
||||||
|
*
|
||||||
|
* python3 -m http.server 8091
|
||||||
|
* node dev/cdp-firefox.mjs http://127.0.0.1:8091/dev/zorder-test.html
|
||||||
|
*/
|
||||||
|
import Phaser from '../js/vendor/phaser.js';
|
||||||
|
import { config } from '../js/config/Config.js';
|
||||||
|
import { ConfigLoader } from '../js/config/ConfigLoader.js';
|
||||||
|
import { createGameConfig } from '../js/config/GameConfig.js';
|
||||||
|
import { GameScene } from '../js/scenes/GameScene.js';
|
||||||
|
|
||||||
|
const data = await ConfigLoader.load();
|
||||||
|
config.init(data);
|
||||||
|
|
||||||
|
globalThis.__ORBIT_DEV_SEED = 'ZORDER';
|
||||||
|
const gameConfig = createGameConfig();
|
||||||
|
gameConfig.scene = [GameScene];
|
||||||
|
if (typeof Phaser !== 'undefined') Phaser.NoAudioContext = true;
|
||||||
|
|
||||||
|
const game = new Phaser.Game(gameConfig);
|
||||||
|
window.game = game;
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
const check = (label, cond) => {
|
||||||
|
const pass = !!cond;
|
||||||
|
results.push({ label, pass });
|
||||||
|
console.log(`${pass ? '✔' : '✘ FAIL'} ${label}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Throttling-proof wait on the game's own clock (see clickout-test).
|
||||||
|
const gameClock = () => {
|
||||||
|
try {
|
||||||
|
const s = window.game.scene.getScenes(true)[0];
|
||||||
|
if (s && typeof s.time.now === 'number') return s.time.now;
|
||||||
|
} catch {}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
const wait = (ms) => new Promise((resolve) => {
|
||||||
|
const base = gameClock();
|
||||||
|
if (base === null) {
|
||||||
|
setTimeout(() => resolve(), ms);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const poll = () => {
|
||||||
|
const now = gameClock();
|
||||||
|
if (now !== null && now - base >= ms) return resolve();
|
||||||
|
requestAnimationFrame(poll);
|
||||||
|
};
|
||||||
|
requestAnimationFrame(poll);
|
||||||
|
});
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
await wait(900); // boot
|
||||||
|
const scene = game.scene.getScene('GameScene');
|
||||||
|
check('boots into GameScene', game.scene.isActive('GameScene'));
|
||||||
|
|
||||||
|
// ---- declared depths ---------------------------------------------------
|
||||||
|
check('planets sit at depth 5', scene.planet.depth === 5);
|
||||||
|
check('the ship sits at depth 10', scene.ship.depth === 10);
|
||||||
|
check('the command deck sits at depth 50', scene.actionBar.depth === 50);
|
||||||
|
check('the menu sub-bar sits at depth 60', scene.menuSubBar.depth === 60);
|
||||||
|
check('the save pop-up sits at depth 70', scene.savePanel.depth === 70);
|
||||||
|
|
||||||
|
// ---- paint order: the scene display list -------------------------------
|
||||||
|
const dl = scene.sys.displayList.getChildren();
|
||||||
|
const idx = (o) => dl.indexOf(o);
|
||||||
|
const iPlanet = idx(scene.planet);
|
||||||
|
const iShip = idx(scene.ship);
|
||||||
|
const iCompass = idx(scene.compass);
|
||||||
|
const iDeck = idx(scene.actionBar);
|
||||||
|
const iSubBar = idx(scene.menuSubBar);
|
||||||
|
const iPanel = idx(scene.savePanel);
|
||||||
|
const name = (o) => o === scene.planet ? 'planet' : o === scene.ship ? 'ship'
|
||||||
|
: o === scene.compass ? 'compass' : o === scene.actionBar ? 'actionBar'
|
||||||
|
: o === scene.menuSubBar ? 'menuSubBar' : o === scene.savePanel ? 'savePanel'
|
||||||
|
: (o.type || 'obj') + '@' + (o._depth !== undefined ? o._depth : o.depth);
|
||||||
|
console.log('displayList tail:', dl.slice(-8).map(name).join(' < '));
|
||||||
|
check('every probed object is in the display list',
|
||||||
|
[iPlanet, iShip, iCompass, iDeck, iSubBar, iPanel].every((i) => i >= 0));
|
||||||
|
results.push({ label: `DL planet=${iPlanet} ship=${iShip} compass=${iCompass} actionBar=${iDeck} menuSubBar=${iSubBar} savePanel=${iPanel} len=${dl.length}`, pass: true });
|
||||||
|
check('the display list is depth-sorted (ascending)',
|
||||||
|
dl.slice(-20).every((o, i, a) => i === 0 || o._depth >= a[i - 1]._depth));
|
||||||
|
check('world (planet, ship) paints UNDER the command deck', iPlanet < iDeck && iShip < iDeck);
|
||||||
|
check('the command deck paints UNDER the sub-bar', iDeck < iSubBar);
|
||||||
|
check('the sub-bar paints UNDER the save pop-up', iSubBar < iPanel);
|
||||||
|
const kids = (() => {
|
||||||
|
const p = scene.savePanel;
|
||||||
|
if (p.list && Array.isArray(p.list)) return p.list; // v4 Container children live in `list`
|
||||||
|
const c = p.children;
|
||||||
|
if (!c) return null;
|
||||||
|
if (Array.isArray(c)) return c;
|
||||||
|
if (c.list && Array.isArray(c.list)) return c.list;
|
||||||
|
if (typeof c.getChildren === 'function') return c.getChildren();
|
||||||
|
return null;
|
||||||
|
})();
|
||||||
|
check('the confirm dialog paints above the panel contents (only the toast trails it)', (() => {
|
||||||
|
if (!Array.isArray(kids) || kids.length === 0) return false;
|
||||||
|
const ci = kids.indexOf(scene.savePanel.confirm);
|
||||||
|
return ci >= 0 && kids.slice(ci + 1).every((c) => c === scene.savePanel.toast);
|
||||||
|
})());
|
||||||
|
|
||||||
|
// ---- input states (v4: input.enabled) ----------------------------------
|
||||||
|
const enabled = (o) => !!(o.input && o.input.enabled);
|
||||||
|
check('sub-bar buttons are input-INERT while the bar is closed',
|
||||||
|
scene.menuSubBar.buttons.every((s) => !enabled(s.btn.panel)));
|
||||||
|
check('pop-up scrim/cards/footer are input-INERT while hidden',
|
||||||
|
!enabled(scene.savePanel.scrim)
|
||||||
|
&& scene.savePanel.cards.every((c) => !enabled(c.panel))
|
||||||
|
&& !enabled(scene.savePanel.cancelBtn.panel) && !enabled(scene.savePanel.downloadBtn.panel));
|
||||||
|
check('confirm scrim + buttons are input-INERT while the dialog is hidden',
|
||||||
|
!enabled(scene.savePanel.confirm.scrim)
|
||||||
|
&& !enabled(scene.savePanel.confirm.confirmBtn.panel)
|
||||||
|
&& !enabled(scene.savePanel.confirm.cancelBtn.panel));
|
||||||
|
|
||||||
|
scene.menuAction(); // open the sub-bar
|
||||||
|
await wait(450);
|
||||||
|
check('sub-bar buttons are input-LIVE while the bar is open',
|
||||||
|
scene.menuSubBar.isOpen && scene.menuSubBar.buttons.every((s) => enabled(s.btn.panel)));
|
||||||
|
|
||||||
|
scene.subBarAction('save'); // open the pop-up (save mode)
|
||||||
|
await wait(450);
|
||||||
|
check('pop-up scrim + cards + footer are input-LIVE while shown',
|
||||||
|
scene.savePanel.isOpen
|
||||||
|
&& enabled(scene.savePanel.scrim)
|
||||||
|
&& scene.savePanel.cards.every((c) => enabled(c.panel))
|
||||||
|
&& enabled(scene.savePanel.cancelBtn.panel) && enabled(scene.savePanel.downloadBtn.panel));
|
||||||
|
|
||||||
|
scene.savePanel.confirmOverwrite(1, { galaxyName: 'X', savedAt: new Date().toISOString() });
|
||||||
|
await wait(300);
|
||||||
|
check('confirm scrim + buttons are input-LIVE while the dialog is up',
|
||||||
|
scene.savePanel.confirm.isOpen
|
||||||
|
&& enabled(scene.savePanel.confirm.scrim)
|
||||||
|
&& enabled(scene.savePanel.confirm.confirmBtn.panel)
|
||||||
|
&& enabled(scene.savePanel.confirm.cancelBtn.panel));
|
||||||
|
scene.savePanel.confirm.cancel();
|
||||||
|
await wait(350);
|
||||||
|
check('confirm goes inert again after CANCEL',
|
||||||
|
scene.savePanel.confirm.isOpen === false
|
||||||
|
&& !enabled(scene.savePanel.confirm.scrim)
|
||||||
|
&& !enabled(scene.savePanel.confirm.confirmBtn.panel));
|
||||||
|
|
||||||
|
scene.savePanel.close();
|
||||||
|
await wait(400);
|
||||||
|
scene.menuSubBar.close();
|
||||||
|
await wait(400);
|
||||||
|
check('sub-bar goes inert again after close',
|
||||||
|
scene.menuSubBar.state === 'closed'
|
||||||
|
&& scene.menuSubBar.buttons.every((s) => !enabled(s.btn.panel)));
|
||||||
|
check('pop-up goes inert again after close',
|
||||||
|
scene.savePanel.isOpen === false && !enabled(scene.savePanel.scrim)
|
||||||
|
&& scene.savePanel.cards.every((c) => !enabled(c.panel)));
|
||||||
|
};
|
||||||
|
|
||||||
|
let done = false;
|
||||||
|
game.events.once('ready', async () => {
|
||||||
|
try {
|
||||||
|
await run();
|
||||||
|
} catch (err) {
|
||||||
|
results.push({ label: `THREW: ${String(err && err.message || err)}`, pass: false });
|
||||||
|
}
|
||||||
|
const pass = results.length > 0 && results.every((r) => r.pass);
|
||||||
|
window.__ZORDER__ = { pass, results, errors: window.__CAPTURED_ERRORS__ || [] };
|
||||||
|
done = true;
|
||||||
|
console.log(pass ? 'ZORDER PASS' : 'ZORDER FAIL');
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!done) window.__ZORDER__ = { pass: false, results: [...results, { label: 'TIMED OUT (300s)', pass: false }] };
|
||||||
|
}, 300000);
|
||||||
|
|
@ -4,6 +4,7 @@ import { toColor, toCss } from '../utils/Color.js';
|
||||||
import { fontStack } from '../utils/Theme.js';
|
import { fontStack } from '../utils/Theme.js';
|
||||||
import { CyberShape } from './CyberShape.js';
|
import { CyberShape } from './CyberShape.js';
|
||||||
import { MenuButton } from './MenuButton.js';
|
import { MenuButton } from './MenuButton.js';
|
||||||
|
import { setInteractiveEnabled } from '../utils/Input.js';
|
||||||
import { ScrambleDecode } from '../utils/Decode.js';
|
import { ScrambleDecode } from '../utils/Decode.js';
|
||||||
|
|
||||||
const FONT_FALLBACK = "'Segoe UI', 'Helvetica Neue', Arial, sans-serif";
|
const FONT_FALLBACK = "'Segoe UI', 'Helvetica Neue', Arial, sans-serif";
|
||||||
|
|
@ -59,7 +60,10 @@ export class ConfirmOverlay extends Phaser.GameObjects.Container {
|
||||||
hitAreaCallback: () => true,
|
hitAreaCallback: () => true,
|
||||||
});
|
});
|
||||||
this.scrim.on('pointerdown', () => this.cancel());
|
this.scrim.on('pointerdown', () => this.cancel());
|
||||||
this.scrim.ignorePointer = true; // armed while shown only
|
// Armed while shown ONLY (v4: `input.enabled` — the `ignorePointer`
|
||||||
|
// idiom is a physics-world property and a no-op, which is why this
|
||||||
|
// invisible scrim used to swallow every click beneath it).
|
||||||
|
setInteractiveEnabled(this.scrim, false);
|
||||||
this.add(this.scrim);
|
this.add(this.scrim);
|
||||||
|
|
||||||
this.panelG = scene.add.graphics().setScrollFactor(0);
|
this.panelG = scene.add.graphics().setScrollFactor(0);
|
||||||
|
|
@ -117,6 +121,11 @@ export class ConfirmOverlay extends Phaser.GameObjects.Container {
|
||||||
this.confirmBtn.setAlpha(0);
|
this.confirmBtn.setAlpha(0);
|
||||||
this.cancelBtn.setAlpha(0);
|
this.cancelBtn.setAlpha(0);
|
||||||
this.add([this.cancelBtn, this.confirmBtn]);
|
this.add([this.cancelBtn, this.confirmBtn]);
|
||||||
|
// Hidden dialog = inert buttons (same v4 `input.enabled` rule as the
|
||||||
|
// scrim — otherwise the invisible CONFIRM/CANCEL swallow centre-
|
||||||
|
// screen clicks while the dialog is closed).
|
||||||
|
setInteractiveEnabled(this.confirmBtn.panel, false);
|
||||||
|
setInteractiveEnabled(this.cancelBtn.panel, false);
|
||||||
|
|
||||||
this._titleDec = null;
|
this._titleDec = null;
|
||||||
this._btnsUp = false;
|
this._btnsUp = false;
|
||||||
|
|
@ -165,9 +174,9 @@ export class ConfirmOverlay extends Phaser.GameObjects.Container {
|
||||||
this.cancelBtn.setAlpha(0);
|
this.cancelBtn.setAlpha(0);
|
||||||
this._btnsUp = false;
|
this._btnsUp = false;
|
||||||
this._confirmUp = false;
|
this._confirmUp = false;
|
||||||
this.scrim.ignorePointer = false;
|
setInteractiveEnabled(this.scrim, true);
|
||||||
this.confirmBtn.panel.ignorePointer = false;
|
setInteractiveEnabled(this.confirmBtn.panel, true);
|
||||||
this.cancelBtn.panel.ignorePointer = false;
|
setInteractiveEnabled(this.cancelBtn.panel, true);
|
||||||
|
|
||||||
const cfg = config.section('save.confirm.animation', {});
|
const cfg = config.section('save.confirm.animation', {});
|
||||||
this.openDur = cfg.openMs ?? 170;
|
this.openDur = cfg.openMs ?? 170;
|
||||||
|
|
@ -282,9 +291,9 @@ export class ConfirmOverlay extends Phaser.GameObjects.Container {
|
||||||
this.title.setText('');
|
this.title.setText('');
|
||||||
this.bodyTexts.forEach((t) => t.setText(''));
|
this.bodyTexts.forEach((t) => t.setText(''));
|
||||||
this.state = 'hidden';
|
this.state = 'hidden';
|
||||||
this.scrim.ignorePointer = true;
|
setInteractiveEnabled(this.scrim, false);
|
||||||
this.confirmBtn.panel.ignorePointer = true;
|
setInteractiveEnabled(this.confirmBtn.panel, false);
|
||||||
this.cancelBtn.panel.ignorePointer = true;
|
setInteractiveEnabled(this.cancelBtn.panel, false);
|
||||||
const fired = this._fired;
|
const fired = this._fired;
|
||||||
this._fired = null;
|
this._fired = null;
|
||||||
const pending = this._pending;
|
const pending = this._pending;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { config } from '../config/Config.js';
|
||||||
import { toColor, toCss } from '../utils/Color.js';
|
import { toColor, toCss } from '../utils/Color.js';
|
||||||
import { canvasTexture } from '../utils/Textures.js';
|
import { canvasTexture } from '../utils/Textures.js';
|
||||||
import { MenuButton } from './MenuButton.js';
|
import { MenuButton } from './MenuButton.js';
|
||||||
|
import { setInteractiveEnabled } from '../utils/Input.js';
|
||||||
import { ScrambleDecode, decodeDur } from '../utils/Decode.js';
|
import { ScrambleDecode, decodeDur } from '../utils/Decode.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -55,6 +56,10 @@ export class MenuSubBar extends Phaser.GameObjects.Container {
|
||||||
super(scene, 0, 0);
|
super(scene, 0, 0);
|
||||||
this.scene.add.existing(this);
|
this.scene.add.existing(this);
|
||||||
this.setScrollFactor(0); // screen-fixed UI
|
this.setScrollFactor(0); // screen-fixed UI
|
||||||
|
// The drawer belongs to the deck but RISES above it — the planets (5),
|
||||||
|
// HUD (30), compass (40), toast (45) and the deck itself (50) must all
|
||||||
|
// paint UNDER the menu, or the bar gets overlapped the moment it opens.
|
||||||
|
this.setDepth(60); // above the command deck (50)
|
||||||
|
|
||||||
const cfg = config.section('save.subBar', {});
|
const cfg = config.section('save.subBar', {});
|
||||||
const anim = cfg.animation ?? {};
|
const anim = cfg.animation ?? {};
|
||||||
|
|
@ -166,6 +171,11 @@ export class MenuSubBar extends Phaser.GameObjects.Container {
|
||||||
bx += widths[i] / 2 + this.gap + (i < items.length - 1 ? widths[i + 1] / 2 : 0);
|
bx += widths[i] / 2 + this.gap + (i < items.length - 1 ? widths[i + 1] / 2 : 0);
|
||||||
return slot;
|
return slot;
|
||||||
});
|
});
|
||||||
|
// CLOSED = input-inert. The v4 `ignorePointer` idiom is a no-op (it's a
|
||||||
|
// physics-world property — see js/utils/Input.js), so while closed the
|
||||||
|
// ghost buttons must be switched off via `input.enabled`, or they
|
||||||
|
// swallow every world click in their band (and the bar starts closed).
|
||||||
|
this.buttons.forEach((s) => setInteractiveEnabled(s.btn.panel, false));
|
||||||
// ---- the anchor: right-aligned over the menu button, seam on its top edge
|
// ---- the anchor: right-aligned over the menu button, seam on its top edge
|
||||||
const a = o.anchor ?? { x: sceneW / 2, y: scene.scale.height - 60, w: 120 };
|
const a = o.anchor ?? { x: sceneW / 2, y: scene.scale.height - 60, w: 120 };
|
||||||
let cx = a.x + a.w / 2 - this.W / 2; // bar's right edge on the button's right edge
|
let cx = a.x + a.w / 2 - this.W / 2; // bar's right edge on the button's right edge
|
||||||
|
|
@ -210,6 +220,8 @@ export class MenuSubBar extends Phaser.GameObjects.Container {
|
||||||
if (this.state !== 'closed' || this.dead) return;
|
if (this.state !== 'closed' || this.dead) return;
|
||||||
this.state = 'opening';
|
this.state = 'opening';
|
||||||
this.t0 = this.lastTime ?? this.scene.time.now;
|
this.t0 = this.lastTime ?? this.scene.time.now;
|
||||||
|
// Input on — the buttons catch clicks (v4: input.enabled, not ignorePointer).
|
||||||
|
this.buttons.forEach((s) => setInteractiveEnabled(s.btn.panel, true));
|
||||||
this.buttons.forEach((s) => {
|
this.buttons.forEach((s) => {
|
||||||
s.up = false;
|
s.up = false;
|
||||||
s._dec = null;
|
s._dec = null;
|
||||||
|
|
@ -228,6 +240,8 @@ export class MenuSubBar extends Phaser.GameObjects.Container {
|
||||||
if (this.state !== 'open' || this.dead) return;
|
if (this.state !== 'open' || this.dead) return;
|
||||||
this.state = 'closing';
|
this.state = 'closing';
|
||||||
this.t0 = this.lastTime ?? this.scene.time.now;
|
this.t0 = this.lastTime ?? this.scene.time.now;
|
||||||
|
// Input off — the folding bar must not swallow clicks as it dies.
|
||||||
|
this.buttons.forEach((s) => setInteractiveEnabled(s.btn.panel, false));
|
||||||
// Labels deconstruct (the reverse decode) while the edge folds down.
|
// Labels deconstruct (the reverse decode) while the edge folds down.
|
||||||
this.buttons.forEach((s, i) => {
|
this.buttons.forEach((s, i) => {
|
||||||
s._dec = new ScrambleDecode(s.finalLabel, this.t0, 150, true);
|
s._dec = new ScrambleDecode(s.finalLabel, this.t0, 150, true);
|
||||||
|
|
@ -246,7 +260,10 @@ export class MenuSubBar extends Phaser.GameObjects.Container {
|
||||||
this.ghostM.setAlpha(0);
|
this.ghostM.setAlpha(0);
|
||||||
this.seamG.clear();
|
this.seamG.clear();
|
||||||
this.scan.setAlpha(0).setSize(this.W, 0);
|
this.scan.setAlpha(0).setSize(this.W, 0);
|
||||||
this.buttons.forEach((s) => s.btn.setAlpha(0));
|
this.buttons.forEach((s) => {
|
||||||
|
s.btn.setAlpha(0);
|
||||||
|
setInteractiveEnabled(s.btn.panel, false);
|
||||||
|
});
|
||||||
for (const sl of this.slices) sl.g.destroy();
|
for (const sl of this.slices) sl.g.destroy();
|
||||||
this.slices.length = 0;
|
this.slices.length = 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { ConfirmOverlay } from './ConfirmOverlay.js';
|
||||||
import { Toast } from './Toast.js';
|
import { Toast } from './Toast.js';
|
||||||
import { SaveManager } from '../save/SaveManager.js';
|
import { SaveManager } from '../save/SaveManager.js';
|
||||||
import { captureState, prepareLoad } from '../save/SaveData.js';
|
import { captureState, prepareLoad } from '../save/SaveData.js';
|
||||||
|
import { setInteractiveEnabled } from '../utils/Input.js';
|
||||||
import { ScrambleDecode, decodeDur } from '../utils/Decode.js';
|
import { ScrambleDecode, decodeDur } from '../utils/Decode.js';
|
||||||
|
|
||||||
const FONT_FALLBACK = "'Segoe UI', 'Helvetica Neue', Arial, sans-serif";
|
const FONT_FALLBACK = "'Segoe UI', 'Helvetica Neue', Arial, sans-serif";
|
||||||
|
|
@ -59,6 +60,11 @@ export class SavePanel extends Phaser.GameObjects.Container {
|
||||||
super(scene, 0, 0);
|
super(scene, 0, 0);
|
||||||
this.scene.add.existing(this);
|
this.scene.add.existing(this);
|
||||||
this.setScrollFactor(0); // screen-fixed UI
|
this.setScrollFactor(0); // screen-fixed UI
|
||||||
|
// The pop-up is a MODAL — it must paint above the world (planets 5,
|
||||||
|
// HUD 30, compass 40, toast 45), the deck (50) and the sub-bar (60),
|
||||||
|
// or planets overlap it the moment it cracks open and its scrim can't
|
||||||
|
// darken the deck.
|
||||||
|
this.setDepth(70); // above the sub-bar (60) and the deck (50)
|
||||||
|
|
||||||
const cfg = config.section('save.panel', {});
|
const cfg = config.section('save.panel', {});
|
||||||
const cols = cfg.cols ?? 5;
|
const cols = cfg.cols ?? 5;
|
||||||
|
|
@ -96,7 +102,7 @@ export class SavePanel extends Phaser.GameObjects.Container {
|
||||||
hitAreaCallback: () => true,
|
hitAreaCallback: () => true,
|
||||||
});
|
});
|
||||||
this.scrim.on('pointerdown', () => this.close());
|
this.scrim.on('pointerdown', () => this.close());
|
||||||
this.scrim.ignorePointer = true; // off while hidden
|
setInteractiveEnabled(this.scrim, false); // off while hidden (v4: input.enabled — `ignorePointer` is a no-op)
|
||||||
this.add(this.scrim);
|
this.add(this.scrim);
|
||||||
|
|
||||||
// ---- the plate (redrawn taller every frame while it cracks open) --
|
// ---- the plate (redrawn taller every frame while it cracks open) --
|
||||||
|
|
@ -170,6 +176,10 @@ export class SavePanel extends Phaser.GameObjects.Container {
|
||||||
card.setAlpha(0);
|
card.setAlpha(0);
|
||||||
card._up = false;
|
card._up = false;
|
||||||
card._locked = true;
|
card._locked = true;
|
||||||
|
// Hidden panel = inert cards (v4: `input.enabled`; the old
|
||||||
|
// `ignorePointer` idiom is a no-op and left these swallowing
|
||||||
|
// centre-screen clicks forever).
|
||||||
|
setInteractiveEnabled(card.panel, false);
|
||||||
this.cards.push(card);
|
this.cards.push(card);
|
||||||
this.add(card);
|
this.add(card);
|
||||||
}
|
}
|
||||||
|
|
@ -211,8 +221,10 @@ export class SavePanel extends Phaser.GameObjects.Container {
|
||||||
);
|
);
|
||||||
this.cancelBtn.setAlpha(0);
|
this.cancelBtn.setAlpha(0);
|
||||||
this.downloadBtn.setAlpha(0);
|
this.downloadBtn.setAlpha(0);
|
||||||
this.cancelBtn.ignorePointer = true;
|
// Hidden = input-inert (v4: `input.enabled` — the `ignorePointer`
|
||||||
this.downloadBtn.ignorePointer = true;
|
// idiom is a physics-world property and a no-op here).
|
||||||
|
setInteractiveEnabled(this.cancelBtn.panel, false);
|
||||||
|
setInteractiveEnabled(this.downloadBtn.panel, false);
|
||||||
this.add([this.cancelBtn, this.downloadBtn]);
|
this.add([this.cancelBtn, this.downloadBtn]);
|
||||||
|
|
||||||
// ---- the confirm dialog + toast ------------------------------------
|
// ---- the confirm dialog + toast ------------------------------------
|
||||||
|
|
@ -278,17 +290,17 @@ export class SavePanel extends Phaser.GameObjects.Container {
|
||||||
card.setAccent(this.accent);
|
card.setAccent(this.accent);
|
||||||
card._up = false;
|
card._up = false;
|
||||||
card._locked = false;
|
card._locked = false;
|
||||||
card.panel.ignorePointer = false;
|
setInteractiveEnabled(card.panel, true);
|
||||||
card.setRecord(records[i]);
|
card.setRecord(records[i]);
|
||||||
card.setDisabled(this.mode === 'load' && records[i] === null);
|
card.setDisabled(this.mode === 'load' && records[i] === null);
|
||||||
card.setAlpha(0);
|
card.setAlpha(0);
|
||||||
card.setY(card.y + 5);
|
card.setY(card.y + 5);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Input back on.
|
// Input back on (v4: `input.enabled` — `ignorePointer` is a no-op).
|
||||||
this.scrim.ignorePointer = false;
|
setInteractiveEnabled(this.scrim, true);
|
||||||
this.cancelBtn.panel.ignorePointer = false;
|
setInteractiveEnabled(this.cancelBtn.panel, true);
|
||||||
this.downloadBtn.panel.ignorePointer = false;
|
setInteractiveEnabled(this.downloadBtn.panel, true);
|
||||||
this.cancelBtn.setAlpha(0);
|
this.cancelBtn.setAlpha(0);
|
||||||
this.downloadBtn.setAlpha(0);
|
this.downloadBtn.setAlpha(0);
|
||||||
|
|
||||||
|
|
@ -326,14 +338,14 @@ export class SavePanel extends Phaser.GameObjects.Container {
|
||||||
this.cards.forEach((c) => {
|
this.cards.forEach((c) => {
|
||||||
c._up = true;
|
c._up = true;
|
||||||
c._dec = null;
|
c._dec = null;
|
||||||
c.panel.ignorePointer = true;
|
setInteractiveEnabled(c.panel, false);
|
||||||
});
|
});
|
||||||
this.scene.tweens.add({ targets: this.cards, alpha: 0, duration: 110, ease: 'Sine.easeIn' });
|
this.scene.tweens.add({ targets: this.cards, alpha: 0, duration: 110, ease: 'Sine.easeIn' });
|
||||||
this.scene.tweens.add({ targets: [this.cancelBtn, this.downloadBtn], alpha: 0, duration: 90 });
|
this.scene.tweens.add({ targets: [this.cancelBtn, this.downloadBtn], alpha: 0, duration: 90 });
|
||||||
this.scene.tweens.add({ targets: this.scrim, alpha: 0, duration: this.closeDur * 0.8, ease: 'Sine.easeIn' });
|
this.scene.tweens.add({ targets: this.scrim, alpha: 0, duration: this.closeDur * 0.8, ease: 'Sine.easeIn' });
|
||||||
this.scrim.ignorePointer = true;
|
setInteractiveEnabled(this.scrim, false);
|
||||||
this.cancelBtn.panel.ignorePointer = true;
|
setInteractiveEnabled(this.cancelBtn.panel, false);
|
||||||
this.downloadBtn.panel.ignorePointer = true;
|
setInteractiveEnabled(this.downloadBtn.panel, false);
|
||||||
this.scene.playSfx?.('deconstruct');
|
this.scene.playSfx?.('deconstruct');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/**
|
||||||
|
* Enable / disable hit-testing on an interactive object (Phaser v4).
|
||||||
|
*
|
||||||
|
* v4 (4.2.1 Giedi) quirk — the trap behind the dead sub-bar buttons:
|
||||||
|
* the Phaser 3 idiom `obj.ignorePointer = true` is a NO-OP in the INPUT
|
||||||
|
* system (that property belongs to the physics world — matter.js — only).
|
||||||
|
* v4's InputManager gates hit-testing on `obj.input.enabled` (see
|
||||||
|
* InputManager.inputCandidate: `if (!i || !i.enabled || ...) return false`),
|
||||||
|
* and a pointer press lands on only the single TOPMOST hit (topOnly) —
|
||||||
|
* so an invisible rect that was "supposed to be off" keeps swallowing
|
||||||
|
* every click beneath it, forever.
|
||||||
|
*
|
||||||
|
* Use this wherever the old code wrote `ignorePointer`:
|
||||||
|
*
|
||||||
|
* setInteractiveEnabled(obj, false) // the object stops catching clicks
|
||||||
|
* setInteractiveEnabled(obj, true) // it catches them again
|
||||||
|
*
|
||||||
|
* No-op if the object never got setInteractive() — safe either way.
|
||||||
|
*/
|
||||||
|
export function setInteractiveEnabled(obj, enabled) {
|
||||||
|
if (obj && obj.input) obj.input.enabled = !!enabled;
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue