# AI guide: RPG Desk sheets and tables (rpgdesk.js 1)

You are going to create a **character sheet** (or a **room table**) for RPG Desk, a virtual tabletop
for RPGs. The sheet is **a single HTML file**, with its CSS and JavaScript inside it. RPG Desk shows
that file in an isolated frame and keeps the sheet's data for everyone at the table.

Follow the rules below to the letter. Always deliver **the whole `.html` file**, ready to save
(or a `.zip` package, only if the sheet uses a library; see rule 1 in section 2).

## 1. The file format

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <script type="application/rpgdesk+json" id="rpgdesk-manifest">
  {
    "rpgdesk": 1,
    "id": "com.yourname.system",
    "name": "System Name",
    "version": "1.0.0",
    "author": "Your name",
    "sdk": 1,
    "sheets": [{ "type": "com.yourname.system.sheet", "title": "Character" }]
  }
  </script>
  <script src="https://docs.rpgdesk.com/_sdk/1/rpgdesk.js"></script>
  <link rel="stylesheet" href="https://docs.rpgdesk.com/_sdk/1/rpgdesk.css">
  <style>
    /* the sheet's CSS */
  </style>
</head>
<body>
  <!-- the sheet -->
  <script>
    // the sheet's JavaScript (optional)
  </script>
</body>
</html>
```

- The `<script type="application/rpgdesk+json">` is the **manifest**. It must be valid JSON: double
  quotes, no comments, no comma after the last item.
- `id`: lowercase letters, numbers, `.`, `-` and `_` only. Use a "reverse domain"
  (`com.yourname.system`).
- The sheet's `type`: **starts with the `id` and a dot** (`com.yourname.system.sheet`).
- `version`: three numbers (`1.0.0`). Bump the version with every change.
- For a **room table** (NPC list, initiative, etc.), replace `"sheets"` with
  `"tables": [{ "type": "com.yourname.system.npcs", "title": "NPCs" }]`. A file holds a sheet **or**
  a table, never both.
- The two lines for `rpgdesk.js` and `rpgdesk.css` go exactly as above. `rpgdesk.css` is optional (it
  gives the RPG Desk look); without it, write your own CSS.

## 2. Environment rules (mandatory)

The sheet runs in an isolated frame **with no internet**. So:

1. **No outside addresses**: no `<script src="https://...">` (other than rpgdesk.js), no Tailwind
   from a CDN, no Google Fonts, no React from a CDN. Prefer plain JavaScript and plain CSS, all
   inside the file.
   - **Libraries such as Alpine.js and jQuery work**, as long as their file goes along. For that,
     deliver a **`.zip` package** instead of the `.html`: the manifest goes in a `manifest.json` at
     the root, with `"entry": "sheet.html"` on the sheet, and the library in a `vendor/` folder
     (`<script src="vendor/alpine.min.js"></script>`). Don't write the library from memory: tell the
     person where to download the `.min.js` and where to save it.
   - With Alpine, use `x-data`, `x-show` and `x-for` for the screen (tabs, showing and hiding) and
     `data-field` for the data. Never `x-model` and `data-field` on the same field.
2. **No `fetch` to the internet**, no `XMLHttpRequest`, no `WebSocket`.
3. **No `localStorage`, `sessionStorage`, `indexedDB` or cookies** (the frame blocks them). The
   sheet's data goes in the document (`doc`); the person's preferences, in `RPGDesk.storage`.
4. **No `alert`, `confirm` or `prompt`** (they don't work). Use `RPGDesk.ui.toast`,
   `await RPGDesk.ui.confirm(...)` and `await RPGDesk.ui.prompt(...)`.
5. **No `<form>` that submits, no links that change the page, no `window.open`, no
   `location = ...`**. The sheet is a single page; for different parts, use tabs (`data-tabs`). To
   open a website: `RPGDesk.ui.openLink(url)`.
6. **Images**: small drawings as SVG inside the HTML, or `data:` URLs. The character's portrait
   comes from RPG Desk with `<img data-image="portrait">` (see below). Don't use images from the
   internet.
7. Don't use `<iframe>`.

## 3. The sheet's data

Each sheet has a **document** with fields. A field has a **path**:

- `name`, `level`, `attributes.strength`, `skills.stealth.bonus`
- Names: **letters without accents, numbers and `_`**, starting with a letter. `strength`, not
  `strength level`.
- Case doesn't matter: `Strength` and `strength` are the same field.
- Values: text, number or true/false.
- **Lists** (inventory, spells, attacks): each item has its own `id` and its own fields.

Who can write: on a sheet, **the owner and the GMs**; everyone else only reads. On a room table,
**only the GMs** write. rpgdesk.js locks the fields of whoever can't write on its own.

## 4. HTML attributes (sheets with no JavaScript)

rpgdesk.js wires these attributes on its own, including on elements created later:

| Attribute | Where | What it does |
| --- | --- | --- |
| `data-field="path"` | `<input>`, `<textarea>`, `<select>` | binds the field to the document. `type="number"` saves a number; `type="checkbox"`, true/false; `type="radio"`, the checked `value` |
| `data-text="path"` | any element | shows the field's value as text |
| `data-image="path"` | `<img>` | shows the chosen image; clicking opens RPG Desk's image picker |
| `data-list="path"` + `<template>` | `<ul>`, `<div>`… | repeats the `<template>` for each item; inside it, paths are relative to the item |
| `data-list-add="path"` | `<button>` | adds an empty item to the list |
| `data-list-remove` | `<button>` inside the `<template>` | removes that item |
| `data-roll="1d20 + {path}"` | `<button>` | rolls the dice in RPG Desk (shows in the chat for everyone); `{path}` becomes the field's number |
| `data-label="{name}'s check"` | next to `data-roll` | the roll's name; `{path}` becomes the field's text |
| `data-tabs` + `data-tab="x"` + `data-tab-panel="x"` | buttons and sections | tabs: only the chosen tab's section shows |
| `data-role="gm"` | any element | only GMs see it (it's just looks, not a secret) |
| `data-readonly` | field | never edits (shows the value) |

Rolls: `1d20+5`, `2d6 + 3`, `3d6`, `1d100`, `4dF` (Fudge), `5d10 > 7` (counts successes of 7 or
more). In `data-roll`, use `{path}` only with numeric fields.

List example:

```html
<ul data-list="inventory">
  <template>
    <li class="rd-row">
      <input data-field="name" placeholder="Item">
      <input type="number" data-field="qty" min="0">
      <button data-list-remove>×</button>
    </li>
  </template>
</ul>
<button data-list-add="inventory">+ Item</button>
```

Tabs example:

```html
<nav data-tabs>
  <button data-tab="attributes">Attributes</button>
  <button data-tab="inventory">Inventory</button>
</nav>
<section data-tab-panel="attributes">…</section>
<section data-tab-panel="inventory">…</section>
```

## 5. The JavaScript API

Use it when the attributes aren't enough (calculations, special buttons).

```js
RPGDesk.connect().then(({ doc, me, kind, item, room, language }) => {
  // doc: the sheet's document
  // me: { id, nick, role: 'gm' | 'player' | 'spectator', isOwner }
});
```

**Document (`doc`)**

- `doc.get('attributes.strength')`: the value (or `undefined`). `doc.get('attributes')` returns an
  object.
- `await doc.set('attributes.strength', 14)`: saves. `doc.set('hp', { current: 10, max: 12 })` saves
  field by field. `doc.set('field', null)` deletes.
- `await doc.update({ 'hp.current': 3, 'hp.max': 10 })`: several fields at once.
- `doc.watch('hp', (value, { local }) => { ... })`: notifies on every change (from anyone).
  `{ now: true }` as the third argument calls it right away with the current value. Returns the
  function that stops it.
- `doc.compute(target, [sources], function)`: **computed field**. Example:
  `doc.compute('str_mod', ['attributes.strength'], (s) => Math.floor(((Number(s) || 10) - 10) / 2))`.
  Always use `compute` for derived values; **don't** save inside a `watch` (everyone with the sheet
  open would save again).
- `doc.canWrite('path')`: whether the person can write there.
- Lists: `const inv = doc.list('inventory')`
  - `inv.items()`: `[{ id, name, qty }, ...]`
  - `const id = inv.add({ name: 'Rope', qty: 1 })`
  - `inv.item(id).set('qty', 2)`, `inv.remove(id)`, `inv.move(id, 0)`
  - `inv.watch((items) => { ... })`
- `doc.on('rejected', (message) => ...)`: a write was refused (no permission).

**Dice, chat and table**

- `const r = await RPGDesk.dice.roll('1d20+5', { label: 'Attack' })` → `r.total`, `r.items`.
  The roll shows in the chat for everyone, with the 3D dice.
- `RPGDesk.chat.send('attacks the orc', { kind: 'action' })`. `kind`: `'chat'`, `'action'`,
  `'narrator'`, `'npc'` (with `name`).
- `RPGDesk.chat.local('only I see this')`.
- `await RPGDesk.room.players()` → `[{ id, nick, role, avatar }]`.
- `RPGDesk.card.setBar(0, currentHp, maxHp)`: bar 0 on the card of the player who owns the sheet
  (health, for example). `RPGDesk.card.setLine(0, 'Level 3')`. Bars and lines go from 0 to 3.

**Interface**

- `RPGDesk.ui.toast('Saved!')`, `await RPGDesk.ui.confirm('Erase everything?')` (true/false),
  `await RPGDesk.ui.prompt('Item name?', 'Rope')` (text or null).
- `await RPGDesk.ui.menu([{ id: 'a', label: 'Attack' }, { separator: true }, { id: 'x', label: 'Delete', danger: true }], event)`.
- `RPGDesk.ui.openLink('https://...')`, `RPGDesk.ui.copy(text)`, `RPGDesk.ui.download('sheet.json', text)`.
- `const img = await RPGDesk.files.pickImage()` → `{ id }`; keep the `id` in a field and show it with
  `<img data-image="field">` (or `img.src = await RPGDesk.files.url(id)`).
- `RPGDesk.storage.get(key)` / `RPGDesk.storage.set(key, value)`: the person's preferences.
- `RPGDesk.view.get(key)` / `RPGDesk.view.set(key, value)`: screen state (open tab, filter).

**Integrations (optional)**: if the sheet needs to send something out (Discord, a spreadsheet,
lights), declare in the manifest `"webhooks": [{ "name": "rolls", "description": "Every roll" }]`
and call `await RPGDesk.webhooks.send('rolls', { total: 17 })`. The GM registers the address
(Zapier, Make or IFTTT) in RPG Desk. Don't put addresses in the code.

**GM-only parts**: in the sheet's manifest,
`"sheets": [{ "type": "...", "title": "...", "private": { "gm_notes": "gm" } }]`. The field
`gm_notes` (and whatever is inside it) only reaches the GMs. With `"owner"`, only the sheet's
owner and the GMs.

## 6. Look

- `rpgdesk.css` already styles fields, buttons, tabs (`data-tabs`) and lists in RPG Desk's dark theme.
- Ready-made classes: `rd-row` (a row that wraps), `rd-col` (a column), `rd-grid` (a grid of
  cards), `rd-card` (a box), `rd-grow` (takes the remaining space), `rd-primary` (a highlighted
  button), `rd-muted` (discreet text).
- Theme colors as variables: `var(--rd-text)`, `var(--rd-muted)`, `var(--rd-accent)`,
  `var(--rd-bg)`, `var(--rd-bg-2)`, `var(--rd-bg-3)`, `var(--rd-line)`, `var(--rd-error)`.
- The sheet shows in a panel that can be narrow (about 350 px) or wide: use `flex-wrap` and grids
  that adapt. The page scrolls on its own.

## 7. Testing

Open the file in the browser (double-click). Without RPG Desk around it, rpgdesk.js enters **test
mode**: the data stays in the browser, rolls show up in the "Log", and the band in the corner lets
you switch roles (GM, sheet owner, another player) to see what each can do.

## 8. Before delivering, check

- [ ] The manifest is valid JSON, with `rpgdesk`, `id`, `name`, `version`, `sdk` and `sheets` (or
  `tables`).
- [ ] The `type` starts with the `id` and a dot.
- [ ] The `rpgdesk.js` and `rpgdesk.css` lines are in the `<head>`, exactly as in the template.
- [ ] No outside `http` address (scripts, fonts, images, `fetch`).
- [ ] No `alert`, `confirm`, `prompt`, `localStorage`, submitting `<form>` or `location =`.
- [ ] Every `data-field`, `data-text`, `data-list` uses names without accents or spaces.
- [ ] Computed values use `doc.compute`.
- [ ] It works narrow (350 px) and wide.

## 9. Complete example

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <script type="application/rpgdesk+json" id="rpgdesk-manifest">
  {
    "rpgdesk": 1,
    "id": "br.rpgdesk.example.d20",
    "name": "Generic D20",
    "version": "1.0.0",
    "author": "RPG Desk",
    "sdk": 1,
    "sheets": [{ "type": "br.rpgdesk.example.d20.sheet", "title": "Character", "private": { "gm": "gm" } }]
  }
  </script>
  <script src="https://docs.rpgdesk.com/_sdk/1/rpgdesk.js"></script>
  <link rel="stylesheet" href="https://docs.rpgdesk.com/_sdk/1/rpgdesk.css">
  <style>
    .top { display: flex; gap: 12px; align-items: flex-start; flex-wrap: wrap; }
    .portrait { width: 96px; height: 96px; }
    .attribute { text-align: center; }
    .attribute b { display: block; font-size: 20px; }
  </style>
</head>
<body>
  <div class="top">
    <img class="portrait" data-image="portrait" alt="Portrait">
    <div class="rd-col rd-grow">
      <label>Name <input data-field="name"></label>
      <div class="rd-row">
        <label>Class <input data-field="class"></label>
        <label>Level <input type="number" data-field="level" min="1"></label>
        <label>HP <input type="number" data-field="hp.current"></label>
        <label>Max HP <input type="number" data-field="hp.max"></label>
      </div>
    </div>
  </div>

  <nav data-tabs>
    <button data-tab="attributes">Attributes</button>
    <button data-tab="inventory">Inventory</button>
    <button data-tab="gm" data-role="gm">GM</button>
  </nav>

  <section data-tab-panel="attributes" class="rd-grid">
    <div class="rd-card attribute">
      <label>Strength <input type="number" data-field="strength"></label>
      <b data-text="str_mod"></b>
      <button data-roll="1d20 + {str_mod}" data-label="{name}'s Strength">Roll</button>
    </div>
    <div class="rd-card attribute">
      <label>Dexterity <input type="number" data-field="dexterity"></label>
      <b data-text="dex_mod"></b>
      <button data-roll="1d20 + {dex_mod}" data-label="{name}'s Dexterity">Roll</button>
    </div>
  </section>

  <section data-tab-panel="inventory">
    <ul data-list="inventory">
      <template>
        <li class="rd-row">
          <input class="rd-grow" data-field="name" placeholder="Item">
          <input type="number" data-field="qty" min="0">
          <button data-list-remove>×</button>
        </li>
      </template>
    </ul>
    <button data-list-add="inventory">+ Item</button>
  </section>

  <section data-tab-panel="gm">
    <textarea data-field="gm.notes" placeholder="Only GMs see this"></textarea>
  </section>

  <script>
    const mod = (v) => Math.floor(((Number(v) || 10) - 10) / 2);
    RPGDesk.connect().then(({ doc }) => {
      doc.compute('str_mod', ['strength'], mod);
      doc.compute('dex_mod', ['dexterity'], mod);
      // hit points go to bar 0 of the player's card
      doc.watch('hp', (hp) => {
        if (hp && typeof hp === 'object') RPGDesk.card.setBar(0, Number(hp.current) || 0, Number(hp.max) || 0);
      }, { now: true });
    });
  </script>
</body>
</html>
```
