InícioDocs para devsCriar, instalar e atualizar seu primeiro módulo
Tutoriais de implementação 7 min de leitura

Criar, instalar e atualizar seu primeiro módulo

Da pasta vazia ao botão na mesa, com arquivos completos, instalação, ativação e uma segunda versão.

O exemplo de resumo lê um ator autorizado e abre o modal nativo. Gravewright 0.3.0, interface em inglês.
O exemplo de resumo lê um ator autorizado e abre o modal nativo. Gravewright 0.3.0, interface em inglês. · Abrir captura completa ↗

O que você vai construir

Um módulo de navegador chamado Example campaign summary. Ele acrescenta o botão Campaign summary à mesa, lê os atores que a conta pode abrir e exibe a contagem em um modal nativo. O resultado final é um ZIP instalável na interface. Você não precisa alterar o código do VTT, instalar um SDK npm ou configurar um servidor Python para esse módulo.

Você precisa de uma instalação de desenvolvimento do Gravewright 0.3.0, acesso de proprietário para instalar e uma conta GM em uma campanha de teste. Para escrever e empacotar, use um editor de texto e Python no terminal. Se a instalação que você usa tem dados importantes, prepare outra instalação para os exemplos. A conta do proprietário da instalação e o papel de GM da campanha são permissões diferentes.

1. Comece com uma pasta vazia

Crie campaign-summary na sua área de trabalho de desenvolvimento. Crie os quatro arquivos abaixo usando UTF-8. Confira extensões: main.mjs.txt não é main.mjs. Você também pode baixar o projeto pronto, extrair e comparar arquivo por arquivo.

text
campaign-summary/
  manifest.json
  main.mjs
  styles.css
  LICENSE.txt

Não coloque banco de dados, .env, credenciais nem arquivos da campanha nessa pasta. O ZIP precisa apenas do módulo. O código a seguir é o mesmo do download.

2. Declare o pacote em manifest.json

json
{
  "id": "example.campaign-summary",
  "name": "Example campaign summary",
  "version": "1.0.0",
  "sdk": {
    "requires": ">=1.0.0 <2.0.0",
    "tested": "1.0.0"
  },
  "description": "Adds an authorized read-only summary in a native modal.",
  "author": "Gravewright documentation examples",
  "license": "GPL-3.0-only",
  "entry": "main.mjs"
}

id identifica o pacote e continua igual entre suas versões. name é o nome mostrado para o dono e o GM. version identifica bytes imutáveis. entry é um caminho relativo dentro do ZIP. sdk descreve o contrato do módulo, não a versão do VTT. Não acrescente kind: journal ou type: compendium aqui: o que o pacote faz será definido pelo código e pelos blocos suportados do manifesto.

3. Escreva main.mjs

javascript
export default {
  start({styles}) { styles.use('styles.css'); },
  register({register}) {
    register('table.interface', ({root, api, signal, onDispose}) => {
      const doc=root.ownerDocument;
      const panel=doc.createElement('section');
      panel.className='example-panel';
      const button=doc.createElement('button');
      button.type='button';
      button.textContent='Campaign summary';
      const status=doc.createElement('p');
      status.setAttribute('role','status');
      panel.append(button,status);
      root.append(panel);
      let modal;
      button.addEventListener('click', async () => {
        button.disabled=true;
        try {
          const actors=await api.actors.list({}, {signal});
          if(signal.aborted) return;
          if(modal) await modal.close();
          if(signal.aborted) return;
          const content=doc.createElement('section');
          content.setAttribute('role','dialog');
          content.setAttribute('aria-modal','true');
          content.setAttribute('aria-label','Campaign summary');
          const text=doc.createElement('p');
          text.textContent='Readable actors: '+actors.filter(actor=>actor.canOpen).length;
          const close=doc.createElement('button');
          close.type='button';close.textContent='Close';
          content.append(text,close);
          modal=api.ui.openModal(content);
          close.addEventListener('click',()=>void modal.close(),{signal});
          status.textContent='Summary loaded.';
        } catch(error) {
          if(!signal.aborted) status.textContent=error.code || 'unavailable';
        } finally { if(!signal.aborted) button.disabled=false; }
      },{signal});
      onDispose(()=>{panel.remove();});
    });
  },
  stop() {}
};

start carrega estilos uma vez para a ativação. register informa ao host que há uma extensão em table.interface. A função registrada roda quando a mesa oferece essa superfície e recebe um root próprio, api limitado à montagem e signal de cancelamento. O botão só lê atores ao ser clicado. actors.list retorna a coleção autorizada; canOpen separa documentos legíveis de entradas que permitem apenas listagem.

api.ui.openModal usa a infraestrutura de modal do VTT. O exemplo não cria um elemento HTML dialog nem usa alert. signal impede que uma resposta tardia abra uma janela depois que o módulo foi desativado. onDispose remove o painel; listeners vinculados ao signal são cancelados junto da montagem.

4. Escreva styles.css e copie a licença

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; }

O seletor data-module-id mantém o posicionamento do botão específico para este pacote. O painel fica acima da mesa e abaixo das janelas nativas, sem bloquear os controles.

Baixe LICENSE.txt completo e salve-o com esse nome na pasta. Preserve o texto da licença ao reutilizar o exemplo. A licença do exemplo é GPL-3.0-only; para um módulo autoral diferente, escolha a licença aplicável e mantenha manifesto e arquivo consistentes. O nome do pacote pode mudar; não distribua apenas um rótulo de licença sem seu texto.

5. Gere e confira o ZIP

No PowerShell, partindo da pasta que contém campaign-summary:

powershell
Set-Location .\campaign-summary
python -m json.tool manifest.json
python -m zipfile -c ..\campaign-summary-1.0.0.zip manifest.json main.mjs styles.css LICENSE.txt
python -m zipfile -l ..\campaign-summary-1.0.0.zip

No Linux/macOS:

sh
cd campaign-summary
python3 -m json.tool manifest.json
python3 -m zipfile -c ../campaign-summary-1.0.0.zip manifest.json main.mjs styles.css LICENSE.txt
python3 -m zipfile -l ../campaign-summary-1.0.0.zip

json.tool precisa encerrar sem erro. Na listagem do ZIP, manifest.json aparece diretamente na raiz, ao lado de main.mjs. Se aparecer campaign-summary/manifest.json, você compactou a pasta em vez dos seus arquivos. Feche o ZIP antes de instalar.

6. Instale como proprietário

  1. Entre na área Inside e abra Módulos (Modules na interface em inglês).

  2. Clique em Instalar módulo / Install module, depois Instalar de ZIP / Install from ZIP.

  3. Escolha campaign-summary-1.0.0.zip no campo Arquivo ZIP / ZIP file e clique Instalar / Install.

  4. Espere o pacote Example campaign summary 1.0.0 aparecer na lista. Ainda falta ativá-lo na campanha.

O upload local é o caminho de desenvolvimento para código que o proprietário revisou. Publicar no Marketplace é outro fluxo, com catálogo e assinatura. Instalar o ZIP não ativa o módulo em todas as mesas.

7. Ative como GM e veja o resultado

  1. Abra uma campanha de teste como GM. No menu Gravewright, entre em Extensions e depois Table modules. Localize Example campaign summary 1.0.0 e clique Activate. Os botões Activate/Deactivate desta lista permanecem em inglês na release documentada.

  2. Volte à mesa. O botão Campaign summary aparece na parte inferior esquerda.

  3. Crie um ator pelo diretório nativo, conceda à sua conta o acesso apropriado e clique no botão. O modal mostra Readable actors: 1 se houver um único ator legível; o número depende da campanha.

  4. Feche por Close ou Escape. Reabra, depois desative o módulo. O botão deve desaparecer; os atores continuam na campanha.

Se a campanha não usa um sistema com tipos de ator, teste primeiro com a contagem zero ou escolha um sistema apropriado na campanha de desenvolvimento. Não invente um tipo de ator apenas para o módulo carregar.

8. Faça uma alteração e instale a versão 1.0.1

No main.mjs, troque o texto Campaign summary do botão por Campaign notebook. Em manifest.json, troque apenas version de 1.0.0 para 1.0.1. Mantenha id igual. Gere outro ZIP com os mesmos quatro arquivos e nome campaign-summary-1.0.1.zip; repita a instalação.

Na mesa, ative a versão 1.0.1, confira o novo texto e use o botão. O fato de a versão nova estar instalada não muda automaticamente a versão já selecionada na mesa. Não sobrescreva arquivos dentro do diretório de pacotes instalados e não tente reinstalar bytes diferentes como 1.0.0: o instalador pode rejeitar isso com conflict. Não há hot reload de pasta-fonte; o ciclo é editar, mudar versão, empacotar, instalar e ativar.

9. Descubra em qual etapa uma falha ocorreu

SintomaConfira
O ZIP é recusadoRaiz do arquivo, JSON válido, entry existente, licença e versão numérica
conflict ao instalarMesmo id/version já instalado com bytes diferentes; publique 1.0.1
Instalado, mas sem botãoConta GM, ativação exata na mesa e console de importação de main.mjs
Botão aparece, mas leitura falhaAcesso da conta, rede e error.code no texto do painel
Jogador vê contagem diferentePermissões dos atores; o módulo lê a coleção autorizada daquele usuário
Botão antigo continuaVersão ativada, abas abertas e recarregamento após mudar a seleção

Para investigar, abra as ferramentas de desenvolvimento do navegador e veja Console e Network enquanto reproduz a ação. Não cole cookies ou tokens da sessão em uma issue. Código JavaScript escrito na entrada precisa ser um módulo ES válido; remover export default impede o carregamento.

10. Escolha sua próxima implementação

Como usar a IA para adaptar este projeto

text
Use o ZIP campaign-summary do tutorial como ponto de partida no Gravewright 0.3.0. Quero substituir o resumo por [ação]. Explique se continuo em table.interface ou preciso de outra montagem. Entregue main.mjs, manifest.json e CSS completos. Preserve API do contexto, signal, onDispose e modal nativo. Gere uma nova versão do pacote e explique o teste depois da instalação/ativação, incluindo o que ver no console em caso de falha.