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 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.
campaign-summary/
manifest.json
main.mjs
styles.css
LICENSE.txtNã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
{
"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
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
.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:
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.zipNo Linux/macOS:
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.zipjson.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
Entre na área Inside e abra Módulos (Modules na interface em inglês).
Clique em Instalar módulo / Install module, depois Instalar de ZIP / Install from ZIP.
Escolha campaign-summary-1.0.0.zip no campo Arquivo ZIP / ZIP file e clique Instalar / Install.
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
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.
Volte à mesa. O botão Campaign summary aparece na parte inferior esquerda.
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.
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
| Sintoma | Confira |
|---|---|
| O ZIP é recusado | Raiz do arquivo, JSON válido, entry existente, licença e versão numérica |
| conflict ao instalar | Mesmo id/version já instalado com bytes diferentes; publique 1.0.1 |
| Instalado, mas sem botão | Conta GM, ativação exata na mesa e console de importação de main.mjs |
| Botão aparece, mas leitura falha | Acesso da conta, rede e error.code no texto do painel |
| Jogador vê contagem diferente | Permissões dos atores; o módulo lê a coleção autorizada daquele usuário |
| Botão antigo continua | Versã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
Criar um módulo de diário: duas montagens, criação de documento e uma ação na janela aberta.
Atuar na ficha: alterar um dado genérico sem perder os campos existentes.
Criar um módulo de compêndio: criar conteúdo da campanha e capturar snapshots.
Distribuir compêndios no sistema: arquivos JSON e manifesto no mesmo ZIP.
Como usar a IA para adaptar este projeto
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.