foundryvtt-reve-de-dragon/module/rdd-combat.js
Vincent Vandemeulebrouck 3f62582af5 Améliorations du combat
- nettoyage des infos et messages de combat améliorés
- message de status envoyé uniquement par le GM (pour éviter les
doublons)
- stockage des infos de passes d'armes géré par RdDCombat

Déplacement des Hooks/notifications de messages socket:
- liés au combat dans la méthode RdDCombat.init

Convention de nommage: les méthodes de gestion de notification par
hook/socket sont préfixées 'on'
ex: onSocketMessage, onUpdateCombat, onPreDeleteCombat
2021-01-13 03:00:10 +01:00

869 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ChatUtility } from "./chat-utility.js";
import { RdDItemArme } from "./item-arme.js";
import { RdDItemCompetence } from "./item-competence.js";
import { Misc } from "./misc.js";
import { RdDBonus } from "./rdd-bonus.js";
import { RdDResolutionTable } from "./rdd-resolution-table.js";
import { RdDRoll } from "./rdd-roll.js";
import { RdDRollTables } from "./rdd-rolltables.js";
export class RdDCombat {
static init() {
this.initStorePasseArmes();
Hooks.on("updateCombat", (combat, data) => { RdDCombat.onUpdateCombat(combat, data) });
Hooks.on("preDeleteCombat", (combat, options) => { RdDCombat.onPreDeleteCombat(combat, options); });
}
/* -------------------------------------------- */
static initStorePasseArmes() {
game.system.rdd.combatStore = {
attaques: {},
defenses: {}
};
}
/* -------------------------------------------- */
static onSocketMessage(sockmsg) {
switch (sockmsg.msg) {
case "msg_encaisser":
return RdDCombat.terminerPasseArmes(data);
case "msg_defense":
return RdDCombat.handleMsgDefense(sockmsg.data);
}
}
/* -------------------------------------------- */
static onUpdateCombat(combat, data) {
if (combat.data.round != 0 && combat.turns && combat.data.active) {
RdDCombat.combatNouveauRound(combat);
}
}
/* -------------------------------------------- */
static onPreDeleteCombat(combat, options) {
if (game.user.isGM) {
ChatUtility.removeMyChatMessageContaining(`<div data-combatid="${combat.id}" data-combatmessage="actor-turn-summary">`)
/*
* TODO: support de plusieurs combats parallèles
* il faudrait avoir un id de combat en plus de celui de passe d'armes
*/
for (const key in game.system.rdd.combatStore.attaques) {
const attackerRoll = game.system.rdd.combatStore.attaques[key];
ChatUtility.removeChatMessageActionsPasseArme(`<div data-passearme="${attackerRoll.passeArme}">`);
}
for (const key in game.system.rdd.combatStore.defenses) {
const defenderRoll = game.system.rdd.combatStore.defenses[key];
ChatUtility.removeMyChatMessageContaining(`<div data-passearme="${defenderRoll.passeArme}">`);
}
RdDCombat.initStorePasseArmes();
}
}
/* -------------------------------------------- */
static combatNouveauRound(combat) {
let turn = combat.turns.find(t => t.tokenId == combat.current.tokenId);
if (game.user.isGM) {
// seul le GM notifie le status
this.displayActorCombatStatus(combat, turn.actor);
// TODO Playaudio for player??
}
}
/* -------------------------------------------- */
static isActive() {
return true;
}
/* -------------------------------------------- */
static createUsingTarget(attacker) {
const target = RdDCombat.getTarget();
if (target == undefined) {
ui.notifications.warn((game.user.targets?.size ?? 0) > 1
? "Vous devez choisir <strong>une seule</strong> cible à attaquer!"
: "Vous devez choisir une cible à attaquer!");
}
const defender = target?.actor;
const defenderTokenId = target?.data._id;
return this.create(attacker, defender, defenderTokenId, 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, defenderTokenId, target = undefined) {
return new RdDCombat(attacker, defender, defenderTokenId, 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) {
const defenderTokenId = dataDefenderTokenId.value;
let defenderToken = canvas.tokens.get(defenderTokenId);
let defender = defenderToken.actor;
return RdDCombat.create(attacker, defender, defenderTokenId);
}
return RdDCombat.createUsingTarget(attacker)
}
/* -------------------------------------------- */
static handleMsgDefense(data) {
let defenderToken = canvas.tokens.get(data.defenderTokenId);
if (defenderToken) {
if (!game.user.isGM && game.user.character == undefined) { // vérification / sanity check
ui.notifications.error("Le joueur " + game.user.name + " n'est connecté à aucun personnage. Impossible de continuer.");
return;
}
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.combatStore.attaques[data.attackerId] = duplicate(data.rollData);
data.whisper = [game.user];
data.blind = true;
data.rollMode = "blindroll";
ChatMessage.create(data);
}
}
}
static terminerPasseArmes(data) {
if (game.user.isGM) { // Seul le GM nettoie le stockage des données de combat
let attackerRoll = game.system.rdd.combatStore.attaques[data.attackerId]; // Retrieve the rolldata from the store
game.system.rdd.combatStore.attaques[data.attackerId] = undefined;
game.system.rdd.combatStore.defenses[attackerRoll.passeArme] = undefined;
}
}
/* -------------------------------------------- */
static _sendRollMessage(sender, recipient, defenderTokenId, topic, message, rollData) {
let chatMessage = {
content: message,
whisper: ChatUtility.getWhisperRecipients("blindroll", recipient.name),
};
// envoyer le message au destinataire
if (!game.user.isGM || recipient.hasPlayerOwner) {
let data = {
attackerId: sender?.data._id,
defenderId: recipient?.data._id,
defenderTokenId: defenderTokenId,
rollData: duplicate(rollData),
rollMode: true
};
mergeObject(data, chatMessage);
game.socket.emit("system.foundryvtt-reve-de-dragon", { msg: topic, data: data });
} else {
chatMessage.whisper = [game.user];
}
if (game.user.isGM) { // Always push the message to the MJ
ChatMessage.create(chatMessage);
}
}
/* -------------------------------------------- */
static _callJetDeVie(event) {
let actorId = event.currentTarget.attributes['data-actorId'].value;
let actor = game.actors.get(actorId);
actor.jetVie();
}
/* -------------------------------------------- */
static registerChatCallbacks(html) {
for (let button of [
'#parer-button',
'#esquiver-button',
'#particuliere-attaque',
'#encaisser-button',
'#appel-chance-defense',
'#appel-destinee-defense',
'#appel-chance-attaque',
'#appel-destinee-attaque',
'#echec-total-attaque',
]) {
html.on("click", button, event => {
event.preventDefault();
RdDCombat.createForEvent(event).onEvent(button, event);
});
}
html.on("click", '#chat-jet-vie', event => {
event.preventDefault();
RdDCombat._callJetDeVie(event);
});
}
/* -------------------------------------------- */
constructor(attacker, defender, defenderTokenId, target) {
this.attacker = attacker;
this.defender = defender;
this.target = target;
this.attackerId = this.attacker.data._id;
this.defenderId = this.defender.data._id;
this.defenderTokenId = defenderTokenId;
}
/* -------------------------------------------- */
async onEvent(button, event) {
let attackerRoll = game.system.rdd.combatStore.attaques[this.attackerId];
if (!attackerRoll) {
ui.notifications.warn("Action automatisée impossible, le jet de l'attaquant a été perdu (suite à un raffraichissement?)")
return;
}
const defenderTokenId = event.currentTarget.attributes['data-defenderTokenId'].value;
const armeParadeId = event.currentTarget.attributes['data-armeid']?.value;
switch (button) {
case '#particuliere-attaque': return await this.choixParticuliere(attackerRoll, event.currentTarget.attributes['data-mode'].value);
case '#parer-button': return this.parade(attackerRoll, armeParadeId);
case '#esquiver-button': return this.esquive(attackerRoll);
case '#encaisser-button': return this.encaisser(attackerRoll, defenderTokenId);
case '#echec-total-attaque': return this._onEchecTotal(attackerRoll);
case '#appel-chance-attaque': return this.attacker.rollAppelChance(
() => this.attaqueChanceuse(attackerRoll),
() => this._onEchecTotal(attackerRoll));
case '#appel-chance-defense': return this.defender.rollAppelChance(
() => this.defenseChanceuse(attackerRoll),
() => this.afficherOptionsDefense(attackerRoll, { defenseChance: true }));
case '#appel-destinee-attaque': return this.attacker.appelDestinee(
() => this.attaqueSignificative(attackerRoll),
() => { });
case '#appel-destinee-defense': return this.defender.appelDestinee(
() => this.defenseDestinee(attackerRoll),
() => { });
}
}
/* -------------------------------------------- */
_consumeDefense(passeArme) {
let defenderRoll = this._getDefense(passeArme);
game.system.rdd.combatStore.defenses[passeArme] = undefined;
return defenderRoll;
}
/* -------------------------------------------- */
_getDefense(passeArme) {
return game.system.rdd.combatStore.defenses[passeArme];
}
/* -------------------------------------------- */
_storeDefense(defenderRoll) {
game.system.rdd.combatStore.defenses[defenderRoll.passeArme] = defenderRoll;
}
/* -------------------------------------------- */
attaqueChanceuse(attackerRoll) {
ui.notifications.info("L'attaque est rejouée grâce à la chance")
attackerRoll.essais.attaqueChance = true;
this.attaque(attackerRoll, attackerRoll.arme);
}
/* -------------------------------------------- */
attaqueDestinee(attackerRoll) {
ui.notifications.info('Attaque significative grâce à la destinée')
RdDResolutionTable.forceSignificative(attackerRoll.rolled);
this.removeChatMessageActionsPasseArme(attackerRoll.passeArme);
this._onAttaqueNormale(attackerRoll);
}
/* -------------------------------------------- */
defenseChanceuse(attackerRoll) {
ui.notifications.info("La défense est rejouée grâce à la chance")
attackerRoll.essais.defenseChance = true;
attackerRoll.essais.defense = false;
this.removeChatMessageActionsPasseArme(attackerRoll.passeArme);
this._sendMessageDefense(attackerRoll);
}
/* -------------------------------------------- */
defenseDestinee(attackerRoll) {
let defenderRoll = this._getDefense(attackerRoll.passeArme);
if (defenderRoll) {
ui.notifications.info('Défense significative grâce à la destinée')
RdDResolutionTable.forceSignificative(defenderRoll.rolled);
this.removeChatMessageActionsPasseArme(defenderRoll.passeArme);
if (defenderRoll.arme) {
this._onParadeNormale(defenderRoll);
}
else {
this._onEsquiveNormale(defenderRoll);
}
}
else {
ui.notifications.warn("Appel à la destinée impossible, la passe d'armes est déjà terminée!")
}
}
/* -------------------------------------------- */
afficherOptionsDefense(attackerRoll, essais) {
ui.notifications.info("La chance n'est pas avec vous");
this._sendMessageDefense(attackerRoll, essais);
}
/* -------------------------------------------- */
removeChatMessageActionsPasseArme(passeArme) {
if (game.settings.get("foundryvtt-reve-de-dragon", "supprimer-dialogues-combat-chat")) {
ChatUtility.removeMyChatMessageContaining(`<div data-passearme="${passeArme}">`);
}
}
/* -------------------------------------------- */
static isEchec(rollData) {
switch (rollData.surprise) {
case 'totale': return true;
}
return rollData.rolled.isEchec;
}
/* -------------------------------------------- */
static isEchecTotal(rollData) {
if (!rollData.attackerRoll && rollData.surprise) {
return rollData.rolled.isEchec;
}
return rollData.rolled.isETotal;
}
/* -------------------------------------------- */
static isParticuliere(rollData) {
if (!rollData.attackerRoll && rollData.surprise) {
return false;
}
return rollData.rolled.isPart;
}
/* -------------------------------------------- */
static isReussite(rollData) {
switch (rollData.surprise) {
case 'totale': return false;
}
return rollData.rolled.isSuccess;
}
/* -------------------------------------------- */
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',
options: { height: 540 }
}, {
name: 'jet-attaque',
label: 'Attaque: ' + (arme?.name ?? competence.name),
callbacks: [
this.attacker.createCallbackExperience(),
{ action: r => this.removeChatMessageActionsPasseArme(r.passeArme) },
{ condition: r => (RdDCombat.isReussite(r) && !RdDCombat.isParticuliere(r)), action: r => this._onAttaqueNormale(r) },
{ condition: RdDCombat.isParticuliere, action: r => this._onAttaqueParticuliere(r) },
{ condition: RdDCombat.isEchec, action: r => this._onAttaqueEchec(r) },
{ condition: RdDCombat.isEchecTotal, action: r => this._onAttaqueEchecTotal(r) },
]
});
dialog.render(true);
}
/* -------------------------------------------- */
_prepareAttaque(competence, arme) {
let rollData = {
passeArme: randomID(16),
coupsNonMortels: false,
competence: competence,
surprise: this.attacker.getSurprise(),
surpriseDefenseur: this.defender.getSurprise(),
essais: {}
};
if (this.attacker.isCreature()) {
RdDItemCompetence.setRollDataCreature(rollData);
}
else if (arme) {
// Usual competence
rollData.arme = RdDItemArme.armeUneOuDeuxMains(arme, RdDItemCompetence.isArmeUneMain(competence));
}
else {
// sans armes: à mains nues
rollData.arme = RdDItemArme.mainsNues({ niveau: competence.data.niveau });
}
return rollData;
}
/* -------------------------------------------- */
async _onAttaqueParticuliere(rollData) {
game.system.rdd.combatStore.attaques[this.attackerId] = duplicate(rollData);
// Finesse et Rapidité seulement en mêlée et si la difficulté libre est de -1 minimum
const isMeleeDiffNegative = rollData.selectedCarac.label == "Mêlée" && rollData.diffLibre < 0;
ChatMessage.create({
whisper: ChatMessage.getWhisperRecipients(this.attacker.name),
content: await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/chat-demande-defense.html', {
attackerId: this.attackerId,
defenderTokenId: this.defenderTokenId,
isFinesse: isMeleeDiffNegative,
isRapide: isMeleeDiffNegative && rollData.arme.data.rapide
})
});
}
/* -------------------------------------------- */
async _onAttaqueNormale(attackerRoll) {
console.log("RdDCombat.onAttaqueNormale >>>", attackerRoll);
attackerRoll.dmg = RdDBonus.dmg(attackerRoll, this.attacker.getBonusDegat(), this.defender.isEntiteCauchemar());
// Save rollData for defender
game.system.rdd.combatStore.attaques[this.attackerId] = duplicate(attackerRoll);
attackerRoll.show = {
cible: this.target ? this.defender.data.name : 'la cible',
isRecul: (attackerRoll.particuliere == 'force' || attackerRoll.tactique == 'charge')
}
await RdDResolutionTable.displayRollData(attackerRoll, this.attacker, 'chat-resultat-attaque.html');
if (!await this.accorderEntite('avant-defense')) {
return;
}
if (this.target) {
await this._sendMessageDefense(attackerRoll);
}
}
/* -------------------------------------------- */
async _sendMessageDefense(attackerRoll, essais = {}) {
console.log("RdDCombat._sendMessageDefense", attackerRoll, essais, " / ", this.attacker, this.target, this.attackerId, attackerRoll.competence.data.categorie);
this.removeChatMessageActionsPasseArme(attackerRoll.passeArme);
mergeObject(attackerRoll.essais, essais, { overwrite: true });
const paramDemandeDefense = {
passeArme: attackerRoll.passeArme,
essais: attackerRoll.essais,
surprise: this.defender.getSurprise(),
defender: this.defender,
attackerId: this.attackerId,
defenderTokenId: this.defenderTokenId,
mainsNues: attackerRoll.dmg.mortalite != 'mortel' && this.defender.getCompetence("Corps à corps"),
armes: this._filterArmesParade(this.defender.data.items, attackerRoll.competence, attackerRoll.arme),
diffLibre: attackerRoll.ajustements?.diffLibre?.value ?? 0,
dmg: attackerRoll.dmg
};
let message = await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/chat-demande-defense.html', paramDemandeDefense);
RdDCombat._sendRollMessage(this.attacker, this.defender, this.defenderTokenId, "msg_defense", message, attackerRoll);
}
/* -------------------------------------------- */
_filterArmesParade(items, competence) {
items = items.filter(item => (item.type == 'arme' && item.data.equipe) || (item.type == 'competencecreature' && item.data.isparade));
switch (competence.data.categorie) {
case 'tir':
case 'lancer':
return items.filter(item => RdDItemArme.getCategorieParade(item) == 'boucliers')
default:
// Le fléau ne peut être paré quau bouclier p115
if (competence.name == "Fléau") {
return items.filter(item => RdDItemArme.getCategorieParade(item) == 'boucliers')
}
return items.filter(item => RdDItemArme.getCategorieParade(item));
}
}
/* -------------------------------------------- */
async _onAttaqueEchecTotal(attackerRoll) {
game.system.rdd.combatStore.attaques[this.attackerId] = duplicate(attackerRoll);
// Finesse et Rapidité seulement en mêlée et si la difficulté libre est de -1 minimum
ChatMessage.create({
whisper: ChatMessage.getWhisperRecipients(this.attacker.name),
content: await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/chat-demande-attaque-etotal.html', {
attackerId: this.attackerId,
attacker: this.attacker,
defenderTokenId: this.defenderTokenId,
essais: attackerRoll.essais
})
});
}
/* -------------------------------------------- */
async _onEchecTotal(rollData) {
console.log("RdDCombat._onEchecTotal >>>", rollData);
const arme = rollData.arme;
const avecArme = arme?.data.categorie_parade != 'sans-armes';
const action = (rollData.attackerRoll ? (arme ? "la parade" : "l'esquive") : "l'attaque");
ChatUtility.createChatWithRollMode(this.defender.name, {
content: `<strong>Echec total à ${action}!</strong> ` + await RdDRollTables.getMaladresse({ arme: avecArme })
});
}
/* -------------------------------------------- */
async _onAttaqueEchec(rollData) {
console.log("RdDCombat.onAttaqueEchec >>>", rollData);
await RdDResolutionTable.displayRollData(rollData, this.attacker, 'chat-resultat-attaque.html');
}
/* -------------------------------------------- */
async choixParticuliere(rollData, choix) {
console.log("RdDCombat.choixParticuliere >>>", rollData, choix);
// TODO
rollData.particuliere = choix;
await this._onAttaqueNormale(rollData);
}
/* -------------------------------------------- */
async parade(attackerRoll, armeParadeId) {
let arme = RdDItemArme.getArmeData(armeParadeId ? this.defender.getOwnedItem(armeParadeId) : null);
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',
options: { height: 540 }
}, {
name: 'jet-parade',
label: 'Parade: ' + (arme ? arme.name : rollData.competence.name),
callbacks: [
this.defender.createCallbackExperience(),
{ action: r => this.removeChatMessageActionsPasseArme(r.passeArme) },
{ condition: RdDCombat.isReussite, action: r => this._onParadeNormale(r) },
{ condition: RdDCombat.isParticuliere, action: r => this._onParadeParticuliere(r) },
{ condition: RdDCombat.isEchec, action: r => this._onParadeEchec(r) },
]
});
dialog.render(true);
}
_prepareParade(attackerRoll, armeParade) {
const isCreature = this.defender.isCreature();
const compName = armeParade.data.competence;
const armeAttaque = attackerRoll.arme;
let rollData = {
passeArme: attackerRoll.passeArme,
forceValue: this.defender.getForceValue(),
diffLibre: attackerRoll.diffLibre,
attackerRoll: attackerRoll,
competence: this.defender.getCompetence(compName),
arme: armeParade,
surprise: this.defender.getSurprise(),
needParadeSignificative: RdDItemArme.needParadeSignificative(armeAttaque, armeParade),
needResist: RdDItemArme.needArmeResist(armeAttaque, armeParade),
carac: this.defender.data.data.carac,
show: {}
};
rollData.diviseur = this._getDiviseurSignificative(rollData);
if (isCreature) {
RdDItemCompetence.setRollDataCreature(rollData);
}
return rollData;
}
/* -------------------------------------------- */
_getDiviseurSignificative(defenderRoll) {
let facteurSign = (this.defender.isDemiSurprise() || defenderRoll.needParadeSignificative) ? 2 : 1;
if (RdDBonus.isDefenseAttaqueFinesse(defenderRoll)) {
facteurSign *= 2;
}
return facteurSign;
}
/* -------------------------------------------- */
_onParadeParticuliere(defenderRoll) {
console.log("RdDCombat._onParadeParticuliere >>>", defenderRoll);
if (!defenderRoll.attackerRoll.isPart) {
// TODO: attaquant doit jouer résistance et peut être désarmé p132
ChatUtility.createChatWithRollMode(this.defender.name, {
content: `(à gérer) L'attaquant doit jouer résistance et peut être désarmé (p132)`
});
}
}
/* -------------------------------------------- */
async _onParadeNormale(defenderRoll) {
console.log("RdDCombat._onParadeNormale >>>", defenderRoll);
this._consumeDefense(defenderRoll.passeArme);
await this.computeRecul(defenderRoll);
await this.computeDeteriorationArme(defenderRoll);
await RdDResolutionTable.displayRollData(defenderRoll, this.defender, 'chat-resultat-parade.html');
}
/* -------------------------------------------- */
async _onParadeEchec(defenderRoll) {
console.log("RdDCombat._onParadeEchec >>>", defenderRoll);
await RdDResolutionTable.displayRollData(defenderRoll, this.defender, 'chat-resultat-parade.html');
this.removeChatMessageActionsPasseArme(defenderRoll.passeArme);
this._sendMessageDefense(defenderRoll.attackerRoll, { defense: true });
this._storeDefense(defenderRoll);
}
/* -------------------------------------------- */
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(),
{ action: r => this.removeChatMessageActionsPasseArme(r.passeArme) },
{ condition: RdDCombat.isReussite, action: r => this._onEsquiveNormale(r) },
{ condition: RdDCombat.isParticuliere, action: r => this._onEsquiveParticuliere(r) },
{ condition: RdDCombat.isEchec, action: r => this._onEsquiveEchec(r) },
]
});
dialog.render(true);
}
/* -------------------------------------------- */
_prepareEsquive(attackerRoll, competence) {
let rollData = {
passeArme: attackerRoll.passeArme,
forceValue: this.defender.getForceValue(),
diffLibre: attackerRoll.diffLibre,
attackerRoll: attackerRoll,
competence: competence,
surprise: this.defender.getSurprise(),
surpriseDefenseur: this.defender.getSurprise(),
carac: this.defender.data.data.carac,
show: {}
};
rollData.diviseur = this._getDiviseurSignificative(rollData);
if (this.defender.isCreature()) {
RdDItemCompetence.setRollDataCreature(rollData);
}
return rollData;
}
/* -------------------------------------------- */
_onEsquiveParticuliere(rollData) {
console.log("RdDCombat._onEsquiveParticuliere >>>", rollData);
ChatUtility.createChatWithRollMode(this.defender.name, {
content: "<strong>Vous pouvez esquiver une deuxième esquive!</strong>"
});
}
/* -------------------------------------------- */
async _onEsquiveNormale(rollData) {
console.log("RdDCombat._onEsquiveNormal >>>", rollData);
this._consumeDefense(rollData.passeArme);
await RdDResolutionTable.displayRollData(rollData, this.defender, 'chat-resultat-esquive.html');
}
/* -------------------------------------------- */
async _onEsquiveEchec(rollData) {
console.log("RdDCombat._onEsquiveEchec >>>", rollData);
await RdDResolutionTable.displayRollData(rollData, this.defender, 'chat-resultat-esquive.html');
this.removeChatMessageActionsPasseArme(rollData.passeArme);
this._sendMessageDefense(rollData.attackerRoll, { defense: true })
this._storeDefense(rollData);
}
/* -------------------------------------------- */
async computeDeteriorationArme(rollData) {
const attackerRoll = rollData.attackerRoll;
// Est-ce une parade normale?
if (rollData.arme && attackerRoll && !rollData.rolled.isPart) {
// Est-ce que l'attaque est une particulière en force ou une charge
if (rollData.needResist || attackerRoll.particuliere == 'force' || attackerRoll.tactique == 'charge') {
rollData.show = rollData.show || {}
const dmg = attackerRoll.dmg.dmgArme + attackerRoll.dmg.dmgActor;
let resistance = Misc.toInt(rollData.arme.data.resistance);
let msg = "";
// Jet de résistance de l'arme de parade (p.132)
let resistRoll = await RdDResolutionTable.rollData({
caracValue: resistance,
finalLevel: - dmg,
showDice: false
});
if (resistRoll.rolled.isSuccess) { // Perte de résistance
rollData.show.deteriorationArme = 'resiste';
} else {
resistance -= dmg;
if (resistance <= 0) {
this.defender.deleteEmbeddedEntity("OwnedItem", rollData.arme._id);
rollData.show.deteriorationArme = 'brise';
} else {
this.defender.updateEmbeddedEntity("OwnedItem", { _id: rollData.arme._id, 'data.resistance': resistance });
rollData.show.deteriorationArme = 'perte';
rollData.show.perteResistance = dmg;
}
}
// Si l'arme de parade n'est pas un bouclier, jet de désarmement (p.132)
if (resistance > 0 && !RdDItemArme.getCategorieParade(rollData.arme) == 'boucliers') {
let desarme = await RdDResolutionTable.rollData({
caracValue: this.defender.data.data.carac.force.value,
finalLevel: Misc.toInt(rollData.competence.data.niveau) - dmg,
showDice: false
});
rollData.show.desarme = desarme.rolled.isEchec;
if (desarme.rolled.isEchec) {
rollData.show.desarme = true;
}
}
}
}
}
/* -------------------------------------------- */
async computeRecul(defenderRoll) { // Calcul du recul (p. 132)
const attackerRoll = defenderRoll.attackerRoll;
if (this._isAttaqueCauseRecul(attackerRoll)) {
let impactRecul = this._computeImpactRecul(attackerRoll);
const agilite = this.defender.isEntiteCauchemar()
? this.defender.data.data.carac.reve.value
: this.defender.data.data.carac.agilite.value;
let rollRecul = await RdDResolutionTable.rollData({ caracValue: 10, finalLevel: impactRecul, showDice: false });
if (rollRecul.isSuccess) {
defenderRoll.show.recul = 'encaisse';
} else if (rollRecul.isETotal) {
defenderRoll.show.recul = 'chute';
}
else {
let chute = await RdDResolutionTable.rollData({ caracValue: agilite, finalLevel: impactRecul, showDice: false });
defenderRoll.show.recul = (chute.isSuccess)
? 'recul'
: 'chute';
}
}
}
_isAttaqueCauseRecul(attaque) {
return attaque.particuliere == 'force' || attaque.tactique == 'charge';
}
_computeImpactRecul(attaque) {
return Misc.toInt(this.defender.data.data.carac.taille.value) - (attaque.forceValue + attaque.arme.data.dommagesReels);
}
/* -------------------------------------------- */
_sendMessageEncaisser(rollData) {
let message = "<strong>" + this.defender.name + "</strong> doit:" + this._buildMessageEncaisser(rollData);
RdDCombat._sendRollMessage(this.attacker, this.defender, this.defenderTokenId, "msg_encaisser", message, rollData);
}
/* -------------------------------------------- */
async encaisser(attackerRoll, defenderTokenId) {
defenderTokenId = defenderTokenId || this.defenderTokenId;
console.log("RdDCombat.encaisser >>>", attackerRoll, defenderTokenId);
let defenderRoll = this._consumeDefense(attackerRoll.passeArme);
if (!defenderRoll) {
defenderRoll = {
attackerRoll: attackerRoll,
show: {}
};
}
else if (RdDCombat.isEchecTotal(defenderRoll)) {
// TODO: echec total!!!
this._onEchecTotal(defenderRoll);
}
if (game.user.isGM) { // Current user is the GM -> direct access
attackerRoll.attackerId = this.attackerId;
attackerRoll.defenderTokenId = defenderTokenId;
await this.computeRecul(defenderRoll);
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) + "<br>",
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;
}
/* -------------------------------------------- */
static async displayActorCombatStatus(combat, actor) {
let data = {
combatId: combat._id,
alias: actor.name,
etatGeneral: actor.getEtatGeneral(),
isSonne: actor.getSonne(),
blessuresStatus: actor.computeResumeBlessure(),
SConst: actor.getSConst(),
actorId: actor.data._id,
isGrave: false,
isCritique: false
}
if (actor.countBlessuresByName("critiques") > 0) { // Pour éviter le cumul grave + critique
data.isCritique = true;
} else if (actor.countBlessuresByName("graves") > 0) {
data.isGrave = true;
}
ChatUtility.createChatWithRollMode(actor.name, {
content: await renderTemplate(`systems/foundryvtt-reve-de-dragon/templates/chat-actor-turn-summary.html`, data)
});
}
}