Fix SavePanel button overflow, header decode freeze, and card lock state
- Pin DOWNLOAD ALL button's right edge to the panel inner boundary using its actual rendered width instead of a fixed 210px assumption that let the label spill past the window - Drive title/subtitle decode animations through both 'opening' and 'shown' states so the scramble completes rather than freezing mid-glyph when the crack-open settles - Set `card.locked` (no underscore) to match what SlotCard.press() actually gates on, preventing stale `_locked` writes from leaving cards inert - Add a real pointer-press helper in the dev test that maps canvas coords through the FIT-mode zoom and dispatches mouseover/down/up to exercise the full click pipeline end-to-end
This commit is contained in:
parent
bf43e12a1c
commit
2bad1f1e84
|
|
@ -74,6 +74,29 @@ const wait = (ms) => new Promise((resolve) => {
|
|||
requestAnimationFrame(poll);
|
||||
});
|
||||
|
||||
// A real mouse press at CANVAS screen coords (x, y). Phaser 4.2.1 binds
|
||||
// mousedown/mouseup/mousemove on the canvas element, so a dispatched
|
||||
// MouseEvent drives the full input pipeline (pointer → scene handler →
|
||||
// interactive objects). FIT mode scales the canvas, so map canvas →
|
||||
// client space with the rendered rect (not 1:1 pixels).
|
||||
const press = (x, y) => {
|
||||
const canvas = game.canvas;
|
||||
const r = canvas.getBoundingClientRect();
|
||||
const zoom = r.width / canvas.width;
|
||||
const mk = (type, buttons) => new MouseEvent(type, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
button: 0,
|
||||
buttons,
|
||||
clientX: r.left + x * zoom,
|
||||
clientY: r.top + y * zoom,
|
||||
});
|
||||
canvas.dispatchEvent(mk('mouseover', 0));
|
||||
canvas.dispatchEvent(mk('mousedown', 1));
|
||||
canvas.dispatchEvent(mk('mouseup', 0));
|
||||
};
|
||||
|
||||
|
||||
const run = async () => {
|
||||
const scene = game.scene.getScene('GameScene');
|
||||
|
|
@ -116,6 +139,33 @@ const run = async () => {
|
|||
&& [scene.savePanel.cancelBtn, scene.savePanel.downloadBtn].every((b) =>
|
||||
Number.isFinite(scene.savePanel.x + b.x) && Number.isFinite(scene.savePanel.y + b.y)));
|
||||
|
||||
// DOWNLOAD ALL SAVED GAMES fits INSIDE the window (right edge ≤ the
|
||||
// plate's inner edge — the label outgrows the fixed 210 px box, so the
|
||||
// button must be pinned by its actual width).
|
||||
check('the DOWNLOAD ALL button fits inside the panel',
|
||||
scene.savePanel.x + scene.savePanel.downloadBtn.x + scene.savePanel.downloadBtn.width / 2
|
||||
<= scene.savePanel.x + scene.savePanel.W / 2 + 1);
|
||||
|
||||
// The header decodes run LONGER than the crack-open (decodeDur > openMs);
|
||||
// once the window settles they must hold the FINAL strings — not the
|
||||
// mid-scramble garbage the frozen drive left on screen.
|
||||
await wait(1200);
|
||||
check('the title decode completes (no mid-scramble garbage)',
|
||||
scene.savePanel.title.text === String(config.get('save.panel.title.save', 'SAVE GAME')).toUpperCase());
|
||||
check('the subtitle decode completes',
|
||||
scene.savePanel.subtitle.text === String(config.get('save.panel.subtitle.save', '')).toUpperCase());
|
||||
|
||||
// A REAL pointer press on an empty slot saves the game — the whole
|
||||
// click path (hit-test → SlotCard.press → onCard → doSave → bank), not
|
||||
// a direct doSave() call.
|
||||
const card2 = scene.savePanel.cards[1];
|
||||
check('slot 2 is empty before the click', scene.saveManager.get(2) === null);
|
||||
press(scene.savePanel.x + card2.x, scene.savePanel.y + card2.y);
|
||||
await wait(250);
|
||||
const rec2 = scene.saveManager.get(2);
|
||||
check('a pointer click on slot 2 saves the game',
|
||||
rec2 !== null && rec2.seed === scene.galaxy.seed && rec2.ship.x === scene.ship.x);
|
||||
|
||||
// A direct write to slot 1 (the same path the confirm dialog uses).
|
||||
scene.savePanel.doSave(1);
|
||||
await wait(120);
|
||||
|
|
@ -143,7 +193,7 @@ const run = async () => {
|
|||
await wait(450);
|
||||
check('the panel reopens in LOAD mode', scene.savePanel.isOpen && scene.savePanel.mode === 'load');
|
||||
check('empty slots are inert in load mode',
|
||||
scene.savePanel.cards[1].disabled === true && scene.savePanel.cards[0].disabled === false);
|
||||
scene.savePanel.cards[2].disabled === true && scene.savePanel.cards[0].disabled === false);
|
||||
|
||||
// ---- load confirm: stage the restore, cut to the menu -----------------
|
||||
const shipX = scene.ship.x;
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ export class SavePanel extends Phaser.GameObjects.Container {
|
|||
card.setRecord(null);
|
||||
card.setAlpha(0);
|
||||
card._up = false;
|
||||
card._locked = true;
|
||||
card.locked = true; // SlotCard.press() gates on `locked` (no underscore)
|
||||
// Hidden panel = inert cards (v4: `input.enabled`; the old
|
||||
// `ignorePointer` idiom is a no-op and left these swallowing
|
||||
// centre-screen clicks forever).
|
||||
|
|
@ -227,6 +227,13 @@ export class SavePanel extends Phaser.GameObjects.Container {
|
|||
setInteractiveEnabled(this.downloadBtn.panel, false);
|
||||
this.add([this.cancelBtn, this.downloadBtn]);
|
||||
|
||||
// The export label outgrows its configured box (MenuButton widens to
|
||||
// max(width, textW + 20)), so pin its RIGHT edge to the plate's inner
|
||||
// edge using the button's actual width — the old fixed offset
|
||||
// (W/2 - pad - 100, assuming a 210 px button) spilled the label past
|
||||
// the window's right edge.
|
||||
this.downloadBtn.setX(this.W / 2 - pad - this.downloadBtn.width / 2);
|
||||
|
||||
// ---- the confirm dialog + toast ------------------------------------
|
||||
this.confirm = new ConfirmOverlay(
|
||||
scene,
|
||||
|
|
@ -289,7 +296,7 @@ export class SavePanel extends Phaser.GameObjects.Container {
|
|||
this.cards.forEach((card, i) => {
|
||||
card.setAccent(this.accent);
|
||||
card._up = false;
|
||||
card._locked = false;
|
||||
card.locked = false; // unlock SlotCard.press()
|
||||
setInteractiveEnabled(card.panel, true);
|
||||
card.setRecord(records[i]);
|
||||
card.setDisabled(this.mode === 'load' && records[i] === null);
|
||||
|
|
@ -600,6 +607,15 @@ export class SavePanel extends Phaser.GameObjects.Container {
|
|||
this.confirm.update(time);
|
||||
this.toast.update(time);
|
||||
|
||||
// Header decodes — the scramble runs LONGER than the crack open
|
||||
// (decodeDur(…) > openMs), so drive them while the panel settles AND
|
||||
// after it lands in 'shown'; stopping the drive at 'shown' froze the
|
||||
// title mid-scramble and left garbage glyphs on screen.
|
||||
if (this.state === 'opening' || this.state === 'shown') {
|
||||
this.driveDecode(this._titleDec, this.title, time, this._titleFinal);
|
||||
this.driveDecode(this._subDec, this.subtitle, time, this._subFinal);
|
||||
}
|
||||
|
||||
if (this.state === 'opening') {
|
||||
const p = Phaser.Math.Clamp((time - this.t0) / this.openDur, 0, 1);
|
||||
const e = 1 - Math.pow(1 - p, 3);
|
||||
|
|
@ -611,9 +627,6 @@ export class SavePanel extends Phaser.GameObjects.Container {
|
|||
const off = this.ghostStart * (1 - e);
|
||||
this.ghostC.setAlpha(0.5 * (1 - e)).setPosition(off * 0.85, -off * 0.35);
|
||||
this.ghostM.setAlpha(0.5 * (1 - e)).setPosition(-off, off * 0.45);
|
||||
// Header decodes.
|
||||
this.driveDecode(this._titleDec, this.title, time, this._titleFinal);
|
||||
this.driveDecode(this._subDec, this.subtitle, time, this._subFinal);
|
||||
// The slot cards flicker up one by one.
|
||||
this.cards.forEach((card, i) => {
|
||||
if (card._up) return;
|
||||
|
|
|
|||
Loading…
Reference in New Issue