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>
84 lines
3.4 KiB
JavaScript
84 lines
3.4 KiB
JavaScript
import js from '@eslint/js';
|
||
import prettier from 'eslint-config-prettier';
|
||
import globals from 'globals';
|
||
import tseslint from 'typescript-eslint';
|
||
|
||
/**
|
||
* Formatting is Prettier's job – `eslint-config-prettier` switches every stylistic rule off,
|
||
* so a rule here always means "this is a mistake", never "this looks different".
|
||
*/
|
||
export default tseslint.config(
|
||
// `rpgmz/` is gitignored: a copy of the RPG Maker MZ runtime kept next to the project to read the
|
||
// autotile tables from (#118). It is nobody's source, so it is not linted either.
|
||
{ ignores: ['dist/**', 'data/**', 'node_modules/**', 'rpgmz/**', 'wiki/**', 'index.html', 'play.html'] },
|
||
|
||
// Type-aware linting for everything the tsconfig covers (src, tests, vite.config.ts).
|
||
{
|
||
files: ['src/**/*.ts', 'tests/**/*.ts', 'vite.config.ts'],
|
||
extends: [js.configs.recommended, ...tseslint.configs.recommendedTypeChecked],
|
||
languageOptions: {
|
||
parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
|
||
globals: globals.browser,
|
||
},
|
||
rules: {
|
||
// The duplicate `import` statements the last refactoring round had to find by hand.
|
||
'no-duplicate-imports': 'error',
|
||
// `tsc` already reports unused locals; keep ESLint quiet about the same thing.
|
||
'@typescript-eslint/no-unused-vars': 'off',
|
||
// Off by design: the codebase builds objects from closures (`createFieldRow` returns
|
||
// `{ setValue }`, test fixtures return `{ sceneOf, object }`). Those have no `this`,
|
||
// but the rule cannot tell them from class methods and fired ~50 times on them.
|
||
'@typescript-eslint/unbound-method': 'off',
|
||
// A `?? []` on something that is never nullish hides that the type says otherwise.
|
||
'@typescript-eslint/no-unnecessary-condition': 'error',
|
||
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
|
||
'@typescript-eslint/consistent-type-imports': ['error', { fixStyle: 'inline-type-imports' }],
|
||
'@typescript-eslint/no-import-type-side-effects': 'error',
|
||
// Async work that nobody awaits silently swallows its rejection.
|
||
'@typescript-eslint/no-floating-promises': 'error',
|
||
'@typescript-eslint/no-misused-promises': ['error', { checksVoidReturn: false }],
|
||
eqeqeq: ['error', 'always', { null: 'ignore' }],
|
||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||
'prefer-const': 'error',
|
||
'object-shorthand': ['error', 'properties'],
|
||
},
|
||
},
|
||
|
||
// Node scripts: plain JavaScript, no type information available.
|
||
{
|
||
files: ['tools/**/*.mjs'],
|
||
extends: [js.configs.recommended],
|
||
languageOptions: {
|
||
ecmaVersion: 2023,
|
||
sourceType: 'module',
|
||
globals: globals.node,
|
||
},
|
||
rules: {
|
||
'no-duplicate-imports': 'error',
|
||
eqeqeq: ['error', 'always', { null: 'ignore' }],
|
||
'prefer-const': 'error',
|
||
},
|
||
},
|
||
|
||
// Editor plugins: browser modules that get everything through `api`, so they import nothing.
|
||
{
|
||
files: ['plugins/**/*.js'],
|
||
extends: [js.configs.recommended],
|
||
languageOptions: {
|
||
ecmaVersion: 2023,
|
||
sourceType: 'module',
|
||
globals: globals.browser,
|
||
},
|
||
rules: {
|
||
'no-restricted-syntax': [
|
||
'error',
|
||
{ selector: 'ImportDeclaration', message: 'A plugin gets everything through `api`.' },
|
||
],
|
||
eqeqeq: ['error', 'always', { null: 'ignore' }],
|
||
'prefer-const': 'error',
|
||
},
|
||
},
|
||
|
||
prettier,
|
||
);
|