Skip to content

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.

.md

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.

FileUse it for
https://www.fradiation.games/sdk/fradiation-sdk.jsA script tag. Sets window.fradiation.
https://www.fradiation.games/sdk/fradiation-sdk.mjsAn ES module: import { fradiation } from "./fradiation-sdk.mjs"
https://www.fradiation.games/sdk/fradiation-sdk.d.mtsTypeScript 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
<script src="fradiation-sdk.js"></script>
<script>
  fradiation.ready().then((session) => console.log(session.mode));
</script>

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;
}

ready

ts
ready(timeoutMs?: number): Promise<Session>

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.
  • 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<UnlockResult>

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. 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<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, 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<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.
  • 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().

ModeWhereMutations and scorestrack and errorsaskForFeedback
liveThe public game pageChecked and kept for signed-in players. Rads awarded.Ignored{ opened: false }
previewYour draft at /games/<slug>, visible to you and adminsChecked, not kept. A "Test unlock" or "Test score" toast says so.Ignored{ opened: false }
playtestA private /t/<token> linkLogged on the timeline, not checked, not kept. A test toast says so.LoggedOpens 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.

WhereWhat you get
Local dev, any other hostconnected: 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 sitePreview 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 linkPlaytest 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.

ts
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.

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.
  • Godot: see Godot.
  • Three.js and plain web: see Three.js and 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://<slug>.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.