# Web build

The game exports to WebAssembly + WebGL2. The export is **one folder** —
`index.html` and everything it loads, and nothing else — so putting the game on
the web is copying that folder onto any static host.

## One command

```sh
python tools/deploy.py
```

which does, in order:

1. **exports** the `Web` preset into a *clean* `build/web/` (a stale `index.wasm`
   beside a fresh `index.js` is a bug nobody can read);
2. **stamps** it with the commit it came from — `build.json`, generated, never
   committed, shown at the foot of the loading page so a bug report can name the
   exact build — and drops in `_headers` and `.htaccess` for the host;
3. **checks** the folder really is a game and really is self-contained: the four
   files that must exist and be non-empty, no unreplaced `$GODOT_` placeholder,
   and every local `src`/`href` in `index.html` present in the folder. A file
   that is missing now fails here, on your machine, and not after the upload;
4. **zips** it into `dist/masterpiece-or-not-html5.zip` with `index.html` at the
   **root of the archive**, which is where itch.io looks;
5. **pushes** that zip to itch.io with `butler`.

Any step whose credential is missing is skipped with a line saying so, and the
run still succeeds: a missing itch.io token must never break a build.

```sh
python tools/deploy.py --no-itch      # build and zip, keep it local
python tools/deploy.py --zip-only     # re-zip build/web as it stands
python tools/deploy.py --no-threads   # for a host that cannot set headers (below)
python tools/deploy.py --serve        # build, then serve it locally
python tools/deploy.py --status       # ask butler what it can see; changes nothing
```

## Configuration — `.env`

Copy `.env.example` to `.env` (git-ignored) and fill in what you have. A real
environment variable beats the file, which is what makes this work in CI.

| Key | What it does |
| --- | --- |
| `GODOT` | the engine binary; defaults to the one this project is built with, then `godot` on the PATH |
| `SITE_URL` | makes the social card's `og:url` / `og:image` absolute — a scraper does not resolve a relative one |
| `ITCH_PROJECT` | `user/game`, **without** the channel |
| `ITCH_CHANNEL` | the build slot; `html5` is the one that plays in the browser |
| `ITCH_USERVERSION` | what to call the build on itch; defaults to the commit hash |
| `BUTLER_API_KEY` | from itch.io/user/settings/api-keys — or run `butler login` once |

Check before committing: `git check-ignore .env`.

## itch.io

There is no simple REST upload for itch builds: it goes through
[butler](https://itch.io/docs/butler/), the official CLI, which diffs and sends
only the changed bytes. Install it and put it on the PATH.

On the game's page: *Kind of project* = **HTML**, tick *This file will be played
in the browser*, and — for this build — tick **SharedArrayBuffer support** (see
below). Keep **one** playable upload: itch embeds the one at the top of the list,
which can otherwise be an old manual upload. In the embed, 1280×720 with the
fullscreen button.

A cross-origin-isolated page — which the threads build is — makes the browser
drop itch's own enlarge icon: itch serves it without a
`Cross-Origin-Resource-Policy` header, and the console says so on every load.
Nothing on this side can fix that, and nothing needs to: the game carries its
own full-screen button, bottom right, drawn in the page (`shell.html`) where no
header can reach it. The complaint in the console is itch's, and cosmetic.

## Your own web server

Copy `build/web/` to the document root (or any subfolder) and serve it. It is
static: no build step, no runtime dependency, nothing fetched from a CDN.

**The one requirement:** this build uses threads, which need `SharedArrayBuffer`,
which browsers only expose to a **cross-origin isolated** document. The host must
send, on the page:

```
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
```

- **Netlify / Cloudflare Pages** — the `_headers` file is already in the folder.
- **Apache** — the `.htaccess` is already in the folder (needs `mod_headers`).
- **nginx** — no drop-in file; add to the location block:
  ```nginx
  add_header Cross-Origin-Opener-Policy same-origin;
  add_header Cross-Origin-Embedder-Policy require-corp;
  add_header Cross-Origin-Resource-Policy cross-origin;
  types { application/wasm wasm; }
  ```
- **GitHub Pages** — cannot set headers. Use `--no-threads`.

Without the headers the page says so plainly instead of hanging.

`python tools/deploy.py --no-threads` exports the `Web (no threads)` preset
instead: it runs **anywhere**, with no headers at all, at some cost to
performance and audio latency.

Under COEP `require-corp`, anything the page pulls cross-origin must itself send
`Cross-Origin-Resource-Policy: cross-origin` — including the art server's images.

## The art server

The build bundles no paintings: every run and canvas is fetched from the REST API
at runtime (see `../do.md`). A web export has **no environment variables**, so the
base URL is baked into the project setting `application/config/api_base` before
exporting. The browser must **trust the API's TLS certificate** — the
`API_INSECURE_TLS` escape hatch is native-only.

The API lives on its own host (`masterpieceornot-api.allweb.fun`), not the one
the game is served from, so **every call from the browser is cross-origin** and
the server's CORS headers are what let the game run at all. It already sends
`Access-Control-Allow-Origin: *`, which covers the reads. The one call that asks
more of it is `POST /api/run/result`: `Content-Type: application/json`, plus
`X-Api-Token` where a token is set, is enough to make the browser send a
**preflight** `OPTIONS` first, and the server has to answer that one with
`Access-Control-Allow-Methods` and `Access-Control-Allow-Headers` or the result
of the run is quietly never posted. The game itself never sees the difference:
its requests go through the engine's HTTP, in CORS mode, so
`Cross-Origin-Resource-Policy` does not enter into it: that header is for what
the *page* pulls, not what the game fetches.

## The page itself

`web/shell.html` is the custom HTML shell the export is poured into (the Godot
placeholders — `$GODOT_URL`, `$GODOT_CONFIG`, `$GODOT_SPLASH` and the rest — are
substituted at export time; `tools/deploy.py` fails the build if any survives).
It carries the gallery-dark loading card, the game's own logo on paper, the
social card, and the same two promises the game keeps inside:
`prefers-reduced-motion` stills what little moves, `prefers-contrast: more`
firms the lines. See `../ACCESSIBILITY.md`.

## Test locally

Most local servers — VS Code Live Server included — do not send the isolation
headers, so the threads build reports them missing.

```sh
python web/serve.py            # serves build/web on http://localhost:8000, headers set
python web/serve.py build/web 8080
```

In the console, `crossOriginIsolated` must be `true`.

## Cache

File names do not change between versions, so a browser and a CDN will both keep
the old build until told otherwise. After a deploy: purge the CDN for the
hostname, and **Ctrl+F5** locally.

## Embed in your own page

Serve `build/web/` somewhere and iframe it — see `embed-example.html`. Both your
page and the iframe source need the two headers for the threads build to start.
