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.

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
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
{
"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
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
.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
Revise e instale o ZIP-fonte por Sistemas → Instalar de ZIP em um host descartável; selecione Example Expedition em uma campanha nova.
Crie Character ou NPC, abra a ficha, altere Stamina e clique em Save. Feche e reabra.
Conceda ao jogador primeiro leitura e depois edição do ator. Compare controles e resposta do servidor.
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
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.