267 lines
8.0 KiB
JavaScript
267 lines
8.0 KiB
JavaScript
/**
|
||
* Donjon & Cie - Systeme FoundryVTT
|
||
*
|
||
* Donjon & Cie est un jeu de role edite par John Doe.
|
||
* Ce systeme FoundryVTT est une implementation independante et n'est pas
|
||
* affilie a John Doe.
|
||
*
|
||
* @author LeRatierBretonnien
|
||
* @copyright 2025–2026 LeRatierBretonnien
|
||
* @license CC BY-NC-SA 4.0 – https://creativecommons.org/licenses/by-nc-sa/4.0/
|
||
*/
|
||
|
||
import { DonjonEtCieUtility } from "./donjon-et-cie-utility.mjs";
|
||
import { DonjonEtCieRollDialog } from "./applications/donjon-et-cie-roll-dialog.mjs";
|
||
|
||
export class DonjonEtCieActor extends Actor {
|
||
static defaultPnjAttack = {
|
||
nom: "Attaque",
|
||
degats: "1d6",
|
||
notes: ""
|
||
};
|
||
|
||
prepareDerivedData() {
|
||
super.prepareDerivedData();
|
||
|
||
const pv = this.system.sante?.pv;
|
||
if (pv && pv.value > pv.max) {
|
||
pv.max = pv.value;
|
||
}
|
||
}
|
||
|
||
getCharacteristicEntries() {
|
||
return DonjonEtCieUtility.getCharacteristicEntries(this.system);
|
||
}
|
||
|
||
getSectionData() {
|
||
return DonjonEtCieUtility.buildActorSections(this);
|
||
}
|
||
|
||
getFavorEntries() {
|
||
return DonjonEtCieUtility.getFavorEntries(this.system);
|
||
}
|
||
|
||
#getStoredArmorContext() {
|
||
if (this.type === "pnj") {
|
||
const stored = Number(this.system.defense?.armure?.resultatProtection ?? 0);
|
||
return {
|
||
label: "ARM",
|
||
hasArmor: true,
|
||
before: stored,
|
||
update: async (value) => this.update({ "system.defense.armure.resultatProtection": Math.max(0, Number(value ?? 0)) })
|
||
};
|
||
}
|
||
|
||
const armors = [...this.items.filter((item) => item.type === "armure")].sort((a, b) => {
|
||
const equippedScore = Number(Boolean(b.system.equipee)) - Number(Boolean(a.system.equipee));
|
||
if (equippedScore) return equippedScore;
|
||
|
||
const protectionScore = Number(b.system.resultatProtection ?? 0) - Number(a.system.resultatProtection ?? 0);
|
||
if (protectionScore) return protectionScore;
|
||
|
||
return a.name.localeCompare(b.name, "fr", { sensitivity: "base" });
|
||
});
|
||
|
||
const armor = armors.find((item) => item.system.equipee || Number(item.system.resultatProtection ?? 0) > 0) ?? null;
|
||
if (!armor) {
|
||
return {
|
||
label: "Armure",
|
||
hasArmor: false,
|
||
before: 0,
|
||
update: null
|
||
};
|
||
}
|
||
|
||
return {
|
||
label: armor.name,
|
||
hasArmor: true,
|
||
before: Number(armor.system.resultatProtection ?? 0),
|
||
update: async (value) => armor.update({ "system.resultatProtection": Math.max(0, Number(value ?? 0)) })
|
||
};
|
||
}
|
||
|
||
async adjustNumericField(path, delta) {
|
||
const current = Number(foundry.utils.getProperty(this, path) ?? 0);
|
||
let next = current + Number(delta);
|
||
|
||
if (path === "system.sante.pv.value") {
|
||
const max = Number(this.system.sante?.pv?.max ?? next);
|
||
next = Math.max(0, Math.min(next, max));
|
||
} else {
|
||
next = Math.max(0, next);
|
||
}
|
||
|
||
return this.update({ [path]: next });
|
||
}
|
||
|
||
async applyIncomingDamage(damage, { useArmor = false } = {}) {
|
||
const incoming = Math.max(0, Number(damage ?? 0));
|
||
const pvBefore = Number(this.system.sante?.pv?.value ?? 0);
|
||
const pvMax = Number(this.system.sante?.pv?.max ?? pvBefore);
|
||
const armor = this.#getStoredArmorContext();
|
||
const armorBefore = useArmor ? Number(armor.before ?? 0) : 0;
|
||
const armorAbsorbed = Math.min(incoming, armorBefore);
|
||
const armorAfter = Math.max(armorBefore - armorAbsorbed, 0);
|
||
const hpDamage = Math.max(incoming - armorAbsorbed, 0);
|
||
const pvAfter = Math.max(pvBefore - hpDamage, 0);
|
||
|
||
if (useArmor && armor.hasArmor && armor.update && armorAfter !== armorBefore) {
|
||
await armor.update(armorAfter);
|
||
}
|
||
|
||
if (hpDamage !== 0) {
|
||
await this.update({ "system.sante.pv.value": pvAfter });
|
||
}
|
||
|
||
return {
|
||
incoming,
|
||
useArmor,
|
||
armorLabel: armor.label,
|
||
armorAvailable: armor.hasArmor,
|
||
armorBefore,
|
||
armorAbsorbed,
|
||
armorAfter,
|
||
hpDamage,
|
||
pvBefore,
|
||
pvAfter,
|
||
pvMax
|
||
};
|
||
}
|
||
|
||
async rollCharacteristic(key) {
|
||
return DonjonEtCieRollDialog.createCharacteristic(this, key);
|
||
}
|
||
|
||
async useFavorService(departmentKey) {
|
||
return game.system.donjonEtCie.rolls.useFavorService(this, departmentKey);
|
||
}
|
||
|
||
async rollInitiative() {
|
||
return DonjonEtCieRollDialog.createInitiative(this);
|
||
}
|
||
|
||
async rollHitDice() {
|
||
return game.system.donjonEtCie.rolls.rollHitDice(this);
|
||
}
|
||
|
||
async rollWeapon(itemId) {
|
||
const item = this.items.get(itemId);
|
||
if (item) return DonjonEtCieRollDialog.createWeapon(this, item);
|
||
}
|
||
|
||
async rollDamage(itemId) {
|
||
const item = this.items.get(itemId);
|
||
if (item) return DonjonEtCieRollDialog.createDamage(this, item);
|
||
}
|
||
|
||
async rollSpell(itemId) {
|
||
const item = this.items.get(itemId);
|
||
if (item) return DonjonEtCieRollDialog.createSpell(this, item);
|
||
}
|
||
|
||
async rollUsage(itemId) {
|
||
const item = this.items.get(itemId);
|
||
if (item) return DonjonEtCieRollDialog.createUsage(item);
|
||
}
|
||
|
||
async resetUsage(itemId) {
|
||
const item = this.items.get(itemId);
|
||
if (item?.type === "entrainement") return item.resetUsageDie();
|
||
}
|
||
|
||
getPnjAttacks() {
|
||
if (this.type !== "pnj") return [];
|
||
|
||
const attacks = Array.isArray(this.system.attaques) ? this.system.attaques : [];
|
||
if (attacks.length) return attacks.map((attack, index) => ({
|
||
index,
|
||
nom: attack.nom || `Attaque ${index + 1}`,
|
||
degats: attack.degats || "",
|
||
notes: attack.notes || ""
|
||
}));
|
||
|
||
const legacy = this.system.attaque;
|
||
if (legacy) {
|
||
return [{
|
||
index: 0,
|
||
nom: legacy.nom || "Attaque",
|
||
degats: legacy.degats || "",
|
||
notes: legacy.notes || ""
|
||
}];
|
||
}
|
||
|
||
return [{ index: 0, ...foundry.utils.deepClone(this.constructor.defaultPnjAttack) }];
|
||
}
|
||
|
||
async createPnjAttack() {
|
||
if (this.type !== "pnj") return null;
|
||
const attaques = this.getPnjAttacks().map(({ nom, degats, notes }) => ({ nom, degats, notes }));
|
||
attaques.push(foundry.utils.deepClone(this.constructor.defaultPnjAttack));
|
||
return this.update({ "system.attaques": attaques });
|
||
}
|
||
|
||
async deletePnjAttack(index) {
|
||
if (this.type !== "pnj") return null;
|
||
const attaques = this.getPnjAttacks().map(({ nom, degats, notes }) => ({ nom, degats, notes }));
|
||
attaques.splice(Number(index), 1);
|
||
if (!attaques.length) attaques.push(foundry.utils.deepClone(this.constructor.defaultPnjAttack));
|
||
return this.update({ "system.attaques": attaques });
|
||
}
|
||
|
||
#createPnjResourceProxy({ label, deltaPath, protectionPath = null }) {
|
||
const delta = Number(foundry.utils.getProperty(this, deltaPath) ?? 0);
|
||
const protection = protectionPath ? Number(foundry.utils.getProperty(this, protectionPath) ?? 0) : 0;
|
||
|
||
return {
|
||
actor: this,
|
||
type: protectionPath ? "armure" : "ressource",
|
||
name: `${this.name} · ${label}`,
|
||
system: {
|
||
delta,
|
||
resultatProtection: protection
|
||
},
|
||
update: async (data) => {
|
||
const updateData = {};
|
||
if (Object.hasOwn(data, "system.delta")) {
|
||
updateData[deltaPath] = data["system.delta"];
|
||
}
|
||
if (protectionPath && Object.hasOwn(data, "system.resultatProtection")) {
|
||
updateData[protectionPath] = data["system.resultatProtection"];
|
||
}
|
||
return Object.keys(updateData).length ? this.update(updateData) : this;
|
||
}
|
||
};
|
||
}
|
||
|
||
async rollPnjArmor() {
|
||
return DonjonEtCieRollDialog.createUsage(this.#createPnjResourceProxy({
|
||
label: "ARM",
|
||
deltaPath: "system.defense.armure.delta",
|
||
protectionPath: "system.defense.armure.resultatProtection"
|
||
}));
|
||
}
|
||
|
||
async rollPnjCourage() {
|
||
return DonjonEtCieRollDialog.createUsage(this.#createPnjResourceProxy({
|
||
label: "COU",
|
||
deltaPath: "system.defense.courage.delta"
|
||
}));
|
||
}
|
||
|
||
async rollPnjAttackDamage(index = 0) {
|
||
const attack = this.getPnjAttacks()[Number(index)] ?? null;
|
||
const attackName = attack?.nom || "Attaque";
|
||
const attackDamage = attack?.degats || "";
|
||
if (!attackDamage) return null;
|
||
|
||
return DonjonEtCieRollDialog.createDamage(this, {
|
||
name: `${this.name} · ${attackName}`,
|
||
type: "attaque",
|
||
system: {
|
||
degats: attackDamage,
|
||
portee: attack?.notes || ""
|
||
}
|
||
});
|
||
}
|
||
}
|