castle-web-sdk
v0.4.6
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.
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();
}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 the centered 5:7 card shell around whatever the deck renders
into #root (when the deck is being played standalone in a browser).
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 centered,
viewport-sized <div> with the standard Castle 5:7 card aspect ratio.
The div resizes itself when the window resizes.
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.
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.
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.
