# Fradiation Games docs > Every page of https://www.fradiation.games/docs in one file. # Publish a game > The full path from invite code to released game. Create a draft, prepare and upload a build (in the browser or from a terminal), add a cover, preview it and submit it to containment. Most steps happen in your browser, at `/dev`. Builds can also go up from a terminal or a coding agent, with an API token. The last step is a 48-hour vote by strangers. ## Checklist An agent connected to the [MCP server](/docs/agents#connect-your-agent) can do steps 3 to 8: create the draft, prepare and deploy the build, upload a cover, fill in the description, and submit it when you say so. Without MCP, an agent can do steps 4 to 6 with an API token. 1. Sign in with Discord or GitHub and claim a handle at `/welcome`. That's your badge. 2. Redeem an invite code at `/dev`. Codes come from an admin. 3. Create the game with **New game** on My games (`/dev`). The slug can't change later. 4. Build for the web: `index.html` at the top, relative paths, allowed file types only. Run `node check-build.mjs ` until it is clean. 5. Upload the zip or folder at `/dev/`, or deploy it from a terminal: `node check-build.mjs deploy --game `. Add patch notes if you want them. 6. Add a cover on the same page. 7. Make sure the game has a description. Click **Preview** and play the draft. 8. Hold the **Hold to submit** button. A draft needs a live build, a cover and a description. 9. Wait 48 hours. Released games are listed on `/games`. Buried games can come back after 7 days with a new build. ## Get access Developer access is invite-only. 1. Sign in with Discord or GitHub. 2. Claim a handle at `/welcome`. That's your badge. Your handle appears on your games and your profile. 3. Open `/dev`. Without developer access the page shows an **Invite code** field. 4. Enter the code. The format is `XXXX-XXXX-XXXX`. Case and dashes don't matter. Every code has a use limit, and some expire. A code that is used up, expired or mistyped is refused. `/dev` then becomes **My games**, which is also in the menu under your avatar. ## Create the game Click **New game** on My games. `/dev/new` opens the same form. | Field | Rules | | --- | --- | | Title | Required. 1 to 60 characters. Editable later. | | Slug | Permanent. See below. | | Engine | Three.js, Unity, Godot or HTML5. A label on the game's card. It doesn't change how the build runs or what the uploader accepts. | | Origin | Handmade, AI-assisted or AI-generated. Shown on every card. Be straight about it. | | What did AI make? | Appears for AI-assisted and AI-generated. Pick at least one of Code, Art, Audio and Text. | | Description | Up to 600 characters. Optional now, required before containment. A sentence or two: what it is and how it plays. | ### Slug The slug is the game's own origin: `https://.containment.cloud`. That is where its saves live, so it can't be changed. - 1 to 47 characters. - Lowercase `a-z`, digits `0-9` and single dashes. No `--`. - Starts and ends with a letter or a digit. - Some names are reserved, for example `www`, `api`, `admin`, `dev`, `docs`, `play` and `test`. Anything starting with `xn-` is refused. The form fills the slug from the title until you edit it, and checks availability as you type. The **Create draft** button stays disabled until the slug is free. ### AI disclosure Handmade means no AI flags. AI-assisted and AI-generated need at least one. The form and the server both refuse any other combination. The new game is a **draft**. Only you and admins can see it. You can have ten drafts at once. Everything except the slug can be edited later under **Details** on the game's page. ## Prepare a build A build is the web export of your game. Before you upload: - `index.html` sits at the top of the build, or `fradiation.json` names another `entry`. - Every path is relative. `assets/game.js` works. `/assets/game.js` breaks. - Every file has an extension on the allowed list. - The game fills its window and handles resize. `fradiation.json` at the top is optional. It sets `entry`, `viewport` and `threads`, and declares mutations and scoreboards. See [fradiation.json](/docs/fradiation-json). Games talk to the site through the [SDK](/docs/sdk), which is also optional. | Engine | Guide | | --- | --- | | Unity | [Unity](/docs/unity) | | Godot | [Godot](/docs/godot) | | Three.js, Vite, plain HTML5 | [Web](/docs/web) | [Builds and uploads](/docs/builds) has every limit, path rule and file type. The machine-readable version is `https://www.fradiation.games/docs/build-rules.json`. Check a build before you open the browser: ```bash node check-build.mjs ``` Download the script and see the other commands in [Check a build first](/docs/builds#check-a-build-first). ## Upload the build Open `/dev/` and find the **Upload a build** panel. To upload from a terminal instead, see [From a terminal](#from-a-terminal). 1. Drop a `.zip` on the tray, or click **browse**. To upload a folder, click **pick a folder**. Dropping a folder doesn't work. 2. The browser reads the build and checks it. Problems are listed, up to eight, and **Upload build** stays disabled until none remain. 3. Set the **Stage size**. It appears only if `fradiation.json` has no `viewport`. Width is 160 to 4096 and height is 120 to 4096. The default is 1280 by 720, or the size of your current build. The site letterboxes the game to that aspect ratio. 4. Write **Patch notes** if you want to. Up to 2,000 characters, shown in the game's update log. 5. Choose what happens when the build is sealed. The choices depend on the game, as the table below shows. 6. Click **Upload build**. ### When it's sealed | Game status | Choice | Result | | --- | --- | --- | | Draft or buried | none | The upload becomes the live build. A draft always runs its newest build. | | In containment or released | **Go live now** (default) or **Upload only** | Go live now makes it the live build. Upload only seals it and leaves the live build alone. | | Any game with a playtest | **Send to testers** (default) or **Keep their build** | Testers get the new build on their next load, with your patch notes, or keep the one they have. | ### What the uploader checks Before it sends anything: - The entry file is at the top. - At most 1,000 files, 500 MB in total and 200 MB per file. - Every path follows the path rules and every file type is allowed. - `fradiation.json`, if present, is valid JSON. The site then checks `fradiation.json` against its schema and confirms the entry file is in the file list. After the upload it checks that every file arrived at its full size. Then it seals the build. A sealed build never changes. ### Progress The bar moves through **Checking files**, **Requesting a grant**, **Uploading** and **Verifying and sealing**. Stay on the page. Leaving mid-upload asks you to confirm. If an upload fails or you cancel, start it again. The site cleans up what it left behind. Only files the game doesn't already have are sent. An update that changes one file uploads one file. Each build in the Builds list shows how much it sent, for example `(1.6 KB new)` or `(nothing new)`. ### From a terminal `check-build.mjs` uploads the same way the browser uploader does, with an API token instead of your browser session. 1. Under **Agents & tokens** (`/dev/agents`), create a token in the **API tokens** panel, for all your games or one. Copy it: it's shown once. 2. Set it as `FRADIATION_TOKEN` in your shell or your CI secrets. 3. Deploy the build folder or zip: ```bash node check-build.mjs deploy Builds/Fradiation --game --notes "What changed" ``` It checks the build first, uploads only what the game doesn't have, and seals it. `--live` makes it live and `--playtest` sends it to testers, with the same rules as the choices above. `node check-build.mjs live ` ships or rolls back later. See [Deploy from the command line](/docs/agents#deploy-from-the-command-line) for every option and the API behind it. ### Limits - 20 uploads per rolling 24 hours, across all your games, from the browser and from tokens together. Failed and cancelled uploads count. - Files over 64 MB upload in parts. This is automatic. - Storage has a soft limit of 2 GB per developer. See [Retention and storage](/docs/builds#retention-and-storage). ## Add a cover Use the **Cover** panel on the game's page. 1. Drop an image on it, or click to browse. PNG, JPEG, WebP or AVIF. 2. The browser crops it to 4:3 from the center and re-encodes it as an 800 by 600 JPEG. The upload limit is 2 MB. 3. It uploads at once. There is no separate save. A draft needs a cover before it can be submitted. You can replace the cover at any time. From a terminal or an agent: `node check-build.mjs cover cover.png --game ` with an API token, or the MCP server's `upload_cover` (a presigned URL the agent PUTs the image to). The site does the same crop, from images up to 15 MB. See [How covers work](/docs/agents#how-covers-work). ## Preview the draft Once the draft has a build, click **Preview** at the top of `/dev/`. That opens `/games/`, the real game page. A **Draft** notice says only you and admins can see it. Click **Start** to run the build inside the cabinet. - SDK calls run in test mode. `ready()` reports the mode `preview`. Calls are checked, and nothing is kept. - A mutation key the build doesn't declare, a negative score and a score above the board's `max` are rejected. - A toast marks each call: `Test unlock: ` or `Test score: `. - Your own plays of your own draft don't count as plays. - **Open** on a build in the Builds list loads it directly on `https://.containment.cloud//`, outside the cabinet. There is no SDK connection there. Other players can't see a draft. To get feedback before you submit, start a private playtest. See [Playtests](/docs/playtests). ## Submit to containment A draft can be submitted when the status panel shows all three checks: - A live build. - A cover. - A description. Hold the **Hold to submit** button for about a second. The game moves to **In containment** and is listed on `/containment`. For the next 48 hours, players who have played it vote to release it or bury it. You can't vote on your own game, and the tally is hidden until judgment closes. You can keep uploading builds while it's in there. Send people the link to your game's page. Votes come only from players who have played it. When judgment closes, the game is released or buried. [Containment](/docs/containment) has the exact rules, the payouts and the way back from a burial. ## After release A released game has the same page at `/dev/`. New builds go live when you say so. ### Updates Upload a new build. Choose **Go live now** and players get it on their next load. Choose **Upload only** and the build is sealed but not live. Make it live later from the Builds list. From a terminal, `deploy` without `--live` is **Upload only**, and `deploy --live` is **Go live now**. Patch notes show in the game's update log. Saves survive updates, because the game's origin doesn't change. ### Rollbacks Every sealed build that still has its files can be made live. Open the Builds list, find the build and click **Make live**, or run `node check-build.mjs live --game `. That is how you ship and how you roll back. Builds whose files were deleted can't be made live. Upload them again. Pin a build to keep its files. See [The builds list](/docs/builds#the-builds-list). ### Playtests A playtest is a private link that serves any sealed build you choose, independent of the live one. Use it to try your next update on testers before players get it. With a playtest open, an upload offers **Send to testers** or **Keep their build**. See [Playtests](/docs/playtests). ### If it was buried A buried game keeps its page, and its developer can send it back for another round after 7 days with a new build. See [Coming back](/docs/containment#coming-back). --- # Builds and uploads > What a build is and what the uploader accepts. Zip or folder, limits, path rules, allowed file types, relative paths, the sandbox, caching, the builds list, retention and the storage limit. A build is one upload of your game's web files. Once it is sealed, nobody edits it, including you. This page is the reference. For the walkthrough, see [Publish a game](/docs/publish). The limits, allowed file types and path rules here are generated from the code the uploader and the server use, and published as `https://www.fradiation.games/docs/build-rules.json`. If this page and that file disagree, the file wins. ## What a build is - A set of files that forms a web game, with an entry page. - Uploaded at `/dev/` or with `node check-build.mjs deploy`, checked, and then sealed. A sealed build never changes. - Identified by a 16-character hex id. The Builds list shows the first 7. - Served at `https://.containment.cloud//`. - One of a game's builds is **Live**. That is the one players get. Each upload creates a new build. To change a file, upload a new build. ## Zip or folder You give the uploader either a `.zip` or a folder. Drop a zip on the tray or click **browse**. For a folder, click **pick a folder**. Dropping a folder doesn't work. The browser unzips and checks the build before anything is sent. Picking the folder skips zipping and unzipping, and doesn't load the whole build into memory. ### The entry file `index.html` must be at the top of the build. To use another page, set `entry` in `fradiation.json` to its path, for example `"entry": "game/start.html"`. The path is relative to the top of the build and must match a file exactly. A URL that ends in `/` serves the `index.html` in that folder. ### One top-level folder If every file sits inside one top-level folder, that folder is dropped, once. This makes a zipped project folder work. ```text MyGame.zip MyGame/index.html becomes index.html MyGame/assets/a.js becomes assets/a.js ``` A second level of folders isn't dropped. If the entry file still isn't at the top, the upload stops with `No index.html at the top of the build.` ### Junk files These are skipped silently. They aren't uploaded and they don't count toward the limits. - `__MACOSX` folders - `.DS_Store` - `Thumbs.db` - `desktop.ini` - `.git` folders - Any file or folder whose name starts with `._` ### fradiation.json `fradiation.json` at the top of the build is read for `entry`, `viewport`, `threads`, `mutations` and `boards`. The uploader doesn't store it as a build file, and it isn't served from your game's origin. See [fradiation.json](/docs/fradiation-json). ## Limits Sizes use binary units: 1 MB is 1,048,576 bytes, the same as the uploader. | Limit | Value | | --- | --- | | Files per build | 1,000 | | Total size | 500 MB. Every path counts, even two paths with identical bytes. | | One file | 200 MB | | Path length | 240 characters | | Files uploaded in parts | Files over 64 MB, automatically. Not a limit. | | Stage size | 160 to 4096 wide, 120 to 4096 high | | Patch notes | 2,000 characters | | Mutations per upload | 30, of which at most 3 `gamma` and 1 `cherenkov` | | Scoreboards per upload | 10 | | Uploads | 20 per rolling 24 hours, across all your games. Failed and cancelled uploads count. | | Upload time | The upload grant lasts 3 hours. | | Cover | 800 by 600 JPEG made in your browser. Up to 2 MB. | | Storage | 2 GB per developer, soft. See [Retention and storage](#retention-and-storage). | ## Path rules Every file path in the build must follow these rules. - Relative, with forward slashes, and no leading slash. - At most 240 characters. - Each folder and file name starts with a letter, a digit or an underscore. - Each name uses only `A-Z a-z 0-9 . _ space ( ) + -`. - No name ends in a dot or a space. - No path appears twice. - The file has an extension from the allowed list. What that rules out: - Dotfiles and dot folders such as `.gitignore`, `.htaccess` and `.well-known/`. - Non-ASCII names such as `é.png` or `日本語.png`. - `#`, `%`, `&`, `[`, `]`, `@`, `=`, `,` and `'` in names. - Backslashes in paths. Paths are case-sensitive on the server. `Assets/a.png` and `assets/a.png` are different files, and a reference with the wrong case returns 404. Windows and most macOS setups ignore case locally, so a mismatch shows up only after the upload. Spaces are allowed, but they turn into `%20` in URLs. Prefer dashes or underscores. ## Allowed file types Extensions are matched without regard to case. Anything not on this list is refused, and so is a file with no extension. | Kind | Extensions | | --- | --- | | Pages and scripts | `html` `htm` `js` `mjs` `css` | | Text and data | `json` `map` `txt` `xml` | | WebAssembly and engine data | `wasm` `data` `pck` `bin` `unityweb` `mem` `basis` | | Images | `png` `jpg` `jpeg` `gif` `webp` `avif` `ico` `svg` `ktx2` | | 3D models | `glb` `gltf` | | Audio | `mp3` `ogg` `wav` `m4a` | | Video | `mp4` `webm` | | Fonts | `woff` `woff2` `ttf` `otf` | Refused, for example: `exe`, `dll`, `zip`, `md`, `csv`, `bmp`, `tga`, `dds`, `flac`, `aac`, `opus`, `fbx`, `obj`. Convert or remove them. The machine-readable list is in [build-rules.json](https://www.fradiation.games/docs/build-rules.json). ### Pre-compressed files Add `.br` (Brotli) or `.gz` (gzip) after an allowed extension: `Game.wasm.br`, `Game.framework.js.gz`, `Game.data.br`. - The file is served with the `Content-Type` of the inner extension and the matching `Content-Encoding`. - Unity's Brotli and gzip builds work with Decompression Fallback off. - `Game.data.gz` is served as `application/gzip`. That is Unity's Safari workaround. - The inner extension must be allowed. `save.tar.gz` is refused. - The server labels a file by its name and doesn't read the bytes. A `.br` file must really be Brotli. ## Relative paths Every build has its own URL: ```text https://.containment.cloud//index.html https://.containment.cloud//assets/game.js ``` The build id changes with every upload. A path that starts with `/` skips it. `/assets/game.js` resolves to `https://.containment.cloud/assets/game.js`, which isn't inside any build, and the server answers 404. Wrong: ```html ``` Right: ```html ``` The same goes for CSS `url()` values and anything your engine or bundler generates. In Vite, set `base: "./"`. Other bundlers have an equivalent public path setting. Never hard-code the slug or the build id. Absolute URLs to other origins are fine, for example an API. With `threads` on, they need extra headers. See [Threads](#threads). ## The frame Your game runs in a sandboxed iframe on its own origin. It loads after the player clicks **Start**. **Restart** reloads it. ```html ``` Every file a game origin serves also carries this header: ```text Content-Security-Policy: frame-ancestors ; base-uri 'self'; form-action 'none'; object-src 'none' ``` `allow-same-origin` gives the game its real origin, so `localStorage` and `IndexedDB` work. Unity PlayerPrefs and Godot `user://` saves depend on that. It is safe because the game's origin is a different site from Fradiation. What your game can do: - Run scripts, WebAssembly and WebGL. - Use pointer lock, fullscreen, gamepads, motion sensors and XR. - Open popups, and use `alert`, `confirm` and `prompt`. - Lock the screen orientation. - Call external APIs with `fetch`. - Store data on its own origin. - Talk to the site through the [SDK](/docs/sdk). What it can't do: - Touch the main site. It is on another origin, and its only channel is the SDK. - Navigate the page around it. `allow-top-navigation` isn't granted. - Submit forms. `allow-forms` isn't granted, and the CSP sets `form-action 'none'`. - Load plugins (`object-src 'none'`) or point `` at another origin (`base-uri 'self'`). - Be embedded on another site. `frame-ancestors` allows only Fradiation. - Use the camera, the microphone or geolocation. They aren't in the `allow` list. - Count on downloads. `allow-downloads` isn't in the sandbox list, so browsers can refuse a download the game starts. ### Storage Each game has its own origin, `https://.containment.cloud`. Its `localStorage`, `IndexedDB` and Unity cache belong to that origin. - No other game can read them, and neither can the site. - All of a game's builds share them, because the origin doesn't change with the build. Saves survive updates. Keep your save format readable by newer and older builds. - The slug is permanent, so the origin is too. ## Viewport The cabinet letterboxes the game to the aspect ratio of `viewport`. Set `viewport` in `fradiation.json`, or enter a stage size in the uploader. It is the game's native resolution. Make the game fill its window, 100% wide and 100% high, and handle resize. Don't hard-code a canvas size. Players can also go fullscreen. ```html ``` ## Threads > [!WARNING] > Threaded builds don't run on the site yet. A frame only gets `SharedArrayBuffer` when every page above it is cross-origin isolated too, and Fradiation's game pages aren't, so `crossOriginIsolated` is false inside the cabinet. Export without threads: in Unity leave multithreading off, and in Godot use 4.3 or later with Thread Support off. `"threads": true` is accepted and already changes how the build is served, ready for when the game pages are isolated: - The build is served under `https://.containment.cloud/coi//…`. - Files carry `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp`. - Everything the game loads from another origin, scripts, fonts, images and `fetch` calls, must send CORP or CORS headers. Bundle everything instead. - Files from your own build already send `Cross-Origin-Resource-Policy: cross-origin`. - The setting belongs to each build. Leave it `false` unless the engine needs threads. ## Caching - HTML files (`html`, `htm`) are sent with `Cache-Control: no-cache` and revalidated on every load. - Every other file is sent with `Cache-Control: public, max-age=31536000, immutable`, a year. - A new build has a new URL, so an update never fights a cache. - You can't patch a file in a sealed build. Upload a new build. ## Content-addressed uploads Only files the game doesn't have yet are uploaded. 1. The uploader (the browser uploader on the game's Manage page, or `check-build.mjs deploy`) reads every file and computes its SHA-256. 2. It sends the site the file list: paths, sizes and hashes. 3. The site answers with the hashes the game doesn't have. 4. The uploader sends only those files, straight to the play server, four at a time. Files over 64 MB go up in parts. 5. The site checks that every file arrived at its full size, writes the build's file list and seals the build. What follows from it: - A file's identity is its bytes. Renaming a file doesn't upload it again, and two paths with identical bytes are stored once. - Bytes that don't change aren't sent. A hashed bundler filename such as `index-a1b2c3.js` is fine. Anything that changes a file's bytes, such as an embedded build timestamp, sends it again. - Storage is per game. Nothing is shared between games. - The Builds list shows what each upload sent: `(1.6 KB new)` or `(nothing new)`. The uploader shows how much was left unchanged, and so does `deploy` (`newFiles` and `uploadedBytes` with `--json`). - The 500 MB limit counts every path. Storage counts each distinct file once per game. ## The builds list The **Builds** panel on `/dev/` lists every build, newest first: the first 7 characters of its id, date, file count, size, how much was new, stage size and patch notes. | Tag | Meaning | | --- | --- | | Live | The build players get. A draft's live build is always its newest. | | Testing | The build a playtest serves. | | Pinned | Its files are kept whatever retention says. | | Uploading | The upload hasn't finished. | | Action | What it does | | --- | --- | | Open | Opens the build directly on its origin, outside the cabinet. The SDK has no site to connect to there. | | Test | Sends the build to your playtest. Shown when a playtest exists. | | Make live | Makes the build the live one. That is how you ship an update and how you roll back. | | Pin | Keeps its files. Click again to unpin. | | Hold to delete files | Deletes the build's files now. See below for when it appears. | Any sealed build that still has its files can be made live. Players get it on their next load. A build whose files were deleted can't be made live, sent to testers or opened. Upload it again as a new build. From a terminal, `node check-build.mjs builds --game ` prints the same list, and `live ` and `playtest ` do what **Make live** and **Test** do. See [Deploy from the command line](/docs/agents#deploy-from-the-command-line). ## Retention and storage There is no build quota. Old builds lose their files on a schedule, and their rows stay. ### What keeps its files For each game: - The live build. - The build its playtest serves, even while the playtest is closed. - Pinned builds. - The 5 newest builds that still have files. ### Everything else Any other build is scheduled for deletion. Its files are deleted 30 days later. - The 30 days count from when the site first notices the build is no longer needed. That happens after an upload, **Make live**, a change to the playtest build or a pin, and when you load `/dev` or `/dev/`. A late notice only makes the wait longer. - The Builds list shows `Files will be deleted . Pin it to keep them.` - If the build becomes needed again before then, the clock resets. - When the files go, the build's row, patch notes, playtest feedback and judgment history stay. The list shows `Files deleted .` - A file shared by several builds stays until the last build that uses it loses its files. - Failed and abandoned uploads are deleted with their files on the next sweep. An upload left open for 4 hours is marked failed. ### Storage limit The soft limit is 2 GB per developer. - It counts every build that still has files, including uploads in progress. Each distinct file counts once per game. Covers aren't counted. - One upload may cross the limit. - Once your total is at or over 2 GB, the next upload first deletes the files of builds already scheduled for deletion, without waiting for the 30 days. - The upload is refused only if you are still at or over the limit: `Your builds use X of 2 GB. Delete or unpin some to upload more.` - At 80% the game's page warns you. Every build that isn't live or testing then gets **Hold to delete files**. Before that, it appears only on builds already scheduled for deletion. - Deleting files removes the pin. It doesn't work on the live build or the testing build. `/dev` shows a storage meter and each game's size. ## Check a build first `check-build.mjs` is a zero-dependency Node 20 or later script. It checks a build against the real rules on your machine. ```bash curl -fsSL https://www.fradiation.games/skills/fradiation/check-build.mjs -o check-build.mjs ``` ```powershell Invoke-WebRequest -UseBasicParsing https://www.fradiation.games/skills/fradiation/check-build.mjs -OutFile check-build.mjs ``` ```bash node check-build.mjs node check-build.mjs --json node check-build.mjs pack [out.zip] ``` - The first command checks a folder or a zip. - `--json` prints a machine-readable report, for agents. It can go anywhere in the command. - `pack` takes a folder. It checks it, and zips it the way the uploader expects only if the check passes. Without `out.zip`, it writes `.zip` next to the folder. It won't write the zip inside the folder. - Exit code 1 means the build would be refused. With an API token in `FRADIATION_TOKEN`, the same script uploads: ```bash node check-build.mjs deploy --game [--live] [--playtest] [--notes "..."] ``` It runs the check first and uploads nothing if the check fails. See [Deploy from the command line](/docs/agents#deploy-from-the-command-line). The script is also in the agent skill at `https://www.fradiation.games/skills/fradiation/SKILL.md`. See [Working with agents](/docs/agents). ## Zipping The uploader refuses a zip whose entry names contain backslashes. Windows PowerShell 5.1 `Compress-Archive` can write them: ```text assets\a.js: paths must be relative, with forward slashes ``` Use one of these instead: 1. Pick the folder directly in the uploader. There is no zip. 2. `node check-build.mjs deploy `. It uploads the folder with an API token. There is no zip. 3. `node check-build.mjs pack `. It checks the build and zips it. 4. `tar` on Windows 10 or later, and on macOS. Both ship bsdtar, which writes zips. ```bash tar -a -c -f build.zip -C . ``` 5. On macOS or Linux, Info-ZIP: ```bash cd && zip -r ../build.zip . ``` > [!WARNING] > Git Bash and most Linux distributions ship GNU tar. It can't write zips. `tar -a -c -f build.zip` there writes a tar archive named `build.zip`, and the uploader says `That zip couldn't be read.` In Git Bash, call Windows' own tar: `/c/Windows/System32/tar.exe`. ## Errors | Message | Cause and fix | | --- | --- | | `No index.html at the top of the build.` | The entry file isn't at the top, after the one top-level folder is dropped. Move it, or set `entry`. | | `: paths must be relative, with forward slashes` | A backslash or a leading slash. See [Zipping](#zipping). | | `: file type not allowed` | The extension isn't on the list, or there is none. | | `: "" isn't an allowed file or folder name` | A name starts with a dot, uses a character outside the allowed set, or ends with a dot or a space. Rename it. | | ` is in the build twice.` | Two entries in the zip have the same path. | | ` is over 200 MB.` | One file is over the limit. | | ` files; the limit is 1000.` | Too many files. | | `; the limit is 500 MB.` | The build is too big. | | `There's nothing in it.` | No files after junk is skipped. | | `That zip couldn't be read.` | The file isn't a zip, or it is damaged. See [Zipping](#zipping). | | `fradiation.json isn't valid JSON.` | A syntax error. JSON has no comments and no trailing commas. | | `fradiation.json: : ` | A field breaks the schema. See [fradiation.json](/docs/fradiation-json). | | `Twenty uploads in a day is the limit. Try again tomorrow.` | The daily upload limit. | | `Your builds use X of 2 GB. Delete or unpin some to upload more.` | The storage limit. See above. | | ` files didn't arrive intact (). Upload the build again.` | The connection dropped, or a file was cut short. Upload again. | | `That build's files were deleted. Upload it again.` | The build was past retention. Upload it as a new build. | | `That token doesn't exist.`, `was revoked` or `expired on ` | `deploy` and the API only. Make a new token under Agents & tokens (`/dev/agents`) and set `FRADIATION_TOKEN` again. | | `This token only works for .` | A one-game token was used for another game. Leave out `--game`, or use a token for that game. | | `Pass --game . This token works for all your games: ...` | Say which game. | | `Start a playtest first.` | `playtest ` on a game with no playtest. Start one on the game's Manage page. (`deploy --playtest` still uploads, and warns.) | | `That upload isn't open any more. Start it again.` | The upload was finished, cancelled or abandoned (open for over 4 hours). Deploy again. | --- # fradiation.json > The optional file at the top of a build that sets the entry point, viewport and threads, and declares mutations and scoreboards. Every field, limit and icon name. One JSON file tells the site what your game can unlock and score. The site is very literal about it. The file is optional. Without it a build has no mutations and no scoreboards, the entry point is `index.html`, and the uploader asks you for a stage size. ## Where it goes - Name it `fradiation.json`, lowercase, and put it at the top of the build, next to `index.html` (or whatever your `entry` is). - In a zip where everything sits inside one top-level folder, that folder is dropped first. The file goes inside that folder, beside `index.html`. - It is plain JSON. No comments, no trailing commas. - The uploader reads it in your browser and does not store it as a build file. It isn't served from your game's origin. - The [Unity kit](/docs/unity) copies `fradiation.json` from your project root into the build. ## What a web upload reads Five fields: `entry`, `viewport`, `threads`, `mutations`, `boards`. Anything else in the file is ignored, including `$schema`. Title, slug, description, engine, origin and AI disclosure come from the game's details form, on its Manage page. Setting `"title"` or `"engine"` in this file does nothing. ## Example ```json { "$schema": "https://www.fradiation.games/fradiation.schema.json", "entry": "index.html", "viewport": { "width": 1280, "height": 720 }, "threads": false, "mutations": [ { "key": "first-blood", "name": "First Blood", "description": "Destroy your first drone.", "tier": "alpha", "icon": "crosshair" }, { "key": "untouched", "name": "Untouched", "description": "Clear a level without taking damage.", "tier": "beta", "icon": "shield" }, { "key": "waterfall-room", "name": "Damp Discovery", "description": "Find the room behind the waterfall.", "tier": "beta", "icon": "waves", "secret": true }, { "key": "boss-hard", "name": "No Mercy", "description": "Beat the final boss on Hard.", "tier": "gamma", "icon": "crown" }, { "key": "speedrun", "name": "Under Ten", "description": "Finish the game in under 10 minutes.", "tier": "gamma", "icon": "timer" }, { "key": "everything", "name": "Completionist", "description": "Collect every item in the game.", "tier": "cherenkov", "icon": "trophy" } ], "boards": [ { "key": "high-score", "name": "High score", "order": "desc", "format": "number", "max": 5000000 }, { "key": "fastest-clear", "name": "Fastest clear", "order": "asc", "format": "time", "max": 3600000 } ] } ``` The game refers to these by `key`: `fradiation.unlockMutation("first-blood")`, `fradiation.submitScore("fastest-clear", ms)`. See [The SDK](/docs/sdk). ## Fields ### Top level | Field | Type | Default | Notes | | --- | --- | --- | --- | | `entry` | string | `"index.html"` | The HTML file to load. Must match a file in the build exactly: a relative path, no leading `./` or `/`. | | `viewport` | object | none | Native size of the game. See below. Without it the uploader asks for a stage size (default 1280 x 720). | | `threads` | boolean | `false` | Serve the build cross-origin isolated. See below. | | `mutations` | array | `[]` | At most 30 per file, of which at most 3 `gamma` and 1 `cherenkov`. | | `boards` | array | `[]` | At most 10 per file. | ### viewport | Field | Type | Limits | | --- | --- | --- | | `width` | integer | 160 to 4096 | | `height` | integer | 120 to 4096 | The site letterboxes the game to this aspect ratio. Make the game fill its window (100% width and height) and handle resize. Don't hard-code a canvas size. ### threads `true` serves the build cross-origin isolated (COOP `same-origin`, COEP `require-corp`), for engines that need `SharedArrayBuffer`: multithreaded Unity, threaded Godot. Anything loaded from another origin must then send CORP or CORS headers, so bundle everything. Leave it `false` unless your engine needs threads. > [!WARNING] > Threaded builds don't run on the site yet: the game pages that frame them aren't cross-origin isolated, so `SharedArrayBuffer` is unavailable inside the cabinet. Export without threads for now. See [Threads](/docs/builds#threads). ## Mutations Mutations are a game's achievements. Each one is declared here and unlocked from game code with `unlockMutation(key)`. | Field | Type | Default | Limits | | --- | --- | --- | --- | | `key` | string | required | Matches `^[a-z0-9-]{2,40}$`: lowercase letters, digits and dashes, 2 to 40 characters. This is the id your game passes to the SDK. | | `name` | string | required | 1 to 40 characters | | `description` | string | required | 1 to 120 characters | | `tier` | string | required | `alpha`, `beta`, `gamma` or `cherenkov` | | `icon` | string | required | One of the names in [Icons](#icons) | | `secret` | boolean | `false` | See [Secret mutations](#secret-mutations) | ### Tiers Tiers run alpha, beta, gamma, cherenkov, from common to rare. Pick the tier by how hard the mutation is to earn. The site does not measure that for you. | Tier | Rads to the player | Cap per file | | --- | --- | --- | | `alpha` | 25 | none beyond the 30 total | | `beta` | 50 | none beyond the 30 total | | `gamma` | 100 | 3 | | `cherenkov` | 250 | 1 | Rads are paid the first time a signed-in player unlocks the mutation. Developers can unlock their own mutations but get no rads from their own games. The caps apply to the file in each upload, and the file as a whole is limited to 30 mutations. Secret mutations count toward the same caps. An upload over a cap is refused with a message such as `fradiation.json: at most 3 gamma mutations.` ### Secret mutations `"secret": true` hides a mutation until the player unlocks it. Before that it shows as a locked "?" with the name, icon and description withheld. Use it for things you don't want to give away. ### Icons `icon` must be one of these 100 names, exactly as written. They are [Lucide](https://lucide.dev/icons) icon names. ```text skull flame zap bomb crosshair shield shield-alert trophy crown star rocket ghost heart swords sword target timer gem key eye radiation biohazard atom bug sparkles medal award flag map compass footprints gauge infinity moon sun snowflake droplet anchor bot radar satellite plane car hand-fist dumbbell brain clover dice-5 puzzle gamepad-2 joystick coins package hammer wrench lock unlock hourglass siren tent tree-pine mountain waves wind cloud-lightning egg fish bird cat dog rabbit turtle axe shovel pickaxe wand-sparkles scroll cake pizza beer coffee music headphones camera film tv cpu battery-charging plug magnet flask-conical syringe pill bone ear hand thumbs-up party-popper fire-extinguisher shell ``` Any other name is refused. The full list is also in the [JSON Schema](#json-schema). ## Boards A board is a scoreboard. The game submits a number with `submitScore(key, value)` and the site keeps each player's best. | Field | Type | Default | Notes | | --- | --- | --- | --- | | `key` | string | required | Matches `^[a-z0-9-]{2,40}$`. | | `name` | string | required | 1 to 40 characters. | | `order` | string | `"desc"` | `"desc"`: higher is better. `"asc"`: lower is better, for times. | | `format` | string | `"number"` | `"number"`: shown as a whole number with separators. `"time"`: the value is in milliseconds, shown as `m:ss.mmm`. | | `max` | number | none | Greater than 0. Scores above it are rejected as implausible. | - Negative scores are always rejected. There is no minimum. - `max` is a ceiling for both orders. On an `"asc"` time board it rejects slow times, not fast ones. Set it above the slowest run you would accept. - A `"number"` board displays whole numbers. To keep two decimals, submit the value times 100. - The first score a player posts on a board pays 5 rads, once per board. Developers get none. ## How changes apply - Mutations and boards are upserted by `key` when a build is sealed, which is when its upload finishes. It does not wait for the build to go live. Changing a name, description, tier or icon changes it for the whole game at once. - Renaming a `key` makes a new mutation or board. The old one stays, along with anything players unlocked or scored on it. - Removing an entry from the file does not remove it from the game. - Caps are checked against the file in the upload, not against everything the game has ever registered. - A draft runs its newest build, so a draft picks up changes on the next upload. ## JSON Schema The schema for this file is at `https://www.fradiation.games/fradiation.schema.json`. Put it first in the file and editors that support `$schema`, VS Code included, give you autocomplete and inline errors: ```json { "$schema": "https://www.fradiation.games/fradiation.schema.json" } ``` Give the same URL to a coding agent when it writes the file. See [Working with agents](/docs/agents) for the other machine-readable files. ## Common mistakes - **Comments or trailing commas.** The uploader stops with `fradiation.json isn't valid JSON.` - **Wrong place.** The file must sit at the top of the build, not in a subfolder of it. - **Expecting the file to set the title.** `title`, `slug`, `description`, `engine`, `origin` and `ai` are ignored. Use the game's Manage page. - **An icon that isn't in the list.** The upload is refused with a message that names the field, for example `fradiation.json: mutations.2.icon: ...`. Lucide has many more icons than this list. Only these 100 are allowed. - **Keys with capitals or underscores.** `First_Blood` fails. `first-blood` works. - **Too many gamma or cherenkov mutations.** The limits are 3 and 1. The message reads `fradiation.json: at most 3 gamma mutations.` or `at most 1 cherenkov mutation.` - **A key in game code that isn't in the file.** The SDK does not throw. The call resolves with the same defaults as outside the site (`signedIn: false`), and nothing is kept. See [Testing](/docs/sdk#testing). - **Times in seconds.** A `"time"` board reads milliseconds. Submitting `92` shows `0:00.092`. - **`"order": "desc"` on a time board.** The slowest player would rank first. Use `"asc"`. - **`max` set too low.** Real scores are rejected and the game gets the default result back. - **`entry` written as `./index.html` or `/index.html`.** It must equal a file path in the build: `index.html`. - **Renaming a key after launch.** Players who unlocked the old one keep it, and the new one starts at zero unlocks. --- # Unity > Build a Unity Web (WebGL) game for Fradiation Games with the Unity kit, or set it up by hand, and call the SDK from C#. A Unity web build is a wasm file with a canvas attached. Fradiation serves it as is, once it has the right compression, a template that fills the frame and relative paths. The Unity kit sets all of that with one menu item. The kit is tested with Unity 6 (6000.x). The APIs it uses exist in Unity 2021.3 and later. ## Quick path 1. Download [the Unity kit](https://www.fradiation.games/kits/fradiation-unity-kit.zip) and unzip it into your project folder, so the kit's `Assets` folder merges with yours. The files land here: | Path | What it is | | --- | --- | | `Assets/Fradiation/Editor/FradiationBuild.cs` | The build menu item and the batch method. | | `Assets/Fradiation/Runtime/Fradiation.cs` | The C# API. | | `Assets/Plugins/WebGL/Fradiation.jslib` | The bridge from C# to `window.fradiation`. | | `Assets/WebGLTemplates/Fradiation/` | A WebGL template that fills the frame. | 2. Put `fradiation.json` in the project root, next to `Assets/`. See [fradiation.json](/docs/fradiation-json) for every field. ```json { "$schema": "https://www.fradiation.games/fradiation.schema.json", "viewport": { "width": 1280, "height": 720 }, "threads": false, "mutations": [ { "key": "boss-down", "name": "Boss Down", "description": "Defeat the first boss.", "tier": "alpha", "icon": "skull" } ], "boards": [ { "key": "fastest-clear", "name": "Fastest clear", "order": "asc", "format": "time" } ] } ``` 3. Build. In the editor, choose **Fradiation → Build for Fradiation**. From the command line, with the project closed in the editor: ```bash Unity -batchmode -quit -projectPath . -buildTarget WebGL -executeMethod FradiationBuild.Build ``` `Unity` stands for the path to the editor executable of the version your project uses, for example `"C:\Program Files\Unity\Hub\Editor\\Editor\Unity.exe"`. The editor needs its Web (WebGL) build support module installed. - The build goes to `Builds/Fradiation`. Add `-fradiationOut ` to change that. - `fradiation.json` from the project root is copied into the build if it exists. - The process exits with code 1 if the build fails. An agent driving the open editor through an editor-automation tool should call `FradiationBuild.BuildDeferred()` instead. It returns at once, builds on the next editor tick (so a request timeout can't cut the build off part-way), and writes `Builds/Fradiation.status.json` to poll: `queued`, `building`, then `succeeded` or `failed`, with the output path and any error. 4. Check it, then upload. Run `node check-build.mjs Builds/Fradiation` ([download the script](https://www.fradiation.games/skills/fradiation/check-build.mjs)), then upload the `Builds/Fradiation` folder or a zip of it on `/dev/`. See [Packaging](#packaging) and [Publish a game](/docs/publish). ## What the kit sets, and doing it by hand If you skip the kit, set these yourself. | Setting | Value | Where | | --- | --- | --- | | Build target | Web | Unity 6: File → Build Profiles → Web. Earlier versions: File → Build Settings → WebGL. | | WebGL Template | A template that fills the window | Player Settings → Resolution and Presentation | | Compression Format | Brotli (or Gzip) | Player Settings → Publishing Settings | | Decompression Fallback | Off | Player Settings → Publishing Settings | | Data Caching | On | Player Settings → Publishing Settings | | Name Files As Hashes | Not needed | Player Settings → Publishing Settings | The same settings from an editor script, as the kit sets them: ```csharp PlayerSettings.WebGL.template = "PROJECT:Fradiation"; PlayerSettings.WebGL.compressionFormat = WebGLCompressionFormat.Brotli; PlayerSettings.WebGL.decompressionFallback = false; PlayerSettings.WebGL.dataCaching = true; ``` ### Compression The uploader accepts pre-compressed files ending in `.br` or `.gz`, such as `Fradiation.wasm.br`. The play server sends them with the matching `Content-Encoding`, so the browser unpacks them itself. Brotli and Gzip builds both work. Unity's own description of Compression Format says it doesn't affect development builds. A development build is uncompressed. Its files are allowed but larger. ### Decompression Fallback Leave it off. Unity describes the option as decompression code for hosts that can't set response headers to match the compression. Fradiation sets them, so the fallback adds loader code you don't use. ### Data Caching On. Unity keeps the data file in the browser's IndexedDB, which belongs to your game's own origin. ### Name Files As Hashes Not needed. Every build is served from its own URL, so file names don't have to change to defeat caches. ### The template Unity's default template gives the canvas a fixed size. On desktop it sets the width and height from Player Settings (960×600 by default), and it adds a footer with a logo and a fullscreen button. That doesn't fit the cabinet, which letterboxes to your `viewport` and expects the game to fill its window. Create `Assets/WebGLTemplates/Fradiation/index.html`, copy [fradiation-sdk.js](https://www.fradiation.games/sdk/fradiation-sdk.js) into the same folder, and pick the template in Player Settings. Unity copies every file in the template folder into the build. ```html {{{ PRODUCT_NAME }}}
Loading
``` This does what the kit's template does, without the segmented loading bar. All URLs are relative. Set the `viewport` in `fradiation.json` to your game's aspect ratio. ## Calling the SDK from C# The kit's `Fradiation` class is static and fire-and-forget. It returns nothing, so a call can't tell you whether a mutation unlocked or what rank a score got. In the editor and on other platforms it only logs. | Call | What it does | | --- | --- | | `Fradiation.UnlockMutation(string key)` | Unlocks a mutation declared in `fradiation.json`. Repeat calls are harmless. | | `Fradiation.SubmitScore(string board, double value)` | Submits a score to a declared board. The site keeps each player's best. | | `Fradiation.Track(string name, string json = null)` | Marks a moment on the playtest timeline. Extra data goes in as a JSON string. | | `Fradiation.AskForFeedback(string prompt = null)` | Opens the feedback panel in playtests. The prompt is up to 200 characters. | ```csharp using UnityEngine; public class RunEvents : MonoBehaviour { // Unlock on an event. public void OnBossDefeated() { Fradiation.UnlockMutation("boss-down"); } // Submit a time score. A board with "format": "time" takes milliseconds. public void OnLevelComplete(float seconds) { Fradiation.SubmitScore("fastest-clear", System.Math.Round(seconds * 1000.0)); Fradiation.Track("level-complete", "{\"level\":3}"); } // Ask for feedback at game over. public void OnGameOver() { Fradiation.AskForFeedback("How was that boss?"); } } ``` - Declare every mutation and board key in `fradiation.json`. Scores above a board's `max` are rejected as implausible. - `Track` is ignored outside playtests. `AskForFeedback` opens the panel only in playtests. - While the game is a draft, calls are checked but not kept, and a toast says so. Developers get no rads from their own games. - Outside the site, for example a build opened from your own disk, every call does nothing. The [SDK](/docs/sdk) page has the full behavior. ### Without the kit The bridge is a `.jslib` file and a small C# wrapper. Skip this if you installed the kit. ```js // Assets/Plugins/WebGL/FradiationBridge.jslib mergeInto(LibraryManager.library, { Fradiation_UnlockMutation: function (keyPtr) { var key = UTF8ToString(keyPtr); if (window.fradiation) window.fradiation.unlockMutation(key); }, Fradiation_SubmitScore: function (boardPtr, value) { var board = UTF8ToString(boardPtr); if (window.fradiation) window.fradiation.submitScore(board, value); }, }); ``` ```csharp using System.Runtime.InteropServices; using UnityEngine; public static class FradiationBridge { #if UNITY_WEBGL && !UNITY_EDITOR [DllImport("__Internal")] private static extern void Fradiation_UnlockMutation(string key); [DllImport("__Internal")] private static extern void Fradiation_SubmitScore(string board, double value); #endif public static void UnlockMutation(string key) { #if UNITY_WEBGL && !UNITY_EDITOR Fradiation_UnlockMutation(key); #else Debug.Log("[Fradiation] mutation: " + key); #endif } public static void SubmitScore(string board, double value) { #if UNITY_WEBGL && !UNITY_EDITOR Fradiation_SubmitScore(board, value); #else Debug.Log("[Fradiation] score " + board + ": " + value); #endif } } ``` ## Multithreading Leave it off. Unity 6 has an experimental option in Player Settings, labelled **Enable Native C/C++ Multithreading** in 6000.5 (Unity's description says it doesn't multithread C# code). A build made with it needs `SharedArrayBuffer`, and that isn't available inside the cabinet yet. > [!WARNING] > Threaded builds don't run on the site yet: the game pages that frame them aren't cross-origin isolated. `"threads": true` is accepted and serves the build isolated, ready for when they are. See [Threads](/docs/builds#threads). The kit leaves the setting alone, and warns in the console if it's on while `fradiation.json` doesn't set `"threads": true`. ## What the build folder looks like ```text Builds/Fradiation/ index.html fradiation-sdk.js fradiation.json Build/ Fradiation.data.br Fradiation.framework.js.br Fradiation.loader.js Fradiation.wasm.br StreamingAssets/ only if your project has any ``` - Unity names the files in `Build/` after the output folder, so a folder called `Web` gives `Web.data.br`. - `index.html` sits at the top. Every path in it is relative. - Limits: 1,000 files, 500 MB in total, 200 MB per file. The data file is usually the largest. One shipped Unity game has a 93 MB `.data.br`. - Files in `StreamingAssets` follow the same file type rules as everything else. An extension that isn't on the allowed list is refused. See [Builds and uploads](/docs/builds). ## Packaging Pick one. - **The uploader.** On `/dev/`, drop a `.zip` or pick the `Builds/Fradiation` folder. The browser reads and checks it before anything goes up. - **check-build.mjs.** [Download the script](https://www.fradiation.games/skills/fradiation/check-build.mjs) (Node 20 or later). This checks the folder and zips it the way the uploader expects: ```bash node check-build.mjs pack Builds/Fradiation Fradiation.zip ``` - **tar.** Windows 10 and 11 ship a `tar` that writes zips. Run it in PowerShell or cmd from the project root: ```powershell tar -a -c -f Fradiation.zip -C Builds Fradiation ``` The zip holds one top-level folder, `Fradiation/`, and the uploader drops it. In Git Bash, `tar` is GNU tar and doesn't write zips. On macOS and Linux, run `zip -r Fradiation.zip Fradiation` from inside `Builds`. Zip the build folder or its contents. Don't zip `Builds` itself: the uploader drops one top-level folder, so `index.html` would end up one level too deep. > [!WARNING] > Some versions of Windows PowerShell 5.1 write backslashes into zip entry names with `Compress-Archive`. The uploader refuses them with "paths must be relative, with forward slashes". Use `tar` or `check-build.mjs pack`, and check any zip with `node check-build.mjs ` first. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | Blank frame, or 404s in the network tab | A template with root-absolute URLs such as `/Build/...`. Builds are served from `https://.containment.cloud//`, so the root is the wrong place. | Use relative URLs (`Build/...`). Use the Fradiation template. | | "No index.html at the top of the build" on upload | The zip has an extra folder level, or `index.html` has another name. | Zip the build folder or its contents. If the entry file has another name, set `entry` in `fradiation.json`. | | "Unable to parse Build/..." or a content-encoding error | A `.br` or `.gz` file that isn't in that format, because it was renamed or re-compressed by another tool. Or files from builds with different compression settings mixed in one folder. | Rebuild with Compression Format Brotli or Gzip and Decompression Fallback off. Upload the folder Unity wrote, without re-compressing anything. | | The game doesn't fill the frame | The default template's fixed canvas size. | Use the Fradiation template, or set the canvas to 100% width and height. Set `viewport` in `fradiation.json`. | | SDK calls do nothing | The template didn't load `fradiation-sdk.js`, so `window.fradiation` doesn't exist. Or the game runs outside the site. Or the key isn't declared in `fradiation.json`. | Check that `fradiation-sdk.js` is in the build root and the template has its script tag. On the site, open DevTools, switch the console to the game's frame and run `await fradiation.ready()`. `connected: true` means the handshake worked. In a draft, a successful call shows a toast. No toast means the site rejected the call. | | An upload is refused with "file type not allowed" | A file with an extension off the allowed list, often under `StreamingAssets`. | Convert or remove it. Run `check-build.mjs` to list every problem at once. | | An old version shows up | You're looking at a different build. An upload made with **Upload only** doesn't replace the live build. | Make the build live from the builds list. Each build has its own URL, and `index.html` is revalidated on every load, so a hard reload rarely matters. | ## See also - [SDK](/docs/sdk) for the full API and modes. - [fradiation.json](/docs/fradiation-json) for `viewport`, `threads`, mutations and boards. - [Builds and uploads](/docs/builds) for limits, paths and file types. - [Publish a game](/docs/publish) for the upload steps. --- # Godot > Export a Godot 4 project for the web and upload it to Fradiation Games. Untested on the site; it follows Godot's web export requirements and Fradiation's file rules. Godot exports to the web as a folder of static files. Upload the folder. > [!NOTE] > No Godot game has shipped on Fradiation yet. This guide follows Godot's own web export requirements and our file rules, and the SDK example is untested on the site. If an export won't upload or won't run, `node check-build.mjs ` explains most problems. ## Export 1. Install the export templates that match your editor version: **Editor → Manage Export Templates → Download and Install**. 2. Open **Project → Export**, choose **Add**, then **Web**. 3. Set the export path to a folder, with the HTML file named `index.html`, for example `build/index.html`. That puts `index.html` at the top of the build. Godot names every other file after the HTML file (`index.js`, `index.wasm`), so keep the names it gives them. 4. Export the project. 5. Put `fradiation.json` in the export folder, next to `index.html`. Godot doesn't write it. ```json { "$schema": "https://www.fradiation.games/fradiation.schema.json", "viewport": { "width": 1152, "height": 648 }, "threads": false } ``` `viewport` sets the aspect ratio the cabinet letterboxes to. 1152×648 is Godot 4's default window size. See [fradiation.json](/docs/fradiation-json). If your HTML file has another name, set `entry` in `fradiation.json` to that name. ### Files in the export A Godot 4 web export usually contains these files. The set varies with the Godot version and your export options. | File | Notes | | --- | --- | | `index.html` | The page. | | `index.js` | Engine startup code. | | `index.wasm` | The engine. | | `index.pck` | Your game data. | | `index.png` | Splash image. | | `index.audio.worklet.js` | Audio. Newer versions also write `index.audio.position.worklet.js`. | | `index.icon.png`, `index.apple-touch-icon.png` | Written when the Export Icon option is on. | | `index.side.wasm` | Only with Extensions Support. | | `index.manifest.json`, `index.service.worker.js`, `index.offline.html` | Only with Progressive Web App on. It is off by default, and untested here. Leave it off. | | `index.worker.js` | Godot 4.0 to 4.2 only. | Every one of these has an allowed extension (`html`, `js`, `wasm`, `pck`, `png`, `json`). Run `node check-build.mjs build` on the folder to confirm. See [Builds and uploads](/docs/builds#check-a-build-first). ## Threads Use Godot 4.3 or later and leave the export preset's **Thread Support** option off (it's off unless you turn it on). A single-threaded export needs no `SharedArrayBuffer`, and `"threads"` stays `false`. > [!WARNING] > Threaded builds don't run on the site yet: the game pages that frame them aren't cross-origin isolated, so `SharedArrayBuffer` is unavailable inside the cabinet. Godot 4.0 to 4.2 have no single-threaded web export, so games on those versions can't run here until that changes. See [Threads](/docs/builds#threads). ## C# projects Godot 4 doesn't support exporting C# projects to the web. Godot's web export docs say so. Check them for your version before you start. ## Command line export ```bash mkdir -p build godot --headless --export-release "Web" build/index.html ``` `"Web"` is the name of the export preset. A relative output path is relative to the folder that holds `project.godot`, not to the current directory. Pass `--path ` to run from elsewhere. For a full build script, see [Batch builds](/docs/agents#batch-builds). ## Filling the frame The game runs in an iframe. The cabinet letterboxes it to your `viewport` and expects the game to fill its window. - In the Web preset, leave **Canvas Resize Policy** on **Adaptive**, its default. Godot then sizes the canvas to the whole browser window, which here is the frame. The default HTML shell fills the window. - Set the project's stretch settings so the game scales: **Project Settings → Display → Window → Stretch**. Choose a Mode (`canvas_items` for 2D, `viewport` for pixel art) and an Aspect (`keep` or `expand`). - Don't use **None** unless your own HTML shell sizes the canvas. ## Calling the SDK from GDScript Godot reaches JavaScript through `JavaScriptBridge`. The SDK sets `window.fradiation`, so load it in the page first. 1. Download [fradiation-sdk.js](https://www.fradiation.games/sdk/fradiation-sdk.js) into the export folder, next to `index.html`. 2. In the Web preset, under HTML, set **Head Include** to: ```html ``` Godot adds Head Include to the `` of the exported page. 3. Copy `fradiation-sdk.js` into the folder again if you clear the export folder before exporting. Save this as `fradiation.gd` and add it as an autoload named `Fradiation` in Project Settings. This example is untested on the site. ```gdscript extends Node var _fradiation func _ready() -> void: if OS.has_feature("web"): _fradiation = JavaScriptBridge.get_interface("fradiation") if _fradiation: _fradiation.ready() func unlock_mutation(key: String) -> void: if _fradiation: _fradiation.unlockMutation(key) func submit_score(board: String, value: float) -> void: if _fradiation: _fradiation.submitScore(board, value) func track(event_name: String) -> void: if _fradiation: _fradiation.track(event_name) func ask_for_feedback(prompt: String = "") -> void: if _fradiation: if prompt.is_empty(): _fradiation.askForFeedback() else: _fradiation.askForFeedback(prompt) ``` Call it from anywhere: ```gdscript Fradiation.unlock_mutation("boss-down") Fradiation.submit_score("fastest-clear", elapsed_seconds * 1000.0) ``` - `JavaScriptBridge.get_interface("fradiation")` returns the `window.fradiation` object. Godot has no `get_window()` method. - `OS.has_feature("web")` is false in the editor and on desktop, so those calls do nothing there. Godot's docs recommend this guard. - Declare every mutation and board key in `fradiation.json`. A board with `"format": "time"` takes milliseconds. - The SDK methods return promises. The example ignores them. Reading a result needs a callback made with `JavaScriptBridge.create_callback`, which Godot calls with one Array argument. This guide doesn't cover it. - Outside the site, the SDK resolves every call with defaults. See [SDK](/docs/sdk). ## Upload Upload the export folder, or a zip of it, on `/dev/`. To check and zip in one step, [download check-build.mjs](https://www.fradiation.games/skills/fradiation/check-build.mjs) (Node 20 or later): ```bash node check-build.mjs pack build Godot.zip ``` See [Publish a game](/docs/publish) for the upload steps. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | Blank frame or 404s | `index.html` isn't at the top of the upload, or the HTML file has another name. | Export to `index.html`, or set `entry` in `fradiation.json`. | | Godot's start page lists missing features such as cross-origin isolation or `SharedArrayBuffer` | The export has Thread Support on. Threaded builds don't run on the site yet. | Export without thread support (Godot 4.3 and later). | | The game doesn't fill the frame | Canvas Resize Policy is **None** or **Project**, or the stretch settings are off. | Set it to **Adaptive** and set the stretch Mode and Aspect. | | The SDK does nothing | Head Include is empty, `fradiation-sdk.js` isn't in the export folder, or the game runs outside the site. | Check both. On the site, open DevTools, switch the console to the game's frame and run `await fradiation.ready()`. `connected: true` means the handshake worked. In a draft, a successful call shows a toast. No toast means the site rejected the call. | | An upload is refused with "file type not allowed" | A file with an extension off the allowed list. | Remove it, or run `check-build.mjs` to list every problem. | | No sound until the player clicks | Browsers block audio until the player interacts. | Godot's docs suggest asking the player to click, tap or press a key first. | --- # Three.js and web engines > Ship a Vite, Three.js or plain HTML and JavaScript game on Fradiation Games. Covers relative paths, the SDK, resizing, browser permissions and what to watch for in other web engines. If it runs in a browser, it runs here, provided every URL in it is relative. This page covers games that are already HTML and JavaScript: Vite and Three.js, other bundlers, and plain canvas games. A build is served from `https://.containment.cloud//`. A root-absolute URL such as `/assets/game.js` points at the root of that origin, not at your build, and returns a 404. `assets/game.js` and `./assets/game.js` work. ## Vite Set `base` to `"./"`. Vite's default is `"/"`, which writes root-absolute URLs into `index.html`. ```ts // vite.config.ts import { defineConfig } from "vite"; export default defineConfig({ // Relative asset URLs. A build is served from //, not from the site root. base: "./", build: { outDir: "dist", emptyOutDir: true, target: "es2022" }, }); ``` Containment Breach, a Three.js game on Fradiation, builds with this `base`. Then: 1. Put `fradiation.json` in `public/`. Vite copies `public/` to the top of `dist/`, which is where the uploader reads it. See [fradiation.json](/docs/fradiation-json). 2. Build. 3. Open `dist/index.html`. Script and link URLs should start with `./`, not `/`: ```html ``` 4. Check and zip. [Download check-build.mjs](https://www.fradiation.games/skills/fradiation/check-build.mjs) (Node 20 or later): ```bash bun run build # or: npm run build node check-build.mjs pack dist game.zip ``` Upload `game.zip`, or pick the `dist` folder, on `/dev/`. See [Publish a game](/docs/publish). ## Loading assets in Three.js Three.js loaders resolve a relative URL against the page. Put static files in `public/` and refer to them with `./`: ```ts import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; // public/models/ship.glb becomes dist/models/ship.glb new GLTFLoader().load("./models/ship.glb", (gltf) => scene.add(gltf.scene)); ``` `"/models/ship.glb"` breaks. To let Vite fingerprint a file instead, import its URL: ```ts import shipUrl from "./ship.glb?url"; ``` Files that a loader fetches at run time need to be inside the build too: - **Draco.** Copy `draco_decoder.js`, `draco_decoder.wasm` and `draco_wasm_wrapper.js` from `three/examples/jsm/libs/draco/` into `public/draco/`, and call `dracoLoader.setDecoderPath("./draco/")`. Leave the `README.md` in that folder behind. It isn't an allowed type. - **KTX2.** `.ktx2` textures are allowed. Copy `basis_transcoder.js` and `basis_transcoder.wasm` from `three/examples/jsm/libs/basis/` into `public/basis/`, and call `ktx2Loader.setTranscoderPath("./basis/")`. - **Fonts.** `woff` and `woff2` are allowed. Self-host them instead of loading them from another site, especially if you use threads. - **Formats off the list.** `.hdr`, `.exr`, `.fbx`, `.obj`, `.mtl` and `.drc` aren't allowed. Convert models to `.glb`. Loaders that read raw bytes without checking the extension, such as three's `RGBELoader` and `DRACOLoader`, work with the file renamed to `.bin`. The full list is in [Builds and uploads](/docs/builds). ## Other bundlers The rule is the same everywhere: asset URLs are relative. | Tool | Setting | | --- | --- | | Vite | `base: "./"` | | webpack | `output.publicPath` of `""` | | Parcel | `--public-url ./` | | Rollup, esbuild and others | Don't write a leading `/` in asset URLs. | After a build, open the output `index.html` and search it for `src="/` and `href="/`. Then check every URL your code builds at run time. A local server that serves the output folder at its root hides this mistake. A draft on the site shows it: a draft always runs its newest build. ## Plain HTML and canvas A game with no build step needs no tooling. Keep everything in one folder with relative references, and zip the folder. ```text my-game/ index.html game.js fradiation.json fradiation-sdk.js sprites/ ship.png sounds/ shot.ogg ``` ```html My Game ``` Zip the `my-game` folder. The uploader drops a single top-level folder, so `index.html` ends up at the top. Or run `node check-build.mjs pack my-game`. ## Load the SDK The SDK is not on npm. Download a file and ship it inside your build. See [SDK](/docs/sdk) for the API. **ES module.** Save [fradiation-sdk.mjs](https://www.fradiation.games/sdk/fradiation-sdk.mjs) in your source folder and import it by path: ```ts import { fradiation } from "./fradiation-sdk.mjs"; const { connected, mode } = await fradiation.ready(); await fradiation.unlockMutation("first-blood"); await fradiation.submitScore("high-score", 12345); fradiation.track("reached-level", { level: 3 }); ``` For TypeScript, save [fradiation-sdk.d.mts](https://www.fradiation.games/sdk/fradiation-sdk.d.mts) next to the module. TypeScript picks it up for the `.mjs` import. **Script tag.** Save [fradiation-sdk.js](https://www.fradiation.games/sdk/fradiation-sdk.js) next to `index.html`, load it before your game's scripts, and use `window.fradiation`: ```html ``` Declare mutations and boards in `fradiation.json` first. Outside the site, every call resolves with defaults, so the same code runs in local dev. ## Fill the frame The game runs in an iframe. The cabinet letterboxes it to the `viewport` aspect ratio in `fradiation.json`, and the frame changes size when the player resizes the window or goes fullscreen. Fill the window and handle resize. Don't hard-code a canvas size. ```html ``` ```ts function resize() { const w = innerWidth; const h = innerHeight; renderer.setSize(w, h); camera.aspect = w / h; camera.updateProjectionMatrix(); } addEventListener("resize", resize); resize(); ``` If you use a post-processing composer, call `composer.setSize(w, h)` in the same function. ## Audio, pointer lock and fullscreen The frame is sandboxed. It allows pointer lock, popups, modal dialogs and orientation lock, and it delegates `fullscreen`, `gamepad`, `autoplay`, `accelerometer`, `gyroscope` and `xr-spatial-tracking`. - **Audio.** Browsers can still block sound until the player interacts, even with `autoplay` delegated. Create or resume your `AudioContext` inside the first click or key handler. ```ts addEventListener("pointerdown", () => audioContext.resume(), { once: true }); ``` - **Pointer lock.** Request it from a click handler: `canvas.requestPointerLock()`. - **Fullscreen.** Request it from a click or key handler: `canvas.requestFullscreen()`. - **Navigation and forms.** The game can't navigate the site or submit forms. Scripts, `fetch` to external APIs and popups work. ## Storage Each game has its own origin, `.containment.cloud`. Its `localStorage`, `IndexedDB` and caches belong to that origin and persist across builds. Other games can't read them. Browsers can block storage in an embedded frame, so wrap it and keep the game playable without it: ```ts try { localStorage.setItem("best", String(best)); } catch { // storage may be blocked } ``` ## Other engines Every engine follows the same two rules: relative paths, and file types on the allowed list. This table covers what to watch for. See [Unity](/docs/unity) and [Godot](/docs/godot) for their own guides. | Engine | Watch for | | --- | --- | | Phaser | Relative paths in loader calls: `this.load.image("ship", "assets/ship.png")`, not `"/assets/ship.png"`. Bitmap font `.fnt` files aren't allowed: use the XML format with an `.xml` extension. Tiled maps must be JSON with a `.json` extension. `.tmx` and `.tmj` aren't allowed. | | Babylon.js | `.glb` and `.gltf` are allowed. `.babylon`, `.env` and `.dds` aren't on the list. | | Emscripten and custom WebAssembly | `.html`, `.js`, `.wasm` and `.data` are allowed. Build without pthreads: threaded builds don't run on the site yet (see [Threads](/docs/builds#threads)). | | Anything else | Run `node check-build.mjs `. It lists every file the rules refuse. A file type that isn't on the allowed list stays refused until it's added. | ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | Blank frame, 404s for scripts or assets | Root-absolute URLs. | Use relative URLs. In Vite, set `base: "./"`. | | "No index.html at the top of the build" | An extra folder level in the zip, or another entry file name. | Zip the build folder or its contents, or set `entry` in `fradiation.json`. | | An upload is refused with "file type not allowed" | A file with an extension off the allowed list. | Convert it, or store it under an allowed extension if the loader ignores the extension. Run `check-build.mjs` for the full list. | | The game doesn't fill the frame | A fixed canvas size. | Size the canvas to 100% and handle `resize`. | | SDK calls do nothing | The SDK file isn't in the build, the script tag or import path is wrong, the key isn't declared in `fradiation.json`, or the game runs outside the site. | On the site, open DevTools, switch the console to the game's frame and run `await fradiation.ready()`. `connected: true` means the handshake worked. In a draft, a successful call shows a toast. No toast means the site rejected the call. | | No sound | The browser blocks audio until the player interacts. | Start audio from a click or key handler. | --- # The SDK > The JavaScript SDK a hosted game uses for mutations, scoreboards, playtest events and feedback. Files, API, modes and patterns. Five calls, no keys, no accounts to manage. The site knows who is playing, so your game doesn't have to. The SDK gives a game mutations (achievements), scoreboards, and playtest events and feedback. Mutations and boards are declared in [fradiation.json](/docs/fradiation-json) first, then referenced by key from code. ## Get the SDK The SDK is not on npm. Download one of these files and ship it inside your build. | File | Use it for | | --- | --- | | `https://www.fradiation.games/sdk/fradiation-sdk.js` | A script tag. Sets `window.fradiation`. | | `https://www.fradiation.games/sdk/fradiation-sdk.mjs` | An ES module: `import { fradiation } from "./fradiation-sdk.mjs"` | | `https://www.fradiation.games/sdk/fradiation-sdk.d.mts` | TypeScript types for the ES module. Save it next to `fradiation-sdk.mjs` and TypeScript picks it up. | ```bash curl -fsSLO https://www.fradiation.games/sdk/fradiation-sdk.js curl -fsSLO https://www.fradiation.games/sdk/fradiation-sdk.mjs curl -fsSLO https://www.fradiation.games/sdk/fradiation-sdk.d.mts ``` ```powershell Invoke-WebRequest -UseBasicParsing https://www.fradiation.games/sdk/fradiation-sdk.js -OutFile fradiation-sdk.js Invoke-WebRequest -UseBasicParsing https://www.fradiation.games/sdk/fradiation-sdk.mjs -OutFile fradiation-sdk.mjs Invoke-WebRequest -UseBasicParsing https://www.fradiation.games/sdk/fradiation-sdk.d.mts -OutFile fradiation-sdk.d.mts ``` Take only the file you use. The current version is 0.2.0, also available as `fradiation.VERSION`. ## Load it All paths in a build must be relative. `fradiation-sdk.js`, not `/fradiation-sdk.js`. ### Script tag Load it before your game's scripts. It sets `window.fradiation`. ```html ``` ### ES module ```js import { fradiation } from "./fradiation-sdk.mjs"; const session = await fradiation.ready(); ``` The module also has a default export (`import fradiation from "./fradiation-sdk.mjs"`). Bundlers such as Vite pick it up like any other local file. ## API Every call is safe to make from anywhere in your game. None of them throws or rejects. The module exports these types: ```ts type Mode = "live" | "preview" | "playtest"; interface Player { handle: string; displayName: string; } interface Session { connected: boolean; player: Player | null; mode: Mode; } interface UnlockResult { signedIn: boolean; unlocked: boolean; } interface ScoreResult { signedIn: boolean; isBest: boolean; best: number | null; rank: number | null; } interface FeedbackResult { opened: boolean; } ``` > [!NOTE] > There is no save or load call. Keep saves in `localStorage` or IndexedDB. Each game runs on its own origin, so its storage is its own and persists across builds. ### ready ```ts ready(timeoutMs?: number): Promise ``` Handshake with the site. `timeoutMs` defaults to 1500. - `connected`: `true` when the site answered. - `player`: the signed-in player, or `null`. - `mode`: `"live"`, `"preview"` or `"playtest"`. See [Modes](#modes). - Safe to call repeatedly. Every call returns the same promise, and only the first call's timeout counts. - Outside the site it resolves with `{ connected: false, player: null, mode: "live" }`: at once at the top level of a page, after the timeout inside someone else's frame. - The other calls run it for you. Call it yourself to read `player` and `mode`, and call it early: error capture starts when the handshake completes. ```ts const { connected, player, mode } = await fradiation.ready(); if (player) { document.querySelector("#player-name")!.textContent = player.displayName; } ``` ### unlockMutation ```ts unlockMutation(key: string): Promise ``` Unlocks a mutation declared in `fradiation.json`. - `unlocked` is `true` the first time this player unlocks it and `false` after that. Calling again is harmless. - `signedIn` is `false` when nobody is signed in. Nothing is kept, and the site shows a one-time notice that progress isn't saved. - The site shows the unlock toast. Your game does not need to draw one. - The player earns rads by tier. See [Tiers](/docs/fradiation-json#tiers). Developers get none from their own games. - A key that isn't in `fradiation.json` resolves with the defaults: `{ signedIn: false, unlocked: false }`. ```ts async function onBossDefeated(difficulty: "normal" | "hard") { await fradiation.unlockMutation("first-blood"); if (difficulty === "hard") await fradiation.unlockMutation("boss-hard"); } ``` ### submitScore ```ts submitScore(board: string, value: number): Promise ``` Submits a score to a board declared in `fradiation.json`. The site keeps each player's best only. - `isBest`: this score beat the player's previous best. - `best`: the player's best on this board after this submission. - `rank`: 1-based position on the board. - Signed out: `{ signedIn: false, isBest: false, best: null, rank: null }`. - Rejected, so the defaults come back: a board key that isn't in the file, a negative value, `NaN` or `Infinity`, and any value above the board's `max`. - Time boards take milliseconds. - The first score on a board pays the player 5 rads, once. Developers get none. ```ts const { signedIn, isBest, rank } = await fradiation.submitScore("high-score", 48210); if (signedIn && isBest) console.log(`New best, rank ${rank}`); ``` ### track ```ts track(name: string, data?: unknown): void ``` Marks a moment on the playtest timeline. It returns at once and there is nothing to await. - `name` is cut to 80 characters. - `data` must be cloneable: objects, arrays, strings, numbers, booleans, `null`. Anything else is dropped without an error. Data over 1,500 characters of JSON is dropped and the event keeps its name. - Ignored outside playtests, so leave the calls in. - A playtest session keeps at most 300 events. ```ts fradiation.track("level-start", { level: 3 }); fradiation.track("died", { level: 3, cause: "lava" }); ``` ### askForFeedback ```ts askForFeedback(prompt?: string): Promise ``` Asks the player for feedback now, for example at game over. - In a playtest it opens the feedback panel: the panel scrolls into view, shows the prompt and focuses the notes field. The prompt is saved with the feedback. - `prompt` is cut to 200 characters. Without one the panel says the game is asking for the player's thoughts. - Everywhere else it resolves `{ opened: false }`. That includes you playing your own playtest link. ```ts void fradiation.askForFeedback("How was that last section?"); ``` ### Automatic error capture Once connected, the SDK reports uncaught errors and unhandled promise rejections. You don't call anything. - Message up to 300 characters, stack up to 1,500. - The same message is reported once. At most 20 errors per page load. - Only playtests keep them: they show on the tester's timeline and in the error count next to their feedback. Elsewhere they are dropped. - Errors before the handshake completes aren't captured. Call `ready()` first thing. - `console.error` calls are not captured. ## Modes The site tells the game which mode it is in. Read it from `ready()`. | Mode | Where | Mutations and scores | `track` and errors | `askForFeedback` | | --- | --- | --- | --- | --- | | `live` | The public game page | Checked and kept for signed-in players. Rads awarded. | Ignored | `{ opened: false }` | | `preview` | Your draft at `/games/`, visible to you and admins | Checked, not kept. A "Test unlock" or "Test score" toast says so. | Ignored | `{ opened: false }` | | `playtest` | A private `/t/` link | Logged on the timeline, not checked, not kept. A test toast says so. | Logged | Opens the feedback panel for testers | In `preview` and `playtest` the results are canned. `unlockMutation` returns `unlocked: true` every time and `submitScore` returns `isBest: true`, with the score as `best`. `rank` is `1` in preview and `null` in a playtest. In a playtest, `signedIn` reflects whether the tester is signed in. Don't build logic on these values while testing. Standalone reports `mode: "live"` with `connected: false`. Test `connected`, not `mode`, to know whether the site is there. ## Testing Outside the site, every call resolves with defaults and does nothing. Your game runs the same code path everywhere. | Where | What you get | | --- | --- | | Local dev, any other host | `connected: false`. `unlockMutation` gives `{ signedIn: false, unlocked: false }`. `submitScore` gives `{ signedIn: false, isBest: false, best: null, rank: null }`. `askForFeedback` gives `{ opened: false }`. | | A draft on the site | Preview mode. Upload the build to `/dev/` and open the game page. Calls are checked against the mutations and boards registered from your uploaded builds. | | A playtest link | Playtest mode. `track` calls and errors appear on the tester's timeline in `/dev//feedback`. | - In preview, a successful call shows a toast. If no toast appears, the site rejected the call: an unknown key or board, a bad or too-large score, or the rate limit. The SDK returns the defaults and no error text. - Your own plays on your playtest link don't create sessions, so nothing reaches the timeline. Open the link in a private window as a guest, or from another account, to see events and errors. See [Playtests](/docs/playtests). ## Patterns ### Unlock on events Keep a local record so each event calls the SDK once. Every call is a round trip and counts against the [rate limit](#how-it-talks-to-the-site). ```ts const unlocked = new Set(); function unlock(key: string) { if (unlocked.has(key)) return; unlocked.add(key); void fradiation.unlockMutation(key); } let kills = 0; function onDroneDestroyed() { kills += 1; if (kills === 1) unlock("first-blood"); } ``` ### Submit a time score Declare the board with `"order": "asc"` and `"format": "time"`. Submit whole milliseconds. ```ts let runMs = 0; let paused = false; function update(dtMs: number) { if (!paused) runMs += dtMs; } async function onGameCleared() { const { isBest, rank } = await fradiation.submitScore("fastest-clear", Math.round(runMs)); if (isBest) console.log(`Personal best. Rank ${rank}.`); } ``` ### Ask for feedback at game over ```ts function onGameOver() { showGameOverScreen(); void fradiation.askForFeedback("How was that last section?"); } ``` Outside playtests it does nothing, so you don't need to check the mode. ### Handle signed-out players Anonymous players are normal. Don't block the game on the site and don't show a sign-in wall of your own. Draw your own results first, then use the reply when it arrives. ```ts async function finishRun(ms: number) { showResults(ms); const result = await fradiation.submitScore("fastest-clear", ms); if (result.signedIn && result.isBest) showBest(result.best, result.rank); } ``` The site tells anonymous players once per page load that their progress isn't kept. ## Engines - **Unity:** the Unity kit wraps this SDK in a C# API and a WebGL template. See [Unity](/docs/unity). - **Godot:** see [Godot](/docs/godot). - **Three.js and plain web:** see [Three.js and web](/docs/web). ## How it talks to the site - The game sends one `hello` message to its parent window. It carries the SDK version and nothing sensitive. - The site checks that the message came from the game's own frame and from the game's own origin, `https://.containment.cloud`. It then answers with a private `MessageChannel` port. Every later message goes over that port. - The SDK accepts the answer only from its direct parent, and only if that parent is `https://www.fradiation.games`, `https://fradiation.games` or `http://localhost:3000`. The SDK ignores any other parent, and every call resolves with defaults. - The game runs in a sandboxed iframe on its own origin. It can't read the site's page, cookies or session, and it never handles a token. This channel is the only way the game reaches the site. - Rate limits apply per game frame. Per 10 seconds: 30 calls (`unlockMutation`, `submitScore`, `askForFeedback`) and 60 events (`track` and errors). Calls over the limit resolve with defaults. Events over the limit are dropped. - A call with no reply after 10 seconds resolves with defaults. --- # Working with agents > Connect your coding agent to Fradiation over MCP, what it can do there, the machine-readable files and skill, deploying with an API token, the upload API, and prompts to give it. Most games here are built with a coding agent in the loop. Agents read the docs faster than you do and argue with them less. Claude Code, Cursor, Codex and similar tools can do the work between "it runs on my machine" and "testers are playing it". The quickest way to give them everything is the MCP server: connect once, then ask for what you want. ## Connect your agent The Fradiation MCP server gives your agent the docs, the build rules and your games in one place, at `https://www.fradiation.games/api/mcp`. Add it to your agent. The first time you connect, a browser opens: sign in with Discord or GitHub, see what the agent will be able to do, and click **Allow**. There's no token to copy. **The Claude app (desktop and claude.ai).** Add Fradiation as a custom connector. No terminal is needed. It works in the desktop app's Code and Chat tabs and on claude.ai, and the Claude Code CLI loads it too when you're signed in with the same account. 1. Open your connectors: in the desktop app, **Settings → Connectors** (or **+** by the prompt → **Connectors** → **Manage connectors**). On the web, go to [claude.ai/customize/connectors](https://claude.ai/customize/connectors). 2. Click **+ Add**, then **Add custom connector**. 3. Name it `Fradiation` and paste `https://www.fradiation.games/api/mcp`. Click **Continue**. 4. For the OAuth client, choose **Register automatically**. Click **Add**, then **Connect**, and sign in. 5. Start a new session. Fradiation's tools are there. **Claude Code in a terminal.** If you only use the CLI, you can add the server there instead. Run this once, then type `/mcp` in Claude Code, pick `fradiation` and sign in. `--scope user` makes it work in every project. ```bash claude mcp add --transport http --scope user fradiation https://www.fradiation.games/api/mcp ``` The desktop app also loads servers added this way, but only when a session starts, and it has no `/mcp` panel to sign in or reconnect them. In the desktop app, use the custom connector. **Cursor.** In `~/.cursor/mcp.json`, or `.cursor/mcp.json` in the project. Cursor shows a **Connect** button that opens the sign-in. ```json { "mcpServers": { "fradiation": { "url": "https://www.fradiation.games/api/mcp" } } } ``` **VS Code.** In `.vscode/mcp.json`. VS Code asks you to sign in the first time the server starts. The file holds no secret, so it's safe to commit. ```json { "servers": { "fradiation": { "type": "http", "url": "https://www.fradiation.games/api/mcp" } } } ``` **Codex.** In `~/.codex/config.toml`, then run `codex mcp login fradiation`. ```toml [mcp_servers.fradiation] url = "https://www.fradiation.games/api/mcp" ``` **Anything else** that supports MCP sign-in: the URL is all it needs. For a client that only runs local servers, bridge it with `npx -y mcp-remote https://www.fradiation.games/api/mcp`, which handles the sign-in too. Then try: ```text Use the fradiation MCP server to put this game on Fradiation: wire in the SDK with a few mutations and a high-score board, make a cover, then deploy it to a draft. ``` ### Signing in - The game tools need a badge and developer access. A signed-in account without developer access can still read the docs and check files; the agent says how to get access. - The agent stays signed in. Its access lasts an hour at a time and renews in the background. An agent that isn't used for 90 days signs in again. - When Fradiation adds or changes tools, start a new session to see them. Agents load a server's tool list when a session starts. - **Signed-in agents**, at the bottom of **Agents & tokens** (`/dev/agents`, in the menu under your avatar), lists every agent you've allowed. **Hold to remove** signs one out at once: its next request fails and it has to sign in again. ### With a token instead For a client that can't open a browser (CI, a remote box, an older client), send an API token as a header instead. Under **Agents & tokens** (`/dev/agents`), click **No browser sign-in? Use a token**, then **Create a token for it**: the snippet for your agent fills in with a new 90-day token. Any token from the **API tokens** panel works too. For Claude Code: ```bash claude mcp add --transport http --scope user fradiation https://www.fradiation.games/api/mcp --header "Authorization: Bearer " ``` Other clients take the same header in their config (`headers` in JSON, `http_headers` in Codex's TOML). VS Code can prompt for the token and keep it in its secret storage; the panel shows that snippet. ### What it can do | Tool | What it does | Needs | | --- | --- | --- | | `read_doc` | Any page of these docs, or the skill, as Markdown | Any account | | `get_build_rules` | The upload rules and the `fradiation.json` schema, from the code that enforces them | Any account | | `check_manifest` | Checks a `fradiation.json` the way an upload does, and lists every problem | Any account | | `list_games` | Your games: status, live build, plays, new feedback | Yes | | `get_game` | One game: details, what it still needs before containment, mutations, boards, recent builds, playtest | Yes | | `create_game` | Creates a draft. The slug is permanent. | Yes | | `update_game` | Title, description, engine and AI disclosure | Yes | | `begin_upload` | Checks a build's file list and `fradiation.json`, and gives a presigned upload URL for each file the game doesn't have (below) | Yes | | `upload_urls` | Fresh upload URLs for whatever an open upload is still missing | Yes | | `finish_upload` | Seals the upload, and optionally sends it to testers or makes it live | Yes | | `cancel_upload` | Gives up on an open upload | Yes | | `upload_cover` | A presigned URL to upload the cover to (below) | Yes | | `make_build_live` | Ships a build, or rolls back to an older one | Yes | | `start_playtest` | Opens the private test link, optionally with a given build | Yes | | `get_feedback` | Playtest feedback with play time, device, and the errors and events the SDK caught, plus sessions that hit errors | Yes | | `set_feedback_status` | Marks feedback read or addressed | Yes | | `submit_for_judgment` | Sends a draft into containment | Yes | The server tells the agent to ask you before it makes a build live on a game that isn't a draft, and before it submits one for judgment. The agent decides whether it listens, so read what it plans before you approve those calls. Still in the browser: the playtest's note and questions, deleting a draft, pinning builds, and managing tokens and signed-in agents. ### How uploads work The build is on your machine and the server isn't, so the agent uploads it in three steps. No token or password reaches the agent or its shell, and it runs no downloaded code. 1. **`begin_upload`.** The agent hashes every file in the build folder (path, size, SHA-256: `sha256sum` in bash, `Get-FileHash` in PowerShell) and sends the list, with the contents of `fradiation.json`. The server checks them against the same rules as any upload: paths, file types, limits, `.br`/`.gz`, the entry file and the manifest. Then it answers with a **presigned URL** for each file the game doesn't already have. 2. **PUTs.** The agent sends each file's bytes to its URL with plain `curl` or `Invoke-RestMethod`. No headers are needed. Files over 64 MB go up in parts, one URL per part. 3. **`finish_upload`.** The server checks that every file arrived intact and seals the build. With `playtest: true` the build goes to testers, and with `live: true` it goes to players. It answers with the build id, its URL and the testers link. If something is missing, it says what and leaves the upload open; `upload_urls` gives fresh URLs. A presigned URL allows one thing: uploading one file, whose bytes must match the hash it was signed for, to one game, within the hour. It isn't an account credential and can't do anything else. Files the game already has from earlier builds aren't sent again, so an update only uploads what changed. An upload counts toward the 20 a day, as any upload does. ### How covers work `upload_cover` answers with one presigned URL, good for 15 minutes. The agent PUTs the image to it the same way. The site does what the uploader in your browser does: it crops the image to 4:3 from the centre, scales it to 800×600 and stores a JPEG with the metadata stripped. - PNG, JPEG, WebP, AVIF or GIF, up to 15 MB. - Make it 4:3, or wider with the subject in the middle. Keep text away from the edges; the crop and small card sizes cut it. - It replaces the current cover at once, everywhere the game appears. ### Long builds Agents stay signed in. An access token lasts an hour and renews in the background, and an agent that isn't used for 90 days signs in again. A Unity build that takes 20 minutes doesn't interrupt anything. If your agent drives an open Unity editor through an editor-automation tool, have it call `FradiationBuild.BuildDeferred()` from the [Unity kit](/docs/unity) rather than `Build()`. A long build run inside a tool request can be cut off by the request's timeout ("Tundra build interrupted"). `BuildDeferred()` returns at once and builds on the next editor tick. It writes `Builds/Fradiation.status.json` (`queued`, `building`, `succeeded` or `failed`) for the agent to poll. ## What an agent can do without MCP - Prepare a build for any engine: Unity, Godot, Three.js/Vite or plain HTML5. - Write and validate `fradiation.json`. - Wire in the SDK: mutations, scoreboards, playtest events. - Check the build against the upload rules, and package it as a zip. - Deploy it with an API token in `FRADIATION_TOKEN`: upload it, send it to testers or make it live, and roll back. See [Deploy from the command line](#deploy-from-the-command-line). Without MCP, creating the game, its details, playtests and submitting to containment happen in the browser. > [!NOTE] > Without MCP, create the game first with **New game** on My games (`/dev`). The slug you pick there is the game's origin (`https://.containment.cloud`), and every deploy names it. ## Machine-readable files Every doc page has a raw Markdown twin: add `.md` to the address. `/docs/sdk` is also at `/docs/sdk.md`. The rest are below. | File | What it is | Use it when | | --- | --- | --- | | `https://www.fradiation.games/llms.txt` | Index of every doc, as Markdown links | An agent has no other context. Start here. | | `https://www.fradiation.games/llms-full.txt` | Every doc concatenated | You paste docs into a chat that can't browse. | | `https://www.fradiation.games/docs/.md` | Raw Markdown of one page | The agent needs one topic, for example `/docs/unity.md`. | | `https://www.fradiation.games/fradiation.schema.json` | JSON Schema for `fradiation.json` | Editor autocomplete, and the source for field names and mutation icon names. | | `https://www.fradiation.games/docs/build-rules.json` | Limits, allowed file types and path rules, generated from the code the uploader and server use | Anything about limits. If a doc and this file disagree, this file wins. | | `https://www.fradiation.games/sdk/fradiation-sdk.js` | The SDK as a script tag. Sets `window.fradiation`. | Unity templates, plain HTML, anything without a bundler. | | `https://www.fradiation.games/sdk/fradiation-sdk.mjs` | The SDK as an ES module | Vite and other bundlers. | | `https://www.fradiation.games/sdk/fradiation-sdk.d.mts` | TypeScript types for the ES module | TypeScript projects. Save it next to `fradiation-sdk.mjs`. | | `https://www.fradiation.games/kits/fradiation-unity-kit.zip` | Unity kit: build menu, batch method, C# API, WebGL template | Any Unity project. See [Unity](/docs/unity). | | `https://www.fradiation.games/skills/fradiation/SKILL.md` | Agent skill for preparing, checking and deploying builds | You want the agent to know the rules without being told. | | `https://www.fradiation.games/skills/fradiation/check-build.mjs` | Build checker, packager and deploy tool | Before every upload, and for deploying. | | `https://www.fradiation.games/skills/fradiation.zip` | The skill folder as a zip | You prefer one download to two. | > [!NOTE] > The SDK is not on npm. Download one of the SDK files and ship it inside your build. ## The Fradiation skill A skill is a folder an agent loads when a task matches it. The Fradiation skill holds the build rules, the workflow for each engine, the `fradiation.json` format and the SDK calls. It comes with `check-build.mjs`, so the agent can verify its own work and, with a token, deploy it. It loads when a task involves a Fradiation build, `fradiation.json`, mutations, scoreboards, the SDK, deploying, or an upload that fails. ### Install for Claude Code Two places work: - Project: `.claude/skills/fradiation/`. Commit it and everyone on the repo gets it. - Personal: `~/.claude/skills/fradiation/`. Every project on your machine gets it. Project install: ```bash DIR=.claude/skills/fradiation # personal install: DIR="$HOME/.claude/skills/fradiation" mkdir -p "$DIR" curl -fsSL https://www.fradiation.games/skills/fradiation/SKILL.md -o "$DIR/SKILL.md" curl -fsSL https://www.fradiation.games/skills/fradiation/check-build.mjs -o "$DIR/check-build.mjs" ``` ```powershell $dir = ".claude/skills/fradiation" # personal install: $dir = "$HOME\.claude\skills\fradiation" New-Item -ItemType Directory -Force $dir | Out-Null Invoke-WebRequest -UseBasicParsing https://www.fradiation.games/skills/fradiation/SKILL.md -OutFile "$dir/SKILL.md" Invoke-WebRequest -UseBasicParsing https://www.fradiation.games/skills/fradiation/check-build.mjs -OutFile "$dir/check-build.mjs" ``` Or download the zip. It holds the `fradiation` folder, so unzip it into `skills/`: ```bash curl -fsSL https://www.fradiation.games/skills/fradiation.zip -o fradiation.zip unzip -o fradiation.zip -d .claude/skills rm fradiation.zip ``` ```powershell Invoke-WebRequest -UseBasicParsing https://www.fradiation.games/skills/fradiation.zip -OutFile fradiation.zip Expand-Archive fradiation.zip -DestinationPath .claude/skills -Force Remove-Item fradiation.zip ``` Either way, `.claude/skills/fradiation/SKILL.md` must exist afterwards. Restart Claude Code if it was running. Describe a Fradiation task and the skill loads on its own, or call it by name with `/fradiation`. ### Other agents Agents that read `SKILL.md` files can use the same folder, in the place their tool looks for skills. Agents that read plain Markdown can be pointed at the `SKILL.md` URL, or at `https://www.fradiation.games/llms.txt`. To make that permanent, add a line to your agent instructions file (`AGENTS.md`, `CLAUDE.md`, or your editor's rules): ```text Fradiation Games build rules, SDK and fradiation.json: https://www.fradiation.games/llms.txt Check a build with: node check-build.mjs (https://www.fradiation.games/skills/fradiation/check-build.mjs) Deploy with: node check-build.mjs deploy --playtest (FRADIATION_TOKEN is set; never print it) ``` ## check-build.mjs A zero-dependency script for Node 20 or later. It applies the upload rules on your machine, so you find out about a bad build before anything is uploaded. With an API token it also deploys; see [Deploy from the command line](#deploy-from-the-command-line). ```bash node check-build.mjs Builds/Fradiation # check a folder node check-build.mjs build.zip # check a zip node check-build.mjs Builds/Fradiation --json # machine-readable report, for agents node check-build.mjs pack Builds/Fradiation # check, then zip it the way the uploader expects node check-build.mjs pack Builds/Fradiation game.zip # same, to a path you choose ``` If you installed the skill, the script is at `.claude/skills/fradiation/check-build.mjs`. It checks the rules in `build-rules.json`: - The entry file (`index.html`, or `entry` in `fradiation.json`) sits at the top. - At most 1,000 files, 500 MB in total, 200 MB per file. - Every file type is on the allowed list, and every path follows the path rules. - `fradiation.json` follows the schema: viewport, threads, mutations, boards. The exit code is non-zero when there are problems, so it works in CI and in an agent loop. `pack` checks the folder, then zips it. See [Builds](/docs/builds) for the rules in full. ## Deploy from the command line With an API token, the same script uploads builds. No browser, no zip. ```bash node check-build.mjs deploy dist --game my-game --playtest --notes "Fixed the boss fight" ``` ### Make a token 1. Open **Agents & tokens** (`/dev/agents`, in the menu under your avatar). The **API tokens** panel is under Connect your agent. 2. Give it a **Name** that says where it will live: `laptop`, `CI`, `claude`. 3. Pick **Games**: all your games, or one game. A one-game token can't touch your other games, and commands with it don't need `--game`. 4. Pick when it **Expires**: 7, 30 or 90 days, or a year. 5. Click **Create token** and copy it. It is shown once. The site keeps only a hash of it. Put it in the environment as `FRADIATION_TOKEN`: ```bash export FRADIATION_TOKEN=frad_... ``` ```powershell $env:FRADIATION_TOKEN = "frad_..." ``` In CI, store it as a secret. - A token acts as you, on your own games only. That holds for admins too. - You can have 10 tokens at a time. **Hold to revoke** stops one at once. Expired tokens stay in the list, marked expired, until you remove them. - A token stops working if your developer access is removed. - The list shows when each token was last used. > [!CAUTION] > Anyone holding the token can upload builds to your games and make them live. Don't commit it, paste it into a chat or put it in a build. Give an agent a one-game token with a short expiry. If a token leaks, revoke it. ### deploy ```bash node check-build.mjs deploy [--game ] [--live] [--playtest] [--notes | --notes-file ] [--viewport x] [--json] ``` What it does: 1. Checks the build, exactly like `node check-build.mjs `. On errors it stops without contacting the site. 2. Computes every file's SHA-256 and sends the file list to the site. 3. Uploads only the files the game doesn't have yet, straight to the play server, four at a time. Files over 64 MB go up in parts. 4. The site checks that every file arrived at its full size, then seals the build. 5. Prints the build id, its URL, the game page and its Manage page. | Option | Effect | | --- | --- | | `--game ` | The game. Leave it out when the token only works for one game. | | `--live` | Make the build live once it's sealed. See the table below. | | `--playtest` | Send the build to the game's playtest. Testers get it on their next load, with the patch notes. Start the playtest on the game's Manage page first. | | `--notes ` | Patch notes, up to 2,000 characters. Shown in the game's update log. | | `--notes-file ` | Patch notes from a file. Easier for several lines. | | `--viewport x` | The stage size, used only when `fradiation.json` has no `viewport`. Without either, a deploy keeps the live build's size, or uses 1280x720. | | `--json` | Print one JSON object and nothing else. | When the build goes live: | Game status | Without `--live` | With `--live` | | --- | --- | --- | | Draft | Live. A draft always runs its newest build. Only you and admins can see a draft. | Live | | Any game with no live build | Live | Live | | In containment, released or buried | Sealed, not live. Players keep the build they have. | Live. Players get it on their next load. | A deploy is an upload like any other: it counts toward the 20 uploads a day and the storage limit, and it shows in the Builds list on the game's Manage page with its patch notes. If it stops half-way, on an error or Ctrl+C, it cancels the upload and the site cleans up what was sent. ### Other commands ```bash node check-build.mjs builds --game # the 30 newest builds: id, date, files, size, tags, notes node check-build.mjs live --game # make a build live: ship an update, or roll back node check-build.mjs playtest --game # send a build to testers node check-build.mjs whoami # the token's developer, expiry and games node check-build.mjs cover cover.png --game # set the cover; the site crops it to 800x600 ``` A build id can be shortened to its first 7 characters, as the Manage page shows it, or any unique start of at least 4. `live` and `playtest` refuse builds whose files were deleted. ### JSON output and exit codes Every command exits with 0 on success and 1 on failure, and takes `--json`. A deploy prints: ```json { "ok": true, "game": "my-game", "status": "released", "buildId": "3f2a9c1d8e7b6a5f", "live": false, "playtest": true, "url": "https://my-game.containment.cloud/3f2a9c1d8e7b6a5f/index.html", "page": "https://www.fradiation.games/games/my-game", "dashboard": "https://www.fradiation.games/dev/my-game", "testers": "https://www.fradiation.games/t/k3x9m2p7q4w8", "files": 12, "totalBytes": 3565158, "newFiles": 1, "uploadedBytes": 1634, "warnings": [] } ``` A failure prints `ok: false`, the `stage` it failed at (`check`, `hash`, `begin`, `upload` or `finish`) and `errors` with a `code` and a `message`. The codes are the API's (below), plus the checker's own for a build that fails the check, `no-token` when `FRADIATION_TOKEN` isn't set, `network` when the site can't be reached, and `upload` when the play server refused a file. ### In CI A GitHub Actions step that sends every push on `main` to testers. Use a one-game token. ```yaml - name: Deploy to Fradiation env: FRADIATION_TOKEN: ${{ secrets.FRADIATION_TOKEN }} run: | curl -fsSL https://www.fradiation.games/skills/fradiation/check-build.mjs -o check-build.mjs git log -1 --pretty=%B > notes.txt node check-build.mjs deploy dist --playtest --notes-file notes.txt ``` ## The upload API `check-build.mjs` is a client of this API, and its source is a readable reference implementation. Use the API directly only if you're writing another client. - Base URL: `https://www.fradiation.games/api/v1`. - Every request sends `Authorization: Bearer `. - Bodies are JSON. Every answer is `{ "ok": true, ... }` or `{ "ok": false, "code": "...", "error": "..." }`. - Every endpoint takes `game`, the slug. Leave it out with a one-game token. | Endpoint | Body | Answer | | --- | --- | --- | | `GET /me` | none | `developer`, `token` (`name`, `game`, `expiresAt`), `games` (`slug`, `title`, `status`, `page`, `dashboard`) | | `GET /builds?game=` | none | `game` (`slug`, `title`, `status`, `page`, `dashboard`, `testers` if a playtest is open), `builds`: the 30 newest, each with `id`, `status`, `live`, `testing`, `pinned`, `files`, `bytes`, `uploadedBytes`, `notes`, `createdAt`, `filesDeleteAt`, `filesDeletedAt`, `url` | | `POST /builds/begin` | `files` (`path`, `size`, `sha256`), `manifest`, `viewport`, `notes` | `buildId`, `prefix`, `missing`, `uploadedBytes`, `grant`, `endpoint`, `partBytes` | | `POST /builds/finish` | `buildId`, `live`, `playtest` | `buildId`, `status`, `live`, `playtest`, `url`, `page`, `dashboard`, `testers` | | `POST /builds/cancel` | `buildId` | `buildId` | | `POST /builds/live` | `buildId` | `buildId`, `status`, `url`, `page`, `dashboard` | | `POST /builds/playtest` | `buildId` | `buildId`, `page`, `dashboard`, `testers` | | `POST /cover?game=` | the image's bytes (PNG, JPEG, WebP, AVIF or GIF, up to 15 MB) | `game`, `cover` (its URL), `page`, `dashboard` | | Status | `code` | When | | --- | --- | --- | | 400 | `invalid` | The body breaks a rule. The message names the file or field. | | 401 | `auth` | No token, or it's unknown, revoked or expired. | | 403 | `forbidden` | Not your game, a one-game token used for another game, or no developer access. | | 404 | `missing` | No such game or build, or the upload isn't open any more. | | 429 | `rate` | 20 uploads in a day, or the storage limit. | | 500 | `server` | Something broke on our side. Try again. | ### An upload, step by step 1. **begin.** Send every file except `fradiation.json`: its path from the top of the build, its size in bytes and its SHA-256 as lowercase hex. Send the parsed `fradiation.json` as `manifest`. The site applies the same rules as the uploader and answers with the hashes it doesn't have (`missing`) and a grant for 3 hours. 2. **Upload.** For each hash in `missing`, once even if several paths share it, `PUT` the bytes to `/` with the header `x-upload-grant: `. The play server refuses bytes that don't match the hash. A blob it already has answers `{ "exists": true }`. Files over `partBytes` (64 MB) go up in parts, at that URL: - `POST ?mpu=create` answers `{ "uploadId" }`, or `{ "exists": true }`. - `PUT ?mpu=&part=`, for n from 1, with exactly `partBytes` bytes in every part but the last, answers `{ "partNumber", "etag" }`. - `POST ?mpu=&complete` with `{ "parts": [{ "partNumber", "etag" }] }`. - `DELETE ?mpu=` gives up on the file. 3. **finish.** The site checks that every file is there at its size, seals the build, registers mutations and scoreboards, and applies `live` and `playtest`. If a file is missing, the build fails and the message names it. Start again from begin. 4. **cancel** an upload you won't finish, so the site can clean up. ## Recipes ### With the MCP server With the server connected, short prompts work. The agent reads what it needs with `read_doc`. ```text Use the fradiation MCP server. Make this Unity project a Fradiation game called "": create the draft, add the Unity kit, a high-score board and three mutations, build it, check it and deploy it. Make a 4:3 cover from a gameplay screenshot and upload it. ``` ```text Use the fradiation MCP server. Get the unaddressed playtest feedback for <slug>, fix the crashes and the top complaint, deploy the fix to the playtest, and mark what you fixed as addressed. Don't make anything live. ``` ```text Use the fradiation MCP server to deploy Builds/Fradiation to <slug> as a playtest build, with patch notes from the commits since the last build. ``` ### Without it Each prompt below names what the agent should read, so it works with or without the skill installed. Replace the parts in angle brackets. ### Unity build ```text Make this Unity project build for Fradiation Games. Read https://www.fradiation.games/docs/unity.md and https://www.fradiation.games/docs/build-rules.json. Download https://www.fradiation.games/kits/fradiation-unity-kit.zip and unzip it into the project folder (the zip holds an Assets/ folder that merges with the project's). Write fradiation.json in the project root, with the $schema line and a viewport that matches the game's native resolution. Build with the batch command from the Unity page. Then run check-build.mjs (https://www.fradiation.games/skills/fradiation/check-build.mjs) on Builds/Fradiation and fix every error. Tell me when the folder is ready to upload. ``` ### Vite build ```text Package this Vite game for upload to Fradiation Games. Read https://www.fradiation.games/docs/web.md and https://www.fradiation.games/docs/builds.md. Set base to "./" in the Vite config. Put fradiation.json in public/ so the build copies it to the top of dist/. Build, then run "node check-build.mjs pack dist" (https://www.fradiation.games/skills/fradiation/check-build.mjs). Fix errors in the project, not in dist/. Give me the path of the zip. ``` ### Godot export ```text Make this Godot project export for Fradiation Games. Read https://www.fradiation.games/docs/godot.md and https://www.fradiation.games/docs/build-rules.json. Add a Web export preset named "Web" if there isn't one. Export with: godot --headless --export-release "Web" build/index.html Copy fradiation.json into build/. Run check-build.mjs (https://www.fradiation.games/skills/fradiation/check-build.mjs) on build/ and fix every error. ``` ### Mutations ```text Add mutations to my Fradiation game and wire them in: <name, what earns it, tier>, <name, what earns it, tier>. Read https://www.fradiation.games/docs/fradiation-json.md, https://www.fradiation.games/docs/sdk.md and https://www.fradiation.games/fradiation.schema.json. Add them to fradiation.json. Take icon names only from the icon enum in the schema. Call unlockMutation with the key at the moment each one is earned. Then run check-build.mjs on the build. ``` ### Scoreboard ```text Add a scoreboard to my Fradiation game for <what is scored>. Read https://www.fradiation.games/docs/fradiation-json.md and https://www.fradiation.games/docs/sdk.md. Add a board to fradiation.json. Set order and format to match the score (format "time" is milliseconds) and set max to a value no honest run can pass. Call submitScore when a run ends. ``` ### Playtest hooks ```text Add playtest hooks to my Fradiation game. Read https://www.fradiation.games/docs/playtests.md and https://www.fradiation.games/docs/sdk.md. Call fradiation.track with short names at level start, death and win. Call fradiation.askForFeedback at game over. Do not change gameplay. ``` ### Deploy to testers Set `FRADIATION_TOKEN` in the agent's environment first, with a one-game token. Don't paste the token into the chat. ```text Deploy this build of my Fradiation game to its playtest. FRADIATION_TOKEN is set in the environment. Never print it, write it to a file or ask me for it. Read the "Deploy from the command line" section of https://www.fradiation.games/docs/agents.md. Write patch notes from the commits since the last deploy into notes.txt, one line per change. Run: node check-build.mjs deploy <folder> --playtest --notes-file notes.txt --json If it fails at the check stage, fix the project, rebuild and try again. For any other failure, stop and tell me the code and message. Don't pass --live. Tell me the build id and the testers link. ``` ### Ship or roll back ```text Make build <id> of my Fradiation game live. FRADIATION_TOKEN is set in the environment. Never print it. Run node check-build.mjs builds --json first and confirm the build exists and still has its files, then run node check-build.mjs live <id> --json. ``` ### A failing check ```text check-build.mjs fails on my build. Run it with --json and read the errors. Read https://www.fradiation.games/docs/builds.md and https://www.fradiation.games/docs/build-rules.json. Fix the cause in the project or its build settings, not in the output folder. Rebuild and run the check until it exits with code 0. ``` ## Batch builds For CI, or for an agent that can't click through an editor. ### Unity Close the project in the editor first, and install the WebGL Build Support module for the editor version the project uses. With the [Unity kit](/docs/unity) in `Assets/`: ```bash Unity -batchmode -quit -projectPath . -buildTarget WebGL -executeMethod FradiationBuild.Build ``` `Unity` stands for the path to that editor's executable. Add `-logFile -` to print the build log to the terminal. - The build goes to `Builds/Fradiation`. Add `-fradiationOut <folder>` to change that. - `fradiation.json` in the project root is copied into the build. - The process exits with code 1 when the build fails. - The method sets the Fradiation WebGL template, Brotli compression, decompression fallback off and data caching on. Leave decompression fallback off. ### Godot With a Web export preset named `Web` and the export templates installed: ```bash mkdir -p build godot --headless --export-release "Web" build/index.html cp fradiation.json build/ node check-build.mjs pack build ``` The output name sets the names of the other files: `index.js`, `index.wasm`, `index.pck`. Keep thread support off in the preset: threaded builds don't run on the site yet. See [Godot](/docs/godot). ## Good habits - Keep `fradiation.json` in the repo, with the `$schema` line at the top. Editors then autocomplete it, and agents stop guessing field names. - Run the check before every upload. It takes seconds, and `deploy` runs it for you. - Write patch notes in the commit message, then pass them with `--notes-file` or paste them into the uploader. They can be up to 2,000 characters and show in the game's update log. An agent can draft them from `git log` since your last upload. - Don't let agents invent mutation icon names. The `icon` field takes one of the values in the schema's enum, and the server refuses anything else. - Fix problems in the project, not in the output folder. The next build overwrites the output. - Send agent deploys to testers (`--playtest`) and make them live yourself, or say so explicitly. A released game's players get a live build on their next load. - Give each agent or CI job its own one-game token with a short expiry, rather than your signed-in browser. Revoke tokens you stop using. --- <!-- https://www.fradiation.games/docs/playtests --> # Playtests > Share a private link that serves any build you choose, then read what testers said, with play time, device and errors attached. A playtest is one private link to one build of your game. Testers play it, answer your questions and leave notes, and you read all of it in an inbox. It is the one place on the site where strangers are asked to find your bugs on purpose. Playtests are for developers. If you were sent a link to test something, jump to [For testers](#for-testers). ## The loop 1. [Upload a build](/docs/publish). 2. Start a playtest and send the link to people. 3. Read the feedback in your inbox. 4. Fix things, upload again, and choose **Send to testers**. 5. Mark the feedback **Addressed**. Testers see which build fixed it. 6. Repeat. ## Start a playtest You need [developer access](/docs/publish) and at least one uploaded build. 1. Open `/dev/<slug>` and find the **Playtest** panel. 2. Press **Start a playtest**. The button stays disabled until the game has a build. 3. Copy the **Playtest link**. A game has one playtest. It works on a draft or a released game. It starts on the live build, or on the newest build if nothing is live yet. You can change that at any time (see [Choose the build](#choose-the-build)). ## The link and who can use it The link is `https://www.fradiation.games/t/<token>`. The token is 12 random lowercase letters and digits. It is the only access control: anyone who has the link can open it. The page is not listed anywhere, is marked `noindex`, and sends no referrer. Under **Who can test** you pick one of two settings and press **Save**: | Setting | Effect | | --- | --- | | Anyone with the link | Guests can play and leave feedback with no account. This is the default. | | Badges only | The server only accepts sessions and feedback from testers signed in with a handle. Guests see a sign-in prompt instead of the feedback form. | > [!NOTE] > Badges only limits who can record sessions and leave feedback. Anyone with the link can still load the page and start the game. Other controls in the panel: - **Close playtest.** The link shows "This playtest is closed." Feedback so far stays in your inbox. **Reopen** brings back the same link. - **Hold to reset link.** Creates a new link. The old one stops working (it returns a 404). Use it when the link went further than you meant. Sessions and feedback are kept. Reset, the note, the questions and **Who can test** are only available while the playtest is open. Testers see changes the next time they load the link. ## Choose the build The build testers get is independent of the live build. A released game can keep its live build while testers play the next update. - **When you upload.** Once the game has a playtest, the upload form has a **Playtest** choice: **Send to testers** (the default) or **Keep their build**. It is separate from **Go live now** and **Upload only**. - **From the builds list.** The build testers have is tagged **Testing**. Any other ready build has a **Test** button that points testers at it. Use it to roll testers forward or back. Testers get the new build the next time they load the link, not in the middle of a session. The build testers have is never deleted by [build retention](/docs/builds). ## Note and questions Both live in the **Playtest** panel while the playtest is open. | Field | Limit | Where testers see it | | --- | --- | --- | | Note to testers | 1,000 characters, optional | Above the game, labelled with your handle. Say what to try and what is broken on purpose. | | Questions | Up to 5, 200 characters each | As extra fields in the feedback form. Answers can be 2,000 characters. | Blank and duplicate questions are dropped. Only answers to questions that are currently set are stored, so if you reword a question while a tester has the page open, their answer to the old wording is discarded. Change questions between rounds. ## What is captured A session starts when a tester presses **Start** on the game. Your own plays on your own playtest link do not create sessions. | What | Detail | | --- | --- | | Play time | Counted only while the tab is visible. Reported every 20 seconds and when the page is hidden or closed. Counts up to 6 hours per session. | | Browser | Chrome, Edge, Firefox or Safari with the major version. Anything else is "Other". | | Operating system | iOS and Android with the major version. Windows, macOS, ChromeOS and Linux by family only. | | Mobile | Yes or no. | | Screen | Width, height and pixel ratio, for example `1920×1080 @1x`. | | GPU renderer | The graphics renderer name the browser reports through WebGL, up to 160 characters. | | CPU cores | The core count the browser reports. | | Memory | The rounded device memory figure, only in browsers that report one. | | Errors and events | Uncaught errors, plus anything the game sends through the [SDK](/docs/sdk). See below. | That is the complete list. The server accepts only these fields and drops anything else a browser sends. Playtest data has no field for the full user-agent string, the exact browser version or an IP address. Screen size, GPU, cores and memory together narrow a device down more than a browser name does. They are stored so you can reproduce a bug ("crashes on Safari 18, iPhone"), and only you and site admins can read them. There is no setting to turn device capture off. Testers are told what is sent under the game before they press **Start**, again next to the send button, and in [For testers](#for-testers). ## SDK hooks The [SDK](/docs/sdk) reports to your inbox automatically once it is loaded. Two calls add more. ```js fradiation.track("reached-level", { level: 3 }); fradiation.askForFeedback("How was that boss?"); ``` ```csharp Fradiation.Track("reached-level", "{\"level\":3}"); Fradiation.AskForFeedback("How was that boss?"); ``` - **`track(name, data?)`** adds a moment to the session timeline. The name is cut to 80 characters. Ignored outside playtests. - **`askForFeedback(prompt?)`** scrolls the tester to the feedback form and focuses the notes field. The prompt (up to 200 characters) is shown above the form and saved with the feedback. It returns `{ opened }`, which is `false` outside playtests and on your own view of the link. - **Errors** are captured with no code from you: uncaught errors and unhandled promise rejections, deduplicated, at most 20 per page load. - **Mutations and scores** the game reports during a playtest are logged on the timeline and not kept. Testers get a "test" toast. Timeline limits: 300 events per session, 1.5 KB of data per event (larger data is dropped and the name kept), and 60 events per 10 seconds per game frame. The game gets `mode: "playtest"` from `ready()`. ## The inbox Open it from the **Inbox** button in the Playtest panel, or at `/dev/<slug>/feedback`. Your game list at `/dev` shows "N new" next to games with unread feedback. The inbox holds the newest 300 feedback items and 200 sessions. The top of the page shows testers, sessions, total play time, errors and feedback counts. ### Feedback tab - Items are grouped by the build the tester played, newest build first. Each group shows that build's patch notes and, for the current one, "in the playtest now". - Each item shows who sent it, when, the fun rating (0 to 5 in half steps), difficulty (too easy, about right, too hard), whether they would play again, answers, notes, and the prompt if the game asked for feedback. - Expand the context line to see play time, browser, OS, screen, GPU, error count and the event timeline. - Signed-in testers appear as `@handle` with a link to their profile. Guests appear as "Name (guest)" if they gave a name, otherwise "Guest" and four characters of their tester id. Filters: | Filter | Shows | | --- | --- | | New | Items you haven't touched. The default when there are any. | | Not addressed | New and read items. | | All | Everything. The default when nothing is new. | ### Sessions tab Every session, including testers who never wrote anything: who, when, build, play time, errors, how many feedback items came from it, device and timeline. ### Read and addressed Each item has **Mark read** (while it is new) and **Addressed**. An addressed item has **Reopen**, which sets it back to read and clears its fix. ## Fixed in a build Marking an item **Addressed** tells the tester where it was fixed. What is recorded depends on which build testers have when you press the button. | Testers have | What happens | Tester sees | | --- | --- | --- | | A different build than the one the feedback was about | The build they have now is recorded as the fix. | Fixed in `a1b2c3d` | | The same build the feedback was about | The fix is pending. Your inbox says "Addressed · ships with the next test build". | Fix coming | A pending fix is stamped by the next build you send to testers, through **Send to testers** at upload or **Test** in the builds list. That build becomes the "Fixed in" build for every pending item at that moment. > [!TIP] > Either order works: mark items addressed and then send the fix, or send the fix and then mark them. Only mark what you fixed, because the next send stamps everything pending. Build ids are the first 7 characters of the build hash. ## What returning testers see - **New build since you played.** When the build now in the playtest differs from the one in their last session, they see a panel with the new build's patch notes ("No patch notes for this one." if you left them blank). Write patch notes when you upload. - **Your feedback.** A list of their latest 20 feedback items with a status: Sent, Read, Fix coming or Fixed in `<build>`. - **Your note**, still above the game. Guests are remembered by a cookie, so they see this in the same browser. Signed-in testers see it on any device. ## Limits | Limit | Value | | --- | --- | | Playtests per game | 1 | | Note | 1,000 characters | | Questions | 5, at 200 characters each | | Feedback text | Notes 4,000 characters, each answer 2,000, guest name 40 | | Feedback per tester | 20 per playtest per rolling 24 hours | | Sessions per tester | 60 per playtest per rolling 24 hours | | Play time counted | 6 hours per session | | Session updates | A session stops accepting updates 24 hours after it starts | | Timeline events | 300 per session, 1.5 KB of data each | | Uncaught errors | 20 per page load, duplicates counted once | | Inbox | Newest 300 feedback items and 200 sessions | Uploading and build limits are in [Publishing](/docs/publish) and [Builds](/docs/builds). ## For testers You were sent a link. Open it and press **Start**. - **No account needed.** Guests can play and send feedback. Some playtests are badges only and ask you to sign in first. - **The form.** Rate how fun it was, say whether it was too easy or too hard, say whether you would play again, answer the developer's questions and write whatever else you want. Fill in at least one thing. You can send more than once. - **What the developer sees.** Your answers, your play time, your browser and OS (family and major version), screen size, graphics renderer, CPU cores, memory, any errors the game hit, and events the game logs. Signed-in testers show as `@handle`, which links to their public profile. Guests show as the name they typed, or "Guest" with four characters of an id. - **The guest cookie.** Guests are told apart by a first-party cookie named `fradiation_tester`, set when you press **Start** or send feedback. It lasts a year. Clear it and you are a new tester. - **What the developer does not see.** Your email or anything else private from your account. A guest is only a name you chose and an id. See [Privacy](/privacy). - **Rads.** Testers signed in with a handle earn 10 rads per feedback (the first 10 in any 24 hours) and the Test Subject mutation the first time. Playing a playtest pays nothing, and mutations and scores from the game are not kept. See [Rads and mutations](/docs/rads). - **Coming back.** When the developer sends a new build, you see what changed and what happened to your feedback. --- <!-- https://www.fradiation.games/docs/containment --> # Containment > How new games are judged. A 48-hour vote by players to release or bury, who can vote, how weight works, the exact bury rule, payouts, and how a buried game comes back. Every new game spends 48 hours in containment while players decide whether it gets out. Nobody is obliged to be gentle. ## How it works 1. A developer submits a finished draft. It appears on `/containment`. 2. For 48 hours, players who have played it vote to **release** it or **bury** it. 3. Judgment closes. A released game is listed on the site. A buried game keeps its page, but the game doesn't run. 4. A buried game can come back for another round after 7 days, with a new build. Anyone can play a game in containment. Comments and ratings are open, and they have no effect on the vote. ## Submitting Only developers submit, from `/dev/<slug>`. A draft needs a live build, a cover and a description. Hold the **Hold to submit** button for about a second. See [Publish a game](/docs/publish#submit-to-containment). The game moves to **In containment**, and the judgment window opens. The developer can keep uploading builds while it's in there. ## Who can vote All of these must be true: - You're signed in and have claimed a handle (a badge). - You have played the game while signed in. Starting the game from its page counts. - You aren't its developer. - The 48 hours haven't ended. Once you're signed in, the **Release** and **Bury** buttons stay locked until you have played the game. You can change your vote at any time before the countdown ends. Only your last vote counts. ## Vote weight A vote counts for more the higher your exposure level. The weight is set when you cast the vote, and worked out again if you change it. | Exposure levels | Rads | Vote weight | | --- | --- | --- | | 0 Clean, 1 Exposed, 2 Irradiated | 0 to 699 | 1 | | 3 Glowing, 4 Contaminated, 5 Mutating | 700 to 5,999 | 2 | | 6 Mutant, 7 Radioactive, 8 Critical | 6,000 to 49,999 | 3 | | 9 Meltdown | 50,000 or more | 4 | See [Rads and exposure](/docs/rads#vote-weight). ## The bury rule A game is buried only if both of these are true when judgment closes: 1. The total bury weight is greater than the total release weight. 2. At least 3 people voted bury. This counts people, not weight. Anything else is a release. That includes a tie, a game nobody voted on, and a game that has a bury majority but fewer than 3 buriers. | Release | Bury | Result | Why | | --- | --- | --- | --- | | No votes | No votes | Released | Nobody voted. | | 3 votes, weight 3 | 3 votes, weight 3 | Released | A tie. | | 5 votes, weight 5 | 3 votes, weight 3 | Released | Bury doesn't outweigh release. | | 1 vote, weight 1 | 2 votes, weight 8 | Released | Only 2 people voted bury. | | 2 votes, weight 2 | 3 votes, weight 3 | Buried | Bury outweighs release, and 3 people voted bury. | ## The tally While judgment is open, everyone sees how many people have voted. Nobody, including the developer, sees the release and bury split. Admins can see it. When judgment closes, the count is frozen on the game as `release <weight> · bury <weight> · <n> voters`. The release and bury numbers are weights. The voter count is people. A buried game's page shows the final count to everyone. The game's Manage page shows its developer every closed round. ## Closing Judgment ends 48 hours after the game was submitted. Votes stop at that moment. Closing has no timer. The first time any page that lists or shows games loads after the deadline, the site closes the judgment: it counts the votes, sets the status and pays out. Payouts can't happen twice. If nobody loads such a page, the close waits for the next visitor. The result is the same either way, because votes stop at 48 hours. Admins can release or bury a game before the 48 hours are up. An early close pays out like any other, and the judgment history marks it as an admin decision. ## Payouts | Who | When | Reward | | --- | --- | --- | | Voter | Casting a vote. Once per game per round. Changing your vote pays nothing more. | 3 rads and the Juror mutation | | Voter | Judgment closes and your last vote matches the result. Once per game per round. | 10 rads and the Called It mutation | | Developer | The game is released. Once per game. | 100 rads and the Breach mutation | | Developer | The game is buried. | The secret mutation Buried Alive | See [Rads and exposure](/docs/rads) for everything else that pays. ## Release A released game: - Leaves `/containment` and is listed with the other games on `/games`. - Pays its developer 100 rads and the Breach mutation. - Keeps its comments and ratings. - Gets new builds whenever its developer makes them live. ## Burial A buried game isn't deleted. - Its page stays up at `/games/<slug>`, marked **Buried**, with the final count, the round and the date its developer can send it back. - The game itself doesn't run on the page. The game is out of the listings and isn't indexed by search engines. - Comments stay readable but closed. Nobody can add comments or replies. They stay up so the developer knows what to fix. - The game's Manage page shows the developer the verdict and what is needed to come back. ## Coming back A buried game can go back into containment for another round. All of these must be true: | Requirement | Detail | | --- | --- | | 7 days | Since the verdict. | | A new build | A live build uploaded since the verdict. An upload to a buried game becomes the live build automatically. | | A cover and a description | The same as a first submission. | The Manage page shows each check, and a cooldown timer. The **Hold to resubmit** button stays disabled until all are met. The server refuses an early attempt with `It can go back in on <date>.` or `Make a build uploaded since the verdict live first. The next round judges the new version.` A new round: - Runs another 48 hours, from the moment it is submitted. - Clears every vote from earlier rounds. - Requires voters to play the new version. A play from before the round opened doesn't count, and the vote panel says `Play this version first, then judge it.` - Shows as `Containment · round 2` on the game's page, and as `R2` on its card. - Pays voters again: 3 rads and 10 rads are paid once per round. The Juror and Called It mutations are one-time. The developer's release payout is once per game. Nothing in the site limits how many rounds a game can go through. ## Judgment history Every closed round is kept. The game's Manage page lists them under **Judgment history**: the round, the outcome, the release and bury weights, the number of voters, whether an admin decided, the closing date and the first 7 characters of the build that was judged. A buried game's page shows the latest round's final count. ## As a voter - Sign in, open a game from `/containment`, click **Start**, then choose **Release** or **Bury**. - Play enough to judge it. You can change your vote until the countdown ends. - The vote decides whether the game is listed. Use ratings and comments for the rest. - Bury only counts if it outweighs release and at least 3 people agree. A lone bury vote doesn't sink a game. - Use the report button on the game's page for anything that breaks the rules. Reports go to admins. They aren't part of the vote. - Voting pays 3 rads whichever way you vote. It pays 10 more if your last vote matches the result. ## As a developer - Run a playtest first. It's a private link with a feedback form, and it works while the game is a draft. Fix what breaks before strangers see it. See [Playtests](/docs/playtests). - Tell people to play it. Votes come only from players who have played. A game nobody votes on is released, but nobody has judged it. - Have the cover and description ready. They're what people see on the containment page. - Keep the live build steady. Uploading a new build during judgment doesn't reset the votes, so voters may have judged an earlier version. Choose **Upload only** to hold an update until judgment closes. - Read the comments. They stay readable if the game is buried. - If it's buried, read the comments, run a playtest, fix what needs fixing, upload a new build and wait out the 7 days. Then resubmit and tell people to play the new version. They have to. --- <!-- https://www.fradiation.games/docs/rads --> # Rads and mutations > How players earn rads, what exposure levels do, how mutations and badges work, and what happens to progress made before you sign in. Rads are what the site pays you in. There is nothing to spend them on, and they only go up. Enough of them raise your exposure level, and a few things you do along the way hand out mutations. You don't need any of this to play a game. ## Earn rads You need a badge (an account with a handle) to earn rads. See [Badges](#badges). | Action | Rads | How often | | --- | --- | --- | | Play a game | 5 | Once per game per day (UTC). | | Rate a game | 10 | Once per game. Changing your rating doesn't pay again. | | Post a comment | 15 | Every comment, but only the first 10 in any 24 hours pay. | | Irradiate a comment | 2 | Once per comment. | | Have your comment irradiated | 1 | Once per person per comment. | | Set a score on a game's leaderboard | 5 | Once per leaderboard per game. | | Vote in containment | 3 | Once per game per round. You can change your vote for free. | | Vote with the containment verdict | 10 | Paid when judgment closes. Once per game per round. | | Leave feedback on a playtest | 10 | Every feedback, but only the first 10 in any 24 hours pay. | | Unlock a mutation | 25, 50, 100 or 250 | Once per mutation, by [tier](#tiers). | | Get your game released from containment | 100 | Developers only. Once per game. | Every award is recorded once. Repeating an action pays nothing, and nothing pays twice. Votes are explained in [Containment](/docs/containment) and feedback in [Playtests](/docs/playtests). ## Exposure levels Your total rads set your exposure level, from 0 to 9. Your level shows on your profile, and as a tag like `L3` beside your name on comments. | Level | Name | Rads needed | | --- | --- | --- | | 0 | Clean | 0 | | 1 | Exposed | 100 | | 2 | Irradiated | 300 | | 3 | Glowing | 700 | | 4 | Contaminated | 1,500 | | 5 | Mutating | 3,000 | | 6 | Mutant | 6,000 | | 7 | Radioactive | 12,000 | | 8 | Critical | 25,000 | | 9 | Meltdown | 50,000 | ### What your level does Two things. - **It sets your weight in containment votes.** See the table below. - **Level 8 unlocks the Critical Mass mutation.** Nothing else on the site depends on your level. It doesn't unlock features and it doesn't change what your comments or ratings do. ### Vote weight When new games are in [containment](/docs/containment), your vote counts for more the higher your level. | Levels | Vote weight | | --- | --- | | 0 to 2 | 1 | | 3 to 5 | 2 | | 6 to 8 | 3 | | 9 | 4 | Your weight is set when you vote. If you change your vote, it is worked out again at that moment. ## Mutations Mutations are achievements. Each one pays rads once, when you unlock it. ### Tiers | Tier | Rads | | --- | --- | | Alpha | 25 | | Beta | 50 | | Gamma | 100 | | Cherenkov | 250 | Alpha is the most common and Cherenkov is the rarest. ### Site mutations These come from things you do on the site. | Mutation | Tier | How | | --- | --- | --- | | First Contact | Alpha | Visit the site. It unlocks by itself. | | Geiger Happy | Alpha | Rate a game. | | Juror | Alpha | Vote in containment. | | Test Subject | Alpha | Leave feedback on a playtest. | | Field Reporter | Beta | Post a comment. | | Called It | Beta | Vote the way containment went. | | Cleared | Beta | Get developer access. | | Breach | Gamma | Get one of your games released from containment. | | Critical Mass | Cherenkov | Reach exposure level 8. | The set also includes a few easter eggs and some secret mutations. Secret ones show as a question mark on your profile until you unlock them, and this page won't spoil them. ### Game mutations Developers can give their games their own mutations, in the same four tiers with the same rads. You unlock one by playing the game while signed in with a badge. They appear on your profile under **Game mutations**, grouped by game. Game mutations only count when the game is public. In a draft preview or a [playtest](/docs/playtests), an unlock is logged for the developer and nothing is kept. ## Badges Your badge is your account. 1. Sign in with Discord or GitHub. Accounts that share an email address are linked. 2. Claim a handle. It is 3 to 20 characters: lowercase letters, numbers and underscores. Some words are reserved. It becomes your profile address. 3. Choose a display name, up to 40 characters. Until you claim a handle you can browse, but you can't rate, comment, vote or earn rads. ### Your profile Your profile is at `/u/<handle>`, and the account menu links to it as **Your badge**. It is public. It shows your level and rads, your mutations (locked ones included), your game mutations, your recent ratings and comments, and the games you have made if you are a developer. ## Irradiate a comment To irradiate a comment is to like it. Press the irradiate button on any comment except your own. - You get 2 rads and the author gets 1. - Pressing it again removes it. That doesn't take rads back, and irradiating the same comment again doesn't pay again. - You need a badge. ## Before you sign in Without an account, your progress lives in your browser's local storage. Playing games and the easter-egg mutations earn there. Rating, comments, votes, scores and game mutations need a badge, so they don't count. When you claim a handle: - **Mutations carry over.** First Contact and any easter-egg mutations you unlocked in this browser come with you. Each pays its tier's rads on your new account. - **Loose rads don't.** The server has to see you earn them, so rads counted only in your browser are left behind. - **The server's numbers win.** Once you are signed in, the level and rads you see are the account's. Clearing your browser's site data clears local progress. ## Developers Developers earn rads like anyone else, except from their own games: - Playing, commenting on, unlocking mutations in and setting scores on their own games pay nothing. They can still unlock the mutations and comment. - They can't rate their own games or vote on them in containment. - They can't leave feedback on their own playtest. - Plays of their own drafts don't count as plays. A developer whose game is released earns 100 rads and the Breach mutation.