foundryvtt-reve-de-dragon/module/actor.js
Vincent Vandemeulebrouck 84c6f9a466 Fix liste des liens subacteurs
Dans le cas d'un import de personnage ayant un lien sur un autre
acteur qui n'est pas importé, on ne pouvait plus ouvrir la feuille de
perso
2021-01-22 02:08:49 +01:00

2713 lines
102 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 { RdDAstrologieJoueur } from "./rdd-astrologie-joueur.js";
import { RdDResolutionTable } from "./rdd-resolution-table.js";
import { RdDDice } from "./rdd-dice.js";
import { RdDRollTables } from "./rdd-rolltables.js";
import { ChatUtility } from "./chat-utility.js";
import { RdDItemSort } from "./item-sort.js";
import { Grammar } from "./grammar.js";
import { RdDEncaisser } from "./rdd-roll-encaisser.js";
import { RdDCombat } from "./rdd-combat.js";
import { DeDraconique } from "./de-draconique.js";
import { RdDAudio } from "./rdd-audio.js";
import { RdDItemCompetence } from "./item-competence.js";
import { RdDItemArme } from "./item-arme.js";
import { RdDAlchimie } from "./rdd-alchimie.js";
import { StatusEffects } from "./status-effects.js";
import { RdDItemCompetenceCreature } from "./item-competencecreature.js";
/* -------------------------------------------- */
/**
* Extend the base Actor entity by defining a custom roll data structure which is ideal for the Simple system.
* @extends {Actor}
*/
export class RdDActor extends Actor {
/* -------------------------------------------- */
static init() {
Hooks.on("deleteActiveEffect", (actor, effect, options) => actor.onDeleteActiveEffect(effect, options));
Hooks.on("createActiveEffect", (actor, effect, options) => actor.onCreateActiveEffect(effect, options));
}
/* -------------------------------------------- */
/**
* Override the create() function to provide additional RdD functionality.
*
* This overrided create() function adds initial items
* Namely: Basic skills, money,
*
* @param {Object} data Barebones actor data which this function adds onto.
* @param {Object} options (Unused) Additional options which customize the creation workflow.
*
*/
static async create(data, options) {
// Case of compendium global import
if (data instanceof Array) {
return super.create(data, options);
}
// If the created actor has items (only applicable to duplicated actors) bypass the new actor creation logic
if (data.items) {
let actor = super.create(data, options);
if (data.type == "personnage") {
await actor.checkMonnaiePresence(data.items);
}
return actor;
}
let compendiumName;
if (data.type == "personnage") {
compendiumName = "foundryvtt-reve-de-dragon.competences";
} else if (data.type == "creature") {
compendiumName = "foundryvtt-reve-de-dragon.competences-creatures";
} else if (data.type == "entite") {
compendiumName = "foundryvtt-reve-de-dragon.competences-entites";
}
if ( compendiumName ) {
data.items = await RdDUtility.loadCompendium(compendiumName);
}
// Ajout monnaie
if (data.type == "personnage" && data.items) {
await RdDActor.ajouterMonnaie(data.items);
}
return super.create(data, options);
}
/* -------------------------------------------- */
prepareData() {
super.prepareData();
const actorData = this.data;
// Dynamic computing fields
this.encTotal = 0;
/*
// Auto-resize token
if (this.isToken) {
let tokenSize = actorData.data.carac.taille.value/10;
this.token.update({height: tokenSize, width: tokenSize } );
}*/
// Make separate methods for each Actor type (character, npc, etc.) to keep
// things organized.
if (actorData.type === 'personnage') this._prepareCharacterData(actorData);
if (actorData.type === 'creature') this.prepareCreatureData(actorData);
if (actorData.type === 'vehicule') this.prepareVehiculeData(actorData);
}
/* -------------------------------------------- */
prepareCreatureData(actorData) {
this.computeEncombrementTotalEtMalusArmure();
this.computeEtatGeneral();
}
/* -------------------------------------------- */
prepareVehiculeData( actorData ) {
this.computeEncombrementTotalEtMalusArmure();
}
/* -------------------------------------------- */
/**
* Prepare Character type specific data
*/
async _prepareCharacterData(actorData) {
// Initialize empty items
RdDUtility.computeCarac(actorData.data);
this.computeEncombrementTotalEtMalusArmure();
this.computeEtatGeneral();
// Sanity check
await this.checkMonnaiePresence(actorData.items);
}
/* -------------------------------------------- */
async checkMonnaiePresence(items) { // Ajout opportuniste si les pièces n'existent pas.
if (!items) return; // Sanity check during import
let piece = items.find(item => item.type == 'monnaie' && Number(item.data.valeur_deniers) == 1);
let newMonnaie = [];
if (!piece) {
newMonnaie.push(RdDUtility.createMonnaie("Etain (1 denier)", 1, "systems/foundryvtt-reve-de-dragon/icons/objets/piece_etain_poisson.webp"));
}
piece = items.find(item => item.type == 'monnaie' && Number(item.data.valeur_deniers) == 10);
if (!piece) {
newMonnaie.push(RdDUtility.createMonnaie("Bronze (10 deniers)", 10, "systems/foundryvtt-reve-de-dragon/icons/objets/piece_bronze_epees.webp"));
}
piece = items.find(item => item.type == 'monnaie' && Number(item.data.valeur_deniers) == 100);
if (!piece) {
newMonnaie.push(RdDUtility.createMonnaie("Argent (1 sol)", 100, "systems/foundryvtt-reve-de-dragon/icons/objets/piece_argent_sol.webp"));
}
piece = items.find(item => item.type == 'monnaie' && Number(item.data.valeur_deniers) == 1000);
if (!piece) {
newMonnaie.push(RdDUtility.createMonnaie("Or (10 sols)", 1000, "systems/foundryvtt-reve-de-dragon/icons/objets/piece_or_sol.webp"));
}
if (newMonnaie.length > 0) {
await this.createOwnedItem(newMonnaie);
}
}
/* -------------------------------------------- */
static async ajouterMonnaie(items) { // Creation auto à la création du personnage
let etain = RdDUtility.createMonnaie("Etain (1 denier)", 1, "systems/foundryvtt-reve-de-dragon/icons/objets/piece_etain_poisson.webp");
items.push(etain);
let bronze = RdDUtility.createMonnaie("Bronze (10 deniers)", 10, "systems/foundryvtt-reve-de-dragon/icons/objets/piece_bronze_epees.webp");
items.push(bronze);
let argent = RdDUtility.createMonnaie("Argent (1 sol)", 100, "systems/foundryvtt-reve-de-dragon/icons/objets/piece_argent_sol.webp");
items.push(argent);
let or = RdDUtility.createMonnaie("Or (10 sols)", 1000, "systems/foundryvtt-reve-de-dragon/icons/objets/piece_or_sol.webp");
items.push(or);
}
/* -------------------------------------------- */
isCreature() {
return this.data.type == 'creature' || this.data.type == 'entite';
}
/* -------------------------------------------- */
isPersonnage() {
return this.data.type == 'personnage';
}
/* -------------------------------------------- */
getReveActuel() {
return this.data.data.reve?.reve?.value ?? this.data.data.carac.reve.value;
}
getChanceActuel() {
return this.data.data.compteurs.chance?.value ?? 10;
}
/* -------------------------------------------- */
getForceValue() {
return this.data.data.carac.force?.force ?? this.data.data.carac.reve.value;
}
getMoralTotal() {
return this.data.data.compteurs.moral?.value ?? 0;
}
/* -------------------------------------------- */
getBonusDegat() {
// TODO: gérer séparation et +dom créature/entité indépendament de la compétence
return Misc.toInt(this.data.data.attributs.plusdom.value);
}
/* -------------------------------------------- */
getProtectionNaturelle() {
return Misc.toInt(this.data.data.attributs.protection.value);
}
/* -------------------------------------------- */
getEtatGeneral() {
return this.data.data.compteurs.etat?.value ?? 0;
}
getMalusArmure() {
return this.data.data.attributs?.malusarmure?.value ?? 0;
}
getEncTotal() {
return Math.floor(this.encTotal ?? 0);
}
getSurenc() {
return this.data.data.compteurs.surenc?.value ?? 0;
}
/* -------------------------------------------- */
loadCompendiumNames() {
return this.data.items.filter((item) => item.type == 'competence');
}
/* -------------------------------------------- */
getCompetence(compName) {
return RdDItemCompetence.findCompetence(this.data.items, compName);
}
/* -------------------------------------------- */
getTache(id) {
return this.data.items.find(item => item.type == 'tache' && item._id == id);
}
getMeditation(id) {
return this.data.items.find(item => item.type == 'meditation' && item._id == id);
}
/* -------------------------------------------- */
getBestDraconic() {
const list = this.getDraconicList().sort((a, b) => b.data.niveau - a.data.niveau);
if (list.length == 0) {
return { name: "none", niveau: -11 };
}
return duplicate(list[0]);
}
/* -------------------------------------------- */
async deleteSortReserve(sortReserve) {
let reserve = duplicate(this.data.data.reve.reserve);
let len = reserve.list.length;
let i = 0;
let newTable = [];
for (i = 0; i < len; i++) {
if (reserve.list[i].coord != sortReserve.coord && reserve.list[i].sort.name != sortReserve.sort.name)
newTable.push(reserve.list[i]);
}
if (newTable.length != len) {
reserve.list = newTable;
await this.update({ "data.reve.reserve": reserve });
}
}
/* -------------------------------------------- */
getSurprise(isCombat = true) {
let niveauSurprise = Array.from(this.effects?.values() ?? [])
.map(effect => StatusEffects.valeurSurprise(effect.data, isCombat))
.reduce((a,b)=> a+b, 0);
if (niveauSurprise>1) {
return 'totale';
}
if (niveauSurprise==1 || this.getSonne()) {
return 'demi';
}
return '';
}
/* -------------------------------------------- */
async dormirChateauDormant() {
let message = {
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: ""
};
const blessures = duplicate(this.data.data.blessures);
console.log("dormirChateauDormant", blessures)
await this._recupererBlessures(message, "legere", blessures.legeres.liste.filter(b => b.active), []);
await this._recupererBlessures(message, "grave", blessures.graves.liste.filter(b => b.active), blessures.legeres.liste);
await this._recupererBlessures(message, "critique", blessures.critiques.liste.filter(b => b.active), blessures.graves.liste);
await this.update({ "data.blessures": blessures });
await this._recupererVie(message);
await this.transformerStress(message);
await this.retourSeuilDeReve(message);
message.content = `A la fin Chateau Dormant, ${message.content}<br>Un nouveau jour se lève`;
ChatMessage.create(message);
}
/* -------------------------------------------- */
async _recupererBlessures(message, type, liste, moindres) {
let count = 0;
const definitions = RdDUtility.getDefinitionsBlessures();
let definition = definitions.find(d => d.type == type);
for (let blessure of liste) {
if (blessure.jours >= definition.facteur) {
let rolled = await this._jetRecuperationConstitution(Misc.toInt(blessure.soins_complets), message);
blessure.soins_complets = 0;
if (rolled.isSuccess && this._retrograderBlessure(type, blessure, moindres)) {
message.content += ` -- une blessure ${type} cicatrise`;
count++;
}
else if (rolled.isETotal) {
message.content += ` -- une blessure ${type} s'infecte (temps de guérison augmenté de ${definition.facteur} jours, perte de vie)`;
blessure.jours = 0;
await this.santeIncDec("vie", -1);
}
else {
message.content += ` -- une blessure ${type} reste stable`;
}
}
else {
blessure.jours++;
}
}
}
/* -------------------------------------------- */
_retrograderBlessure(type, blessure, blessuresMoindres) {
if (type != "legere") {
let retrograde = blessuresMoindres.find(b => !b.active);
if (!retrograde) {
return false;
}
mergeObject(retrograde, { "active": true, "premiers_soins": 0, "soins_complets": 0, "jours": 0, "loc": blessure.loc });
}
this._supprimerBlessure(blessure);
return true;
}
/* -------------------------------------------- */
_supprimerBlessure(blessure) {
mergeObject(blessure, { "active": false, "premiers_soins": 0, "soins_complets": 0, "jours": 0, "loc": "" });
}
/* -------------------------------------------- */
async _recupererVie(message) {
let blessures = [].concat(this.data.data.blessures.legeres.liste).concat(this.data.data.blessures.graves.liste).concat(this.data.data.blessures.critiques.liste);
let nbBlessures = blessures.filter(b => b.active);
let vieManquante = this.data.data.sante.vie.max - this.data.data.sante.vie.value;
if (nbBlessures == 0 && vieManquante > 0) {
let bonusSoins = 0;
for (let b of blessures) {
bonusSoins = Math.max(bonusSoins, Misc.toInt(b.soins_complets));
}
let rolled = await this._jetRecuperationConstitution(bonusSoins, message)
if (rolled.isSuccess) {
const gain = Math.min(rolled.isPart ? 2 : 1, vieManquante);
message.content += " -- récupération de vie: " + gain;
await this.santeIncDec("vie", gain);
}
else if (rolled.isETotal) {
message.content += " -- perte de vie: 1";
await this.santeIncDec("vie", -1);
}
else {
message.content += " -- vie stationnaire ";
}
}
}
/* -------------------------------------------- */
async _jetRecuperationConstitution(bonusSoins, message = undefined) {
let difficulte = Misc.toInt(bonusSoins) + Math.min(0, this.data.data.sante.vie.value - this.data.data.sante.vie.max);
let rolled = await RdDResolutionTable.roll(this.data.data.carac.constitution.value, difficulte);
if (message) {
message.content += RdDResolutionTable.explain(rolled).replace(/Jet :/, "Constitution :");
}
return rolled;
}
/* -------------------------------------------- */
async remiseANeuf() {
let message = {
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: "Remise à neuf de " + this.name
};
if (this.isEntiteCauchemar()) {
await this.santeIncDec("endurance", this.data.data.sante.endurance.max - this.data.data.sante.endurance.value);
}
else {
if (this.data.data.blessures) {
const blessures = duplicate(this.data.data.blessures);
for (let listeBlessures of [blessures.legeres.liste, blessures.graves.liste, blessures.critiques.liste]) {
for (let blessure of listeBlessures) {
this._supprimerBlessure(blessure);
}
}
await this.update({ "data.blessures": blessures });
}
if (this.isPersonnage()) {
await this.setEthylisme(1);
}
await this.santeIncDec("vie", this.data.data.sante.vie.max - this.data.data.sante.vie.value);
await this.santeIncDec("endurance", this.data.data.sante.endurance.max - this.data.data.sante.endurance.value);
if (this.data.data.sante.fatigue) {
let fatigue = duplicate(this.data.data.sante.fatigue)
fatigue.value = 0;
await this.update({ "data.sante.fatigue": fatigue });
}
}
ChatMessage.create(message);
}
/* -------------------------------------------- */
async dormir(heures = 1) {
let message = {
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: this.name +": Vous dormez " + heures + " heure" + (heures > 1 ? "s" : "")
};
await this.recupereEndurance(message);
for (let i = 0; i < heures; i++) {
await this._recupererEthylisme(message);
await this.recupererFatigue(message);
await this.recuperationReve(message);
}
ChatMessage.create(message);
}
/* -------------------------------------------- */
async _recupererEthylisme(message) {
let ethylisme = duplicate(this.data.data.compteurs.ethylisme);
ethylisme.nb_doses = 0;
ethylisme.jet_moral = false;
if (ethylisme.value < 1) {
ethylisme.value = Math.min(ethylisme.value + 1, 1);
if (ethylisme.value <= 0) {
message.content += `<br>Vous dégrisez un peu (${RdDUtility.getNomEthylisme(ethylisme.value)})`;
}
}
await this.update({ "data.compteurs.ethylisme": ethylisme });
}
/* -------------------------------------------- */
async recupereEndurance(message) {
const manquant = this._computeEnduranceMax() - this.data.data.sante.endurance.value;
if (manquant > 0) {
await this.santeIncDec("endurance", manquant);
message.content += "<br>Vous récuperez " + manquant + " points d'endurance";
}
}
/* -------------------------------------------- */
async recupererFatigue(message) {
let fatigue = duplicate(this.data.data.sante.fatigue)
const fatigueMin = this._computeFatigueMin();
if (fatigue.value <= fatigueMin) {
message.content += "<br>Vous êtes déjà reposé";
return;
}
fatigue.value = Math.max(fatigueMin, this._calculRecuperationSegment(fatigue.value));
console.log("recupererFatigue", fatigue)
await this.update({ "data.sante.fatigue": fatigue });
if (fatigue.value == 0) {
message.content += "<br>Vous êtes bien reposé";
}
}
/* -------------------------------------------- */
_calculRecuperationSegment(actuel) {
const segments = RdDUtility.getSegmentsFatigue(this.data.data.sante.endurance.max);
let cumul = 0;
let i;
for (i = 0; i < 11; i++) {
cumul += segments[i];
let diff = cumul - actuel;
if (diff >= 0) {
const limit2Segments = Math.floor(segments[i] / 2);
if (diff > limit2Segments && i > 0) {
cumul -= segments[i - 1]; // le segment est à moins de la moitié, il est récupéré
}
cumul -= segments[i];
break;
}
};
return cumul;
}
/* -------------------------------------------- */
async recuperationReve(message) {
const seuil = this.data.data.reve.seuil.value;
const reveActuel = this.getReveActuel();
if (reveActuel >= seuil) {
message.content += `<br>Vous avez suffisament rêvé (seuil ${seuil}, rêve actuel ${reveActuel})`;
}
else {
let deRecuperation = (await DeDraconique.ddr("selfroll")).total;
console.log("recuperationReve", deRecuperation);
if (deRecuperation >= 7) {
// Rêve de Dragon !
message.content += `<br>Vous faites un <strong>Rêve de Dragon</strong> de ${deRecuperation} Points de rêve`;
message.content += await this.combattreReveDeDragon(deRecuperation);
}
else {
message.content += `<br>Vous récupérez ${deRecuperation} Points de rêve`;
await this.reveActuelIncDec(deRecuperation);
}
}
}
/* -------------------------------------------- */
async retourSeuilDeReve(message) {
const seuil = this.data.data.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 combattreReveDeDragon(force) {
let draconic = this.getBestDraconic();
let niveau = Math.max(0, draconic.data.niveau);
let etat = this.getEtatGeneral();
let difficulte = niveau - etat - force;
let reveActuel = this.getReveActuel();
let rolled = await RdDResolutionTable.roll(reveActuel, difficulte);
// TODO: xp particulière
console.log("combattreReveDeDragon", rolled);
return await this.appliquerReveDeDragon(rolled, force);
}
/* -------------------------------------------- */
async appliquerReveDeDragon(roll, force) {
let message = "";
if (roll.isSuccess) {
message += "<br>Vous gagnez " + force + " points de Rêve";
await this.updatePointDeSeuil();
await this.reveActuelIncDec(force);
}
if (roll.isPart) {
// TODO: Dialog pour choix entre HR opu général?
message += "<br>Vous gagnez une Tête de dragon: Demander à votre MJ d'effectuer un tirage sur la table des Hauts Rêvants ou des Vrais Rêvants, selon votre choix.";
}
if (roll.isEchec) {
message += "<br>Vous subissez une Queue de Dragon: " + await this.ajouterQueue();
}
if (roll.isETotal) {
message += "<br>A cause de votre échec total, vous subissez une deuxième Queue de Dragon: " + await this.ajouterQueue();
}
return message;
}
/* -------------------------------------------- */
async sortMisEnReserve(rollData, sort) {
let reserve = duplicate(this.data.data.reve.reserve);
reserve.list.push({ coord: rollData.coord, sort: sort, draconic: duplicate(rollData.competence) });
await this.update({ "data.reve.reserve": reserve });
this.currentTMR.updateSortReserve();
}
/* -------------------------------------------- */
async updateCarac(caracName, caracValue) {
let caracpath = "data.carac." + caracName + ".value"
if (caracName == "force") {
let caracTaille = this.data.data.carac.taille;
if ( Number(caracValue) > Number(caracTaille.value)+4) {
ui.notifications.warn("Votre FORCE doit être au maximum de TAILLE+4");
return;
}
}
if (caracName == "reve") {
if (caracValue > Misc.toInt(this.data.data.reve.seuil.value)) {
this.setPointsDeSeuil(caracValue);
}
}
if (caracName == "chance") {
if (caracValue > Misc.toInt(this.data.data.compteurs.chance.value)) {
this.setPointsDeChance(caracValue);
}
}
await this.update({ [caracpath]: caracValue });
}
/* -------------------------------------------- */
async updateCaracXP(caracName, caracXP) {
let caracpath = "data.carac." + caracName + ".xp";
await this.update({ [caracpath]: caracXP });
this.checkCaracXP(caracName);
}
/* -------------------------------------------- */
async updateCreatureCompetence(compName, fieldName, compValue) {
let comp = this.getCompetence(compName);
console.log(comp);
if (comp) {
const update = { _id: comp._id }
if (fieldName == "niveau")
update['data.niveau'] = compValue;
else if (fieldName == "dommages")
update['data.dommages'] = compValue;
else
update['data.carac_value'] = compValue;
console.log(update);
const updated = await this.updateEmbeddedEntity("OwnedItem", update); // Updates one EmbeddedEntity
}
}
/* -------------------------------------------- */
async updateCompetence(compName, compValue) {
let comp = this.getCompetence(compName);
if (comp) {
let troncList = RdDItemCompetence.isTronc(compName);
let maxNiveau = compValue;
if (troncList) {
let message = "Vous avez modifié une compétence 'tronc'. Vérifiez que les compétences suivantes évoluent ensemble jusqu'au niveau 0 : ";
for (let troncName of troncList) {
message += "<br>" + troncName;
}
ChatMessage.create({
whisper: ChatMessage.getWhisperRecipients(game.user.name),
content: message
});
}
const update = { _id: comp._id, 'data.niveau': maxNiveau };
const updated = await this.updateEmbeddedEntity("OwnedItem", update); // Updates one EmbeddedEntity
} else {
console.log("Competence not found", compName);
}
}
/* -------------------------------------------- */
async updateCompetenceXP(compName, compValue) {
let comp = this.getCompetence(compName);
if (comp) {
this.checkCompetenceXP(compName, compValue);
const update = { _id: comp._id, 'data.xp': compValue };
const updated = await this.updateEmbeddedEntity("OwnedItem", update); // Updates one EmbeddedEntity
} else {
console.log("Competence not found", compName);
}
}
/* -------------------------------------------- */
async updateCompetenceArchetype(compName, compValue) {
let comp = this.getCompetence(compName);
if (comp) {
const update = { _id: comp._id, 'data.niveau_archetype': compValue };
const updated = await this.updateEmbeddedEntity("OwnedItem", update); // Updates one EmbeddedEntity
} else {
console.log("Competence not found", compName);
}
}
/* -------------------------------------------- */
async updateCompteurValue(fieldName, fieldValue) {
//console.log("Update", fieldName, fieldValue);
let compteurs = duplicate(this.data.data.compteurs);
compteurs[fieldName].value = fieldValue;
await this.update({ "data.compteurs": compteurs });
}
/* -------------------------------------------- */
async updateProtectionValue(fieldName, fieldValue) {
let attributs = duplicate(this.data.data.attributs);
attributs[fieldName].value = fieldValue;
await this.update({ "data.attributs": attributs });
}
/* -------------------------------------------- */
validateConteneur(itemId, conteneurId) {
let conteneurDest = this.items.find(conteneur => conteneurId == conteneur._id); // recup conteneur
let conteneurSrc = this.items.find(conteneur => itemId == conteneur._id && conteneur.type == 'conteneur');
if (conteneurSrc) { // Si c'est un conteneur, il faut vérifier qu'on ne le déplace pas vers un sous-conteneur lui appartenant
for (let id of conteneurSrc.data.data.contenu) {
let subObjet = this.items.find(subobj => id == subobj._id);
if (subObjet && subObjet._id == conteneurDest._id) {
ui.notifications.warn("Impossible de déplacer un conteneur parent dans son fils !");
return false; // Loop detected !
}
if (subObjet && subObjet.type == 'conteneur') {
return this.validateConteneur(subObjet._id, conteneurId);
}
}
}
return true;
}
/* -------------------------------------------- */
/** Teste si le conteneur de destination a suffisament de capacité
* pour recevoir le nouvel objet
*/
testConteneurCapacite(itemId, conteneurId) {
if (!conteneurId) return true; // pas de conteneur (porté sur soi), donc toujours OK.
let conteneur = this.items.find(conteneur => conteneurId == conteneur._id); // recup conteneur
//console.log("Conteneur trouvé : ", conteneur);
if (conteneur && conteneur.type == "conteneur") {
let currentEnc = 0; // Calculer le total actuel des contenus
for (let id of conteneur.data.data.contenu) {
let objet = this.items.find(objet => (id == objet._id));
currentEnc += (objet) ? objet.data.data.encombrement : 0;
}
// Et gérer le nouvel objet
let nouvelObjet = this.items.find(objet => (itemId == objet._id));
if (nouvelObjet && currentEnc + nouvelObjet.data.data.encombrement > Number(conteneur.data.data.capacite)) {
ui.notifications.warn("Capacité d'encombrement insuffisante dans le conteneur !");
return false;
}
}
return true;
}
/* -------------------------------------------- */
buildSubConteneurObjetList(conteneurId, deleteList) {
let conteneur = this.items.find(conteneur => conteneurId == conteneur._id); // recup conteneur
if (conteneur && conteneur.type == 'conteneur') { // Si présent
for (let subId of conteneur.data.data.contenu) {
let subObj = this.items.find(subobjet => subId == subobjet._id); // recup conteneur
if (subObj && subObj.type == 'conteneur') {
this.buildSubConteneurObjetList(subId, deleteList);
}
if ( subObj) // Robust...
deleteList.push( {id: subId, conteneurId: conteneurId } );
}
}
}
/* -------------------------------------------- */
async deleteAllConteneur(itemId) {
let list = [];
list.push( {id: itemId, conteneurId: undefined }); // Init list
this.buildSubConteneurObjetList(itemId, list);
//console.log("List to delete", list);
for (let item of list) {
await this.deleteOwnedItem(item.id);
}
}
/* -------------------------------------------- */
/** Supprime un item d'un conteneur, sur la base
* de leurs ID */
async enleverDeConteneur(itemId, conteneurId) {
if (!conteneurId) return; // pas de conteneur (porté sur soi)
let conteneur = this.items.find(conteneur => conteneurId == conteneur._id); // recup conteneur
if (conteneur) { // Si présent
let data2use = duplicate(conteneur.data);
//console.log("Suppression du conteneur1", conteneurId, itemId, conteneur.data.data.contenu);
let contenu = data2use.data.contenu;
let index = contenu.indexOf(itemId);
while (index >= 0) { // Force cleanup, itemId is unique
contenu.splice(index, 1);
index = contenu.indexOf(itemId);
}
await this.updateEmbeddedEntity("OwnedItem", data2use);
}
}
/* -------------------------------------------- */
/** Ajoute un item dans un conteneur, sur la base
* de leurs ID */
async ajouterAConteneur(itemId, conteneurId) {
if (!conteneurId) return; // pas de conteneur (porté sur soi)
let conteneur = this.items.find(conteneur => conteneurId == conteneur._id);
if (conteneur && conteneur.type == 'conteneur') {
let data2use = duplicate(conteneur.data);
data2use.data.contenu.push(itemId);
await this.updateEmbeddedEntity("OwnedItem", data2use);
}
}
/* -------------------------------------------- */
/** Fonction de remise à plat de l'équipement (ie vide les champs 'contenu') */
async nettoyerConteneurs() {
let conteneurList = this.items.filter(conteneur => conteneur.type == 'conteneur');
let conteneurFixedList = [];
for (let conteneur of conteneurList) {
if (conteneur.data.data.contenu.length > 0) {
conteneurFixedList.push({ _id: conteneur._id, 'data.contenu': [] });
}
}
if (conteneurFixedList.length > 0)
await this.updateOwnedItem(conteneurFixedList);
}
/* -------------------------------------------- */
async moveItemsBetweenActors(itemId, sourceActorId) {
let itemsList = []
let sourceActor = game.actors.get( sourceActorId );
itemsList.push( {id: itemId, conteneurId: undefined }); // Init list
sourceActor.buildSubConteneurObjetList( itemId, itemsList ); // Get itemId list
let conteneurMap = {};
for (let item of itemsList) {
let copyItem = sourceActor.data.items.find( subItem => subItem._id == item.id );
let newItem = await this.createOwnedItem( duplicate(copyItem) );
console.log('New object', newItem);
if (copyItem.type == 'conteneur') {
conteneurMap[copyItem._id] = newItem._id;
}
// gestion conteneur/contenu
if ( item.conteneurId) { // l'Objet était dans un conteneur
let newConteneurId = conteneurMap[item.conteneurId];
let newConteneur = this.data.items.find( subItem => subItem._id == newConteneurId );
console.log('New conteneur filling!', newConteneur);
let contenu = duplicate(newConteneur.data.contenu);
contenu.push( newItem._id );
await this.updateOwnedItem( {_id: newConteneurId, 'data.contenu': contenu });
}
}
for( let item of itemsList) {
await sourceActor.deleteOwnedItem( item.id );
}
}
/* -------------------------------------------- */
detectSurEncombrement() {
let maxEnc = 0;
if ( this.data.type == 'vehicule')
maxEnc = this.data.data.capacite_encombrement;
else
maxEnc = this.data.data.attributs.encombrement.value;
let diffEnc = Number(this.encTotal) - Number(maxEnc);
return Math.max(0, Math.ceil(diffEnc));
}
/* -------------------------------------------- */
async computeEncombrementTotalEtMalusArmure() {
let encTotal = 0;
let malusArmureData = (this.data.data.attributs && this.data.data.attributs.malusarmure) ? duplicate(this.data.data.attributs.malusarmure) : {};
let newMalusArmure = 0;
for (const item of this.data.items) {
if (item.type == 'armure' && item.data.equipe) { // Armure équipée, intégration du malus armure total
newMalusArmure += item.data.malus;
}
// Calcul encombrement
if (item.data && item.data.encombrement != undefined) {
if (!Number(item.data.encombrement)) item.data.encombrement = 0; // Auto-fix
if (item.data.quantite == undefined) item.data.quantite = 1; // Auto-fix
if (item.data.quantite < 0) item.data.quantite = 0; // Auto-fix
item.data.encTotal = Number(item.data.encombrement) * Number(item.data.quantite);
//console.log("Enc:", item.name, item.data.encombrement, item.data.quantite, item.data.encTotal);
encTotal += item.data.encTotal;
} else {
item.data.encTotal = 0; // Force default enc
}
}
// Mise à jour valeur totale et états
this.encTotal = encTotal;
this.detectSurEncombrement();
// Mise à jour éventuelle du malus armure
if (this.data.data.attributs && this.data.data.attributs.malusarmure && newMalusArmure != malusArmureData.value) {
malusArmureData.value = newMalusArmure;
await this.update({ "data.attributs.malusarmure": malusArmureData });
}
}
/* -------------------------------------------- */
computeResumeBlessure(blessures = this.data.data.blessures) {
let nbLegeres = this.countBlessures(blessures.legeres.liste);
let nbGraves = this.countBlessures(blessures.graves.liste);
let nbCritiques = this.countBlessures(blessures.critiques.liste);
let resume = "Blessures:";
if (nbCritiques > 0 || nbGraves > 0 || nbLegeres > 0) {
if (nbLegeres > 0) {
resume += " " + nbLegeres + " légères";
}
if (nbGraves > 0) {
if (nbLegeres > 0)
resume += ",";
resume += " " + nbGraves + " graves";
}
if (nbCritiques > 0) {
if (nbGraves > 0 || nbLegeres > 0)
resume += ",";
resume += " une CRITIQUE !";
}
}
else {
resume += " aucune";
}
return resume;
}
/* -------------------------------------------- */
computeEtatGeneral() {
let data = this.data.data;
// Pas d'état général pour les entités forçage à 0
if (this.data.type == 'entite') {
data.compteurs.etat.value = 0;
return;
}
// Pour les autres
let state = - (data.sante.vie.max - data.sante.vie.value);
if (data.sante.fatigue) {
// Creatures n'ont pas de fatigue
state += RdDUtility.currentFatigueMalus(data.sante.fatigue.value, data.sante.endurance.max);
}
// Ajout de l'éthylisme
state += Math.min(0, (data.compteurs.ethylisme?.value ?? 0));
data.compteurs.etat.value = state;
if (data.compteurs && data.compteurs.surenc) {
data.compteurs.surenc.value = - this.detectSurEncombrement();
}
}
/* -------------------------------------------- */
async ajouterRefoulement(value = 1) {
let ret = "none";
let refoulement = duplicate(this.data.data.reve.refoulement);
refoulement.value = refoulement.value + value;
let total = new Roll("1d20").roll().total;
if (total <= refoulement.value) {
refoulement.value = 0;
this.ajouterSouffle({ chat: true });
ret = "souffle";
}
await this.update({ "data.reve.refoulement": refoulement });
return ret;
}
/* -------------------------------------------- */
async ajouterSouffle(options = { chat: false }) {
let souffle = await RdDRollTables.getSouffle();
await this.createOwnedItem(souffle);
if (options.chat) {
ChatMessage.create({
whisper: ChatMessage.getWhisperRecipients([ "GM", game.user.name] ),
content: this.name + " subit un Souffle de Dragon : " + souffle.name
});
}
return souffle;
}
/* -------------------------------------------- */
async ajouterQueue(options = { chat: false }) {
// TODO: Déterminer si Thanatos a été utilisé? => laisser le joueur ne pas choisir Thanatos => choisir sa voie?
let utiliseThanatos = false;
let queue;
if (utiliseThanatos) {
queue = await RdDRollTables.getOmbre();
// mettre à jour: plus d'ombre en vue
}
else {
queue = await RdDRollTables.getQueue();
}
await this.createOwnedItem(queue);
if (options.chat) {
ChatMessage.create({
whisper: ChatMessage.getWhisperRecipients([ "GM", game.user.name] ),
content: this.name + " subit une Queue de Dragon : " + queue.name
});
}
return queue.name;
}
/* -------------------------------------------- */
displayTMRQueueSouffleInformation() {
for (let item of this.data.items) {
let content
if (item.type == 'queue') {
if (item.name.toLowerCase() == 'conquête') {
content = "RAPPEL ! Vous souffrez d'une <strong>Conquête</strong> : " + item.data.description;
}
else if (item.name.toLowerCase() == 'pélerinage') {
content = "RAPPEL ! Vous souffrez d'un <strong>Pélerinage</strong> : " + item.data.description;
}
else if (item.name.toLowerCase() == 'urgence draconique') {
content = "RAPPEL ! Vous souffrez d'une <strong>Urgence Draconique</strong> : " + item.data.description;
}
} else if (item.type == 'souffle') {
if (item.name.toLowerCase() == 'périple') {
content = "RAPPEL ! Vous souffrez du Souffle <strong>Périple</strong>. Vous devez gérer manuellement le détail du Périple.<br>" + item.data.description;
} else if (item.name.toLowerCase() == 'fermeture des cités') {
content = "RAPPEL ! Vous souffrez du Souffle <strong>Fermeture des Cités</strong>. Vous devez gérer manuellement le détail des Citées ré-ouvertes.<br>" + item.data.description;
} else if (item.name.toLowerCase() == 'désorientation') {
content = "RAPPEL ! Vous souffrez du Souffle <strong>Désorientation</strong>. Vous devez gérer avec votre MJ les effets de ce souffle.<br>" + item.data.description;
} else if (item.name.toLowerCase() == 'double résistance du fleuve') {
content = "RAPPEL ! Vous souffrez du Souffle <strong>Double Résistance du Fleuve</strong>. Vous devez gérer avec votre MJ les effets de ce souffle.<br>" + item.data.description;
}
}
if (content) {
ChatMessage.create({
whisper: ChatMessage.getWhisperRecipients([ "GM", game.user.name] ),
content: content
});
}
}
}
/* -------------------------------------------- */
async deleteTMRRencontreAtPosition() {
let rencontres = duplicate(this.data.data.reve.rencontre);
let len = rencontres.list.length;
let i = 0;
//console.log("List", rencontres, len);
let newTable = [];
for (i = 0; i < len; i++) {
if (rencontres.list[i].coord != this.data.data.reve.tmrpos.coord)
newTable.push(rencontres.list[i]);
}
if (newTable.length != len) {
rencontres.list = newTable;
//console.log("Result: ", rencontres);
await this.update({ "data.reve.rencontre": rencontres });
}
}
/* -------------------------------------------- */
async addTMRRencontre(currentRencontre) {
let rencontres = duplicate(this.data.data.reve.rencontre);
let len = rencontres.list.length;
let i = 0;
let already = false;
for (i = 0; i < len; i++) {
if (rencontres.list[i].coord == this.data.data.reve.tmrpos.coord)
already = true;
}
if (!already) {
rencontres.list.push({ coord: this.data.data.reve.tmrpos.coord, rencontre: currentRencontre });
await this.update({ "data.reve.rencontre": rencontres });
}
}
/* -------------------------------------------- */
async updateCoordTMR(coord) {
let tmrPos = duplicate(this.data.data.reve.tmrpos);
tmrPos.coord = coord;
await this.update({ "data.reve.tmrpos": tmrPos });
}
/* -------------------------------------------- */
async reveActuelIncDec(value) {
let reve = duplicate(this.data.data.reve.reve);
reve.value = Math.max(reve.value + value, 0);
await this.update({ "data.reve.reve": reve });
}
/* -------------------------------------------- */
async updatePointDeSeuil(value = 1) {
const seuil = Misc.toInt(this.data.data.reve.seuil.value);
const reve = Misc.toInt(this.data.data.carac.reve.value);
if (seuil < reve) {
await this.setPointsDeSeuil(Math.min(seuil + value, reve));
}
}
/* -------------------------------------------- */
async setPointsDeSeuil(value) {
let seuil = duplicate(this.data.data.reve.seuil);
seuil.value = value;
await this.update({ "data.reve.seuil": seuil });
}
/* -------------------------------------------- */
async setPointsDeChance(value) {
let chance = duplicate(this.data.data.compteurs.chance);
chance.value = value;
await this.update({ "data.compteurs.chance": chance });
}
/* -------------------------------------------- */
getSonne() {
return !this.isEntiteCauchemar() && (this.data.data.sante.sonne?.value ?? false);
}
async setSonne(sonne = true) {
if (this.isEntiteCauchemar()) {
return;
}
await this.setStatusSonne(sonne);
await this.setStateSonne(sonne);
}
async setStateSonne(sonne) {
if (this.isEntiteCauchemar()) {
return;
}
await this.update({ "data.sante.sonne.value": sonne });
}
/* -------------------------------------------- */
getSConst() {
if (this.isEntiteCauchemar()) {
return 0;
}
return this.data.data.attributs?.sconst?.value ?? 0;
}
/* -------------------------------------------- */
async testSiSonne(sante, endurance) {
const roll = new Roll("1d20").roll();
roll.showDice = true;
RdDDice.show(roll);
let result = {
roll: roll,
sonne: roll.total > endurance || roll.total == 20
}
if (roll.total == 1) {
let xp = Misc.toInt(this.data.data.carac.constitution.xp) + 1;
this.update({ "data.carac.constitution.xp": xp }); // +1 XP !
// TODO : Output to chat
}
if (result.sonne) {
// 20 is always a failure
await this.setSonne();
sante.sonne.value = true;
}
return result;
}
/* -------------------------------------------- */
countBlessures(blessuresListe) {
return blessuresListe.filter(b => b.active).length
}
/* -------------------------------------------- */
countBlessuresByName(name) {
return this.countBlessures(this.data.data.blessures[name].liste);
}
/* -------------------------------------------- */
async jetVie() {
let myRoll = new Roll("1d20").roll();
myRoll.showDice = true;
await RdDDice.show(myRoll);
let msgText = "Jet de Vie : " + myRoll.total + " / " + this.data.data.sante.vie.value + "<br>";
if (myRoll.total <= this.data.data.sante.vie.value) {
msgText += "Jet réussi, pas de perte de point de vie (prochain jet dans 1 round pour 1 critique, SC minutes pour une grave)";
if (myRoll.total == 1) {
msgText += "La durée entre 2 jets de vie est multipliée par 20 (20 rounds pour une critique, SCx20 minutes pour une grave)";
}
} else {
msgText += "Jet échoué, vous perdez 1 point de vie";
await this.santeIncDec("vie", -1);
if (myRoll.total == 20) {
msgText += "Votre personnage est mort !!!!!";
}
}
const message = {
content: msgText,
whisper: ChatMessage.getWhisperRecipients(game.user.name)
};
ChatMessage.create(message);
}
/* -------------------------------------------- */
async santeIncDec(name, inc, isCritique = false) {
const sante = duplicate(this.data.data.sante);
let compteur = sante[name];
if (!compteur) {
return ;
}
let result = {
sonne: false,
};
let minValue = 0;
if (this.type == 'personnage') {
// TODO: les animaux/humanoïdes on théoriquement aussi un sconst, mais la SPA n'est pas passé par là
minValue = name == "vie" ? -Number(this.data.data.attributs.sconst.value) : 0;
}
result.newValue = Math.max(minValue, Math.min(compteur.value + inc, compteur.max));
//console.log("New value ", inc, minValue, result.newValue);
let fatigue = 0;
if (name == "endurance" && this.data.type != 'entite') {
if (!isCritique && result.newValue == 0 && inc < 0) { // perte endurance et endurance devient 0 -> -1 vie sauf si coup critique
sante.vie.value = sante.vie.value - 1;
}
result.newValue = Math.max(0, result.newValue);
if (inc > 0) { // le max d'endurance s'applique seulement à la récupération
result.newValue = Math.min(result.newValue, this._computeEnduranceMax())
}
const perte = compteur.value - result.newValue;
if (perte > 1) {
// Peut-être sonné si 2 points d'endurance perdus d'un coup
const testIsSonne = await this.testSiSonne(sante, result.newValue);
result.sonne = testIsSonne.sonne;
result.jetEndurance = testIsSonne.roll.total;
} else if (inc > 0) {
await this.setSonne(false);
}
if (sante.fatigue && inc < 0) { // Each endurance lost -> fatigue lost
fatigue = perte;
}
}
compteur.value = result.newValue;
//console.log(name, inc, data.value, result.newValue, minValue, data.max);
// If endurance lost, then the same amount of fatigue cannot be recovered
if (sante.fatigue && fatigue > 0) {
sante.fatigue.value = Math.max(sante.fatigue.value + fatigue, this._computeFatigueMin());
}
await this.update({ "data.sante": sante });
return result;
}
/* -------------------------------------------- */
_computeFatigueMin() {
return this.data.data.sante.endurance.max - this.data.data.sante.endurance.value;
}
/* -------------------------------------------- */
_computeEnduranceMax() {
let blessures = this.data.data.blessures;
let diffVie = this.data.data.sante.vie.max - this.data.data.sante.vie.value;
let maxEndVie = this.data.data.sante.endurance.max - (diffVie * 2);
let nbGraves = this.countBlessures(blessures.graves.liste);
let nbCritiques = this.countBlessures(blessures.critiques.liste);
let maxEndGraves = Math.floor(this.data.data.sante.endurance.max / (2 * nbGraves));
let maxEndCritiques = nbCritiques > 0 ? 1 : this.data.data.sante.endurance.max;
return Math.max(0, Math.min(maxEndVie, maxEndGraves, maxEndCritiques));
}
/* -------------------------------------------- */
async manageBlessureFromSheet(bType, index, active) {
let bList = duplicate(this.data.data.blessures);
let blessure = bList[bType + "s"].liste[index];
blessure.active = !blessure.active;
if (!blessure.active) {
blessure.premiers_soins = 0;
blessure.soins_complets = 0;
blessure.jours = 0;
blessure.loc = "";
}
//console.log("Blessure update", bType, index, blessure, bList );
await this.update({ 'data.blessures': bList });
}
/* -------------------------------------------- */
async setDataBlessureFromSheet(bType, index, psoins, pcomplets, jours, loc) {
let bList = duplicate(this.data.data.blessures);
let blessure = bList[bType + "s"].liste[index];
blessure.premiers_soins = psoins;
blessure.soins_complets = pcomplets;
blessure.jours = jours;
blessure.loc = loc;
await this.update({ 'data.blessures': bList });
}
/* -------------------------------------------- */
async jetDeMoral(situation) {
let jetMoral = new Roll("1d20").roll();
RdDDice.show(jetMoral);
let moralActuel = Misc.toInt(this.data.data.compteurs.moral.value);
const difficulte = 10 + moralActuel;
const succes = jetMoral.total <= difficulte;
let ajustementMoral = this._calculAjustementMoral(succes, moralActuel, situation);
await this.moralIncDec(ajustementMoral);
ChatMessage.create({
whisper: ChatUtility.getWhisperRecipientsAndGMs(game.user.name),
content: `Jet de moral ${succes ? "réussi" : "manqué"} en situation ${situation} (${jetMoral.total}/${difficulte}), vous ${ajustementMoral > 0 ? "gagnez du moral" : ajustementMoral < 0 ? "perdez du moral" : "gardez votre moral"}`
});
return ajustementMoral;
}
/* -------------------------------------------- */
async moralIncDec(ajustementMoral) {
let compteurs = duplicate(this.data.data.compteurs);
compteurs.moral.value = Misc.toInt(compteurs.moral.value);;
if (ajustementMoral != 0) {
compteurs.moral.value += ajustementMoral;
if (compteurs.moral.value > 3) {
// exaltation
compteurs.moral.value--;
compteurs.exaltation.value = Misc.toInt(compteurs.exaltation.value) + 1;
}
if (compteurs.moral.value < -3) {
// dissolution
compteurs.moral.value++;
compteurs.dissolution.value = Misc.toInt(compteurs.dissolution.value) + 1;
}
await this.update({ 'data.compteurs': compteurs });
}
}
/* -------------------------------------------- */
_calculAjustementMoral(succes, moral, situation) {
switch (situation) {
case 'heureuse': return succes ? 1 : 0;
case 'malheureuse': return succes ? 0 : -1;
case 'neutre':
if (succes && moral <= 0) return 1;
if (!succes && moral > 0) return -1;
}
return 0;
}
/* -------------------------------------------- */
async setEthylisme(degre) {
let ethylisme = duplicate(this.data.data.compteurs.ethylisme);
ethylisme.value = degre;
ethylisme.nb_doses = 0;
if (degre == 1) {
ethylisme.jet_moral = false;
}
await this.update({ "data.compteurs.ethylisme": ethylisme });
}
/* -------------------------------------------- */
async ethylismeTest() {
let rollData = {
vieValue: this.data.data.sante.vie.value,
etat: this.getEtatGeneral() - Math.min(0, this.data.data.compteurs.ethylisme.value), // Pour les jets d'Ethylisme, on ignore le degré d'éthylisme (p.162)
diffNbDoses: -Number(this.data.data.compteurs.ethylisme.nb_doses || 0),
finalLevel: 0,
diffConditions: 0,
ajustementsConditions: CONFIG.RDD.ajustementsConditions,
forceAlcool: 0
}
let html = await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/dialog-roll-ethylisme.html', rollData);
new RdDRollDialogEthylisme(html, rollData, this).render(true);
}
/* -------------------------------------------- */
async performEthylisme(rollData) {
let ethylisme = duplicate(this.data.data.compteurs.ethylisme);
// Je d'ethylisme
let rollEthylisme = await RdDResolutionTable.roll(rollData.vieValue, rollData.finalLevel);
let msgText = RdDResolutionTable.explain(rollEthylisme) + "<br>";
if (rollEthylisme.isSuccess) {
ethylisme.nb_doses++;
msgText += `Vous avez réussi votre jet d'éthylisme, vous avez consommé ${ethylisme.nb_doses} doses sans effet.`;
} else {
ethylisme.value = Math.max(ethylisme.value - 1, -7);
ethylisme.nb_doses = 0;
let enduranceLost = new Roll("1d6").roll().total;
await this.santeIncDec("endurance", -enduranceLost);
const ajustementEthylique = ethylisme.value;
// Qui a bu boira (p 164)
let rollVolonte = await RdDResolutionTable.roll(this.data.data.carac.volonte.value, Math.min(ajustementEthylique, 0) + this.data.data.compteurs.moral.value);
msgText += `Vous avez échoué à votre jet d'éthylisme, vous êtes maintenant ${RdDUtility.getNomEthylisme(ajustementEthylique)} (${ajustementEthylique}).`
msgText += "<br>" + RdDResolutionTable.explain(rollVolonte) + "<br>";
msgText += "Qui a bu boira : " + (rollVolonte.isSuccess
? "vous êtes libre de continuer à boire ou pas."
: "vous avez une envie irrépréssible de reprendre un verre.");
}
ChatMessage.create({
content: msgText,
whisper: ChatUtility.getWhisperRecipientsAndGMs(game.user.name)
});
if (rollEthylisme.isEchec) {
await this._jetDeMoralEthylique(ethylisme);
}
await this.update({ 'data.compteurs.ethylisme': ethylisme });
}
/* -------------------------------------------- */
async _jetDeMoralEthylique(ethylisme) {
if (ethylisme.value >= -1 && !ethylisme.jet_moral) {
let adjust = await this.jetDeMoral('heureuse');
if (adjust > 0 || ethylisme.value == -1) {
ethylisme.jet_moral = true;
}
if (ethylisme.value == -1 && adjust <= 0) {
// alcool triste
ChatMessage.create({
content: "Décidément, vous avez l'alcool triste, vous perdez finalement un point de moral!",
whisper: ChatUtility.getWhisperRecipientsAndGMs(game.user.name)
});
this.moralIncDec(-1);
}
}
}
/* -------------------------------------------- */
async stressTest() {
const message = {
content: "",
whisper: ChatUtility.getWhisperRecipientsAndGMs(game.user.name)
};
await this.transformerStress(message);
ChatMessage.create(message);
}
/* -------------------------------------------- */
async transformerStress(message) {
let compteurs = duplicate(this.data.data.compteurs);
const stress = Misc.toInt(compteurs.stress.value);
if (stress <= 0) {
return false;
}
let stressRoll = await this._stressRoll();
let convertis = Math.floor(stress * stressRoll.factor);
compteurs.stress.value = Math.max(stress - convertis - 1, 0);
let dissolution = Math.max(0, Misc.toInt(compteurs.dissolution.value));
let exaltation = Math.max(0, Misc.toInt(compteurs.exaltation.value));
const annule = Math.min(dissolution, exaltation);
dissolution -= annule;
exaltation -= annule;
if (dissolution > 0) {
const perdus = Math.min(dissolution, convertis);
convertis -= perdus;
dissolution -= perdus;
}
compteurs.experience.value += convertis + exaltation;
compteurs.dissolution.value = dissolution;
compteurs.exaltation.value = 0;
message.content += "<br>Vous transformez " + convertis + " points de Stress en Expérience" + stressRoll.comment;
await this.update({ "data.compteurs": compteurs });
return true;
}
/* -------------------------------------------- */
async _stressRoll() {
let reveActuel = this.getReveActuel();
let result = await RdDResolutionTable.roll(reveActuel, 0);
console.log("_stressRoll", result);
switch (result.code) {
case "sign": return { factor: 0.75, comment: " (75%): " + result.quality + " - " + result.roll + " sur " + result.score + "%" }
case "norm": return { factor: 0.5, comment: " (50%): " + result.quality + " - " + result.roll + " sur " + result.score + "%" }
case "echec": return { factor: 0.2, comment: " (20%): " + result.quality + " - " + result.roll + " sur " + result.score + "%" }
case "epart": return { factor: 0.1, comment: " (10%): " + result.quality + " - " + result.roll + " sur " + result.score + "%" }
case "etotal": return { factor: 0, comment: " (0%): " + result.quality + " - " + result.roll + " sur " + result.score + "%" }
case "part":
{
let second = await RdDResolutionTable.roll(reveActuel, 0);
console.log("_stressRoll", second);
switch (second.code) {
case "part": case "sign":
return { factor: 1.5, comment: " (150%): Double Particulière - " + result.roll + " puis " + second.roll + " sur " + result.score + "%" }
default:
return { factor: 1, comment: " (100%): " + result.quality + " - " + result.roll + " puis " + second.roll + " sur " + result.score + "%" }
}
}
}
}
/* -------------------------------------------- */
createCallbackExperience() {
return {
condition: r => r.rolled.isPart && r.finalLevel < 0 && game.settings.get("core", "rollMode") != 'selfroll',
action: r => this._appliquerAjoutExperience(r, game.settings.get("core", "rollMode") != 'blindroll')
};
}
/* -------------------------------------------- */
async checkCaracXP(caracName) {
let carac = this.data.data.carac[caracName];
if (carac && carac.xp > 0) {
let xpNeeded = RdDUtility.getCaracNextXp(carac.value+1);
if (carac.xp >= xpNeeded) {
carac = duplicate(carac);
carac.value = Number(carac.value) + 1;
let xpData = {
alias: this.name,
carac: caracName,
value: carac.value,
xp: carac.xp
}
ChatUtility.createChatMessage(this.name, "default", {
content: await renderTemplate(`systems/foundryvtt-reve-de-dragon/templates/chat-actor-carac-xp.html`, xpData)
});
}
}
}
/* -------------------------------------------- */
async checkCompetenceXP(compName, newXP = undefined) {
let competence = RdDItemCompetence.findCompetence(this.data.items, compName);
if ( competence && newXP && newXP == competence.data.xp ) { // Si édition, mais sans changement XP
return;
}
newXP = (newXP) ? newXP : competence.data.xp;
if (competence && newXP > 0) {
let xpNeeded = RdDItemCompetence.getCompetenceNextXp(competence.data.niveau+1);
if (newXP >= xpNeeded) {
let newCompetence = duplicate(competence);
newCompetence.data.niveau += 1;
newCompetence.data.xp = newXP;
let xpData = {
alias: this.name,
competence: newCompetence.name,
niveau: newCompetence.data.niveau,
xp: newCompetence.data.xp,
archetype: newCompetence.data.niveau_archetype,
archetypeWarning: newCompetence.data.niveau > competence.data.niveau_archetype
}
ChatUtility.createChatMessage(this.name, "default", {
content: await renderTemplate(`systems/foundryvtt-reve-de-dragon/templates/chat-actor-competence-xp.html`, xpData)
});
}
}
}
/* -------------------------------------------- */
async _appliquerAjoutExperience(rollData, display = true) {
if (!this.isPersonnage()) return;
let xpResult = this.appliquerExperience(rollData.rolled, rollData.selectedCarac.label, rollData.competence);
if (display && xpResult.result) {
let xpmsg = "<br>Points d'expérience gagnés ! Carac: " + xpResult.xpCarac + ", Comp: " + xpResult.xpCompetence;
let message = {
whisher: ChatMessage.getWhisperRecipients( ["GM", this.name ] ),
content: "<strong>" + rollData.selectedCarac.label + "</strong>" + xpmsg,
}
ChatMessage.create(message);
}
if (xpResult && xpResult.xpComp > 0 && rollData.competence) {
this.checkCompetenceXP(rollData.competence.name);
}
if (xpResult && xpResult.xpCarac > 0 && rollData.selectedCarac) {
this.checkCaracXP(rollData.selectedCarac.name);
}
}
/* -------------------------------------------- */
async rollUnSort(coord) {
let sortList = duplicate(this.getSortList()); // Duplication car les pts de reve sont modifiés dans le sort
if (!sortList || sortList.length == 0) {
ui.notifications.info("Aucun sort disponible!");
return;
}
if (this.currentTMR) this.currentTMR.minimize(); // Hide
let rollData = {
selectedCarac: this.data.data.carac.reve,
draconicList: this.getDraconicList(),
sortList: sortList,
competence: this.getBestDraconic(),
selectedSort: sortList[0],
coord: coord,
coordLabel: TMRUtility.getTMRDescription(coord).label,
diffLibre: sortList[0].data.difficulte, // Per default at startup
coutreve: Array(20).fill().map((item, index) => 1 + index)
}
const dialog = await RdDRoll.create(this, rollData,
{
html: 'systems/foundryvtt-reve-de-dragon/templates/dialog-roll-sort.html',
close: html => { this.currentTMR.maximize() } // Re-display TMR
},
{
name: 'lancer-un-sort',
label: 'Lancer un sort',
callbacks: [
this.createCallbackExperience(),
{ action: r => this._rollUnSortResult(r, false) }
]
},
{
name: 'mettre-en-reserve',
label: 'Mettre un sort en réserve',
callbacks: [
this.createCallbackExperience(),
{ action: r => this._rollUnSortResult(r, true) }
]
}
);
dialog.render(true);
}
/* -------------------------------------------- */
isRencontreSpeciale() { // Gestion queue/souffle 'Mauvaise Rencontre en Perpective'
let addMsg = "";
let rencSpecial = this.data.items.find(item => (item.type == 'queue' || item.type == 'souffle') && item.name.toLowerCase().includes('mauvaise rencontre'));
if (rencSpecial) {
rencSpecial = duplicate(rencSpecial); // To keep it
if (rencSpecial.type == 'queue') {
this.deleteOwnedItem(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 manuellement.";
}
ChatMessage.create({
content: "Vous êtes sous le coup d'une Mauvaise Rencontre en Persective." + addMsg,
whisper: ChatMessage.getWhisperRecipients(game.user.name)
});
}
return rencSpecial;
}
/* -------------------------------------------- */
getTMRFatigue() { // Pour l'instant uniquement Inertie Draconique
let inertieDraconique = this.data.items.find(item => item.type == 'queue' && item.name.toLowerCase().includes('inertie draconique'));
if (inertieDraconique) {
ChatMessage.create({
content: "Vous êtes sous le coup d'une Inertie Draconique : vous perdez 2 cases de Fatigue par déplacement au lieu d'1.",
whisper: ChatMessage.getWhisperRecipients(game.user.name)
});
return 2;
}
return 1;
}
/* -------------------------------------------- */
isConnaissanceFleuve() {
return this.data.items.find(item => item.type == 'tete' && item.name.toLowerCase().includes('connaissance du fleuve'));
}
/* -------------------------------------------- */
isReserveEnSecurite() {
let reserveSecurite = this.data.items.find(item => item.type == 'tete' && item.name.toLowerCase().includes(' en sécurité'));
return reserveSecurite;
}
/* -------------------------------------------- */
isDoubleResistanceFleuve() {
let resistFleuve = this.data.items.find(item => item.type == 'souffle' && item.name.toLowerCase().includes('résistance du fleuve'));
if (resistFleuve) {
ChatMessage.create({
content: "Vous êtes sous le coup d'une Double Résistance du Fleuve : vous devez maîtriser 2 fois chaque case humide, un second jet est donc effectué.",
whisper: ChatMessage.getWhisperRecipients(game.user.name)
});
return true;
}
return false;
}
/* -------------------------------------------- */
async checkSoufflePeage(cellDescr) {
let peage = this.data.items.find(item => item.type == 'souffle' && item.name.toLowerCase().includes('péage'));
if (peage && (cellDescr.type == 'pont' || cellDescr.type == 'cite')) {
await this.reveActuelIncDec(-1);
ChatMessage.create({
content: "Vous êtes sous le coup d'un Péage : l'entrée sur cette case vous coûte 1 Point de Rêve (déduit automatiquement).",
whisper: ChatMessage.getWhisperRecipients(game.user.name)
});
}
}
/* -------------------------------------------- */
checkTeteDeplacementAccelere() {
let deplAccelere = this.data.items.find(item => item.type == 'tete' && item.name.toLowerCase().includes(' déplacement accéléré'));
if (deplAccelere) {
return true;
}
return false;
}
/* -------------------------------------------- */
checkIsAdditionnalHumide(cellDescr, coordTMR) {
let pontHumide = this.data.items.find(item => item.type == 'souffle' && item.name.toLowerCase().includes(' des ponts'));
if (pontHumide && cellDescr.type == 'pont') {
ChatMessage.create({
content: "Vous êtes sous le coup d'une Impraticabilité des Ponts : ils doivent être maîtrisés comme des cases humides.",
whisper: ChatMessage.getWhisperRecipients(game.user.name)
});
return true;
}
// Débordement ?
let debordementList = this.data.items.filter(item => item.type == 'casetmr' && item.data.specific == 'debordement');
for (let caseTMR of debordementList) {
if (caseTMR.data.coord == coordTMR)
return true;
}
return false;
}
/* -------------------------------------------- */
async _rollUnSortResult(rollData, isSortReserve = false) {
let rolled = rollData.rolled;
let selectedSort = rollData.selectedSort;
let closeTMR = !isSortReserve;
if (selectedSort.data.isrituel && isSortReserve) {
ui.notifications.error("Impossible de mettre le rituel '" + selectedSort.name + "' en réserve");
this.currentTMR.close(); // Close TMR !
return;
}
rollData.isSortReserve = isSortReserve;
rollData.show = {}
rollData.depenseReve = Number(selectedSort.data.ptreve_reel);
let myReve = duplicate(this.data.data.reve.reve);
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 (myReve.value > rollData.depenseReve) {
// Incrémenter/gére le bonus de case
RdDItemSort.incrementBonusCase(this, selectedSort, rollData.coord);
if (rollData.isSortReserve) {
await this.sortMisEnReserve(rollData, selectedSort);
closeTMR = false;
}
}
else {
rollData.depenseReve = 0;
rollData.show.reveInsuffisant = true;
mergeObject(rollData.rolled, RdDResolutionTable.getResultat("echec"), { overwrite: true });
}
} else {
if (rolled.isETotal) { // Echec total !
rollData.depenseReve = Math.min(myReve.value, Math.floor(rollData.depenseReve * 1.5))
// TODO: mise en réserve d'un échec total...
} else {
rollData.depenseReve = 0
}
}
myReve.value = Math.max(myReve.value - rollData.depenseReve, 0);
await this.update({ "data.reve.reve": myReve });
if (closeTMR) {
this.currentTMR.close(); // Close TMR !
} else {
this.currentTMR.maximize(); // Re-display TMR
}
// Final chat message
RdDResolutionTable.displayRollData(rollData, this, 'chat-resultat-sort.html');
if (myReve.value == 0) { // 0 points de reve
ChatMessage.create({ content: this.name + " est réduit à 0 Points de Rêve, et tombe endormi !" });
closeTMR = true;
}
}
/* -------------------------------------------- */
async rollCarac(caracName) {
let rollData = { selectedCarac: this.getCaracByName(caracName) };
const dialog = await RdDRoll.create(this, rollData,
{ html: 'systems/foundryvtt-reve-de-dragon/templates/dialog-roll-carac.html' },
{
name: 'jet-' + caracName,
label: 'Jet ' + Grammar.apostrophe('de', rollData.selectedCarac.label),
callbacks: [
this.createCallbackExperience(),
{ action: r => this._onRollCaracResult(r) }
]
}
);
dialog.render(true);
}
/* -------------------------------------------- */
async _onRollCaracResult(rollData) {
// Final chat message
RdDResolutionTable.displayRollData(rollData, this, 'chat-resultat-general.html');
}
/* -------------------------------------------- */
async rollCompetence(name) {
let rollData = { competence: this.getCompetence(name) }
if (rollData.competence.type == 'competencecreature') {
if (rollData.competence.data.iscombat) {
const arme = RdDItemCompetenceCreature.toArme(competence);
RdDCombat.createUsingTarget(this).attaque(competence, arme);
return;
}
// Fake competence pour créature
RdDItemCompetenceCreature.setRollDataCreature(rollData);
}
else {
rollData.carac = this.data.data.carac;
}
console.log("rollCompetence !!!", rollData);
const dialog = await RdDRoll.create(this, rollData, { html: 'systems/foundryvtt-reve-de-dragon/templates/dialog-competence.html' }, {
name: 'jet-competence',
label: 'Jet ' + Grammar.apostrophe('de', name),
callbacks: [
this.createCallbackExperience(),
{ action: r => this._competenceResult(r) }
]
});
dialog.render(true);
}
/* -------------------------------------------- */
async creerTacheDepuisLivre(item) {
let tache = {
name: "Lire " + item.name, type: 'tache',
data: {
carac: 'intellect',
competence: 'Ecriture',
difficulte: item.data.data.difficulte,
periodicite: "60 minutes",
fatigue: 2,
points_de_tache: item.data.data.points_de_tache,
points_de_tache_courant: 0,
description: "Lecture du livre " + item.name +
" - XP : " + item.data.data.xp + " - Compétences : " + item.data.data.competence
}
}
await this.createOwnedItem(tache, { renderSheet: true });
}
/* -------------------------------------------- */
async rollTache(id) {
let tache = duplicate(this.getTache(id));
let competence = duplicate(this.getCompetence(tache.data.competence));
competence.data.defaut_carac = tache.data.carac; // Patch !
let rollData = {
competence: competence,
tache: tache,
diffConditions: tache.data.difficulte,
use: { libre: false, conditions: false},
carac: {}
};
rollData.carac[tache.data.carac] = duplicate(this.data.data.carac[tache.data.carac]); // Single carac
console.log("rollTache !!!", rollData);
const dialog = await RdDRoll.create(this, rollData, { html: 'systems/foundryvtt-reve-de-dragon/templates/dialog-competence.html' }, {
name: 'jet-competence',
label: 'Jet de Tâche ' + tache.name,
callbacks: [
this.createCallbackExperience(),
{ condition: r => r.rolled.isETotal, action: r => this._tacheETotal(r) },
{ action: r => this._tacheResult(r) }
]
});
dialog.render(true);
}
/* -------------------------------------------- */
async _tacheResult(rollData) {
// Mise à jour de la tache
rollData.tache.data.points_de_tache_courant += rollData.rolled.ptTache;
this.updateEmbeddedEntity("OwnedItem", rollData.tache);
this.santeIncDec("fatigue", rollData.tache.data.fatigue);
RdDResolutionTable.displayRollData(rollData, this, 'chat-resultat-tache.html');
}
/* -------------------------------------------- */
_tacheETotal(rollData) {
rollData.tache.data.difficulte--;
this.updateEmbeddedEntity("OwnedItem", rollData.tache);
}
/* -------------------------------------------- */
async rollMeditation(id) {
let meditation = duplicate(this.getMeditation(id));
let competence = duplicate(this.getCompetence(meditation.data.competence));
competence.data.defaut_carac = "intellect"; // Meditation = tjs 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: {}
};
meditationData.carac["intellect"] = duplicate(this.data.data.carac["intellect"]);
console.log("rollMeditation !!!", meditationData);
const dialog = await RdDRoll.create(this, meditationData, { html: 'systems/foundryvtt-reve-de-dragon/templates/dialog-roll-meditation.html' }, {
name: 'jet-meditation',
label: 'Jet de Meditation ' + meditation.name,
height: 600,
callbacks: [
this.createCallbackExperience(),
{ condition: r => r.rolled.isETotal, action: r => this._meditationETotal(r) },
{ action: r => this._meditationResult(r) }
]
});
dialog.render(true);
}
/* -------------------------------------------- */
async _meditationResult(meditationData) {
this.santeIncDec("fatigue", 2);
meditationData.diffLecture = -7;
if (meditationData.rolled.isPart)
meditationData.diffLecture = 0;
else if (meditationData.rolled.isSign)
meditationData.diffLecture = -3;
RdDResolutionTable.displayRollData(meditationData, this.name, 'chat-resultat-meditation.html');
}
/* -------------------------------------------- */
_meditationETotal(meditationData) {
meditationData.meditation.data.malus--;
this.updateEmbeddedEntity("OwnedItem", meditationData.meditation);
}
/* -------------------------------------------- */
async _competenceResult(rollData) {
RdDResolutionTable.displayRollData(rollData, this, 'chat-resultat-competence.html')
}
/* -------------------------------------------- */
async rollAppelChance(onSuccess = () => { }, onEchec = () => { }) {
let rollData = { selectedCarac: this.getCaracByName('chance-actuelle'), surprise: '' };
const dialog = await RdDRoll.create(this, rollData,
{ html: 'systems/foundryvtt-reve-de-dragon/templates/dialog-roll-carac.html' },
{
name: 'appelChance',
label: 'Appel à la chance',
callbacks: [
this.createCallbackExperience(),
{ action: r => this._appelChanceResult(r, onSuccess, onEchec) },
]
}
);
dialog.render(true);
}
/* -------------------------------------------- */
async _appelChanceResult(rollData, onSuccess = () => { }, onEchec = () => { }) {
await RdDResolutionTable.displayRollData(rollData, this, 'chat-resultat-appelchance.html')
if (rollData.rolled.isSuccess) {
await this.chanceActuelleIncDec(-1)
onSuccess();
}
else {
onEchec();
}
}
/* -------------------------------------------- */
async chanceActuelleIncDec(value) {
let chance = duplicate(this.data.data.compteurs.chance);
chance.value = Math.max(chance.value + value, 0);
await this.update({ "data.compteurs.chance": chance });
}
/* -------------------------------------------- */
async appelDestinee(onSuccess = () => { }, onEchec = () => { }) {
if (this.data.data.compteurs.destinee?.value ?? 0 > 0) {
ChatMessage.create({ content: `<span class="rdd-roll-part">${this.name} a fait appel à la Destinée !</span>` });
let destinee = duplicate(this.data.data.compteurs.destinee);
destinee.value = destinee.value - 1;
await this.update({ "data.compteurs.destinee": destinee });
onSuccess();
}
else {
onEchec();
}
}
/* -------------------------------------------- */
ajustementAstrologique() {
if (this.isCreature()) {
return 0;
}
// selon l'heure de naissance...
return game.system.rdd.calendrier.getAjustementAstrologique(this.data.data.heure, this.data.name);
}
/* -------------------------------------------- */
checkDesirLancinant() {
let queue = this.data.items.filter((item) => item.name.toLowerCase().includes('lancinant'));
return (queue.length > 0);
}
/* -------------------------------------------- */
async appliquerExperience(rolled, caracName, competence = undefined) {
if (rolled.isPart && rolled.finalLevel < 0) {
// Cas de désir lancinant, pas d'expérience sur particulière
if (this.checkDesirLancinant()) {
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(game.user.name)
});
return { result: false, xpcarac: 0, xpCompetence: 0 };
}
if (caracName == 'derobee') caracName = 'agilite';
let xp = Math.abs(rolled.finalLevel);
let xpCarac = Math.floor(xp / 2); // impair: arrondi inférieur en carac
let xpComp = 0;
if (competence) {
xpComp = xp - xpCarac;
competence = duplicate(competence);
competence.data.xp = Misc.toInt(competence.data.xp) + xpComp;
await this.updateEmbeddedEntity("OwnedItem", competence);
} else {
xpCarac = Math.max(xpCarac, 1);
}
if (xpCarac > 0) {
let carac = duplicate(this.data.data.carac);
let selectedCarac = RdDActor._findCaracByName(carac, caracName);
if (!selectedCarac.derivee) {
selectedCarac.xp = Misc.toInt(selectedCarac.xp) + xpCarac;
await this.update({ "data.carac": carac });
} else {
ChatMessage.create({
content: `Vous avez ${xpCarac} à répartir pour la caractérisque dérivée ${caracName}. Vous devez le faire manuellement.`,
whisper: ChatMessage.getWhisperRecipients(game.user.name)
});
}
}
return { result: true, xpCarac: xpCarac, xpCompetence: xpComp }; //XP
}
return { result: false, xpCarac: 0, xpCompetence: 0 }; // Pas d'XP
}
/* -------------------------------------------- */
async ajouteNombreAstral(data) {
// Gestion expérience (si existante)
data.competence = RdDItemCompetence.findCompetence(this.data.items, "astrologie");
data.selectedCarac = this.data.data.carac["vue"];
this._appliquerAjoutExperience(data);
// Ajout du nombre astral
const item = {
name: "Nombre Astral", type: "nombreastral", data:
{ value: data.nbAstral, istrue: data.isvalid, jourindex: Number(data.date), jourlabel: game.system.rdd.calendrier.getDateFromIndex(Number(data.date)) }
};
await this.createEmbeddedEntity("OwnedItem", item);
// Suppression des anciens nombres astraux
let toDelete = this.data.items.filter((item) => item.data.jourindex < game.system.rdd.calendrier.getCurrentDayIndex());
const deletions = toDelete.map(i => i._id);
await this.deleteEmbeddedEntity("OwnedItem", deletions);
// Affichage Dialog
this.astrologieNombresAstraux();
}
/* -------------------------------------------- */
async astrologieNombresAstraux() {
// Afficher l'interface spéciale
const astrologieDialog = await RdDAstrologieJoueur.create(this, {});
astrologieDialog.render(true);
}
/* -------------------------------------------- */
getCaracByName(caracName) {
switch (caracName) {
case 'reve-actuel': case 'Rêve actuel':
return {
label: 'Rêve actuel',
value: this.getReveActuel(),
type: "number"
};
case 'chance-actuelle': case 'Chance actuelle':
return {
label: 'Chance actuelle',
value: this.getChanceActuel(),
type: "number"
};
}
return RdDActor._findCaracByName(this.data.data.carac, caracName);
}
static _findCaracByName(carac, name) {
name = name.toLowerCase();
switch (name) {
case 'reve-actuel': case 'rêve actuel':
return carac.reve;
case 'chance-actuelle': case 'chance actuelle':
return carac.chance;
}
for (const [key, value] of Object.entries(carac)) {
if (name == key || name == value.label.toLowerCase()) {
return carac[key];
}
}
return carac[name]; // Per default
}
/* -------------------------------------------- */
getSortList() {
return this.data.items.filter(item => item.type == "sort");
}
/* -------------------------------------------- */
getDraconicList() {
return this.data.items.filter(item => item.data.categorie == 'draconic')
}
/* -------------------------------------------- */
checkMonteeLaborieuse() { // Return +1 si la queue Montée Laborieuse est présente, 0 sinon
let monteLaborieuse = this.data.items.find(item => (item.type == 'queue' || item.type == 'souffle') && item.name.toLowerCase().includes('montée laborieuse'));
if (monteLaborieuse) {
ChatMessage.create({
content: "Vous êtes sous le coup d'une Montée Laborieuse : vos montées en TMR coûtent 1 Point de Rêve de plus.",
whisper: ChatMessage.getWhisperRecipients(game.user.name)
});
return 1;
}
return 0;
}
/* -------------------------------------------- */
async displayTMR(mode = "normal") {
let isRapide = mode == "rapide";
if (mode != "visu") {
let minReveValue = (isRapide) ? 3 : 2;
if (this.getReveActuel() < minReveValue) {
ChatMessage.create({
content: "Vous n'avez plus assez de Points de Reve pour monter dans les Terres Médianes",
whisper: ChatMessage.getWhisperRecipients(game.user.name)
});
return;
}
}
if (mode != 'visu') {
// Notification au MJ
ChatMessage.create({ content: this.name + " est monté dans les TMR en mode : " + mode, whisper: ChatMessage.getWhisperRecipients("GM") });
}
let data = {
fatigue: {
malus: RdDUtility.calculMalusFatigue(this.data.data.sante.fatigue.value, this.data.data.sante.endurance.max),
html: "<table class='table-fatigue'>" + RdDUtility.makeHTMLfatigueMatrix(this.data.data.sante.fatigue.value, this.data.data.sante.endurance.max).html() + "</table>"
},
draconic: this.getDraconicList(),
sort: this.getSortList(),
caracReve: this.data.data.carac.reve.value,
pointsReve: this.getReveActuel(),
isRapide: isRapide
}
let html = await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/dialog-tmr.html', data);
this.currentTMR = new RdDTMRDialog(html, this, data, mode);
this.currentTMR.render(true);
}
/* -------------------------------------------- */
rollArme(compName, armeName = undefined) {
let arme = armeName ? this.data.items.find(item => item.name == armeName && RdDItemArme.isArme(item)) : undefined;
let competence = this.getCompetence(compName);
if (arme || armeName || (competence.type == 'competencecreature' && competence.data.iscombat)) {
RdDCombat.createUsingTarget(this).attaque(competence, arme);
} else {
this.rollCompetence(competence.name);
}
}
/* -------------------------------------------- */
_getTarget() {
if (game.user.targets && game.user.targets.size == 1) {
for (let target of game.user.targets) {
return target;
}
}
return undefined;
}
getArmeParade(armeParadeId) {
const item = armeParadeId ? this.getOwnedItem(armeParadeId) : undefined;
return RdDItemArme.getArmeData(item);
}
/* -------------------------------------------- */
async equiperObjet(itemID) {
let item = this.getOwnedItem(itemID);
if (item && item.data.data) {
let update = { _id: item._id, "data.equipe": !item.data.data.equipe };
await this.updateEmbeddedEntity("OwnedItem", update);
this.computeEncombrementTotalEtMalusArmure(); // Mise à jour encombrement
}
}
/* -------------------------------------------- */
computeArmure(attackerRoll) {
let dmg = (attackerRoll.dmg.dmgArme ??0) + (attackerRoll.dmg.dmgActor ?? 0);
let arme = attackerRoll.arme;
let protection = 0;
const armures = this.data.items.filter(it => it.type == "armure" && it.data.equipe);
for (const item of armures) {
protection += new Roll(item.data.protection.toString()).roll().total;
if (dmg > 0) {
this._deteriorerArmure(item, dmg);
dmg = 0;
}
}
// TODO: max armure sur chutes...
const penetration = arme ? Misc.toInt(arme.data.penetration) : 0;
protection = Math.max(protection - penetration, 0);
protection += this.getProtectionNaturelle();
console.log("Final protect", protection);
return protection;
}
_deteriorerArmure(item, dmg) {
let update = duplicate(item);
update.data.deterioration = (update.data.deterioration ?? 0) + dmg;
if (update.data.deterioration >= 10) {
update.data.deterioration = 0;
let res = /\d+/.exec(update.data.protection);
if (!res) {
update.data.protection = "1d" + update.data.protection;
}
else if (res = /(\d+d\d+)(\-\d+)?/.exec(update.data.protection)) {
let malus = Misc.toInt(res[2]) - 1;
update.data.protection = res[1] + malus;
}
else {
ui.notifications.warn(`La valeur d'armure de votre ${item.name} est incorrecte`);
}
ChatMessage.create({ content: "Votre armure s'est détériorée, elle protège maintenant de " + update.data.protection });
}
this.updateEmbeddedEntity("OwnedItem", update);
}
/* -------------------------------------------- */
async encaisser() {
let data = { ajustementsEncaissement: RdDUtility.getAjustementsEncaissement() };
let html = await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/dialog-roll-encaisser.html', data);
new RdDEncaisser(html, this).render(true);
}
/* -------------------------------------------- */
async encaisserDommages(rollData, attacker = undefined) {
if (attacker && !await attacker.accorder(this, 'avant-encaissement')) {
return;
}
console.log("encaisserDommages", rollData)
let santeOrig = duplicate(this.data.data.sante);
let encaissement = this.jetEncaissement(rollData);
this.ajouterBlessure(encaissement); // Will upate the result table
const perteVie = this.isEntiteCauchemar()
? { newValue: 0}
: await this.santeIncDec("vie", - encaissement.vie);
const perteEndurance = await this.santeIncDec("endurance", -encaissement.endurance, (encaissement.critiques > 0));
this.computeEtatGeneral();
this.sheet.render(false);
let santeActuelle = duplicate(this.data.data.sante);
encaissement.alias = this.data.name;
encaissement.hasPlayerOwner = this.hasPlayerOwner;
encaissement.resteEndurance = santeActuelle.endurance.value
encaissement.sonne = perteEndurance.sonne;
encaissement.jetEndurance = perteEndurance.jetEndurance;
encaissement.endurance = santeOrig.endurance.value - perteEndurance.newValue;
encaissement.vie = this.isEntiteCauchemar() ? 0 : (santeOrig.vie.value - perteVie.newValue);
ChatUtility.createChatWithRollMode(this.name, {
roll: encaissement.roll,
content: await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/chat-resultat-encaissement.html', encaissement)
});
if (!encaissement.hasPlayerOwner && encaissement.endurance != 0) {
encaissement = duplicate(encaissement);
encaissement.isGM = true;
ChatMessage.create({
whisper: ChatMessage.getWhisperRecipients("GM"),
content: await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/chat-resultat-encaissement.html', encaissement)
});
}
}
/* -------------------------------------------- */
jetEncaissement(rollData) {
const roll = new Roll("2d10").roll();
roll.showDice = true;
RdDDice.show(roll, game.settings.get("core", "rollMode"));
const armure = this.computeArmure(rollData);
const jetTotal = roll.total + rollData.dmg.total - armure;
let encaissement = RdDUtility.selectEncaissement(jetTotal, rollData.dmg.mortalite)
let over20 = Math.max(jetTotal - 20, 0);
encaissement.dmg = rollData.dmg;
encaissement.dmg.loc = rollData.dmg.loc ?? RdDUtility.getLocalisation();
encaissement.dmg.loc.label = encaissement.dmg.loc.label ?? 'Corps;'
encaissement.roll = roll;
encaissement.armure = armure;
encaissement.total = jetTotal;
encaissement.vie = RdDUtility._evaluatePerte(encaissement.vie, over20);
encaissement.endurance = RdDUtility._evaluatePerte(encaissement.endurance, over20);
encaissement.penetration = rollData.arme?.data.penetration ?? 0;
return encaissement;
}
/* -------------------------------------------- */
ajouterBlessure(encaissement) {
// Fast exit
if (this.data.type == 'entite') return; // Une entité n'a pas de blessures
if (encaissement.legeres + encaissement.graves + encaissement.critiques == 0) return;
const endActuelle = Number(this.data.data.sante.endurance.value);
let blessures = duplicate(this.data.data.blessures);
let count = encaissement.legeres;
// Manage blessures
while (count > 0) {
let legere = blessures.legeres.liste.find(it => !it.active);
if (legere) {
this._setBlessure(legere, encaissement);
count--;
}
else {
encaissement.graves += count;
encaissement.legeres -= count;
break;
}
}
count = encaissement.graves;
while (count > 0) {
let grave = blessures.graves.liste.find(it => !it.active);
if (grave) {
this._setBlessure(grave, encaissement);
count--;
}
else {
encaissement.critiques += count;
encaissement.graves -= count;
encaissement.endurance = -endActuelle;
encaissement.vie = -4;
break;
}
}
count = encaissement.critique;
while (count > 0) {
let critique = blessures.critiques.liste[0];
if (!critique.active) {
this._setBlessure(critique, encaissement);
count--;
} else {
// TODO: status effect dead
ChatMessage.create({ content: `<strong>${this.name} vient de succomber à une seconde blessure critique ! Que les Dragons gardent son Archétype en paix !</strong>` });
encaissement.critique -= count;
break;
}
}
encaissement.endurance = Math.max(encaissement.endurance, -endActuelle);
this.update({ "data.blessures": blessures });
}
/* -------------------------------------------- */
_setBlessure(blessure, encaissement) {
blessure.active = true;
blessure.loc = encaissement.locName;
}
/* -------------------------------------------- */
/** @override */
getRollData() {
const data = super.getRollData();
return data;
}
/* -------------------------------------------- */
/* -- entites -- */
/* retourne true si on peut continuer, false si on ne peut pas continuer */
async targetEntiteNonAccordee(target, when = 'avant-encaissement') {
if (target) {
return !await this.accorder(target.actor, when);
}
return false;
}
/* -------------------------------------------- */
async accorder(entite, when = 'avant-encaissement') {
if (when != game.settings.get("foundryvtt-reve-de-dragon", "accorder-entite-cauchemar")
|| !entite.isEntiteCauchemar()
|| entite.isEntiteCauchemarAccordee(this)) {
return true;
}
let rolled = await RdDResolutionTable.roll(this.getReveActuel(), - Number(entite.data.data.carac.niveau.value));
const rollData = {
alias: this.name,
rolled: rolled,
entite: entite.name,
selectedCarac: this.data.data.carac.reve
};
if (rolled.isSuccess) {
await entite.setEntiteReveAccordee(this);
}
await RdDResolutionTable.displayRollData(rollData, this, 'chat-resultat-accorder-cauchemar.html');
if (rolled.isPart) {
await this._appliquerAjoutExperience(rollData, true);
}
return rolled.isSuccess;
}
/* -------------------------------------------- */
isEntiteCauchemar() {
return this.data.type == 'entite';
}
/* -------------------------------------------- */
isEntiteCauchemarAccordee(attaquant) {
if (!this.isEntiteCauchemar()) { return true; }
let resonnance = this.data.data.sante.resonnance;
return (resonnance.actors.find(it => it == attaquant._id));
}
/* -------------------------------------------- */
async setEntiteReveAccordee(attaquant) {
if (!this.isEntiteCauchemar()) {
ui.notifications.error("Impossible de s'accorder à " + this.name + ": ce n'est pas une entite de cauchemer/rêve");
return;
}
let resonnance = duplicate(this.data.data.sante.resonnance);
if (resonnance.actors.find(it => it == attaquant._id)) {
// déjà accordé
return;
}
resonnance.actors.push(attaquant._id);
await this.update({ "data.sante.resonnance": resonnance });
return;
}
/* -------------------------------------------- */
async optimizeArgent(sumDenier) {
let sols = Math.floor(sumDenier / 100);
let deniers = sumDenier - (sols * 100);
let nbOr = Math.floor(sols / 10);
let nbArgent = sols - (nbOr * 10);
let nbBronze = Math.floor(deniers / 10);
let nbEtain = deniers - (nbBronze * 10);
// console.log("ARGENT", nbOr, nbArgent, nbBronze, nbEtain);
let piece = this.data.items.find(item => item.type == 'monnaie' && item.data.valeur_deniers == 1000);
if (piece) {
let update = { _id: piece._id, 'data.quantite': nbOr };
const updated = await this.updateEmbeddedEntity("OwnedItem", update);
}
piece = this.data.items.find(item => item.type == 'monnaie' && item.data.valeur_deniers == 100);
if (piece) {
let update = { _id: piece._id, 'data.quantite': nbArgent };
const updated = await this.updateEmbeddedEntity("OwnedItem", update);
}
piece = this.data.items.find(item => item.type == 'monnaie' && item.data.valeur_deniers == 10);
if (piece) {
let update = { _id: piece._id, 'data.quantite': nbBronze };
const updated = await this.updateEmbeddedEntity("OwnedItem", update);
}
piece = this.data.items.find(item => item.type == 'monnaie' && item.data.valeur_deniers == 1);
if (piece) {
let update = { _id: piece._id, 'data.quantite': nbEtain };
const updated = await this.updateEmbeddedEntity("OwnedItem", update);
}
}
/* -------------------------------------------- */
async payerDenier(sumDenier, dataObj = undefined) {
sumDenier = Number(sumDenier);
let denierDisponible = 0;
let monnaie = this.data.items.filter(item => item.type == 'monnaie');
for (let piece of monnaie) {
denierDisponible += piece.data.valeur_deniers * Number(piece.data.quantite);
}
console.log("DENIER", game.user.character, sumDenier, denierDisponible);
let msg = "";
if (denierDisponible >= sumDenier) {
denierDisponible -= sumDenier;
this.optimizeArgent(denierDisponible);
msg = `Vous avez payé <strong>${sumDenier} Deniers</strong>, qui ont été soustraits de votre argent.`;
RdDAudio.PlayContextAudio("argent"); // Petit son
} else {
msg = "Vous n'avez pas assez d'argent pour paye cette somme !";
}
if (dataObj) {
dataObj.payload.data.cout = sumDenier / 100; // Mise à jour du prix en sols , avec le prix acheté
await this.createOwnedItem(dataObj.payload);
msg += `<br>Et l'objet <strong>${dataObj.payload.name}</strong> a été ajouté à votre inventaire.`;
}
let message = {
whisper: ChatUtility.getWhisperRecipientsAndGMs(this.name),
content: msg
};
ChatMessage.create(message);
}
/* -------------------------------------------- */
async monnaieIncDec(id, value) {
let monnaie = this.data.items.find(item => item.type == 'monnaie' && item._id == id);
if (monnaie) {
monnaie.data.quantite += value;
if (monnaie.data.quantite < 0) monnaie.data.quantite = 0; // Sanity check
const update = { _id: monnaie._id, 'data.quantite': monnaie.data.quantite };
const updated = await this.updateEmbeddedEntity("OwnedItem", update);
}
}
/* -------------------------------------------- */
async effectuerTacheAlchimie(recetteId, alchimieName, alchimieData) {
let recette = this.data.items.find(item => item.type == 'recettealchimique' && item._id == recetteId);
if (recette) {
let competence = this.data.items.find(item => item.type == 'competence' && item.name.toLowerCase() == "alchimie");
let diffAlchimie = RdDAlchimie.getDifficulte(alchimieData);
let rollData = {
recette: recette,
competence: competence,
diffLibre: diffAlchimie // Per default at startup
}
if (alchimieName == "couleur") {
rollData.selectedCarac = this.data.data.carac.vue,
rollData.alchimieTexte = `Couleurs ${alchimieData} (${diffAlchimie}) (Malus de -4 si vous ne possédez pas de Cristal Alchimique)`;
} else {
rollData.selectedCarac = this.data.data.carac.dexterite,
rollData.alchimieTexte = `Consistances ${alchimieData} (${diffAlchimie})`;
}
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(),
{ action: r => this._alchimieResult(r, false) }
]
}
);
dialog.render(true);
}
}
/* -------------------------------------------- */
_alchimieResult(rollData) {
RdDResolutionTable.displayRollData(rollData, this, 'chat-resultat-alchimie.html');
}
/* -------------------------------------------- */
buildVehiculesList() {
return this._buildActorLinksList(
this.data.data.subacteurs?.vehicules??[],
vehicle => {return { id: vehicle.id, name: vehicle.data.name, categorie: vehicle.data.data.categorie,
structure: vehicle.data.data.structure, img: vehicle.data.img } ;});
}
/* -------------------------------------------- */
buildSuivantsList() {
return this._buildActorLinksList(this.data.data.subacteurs?.suivants ?? []);
}
/* -------------------------------------------- */
buildMonturesList() {
return this._buildActorLinksList(this.data.data.subacteurs?.montures ?? []);
}
_buildActorLinksList(links, actorTransformation=it => { return { id: it.id, name: it.data.name, img: it.data.img }; }) {
return links.map(link => game.actors.get(link.id))
.filter(it => it != null)
.map(actorTransformation);
}
/* -------------------------------------------- */
async pushSubacteur( actor, dataArray, dataPath, dataName ) {
let alreadyPresent = dataArray.find( attached => attached.id == actor.data._id);
if ( !alreadyPresent ) {
let newArray = duplicate(dataArray);
newArray.push( { id: actor.data._id });
await this.update( { [dataPath]: newArray });
} else {
ui.notifications.warn(dataName+" est déja attaché à ce Personnage.");
}
}
/* -------------------------------------------- */
addSubacteur( actorId ) {
let actor = game.actors.get( actorId );
//console.log("Ajout acteur : ", actor, this);
if (actor && actor.owner ) {
if (actor.data.type == 'vehicule') {
this.pushSubacteur( actor, this.data.data.subacteurs.vehicules, 'data.subacteurs.vehicules', 'Ce Véhicule' );
} else if (actor.data.type == 'creature') {
this.pushSubacteur( actor, this.data.data.subacteurs.montures, 'data.subacteurs.montures', 'Cette Monture' );
} else if (actor.data.type == 'personnage') {
this.pushSubacteur( actor, this.data.data.subacteurs.suivants, 'data.subacteurs.suivants', 'Ce Suivant' );
}
} else {
ui.notifications.warn("Vous n'avez pas les droits sur l'acteur que vous attachez.")
}
}
/* -------------------------------------------- */
async removeSubacteur( actorId ) {
let newVehicules = this.data.data.subacteurs.vehicules.filter(function(obj, index, arr){ return obj.id != actorId } );
let newSuivants = this.data.data.subacteurs.suivants.filter(function(obj, index, arr){ return obj.id != actorId } );
let newMontures = this.data.data.subacteurs.montures.filter(function(obj, index, arr){ return obj.id != actorId } );
await this.update( { 'data.subacteurs.vehicules': newVehicules });
await this.update( { 'data.subacteurs.suivants': newSuivants });
await this.update( { 'data.subacteurs.montures': newMontures });
}
/* -------------------------------------------- */
async onCreateActiveEffect(effect, options) {
switch (StatusEffects.statusId(effect)) {
case 'sonne':
await this.setStateSonne(true);
return;
}
}
/* -------------------------------------------- */
async onDeleteActiveEffect(effect, options) {
switch (StatusEffects.statusId(effect)) {
case 'sonne':
await this.setStateSonne(false);
return;
}
}
/* -------------------------------------------- */
enleverTousLesEffets() {
this.deleteEmbeddedEntity('ActiveEffect', Array.from(this.effects?.keys() ?? []));
}
/* -------------------------------------------- */
listeEffets(matching = it => true) {
const all = Array.from(this.effects?.values() ?? []);
const filtered = all.filter(it => matching(it.data));
return filtered;
}
/* -------------------------------------------- */
async setStatusDemiReve(status) {
const options = { renderSheet: true/*, noHook: from == 'hook' */ };
if (status) {
await this.addEffect(StatusEffects.demiReve(), options)
}
else {
this.deleteEffect(StatusEffects.demiReve(), options)
}
}
/* -------------------------------------------- */
async setStatusSonne(sonne) {
if (this.isEntiteCauchemar()) {
return;
}
const id = 'sonne';
const options = { renderSheet: true/*, noHook: from == 'hook' */ };
if (sonne) {
await this.addById(id, options);
}
else /* if (!sonne)*/ {
this.deleteById(id, options)
}
}
/* -------------------------------------------- */
deleteById(id, options) {
const effects = Array.from(this.effects?.values())
.filter(it => it.data.flags.core?.statusId == id);
this._deleteAll(effects, options);
}
/* -------------------------------------------- */
deleteEffect(effect, options) {
const toDelete = Array.from(this.effects?.values())
.filter(it => StatusEffects.statusId(it.data) == StatusEffects.statusId(effect));
this._deleteAll(toDelete, options);
}
/* -------------------------------------------- */
_deleteAll(effects, options) {
this._deleteAllIds(effects.map(it => it.id), options);
}
/* -------------------------------------------- */
_deleteAllIds(effectIds, options) {
this.deleteEmbeddedEntity('ActiveEffect', effectIds, options);
this.applyActiveEffects();
}
/* -------------------------------------------- */
async addById(id, options) {
const statusEffect = CONFIG.statusEffects.find(it => it.id == id);
await this.addEffect(statusEffect, options);
}
/* -------------------------------------------- */
async addEffect(statusEffect, options) {
this.deleteById(statusEffect.id, options);
const effet = duplicate(statusEffect);
effet["flags.core.statusId"] = effet.id;
await this.createEmbeddedEntity('ActiveEffect', effet, options);
this.applyActiveEffects();
}
/* -------------------------------------------- */
async updateEmbeddedEntity(embeddedName, data, options) {
if ( data && data['data.defaut_carac'] && data['data.xp'] ) { // C'est une compétence
this.checkCompetenceXP(data['name'], data['data.xp'] );
}
return super.updateEmbeddedEntity(embeddedName, data, options);
}
}