From 2cbee42180149a7c2ed1f33ce5120e40fe923f5b Mon Sep 17 00:00:00 2001 From: Vincent Vandemeulebrouck Date: Tue, 21 Oct 2025 02:26:44 +0200 Subject: [PATCH] Empoignade V2 --- assets/actions/empoignade.svg | 1 + assets/{ui => actions}/encaisser.svg | 0 changelog.md | 11 +- css/foundryvtt-reve-de-dragon.css | 9 ++ less/roll-dialog.less | 9 ++ module/actor.js | 9 +- module/actor/base-actor-reve.js | 5 +- module/constants.js | 2 +- module/initiative.mjs | 1 + module/item/arme.js | 9 ++ module/rdd-bonus.js | 1 + module/rdd-combat.js | 31 +++--- module/rdd-empoignade.js | 105 ++++++++++++++---- module/rdd-roll.js | 2 +- module/roll/chat-roll-result.mjs | 42 +++++-- module/roll/roll-basic-parts.mjs | 16 ++- module/roll/roll-dialog-adapter.mjs | 2 +- module/roll/roll-dialog.mjs | 2 + module/roll/roll-part-attaque.mjs | 48 +++++++- module/roll/roll-part-carac.mjs | 7 ++ module/roll/roll-part-comp.mjs | 16 ++- module/roll/roll-part-defense.mjs | 23 +++- module/roll/roll-part-empoignade.mjs | 31 ++++++ module/roll/roll-type.mjs | 1 + module/settings/status-effects.js | 7 +- templates/chat-demande-defense.hbs | 42 +++---- .../roll/result/partial-encaissement.hbs | 34 ++++-- templates/roll/roll-part-attaque.hbs | 31 ++++-- 28 files changed, 383 insertions(+), 114 deletions(-) create mode 100644 assets/actions/empoignade.svg rename assets/{ui => actions}/encaisser.svg (100%) create mode 100644 module/roll/roll-part-empoignade.mjs diff --git a/assets/actions/empoignade.svg b/assets/actions/empoignade.svg new file mode 100644 index 00000000..3385ffc2 --- /dev/null +++ b/assets/actions/empoignade.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/ui/encaisser.svg b/assets/actions/encaisser.svg similarity index 100% rename from assets/ui/encaisser.svg rename to assets/actions/encaisser.svg diff --git a/changelog.md b/changelog.md index c995eef6..a515400a 100644 --- a/changelog.md +++ b/changelog.md @@ -8,8 +8,17 @@ - l'encaissement indique une blessure dans le tchat... même si ce n'est que de l'endurance - les blurettes suivent les règles des entités de cauchemar (p322) - Nouvelle fenêtre de jets de dés - - Attaque/défense des créatures + - attaque/défense des créatures - les attaques/parades avec une arme trop lourde se font en demi-surprise + - les demandes de défense disparaîssent une fois prises en compte + - empoignade + - l'empoignade est possible avec une initiative d'empoignade, ou en cours d'empoignade + - seule la dague, le pugilat et la dague sont possibles en cours d'empoignade + - jet de Dextérité/Dague pour utiliser la dague en cours d'empoignade + - attaquer avec une arme un empoigneur donne un +4 si pas d'empoignade + - les dommages de l'empoignade ajoutent/enlèvent un point d'empoignade + - le statut d'empoignade est affiché sur les tokens + - les défenses contre une empoignade sont corrigées ## 13.0.13 - L'épanouissement d'Illysis diff --git a/css/foundryvtt-reve-de-dragon.css b/css/foundryvtt-reve-de-dragon.css index a7739aef..8eb5bf83 100644 --- a/css/foundryvtt-reve-de-dragon.css +++ b/css/foundryvtt-reve-de-dragon.css @@ -648,6 +648,15 @@ select, .system-foundryvtt-reve-de-dragon .roll-dialog roll-conditions roll-section[name="coeur"] select[name="coeur"] { max-width: 4rem; } +.system-foundryvtt-reve-de-dragon .roll-dialog roll-conditions roll-section[name="empoignade"] img { + /* image de d100 */ + max-width: 1rem; + max-height: 1rem; + gap: 0; + margin: 0; + padding: 0; + filter: invert(0.8); +} .system-foundryvtt-reve-de-dragon .roll-dialog roll-conditions roll-section[name="tricher"] img { /* image de d100 */ max-width: 2.5rem; diff --git a/less/roll-dialog.less b/less/roll-dialog.less index 567c0f33..f2e4767b 100644 --- a/less/roll-dialog.less +++ b/less/roll-dialog.less @@ -224,6 +224,15 @@ max-width: 4rem; } + roll-conditions roll-section[name="empoignade"] img { + /* image de d100 */ + max-width: 1rem; + max-height: 1rem; + gap: 0; + margin: 0; + padding: 0; + filter: invert(0.8); + } roll-conditions roll-section[name="tricher"] img { /* image de d100 */ max-width: 2.5rem; diff --git a/module/actor.js b/module/actor.js index 24368810..98b67afd 100644 --- a/module/actor.js +++ b/module/actor.js @@ -185,7 +185,7 @@ export class RdDActor extends RdDBaseActorSang { const actions = [] const uniques = [] - const addAttaque = (arme, main = undefined, action = 'attaque') => { + const addAttaque = (arme, main = undefined) => { const dommages = RdDItemArme.valeurMain(arme.system.dommages, main) const forceRequise = RdDItemArme.valeurMain(arme.system.force ?? 0, main) const ecaillesEfficacite = arme.system.magique ? arme.system.ecaille_efficacite : 0; @@ -218,6 +218,7 @@ export class RdDActor extends RdDBaseActorSang { }) } + addAttaque(RdDItemArme.empoignade(this), ATTAQUE_TYPE.CORPS_A_CORPS) this.itemTypes[ITEM_TYPES.arme] .filter(it => it.isAttaque()) .sort(Misc.ascending(it => it.name)) @@ -227,9 +228,7 @@ export class RdDActor extends RdDBaseActorSang { if (arme.system.lancer && arme.system.resistance > 0) { addAttaque(arme, ATTAQUE_TYPE.LANCER) } if (arme.system.tir) { addAttaque(arme, ATTAQUE_TYPE.TIR) } }) - - addAttaque(RdDItemArme.pugilat(this), ATTAQUE_TYPE.CORPS_A_CORPS) - addAttaque(RdDItemArme.empoignade(this), ATTAQUE_TYPE.CORPS_A_CORPS, 'empoignade') + addAttaque(RdDItemArme.pugilat(this), ATTAQUE_TYPE.CORPS_A_CORPS) return actions } @@ -3040,6 +3039,8 @@ export class RdDActor extends RdDBaseActorSang { await this.onDeleteOwnedCaseTmr(item, options, id) break case ITEM_TYPES.empoignade: + await this.setEffect(STATUSES.StatusGrappled, false) + await this.setEffect(STATUSES.StatusGrappling, false) await RdDEmpoignade.deleteLinkedEmpoignade(this.id, item) break } diff --git a/module/actor/base-actor-reve.js b/module/actor/base-actor-reve.js index 6379cfd7..92a60e9d 100644 --- a/module/actor/base-actor-reve.js +++ b/module/actor/base-actor-reve.js @@ -126,6 +126,8 @@ export class RdDBaseActorReve extends RdDBaseActor { async remiseANeuf() { } async appliquerAjoutExperience(rollData, hideChatMessage = 'show') { } + computeResumeBlessure() {} + countBlessures(filter = it => !it.isContusion()) { return 0 } async santeIncDec(name, inc, isCritique = false) { } async finDeRound(options = { terminer: false }) { @@ -204,6 +206,7 @@ export class RdDBaseActorReve extends RdDBaseActor { getPossession(possessionId) { return this.itemTypes[ITEM_TYPES.possession].find(it => it.system.possessionid == possessionId); } + getEmpoignades() { return this.itemTypes[ITEM_TYPES.empoignade]; } @@ -245,7 +248,7 @@ export class RdDBaseActorReve extends RdDBaseActor { async setEffect(statusId, status) { if (this.isEffectAllowed(statusId)) { - const effect = this.getEffectByStatus(statusId); + const effect = this.getEffectByStatus(statusId) if (!status && effect) { await this.deleteEmbeddedDocuments('ActiveEffect', [effect.id], { render: true }) } diff --git a/module/constants.js b/module/constants.js index 37e45b6f..fbd60dc0 100644 --- a/module/constants.js +++ b/module/constants.js @@ -60,7 +60,7 @@ export const RDD_CONFIG = { icons: { armesDisparates: 'systems/foundryvtt-reve-de-dragon/assets/actions/armes-disparates.svg', demiReve: 'systems/foundryvtt-reve-de-dragon/assets/actions/sort.svg', - empoignade: 'systems/foundryvtt-reve-de-dragon/icons/empoignade.webp', + empoignade: 'systems/foundryvtt-reve-de-dragon/assets/actions/empoignade.svg', forceWeak: 'systems/foundryvtt-reve-de-dragon/assets/actions/weak.svg', }, encaissement: { diff --git a/module/initiative.mjs b/module/initiative.mjs index 4ddf5c10..d1ceed40 100644 --- a/module/initiative.mjs +++ b/module/initiative.mjs @@ -58,6 +58,7 @@ export class RdDInitiative { return { roll: roll, value: value, + rang: formule.phase.rang, init: formule.phase.rang + value / 100, label: formule.phase.label } diff --git a/module/item/arme.js b/module/item/arme.js index bd29ead1..74ff0d35 100644 --- a/module/item/arme.js +++ b/module/item/arme.js @@ -27,6 +27,7 @@ export const ATTAQUE_TYPE = { TIR: '(tir)', LANCER: '(lancer)' } +export const ATTAQUE_TYPE_MELEE = [ATTAQUE_TYPE.UNE_MAIN, ATTAQUE_TYPE.DEUX_MAINS, ATTAQUE_TYPE.CORPS_A_CORPS] export const CORPS_A_CORPS = 'Corps à corps' export const PUGILAT = 'pugilat' @@ -254,6 +255,14 @@ export class RdDItemArme extends RdDItem { return this.system.resistance > 0 || (this.system.tir != '' && this.system.portee_courte > 0) } + isEmpoignade() { + return this.system.mortalite == RDD_CONFIG.encaissement.empoignade + } + + isUtilisableEmpoigne() { + return this.system.baseInit == 3 || this.system.baseInit == 4 || this.system.competence == "Dague" + } + static pugilat(actor) { return RdDItemArme.$corpsACorps(actor, 'Pugilat', PUGILAT) } diff --git a/module/rdd-bonus.js b/module/rdd-bonus.js index 42d0f812..be0efff0 100644 --- a/module/rdd-bonus.js +++ b/module/rdd-bonus.js @@ -74,6 +74,7 @@ export class RdDBonus { dmgForceInsuffisante: Math.min(0, actor.getForce() - (attaque.forceRequise ?? 0)), dmgDiffLibre: ReglesOptionnelles.isUsing('degat-ajout-malus-libre') ? Math.abs(attaque.diff ?? 0) : 0 } + dmg.isEmpoignade = dmg.mortalite == RDD_CONFIG.encaissement.empoignade dmg.total = dmg.dmgSurprise + dmg.dmgTactique + dmg.dmgArme + dmg.dmgActor + dmg.dmgParticuliere + dmg.dmgForceInsuffisante + dmg.dmgDiffLibre return dmg } diff --git a/module/rdd-combat.js b/module/rdd-combat.js index 72d10848..676f6a13 100644 --- a/module/rdd-combat.js +++ b/module/rdd-combat.js @@ -56,6 +56,12 @@ export class RdDCombatManager extends Combat { } } + + static getCombatant(actorId, tokenId) { + return game.combat.combatants.find(it => it.actor.id == actorId && + it.token.id == tokenId + ); + } /* -------------------------------------------- */ async nextRound() { await this.finDeRound(); @@ -402,7 +408,6 @@ export class RdDCombat { /* -------------------------------------------- */ static registerChatCallbacks(html) { for (let button of [ - '.button-defense', '.button-parade', '.button-esquive', '.button-encaisser', @@ -474,8 +479,6 @@ export class RdDCombat { switch (button) { case '.particuliere-attaque': return await this.choixParticuliere(attackerRoll, event.currentTarget.attributes['data-mode'].value); - case '.button-defense': return this.defenseV2(attackerRoll); - case '.button-parade': return this.parade(attackerRoll, armeParadeId); case '.button-esquive': return this.esquive(attackerRoll, compId, competence); case '.button-encaisser': return this.encaisser(attackerRoll, defenderRoll); @@ -725,24 +728,24 @@ export class RdDCombat { async _chatMessageDefenseV2(paramDemandeDefense) { const attackerRoll = paramDemandeDefense.attackerRoll; RollBasicParts.loadSurprises(attackerRoll) - attackerRoll.passeArme = attackerRoll.passeArme ?? foundry.utils.randomID(16) attackerRoll.dmg = RdDBonus.dmgRollV2(attackerRoll, attackerRoll.current.attaque) - const attaque = RollDialog.saveParts(attackerRoll) - const defense = { - attackerRoll: attaque, - ids: RollBasicParts.reverseIds(attaque), - passeArme: attaque.passeArme ?? foundry.utils.randomID(16) - } + + const defenseData = RollBasicParts.prepareDefense(attackerRoll) const choixDefense = await ChatMessage.create({ // message privé: du défenseur à lui même (et aux GMs) speaker: ChatMessage.getSpeaker(this.defender, canvas.tokens.get(this.defenderTokenId)), alias: this.attacker?.getAlias(), whisper: ChatUtility.getOwners(this.defender), - content: await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/chat-demande-defense.hbs', attackerRoll) + content: await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/chat-demande-defense.hbs', defenseData) }); // flag pour garder les jets d'attaque/defense - ChatUtility.setMessageData(choixDefense, 'rollData', defense) + ChatUtility.setMessageData(choixDefense, 'demande-defense', true) + ChatUtility.setMessageData(choixDefense, 'rollData', { + ids: defenseData.ids, + attackerRoll: RollDialog.saveParts(attackerRoll), + passeArme: defenseData.passeArme + }) } /* -------------------------------------------- */ @@ -1052,7 +1055,7 @@ export class RdDCombat { dialog.render(true); } - async defenseV2(attackerRoll) { + async defenseV2(attackerRoll, callbacks = []) { // this._prepareParade(attackerRoll, arme, competence); RollDialog.loadRollData(attackerRoll) await this.doRollDefense({ @@ -1065,7 +1068,7 @@ export class RdDCombat { type: { allowed: [ROLL_TYPE_DEFENSE], current: ROLL_TYPE_DEFENSE }, attackerRoll: attackerRoll, passeArme: attackerRoll.passeArme, - }) + }, callbacks) } async doRollDefense(rollData, callbacks = []) { diff --git a/module/rdd-empoignade.js b/module/rdd-empoignade.js index dbdf0208..4536dac6 100644 --- a/module/rdd-empoignade.js +++ b/module/rdd-empoignade.js @@ -4,6 +4,8 @@ import { ChatUtility } from "./chat-utility.js"; import { RdDRollResult } from "./rdd-roll-result.js"; import { RdDRoll } from "./rdd-roll.js"; import { MappingCreatureArme } from "./item/mapping-creature-arme.mjs"; +import { MAP_PHASE } from "./initiative.mjs"; +import { RdDCombatManager } from "./rdd-combat.js"; /* -------------------------------------------- */ export class RdDEmpoignade { @@ -12,6 +14,49 @@ export class RdDEmpoignade { static init() { } + /* -------------------------------------------- */ + static isCombatantEmpoignade(actorId, tokenId) { + const combatant = RdDCombatManager.getCombatant(actorId, tokenId) + return MAP_PHASE.empoignade.rang == combatant?.system.init.rang + } + + static async ajustementEmpoignade(attacker, defender, adjust = 1) { + const empoignade = RdDEmpoignade.getEmpoignade(attacker, defender) + const empId = empoignade?.system.empoignadeid ?? foundry.utils.randomID(16) + if (empoignade) { + if (empoignade.system.empoigneurid == defender.id) { + adjust = - adjust + } + empoignade.system.pointsemp += adjust + await RdDEmpoignade.$updateEtatEmpoignade(empoignade, attacker, defender) + } + else { + await RdDEmpoignade.$createEtatEmpoignade({ + name: `Empoignade de ${attacker.name} sur ${defender.name}`, + type: ITEM_TYPES.empoignade, + system: { + description: "", + empoignadeid: empId, + empoigneurid: attacker.id, + empoigneid: defender.id, + pointsemp: adjust, + empoigneurname: attacker.name, + empoignename: defender.name + } + }, attacker, defender) + } + const result = RdDEmpoignade.getEmpoignadeById(defender, empId); + const defGrappled = result.system.pointsemp == (result.system.empoigneid == defender.id ? 2 : -2) + const attGrappled = result.system.pointsemp == (result.system.empoigneurid == attacker.id ? -2 : 2) + const grappling = Math.abs(result.system.pointsemp) > 0 + await defender.setEffect(STATUSES.StatusGrappling, grappling && !defGrappled) + await attacker.setEffect(STATUSES.StatusGrappling, grappling && !attGrappled) + await defender.setEffect(STATUSES.StatusGrappled, defGrappled) + await attacker.setEffect(STATUSES.StatusGrappled, attGrappled) + return result + } + + /* -------------------------------------------- */ static registerChatCallbacks(html) { $(html).on("click", '.defense-empoignade-cac', event => { @@ -237,10 +282,7 @@ export class RdDEmpoignade { if (rollData.rolled.isSuccess && isNouvelle) { - const objectEmpoignade = rollData.empoignade.toObject(); - // Creer l'empoignade sur attaquant/defenseur - attacker.creerObjetParMJ(objectEmpoignade); - defender.creerObjetParMJ(objectEmpoignade); + RdDEmpoignade.$createEtatEmpoignade(rollData.empoignade) } rollData.empoignade.isSuccess = rollData.rolled.isSuccess; @@ -259,7 +301,7 @@ export class RdDEmpoignade { return } - let empoignade = this.getEmpoignade(attacker, defender) + let empoignade = RdDEmpoignade.getEmpoignade(attacker, defender) if (!empoignade) { ui.notifications.warn("Une erreur s'est produite : Aucune empoignade trouvée !!") @@ -317,18 +359,33 @@ export class RdDEmpoignade { } /* -------------------------------------------- */ - static async $updateEtatEmpoignade(empoignade) { - console.log("UPDATE Empoignade", empoignade) + static async $createEtatEmpoignade(empoignade) { + console.log("CREATE Empoignade", empoignade) let defender = game.actors.get(empoignade.system.empoigneid) - let emp = RdDEmpoignade.getEmpoignadeById(defender, empoignade.system.empoignadeid) - let update = { _id: emp._id, "system.pointsemp": empoignade.system.pointsemp, "system.ausol": empoignade.system.ausol } - await defender.updateEmbeddedDocuments('Item', [update]) - let attacker = game.actors.get(empoignade.system.empoigneurid) - emp = RdDEmpoignade.getEmpoignadeById(attacker, empoignade.system.empoignadeid) - update = { _id: emp._id, "system.pointsemp": empoignade.system.pointsemp, "system.ausol": empoignade.system.ausol } - await attacker.updateEmbeddedDocuments('Item', [update]) + + // Creer l'empoignade sur attaquant/defenseur + await attacker.creerObjetParMJ(empoignade) + await defender.creerObjetParMJ(empoignade) + } + + /* -------------------------------------------- */ + static async $updateEtatEmpoignade(empoignade, attacker, defender) { + console.log("UPDATE Empoignade", empoignade) + const belligerants = [ + attacker ?? game.actors.get(empoignade.system.empoigneurid), + defender ?? game.actors.get(empoignade.system.empoigneid)] + + await Promise.all( + belligerants.map(async belligerant => { + const emp = RdDEmpoignade.getEmpoignadeById(belligerant, empoignade.system.empoignadeid) + return await belligerant.updateEmbeddedDocuments('Item', [{ + _id: emp._id, + "system.pointsemp": empoignade.system.pointsemp, + "system.ausol": empoignade.system.ausol + }]) + })) } /* -------------------------------------------- */ @@ -347,7 +404,7 @@ export class RdDEmpoignade { if (!RdDEmpoignade.isActionAutorisee("immobilise", attacker, defender)) { return } - let empoignade = this.getEmpoignade(attacker, defender) + let empoignade = RdDEmpoignade.getEmpoignade(attacker, defender) empoignade.system.ausol = true await this.$updateEtatEmpoignade(empoignade) @@ -366,7 +423,7 @@ export class RdDEmpoignade { if (!RdDEmpoignade.isActionAutorisee("immobilise", attacker, defender)) { return } - let empoignade = this.getEmpoignade(attacker, defender) + let empoignade = RdDEmpoignade.getEmpoignade(attacker, defender) await defender.setEffect(STATUSES.StatusProne, true); await this.$deleteEmpoignade(empoignade) @@ -382,7 +439,7 @@ export class RdDEmpoignade { if (!RdDEmpoignade.isActionAutorisee("immobilise", attacker, defender)) { return } - let empoignade = this.getEmpoignade(attacker, defender) + let empoignade = RdDEmpoignade.getEmpoignade(attacker, defender) //console.log("Perte d'endurance :!!!", perteMode) let endValue = defender.system.sante.endurance.value @@ -423,9 +480,17 @@ export class RdDEmpoignade { /* -------------------------------------------- */ static async createEmpoignade(attacker, defender) { return await Item.create({ - name: "Empoignade en cours de " + attacker.name + ' sur ' + defender.name, - type: 'empoignade', - system: { description: "", empoignadeid: foundry.utils.randomID(16), compteempoigne: 0, empoigneurid: attacker.id, empoigneid: defender.id, ptsemp: 0, empoigneurname: attacker.name, empoignename: defender.name } + name: "Empoignade de " + attacker.name + ' sur ' + defender.name, + type: ITEM_TYPES.empoignade, + system: { + description: "", + empoignadeid: foundry.utils.randomID(16), + empoigneurid: attacker.id, + empoigneid: defender.id, + pointsemp: 0, + empoigneurname: attacker.name, + empoignename: defender.name + } }, { temporary: true diff --git a/module/rdd-roll.js b/module/rdd-roll.js index 3c2bd93b..5eeda886 100644 --- a/module/rdd-roll.js +++ b/module/rdd-roll.js @@ -334,7 +334,7 @@ export class RdDRoll extends Dialog { // Mise à jour valeurs this.html.find(".dialog-roll-title").text(this._getTitle(rollData)); this.html.find("input.check-mortalite").prop('checked', rollData.dmg.mortalite == RDD_CONFIG.encaissement.nonmortel); - this.html.find("label.dmg-arme-actor").text(rollData.dmg.mortalite == EMPOIGNADE ? EMPOIGNADE : Misc.toSignedString(rollData.dmg.total)); + this.html.find("label.dmg-arme-actor").text(rollData.dmg.isEmpoignade ? EMPOIGNADE : Misc.toSignedString(rollData.dmg.total)); this.html.find("label.arme-mortalite").text(rollData.dmg.mortalite); this.html.find("div.placeholder-ajustements").empty().append(adjustements); this.html.find("div.placeholder-resolution").empty().append(resolutionTable) diff --git a/module/roll/chat-roll-result.mjs b/module/roll/chat-roll-result.mjs index d64212bd..6a90fb2d 100644 --- a/module/roll/chat-roll-result.mjs +++ b/module/roll/chat-roll-result.mjs @@ -5,13 +5,13 @@ import { RdDCombat } from "../rdd-combat.js" import { ROLL_TYPE_ATTAQUE, ROLL_TYPE_DEFENSE } from "./roll-constants.mjs" import { RdDResolutionTable } from "../rdd-resolution-table.js" import { RDD_CONFIG, renderTemplate } from "../constants.js" -import { EMPOIGNADE } from "../item/arme.js" import { RdDTextEditor } from "../apps/rdd-text-roll-editor.js" import { RollTypeCuisine } from "./roll-type-cuisine.mjs" import { RollTypeMeditation } from "./roll-type-meditation.mjs" import { PART_DEFENSE } from "./roll-part-defense.mjs" import { PART_ATTAQUE } from "./roll-part-attaque.mjs" import { RdDRollTables } from "../rdd-rolltables.js" +import { RdDEmpoignade } from "../rdd-empoignade.js" export default class ChatRollResult { static init() { @@ -67,7 +67,7 @@ export default class ChatRollResult { isShowEncaissement(roll) { switch (roll.type.current) { case ROLL_TYPE_DEFENSE: - return roll.rolled.isEchec && roll.attackerRoll?.dmg.mortalite != EMPOIGNADE + return roll.rolled.isEchec } return false } @@ -132,6 +132,7 @@ export default class ChatRollResult { async chatListeners(html) { $(html).on("click", '.appel-chance', event => this.onClickAppelChance(event)) $(html).on("click", '.appel-destinee', event => this.onClickAppelDestinee(event)) + $(html).on("click", '.button-defense', event => this.onClickDefense(event)) $(html).on("click", '.encaissement', event => this.onClickEncaissement(event)) $(html).on("click", '.resister-recul', event => this.onClickRecul(event)) $(html).on("click", '.choix-particuliere', event => this.onClickChoixParticuliere(event)) @@ -219,18 +220,41 @@ export default class ChatRollResult { }) } - async onClickEncaissement(event) { + async onClickDefense(event) { const chatMessage = ChatUtility.getChatMessage(event) const savedRoll = this.loadChatMessageRoll(chatMessage) + const attackerRoll = savedRoll.attackerRoll + this.getCombat(attackerRoll)?.defenseV2(attackerRoll, + [roll => { ChatUtility.removeChatMessageId(chatMessage.id) }] + ) + } + + async onClickEncaissement(event) { + const chatMessage = ChatUtility.getChatMessage(event) + const isMessageDemande = ChatUtility.getMessageData(chatMessage, 'demande-defense') + const savedRoll = this.loadChatMessageRoll(chatMessage) const attaque = savedRoll.attackerRoll - const defender = game.actors.get(savedRoll.ids.actorId) - const attacker = game.actors.get(savedRoll.ids.opponentId) const defenderToken = savedRoll.ids.actorTokenId ? canvas.tokens.get(savedRoll.ids.actorTokenId) : undefined const attackerToken = savedRoll.ids.opponentTokenId ? canvas.tokens.get(savedRoll.ids.opponentTokenId) : undefined - await defender?.encaisserDommages(attaque.dmg, attacker, undefined, attackerToken, defenderToken) - - savedRoll.done.encaissement = true - await this.updateChatMessage(chatMessage, savedRoll) + switch (attaque.dmg.mortalite) { + case RDD_CONFIG.encaissement.empoignade: + savedRoll.done = savedRoll.done ?? {} + savedRoll.done.empoignade = await RdDEmpoignade.ajustementEmpoignade(attackerToken.actor, defenderToken.actor) + break + case RDD_CONFIG.encaissement.entiteincarnee: + case RDD_CONFIG.encaissement.nonmortel: + case RDD_CONFIG.encaissement.mortel: + const defender = defenderToken?.actor ?? game.actors.get(savedRoll.ids.actorId) + const attacker = attackerToken?.actor ?? game.actors.get(savedRoll.ids.opponentId) + await defender?.encaisserDommages(attaque.dmg, attacker, undefined, attackerToken, defenderToken) + break + } + if (isMessageDemande) { + ChatUtility.removeChatMessageId(chatMessage.id) + } else { + savedRoll.done.encaissement = true + await this.updateChatMessage(chatMessage, savedRoll) + } } async onClickRecul(event) { diff --git a/module/roll/roll-basic-parts.mjs b/module/roll/roll-basic-parts.mjs index 8e5365e2..d8c9c51b 100644 --- a/module/roll/roll-basic-parts.mjs +++ b/module/roll/roll-basic-parts.mjs @@ -54,13 +54,27 @@ export class RollBasicParts { } } + static prepareDefense(attackerRoll) { + if (!attackerRoll.passeArme) { + attackerRoll.passeArme = foundry.utils.randomID(16); + } + return { + ids: RollBasicParts.reverseIds(attackerRoll), + active: attackerRoll.opponent, + opponent: attackerRoll.active, + attackerRoll: attackerRoll, + passeArme: attackerRoll.passeArme, + show: { encaissement: true } + } + } + static reverseIds(rollData) { return { sceneId: rollData.ids.sceneId, actorId: rollData.ids.opponentId, actorTokenId: rollData.ids.opponentTokenId, opponentId: rollData.ids.actorId, - opponentTokenId: rollData.actorTokenId + opponentTokenId: rollData.ids.actorTokenId } } diff --git a/module/roll/roll-dialog-adapter.mjs b/module/roll/roll-dialog-adapter.mjs index 809b3258..dc653679 100644 --- a/module/roll/roll-dialog-adapter.mjs +++ b/module/roll/roll-dialog-adapter.mjs @@ -124,7 +124,7 @@ export class RollDialogAdapter { const attaque = rollData.current.attaque; const choix = [] - const isEmpoignade = attaque.dmg.mortalite == 'empoignade'; + const isEmpoignade = attaque.dmg.isEmpoignade const isCharge = attaque.tactique == 'charge' /* TODO: cas de créatures faisant des lancers, Glou, Glipzouk */ const isMeleeDiffNegative = (attaque.comp.type == ITEM_TYPES.competencecreature || rollData.current.carac.key == CARACS.MELEE) diff --git a/module/roll/roll-dialog.mjs b/module/roll/roll-dialog.mjs index d2eefe70..96667294 100644 --- a/module/roll/roll-dialog.mjs +++ b/module/roll/roll-dialog.mjs @@ -44,6 +44,7 @@ import { RollTypeCuisine } from "./roll-type-cuisine.mjs"; import { RollPartCuisine } from "./roll-part-cuisine.mjs"; import { OptionsAvancees, ROLL_DIALOG_V2_TEST } from "../settings/options-avancees.js"; import { ActorImpacts } from "../technical/actor-impacts.mjs"; +import { RollPartEmpoignade } from "./roll-part-empoignade.mjs"; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api @@ -85,6 +86,7 @@ const ROLL_PARTS = [ new RollPartConditions(), new RollPartEthylisme(), new RollPartMalusArmure(), + new RollPartEmpoignade(), new RollPartEncTotal(), new RollPartSurEnc(), new RollPartAppelMoral(), diff --git a/module/roll/roll-part-attaque.mjs b/module/roll/roll-part-attaque.mjs index 20b0c52b..6ca3827a 100644 --- a/module/roll/roll-part-attaque.mjs +++ b/module/roll/roll-part-attaque.mjs @@ -1,6 +1,10 @@ import { RDD_CONFIG } from "../constants.js" +import { ATTAQUE_TYPE_MELEE } from "../item/arme.js" import { RdDBonus } from "../rdd-bonus.js" -import { DIFF, ROLL_TYPE_ATTAQUE } from "./roll-constants.mjs" +import { CARACS } from "../rdd-carac.js" +import { RdDEmpoignade } from "../rdd-empoignade.js" +import { DIFF, ROLL_TYPE_ATTAQUE, ROLL_TYPE_COMP } from "./roll-constants.mjs" +import RollDialog from "./roll-dialog.mjs" import { PART_CARAC } from "./roll-part-carac.mjs" import { PART_COMP } from "./roll-part-comp.mjs" import { PART_DIFF } from "./roll-part-diff.mjs" @@ -12,6 +16,10 @@ export const PART_ATTAQUE = 'attaque' const TACTIQUES = RdDBonus.tactiques.filter(it => it.isTactique) +const FILTER_ATTAQUE_EMPOIGNADE = attaque => attaque.arme.isEmpoignade() +const FILTER_ATTAQUE_NON_EMPOIGNADE = attaque => !attaque.arme.isEmpoignade() +const FILTER_ATTAQUE_EMPOIGNE = attaque => attaque.arme.isUtilisableEmpoigne() && ATTAQUE_TYPE_MELEE.includes(attaque.main) + export class RollPartAttaque extends RollPartSelect { get code() { return PART_ATTAQUE } @@ -22,7 +30,8 @@ export class RollPartAttaque extends RollPartSelect { loadRefs(rollData) { const refs = this.getRefs(rollData) const attaques = rollData.active.actor.listAttaques() - refs.attaques = attaques.map(it => RollPartAttaque.$extractAttaque(it, rollData.active.actor)) + refs.all = attaques.map(it => RollPartAttaque.$extractAttaque(it, rollData.active.actor)) + this.filterAttaquesEmpoignade(rollData) refs.tactiques = TACTIQUES if (refs.attaques.length > 0) { const attaque = this.findAttaque(refs.attaques, this.getSaved(rollData)) @@ -30,6 +39,10 @@ export class RollPartAttaque extends RollPartSelect { } } + isAttaqueEmpoignade(it) { + return it.arme.isEmpoignade() + } + store(rollData, targetData) { super.store(rollData, targetData) this.getSaved(targetData).dmg = this.getCurrent(rollData).dmg @@ -59,10 +72,23 @@ export class RollPartAttaque extends RollPartSelect { } prepareContext(rollData) { + this.filterAttaquesEmpoignade(rollData) const current = this.getCurrent(rollData) current.dmg = RdDBonus.dmgRollV2(rollData, current) } + filterAttaquesEmpoignade(rollData) { + const refs = this.getRefs(rollData) + const isEmpoignade = RdDEmpoignade.isCombatantEmpoignade(rollData.ids.actorId, rollData.ids.actorTokenId) + refs.isEmpoignadeEnCours = RdDEmpoignade.isEmpoignadeEnCours(rollData.active.actor) + const filterAttaques = isEmpoignade ? + FILTER_ATTAQUE_EMPOIGNADE + : refs.isEmpoignadeEnCours + ? FILTER_ATTAQUE_EMPOIGNE + : FILTER_ATTAQUE_NON_EMPOIGNADE + refs.attaques = refs.all.filter(filterAttaques) + } + getAjustements(rollData) { const current = this.getCurrent(rollData) const tactique = current.tactique ? [{ label: current.tactique.label, value: current.tactique.attaque }] : [] @@ -79,6 +105,7 @@ export class RollPartAttaque extends RollPartSelect { const selectAttaque = rollDialog.element.querySelector(`roll-section[name="${this.code}"] select[name="select-attaque"]`) const selectTactique = rollDialog.element.querySelector(`roll-section[name="${this.code}"] select[name="select-tactique"]`) const checkMortalite = rollDialog.element.querySelector(`roll-section[name="${this.code}"] input[name="check-mortalite"]`) + const utiliserDagueEmpoignade = rollDialog.element.querySelector(`roll-section[name="${this.code}"] a.utiliser-dague-empoignade`) const current = this.getCurrent(rollDialog.rollData) selectAttaque.addEventListener("change", e => { @@ -99,6 +126,23 @@ export class RollPartAttaque extends RollPartSelect { current.dmg.mortalite = (e.currentTarget.checked ? RDD_CONFIG.encaissement.mortel : RDD_CONFIG.encaissement.nonmortel) rollDialog.render() }) + utiliserDagueEmpoignade?.addEventListener("click", e => { + e.preventDefault() + const rollData = rollDialog.rollData + this.utiliserDagueEmpoignade(rollData) + }) + } + + utiliserDagueEmpoignade(rollData) { + RollDialog.create({ + ids: { actorId: rollData.ids.actorId, actorTokenId: rollData.ids.actorTokenId }, + type: { allowed: [ROLL_TYPE_COMP], current: ROLL_TYPE_COMP }, + selected: { + carac: { key: CARACS.DEXTERITE, forced: true }, + comp: { key: 'Dague', forced: true }, + diff: { type: DIFF.IMPOSEE, value: -4 } + } + }) } impactOtherPart(part, rollData) { diff --git a/module/roll/roll-part-carac.mjs b/module/roll/roll-part-carac.mjs index 065549ca..55801b36 100644 --- a/module/roll/roll-part-carac.mjs +++ b/module/roll/roll-part-carac.mjs @@ -1,3 +1,4 @@ +import { Grammar } from "../grammar.js" import { RollPartSelect } from "./roll-part-select.mjs" import { ROLLDIALOG_SECTION } from "./roll-part.mjs" @@ -12,8 +13,14 @@ export class RollPartCarac extends RollPartSelect { loadRefs(rollData) { const refs = this.getRefs(rollData) + const selected = this.getSelected(rollData) const actor = rollData.active.actor refs.all = [...this.$getActorCaracs(actor), ...this.$getCaracCompetenceCreature(actor)] + .filter(c => !selected.forced || + (selected.key ? + Grammar.includesLowerCaseNoAccent(c.label, selected.key) + : c.key == '') + ) refs.caracs = refs.all this.$selectCarac(rollData) } diff --git a/module/roll/roll-part-comp.mjs b/module/roll/roll-part-comp.mjs index 06f3f5cc..e38832be 100644 --- a/module/roll/roll-part-comp.mjs +++ b/module/roll/roll-part-comp.mjs @@ -18,12 +18,16 @@ export class RollPartComp extends RollPartSelect { loadRefs(rollData) { const refs = this.getRefs(rollData) const selected = this.getSelected(rollData) - refs.all = this.$getActorComps(rollData) - .filter(comp => !selected.forced || - (selected.key ? - Grammar.includesLowerCaseNoAccent(comp.name, selected.key) - : comp.key == '') - ) + const all = this.$getActorComps(rollData) + if (selected.forced) { + refs.all = all.filter(comp => Grammar.equalsInsensitive(comp.label, selected.key)) + if (refs.all.length == 0) { + refs.all = all.filter(comp => Grammar.includesLowerCaseNoAccent(comp.label, selected.key)) + } + } + else { + refs.all = all + } refs.comps = refs.all this.$selectComp(rollData) } diff --git a/module/roll/roll-part-defense.mjs b/module/roll/roll-part-defense.mjs index 88a0c595..d89935d6 100644 --- a/module/roll/roll-part-defense.mjs +++ b/module/roll/roll-part-defense.mjs @@ -1,4 +1,4 @@ -import { ITEM_TYPES } from "../constants.js" +import { ITEM_TYPES, RDD_CONFIG } from "../constants.js" import { ATTAQUE_TYPE, RdDItemArme } from "../item/arme.js" import { CARACS } from "../rdd-carac.js" import { DIFF, ROLL_TYPE_DEFENSE } from "./roll-constants.mjs" @@ -15,6 +15,8 @@ export class RollPartDefense extends RollPartSelect { get code() { return PART_DEFENSE } get section() { return ROLLDIALOG_SECTION.CHOIX } + + isValid(rollData) { return rollData.attackerRoll != undefined } visible(rollData) { return this.isRollType(rollData, ROLL_TYPE_DEFENSE) } static getDiffAttaque(attackerRoll) { @@ -27,13 +29,22 @@ export class RollPartDefense extends RollPartSelect { const attackerRoll = rollData.attackerRoll const defenseur = rollData.active.actor refs.isDistance = [ATTAQUE_TYPE.TIR, ATTAQUE_TYPE.LANCER].find(it => it == attackerRoll?.main) - const esquives = refs.isDistance == ATTAQUE_TYPE.TIR ? [] : defenseur.getCompetencesEsquive() - .map(it => RollPartDefense.$extractEsquive(it, defenseur)) + const isEmpoignade = attackerRoll.dmg.isEmpoignade + const isEmpoignadeEnCours = isEmpoignade && defenseur.itemTypes[ITEM_TYPES.empoignade].find(it => + [it.system.empoigneurid, it.system.empoigneid].includes(rollData.ids.opponentId) && + it.system.pointsemp != 0) - const parades = defenseur.items.filter(it => it.isParade() && (!refs.isDistance || it.isBouclier())) - .map(it => RollPartDefense.$extractParade(it, attackerRoll?.arme, defenseur)) + const esquives = (refs.isDistance == ATTAQUE_TYPE.TIR || isEmpoignadeEnCours) + ? [] + : defenseur.getCompetencesEsquive() + const parades = isEmpoignade + ? [RdDItemArme.empoignade(defenseur)] + : defenseur.items.filter(it => it.isParade() && (!refs.isDistance || it.isBouclier())) - refs.defenses = [...esquives, ...parades].filter(it => it != undefined) + refs.defenses = [ + ...esquives.map(it => RollPartDefense.$extractEsquive(it, defenseur)), + ...parades.map(it => RollPartDefense.$extractParade(it, attackerRoll?.arme, defenseur)) + ] this.$selectDefense(rollData) } diff --git a/module/roll/roll-part-empoignade.mjs b/module/roll/roll-part-empoignade.mjs new file mode 100644 index 00000000..73c9bd65 --- /dev/null +++ b/module/roll/roll-part-empoignade.mjs @@ -0,0 +1,31 @@ +import { RDD_CONFIG } from "../constants.js" +import { ATTAQUE_TYPE_MELEE } from "../item/arme.js" +import { RdDEmpoignade } from "../rdd-empoignade.js" +import { ROLL_TYPE_ATTAQUE } from "./roll-constants.mjs" +import { PART_ATTAQUE } from "./roll-part-attaque.mjs" +import { RollPartCheckbox } from "./roll-part-checkbox.mjs" + +const EMPOIGNADE = "empoignade" + +export class RollPartEmpoignade extends RollPartCheckbox { + + get code() { return EMPOIGNADE } + + isValid(rollData) { + return RdDEmpoignade.isCombatantEmpoignade(rollData.ids.opponentId, rollData.ids.opponentTokenId) && + !RdDEmpoignade.isCombatantEmpoignade(rollData.ids.actorId, rollData.ids.actorTokenId) + } + + visible(rollData) { + return rollData.type.current == ROLL_TYPE_ATTAQUE && + ATTAQUE_TYPE_MELEE.includes(rollData.current[PART_ATTAQUE].main) && + RdDEmpoignade.isCombatantEmpoignade(rollData.ids.opponentId, rollData.ids.opponentTokenId) && + !RdDEmpoignade.isCombatantEmpoignade(rollData.ids.actorId, rollData.ids.actorTokenId) && + !RdDEmpoignade.isEmpoignadeEnCours(rollData.active.actor) + } + + + getCheckboxIcon(rollData) { return `` } + getCheckboxLabel(rollData) { return "vs. empoigneur" } + getCheckboxValue(rollData) { return 4 } +} diff --git a/module/roll/roll-type.mjs b/module/roll/roll-type.mjs index 4f7ed51e..bc21742c 100644 --- a/module/roll/roll-type.mjs +++ b/module/roll/roll-type.mjs @@ -47,6 +47,7 @@ export class RollType { } setDiffType(rollData, type) { + type = rollData.selected[PART_DIFF].type ?? type rollData.current[PART_DIFF].type = type this.setRollDataType(rollData) } diff --git a/module/settings/status-effects.js b/module/settings/status-effects.js index 74589c8c..43ce4a20 100644 --- a/module/settings/status-effects.js +++ b/module/settings/status-effects.js @@ -22,8 +22,8 @@ export const demiReveStatusEffect = { rdd: true, id: STATUSES.StatusDemiReve, name: 'EFFECT.StatusDemiReve', img: RDD_CONFIG.icons.demiReve }; const rddStatusEffects = [ - { rdd: true, id: STATUSES.StatusGrappling, tint: '#33cc33', name: 'EFFECT.StatusGrappling', img: RDD_CONFIG.icons.empoignade }, - { rdd: true, id: STATUSES.StatusGrappled, tint: '#ff9900', name: 'EFFECT.StatusGrappled', img: RDD_CONFIG.icons.empoignade }, + { rdd: true, id: STATUSES.StatusGrappling, name: 'EFFECT.StatusGrappling', img: RDD_CONFIG.icons.empoignade }, + { rdd: true, id: STATUSES.StatusGrappled, tint: '#d5633d', name: 'EFFECT.StatusGrappled', img: RDD_CONFIG.icons.empoignade }, { rdd: true, id: STATUSES.StatusRestrained, name: 'EFFECT.StatusRestrained', img: 'icons/svg/net.svg' }, { rdd: true, id: STATUSES.StatusStunned, name: 'EFFECT.StatusStunned', img: 'icons/svg/stoned.svg', "duration.rounds": 1 }, @@ -45,8 +45,9 @@ const statusSurpriseTotale = new Set([STATUSES.StatusUnconscious, STATUSES.Statu export class StatusEffects extends FormApplication { static onReady() { - const rddEffectIds = rddStatusEffects.map(it => it.id); + const rddEffectIds = rddStatusEffects.map(it => it.id) rddStatusEffects.forEach(it => { + it.name = game.i18n.localize(it.name) it.statuses = new Set([it.id]) }) const defaultStatusEffectIds = CONFIG.statusEffects.map(it => it.id); diff --git a/templates/chat-demande-defense.hbs b/templates/chat-demande-defense.hbs index 75d544be..62ae5807 100644 --- a/templates/chat-demande-defense.hbs +++ b/templates/chat-demande-defense.hbs @@ -1,25 +1,24 @@ -{{log this}}
- +
-

Défense de {{opponent.name}}

+

Défense de {{active.name}}

- {{#if (eq opponent.surprise.key 'totale')}} - {{opponent.name}} est totalement surpris + {{#if (eq active.surprise.key 'totale')}} + {{active.name}} est totalement surpris {{else}} - {{opponent.name}} doit se défendre - {{~#if (eq opponent.surprise.key 'demi')}} avec une significative {{/if}} d'une attaque - {{~#if particuliere}} particulière en - {{~#if (eq particuliere 'finesse')}} finesse + {{active.name}} doit se défendre + {{~#if (eq active.surprise.key 'demi')}} avec une significative {{/if}} d'une attaque + {{~#if attackerRoll.particuliere}} particulière en + {{~#if (eq attackerRoll.particuliere 'finesse')}} finesse {{else if (eq particuliere 'force')}} force {{else if (eq particuliere 'rapidite')}} rapidité {{/if~}} - {{/if}} de {{active.name}} ({{current.attaque.label}}): + {{/if}} de {{opponent.name}} ({{attackerRoll.current.attaque.label}}): {{/if}}
@@ -28,30 +27,23 @@
diff --git a/templates/roll/result/partial-encaissement.hbs b/templates/roll/result/partial-encaissement.hbs index 8adbcb60..45a29d44 100644 --- a/templates/roll/result/partial-encaissement.hbs +++ b/templates/roll/result/partial-encaissement.hbs @@ -1,15 +1,29 @@ +{{log 'partial-encaissement' this}} {{#if show.encaissement}} {{#if done.encaissement}} - - - {{active.name}} a encaissé - + + {{#if (eq attackerRoll.dmg.mortalite 'empoignade')}} + + {{opponent.name}} a {{done.empoignade.system.pointsemp}} point d'empoignade contre {{active.name}}. + {{#if (gt done.empoignade.system.pointsemp 2)}} Si {{active.name}} ne se libère pas, il sera immobilisé à la fin du round{{/if}} + {{else}} + + {{active.name}} a encaissé + {{/if}} + {{else}} - - Encaisser à {{plusMoins attackerRoll.dmg.total}} - {{#if (eq attackerRoll.dmg.mortalite 'non-mortel')~}}(non-mortel){{/if}} - + {{#if (eq attackerRoll.dmg.mortalite 'empoignade')}} + + + Marquer un point d'empoignade + + {{else}} + + Encaisser à {{plusMoins attackerRoll.dmg.total}} + {{#if (eq attackerRoll.dmg.mortalite 'non-mortel')~}}(non-mortel){{/if}} + + {{/if}} {{/if}} {{/if}} diff --git a/templates/roll/roll-part-attaque.hbs b/templates/roll/roll-part-attaque.hbs index 28b6e7dd..2b1bf3e9 100644 --- a/templates/roll/roll-part-attaque.hbs +++ b/templates/roll/roll-part-attaque.hbs @@ -15,19 +15,32 @@ {{selectOptions refs.tactiques selected=current.tactique.key valueAttr="key" labelAttr="label"}} - - {{#if (eq current.arme.system.mortalite 'empoignade')}} - Empoignade, pas de dommages directs - {{else}} - {{#if (and (ne current.arme.system.mortalite 'non-mortel') (eq current.dmg.penetration 0))}} - - {{/if}} + {{#if (and refs.isEmpoignadeEnCours (eq current.arme.system.competence 'Dague'))}} + + + Pour pouvoir attaquer avec une dague en cours d'empoignade, il faut réussir:
+ DEXTÉRITÉ / Dague à -4 +
En cas d'échec total, {{rollData.active.name}} sera désarmé. +
+
+ {{/if}} + {{#if (eq current.arme.system.mortalite 'empoignade')}} + + Empoignade, pas de dommages directs.
+ Si {{either rollData.opponent.name}} est équipé d'une arme de mêlée, ou attaque + à mains nues (pugilat), il bénéficie d'un bonus de +4 à son attaque. +
+ {{else}} + + {{#if (and (ne current.arme.system.mortalite 'non-mortel') (eq current.dmg.penetration 0))}} + + {{/if}} - {{/if}} - +
+ {{/if}} {{#if rollData.active.effets}}