import { RdDUtility } from "./rdd-utility.js";
import { TMRUtility } from "./tmr-utility.js";
import { RdDRollDialogEthylisme } from "./rdd-roll-ethylisme.js";
import { RdDRoll } from "./rdd-roll.js";
import { RdDTMRDialog } from "./rdd-tmr-dialog.js";
import { Misc } from "./misc.js";
import { RdDAstrologieJoueur } from "./rdd-astrologie-joueur.js";
import { RdDResolutionTable } from "./rdd-resolution-table.js";
import { RdDDice } from "./rdd-dice.js";
import { RdDRollTables } from "./rdd-rolltables.js";
import { ChatUtility } from "./chat-utility.js";
import { RdDItemSort } from "./item-sort.js";
import { Grammar } from "./grammar.js";
import { RdDEncaisser } from "./rdd-roll-encaisser.js";
import { RdDCombat } from "./rdd-combat.js";
import { RdDAudio } from "./rdd-audio.js";
import { RdDItemCompetence } from "./item-competence.js";
import { RdDItemArme } from "./item-arme.js";
import { RdDAlchimie } from "./rdd-alchimie.js";
import { STATUSES, StatusEffects } from "./status-effects.js";
import { RdDItemCompetenceCreature } from "./item-competencecreature.js";
import { RdDItemSigneDraconique } from "./item-signedraconique.js";
import { ReglesOptionelles } from "./regles-optionelles.js";
import { TMRRencontres } from "./tmr-rencontres.js";
import { Poetique } from "./poetique.js";
import { EffetsDraconiques } from "./tmr/effets-draconiques.js";
import { Draconique } from "./tmr/draconique.js";
import { RdDCarac } from "./rdd-carac.js";
import { Monnaie } from "./item-monnaie.js";
import { DialogConsommer } from "./dialog-item-consommer.js";
import { DialogFabriquerPotion } from "./dialog-fabriquer-potion.js";
import { RollDataAjustements } from "./rolldata-ajustements.js";
import { DialogItemAchat } from "./dialog-item-achat.js";
import { RdDItem } from "./item.js";
import { RdDPossession } from "./rdd-possession.js";
import { ENTITE_BLURETTE, ENTITE_INCARNE, ENTITE_NONINCARNE, SYSTEM_RDD, SYSTEM_SOCKET_ID } from "./constants.js";
import { RdDConfirm } from "./rdd-confirm.js";
const POSSESSION_SANS_DRACONIC = {
img: 'systems/foundryvtt-reve-de-dragon/icons/entites/possession.webp',
name: 'Sans draconic',
system: {
niveau: 0,
defaut_carac: "reve",
}
};
const PAS_DE_BLESSURE = { "active": false, "psdone": false, "scdone": false, "premiers_soins": 0, "soins_complets": 0, "jours": 0, "loc": "" };
/* -------------------------------------------- */
/**
* Extend the base Actor entity by defining a custom roll data structure which is ideal for the Simple system.
* @extends {Actor}
*/
export class RdDActor extends Actor {
/* -------------------------------------------- */
static init() {
Hooks.on("preUpdateItem", (item, change, options, id) => RdDActor.getParentActor(item)?.onPreUpdateItem(item, change, options, id));
Hooks.on("createItem", (item, options, id) => RdDActor.getParentActor(item)?.onCreateItem(item, options, id));
Hooks.on("deleteItem", (item, options, id) => RdDActor.getParentActor(item)?.onDeleteItem(item, options, id));
Hooks.on("updateActor", (actor, change, options, actorId) => actor.onUpdateActor(change, options, actorId));
}
static onSocketMessage(sockmsg) {
switch (sockmsg.msg) {
case "msg_remote_actor_call":
return RdDActor.onRemoteActorCall(sockmsg.data);
case "msg_reset_nombre_astral":
console.log("RESET ASTRAL", game.user.character);
game.user.character.resetNombreAstral();
return;
}
}
static remoteActorCall(callData, canExecuteLocally = () => Misc.isUniqueConnectedGM()) {
if (canExecuteLocally()) {
RdDActor.onRemoteActorCall(callData);
return false;
}
else {
game.socket.emit(SYSTEM_SOCKET_ID, { msg: "msg_remote_actor_call", data: callData });
return true;
}
}
static onRemoteActorCall(callData) {
const actor = game.actors.get(callData?.actorId);
if (Misc.isOwnerPlayerOrUniqueConnectedGM(actor)) { // Seul le joueur choisi effectue l'appel: le joueur courant si propriétaire de l'actor, ou le MJ sinon
const args = callData.args;
console.info(`RdDActor.onRemoteActorCall: pour l'Actor ${callData.actorId}, appel de RdDActor.${callData.method}(`, ...args, ')');
actor[callData.method](...args);
}
}
/* -------------------------------------------- */
static getParentActor(document) {
return document?.parent instanceof Actor ? document.parent : undefined
}
/* -------------------------------------------- */
/**
* Override the create() function to provide additional RdD functionality.
*
* This overrided create() function adds initial items
* Namely: Basic skills, money,
*
* @param {Object} actorData Barebones actor template data which this function adds onto.
* @param {Object} options Additional options which customize the creation workflow.
*
*/
static async create(actorData, options) {
// Case of compendium global import
if (actorData instanceof Array) {
return super.create(actorData, options);
}
const isPersonnage = actorData.type == "personnage";
// If the created actor has items (only applicable to duplicated actors) bypass the new actor creation logic
if (actorData.items) {
let actor = await super.create(actorData, options);
if (isPersonnage) {
await actor.checkMonnaiePresence();
}
return actor;
}
if (isPersonnage) {
const competences = await RdDUtility.loadCompendium(RdDItemCompetence.actorCompendium(actorData.type));
actorData.items = competences.map(i => i.toObject());
actorData.items = actorData.items.concat(Monnaie.monnaiesData());
}
else {
actorData.items = [];
}
return super.create(actorData, options);
}
/* -------------------------------------------- */
prepareData() {
super.prepareData();
// Dynamic computing fields
this.encTotal = 0;
// Make separate methods for each Actor type (character, npc, etc.) to keep
// things organized.
if (this.type === 'personnage') this._prepareCharacterData(this)
if (this.type === 'creature') this._prepareCreatureData(this)
if (this.type === 'vehicule') this._prepareVehiculeData(this)
}
/* -------------------------------------------- */
setRollWindowsOpened(flag) {
this.rollWindowsOpened = flag;
}
/* -------------------------------------------- */
isRollWindowsOpened() {
return this.rollWindowsOpened;
}
/* -------------------------------------------- */
_prepareCreatureData(actorData) {
this.computeEncombrementTotalEtMalusArmure();
this.computeEtatGeneral();
}
/* -------------------------------------------- */
_prepareVehiculeData(actorData) {
this.computeEncombrementTotalEtMalusArmure();
}
/* -------------------------------------------- */
/**
* Prepare Character type specific data
*/
async _prepareCharacterData(actorData) {
// Initialize empty items
RdDCarac.computeCarac(actorData.system)
this.computeIsHautRevant();
await this.cleanupConteneurs();
await this.computeEncombrementTotalEtMalusArmure();
this.computePrixTotalEquipement();
this.computeEtatGeneral();
// Sanity check
await this.checkMonnaiePresence();
}
/* -------------------------------------------- */
async cleanupConteneurs() {
let updates = this.listItemsData('conteneur')
.filter(c => c.system.contenu.filter(id => this.getObjet(id) == undefined).length > 0)
.map(c => { return { _id: c._id, 'system.contenu': c.system.contenu.filter(id => this.getObjet(id) != undefined) } });
if (updates.length > 0) {
await this.updateEmbeddedDocuments("Item", updates)
}
}
/* -------------------------------------------- */
async checkMonnaiePresence() { // Ajout opportuniste si les pièces n'existent pas.
if (!this.items) return; // Sanity check during import
let manquantes = Monnaie.monnaiesManquantes(this);
if (manquantes.length > 0) {
await this.createEmbeddedDocuments('Item', manquantes, { renderSheet: false });
}
}
/* -------------------------------------------- */
isCreature() {
return this.type == 'creature' || this.type == 'entite';
}
/* -------------------------------------------- */
isPersonnage() {
return this.type == 'personnage';
}
/* -------------------------------------------- */
isHautRevant() {
return this.isPersonnage() && this.system.attributs.hautrevant.value != ""
}
/* -------------------------------------------- */
getFatigueActuelle() {
if (ReglesOptionelles.isUsing("appliquer-fatigue") && this.isPersonnage()) {
return this.system.sante.fatigue?.value;
}
return 0;
}
/* -------------------------------------------- */
getFatigueMax() {
if (!this.isPersonnage()) {
return 1;
}
return Misc.toInt(this.system.sante.fatigue?.max);
}
/* -------------------------------------------- */
getReveActuel() {
return Misc.toInt(this.system.reve?.reve?.value ?? this.carac.reve.value);
}
/* -------------------------------------------- */
getChanceActuel() {
return Misc.toInt(this.system.compteurs.chance?.value ?? 10);
}
/* -------------------------------------------- */
getTaille() {
return Misc.toInt(this.system.carac.taille?.value);
}
/* -------------------------------------------- */
getForce() {
if (this.isEntite()) {
return Misc.toInt(this.system.carac.reve?.value);
}
return Misc.toInt(this.system.carac.force?.value);
}
/* -------------------------------------------- */
getAgilite() {
switch (this.type) {
case 'personnage': return Misc.toInt(this.system.carac.agilite?.value);
case 'creature': return Misc.toInt(this.system.carac.force?.value);
case 'entite': return Misc.toInt(this.system.carac.reve?.value);
}
return 10;
}
/* -------------------------------------------- */
getChance() {
return Misc.toInt(this.system.carac.chance?.value ?? 10);
}
getMoralTotal() {
return Misc.toInt(this.system.compteurs.moral?.value);
}
/* -------------------------------------------- */
getBonusDegat() {
// TODO: gérer séparation et +dom créature/entité indépendament de la compétence
return Misc.toInt(this.system.attributs.plusdom.value);
}
/* -------------------------------------------- */
getProtectionNaturelle() {
return Misc.toInt(this.system.attributs.protection.value);
}
/* -------------------------------------------- */
getEtatGeneral(options = { ethylisme: false }) {
let etatGeneral = Misc.toInt(this.system.compteurs.etat?.value)
if (options.ethylisme) {
// Pour les jets d'Ethylisme, on ignore le degré d'éthylisme (p.162)
etatGeneral -= Math.min(0, this.system.compteurs.ethylisme.value)
}
return etatGeneral
}
/* -------------------------------------------- */
getActivePoisons() {
return duplicate(this.items.filter(item => item.type == 'poison' && item.system.active))
}
/* -------------------------------------------- */
getMalusArmure() {
return Misc.toInt(this.system.attributs?.malusarmure?.value)
}
/* -------------------------------------------- */
getEncTotal() {
return Math.floor(this.encTotal ?? 0);
}
/* -------------------------------------------- */
getCompetence(idOrName, options = {}) {
return RdDItemCompetence.findCompetence(this.items, idOrName, options)
}
getCompetences(name) {
return RdDItemCompetence.findCompetences(this.items, name)
}
/* -------------------------------------------- */
getObjet(id) {
return id ? this.items.find(it => it.id == id) : undefined;
}
listItemsData(type) {
return this.itemTypes[type];
}
filterItems(filter) {
return this.items.filter(filter);
}
getItemOfType(idOrName, type) {
return this.items.find(it => it.id == idOrName && it.type == type)
?? Misc.findFirstLike(idOrName, this.items, { filter: it => it.type == type, description: type });
}
getMonnaie(id) {
return this.getItemOfType(id, 'monnaie');
}
getTache(id) {
return this.getItemOfType(id, 'tache');
}
getMeditation(id) {
return this.getItemOfType(id, 'meditation');
}
getChant(id) {
return this.getItemOfType(id, 'chant');
}
getDanse(id) {
return this.getItemOfType(id, 'danse');
}
getMusique(id) {
return this.getItemOfType(id, 'musique');
}
getOeuvre(id, type = 'oeuvre') {
return this.getItemOfType(id, type);
}
getJeu(id) {
return this.getItemOfType(id, 'jeu');
}
getRecetteCuisine(id) {
return this.getItemOfType(id, 'recettecuisine');
}
/* -------------------------------------------- */
getDraconicList() {
return this.items.filter(it => it.type == 'competence' && it.system.categorie == 'draconic')
}
/* -------------------------------------------- */
getBestDraconic() {
const list = this.getDraconicList()
.sort(Misc.descending(it => it.system.niveau))
return duplicate(list[0])
}
getDraconicOuPossession() {
const possessions = this.items.filter(it => it.type == 'competencecreature' && it.system.ispossession)
.sort(Misc.descending(it => it.system.niveau));
if (possessions.length > 0) {
return duplicate(possessions[0]);
}
const draconics = [...this.getDraconicList().filter(it => it.system.niveau >= 0),
POSSESSION_SANS_DRACONIC]
.sort(Misc.descending(it => it.system.niveau));
return duplicate(draconics[0]);
}
getPossession(possessionId) {
return this.items.find(it => it.type == 'possession' && it.system.possessionid == possessionId);
}
getPossessions() {
return this.items.filter(it => it.type == 'possession');
}
getDemiReve() {
return this.system.reve.tmrpos.coord;
}
/* -------------------------------------------- */
async verifierPotionsEnchantees() {
let potionsEnchantees = this.filterItems(it => it.type == 'potion' && it.system.categorie.toLowerCase().includes('enchant'));
for (let potion of potionsEnchantees) {
if (!potion.system.prpermanent) {
console.log(potion);
let newPr = (potion.system.pr > 0) ? potion.system.pr - 1 : 0;
let update = { _id: potion._id, 'system.pr': newPr };
const updated = await this.updateEmbeddedDocuments('Item', [update]); // Updates one EmbeddedEntity
let messageData = {
pr: newPr,
alias: this.name,
potionName: potion.name,
potionImg: potion.img
}
ChatMessage.create({
whisper: ChatUtility.getWhisperRecipientsAndGMs(game.user.name),
content: await renderTemplate(`systems/foundryvtt-reve-de-dragon/templates/chat-potionenchantee-chateaudormant.html`, messageData)
});
}
}
}
/* -------------------------------------------- */
getSurprise(isCombat = undefined) {
let niveauSurprise = this.getEffects()
.map(effect => StatusEffects.valeurSurprise(effect, isCombat))
.reduce(Misc.sum(), 0);
if (niveauSurprise > 1) {
return 'totale';
}
if (niveauSurprise == 1) {
return 'demi';
}
return '';
}
/* -------------------------------------------- */
async grisReve(nGrisReve) {
let message = {
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: `${nGrisReve} jours de gris rêve sont passés. `
};
for (let i = 0; i < nGrisReve; i++) {
await this.dormir(6, { grisReve: true });
const blessures = duplicate(this.system.blessures);
await this._recupererBlessures(message, "legere", blessures.legeres.liste.filter(b => b.active), []);
await this._recupererBlessures(message, "grave", blessures.graves.liste.filter(b => b.active), blessures.legeres.liste);
await this._recupererBlessures(message, "critique", blessures.critiques.liste.filter(b => b.active), blessures.graves.liste);
await this.update({ "system.blessures": blessures });
await this._recupererVie(message);
const moralActuel = Misc.toInt(this.system.compteurs.moral.value);
if (moralActuel != 0) {
await this.moralIncDec(-Math.sign(moralActuel));
}
await this._recupereChance();
await this.transformerStress();
this.bonusRecuperationPotion = 0; // Reset potion
}
ChatMessage.create(message);
this.sheet.render(true);
}
/* -------------------------------------------- */
async dormirChateauDormant() {
let message = {
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: ""
};
const blessures = duplicate(this.system.blessures)
await this._recupererBlessures(message, "legere", blessures.legeres.liste.filter(b => b.active), []);
await this._recupererBlessures(message, "grave", blessures.graves.liste.filter(b => b.active), blessures.legeres.liste);
await this._recupererBlessures(message, "critique", blessures.critiques.liste.filter(b => b.active), blessures.graves.liste);
await this.update({ "system.blessures": blessures });
await this._recupererVie(message);
await this._jetDeMoralChateauDormant(message);
await this._recupereChance();
await this.transformerStress();
await this.retourSeuilDeReve(message);
this.bonusRecuperationPotion = 0; // Reset potion
await this.retourSust(message);
await this.verifierPotionsEnchantees();
if (message.content != "") {
message.content = `A la fin Chateau Dormant, ${message.content}
Un nouveau jour se lève`;
ChatMessage.create(message);
}
this.sheet.render(true);
}
/* -------------------------------------------- */
async _recupereChance() {
// On ne récupère un point de chance que si aucun appel à la chance dans la journée
if (this.getChanceActuel() < this.getChance() && !this.getFlag(SYSTEM_RDD, 'utilisationChance')) {
await this.chanceActuelleIncDec(1);
}
// Nouveau jour, suppression du flag
await this.unsetFlag(SYSTEM_RDD, 'utilisationChance');
}
async _jetDeMoralChateauDormant(message) {
const jetMoral = await this._jetDeMoral('neutre');
message.content += jetMoral.ajustement == 0 ? ' -- le moral reste stable' : ' -- le moral retourne vers 0';
}
/* -------------------------------------------- */
async _recupererBlessures(message, type, liste, moindres) {
if (!this.bonusRecuperationPotion) this.bonusRecuperationPotion = 0;
let count = 0;
const definitions = RdDUtility.getDefinitionsBlessures();
let definition = definitions.find(d => d.type == type);
for (let blessure of liste) {
if (blessure.jours >= definition.facteur) {
let rolled = await this._jetRecuperationConstitution(Misc.toInt(blessure.soins_complets) + this.bonusRecuperationPotion, message);
blessure.soins_complets = 0;
if (rolled.isSuccess && this._retrograderBlessure(type, blessure, moindres)) {
message.content += ` -- une blessure ${type} cicatrise`;
count++;
}
else if (rolled.isETotal) {
message.content += ` -- une blessure ${type} s'infecte (temps de guérison augmenté de ${definition.facteur} jours, perte de vie)`;
blessure.jours = 0;
await this.santeIncDec("vie", -1);
}
else {
blessure.jours++;
message.content += ` -- une blessure ${type} reste stable`;
}
}
else {
blessure.jours++;
}
}
}
/* -------------------------------------------- */
_retrograderBlessure(type, blessure, blessuresMoindres) {
if (type != "legere") {
let retrograde = blessuresMoindres.find(b => !b.active);
if (!retrograde) {
return false;
}
mergeObject(retrograde, { "active": true, "psdone": blessure.psdone, "scdone": blessure.scdone, "premiers_soins": 0, "soins_complets": 0, "jours": 0, "loc": blessure.loc });
}
this._supprimerBlessure(blessure);
return true;
}
/* -------------------------------------------- */
_supprimerBlessure(blessure) {
mergeObject(blessure, PAS_DE_BLESSURE);
}
/* -------------------------------------------- */
async _recupererVie(message) {
const tData = this.system
let blessures = [].concat(tData.blessures.legeres.liste).concat(tData.blessures.graves.liste).concat(tData.blessures.critiques.liste);
let nbBlessures = blessures.filter(b => b.active);
let vieManquante = tData.sante.vie.max - tData.sante.vie.value;
if (nbBlessures == 0 && vieManquante > 0) {
let bonusSoins = 0;
for (let b of blessures) {
bonusSoins = Math.max(bonusSoins, Misc.toInt(b.soins_complets));
}
let rolled = await this._jetRecuperationConstitution(bonusSoins, message)
if (rolled.isSuccess) {
const gain = Math.min(rolled.isPart ? 2 : 1, vieManquante);
message.content += " -- récupération de vie: " + gain;
await this.santeIncDec("vie", gain);
}
else if (rolled.isETotal) {
message.content += " -- perte de vie: 1";
await this.santeIncDec("vie", -1);
}
else {
message.content += " -- vie stationnaire ";
}
}
}
/* -------------------------------------------- */
async _jetRecuperationConstitution(bonusSoins, message = undefined) {
const tData = this.system;
let difficulte = Misc.toInt(bonusSoins) + Math.min(0, tData.sante.vie.value - tData.sante.vie.max);
let rolled = await RdDResolutionTable.roll(tData.carac.constitution.value, difficulte);
if (message) {
message.content += RdDResolutionTable.explain(rolled).replace(/Jet :/, "Constitution :");
}
return rolled;
}
/* -------------------------------------------- */
async remiseANeuf() {
if (this.isEntite([ENTITE_NONINCARNE])) {
return;
}
ChatMessage.create({
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: 'Remise à neuf de ' + this.name
});
const updates = {
'system.sante.endurance.value' : this.system.sante.endurance.max
};
if (!this.isEntite([ENTITE_INCARNE, ENTITE_BLURETTE])) {
if (this.system.blessures) {
updates['system.blessures.legeres.liste'] = [PAS_DE_BLESSURE, PAS_DE_BLESSURE, PAS_DE_BLESSURE, PAS_DE_BLESSURE, PAS_DE_BLESSURE];
updates['system.blessures.graves.liste'] = [PAS_DE_BLESSURE, PAS_DE_BLESSURE];
updates['system.blessures.critiques.liste'] = [PAS_DE_BLESSURE];
}
updates['system.sante.vie.value'] = this.system.sante.vie.max;
updates['system.sante.fatigue.value'] = 0;
if (this.isPersonnage()) {
updates['system.compteurs.ethylisme'] = { value:1, nb_doses: 0, jet_moral: false};
}
}
await this.update(updates);
await this.removeEffects(e => e.flags.core.statusId !== STATUSES.StatusDemiReve);
}
/* -------------------------------------------- */
async dormir(heures, options = { grisReve: false }) {
let message = {
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: ""
};
await this.recupereEndurance(message);
let sep = ""
let recuperationReve = "";
let i = 0;
for (; i < heures; i++) {
await this._recupererEthylisme(message);
await this.recupererFatigue(message);
if (!options.grisReve) {
let r = await this.recuperationReve(message);
if (r >= 0) {
recuperationReve += sep + r;
sep = "+";
}
if (r >= 0 && EffetsDraconiques.isDonDoubleReve(this)) {
r = await this.recuperationReve(message);
if (r >= 0) {
recuperationReve += sep + r;
}
}
if (r < 0) {
i++;// rêve de dragon pendant l'heure en cours
break;
}
}
}
if (!options.grisReve) {
message.content = `${this.name}: Vous dormez ${i == 0 ? 'une' : i} heure${i == 1 ? '' : 's'}. `
+ (recuperationReve == "" ? "" : `Vous récupérez ${recuperationReve} Points de rêve. `)
+ message.content;
ChatMessage.create(message);
}
this.sheet.render(true);
return i;
}
/* -------------------------------------------- */
async _recupererEthylisme(message) {
let ethylisme = duplicate(this.system.compteurs.ethylisme);
ethylisme.nb_doses = 0;
ethylisme.jet_moral = false;
if (ethylisme.value < 1) {
ethylisme.value = Math.min(ethylisme.value + 1, 1);
if (ethylisme.value <= 0) {
message.content += `Vous dégrisez un peu (${RdDUtility.getNomEthylisme(ethylisme.value)}). `;
}
}
await this.update({ "system.compteurs.ethylisme": ethylisme });
}
/* -------------------------------------------- */
async recupereEndurance(message) {
const manquant = this._computeEnduranceMax() - this.system.sante.endurance.value;
if (manquant > 0) {
await this.santeIncDec("endurance", manquant);
message.content += "Vous récuperez " + manquant + " points d'endurance. ";
}
}
/* -------------------------------------------- */
async recupererFatigue(message) {
if (ReglesOptionelles.isUsing("appliquer-fatigue")) {
let fatigue = this.system.sante.fatigue.value;
const fatigueMin = this._computeFatigueMin();
if (fatigue <= fatigueMin) {
return;
}
fatigue = Math.max(fatigueMin, this._calculRecuperationSegment(fatigue));
await this.update({ "system.sante.fatigue.value": fatigue });
if (fatigue == 0) {
message.content += "Vous êtes complêtement reposé. ";
}
}
}
/* -------------------------------------------- */
_calculRecuperationSegment(actuel) {
const segments = RdDUtility.getSegmentsFatigue(this.system.sante.endurance.max);
let cumul = 0;
let i;
for (i = 0; i < 11; i++) {
cumul += segments[i];
let diff = cumul - actuel;
if (diff >= 0) {
const limit2Segments = Math.floor(segments[i] / 2);
if (diff > limit2Segments && i > 0) {
cumul -= segments[i - 1]; // le segment est à moins de la moitié, il est récupéré
}
cumul -= segments[i];
break;
}
};
return cumul;
}
/* -------------------------------------------- */
async recuperationReve(message) {
const seuil = this.system.reve.seuil.value;
const reveActuel = this.getReveActuel();
if (reveActuel < seuil) {
let deRecuperation = await RdDDice.rollTotal("1dr");
console.log("recuperationReve", deRecuperation);
if (deRecuperation >= 7) {
// Rêve de Dragon !
message.content += `Vous faites un Rêve de Dragon de ${deRecuperation} Points de rêve qui vous réveille! `;
await this.combattreReveDeDragon(deRecuperation);
return -1;
}
else {
await this.reveActuelIncDec(deRecuperation);
return deRecuperation;
}
}
return 0;
}
/* -------------------------------------------- */
async retourSeuilDeReve(message) {
const seuil = this.system.reve.seuil.value;
const reveActuel = this.getReveActuel();
if (reveActuel > seuil) {
message.content += `
Votre rêve redescend vers son seuil naturel (${seuil}, nouveau rêve actuel ${(reveActuel - 1)})`;
await this.reveActuelIncDec(-1);
}
}
async retourSust(message) {
const tplData = this.system;
const sustNeeded = tplData.attributs.sust.value;
const sustConsomme = tplData.compteurs.sust.value;
const eauConsomme = tplData.compteurs.eau.value;
if (game.settings.get(SYSTEM_RDD, "appliquer-famine-soif").includes('famine') && sustConsomme < sustNeeded) {
const perte = sustConsomme < Math.min(0.5, sustNeeded) ? 3 : (sustConsomme <= (sustNeeded / 2) ? 2 : 1);
message.content += `
Vous ne vous êtes sustenté que de ${sustConsomme} pour un appétit de ${sustNeeded}, vous avez faim!
La famine devrait vous faire ${perte} points d'endurance non récupérables, notez le cumul de côté et ajustez l'endurance`;
}
if (game.settings.get(SYSTEM_RDD, "appliquer-famine-soif").includes('soif') && eauConsomme < sustNeeded) {
const perte = eauConsomme < Math.min(0.5, sustNeeded) ? 12 : (eauConsomme <= (sustNeeded / 2) ? 6 : 3);
message.content += `
Vous n'avez bu que ${eauConsomme} doses de liquide pour une soif de ${sustNeeded}, vous avez soif!
La soif devrait vous faire ${perte} points d'endurance non récupérables, notez le cumul de côté et ajustez l'endurance`;
}
await this.updateCompteurValue('sust', 0);
await this.updateCompteurValue('eau', 0);
}
/* -------------------------------------------- */
async combattreReveDeDragon(force) {
let rollData = {
actor: this,
competence: duplicate(this.getDraconicOuPossession()),
canClose: false,
rencontre: duplicate(TMRRencontres.getRencontre('rdd')),
tmr: true,
use: { libre: false, conditions: false },
forceCarac: { 'reve-actuel': { label: "Rêve Actuel", value: this.getReveActuel() } }
}
rollData.rencontre.force = force;
rollData.competence.system.defaut_carac = 'reve-actuel';
const dialog = await RdDRoll.create(this, rollData,
{
html: 'systems/foundryvtt-reve-de-dragon/templates/dialog-roll-reve-de-dragon.html',
options: { height: 400 }
},
{
name: 'maitrise',
label: 'Maîtriser le Rêve de Dragon',
callbacks: [
this.createCallbackExperience(),
{ action: async r => this.resultCombatReveDeDragon(r) }
]
}
);
dialog.render(true);
}
/* -------------------------------------------- */
async resultCombatReveDeDragon(rollData) {
rollData.queues = [];
if (rollData.rolled.isEchec) {
rollData.queues.push(await this.ajouterQueue());
}
if (rollData.rolled.isETotal) {
rollData.queues.push(await this.ajouterQueue());
}
if (rollData.rolled.isSuccess) {
await this.updatePointDeSeuil();
await this.reveActuelIncDec(rollData.rencontre.force);
}
if (rollData.rolled.isPart) {
// TODO: un dialogue pour demander le type de tête?
rollData.tete = true;
}
rollData.poesie = await Poetique.getExtrait();
ChatMessage.create({
whisper: ChatUtility.getWhisperRecipientsAndGMs(game.user.name),
content: await renderTemplate(`systems/foundryvtt-reve-de-dragon/templates/chat-resultat-reve-de-dragon.html`, rollData)
});
}
/* -------------------------------------------- */
async sortMisEnReserve(sort, draconic, coord, ptreve) {
await this.createEmbeddedDocuments("Item", [{
type: 'sortreserve',
name: sort.name,
img: sort.img,
system: { sortid: sort.id, draconic: (draconic ?? sort.system.draconic), ptreve: ptreve, coord: coord, heurecible: 'Vaisseau' } }],
{ renderSheet: false});
this.currentTMR.updateTokens();
}
/* -------------------------------------------- */
async updateCarac(caracName, caracValue) {
if (caracName == "force") {
if (Number(caracValue) > this.getTaille() + 4) {
ui.notifications.warn("Votre FORCE doit être au maximum de TAILLE+4");
return;
}
}
if (caracName == "reve") {
if (caracValue > Misc.toInt(this.system.reve.seuil.value)) {
this.setPointsDeSeuil(caracValue);
}
}
if (caracName == "chance") {
if (caracValue > Misc.toInt(this.system.compteurs.chance.value)) {
this.setPointsDeChance(caracValue);
}
}
await this.update({ [`system.carac.${caracName}.value`]: caracValue });
}
/* -------------------------------------------- */
async updateCaracXP(caracName, caracXP) {
if (caracName == 'Taille') {
return;
}
this.checkCaracXP(caracName);
}
/* -------------------------------------------- */
async updateCaracXPAuto(caracName) {
if (caracName == 'Taille') {
return;
}
let carac = RdDActor._findCaracByName(this.system.carac, caracName);
if (carac) {
carac = duplicate(carac);
let xp = Number(carac.xp);
let value = Number(carac.value);
while (xp >= RdDCarac.getCaracNextXp(value) && xp > 0) {
xp -= RdDCarac.getCaracNextXp(value);
value++;
}
carac.xp = xp;
carac.value = value;
await this.update({ [`system.carac.${caracName}`]: carac });
this.updateExperienceLog("Carac +", xp, caracName + " passée à " + value);
}
}
/* -------------------------------------------- */
async updateCompetenceXPAuto(idOrName) {
let competence = this.getCompetence(idOrName);
if (competence) {
let xp = Number(competence.system.xp);
let niveau = Number(competence.system.niveau);
while (xp >= RdDItemCompetence.getCompetenceNextXp(niveau) && xp > 0) {
xp -= RdDItemCompetence.getCompetenceNextXp(niveau);
niveau++;
}
await competence.update({
"system.xp": xp,
"system.niveau": niveau,
});
this.updateExperienceLog("Compétence +", xp, competence.name + " passée à " + niveau);
}
}
async updateCompetenceStress(idOrName) {
const competence = this.getCompetence(idOrName);
if (!competence) {
return;
}
const stress = this.system.compteurs.experience.value;
const niveau = Number(competence.system.niveau);
const xpSuivant = RdDItemCompetence.getCompetenceNextXp(niveau);
const xpRequis = xpSuivant - competence.system.xp;
if (stress <= 0 || niveau >= competence.system.niveau_archetype) {
ui.notifications.info(`La compétence ne peut pas augmenter!
stress disponible: ${stress}
expérience requise: ${xpRequis}
niveau : ${niveau}
archétype : ${competence.system.niveau_archetype}`);
return;
}
const xpUtilise = Math.max(0, Math.min(stress, xpRequis));
const gainNiveau = (xpUtilise >= xpRequis || xpRequis <=0) ? 1 : 0;
const nouveauNiveau = niveau + gainNiveau;
const nouveauXp = gainNiveau > 0 ? Math.max(competence.system.xp - xpSuivant, 0) : (competence.system.xp + xpUtilise);
await competence.update({
"system.xp": nouveauXp,
"system.niveau": nouveauNiveau,
});
const stressTransformeRestant = Math.max(0, stress - xpUtilise);
await this.update({ "system.compteurs.experience.value": stressTransformeRestant });
this.updateExperienceLog('Dépense stress', xpUtilise, `Stress en ${competence.name} ${gainNiveau ? "pour passer à " + nouveauNiveau : ""}`);
}
/* -------------------------------------------- */
async updateCreatureCompetence(idOrName, fieldName, compValue) {
let competence = this.getCompetence(idOrName);
if (competence) {
const update = { _id: competence.id }
if (fieldName == "niveau")
update['system.niveau'] = compValue;
else if (fieldName == "dommages")
update['system.dommages'] = compValue;
else
update['system.carac_value'] = compValue;
await this.updateEmbeddedDocuments('Item', [update]); // updates one EmbeddedEntity
}
}
/* -------------------------------------------- */
async updateCompetence(idOrName, compValue) {
let competence = this.getCompetence(idOrName);
if (competence) {
let nouveauNiveau = compValue ?? RdDItemCompetence.getNiveauBase(competence.system.categorie);
const tronc = RdDItemCompetence.getListTronc(competence.name).filter(it => {
const comp = this.getCompetence(it);
const niveauTr = competence ? competence.system.niveau : 0;
return niveauTr < 0 && niveauTr < nouveauNiveau;
});
if (tronc.length > 0) {
let message = "Vous avez modifié une compétence 'tronc'. Vérifiez que les compétences suivantes évoluent ensemble jusqu'au niveau 0 : ";
for (let troncName of tronc) {
message += "
" + troncName;
}
ChatMessage.create({
whisper: ChatMessage.getWhisperRecipients(game.user.name),
content: message
});
}
const update = { _id: competence.id, 'system.niveau': nouveauNiveau };
await this.updateEmbeddedDocuments('Item', [update]); // Updates one EmbeddedEntity
} else {
console.log("Competence not found", idOrName);
}
}
/* -------------------------------------------- */
async updateCompetenceXP(idOrName, newXp) {
let competence = this.getCompetence(idOrName);
if (competence) {
if (isNaN(newXp) || typeof (newXp) != 'number') newXp = 0;
this.checkCompetenceXP(idOrName, newXp);
const update = { _id: competence.id, 'system.xp': newXp };
await this.updateEmbeddedDocuments('Item', [update]); // Updates one EmbeddedEntity
this.updateExperienceLog("XP", newXp, "XP modifié en " + competence.name);
} else {
console.log("Competence not found", idOrName);
}
RdDUtility.checkThanatosXP(idOrName);
}
/* -------------------------------------------- */
async updateCompetenceXPSort(idOrName, compValue) {
let competence = this.getCompetence(idOrName);
if (competence) {
if (isNaN(compValue) || typeof (compValue) != 'number') compValue = 0;
const update = { _id: competence.id, 'system.xp_sort': compValue };
await this.updateEmbeddedDocuments('Item', [update]); // Updates one EmbeddedEntity
this.updateExperienceLog("XP Sort", compValue, "XP modifié en sort de " + competence.name);
} else {
console.log("Competence not found", idOrName);
}
}
/* -------------------------------------------- */
async updateCompetenceArchetype(idOrName, compValue) {
let competence = this.getCompetence(idOrName);
if (competence) {
compValue = compValue ?? 0;
const update = { _id: competence.id, 'system.niveau_archetype': compValue };
await this.updateEmbeddedDocuments('Item', [update]); // Updates one EmbeddedEntity
} else {
console.log("Competence not found", idOrName);
}
}
/* -------------------------------------------- */
async updateExperienceLog(modeXP, valeurXP, raisonXP = 'Inconnue') {
let d = new Date();
let expLog = duplicate(this.system.experiencelog);
expLog.push({
mode: Misc.upperFirst(modeXP), valeur: valeurXP, raison: Misc.upperFirst(raisonXP),
daterdd: game.system.rdd.calendrier.getDateFromIndex(),
datereel: `${d.getDate()}/${d.getMonth() + 1}/${d.getFullYear()}`
});
await this.update({ [`system.experiencelog`]: expLog });
}
async deleteExperienceLog(from, count) {
if (from >= 0 && count > 0) {
let expLog = duplicate(this.system.experiencelog);
expLog.splice(from, count);
await this.update({ [`system.experiencelog`]: expLog });
}
}
/* -------------------------------------------- */
async updateCompteurValue(fieldName, fieldValue, raison = 'Inconnue') {
await this.update({ [`system.compteurs.${fieldName}.value`]: fieldValue });
await this.addStressExperienceLog(fieldName, fieldValue, 'forcé: ' + raison);
}
/* -------------------------------------------- */
async addCompteurValue(fieldName, fieldValue, raison = 'Inconnue') {
let oldValue = this.system.compteurs[fieldName].value;
await this.update({ [`system.compteurs.${fieldName}.value`]: Number(oldValue) + Number(fieldValue) });
await this.addStressExperienceLog(fieldName, fieldValue, raison);
}
async addStressExperienceLog(fieldName, fieldValue, raison) {
switch (fieldName) {
case 'stress': case 'experience':
await this.updateExperienceLog(fieldName, fieldValue, raison);
}
}
/* -------------------------------------------- */
distribuerStress(compteur, stress, motif) {
if (game.user.isGM && this.hasPlayerOwner && this.isPersonnage()) {
switch (compteur) {
case 'stress': case 'experience':
const message = `${this.name} a reçu ${stress} points ${compteur == 'stress' ? "de stress" : "d'expérience"} (raison : ${motif})`;
this.addCompteurValue(compteur, stress, motif);
ui.notifications.info(message);
game.users.players.filter(player => player.active && player.character?.id == this.id)
.forEach(player => ChatUtility.notifyUser(player.id, 'info', message));
}
}
}
/* -------------------------------------------- */
async updateAttributeValue(fieldName, fieldValue) {
await this.update({ [`system.attributs.${fieldName}.value`]: fieldValue });
}
/* -------------------------------------------- */
_isConteneurContenu(item, conteneur) {
if (item?.isConteneur()) { // Si c'est un conteneur, il faut vérifier qu'on ne le déplace pas vers un sous-conteneur lui appartenant
for (let id of item.system.contenu) {
let subObjet = this.getObjet(id);
if (subObjet?.id == conteneur.id) {
return true; // Loop detected !
}
if (subObjet?.isConteneur()) {
return this._isConteneurContenu(subObjet, conteneur);
}
}
}
return false;
}
/* -------------------------------------------- */
getRecursiveEnc(objet) {
if (!objet) {
return 0;
}
const tplData = objet.system;
if (objet.type != 'conteneur') {
return Number(tplData.encombrement) * Number(tplData.quantite);
}
const encContenus = tplData.contenu.map(idContenu => this.getRecursiveEnc(this.getObjet(idContenu)));
return encContenus.reduce(Misc.sum(), 0)
+ Number(tplData.encombrement) /* TODO? Number(tplData.quantite) -- on pourrait avoir plusieurs conteneurs...*/
}
/* -------------------------------------------- */
buildSubConteneurObjetList(conteneurId, deleteList) {
let conteneur = this.getObjet(conteneurId);
if (conteneur?.type == 'conteneur') { // Si c'est un conteneur
for (let subId of conteneur.system.contenu) {
let subObj = this.getObjet(subId);
if (subObj) {
if (subObj.type == 'conteneur') {
this.buildSubConteneurObjetList(subId, deleteList);
}
deleteList.push({ id: subId, conteneurId: conteneurId });
}
}
}
}
/* -------------------------------------------- */
async deleteAllConteneur(itemId, options) {
let list = [];
list.push({ id: itemId, conteneurId: undefined }); // Init list
this.buildSubConteneurObjetList(itemId, list);
await this.deleteEmbeddedDocuments('Item', list.map(it => it.id), options);
}
/* -------------------------------------------- */
/** Supprime un item d'un conteneur, sur la base
* de leurs ID */
async enleverDeConteneur(item, conteneur, onEnleverDeConteneur) {
if (conteneur?.isConteneur()) {
item.estContenu = false;
await this.updateEmbeddedDocuments('Item', [{
_id: conteneur.id,
'system.contenu': conteneur.system.contenu.filter(id => id != item.id)
}]);
onEnleverDeConteneur();
}
}
/* -------------------------------------------- */
/** Ajoute un item dans un conteneur, sur la base
* de leurs ID */
async ajouterDansConteneur(item, conteneur, onAjouterDansConteneur) {
if (!conteneur) {
// TODO: afficher
item.estContenu = false;
}
else if (conteneur.isConteneur()) {
item.estContenu = true;
await this.updateEmbeddedDocuments('Item', [{
_id: conteneur.id,
'system.contenu': [...conteneur.system.contenu, item.id]
}]);
onAjouterDansConteneur(item.id, conteneur.id);
}
}
/* -------------------------------------------- */
/** Fonction de remise à plat de l'équipement (ie vide les champs 'contenu') */
async nettoyerConteneurs() {
RdDConfirm.confirmer({
settingConfirmer: "confirmation-vider",
content: `
Etes vous certain de vouloir vider tous les conteneurs ?
`, title: 'Vider les conteneurs', buttonLabel: 'Vider', onAction: async () => { const corrections = []; for (let item of this.items) { if (item.estContenu) { item.estContenu = undefined; } if (item.type == 'conteneur' && item.system.contenu.length > 0) { corrections.push({ _id: item.id, 'system.contenu': [] }); } } if (corrections.length > 0) { await this.updateEmbeddedDocuments('Item', corrections); } } }); } async processDropItem(params) { const targetActorId = this.id; const sourceActorId = params.sourceActorId; const itemId = params.itemId; const destId = params.destId; const srcId = params.srcId; if (sourceActorId && sourceActorId != targetActorId) { console.log("Moving objects", sourceActorId, targetActorId, itemId); this.moveItemsBetweenActors(itemId, sourceActorId); return false; } let result = true; const item = this.getObjet(itemId); if (item?.isEquipement() && sourceActorId == targetActorId) { // rangement if (srcId != destId && itemId != destId) { // déplacement de l'objet const src = this.getObjet(srcId); const dest = this.getObjet(destId); const cible = this.getContenantOrParent(dest); const messageEquipementDifferent = item.messageEquipementDifferent(dest); if (dest && !messageEquipementDifferent) { await this.regrouperEquipementsSimilaires(item, dest); result = false; } // changer de conteneur else if (!cible || this.conteneurPeutContenir(cible, item)) { await this.enleverDeConteneur(item, src, params.onEnleverConteneur); await this.ajouterDansConteneur(item, cible, params.onAjouterDansConteneur); } else { ui.notifications.info(messageEquipementDifferent); } } } await this.computeEncombrementTotalEtMalusArmure(); return result; } getContenantOrParent(dest) { if (!dest || dest.isConteneur()) { return dest; } return this.getContenant(dest); } getContenant(item) { return this.itemTypes['conteneur'].find(it => it.system.contenu.includes(item.id)); } /* -------------------------------------------- */ conteneurPeutContenir(dest, item) { if (!dest) { return true; } if (!dest.isConteneur()) { return false; } const destData = dest if (this._isConteneurContenu(item, dest)) { ui.notifications.warn(`Impossible de déplacer un conteneur parent (${item.name}) dans un de ses contenus ${destData.name} !`); return false; // Loop detected ! } // Calculer le total actuel des contenus let encContenu = this.getRecursiveEnc(dest) - Number(destData.system.encombrement); let newEnc = this.getRecursiveEnc(item); // Calculer le total actuel du nouvel objet // Teste si le conteneur de destination a suffisament de capacité pour recevoir le nouvel objet if (Number(destData.system.capacite) < encContenu + newEnc) { ui.notifications.warn( `Le conteneur ${dest.name} a une capacité de ${destData.system.capacite}, et contient déjà ${encContenu}. Impossible d'y ranger: ${item.name} d'encombrement ${newEnc}!`); return false; } return true; } /* -------------------------------------------- */ async moveItemsBetweenActors(itemId, sourceActorId) { let itemsList = [] let sourceActor = game.actors.get(sourceActorId); itemsList.push({ id: itemId, conteneurId: undefined }); // Init list sourceActor.buildSubConteneurObjetList(itemId, itemsList); // Get itemId list const itemsDataToCreate = itemsList.map(it => sourceActor.getObjet(it.id)) .map(it => duplicate(it)) .map(it => { it.system.contenu = []; return it; }); let newItems = await this.createEmbeddedDocuments('Item', itemsDataToCreate); let itemMap = this._buildMapOldNewId(itemsList, newItems); for (let item of itemsList) { // Second boucle pour traiter la remise en conteneurs // gestion conteneur/contenu if (item.conteneurId) { // l'Objet était dans un conteneur let newConteneurId = itemMap[item.conteneurId]; // Get conteneur let newConteneur = this.getObjet(newConteneurId); let newItemId = itemMap[item.id]; // Get newItem console.log('New conteneur filling!', newConteneur, newItemId, item); let contenu = duplicate(newConteneur.system.contenu); contenu.push(newItemId); await this.updateEmbeddedDocuments('Item', [{ _id: newConteneurId, 'system.contenu': contenu }]); } } for (let item of itemsList) { await sourceActor.deleteEmbeddedDocuments('Item', [item.id]); } } _buildMapOldNewId(itemsList, newItems) { let itemMap = {}; for (let i = 0; i < itemsList.length; i++) { itemMap[itemsList[i].id] = newItems[i].id; // Pour garder le lien ancien / nouveau } return itemMap; } async regrouperEquipementsSimilaires(item, dest) { await dest.quantiteIncDec(item.system.quantite); await item.delete(); } /* -------------------------------------------- */ computeMalusSurEncombrement() { switch (this.type) { case 'entite': case 'vehicule': return 0; } return Math.min(0, this.getEncombrementMax() - Math.ceil(Number(this.getEncTotal()))); } getMessageSurEncombrement() { return this.computeMalusSurEncombrement() < 0 ? "Sur-Encombrement!" : ""; } /* -------------------------------------------- */ getEncombrementMax() { switch (this.type) { case 'vehicule': return this.system.capacite_encombrement; case 'entite': return 0; default: return this.system.attributs.encombrement.value } } /* -------------------------------------------- */ computeIsHautRevant() { if (this.isPersonnage()) { this.system.attributs.hautrevant.value = this.hasItemNamed('tete', 'don de haut-reve') ? "Haut rêvant" : ""; } } hasItemNamed(type, name) { name = Grammar.toLowerCaseNoAccent(name); return this.listItemsData(type).find(it => Grammar.toLowerCaseNoAccent(it.name) == name); } /* -------------------------------------------- */ async computeEncombrementTotalEtMalusArmure() { if (!this.pack) { await this.computeMalusArmure(); return this.computeEncombrement(); } return 0; } /* -------------------------------------------- */ computeEncombrement() { this.encTotal = this.items.map(it => it.getEncTotal()).reduce(Misc.sum(), 0); return this.encTotal; } /* -------------------------------------------- */ async computeMalusArmure() { const newMalusArmure = this.filterItems(it => it.type == 'armure' && it.system.equipe) .map(it => it.system.malus ?? 0) .reduce(Misc.sum(), 0); // Mise à jour éventuelle du malus armure if (this.system.attributs?.malusarmure?.value != newMalusArmure) { await this.updateAttributeValue("malusarmure", newMalusArmure); } return newMalusArmure; } /* -------------------------------------------- */ computePrixTotalEquipement() { const deniers = this.items.filter(it => it.isEquipement()) .map(it => it.prixTotalDeniers()) .reduce(Misc.sum(), 0); return deniers / 100; } /* -------------------------------------------- */ computeResumeBlessure(blessures = undefined) { blessures = blessures ?? this.system.blessures; if (!blessures) { return "Pas de blessures possibles"; } let nbLegeres = this.countBlessures(blessures.legeres.liste); let nbGraves = this.countBlessures(blessures.graves.liste); let nbCritiques = this.countBlessures(blessures.critiques.liste); let resume = "Blessures:"; if (nbCritiques > 0 || nbGraves > 0 || nbLegeres > 0) { if (nbLegeres > 0) { resume += " " + nbLegeres + " légère" + (nbLegeres > 1 ? "s" : ""); } if (nbGraves > 0) { if (nbLegeres > 0) resume += ","; resume += " " + nbGraves + " grave" + (nbGraves > 1 ? "s" : ""); } if (nbCritiques > 0) { if (nbGraves > 0 || nbLegeres > 0) resume += ","; resume += " une CRITIQUE !"; } return resume; } else { return "Aucune blessure"; } } /* -------------------------------------------- */ computeEtatGeneral() { // Pas d'état général pour les entités forçage à 0 if (this.type == 'entite') { this.system.compteurs.etat.value = 0; return } // Pour les autres let sante = this.system.sante let compteurs = this.system.compteurs let state = Math.min(sante.vie.value - sante.vie.max, 0); if (ReglesOptionelles.isUsing("appliquer-fatigue") && sante.fatigue) { state += RdDUtility.currentFatigueMalus(sante.fatigue.value, sante.endurance.max); } // Ajout de l'éthylisme state += Math.min(0, (compteurs.ethylisme?.value ?? 0)); compteurs.etat.value = state; if (compteurs?.surenc) { compteurs.surenc.value = this.computeMalusSurEncombrement(); } } /* -------------------------------------------- */ async actionRefoulement(item) { const refoulement = item?.system.refoulement ?? 0; if (refoulement>0){ await this.ajouterRefoulement(refoulement); await item.delete(); } } /* -------------------------------------------- */ async ajouterRefoulement(value = 1) { let refoulement = this.system.reve.refoulement.value + value; let total = await RdDDice.rollTotal("1d20"); if (total <= refoulement) { refoulement = 0; await this.ajouterSouffle({ chat: true }); } await this.update({ "system.reve.refoulement.value": refoulement }); return refoulement == 0 ? "souffle" : "none"; } /* -------------------------------------------- */ async ajouterSouffle(options = { chat: false }) { let souffle = await RdDRollTables.getSouffle() //souffle.id = undefined; //TBC await this.createEmbeddedDocuments('Item', [souffle]); if (options.chat) { ChatMessage.create({ whisper: ChatUtility.getWhisperRecipientsAndGMs(game.user.name), content: this.name + " subit un Souffle de Dragon : " + souffle.name }); } return souffle; } /* -------------------------------------------- */ async ajouterQueue(options = { chat: false }) { let queue; if (this.system.reve.reve.thanatosused) { queue = await RdDRollTables.getOmbre(); await this.update({ "system.reve.reve.thanatosused": false }); } else { queue = await RdDRollTables.getQueue(); } await this.createEmbeddedDocuments('Item', [queue]); if (options.chat) { ChatMessage.create({ whisper: ChatUtility.getWhisperRecipientsAndGMs(game.user.name), content: this.name + " subit une Queue de Dragon : " + queue.name }); } return queue; } /* -------------------------------------------- */ /* -------------------------------------------- */ async changeTMRVisible() { await this.setTMRVisible(this.system.reve.tmrpos.cache ? true : false); } async setTMRVisible(newState) { await this.update({ 'system.reve.tmrpos.cache': !newState }); this.notifyRefreshTMR(); } isTMRCache() { return this.system.reve.tmrpos.cache; } notifyRefreshTMR() { game.socket.emit(SYSTEM_SOCKET_ID, { msg: "msg_tmr_move", data: { actorId: this._id, tmrPos: this.system.reve.tmrpos } }); } /* -------------------------------------------- */ async reinsertionAleatoire(raison, accessible = tmr => true) { const innaccessible = this.buildTMRInnaccessible(); let tmr = await TMRUtility.getTMRAleatoire(tmr => accessible(tmr) && !innaccessible.includes(tmr.coord)); ChatMessage.create({ content: `${raison} : ré-insertion aléatoire.`, whisper: ChatUtility.getWhisperRecipientsAndGMs(game.user.name) }); await this.forcerPositionTMRInconnue(tmr); return tmr; } async forcerPositionTMRInconnue(tmr) { await this.setTMRVisible(false); await this.updateCoordTMR(tmr.coord); this.notifyRefreshTMR(); } /* -------------------------------------------- */ buildTMRInnaccessible() { const tmrInnaccessibles = this.filterItems(it => Draconique.isCaseTMR(it) && EffetsDraconiques.isInnaccessible(it)); return tmrInnaccessibles.map(it => it.system.coord); } /* -------------------------------------------- */ getTMRRencontres() { return this.system.reve.rencontre.list; } /* -------------------------------------------- */ async deleteTMRRencontreAtPosition() { let rencontres = this.getTMRRencontres(); let newRencontres = rencontres.filter(it => it.coord != this.getDemiReve()); if (newRencontres.length != rencontres.length) { await this.update({ "system.reve.rencontre.list": newRencontres }); } } /* -------------------------------------------- */ async addTMRRencontre(currentRencontre) { let rencontres = this.getTMRRencontres(); let newRencontres = rencontres.filter(it => it.coord != this.getDemiReve()); if (newRencontres.length == rencontres.length) { newRencontres.push(currentRencontre); await this.update({ "system.reve.rencontre.list": newRencontres }); } } /* -------------------------------------------- */ async deleteTMRRencontre(rencontreKey) { let list = duplicate(this.system.reve.rencontre.list); let newList = []; for (let i = 0; i < list.length; i++) { if (i != rencontreKey) newList.push(list[i]); } await this.update({ "system.reve.rencontre.list": newList }); } /* -------------------------------------------- */ async updateCoordTMR(coord) { //console.log("UPDATE TMR", coord); await this.update({ "system.reve.tmrpos.coord": coord }); } /* -------------------------------------------- */ async reveActuelIncDec(value) { let reve = Math.max(this.system.reve.reve.value + value, 0); await this.update({ "system.reve.reve.value": reve }); } /* -------------------------------------------- */ async updatePointDeSeuil(value = 1) { const seuil = Misc.toInt(this.system.reve.seuil.value); const reve = Misc.toInt(this.system.carac.reve.value); if (seuil < reve) { await this.setPointsDeSeuil(Math.min(seuil + value, reve)); } } /* -------------------------------------------- */ async setPointsDeSeuil(seuil) { await this.update({ "system.reve.seuil.value": seuil }); } /* -------------------------------------------- */ async setPointsDeChance(chance) { await this.updateCompteurValue("chance", chance); } /* -------------------------------------------- */ getSonne() { return this.getEffect(STATUSES.StatusStunned); } /* -------------------------------------------- */ async finDeRound(options = { terminer: false }) { for (let effect of this.getEffects()) { if (effect.duration.type !== 'none' && (effect.duration.remaining <= 0 || options.terminer)) { if (effect.system.origin) { await effect.update({ 'disabled': true }); } else { await effect.delete(); } ChatMessage.create({ content: `${this.name} n'est plus ${Misc.lowerFirst(game.i18n.localize(effect.system.label))} !` }); } } if (this.type == 'personnage') { // Gestion blessure graves : -1 pt endurance let nbGraves = this.countBlessuresNonSoigneeByName('graves'); if (nbGraves > 0) { await this.santeIncDec("endurance", -1); } } } /* -------------------------------------------- */ async setSonne(sonne = true) { if (this.isEntite()) { return; } if (!game.combat && sonne) { ui.notifications.info("Le personnage est hors combat, il ne reste donc pas sonné"); return; } await this.setEffect(STATUSES.StatusStunned, sonne); } /* -------------------------------------------- */ getSConst() { if (this.isEntite()) { return 0; } return RdDCarac.calculSConst(this.system.carac.constitution.value) } async ajoutXpConstitution(xp) { await this.update({ "system.carac.constitution.xp": Misc.toInt(this.system.carac.constitution.xp) + xp }); } /* -------------------------------------------- */ countBlessures(blessuresListe) { return blessuresListe.filter(b => b.active).length } /* -------------------------------------------- */ countBlessuresByName(name) { return this.countBlessures(this.system.blessures[name].liste); } countBlessuresNonSoigneeByName(name) { if (this.system.blessures) { let blessures = this.system.blessures[name].liste; return blessures.filter(b => b.active && !b.psdone).length; } return 0; } /* -------------------------------------------- */ async testSiSonne(endurance) { const result = await this._jetEndurance(endurance); if (result.roll.total == 1) { ChatMessage.create({ content: await this._gainXpConstitutionJetEndurance() }); } return result; } /* -------------------------------------------- */ async jetEndurance() { const endurance = this.system.sante.endurance.value; const result = await this._jetEndurance(this.system.sante.endurance.value) const message = { content: "Jet d'Endurance : " + result.roll.total + " / " + endurance + "Voulez vous monter dans les TMR en mode ${mode}?
`, title: 'Confirmer la montée dans les TMR', buttonLabel: 'Monter dans les TMR', onAction: async () => await this._doDisplayTMR(mode) }); } async _doDisplayTMR(mode) { let isRapide = mode == "rapide"; if (mode != "visu") { let minReveValue = (isRapide && !EffetsDraconiques.isDeplacementAccelere(this) ? 3 : 2) + this.countMonteeLaborieuse(); if (this.getReveActuel() < minReveValue) { ChatMessage.create({ content: `Vous n'avez les ${minReveValue} Points de Reve nécessaires pour monter dans les Terres Médianes`, whisper: ChatMessage.getWhisperRecipients(game.user.name) }); return; } await this.setEffect(STATUSES.StatusDemiReve, true); } const fatigue = this.system.sante.fatigue.value; const endurance = this.system.sante.endurance.max; let tmrFormData = { mode: mode, fatigue: RdDUtility.calculFatigueHtml(fatigue, endurance), draconic: this.getDraconicList(), sort: this.getSortList(), signes: this.listItemsData("signedraconique"), caracReve: this.system.carac.reve.value, pointsReve: this.getReveActuel(), isRapide: isRapide } let html = await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/dialog-tmr.html', tmrFormData); this.currentTMR = await RdDTMRDialog.create(html, this, tmrFormData); this.currentTMR.render(true); } /* -------------------------------------------- */ rollArme(arme) { let competence = this.getCompetence(arme.system.competence) if (arme || (competence.type == 'competencecreature' && competence.system.iscombat)) { if (competence.system.ispossession) { RdDPossession.onAttaquePossession(this, competence); } else { RdDCombat.createUsingTarget(this)?.attaque(competence, arme); } } else { this.rollCompetence(competence.name); } } /* -------------------------------------------- */ _getTarget() { if (game.user.targets && game.user.targets.size == 1) { for (let target of game.user.targets) { return target; } } return undefined; } /* -------------------------------------------- */ getArmeParade(armeParadeId) { const item = armeParadeId ? this.getEmbeddedDocument('Item', armeParadeId) : undefined; return RdDItemArme.getArme(item); } /* -------------------------------------------- */ verifierForceMin(item) { if (item.type == 'arme' && item.system.force > this.system.carac.force.value) { ChatMessage.create({ content: `${this.name} s'est équipé(e) de l'arme ${item.name}, mais n'a pas une force suffisante pour l'utiliser normalement (${item.system.force} nécessaire pour une Force de ${this.system.carac.force.value})` }); } } /* -------------------------------------------- */ async equiperObjet(itemID) { let item = this.getEmbeddedDocument('Item', itemID); if (item?.system) { const isEquipe = !item.system.equipe; let update = { _id: item.id, "system.equipe": isEquipe }; await this.updateEmbeddedDocuments('Item', [update]); this.computeEncombrementTotalEtMalusArmure(); // Mise à jour encombrement this.computePrixTotalEquipement(); // Mis à jour du prix total de l'équipement if (isEquipe) this.verifierForceMin(item); } } /* -------------------------------------------- */ async computeArmure(attackerRoll) { let dmg = (attackerRoll.dmg.dmgArme ?? 0) + (attackerRoll.dmg.dmgActor ?? 0); let armeData = attackerRoll.arme; let protection = 0; const armures = this.items.filter(it => it.type == "armure" && it.system.equipe); for (const armure of armures) { protection += await RdDDice.rollTotal(armure.system.protection.toString()); if (dmg > 0) { this._deteriorerArmure(armure, dmg); dmg = 0; } } const penetration = Misc.toInt(armeData?.system.penetration ?? 0); protection = Math.max(protection - penetration, 0); protection += this.getProtectionNaturelle(); // Gestion des cas particuliers sur la fenêtre d'encaissement if (attackerRoll.dmg.encaisserSpecial == "noarmure") { protection = 0; } if (attackerRoll.dmg.encaisserSpecial == "chute") { protection = Math.min(protection, 2); } console.log("Final protect", protection, attackerRoll); return protection; } /* -------------------------------------------- */ _deteriorerArmure(armure, dmg) { armure = duplicate(armure); if (!ReglesOptionelles.isUsing('deteriorationArmure') || armure.system.protection == '0') { return; } armure.system.deterioration = (armure.system.deterioration ?? 0) + dmg; if (armure.system.deterioration >= 10) { armure.system.deterioration -= 10; let res = /(\d+)?d(\d+)(\-\d+)?/.exec(armure.system.protection); if (res) { let malus = Misc.toInt(res[3]) - 1; let armure = Misc.toInt(res[2]); if (armure + malus <= 0) { armure.system.protection = 0; } else { armure.system.protection = '' + (res[1] ?? '1') + 'd' + armure + malus; } } else if (/\d+/.exec(armure.system.protection)) { armure.system.protection = "1d" + armure.system.protection; } else { ui.notifications.warn(`La valeur d'armure de votre ${armure.name} est incorrecte`); } ChatMessage.create({ content: "Votre armure s'est détériorée, elle protège maintenant de " + armure.system.protection }); } this.updateEmbeddedDocuments('Item', [armure]); } /* -------------------------------------------- */ async encaisser() { let dialogData = { ajustementsEncaissement: RdDUtility.getAjustementsEncaissement() }; let html = await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/dialog-roll-encaisser.html', dialogData); new RdDEncaisser(html, this).render(true); } /* -------------------------------------------- */ async encaisserDommages(rollData, attacker = undefined, defenderRoll = undefined) { if (attacker && !await attacker.accorder(this, 'avant-encaissement')) { return; } console.log("encaisserDommages", rollData) let santeOrig = duplicate(this.system.sante); let encaissement = await this.jetEncaissement(rollData); this.ajouterBlessure(encaissement); // Will upate the result table const perteVie = this.isEntite() ? { newValue: 0 } : await this.santeIncDec("vie", - encaissement.vie); const perteEndurance = await this.santeIncDec("endurance", -encaissement.endurance, encaissement.critiques > 0); this.computeEtatGeneral(); mergeObject(encaissement, { alias: this.name, hasPlayerOwner: this.hasPlayerOwner, resteEndurance: this.system.sante.endurance.value, sonne: perteEndurance.sonne, jetEndurance: perteEndurance.jetEndurance, endurance: santeOrig.endurance.value - perteEndurance.newValue, vie: this.isEntite() ? 0 : (santeOrig.vie.value - perteVie.newValue), show: defenderRoll?.show ?? {} }); await ChatUtility.createChatWithRollMode(this.name, { roll: encaissement.roll, content: await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/chat-resultat-encaissement.html', encaissement) }); if (!encaissement.hasPlayerOwner && encaissement.endurance != 0) { encaissement = duplicate(encaissement); encaissement.isGM = true; ChatMessage.create({ whisper: ChatMessage.getWhisperRecipients("GM"), content: await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/chat-resultat-encaissement.html', encaissement) }); } } /* -------------------------------------------- */ async jetEncaissement(rollData) { let formula = "2d10"; // Chaque dé fait au minmum la difficulté libre if (ReglesOptionelles.isUsing('degat-minimum-malus-libre')) { if (rollData.diffLibre < 0) { let valeurMin = Math.abs(rollData.diffLibre); formula += "min" + valeurMin; } } // Chaque dé fait au minmum la difficulté libre if (ReglesOptionelles.isUsing('degat-ajout-malus-libre')) { if (rollData.diffLibre < 0) { let valeurMin = Math.abs(rollData.diffLibre); formula += "+" + valeurMin; } } let roll = await RdDDice.roll(formula); // 1 dé fait au minmum la difficulté libre if (ReglesOptionelles.isUsing('degat-minimum-malus-libre-simple')) { if (rollData.diffLibre < 0) { let valeurMin = Math.abs(rollData.diffLibre); if (roll.terms[0].results[0].result < valeurMin) { roll.terms[0].results[0].result = valeurMin; } else if (roll.terms[0].results[1].result < valeurMin) { roll.terms[0].results[1].result = valeurMin; } roll._total = roll.terms[0].results[0].result + roll.terms[0].results[1].result; } } const armure = await this.computeArmure(rollData); const jetTotal = roll.total + rollData.dmg.total - armure; let encaissement = RdDUtility.selectEncaissement(jetTotal, rollData.dmg.mortalite) let over20 = Math.max(jetTotal - 20, 0); encaissement.dmg = rollData.dmg; encaissement.dmg.loc = rollData.dmg.loc ?? await RdDUtility.getLocalisation(this.type); encaissement.dmg.loc.label = encaissement.dmg.loc.label ?? 'Corps;' encaissement.roll = roll; encaissement.armure = armure; encaissement.total = jetTotal; encaissement.vie = await RdDActor._evaluatePerte(encaissement.vie, over20); encaissement.endurance = await RdDActor._evaluatePerte(encaissement.endurance, over20); encaissement.penetration = rollData.arme?.system.penetration ?? 0; return encaissement; } /* -------------------------------------------- */ static async _evaluatePerte(formula, over20) { let perte = new Roll(formula, { over20: over20 }); await perte.evaluate({ async: true }); return perte.total; } /* -------------------------------------------- */ ajouterBlessure(encaissement) { if (this.type == 'entite') return; // Une entité n'a pas de blessures if (encaissement.legeres + encaissement.graves + encaissement.critiques == 0) return; const endActuelle = Number(this.system.sante.endurance.value); let blessures = duplicate(this.system.blessures); let count = encaissement.legeres; // Manage blessures while (count > 0) { let legere = blessures.legeres.liste.find(it => !it.active); if (legere) { this._setBlessure(legere, encaissement); count--; } else { encaissement.graves += count; encaissement.legeres -= count; break; } } count = encaissement.graves; while (count > 0) { let grave = blessures.graves.liste.find(it => !it.active); if (grave) { this._setBlessure(grave, encaissement); count--; } else { encaissement.critiques += count; encaissement.graves -= count; encaissement.endurance = endActuelle; encaissement.vie = 4; break; } } count = encaissement.critiques; while (count > 0) { let critique = blessures.critiques.liste[0]; if (!critique.active) { this._setBlessure(critique, encaissement); count--; } else { // TODO: status effect dead this.setEffect(STATUSES.StatusComma, true); ChatMessage.create({ content: ` ${this.name} vient de succomber à une seconde blessure critique ! Que les Dragons gardent son Archétype en paix !` }); encaissement.critiques -= count; encaissement.mort = true; break; } } encaissement.endurance = Math.max(encaissement.endurance, -endActuelle); this.update({ "system.blessures": blessures }); } /* -------------------------------------------- */ _setBlessure(blessure, encaissement) { blessure.active = true; blessure.psdone = false; blessure.scdone = false; blessure.loc = encaissement.locName; } /* -------------------------------------------- */ /** @override */ getRollData() { const rollData = super.getRollData(); return rollData; } /* -------------------------------------------- */ async resetItemUse() { await this.unsetFlag(SYSTEM_RDD, 'itemUse'); await this.setFlag(SYSTEM_RDD, 'itemUse', {}); } /* -------------------------------------------- */ async incDecItemUse(itemId, inc = 1) { let itemUse = duplicate(this.getFlag(SYSTEM_RDD, 'itemUse') ?? {}); itemUse[itemId] = (itemUse[itemId] ?? 0) + inc; await this.setFlag(SYSTEM_RDD, 'itemUse', itemUse); console.log("ITEM USE INC", inc, itemUse); } /* -------------------------------------------- */ getItemUse(itemId) { let itemUse = this.getFlag(SYSTEM_RDD, 'itemUse') ?? {}; console.log("ITEM USE GET", itemUse); return itemUse[itemId] ?? 0; } /* -------------------------------------------- */ /* -- entites -- */ /* retourne true si on peut continuer, false si on ne peut pas continuer */ async targetEntiteNonAccordee(target, when = 'avant-encaissement') { if (target) { return !await this.accorder(target.actor, when); } return false; } /* -------------------------------------------- */ async accorder(entite, when = 'avant-encaissement') { if (when != game.settings.get(SYSTEM_RDD, "accorder-entite-cauchemar") || !entite.isEntite([ENTITE_INCARNE]) || entite.isEntiteAccordee(this)) { return true; } const tplData = this.system; let rolled = await RdDResolutionTable.roll(this.getReveActuel(), - Number(entite.system.carac.niveau.value)); const rollData = { alias: this.name, rolled: rolled, entite: entite.name, selectedCarac: tplData.carac.reve }; if (rolled.isSuccess) { await entite.setEntiteReveAccordee(this); } await RdDResolutionTable.displayRollData(rollData, this, 'chat-resultat-accorder-cauchemar.html'); if (rolled.isPart) { await this.appliquerAjoutExperience(rollData, true); } return rolled.isSuccess; } /* -------------------------------------------- */ isEntite(typeentite = []) { return this.type == 'entite' && (typeentite.length == 0 || typeentite.includes(this.system.definition.typeentite)); } /* -------------------------------------------- */ isEntiteAccordee(attaquant) { if (!this.isEntite([ENTITE_INCARNE])) { return true; } let resonnance = this.system.sante.resonnance; return (resonnance.actors.find(it => it == attaquant.id)); } /* -------------------------------------------- */ async setEntiteReveAccordee(attaquant) { if (!this.isEntite([ENTITE_INCARNE])) { ui.notifications.error("Impossible de s'accorder à " + this.name + ": ce n'est pas une entite de cauchemer/rêve"); return; } let resonnance = duplicate(this.system.sante.resonnance); if (resonnance.actors.find(it => it == attaquant.id)) { // déjà accordé return; } resonnance.actors.push(attaquant.id); await this.update({ "system.sante.resonnance": resonnance }); return; } /* -------------------------------------------- */ getFortune() { let monnaies = this.itemTypes['monnaie']; if (monnaies.length < 4) { ui.notifications.error("Problème de monnaies manquantes, impossible de payer correctement!") throw "Problème de monnaies manquantes, impossible de payer correctement!"; } return monnaies.map(m => Number(m.system.valeur_deniers) * Number(m.system.quantite)) .reduce(Misc.sum(), 0); } /* -------------------------------------------- */ async optimizeArgent(fortuneTotale) { let monnaies = this.itemTypes['monnaie']; let parValeur = Misc.classifyFirst(monnaies, it => it.system.valeur_deniers); let nouvelleFortune = { 1000: Math.floor(fortuneTotale / 1000), // or 100: Math.floor(fortuneTotale / 100) % 10, // argent 10: Math.floor(fortuneTotale / 10) % 10, // bronze 1: fortuneTotale % 10 // étain } console.log('RdDActor.optimizeArgent', fortuneTotale, 'nouvelleFortune', nouvelleFortune, 'monnaie_par_valeur', parValeur); let updates = []; for (const [valeur, nombre] of Object.entries(nouvelleFortune)) { updates.push({ _id: parValeur[valeur].id, 'system.quantite': nombre }); } await this.updateEmbeddedDocuments('Item', updates); } /* -------------------------------------------- */ async depenserDeniers(depense, dataObj = undefined, quantite = 1, toActorId) { depense = Number(depense); let fortune = this.getFortune(); console.log("depenserDeniers", game.user.character, depense, fortune); let msg = ""; if (depense == 0) { if (dataObj) { dataObj.payload.system.cout = depense / 100; // Mise à jour du prix en sols , avec le prix acheté dataObj.payload.system.quantite = quantite; await this.createEmbeddedDocuments('Item', [dataObj.payload]); msg += `