import { MAX_ENDURANCE_FATIGUE, 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 { RdDBaseActorVivant } from "./actor/base-actor-vivant.js";
const POSSESSION_SANS_DRACONIC = {
img: 'systems/foundryvtt-reve-de-dragon/icons/entites/possession.webp',
name: 'Sans draconic',
system: {
niveau: 0,
defaut_carac: "reve-actuel",
}
};
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 RdDBaseActorVivant {
/* -------------------------------------------- */
/**
* Prepare Character type specific data
*/
prepareActorData() {
// TODO: separate derived/base data preparation
// TODO: split by actor class
if (this.isPersonnage()) this.$prepareCharacterData()
}
$prepareCharacterData() {
// Initialize empty items
this.$computeCaracDerivee()
this.computeIsHautRevant();
this.cleanupConteneurs();
}
/* -------------------------------------------- */
$computeCaracDerivee() {
this.system.carac.force.value = Math.min(this.system.carac.force.value, parseInt(this.system.carac.taille.value) + 4);
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.sconst.value = RdDCarac.calculSConst(this.system.carac.constitution.value);
this.system.attributs.sust.value = RdDCarac.getCaracDerivee(this.system.carac.taille.value).sust;
this.system.attributs.encombrement.value = (parseInt(this.system.carac.force.value) + parseInt(this.system.carac.taille.value)) / 2;
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.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;
}
canReceive(item) {
if (this.isCreature()) {
return item.type == TYPES.competencecreature || item.isInventaire();
}
if (this.isPersonnage()) {
switch (item.type) {
case 'competencecreature': case 'tarot': case 'service':
return false;
}
return true;
}
return false;
}
/* -------------------------------------------- */
isHautRevant() {
return this.isPersonnage() && this.system.attributs.hautrevant.value != ""
}
/* -------------------------------------------- */
getReveActuel() {
switch (this.type) {
case 'personnage':
return Misc.toInt(this.system.reve?.reve?.value ?? this.carac.reve.value);
case 'creature':
return Misc.toInt(this.system.carac.reve?.value)
default:
return 0;
}
}
/* -------------------------------------------- */
getChanceActuel() {
return Misc.toInt(this.system.compteurs.chance?.value ?? 10);
}
/* -------------------------------------------- */
getTaille() {
return Misc.toInt(this.system.carac.taille?.value);
}
/* -------------------------------------------- */
getForce() {
return Misc.toInt(this.system.carac.force?.value);
}
/* -------------------------------------------- */
getAgilite() {
switch (this.type) {
case 'personnage': return Misc.toInt(this.system.carac.agilite?.value);
case 'creature': return Misc.toInt(this.system.carac.force?.value);
}
return 10;
}
/* -------------------------------------------- */
getChance() {
return Number(this.system.carac.chance?.value ?? 10);
}
getMoralTotal() {
return Number(this.system.compteurs.moral?.value ?? 0);
}
/* -------------------------------------------- */
getBonusDegat() {
// TODO: gérer séparation et +dom créature/entité indépendament de la compétence
return Number(this.system.attributs.plusdom.value ?? 0);
}
/* -------------------------------------------- */
getProtectionNaturelle() {
return Number(this.system.attributs.protection.value ?? 0);
}
/* -------------------------------------------- */
getEtatGeneral(options = { ethylisme: false }) {
let etatGeneral = Misc.toInt(this.system.compteurs.etat?.value)
if (options.ethylisme) {
// Pour les jets d'Ethylisme, on ignore le degré d'éthylisme (p.162)
etatGeneral -= Math.min(0, this.system.compteurs.ethylisme.value)
}
return etatGeneral
}
/* -------------------------------------------- */
getActivePoisons() {
return duplicate(this.items.filter(item => item.type == 'poison' && item.system.active))
}
/* -------------------------------------------- */
getMalusArmure() {
if (this.isPersonnage()) {
return this.itemTypes[TYPES.armure].filter(it => it.system.equipe)
.map(it => it.system.malus)
.reduce(Misc.sum(), 0);
}
return 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');
}
/* -------------------------------------------- */
getDraconicList() {
return this.itemTypes[TYPES.competence].filter(it => it.system.categorie == 'draconic')
}
/* -------------------------------------------- */
getBestDraconic() {
const list = this.getDraconicList()
.sort(Misc.descending(it => it.system.niveau))
return duplicate(list[0])
}
getDraconicOuPossession() {
const possession = this.itemTypes[TYPES.competencecreature].filter(it => it.system.categorie == 'possession')
.sort(Misc.descending(it => it.system.niveau))
.find(it => true);
if (possession) {
return possession;
}
const draconics = [...this.getDraconicList().filter(it => it.system.niveau >= 0),
POSSESSION_SANS_DRACONIC]
.sort(Misc.descending(it => it.system.niveau));
return draconics[0];
}
getDemiReve() {
return this.system.reve.tmrpos.coord;
}
/* -------------------------------------------- */
async verifierPotionsEnchantees() {
let potionsEnchantees = this.filterItems(it => it.type == 'potion' && it.system.categorie.toLowerCase().includes('enchant'));
for (let potion of potionsEnchantees) {
if (!potion.system.prpermanent) {
console.log(potion);
let newPr = (potion.system.pr > 0) ? potion.system.pr - 1 : 0;
let update = { _id: potion._id, 'system.pr': newPr };
const updated = await this.updateEmbeddedDocuments('Item', [update]); // Updates one EmbeddedEntity
let messageData = {
pr: newPr,
alias: this.name,
potionName: potion.name,
potionImg: potion.img
}
ChatMessage.create({
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: await renderTemplate(`systems/foundryvtt-reve-de-dragon/templates/chat-potionenchantee-chateaudormant.html`, messageData)
});
}
}
}
/* -------------------------------------------- */
hasArmeeMeleeEquipee() { // Return true si l'acteur possède au moins 1 arme de mêlée équipée
return this.itemTypes['arme'].find(it => it.system.equipe && it.system.competence != "")
}
/* -------------------------------------------- */
async roll() {
RdDEmpoignade.checkEmpoignadeEnCours(this)
const carac = mergeObject(duplicate(this.system.carac),
{
'reve-actuel': this.getCaracReveActuel(),
'chance-actuelle': this.getCaracChanceActuelle()
});
await this.openRollDialog({
name: `jet-${this.id}`,
label: `Jet de ${this.name}`,
template: 'systems/foundryvtt-reve-de-dragon/templates/dialog-roll.html',
rollData: {
carac: carac,
selectedCarac: carac.apparence,
selectedCaracName: 'apparence',
competences: this.itemTypes['competence']
},
callbackAction: r => this.$onRollCaracResult(r)
});
}
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}`);
}
}
findPlayer() {
return game.users.players.find(player => player.active && player.character?.id == this.id);
}
async onTimeChanging(oldTimestamp, newTimestamp) {
await super.onTimeChanging(oldTimestamp, newTimestamp);
await this.setInfoSommeilInsomnie();
}
async repos() {
await DialogRepos.create(this);
}
/* -------------------------------------------- */
async grisReve(nGrisReve) {
let message = {
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: `${nGrisReve} jours de gris rêve sont passés. `
};
for (let i = 0; i < nGrisReve; i++) {
await this.dormir(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._maladiePoisons(message);
const isMaladeEmpoisonne = maladiesPoisons.length > 0;
this._messageRecuperationMaladiePoisons(maladiesPoisons, message);
await this._recuperationBlessures(message, isMaladeEmpoisonne);
await this._recupererVie(message, isMaladeEmpoisonne);
}
_maladiePoisons(message) {
const actifs = this.items.filter(item => item.type == 'maladie' || (item.type == 'poison' && item.system.active));
return actifs;
}
_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) {
if (nonIdentifies > 0) {
message.content += ' et';
} else {
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._jetDeMoralChateauDormant(message);
await this._recupereChance();
await this.transformerStress();
await this.retourSeuilDeReve(message);
await this.setBonusPotionSoin(0);
await this.retourSust(message);
await this.verifierPotionsEnchantees();
if (message.content != "") {
message.content = `A la fin Chateau Dormant, ${message.content}
Un nouveau jour se lève`;
ChatMessage.create(message);
}
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() {
await this.update({ 'system.sommeil.insomnie': EffetsDraconiques.isSujetInsomnie(this) });
}
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 _jetDeMoralChateauDormant(message) {
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 _recuperationBlessures(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(b => b.recuperationBlessure({
actor: this,
timestamp,
message,
isMaladeEmpoisonne,
blessures
})));
await this.supprimerBlessures(it => it.system.gravite <= 0);
}
async supprimerBlessures(filterToDelete) {
const toDelete = this.filterItems(filterToDelete, TYPES.blessure)
.map(it => it.id);
await this.deleteEmbeddedDocuments('Item', toDelete);
}
/* -------------------------------------------- */
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
});
const updates = {
'system.sante.endurance.value': this.system.sante.endurance.max
};
if (this.isPersonnage() || this.isCreature()) {
await this.supprimerBlessures(it => true);
updates['system.sante.vie.value'] = this.system.sante.vie.max;
updates['system.sante.fatigue.value'] = 0;
}
if (this.isPersonnage()) {
updates['system.compteurs.ethylisme'] = { value: 1, nb_doses: 0, jet_moral: false };
}
await this.update(updates);
await this.removeEffects(e => e.flags.core.statusId !== STATUSES.StatusDemiReve);
}
/* -------------------------------------------- */
async dormir(heures, options = { grisReve: false, 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 ? '(dragon)' : 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) {
message.content += 'Vous êtes réveillé par un Rêve de Dragon.';
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 Rêve de Dragon 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 += `
Votre rêve redescend vers son seuil naturel (${seuil}, nouveau rêve actuel ${(reveActuel - 1)})`;
await this.reveActuelIncDec(-1);
}
}
async retourSust(message) {
const tplData = this.system;
const sustNeeded = tplData.attributs.sust.value;
const sustConsomme = tplData.compteurs.sust.value;
const eauConsomme = tplData.compteurs.eau.value;
if (game.settings.get(SYSTEM_RDD, "appliquer-famine-soif").includes('famine') && sustConsomme < sustNeeded) {
const perte = sustConsomme < Math.min(0.5, sustNeeded) ? 3 : (sustConsomme <= (sustNeeded / 2) ? 2 : 1);
message.content += `
Vous ne vous êtes sustenté que de ${sustConsomme} pour un appétit de ${sustNeeded}, vous avez faim!
La famine devrait vous faire ${perte} points d'endurance non récupérables, notez le cumul de côté et ajustez l'endurance`;
}
if (game.settings.get(SYSTEM_RDD, "appliquer-famine-soif").includes('soif') && eauConsomme < sustNeeded) {
const perte = eauConsomme < Math.min(0.5, sustNeeded) ? 12 : (eauConsomme <= (sustNeeded / 2) ? 6 : 3);
message.content += `
Vous n'avez bu que ${eauConsomme} doses de liquide pour une soif de ${sustNeeded}, vous avez soif!
La soif devrait vous faire ${perte} points d'endurance non récupérables, notez le cumul de côté et ajustez l'endurance`;
}
await this.updateCompteurValue('sust', 0);
await this.updateCompteurValue('eau', 0);
}
/* -------------------------------------------- */
async combattreReveDeDragon(force) {
let rollData = {
actor: this,
competence: 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: '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 = RdDBaseActorVivant._findCaracByName(this.system.carac, 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 = RdDBaseActorVivant._findCaracByName(this.system.carac, 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 = RdDBaseActorVivant._findCaracByName(this.system.carac, caracName);
if (carac) {
carac = 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 updateCreatureCompetence(idOrName, fieldName, value) {
let competence = this.getCompetence(idOrName);
if (competence) {
function getPath(fieldName) {
switch (fieldName) {
case "niveau": return 'system.niveau';
case "dommages": return 'system.dommages';
case "carac_value": return 'system.carac_value';
}
return undefined
}
const path = getPath(fieldName);
if (path) {
await this.updateEmbeddedDocuments('Item', [{ _id: competence.id, [path]: value }]); // updates one EmbeddedEntity
}
}
}
/* -------------------------------------------- */
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 this.updateEmbeddedDocuments('Item', [{ _id: competence.id, 'system.niveau': toNiveau }]);
await ExperienceLog.add(this, XP_TOPIC.NIVEAU, fromNiveau, toNiveau, competence.name, true);
} else {
console.log("Competence not found", idOrName);
}
}
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 :
"
+ Misc.join(listTronc, '
'));
}
}
/* -------------------------------------------- */
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 this.updateEmbeddedDocuments('Item', [{ _id: competence.id, 'system.xp': toXp }]);
await ExperienceLog.add(this, XP_TOPIC.XP, fromXp, toXp, competence.name, true);
if (toXp > fromXp) {
RdDUtility.checkThanatosXP(idOrName);
}
} else {
console.log("Competence not found", 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 this.updateEmbeddedDocuments('Item', [{ _id: competence.id, 'system.xp_sort': toXpSort }]);
await ExperienceLog.add(this, XP_TOPIC.XPSORT, fromXpSort, toXpSort, competence.name, true);
if (toXpSort > fromXpSort) {
RdDUtility.checkThanatosXP(idOrName);
}
} else {
console.log("Competence not found", idOrName);
}
}
/* -------------------------------------------- */
async updateCompetenceArchetype(idOrName, compValue) {
let competence = this.getCompetence(idOrName);
if (competence) {
await this.updateEmbeddedDocuments('Item', [{ _id: competence.id, 'system.niveau_archetype': Math.max(compValue ?? 0, 0) }]);
} else {
console.log("Competence not found", idOrName);
}
}
async deleteExperienceLog(from, count) {
if (from >= 0 && count > 0) {
let expLog = duplicate(this.system.experiencelog);
expLog.splice(from, count);
await this.update({ [`system.experiencelog`]: expLog });
}
}
/* -------------------------------------------- */
async updateCompteurValue(fieldName, 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 && this.isPersonnage()) {
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 });
}
isSurenc() {
return this.isPersonnage() ? (this.computeMalusSurEncombrement() < 0) : false
}
/* -------------------------------------------- */
computeMalusSurEncombrement() {
return Math.min(0, Math.floor(this.getEncombrementMax() - this.encTotal));
}
getMessageSurEncombrement() {
return this.computeMalusSurEncombrement() < 0 ? "Sur-Encombrement!" : "";
}
/* -------------------------------------------- */
getEncombrementMax() {
return this.system.attributs.encombrement.value
}
/* -------------------------------------------- */
computeIsHautRevant() {
if (this.isPersonnage()) {
this.system.attributs.hautrevant.value = this.itemTypes['tete'].find(it => Grammar.equalsInsensitive(it.name, 'don de haut-reve'))
? "Haut rêvant"
: "";
}
}
/* -------------------------------------------- */
computeResumeBlessure() {
const blessures = this.filterItems(it => it.system.gravite > 0, 'blessure')
const nbLegeres = blessures.filter(it => it.isLegere()).length;
const nbGraves = blessures.filter(it => it.isGrave()).length;
const nbCritiques = blessures.filter(it => it.isCritique()).length;
if (nbLegeres + nbGraves + nbCritiques == 0) {
return "Aucune blessure";
}
let resume = "Blessures:";
if (nbLegeres > 0) {
resume += " " + nbLegeres + " légère" + (nbLegeres > 1 ? "s" : "");
}
if (nbGraves > 0) {
if (nbLegeres > 0)
resume += ",";
resume += " " + nbGraves + " grave" + (nbGraves > 1 ? "s" : "");
}
if (nbCritiques > 0) {
if (nbGraves > 0 || nbLegeres > 0)
resume += ",";
resume += " une CRITIQUE !";
}
return resume;
}
/* -------------------------------------------- */
async computeEtatGeneral() {
this.system.compteurs.etat.value = this.$malusVie() + this.$malusFatigue() + this.$malusEthylisme();
}
$malusVie() {
return Math.min(this.system.sante.vie.value - this.system.sante.vie.max, 0);
}
$malusEthylisme() {
return Math.min(0, (this.system.compteurs.ethylisme?.value ?? 0));
}
/* -------------------------------------------- */
getEnduranceActuelle() {
return Number(this.system.sante.endurance.value);
}
getFatigueActuelle() {
if (ReglesOptionnelles.isUsing("appliquer-fatigue") && this.isPersonnage()) {
return Math.max(0, Math.min(this.$getFatigueMax(), this.system.sante.fatigue?.value));
}
return 0;
}
getFatigueRestante() {
return this.$getFatigueMax() - this.getFatigueActuelle();
}
getFatigueMax() {
return this.isPersonnage() ? this.$getFatigueMax() : 1;
}
$getFatigueMin() {
return this.system.sante.endurance.max - this.system.sante.endurance.value;
}
$getFatigueMax() {
return this.$getEnduranceMax() * 2;
}
$getEnduranceMax() {
return Math.max(1, Math.min(this.system.sante.endurance.max, MAX_ENDURANCE_FATIGUE));
}
$malusFatigue() {
if (ReglesOptionnelles.isUsing("appliquer-fatigue") && this.isPersonnage()) {
const fatigueMax = this.$getFatigueMax();
const fatigue = this.getFatigueActuelle();
return RdDUtility.calculMalusFatigue(fatigue, this.$getEnduranceMax())
}
return 0;
}
/* -------------------------------------------- */
async actionRefoulement(item) {
const refoulement = item?.system.refoulement ?? 0;
if (refoulement > 0) {
RdDConfirm.confirmer({
settingConfirmer: "confirmation-refouler",
content: `
Prennez-vous le risque de refouler ${item.name} pour ${refoulement} points de refoulement ?
`, title: 'Confirmer la refoulement', buttonLabel: 'Refouler', onAction: async () => { await this.ajouterRefoulement(refoulement, `une queue ${item.name}`); await item.delete(); } }); } } /* -------------------------------------------- */ async ajouterRefoulement(value = 1, refouler) { const refoulement = this.system.reve.refoulement.value + value; const roll = new Roll("1d20"); await roll.evaluate({ async: true }); 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() { const tmrInnaccessibles = this.filterItems(it => Draconique.isCaseTMR(it) && EffetsDraconiques.isInnaccessible(it)); return tmrInnaccessibles.map(it => it.system.coord); } /* -------------------------------------------- */ getTMRRencontres() { return this.itemTypes['rencontre']; } /* -------------------------------------------- */ async deleteTMRRencontreAtPosition() { const demiReve = this.getDemiReve() let rencontreIds = this.items.filter(it => it.type == 'rencontre' && it.system.coord == demiReve).map(it => it.id); if (rencontreIds.length > 0) { await this.deleteEmbeddedDocuments('Item', rencontreIds); } } /* -------------------------------------------- */ 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); } /* -------------------------------------------- */ getSonne() { return this.getEffect(STATUSES.StatusStunned); } /* -------------------------------------------- */ async finDeRound(options = { terminer: false }) { await this.$finDeRoundSuppressionEffetsTermines(options); await this.$finDeRoundBlessuresGraves(); await this.$finDeRoundSupprimerObsoletes(); await this.$finDeRoundEmpoignade(); } async $finDeRoundSuppressionEffetsTermines(options) { for (let effect of this.getEffects()) { if (effect.duration.type !== 'none' && (effect.duration.remaining <= 0 || options.terminer)) { await effect.delete(); ChatMessage.create({ content: `${this.name} n'est plus ${Misc.lowerFirst(game.i18n.localize(effect.system.label))} !` }); } } } async $finDeRoundBlessuresGraves() { if (this.isPersonnage() || this.isCreature()) { const nbGraves = this.filterItems(it => it.isGrave(), 'blessure').length; if (nbGraves > 0) { // Gestion blessure graves : -1 pt endurance par blessure grave await this.santeIncDec("endurance", -nbGraves); } } } async $finDeRoundSupprimerObsoletes() { const obsoletes = [] .concat(this.itemTypes[TYPES.empoignade].filter(it => it.system.pointsemp <= 0)) .concat(this.itemTypes[TYPES.possession].filter(it => it.system.compteur < -2 || it.system.compteur > 2)) .map(it => it.id); await this.deleteEmbeddedDocuments('Item', obsoletes); } async $finDeRoundEmpoignade() { const immobilisations = this.itemTypes[TYPES.empoignade].filter(it => it.system.pointsemp >= 2 && it.system.empoigneurid == this.id); immobilisations.forEach(emp => RdDEmpoignade.onImmobilisation(this, game.actors.get(emp.system.empoigneid), emp )) } /* -------------------------------------------- */ async setSonne(sonne = true) { if (!game.combat && sonne) { ui.notifications.info("Le personnage est hors combat, il ne reste donc pas sonné"); return; } await this.setEffect(STATUSES.StatusStunned, sonne); } /* -------------------------------------------- */ getSConst() { return RdDCarac.calculSConst(this.system.carac.constitution.value) } async ajoutXpConstitution(xp) { await this.update({ "system.carac.constitution.xp": Misc.toInt(this.system.carac.constitution.xp) + xp }); } /* -------------------------------------------- */ countBlessures(filter = it => !it.isContusion()) { return this.filterItems(filter, 'blessure').length } /* -------------------------------------------- */ async testSiSonne(endurance) { const result = await this._jetEndurance(endurance); if (result.roll.total == 1) { ChatMessage.create({ content: await this._gainXpConstitutionJetEndurance() }); } return result; } /* -------------------------------------------- */ async jetEndurance() { const endurance = this.system.sante.endurance.value; const result = await this._jetEndurance(this.system.sante.endurance.value) const message = { content: "Jet d'Endurance : " + result.roll.total + " / " + endurance + "Voulez vous monter dans les TMR en mode ${mode}?
`, title: 'Confirmer la montée dans les TMR', buttonLabel: 'Monter dans les TMR', onAction: async () => await this._doDisplayTMR(mode) }); } async _doDisplayTMR(mode) { let isRapide = mode == "rapide"; if (mode != "visu") { let minReveValue = (isRapide && !EffetsDraconiques.isDeplacementAccelere(this) ? 3 : 2) + this.countMonteeLaborieuse(); if (this.getReveActuel() < minReveValue) { ChatMessage.create({ content: `Vous n'avez les ${minReveValue} Points de Reve nécessaires pour monter dans les Terres Médianes`, whisper: ChatMessage.getWhisperRecipients(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: this.system.carac.reve.value, pointsReve: this.getReveActuel(), isRapide: isRapide, isGM: game.user.isGM, hasPlayerOwner: this.hasPlayerOwner } this.tmrApp = await RdDTMRDialog.create(this, tmrFormData); this.tmrApp.render(true); } /* -------------------------------------------- */ 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 > this.system.carac.force.value) { ChatMessage.create({ content: `${this.name} s'est équipé(e) de l'arme ${item.name}, mais n'a pas une force suffisante pour l'utiliser normalement (${item.system.force} nécessaire pour une Force de ${this.system.carac.force.value})` }); } } /* -------------------------------------------- */ async equiperObjet(itemID) { let item = this.getEmbeddedDocument('Item', itemID); if (item?.isEquipable()) { const isEquipe = !item.system.equipe; await this.updateEmbeddedDocuments('Item', [{ _id: item.id, "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; } /* -------------------------------------------- */ async onAppliquerJetEncaissement(encaissement, attacker) { const santeOrig = duplicate(this.system.sante); const blessure = await this.ajouterBlessure(encaissement, attacker); // Will update the result table const perteVie = await this.santeIncDec("vie", -encaissement.vie); const perteEndurance = await this.santeIncDec("endurance", -encaissement.endurance, blessure?.isCritique()); mergeObject(encaissement, { resteEndurance: perteEndurance.newValue, sonne: perteEndurance.sonne, jetEndurance: perteEndurance.jetEndurance, endurance: perteEndurance.perte, vie: santeOrig.vie.value - perteVie.newValue, blessure: blessure }); } /* -------------------------------------------- */ async ajouterBlessure(encaissement, attacker = undefined) { if (encaissement.gravite < 0) return; if (encaissement.gravite > 0) { while (this.countBlessures(it => it.system.gravite == encaissement.gravite) >= RdDItemBlessure.maxBlessures(encaissement.gravite) && encaissement.gravite <= 6) { // Aggravation encaissement.gravite += 2 if (encaissement.gravite > 2) { encaissement.vie += 2; } } } const endActuelle = this.getEnduranceActuelle(); const blessure = await RdDItemBlessure.createBlessure(this, encaissement.gravite, encaissement.dmg.loc.label, attacker); if (blessure.isCritique()) { encaissement.endurance = endActuelle; } if (blessure.isMort()) { this.setEffect(STATUSES.StatusComma, true); encaissement.mort = true; ChatMessage.create({ content: ` ${this.name} vient de succomber à une seconde blessure critique ! Que les Dragons gardent son Archétype en paix !` }); } return blessure; } /* -------------------------------------------- */ /** @override */ getRollData() { const rollData = super.getRollData(); return rollData; } /* -------------------------------------------- */ async resetItemUse() { await this.unsetFlag(SYSTEM_RDD, 'itemUse'); await this.setFlag(SYSTEM_RDD, 'itemUse', {}); } /* -------------------------------------------- */ async incDecItemUse(itemId, inc = 1) { let itemUse = duplicate(this.getFlag(SYSTEM_RDD, 'itemUse') ?? {}); itemUse[itemId] = (itemUse[itemId] ?? 0) + inc; await this.setFlag(SYSTEM_RDD, 'itemUse', itemUse); console.log("ITEM USE INC", inc, itemUse); } /* -------------------------------------------- */ getItemUse(itemId) { let itemUse = this.getFlag(SYSTEM_RDD, 'itemUse') ?? {}; console.log("ITEM USE GET", itemUse); return itemUse[itemId] ?? 0; } /* -------------------------------------------- */ 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'); } /* -------------------------------------------- */ listeVehicules() { const listeVehichules = this.system.subacteurs?.vehicules ?? []; return this._buildActorLinksList(listeVehichules, vehicle => RdDActor._vehicleData(vehicle)); } /* -------------------------------------------- */ listeSuivants() { return this._buildActorLinksList(this.system.subacteurs?.suivants ?? []); } /* -------------------------------------------- */ listeMontures() { return this._buildActorLinksList(this.system.subacteurs?.montures ?? []); } /* -------------------------------------------- */ _buildActorLinksList(links, actorTransformation = it => RdDActor._buildActorData(it)) { return links.map(link => game.actors.get(link.id)) .filter(it => it != undefined) .map(actorTransformation); } /* -------------------------------------------- */ static _vehicleData(vehicle) { return { id: vehicle.id, name: vehicle.name, img: vehicle.img, system: { categorie: vehicle.system.categorie, etat: vehicle.system.etat } }; } /* -------------------------------------------- */ static _buildActorData(it) { return { id: it.id, name: it.name, img: it.img }; } /* -------------------------------------------- */ async pushSubacteur(actor, dataArray, dataPath, dataName) { let alreadyPresent = dataArray.find(attached => attached.id == actor._id); if (!alreadyPresent) { let newArray = duplicate(dataArray); newArray.push({ id: actor._id }); await this.update({ [dataPath]: newArray }); } else { ui.notifications.warn(dataName + " est déja attaché à ce Personnage."); } } /* -------------------------------------------- */ addSubActeur(subActor) { if (subActor?.id == this.id) { ui.notifications.warn("Vous ne pouvez pas attacher un acteur à lui même") } else if (!subActor?.isOwner) { ui.notifications.warn("Vous n'avez pas les droits sur l'acteur que vous attachez.") } else { if (subActor.type == 'vehicule') { this.pushSubacteur(subActor, this.system.subacteurs.vehicules, 'system.subacteurs.vehicules', 'Ce Véhicule'); } else if (subActor.type == 'creature') { this.pushSubacteur(subActor, this.system.subacteurs.montures, 'system.subacteurs.montures', 'Cette Monture'); } else if (subActor.type == 'personnage') { this.pushSubacteur(subActor, this.system.subacteurs.suivants, 'system.subacteurs.suivants', 'Ce Suivant'); } } } /* -------------------------------------------- */ async removeSubacteur(actorId) { let newVehicules = this.system.subacteurs.vehicules.filter(function (obj, index, arr) { return obj.id != actorId }); let newSuivants = this.system.subacteurs.suivants.filter(function (obj, index, arr) { return obj.id != actorId }); let newMontures = this.system.subacteurs.montures.filter(function (obj, index, arr) { return obj.id != actorId }); await this.update({ 'system.subacteurs.vehicules': newVehicules }, { renderSheet: false }); await this.update({ 'system.subacteurs.suivants': newSuivants }, { renderSheet: false }); await this.update({ 'system.subacteurs.montures': newMontures }, { 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) } } /* -------------------------------------------- */ isEffectAllowed(statusId) { return true } /* -------------------------------------------- */ 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: 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); } }