/** * 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 2025–2026 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); } }); } }