foundryvtt-reve-de-dragon/module/actor.js
Vincent Vandemeulebrouck df5954bf75 Fix reférence circulaire
La méthode rollData fait le jet fourni dans rollData et l'ajoute dans
rollData.rolled.

Pas besoin de retourner rollData, la tentation est forte de mettre le
résultat dans "rolled"
2020-12-09 00:36:59 +01:00

1876 lines
73 KiB
JavaScript

/**
* Extend the base Actor entity by defining a custom roll data structure which is ideal for the Simple system.
* @extends {Actor}
*/
import { RdDUtility } from "./rdd-utility.js";
import { TMRUtility } from "./tmr-utility.js";
import { RdDRollDialog } from "./rdd-roll-dialog.js";
import { RdDRollDialogEthylisme } from "./rdd-roll-ethylisme.js";
import { RdDRoll } from "./rdd-roll.js";
import { RdDTMRDialog } from "./rdd-tmr-dialog.js";
import { Misc } from "./misc.js";
import { RdDResolutionTable } from "./rdd-resolution-table.js";
import { RdDDice } from "./rdd-dice.js";
import { RdDRollTables } from "./rdd-rolltables.js";
import { ChatUtility } from "./chat-utility.js";
import { RdDItemSort } from "./item-sort.js";
import { Grammar } from "./grammar.js";
export class RdDActor extends Actor {
/* -------------------------------------------- */
/**
* 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) {
return super.create(data, options);
}
data.items = [];
let compendiumName = "";
if (data.type == "personnage") {
compendiumName = "foundryvtt-reve-de-dragon.competences";
} else if (data.type == "humanoide") {
compendiumName = "foundryvtt-reve-de-dragon.competences-humanoides";
} else if (data.type == "creature") {
compendiumName = "foundryvtt-reve-de-dragon.competences-creatures";
} else if (data.type == "entite") {
compendiumName = "foundryvtt-reve-de-dragon.competences-entites";
}
let competences = [];
const pack = game.packs.get(compendiumName);
await pack.getIndex().then(index => competences = index);
for (let comp of competences)
{
let compItem = undefined;
await pack.getEntity(comp._id).then(skill => compItem = skill);
data.items.push(compItem);
}
return super.create(data, options);
}
/* -------------------------------------------- */
/* -------------------------------------------- */
prepareData() {
super.prepareData();
const actorData = this.data;
// Dynamic computing fields
this.encombrementTotal = 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.computeEtatGeneral(actorData);
if (actorData.type === 'humanoide') this.computeEtatGeneral(actorData);
}
/* -------------------------------------------- */
/**
* Prepare Character type specific data
*/
_prepareCharacterData(actorData) {
// Initialize empty items
RdDUtility.computeCarac(actorData.data);
this.computeEncombrementTotalEtMalusArmure();
this.computeEtatGeneral();
}
/* -------------------------------------------- */
getReveActuel() {
return this.data.data.reve.reve.value;
}
getChanceActuel() {
return this.data.data.compteurs.chance.value;
}
/* -------------------------------------------- */
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(coordTMR) {
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 != coordTMR )
newTable.push(reserve.list[i]);
}
if ( newTable.length != len ) {
reserve.list = newTable;
await this.update( {"data.reve.reserve": reserve } );
}
}
/* -------------------------------------------- */
async performRoll(rollData, attacker = undefined) {
// garder le résultat
await RdDResolutionTable.rollData(rollData);
//console.log("performRoll", rollData)
if ( !rollData.attackerRoll) {// Store in the registry if not a defense roll
game.system.rdd.rollDataHandler[this.data._id] = rollData;
}
if (rollData.rolled.isPart && rollData.arme && !rollData.attackerRoll) { // Réussite particulière avec attaque -> choix !
let message = "<strong>Réussite particulière en attaque</strong>";
message = message + "<br><a class='chat-card-button' id='particuliere-attaque' data-mode='force' data-attackerid='" + this.data._id + "'>Attaquer en Force</a>";
// Finesse et Rapidité seulement en mêlée et si la difficulté libre est de -1 minimum
if (rollData.selectedCarac.label == "Mêlée" && rollData.diffLibre < 0 ) {
message = message + "<br><a class='chat-card-button' id='particuliere-attaque' data-mode='rapidite' data-attackerid='"+ this.data._id + "'>Attaquer en Rapidité</a>";
message = message + "<br><a class='chat-card-button' id='particuliere-attaque' data-mode='finesse' data-attackerid='"+ this.data._id + "'>Attaquer en Finesse</a>";
}
ChatMessage.create( {content : message, whisper: ChatMessage.getWhisperRecipients( this.name ) } );
} else {
this.continueRoll(rollData, attacker);
}
}
/* -------------------------------------------- */
computeDeteriorationArme( rollData ) {
if ( rollData.arme && rollData.attackerRoll) { // C'est une parade
// Est-ce que l'attaque est une particulière, en force ou charge et que l'attaque n'en est pas une ?
if ( rollData.attackerRoll.rolled.isPart
&& ( (rollData.attackerRoll.particuliereAttaque && rollData.attackerRoll.particuliereAttaque == 'force') || rollData.attackerRoll.isCharge)
&& !rollData.rolled.isPart ) {
// Jet de résistance de l'arme de parade (p.132)
let resist = RdDResolutionTable.roll( rollData.arme.data.resistance, rollData.attackerRoll.domArmePlusDom );
let msg = "";
if (resist.isSuccess) { // Perte de résistance
msg = "Jet de résistance de votre arme réussit !"
} else {
rollData.arme.data.resistance -= rollData.attackerRoll.domArmePlusDom;
if ( rollData.arme.data.resistance <= 0 ) {
this.deleteEmbeddedEntity("OwnedItem", rollData.arme._id);
msg = "Votre arme s'est brisée sous le coup de la parade : " + rollData.arme.name;
} else {
this.updateEmbeddedEntity("OwnedItem", {_id: rollData.arme._id, 'data.resistance': rollData.arme.data.resistance });
msg = "Votre arme a perdu de la résistance : " + rollData.arme.name + " - " + rollData.arme.data.resistance;
}
}
// Jet de désarmement
if ( !rollData.arme.includes('Bouclier') ) { // Si l'arme de parade n'est pas un bouclier, jet de désarmement (p.132)
let desarme = RdDResolutionTable.roll( this.data.data.carac.force.value, Number(rollData.competence.data.niveau) + Number(rollData.attackerRoll.domArmePlusDom) );
if ( !desarme.isSucess) {
msg += "<br>De plus, vous êtes désarmé ! Votre arme " + rollData.arme.name + "tombe au sol à vos pieds";
}
}
ChatMessage.create( { content: msg,
user: game.user._id,
whisper: [game.user._id, ChatMessage.getWhisperRecipients("GM") ] } );
}
}
}
/* -------------------------------------------- */
computeRecul( rollData, encaisser = undefined ) { // Calcul du recul (p. 132)
if ( rollData.arme || encaisser ) {
if ( (rollData.attackerRoll.particuliereAttaque && rollData.attackerRoll.particuliereAttaque == 'force') || rollData.attackerRoll.isCharge) {
let reculNiveau = this.data.data.taille.value - (rollData.attackerRoll.forceValue+rollData.attackerRoll.arme.dommages);
let recul = RdDResolutionTable.roll( 10, reculNiveau );
let msg = "";
if (recul.isSuccess) {
msg = "Jet de Recul réussit, aucun effet !";
} else {
let chute = RdDResolutionTable.roll( this.data.data.carac.agilite.value, reculNiveau );
if ( !chute.isSuccess || recul.isETotal ) {
msg = "Jet de Recul : Vous subissez le recul du coup, et vous chutez au sol ! Vous ne pouvez plus attaquer ce round.";
} else {
msg = "Jet de Recul : Vous subissez le recul du coup, et vous reculez de quelques mètres ! Vous ne pouvez plus attaquer ce round.";
}
}
ChatMessage( {content: msg,
user: game.user._id,
whisper: [game.user._id, ChatMessage.getWhisperRecipients("GM") ] } );
}
}
}
/* -------------------------------------------- */
async continueRoll(rollData, attacker = undefined) {
let rolled = rollData.rolled;
let quality = rolled.quality
// Manage weapon categories when parrying (cf. page 115 )
let need_resist = false; // Do we need to make resistance roll for defender ?
if (rollData.arme && rollData.attackerRoll) { // Manage parade depending on weapon type, and change roll results
let attCategory = RdDUtility.getArmeCategory(rollData.attackerRoll.arme);
let defCategory = RdDUtility.getArmeCategory(rollData.arme);
if (defCategory == "bouclier")
rollData.needSignificative = false;
else if (attCategory != defCategory)
rollData.needSignificative = true;
if (attCategory.match("epee") && (defCategory == "hache" || defCategory == "lance"))
need_resist = true;
}
if (!this.isEntiteCauchemar() && (this.data.data.sante.sonne.value || rollData.particuliereAttaque == "finesse")) {
rollData.needSignificative = true;
}
console.log(">>> ROLL", rollData, rolled);
let xpmsg = RdDResolutionTable.buildXpMessage(rolled, rollData.finalLevel);
let resumeCompetence = (rollData.competence) ? rollData.competence.name : (rollData.diffLibre + rollData.diffConditions);
let explications = "<br>Points de taches : " + rolled.ptTache + ", ajustement qualité: " + rolled.ptQualite;
// Fight management !
let defenseMsg;
let encaisser = false;
if (rollData.arme || (rollData.competence && rollData.competence.name.toLowerCase() == 'esquive') ) {
explications = ""
// In case of fight, replace the message per dommages + localization. it indicates if result is OK or not
if (rollData.attackerRoll) { // Defense case !
if (rolled.isSign || (!rollData.needSignificative && rolled.isSuccess)) {
this.computeDeteriorationArme( rollData );
explications += "<br><strong>Attaque parée/esquivée !</strong>";
} else {
explications += "<br><strong>Esquive/Parade échouée, encaissement !</strong>";
if (rollData.needSignificative)
explications += " Significative nécessaire!";
}
encaisser = rollData.needSignificative ? !rolled.isSign : !rolled.isSuccess;
this.computeRecul( rollData, encaisser );
} else { // This is the attack roll!
if (rolled.isSuccess) {
let target = this._getTarget();
if (await this.targetEntiteNonAccordee(target, 'avant-defense')) {
return;
}
// Message spécial pour la rapidité, qui reste difficile à gérer automatiquement
if ( rollData.particuliereAttaque == 'rapidite') {
ChatMessage.create( { content: "Vous avez attaqué en Rapidité. Ce cas n'est pas géré autmatiquement, donc suivez les directives de votre MJ pour gérer ce cas.",
whisper: ChatMessage.getWhisperRecipients( this.name ) } );
}
rollData.domArmePlusDom = this._calculBonusDommages(rollData.selectedCarac, rollData.arme, rollData.particuliereAttaque == 'force' );
rollData.degats = new Roll("2d10").roll().total + rollData.domArmePlusDom + ((rollData.isCharge)?2:0); // Dégats totaux
rollData.loc = RdDUtility.getLocalisation();
if (target)
{
rollData.mortalite = RdDActor._calculMortaliteEncaissement(rollData, target);
defenseMsg = RdDUtility.buildDefenseChatCard(this, target, rollData);
explications += "<br><strong>Cible</strong> : " + target.actor.data.name;
}
explications += "<br>Encaissement : " + rollData.degats + "<br>Localisation : " + rollData.loc.label;
} else {
explications = "<br>Echec ! Pas de dégâts";
}
}
}
// Save it for fight in the flags area
game.system.rdd.rollDataHandler[this.data._id] = duplicate(rollData);
// Final chat message
let chatOptions = {
content: "<strong>Test : " + rollData.selectedCarac.label + " / " + resumeCompetence + "</strong>"
+ "<br>Difficultés <strong>libre : " + rollData.diffLibre + "</strong> / conditions : " + Misc.toSignedString(rollData.diffConditions) +" / état : " + rollData.etat
+ RdDResolutionTable.explain(rolled)
+ "<br><strong>" + quality + "</strong>"
+ explications + xpmsg
}
ChatUtility.chatWithRollMode(chatOptions, this.name)
// This an attack, generate the defense message
if (defenseMsg) {
defenseMsg.rollData = duplicate(rollData);
if (defenseMsg.toSocket) {
game.socket.emit("system.foundryvtt-reve-de-dragon", {
msg: "msg_defense",
data: defenseMsg
});
if ( game.user.isGM ) { // Always push the message to the MJ
ChatMessage.create(defenseMsg);
}
} else {
defenseMsg.whisper = [game.user];
ChatMessage.create(defenseMsg);
}
}
// Get damages!
if (encaisser) {
this.encaisserDommages(rollData.attackerRoll, attacker);
}
}
/* -------------------------------------------- */
static _calculMortaliteEncaissement(rollData, target) {
const mortalite = target.actor.isEntiteCauchemar() ? "cauchemar" : (rollData.mortalite ? rollData.mortalite : "mortel");
console.log("Mortalité : ", mortalite, target.actor.data.type);
return mortalite;
}
/* -------------------------------------------- */
_calculBonusDommages(carac, arme, isForce=false) {
if ( arme.name.toLowerCase() == "esquive") return 0; // Specific case management
let dmgArme = 0;
const dmgPerso = parseInt(this.data.data.attributs.plusdom.value);
if ( arme.data.dommages ) {
dmgArme = parseInt(arme.data.dommages) + (isForce)? 5 : 0;
if (carac.label == "Tir") {
return dmgArme;
}
if (carac.label == "Lancer") {
return dmgArme + Math.min(dmgArme, dmgPerso);
}
}
return dmgArme + dmgPerso;
}
/* -------------------------------------------- */
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,"legere", 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, "localisation": blessure.localisation });
}
this._supprimerBlessure(blessure);
return true;
}
/* -------------------------------------------- */
_supprimerBlessure(blessure) {
mergeObject(blessure, { "active": false, "premiers_soins": 0, "soins_complets": 0, "jours": 0, "localisation": "" });
}
/* -------------------------------------------- */
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
};
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 } );
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);
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 : "Vous dormez " + heures + " heure" + (heures > 1 ? "s": "")
};
await this.recupereEndurance(message);
for (let i=0; i<heures; i++) {
await this.recupererFatigue(message);
await this.recuperationReve(message);
}
ChatMessage.create( message );
}
/* -------------------------------------------- */
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 RdDDice.deDraconique();
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 " + 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.data.data.compteurs.etat.value;
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?
let tete = "à déterminer";
message += "<br>Vous gagnez une Tête de dragon: " + tete;
}
if (roll.isEchec) {
message += "<br>Vous subissez une Queue de Dragon";
this.ajouterQueue();
}
if (roll.isETotal) {
message += "<br>A cause de votre échec total, vous subissez une deuxième Queue de Dragon !"
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.selectedDraconic) });
await this.update({ "data.reve.reserve": reserve });
this.currentTMR.updateSortReserve();
}
/* -------------------------------------------- */
updateCarac( caracName, caracValue )
{
let caracpath = "data.carac." + caracName + ".value"
if (caracName == "reve") {
if (caracValue > Misc.toInt(this.data.data.reve.seuil.value)) {
this.setPointsDeSeuil(caracValue);
}
}
this.update( { caracpath: caracValue } );
}
/* -------------------------------------------- */
async updateCreatureCompetence( compName, fieldName, compValue )
{
let comp = RdDUtility.findCompetence( this.data.items, 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 = RdDUtility.findCompetence( this.data.items, compName);
if ( comp ) {
let troncList = RdDUtility.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( { title : "Compétence Tronc",
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 = RdDUtility.findCompetence( this.data.items, compName);
if ( comp ) {
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 updateCompteurValue( fieldName, fieldValue )
{
//console.log("Update", fieldName, fieldValue);
let content;
let compteurs = duplicate(this.data.data.compteurs);
compteurs[fieldName].value = fieldValue;
await this.update( {"data.compteurs": compteurs } );
}
/* -------------------------------------------- */
/** 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 ( currentEnc + nouvelObjet.data.data.encombrement > Number(conteneur.data.data.capacite) )
return false;
}
return true;
}
/* -------------------------------------------- */
/** 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 );
}
}
/* -------------------------------------------- */
detectSurEncombrement( ) {
let diffEnc = Number(this.encombrementTotal) - Number(this.data.data.attributs.encombrement.value);
if ( diffEnc > 0 ) { // Sur-encombrement
let malus = Math.round( diffEnc);
malus = (malus == 0) ? 1 : malus; // Always 1 at least
//console.log("Sur enc malus", malus);
return malus;
}
return 0;
}
/* -------------------------------------------- */
async computeEncombrementTotalEtMalusArmure( ) {
let totalEnc = 0;
let malusArmureData = 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) item.data.quantite = 1; // 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);
totalEnc += item.data.encTotal;
} else {
item.data.encTotal = 0; // Force default enc
}
}
// Mise à jour valeur totale et états
this.encombrementTotal = totalEnc;
this.detectSurEncombrement();
// Mise à jour éventuelle du malus armure
if (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 = 0, surenc = 0;
state = state - (data.sante.vie.max - data.sante.vie.value);
if (data.sante.fatigue) // Creatures n'ont pas de fatigue
state = state + RdDUtility.currentFatigueMalus(data.sante.fatigue.value, data.sante.endurance.max);
if (data.compteurs && data.compteurs.ethylisme) // Ajout de l'éthylisme
state = state + data.compteurs.ethylisme.value;
state = state;
data.compteurs.etat.value = state;
if ( data.compteurs && data.compteurs.surenc) {
surenc = -this.detectSurEncombrement();
data.compteurs.surenc.value = surenc;
}
}
/* -------------------------------------------- */
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();
ret = "souffle";
}
await this.update( {"data.reve.refoulement": refoulement } );
return ret;
}
/* -------------------------------------------- */
ajouterSouffle() {
let souffle = RdDRollTables.getSouffle();
// ChatMessage.create({
// title: "Souffle de Dragon",
// content: this.name + " subit un Souffle de Dragon : " + souffle.name
// });
// this.actor.createOwnedItem(souffle);
}
/* -------------------------------------------- */
async ajouterQueue() {
// 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();
}
/*
// TODO: convertir la queue obtenue en nouvel item ...
// ou bien l'ajouter à la liste spécifique => this.data.data.reve.queues
this.createOwnedItem(queue);
ChatMessage.create({
content: this.name + " subit un Queue de Dragon : " + queue.name
});
return queue.name;
*/
}
/* -------------------------------------------- */
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 } );
}
/* -------------------------------------------- */
testSiSonne( sante, endurance )
{
let result = new Roll("1d20").roll().total;
if ( result <= endurance)
sante.sonne.value = false;
if ( result > endurance || result == 20) // 20 is always a failure
sante.sonne.value = true;
if (result == 1) {
sante.sonne.value = false;
let xp = Misc.toInt(this.data.data.carac.constitution.xp) + 1;
this.update( {"data.carac.constitution.xp": xp } ); // +1 XP !
// TODO : Output to chat
}
}
/* -------------------------------------------- */
countBlessures( blessuresListe )
{
return blessuresListe.filter(b => b.active).length
}
/* -------------------------------------------- */
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 data = sante[name];
if (data==undefined) {
return;
}
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;
}
let newValue = Math.max(minValue, Math.min(data.value + inc, data.max));
//console.log("New value ", inc, minValue, newValue);
if (name == "endurance" && this.data.type != 'entite' ) {
if ( sante.fatigue && inc < 0 ) { // Each endurance lost -> fatigue lost
sante.fatigue.value = sante.fatigue.value - inc
}
if ( !isCritique && newValue == 0 && inc < 0) { // perte endurance et endurance devient 0 -> -1 vie sauf si coup critique
sante.vie.value = sante.vie.value - 1;
}
newValue = Math.max(0, newValue);
if (inc>0) { // le max d'endurance s'applique seulement à la récupération
newValue = Math.min(newValue, this._computeEnduranceMax())
}
if (data.value - newValue > 1) {
this.testSiSonne(sante, newValue); // Peut-être sonné si 2 points d'endurance perdus d'un coup
} else if (inc>0) {
sante.sonne.value = false;
}
}
data.value = newValue;
//console.log(name, inc, data.value, newValue, minValue, data.max);
if ( sante.fatigue) { // If endurance lost, then the same amount of fatigue cannot be recovered
sante.fatigue.value = Math.max(sante.fatigue.value, this._computeFatigueMin());
}
//console.log("SANTE::::", sante);
await this.update( {"data.sante": sante } );
}
/* -------------------------------------------- */
_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.localisation = "";
}
//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.localisation = loc;
await this.update( { 'data.blessures': bList } );
}
/* -------------------------------------------- */
manageBlessures( blessuresData )
{
// Fast exit
if ( this.data.type == 'entite') return; // Une entité n'a pas de blessures
if ( blessuresData.legeres + blessuresData.graves + blessuresData.critiques == 0 ) return;
let workData = duplicate(blessuresData);
let blessures = duplicate(this.data.data.blessures);
// Manage blessures
if ( workData.legeres > 0 ) {
for (let k=0; k<blessures.legeres.liste.length; k++) {
let bless = blessures.legeres.liste[k];
if ( !bless.active ){
bless.active = true;
bless.loc = workData.locName;
workData.legeres--;
}
if (workData.legeres == 0) break;
}
}
if ( workData.legeres > 0 ) {
workData.graves += 1;
blessuresData.graves += 1;
}
if ( workData.graves > 0) {
for (let k=0; k<blessures.graves.liste.length; k++) {
let bless = blessures.graves.liste[k];
if ( !bless.active ) {
bless.active = true;
bless.loc = workData.locName;
workData.graves--;
}
if ( workData.graves == 0) break;
}
}
if ( workData.graves > 0 ) {
workData.critiques = 1;
blessuresData.critiques = 1;
}
if ( workData.critiques > 0 ) {
workData.endurance = this.data.data.sante.endurance.value; // Patch with real endurance current value (ie end -> 0 when critique)
blessures.critiques.liste[0].active = true;
blessures.critiques.liste[0].loc = workData.locName;
}
this.update( { "data.blessures": blessures } );
}
/* -------------------------------------------- */
async ethylismeTest() {
let rollData = {
vieValue: this.data.data.sante.vie.value,
etat: this.data.data.compteurs.etat.value - this.data.data.compteurs.etat.value, // Pour les jets d'Ethylisme, on ignore le degré d'éthylisme (p.162)
niveauEthylisme: this.data.data.compteurs.ethylisme.value,
nbDoses: 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 = ethylisme.nb_doses + 1;
msgText += "Vous avez réussi votre jet d'éthylisme, votre vous avez désormais " + ethylisme.nb_doses + " doses sans effet.";
} else {
ethylisme.value = ethylisme.value - 1;
if ( ethylisme.value > 7) ethylisme.value = 7; // Niveau max
let enduranceLost = new Roll("1d6").roll().total;
await this.santeIncDec("endurance", -enduranceLost);
msgText += "Vous avez échoué à votre jet d'éthylisme, votre niveau d'éthylisme est de " + ethylisme.value
+ "(" + RdDUtility.getNomEthylisme(ethylisme.value) + ").";
// Qui a bu boira (p 164)
let rollVolonte = await RdDResolutionTable.roll( this.data.data.carac.volonte.value, -ethylisme.value);
msgText += "<br>" + RdDResolutionTable.explain(rollVolonte) + "<br>";
if ( rollVolonte.isSuccess)
msgText += "Qui a bu boira : vous êtes libre de continuer à boire ou pas.";
else
msgText += "Qui a bu boira : vous avez une envie irrépréssible de reprendre un verre.";
}
await this.update( { 'data.compteurs.ethylisme': ethylisme} );
const message = {
content: msgText,
whisper: ChatMessage.getWhisperRecipients(game.user.name)
};
ChatMessage.create(message);
}
/* -------------------------------------------- */
async stressTest() {
const message = {
content: "",
whisper: ChatMessage.getWhisperRecipients(game.user.name)
};
await this.transformerStress(message);
ChatMessage.create(message);
}
/* -------------------------------------------- */
async transformerStress(message) {
const stress = Misc.toInt(this.data.data.compteurs.stress.value);
if (stress<=0) {
return;
}
let stressRoll = await this._stressRoll();
let convertis = Math.floor(stress * stressRoll.factor);
let compteurs = duplicate(this.data.data.compteurs);
compteurs.experience.value += convertis;
compteurs.stress.value = Math.max(stress - convertis - 1, 0);
message.content += "<br>Vous transformez " + convertis + " points de Stress en Expérience" + stressRoll.comment;
await this.update({ "data.compteurs": compteurs });
}
/* -------------------------------------------- */
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: " (150%): " + 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)
};
}
async _appliquerAjoutExperience(rollData) {
// TODO: si pas de compétence, minimum 1 pour carac
// TODO: appliquer l'expérience automatiquement
let xpmsg = RdDResolutionTable.buildXpMessage(rollData.rolled, rollData.finalLevel);
let message = ChatUtility.prepareChatMessage('gmroll', this.name);
message.content = "<strong>" + rollData.selectedCarac.label + "</strong>"
+ xpmsg;
ChatMessage.create(message);
}
/* -------------------------------------------- */
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;
}
let rollData = {
selectedCarac: this.data.data.carac.reve,
draconicList: this.getDraconicList(),
sortList: sortList,
selectedDraconic: 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)
}
if ( this.currentTMR) this.currentTMR.minimize(); // Hide
const dialog = await RdDRoll.create(this, rollData,
{html: 'systems/foundryvtt-reve-de-dragon/templates/dialog-roll-sort.html'},
{
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);
}
async _rollUnSortResult(rollData, isSortReserve = false) {
rollData.isSortReserve = isSortReserve;
let rolled = rollData.rolled;
let sort = rollData.selectedSort;
let closeTMR = !rollData.isSortReserve;
let explications = rollData.isSortReserve
? ("<br>Mise en réserve du sort en " + rollData.coordLabel + "(" + rollData.coord + ")<strong>")
: "<br>Lancement du sort <strong>";
explications += sort.name + "</strong> : " + Misc.upperFirst(sort.data.draconic)
+ " pour " + sort.data.ptreve_reel + " points de Rêve"
+ "<br>Depuis la case " + rollData.coord + " (" + TMRUtility.getTMRDescription(rollData.coord).label + ")";
let depenseReve = sort.data.ptreve_reel;
let myReve = duplicate(this.data.data.reve.reve);
if (rolled.isSuccess) { // Réussite du sort !
//sort.ptreve_reel = coutReve;
if (rolled.isPart) {
depenseReve = Math.max(Math.floor(depenseReve / 2), 1);
}
if (rollData.isSortReserve) {
depenseReve++;
}
if (myReve.value > depenseReve) {
explications += "<br>Réussite du sort: " + depenseReve + " points de Rêve sont dépensés (Bonus de case en " + rollData.coord + ": +" + rolled.bonus + "%)";
// Incrémenter/gére le bonus de case
RdDItemSort.incrementBonusCase(this, sort, rollData.coord);
if (rollData.isSortReserve) {
await this.sortMisEnReserve(rollData, sort);
closeTMR = false;
}
}
else {
// Todo 0 pts de reve !!!!
depenseReve = 0;
explications += "<br>Pas assez de rêve";
mergeObject(rollData, RdDResolutionTable.getResultat("echec"));
}
} else {
if (rolled.isETotal) { // Echec total !
depenseReve = Math.max(myReve.value, Math.floor(depenseReve * 1.5))
explications += "<br><strong>Echec TOTAL</strong> du sort : " + depenseReve + " Points de Rêve";
// TODO: mise en réserve d'un échec total...
} else {
depenseReve = 0
explications += "<br>Echec du sort !";
}
}
myReve.value = Math.max(myReve.value - depenseReve, 0);
await this.update({ "data.reve.reve": myReve });
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;
}
if (closeTMR) {
this.currentTMR.close(); // Close TMR !
} else {
this.currentTMR.maximize(); // Re-display TMR
}
// Final chat message
let chatOptions = {
content: "<strong>Test : " + rollData.selectedCarac.label + " / " + rollData.selectedDraconic.name + " / " + rollData.selectedSort.name + "</strong>"
+ "<br>Difficultés <strong>libre : " + rollData.diffLibre + "</strong> / conditions : " + Misc.toSignedString(rollData.diffConditions) +" / état : " + rollData.etat
+ RdDResolutionTable.explain(rolled)
+ explications
}
ChatUtility.chatWithRollMode(chatOptions, this.name)
}
/* -------------------------------------------- */
async rollCarac( caracName ) {
let rollData = {
selectedCarac: this.getCaracByName(caracName),
needSignificative : !this.isEntiteCauchemar() && this.data.data.sante.sonne.value
};
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: this._rollCaracResult }
]
}
);
dialog.render(true);
}
async _rollCaracResult(rollData) {
let rolled = rollData.rolled;
let resumeCompetence = (rollData.diffLibre + rollData.diffConditions);
let explications = "<br>Points de taches : " + rolled.ptTache;
// Final chat message
let chatOptions = {
content: "<strong>Test : " + rollData.selectedCarac.label + " / " + resumeCompetence + "</strong>"
+ "<br>Difficultés <strong>libre : " + rollData.diffLibre + "</strong> / conditions : " + Misc.toSignedString(rollData.diffConditions) +" / état : " + rollData.etat
+ RdDResolutionTable.explain(rolled)
+ explications
}
ChatUtility.chatWithRollMode(chatOptions, this.name)
}
/* -------------------------------------------- */
async rollCompetence( name ) {
let rollData = {
competence: duplicate(RdDUtility.findCompetence( this.data.items, name)),
needSignificative : !this.isEntiteCauchemar() && this.data.data.sante.sonne.value
}
if (rollData.competence.type == 'competencecreature') {
// Fake competence pour créature
mergeObject(rollData.competence, { data : { defaut_carac: "carac_creature", categorie: "creature" } });
rollData.carac = { carac_creature: { label: competence.name, value: competence.data.carac_value } };
}
else{
rollData.carac = this.data.data.carac;
}
console.log("rollCompetence !!!", rollData.competence);
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: this._competenceResult }
]
}
);
dialog.render(true);
}
_competenceResult(rollData) {
ChatUtility.chatWithRollMode({
content: "<strong>Test : " + rollData.selectedCarac.label + " / " + rollData.competence.name + "</strong>"
+ "<br>Difficultés <strong>libre : " + rollData.diffLibre + "</strong> / conditions : " + Misc.toSignedString(rollData.diffConditions) +" / état : " + rollData.etat
+ RdDResolutionTable.explain(rollData.rolled)
+ "<br>Points de taches : " + rollData.rolled.ptTache + ", ajustement qualité: " + rollData.rolled.ptQualite
}, this.name)
}
/* -------------------------------------------- */
async rollAppelChance( )
{
let rollData = {
selectedCarac: this.getCaracByName('chance-actuelle'),
diffConditions: this.ajustementAstrologique()
}
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: this._appelChanceResult }
]
}
);
dialog.render(true);
}
_appelChanceResult(rollData) {
const message = {
user: game.user._id,
alias: this.name,
content: this.name + " fait appel à la chance" + RdDResolutionTable.explain(rollData.rolled)
};
if (rollData.rolled.isSuccess) {
message.content += "<br>Un point de chance est dépensée, l'action peut être retentée"
this.chanceActuelleIncDec(-1)
}
ChatMessage.create(message);
}
/* -------------------------------------------- */
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 } );
}
/* -------------------------------------------- */
ajustementAstrologique() {
//TODO: selon heure et heure de naissance...
return 0;
}
/* -------------------------------------------- */
getCaracByName(caracName) {
switch (caracName)
{
case 'reve-actuel':
return {
label: 'Rêve Actuel',
value: this.getReveActuel(),
type: "number",
ignoreEtatGeneral: true
};
case 'chance-actuelle':
return {
type: "number",
value: this.getChanceActuel(),
label: 'Chance actuelle',
ignoreEtatGeneral: true
};
default:
return this.data.data.carac[caracName]; // Per default
}
}
/* -------------------------------------------- */
getSortList() {
return this.data.items.filter(item => item.type == "sort");
}
/* -------------------------------------------- */
getDraconicList() {
return this.data.items.filter(item => item.data.categorie == 'draconic')
}
/* -------------------------------------------- */
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;
}
}
// Notification au MJ
ChatMessage.create( { content: game.user.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);
}
async rollCompetenceCreature( compName ) {
let competence = RdDUtility.findCompetence( this.data.items, compName);
if ( competence.type == 'competencecreature' && competence.data.iscombat ) {
armeItem = { name: compName, data: { dommages: competence.data.dommages, dommagesReels: competence.data.dommages} };
this.rollCompetenceCombat(competence, armeItem);
}
else {
this.rollCompetence(name);
}
}
/* -------------------------------------------- */
rollArme(competenceName, armeName) {
let armeItem = this.data.items.find(item=>item.type==="arme" && (item.name === armeName));
if (armeItem && competenceName == undefined) competenceName = armeItem.data.competence;
let competence = RdDUtility.findCompetence(this.data.items, competenceName == undefined? armeName : competenceName);
if (armeItem==undefined && competence.type == 'competencecreature' && competence.data.iscombat ) {
armeItem = { name: name, data: { dommages: competence.data.dommages, dommagesReels: competence.data.dommages} };
}
if (armeItem || armeName) {
this.rollCompetenceCombat( competenceName, armeItem );
} else {
this.rollCompetence( competence.name );
}
}
/* -------------------------------------------- */
async rollCompetenceCombat( name, armeItem=undefined, attackerRoll=undefined, attacker = undefined) {
let competence = RdDUtility.findCompetence( this.data.items, name);
if ( competence.type == 'competencecreature' && competence.data.iscombat ) {
armeItem = { name: name, data: { dommages: competence.data.dommages, dommagesReels: competence.data.dommages} };
}
console.log("rollCompetenceCombat !!!", competence, armeItem, attackerRoll);
// Common rollData values
let rollData = {
ajustementsConditions: CONFIG.RDD.ajustementsConditions,
difficultesLibres: CONFIG.RDD.difficultesLibres,
etat: this.data.data.compteurs.etat.value,
diffConditions: 0,
forceValue : attackerRoll ? (this.data.data.carac.force ? this.data.data.carac.force.value : this.data.data.carac.reve.value) : 0, // Utilisé pour le jet de recul
diffLibre: (attackerRoll) ? attackerRoll.diffLibre : 0,
attackerRoll: attackerRoll,
finalLevel: 0,
coupsNonMortels: false,
malusArmureValue: 0,
surencMalusFlag: false,
surencMalusValue: 0,
surencMalusApply: false,
isNatation: false,
useEncForNatation: false,
encValueForNatation: 0
}
if ( this.type == 'personnage ') {
rollData.malusArmureValue = (this.data.data.attributs && this.data.data.attributs.malusarmure) ? this.data.data.attributs.malusarmure.value : 0,
rollData.surencMalusFlag = (this.data.data.compteurs.surenc.value < 0);
rollData.surencMalusValue = this.data.data.compteurs.surenc.value;
rollData.surencMalusApply = false;
rollData.isNatation = name.toLowerCase().includes("natation");
rollData.useEncForNatation = false;
rollData.encValueForNatation = (this.encombrementTotal) ? Math.round(this.encombrementTotal) : 0;
}
if ( competence.type == 'competencecreature') { // Specific case for Creatures
competence.data.defaut_carac = "carac_creature"; // Fake default competence
competence.data.categorie = "creature"; // Fake default competence
rollData.competence = competence;
rollData.arme = armeItem;
rollData.carac = { carac_creature: { label: name, value: competence.data.carac_value } };
} else { // Usual competence
rollData.competence = competence;
if (armeItem ) {
armeItem.data.dommagesReels = armeItem.data.dommages; // Per default
if ( !armeItem.data.unemain && !armeItem.data.deuxmains) // Force default
armeItem.data.unemain = true;
if (armeItem.data.unemain && armeItem.data.deuxmains) { // manage 1/2 main
//console.log("Weapon", armeItem.data.dommages);
if ( armeItem.data.dommages.includes("/") ) { // Sanity check
if ( name.toLowerCase().includes("1 main") )
armeItem.data.dommagesReels = Number(armeItem.data.dommages.split("/")[0]);
else // 2 mains
armeItem.data.dommagesReels = Number(armeItem.data.dommages.split("/")[1]);
} else {
ui.notifications.info("Les dommages de l'arme à 1/2 mains " + name + " ne sont pas corrects (ie sous la forme X/Y)");
}
}
}
rollData.arme = armeItem;
rollData.carac = this.data.data.carac;
}
let html = await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/dialog-competence.html', rollData);
if (rollData.arme) {
if (await this.targetEntiteNonAccordee(this._getTarget(), 'avant-attaque')) {
return;
}
new RdDRollDialog("arme", html, rollData, this, attacker).render(true);
} else {
new RdDRollDialog("competence", html, rollData, this, attacker).render(true);
}
}
_getTarget() {
if (game.user.targets && game.user.targets.size == 1) {
for (let target of game.user.targets) {
return target;
}
}
return undefined;
}
/* -------------------------------------------- */
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( locData, domArmePlusDom )
{
let protection = 0;
for (const item of this.data.items) {
if (item.type == "armure" && item.data.equipe) {
let update = duplicate(item);
protection += new Roll(update.data.protection.toString()).roll().total;
update.data.deterioration += domArmePlusDom;
domArmePlusDom = 0; // Reset it
if ( update.data.deterioration >= 10) {
update.data.deterioration = 0;
if ( update.data.protection.toString().length == 1 )
update.data.protection = "1d"+update.data.protection+"-0";
else {
let regex = /d\(d+)\-(\d+)/g;
let res = regex.exec( update.data.protection );
update.data.protection = "1d"+res[1]+"-"+(parseInt(res[2])+1);
}
ChatMessage.create( {content: "Détérioration d'armure: " + update.data.protection } );
}
this.updateEmbeddedEntity("OwnedItem", update);
}
}
console.log("Final protect", protection);
return protection;
}
/* -------------------------------------------- */
async encaisserDommages( attackerRoll, attacker = undefined ) {
if (attacker && !await attacker.accorder(this, 'avant-encaissement')) {
return;
}
console.log("encaisserDommages", attackerRoll )
const armure = this.computeArmure( attackerRoll.loc, attackerRoll.domArmePlusDom);
let degatsReel = attackerRoll.degats - armure;
let result = RdDUtility.computeBlessuresSante(degatsReel, attackerRoll.mortalite);
result.endurance = Math.max(result.endurance, -Number(this.data.data.sante.endurance.value));
await this.santeIncDec("vie", result.vie);
await this.santeIncDec("endurance", result.endurance, (result.critiques > 0));
result.locName = (attackerRoll.loc) ? attackerRoll.loc.label : "Corps";
this.manageBlessures(result); // Will upate the result table
const blessureLegere = (result.legeres > 0 ? "une blessure légère" : "");
const blessureGrave = (result.graves > 0 ? "une blessure grave" : "");
const blessureCritique = (result.critiques > 0 ? "une blessure critique" : "");
let commonMsg = { title: "Blessures !", content: this.data.name + " a encaissé : " +
"<br>Encaissement final : " + degatsReel +
"<br>" + blessureLegere + blessureGrave + blessureCritique }
let addonMsg = "<br>Et a perdu : <br>" + result.endurance + " Endurance et " + result.vie + " Points de Vie";
if ( this.hasPlayerOwner ) {
commonMsg.content += addonMsg; // Message pour tout le monde
ChatMessage.create( commonMsg );
} else { // Le defenseur n'est pas un PJ, donc message complet uniquement pour le MJ
ChatMessage.create( commonMsg ); // Message pour tout le monde
let gmMsg = duplicate(commonMsg);
gmMsg.content = addonMsg; // Et message complémentaire uniquement pour le MJ
gmMsg.whisper = ChatMessage.getWhisperRecipients( "GM" );
ChatMessage.create( gmMsg );
}
this.computeEtatGeneral();
this.sheet.render(true);
}
/* -------------------------------------------- */
parerAttaque( attackerRoll, armeId, attacker = undefined )
{
let armeItem = this.getOwnedItem(armeId); // Item.data.data !
console.log("Going to PARY !!!!!!!!!", armeItem, attackerRoll.diffLibre);
if (armeItem.type == 'competencecreature') {
this.rollCompetenceCombat( armeItem.name, armeItem.data, attackerRoll, attacker);
} else {
this.rollCompetenceCombat( armeItem.data.data.competence, armeItem.data, attackerRoll, attacker);
}
}
/* -------------------------------------------- */
esquiverAttaque( attackerRoll, attacker = undefined )
{
this.rollCompetenceCombat( "esquive", undefined, attackerRoll, attacker );
}
/* -------------------------------------------- */
/** @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));
let message = {
content: "Jet de points actuels de rêve à " + rolled.finalLevel + RdDResolutionTable.explain(rolled) + "<br>",
whisper: ChatMessage.getWhisperRecipients(this.name)
};
if (rolled.isSuccess) {
await entite.setEntiteReveAccordee(this);
message.content += this.name + " s'est accordé avec " + entite.name;
}
else {
message.content+= this.name + " n'est pas accordé avec " + entite.name;
}
ChatMessage.create( message );
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;
}
}