The page of a released game always drew the camera at Auto, the
largest whole step that fits. A camera of 300x180 in a window of
1280x800 stands at step 4 and leaves a border left and right. The
issue asks for Fit next to it. Answered there: a setting of its own
rather than the scale of the Game tab, and no switch for the player.
- The project has a field gameLinkZoom, 'auto' or 'fit'. A document
without it reads as 'auto', so nothing changes for a project that
is there already. No format bump.
- Project settings has a row "Link zoom" with a menu of Auto and
Fit. It is an undo step of its own, SetGameLinkZoomCommand.
- GameScreen hands the field to cameraView where it passed a fixed
'auto'.
- gameScale, the scale of the Game tab, is not touched and does not
reach the link.
- The released copy is frozen, so the change reaches the link only
with "Update release".
Decided here:
- The row is called "Link zoom", not "Game link zoom" as proposed.
The longer label is wider than the label column of the dialog and
pushed its button 12 px out of line with the others.
- The undo step keeps the name "Game link zoom", which is what the
tooltip of the undo button shows.
- A note under the row says "The released game at the largest whole
step that fits, or filling the window."
Tests:
- Gate on melektaus-atx: 211 files, 3227 tests (3219 + 8), exit 0
- e2e 60 on melektaus-atx: 19 checks, all pass
- e2e 35,46,59,60: 74 checks, all pass
- No full run and no smokes. The sum of 1671 (1668 + 3) is reckoned,
1668 is what CI counted for 9e869df.
- Eight unit tests: the default, saving and loading, values that
are not a choice, undo, gameScale left alone, the factor of Fit
for a window larger and smaller than the camera, and the rule
behind Apply.
- Three e2e checks in block 60i of module 60, with the real mouse:
Auto to Fit in the dialog, update the release, open the link
without a session and measure scale, frame and pixels. Fit reads
4.267 where Auto reads 4.
- Nine unit counter-probes and three browser counter-probes all fail
their tests.
- Not checked in the browser: Fit in a window smaller than the
camera, which has a unit test, and Fit under a display scale above
100 %. Nothing was looked at after the build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
139 lines
5.6 KiB
TypeScript
139 lines
5.6 KiB
TypeScript
/**
|
|
* Which scene Play starts and the game view scale (#226): both live in the project now, not in the
|
|
* browser. The start-scene switch is an undo step like every other project setting; the scale is
|
|
* saved but never lands on the undo stack.
|
|
*/
|
|
import { describe, expect, it } from 'vitest';
|
|
import { SetGameLinkZoomCommand, SetPlayFromCommand } from '../../src/core/commands/projectCommands';
|
|
import { gameLinkZoomOf, gameScaleOf, playFromOf } from '../../src/core/model/playSettings';
|
|
import { createEmptyProject } from '../../src/core/model/Project';
|
|
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
|
|
import { EditorState } from '../../src/editor/state';
|
|
import { cameraView } from '../../src/viewport/view';
|
|
|
|
function loaded() {
|
|
const state = new EditorState();
|
|
state.loadProject(createEmptyProject('p', 'P'), 1);
|
|
return state;
|
|
}
|
|
|
|
describe('play settings in the project', () => {
|
|
it('start from the start scene at 100% in a new project and in one written before there were any', () => {
|
|
const fresh = createEmptyProject('p', 'P');
|
|
const old = deserializeProject({ id: 'p', name: 'P', assets: [] });
|
|
expect([fresh.playFrom, fresh.gameScale]).toEqual(['start', 1]);
|
|
expect([old.playFrom, old.gameScale]).toEqual(['start', 1]);
|
|
});
|
|
|
|
it('survive a save and a load', () => {
|
|
const project = {
|
|
...createEmptyProject('p', 'P'),
|
|
playFrom: 'active' as const,
|
|
gameScale: 'auto' as const,
|
|
};
|
|
const back = deserializeProject(serializeProject(project));
|
|
expect([back.playFrom, back.gameScale]).toEqual(['active', 'auto']);
|
|
});
|
|
|
|
it('read untrusted JSON as the defaults unless it is one of the choices', () => {
|
|
expect(playFromOf({ playFrom: 'active' })).toBe('active');
|
|
expect(playFromOf({ playFrom: 'elsewhere' })).toBe('start');
|
|
expect(playFromOf(null)).toBe('start');
|
|
expect(gameScaleOf({ gameScale: 'fit' })).toBe('fit');
|
|
expect(gameScaleOf({ gameScale: 0.25 })).toBe(0.25);
|
|
expect(gameScaleOf({ gameScale: 3 })).toBe(3);
|
|
expect(gameScaleOf({ gameScale: 6 })).toBe(6);
|
|
expect(gameScaleOf({ gameScale: 2.5 })).toBe(1);
|
|
expect(gameScaleOf({ gameScale: 8 })).toBe(1);
|
|
expect(gameScaleOf({ gameScale: '2' })).toBe(1);
|
|
});
|
|
|
|
it('make the start-scene switch one undo step', () => {
|
|
const state = loaded();
|
|
state.execute(new SetPlayFromCommand('active'));
|
|
expect(state.playFrom).toBe('active');
|
|
expect(state.saveStatus).toBe('dirty');
|
|
state.undo();
|
|
expect(state.playFrom).toBe('start');
|
|
});
|
|
|
|
it('save the game view scale without an undo step', () => {
|
|
const state = loaded();
|
|
state.setGameScale(4);
|
|
expect(state.project.gameScale).toBe(4);
|
|
expect(state.saveStatus).toBe('dirty');
|
|
expect(state.commands.canUndo).toBe(false);
|
|
});
|
|
|
|
it('keep a scale picked while playing after Stop', () => {
|
|
const state = loaded();
|
|
state.enterPlay();
|
|
state.setGameScale('fit');
|
|
expect(state.gameScale).toBe('fit');
|
|
state.exitPlay();
|
|
expect(state.gameScale).toBe('fit');
|
|
expect(state.saveStatus).toBe('dirty');
|
|
});
|
|
});
|
|
|
|
describe('the zoom of the game link (#279)', () => {
|
|
it('is Auto in a new project and in one written before there was a choice', () => {
|
|
expect(createEmptyProject('p', 'P').gameLinkZoom).toBe('auto');
|
|
expect(deserializeProject({ id: 'p', name: 'P', assets: [] }).gameLinkZoom).toBe('auto');
|
|
});
|
|
|
|
it('survives a save and a load, apart from the scale of the game view', () => {
|
|
const project = { ...createEmptyProject('p', 'P'), gameLinkZoom: 'fit' as const, gameScale: 2 };
|
|
const back = deserializeProject(serializeProject(project));
|
|
expect([back.gameLinkZoom, back.gameScale]).toEqual(['fit', 2]);
|
|
});
|
|
|
|
it('reads untrusted JSON as Auto unless it is one of the two choices', () => {
|
|
expect(gameLinkZoomOf({ gameLinkZoom: 'fit' })).toBe('fit');
|
|
expect(gameLinkZoomOf({ gameLinkZoom: 'auto' })).toBe('auto');
|
|
expect(gameLinkZoomOf({ gameLinkZoom: 2 })).toBe('auto');
|
|
expect(gameLinkZoomOf({ gameLinkZoom: 'Fit' })).toBe('auto');
|
|
expect(gameLinkZoomOf({ gameScale: 'fit' })).toBe('auto');
|
|
expect(gameLinkZoomOf(null)).toBe('auto');
|
|
});
|
|
|
|
it('is one undo step', () => {
|
|
const state = loaded();
|
|
state.execute(new SetGameLinkZoomCommand('fit'));
|
|
expect(state.project.gameLinkZoom).toBe('fit');
|
|
expect(state.saveStatus).toBe('dirty');
|
|
state.undo();
|
|
expect(state.project.gameLinkZoom).toBe('auto');
|
|
state.redo();
|
|
expect(state.project.gameLinkZoom).toBe('fit');
|
|
});
|
|
|
|
it('leaves the scale of the game view alone', () => {
|
|
const state = loaded();
|
|
state.setGameScale(3);
|
|
state.execute(new SetGameLinkZoomCommand('fit'));
|
|
expect(state.gameScale).toBe(3);
|
|
});
|
|
|
|
it('fills the window at Fit where Auto stops at the step below', () => {
|
|
const camera = { x: 0, y: 0, props: { width: 300, height: 180, zoom: 1 } };
|
|
const auto = cameraView(camera, 1000, 700, 'auto');
|
|
const fit = cameraView(camera, 1000, 700, 'fit');
|
|
expect(auto.scale).toBe(3);
|
|
expect(auto.rect).toEqual({ x: 50, y: 80, w: 900, h: 540 });
|
|
expect(fit.scale).toBeCloseTo(10 / 3, 9);
|
|
expect(fit.rect.x).toBeCloseTo(0, 9);
|
|
expect(fit.rect.w).toBeCloseTo(1000, 9);
|
|
expect(fit.rect.h).toBeCloseTo(600, 9);
|
|
expect(fit.rect.y).toBeCloseTo(50, 9);
|
|
});
|
|
|
|
it('shrinks the picture at Fit in a window smaller than the camera', () => {
|
|
const camera = { x: 0, y: 0, props: { width: 300, height: 180, zoom: 1 } };
|
|
const fit = cameraView(camera, 200, 400, 'fit');
|
|
expect(fit.scale).toBeCloseTo(2 / 3, 9);
|
|
expect(fit.rect.w).toBeCloseTo(200, 9);
|
|
expect(fit.rect.h).toBeCloseTo(120, 9);
|
|
});
|
|
});
|