From 2bcc1a7ba384781f0e5ab96fec1999167334125b Mon Sep 17 00:00:00 2001 From: Vincent Vandemeulebrouck Date: Sat, 12 Dec 2020 21:58:44 +0100 Subject: [PATCH] =?UTF-8?q?Combat=20s=C3=A9par=C3=A9=20par=20actions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit la classe RdDCombat a pour vocation de gérer les interactions entre attaques, défenses, ... Séparation de: - attaque - parades - esquive - encaisser gestion des résultats de dés par actions - _onAttaqueParticuliere - _onAttaqueNormale - _onAttaqueEchec - _onAttaqueEchecTotal - _onParadeParticuliere - _onParadeNormale - _onParadeEchec - _onParadeEchecTotal - _onEsquiveParticuliere - _onEsquiveNormale - _onEsquiveEchec - _onEsquiveEchecTotal Séparation de demiSurprise et de needSignificative les callbacks des boutons dans le chat sont enregistrés cette classe Par ailleurs: - Fix mortel/non-mortel (coche puis décoche restait non-mortel) - création de classes pour les armes, les compétences - fix du recul (ne pouvait pas marcher) --- module/actor.js | 226 +++++++------ module/item-arme.js | 71 ++++ module/item-competence.js | 11 + module/rdd-combat.js | 572 +++++++++++++++++++++++++++++++++ module/rdd-main.js | 31 +- module/rdd-resolution-table.js | 83 +++-- module/rdd-roll-dialog.js | 16 +- module/rdd-roll.js | 17 +- module/rdd-utility.js | 101 +++--- 9 files changed, 925 insertions(+), 203 deletions(-) create mode 100644 module/item-arme.js create mode 100644 module/item-competence.js create mode 100644 module/rdd-combat.js diff --git a/module/actor.js b/module/actor.js index 2465332c..ec4b7044 100644 --- a/module/actor.js +++ b/module/actor.js @@ -18,6 +18,8 @@ import { ChatUtility } from "./chat-utility.js"; import { RdDItemSort } from "./item-sort.js"; import { Grammar } from "./grammar.js"; import { RdDCalendrier } from "./rdd-calendrier.js"; +import { RdDItemArme } from "./item-arme.js"; +import { RdDCombat } from "./rdd-combat.js"; export class RdDActor extends Actor { @@ -105,6 +107,10 @@ export class RdDActor extends Actor { this.computeEtatGeneral(); } + /* -------------------------------------------- */ + isCreature() { + return this.data.type == 'creature' || this.data.type == 'entite'; + } /* -------------------------------------------- */ getReveActuel() { return this.data.data.reve.reve.value; @@ -114,6 +120,14 @@ export class RdDActor extends Actor { return this.data.data.compteurs.chance.value; } + getForceValue() { + return this.data.data.carac.force ? this.data.data.carac.force.value : this.data.data.carac.reve.value; + } + + /* -------------------------------------------- */ + getCompetence(compName) { + return RdDUtility.findCompetence(this.data.items, compName); + } /* -------------------------------------------- */ getBestDraconic() { const list = this.getDraconicList().sort((a, b) => b.data.niveau - a.data.niveau); @@ -142,6 +156,24 @@ export class RdDActor extends Actor { /* -------------------------------------------- */ async performRoll(rollData, attacker = undefined) { + rollData.demiSurprise = this.isDemiSurprise(); + + // Manage weapon categories when parrying (cf. page 115 ) + if (rollData.arme && rollData.attackerRoll) { // Manage parade depending on weapon type, and change roll results + let attCategory = RdDItemArme.getCategorieArme(rollData.attackerRoll.arme); + let defCategory = RdDItemArme.getCategorieArme(rollData.arme); + if (defCategory == "bouclier") + rollData.needSignificative = false; + else if (attCategory != defCategory) + rollData.needSignificative = true; + // Do we need to make resistance roll for defender ? + if (attCategory.match("epee") && (defCategory == "hache" || defCategory == "lance")) + rollData.needResist = true; + } + if (!this.isEntiteCauchemar() && rollData.particuliereAttaque == "finesse") { + rollData.needSignificative = true; + } + // garder le résultat await RdDResolutionTable.rollData(rollData); @@ -152,11 +184,11 @@ export class RdDActor extends Actor { 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"; + 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"; + message = message + "
Attaquer en Rapidité"; + message = message + "
Attaquer en Finesse"; } ChatMessage.create( {content : message, whisper: ChatMessage.getWhisperRecipients( this.name ) } ); } else { @@ -201,23 +233,23 @@ export class RdDActor extends Actor { } /* -------------------------------------------- */ - computeRecul( rollData, encaisser = undefined ) { // Calcul du recul (p. 132) + async 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 reculNiveau = Misc.toInt(this.data.data.carac.taille.value) - (rollData.attackerRoll.forceValue+rollData.attackerRoll.arme.data.dommagesReels); + let recul = await RdDResolutionTable.roll( 10, reculNiveau ); let msg = ""; if (recul.isSuccess) { - msg = "Jet de Recul réussit, aucun effet !"; + msg = "Jet de Recul réussi, aucun effet !"; } else { - let chute = RdDResolutionTable.roll( this.data.data.carac.agilite.value, reculNiveau ); + let chute = await 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, + ChatMessage.create( {content: msg, user: game.user._id, whisper: [game.user._id, ChatMessage.getWhisperRecipients("GM") ] } ); } @@ -229,22 +261,6 @@ export class RdDActor extends Actor { 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); @@ -267,7 +283,7 @@ export class RdDActor extends Actor { explications += " Significative nécessaire!"; } encaisser = rollData.needSignificative ? !rolled.isSign : !rolled.isSuccess; - this.computeRecul( rollData, encaisser ); + await this.computeRecul( rollData, encaisser ); } else { // This is the attack roll! if (rolled.isSuccess) { let target = this._getTarget(); @@ -280,8 +296,8 @@ export class RdDActor extends Actor { 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.domArmePlusDom = this._calculBonusDegats(rollData); + rollData.degats = new Roll("2d10").roll().total + rollData.domArmePlusDom; // Dégats totaux rollData.loc = RdDUtility.getLocalisation(); if (target) @@ -334,6 +350,25 @@ export class RdDActor extends Actor { } } + getSurprise() { + if (this.isEntiteCauchemar()) { + return ''; + } + // TODO: gestion des conditions de demi-surprise + if ( this.data.data.sante.sonne.value) { + return 'demi'; + } + return ''; + } + + isDemiSurprise() { + return this.getSurprise() == 'demi'; + } + + isSurpriseTotale() { + return this.getSurprise() == 'totale'; + } + /* -------------------------------------------- */ static _calculMortaliteEncaissement(rollData, target) { const mortalite = target.actor.isEntiteCauchemar() ? "cauchemar" : (rollData.mortalite ? rollData.mortalite : "mortel"); @@ -342,20 +377,23 @@ export class RdDActor extends Actor { } /* -------------------------------------------- */ - _calculBonusDommages(carac, arme, isForce=false) { - if ( arme.name.toLowerCase() == "esquive") return 0; // Specific case management - let dmgArme = 0; + _calculBonusDegats(rollData) { + if ( rollData.arme.name.toLowerCase() == "esquive") return 0; // Specific case management + + const dmgConditions = rollData.isCharge ? 2 : 0; + const dmgParticuliere = rollData.particuliereAttaque == 'force' ? 5 : 0; + const dmgArme = parseInt(rollData.arme.data.dommages); + const dmgPerso = this._calculBonusDegatsActor(rollData.selectedCarac.label, dmgArme); + return dmgArme + dmgPerso + dmgConditions + dmgParticuliere; + } + + _calculBonusDegatsActor(caracName, dmgArme) { 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); - } + switch (caracName) { + case "Tir": return 0; + case "Lancer": return Math.max(0, Math.min(dmgArme, dmgPerso)); } - return dmgArme + dmgPerso; + return dmgPerso; } /* -------------------------------------------- */ @@ -639,7 +677,7 @@ export class RdDActor extends Actor { /* -------------------------------------------- */ async updateCreatureCompetence( compName, fieldName, compValue ) { - let comp = RdDUtility.findCompetence( this.data.items, compName); + let comp = this.getCompetence(compName); console.log( comp ); if ( comp ) { const update = {_id: comp._id } @@ -657,7 +695,7 @@ export class RdDActor extends Actor { /* -------------------------------------------- */ async updateCompetence( compName, compValue ) { - let comp = RdDUtility.findCompetence( this.data.items, compName); + let comp = this.getCompetence(compName); if ( comp ) { let troncList = RdDUtility.isTronc( compName ); let maxNiveau = compValue; @@ -679,7 +717,7 @@ export class RdDActor extends Actor { /* -------------------------------------------- */ async updateCompetenceXP( compName, compValue ) { - let comp = RdDUtility.findCompetence( this.data.items, compName); + let comp = this.getCompetence(compName); if ( comp ) { const update = {_id: comp._id, 'data.xp': compValue }; const updated = await this.updateEmbeddedEntity("OwnedItem", update); // Updates one EmbeddedEntity @@ -1251,13 +1289,13 @@ export class RdDActor extends Actor { } /* -------------------------------------------- */ - _createCallbackExperience() { + createCallbackExperience() { return { condition: r => r.rolled.isPart && r.finalLevel < 0 && game.settings.get("core", "rollMode") != 'selfroll', action: r => this._appliquerAjoutExperience(r) }; } - + /* -------------------------------------------- */ async _appliquerAjoutExperience(rollData) { this.appliquerExperience( rollData.rolled, rollData.selectedCarac.label, (rollData.competence) ? rollData.competence.data.name: undefined ); @@ -1296,7 +1334,7 @@ export class RdDActor extends Actor { name: 'lancer-un-sort', label: 'Lancer un sort', callbacks: [ - this._createCallbackExperience(), + this.createCallbackExperience(), { action: r => this._rollUnSortResult(r, false) } ] }, @@ -1304,7 +1342,7 @@ export class RdDActor extends Actor { name: 'mettre-en-reserve', label: 'Mettre un sort en réserve', callbacks: [ - this._createCallbackExperience(), + this.createCallbackExperience(), { action: r => this._rollUnSortResult(r, true) } ] } @@ -1405,7 +1443,7 @@ export class RdDActor extends Actor { name: 'jet-'+caracName, label: 'Jet ' + Grammar.apostrophe('de', rollData.selectedCarac.label), callbacks: [ - this._createCallbackExperience(), + this.createCallbackExperience(), { action: this._rollCaracResult } ] } @@ -1434,25 +1472,25 @@ export class RdDActor extends Actor { /* -------------------------------------------- */ async rollCompetence( name ) { let rollData = { - competence: duplicate(RdDUtility.findCompetence( this.data.items, name)), + competence: duplicate(this.getCompetence(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.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); + console.log("rollCompetence !!!", rollData); const dialog = await RdDRoll.create(this, rollData, {html: 'systems/foundryvtt-reve-de-dragon/templates/dialog-competence.html'}, { name: 'jet-competence', label: 'Jet ' +Grammar.apostrophe('de', name), callbacks: [ - this._createCallbackExperience(), + this.createCallbackExperience(), { action: this._competenceResult } ] } ); @@ -1480,7 +1518,7 @@ export class RdDActor extends Actor { name: 'appelChance', label: 'Appel à la chance', callbacks: [ - this._createCallbackExperience(), + this.createCallbackExperience(), { action: r => this._appelChanceResult(r) } ] } @@ -1516,21 +1554,24 @@ export class RdDActor extends Actor { /* -------------------------------------------- */ appliquerExperience( rolled, caracName, competenceName = undefined ) { - // TODO gestion derobe/tir/lance/melee - if (caracName == 'derobee') caracName = 'agilite'; - + // TODO: en cas de désir lancinant, pas d'expérience sur particulière if ( rolled.isPart && rolled.finalLevel < 0) { - if ( !compentenceName) { - this.actor.data.data.carac[caracName].xp += 1; + + if (caracName == 'derobee') caracName = 'agilite'; + + let xp = Math.abs(rolled.finalLevel); + let xpCarac = Math.floor(xp / 2); // impair: arrondi inférieur en carac + + if ( !competenceName) { + this.actor.data.data.carac[caracName].xp += Math.max(xpCarac, 1); } else { let competence = RdDUtility.findCompetence( this.data.items, competenceName ); - let xpCarac = Math.floor(Math.abs(rolled.finalLevel) / 2); - if ( xpCarac == 0 ) { - competence.data.xp += 1; // XP en priorité à la compétencence - } else - competence.data.xp += Math.ceil(Math.abs(rolled.finalLevel) / 2); // XP majoritaire en compentece (ie cas impair) + let xpComp = xp - xpCarac; + + competence.data.xp += xpComp; this.actor.data.data.carac[caracName].xp += xpCarac; } + // TODO: save actor? } } @@ -1630,10 +1671,8 @@ export class RdDActor extends Actor { /* -------------------------------------------- */ async rollCompetenceCreature( compName ) { - let competence = RdDUtility.findCompetence( this.data.items, compName); - + let competence = this.getCompetence(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 { @@ -1642,27 +1681,31 @@ export class RdDActor extends Actor { } /* -------------------------------------------- */ - rollArme(competenceName, armeName) { + rollArme(compName, armeName = undefined) { + 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 ); + if (armeItem && compName == undefined) compName = armeItem.data.competence; + let competence = this.getCompetence(compName == undefined? armeName : compName); + + if (armeItem || armeName || (competence.type == 'competencecreature' && competence.data.iscombat)) { + this.rollCompetenceCombat( compName, armeItem ); } else { this.rollCompetence( competence.name ); } } - + /* -------------------------------------------- */ - async rollCompetenceCombat( name, armeItem=undefined, attackerRoll=undefined, attacker = undefined) { - let competence = RdDUtility.findCompetence( this.data.items, name); + async rollCompetenceCombat( compName, armeItem=undefined, attackerRoll=undefined, attacker = undefined) { + let competence = this.getCompetence(compName); + + if (RdDCombat.isActive()) { + const assaut = RdDCombat.createUsingTarget(this); + assaut.attaque(competence, armeItem); + return; + } + if ( competence.type == 'competencecreature' && competence.data.iscombat ) { - armeItem = { name: name, data: { dommages: competence.data.dommages, dommagesReels: competence.data.dommages} }; + armeItem = { name: compName, data: { dommages: competence.data.dommages, dommagesReels: competence.data.dommages} }; } console.log("rollCompetenceCombat !!!", competence, armeItem, attackerRoll); @@ -1672,7 +1715,7 @@ export class RdDActor extends Actor { 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 + forceValue : attackerRoll ? this.getForceValue() : 0, // Utilisé pour le jet de recul diffLibre: (attackerRoll) ? attackerRoll.diffLibre : 0, attackerRoll: attackerRoll, finalLevel: 0, @@ -1690,7 +1733,7 @@ export class RdDActor extends Actor { 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.isNatation = compName.toLowerCase().includes("natation"); rollData.useEncForNatation = false; rollData.encValueForNatation = (this.encombrementTotal) ? Math.round(this.encombrementTotal) : 0; } @@ -1700,24 +1743,11 @@ export class RdDActor extends Actor { competence.data.categorie = "creature"; // Fake default competence rollData.competence = competence; rollData.arme = armeItem; - rollData.carac = { carac_creature: { label: name, value: competence.data.carac_value } }; + rollData.carac = { carac_creature: { label: compName, 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)"); - } - } + if (armeItem) { + armeItem = RdDItemArme.armeUneOuDeuxMains(armeItem, compName.toLowerCase().includes("1 main")); } rollData.arme = armeItem; rollData.carac = this.data.data.carac; diff --git a/module/item-arme.js b/module/item-arme.js new file mode 100644 index 00000000..c4fab6c7 --- /dev/null +++ b/module/item-arme.js @@ -0,0 +1,71 @@ + +export class RdDItemArme extends Item { + + /* -------------------------------------------- */ + static getCategorieArme(arme) { + if (arme.data.competence == undefined) { + return 'competencecreature'; + } + let compname = arme.data.competence.toLowerCase(); + + if (compname.match("hache")) return "hache"; + if (compname.match("hast")) return "hast"; + if (compname.match("lance")) return "lance"; + if (compname.match("bouclier")) return "bouclier"; + if (compname.match("masse")) return "masse"; + if (compname.match("fléau")) return "fleau"; + if (compname.match("epée") || compname.match("épée")) { + let armename = arme.name.toLowerCase(); + if (armename.match("gnome")) + return "epee_courte"; + return "epee_longue"; + } + if (compname.match("dague")) { + return "epee_courte"; + } + return ""; + } + + /* -------------------------------------------- */ + static needParadeSignificative(armeAttaque, armeParade) { + let attCategory = RdDItemArme.getCategorieArme(armeAttaque); + let defCategory = RdDItemArme.getCategorieArme(armeParade); + if (defCategory == "bouclier" || attCategory == defCategory) { + return false; + } + let armeParadeName = armeParade.name.toLowerCase(); + let armeAttaqueName = armeAttaque.name.toLowerCase(); + if (armeParadeName.match("dague") && armeAttaqueName.match(/((e|é)pée dragone|esparlongue|demi-dragonne)/)) { + return false; + } + // Manage weapon categories when parrying (cf. page 115 ) + return true; + } + + /* -------------------------------------------- */ + static armeUneOuDeuxMains(arme, aUneMain) { + arme.data.unemain = arme.data.unemain || !arme.data.deuxmains; + const uneOuDeuxMains = arme.data.unemain && arme.data.deuxmains; + if (arme.data.dommages.includes("/")) { // Sanity check + arme = duplicate(arme); + + const tableauDegats = arme.data.dommages.split("/"); + if (aUneMain) + arme.data.dommagesReels = Number(tableauDegats[0]); + else // 2 mains + arme.data.dommagesReels = Number(tableauDegats[1]); + } + else { + arme.data.dommagesReels = Number(arme.data.dommages); + } + + if ( + (uneOuDeuxMains && !arme.data.dommages.includes("/")) || + (!uneOuDeuxMains && arme.data.dommages.includes("/"))) + { + ui.notifications.info("Les dommages de l'arme à 1/2 mains " + arme.name + " ne sont pas corrects (ie sous la forme X/Y)"); + } + return arme; + } + +} diff --git a/module/item-competence.js b/module/item-competence.js new file mode 100644 index 00000000..70e7ed9b --- /dev/null +++ b/module/item-competence.js @@ -0,0 +1,11 @@ +export class RdDItemCompetence extends Item { + + /* -------------------------------------------- */ + static isCompetenceMelee(name) { + return name.toLowerCase().match(/(epée|épée|hache|fleau|fléau|masse|lance|hast|dague|bouclier)/); + } + static isArmeUneMain(competence) { + return competence.name.toLowerCase().includes("1 main"); + } + +} \ No newline at end of file diff --git a/module/rdd-combat.js b/module/rdd-combat.js new file mode 100644 index 00000000..193fdcdc --- /dev/null +++ b/module/rdd-combat.js @@ -0,0 +1,572 @@ +import { RdDActor } from "./actor.js"; +import { ChatUtility } from "./chat-utility.js"; +import { RdDItemArme } from "./item-arme.js"; +import { RdDItemCompetence } from "./item-competence.js"; +import { Misc } from "./misc.js"; +import { RdDResolutionTable } from "./rdd-resolution-table.js"; +import { RdDRoll } from "./rdd-roll.js"; +import { RdDUtility } from "./rdd-utility.js"; + +export class RdDCombat { + + static isActive() { + return game.settings.get("foundryvtt-reve-de-dragon", "moteur-combat") == 'experimental'; + } + + /* -------------------------------------------- */ + static createUsingTarget(attacker) { + const target = RdDCombat.getTarget(); + if (target == undefined) { + ui.notifications.warn("Vous devriez choisir une cible à attaquer!"); + } + return this.create(attacker, target ? target.actor : undefined, target) + } + + static getTarget() { + if (game.user.targets && game.user.targets.size == 1) { + for (let target of game.user.targets) { + return target; + } + } + return undefined; + } + + static create(attacker, defender, target = undefined) { + return new RdDCombat(attacker, defender, target) + } + + static createForEvent(event) { + let attackerId = event.currentTarget.attributes['data-attackerId'].value; + let attacker = game.actors.get(attackerId); + + const dataDefenderTokenId = event.currentTarget.attributes['data-defenderTokenId']; + if (dataDefenderTokenId) { + let defenderToken = canvas.tokens.get(dataDefenderTokenId.value); + let defender = defenderToken.actor; + + return this.create(attacker, defender); + } + return this.createUsingTarget(attacker) + } + + constructor(attacker, defender, target) { + this.attacker = attacker; + this.defender = defender; + this.target = target; + this.attackerId = this.attacker.data._id; + this.defenderId = this.defender.data._id; + this.defenderTokenId = target ? target.data._id : undefined; + } + + /* -------------------------------------------- */ + static registerChatCallbacks(html) { + for (let button of ['#parer-button', '#esquiver-button', '#particuliere-attaque', '#encaisser-button']) { + html.on("click", button, event => { + event.preventDefault(); + RdDCombat.createForEvent(event).onEvent(button, event); + }); + } + } + + /* -------------------------------------------- */ + async onEvent(button, event) { + if (!RdDCombat.isActive()) { + return; + } + let rollData = game.system.rdd.rollDataHandler[this.attackerId]; + // TODO: enlever le ChatMessage? + switch (button) { + case '#particuliere-attaque': return await this.choixParticuliere(rollData, event.currentTarget.attributes['data-mode'].value); + case '#parer-button': return this.parade(rollData, event.currentTarget.attributes['data-armeid'].value); + case '#esquiver-button': return this.esquive(rollData); + case '#encaisser-button': return this.encaisser(rollData, event.currentTarget.attributes['data-defenderTokenId'].value); + } + } + + /* -------------------------------------------- */ + async attaque(competence, arme) { + if (!await this.accorderEntite('avant-attaque')) { + return; + } + + let rollData = this._prepareAttaque(competence, arme); + console.log("RdDCombat.attaque >>>", rollData); + + const dialog = await RdDRoll.create(this.attacker, rollData, + { html: 'systems/foundryvtt-reve-de-dragon/templates/dialog-competence.html' }, { + name: 'jet-attaque', + label: 'Attaque: ' + (arme ? arme.name : competence.name), + callbacks: [ + this.attacker.createCallbackExperience(), + { condition: RdDResolutionTable.isParticuliere, action: r => this._onAttaqueParticuliere(r) }, + { condition: r => (RdDResolutionTable.isReussite(r) && !RdDResolutionTable.isParticuliere(r)), action: r => this._onAttaqueNormale(r) }, + { condition: RdDResolutionTable.isEchecTotal, action: r => this._onAttaqueEchecTotal(r) }, + { condition: RdDResolutionTable.isEchec, action: r => this._onAttaqueEchec(r) } + ] + }); + dialog.render(true); + } + + /* -------------------------------------------- */ + _prepareAttaque(competence, arme) { + let rollData = { + coupsNonMortels: false, + competence: competence, + demiSurprise: this.attacker.isDemiSurprise() + }; + + if (this.attacker.isCreature()) { + this._prepareRollDataCreature(rollData, competence); + } + else { + // Usual competence + rollData.arme = RdDItemArme.armeUneOuDeuxMains(arme, RdDItemCompetence.isArmeUneMain(competence)); + } + return rollData; + } + + _prepareRollDataCreature(rollData, competence) { + competence = duplicate(competence); + competence.data.defaut_carac = "carac_creature"; + competence.data.categorie = "creature"; + + rollData.competence = competence; + rollData.carac = { "carac_creature": { label: competence.name, value: competence.data.carac_value } }; + rollData.arme = { + name: competence.name, data: { + dommages: competence.data.dommages, + dommagesReels: competence.data.dommages + } + }; + } + + /* -------------------------------------------- */ + _onAttaqueParticuliere(rollData) { + console.log("RdDCombat.onAttaqueParticuliere >>>", rollData); + let message = "Réussite particulière en attaque"; + 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 += "
Attaquer en Rapidité"; + message += "
Attaquer en Finesse"; + } + game.system.rdd.rollDataHandler[this.attackerId] = rollData; + // TODO: use a dialog? + ChatMessage.create({ content: message, whisper: ChatMessage.getWhisperRecipients(this.attacker.name) }); + } + + /* -------------------------------------------- */ + async _onAttaqueNormale(rollData) { + console.log("RdDCombat.onAttaqueNormale >>>", rollData); + if (!await this.accorderEntite('avant-defense')) { + return; + } + + let explications = ""; + + // Message spécial pour la rapidité, qui reste difficile à gérer automatiquement + if (rollData.particuliereAttaque == 'rapidite') { + ChatMessage.create({ + content: "Vous avez attaqué en Rapidité. Vous pourrez faire une deuxième attaque, ou utiliser votre arme pour vous défendre.", + whisper: ChatMessage.getWhisperRecipients(this.attacker.name) + }); + } + + rollData.domArmePlusDom = this.attacker._calculBonusDegats(rollData); + rollData.degats = new Roll("2d10").roll().total + rollData.domArmePlusDom; // Dégats totaux + rollData.loc = RdDUtility.getLocalisation(); + + if (this.target) { + rollData.mortalite = this._calculMortaliteEncaissement(rollData); + explications += "
Cible : " + this.defender.data.name; + } + explications += "
Encaissement : " + rollData.degats + "
Localisation : " + rollData.loc.label; + + // Save rollData for defender + game.system.rdd.rollDataHandler[this.attackerId] = duplicate(rollData); + + // Final chat message + let chatOptions = { + 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) + + explications + } + ChatUtility.chatWithRollMode(chatOptions, this.attacker.name) + if (this.target) { + this._messageDefenseur(rollData); + } + } + + /* -------------------------------------------- */ + _messageDefenseur(rollData) { + console.log("RdDCombat._messageDefenseur", rollData, " / ", this.attacker, this.target, this.target.actor.isToken, this.attacker.data._id, rollData.competence.data.categorie); + + let content = "" + this.defender.name + " doit se défendre :
"; + + // parades + let filterArmesParade = this._getFilterArmesParade(rollData.competence.data.categorie); + for (const arme of this.defender.data.items.filter(filterArmesParade)) { + content += "
Parer avec " + arme.name + ""; + } + + // esquive + if (rollData.competence.data.categorie == 'melee' || rollData.competence.data.categorie == "lancer" || rollData.competence.data.categorie == 'competencecreature') { + content += "
Esquiver"; + } + + // encaisser + content += "
Encaisser !"; + + content += "
" + + let defense = { + title: "Défense en combat", + content: content, + whisper: ChatMessage.getWhisperRecipients(this.defender.name), + attackerId: this.attackerId, + defenderTokenId: this.defenderTokenId, + rollMode: true, + rollData: duplicate(rollData) + }; + + // envoyer le message de defense + if (!game.user.isGM || this.defender.hasPlayerOwner) { + game.socket.emit("system.foundryvtt-reve-de-dragon", { msg: "msg_defense", data: defense }); + } else { + defense.whisper = [game.user]; + } + + if (game.user.isGM) { // Always push the message to the MJ + ChatMessage.create(defense); + } + } + + /* -------------------------------------------- */ + _getFilterArmesParade(categorie) { + switch (categorie) { + case 'tir': + case 'lancer': + return arme => arme.type == "arme" && arme.data.competence.toLowerCase().match("bouclier"); + default: + return arme => (arme.type == "arme" && RdDItemCompetence.isCompetenceMelee(arme.data.competence)) || (arme.type == "competencecreature" && arme.data.isparade) + } + } + /* -------------------------------------------- */ + _calculMortaliteEncaissement(rollData) { + const mortalite = this.defender.isEntiteCauchemar() ? "cauchemar" : (rollData.mortalite ? rollData.mortalite : "mortel"); + console.log("Mortalité : ", mortalite, this.defender.data.type); + return mortalite; + } + + + _onAttaqueEchecTotal(rollData) { + console.log("RdDCombat.onEchecTotal >>>", rollData); + // TODO: proposer un résultat d'échec total + let chatOptions = { + content: "Echec total à l'attaque!" + } + ChatUtility.chatWithRollMode(chatOptions, this.attacker.name) + } + + _onAttaqueEchec(rollData) { + console.log("RdDCombat.onAttaqueEchec >>>", rollData); + let target = this._getTarget(); + let chatOptions = { + content: "Test : " + rollData.selectedCarac.label + " / " + rollData.competence.name + "" + + "
Difficultés libre : " + rollData.diffLibre + " / conditions : " + Misc.toSignedString(rollData.diffConditions) + " / état : " + rollData.etat + + RdDResolutionTable.explain(rolled) + + (target ? "
Cible : " + this.defender.data.name : "") + } + ChatUtility.chatWithRollMode(chatOptions, this.attacker.name) + } + + /* -------------------------------------------- */ + async choixParticuliere(rollData, choix) { + console.log("RdDCombat.choixParticuliere >>>", rollData, choix); + rollData.particuliereAttaque = choix; + await this._onAttaqueNormale(rollData); + } + + /* -------------------------------------------- */ + async parade(attackerRoll, armeParadeId) { + let arme = this.defender.getOwnedItem(armeParadeId); + + console.log("RdDCombat.parade >>>", attackerRoll, armeParadeId, arme); + + let rollData = this._prepareParade(attackerRoll, arme); + + const dialog = await RdDRoll.create(this.defender, rollData, + { html: 'systems/foundryvtt-reve-de-dragon/templates/dialog-competence.html' }, { + name: 'jet-parade', + label: 'Parade: ' + (arme ? arme.name : rollData.competence.name), + callbacks: [ + this.defender.createCallbackExperience(), + { condition: RdDResolutionTable.isParticuliere, action: r => this._onParadeParticuliere(r) }, + { condition: RdDResolutionTable.isReussite, action: r => this._onParadeNormale(r) }, + { condition: RdDResolutionTable.isEchecTotal, action: r => this._onParadeEchecTotal(r) }, + { condition: RdDResolutionTable.isEchec, action: r => this._onParadeEchec(r) } + ] + }); + dialog.render(true); + } + + _prepareParade(attackerRoll, arme) { + const isCreature = this.defender.isCreature(); + const compName = isCreature ? arme.name : arme.data.data.competence; + const competence = this.defender.getCompetence(compName); + const armeAttaque = attackerRoll.arme; + const armeParade = arme.data; + + if (compName != competence.name) { + // TODO: toujours utiliser competence.name ... + ui.notifications.warn("Différence entre compétence " + competence.name + " et compétence de l'arme " + compName); + } + + let rollData = { + forceValue: this.defender.getForceValue(), + diffLibre: attackerRoll.diffLibre, + attackerRoll: attackerRoll, + competence: competence, + arme: arme.data, + demiSurprise: this.defender.isDemiSurprise(), + needSignificative: this._needSignificative(attackerRoll) || RdDItemArme.needParadeSignificative(armeAttaque, armeParade), + needResist: this._needResist(armeAttaque, armeParade), + carac: this.defender.data.data.carac + }; + if (isCreature) { + this._prepareRollDataCreature(rollData, competence); + } + return rollData; + } + + /* -------------------------------------------- */ + _needSignificative(attackerRoll) { + return attackerRoll.particuliereAttaque == 'finesse'; + } + + /* -------------------------------------------- */ + _needResist(armeAttaque, armeParade) { + // Manage weapon categories when parrying (cf. page 115 ) + let attCategory = RdDItemArme.getCategorieArme(armeAttaque); + let defCategory = RdDItemArme.getCategorieArme(armeParade); + + return (attCategory.match("epee") && (defCategory == "hache" || defCategory == "lance")); + } + + /* -------------------------------------------- */ + _onParadeParticuliere(rollData) { + console.log("RdDCombat._onParadeParticuliere >>>", rollData); + if (!rollData.attackerRoll.isPart) { + // TODO: attaquant doit jouer résistance et peut être désarmé p132 + } + let chatOptions = { + content: "Vous pouvez utiliser votre arme pour une deuxième parade!" + } + ChatUtility.chatWithRollMode(chatOptions, this.defender.name) + } + + /* -------------------------------------------- */ + async _onParadeNormale(rollData) { + console.log("RdDCombat._onParadeNormale >>>", rollData); + if (rollData.needResist && !rollData.rolled.isPart) { + // TODO: déplacer la logique détérioration armure dans RdDCombat + this.defender.computeDeteriorationArme(rollData); + } + await this.defender.computeRecul(rollData, false); + + let chatOptions = { + 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) + + "
Attaque parée!" + } + + ChatUtility.chatWithRollMode(chatOptions, this.defender.name) + } + + /* -------------------------------------------- */ + _onParadeEchecTotal(rollData) { + console.log("RdDCombat._onParadeEchecTotal >>>", rollData); + // TODO: proposer un résultat d'échec total + let chatOptions = { + content: "Echec total à la parade!" + } + ChatUtility.chatWithRollMode(chatOptions, this.defender.name) + } + + /* -------------------------------------------- */ + async _onParadeEchec(rollData) { + console.log("RdDCombat._onParadeEchec >>>", rollData); + + let explications = "
Parade échouée, encaissement !"; + if (rollData.demiSurprise) { + explications += " Demi surprise!"; + } + if (rollData.needSignificative) { + explications += " Significative nécessaire!"; + } + + let chatOptions = { + 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) + + explications + } + + ChatUtility.chatWithRollMode(chatOptions, this.defender.name) + + await this.defender.computeRecul(rollData, true); + // TODO: gestion message pour chance/encaissement + this.encaisser(rollData.attackerRoll); + } + + /* -------------------------------------------- */ + async esquive(attackerRoll) { + let esquive = this.defender.getCompetence("esquive"); + if (esquive == undefined) { + ui.notifications.error(this.defender.name + " n'a pas de compétence 'esquive'"); + return; + } + console.log("RdDCombat.esquive >>>", attackerRoll, esquive); + let rollData = this._prepareEsquive(attackerRoll, esquive); + + const dialog = await RdDRoll.create(this.defender, rollData, + { html: 'systems/foundryvtt-reve-de-dragon/templates/dialog-competence.html' }, { + name: 'jet-esquive', + label: 'Esquiver', + callbacks: [ + this.defender.createCallbackExperience(), + { condition: RdDResolutionTable.isParticuliere, action: r => this._onEsquiveParticuliere(r) }, + { condition: RdDResolutionTable.isReussite, action: r => this._onEsquiveNormale(r) }, + { condition: RdDResolutionTable.isEchecTotal, action: r => this._onEsquiveEchecTotal(r) }, + { condition: RdDResolutionTable.isEchec, action: r => this._onEsquiveEchec(r) }, + ] + }); + dialog.render(true); + } + + _prepareEsquive(attackerRoll, competence) { + let rollData = { + forceValue: this.defender.getForceValue(), + diffLibre: attackerRoll.diffLibre, + attackerRoll: attackerRoll, + competence: competence, + demiSurprise: this.defender.isDemiSurprise(), + needSignificative: this._needSignificative(attackerRoll), + carac: this.defender.data.data.carac + }; + + if (this.defender.isCreature()) { + this._prepareRollDataCreature(rollData, competence); + } + return rollData; + } + + /* -------------------------------------------- */ + _onEsquiveParticuliere(rollData) { + console.log("RdDCombat._onEsquiveParticuliere >>>", rollData); + let chatOptions = { + content: "Vous pouvez esquiver une deuxième attaque!" + } + ChatUtility.chatWithRollMode(chatOptions, this.defender.name) + } + + /* -------------------------------------------- */ + _onEsquiveNormale(rollData) { + console.log("RdDCombat._onEsquiveNormal >>>", rollData); + let chatOptions = { + 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) + + "
Attaque esquivée!" + } + + ChatUtility.chatWithRollMode(chatOptions, this.defender.name) + } + /* -------------------------------------------- */ + _onEsquiveEchecTotal(rollData) { + console.log("RdDCombat._onEsquiveEchecTotal >>>", rollData); + // TODO: proposer un résultat d'échec total + let chatOptions = { + content: "Echec total à l'esquive'!" + } + ChatUtility.chatWithRollMode(chatOptions, this.defender.name) + } + /* -------------------------------------------- */ + async _onEsquiveEchec(rollData) { + console.log("RdDCombat._onEsquiveEchec >>>", rollData); + + let explications = "
Esquive échouée, encaissement !"; + if (rollData.demiSurprise) { + explications += " Demi surprise!"; + } + if (rollData.needSignificative) { + explications += " Significative nécessaire!"; + } + + let chatOptions = { + 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) + + explications + } + + ChatUtility.chatWithRollMode(chatOptions, this.defender.name) + + await this.defender.computeRecul(rollData, true); + this.encaisser(rollData.attackerRoll); + } + /* -------------------------------------------- */ + encaisser(attackerRoll) { + // TODO: gestion message pour chance/encaissement + this.encaisser(attackerRoll, this.defenderTokenId); + } + + /* -------------------------------------------- */ + encaisser(attackerRoll, defenderTokenId) { + defenderTokenId = defenderTokenId || this.defenderTokenId; + console.log("RdDCombat.encaisser >>>", attackerRoll, defenderTokenId); + + if (game.user.isGM) { // Current user is the GM -> direct access + attackerRoll.attackerId = this.attackerId; + attackerRoll.defenderTokenId = defenderTokenId; + this.defender.encaisserDommages(attackerRoll, this.attacker); + } else { // Emit message for GM + game.socket.emit("system.foundryvtt-reve-de-dragon", { + msg: "msg_encaisser", + data: { attackerId: this.attackerId, defenderTokenId: defenderTokenId } + }); + } + } + + /* -------------------------------------------- */ + /* retourne true si on peut continuer, false si on ne peut pas continuer */ + async accorderEntite(when = 'avant-encaissement') { + if (when != game.settings.get("foundryvtt-reve-de-dragon", "accorder-entite-cauchemar") + || this.defender == undefined + || !this.defender.isEntiteCauchemar() + || this.defender.isEntiteCauchemarAccordee(this.attacker)) { + return true; + } + + let rolled = await RdDResolutionTable.roll(this.attacker.getReveActuel(), - Number(this.defender.data.data.carac.niveau.value)); + + let message = { + content: "Jet de points actuels de rêve à " + rolled.finalLevel + RdDResolutionTable.explain(rolled) + "
", + whisper: ChatMessage.getWhisperRecipients(this.attacker.name) + }; + + if (rolled.isSuccess) { + await this.defender.setEntiteReveAccordee(this.attacker); + message.content += this.attacker.name + " s'est accordé avec " + this.defender.name; + } + else { + message.content += this.attacker.name + " n'est pas accordé avec " + this.defender.name; + } + + ChatMessage.create(message); + return rolled.isSuccess; + } + +} \ No newline at end of file diff --git a/module/rdd-main.js b/module/rdd-main.js index 7fb9dec1..f31bd739 100644 --- a/module/rdd-main.js +++ b/module/rdd-main.js @@ -123,25 +123,41 @@ Hooks.once("init", async function() { /* -------------------------------------------- */ game.settings.register("foundryvtt-reve-de-dragon", "calendrier", { - name: "calendrier", - scope: "world", - config: false, - type: Object - }); + name: "calendrier", + scope: "world", + config: false, + type: Object + }); + /* -------------------------------------------- */ game.settings.register("foundryvtt-reve-de-dragon", "liste-nombre-astral", { name: "liste-nombre-astral", scope: "world", config: false, type: Object - }); + }); + /* -------------------------------------------- */ game.settings.register("foundryvtt-reve-de-dragon", "calendrier-pos", { name: "calendrierPos", scope: "world", config: false, type: Object - }); + }); + + /* -------------------------------------------- */ + game.settings.register("foundryvtt-reve-de-dragon", "moteur-combat", { + name: "Utiliser le moteur de combat", + scope: "world", + config: true, + type: String, + choices: { // If choices are defined, the resulting setting will be a select menu + "standard": "Standard", + "experimental": "Expérimental" + }, + default: "standard" + }); + /* -------------------------------------------- */ game.settings.register("foundryvtt-reve-de-dragon", "dice-so-nice", { name: "Montrer les dés pour toutes les jets", @@ -151,6 +167,7 @@ Hooks.once("init", async function() { default: false, type: Boolean }); + //game.settings.get("","") to retrieve it and game.settings.set("","", ) /* -------------------------------------------- */ diff --git a/module/rdd-resolution-table.js b/module/rdd-resolution-table.js index 6cc2989d..7d5788da 100644 --- a/module/rdd-resolution-table.js +++ b/module/rdd-resolution-table.js @@ -42,13 +42,13 @@ const specialResults = [ const reussites = [ - { code: "etotal", isPart: false, isSign: false, isSuccess: false, isEchec: true, isEPart: true, isETotal: true, ptTache: -4, ptQualite: -6, quality: "Echec total", condition: (target, roll) => roll >= target.etotal && roll <= 100 }, - { code: "epart", isPart: false, isSign: false, isSuccess: false, isEchec: true, isEPart: true, isETotal: false, ptTache: -2, ptQualite: -4, quality: "Echec particulier", condition: (target, roll) => (roll >= target.epart && roll < target.etotal) }, - { code: "echec", isPart: false, isSign: false, isSuccess: false, isEchec: true, isEPart: false, isETotal: false, ptTache: 0, ptQualite: -2, quality: "Echec normal", condition: (target, roll) => (roll > target.score && roll < target.etotal) }, - { code: "norm", isPart: false, isSign: false, isSuccess: true, isEchec: false, isEPart: false, isETotal: false, ptTache: 1, ptQualite: 0, quality: "Réussite normale", condition: (target, roll) => (roll > target.sign && roll <= target.score) }, - { code: "sign", isPart: false, isSign: true, isSuccess: true, isEchec: false, isEPart: false, isETotal: false, ptTache: 2, ptQualite: 1, quality: "Réussite significative", condition: (target, roll) => (roll > target.part && roll <= target.sign) }, - { code: "part", isPart: true, isSign: true, isSuccess: true, isEchec: false, isEPart: false, isETotal: false, ptTache: 3, ptQualite: 2, quality: "Réussite Particulière!", condition: (target, roll) => (roll > 0 && roll <= target.part) }, - { code: "error", isPart: false, isSign: false, isSuccess: false, isEchec: true, isEPart: true, isETotal: true, ptTache: 0, ptQualite: 0, quality: "Jet de dés invalide", condition: (target, roll) => (roll <= 0 || roll > 100) } + { code: "etotal", isPart: false, isSign: false, isSuccess: false, isEchec: true, isEPart: true, isETotal: true, ptTache: -4, ptQualite: -6, quality: "Echec total", condition: (target, roll) => roll >= target.etotal && roll <= 100 }, + { code: "epart", isPart: false, isSign: false, isSuccess: false, isEchec: true, isEPart: true, isETotal: false, ptTache: -2, ptQualite: -4, quality: "Echec particulier", condition: (target, roll) => (roll >= target.epart && roll < target.etotal) }, + { code: "echec", isPart: false, isSign: false, isSuccess: false, isEchec: true, isEPart: false, isETotal: false, ptTache: 0, ptQualite: -2, quality: "Echec normal", condition: (target, roll) => (roll > target.score && roll < target.etotal) }, + { code: "norm", isPart: false, isSign: false, isSuccess: true, isEchec: false, isEPart: false, isETotal: false, ptTache: 1, ptQualite: 0, quality: "Réussite normale", condition: (target, roll) => (roll > target.sign && roll <= target.score) }, + { code: "sign", isPart: false, isSign: true, isSuccess: true, isEchec: false, isEPart: false, isETotal: false, ptTache: 2, ptQualite: 1, quality: "Réussite significative", condition: (target, roll) => (roll > target.part && roll <= target.sign) }, + { code: "part", isPart: true, isSign: true, isSuccess: true, isEchec: false, isEPart: false, isETotal: false, ptTache: 3, ptQualite: 2, quality: "Réussite Particulière!", condition: (target, roll) => (roll > 0 && roll <= target.part) }, + { code: "error", isPart: false, isSign: false, isSuccess: false, isEchec: true, isEPart: true, isETotal: true, ptTache: 0, ptQualite: 0, quality: "Jet de dés invalide", condition: (target, roll) => (roll <= 0 || roll > 100) } ]; /* -------------------------------------------- */ @@ -81,8 +81,9 @@ export class RdDResolutionTable { /* -------------------------------------------- */ static explain(rolled) { let message = "
Jet : " + rolled.roll + " sur " + rolled.score + "% "; - if (rolled.caracValue != null && rolled.finalLevel!= null) { - message += "(" + rolled.caracValue + " à " + Misc.toSignedString(rolled.finalLevel) + ") "; + if (rolled.caracValue != null && rolled.finalLevel != null) { + message += (rolled.needSignificative ? "(significative sur " : "(") + + rolled.caracValue + " à " + Misc.toSignedString(rolled.finalLevel) + ") "; } message += '' + rolled.quality + '' return message; @@ -90,28 +91,39 @@ export class RdDResolutionTable { } /* -------------------------------------------- */ - static updateChancesWithBonus( chances, bonus ) { - if (bonus) { - let newScore = Number(chances.score) + Number(bonus); - mergeObject(chances, this._computeCell(null, newScore), {overwrite: true}); + static async rollData(rollData) { + rollData.rolled = await this.roll(rollData.caracValue, rollData.finalLevel, rollData.bonus, rollData.needSignificative); + } + + /* -------------------------------------------- */ + static async roll(caracValue, finalLevel, bonus = undefined, needSignificative = undefined, showDice = true) { + let chances = this.computeChances(caracValue, finalLevel); + this._updateChancesWithBonus(chances, bonus); + this._updateChancesNeedSignificative(chances, needSignificative); + chances.showDice = showDice; + + let rolled = await this.rollChances(chances); + rolled.caracValue = caracValue; + rolled.finalLevel = finalLevel; + rolled.bonus = bonus; + rolled.needSignificative = needSignificative; + return rolled; + } + + /* -------------------------------------------- */ + static _updateChancesNeedSignificative(chances, needSignificative) { + if (needSignificative) { + let newScore = Math.floor(Number(chances.score) / 2); + mergeObject(chances, this._computeCell(null, newScore), { overwrite: true }); } } /* -------------------------------------------- */ - static async rollData(rollData ) { - rollData.rolled = await this.roll(rollData.caracValue, rollData.finalLevel, rollData.bonus); - } - - /* -------------------------------------------- */ - static async roll(caracValue, finalLevel, bonus = undefined, showDice = true ) { - let chances = this.computeChances(caracValue, finalLevel); - chances.showDice = showDice; - this.updateChancesWithBonus( chances, bonus); - let rolled = await this.rollChances(chances); - rolled.caracValue = caracValue; - rolled.finalLevel = finalLevel; - rolled.bonus = bonus; - return rolled; + static _updateChancesWithBonus(chances, bonus) { + if (bonus) { + let newScore = Number(chances.score) + Number(bonus); + mergeObject(chances, this._computeCell(null, newScore), { overwrite: true }); + } } /* -------------------------------------------- */ @@ -146,6 +158,23 @@ export class RdDResolutionTable { return ""; } + + static isEchec(rollData) { + return rollData.demiSurprise ? !rollData.rolled.isSign : rollData.rolled.isEchec; + } + + static isEchecTotal(rollData) { + return (rollData.demiSurprise && rollData.arme) ? rollData.rolled.isEchec : rollData.rolled.isETotal; + } + + static isParticuliere(rollData) { + return (rollData.demiSurprise && rollData.arme) ? false : rollData.rolled.isPart; + } + + static isReussite(rollData) { + return rollData.demiSurprise ? rollData.rolled.isSign : rollData.rolled.isSuccess; + } + /* -------------------------------------------- */ static _computeReussite(chances, roll) { const reussite = reussites.find(x => x.condition(chances, roll)); diff --git a/module/rdd-roll-dialog.js b/module/rdd-roll-dialog.js index a8dd5e38..0e453b1a 100644 --- a/module/rdd-roll-dialog.js +++ b/module/rdd-roll-dialog.js @@ -22,7 +22,7 @@ export class RdDRollDialog extends Dialog { else { myButtons = { rollButton: { label: "Lancer", callback: html => this.actor.performRoll(this.rollData) } - }; + }; } // Common conf @@ -33,13 +33,17 @@ export class RdDRollDialog extends Dialog { if (mode == "competence") { dialogConf.title = "Test de compétence" dialogOptions.height = 430 + ui.notifications.warn("RdDRollDialog - Plus utilisé pour les compétences") + } else if (mode == "arme") { dialogConf.title = "Test de combat/arme" dialogOptions.height = 460 } else if (mode == "carac") { + ui.notifications.warn("RdDRollDialog - Plus utilisé pour les caractéristiques") dialogConf.title = "Test de caractéristique" dialogOptions.height = 420 } else if (mode == "sort") { + ui.notifications.warn("RdDRollDialog - Plus utilisé pour les sorts") dialogConf.title = "Lancer un sort" dialogOptions.height = 460 } @@ -146,19 +150,19 @@ export class RdDRollDialog extends Dialog { updateRollResult(rollData); }); html.find('#coupsNonMortels').change((event) => { - this.rollData.mortalite = event.currentTarget.checked ? "non-mortel" : "non-mortel"; - }); + this.rollData.mortalite = event.currentTarget.checked ? "non-mortel" : "mortel"; + }); html.find('#isCharge').change((event) => { this.rollData.isCharge = event.currentTarget.checked; - }); + }); html.find('#surencMalusApply').change((event) => { this.rollData.surencMalusApply = event.currentTarget.checked; updateRollResult(rollData); - }); + }); html.find('#useEncForNatation').change((event) => { this.rollData.useEncForNatation = event.currentTarget.checked; updateRollResult(rollData); - }); + }); } static _isIgnoreEtatGeneral(rollData) { diff --git a/module/rdd-roll.js b/module/rdd-roll.js index c6b518a4..8b57c191 100644 --- a/module/rdd-roll.js +++ b/module/rdd-roll.js @@ -33,19 +33,27 @@ export class RdDRoll extends Dialog { ajustementsConditions: CONFIG.RDD.ajustementsConditions, difficultesLibres: CONFIG.RDD.difficultesLibres, etat: actor.data.data.compteurs.etat.value, + carac: actor.data.data.carac, finalLevel: 0, diffConditions: 0, diffLibre: 0, + forceValue : actor.getForceValue(), malusArmureValue: (actor.type == 'personnage ' && actor.data.data.attributs && actor.data.data.attributs.malusarmure) ? actor.data.data.attributs.malusarmure.value : 0, surencMalusFlag: actor.type == 'personnage ' ? (actor.data.data.compteurs.surenc.value < 0) : false, surencMalusValue: actor.type == 'personnage ' ? actor.data.data.compteurs.surenc.value : 0, surencMalusApply: false, isNatation: rollData.competence ? rollData.competence.name.toLowerCase().includes("natation") : false, useEncForNatation: false, - encValueForNatation: actor.encombrementTotal ? Math.floor(actor.encombrementTotal) : 0, - ajustementAstrologique: actor.ajustementAstrologique() + encValueForNatation: actor.encombrementTotal ? Math.floor(actor.encombrementTotal) : 0 + // , + // ajustementAstrologique: actor.ajustementAstrologique() }, { overwrite: false }); + + if (actor.isEntiteCauchemar()) + { + rollData.needSignificative = false; + } } /* -------------------------------------------- */ @@ -181,7 +189,10 @@ export class RdDRoll extends Dialog { updateRollResult(rollData); }); html.find('#coupsNonMortels').change((event) => { - this.rollData.mortalite = event.currentTarget.checked ? "non-mortel" : "non-mortel"; + this.rollData.mortalite = event.currentTarget.checked ? "non-mortel" : "mortel"; + }); + html.find('#isCharge').change((event) => { + this.rollData.isCharge = event.currentTarget.checked; }); html.find('#surencMalusApply').change((event) => { this.rollData.surencMalusApply = event.currentTarget.checked; diff --git a/module/rdd-utility.js b/module/rdd-utility.js index d702a77a..4ec8e0fb 100644 --- a/module/rdd-utility.js +++ b/module/rdd-utility.js @@ -3,7 +3,8 @@ import { TMRUtility } from "./tmr-utility.js"; import { RdDRollTables } from "./rdd-rolltables.js"; import { ChatUtility } from "./chat-utility.js"; -import { Misc } from "./misc.js"; +import { RdDItemCompetence } from "./item-competence.js"; +import { RdDCombat } from "./rdd-combat.js"; /* -------------------------------------------- */ const level_category = { @@ -634,36 +635,7 @@ export class RdDUtility { compName = compName.toLowerCase(); return compList.find(item => item.name.toLowerCase() == compName && (item.type =="competence" || item.type == "competencecreature")) } - - /* -------------------------------------------- */ - static getArmeCategory( arme ) - { - if ( arme.data.competence == undefined) return 'competencecreature'; - let compname = arme.data.competence.toLowerCase(); - if ( compname.match("hache") ) return "hache"; - if ( compname.match("hast") ) return "hast"; - if ( compname.match("lance") ) return "lance"; - if ( compname.match("bouclier") ) return "bouclier"; - if ( compname.match("masse") ) return "masse"; - if ( compname.match("fléau") ) return "fleau"; - if ( compname.match("epée") ) { - let armename = arme.name.toLowerCase(); - if (armename == "dague" || armename.match("gnome") ) - return "epee_courte"; - } - return "epee_longue"; - } - - /* -------------------------------------------- */ - static isArmeMelee( compName) - { - let comp = compName.toLowerCase(); - if (comp.match("epée") || comp.match("épée") || comp.match("hache") || comp.match("fleau") || comp.match("mass") || comp.match("lance") || comp.match("hast") || comp == "dague" || comp=="bouclier") - return true; - return false; - } - /* -------------------------------------------- */ static buildDefenseChatCard( attacker, target, rollData ) { @@ -671,9 +643,9 @@ export class RdDUtility { let myTarget = target.actor; let defenseMsg = { title: "Défense en combat", content: ""+myTarget.name+" doit se défendre :
" + - "Encaisser !", + "Encaisser !", whisper: ChatMessage.getWhisperRecipients( myTarget.name ), - attackerid: attacker.data._id, + attackerId: attacker.data._id, defenderTokenId: target.data._id, rollMode: true }; @@ -681,22 +653,22 @@ export class RdDUtility { if ( rollData.competence.data.categorie == 'melee' || rollData.competence.data.categorie == 'competencecreature') { // Melee attack or creature let defenderArmes = []; for (const arme of myTarget.data.items) { - if (arme.type == "arme" && this.isArmeMelee(arme.data.competence)) { + if (arme.type == "arme" && RdDItemCompetence.isCompetenceMelee(arme.data.competence)) { defenderArmes.push( arme ); - defenseMsg.content += "
Parer avec " + arme.name + ""; + defenseMsg.content += "
Parer avec " + arme.name + ""; } if (arme.type == "competencecreature" && arme.data.isparade) { defenderArmes.push( arme ); - defenseMsg.content += "
Parer avec " + arme.name + ""; + defenseMsg.content += "
Parer avec " + arme.name + ""; } } - defenseMsg.content += "
Esquiver"; + defenseMsg.content += "
Esquiver"; } if ( rollData.competence.data.categorie == "tir" ) { for (const arme of myTarget.data.items) { // Bouclier for parry if ( arme.type == "arme" && arme.name.toLowerCase.match("bouclier") ) { defenderArmes.push( arme ); - defenseMsg.content += "
Parer avec " + arme.name + ""; + defenseMsg.content += "
Parer avec " + arme.name + ""; } } } @@ -704,10 +676,10 @@ export class RdDUtility { for (const arme of myTarget.data.items) { // Bouclier for parry Dodge/Esquive if ( arme.type == "arme" && arme.name.toLowerCase.match("bouclier") ) { defenderArmes.push( arme ); - defenseMsg.content += "
Parer avec " + arme.name + ""; + defenseMsg.content += "
Parer avec " + arme.name + ""; } } - defenseMsg.content += "
Esquiver"; + defenseMsg.content += "
Esquiver"; } defenseMsg.toSocket = true; // True per default for all players @@ -754,7 +726,7 @@ export class RdDUtility { } if ((game.user.isGM && !defenderToken.actor.hasPlayerOwner) || (defenderToken.actor.hasPlayerOwner && (game.user.character.id == defenderToken.actor.data._id))) { console.log("User is pushing message...", game.user.name); - game.system.rdd.rollDataHandler[data.attackerid] = duplicate(data.rollData); + game.system.rdd.rollDataHandler[data.attackerId] = duplicate(data.rollData); data.whisper = [game.user]; data.blind = true; data.rollMode = "blindroll"; @@ -865,7 +837,7 @@ export class RdDUtility { /* -------------------------------------------- */ static _handleMsgEncaisser(data) { if (game.user.isGM) { // Seul le GM effectue l'encaissement sur la fiche - let rollData = game.system.rdd.rollDataHandler[data.attackerid]; // Retrieve the rolldata from the store + let rollData = game.system.rdd.rollDataHandler[data.attackerId]; // Retrieve the rolldata from the store let defenderToken = canvas.tokens.get(data.defenderTokenId); defenderToken.actor.encaisserDommages(rollData); } @@ -874,59 +846,64 @@ export class RdDUtility { /* -------------------------------------------- */ static async chatListeners( html ) { + RdDCombat.registerChatCallbacks(html); + html.on("click", '#encaisser-button', event => { event.preventDefault(); - let attackerid = event.currentTarget.attributes['data-attackerid'].value; - let defenderTokenId = event.currentTarget.attributes['data-defendertokenid'].value; + if (RdDCombat.isActive()) return; + let attackerId = event.currentTarget.attributes['data-attackerId'].value; + let defenderTokenId = event.currentTarget.attributes['data-defenderTokenId'].value; if ( game.user.isGM ) { // Current user is the GM -> direct access - let rollData = game.system.rdd.rollDataHandler[attackerid]; - rollData.attackerid = attackerid; + let rollData = game.system.rdd.rollDataHandler[attackerId]; + rollData.attackerId = attackerId; rollData.defenderTokenId = defenderTokenId; let defenderToken = canvas.tokens.get( defenderTokenId ); - defenderToken.actor.encaisserDommages( rollData, game.actors.get(attackerid)); + defenderToken.actor.encaisserDommages( rollData, game.actors.get(attackerId)); } else { // Emit message for GM game.socket.emit("system.foundryvtt-reve-de-dragon", { msg: "msg_encaisser", - data: { attackerid: attackerid, defenderTokenId: defenderTokenId } + data: { attackerId: attackerId, defenderTokenId: defenderTokenId } } ); } }); html.on("click", '#parer-button', event => { event.preventDefault(); - let attackerid = event.currentTarget.attributes['data-attackerid'].value; + if (RdDCombat.isActive()) return; + let attackerId = event.currentTarget.attributes['data-attackerId'].value; let defenderToken = canvas.tokens.get(event.currentTarget.attributes['data-defenderTokenId'].value ); let armeId = event.currentTarget.attributes['data-armeid'].value; - let rollData = game.system.rdd.rollDataHandler[attackerid]; - defenderToken.actor.parerAttaque( rollData, armeId, game.actors.get(attackerid)); + let rollData = game.system.rdd.rollDataHandler[attackerId]; + defenderToken.actor.parerAttaque( rollData, armeId, game.actors.get(attackerId)); }); - + html.on("click", '#esquiver-button', event => { event.preventDefault(); - let attackerid = event.currentTarget.attributes['data-attackerid'].value; + if (RdDCombat.isActive()) return; + let attackerId = event.currentTarget.attributes['data-attackerId'].value; let defenderToken = canvas.tokens.get(event.currentTarget.attributes['data-defenderTokenId'].value ); - let rollData = game.system.rdd.rollDataHandler[attackerid]; + let rollData = game.system.rdd.rollDataHandler[attackerId]; //console.log("Esquive !", rollData, defenderActor); - defenderToken.actor.esquiverAttaque( rollData, game.actors.get(attackerid)); + defenderToken.actor.esquiverAttaque( rollData, game.actors.get(attackerId)); }); - + html.on("click", '#particuliere-attaque', event => { event.preventDefault(); - let attackerid = event.currentTarget.attributes['data-attackerid'].value; - let attackerActor = game.actors.get(event.currentTarget.attributes['data-attackerid'].value ); - let rollData = game.system.rdd.rollDataHandler[attackerid]; - rollData.particuliereAttaque = game.actors.get(event.currentTarget.attributes['data-mode'].value ); + if (RdDCombat.isActive()) return; + let attackerId = event.currentTarget.attributes['data-attackerId'].value; + let attackerActor = game.actors.get(event.currentTarget.attributes['data-attackerId'].value ); + let rollData = game.system.rdd.rollDataHandler[attackerId]; + rollData.particuliereAttaque = event.currentTarget.attributes['data-mode'].value; //console.log("Particulère !", rollData); attackerActor.continueRoll( rollData ); }); - + html.on("click", '.tmr-passeur-coord a', event => { let coord = event.currentTarget.attributes['data-tmr-coord'].value; let actorId = event.currentTarget.attributes['data-actor-id'].value; let actor = game.actors.get( actorId ); actor.tmrApp.forceDemiRevePosition(coord); }); - } /* -------------------------------------------- */ @@ -942,7 +919,7 @@ export class RdDUtility { /* -------------------------------------------- */ /* Manage chat commands */ - static processChatCommand( commands, content, msg ) { + static processChatCommand( commands, content, msg ) { // Setup new message's visibility let rollMode = game.settings.get("core", "rollMode"); if (["gmroll", "blindroll"].includes(rollMode)) msg["whisper"] = ChatMessage.getWhisperRecipients("GM");