A project could only be played inside the editor, behind the login.
To show a game to someone else there was no way short of an account.
A release freezes the saved project under a random token and serves
it on a page that holds nothing but the game view.
- Menu -> Release... opens a dialog with the state of the release,
the link with Copy link and Open, Publish or Update release, and
Take down behind a confirmation.
- Publish saves first and refuses while anything is unsaved, like
snapshots. Updating keeps the link. Take down kills it, and a new
Publish hands out a new token.
- The link is play.html?game=<32 hex>. The page needs no session,
paints Theme.colors.background, zooms with Auto and waits for
"Click to play" so that sound and keyboard focus are allowed.
- The game always starts in the start scene. Persistent variables
are stored per link in the browser. Plugins are not loaded, and the
dialog warns about them and about a project without a scene.
- GET ?r=releases/{token} is the only route besides auth/* that works
without a login. The public answer carries the token as project.id,
so the project id stays hidden. Release files are written with 0640
and nginx does not serve them directly (measured 403).
- Starting, the frame and Load scene moved into runtime/GameRun.ts.
Play in the editor and the play page run through the same code.
- WorldViewport moved to src/viewport/ and no longer needs the
EditorState. Pointer forwarding (gamePointer.ts) and setting up
textures, fonts and sounds (projectFiles.ts) exist only once.
- Vite builds two pages. The play page loads 4 kB of its own code
plus the shared chunk (Pixi, runtime, drawing, 653 kB) and Rapier,
without the editor.
Found on the way, not fixed here: data/projects/<id>.json answers 200
without a login because the files are 0644.
Tests:
- ./check.sh: 194 files, 2808 tests, exit 0.
- tests/php/releases.test.php: 20 checks, also added to CI.
- New e2e module 60 with 10 checks, played in a browser without a
session. Module 46 counts the menu entries and was updated.
- Full e2e run: 1572 checks passed.
- Counter-checks: 3 unit, 2 PHP and 3 browser mutations all fail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
29 lines
1.2 KiB
TypeScript
29 lines
1.2 KiB
TypeScript
import type { SaveStatus } from './state';
|
||
|
||
/** When a snapshot was made, in the local time of the browser: `2026-09-25 17:39`. */
|
||
export function snapshotTime(at: Date): string {
|
||
const two = (n: number) => String(n).padStart(2, '0');
|
||
return `${at.getFullYear()}-${two(at.getMonth() + 1)}-${two(at.getDate())} ${two(at.getHours())}:${two(at.getMinutes())}`;
|
||
}
|
||
|
||
/**
|
||
* Why a snapshot cannot be made or restored right now, or null when it can (#231). The server copies
|
||
* what it has stored, so the editor has to have saved everything first: a snapshot without the
|
||
* last edits would be a stand nobody ever saw. A release is a copy as well (#266) and asks the same.
|
||
*/
|
||
export function snapshotBlocker(
|
||
status: SaveStatus,
|
||
copy: 'snapshot' | 'release' = 'snapshot',
|
||
): string | null {
|
||
switch (status) {
|
||
case 'saved':
|
||
return null;
|
||
case 'conflict':
|
||
return 'This project was changed elsewhere. Reload to continue.';
|
||
case 'error':
|
||
return `The project could not be saved, so there is nothing current to ${copy === 'release' ? 'release' : 'take a snapshot of'}`;
|
||
default:
|
||
return 'The latest changes are not saved yet – stop the game or wait a moment';
|
||
}
|
||
}
|