Describe the game link and the new zoom steps (#266)

A project can now be released as a link that plays without the
editor and without a login.

- Concepts: a Release section, the new main menu entry and the zoom
  steps 300%, 500% and 600% in the game tab table.
- API: the four release routes, and that the public one is the only
  route besides auth that needs no session.
- Architecture: GameRun, the player page, gamePointer, projectFiles
  and WorldViewport in its new place under src/viewport.
- Development: the PHP release test and the measured sum of 1574
  e2e checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
adminandClaude Opus 5.5 committed 2026-09-29 14:15:11 +02:00
1 parent 68c8798a70
commit d0ed6bd81d
4 files changed
+48 -9

No files matched your search

+6 -2
@@ -2,7 +2,7 @@ All routes go through `api/index.php?r=…` (the site's nginx does not pass `PAT
| Method | Route | Body → Result | | Method | Route | Body → Result |
|---|---|---| |---|---|---|
| GET | `auth/session` | `{user, role}` — the name and role (`admin` or `user`) of whoever is signed in, both `null` when nobody is. Always 200, and the only route that needs no session | | GET | `auth/session` | `{user, role}` — the name and role (`admin` or `user`) of whoever is signed in, both `null` when nobody is. Always 200 |
| POST | `auth/login` | `{username,password}` → `{user, role}` and the session cookie; **401** `{error}` for a wrong pair | | POST | `auth/login` | `{username,password}` → `{user, role}` and the session cookie; **401** `{error}` for a wrong pair |
| POST | `auth/logout` | 204, and the cookie is cleared | | POST | `auth/logout` | 204, and the cookie is cleared |
| POST | `auth/password` | `{currentPassword,newPassword}` → 204. Changes the password of whoever is signed in — the only `auth` route that needs a session. **401** for a wrong current password, **422** when the new one is shorter than 8 characters. Every other session of the account ends; the one asking keeps its own | | POST | `auth/password` | `{currentPassword,newPassword}` → 204. Changes the password of whoever is signed in — the only `auth` route that needs a session. **401** for a wrong current password, **422** when the new one is shorter than 8 characters. Every other session of the account ends; the one asking keeps its own |
@@ -10,11 +10,15 @@ All routes go through `api/index.php?r=…` (the site's nginx does not pass `PAT
| POST | `projects` | `{name}` → project document (201) | | POST | `projects` | `{name}` → project document (201) |
| GET | `projects/{id}` | `{id,name,revision,updatedAt,project}` | | GET | `projects/{id}` | `{id,name,revision,updatedAt,project}` |
| PUT | `projects/{id}` | `{revision, project}` → `{revision,updatedAt}` or **409** `{error,revision}` when stale | | PUT | `projects/{id}` | `{revision, project}` → `{revision,updatedAt}` or **409** `{error,revision}` when stale |
| DELETE | `projects/{id}` | 204, and the snapshots of the project go with it | | DELETE | `projects/{id}` | 204, and the snapshots and the release of the project go with it |
| GET | `projects/{id}/snapshots` | `[{id,name,revision,createdAt}]`, newest first — see [Concepts](Concepts) | | GET | `projects/{id}/snapshots` | `[{id,name,revision,createdAt}]`, newest first — see [Concepts](Concepts) |
| POST | `projects/{id}/snapshots` | `{name?}` → `{id,name,revision,createdAt}` (201). Copies the project as stored; a blank name becomes the UTC date and time. **422** over 120 characters | | POST | `projects/{id}/snapshots` | `{name?}` → `{id,name,revision,createdAt}` (201). Copies the project as stored; a blank name becomes the UTC date and time. **422** over 120 characters |
| DELETE | `projects/{id}/snapshots/{sid}` | 204 | | DELETE | `projects/{id}/snapshots/{sid}` | 204 |
| POST | `projects/{id}/snapshots/{sid}/restore` | `{revision}` → `{revision,updatedAt,before}`. Keeps the stored project as the snapshot `before` (`Before restoring <name>`), then saves the snapshot as the next revision. **409** `{error,revision}` when `revision` is stale | | POST | `projects/{id}/snapshots/{sid}/restore` | `{revision}` → `{revision,updatedAt,before}`. Keeps the stored project as the snapshot `before` (`Before restoring <name>`), then saves the snapshot as the next revision. **409** `{error,revision}` when `revision` is stale |
| GET | `projects/{id}/release` | `{token,revision,publishedAt}`, or `null` while the project is not released — see [Concepts](Concepts) |
| POST | `projects/{id}/release` | → `{token,revision,publishedAt}`. Copies the project as stored; publishing again replaces the copy and keeps the token |
| DELETE | `projects/{id}/release` | 204, the link is dead; **404** when the project was not released |
| GET | `releases/{token}` | `{name,publishedAt,project}` — the released copy, **without a session**: the one route besides `auth/*` open to everybody. `project.id` is the token, not the project's id. **404** for an unknown or taken-down token |
| GET | `textures` | `[{file,name,width,height,bytes,updatedAt}]` — what the image picker offers | | GET | `textures` | `[{file,name,width,height,bytes,updatedAt}]` — what the image picker offers |
| POST | `textures&name=hero.png` | **raw image bytes** → `{file,name,width,height,bytes}` (201) | | POST | `textures&name=hero.png` | **raw image bytes** → `{file,name,width,height,bytes}` (201) |
| DELETE | `textures/{file}` | 204 | | DELETE | `textures/{file}` | 204 |
+18 -3
@@ -73,6 +73,8 @@
``` ```
web/index.html Vite source page (dev) index.html built page (generated, served) web/index.html Vite source page (dev) index.html built page (generated, served)
web/play.html a released game (#266) play.html built page (generated, served)
src/main.ts, play.ts the two entries; page.ts is the canvas both are (the WebGL context, root, resize)
src/core/model/ Project (object holders), Asset + AssetTypes, SceneObject, Properties (value types), hierarchy, walk (generic parent/child walks), instance (container content resolved from its template), usages (where an asset is used), container (the cycle guard), placement (what may be dropped where), texture (frames, the slice grid, a frame's collision outlines), frameOutline (#193: a frame's outlines as a collider wears them, centred, a pixel to a unit, cut into pieces once per drawn list), traceCollision (#161: a frame's outlines read off its picture — the pixels from an alpha threshold on are solid, each 4-connected island is walked round along the pixel edges and cut down to a few corners without ever crossing itself; crumbs below 1 % of the frame drop out), generated (pictures that are drawn rather than uploaded, #185: the key `gen:<assetId>@<revision>`, the register of readers that tell the picture path an asset's canvas size and revision, and the `fingerprint` pixel art and vector art make their revision with), vectorart (#129: the canvas as a property, which ring is worth drawing, and the revision as a fingerprint of the drawing rather than a counter, a texture fill's picture included) + vectorPath (what a shape draws, as plain commands: closed rings fill, every ring strokes, a segment is a line while neither of its ends carries a handle; a paint resolved to sorted stops or the picture it names) + vectorPaint (#186: gradients and texture fills — where a paint lies when it is switched on, the colour between two stops, adding, moving and removing a stop, a paint moved with its shape, and the rule that a texture fill takes uploaded pictures only) + vectorBoolean (#187: union, subtract, intersect and exclude on the sampled outlines, each shape read by the even-odd rule; the result rings in the order Pixi's hole detection needs, every outline followed by its holes and preceded by the islands inside them) + vectorCurves (#208: every sampled point remembers its cubic and its `t`, so a run of result points along one cubic becomes that cubic again, cut with de Casteljau), pixelart (#130: the palette a picture is painted in, how the layers of a frame come together, the sheet of all frames, a frame id read as a cut of it, and the revision) + pixelCel (the file of one layer on one frame, and its SHA-256 name worked out in the browser) + pixelDraw (what the tools do to the indices: brush, line, rectangle, ellipse, fill, resize about an anchor, merge, a palette colour taken out) + pixelEdit (every edit of layers, frames and cels as a function from one state to the next), polygon (an outline checked, cut into convex pieces and clipped to a rectangle), tile (the three kinds and their block shapes, reading a tile's pictures, the animation step), autotile (which quarter tile a neighbourhood asks for, the 47/16 tiles a block produces, 1 for a plain tile), tilemap (the cells of a layer, which neighbours a cell is joined to, the cells a stroke or a rectangle covers), tilemapCollision (the solid cells of a map, drawn in or out by their tile's collision scale and merged into rectangles, one group per one-way angle and per surface (`tileMaterials`, #193), the outlined cells as polygons — an autotile's outlines cut into its quarters and pieced together per cell like its picture —, where one sits in the world, and which cells of a placed map a world rectangle can reach, in cells (`cellsWithin`, #184)), tilemapMemo (what a map's cells work out to, kept while they hold the same: a version per cell map compared cell by cell, since strokes paint in place), animation (which frame at time t, a row's value there, what a row may aim at, the players on an object; KeyRef and its lookups) + animationPreview (the root at a frame, written into copies), curve (#247: a drawn easing evaluated at t — each segment a cubic solved for t by bisection, held to its rules by `sanitizedCurve`: ends at 0 and 1, no point overtaking another, no handle past its neighbour — and which keys and `Lerp` nodes use one) + curvePresets (the built-in eases traced as curves, a segment halved only where it strays), graphLookup (what a graph sees of the project: behaviors to run, state machines to read, curves to ease along), structTypes (#253: Struct assets as types — every typed struct variable, struct field and `Pack`/`Expand` found and brought in line with its asset, a renamed field carried through every value first, the cycle rule), Scope (how long a shared value lives: per instance, one for the run, or one per scene and rebuilt with every scene that loads, #164 — for a state machine's current state and for the values of a Variables asset), collisionLayers (which colliders may meet: the project's named layers, a collider's layer and mask, and the one rule `layersMeet` the engine, Solids and the event pair search all answer by) src/core/model/ Project (object holders), Asset + AssetTypes, SceneObject, Properties (value types), hierarchy, walk (generic parent/child walks), instance (container content resolved from its template), usages (where an asset is used), container (the cycle guard), placement (what may be dropped where), texture (frames, the slice grid, a frame's collision outlines), frameOutline (#193: a frame's outlines as a collider wears them, centred, a pixel to a unit, cut into pieces once per drawn list), traceCollision (#161: a frame's outlines read off its picture — the pixels from an alpha threshold on are solid, each 4-connected island is walked round along the pixel edges and cut down to a few corners without ever crossing itself; crumbs below 1 % of the frame drop out), generated (pictures that are drawn rather than uploaded, #185: the key `gen:<assetId>@<revision>`, the register of readers that tell the picture path an asset's canvas size and revision, and the `fingerprint` pixel art and vector art make their revision with), vectorart (#129: the canvas as a property, which ring is worth drawing, and the revision as a fingerprint of the drawing rather than a counter, a texture fill's picture included) + vectorPath (what a shape draws, as plain commands: closed rings fill, every ring strokes, a segment is a line while neither of its ends carries a handle; a paint resolved to sorted stops or the picture it names) + vectorPaint (#186: gradients and texture fills — where a paint lies when it is switched on, the colour between two stops, adding, moving and removing a stop, a paint moved with its shape, and the rule that a texture fill takes uploaded pictures only) + vectorBoolean (#187: union, subtract, intersect and exclude on the sampled outlines, each shape read by the even-odd rule; the result rings in the order Pixi's hole detection needs, every outline followed by its holes and preceded by the islands inside them) + vectorCurves (#208: every sampled point remembers its cubic and its `t`, so a run of result points along one cubic becomes that cubic again, cut with de Casteljau), pixelart (#130: the palette a picture is painted in, how the layers of a frame come together, the sheet of all frames, a frame id read as a cut of it, and the revision) + pixelCel (the file of one layer on one frame, and its SHA-256 name worked out in the browser) + pixelDraw (what the tools do to the indices: brush, line, rectangle, ellipse, fill, resize about an anchor, merge, a palette colour taken out) + pixelEdit (every edit of layers, frames and cels as a function from one state to the next), polygon (an outline checked, cut into convex pieces and clipped to a rectangle), tile (the three kinds and their block shapes, reading a tile's pictures, the animation step), autotile (which quarter tile a neighbourhood asks for, the 47/16 tiles a block produces, 1 for a plain tile), tilemap (the cells of a layer, which neighbours a cell is joined to, the cells a stroke or a rectangle covers), tilemapCollision (the solid cells of a map, drawn in or out by their tile's collision scale and merged into rectangles, one group per one-way angle and per surface (`tileMaterials`, #193), the outlined cells as polygons — an autotile's outlines cut into its quarters and pieced together per cell like its picture —, where one sits in the world, and which cells of a placed map a world rectangle can reach, in cells (`cellsWithin`, #184)), tilemapMemo (what a map's cells work out to, kept while they hold the same: a version per cell map compared cell by cell, since strokes paint in place), animation (which frame at time t, a row's value there, what a row may aim at, the players on an object; KeyRef and its lookups) + animationPreview (the root at a frame, written into copies), curve (#247: a drawn easing evaluated at t — each segment a cubic solved for t by bisection, held to its rules by `sanitizedCurve`: ends at 0 and 1, no point overtaking another, no handle past its neighbour — and which keys and `Lerp` nodes use one) + curvePresets (the built-in eases traced as curves, a segment halved only where it strays), graphLookup (what a graph sees of the project: behaviors to run, state machines to read, curves to ease along), structTypes (#253: Struct assets as types — every typed struct variable, struct field and `Pack`/`Expand` found and brought in line with its asset, a renamed field carried through every value first, the cycle rule), Scope (how long a shared value lives: per instance, one for the run, or one per scene and rebuilt with every scene that loads, #164 — for a state machine's current state and for the values of a Variables asset), collisionLayers (which colliders may meet: the project's named layers, a collider's layer and mask, and the one rule `layersMeet` the engine, Solids and the event pair search all answer by)
src/core/properties/ PropertySchema, kinds (one row per value kind), listItems (an item's kind, read off its value), resolve (the only override merge), deviation (what is gold), collider/camera/bounds geometry, clip (the region clip areas cut each object to, a convex polygon, #260) (with the side a one-way collider blocks from, and the rectangle around a shape a broad phase looks at first, #184), convex (capsules and polygons: rounded convex pieces against each other), ray (where a ray meets a shape, in double precision), rigidbody (what the engine reads off a rigidbody, a collider's material and the scene), aspect, enabled, editorRun, assetRef/textureRef (how a reference reads), declaredRows (the row a Property node declares in a shader or particle graph, and the params that node has in both) src/core/properties/ PropertySchema, kinds (one row per value kind), listItems (an item's kind, read off its value), resolve (the only override merge), deviation (what is gold), collider/camera/bounds geometry, clip (the region clip areas cut each object to, a convex polygon, #260) (with the side a one-way collider blocks from, and the rectangle around a shape a broad phase looks at first, #184), convex (capsules and polygons: rounded convex pieces against each other), ray (where a ray meets a shape, in double precision), rigidbody (what the engine reads off a rigidbody, a collider's material and the scene), aspect, enabled, editorRun, assetRef/textureRef (how a reference reads), declaredRows (the row a Property node declares in a shader or particle graph, and the params that node has in both)
src/core/util/ list (take out, put back and move an item — what a command's undo is made of), vec (dot, the closest point on a segment — shared by the collider maths, the outline hit area and the polygon grips), color (`mixColor`, channel by channel — keys of an animation, stops of a gradient, the particles' Mix colour), recorder (#253: assignments taken back last first — the undo of an edit whose reach is not known in advance) src/core/util/ list (take out, put back and move an item — what a command's undo is made of), vec (dot, the closest point on a segment — shared by the collider maths, the outline hit area and the polygon grips), color (`mixColor`, channel by channel — keys of an animation, stops of a gradient, the particles' Mix colour), recorder (#253: assignments taken back last first — the undo of an edit whose reach is not known in advance)
@@ -92,11 +94,13 @@ src/core/store/ serialize (untrusted JSON → Project), projectPlugins (w
file before it is deserialized, #119), tilemapCells (the compact form of a layer's cells in the file), file before it is deserialized, #119), tilemapCells (the compact form of a layer's cells in the file),
http (one API call), ProjectStore/HttpProjectStore (load takes a `prepare` that sees the raw JSON first), http (one API call), ProjectStore/HttpProjectStore (load takes a `prepare` that sees the raw JSON first),
SnapshotStore/HttpSnapshotStore (the snapshots of a project, #231), SnapshotStore/HttpSnapshotStore (the snapshots of a project, #231),
ReleaseStore/HttpReleaseStore (a project's released copy and the link to it, #266),
AuthStore/HttpAuthStore (signing in and out, #177; changing the password, #182), AuthStore/HttpAuthStore (signing in and out, #177; changing the password, #182),
FileStore (what the server holds of one kind) + HttpFileStore (one generic client over the three FileStore (what the server holds of one kind) + HttpFileStore (one generic client over the three
file collections) — TextureStore, FontStore and SoundStore are its type parameters, nothing else file collections) — TextureStore, FontStore and SoundStore are its type parameters, nothing else
src/core/time.ts the global clock: one scaled step a frame, shared by the world, the particles, the shaders and the animation preview src/core/time.ts the global clock: one scaled step a frame, shared by the world, the particles, the shaders and the animation preview
src/runtime/ World, Entity, System interface, Input tracker, CollisionSystem (the editor run's detection), src/runtime/ GameRun (one run: start, tick, `Load scene` — shared by Play and a released game, #266),
World, Entity, System interface, Input tracker, CollisionSystem (the editor run's detection),
solids (what `Move` collides with and how it slides — one-way solids let it through unless solids (what `Move` collides with and how it slides — one-way solids let it through unless
it lands on them —, the first solid a ray meets, which body a collider belongs to, the it lands on them —, the first solid a ray meets, which body a collider belongs to, the
CollisionSource both collision systems give the behaviors, the contact of a staying pair, CollisionSource both collision systems give the behaviors, the contact of a staying pair,
@@ -116,7 +120,11 @@ src/ui/ widget toolkit (generic, knows nothing about assets); fon
Source Sans 3 in 400, 500, 600 and 700, Source Code Pro 400, Latin only, #202/#214), icons + Source Sans 3 in 400, 500, 600 and 700, Source Code Pro 400, Latin only, #202/#214), icons +
Icon (the menu's tick boxes and current-entry dot as SVG files, and a widget that shows one file Icon (the menu's tick boxes and current-entry dot as SVG files, and a widget that shows one file
on its own: a symbol the Latin subset lacks is never written as a character, #214); SortableList + sortable (the layer list of every editor that has one: rows dragged into order, eye, duplicate, cross, rename in place; the index arithmetic of a list shown upside down, kept apart so it is measured without a canvas) on its own: a symbol the Latin subset lacks is never written as a character, #214); SortableList + sortable (the layer list of every editor that has one: rows dragged into order, eye, duplicate, cross, rename in place; the index arithmetic of a list shown upside down, kept apart so it is measured without a canvas)
src/viewport/ view (transforms), items (what to draw), symbols (how to draw it), src/viewport/ WorldViewport (the drawing of a world or a holder, the base of every view; it draws from
a `ViewSource` — the project and word of a change — so a released game draws without the
editor, #266), gamePointer (the mouse handed to a running world), projectFiles (the file
libraries pointed at the server, and what a project names fetched ahead),
view (transforms), items (what to draw), symbols (how to draw it),
symbolIcons (the SVG files in icons/, parsed once, in the theme's colours, #175), symbolIcons (the SVG files in icons/, parsed once, in the theme's colours, #175),
FileLibrary (load once, never retry a failure, tell the listeners) ← TextureLibrary, FontLibrary; FileLibrary (load once, never retry a failure, tell the listeners) ← TextureLibrary, FontLibrary;
generatedPictures (the drawn half of the picture path, #185: one picture per asset kept generatedPictures (the drawn half of the picture path, #185: one picture per asset kept
@@ -140,6 +148,7 @@ src/editor/ App → Editor (layout + ticker), PlaySession (the run),
OpenProjectDialog (the project picker behind "Open project…"), OpenProjectDialog (the project picker behind "Open project…"),
SnapshotsDialog + snapshots (the list behind "Snapshots…" and the rule of when a SnapshotsDialog + snapshots (the list behind "Snapshots…" and the rule of when a
snapshot may be made: only with everything saved, #231), snapshot may be made: only with everything saved, #231),
ReleaseDialog + releases (the link behind "Release…", what the dialog says and warns about, #266),
ProjectSettingsDialog + settingsChanges (name, the start-scene switch and the collision ProjectSettingsDialog + settingsChanges (name, the start-scene switch and the collision
layers in one dialog; the pure rule of what Apply has to carry out), layers in one dialog; the pure rule of what Apply has to carry out),
EditorRun (run in editor), state, actions, shortcuts, clipboard (#240: copy and paste of EditorRun (run in editor), state, actions, shortcuts, clipboard (#240: copy and paste of
@@ -179,8 +188,12 @@ src/editor/users/ the user management (#179): ManagementPane (the tab behin
server's own sentence. The server decides everything: `api/lib/AccountRules.php` server's own sentence. The server decides everything: `api/lib/AccountRules.php`
(not the own account, not the last administrator, not an account with projects), (not the own account, not the last administrator, not an account with projects),
`Auth::requireAdmin()` (403), `Store` with `seesAll` for an administrator `Auth::requireAdmin()` (403), `Store` with `seesAll` for an administrator
src/editor/viewport/ WorldViewport base → FreeViewport (a camera of its own: fit, pan, wheel zoom) → SceneViewport, PreviewCanvas; src/player/ GameScreen (the game on the page: Auto zoom, the caption before and after a run),
Player (loads, starts on a click, ticks, stops), playerHook (`window.__gameMakerPlay`
for the check scripts) — a released game (#266), without the editor
src/editor/viewport/ FreeViewport (a camera of its own: fit, pan, wheel zoom) → SceneViewport, PreviewCanvas;
GameViewport follows the game's camera instead — editing tools, hence not in src/viewport/. GameViewport follows the game's camera instead — editing tools, hence not in src/viewport/.
All of them extend `src/viewport/WorldViewport`.
A pan only moves the world and redraws the screen-space overlays (`moveCamera`); a full A pan only moves the world and redraws the screen-space overlays (`moveCamera`); a full
`redraw()` follows only when an object crosses the edge of the view. `redraw()` follows only when an object crosses the edge of the view.
The scene view's tools keep their own state and reach the view through `SceneViewHost`: The scene view's tools keep their own state and reach the view through `SceneViewHost`:
@@ -258,6 +271,8 @@ api/ PHP backend: index.php (routing and the sign-in gate),
lib/{Store,TextureStore,FontStore,SoundStore,PluginStore,OriginalName,Response}.php, lib/{Store,TextureStore,FontStore,SoundStore,PluginStore,OriginalName,Response}.php,
lib/{Auth,UserStore,SessionStore}.php (#177: accounts in data/users.json, sessions in data/sessions/) lib/{Auth,UserStore,SessionStore}.php (#177: accounts in data/users.json, sessions in data/sessions/)
lib/SnapshotStore.php (#231: data/snapshots/{projectId}/, one copy of the project per file) lib/SnapshotStore.php (#231: data/snapshots/{projectId}/, one copy of the project per file)
lib/ReleaseStore.php (#266: data/releases/{token}.json and which token a project has, both 0640
so the web server does not hand them out as files)
data/projects/ persisted projects, data/textures/ + data/fonts/ + data/sounds/ content-addressed uploads, data/projects/ persisted projects, data/textures/ + data/fonts/ + data/sounds/ content-addressed uploads,
data/plugins/ editor plugins, filled from plugins/ by the build and the seeder; which of them run is written data/plugins/ editor plugins, filled from plugins/ by the build and the seeder; which of them run is written
in each project (#119) in each project (#119)
+22 -3
@@ -58,8 +58,8 @@ picture (its `width` x `height`) is drawn:
| Choice | Screen pixels per camera pixel | | Choice | Screen pixels per camera pixel |
|---|---| |---|---|
| **100%** (default) | 1. 25%, 50%, 200% and 400% likewise draw exactly that factor. | | **100%** (default) | 1. 25%, 50%, 200%, 300%, 400%, 500% and 600% likewise draw exactly that factor. |
| **Auto** | The largest of those five steps at which the whole picture still fits. Below 25% it stays at 25%. | | **Auto** | The largest of those eight steps at which the whole picture still fits. Below 25% it stays at 25%. |
| **Fit** | Whatever fills the view on its tighter side, the way the tab always drew before. | | **Fit** | Whatever fills the view on its tighter side, the way the tab always drew before. |
The camera's `zoom` magnifies the world on top: a 640x360 camera at zoom 2 shows 320x180 world The camera's `zoom` magnifies the world on top: a 640x360 camera at zoom 2 shows 320x180 world
@@ -102,8 +102,27 @@ sounds and pixel art are stored by their content and never removed, so everythin
still there. Deleting the project deletes its snapshots. Whoever can open a project can use its still there. Deleting the project deletes its snapshots. Whoever can open a project can use its
snapshots, an administrator included. snapshots, an administrator included.
**Release** (#266). *Menu → Release…* turns the project into a link that plays only the game:
no editor, no sign-in, the game view over the whole browser window on the page colour. **Publish**
copies the project as the server has it stored — the autosave is written first, and nothing is
published while anything is unsaved, the same rule as for a snapshot. The copy is frozen: what is
changed in the editor afterwards reaches players only with **Update release**, which replaces the
copy and keeps the link. The dialog says when the copy was taken and whether the saved project has
moved on since, and shows the link with **Copy link** and **Open**. **Take down** asks, then makes
the link dead; publishing again afterwards hands out a new one. Deleting the project takes its
release down. The link is `play.html?game=<token>`, the token 128 random bits and the only key.
The game on the link always starts in the start scene (the *Active scene* switch is the editor's
alone), zoomed like the game view's **Auto**: the largest zoom step at which the whole camera fits,
never a scale between the steps. It waits for a click or Enter before it starts — browsers play no
sound before one, and the keyboard would have nowhere to go. Every key belongs to the game, Tab
included. A game that stops itself (`Stop game`) offers to play again; `Restart game` and
`Load scene` work as in the editor. Persistent variables are saved in the player's browser under
the link, apart from the editor's own saves. Plugins are not loaded on the link; the dialog warns
when the project uses any, and when it has no scene to play.
**Toolbar and panels.** The toolbar carries five **icon buttons** and no captions (#183): the **Toolbar and panels.** The toolbar carries five **icon buttons** and no captions (#183): the
**menu** (three bars) on the left with *New project*, *Open project…*, *Snapshots…* and *Project settings…*, **menu** (three bars) on the left with *New project*, *Open project…*, *Snapshots…*, *Release…* and *Project settings…*,
**undo** and **redo** beside it, and **play** and **pause** in the middle — on the middle of the **undo** and **redo** beside it, and **play** and **pause** in the middle — on the middle of the
window, not of the space the row has left over — where play turns into a stop square while a run is window, not of the space the row has left over — where play turns into a stop square while a run is
on, and pause into a second play sign while it is held. On the right stand the on, and pause into a second play sign while it is held. On the right stand the
+2 -1
@@ -15,7 +15,8 @@ npm run build # writes dist/ and regenerates index.html
php tools/make-user.php <name> <password> [admin|user] # an account; every tool signs in (see Accounts) php tools/make-user.php <name> <password> [admin|user] # an account; every tool signs in (see Accounts)
php tests/php/accounts.test.php # the API's account rules, no server needed (CI's gate job) php tests/php/accounts.test.php # the API's account rules, no server needed (CI's gate job)
php tests/php/snapshots.test.php # the snapshot store (#231), the same way php tests/php/snapshots.test.php # the snapshot store (#231), the same way
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1562 editor checks, screenshots in /tmp/gm-e2e-*.png php tests/php/releases.test.php # the release store (#266), the same way
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1574 editor checks, screenshots in /tmp/gm-e2e-*.png
GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 — a subset while working on one step GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 — a subset while working on one step
./e2e.sh start [GM_STEPS] # the same run detached, see below ./e2e.sh start [GM_STEPS] # the same run detached, see below
./e2e.sh status # live progress, or the last run's result ./e2e.sh status # live progress, or the last run's result