foundryvtt-reve-de-dragon/module/actor.js
Vincent Vandemeulebrouck b9e8c24461 Pas d'expérience si le MJ n'est pas connecté
Pour éviter les jets de dés abusifs quand le MJ n'est pas là,
les réussites particulières ne rapportent plus d'expérience
lorsque le MJ n'est pas connecté
2024-08-30 23:56:20 +02:00

3100 lines
116 KiB
JavaScript

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 { 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 { RdDItemCompetence } from "./item-competence.js";
import { RdDAlchimie } from "./rdd-alchimie.js";
import { STATUSES } from "./settings/status-effects.js";
import { RdDItemSigneDraconique } from "./item/signedraconique.js";
import { ReglesOptionnelles } from "./settings/regles-optionnelles.js";
import { EffetsDraconiques } from "./tmr/effets-draconiques.js";
import { Draconique } from "./tmr/draconique.js";
import { RdDCarac } from "./rdd-carac.js";
import { DialogConsommer } from "./dialog-item-consommer.js";
import { DialogFabriquerPotion } from "./dialog-fabriquer-potion.js";
import { RollDataAjustements } from "./rolldata-ajustements.js";
import { RdDPossession } from "./rdd-possession.js";
import { SHOW_DICE, SYSTEM_RDD, SYSTEM_SOCKET_ID } from "./constants.js";
import { RdDConfirm } from "./rdd-confirm.js";
import { RdDRencontre } from "./item/rencontre.js";
import { DialogRepos } from "./sommeil/dialog-repos.js";
import { RdDBaseActor } from "./actor/base-actor.js";
import { RdDTimestamp } from "./time/rdd-timestamp.js";
import { RdDItemBlessure } from "./item/blessure.js";
import { AppAstrologie } from "./sommeil/app-astrologie.js";
import { RdDEmpoignade } from "./rdd-empoignade.js";
import { ExperienceLog, XP_TOPIC } from "./actor/experience-log.js";
import { TYPES } from "./item.js";
import { RdDBaseActorSang } from "./actor/base-actor-sang.js";
import { RdDCoeur } from "./coeur/rdd-coeur.js";
import { DialogChoixXpCarac } from "./dialog-choix-xp-carac.js";
export const MAINS_DIRECTRICES = ['Droitier', 'Gaucher', 'Ambidextre']
/* -------------------------------------------- */
/**
* 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 RdDBaseActorSang {
/* -------------------------------------------- */
/**
* Prepare Character type specific data
*/
prepareActorData() {
this.$computeCaracDerivee()
this.$computeIsHautRevant()
}
/* -------------------------------------------- */
$computeCaracDerivee() {
this.system.carac.force.value = Math.min(this.system.carac.force.value, parseInt(this.system.carac.taille.value) + 4);
this.system.carac.melee.value = Math.floor((parseInt(this.system.carac.force.value) + parseInt(this.system.carac.agilite.value)) / 2);
this.system.carac.tir.value = Math.floor((parseInt(this.system.carac.vue.value) + parseInt(this.system.carac.dexterite.value)) / 2);
this.system.carac.lancer.value = Math.floor((parseInt(this.system.carac.tir.value) + parseInt(this.system.carac.force.value)) / 2);
this.system.carac.derobee.value = Math.floor(parseInt(((21 - this.system.carac.taille.value)) + parseInt(this.system.carac.agilite.value)) / 2);
let bonusDomKey = Math.floor((parseInt(this.system.carac.force.value) + parseInt(this.system.carac.taille.value)) / 2);
let tailleData = RdDCarac.getCaracDerivee(bonusDomKey);
this.system.attributs.plusdom.value = tailleData.plusdom;
this.system.attributs.encombrement.value = (parseInt(this.system.carac.force.value) + parseInt(this.system.carac.taille.value)) / 2;
this.system.attributs.sconst.value = RdDCarac.calculSConst(this.system.carac.constitution.value);
this.system.attributs.sust.value = RdDCarac.getCaracDerivee(this.system.carac.taille.value).sust;
this.system.sante.vie.max = Math.ceil((parseInt(this.system.carac.taille.value) + parseInt(this.system.carac.constitution.value)) / 2);
this.system.sante.vie.value = Math.min(this.system.sante.vie.value, this.system.sante.vie.max)
this.system.sante.endurance.max = Math.max(parseInt(this.system.carac.taille.value) + parseInt(this.system.carac.constitution.value), parseInt(this.system.sante.vie.max) + parseInt(this.system.carac.volonte.value));
this.system.sante.endurance.value = Math.min(this.system.sante.endurance.value, this.system.sante.endurance.max);
this.system.sante.fatigue.max = this.getFatigueMax();
this.system.sante.fatigue.value = Math.min(this.system.sante.fatigue.value, this.system.sante.fatigue.max);
//Compteurs
this.system.reve.reve.max = this.system.carac.reve.value;
this.system.compteurs.chance.max = this.system.carac.chance.value;
}
$computeIsHautRevant() {
this.system.attributs.hautrevant.value = this.itemTypes['tete'].find(it => Grammar.equalsInsensitive(it.name, 'don de haut-reve'))
? "Haut rêvant"
: "";
}
/* -------------------------------------------- */
canReceive(item) {
return ![TYPES.competencecreature, TYPES.tarot, TYPES.service].includes(item.type)
}
isPersonnageJoueur() {
return this.hasPlayerOwner && this.prototypeToken.actorLink
}
isPersonnage() { return true }
isHautRevant() { return this.system.attributs.hautrevant.value != "" }
/* -------------------------------------------- */
getAgilite() { return this.system.carac.agilite?.value ?? 0 }
getChance() { return this.system.carac.chance?.value ?? 0 }
getReveActuel() { return this.system.reve?.reve?.value ?? this.carac.reve.value ?? 0 }
getChanceActuel() { return this.system.compteurs.chance?.value ?? 10 }
getMoralTotal() { return this.system.compteurs.moral?.value ?? 0 }
/* -------------------------------------------- */
getEtatGeneral(options = { ethylisme: false }) {
const etatGeneral = this.system.compteurs.etat?.value ?? 0
// Pour les jets d'Ethylisme, on retire le malus d'éthylisme (p.162)
const annuleMalusEthylisme = options.ethylisme ? this.malusEthylisme() : 0
return etatGeneral - annuleMalusEthylisme
}
/* -------------------------------------------- */
getActivePoisons() {
return foundry.utils.duplicate(this.items.filter(item => item.type == 'poison' && item.system.active))
}
/* -------------------------------------------- */
getMalusArmure() {
return this.itemTypes[TYPES.armure].filter(it => it.system.equipe)
.map(it => it.system.malus)
.reduce(Misc.sum(), 0);
}
/* -------------------------------------------- */
getTache(id) { return this.findItemLike(id, 'tache') }
getMeditation(id) { return this.findItemLike(id, 'meditation') }
getChant(id) { return this.findItemLike(id, 'chant') }
getDanse(id) { return this.findItemLike(id, 'danse') }
getMusique(id) { return this.findItemLike(id, 'musique') }
getOeuvre(id, type = 'oeuvre') { return this.findItemLike(id, type) }
getJeu(id) { return this.findItemLike(id, 'jeu') }
getRecetteCuisine(id) { return this.findItemLike(id, 'recettecuisine') }
/* -------------------------------------------- */
getDemiReve() { return this.system.reve.tmrpos.coord }
getDraconicList() { return this.itemTypes[TYPES.competence].filter(it => it.system.categorie == 'draconic') }
getBestDraconic() { return foundry.utils.duplicate(this.getDraconicList().sort(Misc.descending(it => it.system.niveau)).find(it => true)) }
getDraconicOuPossession() {
return [...this.getDraconicList().filter(it => it.system.niveau >= 0),
super.getDraconicOuPossession()]
.sort(Misc.descending(it => it.system.niveau))
.find(it => true)
}
/* -------------------------------------------- */
async $perteRevePotionsEnchantees() {
let potions = this.itemTypes[TYPES.potion]
.filter(it => Grammar.includesLowerCaseNoAccent(it.system.categorie, 'enchanté') && !it.system.prpermanent)
const potionUpdates = await Promise.all(potions.map(async it => {
const nouveauReve = Math.max(it.system.pr - 1, 0)
ChatMessage.create({
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: await renderTemplate(`systems/foundryvtt-reve-de-dragon/templates/chat-potionenchantee-chateaudormant.html`, {
pr: nouveauReve,
alias: this.name,
potionName: it.name,
potionImg: it.img
})
})
return {
_id: it._id,
'system.pr': nouveauReve,
'system.quantite': nouveauReve > 0 ? it.system.quantite : 0
}
}))
await this.updateEmbeddedDocuments('Item', potionUpdates);
}
/** --------------------------------------------
* @returns true si l'acteur possède au moins 1 arme de mêlée équipée
*/
hasArmeeMeleeEquipee() {
return this.itemTypes['arme'].find(it => it.system.equipe && it.system.competence != "")
}
async prepareChateauDormant(consigne) {
if (consigne.ignorer) {
return;
}
if (consigne.stress.valeur > 0) {
await this.distribuerStress('stress', consigne.stress.valeur, consigne.stress.motif);
}
await this.update({ 'system.sommeil': consigne.sommeil })
const player = this.findPlayer();
if (player) {
ChatUtility.notifyUser(player.id, 'info', `Vous pouvez gérer la nuit de ${this.name}`);
}
}
async onTimeChanging(oldTimestamp, newTimestamp) {
await super.onTimeChanging(oldTimestamp, newTimestamp);
await this.setInfoSommeilInsomnie();
}
async repos() {
await DialogRepos.create(this);
}
/* -------------------------------------------- */
async grisReve(nbJours) {
let message = {
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: `${nbJours} jours de gris rêve sont passés. `
};
for (let i = 0; i < nbJours; i++) {
await this.dormir(4, { grisReve: true });
await this._recuperationSante(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();
await this.setBonusPotionSoin(0);
}
await this.resetInfoSommeil()
ChatMessage.create(message);
this.sheet.render(true);
}
async _recuperationSante(message) {
const maladiesPoisons = this.getMaladiePoisons();
const isMaladeEmpoisonne = maladiesPoisons.length > 0;
this._messageRecuperationMaladiePoisons(maladiesPoisons, message);
await this._recupererBlessures(message, isMaladeEmpoisonne);
await this._recupererVie(message, isMaladeEmpoisonne);
}
getMaladiePoisons() {
return this.items.filter(item => item.type == 'maladie' || (item.type == 'poison' && item.system.active));
}
_messageRecuperationMaladiePoisons(maladiesPoisons, message) {
if (maladiesPoisons.length > 0) {
const identifies = maladiesPoisons.filter(it => it.system.identifie);
const nonIdentifies = maladiesPoisons.filter(it => !it.system.identifie);
message.content += 'Vous souffrez';
switch (nonIdentifies.length) {
case 0: break;
case 1: message.content += ` d'un mal inconnu`; break;
default: message.content += ` de ${nonIdentifies.length} maux inconnus`; break;
}
if (identifies.length > 0 && nonIdentifies.length > 0) { message.content += ' et' }
if (identifies.length > 0) {
message.content += ' de ' + identifies.map(it => it.name).reduce(Misc.joining(', '));
}
}
}
/* -------------------------------------------- */
async dormirChateauDormant() {
if (!ReglesOptionnelles.isUsing("chateau-dormant-gardien") || !this.system.sommeil || this.system.sommeil.nouveaujour) {
const message = {
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: ""
};
await this._recuperationSante(message)
await this._recupereMoralChateauDormant(message);
await this._recupereChance();
await this.transformerStress();
await this.retourSeuilDeReve(message);
await this.setBonusPotionSoin(0);
await this.retourSust(message);
await this.$perteRevePotionsEnchantees();
await RdDCoeur.applyCoeurChateauDormant(this, message)
if (message.content != "") {
message.content = `A la fin Chateau Dormant, ${message.content}<br>Un nouveau jour se lève`;
ChatMessage.create(message);
}
await this.resetInfoSommeil();
this.sheet.render(true);
}
}
async resetInfoSommeil() {
await this.update({
'system.sommeil': {
nouveaujour: false,
date: game.system.rdd.calendrier.getTimestamp(),
moral: "neutre",
heures: 0,
insomnie: EffetsDraconiques.isSujetInsomnie(this)
}
});
}
async setInfoSommeilInsomnie() {
if (EffetsDraconiques.isSujetInsomnie(this)) {
await this.update({ 'system.sommeil.insomnie': true });
}
}
async setInfoSommeilMoral(situationMoral) {
await this.update({ 'system.sommeil.moral': situationMoral });
}
/* -------------------------------------------- */
async _recupereChance() {
if (!ReglesOptionnelles.isUsing("recuperation-chance")) { return }
// 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 _recupereMoralChateauDormant(message) {
if (!ReglesOptionnelles.isUsing("recuperation-moral")) { return }
const etatMoral = this.system.sommeil?.moral ?? 'neutre';
const jetMoral = await this._jetDeMoral(etatMoral);
message.content += ` -- le jet de moral est ${etatMoral}, le moral ` + this._messageAjustementMoral(jetMoral.ajustement);
}
_messageAjustementMoral(ajustement) {
switch (Math.sign(ajustement)) {
case 1:
return `remonte de ${ajustement}`;
case -1:
return `diminue de ${-ajustement}`;
case 0:
return 'reste stable';
default:
console.error(`Le signe de l'ajustement de moral ${ajustement} est ${Math.sign(ajustement)}, ce qui est innatendu`)
return `est ajusté de ${ajustement} (bizarre)`;
}
}
/* -------------------------------------------- */
async _recupererBlessures(message, isMaladeEmpoisonne) {
const timestamp = game.system.rdd.calendrier.getTimestamp()
const blessures = this.filterItems(it => it.system.gravite > 0, TYPES.blessure).sort(Misc.ascending(it => it.system.gravite))
await Promise.all(blessures.map(async b => b.recuperationBlessure({
actor: this,
timestamp,
message,
isMaladeEmpoisonne,
blessures
})));
await this.supprimerBlessures(it => it.system.gravite <= 0);
}
/* -------------------------------------------- */
async _recupererVie(message, isMaladeEmpoisonne) {
const tData = this.system
let blessures = this.filterItems(it => it.system.gravite > 0, TYPES.blessure);
if (blessures.length > 0) {
return
}
let vieManquante = tData.sante.vie.max - tData.sante.vie.value;
if (vieManquante > 0) {
let rolled = await this.jetRecuperationConstitution(0, message)
if (!isMaladeEmpoisonne && 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) {
let difficulte = Math.min(0, this.system.sante.vie.value - this.system.sante.vie.max) + bonusSoins + this.system.sante.bonusPotion;
let rolled = await RdDResolutionTable.roll(this.system.carac.constitution.value, difficulte);
if (message) {
message.content = await renderTemplate("systems/foundryvtt-reve-de-dragon/templates/roll/explain.hbs", {
actor: this,
carac: this.system.carac.constitution,
rolled
})
}
return rolled;
}
/* -------------------------------------------- */
async remiseANeuf() {
ChatMessage.create({
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: 'Remise à neuf de ' + this.name
});
await this.supprimerBlessures(it => true);
await this.removeEffects(e => e.id != STATUSES.StatusDemiReve);
const updates = {
'system.sante.endurance.value': this.system.sante.endurance.max,
'system.sante.vie.value': this.system.sante.vie.max,
'system.sante.fatigue.value': 0,
'system.compteurs.ethylisme': { value: 1, nb_doses: 0, jet_moral: false }
};
await this.update(updates);
}
/* -------------------------------------------- */
async dormir(heures, options = { grisReve: false, chateauDormant: false }) {
const message = {
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: this.name + ': '
};
const insomnie = this.system.sommeil?.insomnie || heures == 0;
await this.recupereEndurance({ message: message, demi: insomnie });
if (insomnie) {
message.content += 'Vous ne trouvez pas le sommeil';
}
else {
let jetsReve = [];
let dormi = await this.dormirDesHeures(jetsReve, message, heures, options);
if (jetsReve.length > 0) {
message.content += `Vous récupérez ${jetsReve.map(it => it < 0 ? '0 (réveil)' : it).reduce(Misc.joining("+"))} Points de rêve. `;
}
if (dormi.etat == 'eveil') {
await this.reveilReveDeDragon(message, dormi.heures);
}
options.chateauDormant = options.chateauDormant && dormi.heures >= heures;
message.content += `Vous avez dormi ${dormi.heures <= 1 ? 'une heure' : (dormi.heures + ' heures')}. `;
}
if (!options.grisReve) {
ChatMessage.create(message);
}
if (options.chateauDormant) {
await this.dormirChateauDormant();
}
else {
this.sheet.render(true);
}
}
async reveilReveDeDragon(message, heures) {
const restant = Math.max(this.system.sommeil?.heures - heures, 0)
if (restant > 0) {
await this.update({ 'system.sommeil': { heures: restant } });
}
}
async dormirDesHeures(jetsReve, message, heures, options) {
const dormi = { heures: 0, etat: 'dort' };
for (; dormi.heures < heures && dormi.etat == 'dort'; dormi.heures++) {
await this._recupererEthylisme(message);
if (options.grisReve) {
await this.recupererFatigue(message);
}
else if (!this.system.sommeil?.insomnie) {
await this.recupererFatigue(message);
dormi.etat = await this.jetRecuperationReve(jetsReve, message);
if (dormi.etat == 'dort' && EffetsDraconiques.isDonDoubleReve(this)) {
dormi.etat = await this.jetRecuperationReve(jetsReve, message);
}
}
}
return dormi;
}
/* -------------------------------------------- */
async jetRecuperationReve(jetsReve, message) {
if (this.getReveActuel() < this.system.reve.seuil.value) {
let reve = await RdDDice.rollTotal("1dr");
if (reve >= 7) {
// Rêve de Dragon !
message.content += `Vous faites un <strong>Rêve de Dragon</strong> de ${reve} Points de rêve qui vous réveille! `;
await this.combattreReveDeDragon(reve);
jetsReve.push(-1);
return 'eveil';
}
else {
if (!ReglesOptionnelles.isUsing("recuperation-reve")) {
ChatMessage.create({
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: `Pas de récupération de rêve (${reve} points ignorés)`
});
jetsReve.push(0);
}
else {
await this.reveActuelIncDec(reve);
jetsReve.push(reve);
}
}
}
return 'dort';
}
/* -------------------------------------------- */
async _recupererEthylisme(message) {
if (!ReglesOptionnelles.isUsing("recuperation-ethylisme")) { return; }
let value = Math.min(Number.parseInt(this.system.compteurs.ethylisme.value) + 1, 1);
if (value <= 0) {
message.content += `Vous dégrisez un peu (${RdDUtility.getNomEthylisme(value)}). `;
}
await this.update({
'system.compteurs.ethylisme': {
nb_doses: 0,
jet_moral: false,
value: value
}
});
}
/* -------------------------------------------- */
async recupereEndurance({ message, demi }) {
let max = this._computeEnduranceMax();
if (demi) {
max = Math.floor(max / 2);
}
const manquant = max - 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 (ReglesOptionnelles.isUsing("appliquer-fatigue")) {
let fatigue = this.system.sante.fatigue.value;
const fatigueMin = this.getFatigueMin();
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 retourSeuilDeReve(message) {
const seuil = this.system.reve.seuil.value;
const reveActuel = this.getReveActuel();
if (reveActuel > seuil) {
message.content += `<br>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 += `<br>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 += `<br>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: this.getDraconicOuPossession(),
canClose: false,
rencontre: await game.system.rdd.rencontresTMR.getReveDeDragon(force),
tmr: true,
use: { libre: false, conditions: false },
forceCarac: { 'reve-actuel': { label: "Rêve Actuel", value: this.getReveActuel() } }
}
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' },
{
name: 'maitrise',
label: 'Maîtriser le Rêve de Dragon',
callbacks: [
{ action: async r => this.resultCombatReveDeDragon(r) }
]
}
);
dialog.render(true);
}
/* -------------------------------------------- */
async resultCombatReveDeDragon(rollData) {
const result = rollData.rolled.isSuccess
? rollData.rencontre.system.succes
: rollData.rencontre.system.echec;
RdDRencontre.appliquer(result.effets, {}, rollData)
}
/* -------------------------------------------- */
async sortMisEnReserve(sort, draconic, coord, ptreve) {
await this.createEmbeddedDocuments("Item", [{
type: TYPES.sortreserve,
name: sort.name,
img: sort.img,
system: { sortid: sort._id, draconic: (draconic?.name ?? sort.system.draconic), ptreve: ptreve, coord: coord, heurecible: 'Vaisseau' }
}],
{ renderSheet: false });
this.tmrApp.updateTokens();
}
/* -------------------------------------------- */
async updateCarac(caracName, to) {
if (caracName == "force") {
if (Number(to) > this.getTaille() + 4) {
ui.notifications.warn("Votre FORCE doit être au maximum de TAILLE+4");
return;
}
}
if (caracName == "reve") {
if (to > Misc.toInt(this.system.reve.seuil.value)) {
this.setPointsDeSeuil(to);
}
}
if (caracName == "chance") {
if (to > Misc.toInt(this.system.compteurs.chance.value)) {
this.setPointsDeChance(to);
}
}
let selectedCarac = this.findCaracByName(caracName);
const from = selectedCarac.value
await this.update({ [`system.carac.${caracName}.value`]: to });
await ExperienceLog.add(this, XP_TOPIC.CARAC, from, to, caracName);
}
/* -------------------------------------------- */
async updateCaracXP(caracName, to) {
if (caracName == 'Taille') {
return;
}
let selectedCarac = this.findCaracByName(caracName);
if (!selectedCarac.derivee) {
const from = Number(selectedCarac.xp);
await this.update({ [`system.carac.${caracName}.xp`]: to });
await ExperienceLog.add(this, XP_TOPIC.XPCARAC, from, to, caracName);
}
this.checkCaracXP(caracName);
}
/* -------------------------------------------- */
async updateCaracXPAuto(caracName) {
if (caracName == 'Taille') {
return;
}
let carac = this.findCaracByName(caracName);
if (carac) {
carac = foundry.utils.duplicate(carac);
const fromXp = Number(carac.xp);
const fromValue = Number(carac.value);
let toXp = fromXp;
let toValue = fromValue;
while (toXp >= RdDCarac.getCaracNextXp(toValue) && toXp > 0) {
toXp -= RdDCarac.getCaracNextXp(toValue);
toValue++;
}
carac.xp = toXp;
carac.value = toValue;
await this.update({ [`system.carac.${caracName}`]: carac });
await ExperienceLog.add(this, XP_TOPIC.XPCARAC, fromXp, toXp, caracName);
await ExperienceLog.add(this, XP_TOPIC.CARAC, fromValue, toValue, caracName);
}
}
/* -------------------------------------------- */
async updateCompetenceXPAuto(idOrName) {
let competence = this.getCompetence(idOrName);
if (competence) {
const fromXp = Number(competence.system.xp);
const fromNiveau = Number(competence.system.niveau);
let toXp = fromXp;
let toNiveau = fromNiveau;
while (toXp >= RdDItemCompetence.getCompetenceNextXp(toNiveau) && toXp > 0) {
toXp -= RdDItemCompetence.getCompetenceNextXp(toNiveau);
toNiveau++;
}
await competence.update({
"system.xp": toXp,
"system.niveau": toNiveau,
});
await ExperienceLog.add(this, XP_TOPIC.XP, fromXp, toXp, competence.name);
await ExperienceLog.add(this, XP_TOPIC.NIVEAU, fromNiveau, toNiveau, competence.name);
}
}
async updateCompetenceStress(idOrName) {
const competence = this.getCompetence(idOrName);
if (!competence) {
return;
}
const fromXp = competence.system.xp;
const fromXpStress = this.system.compteurs.experience.value;
const fromNiveau = Number(competence.system.niveau);
const xpSuivant = RdDItemCompetence.getCompetenceNextXp(fromNiveau);
const xpRequis = xpSuivant - fromXp;
if (fromXpStress <= 0 || fromNiveau >= competence.system.niveau_archetype) {
ui.notifications.info(`La compétence ne peut pas augmenter!
stress disponible: ${fromXpStress}
expérience requise: ${xpRequis}
niveau : ${fromNiveau}
archétype : ${competence.system.niveau_archetype}`);
return;
}
const xpUtilise = Math.max(0, Math.min(fromXpStress, xpRequis));
const gainNiveau = (xpUtilise >= xpRequis || xpRequis <= 0) ? 1 : 0;
const toNiveau = fromNiveau + gainNiveau;
const newXp = gainNiveau > 0 ? Math.max(fromXp - xpSuivant, 0) : (fromXp + xpUtilise);
await competence.update({
"system.xp": newXp,
"system.niveau": toNiveau,
});
const toXpStress = Math.max(0, fromXpStress - xpUtilise);
await this.update({ "system.compteurs.experience.value": toXpStress });
await ExperienceLog.add(this, XP_TOPIC.TRANSFORM, fromXpStress, toXpStress, `Dépense stress`);
await ExperienceLog.add(this, XP_TOPIC.XP, fromXp, newXp, competence.name);
await ExperienceLog.add(this, XP_TOPIC.NIVEAU, fromNiveau, toNiveau, competence.name);
}
/* -------------------------------------------- */
async updateCompetence(idOrName, compValue) {
const competence = this.getCompetence(idOrName);
if (competence) {
const toNiveau = compValue ?? RdDItemCompetence.getNiveauBase(competence.system.categorie, competence.getCategories());
this.notifyCompetencesTronc(competence, toNiveau);
const fromNiveau = competence.system.niveau;
await competence.update({ 'system.niveau': toNiveau });
await ExperienceLog.add(this, XP_TOPIC.NIVEAU, fromNiveau, toNiveau, competence.name, true);
}
}
notifyCompetencesTronc(competence, toNiveau) {
const listTronc = RdDItemCompetence.getListTronc(competence.name).filter(it => {
const autreComp = this.getCompetence(it);
const niveauTr = autreComp?.system.niveau ?? 0;
return niveauTr < 0 && niveauTr < toNiveau;
});
if (listTronc.length > 0) {
ui.notifications.info(
"Vous avez modifié une compétence 'tronc'. Vérifiez que les compétences suivantes évoluent ensemble jusqu'au niveau 0 : <br>"
+ Misc.join(listTronc, '<br>'));
}
}
/* -------------------------------------------- */
async updateCompetenceXP(idOrName, toXp) {
let competence = this.getCompetence(idOrName);
if (competence) {
if (isNaN(toXp) || typeof (toXp) != 'number') toXp = 0;
const fromXp = competence.system.xp;
this.checkCompetenceXP(idOrName, toXp);
await competence.update({ 'system.xp': toXp });
await ExperienceLog.add(this, XP_TOPIC.XP, fromXp, toXp, competence.name, true);
if (toXp > fromXp) {
RdDUtility.checkThanatosXP(idOrName);
}
}
}
/* -------------------------------------------- */
async updateCompetenceXPSort(idOrName, toXpSort) {
let competence = this.getCompetence(idOrName);
if (competence) {
if (isNaN(toXpSort) || typeof (toXpSort) != 'number') toXpSort = 0;
const fromXpSort = competence.system.xp_sort;
await competence.update({ 'system.xp_sort': toXpSort });
await ExperienceLog.add(this, XP_TOPIC.XPSORT, fromXpSort, toXpSort, competence.name, true);
if (toXpSort > fromXpSort) {
RdDUtility.checkThanatosXP(idOrName);
}
}
}
/* -------------------------------------------- */
async updateCompetenceArchetype(idOrName, compValue) {
let competence = this.getCompetence(idOrName)
if (competence) {
await competence.update({ 'system.niveau_archetype': Math.max(compValue ?? 0, 0) });
}
}
async deleteExperienceLog(from, count) {
if (from >= 0 && count > 0) {
let expLog = foundry.utils.duplicate(this.system.experiencelog);
expLog.splice(from, count);
await this.update({ [`system.experiencelog`]: expLog });
}
}
/* -------------------------------------------- */
async updateCompteurValue(fieldName, to) {
const from = this.system.compteurs[fieldName].value
await this.update({ [`system.compteurs.${fieldName}.value`]: to });
await this.addStressExperienceLog(fieldName, from, to, fieldName, true);
}
/* -------------------------------------------- */
async addCompteurValue(fieldName, add, raison) {
let from = this.system.compteurs[fieldName].value;
const to = Number(from) + Number(add);
await this.update({ [`system.compteurs.${fieldName}.value`]: to });
await this.addStressExperienceLog(fieldName, from, to, raison);
}
async addStressExperienceLog(topic, from, to, raison, manuel) {
switch (topic) {
case 'stress':
return await ExperienceLog.add(this, XP_TOPIC.STRESS, from, to, raison, manuel)
case 'experience':
return await ExperienceLog.add(this, XP_TOPIC.TRANSFORM, from, to, raison, manuel)
}
}
/* -------------------------------------------- */
async distribuerStress(compteur, stress, motif) {
if (game.user.isGM && this.hasPlayerOwner) {
switch (compteur) {
case 'stress': case 'experience':
await this.addCompteurValue(compteur, stress, motif);
const message = `${this.name} a reçu ${stress} points ${compteur == 'stress' ? "de stress" : "d'expérience"} (raison : ${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 });
}
/* -------------------------------------------- */
malusEthylisme() {
return Math.min(0, (this.system.compteurs.ethylisme?.value ?? 0));
}
/* -------------------------------------------- */
async actionRefoulement(item) {
const refoulement = item?.system.refoulement ?? 0;
if (refoulement > 0) {
RdDConfirm.confirmer({
settingConfirmer: "confirmation-refouler",
content: `<p>Prennez-vous le risque de refouler ${item.name} pour ${refoulement} points de refoulement ?</p>`,
title: 'Confirmer la refoulement',
buttonLabel: 'Refouler',
onAction: async () => {
await this.ajouterRefoulement(refoulement, `une queue ${item.name}`);
await item.delete();
}
});
}
}
/* -------------------------------------------- */
async ajouterRefoulement(value = 1, refouler) {
let refoulement = this.system.reve.refoulement.value + value;
const roll = new Roll("1d20");
await roll.evaluate();
await roll.toMessage({ flavor: `${this.name} refoule ${refouler} pour ${value} points de refoulement (total: ${refoulement})` });
if (roll.total <= refoulement) {
refoulement = 0;
await this.ajouterSouffle({ chat: true });
}
await this.update({ "system.reve.refoulement.value": refoulement });
return roll;
}
/* -------------------------------------------- */
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(this.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(this.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(this.name)
});
await this.forcerPositionTMRInconnue(tmr);
return tmr;
}
async forcerPositionTMRInconnue(tmr) {
await this.setTMRVisible(false);
await this.updateCoordTMR(tmr.coord);
this.notifyRefreshTMR();
}
/* -------------------------------------------- */
buildTMRInnaccessible() {
return this.items.filter(it => it.type == TYPES.casetmr).filter(it => EffetsDraconiques.isInnaccessible(it)).map(it => it.system.coord)
}
/* -------------------------------------------- */
getRencontresTMR() {
return this.itemTypes[TYPES.rencontre];
}
/* -------------------------------------------- */
async deleteRencontreTMRAtPosition() {
const rencontreIds = this.itemTypes[TYPES.rencontre].filter(this.filterRencontreTMRDemiReve()).map(it => it.id)
if (rencontreIds.length > 0) {
await this.deleteEmbeddedDocuments('Item', rencontreIds)
}
}
getRencontreTMREnAttente() {
return this.itemTypes[TYPES.rencontre].find(this.filterRencontreTMRDemiReve())
}
filterRencontreTMRDemiReve() {
const position = this.getDemiReve()
return it => it.system.coord == position
}
/* -------------------------------------------- */
async addTMRRencontre(currentRencontre) {
const toCreate = currentRencontre.toObject();
console.log('actor.addTMRRencontre(', toCreate, ')');
this.createEmbeddedDocuments('Item', [toCreate]);
}
/* -------------------------------------------- */
async updateCoordTMR(coord) {
await this.update({ "system.reve.tmrpos.coord": coord });
}
/* -------------------------------------------- */
async reveActuelIncDec(value) {
const reve = Math.max(this.system.reve.reve.value + value, 0);
await this.update({ "system.reve.reve.value": reve });
}
/* -------------------------------------------- */
async regainPointDeSeuil() {
const seuil = Misc.toInt(this.system.reve.seuil.value);
const seuilMax = Misc.toInt(this.system.carac.reve.value)
+ 2 * EffetsDraconiques.countAugmentationSeuil(this);
if (seuil < seuilMax) {
await this.setPointsDeSeuil(Math.min(seuil + 1, seuilMax));
}
}
/* -------------------------------------------- */
async setPointsDeSeuil(seuil) {
await this.update({ "system.reve.seuil.value": seuil });
}
/* -------------------------------------------- */
async setPointsDeChance(chance) {
await this.updateCompteurValue("chance", chance);
}
async jetEndurance(resteEndurance = undefined) {
const result = super.jetEndurance(resteEndurance);
if (result.jetEndurance == 1) {
ChatMessage.create({ content: await this._gainXpConstitutionJetEndurance() });
}
return result;
}
/* -------------------------------------------- */
getSConst() {
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 });
}
async _gainXpConstitutionJetEndurance() {
await this.ajoutXpConstitution(1); // +1 XP !
return `${this.name} a obtenu 1 sur son Jet d'Endurance et a gagné 1 point d'Expérience en Constitution. Ce point d'XP a été ajouté automatiquement.`;
}
/* -------------------------------------------- */
async jetDeMoral(situation, messageReussi = undefined, messageManque = undefined) {
const jetMoral = await this._jetDeMoral(situation);
const finMessage = (jetMoral.succes ? messageReussi : messageManque) ?? (jetMoral.ajustement == 0 ? "Vous gardez votre moral" : jetMoral.ajustement > 0 ? "Vous gagnez du moral" : "Vous perdez du moral");
ChatMessage.create({
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: `${finMessage} - jet ${jetMoral.succes ? "réussi" : "manqué"} en situation ${situation} (${jetMoral.jet}/${jetMoral.difficulte}).`
});
return jetMoral.ajustement;
}
async _jetDeMoral(situation) {
const moralActuel = Misc.toInt(this.system.compteurs.moral.value);
const jet = await RdDDice.rollTotal("1d20");
const difficulte = 10 + moralActuel;
const succes = jet <= difficulte;
const jetMoral = {
actuel: moralActuel,
jet: jet,
situation: situation,
difficulte: difficulte,
succes: succes,
ajustement: this._calculAjustementMoral(succes, moralActuel, situation)
};
await this.moralIncDec(jetMoral.ajustement);
return jetMoral;
}
/* -------------------------------------------- */
async moralIncDec(ajustementMoral) {
if (ajustementMoral != 0) {
const startMoral = parseInt(this.system.compteurs.moral.value)
const moralTheorique = startMoral + ajustementMoral
if (moralTheorique > 3) { // exaltation
const ajoutExaltation = moralTheorique - 3
const exaltation = parseInt(this.system.compteurs.exaltation.value) + ajoutExaltation
await this.updateCompteurValue('exaltation', exaltation)
}
if (moralTheorique < -3) { // dissolution
const ajoutDissolution = -3 - moralTheorique
const dissolution = parseInt(this.system.compteurs.dissolution.value) + ajoutDissolution
await this.updateCompteurValue('dissolution', dissolution)
}
await this.updateCompteurValue('moral', Math.max(-3, Math.min(moralTheorique, 3)));
}
return this.system.compteurs.moral.value;
}
/* -------------------------------------------- */
_calculAjustementMoral(succes, moral, situation) {
switch (situation) {
case 'heureux': case 'heureuse': return succes ? 1 : 0;
case 'malheureuse': case 'malheureux': return succes ? 0 : -1;
case 'neutre':
if (succes && moral < 0) return 1;
if (!succes && moral > 0) return -1;
}
return 0;
}
/* -------------------------------------------- */
async jetEthylisme() {
let rollData = {
vie: this.system.sante.vie.max,
forceAlcool: 0,
etat: this.getEtatGeneral({ ethylisme: true }),
diffNbDoses: -Number(this.system.compteurs.ethylisme.nb_doses || 0),
finalLevel: 0,
diffConditions: 0,
ajustementsForce: CONFIG.RDD.difficultesLibres
}
let html = await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/dialog-roll-ethylisme.html', rollData);
new RdDRollDialogEthylisme(html, rollData, this, r => this.saouler(r.forceAlcool)).render(true);
}
async actionPrincipale(item, onActionItem = async () => { }) {
let result = await super.actionPrincipale(item, onActionItem)
if (result) { return result }
result = await this.actionNourritureboisson(item, onActionItem)
if (result) { return result }
switch (item.type) {
case TYPES.potion: return await this.consommerPotion(item, onActionItem);
case TYPES.livre: return await this.actionLire(item);
case TYPES.conteneur: return await item.sheet.render(true);
case TYPES.herbe: return await this.actionHerbe(item, onActionItem);
case TYPES.queue: case TYPES.ombre: return await this.actionRefoulement(item);
}
return undefined
}
async actionNourritureboisson(item, onActionItem) {
switch (item.getUtilisationCuisine()) {
case 'brut': {
const utilisation = new Dialog({
title: "Nourriture brute",
content: `Que faire de votre ${item.name}`,
buttons: {
'cuisiner': { icon: '<i class="fa-solid fa-utensils"></i>', label: 'Cuisiner', callback: async () => await this.preparerNourriture(item) },
'manger': { icon: '<i class="fa-solid fa-drumstick-bite"></i>', label: 'Manger cru', callback: async () => await this.mangerNourriture(item, onActionItem) }
}
});
return utilisation.render(true);
}
case 'pret':
return await this.mangerNourriture(item, onActionItem);
}
return undefined;
}
async mangerNourriture(item, onActionItem) {
return (await DialogConsommer.create(this, item, onActionItem)).render(true);
}
async actionLire(item) {
const tache = await this.creerTacheDepuisLivre(item, { renderSheet: false });
if (tache) {
await this.rollTache(tache.id);
}
}
async actionHerbe(item, onActionItem = async () => { }) {
if (item.isHerbeAPotion()) {
return DialogFabriquerPotion.create(this, item, onActionItem);
}
return;
}
/* -------------------------------------------- */
async consommer(item, choix) {
switch (item.type) {
case 'nourritureboisson':
case 'herbe': case 'faune':
return await this.consommerNourritureboisson(item.id, choix);
case 'potion':
return await this.consommerPotion(item)
}
}
/* -------------------------------------------- */
async consommerNourritureboisson(itemId, choix = { doses: 1, seForcer: false, supprimerSiZero: false }, userId = undefined) {
if (userId != undefined && userId != game.user.id) {
RdDBaseActor.remoteActorCall({
tokenId: this.token?.id,
actorId: this.id,
method: 'consommerNourritureboisson',
args: [itemId, choix, userId]
},
userId)
return;
}
const item = this.getItem(itemId)
if (!item.getUtilisationCuisine()) {
return;
}
if (choix.doses > item.system.quantite) {
ui.notifications.warn(`Il n'y a pas assez de ${item.name} pour manger ${choix.doses}`)
return;
}
if (!this._apprecierCuisine(item, choix.seForcer)) {
ui.notifications.info(`${this.name} ne n'arrive pas à manger de ${item.name}`)
return;
}
await this.manger(item, choix.doses, { diminuerQuantite: false });
await this.boire(item, choix.doses, { diminuerQuantite: false });
await item.diminuerQuantite(choix.doses, choix);
}
async _apprecierCuisine(item, seForcer) {
const surmonteExotisme = await this._surmonterExotisme(item, seForcer);
if (surmonteExotisme) {
await this.apprecier('gout', 'cuisine', item.system.qualite, item.system.boisson ? "apprécie la boisson" : "apprécie le plat");
}
else if (seForcer) {
await this.jetDeMoral('malheureux');
}
else {
return false;
}
return true;
}
/* -------------------------------------------- */
async _surmonterExotisme(item) {
const qualite = Math.min(item.system.qualite, 0)
const exotisme = item.system.exotisme
if (exotisme < 0 || qualite < 0) {
const competence = qualite > 0 ? 'cuisine' : undefined
const difficulte = Math.min(exotisme, qualite)
const rolled = await this.doRollCaracCompetence('volonte', competence, difficulte, { title: `tente de surmonter l'exotisme de ${item.name}` })
return rolled.isSuccess
}
return true;
}
/* -------------------------------------------- */
async apprecier(carac, compName, qualite, title) {
const competence = this.getCompetence(compName);
const minQualite = Math.max(1, competence?.system.niveau ?? 0);
if (qualite <= minQualite) {
ui.notifications.info(`${this.name} a un niveau ${competence.system.niveau} en ${competence.name}, trop élevé pour apprécier la qualité de ${qualite}`)
return;
}
const rolled = await this.doRollCaracCompetence(carac, undefined, 0, { title });
if (rolled?.isSuccess) {
await this.jetDeMoral('heureux');
}
}
/* -------------------------------------------- */
async manger(item, doses, options = { diminuerQuantite: true }) {
const sust = item.system.sust
if (sust > 0) {
await this.updateCompteurValue('sust', RdDActor.$calculNewSust(this.system.compteurs.sust.value, sust, doses));
}
await item.diminuerQuantite(doses, options);
}
/* -------------------------------------------- */
async boire(item, doses, options = { diminuerQuantite: true }) {
const desaltere = item.system.desaltere;
if (desaltere > 0) {
await this.updateCompteurValue('eau', RdDActor.$calculNewSust(this.system.compteurs.eau.value, desaltere, doses));
}
if (item.isAlcool()) {
for (let i = 0; i < doses; i++) {
await this.saouler(item.system.force, item);
}
}
await item.diminuerQuantite(doses, options);
}
static $calculNewSust(value, sust, doses) {
return Misc.keepDecimals(Number(value) + Number(sust) * Number(doses), 1);
}
/* -------------------------------------------- */
async saouler(forceAlcool, alcool = undefined) {
let ethylisme = foundry.utils.duplicate(this.system.compteurs.ethylisme);
const etat = this.getEtatGeneral({ ethylisme: true });
const nbDoses = Number(this.system.compteurs.ethylisme.nb_doses || 0);
const ethylismeData = {
alias: this.name,
actor: this,
vie: this.system.sante.vie.max,
alcool: alcool,
jetVie: {
forceAlcool: forceAlcool,
nbDoses: nbDoses,
selectedCarac: this.system.sante.vie,
jetResistance: 'ethylisme',
carac: this.system.carac,
caracValue: this.system.sante.vie.max,
finalLevel: etat + forceAlcool - nbDoses
},
}
await RdDResolutionTable.rollData(ethylismeData.jetVie);
this._gererExperience(ethylismeData.jetVie);
RollDataAjustements.calcul(ethylismeData.jetVie, this);
if (ethylismeData.jetVie.rolled.isSuccess) {
ethylisme.nb_doses++;
} else {
ethylisme.value = Math.max(ethylisme.value - 1, -7);
ethylisme.nb_doses = 0;
let perte = await RdDDice.rollTotal("1d6");
ethylismeData.perteEndurance = await this.santeIncDec("endurance", -perte);
if (!ethylisme.jet_moral) {
ethylismeData.jetMoral = await this._jetDeMoral('heureuse');
if (ethylismeData.jetMoral.ajustement == 1) {
ethylismeData.moralAlcool = 'heureux';
ethylisme.jet_moral = true;
} else if (ethylisme.value == -1) {
ethylismeData.jetMoral.ajustement = -1;
ethylismeData.moralAlcool = 'triste';
ethylisme.jet_moral = true;
await this.moralIncDec(-1);
}
}
if (ethylisme.value < 0) {
// Qui a bu boira (p 164)
ethylismeData.jetVolonte = {
selectedCarac: this.system.carac.volonte,
caracValue: this.system.carac.volonte.value,
ethylisme: Number(ethylisme.value),
finalLevel: Number(ethylisme.value) + Number(this.system.compteurs.moral.value)
}
await RdDResolutionTable.rollData(ethylismeData.jetVolonte);
this._gererExperience(ethylismeData.jetVolonte);
RollDataAjustements.calcul(ethylismeData.jetVolonte, this);
}
}
ethylismeData.ajustementEthylique = ethylisme.value;
ethylismeData.nomEthylisme = RdDUtility.getNomEthylisme(ethylisme.value);
ethylismeData.doses = ethylisme.nb_doses;
await this.update({ 'system.compteurs.ethylisme': ethylisme });
await RdDResolutionTable.displayRollData(ethylismeData, this, 'chat-resultat-ethylisme.html');
}
/* -------------------------------------------- */
async jetGoutCuisine() {
console.info('Jet de Gout/Cuisine');
return true;
}
/* -------------------------------------------- */
async transformerStress() {
if (!ReglesOptionnelles.isUsing("transformation-stress")) { return }
const fromStress = Number(this.system.compteurs.stress.value);
if (this.system.sommeil?.insomnie || fromStress <= 0) {
return;
}
const stressRoll = await this._stressRoll(this.getReveActuel());
const conversion = Math.floor(fromStress * stressRoll.factor / 100);
let dissolution = Math.max(0, Number(this.system.compteurs.dissolution.value));
let exaltation = Math.max(0, Number(this.system.compteurs.exaltation.value));
const annule = Math.min(dissolution, exaltation);
dissolution -= annule;
exaltation -= annule;
const perteDissolution = Math.max(0, Math.min(dissolution, conversion));
let stressRollData = {
alias: this.name,
selectedCarac: this.system.carac.reve,
rolled: stressRoll,
stress: fromStress,
perte: Math.min(conversion, fromStress),
convertis: conversion - perteDissolution,
xp: conversion - perteDissolution + exaltation,
dissolution: dissolution,
exaltation: exaltation
};
ChatMessage.create({
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: await renderTemplate(`systems/foundryvtt-reve-de-dragon/templates/chat-resultat-transformer-stress.html`, stressRollData)
});
const toStress = Math.max(fromStress - stressRollData.perte - 1, 0);
const fromXpSress = Number(this.system.compteurs.experience.value);
const toXpStress = fromXpSress + Number(stressRollData.xp);
const updates = {
"system.compteurs.stress.value": toStress,
"system.compteurs.experience.value": toXpStress,
"system.compteurs.dissolution.value": dissolution - perteDissolution,
"system.compteurs.exaltation.value": 0
}
await this.update(updates);
await ExperienceLog.add(this, XP_TOPIC.STRESS, fromStress, toStress, 'Transformation')
await ExperienceLog.add(this, XP_TOPIC.TRANSFORM, fromXpSress, toXpStress, 'Transformation')
}
/* -------------------------------------------- */
async _stressRoll(reveActuel) {
let result = await RdDResolutionTable.roll(reveActuel, 0);
if (result.isPart) {
result.second = await RdDResolutionTable.roll(reveActuel, 0);
}
result.factor = this._getFacteurStress(result);
return result;
}
/* -------------------------------------------- */
_getFacteurStress(stressRoll) {
switch (stressRoll.code) {
case "sign": return 75;
case "norm": return 50;
case "echec": return 20;
case "epart": return 10;
case "etotal": return 0;
case "part":
if (stressRoll.second.isSign) {
stressRoll.quality = "Double Particulière";
return 150;
}
return 100;
}
return 0;
}
/* -------------------------------------------- */
createCallbackExperience() {
return {
condition: r => r.rolled.isPart && r.finalLevel < 0 && game.settings.get("core", "rollMode") != 'selfroll',
action: r => this.appliquerAjoutExperience(r)
};
}
/* -------------------------------------------- */
createCallbackAppelAuMoral() { /* Si l'appel au moral est utilisé, on l'affiche dans le chat et on diminue éventuellement le moral */
return {
condition: r => r.use.appelAuMoral && game.settings.get("core", "rollMode") != 'selfroll',
action: r => this._appliquerAppelMoral(r)
};
}
/* -------------------------------------------- */
isCaracMax(code) {
if (code == 'force' && parseInt(this.system.carac.force.value) >= parseInt(this.system.carac.taille.value) + 4) {
return true;
}
return false
}
async checkCaracXP(caracName, display = true) {
let carac = this.findCaracByName(caracName);
if (carac && carac.xp > 0) {
const niveauSuivant = Number(carac.value) + 1;
let xpNeeded = RdDCarac.getCaracNextXp(niveauSuivant);
if (carac.xp >= xpNeeded) {
carac = foundry.utils.duplicate(carac);
carac.value = niveauSuivant;
let checkXp = {
alias: this.name,
carac: caracName,
value: niveauSuivant,
xp: carac.xp
}
if (display) {
ChatMessage.create({
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: await renderTemplate(`systems/foundryvtt-reve-de-dragon/templates/chat-actor-carac-xp.html`, checkXp)
});
}
return checkXp;
}
}
}
/* -------------------------------------------- */
async checkCompetenceXP(compName, newXP, display = true) {
let compData = this.getCompetence(compName);
if (compData && newXP && newXP == compData.system.xp) { // Si édition, mais sans changement XP
return;
}
newXP = (newXP) ? newXP : compData.system.xp;
if (compData && newXP > 0) {
let xpNeeded = RdDItemCompetence.getCompetenceNextXp(compData.system.niveau + 1);
if (newXP >= xpNeeded) {
let newCompData = foundry.utils.duplicate(compData);
newCompData.system.niveau += 1;
newCompData.system.xp = newXP;
let checkXp = {
alias: this.name,
competence: newCompData.name,
niveau: newCompData.system.niveau,
xp: newCompData.system.xp,
archetype: newCompData.system.niveau_archetype,
archetypeWarning: newCompData.system.niveau > compData.system.niveau_archetype
}
if (display) {
ChatMessage.create({
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: await renderTemplate(`systems/foundryvtt-reve-de-dragon/templates/chat-actor-competence-xp.html`, checkXp)
});
}
return checkXp;
}
}
}
/* -------------------------------------------- */
async appliquerAjoutExperience(rollData, hideChatMessage = 'show') {
if (!Misc.firstConnectedGM()){
return
}
hideChatMessage = hideChatMessage == 'hide' || (Misc.isRollModeHiddenToPlayer() && !game.user.isGM)
let xpData = await this._appliquerExperience(rollData.rolled, rollData.selectedCarac.label, rollData.competence, rollData.jetResistance);
if (xpData.length) {
const content = await renderTemplate(`systems/foundryvtt-reve-de-dragon/templates/chat-actor-gain-xp.html`, {
actor: this,
xpData
})
if (hideChatMessage) {
ChatUtility.blindMessageToGM({ content: content });
}
else {
ChatMessage.create({
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: content
});
}
}
}
/* -------------------------------------------- */
async _appliquerAppelMoral(rollData) {
if (!rollData.use.moral) return;
if (rollData.rolled.isEchec ||
(rollData.ajustements.diviseurSignificative && (rollData.rolled.roll * rollData.ajustements.diviseurSignificative > rollData.score))) {
rollData.perteMoralEchec = rollData.moral <= -3 ? 'dissolution' : 'perte';
rollData.moral = await this.moralIncDec(-1); /* L'appel au moral a échoué. Le personnage perd un point de moral */
}
}
/* -------------------------------------------- */
$filterSortList(sortList, coord) {
let tmr = TMRUtility.getTMR(coord);
let filtered = []
for (let sort of sortList) {
if (sort.system.caseTMR.toLowerCase().includes('variable')) {
filtered.push(sort);
} else if (sort.system.caseTMRspeciale.toLowerCase().includes('variable')) {
filtered.push(sort);
} else if (sort.system.caseTMR.toLowerCase() == tmr.type) {
filtered.push(sort);
} else if (sort.system.caseTMR.toLowerCase().includes('special') && sort.system.caseTMRspeciale.toLowerCase().includes(coord.toLowerCase())) {
filtered.push(sort);
}
}
return filtered;
}
/* -------------------------------------------- */
computeDraconicAndSortIndex(sortList) {
let draconicList = this.getDraconicList();
for (let sort of sortList) {
let draconicsSort = this.getDraconicsSort(draconicList, sort).map(it => it.name);
for (let index = 0; index < draconicList.length && sort.system.listIndex == undefined; index++) {
if (draconicsSort.includes(draconicList[index].name)) {
sort.system.listIndex = index;
}
}
}
return draconicList;
}
/* -------------------------------------------- */
getDraconicsSort(draconicList, sort) {
//console.log(draconicList, bestDraconic, draconic, voie);
switch (Grammar.toLowerCaseNoAccent(sort.name)) {
case "lecture d'aura":
case "detection d'aura":
return draconicList;
case "annulation de magie":
return draconicList.filter(it => !RdDItemCompetence.isThanatos(it));
}
return [RdDItemCompetence.getVoieDraconic(draconicList, sort.system.draconic)];
}
/* -------------------------------------------- */
async rollUnSort(coord) {
RdDEmpoignade.checkEmpoignadeEnCours(this)
if (EffetsDraconiques.isSortImpossible(this)) {
ui.notifications.error("Une queue ou un souffle vous empèche de lancer de sort!");
return;
}
// Duplication car les pts de reve sont modifiés dans le sort
let sorts = foundry.utils.duplicate(this.$filterSortList(this.itemTypes['sort'], coord));
if (sorts.length == 0) {
ui.notifications.info(`Aucun sort disponible en ${TMRUtility.getTMR(coord).label} !`);
return;
}
const draconicList = this.computeDraconicAndSortIndex(sorts);
const reve = foundry.utils.duplicate(this.system.carac.reve);
const dialog = await this.openRollDialog({
name: 'lancer-un-sort',
label: 'Lancer un sort',
template: 'systems/foundryvtt-reve-de-dragon/templates/dialog-roll-sort.html',
rollData: {
carac: { 'reve': reve },
forceCarac: { 'reve': reve },
selectedCarac: reve,
draconicList: draconicList,
competence: draconicList[0],
sortList: sorts,
selectedSort: sorts[0],
tmr: TMRUtility.getTMR(coord),
diffLibre: RdDItemSort.getDifficulte(sorts[0], -7), // Per default at startup
coutreve: Array(30).fill().map((item, index) => 1 + index),
},
callbackAction: async r => {
await this._rollUnSortResult(r);
if (!r.isSortReserve) this.tmrApp?.close();
}
});
this.tmrApp?.setTMRPendingAction(dialog);
}
/* -------------------------------------------- */
isMauvaiseRencontre() { // Gestion queue/souffle 'Mauvaise Rencontre en Perpective'
let addMsg = "";
let rencSpecial = EffetsDraconiques.mauvaiseRencontre(this);
if (rencSpecial) {
if (rencSpecial.type != 'souffle') {
this.deleteEmbeddedDocuments('Item', [rencSpecial.id]); // Suppression dans la liste des queues
addMsg = " La queue a été supprimée de votre fiche automatiquement";
} else {
addMsg = " Vous devez gérer manuellement le décompte de mauvaises rencontres.";
}
ChatMessage.create({
content: "Vous êtes sous le coup d'une Mauvaise Rencontre en Persective." + addMsg,
whisper: ChatMessage.getWhisperRecipients(this.name)
});
}
return rencSpecial;
}
/* -------------------------------------------- */
getCoutFatigueTMR() { // Pour l'instant uniquement Inertie Draconique
let countInertieDraconique = EffetsDraconiques.countInertieDraconique(this);
if (countInertieDraconique > 0) {
ChatMessage.create({
content: `Vous êtes sous le coup d'Inertie Draconique : vous perdrez ${countInertieDraconique + 1} cases de Fatigue par déplacement au lieu d'une.`,
whisper: ChatMessage.getWhisperRecipients(this.name)
});
}
return countInertieDraconique + 1;
}
/* -------------------------------------------- */
async checkSoufflePeage(tmr) {
if ((tmr.type == 'pont' || tmr.type == 'cite') && EffetsDraconiques.isPeage(this)) {
await this.reveActuelIncDec(-1);
ChatMessage.create({
content: "Vous êtes sous le coup d'un Péage : l'entrée sur cette case vous a coûté 1 Point de Rêve (déduit automatiquement).",
whisper: ChatMessage.getWhisperRecipients(this.name)
});
}
}
/* -------------------------------------------- */
async _rollUnSortResult(rollData) {
let rolled = rollData.rolled;
let selectedSort = rollData.selectedSort;
rollData.isSortReserve = rollData.mettreEnReserve && !selectedSort.system.isrituel;
rollData.show = {}
rollData.depenseReve = Number(selectedSort.system.ptreve_reel);
if (rollData.competence.name.includes('Thanatos')) { // Si Thanatos
await this.update({ "system.reve.reve.thanatosused": true });
}
let reveActuel = parseInt(this.system.reve.reve.value)
if (rolled.isSuccess) { // Réussite du sort !
if (rolled.isPart) {
rollData.depenseReve = Math.max(Math.floor(rollData.depenseReve / 2), 1);
}
if (rollData.isSortReserve) {
rollData.depenseReve++;
}
if (reveActuel > rollData.depenseReve) {
// Incrémenter/gére le bonus de case
RdDItemSort.incrementBonusCase(this, selectedSort, rollData.tmr.coord);
if (rollData.isSortReserve) {
await this.sortMisEnReserve(selectedSort, rollData.competence, rollData.tmr.coord, Number(selectedSort.system.ptreve_reel));
}
}
else {
rollData.depenseReve = 0;
rollData.show.reveInsuffisant = true;
foundry.utils.mergeObject(rollData.rolled, RdDResolutionTable.getResultat("echec"), { overwrite: true });
}
} else {
if (rolled.isETotal) { // Echec total !
rollData.depenseReve = Math.min(reveActuel, Math.floor(rollData.depenseReve * 1.5))
// TODO: mise en réserve d'un échec total...
// await dialog mse en réserve, pour traitement échec total
} else {
rollData.depenseReve = 0
}
}
reveActuel = Math.max(reveActuel - rollData.depenseReve, 0);
await this.update({ "system.reve.reve.value": reveActuel });
// Final chat message
await RdDResolutionTable.displayRollData(rollData, this, 'chat-resultat-sort.html');
if (reveActuel == 0) { // 0 points de reve
ChatMessage.create({ content: this.name + " est réduit à 0 Points de Rêve, et tombe endormi !" });
}
}
/**
* Méthode pour faire un jet prédéterminer sans ouvrir la fenêtre de dialogue
* @param {*} caracName
* @param {*} compName
* @param {*} diff
* @param {*} options
* @returns
*/
async doRollCaracCompetence(caracName, compName, diff, options = { title: "" }) {
const carac = this.getCaracByName(caracName);
if (!carac) {
ui.notifications.warn(`${this.name} n'a pas de caractéristique correspondant à ${caracName}`)
return;
}
const competence = this.getCompetence(compName);
let rollData = {
alias: this.name,
caracValue: Number(carac.value),
selectedCarac: carac,
competence: competence,
diffLibre: diff,
show: { title: options?.title ?? '' }
};
RollDataAjustements.calcul(rollData, this);
await RdDResolutionTable.rollData(rollData);
this._gererExperience(rollData);
await RdDResolutionTable.displayRollData(rollData, this)
return rollData.rolled;
}
/* -------------------------------------------- */
_gererExperience(rollData) {
const callback = this.createCallbackExperience();
if (callback.condition(rollData)) {
callback.action(rollData);
}
}
/* -------------------------------------------- */
async creerTacheDepuisLivre(item, options = { renderSheet: true }) {
const nomTache = "Lire " + item.name;
const filterTacheLecture = it => it.type == 'tache' && it.name == nomTache;
let tachesExistantes = this.filterItems(filterTacheLecture);
if (tachesExistantes.length == 0) {
const tache = {
name: nomTache, type: 'tache',
system: {
carac: 'intellect',
competence: 'Ecriture',
difficulte: item.system.difficulte,
periodicite: "60 minutes",
fatigue: 2,
points_de_tache: item.system.points_de_tache,
points_de_tache_courant: 0,
description: "Lecture du livre " + item.name + " - XP : " + item.system.xp + " - Compétences : " + item.system.competence
}
}
await this.createEmbeddedDocuments('Item', [tache], options);
tachesExistantes = this.filterItems(filterTacheLecture);
}
return tachesExistantes.length > 0 ? tachesExistantes[0] : undefined;
}
blessuresASoigner() {
return (this.itemTypes[TYPES.blessure])
.filter(it => it.system.gravite > 0 && it.system.gravite <= 6)
.filter(it => !(it.system.premierssoins.done && it.system.soinscomplets.done))
.sort(Misc.descending(b => (b.system.premierssoins.done ? "A" : "B") + b.system.gravite))
}
async getTacheBlessure(blesse, blessure) {
const gravite = blessure?.system.gravite ?? 0;
if (gravite > 0) {
const tache = this.itemTypes['tache'].find(it => it.system.itemId == blessure.id)
?? await RdDItemBlessure.createTacheSoinBlessure(this, gravite);
await blessure?.updateTacheSoinBlessure(tache);
return tache
}
return undefined;
}
async rollCaracCompetence(caracName, compName, diff, options = { title: "" }) {
RdDEmpoignade.checkEmpoignadeEnCours(this)
const competence = this.getCompetence(compName);
await this.openRollDialog({
name: 'jet-competence',
label: 'Jet ' + Grammar.apostrophe('de', competence.name),
template: 'systems/foundryvtt-reve-de-dragon/templates/dialog-roll-competence.html',
rollData: {
alias: this.name,
carac: this.system.carac,
selectedCarac: this.getCaracByName(caracName),
selectedCaracName: caracName,
diffLibre: diff,
competence: competence,
show: { title: options?.title ?? '' }
},
callbackAction: r => this.$onRollCompetence(r, options)
});
}
/* -------------------------------------------- */
async rollTache(id, options = {}) {
RdDEmpoignade.checkEmpoignadeEnCours(this)
const tacheData = this.getTache(id)
const compData = this.getCompetence(tacheData.system.competence)
compData.system.defaut_carac = tacheData.system.carac; // Patch !
await this.openRollDialog({
name: 'jet-competence',
label: 'Jet de Tâche ' + tacheData.name,
template: 'systems/foundryvtt-reve-de-dragon/templates/dialog-roll-competence.html',
rollData: {
competence: compData,
tache: tacheData,
diffLibre: tacheData.system.difficulte,
diffConditions: 0,
use: { libre: false, conditions: true },
carac: {
[tacheData.system.carac]: foundry.utils.duplicate(this.system.carac[tacheData.system.carac])
}
},
callbackAction: r => this._tacheResult(r, options)
});
}
/* -------------------------------------------- */
async _tacheResult(rollData, options) {
// Mise à jour de la tache
rollData.appliquerFatigue = ReglesOptionnelles.isUsing("appliquer-fatigue");
rollData.tache = foundry.utils.duplicate(rollData.tache);
rollData.tache.system.points_de_tache_courant += rollData.rolled.ptTache;
if (rollData.rolled.isETotal) {
rollData.tache.system.difficulte--;
}
if (rollData.rolled.isSuccess) {
rollData.tache.system.nb_jet_succes++;
} else {
rollData.tache.system.nb_jet_echec++;
}
rollData.tache.system.tentatives = rollData.tache.system.nb_jet_succes + rollData.tache.system.nb_jet_echec;
this.updateEmbeddedDocuments('Item', [rollData.tache]);
this.santeIncDec("fatigue", rollData.tache.system.fatigue);
await RdDResolutionTable.displayRollData(rollData, this, 'chat-resultat-tache.html');
if (options?.onRollAutomate) {
options.onRollAutomate(rollData);
}
}
/* -------------------------------------------- */
async _rollArt(artData, selected, oeuvre, callbackAction = r => this._resultArt(r)) {
oeuvre.system.niveau = oeuvre.system.niveau ?? 0;
foundry.utils.mergeObject(artData,
{
oeuvre: oeuvre,
art: oeuvre.type,
competence: foundry.utils.duplicate(this.getCompetence(artData.compName ?? oeuvre.system.competence ?? artData.art)),
diffLibre: - oeuvre.system.niveau,
diffConditions: 0,
use: { libre: false, conditions: true, surenc: false },
selectedCarac: foundry.utils.duplicate(this.system.carac[selected])
},
{ overwrite: false });
artData.competence.system.defaut_carac = selected;
if (!artData.forceCarac) {
artData.forceCarac = {};
artData.forceCarac[selected] = foundry.utils.duplicate(this.system.carac[selected]);
}
await this.openRollDialog({
name: `jet-${artData.art}`,
label: `${artData.verbe} ${oeuvre.name}`,
template: `systems/foundryvtt-reve-de-dragon/templates/dialog-roll-${oeuvre.type}.html`,
rollData: artData,
callbackAction: callbackAction
});
}
/* -------------------------------------------- */
async _resultArt(artData) {
const niveau = artData.oeuvre.system.niveau ?? 0;
const baseQualite = (artData.rolled.isSuccess ? niveau : artData.competence.system.niveau);
artData.qualiteFinale = Math.min(baseQualite, niveau) + artData.rolled.ptQualite;
await RdDResolutionTable.displayRollData(artData, this.name, `chat-resultat-${artData.art}.html`);
}
/* -------------------------------------------- */
async rollChant(id) {
const artData = { art: 'chant', verbe: 'Chanter' };
const oeuvre = foundry.utils.duplicate(this.getChant(id));
await this._rollArt(artData, "ouie", oeuvre);
}
/* -------------------------------------------- */
async rollDanse(id) {
const artData = { art: 'danse', verbe: 'Danser', forceCarac: {} };
const oeuvre = foundry.utils.duplicate(this.findItemLike(id, artData.art));
if (oeuvre.system.agilite) {
artData.forceCarac['agilite'] = foundry.utils.duplicate(this.system.carac.agilite);
}
if (oeuvre.system.apparence) {
artData.forceCarac['apparence'] = foundry.utils.duplicate(this.system.carac.apparence);
}
const selectedCarac = this._getCaracDanse(oeuvre);
await this._rollArt(artData, selectedCarac, oeuvre);
}
/* -------------------------------------------- */
_getCaracDanse(oeuvre) {
if (oeuvre.system.agilite) { return "agilite"; }
else if (oeuvre.system.apparence) { return "apparence"; }
const compData = this.getCompetence(oeuvre.system.competence);
return compData.system.defaut_carac;
}
/* -------------------------------------------- */
async rollMusique(id) {
const artData = { art: 'musique', verbe: 'Jouer' };
const oeuvre = this.findItemLike(id, artData.art);
await this._rollArt(artData, "ouie", oeuvre);
}
/* -------------------------------------------- */
async rollRecetteCuisine(id) {
const oeuvre = this.getRecetteCuisine(id);
const artData = {
verbe: 'Cuisiner',
compName: 'cuisine',
proportions: 1,
ajouterEquipement: false
};
await this._rollArt(artData, 'odoratgout', oeuvre, r => this._resultRecetteCuisine(r));
}
/* -------------------------------------------- */
async _resultRecetteCuisine(cuisine) {
const niveauRecette = cuisine.oeuvre.system.niveau ?? 0;
const baseQualite = (cuisine.rolled.isSuccess ? niveauRecette : cuisine.competence.system.niveau);
cuisine.qualiteFinale = Math.min(baseQualite, niveauRecette) + cuisine.rolled.ptQualite;
cuisine.exotismeFinal = Math.min(Math.min(cuisine.qualiteFinale, cuisine.oeuvre.system.exotisme ?? 0), 0);
cuisine.sust = cuisine.oeuvre.system.sust * Math.min(cuisine.proportions, cuisine.proportionsMax ?? cuisine.proportions)
const platCuisine = {
name: cuisine.oeuvre.name,
type: 'nourritureboisson',
img: 'systems/foundryvtt-reve-de-dragon/icons/objets/provision_cuite.webp',
system: {
"description": cuisine.oeuvre.system.description,
"sust": 1,
"qualite": cuisine.qualiteFinale,
"exotisme": cuisine.exotismeFinal,
"encombrement": 0.1,
"quantite": Math.max(1, Math.floor(cuisine.sust)),
"cout": Math.max(cuisine.qualiteFinale) * 0.01
}
}
if (cuisine.ajouterEquipement) {
await this.createEmbeddedDocuments('Item', [platCuisine]);
ui.notifications.info(`${platCuisine.system.quantite} rations de ${platCuisine.name} ont été ajoutés à votre équipement`);
}
cuisine.platCuisine = platCuisine;
await RdDResolutionTable.displayRollData(cuisine, this.name, `chat-resultat-${cuisine.art}.html`);
}
async preparerNourriture(item) {
if (item.getUtilisationCuisine() == 'brut') {
const nourriture = {
name: 'Plat de ' + item.name,
type: 'recettecuisine',
img: item.img,
system: {
sust: item.system.sust,
exotisme: item.system.exotisme,
ingredients: item.name
}
};
const artData = {
verbe: 'Préparer',
compName: 'cuisine',
proportions: 1,
proportionsMax: Math.min(50, item.system.quantite),
ajouterEquipement: true
};
await this._rollArt(artData, 'odoratgout', nourriture, async (cuisine) => {
await this._resultRecetteCuisine(cuisine);
const remaining = Math.max(item.system.quantite - cuisine.proportions, 0);
if (remaining > 0) {
await item.update({ 'system.quantite': remaining })
}
else {
await this.deleteEmbeddedDocuments('Item', [item.id]);
}
});
}
}
/* -------------------------------------------- */
async rollJeu(id) {
const oeuvre = this.getJeu(id);
const listCarac = oeuvre.system.caraccomp.toLowerCase().split(/[.,:\/-]/).map(it => it.trim());
const carac = listCarac.length > 0 ? listCarac[0] : 'chance'
const artData = {
art: 'jeu', verbe: 'Jeu',
use: { libre: true, conditions: true, },
competence: foundry.utils.duplicate(this.getCompetence('jeu')),
forceCarac: {}
};
listCarac.forEach(c => artData.forceCarac[c] = this.system.carac[c]);
artData.competence.system.niveauReel = artData.competence.system.niveau;
artData.competence.system.niveau = Math.max(artData.competence.system.niveau, oeuvre.system.base);
await this._rollArt(artData, carac, oeuvre);
}
async rollOeuvre(id) {
const artData = { art: 'oeuvre', verbe: 'Interpréter' }
const oeuvre = foundry.utils.duplicate(this.findItemLike(id, artData.art))
await this._rollArt(artData, oeuvre.system.default_carac, oeuvre)
}
/* -------------------------------------------- */
async rollMeditation(id) {
const meditation = foundry.utils.duplicate(this.getMeditation(id));
const competence = foundry.utils.duplicate(this.getCompetence(meditation.system.competence));
competence.system.defaut_carac = "intellect"; // Meditation = toujours avec intellect
let meditationData = {
competence: competence,
meditation: meditation,
conditionMeditation: { isHeure: false, isVeture: false, isComportement: false, isPurification: false },
diffConditions: 0,
use: { libre: false, conditions: true, },
carac: { "intellect": this.system.carac.intellect }
};
const dialog = await RdDRoll.create(this, meditationData,
{ html: 'systems/foundryvtt-reve-de-dragon/templates/dialog-roll-meditation.html' },
{
name: 'jet-meditation',
label: "Jet de méditation",
callbacks: [
this.createCallbackExperience(),
{ condition: r => r.rolled.isEPart, action: r => this._meditationEPart(r) },
{ action: r => this._meditationResult(r) }
]
});
dialog.render(true);
}
/* -------------------------------------------- */
async _meditationResult(meditationRoll) {
this.santeIncDec("fatigue", 2);
if (meditationRoll.rolled.isSuccess) {
await this.createEmbeddedDocuments("Item", [RdDItemSigneDraconique.prepareSigneDraconiqueMeditation(meditationRoll.meditation, meditationRoll.rolled)]);
}
await RdDResolutionTable.displayRollData(meditationRoll, this.name, 'chat-resultat-meditation.html');
}
/* -------------------------------------------- */
_meditationEPart(meditationRoll) {
this.updateEmbeddedDocuments('Item', [{ _id: meditationRoll.meditation._id, 'system.malus': meditationRoll.meditation.system.malus - 1 }]);
}
/* -------------------------------------------- */
_getSignesDraconiques(coord) {
const type = TMRUtility.getTMRType(coord);
return this.itemTypes["signedraconique"].filter(it => it.system.typesTMR.includes(type));
}
/* -------------------------------------------- */
isResonanceSigneDraconique(coord) {
return this._getSignesDraconiques(coord).length > 0;
}
/* -------------------------------------------- */
async rollLireSigneDraconique(coord) {
if (!this.isHautRevant()) {
ui.notifications.info("Seul un haut rêvant peut lire un signe draconique!");
return;
}
let signes = this._getSignesDraconiques(coord);
if (signes.length == 0) {
ui.notifications.info(`Aucun signe draconiques en ${coord} !`);
return;
}
let draconicList = this.getDraconicList()
.map(draconic => {
let draconicLecture = foundry.utils.duplicate(draconic);
draconicLecture.system.defaut_carac = "intellect";
return draconicLecture;
});
const intellect = this.system.carac.intellect;
let rollData = {
carac: { 'intellect': intellect },
selectedCarac: intellect,
competence: draconicList[0],
draconicList: draconicList,
signe: signes[0],
signes: signes,
tmr: TMRUtility.getTMR(coord),
diffLibre: signes[0].system.difficulte,
}
const dialog = await RdDRoll.create(this, rollData,
{
html: 'systems/foundryvtt-reve-de-dragon/templates/dialog-roll-signedraconique.html',
close: async html => await this._onCloseRollDialog(html)
},
{
name: 'lire-signe-draconique',
label: 'Lire le signe draconique',
callbacks: [
this.createCallbackExperience(),
{ action: r => this._rollLireSigneDraconique(r) }
]
}
);
dialog.render(true);
this.tmrApp?.setTMRPendingAction(dialog);
}
async _onCloseRollDialog(html) {
this.tmrApp?.restoreTMRAfterAction()
}
/* -------------------------------------------- */
async _rollLireSigneDraconique(rollData) {
const compData = rollData.competence;
if (!RdDItemCompetence.isDraconic(compData)) {
ui.notifications.error(`La compétence ${compData.name} n'est pas une compétence draconique`);
return;
}
rollData.xpSort = RdDItemSigneDraconique.getXpSortSigneDraconique(rollData.rolled.code, rollData.signe);
if (rollData.xpSort > 0) {
const fromXp = Number(compData.system.xp_sort);
const toXp = fromXp + rollData.xpSort;
await this.updateEmbeddedDocuments("Item", [{ _id: compData._id, 'system.xp_sort': toXp }]);
await ExperienceLog.add(this, XP_TOPIC.XPSORT, fromXp, toXp, `${rollData.competence.name} : signe draconique`);
}
await this.deleteEmbeddedDocuments("Item", [rollData.signe._id]);
await RdDResolutionTable.displayRollData(rollData, this.name, 'chat-resultat-lecture-signedraconique.html');
this.tmrApp.close();
}
/* -------------------------------------------- */
async rollAppelChance(onSuccess = () => { }, onEchec = () => { }) {
await this.openRollDialog({
name: 'appelChance',
label: 'Appel à la chance',
template: 'systems/foundryvtt-reve-de-dragon/templates/dialog-roll-carac.html',
rollData: { selectedCarac: this.getCaracByName('chance-actuelle'), surprise: '' },
callbackAction: r => this._appelChanceResult(r, onSuccess, onEchec)
});
}
/* -------------------------------------------- */
async _appelChanceResult(rollData, onSuccess, onEchec) {
await RdDResolutionTable.displayRollData(rollData, this, 'chat-resultat-appelchance.html')
if (rollData.rolled.isSuccess) {
await this.setFlag(SYSTEM_RDD, 'utilisationChance', true);
await this.chanceActuelleIncDec(-1);
onSuccess();
}
else {
onEchec();
}
}
/* -------------------------------------------- */
async chanceActuelleIncDec(value) {
const chance = Math.min(this.getChance(), Math.max(this.getChanceActuel() + value, 0));
await this.updateCompteurValue("chance", chance);
}
/* -------------------------------------------- */
async appelDestinee(onSuccess = () => { }, onEchec = () => { }) {
let destinee = this.system.compteurs.destinee?.value ?? 0;
if (destinee > 0) {
ChatMessage.create({ content: `<span class="rdd-roll-part">${this.name} a fait appel à la Destinée !</span>` });
destinee--;
await this.updateCompteurValue("destinee", destinee);
onSuccess();
}
else {
onEchec();
}
}
/* -------------------------------------------- */
getHeureNaissance() {
return this.system.heure ?? 0;
}
/* -------------------------------------------- */
ajustementAstrologique() {
// selon l'heure de naissance...
return game.system.rdd.calendrier.getAjustementAstrologique(this.system.heure, this.name)
}
/* -------------------------------------------- */
checkDesirLancinant() {
let queue = this.filterItems(it => it.type == 'queue' || it.type == 'ombre')
.filter(it => it.system.categorie == 'lancinant');
return (queue.length > 0);
}
/* -------------------------------------------- */
async _appliquerExperience(rolled, caracName, competence, jetResistance) {
// Pas d'XP
if (!rolled.isPart || rolled.finalLevel >= 0) {
return []
}
if (this.checkDesirLancinant()) {
// Cas de désir lancinant, pas d'expérience sur particulière
ChatMessage.create({
content: `Vous souffrez au moins d'un Désir Lancinant, vous ne pouvez pas gagner d'expérience sur une Particulière tant que le désir n'est pas assouvi`,
whisper: ChatMessage.getWhisperRecipients(this.name)
});
return []
}
let xp = Math.abs(rolled.finalLevel);
// impair: arrondi inférieur en carac
let xpCarac = competence ? Math.floor(xp / 2) : Math.max(Math.floor(xp / 2), 1);
const xpCompetence = competence ? xp - xpCarac : 0;
if (jetResistance) {
const message = `Jet de résistance ${jetResistance}, l'expérience est limitée à 1`;
ui.notifications.info(message);
console.log(message)
// max 1 xp sur jets de résistance
xpCarac = Math.min(1, xpCarac);
}
return [
...(await this._xpCompetence({ competence, xpCompetence })),
...(await this._xpCarac({ caracName, xpCarac }))
];
}
/* -------------------------------------------- */
async _xpCompetence(xpData) {
if (xpData.competence) {
const from = Number(xpData.competence.system.xp);
const to = from + xpData.xpCompetence;
await this.updateEmbeddedDocuments('Item', [{ _id: xpData.competence._id, 'system.xp': to }]);
xpData.checkComp = await this.checkCompetenceXP(xpData.competence.name, undefined, false);
await ExperienceLog.add(this, XP_TOPIC.XP, from, to, xpData.competence.name);
return [xpData]
}
return []
}
/* -------------------------------------------- */
async _xpCarac(xpData) {
if (xpData.xpCarac > 0) {
const carac = foundry.utils.duplicate(this.system.carac)
const code = RdDBaseActor._findCaracNode(carac, xpData.caracName)
const selectedCarac = carac[code]
if (this.isCaracMax(code)) {
ui.notifications.info(`Pas d'expérience: la caractéristique '${selectedCarac.label}' est déjà au maximum pour ${this.name}`)
return []
}
if (selectedCarac && !selectedCarac.derivee) {
const from = Number(selectedCarac.xp)
const to = from + xpData.xpCarac
selectedCarac.xp = to
await this.update({ "system.carac": carac })
xpData.checkCarac = await this.checkCaracXP(selectedCarac.label, false)
await ExperienceLog.add(this, XP_TOPIC.XPCARAC, from, to, xpData.caracName)
return [xpData]
} else {
return await this._xpCaracDerivee(xpData)
}
}
return []
}
async _xpCaracDerivee(xpData) {
const caracs = RdDActor._getComposantsCaracDerivee(xpData.caracName)
.map(c => foundry.utils.mergeObject(this.system.carac[c], { isMax: this.isCaracMax(c) }, { inplace: false }))
switch (caracs.filter(it => !it.isMax).length) {
case 0:
xpData.caracRepartitionManuelle = true;
return [xpData]
case 1:
xpData.caracName = caracs.find(it => !it.isMax).label
return this._xpCarac(xpData)
default:
await DialogChoixXpCarac.choix(this, xpData, caracs)
return []
}
}
static _getComposantsCaracDerivee(caracName) {
switch (Grammar.toLowerCaseNoAccent(caracName)) {
case 'reve-actuel': case 'reve actuel': return ['reve']
case 'chance-actuelle': case 'chance actuelle': return ['chance']
case 'vie': return ['constitution']
case 'tir': return ['vue', 'dexterite']
case 'lancer': return ['force', 'dexterite', 'vue']
case 'melee': return ['force', 'agilite']
case 'derobee': return ['agilite']
}
return []
}
/* -------------------------------------------- */
async deleteNombresAstraux() {
const deletions = this.itemTypes['nombreastral'].map(it => it._id);
await this.deleteEmbeddedDocuments("Item", deletions);
}
/* -------------------------------------------- */
async ajouteNombreAstral(date, nbAstral, isValid) {
const indexDate = Number.parseInt(date);
// Ajout du nombre astral
const item = {
name: "Nombre Astral", type: "nombreastral", system:
{
value: nbAstral,
istrue: isValid,
jourindex: indexDate,
jourlabel: RdDTimestamp.formatIndexDate(indexDate)
}
};
await this.createEmbeddedDocuments("Item", [item]);
}
async supprimerAnciensNombresAstraux() {
const calendrier = game.system.rdd.calendrier;
if (calendrier) {
const toDelete = this.itemTypes['nombreastral']
.filter(it => calendrier.isAfterIndexDate(it.system.jourindex))
.map(it => it._id);
await this.deleteEmbeddedDocuments("Item", toDelete);
}
}
/* -------------------------------------------- */
async astrologieNombresAstraux() {
await this.supprimerAnciensNombresAstraux();
await AppAstrologie.create(this);
}
/* -------------------------------------------- */
countMonteeLaborieuse() { // Return +1 par queue/ombre/souffle Montée Laborieuse présente
let countMonteeLaborieuse = EffetsDraconiques.countMonteeLaborieuse(this);
if (countMonteeLaborieuse > 0) {
ChatMessage.create({
content: `Vous êtes sous le coup d'une Montée Laborieuse : vos montées en TMR coûtent ${countMonteeLaborieuse} Point de Rêve de plus.`,
whisper: ChatMessage.getWhisperRecipients(this.name)
});
}
return countMonteeLaborieuse;
}
/* -------------------------------------------- */
refreshTMRView() {
if (this.tmrApp) {
this.tmrApp.externalRefresh();
}
}
/* -------------------------------------------- */
async displayTMR(mode = "normal") {
if (this.tmrApp) {
ui.notifications.warn("Vous êtes déja dans les TMR....")
this.tmrApp.forceTMRDisplay()
return
}
if (mode != 'visu' && this.getEffect(STATUSES.StatusDemiReve)) {
ui.notifications.warn("Les personnage est déjà dans les Terres Médianes, elles s'affichent en visualisation")
mode = "visu"; // bascule le mode en visu automatiquement
}
if (mode == 'visu') {
await this._doDisplayTMR(mode)
}
else {
const rencontre = this.getRencontreTMREnAttente();
const settingConfirmer = rencontre ? "confirmation-tmr-rencontre" : "confirmation-tmr";
const messageRencontre = rencontre ? `<p>Vous devrez combattre ${rencontre.system.genre == 'f' ? 'la' : 'le'} ${rencontre.name} de ${rencontre.system.force} points de Rêve</p>` : '';
RdDConfirm.confirmer({
settingConfirmer: settingConfirmer,
content: `<p>Voulez vous monter dans les TMR en mode ${mode}?</p>` + messageRencontre,
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(this.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.itemTypes['sort'],
signes: this.itemTypes['signedraconique'],
caracReve: parseInt(this.system.carac.reve.value),
pointsReve: this.getReveActuel(),
isRapide: isRapide,
isGM: game.user.isGM,
hasPlayerOwner: this.hasPlayerOwner
}
this.tmrApp = await RdDTMRDialog.create(this, tmrFormData);
await this.tmrApp.render(true);
await this.tmrApp.onDeplacement()
}
/* -------------------------------------------- */
async rollSoins(blesse, blessureId) {
const blessure = blesse.blessuresASoigner().find(it => it.id == blessureId);
if (blessure) {
if (!blessure.system.premierssoins.done) {
const tache = await this.getTacheBlessure(blesse, blessure);
return await this.rollTache(tache.id, {
onRollAutomate: async r => blesse.onRollTachePremiersSoins(blessureId, r)
});
}
if (!blessure.system.soinscomplets.done) {
const diff = blessure.system.difficulte + (blessure.system.premierssoins.bonus ?? 0);
return await this.rollCaracCompetence("dexterite", "Chirurgie", diff, {
title: "Soins complets",
onRollAutomate: r => blesse.onRollSoinsComplets(blessureId, r)
})
}
}
}
async onRollTachePremiersSoins(blessureId, rollData) {
if (!this.isOwner) {
return RdDBaseActor.remoteActorCall({ tokenId: this.token?.id, actorId: this.id, method: 'onRollTachePremiersSoins', args: [blessureId, rollData] });
}
const blessure = this.getItem(blessureId, 'blessure')
console.log('TODO update blessure', this, blessureId, rollData, rollData.tache);
if (blessure && !blessure.system.premierssoins.done) {
const tache = rollData.tache;
if (rollData.rolled.isETotal) {
await blessure.update({
'system.difficulte': blessure.system.difficulte - 1,
'system.premierssoins.tache': Math.max(0, tache.system.points_de_tache_courant)
})
}
else {
const bonus = tache.system.points_de_tache_courant - tache.system.points_de_tache
await blessure.update({
'system.premierssoins': {
done: (bonus >= 0),
bonus: Math.max(0, bonus),
tache: Math.max(0, tache.system.points_de_tache_courant)
}
})
if (bonus >= 0) {
await this.deleteEmbeddedDocuments('Item', [tache.id])
}
}
}
}
async onRollSoinsComplets(blessureId, rollData) {
if (!this.isOwner) {
return RdDBaseActor.remoteActorCall({ tokenId: this.token?.id, actorId: this.id, method: 'onRollSoinsComplets', args: [blessureId, rollData] });
}
const blessure = this.getItem(blessureId, 'blessure')
if (blessure && blessure.system.premierssoins.done && !blessure.system.soinscomplets.done) {
// TODO: update de la blessure: passer par le MJ!
if (rollData.rolled.isETotal) {
await blessure.setSoinsBlessure({
difficulte: blessure.system.difficulte - 1,
premierssoins: { done: false, bonus: 0 }, soinscomplets: { done: false, bonus: 0 },
})
}
else {
// soins complets finis
await blessure.setSoinsBlessure({
soinscomplets: { done: true, bonus: Math.max(0, rollData.rolled.ptTache) },
})
}
}
}
/* -------------------------------------------- */
conjurerPossession(possession) {
RdDPossession.onConjurerPossession(this, possession)
}
/* -------------------------------------------- */
verifierForceMin(item) {
if (item.type == 'arme' && item.system.force > parseInt(this.system.carac.force.value)) {
ChatMessage.create({
content: `<strong>${this.name} s'est équipé(e) de l'arme ${item.name}, mais n'a pas une force suffisante pour l'utiliser normalement </strong>
(${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?.isEquipable()) {
const isEquipe = !item.system.equipe;
await item.update({ "system.equipe": isEquipe });
this.computeEncTotal();
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 && attackerRoll.dmg.encaisserSpecial != "noarmure") {
armure.deteriorerArmure(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;
}
/* -------------------------------------------- */
/** @override */
getRollData() {
const rollData = super.getRollData();
return rollData;
}
/* -------------------------------------------- */
async resetItemUse() {
await this.setFlag(SYSTEM_RDD, 'itemUse', {});
}
/* -------------------------------------------- */
async incDecItemUse(itemId, inc = 1) {
const currentItemUse = this.getFlag(SYSTEM_RDD, 'itemUse');
let itemUse = currentItemUse ? foundry.utils.duplicate(currentItemUse) : {};
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;
}
/* -------------------------------------------- */
async effectuerTacheAlchimie(recetteId, tacheAlchimie, texteTache) {
let recetteData = this.findItemLike(recetteId, 'recettealchimique');
if (recetteData) {
if (tacheAlchimie != "couleur" && tacheAlchimie != "consistance") {
ui.notifications.warn(`L'étape alchimique ${tacheAlchimie} - ${texteTache} est inconnue`);
return;
}
const sansCristal = tacheAlchimie == "couleur" && this.items.filter(it => it.isCristalAlchimique()).length == 0;
const caracTache = RdDAlchimie.getCaracTache(tacheAlchimie);
const alchimieData = this.getCompetence("alchimie");
let rollData = {
recette: recetteData,
carac: { [caracTache]: this.system.carac[caracTache] },
selectedCarac: this.system.carac[caracTache],
competence: alchimieData,
diffLibre: RdDAlchimie.getDifficulte(texteTache),
diffConditions: sansCristal ? -4 : 0,
alchimie: {
tache: Misc.upperFirst(tacheAlchimie),
texte: texteTache,
sansCristal: sansCristal
}
}
rollData.competence.system.defaut_carac = caracTache;
const dialog = await RdDRoll.create(this, rollData,
{ html: 'systems/foundryvtt-reve-de-dragon/templates/dialog-roll-alchimie.html' },
{
name: 'tache-alchimique',
label: 'Tache Alchimique',
callbacks: [
this.createCallbackExperience(),
this.createCallbackAppelAuMoral(),
{ action: async r => await this._alchimieResult(r, false) }
]
}
);
dialog.render(true);
}
}
isCristalAlchimique(it) {
return it.type == 'objet' && Grammar.toLowerCaseNoAccent(it.name) == 'cristal alchimique' && it.system.quantite > 0;
}
/* -------------------------------------------- */
async _alchimieResult(rollData) {
await RdDResolutionTable.displayRollData(rollData, this, 'chat-resultat-alchimie.html');
}
/* -------------------------------------------- */
static $transformSubActeurSuivant = (suivant, link) => {
return foundry.utils.mergeObject(RdDBaseActor.extractActorMin(suivant), {
ephemere: !suivant.prototypeToken.actorLink,
coeur: link.coeur ?? 0,
prochainCoeur: link.prochainCoeur ?? link.coeur ?? 0
})
};
listeSuivants(filter = suivant => true) {
return RdDActor.$buildSubActorLinks(
this.system.subacteurs.suivants.filter(filter), RdDActor.$transformSubActeurSuivant
)
}
getSuivant(subActorId) {
const suivant = this.system.subacteurs.suivants.find(it => it.id == subActorId);
if (suivant) {
return RdDActor.$transformSubActeurSuivant(game.actors.get(suivant.id), suivant);
}
return undefined
}
getPointsCoeur(subActorId) {
return this.getSuivant(subActorId)?.coeur ?? 0;
}
async setPointsCoeur(subActorId, coeurs, options = { immediat: false }) {
const newSuivants = foundry.utils.duplicate(this.system.subacteurs.suivants)
const amoureux = newSuivants.find(it => it.id == subActorId);
if (amoureux) {
amoureux[options.immediat ? 'coeur' : 'prochainCoeur'] = coeurs
await this.update({ 'system.subacteurs.suivants': newSuivants });
}
}
/* -------------------------------------------- */
static $transformSubActeurVehicule = (vehicle, link) => {
return foundry.utils.mergeObject(RdDBaseActor.extractActorMin(vehicle), {
system: { categorie: vehicle.system.categorie, etat: vehicle.system.etat }
})
};
listeVehicules() {
return RdDActor.$buildSubActorLinks(this.system.subacteurs.vehicules, RdDActor.$transformSubActeurVehicule)
}
/* -------------------------------------------- */
static $transformSubActeurCreature = (actor, link) => RdDBaseActor.extractActorMin(actor)
listeMontures() {
return RdDActor.$buildSubActorLinks(this.system.subacteurs.montures, RdDActor.$transformSubActeurCreature);
}
/* -------------------------------------------- */
static $buildSubActorLinks(subActors, actorTransformation = (actor, link) => undefined) {
if (!subActors) {
return []
}
return subActors.map(link => {
const actor = game.actors.get(link.id)
return actor ? actorTransformation(actor, link) : undefined
})
.filter(it => it != undefined)
.sort(Misc.ascending(it => it.name))
}
/* -------------------------------------------- */
addSubActeur(subActor) {
if (!this.isAddSubActeurAllowed(subActor)) {
return
}
const subActorOnlyId = { id: subActor._id };
if (subActor.type == 'vehicule') {
this.pushSubActeur(subActorOnlyId, this.system.subacteurs.vehicules, `system.subacteurs.vehicules`, `Le véhicule ${subActor.name}`)
} else if (subActor.type == 'creature') {
this.pushSubActeur(subActorOnlyId, this.system.subacteurs.montures, 'system.subacteurs.montures', `L'animal ${subActor.name}`)
} else if (subActor.type == 'personnage') {
this.pushSubActeur(subActorOnlyId, this.system.subacteurs.suivants, 'system.subacteurs.suivants', `Le compagnon ${subActor.name}`)
}
}
async pushSubActeur(subActor, dataArray, dataPath, dataName) {
let alreadyPresent = dataArray.find(attached => attached.id == subActor.id);
if (!alreadyPresent) {
let newArray = [...dataArray, subActor]
await this.update({ [dataPath]: newArray });
} else {
ui.notifications.warn(dataName + " est déja attaché à " + this.name);
}
}
isAddSubActeurAllowed(subActor) {
if (subActor?.id == undefined) {
ui.notifications.warn("Aucun acteur à ajouter")
return false
}
if (subActor?.id == this.id) {
ui.notifications.warn("Vous ne pouvez pas attacher un acteur à lui même")
return false
}
else if (!subActor?.isOwner) {
ui.notifications.warn("Vous n'avez pas les droits sur l'acteur que vous attachez.")
return false
}
return true
}
async deleteSubActeur(actorId) {
['vehicules', 'suivants', 'montures'].forEach(async type => {
const subList = this.system.subacteurs[type];
if (subList.find(it => it.id == actorId)) {
let newList = subList.filter(it => it.id != actorId)
await this.update({ [`system.subacteurs.${type}`]: newList }, { renderSheet: false });
}
})
}
/* -------------------------------------------- */
async buildPotionGuerisonList(pointsGuerison) {
const pointsGuerisonInitial = pointsGuerison;
const blessures = this.filterItems(it => it.system.gravite > 0, 'blessure').sort(Misc.descending(it => it.system.gravite))
const ids = []
const guerisonData = { list: [], pointsConsommes: 0 }
for (let blessure of blessures) {
if (pointsGuerison >= blessure.system.gravite) {
pointsGuerison -= blessure.system.gravite;
guerisonData.list.push(`1 Blessure ${blessure.system.label} (${blessure.system.gravite} points)`);
ids.push(blessure.id)
}
}
if (ids.length > 0) {
await this.supprimerBlessures(it => ids.includes(it.id));
}
if (blessures.length == ids.length) {
let pvManquants = this.system.sante.vie.max - this.system.sante.vie.value;
let pvSoignees = Math.min(pvManquants, Math.floor(pointsGuerison / 2));
pointsGuerison -= pvSoignees * 2;
guerisonData.list.push(pvSoignees + " Points de Vie soignés");
await this.santeIncDec('vie', +pvSoignees, false);
}
guerisonData.pointsConsommes = pointsGuerisonInitial - pointsGuerison;
return guerisonData;
}
/* -------------------------------------------- */
async consommerPotionSoin(potionData) {
potionData.alias = this.name;
potionData.supprimer = true;
if (potionData.system.magique) {
// Gestion de la résistance:
potionData.rolled = await RdDResolutionTable.roll(this.getReveActuel(), -8);
if (potionData.rolled.isEchec) {
await this.reveActuelIncDec(-1);
potionData.guerisonData = await this.buildPotionGuerisonList(potionData.system.puissance);
potionData.guerisonMinutes = potionData.guerisonData.pointsConsommes * 5;
}
}
if (!potionData.system.magique || potionData.rolled.isSuccess) {
await this.setBonusPotionSoin(potionData.system.herbebonus);
}
ChatMessage.create({
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: await renderTemplate(`systems/foundryvtt-reve-de-dragon/templates/chat-consommer-potion-soin.html`, potionData)
});
}
async setBonusPotionSoin(bonus) {
await this.update({ 'system.sante.bonusPotion': bonus });
}
/* -------------------------------------------- */
async consommerPotionRepos(potionData) {
potionData.alias = this.name;
potionData.supprimer = true;
if (potionData.system.magique) {
// Gestion de la résistance:
potionData.rolled = await RdDResolutionTable.roll(this.getReveActuel(), -8);
if (potionData.rolled.isEchec) {
await this.reveActuelIncDec(-1);
let fatigueActuelle = this.getFatigueActuelle();
potionData.caseFatigueReel = Math.min(fatigueActuelle, potionData.system.puissance);
potionData.guerisonDureeUnite = (potionData.system.reposalchimique) ? "rounds" : "minutes";
potionData.guerisonDureeValue = (potionData.system.reposalchimique) ? potionData.caseFatigueReel : potionData.caseFatigueReel * 5;
potionData.aphasiePermanente = false;
if (potionData.system.reposalchimique) {
let chanceAphasie = await RdDDice.rollTotal("1d100");
if (chanceAphasie <= potionData.system.pr) {
potionData.aphasiePermanente = true;
}
}
await this.santeIncDec("fatigue", -potionData.caseFatigueReel);
}
}
if (!potionData.system.magique || potionData.rolled.isSuccess) {
this.bonusRepos = potionData.system.herbebonus;
}
ChatMessage.create({
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: await renderTemplate(`systems/foundryvtt-reve-de-dragon/templates/chat-consommer-potion-repos.html`, potionData)
});
}
/* -------------------------------------------- */
async fabriquerPotion(herbeData) {
let newPotion = {
name: `Potion de ${herbeData.system.categorie} (${herbeData.name})`, type: 'potion',
img: "systems/foundryvtt-reve-de-dragon/icons/objets/fiole_verre.webp",
system: {
quantite: 1, cout: 0, encombrement: 0.1,
categorie: herbeData.system.categorie,
herbe: herbeData.name,
rarete: herbeData.system.rarete,
herbebrins: herbeData.nbBrins,
herbebonus: herbeData.herbebonus,
description: ""
}
}
await this.createEmbeddedDocuments('Item', [newPotion], { renderSheet: true });
let newQuantite = herbeData.system.quantite - herbeData.nbBrins;
let messageData = {
alias: this.name,
nbBrinsReste: newQuantite,
potion: newPotion,
herbe: herbeData
}
this.diminuerQuantiteObjet(herbeData._id, herbeData.nbBrins);
ChatMessage.create({
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: await renderTemplate(`systems/foundryvtt-reve-de-dragon/templates/chat-fabriquer-potion-base.html`, messageData)
});
}
/* -------------------------------------------- */
async diminuerQuantiteObjet(id, nb, options = { supprimerSiZero: false }) {
const item = this.getItem(id);
if (item) {
await item.diminuerQuantite(nb, options);
}
}
/* -------------------------------------------- */
async consommerPotionGenerique(potionData) {
potionData.alias = this.name;
if (potionData.system.magique) {
// Gestion de la résistance:
potionData.rolled = await RdDResolutionTable.roll(this.getReveActuel(), -8);
if (potionData.rolled.isEchec) {
await this.reveActuelIncDec(-1);
}
}
ChatMessage.create({
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: await renderTemplate(`systems/foundryvtt-reve-de-dragon/templates/chat-consommer-potion-generique.html`, potionData)
});
}
/* -------------------------------------------- */
async consommerPotion(potion, onActionItem = async () => { }) {
const potionData = potion
if (potionData.system.categorie.includes('Soin')) {
this.consommerPotionSoin(potionData);
} else if (potionData.system.categorie.includes('Repos')) {
this.consommerPotionRepos(potionData);
} else {
this.consommerPotionGenerique(potionData);
}
await this.diminuerQuantiteObjet(potion.id, 1, { supprimerSiZero: potionData.supprimer });
await onActionItem()
}
/* -------------------------------------------- */
async onUpdateActor(update, options, actorId) {
const updatedEndurance = update?.system?.sante?.endurance
if (updatedEndurance && options.diff) {
await this.setEffect(STATUSES.StatusUnconscious, updatedEndurance.value == 0)
}
}
/* -------------------------------------------- */
async onPreUpdateItem(item, change, options, id) {
if (item.isCompetencePersonnage() && item.system.defaut_carac && item.system.xp) {
await this.checkCompetenceXP(item.name, item.system.xp);
}
}
/* -------------------------------------------- */
async onCreateItem(item, options, id) {
switch (item.type) {
case 'tete':
case 'queue':
case 'ombre':
case 'souffle':
await this.onCreateOwnedDraconique(item, options, id);
break;
}
await item.onCreateItemTemporel(this);
await item.onCreateDecoupeComestible(this);
}
async onDeleteItem(item, options, id) {
switch (item.type) {
case 'tete':
case 'queue':
case 'ombre':
case 'souffle':
await this.onDeleteOwnedDraconique(item, options, id);
break;
case 'casetmr':
await this.onDeleteOwnedCaseTmr(item, options, id);
break;
case 'empoignade':
await RdDEmpoignade.deleteLinkedEmpoignade(this.id, item)
break;
}
}
/* -------------------------------------------- */
async onCreateOwnedDraconique(item, options, id) {
if (Misc.isUniqueConnectedGM()) {
let draconique = Draconique.all().find(it => it.match(item));
if (draconique) {
await draconique.onActorCreateOwned(this, item)
this.notifyGestionTeteSouffleQueue(item, draconique.manualMessage());
}
await this.setInfoSommeilInsomnie();
}
}
/* -------------------------------------------- */
async onDeleteOwnedDraconique(item, options, id) {
if (Misc.isUniqueConnectedGM()) {
let draconique = Draconique.all().find(it => it.match(item));
if (draconique) {
await draconique.onActorDeleteOwned(this, item)
}
}
}
/* -------------------------------------------- */
async onDeleteOwnedCaseTmr(item, options, id) {
if (Misc.isUniqueConnectedGM()) {
let draconique = Draconique.all().find(it => it.isCase(item));
if (draconique) {
await draconique.onActorDeleteCaseTmr(this, item)
}
}
}
/* -------------------------------------------- */
notifyGestionTeteSouffleQueue(item, manualMessage = true) {
ChatMessage.create({
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: `${this.name} a reçu un/une ${item.type}: ${item.name}, qui ${manualMessage ? "n'est pas" : "est"} géré(e) automatiquement. ${manualMessage ? manualMessage : ''}`
});
}
async nouvelleIncarnation() {
let incarnation = this.toObject();
incarnation.items = Array.from(this.items.filter(it => it.type == TYPES.competence),
it => {
it = it.toObject();
it.id = undefined;
it.system.niveau = it.system.base;
it.system.niveau_archetype = Math.max(it.system.niveau + (it.system.xp > 0 ? 1 : 0), it.system.niveau_archetype);
it.system.xp = 0;
it.system.xp_sort = 0;
it.system.default_diffLibre = 0;
return it;
});
incarnation.name = 'Réincarnation de ' + incarnation.name
incarnation.system = {
carac: foundry.utils.duplicate(this.system.carac),
heure: RdDTimestamp.defHeure(await RdDDice.rollTotal("1dh", { rollMode: "selfroll", showDice: SHOW_DICE })).key,
age: 18,
biographie: '',
notes: '',
experiencelog: [],
'compteurs.experience.value': 3000,
'reve.seuil.value': this.system.carac.reve.value,
'reve.reve.value': this.system.carac.reve.value,
subacteurs: { suivants: [], montures: [], vehicules: [] },
}
incarnation = await RdDBaseActor.create(incarnation);
await incarnation.deleteEmbeddedDocuments('ActiveEffect', incarnation.getEmbeddedCollection("ActiveEffect").map(it => it.id));
await incarnation.remiseANeuf();
incarnation.sheet.render(true);
}
}