# @mmorada/sdk

The SDK of games on [Mmorada](https://www.mmorada.com): who is playing, cloud saves, achievements,
leaderboards, presence, the store, the play session. Version 1.2.

## Use it

The build loads the SDK from its own origin (the runtime serves it; no install, always current):

```html
<script src="/sdk/v1.js"></script>
<script>
  Mmorada.init().then(async (ctx) => {
    if (!ctx.user) {
      // Library games can be played signed out; ask when it matters.
      document.querySelector("#sign-in").onclick = () => Mmorada.signIn();
      return;
    }
    const save = await Mmorada.saves.get("main");
    start(save?.data ?? newGame());
  });
</script>
```

Or bundle it (types included): `npm i @mmorada/sdk` and `import Mmorada from "@mmorada/sdk"`.
This README, the types and the protocol are also served by the site: https://www.mmorada.com/dev/sdk.md,
https://www.mmorada.com/dev/sdk.d.ts and https://www.mmorada.com/dev/protocol.md; the guide for bringing
a game (written for coding agents too) is https://www.mmorada.com/dev/llms.txt.

Outside the platform (your dev server, a file) the SDK runs in **dev mode**: a local player named
`dev`, saves in `localStorage`, nothing sent anywhere. The same code runs in both places.

## API

- `Mmorada.init(options?)` → `Promise<Context>`: connects to the page around the game. `Context` has
  `appId`, `versionId`, `channel`, `lang` (the language the player reads Mmorada in, a BCP 47 tag: `"es"`, `"en"`, `"pt-BR"`, `"bg"`…), `user` (or `null`), `scopes`, `dev`.
- `Mmorada.user` — the player, or `null`.
- `Mmorada.signIn()` — asks the platform to sign the player in (the page comes back to the game).
- `Mmorada.profile()` — the player, with what the `profile:read` scope allows (avatar, frame, title).
- `Mmorada.saves.get(slot?)`, `.set(slot, data, { ifVersion? })`, `.delete(slot)`, `.list()` — cloud
  saves (the `saves` scope): up to 10 slots of 1 MB of JSON each. `ifVersion` refuses to overwrite a
  newer save from another device; the `MmoradaError` then has `code: "conflict"` and `current`.
- `Mmorada.achievements.unlock(key)` → `{ unlocked, unlockedAt, achievement }` (the `achievements:write`
  scope): unlocks one of the achievements you defined on the portal (Juego tab) for the player; a
  repeat answers `unlocked: false`. `Mmorada.achievements.list()` works signed out too and says what
  the player has unlocked (a hidden one keeps its name until then).
- `Mmorada.leaderboards.submit(key, score)` → `{ best, improved, rank, period }` (the `leaderboards:write`
  scope): an integer score for a table you defined on the portal; the player's best of the period is
  kept. `Mmorada.leaderboards.top(key, { period?, limit? })` reads a table (signed out too), with the
  player's own entry and rank in `me`; `Mmorada.leaderboards.list()` names the tables.
- `Mmorada.competitive.get({ period?, scope?, limit? })` → `Competitive` (no scope; signed out too):
  Mmorada's competitive standard as this game sees it. Every game plays in it without building
  anything: its achievements give ranking points by rarity, its weekly tables give points for the
  places, and its ranked modes play quarterly seasons with tiers (champion, top 10, elite). `board` is
  the ranking (`period`: `"day"`, `"week"` (default), `"month"` or `"all"`; `scope`: `"game"` (default)
  or `"all"` for every game), `season` the current one, and `me` (signed in) the player's level, place,
  firsts, and for each ranked mode their level, matches this season and how the last one ended.
  Show it in a results screen or a menu; the history lives on the player's profile for good.
- `Mmorada.presence.set(detail)` (the `presence:write` scope): "jugando <your game>" appears on the
  player's profile by itself while the tab is open; this adds what they are doing (`"Nivel 3"`, at
  most 60 characters; `""` clears it).
- `Mmorada.store.buy(key)` → `Promise<Purchase>` (the `store` scope): sells one of the items you
  defined on the portal (Tienda tab), or `"app"` for the game itself. The platform's purchase screen
  opens over the game and the promise resolves when the player decides: `state` is `"confirmed"`
  (and `entitlement` says what they own now) or `"cancelled"`. The game never touches money: the
  balance is charged by the platform, on its own screen. `Mmorada.store.list()` (signed out too)
  gives the items with prices in US cents and what the player owns; `.entitlements()` and `.owns(key)`
  say what they own; `.consume(key, n?)` uses up a consumable (`not_enough` when none is left).
  In dev mode everything is bought at once and kept in `localStorage`.
- `Mmorada.getToken()` — the game session token, for a backend of your own (verify it against
  `https://account.mmorada.com/.well-known/jwks.json`; `aud` is your app id).
- `Mmorada.on("user" | "token" | "session-ended", handler)` → unsubscribe.
- `Mmorada.reportError(error)` — uncaught errors are reported by themselves, to your portal.
- `Mmorada.feedback({ kind?, context? })` → `{ sent }` — opens the platform's feedback screen over the game ("an error", "an idea", "something else"); what the player writes reaches your portal's Opiniones tab with the version and your `context` (a small object: the level, the position, the seed; 2 KB at most). Put it behind a key or a pause-menu button.

- `Mmorada.rooms` (the `rooms` scope): playing with other people, in the modes declared on the portal (Multijugador tab).
  `create(mode)` makes a room with a code to share, `join(code)` enters someone's, `find(mode, { onWaiting })` finds a match
  near the player's level (modes with rules and matchmaking), and `rooms.invited` is the code of a room a friend invited the
  player to. A `Room` has `code`, `me`, `players`, `host`, `phase` and events (`on("players" | "state" | "event" | "start" |
  "refused" | "shared" | "mine" | "message" | "end" | "reconnecting" | "close" | "error")`).
  - **Shared rooms** (the platform relays; the game decides): `room.set(patch)` (a shared object), `room.setMine(data)`
    (each player's own), `room.sendMessage(data, to?)`, `room.shared`, `room.mine`. The first player is the host; the next
    one when they leave.
  - **Rules rooms** (the game's `server.js` decides): `room.act(type, data)`, `room.view` (what `view()` gives this player),
    `room.start()` (the host of a room with a code), and `end` with everyone's place. Matches from matchmaking move the
    player's level (OpenSkill); rooms with a code never do.
  - `room.invite(handle)` sends a Mmorada notification; `room.leave()` frees the seat. A dropped connection comes back by
    itself for a minute.

  `server.js` sits at the root of the build (ES modules; it may import other files of the build with relative paths):

  ```js
  export default {
    setup({ players, random }) { return { deck: shuffle(cards), hands: {} }; },
    action(state, { player, type, data }, ctx) { /* check, change the state, or throw to refuse */ },
    timer(state, name, ctx) {}, tick(state, ctx) {}, ghost(state, { stage, player, ghost }, ctx) {}, leave(state, player, ctx) {},
    view(state, player) { return { ...state, hands: { [player]: state.hands[player] } }; }, // hide the rest
  };
  // ctx: players, mode, now, random(), int(n), shuffle(list), setTimer(name, seconds), clearTimer(name),
  //      emit(event, data, to?), end({ winner } | { ranking } | { scores }), saveGhost(player, stage, data), findGhost(player, stage)
  ```

  Chance comes only from `random()` (`Math.random` is the match's too): the same seed and actions replay a match.
  In dev mode, rooms play between tabs of the same browser (open the game twice); pass `Mmorada.init({ devModes })` to
  describe the modes there.

Errors are `MmoradaError` with a `code`: `signed_out`, `no_scope`, `conflict`, `too_big`,
`too_many_slots`, `unknown_key`, `network`, `api`.

## Scopes

Your game asks only for what it uses (on the portal, under Ajustes); the player sees the list the
first time. `openid` and `profile` come with every token; `saves`, `profile:read`,
`achievements:write`, `leaderboards:write`, `presence:write`, `store` and `rooms` can be asked for.

See `PROTOCOL.md` for what the SDK and the play page say to each other.
