Introduction
Les Sheets sont les interfaces visuelles permettant aux joueurs d'interagir avec leurs personnages, objets et autres documents. Avec FoundryVTT v13, une nouvelle API appelee ApplicationV2 remplace l'ancienne API Application v1.
ApplicationV2 apporte des ameliorations majeures : rendu partiel (seules les parties modifiees sont rafraichies), suppression de la dependance jQuery, systeme d'actions declaratif, et meilleure separation des responsabilites.
Application v1 vs ApplicationV2
Voici les differences principales entre les deux API :
| Aspect | Application v1 (Deprecie) | ApplicationV2 (Moderne v13) |
|---|---|---|
| Namespace | Application, FormApplication |
foundry.applications.api.ApplicationV2 |
| Rendu | getData() + activateListeners() |
_prepareContext() + _onRender() |
| Templates | Un template unique | Systeme de PARTS (multiples templates) |
| Options | static get defaultOptions() |
static DEFAULT_OPTIONS |
| Actions | Event listeners jQuery manuels | Systeme declaratif data-action |
| jQuery | Dependance forte | Sans jQuery natif |
| Reactivite | Re-rendu complet | Re-rendu partiel par PART |
Exemple comparatif
Ancienne API (v1) - A eviter :
// Application v1 (ancienne API - DEPRECIE)
class OldSheet extends Application {
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
template: "path/to/template.hbs",
width: 500,
height: 400
});
}
getData() {
return { data: this.object };
}
activateListeners(html) {
super.activateListeners(html);
html.find(".my-button").click(this._onButtonClick.bind(this));
}
}
Nouvelle API (v2) - Recommandee :
// ApplicationV2 (nouvelle API v13)
class ModernSheet extends foundry.applications.api.ApplicationV2 {
static DEFAULT_OPTIONS = {
position: { width: 500, height: 400 },
actions: {
myAction: ModernSheet.#onMyAction
}
};
static PARTS = {
main: { template: "path/to/template.hbs" }
};
async _prepareContext(options) {
return { data: this.document };
}
static #onMyAction(event, target) {
// Gestionnaire d'action (this = instance de la sheet)
console.log("Action declenchee !");
}
}
Structure d'une Sheet moderne
Une Sheet moderne herite typiquement de DocumentSheetV2 ou de ses specialisations
comme ActorSheetV2 et ItemSheetV2.
Hierarchie des classes
// Hierarchie d'heritage pour les Sheets
foundry.applications.api.ApplicationV2
|
+-- HandlebarsApplicationMixin
| +-- ApplicationV2Mixin (systeme)
| +-- Application5e
|
+-- foundry.applications.api.DocumentSheetV2
+-- foundry.applications.sheets.ActorSheetV2
| +-- BaseActorSheet (systeme)
| +-- CharacterActorSheet
| +-- NPCActorSheet
|
+-- foundry.applications.sheets.ItemSheetV2
+-- ItemSheet5e (systeme)
Structure de base
// module/applications/actor/character-sheet.mjs
const { ActorSheetV2 } = foundry.applications.sheets;
const { HandlebarsApplicationMixin } = foundry.applications.api;
export default class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
/** Configuration statique */
static DEFAULT_OPTIONS = {
classes: ["mon-systeme", "actor", "character"],
position: { width: 600, height: 700 },
window: {
resizable: true,
title: "MON_SYSTEME.Sheet.Character"
},
actions: {
rollAttribute: CharacterSheet.#onRollAttribute
},
form: {
submitOnChange: true,
closeOnSubmit: false
}
};
/** Templates modulaires */
static PARTS = {
header: {
template: "systems/mon-systeme/templates/actor/header.hbs"
},
tabs: {
template: "systems/mon-systeme/templates/actor/tabs.hbs"
},
attributes: {
template: "systems/mon-systeme/templates/actor/attributes.hbs",
scrollable: [""]
},
biography: {
template: "systems/mon-systeme/templates/actor/biography.hbs",
scrollable: [""]
}
};
/** Preparation des donnees pour les templates */
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.isEditable = this.isEditable;
context.system = this.actor.system;
return context;
}
/** Gestionnaire d'action statique */
static async #onRollAttribute(event, target) {
const attr = target.dataset.attribute;
await this.actor.rollAttribute(attr);
}
}
DEFAULT_OPTIONS
DEFAULT_OPTIONS est un objet statique qui definit la configuration par defaut de l'application.
static DEFAULT_OPTIONS = {
// Identifiant unique (avec placeholder {id})
id: "my-app-{id}",
// Classes CSS appliquees a l'application
classes: ["mon-systeme", "my-app"],
// Tag HTML de l'application (defaut: "div")
tag: "div",
// Configuration de la fenetre
window: {
title: "Titre de la fenetre",
icon: "fas fa-user", // Icone FontAwesome
resizable: true, // Redimensionnable ?
minimizable: true, // Minimisable ?
controls: [ // Boutons dans l'en-tete
{
action: "configureToken",
icon: "fa-solid fa-user-circle",
label: "Token",
ownership: "OWNER"
}
]
},
// Position et dimensions
position: {
width: 600,
height: 400,
top: null, // null = centre automatique
left: null
},
// Actions declaratives
actions: {
save: MySheet.#onSave,
delete: {
handler: MySheet.#onDelete,
buttons: [0, 2] // Boutons souris acceptes (0=gauche, 2=droit)
}
},
// Configuration du formulaire
form: {
handler: MySheet.#onSubmit,
submitOnChange: true, // Soumet a chaque modification
closeOnSubmit: false // Ferme apres soumission ?
}
};
Les DEFAULT_OPTIONS sont automatiquement fusionnees avec celles de la classe parente.
Vous n'avez besoin de specifier que ce que vous voulez modifier ou ajouter.
Systeme de PARTS
Le systeme de PARTS permet de diviser une application en sections independantes, chacune avec son propre template. Cela permet un rendu partiel : seules les parties modifiees sont re-rendues.
static PARTS = {
// PART simple avec un template
header: {
template: "systems/mon-systeme/templates/actor/header.hbs"
},
// PART avec templates additionnels (partials)
inventory: {
template: "systems/mon-systeme/templates/actor/inventory.hbs",
templates: [
"systems/mon-systeme/templates/inventory/item-row.hbs",
"systems/mon-systeme/templates/inventory/encumbrance.hbs"
],
scrollable: [""] // La PART entiere est scrollable
},
// PART avec conteneur specifique
sidebar: {
container: {
id: "main",
classes: ["main-content"]
},
template: "systems/mon-systeme/templates/actor/sidebar.hbs"
},
// PART conditionnelle
spells: {
container: { classes: ["tab-body"], id: "tabs" },
template: "systems/mon-systeme/templates/actor/spells.hbs",
scrollable: [""]
}
};
Rendu partiel
Pour re-rendre uniquement certaines parties :
// Re-rendre uniquement l'inventaire
await this.render({ parts: ["inventory"] });
// Re-rendre plusieurs parties
await this.render({ parts: ["header", "attributes"] });
Configuration conditionnelle des PARTS
/** @inheritDoc */
_configureRenderParts(options) {
const parts = super._configureRenderParts(options);
// Retirer une PART conditionnellement
if (!this.actor.system.hasSpellcasting) {
delete parts.spells;
}
return parts;
}
_prepareContext()
La methode _prepareContext() remplace l'ancien getData().
Elle prepare les donnees qui seront passees aux templates.
/**
* Prepare le contexte global partage par tous les templates.
* @param {object} options - Options de rendu
* @returns {Promise
_preparePartContext()
Pour enrichir le contexte specifiquement pour chaque PART :
/**
* Prepare le contexte specifique a chaque PART.
* @param {string} partId - Identifiant de la PART
* @param {object} context - Contexte prepare par _prepareContext
* @param {object} options - Options de rendu
*/
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
switch (partId) {
case "header":
context.title = this.document.name;
context.img = this.document.img;
break;
case "attributes":
context.fields = this.document.system.schema.fields;
break;
case "inventory":
// Calcul du poids uniquement pour cette PART
context.totalWeight = this._calculateTotalWeight();
break;
}
return context;
}
Systeme d'actions
Le systeme d'actions remplace activateListeners() avec une approche declarative.
Declaration dans le template
<!-- Bouton avec action simple -->
<button type="button" data-action="rollAttribute" data-attribute="str">
Lancer Force
</button>
<!-- Lien avec action -->
<a data-action="editItem" data-item-id="{{item.id}}">
{{item.name}}
</a>
<!-- Element cliquable quelconque -->
<div class="hp-bar" data-action="adjustHp">
{{system.hp.value}} / {{system.hp.max}}
</div>
Declaration dans la classe
static DEFAULT_OPTIONS = {
actions: {
// Action simple
rollAttribute: CharacterSheet.#onRollAttribute,
// Action avec configuration
editItem: {
handler: CharacterSheet.#onEditItem,
buttons: [0] // Clic gauche uniquement
},
// Action avec plusieurs boutons
adjustHp: {
handler: CharacterSheet.#onAdjustHp,
buttons: [0, 2] // Clic gauche et droit
}
}
};
/**
* Gestionnaire d'action pour le jet de caracteristique.
* @this {CharacterSheet} - Instance de l'application
* @param {Event} event - L'evenement declencheur
* @param {HTMLElement} target - L'element avec data-action
*/
static async #onRollAttribute(event, target) {
event.preventDefault();
const attribute = target.dataset.attribute;
await this.actor.rollAttribute(attribute);
}
static async #onEditItem(event, target) {
const itemId = target.dataset.itemId;
const item = this.actor.items.get(itemId);
item?.sheet.render(true);
}
static #onAdjustHp(event, target) {
const delta = event.button === 0 ? 1 : -1; // Gauche = +1, Droit = -1
const currentHp = this.actor.system.hp.value;
this.actor.update({ "system.hp.value": currentHp + delta });
}
Dans les gestionnaires d'actions statiques, this est automatiquement lie a l'instance
de l'application. Vous pouvez donc utiliser this.actor, this.document, etc.
Enregistrement des Sheets
Les Sheets doivent etre enregistrees dans le hook init pour etre utilisables.
// Dans votre fichier principal (mon-systeme.mjs)
Hooks.once("init", function() {
console.log("Mon Systeme | Initialisation");
const DocumentSheetConfig = foundry.applications.apps.DocumentSheetConfig;
// Desenregistrer les sheets par defaut de Foundry (optionnel)
DocumentSheetConfig.unregisterSheet(
Actor,
"core",
foundry.appv1.sheets.ActorSheet
);
// Enregistrer la sheet pour les personnages
DocumentSheetConfig.registerSheet(Actor, "mon-systeme", CharacterSheet, {
types: ["character"], // Type(s) d'acteur concernes
makeDefault: true, // Sheet par defaut pour ce type
label: "MON_SYSTEME.SheetClass.Character" // Cle de traduction
});
// Enregistrer la sheet pour les PNJ
DocumentSheetConfig.registerSheet(Actor, "mon-systeme", NPCSheet, {
types: ["npc"],
makeDefault: true,
label: "MON_SYSTEME.SheetClass.NPC"
});
// Enregistrer les sheets d'items
DocumentSheetConfig.registerSheet(Item, "mon-systeme", ItemSheet, {
types: ["weapon", "armor", "gear"],
makeDefault: true,
label: "MON_SYSTEME.SheetClass.Item"
});
// Sheet alternative (non par defaut)
DocumentSheetConfig.registerSheet(Actor, "mon-systeme", CharacterSheetCompact, {
types: ["character"],
makeDefault: false,
label: "MON_SYSTEME.SheetClass.CharacterCompact"
});
});
Vous pouvez enregistrer plusieurs sheets pour un meme type de document. Les utilisateurs pourront choisir leur sheet preferee via le menu de configuration du document.
Exercice pratique : Creer une fiche de personnage simple
Creons ensemble une fiche de personnage minimaliste mais fonctionnelle.
1. Structure des fichiers
mon-systeme/
+-- module/
| +-- applications/
| +-- actor/
| +-- character-sheet.mjs
+-- templates/
+-- actor/
+-- character-sheet.hbs
+-- parts/
+-- header.hbs
+-- attributes.hbs
2. La classe CharacterSheet
// module/applications/actor/character-sheet.mjs
const { ActorSheetV2 } = foundry.applications.sheets;
const { HandlebarsApplicationMixin } = foundry.applications.api;
export default class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
static DEFAULT_OPTIONS = {
classes: ["mon-systeme", "actor", "character"],
position: { width: 500, height: 600 },
window: {
resizable: true
},
actions: {
rollAbility: CharacterSheet.#onRollAbility
},
form: {
submitOnChange: true,
closeOnSubmit: false
}
};
static PARTS = {
header: {
template: "systems/mon-systeme/templates/actor/parts/header.hbs"
},
attributes: {
template: "systems/mon-systeme/templates/actor/parts/attributes.hbs",
scrollable: [""]
}
};
/** @override */
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.actor = this.actor;
context.system = this.actor.system;
context.editable = this.isEditable;
// Preparer les caracteristiques
context.abilities = Object.entries(this.actor.system.abilities).map(([key, value]) => ({
key,
label: CONFIG.MON_SYSTEME.abilities[key]?.label ?? key.toUpperCase(),
value: value.value,
mod: Math.floor((value.value - 10) / 2)
}));
return context;
}
static async #onRollAbility(event, target) {
const ability = target.dataset.ability;
const mod = this.actor.system.abilities[ability].mod;
const roll = new Roll("1d20 + @mod", { mod });
await roll.evaluate();
await roll.toMessage({
speaker: ChatMessage.getSpeaker({ actor: this.actor }),
flavor: `Jet de ${CONFIG.MON_SYSTEME.abilities[ability]?.label ?? ability}`
});
}
}
3. Template header.hbs
<!-- templates/actor/parts/header.hbs -->
<header class="sheet-header">
<img class="profile-img" src="{{actor.img}}" alt="{{actor.name}}">
<div class="header-fields">
<input type="text" name="name" value="{{actor.name}}"
placeholder="Nom du personnage"
{{#unless editable}}disabled{{/unless}}>
<div class="level-display">
<label>Niveau</label>
<input type="number" name="system.level" value="{{system.level}}"
min="1" max="20" {{#unless editable}}disabled{{/unless}}>
</div>
</div>
</header>
4. Template attributes.hbs
<!-- templates/actor/parts/attributes.hbs -->
<section class="attributes-panel">
<h2>Caracteristiques</h2>
<div class="abilities-grid">
{{#each abilities}}
<div class="ability-block">
<label class="ability-label">{{this.label}}</label>
<input type="number"
name="system.abilities.{{this.key}}.value"
value="{{this.value}}"
min="1" max="30"
class="ability-value"
{{#unless ../editable}}disabled{{/unless}}>
<button type="button"
class="ability-mod"
data-action="rollAbility"
data-ability="{{this.key}}">
{{#if (gte this.mod 0)}}+{{/if}}{{this.mod}}
</button>
</div>
{{/each}}
</div>
<!-- Points de vie -->
<div class="hp-section">
<h3>Points de Vie</h3>
<div class="hp-inputs">
<input type="number" name="system.hp.value" value="{{system.hp.value}}"
min="0" max="{{system.hp.max}}" class="hp-current">
<span class="hp-separator">/</span>
<input type="number" name="system.hp.max" value="{{system.hp.max}}"
min="1" class="hp-max">
</div>
</div>
</section>
Vous avez cree une fiche de personnage fonctionnelle avec ApplicationV2.
Les valeurs se sauvegardent automatiquement grace a submitOnChange: true,
et les jets de des sont geres par le systeme d'actions.