Docs / Build with it
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 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. |
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.mtsInvoke-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.mtsTake 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.
<script src="fradiation-sdk.js"></script>
<script>
fradiation.ready().then((session) => console.log(session.mode));
</script>ES module
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:
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;
}ready
ready(timeoutMs?: number): Promise<Session>Handshake with the site. timeoutMs defaults to 1500.
connected:truewhen the site answered.player: the signed-in player, ornull.mode:"live","preview"or"playtest". See 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
playerandmode, and call it early: error capture starts when the handshake completes.
const { connected, player, mode } = await fradiation.ready();
if (player) {
document.querySelector("#player-name")!.textContent = player.displayName;
}unlockMutation
unlockMutation(key: string): Promise<UnlockResult>Unlocks a mutation declared in fradiation.json.
unlockedistruethe first time this player unlocks it andfalseafter that. Calling again is harmless.signedInisfalsewhen 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. Developers get none from their own games.
- A key that isn't in
fradiation.jsonresolves with the defaults:{ signedIn: false, unlocked: false }.
async function onBossDefeated(difficulty: "normal" | "hard") {
await fradiation.unlockMutation("first-blood");
if (difficulty === "hard") await fradiation.unlockMutation("boss-hard");
}submitScore
submitScore(board: string, value: number): Promise<ScoreResult>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,
NaNorInfinity, and any value above the board'smax. - Time boards take milliseconds.
- The first score on a board pays the player 5 rads, once. Developers get none.
const { signedIn, isBest, rank } = await fradiation.submitScore("high-score", 48210);
if (signedIn && isBest) console.log(`New best, rank ${rank}`);track
track(name: string, data?: unknown): voidMarks a moment on the playtest timeline. It returns at once and there is nothing to await.
nameis cut to 80 characters.datamust 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.
fradiation.track("level-start", { level: 3 });
fradiation.track("died", { level: 3, cause: "lava" });askForFeedback
askForFeedback(prompt?: string): Promise<FeedbackResult>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.
promptis 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.
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.errorcalls 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/<slug>, visible to you and admins | Checked, not kept. A "Test unlock" or "Test score" toast says so. | Ignored | { opened: false } |
playtest | A private /t/<token> 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/<slug> 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/<slug>/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.
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.
const unlocked = new Set<string>();
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.
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
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.
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.
- Godot: see Godot.
- Three.js and plain web: see Three.js and web.
How it talks to the site
- The game sends one
hellomessage 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://<slug>.containment.cloud. It then answers with a privateMessageChannelport. 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.gamesorhttp://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 (trackand 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.