# Guia para IA: fichas e tabelas do RPG Desk (rpgdesk.js 1)

Você vai criar uma **ficha de personagem** (ou uma **tabela da sala**) para o RPG Desk, uma mesa
virtual de RPG. A ficha é **um arquivo HTML só**, com CSS e JavaScript dentro dele. O RPG Desk
mostra esse arquivo num quadro isolado e guarda os dados da ficha para todos na mesa.

Siga as regras abaixo à risca. Entregue sempre **o arquivo `.html` inteiro**, pronto para salvar
(ou um pacote `.zip`, só se a ficha usar uma biblioteca; veja a regra 1 da seção 2).

## 1. O formato do arquivo

```html
<!doctype html>
<html lang="pt-BR">
<head>
  <meta charset="utf-8">
  <script type="application/rpgdesk+json" id="rpgdesk-manifest">
  {
    "rpgdesk": 1,
    "id": "com.seunome.sistema",
    "name": "Nome do Sistema",
    "version": "1.0.0",
    "author": "Seu nome",
    "sdk": 1,
    "sheets": [{ "type": "com.seunome.sistema.ficha", "title": "Personagem" }]
  }
  </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>
    /* o CSS da ficha */
  </style>
</head>
<body>
  <!-- a ficha -->
  <script>
    // o JavaScript da ficha (opcional)
  </script>
</body>
</html>
```

- O `<script type="application/rpgdesk+json">` é o **manifesto**. Ele precisa ser JSON válido: aspas
  duplas, sem comentários, sem vírgula depois do último item.
- `id`: só minúsculas, números, `.`, `-` e `_`. Use um "domínio invertido" (`com.seunome.sistema`).
- `type` da ficha: **começa com o `id` e um ponto** (`com.seunome.sistema.ficha`).
- `version`: três números (`1.0.0`). Suba a versão a cada mudança.
- Para uma **tabela da sala** (lista de NPCs, iniciativa, etc.), troque `"sheets"` por
  `"tables": [{ "type": "com.seunome.sistema.npcs", "title": "NPCs" }]`. Um arquivo traz uma ficha
  **ou** uma tabela, nunca as duas.
- As duas linhas do `rpgdesk.js` e do `rpgdesk.css` vão exatamente como acima. O `rpgdesk.css` é opcional
  (dá o visual do RPG Desk); sem ele, faça o seu próprio CSS.

## 2. Regras do ambiente (obrigatórias)

A ficha roda num quadro isolado e **sem internet**. Por isso:

1. **Nada de endereços de fora**: nada de `<script src="https://...">` (além do rpgdesk.js),
   Tailwind por CDN, Google Fonts, React por CDN. Prefira JavaScript puro e CSS puro, tudo dentro
   do arquivo.
   - **Bibliotecas como a Alpine.js e o jQuery funcionam**, desde que o arquivo delas vá junto. Para
     isso, entregue um **pacote `.zip`** em vez do `.html`: o manifesto vai num `manifest.json` na
     raiz, com `"entry": "ficha.html"` na ficha, e a biblioteca numa pasta `vendor/`
     (`<script src="vendor/alpine.min.js"></script>`). Não escreva a biblioteca de memória: diga à
     pessoa de onde baixar o `.min.js` e onde salvar.
   - Com a Alpine, use `x-data`, `x-show` e `x-for` para a tela (abas, mostrar e esconder) e
     `data-field` para os dados. Nunca `x-model` e `data-field` no mesmo campo.
2. **Nada de `fetch` para a internet**, `XMLHttpRequest` ou `WebSocket`.
3. **Nada de `localStorage`, `sessionStorage`, `indexedDB` ou cookies** (o quadro bloqueia).
   Os dados da ficha vão no documento (`doc`); preferências da pessoa, em `RPGDesk.storage`.
4. **Nada de `alert`, `confirm` ou `prompt`** (não funcionam). Use `RPGDesk.ui.toast`,
   `await RPGDesk.ui.confirm(...)` e `await RPGDesk.ui.prompt(...)`.
5. **Nada de `<form>` com envio, de links que trocam de página, de `window.open` ou de
   `location = ...`**. A ficha é uma página só; para partes diferentes, use abas (`data-tabs`).
   Para abrir um site: `RPGDesk.ui.openLink(url)`.
6. **Imagens**: desenhos pequenos em SVG dentro do HTML, ou `data:` URLs. A foto do personagem vem
   do RPG Desk com `<img data-image="retrato">` (veja abaixo). Não use imagens da internet.
7. Não use `<iframe>`.

## 3. Os dados da ficha

Cada ficha tem um **documento** com campos. Um campo tem um **caminho**:

- `nome`, `nivel`, `atributos.forca`, `pericias.furtividade.bonus`
- Nomes: **letras sem acento, números e `_`**, começando por letra. `forca`, não `força`.
- Maiúsculas não contam: `Forca` e `forca` são o mesmo campo.
- Valores: texto, número ou verdadeiro/falso.
- **Listas** (inventário, magias, ataques): cada item tem um `id` próprio e os seus campos.

Quem pode escrever: na ficha, **o dono e os mestres**; os outros só leem. Na tabela da sala,
**só os mestres** escrevem. O rpgdesk.js trava sozinho os campos de quem não pode escrever.

## 4. Atributos no HTML (fichas sem JavaScript)

O rpgdesk.js liga sozinho estes atributos, inclusive em elementos criados depois:

| Atributo | Onde | O que faz |
| --- | --- | --- |
| `data-field="caminho"` | `<input>`, `<textarea>`, `<select>` | liga o campo ao documento. `type="number"` grava número; `type="checkbox"`, verdadeiro/falso; `type="radio"`, o `value` marcado |
| `data-text="caminho"` | qualquer elemento | mostra o valor do campo como texto |
| `data-image="caminho"` | `<img>` | mostra a imagem escolhida; clicar abre a escolha de imagem do RPG Desk |
| `data-list="caminho"` + `<template>` | `<ul>`, `<div>`… | repete o `<template>` para cada item; dentro dele, os caminhos são do item |
| `data-list-add="caminho"` | `<button>` | adiciona um item vazio na lista |
| `data-list-remove` | `<button>` dentro do `<template>` | remove aquele item |
| `data-roll="1d20 + {caminho}"` | `<button>` | rola os dados no RPG Desk (aparece no chat para todos); `{caminho}` vira o número do campo |
| `data-label="Teste de {nome}"` | junto do `data-roll` | o nome da rolagem; `{caminho}` vira o texto do campo |
| `data-tabs` + `data-tab="x"` + `data-tab-panel="x"` | botões e seções | abas: só a seção da aba escolhida aparece |
| `data-role="gm"` | qualquer elemento | só mestres veem (é só aparência, não é segredo) |
| `data-readonly` | campo | nunca edita (mostra o valor) |

Rolagens: `1d20+5`, `2d6 + 3`, `3d6`, `1d100`, `4dF` (Fudge), `5d10 > 7` (conta sucessos acima
de 7). Em `data-roll`, use `{caminho}` só com campos numéricos.

Exemplo de lista:

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

Exemplo de abas:

```html
<nav data-tabs>
  <button data-tab="atributos">Atributos</button>
  <button data-tab="inventario">Inventário</button>
</nav>
<section data-tab-panel="atributos">…</section>
<section data-tab-panel="inventario">…</section>
```

## 5. A API em JavaScript

Use quando os atributos não bastam (cálculos, botões especiais).

```js
RPGDesk.connect().then(({ doc, me, kind, item, room, language }) => {
  // doc: o documento da ficha
  // me: { id, nick, role: 'gm' | 'player' | 'spectator', isOwner }
});
```

**Documento (`doc`)**

- `doc.get('atributos.forca')`: o valor (ou `undefined`). `doc.get('atributos')` devolve um objeto.
- `await doc.set('atributos.forca', 14)`: grava. `doc.set('pv', { atual: 10, max: 12 })` grava campo
  a campo. `doc.set('campo', null)` apaga.
- `await doc.update({ 'pv.atual': 3, 'pv.max': 10 })`: vários campos de uma vez.
- `doc.watch('pv', (valor, { local }) => { ... })`: avisa a cada mudança (de qualquer pessoa).
  `{ now: true }` como terceiro argumento chama já com o valor atual. Devolve a função que para.
- `doc.compute(alvo, [fontes], função)`: **campo calculado**. Exemplo:
  `doc.compute('mod_forca', ['atributos.forca'], (f) => Math.floor(((Number(f) || 10) - 10) / 2))`.
  Use sempre `compute` para valores derivados; **não** grave dentro de um `watch` (cada pessoa com a
  ficha aberta gravaria de novo).
- `doc.canWrite('caminho')`: se a pessoa pode escrever ali.
- Listas: `const inv = doc.list('inventario')`
  - `inv.items()`: `[{ id, nome, qtd }, ...]`
  - `const id = inv.add({ nome: 'Corda', qtd: 1 })`
  - `inv.item(id).set('qtd', 2)`, `inv.remove(id)`, `inv.move(id, 0)`
  - `inv.watch((itens) => { ... })`
- `doc.on('rejected', (mensagem) => ...)`: uma escrita foi recusada (sem permissão).

**Dados, chat e mesa**

- `const r = await RPGDesk.dice.roll('1d20+5', { label: 'Ataque' })` → `r.total`, `r.items`.
  A rolagem aparece no chat para todos, com os dados 3D.
- `RPGDesk.chat.send('ataca o orc', { kind: 'action' })`. `kind`: `'chat'`, `'action'`, `'narrator'`,
  `'npc'` (com `name`).
- `RPGDesk.chat.local('só eu vejo isto')`.
- `await RPGDesk.room.players()` → `[{ id, nick, role, avatar }]`.
- `RPGDesk.card.setBar(0, pvAtual, pvMax)`: a barra 0 do cartão do jogador dono da ficha (vida, por
  exemplo). `RPGDesk.card.setLine(0, 'Nível 3')`. Barras e linhas vão de 0 a 3.

**Interface**

- `RPGDesk.ui.toast('Salvo!')`, `await RPGDesk.ui.confirm('Apagar tudo?')` (true/false),
  `await RPGDesk.ui.prompt('Nome do item?', 'Corda')` (texto ou null).
- `await RPGDesk.ui.menu([{ id: 'a', label: 'Atacar' }, { separator: true }, { id: 'x', label: 'Apagar', danger: true }], evento)`.
- `RPGDesk.ui.openLink('https://...')`, `RPGDesk.ui.copy(texto)`, `RPGDesk.ui.download('ficha.json', texto)`.
- `const img = await RPGDesk.files.pickImage()` → `{ id }`; guarde o `id` num campo e mostre com
  `<img data-image="campo">` (ou `img.src = await RPGDesk.files.url(id)`).
- `RPGDesk.storage.get(chave)` / `RPGDesk.storage.set(chave, valor)`: preferências da pessoa.
- `RPGDesk.view.get(chave)` / `RPGDesk.view.set(chave, valor)`: estado da tela (aba aberta, filtro).

**Integrações (opcional)**: se a ficha precisa mandar algo para fora (Discord, planilha, luzes),
declare no manifesto `"webhooks": [{ "name": "rolagens", "description": "Cada rolagem" }]` e chame
`await RPGDesk.webhooks.send('rolagens', { total: 17 })`. O mestre cadastra o endereço (Zapier, Make ou
IFTTT) no RPG Desk. Não ponha endereços no código.

**Partes só do mestre**: no manifesto da ficha,
`"sheets": [{ "type": "...", "title": "...", "private": { "notas_do_mestre": "gm" } }]`. O campo
`notas_do_mestre` (e o que estiver dentro dele) só chega aos mestres. Com `"owner"`, só o dono da
ficha e os mestres.

## 6. Visual

- O `rpgdesk.css` já estiliza campos, botões, abas (`data-tabs`) e listas no tema escuro do RPG Desk.
- Classes prontas: `rd-row` (linha que quebra), `rd-col` (coluna), `rd-grid` (grade de cartões),
  `rd-card` (caixa), `rd-grow` (ocupa o espaço que sobra), `rd-primary` (botão de destaque),
  `rd-muted` (texto discreto).
- Cores do tema em variáveis: `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)`.
- A ficha aparece num painel que pode ser estreito (uns 350 px) ou largo: use `flex-wrap` e grades
  que se ajustam. A rolagem é da própria página.

## 7. Testar

Abra o arquivo no navegador (dois cliques). Sem o RPG Desk em volta, o rpgdesk.js entra no **modo de
teste**: os dados ficam no navegador, as rolagens aparecem no "Registro", e a faixa no canto deixa
trocar o papel (mestre, dono da ficha, outro jogador) para ver o que cada um pode fazer.

## 8. Antes de entregar, confira

- [ ] O manifesto é JSON válido, com `rpgdesk`, `id`, `name`, `version`, `sdk` e `sheets` (ou `tables`).
- [ ] O `type` começa com o `id` e um ponto.
- [ ] As linhas do `rpgdesk.js` e do `rpgdesk.css` estão no `<head>`, exatamente como no modelo.
- [ ] Nenhum endereço `http` de fora (scripts, fontes, imagens, `fetch`).
- [ ] Nenhum `alert`, `confirm`, `prompt`, `localStorage`, `<form>` com envio ou `location =`.
- [ ] Todo `data-field`, `data-text`, `data-list` usa nomes sem acento e sem espaço.
- [ ] Valores calculados usam `doc.compute`.
- [ ] Funciona estreito (350 px) e largo.

## 9. Exemplo completo

```html
<!doctype html>
<html lang="pt-BR">
<head>
  <meta charset="utf-8">
  <script type="application/rpgdesk+json" id="rpgdesk-manifest">
  {
    "rpgdesk": 1,
    "id": "br.rpgdesk.exemplo.d20",
    "name": "D20 Genérico",
    "version": "1.0.0",
    "author": "RPG Desk",
    "sdk": 1,
    "sheets": [{ "type": "br.rpgdesk.exemplo.d20.ficha", "title": "Personagem", "private": { "mestre": "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>
    .topo { display: flex; gap: 12px; align-items: flex-start; flex-wrap: wrap; }
    .retrato { width: 96px; height: 96px; }
    .atributo { text-align: center; }
    .atributo b { display: block; font-size: 20px; }
  </style>
</head>
<body>
  <div class="topo">
    <img class="retrato" data-image="retrato" alt="Retrato">
    <div class="rd-col rd-grow">
      <label>Nome <input data-field="nome"></label>
      <div class="rd-row">
        <label>Classe <input data-field="classe"></label>
        <label>Nível <input type="number" data-field="nivel" min="1"></label>
        <label>PV <input type="number" data-field="pv.atual"></label>
        <label>PV máx. <input type="number" data-field="pv.max"></label>
      </div>
    </div>
  </div>

  <nav data-tabs>
    <button data-tab="atributos">Atributos</button>
    <button data-tab="inventario">Inventário</button>
    <button data-tab="mestre" data-role="gm">Mestre</button>
  </nav>

  <section data-tab-panel="atributos" class="rd-grid">
    <div class="rd-card atributo">
      <label>Força <input type="number" data-field="forca"></label>
      <b data-text="mod_forca"></b>
      <button data-roll="1d20 + {mod_forca}" data-label="Força de {nome}">Rolar</button>
    </div>
    <div class="rd-card atributo">
      <label>Destreza <input type="number" data-field="destreza"></label>
      <b data-text="mod_destreza"></b>
      <button data-roll="1d20 + {mod_destreza}" data-label="Destreza de {nome}">Rolar</button>
    </div>
  </section>

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

  <section data-tab-panel="mestre">
    <textarea data-field="mestre.notas" placeholder="Só os mestres veem isto"></textarea>
  </section>

  <script>
    const mod = (v) => Math.floor(((Number(v) || 10) - 10) / 2);
    RPGDesk.connect().then(({ doc }) => {
      doc.compute('mod_forca', ['forca'], mod);
      doc.compute('mod_destreza', ['destreza'], mod);
      // a vida vai para a barra do cartão do jogador
      doc.watch('pv', (pv) => {
        if (pv && typeof pv === 'object') RPGDesk.card.setBar(0, Number(pv.atual) || 0, Number(pv.max) || 0);
      }, { now: true });
    });
  </script>
</body>
</html>
```
