castle-web-sdk
v0.4.12
Published
`castle-web-sdk` lets a deck use services provided by the Castle platform — for example, a deck can save per-player data, post and read scores on a leaderboard, or get a server-synced time for daily content.
Downloads
574
Readme
Castle Web SDK Reference
castle-web-sdk lets a deck use services provided by the Castle
platform — for example, a deck can save per-player data, post and read
scores on a leaderboard, or get a server-synced time for daily content.
Comes with castle-web init. Import what you need from
castle-web-sdk:
import { setup, initCard, Storage, Leaderboard } from "castle-web-sdk";Contents
Storage
Storage saves data for the current player. Nobody else can read it.
Use it for save files, settings, progress.
Storage.get<T>(key): Promise<T | null>
Returns the value at key, or null if not set.
const level = (await Storage.get("level")) ?? 1;Storage.set(key, value)
Sets key to value. value must be something that can convert to
JSON (null, booleans, finite numbers, strings, arrays, plain
objects). The next get(key) returns the new value immediately. Writes
save in the background.
Storage.set("level", 7);
Storage.set("settings", { sound: true, music: false });Storage.remove(key)
Removes key.
SharedStorage
SharedStorage saves data that other players can read. Values must
be something that can convert to JSON, same as Storage.
Scopes:
'deck'— one shared bucket for the whole deck. Any player can read or write.'user'— a per-player public bucket. Any player can read; only the owning player can write.
SharedStorage.get(scope, key): Promise<T | null>
Reads a shared value. For 'user', omit the user id to read the
current player's bucket, or pass one to read someone else's:
const worldHighScore = await SharedStorage.get("deck", "highScore");
const myColor = await SharedStorage.get("user", "color");
const theirColor = await SharedStorage.get("user", otherUserId, "color");SharedStorage.set(scope, key, value)
Writes a shared value. 'user' writes always go to the current
player's bucket. Writes save in the background.
SharedStorage.set("deck", "highScore", 9001);
SharedStorage.set("user", "color", "red");SharedStorage.remove(scope, key)
Removes a shared value.
Leaderboard
Leaderboard ranks players by a numeric score, per deck and per
variable name. Pick a variable name for each leaderboard the deck has
(e.g. 'score', 'time').
Leaderboard.write(variable, score, options?)
Submits a score. Only the player's best score for that variable (and
scope) is kept. In the editor it does nothing — safe to call from
gameplay code unconditionally.
options is { scope?: string }. By default the score goes to the
deck's global leaderboard for variable; pass scope to write to a
separate leaderboard (for example a daily one, or a custom id):
Leaderboard.write("score", 1200);
// A daily leaderboard: scope by the current Castle day so each day gets
// its own board (see Time.getServerDate).
const { daysSinceCastleEpoch } = await Time.getServerDate();
Leaderboard.write("score", 1200, { scope: `daily-${daysSinceCastleEpoch}` });Leaderboard.fetch(variable, type, options?): Promise<LeaderboardData>
Fetches the leaderboard for variable. type is 'high' (highest
first) or 'low' (lowest first). options.scope works the same as in
write.
If the player has written a score this session, a fetch reflects
their own new score right away — you can write then fetch and
show the result without waiting. Other players' recent scores still
appear on their own normal timing.
The returned LeaderboardData has:
list— array of entries, each{ place, value, username, userId? }.playerRank— the current player's place on the board (if they have a score).playerValue— the current player's score (if they have one).
const data = await Leaderboard.fetch("score", "high");
for (const entry of data.list) {
console.log(`${entry.place}. ${entry.username} — ${entry.value}`);
}
if (data.playerRank) {
console.log(`you are #${data.playerRank} with ${data.playerValue}`);
}Time
Time.getServerTime(): Promise<number>
Returns the current server time as a Unix timestamp in seconds.
const now = await Time.getServerTime();Time.getServerDate(timezone?): Promise<CastleDateParts>
Returns the current server time broken into date parts. timezone is
'Castle' (default; Castle's server timezone, same for every player)
or 'player' (the player's local timezone).
The returned CastleDateParts has:
sec,min,hour— time of day.day(1-31),month(1-12),year— date.wday— day of the week (1-7, Sunday = 1).yday— day of the year (1-366).daysSinceCastleEpoch— a day number that increments every day. Use it for daily content.
const date = await Time.getServerDate("player");
const dailyPuzzle = (date.daysSinceCastleEpoch % 30) + 1;User
User.getCurrent(): Promise<CastleUser>
Returns the signed-in player. Throws CastleError
(LOGIN_REQUIRED) when nobody is signed in.
The returned CastleUser has userId, username, and isActive.
const me = await User.getCurrent();
greet(me.username);Pass
A pass is something a creator sells to players for Castle bricks (the in-app currency): buy it once, own it for good. Use one to gate part of a deck behind a purchase — bonus levels, a cosmetic, supporting the creator. Set up the pass (name, art, price) on Castle; a deck refers to it by id.
Pass.has(passId): Promise<boolean>
Returns true if the current player owns the pass. No UI, nothing
charged — use it to gate content.
if (await Pass.has(bonusLevelsPassId)) {
showBonusLevels();
}Pass.offer(passId): Promise<PassOfferResult>
Presents the pass for the player to buy; resolves when they're done.
Bricks cost real money, so this only works in the Castle mobile app —
elsewhere (the website, the dev server) it resolves unavailable.
PassOfferResult has a status:
'purchased'— just bought it; grant access.'alreadyOwned'— already had it (not charged); grant access.'cancelled'— dismissed without buying.'unavailable'— can't buy here (e.g. the website).
const { status } = await Pass.offer(bonusLevelsPassId);
if (status === "purchased" || status === "alreadyOwned") {
showBonusLevels();
}Portal
A portal sends the player from this deck to another Castle deck, referred to by its deck id.
Portal.open(deckId): Promise<PortalOpenResult>
Sends the player to deckId: the Castle app swipes the feed to it, the
website opens its page. There's nowhere to go in the editor or dev server,
so there it resolves unavailable. PortalOpenResult has a status:
'navigating'— the host is moving to the target deck. Treatopenas final: this deck is torn down as it navigates away, so don't rely on code running afterward.'unavailable'— nowhere to navigate here (the editor or dev server).
portalButton.onclick = async () => {
const { status } = await Portal.open(nextDeckId);
if (status === "unavailable") {
// No feed to navigate here — fall back to your own affordance.
}
};Portal.prefetch(deckId): Promise<PortalPrefetchResult>
A best-effort hint that the player may soon open(deckId). In the Castle app
the host warms that deck (fetching it now so a later open transitions without
a cold load); everywhere else it's a no-op. PortalPrefetchResult
has a status:
'prefetching'— the host accepted the hint (or the deck was already warm).'rejected'— the host declined, e.g. this deck has already prefetched its limit of upcoming decks. Prefetch a few likely destinations, not everything.'unavailable'— the host doesn't prefetch here (the website, editor, or dev server).
link.addEventListener("pointerenter", () => {
Portal.prefetch(link.dataset.deckId);
});Haptics
Haptics plays a short device vibration ("buzz") for tactile feedback —
a tap confirmation, a success chime, an error shake. The host owns the
effect: the Castle mobile app plays a native haptic, the website uses the
browser's vibration API where available, and the dev server or an
unsupported device does nothing.
Haptics.play(style): Promise<HapticsResult>
Plays a haptic in one of seven styles:
'light','medium','heavy'— impact taps of increasing strength.'selection'— a light tick, e.g. moving through options.'success','warning','error'— notification patterns.
Usually you don't await it — fire it and move on:
button.onclick = () => {
Haptics.play("light");
doTheThing();
};The returned HapticsResult has a status, if you want to branch on it:
'triggered'— the host played (or accepted) the haptic.'unavailable'— this host or device can't play haptics (e.g. the dev server, or a browser with no vibration support). Nothing was played.
Haptics respect the player's settings — if a player has muted haptics,
play does nothing and resolves 'unavailable'. Rapid repeated calls may
be coalesced, so it's safe to call on frequent events.
Lifecycle
Lifecycle tells the host when the deck has painted its first frame, so
the Castle feed can reveal it right away instead of waiting out a fixed
delay.
Lifecycle.ready()
Signal that the deck has painted its first presentable frame. Idempotent — only the first call has any effect.
Kits already wire this up to fire as soon as the first frame paints; if you change how the deck first renders, make sure it still fires there. If nothing calls this, the feed reveals the deck on its own after a brief timeout.
If you call it right after kicking off your initial render, wait for the next paint first so the host doesn't reveal a blank frame:
createRoot(root).render(<App />);
requestAnimationFrame(() => requestAnimationFrame(() => Lifecycle.ready()));Setup
Startup, editor-mode check, and a file-write call for editor UI.
setup()
Call this once at the start of the deck, before any other SDK call.
setup() initializes the SDK so the rest of the API is usable and
mounts a 5:7 Castle card around whatever the deck renders into #root
in play mode. The SDK preserves the card aspect ratio; Castle hosts own
the card's max size, placement, and surrounding padding.
While running locally with castle-web serve, it also forwards
console output to the CLI and reloads the page when castle-web
restart runs.
import { setup } from "castle-web-sdk";
setup();initCard(): HTMLDivElement
Use this when the deck draws into a <canvas> (or anything else)
rather than into the React tree at #root. Returns a <div> with the
standard Castle 5:7 card aspect ratio. The div resizes itself when the
window resizes. In Castle-hosted iframes/WebViews it fills the host's
available card frame.
import { setup, initCard } from "castle-web-sdk";
setup();
const card = initCard();
const canvas = document.createElement("canvas");
canvas.style.cssText = "width: 100%; height: 100%; display: block;";
card.appendChild(canvas);If the deck mounts a React tree into #root instead, you don't need
initCard() — setup() already wraps #root's children in a card.
Host-specific layout flags such as CastleEmbed.feed are deprecated.
New code should let Castle hosts provide the frame and let the SDK infer
whether it is running standalone or embedded.
CARD_RATIO
The card aspect ratio (5 / 7). Use this if you need to size something
to match the card.
isEdit(): boolean
true when the deck is being edited, false when it's being played.
Use this to show editor UI only in edit mode.
import { isEdit, setup } from "castle-web-sdk";
setup();
if (isEdit()) {
mountEditor();
} else {
startGame();
}writeFile(path, contents): Promise<void>
Writes a file in the deck directory. path is relative to the deck
root, contents is a string. Use this from editor UI to save scenes,
drawings, or generated source.
import { writeFile } from "castle-web-sdk";
await writeFile("scenes/main.scene", JSON.stringify(scene, null, 2));Only works while editing locally with castle-web serve. Calls from a
published deck fail.
fileUrl(path): string
A URL that serves a deck file as raw bytes. Point an <img>, <audio>,
or <video> at it when editor UI has to show a file the browser renders
natively — writeFile's counterpart reads text, which is nothing an
image or a sound can be read as.
import { fileUrl } from "castle-web-sdk";
<img src={fileUrl("assets/logo.png")} alt="" />;Like writeFile, this only means anything while editing locally with
castle-web serve — a published deck has no serve to ask, and should
load its assets through the bundler instead.
CastleError
Every error the SDK throws is a CastleError. Check code to tell
the kinds apart.
Common codes:
LOGIN_REQUIRED— the player needs to be signed in.MISSING_DECK_ID— the deck hasn't been saved to Castle yet, so it has no id.CASTLE_STORAGE_SERIALIZE_FAILED— value isn't plain JSON (e.g. a class instance, a function, a non-finite number, or a cycle).INVALID_LEADERBOARD_VARIABLE,INVALID_LEADERBOARD_SCORE,INVALID_LEADERBOARD_TYPE— bad argument to aLeaderboardcall.UNSUPPORTED_TIMEZONE—Time.getServerDategot a zone other than'Castle'or'player'.CASTLE_HOST_UNAVAILABLE— the Castle host (the app or website running the deck) didn't handle the request — e.g. it timed out or wasn't reachable. Usually transient; retry or surface a gentle error.
