364 lines
11 KiB
JavaScript
364 lines
11 KiB
JavaScript
/**
|
||
* 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);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|