InícioDocs para devsPacote de sistema: ficha de personagem editável
Exemplos por kind 4 min de leitura

Pacote de sistema: ficha de personagem editável

Um sistema original em pacote único, com tipos de ator, ficha mínima completa e salvamento por revisão.

A ficha original Example Expedition salvou Stamina 17 pela API do contexto. Gravewright 0.3.0, interface em inglês.
A ficha original Example Expedition salvou Stamina 17 pela API do contexto. Gravewright 0.3.0, interface em inglês. · Abrir captura completa ↗

Objetivo e pré-requisitos

Crie uma campanha com Example Expedition. O pacote declara atores character e npc, um tipo vazio de item gear e substitui a ficha por um campo de vigor editável. Não inclui regras publicadas nem catálogos.

Host alvo: 0.3.0, SDK 1.0.0. Textos da interface do exemplo estão em inglês. Preserve a versão solicitada ao adaptar.

Arquivos completos

Baixar pacote-fonte ZIP

Estes são fontes completos, sem assinatura. Depois de revisar o código, teste por Instalar de ZIP em um host descartável. Para publicar pelo Marketplace, prepare a assinatura e o catálogo da referência de assinatura e ativação. Nenhuma chave privada acompanha o download.

Manifesto completo

json
{
  "id": "example.expedition",
  "name": "Example Expedition",
  "version": "1.0.0",
  "sdk": {
    "requires": ">=1.0.0 <2.0.0",
    "tested": "1.0.0"
  },
  "description": "Original character/NPC sheets with a saved stamina field and an empty gear type.",
  "author": "Gravewright documentation examples",
  "license": "GPL-3.0-only",
  "entry": "main.mjs",
  "system": {
    "actorTypes": [
      {
        "id": "character",
        "label": "Character"
      },
      {
        "id": "npc",
        "label": "NPC"
      }
    ],
    "itemTypes": [
      {
        "id": "gear",
        "label": "Gear"
      }
    ]
  }
}

Código do exemplo

main.mjs

javascript
export default {
  start({styles}) { styles.use('styles.css'); },
  register({register}) {
    const sheet={
      mode:'replace',types:['character','npc'],
      async mount({root,context,api,signal,onDispose}) {
        const doc=root.ownerDocument;
        const form=doc.createElement('form');form.className='example-panel';
        form.setAttribute('aria-label','Example Expedition sheet');
        const title=doc.createElement('h2');
        const label=doc.createElement('label');label.textContent='Stamina (0–100)';
        const input=doc.createElement('input');input.type='number';input.min='0';input.max='100';
        label.append(input);
        const saveButton=doc.createElement('button');saveButton.type='submit';saveButton.textContent='Save';
        const reloadButton=doc.createElement('button');reloadButton.type='button';reloadButton.textContent='Reload server value';
        input.disabled=true;saveButton.disabled=true;reloadButton.disabled=true;
        const status=doc.createElement('p');status.setAttribute('role','status');
        form.append(title,label,saveButton,reloadButton,status);root.append(form);
        let current,dirty=false,pending;
        const token=context.tokenId ? {tokenId:context.tokenId} : {};
        async function load() {
          input.disabled=true;saveButton.disabled=true;reloadButton.disabled=true;
          try {
            const sheet=await api.actors.sheet(context.actorId,token,{signal});
            if(signal.aborted) return;
            current=sheet;title.textContent=sheet.name;
            input.value=String(sheet.data.stamina ?? 10);
            input.disabled=!sheet.canEdit;saveButton.disabled=!sheet.canEdit;
            dirty=false;
          } finally { if(!signal.aborted) reloadButton.disabled=false; }
        }
        function save() {
          if(pending) return pending;
          if(!dirty || !current?.canEdit) return Promise.resolve();
          const stamina=Number(input.value);
          if(input.value.trim()==='' || !Number.isInteger(stamina) || stamina<0 || stamina>100) {
            status.textContent='Enter an integer from 0 to 100.';
            return Promise.reject(new Error('Invalid stamina'));
          }
          const payload={actorId:context.actorId,name:current.name,version:current.version,
            sheetVersion:current.sheetVersion,data:{...current.data,stamina},
            ...(current.tokenId ? {tokenId:current.tokenId} : {})};
          const requestId=crypto.randomUUID();
          input.disabled=true;saveButton.disabled=true;reloadButton.disabled=true;
          pending=(async()=>{
            try {
              await api.actors.saveSheet(payload,{requestId,signal});
              if(signal.aborted) return;
              await load();status.textContent='Saved.';
            } catch(error) {
              if(!signal.aborted) {
                if(error.code==='conflict') {
                  await load();
                  status.textContent='Conflict: server value loaded. Review it before editing again.';
                } else status.textContent=error.code || 'Save failed. Reload before retrying.';
              }
              throw error;
            } finally {
              pending=undefined;
              if(!signal.aborted){input.disabled=!current?.canEdit;saveButton.disabled=!current?.canEdit;reloadButton.disabled=false;}
            }
          })();
          return pending;
        }
        input.addEventListener('input',()=>{dirty=true;},{signal});
        form.addEventListener('submit',event=>{event.preventDefault();void save().catch(()=>{});},{signal});
        reloadButton.addEventListener('click',()=>{
          void load().then(()=>{if(!signal.aborted)status.textContent='Server value loaded.';})
            .catch(error=>{if(!signal.aborted)status.textContent=error.code||'unavailable';});
        },{signal});
        root.closest('.gw-window')?.addEventListener('gravewright:sheet-flush',event=>{
          if(pending || dirty) event.detail.waitUntil(save());
        },{signal});
        onDispose(()=>form.remove());
        await load();
      }
    };
    register('actor.sheet',sheet);
    register('token.sheet',sheet);
  },
  stop() {}
};

styles.css

css
.gw-module-root .example-panel { padding: var(--space-4, 16px); font-size: 14px; }
.gw-module-root .example-panel label { display: block; margin-bottom: var(--space-3, 12px); }
.gw-module-root .example-panel button { margin-right: var(--space-2, 8px); }
.gw-module-root[data-module-id="example.campaign-summary"] .example-panel { position: fixed; left: 16px; bottom: 112px; z-index: 20; max-width: min(360px, calc(100vw - 32px)); border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--surface); color: var(--text); pointer-events: auto; }

Como executar

  1. Revise e instale o ZIP-fonte por Sistemas → Instalar de ZIP em um host descartável; selecione Example Expedition em uma campanha nova.

  2. Crie Character ou NPC, abra a ficha, altere Stamina e clique em Save. Feche e reabra.

  3. Conceda ao jogador primeiro leitura e depois edição do ator. Compare controles e resposta do servidor.

  4. Abra o mesmo ator em duas sessões. Salve em uma e depois na desatualizada. Revise o valor recarregado antes de editar novamente.

Resultado esperado e ciclo de vida

O valor salvo permanece ao reabrir. O payload usa version e sheetVersion da mesma leitura. Tokens vinculados usam o ator; independentes usam o tokenId retornado. Salvamentos pendentes participam de gravewright:sheet-flush.

Erros e limites

Um conflito recarrega a ficha autoritativa e pede revisão. A interface de exemplo recusa vigor vazio, não inteiro ou fora dos limites; o JSON de sistemas externos continua sujeito a limites e permissões do host. Validação autoritativa de regras deve usar apenas contratos realmente disponíveis na release alvo.

Como validar

  • Crie/edite/salve/reabra os dois tipos de ator.

  • Confira que acesso de leitura não grava, inclusive por chamada direta.

  • Teste tokens vinculados e independentes separadamente.

  • Feche durante salvamento lento e edição inválida; falha no flush deve manter a janela aberta.

Como usar a IA neste exemplo

text
Amplie Example Expedition com [campos originais]. Preserve pacote único, tipos declarados e comportamento de revisão/sheet-flush. Preserve campos não relacionados dos dados. Não crie Server Rules separado nem exija host mais novo. Declare explicitamente requisitos de backend sem suporte.