/**
 * 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
    rollData.rolled = 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 = "Réussite particulière en attaque";
      message = message + "
Attaquer en Force";
      // 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 + "
Attaquer en Rapidité";
        message = message + "
Attaquer en Finesse";
      }
      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 += "
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 = "
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 += "
Attaque parée/esquivée !";
        } else  {
          explications += "
Esquive/Parade échouée, encaissement !";
          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 += "
Cible : " + target.actor.data.name;
          }
          explications += "
Encaissement : " + rollData.degats + "
Localisation : " + rollData.loc.label;
        } else {
          explications = "
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: "Test : " + rollData.selectedCarac.label + " / " + resumeCompetence + ""
          + "
Difficultés libre : " + rollData.diffLibre + " / conditions : " + Misc.toSignedString(rollData.diffConditions) +" / état : " + rollData.etat
          + RdDResolutionTable.explain(rolled)
          + "
" + quality + ""
          + 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 +"
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 0) {
      await this.santeIncDec("endurance", manquant);
      message.content += "
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 += "
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 += "
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 += "
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 += "
Vous faites un Rêve de Dragon de " + deRecuperation + " Points de rêve";
        message.content += await this.combattreReveDeDragon(deRecuperation);
      }
      else{
        message.content += "
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 += "
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 += "
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 += "
Vous gagnez une Tête de dragon: " + tete;
    }
    if (roll.isEchec) {
      message += "
Vous subissez une Queue de Dragon";
      this.ajouterQueue();
    }
    if (roll.isETotal) {
      message += "
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 += "
" + 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 + "
";
    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 0 ) {
      workData.graves += 1;
      blessuresData.graves += 1;
    }
      
    if ( workData.graves > 0) {
      for (let k=0; k 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) + "
";
    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 += "
" + RdDResolutionTable.explain(rollVolonte) + "
";
      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 += "
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 = "" + rollData.selectedCarac.label + ""
          + 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
    ? ("
Mise en réserve du sort en " + rollData.coordLabel + "(" + rollData.coord + ")")
    : "
Lancement du sort ";
    explications += sort.name + " : " + Misc.upperFirst(sort.data.draconic)
    + " pour " + sort.data.ptreve_reel + " points de Rêve"
    + "
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 += "
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 += "
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 += "
Echec TOTAL du sort : " + depenseReve + " Points de Rêve";
        // TODO: mise en réserve d'un échec total...
      } else {
        depenseReve = 0
        explications += "
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: "Test : " + rollData.selectedCarac.label + " / " + rollData.selectedDraconic.name + " / " + rollData.selectedSort.name + ""
      + "
Difficultés libre : " + rollData.diffLibre + " / 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 = "
Points de taches : " + rolled.ptTache;
    // Final chat message
    let chatOptions = {
      content: "Test : " + rollData.selectedCarac.label + " / " + resumeCompetence + ""
          + "
Difficultés libre : " + rollData.diffLibre + " / 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: "Test : " + rollData.selectedCarac.label + " / " + rollData.competence.name + ""
      + "
Difficultés libre : " + rollData.diffLibre + " / conditions : " + Misc.toSignedString(rollData.diffConditions) +" / état : " + rollData.etat
      + RdDResolutionTable.explain(rollData.rolled)
      + "
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 += "
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: "" + RdDUtility.makeHTMLfatigueMatrix( this.data.data.sante.fatigue.value,  this.data.data.sante.endurance.max ).html() + "
"
      },
      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é : " +
      "
Encaissement final : " + degatsReel +
      "
" + blessureLegere + blessureGrave + blessureCritique }
    let addonMsg = "
Et a perdu : 
" + 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);    
<<<<<<< HEAD
    if (armeItem.type == 'competencecreature') {
      this.rollCompetence(  armeItem.name, armeItem.data, attackerRoll, attacker);
    }  else {
      this.rollCompetence(  armeItem.data.data.competence, armeItem.data, attackerRoll, attacker);
    }
=======
    this.rollCompetenceCombat(  armeItem.data.data.competence, armeItem.data, attackerRoll, attacker);
>>>>>>> e2644f1 (Séparation compétences/combat)
  }
  
  /* -------------------------------------------- */  
  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) + "
",
      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;
  }
}