Ajout dialog employé

This commit is contained in:
2026-06-03 19:20:09 +02:00
parent 48660c9430
commit 6f3996d216
32 changed files with 1758 additions and 53 deletions
@@ -0,0 +1,363 @@
/**
* Donjon & Cie - Systeme FoundryVTT
*
* Fenêtre de dialogue pour afficher les employés (PJ)
* Structure : 2 onglets racine (Employés / Clients),
* avec sous-onglets par PC ou par client.
*
* @author LeRatierBretonnien
* @copyright 20252026 LeRatierBretonnien
* @license CC BY-NC-SA 4.0
*/
import { DonjonEtCieUtility } from "../donjon-et-cie-utility.mjs";
import { DONJON_ET_CIE } from "../donjon-et-cie-config.mjs";
export class DonjonEtCieEmployesDialog {
/**
* Ouvre la fenêtre des employés
*/
static async open() {
const pcs = this.#getPlayerCharacters();
const clientTokens = this.#getClients();
const characteristicKeys = this.#getCharacteristicKeys();
const pcsData = await Promise.all(
pcs.map(async (pc) => this.#preparePcData(pc, characteristicKeys))
);
const templateContext = {
pcs: pcsData,
clients: clientTokens.map(t => this.#prepareClientData(t)),
characteristicKeys
};
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-donjon-et-cie/templates/dialogs/employes-dialog.hbs",
templateContext
);
return foundry.applications.api.DialogV2.wait({
window: {
title: "Employés",
icon: "fa-solid fa-hard-hat"
},
classes: ["fvtt-donjon-et-cie", "dnc-employes-dialog-wrapper"],
content,
modal: false,
buttons: [
{
action: "close",
label: "Fermer",
icon: "fa-solid fa-xmark",
callback: () => true
}
],
rejectClose: false,
render: (event, dialog) => this.#setupTabs(dialog)
});
}
/**
* Récupère les personnages joueurs (employés)
*/
static #getPlayerCharacters() {
return game.actors.filter(a => a.type === "character" || a.hasPlayerOwner);
}
/**
* Récupère les PNJ clients de la scène courante (retourne les TokenDocuments)
*/
static #getClients() {
const scene = canvas?.scene ?? game.scenes?.current;
if (!scene) return [];
return scene.tokens.filter(token => {
const actor = token.actor;
return actor &&
!actor.hasPlayerOwner &&
actor.type === "pnj" &&
actor.system.categorie === "Client";
});
}
/**
* Récupère les clés des caractéristiques
*/
static #getCharacteristicKeys() {
return Object.entries(DONJON_ET_CIE.characteristics).map(([key, metadata]) => ({
key,
label: metadata.label,
short: metadata.short
}));
}
/**
* Prépare les données d'un PJ pour l'affichage
*/
static async #preparePcData(pc, characteristicKeys) {
const sys = pc.system || {};
const items = pc.items?.contents || [];
const pvValue = sys.sante?.pv?.value ?? 0;
const pvMax = sys.sante?.pv?.max ?? 0;
const dv = sys.sante?.dv ?? "1d6";
const meleeAttacks = sys.combat?.attaquesCorpsACorps ?? 1;
const rangedAttacks = sys.combat?.attaquesDistance ?? 1;
const magicResources = DonjonEtCieUtility.getMagicResourceContext(pc);
const weapons = [];
const armors = [];
const spells = [];
const capacities = [];
for (const item of items) {
const itemSys = item.system || {};
if (item.type === "arme") {
const categoryLabel = itemSys.categorie === "distance" ? "Distance" : "Corps à corps";
const handsLabel = (itemSys.mains ?? 1) > 1 ? "2 mains" : "1 main";
weapons.push({
name: item.name,
categoryLabel,
handsLabel,
damage: itemSys.degatsEstUsageDe
? `${itemSys.degats}(Δ)`
: (itemSys.degats || "—"),
range: itemSys.portee || "Contact",
ammunition: itemSys.munitionsDelta != null
? (itemSys.munitionsDelta === 0 ? "Épuisées" : `Δ${itemSys.munitionsDelta}`)
: ""
});
}
if (item.type === "armure") {
armors.push({
name: item.name,
protectionDie: `Δ${itemSys.delta || 0}`,
encumbrance: itemSys.encombrement || "—",
remainingProtection: itemSys.resultatProtection || "—"
});
}
if (item.type === "sortilege") {
spells.push({
name: item.name,
usageLabel: itemSys.delta > 0 ? DonjonEtCieUtility.formatUsageDie(itemSys.delta) : null
});
}
if (item.type === "capacite") {
capacities.push({
name: item.name,
usageLabel: itemSys.delta > 0 ? DonjonEtCieUtility.formatUsageDie(itemSys.delta) : null
});
}
}
// Spread des valeurs de caractéristiques directement sur l'objet pc
const characteristics = {};
characteristicKeys.forEach(({ key }) => {
characteristics[key] = sys.caracteristiques?.[key]?.value ?? 0;
});
return {
actorId: pc.id,
name: pc.name,
concept: pc.system.concept || "",
pvValue,
pvMax,
dv,
meleeAttacks,
rangedAttacks,
weapons,
armors,
spells,
capacities,
magicRank: magicResources.rank,
focusDisplay: magicResources.focusDisplay,
chaosDisplay: magicResources.chaosLabel,
...characteristics
};
}
/**
* Prépare les données d'un client (PNJ) depuis son TokenDocument
*/
static #prepareClientData(token) {
const client = token.actor;
const sys = client.system || {};
const items = client.items?.contents || [];
// Attaques système du modèle PNJ
const attaques = (sys.attaques || []).filter(a => a.nom || a.degats);
// Items du PNJ
const weapons = [];
const spells = [];
const capacities = [];
for (const item of items) {
const itemSys = item.system || {};
if (item.type === "arme") {
weapons.push({
name: item.name,
damage: itemSys.degatsEstUsageDe
? `${itemSys.degats}(Δ)`
: (itemSys.degats || "—"),
categoryLabel: itemSys.categorie === "distance" ? "Distance" : "Corps à corps"
});
}
if (item.type === "sortilege") {
spells.push({
name: item.name,
usageLabel: itemSys.delta > 0 ? DonjonEtCieUtility.formatUsageDie(itemSys.delta) : null
});
}
if (item.type === "capacite") {
capacities.push({
name: item.name,
usageLabel: itemSys.delta > 0 ? DonjonEtCieUtility.formatUsageDie(itemSys.delta) : null
});
}
}
return {
id: client.id,
tokenUuid: token.uuid,
name: client.name,
species: sys.espece || "",
category: sys.categorie || "",
role: sys.role || "",
summary: sys.resume || "",
pvValue: sys.sante?.pv?.value ?? 0,
pvMax: sys.sante?.pv?.max ?? 0,
dv: sys.sante?.dv || "1d8",
armureDelta: sys.defense?.armure?.delta ?? 0,
armureProtection: sys.defense?.armure?.resultatProtection ?? 0,
courageDelta: sys.defense?.courage?.delta ?? 0,
attaques,
weapons,
spells,
capacities,
pouvoirsSpeciaux: sys.pouvoirsSpeciaux || "",
hasMagie: spells.length > 0 || capacities.length > 0 || !!(sys.pouvoirsSpeciaux || "").trim()
};
}
/**
* Configuration des onglets à 2 niveaux
* Niveau 1 : Employés / Clients (root tabs)
* Niveau 2 : un onglet par PC ou par client (sub-tabs)
*/
static #setupTabs(dialog) {
const root = dialog.element;
// ---- Onglets racine ----
const rootTabs = root.querySelectorAll('.dnc-root-tab');
const rootPanels = root.querySelectorAll('.dnc-root-panel');
const activateRoot = (tabName) => {
rootTabs.forEach(btn => btn.classList.toggle('active', btn.dataset.rootTab === tabName));
rootPanels.forEach(panel => panel.classList.toggle('active', panel.dataset.rootPanel === tabName));
};
rootTabs.forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
activateRoot(btn.dataset.rootTab);
});
});
// Activer premier onglet racine par défaut
if (rootTabs.length > 0) activateRoot(rootTabs[0].dataset.rootTab);
// ---- Sous-onglets PC ----
const pcTabs = root.querySelectorAll('.dnc-pc-tab[data-pc-tab]');
const pcPanels = root.querySelectorAll('.dnc-pc-panel[data-pc-panel]');
const activatePc = (actorId) => {
pcTabs.forEach(btn => btn.classList.toggle('active', btn.dataset.pcTab === actorId));
pcPanels.forEach(panel => panel.classList.toggle('active', panel.dataset.pcPanel === actorId));
};
pcTabs.forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
activatePc(btn.dataset.pcTab);
});
});
if (pcTabs.length > 0) activatePc(pcTabs[0].dataset.pcTab);
// ---- Sous-onglets Clients ----
const clientTabs = root.querySelectorAll('.dnc-pc-tab[data-client-tab]');
const clientPanels = root.querySelectorAll('.dnc-client-panel[data-client-panel]');
const activateClient = (clientId) => {
clientTabs.forEach(btn => btn.classList.toggle('active', btn.dataset.clientTab === clientId));
clientPanels.forEach(panel => panel.classList.toggle('active', panel.dataset.clientPanel === clientId));
};
clientTabs.forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
activateClient(btn.dataset.clientTab);
});
});
if (clientTabs.length > 0) activateClient(clientTabs[0].dataset.clientTab);
// ---- Ouverture des fiches ----
root.addEventListener('click', (e) => {
const btn = e.target.closest('.dnc-open-sheet-btn');
if (!btn) return;
e.preventDefault();
e.stopPropagation();
const type = btn.dataset.openSheet;
if (type === "pc") {
const actor = game.actors.get(btn.dataset.actorId);
actor?.sheet.render(true);
} else if (type === "client") {
const tokenDoc = fromUuidSync(btn.dataset.tokenUuid);
const actor = tokenDoc?.actor ?? game.actors.get(btn.dataset.actorId);
actor?.sheet.render(true);
}
});
// ---- Jets de dés depuis la vue clients (même dialogs que la fiche PNJ) ----
root.addEventListener('click', async (e) => {
const btn = e.target.closest('.dnc-roll-btn, .dnc-carac-rollable');
if (!btn) return;
e.preventDefault();
e.stopPropagation();
// Résolution de l'actor : token UUID pour gérer les tokens non-liés
const tokenUuid = btn.dataset.tokenUuid;
const actorId = btn.dataset.actorId;
let actor = null;
if (tokenUuid) {
const tokenDoc = fromUuidSync(tokenUuid);
actor = tokenDoc?.actor ?? null;
}
if (!actor && actorId) {
actor = game.actors.get(actorId);
}
if (!actor) return;
const action = btn.dataset.pnjAction;
switch (action) {
case "rollArmure": return actor.rollPnjArmor();
case "rollCourage": return actor.rollPnjCourage();
case "rollAttaque": return actor.rollPnjAttackDamage(btn.dataset.attackIndex ?? 0);
}
});
}
}