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>
32 lines
1.3 KiB
TypeScript
32 lines
1.3 KiB
TypeScript
import type { Container, FederatedMouseEvent, FederatedPointerEvent, FederatedWheelEvent } from 'pixi.js';
|
|
import type { World } from '../runtime/World';
|
|
import { isInside, screenToWorld, type View, wheelNotches } from './view';
|
|
|
|
/**
|
|
* Hands the mouse over `target` to a running world: position, left button and wheel. Shared by the
|
|
* editor's game view and a released game (#266). Outside the camera frame counts as "not in view":
|
|
* that region is letterbox, not game.
|
|
*/
|
|
export function routePointer(target: Container, world: () => World | null, view: () => View | null): void {
|
|
const report = (e: FederatedMouseEvent): void => {
|
|
const w = world();
|
|
const v = view();
|
|
if (!w || !v) return;
|
|
const local = target.toLocal(e.global);
|
|
const point = screenToWorld(v, local.x, local.y);
|
|
w.movePointer(point.x, point.y, isInside(v, local.x, local.y));
|
|
};
|
|
target.on('pointerdown', (e: FederatedPointerEvent) => {
|
|
if (e.button !== 0) return;
|
|
report(e);
|
|
world()?.setPointerDown(true);
|
|
});
|
|
target.on('globalpointermove', (e: FederatedPointerEvent) => report(e));
|
|
target.on('pointerup', () => world()?.setPointerDown(false));
|
|
target.on('pointerupoutside', () => world()?.setPointerDown(false));
|
|
target.on('wheel', (e: FederatedWheelEvent) => {
|
|
report(e);
|
|
world()?.turnWheel(wheelNotches(e.deltaY, e.deltaMode));
|
|
});
|
|
}
|