diff --git a/dev/saves-ui-test.mjs b/dev/saves-ui-test.mjs index 2328317..b6057a5 100644 --- a/dev/saves-ui-test.mjs +++ b/dev/saves-ui-test.mjs @@ -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; diff --git a/js/ui/SavePanel.js b/js/ui/SavePanel.js index 80793c4..0e9e06b 100644 --- a/js/ui/SavePanel.js +++ b/js/ui/SavePanel.js @@ -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;