diff --git a/module/actor.js b/module/actor.js index cf6b7268..fa0312b9 100644 --- a/module/actor.js +++ b/module/actor.js @@ -12,7 +12,7 @@ import { ChatUtility } from "./chat-utility.js"; import { RdDItemSort } from "./item-sort.js"; import { Grammar } from "./grammar.js"; import { RdDEncaisser } from "./rdd-roll-encaisser.js"; -import { RdDCombat } from "./rdd-combat.js"; +import { RdDCombat, RdDCombatSettings } from "./rdd-combat.js"; import { DeDraconique } from "./de-draconique.js"; import { RdDAudio } from "./rdd-audio.js"; import { RdDItemCompetence } from "./item-competence.js"; @@ -32,6 +32,7 @@ export class RdDActor extends Actor { static init() { Hooks.on("deleteActiveEffect", (actor, effect, options) => actor.onDeleteActiveEffect(effect, options)); Hooks.on("createActiveEffect", (actor, effect, options) => actor.onCreateActiveEffect(effect, options)); + Hooks.on("updateActor", (actor, update, options, actorId) => actor.onUpdateActor(update, options, actorId)); } /* -------------------------------------------- */ @@ -99,18 +100,18 @@ export class RdDActor extends Actor { // Make separate methods for each Actor type (character, npc, etc.) to keep // things organized. if (actorData.type === 'personnage') this._prepareCharacterData(actorData); - if (actorData.type === 'creature') this.prepareCreatureData(actorData); - if (actorData.type === 'vehicule') this.prepareVehiculeData(actorData); + if (actorData.type === 'creature') this._prepareCreatureData(actorData); + if (actorData.type === 'vehicule') this._prepareVehiculeData(actorData); } /* -------------------------------------------- */ - prepareCreatureData(actorData) { + _prepareCreatureData(actorData) { this.computeEncombrementTotalEtMalusArmure(); this.computeEtatGeneral(); } /* -------------------------------------------- */ - prepareVehiculeData( actorData ) { + _prepareVehiculeData( actorData ) { this.computeEncombrementTotalEtMalusArmure(); } @@ -1105,7 +1106,7 @@ export class RdDActor extends Actor { if (this.isEntiteCauchemar()) { return 0; } - return this.data.data.attributs?.sconst?.value ?? 0; + return RdDUtility.calculSConst(this.data.data.carac.constitution.value); } /* -------------------------------------------- */ @@ -1177,18 +1178,14 @@ export class RdDActor extends Actor { sonne: false, }; - let minValue = 0; - if (this.type == 'personnage') { - // TODO: les animaux/humanoïdes on théoriquement aussi un sconst, mais la SPA n'est pas passé par là - minValue = name == "vie" ? -Number(this.data.data.attributs.sconst.value) : 0; - } + let minValue = name == "vie" ? -this.getSConst()-1 : 0; result.newValue = Math.max(minValue, Math.min(compteur.value + inc, compteur.max)); //console.log("New value ", inc, minValue, result.newValue); let fatigue = 0; - if (name == "endurance" && this.data.type != 'entite') { - if (!isCritique && result.newValue == 0 && inc < 0) { // perte endurance et endurance devient 0 -> -1 vie sauf si coup critique - sante.vie.value = sante.vie.value - 1; + if (name == "endurance" && !this.isEntiteCauchemar()) { + if (result.newValue == 0 && inc < 0 && !isCritique) { // perte endurance et endurance devient 0 (sauf critique) -> -1 vie + sante.vie.value --; } result.newValue = Math.max(0, result.newValue); if (inc > 0) { // le max d'endurance s'applique seulement à la récupération @@ -1202,6 +1199,7 @@ export class RdDActor extends Actor { result.jetEndurance = testIsSonne.roll.total; } else if (inc > 0) { await this.setSonne(false); + sante.sonne.value = false; } if (sante.fatigue && inc < 0) { // Each endurance lost -> fatigue lost fatigue = perte; @@ -1213,6 +1211,9 @@ export class RdDActor extends Actor { if (sante.fatigue && fatigue > 0) { sante.fatigue.value = Math.max(sante.fatigue.value + fatigue, this._computeFatigueMin()); } + if (!this.isEntiteCauchemar() && sante.vie.value<-this.getSConst()) { + await this.addStatusEffectById('dead'); + } await this.update({ "data.sante": sante }); return result; @@ -2221,6 +2222,9 @@ export class RdDActor extends Actor { } _deteriorerArmure(item, dmg) { + if (!RdDCombatSettings.isUsingDeteriorationArmure()) { + return; + } let update = duplicate(item); update.data.deterioration = (update.data.deterioration ?? 0) + dmg; if (update.data.deterioration >= 10) { @@ -2366,8 +2370,11 @@ export class RdDActor extends Actor { count--; } else { // TODO: status effect dead - ChatMessage.create({ content: `${this.name} vient de succomber à une seconde blessure critique ! Que les Dragons gardent son Archétype en paix !` }); + this.addStatusEffectById('dead'); + ChatMessage.create({ content: ` + ${this.name} vient de succomber à une seconde blessure critique ! Que les Dragons gardent son Archétype en paix !` }); encaissement.critiques -= count; + encaissement.mort = true; break; } } @@ -2639,6 +2646,13 @@ export class RdDActor extends Actor { await this.update( { 'data.subacteurs.montures': newMontures }); } /* -------------------------------------------- */ + async onUpdateActor(update, options, actorId) { + const updatedEndurance = update?.data?.sante?.endurance; + if (updatedEndurance && options.diff) { + this.forceStatusEffectId('unconscious', updatedEndurance.value == 0); + } + } + /* -------------------------------------------- */ async onCreateActiveEffect(effect, options) { switch (StatusEffects.statusId(effect)) { case 'sonne': @@ -2646,7 +2660,7 @@ export class RdDActor extends Actor { return; } } - + /* -------------------------------------------- */ async onDeleteActiveEffect(effect, options) { switch (StatusEffects.statusId(effect)) { @@ -2655,80 +2669,81 @@ export class RdDActor extends Actor { return; } } - + /* -------------------------------------------- */ enleverTousLesEffets() { this.deleteEmbeddedEntity('ActiveEffect', Array.from(this.effects?.keys() ?? [])); } - + /* -------------------------------------------- */ listeEffets(matching = it => true) { const all = Array.from(this.effects?.values() ?? []); const filtered = all.filter(it => matching(it.data)); return filtered; } - + /* -------------------------------------------- */ async setStatusDemiReve(status) { - const options = { renderSheet: true/*, noHook: from == 'hook' */ }; if (status) { - await this.addEffect(StatusEffects.demiReve(), options) + await this.addStatusEffect(StatusEffects.demiReve()) } else { - this.deleteEffect(StatusEffects.demiReve(), options) + this.deleteStatusEffect(StatusEffects.demiReve()) } } - + /* -------------------------------------------- */ async setStatusSonne(sonne) { if (this.isEntiteCauchemar()) { return; } - const id = 'sonne'; - const options = { renderSheet: true/*, noHook: from == 'hook' */ }; - + await this.forceStatusEffectId('sonne', sonne); + } + + /* -------------------------------------------- */ + async forceStatusEffectId(statusId, sonne) { if (sonne) { - await this.addById(id, options); + await this.addStatusEffectById(statusId); } - else /* if (!sonne)*/ { - this.deleteById(id, options) + else { + this.deleteStatusEffectById(statusId); } } /* -------------------------------------------- */ - deleteById(id, options) { + deleteStatusEffectById(id, options = { renderSheet: true}) { const effects = Array.from(this.effects?.values()) .filter(it => it.data.flags.core?.statusId == id); - this._deleteAll(effects, options); + this._deleteStatusEffects(effects, options); } /* -------------------------------------------- */ - deleteEffect(effect, options) { + deleteStatusEffect(effect, options = { renderSheet: true}) { const toDelete = Array.from(this.effects?.values()) .filter(it => StatusEffects.statusId(it.data) == StatusEffects.statusId(effect)); - this._deleteAll(toDelete, options); + this._deleteStatusEffects(toDelete, options); } /* -------------------------------------------- */ - _deleteAll(effects, options) { - this._deleteAllIds(effects.map(it => it.id), options); + _deleteStatusEffects(effects, options) { + this._deleteStatusEffectsByIds(effects.map(it => it.id), options); } /* -------------------------------------------- */ - _deleteAllIds(effectIds, options) { + _deleteStatusEffectsByIds(effectIds, options) { this.deleteEmbeddedEntity('ActiveEffect', effectIds, options); this.applyActiveEffects(); } /* -------------------------------------------- */ - async addById(id, options) { + async addStatusEffectById(id, options = { renderSheet: true}) { const statusEffect = CONFIG.statusEffects.find(it => it.id == id); - await this.addEffect(statusEffect, options); + await this.addStatusEffect(statusEffect, options); } /* -------------------------------------------- */ - async addEffect(statusEffect, options) { - this.deleteById(statusEffect.id, options); + async addStatusEffect(statusEffect, options = { renderSheet: true}) { + this.deleteStatusEffectById(statusEffect.id, options); const effet = duplicate(statusEffect); effet["flags.core.statusId"] = effet.id; await this.createEmbeddedEntity('ActiveEffect', effet, options); diff --git a/module/rdd-combat.js b/module/rdd-combat.js index 4191f302..99fd9402 100644 --- a/module/rdd-combat.js +++ b/module/rdd-combat.js @@ -11,6 +11,7 @@ import { RdDRollTables } from "./rdd-rolltables.js"; export class RdDCombat { static init() { + RdDCombatSettings.onInit(); this.initStorePasseArmes(); Hooks.on("updateCombat", (combat, data) => { RdDCombat.onUpdateCombat(combat, data) }); Hooks.on("preDeleteCombat", (combat, options) => { RdDCombat.onPreDeleteCombat(combat, options); }); @@ -562,7 +563,7 @@ export class RdDCombat { /* -------------------------------------------- */ async choixParticuliere(rollData, choix) { console.log("RdDCombat.choixParticuliere >>>", rollData, choix); - + this.removeChatMessageActionsPasseArme(rollData.passeArme); rollData.particuliere = choix; await this._onAttaqueNormale(rollData); @@ -594,7 +595,6 @@ export class RdDCombat { dialog.render(true); } - _prepareParade(attackerRoll, armeParade) { const compName = armeParade.data.competence; const armeAttaque = attackerRoll.arme; @@ -606,7 +606,7 @@ export class RdDCombat { competence: this.defender.getCompetence(compName), arme: armeParade, surprise: this.defender.getSurprise(true), - needParadeSignificative: RdDItemArme.needParadeSignificative(armeAttaque, armeParade), + needParadeSignificative: RdDCombatSettings.isUsingCategorieParade() && RdDItemArme.needParadeSignificative(armeAttaque, armeParade), needResist: RdDItemArme.needArmeResist(armeAttaque, armeParade), carac: this.defender.data.data.carac, show: {} @@ -632,6 +632,9 @@ export class RdDCombat { if (RdDBonus.isDefenseAttaqueFinesse(defenderRoll)) { facteurSign *= 2; } + if (!RdDCombatSettings.isUsingTripleSignificative()) { + facteurSign = Math.min(facteurSign, 4); + } return facteurSign; } @@ -741,6 +744,9 @@ export class RdDCombat { /* -------------------------------------------- */ async computeDeteriorationArme(rollData) { + if (!RdDCombatSettings.isUsingResistanceArmeParade()) { + return; + } const attackerRoll = rollData.attackerRoll; // Est-ce une parade normale? if (rollData.arme && attackerRoll && !rollData.rolled.isPart) { @@ -772,7 +778,7 @@ export class RdDCombat { } } // Si l'arme de parade n'est pas un bouclier, jet de désarmement (p.132) - if (resistance > 0 && RdDItemArme.getCategorieParade(rollData.arme) != 'boucliers') { + if (RdDCombatSettings.isUsingDefenseurDesarme() && resistance > 0 && RdDItemArme.getCategorieParade(rollData.arme) != 'boucliers') { let desarme = await RdDResolutionTable.rollData({ caracValue: this.defender.getForce(), finalLevel: Misc.toInt(rollData.competence.data.niveau) - dmg, @@ -783,31 +789,38 @@ export class RdDCombat { } } } + /* -------------------------------------------- */ async computeRecul(defenderRoll) { // Calcul du recul (p. 132) const attackerRoll = defenderRoll.attackerRoll; - if (this._isAttaqueCauseRecul(attackerRoll)) { - + if (RdDCombatSettings.isUsingRecul() && this._isAttaqueCauseRecul(attackerRoll)) { const impact = this._computeImpactRecul(attackerRoll); const rollRecul = await RdDResolutionTable.rollData({ caracValue: 10, finalLevel: impact }); - if (rollRecul.rolled.isSuccess) { defenderRoll.show.recul = 'encaisse'; - } else if (rollRecul.rolled.isETotal) { + } else if (rollRecul.rolled.isETotal || this._isReculCauseChute(impact)) { defenderRoll.show.recul = 'chute'; + await this.defender.addStatusEffectById('prone'); } else { - const agilite = this.defender.getAgilite(); - const chute = await RdDResolutionTable.rollData({ caracValue: agilite, finalLevel: impact }); - defenderRoll.show.recul = (chute.rolled.isSuccess) ? 'recul' : 'chute'; + defenderRoll.show.recul = 'recul'; } } } + /* -------------------------------------------- */ + async _isReculCauseChute(impact) { + const agilite = this.defender.getAgilite(); + const chute = await RdDResolutionTable.rollData({ caracValue: agilite, finalLevel: impact }); + return chute.rolled.isEchec; + } + + /* -------------------------------------------- */ _isAttaqueCauseRecul(attaque) { return attaque.particuliere == 'force' || attaque.tactique == 'charge'; } + /* -------------------------------------------- */ _computeImpactRecul(attaque) { const taille = this.defender.getTaille(); const force = this.attacker.getForce(); @@ -902,5 +915,92 @@ export class RdDCombat { content: await renderTemplate(`systems/foundryvtt-reve-de-dragon/templates/chat-actor-turn-summary.html`, data) }); } +} + +export class RdDCombatSettings extends FormApplication { + static onInit() { + game.settings.register("foundryvtt-reve-de-dragon", "rdd-combat-recul", { name: "rdd-combat-recul", scope: "world", config: false, default: true, type: Boolean }); + game.settings.register("foundryvtt-reve-de-dragon", "rdd-combat-resistanceArmeParade", { name: "rdd-combat-resistanceArmeParade", scope: "world", config: false, default: true, type: Boolean }); + game.settings.register("foundryvtt-reve-de-dragon", "rdd-combat-deteriorationArmure", { name: "rdd-combat-deteriorationArmure", scope: "world", config: false, default: true, type: Boolean }); + game.settings.register("foundryvtt-reve-de-dragon", "rdd-combat-defenseurDesarme", { name: "rdd-combat-defenseurDesarme", scope: "world", config: false, default: true, type: Boolean }); + game.settings.register("foundryvtt-reve-de-dragon", "rdd-combat-categorieParade", { name: "rdd-combat-categorieParade", scope: "world", config: false, default: true, type: Boolean }); + game.settings.register("foundryvtt-reve-de-dragon", "rdd-combat-tripleSignificative", { name: "rdd-combat-tripleSignificative", scope: "world", config: false, default: true, type: Boolean }); + + game.settings.registerMenu("foundryvtt-reve-de-dragon", "rdd-combat-options", { + name: "Choisir les options de combat", + label: "Choix des options de combat", + hint: "Ouvre la fenêtre de sélection des options de combats pour désactiver certaines règles", + icon: "fas fa-bars", + type: RdDCombatSettings, + restricted: true + }); + } + + constructor(...args) { + super(...args); + } + + static get defaultOptions() { + const options = super.defaultOptions; + mergeObject(options, { + id: "combat-settings", + template: "systems/foundryvtt-reve-de-dragon/templates/combat-settings.html", + height: 600, + width: 350, + minimizable: false, + closeOnSubmit: true, + title: "Options de combat" + }); + return options; + } + + getData() { + let data = super.getData(); + data.recul = RdDCombatSettings.isUsingRecul(); + data.resistanceArmeParade = RdDCombatSettings.isUsingResistanceArmeParade(); + data.deteriorationArmure = RdDCombatSettings.isUsingDeteriorationArmure(); + data.defenseurDesarme = RdDCombatSettings.isUsingDefenseurDesarme(); + data.categorieParade = RdDCombatSettings.isUsingCategorieParade(); + data.tripleSignificative = RdDCombatSettings.isUsingTripleSignificative(); + return data; + } + + static isUsingRecul() { + return game.settings.get("foundryvtt-reve-de-dragon", "rdd-combat-recul"); + } + + static isUsingResistanceArmeParade() { + return game.settings.get("foundryvtt-reve-de-dragon", "rdd-combat-resistanceArmeParade"); + } + + static isUsingDeteriorationArmure() { + return game.settings.get("foundryvtt-reve-de-dragon", "rdd-combat-deteriorationArmure"); + } + + static isUsingDefenseurDesarme() { + return game.settings.get("foundryvtt-reve-de-dragon", "rdd-combat-defenseurDesarme"); + } + + static isUsingCategorieParade() { + return game.settings.get("foundryvtt-reve-de-dragon", "rdd-combat-categorieParade"); + } + + static isUsingTripleSignificative() { + return game.settings.get("foundryvtt-reve-de-dragon", "rdd-combat-tripleSignificative"); + } + + activateListeners(html) { + html.find(".select-option").click((event) => { + if (event.currentTarget.attributes.name) { + let id = event.currentTarget.attributes.name.value; + let isChecked = event.currentTarget.checked; + game.settings.set("foundryvtt-reve-de-dragon", id, isChecked); + } + }); + } + + async _updateObject(event, formData) { + this.close(); + } +} -} \ No newline at end of file diff --git a/module/rdd-roll.js b/module/rdd-roll.js index f10e285c..51c0478a 100644 --- a/module/rdd-roll.js +++ b/module/rdd-roll.js @@ -42,8 +42,6 @@ export class RdDRoll extends Dialog { finalLevel: 0, diffConditions: 0, diffLibre: rollData.competence?.data.default_diffLibre ?? 0, - editLibre: true, - editConditions: true, malusArmureValue: actor.getMalusArmure(), surencMalusFlag: actor.isPersonnage() ? (actor.data.data.compteurs.surenc.value < 0) : false, surencMalusValue: actor.getSurenc(), diff --git a/module/rdd-utility.js b/module/rdd-utility.js index da714c1f..0e11e0bb 100644 --- a/module/rdd-utility.js +++ b/module/rdd-utility.js @@ -468,7 +468,7 @@ export class RdDUtility { let tailleData = tableCaracDerivee[bonusDomKey]; data.attributs.plusdom.value = tailleData.plusdom; - data.attributs.sconst.value = tableCaracDerivee[Number(data.carac.constitution.value)].sconst; + data.attributs.sconst.value = RdDUtility.calculSConst(data.carac.constitution.value); data.attributs.sust.value = tableCaracDerivee[Number(data.carac.taille.value)].sust; data.attributs.encombrement.value = (parseInt(data.carac.force.value) + parseInt(data.carac.taille.value)) / 2; @@ -489,6 +489,10 @@ export class RdDUtility { data.compteurs.chance.max = data.carac.chance.value; } + static calculSConst(constitution) { + return Number(tableCaracDerivee[Number(constitution)].sconst); + } + /* -------------------------------------------- */ static getSegmentsFatigue(maxEnd) { maxEnd = Math.max(maxEnd, 1); diff --git a/module/status-effects.js b/module/status-effects.js index 57d2b315..a5efcf6e 100644 --- a/module/status-effects.js +++ b/module/status-effects.js @@ -5,7 +5,7 @@ const rddStatusEffects = [ demiReveStatusEffect ]; const statusDemiSurprise = new Set(['sonne', 'prone', 'restrain']); -const statusSurpriseTotale = new Set(['unconscious', 'blind']); +const statusSurpriseTotale = new Set(['unconscious', 'blind', 'dead']); export class StatusEffects { static onReady() { diff --git a/templates/actor-sheet.html b/templates/actor-sheet.html index 5597990d..e6d3a159 100644 --- a/templates/actor-sheet.html +++ b/templates/actor-sheet.html @@ -41,7 +41,7 @@