ChatMessage.create est async, il faut donc de préférence l'appeler avec un await. Des effets secondaires avaient lieu (ordre de messages, updates ultérieurs parfois pas pris en compte)
1411 lines
54 KiB
JavaScript
1411 lines
54 KiB
JavaScript
import { ChatUtility } from "./chat-utility.js";
|
|
import { HIDE_DICE, renderTemplate, SYSTEM_RDD, SYSTEM_SOCKET_ID } from "./constants.js";
|
|
import { Grammar } from "./grammar.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";
|
|
import { ReglesOptionnelles } from "./settings/regles-optionnelles.js";
|
|
import { Targets } from "./targets.js";
|
|
import { RdDEmpoignade } from "./rdd-empoignade.js";
|
|
import { RdDRollResult } from "./rdd-roll-result.js";
|
|
import { EMPOIGNADE, RdDItemArme } from "./item/arme.js";
|
|
import { RdDItemCompetence } from "./item-competence.js";
|
|
import { MAP_PHASE, RdDInitiative } from "./initiative.mjs";
|
|
import RollDialog from "./roll/roll-dialog.mjs";
|
|
import { PART_DEFENSE } from "./roll/roll-part-defense.mjs";
|
|
import { ROLL_TYPE_ATTAQUE, ROLL_TYPE_DEFENSE } from "./roll/roll-constants.mjs";
|
|
import { OptionsAvancees, ROLL_DIALOG_V2 } from "./settings/options-avancees.js";
|
|
import { MappingCreatureArme } from "./item/mapping-creature-arme.mjs";
|
|
import { RollBasicParts } from "./roll/roll-basic-parts.mjs";
|
|
import { Distance } from "./combat/distance.mjs";
|
|
|
|
/* -------------------------------------------- */
|
|
const premierRoundInit = [
|
|
{ pattern: 'hast' },
|
|
{ pattern: 'lance' },
|
|
{ pattern: 'baton' },
|
|
{ pattern: 'doubledragonne' },
|
|
{ pattern: 'esparlongue' },
|
|
{ pattern: 'epeedragonne' },
|
|
{ pattern: 'epeebatarde' },
|
|
{ pattern: 'epeecyane' },
|
|
{ pattern: 'epeesorde' },
|
|
{ pattern: 'grandehache' },
|
|
{ pattern: 'bataille' },
|
|
{ pattern: 'epeegnome' },
|
|
{ pattern: 'masse' },
|
|
{ pattern: 'gourdin' },
|
|
{ pattern: 'fleau' },
|
|
{ pattern: 'dague' },
|
|
{ pattern: 'autre' },
|
|
];
|
|
|
|
/* -------------------------------------------- */
|
|
export class RdDCombatManager extends Combat {
|
|
|
|
static init() {
|
|
/* -------------------------------------------- */
|
|
Hooks.on("getCombatTrackerEntryContext", (html, options) => { RdDCombatManager.pushInitiativeOptions(html, options); });
|
|
Hooks.on("updateCombat", (combat, change, options, userId) => { RdDCombat.onUpdateCombat(combat, change, options, userId) });
|
|
Hooks.on("preDeleteCombat", (combat, html, id) => { combat.onPreDeleteCombat() })
|
|
Hooks.on("deleteCombat", (combat, html, id) => { combat.onDeleteCombat() })
|
|
|
|
for (let i = 0.0; i < premierRoundInit.length; i++) {
|
|
premierRoundInit[i].init = 5.99 - i / 100
|
|
}
|
|
}
|
|
|
|
|
|
static getCombatant(actorId, tokenId) {
|
|
return game.combat?.combatants.find(it => it.actor.id == actorId &&
|
|
it.token.id == tokenId
|
|
)
|
|
}
|
|
|
|
static getRangInitiativeCombatant(actorId, tokenId) {
|
|
const combatant = RdDCombatManager.getCombatant(actorId, tokenId)
|
|
return combatant?.system.init?.rang
|
|
}
|
|
/* -------------------------------------------- */
|
|
async nextRound() {
|
|
await this.finDeRound();
|
|
return await super.nextRound();
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async onPreDeleteCombat() {
|
|
if (Misc.isFirstConnectedGM()) {
|
|
await this.finDeRound({ terminer: true })
|
|
ChatUtility.removeChatMessageContaining(`<div data-combatid="${this.id}" data-combatmessage="actor-turn-summary">`)
|
|
game.messages.filter(m => ChatUtility.getMessageData(m, 'rollData') != undefined && ChatUtility.getMessageData(m, 'rollData') != undefined)
|
|
.forEach(it => it.delete())
|
|
RdDEmpoignade.deleteAllEmpoignades()
|
|
}
|
|
}
|
|
async onDeleteCombat() {
|
|
if (Misc.isFirstConnectedGM()) {
|
|
if (game.combats.size <= 1) {
|
|
game.actors.forEach(actor => actor.resetItemUse())
|
|
}
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async finDeRound(options = { terminer: false }) {
|
|
this.combatants.map(it => RdDCombatManager.getActorCombatant(it, { warning: false }))
|
|
.filter(it => it != undefined)
|
|
.forEach(async actor => {
|
|
await actor.finDeRound(options)
|
|
await actor.resetItemUse()
|
|
})
|
|
}
|
|
|
|
static getActorCombatant(combatant, options = { warning: true }) {
|
|
if (!combatant.actor) {
|
|
if (options.warning) {
|
|
ui.notifications.warn(`Le combatant ${combatant.name} n'est pas associé à un acteur!`)
|
|
}
|
|
return undefined
|
|
}
|
|
else if (!combatant.actor.isActorCombat()) {
|
|
if (options.warning) {
|
|
ui.notifications.warn(`L'acteur ${combatant.name} ne peut pas combattre!`)
|
|
}
|
|
return undefined
|
|
}
|
|
return combatant.actor
|
|
}
|
|
|
|
static bonusArme(arme) {
|
|
return (arme?.system.magique) ? arme.system.ecaille_efficacite : 0
|
|
}
|
|
|
|
static etatGeneral(actor) {
|
|
return actor.getEtatGeneral() ?? 0;
|
|
}
|
|
|
|
|
|
/** ***********************************************************************************
|
|
* @override lance l'initiative de plusieurs combattants (tous/ous les PNJs) en une fois
|
|
*/
|
|
async rollInitiative(ids, messageOptions = {}) {
|
|
console.log(`${game.system.title} | Combat.rollInitiative()`, ids, messageOptions)
|
|
ids = typeof ids === "string" ? [ids] : ids
|
|
Promise.all(ids.map(id => this.rollInitRdD(id, undefined, messageOptions)))
|
|
return this
|
|
}
|
|
|
|
async rollInitRdD(id, formule, messageOptions = {}) {
|
|
const combatant = this.combatants.get(id);
|
|
const actor = RdDCombatManager.getActorCombatant(combatant)
|
|
if (actor) {
|
|
formule = formule ?? RdDCombatManager.getFirstInitRollFormula(actor)
|
|
const init = await RdDInitiative.roll(formule)
|
|
|
|
await this.updateEmbeddedDocuments("Combatant", [{
|
|
_id: combatant._id || combatant.id,
|
|
initiative: init.init, 'system.init': init
|
|
}])
|
|
|
|
// Send a chat message
|
|
let rollMode = messageOptions.rollMode || game.settings.get("core", "rollMode");
|
|
let messageData = foundry.utils.mergeObject({
|
|
speaker: {
|
|
scene: canvas.scene._id,
|
|
actor: combatant.actor?._id,
|
|
token: combatant.token._id,
|
|
alias: combatant.token?.name,
|
|
sound: CONFIG.sounds.dice,
|
|
},
|
|
flavor: `${combatant.token?.name} a une initiative de ${init.value} : ${messageOptions.info}<br>`
|
|
},
|
|
messageOptions);
|
|
init.roll.toMessage(messageData, { rollMode, create: true });
|
|
|
|
RdDCombatManager.processPremierRoundInit();
|
|
}
|
|
return this;
|
|
}
|
|
|
|
static getFirstInitRollFormula(actor) {
|
|
const actions = actor.listActions({ isEquipe: true })
|
|
if (actions.length > 0) {
|
|
const action = actions[0]
|
|
const init = RdDCombatManager.getInitData(actor, action)
|
|
const ajustement = RdDCombatManager.bonusArme(action.arme) + RdDCombatManager.etatGeneral(actor)
|
|
return RdDInitiative.formule(init.phase, init.carac, init.niveau, ajustement);
|
|
}
|
|
|
|
return RdDInitiative.formule(MAP_PHASE['autre'], 10, 0, actor.getEtatGeneral() ?? 0);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
static processPremierRoundInit() {
|
|
// Check if we have the whole init !
|
|
if (Misc.isFirstConnectedGM() && game.combat.current.round == 1) {
|
|
let initMissing = game.combat.combatants.find(it => !it.initiative);
|
|
if (!initMissing) { // Premier round !
|
|
for (let combatant of game.combat.combatants) {
|
|
if (combatant.initiativeData?.arme?.type == "arme") {
|
|
// TODO: get init data premier round
|
|
const action = combatant.initiativeData.arme;
|
|
const fromArme = Grammar.toLowerCaseNoAccentNoSpace(action.system.initpremierround)
|
|
const initData = premierRoundInit.find(it => fromArme.includes(initData.pattern))
|
|
if (initData) {
|
|
let msg = `<h4>L'initiative de ${combatant.actor.getAlias()} a été modifiée !</h4>
|
|
<hr>
|
|
<div>
|
|
Etant donné son ${action.name}, son initative pour ce premier round est désormais de ${initData.init}.
|
|
</div>`
|
|
ChatMessage.create({ content: msg })
|
|
game.combat.setInitiative(combatant._id, initData.init)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
static applyInitiativeCommand(combatantId, command, commandValue) {
|
|
switch (command) {
|
|
case 'delta': return RdDCombatManager.incDecInit(combatantId, commandValue);
|
|
case 'autre': return RdDCombatManager.rollInitiativeAction(combatantId,
|
|
{ name: "Autre action", action: 'autre', system: { initOnly: true, competence: "Autre action" } });
|
|
}
|
|
}
|
|
|
|
static async incDecInit(combatantId, incDecValue) {
|
|
const combatant = game.combat.combatants.get(combatantId)
|
|
if (combatant?.initiative && incDecValue != 0) {
|
|
const value = combatant.system.init.value + incDecValue
|
|
const newInit = combatant.initiative + incDecValue / 100;
|
|
await game.combat.updateEmbeddedDocuments("Combatant", [{
|
|
_id: combatantId,
|
|
initiative: newInit,
|
|
'system.init.value': value,
|
|
'system.init.init': newInit,
|
|
}])
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
static pushInitiativeOptions(html, options) {
|
|
for (let i = 0; i < options.length; i++) {
|
|
let option = options[i]
|
|
if (option.name == 'COMBAT.CombatantReroll') { // Replace !
|
|
option.name = "Sélectionner l'initiative..."
|
|
option.condition = true
|
|
option.icon = '<i class="far fa-question-circle"></i>'
|
|
option.callback = target => {
|
|
RdDCombatManager.displayInitiativeMenu(html, target.data('combatant-id'))
|
|
}
|
|
}
|
|
}
|
|
options = [
|
|
{ name: "Incrémenter initiative", condition: true, icon: '<i class="fa-solid fa-plus"></i>', callback: target => { RdDCombatManager.incDecInit(target.data('combatant-id'), +1); } },
|
|
{ name: "Décrémenter initiative", condition: true, icon: '<i class="fa-solid fa-minus"></i>', callback: target => { RdDCombatManager.incDecInit(target.data('combatant-id'), -1); } }
|
|
].concat(options);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
static async rollInitiativeAction(combatantId, action) {
|
|
const combatant = game.combat.combatants.get(combatantId)
|
|
const actor = RdDCombatManager.getActorCombatant(combatant)
|
|
|
|
if (actor == undefined) { return }
|
|
|
|
const init = RdDCombatManager.getInitData(actor, action)
|
|
const ajustement = RdDCombatManager.bonusArme(actor, action.arme) + RdDCombatManager.etatGeneral(actor)
|
|
const formule = RdDInitiative.formule(init.phase, init.carac, init.niveau, ajustement);
|
|
|
|
await game.combat.rollInitRdD(combatantId, formule, init);
|
|
combatant.initiativeData = { action, formule } // pour reclasser l'init au round 0
|
|
}
|
|
|
|
static getInitData(actor, action) {
|
|
if (actor.getSurprise() == "totale") { return { phase: MAP_PHASE['totale'], info: "Surprise Totale", carac: 0, niveau: 0 } }
|
|
if (actor.getSurprise() == "demi") { return { phase: MAP_PHASE['demi'], info: "Demi Surprise", carac: 0, niveau: 0 } }
|
|
if (action.action == 'autre') { return { phase: MAP_PHASE['autre'], info: "Autre Action", carac: 0, niveau: 0 } }
|
|
if (action.action == 'possession') { return { phase: MAP_PHASE['possession'], info: "Possession", carac: actor.getReveActuel(), niveau: 0 } }
|
|
if (action.action == 'haut-reve') { return { phase: MAP_PHASE['draconic'], info: "Draconic", carac: actor.getReveActuel(), niveau: 0 } }
|
|
|
|
const comp = action.comp
|
|
return {
|
|
phase: RdDInitiative.phaseArme(comp?.system.categorie, action.arme),
|
|
info: action.label,
|
|
carac: actor.getCaracInit(comp),
|
|
niveau: comp?.system.niveau ?? (['(lancer)', '(tir)'].includes(action.main) ? -8 : -6)
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
static displayInitiativeMenu(html, combatantId) {
|
|
const combatant = game.combat.combatants.get(combatantId)
|
|
const actor = RdDCombatManager.getActorCombatant(combatant, { warning: false })
|
|
if (actor) {
|
|
const actions = RdDCombatManager.listActionsActorCombatant(actor)
|
|
// Build the relevant submenu
|
|
const menuItems = actions.map(action => {
|
|
return {
|
|
name: action.system.competence,
|
|
icon: "<i class='fas fa-dice-d6'></i>",
|
|
callback: target => { RdDCombatManager.rollInitiativeAction(combatantId, action) }
|
|
}
|
|
})
|
|
if (menuItems.length > 0) {
|
|
new ContextMenu(html, ".directory-list", menuItems).render();
|
|
}
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
static listActionsActorCombatant(actor) {
|
|
const possessions = actor.listActionsPossessions()
|
|
const actions = possessions.length > 0
|
|
? possessions
|
|
: actor.listActions({ isEquipe: true })
|
|
|
|
return Misc.indexed(actions)
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
export class RdDCombat {
|
|
|
|
/* -------------------------------------------- */
|
|
static onSocketMessage(sockmsg) {
|
|
switch (sockmsg.msg) {
|
|
case "msg_encaisser": return RdDCombat.onMsgEncaisser(sockmsg.data);
|
|
case "msg_defense": return RdDCombat.onMsgDefense(sockmsg.data);
|
|
case "msg_defense_v2": return RdDCombat.onMsgDefenseV2(sockmsg.data);
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
static onUpdateCombat(combat, change, options, userId) {
|
|
if (combat.round != 0 && combat.turns && combat.active) {
|
|
RdDCombat.combatNouveauTour(combat);
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
static combatNouveauTour(combat) {
|
|
if (Misc.isFirstConnectedGM()) {
|
|
let turn = combat.turns.find(t => t.token?.id == combat.current.tokenId);
|
|
if (turn?.actor) {
|
|
// TODO Playaudio for player??
|
|
RdDCombat.displayActorCombatStatus(combat, turn.actor, turn.token);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
static isActive() {
|
|
return true;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
static rddCombatTarget(target, attacker, attackerToken) {
|
|
return new RdDCombat(attacker, attackerToken?.id, target?.actor, target?.id, target)
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
static rddCombatForAttackerAndDefender(attackerId, attackerTokenId, defenderTokenId) {
|
|
const attacker = game.actors.get(attackerId)
|
|
const defenderToken = defenderTokenId ? canvas.tokens.get(defenderTokenId) : undefined
|
|
let defender = defenderToken?.actor;
|
|
let target = undefined
|
|
if (!defenderTokenId || !defender) {
|
|
console.warn(`RdDCombat.rddCombatForAttackerAndDefender: appel avec defenderTokenId ${defenderTokenId} incorrect, ou pas de defender correspondant`);
|
|
target = Targets.getTarget()
|
|
if (!target) {
|
|
return
|
|
}
|
|
defenderTokenId = target.id;
|
|
defender = target.actor;
|
|
if (!defenderTokenId || !defender) {
|
|
return
|
|
}
|
|
}
|
|
return new RdDCombat(attacker, attackerTokenId, defender, defenderTokenId, target)
|
|
}
|
|
|
|
static rddCombatForAttackV2(attackerRoll) {
|
|
return RdDCombat.rddCombatForDefenseV2(RollBasicParts.prepareDefense(attackerRoll))
|
|
}
|
|
|
|
static rddCombatForDefenseV2(defenderRoll) {
|
|
let defenderToken = canvas.tokens.get(defenderRoll.active.tokenId)
|
|
if (defenderToken) {
|
|
return RdDCombat.rddCombatForAttackerAndDefender(defenderRoll.opponent.id, defenderRoll.opponent.tokenId, defenderRoll.active.tokenId)
|
|
}
|
|
}
|
|
|
|
|
|
/* -------------------------------------------- */
|
|
static onMsgEncaisser(msg) {
|
|
let defender = canvas.tokens.get(msg.defenderToken.id)?.actor;
|
|
if (Misc.isOwnerPlayer(defender)) {
|
|
let attackerRoll = msg.attackerRoll;
|
|
let attacker = msg.attackerId ? game.actors.get(msg.attackerId) : undefined;
|
|
defender.encaisserDommages(attackerRoll.dmg, attacker, msg.attackerToken);
|
|
const rddCombat = RdDCombat.rddCombatForAttackerAndDefender(msg.attackerId, msg.attackerToken.id, msg.defenderToken.id);
|
|
rddCombat?.removeChatMessageActionsPasseArme(attackerRoll.passeArme);
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
static onMsgDefense(msg) {
|
|
let defenderToken = canvas.tokens.get(msg.defenderToken.id)
|
|
if (defenderToken && Misc.isFirstConnectedGM()) {
|
|
const rddCombat = RdDCombat.rddCombatForAttackerAndDefender(msg.attackerId, msg.attackerToken.id, msg.defenderToken.id)
|
|
rddCombat?.removeChatMessageActionsPasseArme(msg.paramChatDefense.attackerRoll.passeArme)
|
|
if (msg.defenderRoll.v2) {/* TODO: delete roll V1 */
|
|
RollDialog.loadRollData(msg.paramChatDefense)
|
|
rddCombat?._chatMessageDefenseV2(msg.paramChatDefense)
|
|
} else {
|
|
rddCombat?._chatMessageDefense(msg.paramChatDefense, msg.defenderRoll)
|
|
}
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
static _callJetDeVie(event) {
|
|
let actorId = event.currentTarget.attributes['data-actorId'].value;
|
|
let tokenId = event.currentTarget.attributes['data-tokenId'].value;
|
|
let token = canvas.tokens.get(tokenId)
|
|
const actor = token?.actor ?? game.actors.get(actorId);
|
|
if (actor?.isOwner) {
|
|
actor.jetDeVie();
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
static registerChatCallbacks(html) {
|
|
for (let button of [
|
|
'.button-parade',
|
|
'.button-esquive',
|
|
'.button-encaisser',
|
|
'.particuliere-attaque',
|
|
'.appel-chance-defense',
|
|
'.appel-destinee-defense',
|
|
'.appel-chance-attaque',
|
|
'.appel-destinee-attaque',
|
|
'.echec-total-attaque',
|
|
// '.appel-chance',
|
|
// '.chat-encaissement',
|
|
// '.resister-recul',
|
|
]) {
|
|
$(html).on("click", button, event => {
|
|
const rddCombat = RdDCombat.rddCombatForAttackerAndDefender(
|
|
event.currentTarget.attributes['data-attackerId']?.value,
|
|
event.currentTarget.attributes['data-attackerTokenId']?.value,
|
|
event.currentTarget.attributes['data-defenderTokenId']?.value);
|
|
if (rddCombat) {
|
|
rddCombat.onEvent(button, event);
|
|
event.preventDefault();
|
|
}
|
|
});
|
|
}
|
|
$(html).on("click", 'a.chat-jet-vie', event => {
|
|
event.preventDefault();
|
|
RdDCombat._callJetDeVie(event);
|
|
});
|
|
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
constructor(attacker, attackerTokenId, defender, defenderTokenId, target) {
|
|
this.attacker = attacker
|
|
this.defender = defender
|
|
this.target = target
|
|
this.attackerId = this.attacker.id
|
|
this.defenderId = this.defender.id
|
|
this.attackerTokenId = attackerTokenId
|
|
this.defenderTokenId = defenderTokenId
|
|
this.attackerToken = Targets.getTokenData(attacker, attackerTokenId)
|
|
this.defenderToken = Targets.getTokenData(defender, defenderTokenId, target)
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async onEvent(button, event) {
|
|
const chatMessage = ChatUtility.getChatMessage(event);
|
|
const defenderRoll = ChatUtility.getMessageData(chatMessage, 'rollData');
|
|
const attackerRoll = defenderRoll?.attackerRoll ?? ChatUtility.getMessageData(chatMessage, 'rollData');
|
|
console.log('RdDCombat', attackerRoll, defenderRoll);
|
|
|
|
const armeParadeId = event.currentTarget.attributes['data-armeid']?.value;
|
|
const competence = event.currentTarget.attributes['data-competence']?.value;
|
|
const compId = event.currentTarget.attributes['data-compid']?.value;
|
|
|
|
switch (button) {
|
|
case '.particuliere-attaque': return await this.choixParticuliere(attackerRoll, event.currentTarget.attributes['data-mode'].value);
|
|
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);
|
|
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, defenderRoll),
|
|
() => this.afficherOptionsDefense(attackerRoll, defenderRoll, { defenseChance: true }));
|
|
case '.appel-destinee-attaque': return this.attacker.appelDestinee(
|
|
() => this.attaqueSignificative(attackerRoll));
|
|
case '.appel-destinee-defense': return this.defender.appelDestinee(
|
|
() => this.defenseDestinee(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.significativeRequise(attackerRoll.rolled);
|
|
this.removeChatMessageActionsPasseArme(attackerRoll.passeArme);
|
|
this._onAttaqueNormale(attackerRoll);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
defenseChanceuse(attackerRoll, defenderRoll) {
|
|
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, defenderRoll, attackerRoll.essais);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
defenseDestinee(defenderRoll) {
|
|
if (defenderRoll) {
|
|
ui.notifications.info('Défense significative grâce à la destinée')
|
|
RdDResolutionTable.significativeRequise(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, defenderRoll, essais) {
|
|
ui.notifications.info("La chance n'est pas avec vous");
|
|
this._sendMessageDefense(attackerRoll, defenderRoll, essais);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
removeChatMessageActionsPasseArme(passeArme) {
|
|
if (game.settings.get(SYSTEM_RDD, "supprimer-dialogues-combat-chat")) {
|
|
ChatUtility.removeChatMessageContaining(`<div data-passearme="${passeArme}">`);
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
static isEchec(rollData) {
|
|
switch (rollData.ajustements.surprise.used) {
|
|
case 'totale': return true;
|
|
case 'demi': return !rollData.rolled.isSign;
|
|
}
|
|
return rollData.rolled.isEchec;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
static isEchecTotal(rollData) {
|
|
if (rollData.v2 /* roll V2*/) {
|
|
// TODO: en cas de demi-surprise à l'attaque, tout échec est un echec total.
|
|
// TODO: en cas de demi-surprise en défense, pas de changement à la règle de base
|
|
return rollData.rolled.isETotal
|
|
}
|
|
if (rollData.mode == ROLL_TYPE_ATTAQUE && rollData.surprise == 'demi') {
|
|
// échec normal à l'attaque en demi surprise
|
|
return rollData.rolled.isEchec && rollData.rolled.code != 'notSign'
|
|
}
|
|
return rollData.rolled.isETotal
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
static isParticuliere(rollData) {
|
|
if (rollData.v2 /* roll V2*/) {
|
|
return rollData.rolled.isPart
|
|
}
|
|
if (rollData.attackerRoll || !rollData.ajustements.surprise.used) {
|
|
return rollData.rolled.isPart
|
|
}
|
|
return false
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
static isReussite(rollData) {
|
|
if (rollData.v2 /* roll V2*/) {
|
|
return rollData.rolled.isSuccess
|
|
}
|
|
if (!rollData.ajustements.surprise.used) {
|
|
return rollData.rolled.isSuccess
|
|
}
|
|
switch (rollData.ajustements.surprise.used) {
|
|
case 'totale': return false
|
|
case 'demi': return rollData.rolled.isSign
|
|
}
|
|
return rollData.rolled.isSuccess
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async proposerAjustementTirLancer(rollData) {
|
|
if (['tir', 'lancer'].includes(rollData.competence.system.categorie)) {
|
|
if (this.defender.isEntiteBlurette()) {
|
|
await ChatMessage.create({
|
|
content: `<strong>La cible est une blurette, l'arme à distance sera perdue dans le blurêve`,
|
|
whisper: ChatUtility.getGMs()
|
|
})
|
|
}
|
|
else {
|
|
const defenderToken = canvas.tokens.get(this.defenderTokenId)
|
|
const info = foundry.utils.mergeObject(
|
|
{
|
|
rollData: rollData,
|
|
attacker: _token,
|
|
defender: defenderToken,
|
|
},
|
|
Distance.ajustements(_token, defenderToken, { arme: rollData.arme, main: rollData.competence.system.categorie })
|
|
)
|
|
await ChatMessage.create({
|
|
content: await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/chat-info-distance.hbs', info),
|
|
whisper: ChatUtility.getGMs()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
async attaqueV2(options = undefined) {
|
|
if (!await this.attacker.accorder(this.defender, 'avant-attaque')) {
|
|
return
|
|
}
|
|
const rollData = {
|
|
ids: {
|
|
actorId: this.attackerId,
|
|
actorTokenId: this.attackerTokenId,
|
|
opponentId: this.defender.id,
|
|
opponentTokenId: this.defenderTokenId,
|
|
},
|
|
type: { allowed: ['attaque'], current: 'attaque' },
|
|
selected: {},
|
|
passeArme: foundry.utils.randomID(16),
|
|
}
|
|
if (options) {
|
|
rollData.selected = {
|
|
attaque: {
|
|
comp: { id: options.comp.id },
|
|
arme: { id: options.arme.id },
|
|
main: options.main
|
|
}
|
|
}
|
|
}
|
|
await this.doRollAttaque(rollData)
|
|
}
|
|
|
|
async doRollAttaque(rollData, callbacks = []) {
|
|
return await RollDialog.create(rollData, {
|
|
onRollDone: RollDialog.onRollDoneClose,
|
|
callbacks: [
|
|
async (roll) => await this.onAttaqueV2(roll),
|
|
...callbacks
|
|
]
|
|
})
|
|
}
|
|
|
|
async onAttaqueV2(attackerRoll) {
|
|
if (!this.defender || !attackerRoll.rolled.isSuccess || attackerRoll.particulieres?.length > 1) {
|
|
return
|
|
}
|
|
if (!await this.attacker.accorder(this.defender, 'avant-defense')) {
|
|
return;
|
|
}
|
|
|
|
RollDialog.loadRollData(attackerRoll)
|
|
|
|
const surpriseDefender = this.defender.getSurprise(true);
|
|
const paramChatDefense = {
|
|
attackerRoll: attackerRoll,
|
|
isPossession: this.isPossession(attackerRoll),
|
|
defender: this.defender,
|
|
attacker: this.attacker,
|
|
attackerId: this.attackerId,
|
|
attackerToken: this.attackerToken,
|
|
defenderToken: this.defenderToken,
|
|
surprise: surpriseDefender,
|
|
}
|
|
|
|
if (Misc.isFirstConnectedGM()) {
|
|
await this._chatMessageDefenseV2(paramChatDefense);
|
|
}
|
|
else {
|
|
this._socketSendMessageDefenseV2(paramChatDefense);
|
|
}
|
|
}
|
|
|
|
_socketSendMessageDefenseV2(paramChatDefense) {
|
|
game.socket.emit(SYSTEM_SOCKET_ID, { msg: "msg_defense_v2", data: { paramChatDefense } })
|
|
}
|
|
|
|
static async onMsgDefenseV2(msg) {
|
|
if (Misc.isFirstConnectedGM()) {
|
|
const paramChatDefense = msg.paramChatDefense
|
|
RollBasicParts.restore(paramChatDefense.attackerRoll)
|
|
const rddCombat = RdDCombat.rddCombatForAttackV2(paramChatDefense.attackerRoll)
|
|
rddCombat?.removeChatMessageActionsPasseArme(paramChatDefense.attackerRoll.passeArme)
|
|
await rddCombat?._chatMessageDefenseV2(paramChatDefense)
|
|
}
|
|
}
|
|
|
|
async _chatMessageDefenseV2(paramDemandeDefense) {
|
|
const attackerRoll = paramDemandeDefense.attackerRoll
|
|
RollBasicParts.loadSurprises(attackerRoll)
|
|
attackerRoll.dmg = RdDBonus.dmgRollV2(attackerRoll, attackerRoll.current.attaque)
|
|
|
|
const defenseData = RollBasicParts.prepareDefense(attackerRoll)
|
|
|
|
const choixDefense = await ChatMessage.create({
|
|
// message privé: du défenseur à lui même (et aux GMs)
|
|
speaker: ChatMessage.getSpeaker({ actor: this.defender, token: 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', defenseData)
|
|
});
|
|
// flag pour garder les jets d'attaque/defense
|
|
ChatUtility.setMessageData(choixDefense, 'demande-defense', true)
|
|
ChatUtility.setMessageData(choixDefense, 'rollData', {
|
|
ids: defenseData.ids,
|
|
attackerRoll: RollDialog.saveParts(attackerRoll),
|
|
passeArme: defenseData.passeArme
|
|
})
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async attaque(competence, arme, main) {
|
|
if (!await this.attacker.accorder(this.defender, 'avant-attaque')) {
|
|
return
|
|
}
|
|
if (OptionsAvancees.isUsing(ROLL_DIALOG_V2)) {
|
|
return this.attaqueV2(
|
|
{ comp: competence, arme: arme, main: main }
|
|
)
|
|
}
|
|
if (arme.system.cac == EMPOIGNADE) {
|
|
RdDEmpoignade.onAttaqueEmpoignade(this.attacker, this.defender)
|
|
return
|
|
}
|
|
RdDEmpoignade.checkEmpoignadeEnCours(this.attacker)
|
|
|
|
let rollData = this._prepareAttaque(competence, arme)
|
|
console.log("RdDCombat.attaque >>>", rollData)
|
|
if (arme) {
|
|
await this.attacker.verifierForceMin(arme);
|
|
}
|
|
await this.proposerAjustementTirLancer(rollData)
|
|
|
|
const dialog = await RdDRoll.create(this.attacker, rollData,
|
|
{ html: 'systems/foundryvtt-reve-de-dragon/templates/dialog-roll-competence.hbs' },
|
|
{
|
|
name: 'jet-attaque',
|
|
label: 'Attaque: ' + (arme?.name ?? competence.name),
|
|
callbacks: [
|
|
this.attacker.createCallbackExperience(),
|
|
this.attacker.createCallbackAppelAuMoral(),
|
|
{ action: r => this.removeChatMessageActionsPasseArme(r.passeArme) },
|
|
{ action: async r => await this.attacker.incDecItemUse(arme._id, arme && !RdDCombat.isParticuliere(r)) },
|
|
{ action: r => this._onAttaque(r) },
|
|
]
|
|
});
|
|
dialog.render(true);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
_prepareAttaque(competence, arme) {
|
|
let rollData = {
|
|
mode: 'attaque',
|
|
alias: this.attacker?.getAlias(),
|
|
passeArme: foundry.utils.randomID(16),
|
|
mortalite: arme?.system.mortalite,
|
|
competence: competence,
|
|
surprise: this.attacker.getSurprise(true),
|
|
surpriseDefenseur: this.defender.getSurprise(true),
|
|
sourceToken: this.attackerToken,
|
|
targetToken: this.defenderToken,
|
|
essais: {}
|
|
};
|
|
|
|
if (this.attacker.isCreatureOuEntite()) {
|
|
MappingCreatureArme.setRollDataCreature(rollData);
|
|
}
|
|
else if (arme) {
|
|
// Usual competence
|
|
rollData.arme = RdDItemArme.armeUneOuDeuxMains(arme, RdDItemCompetence.isArmeUneMain(competence));
|
|
}
|
|
else {
|
|
// sans armes: à mains nues
|
|
rollData.arme = RdDItemArme.pugilat(this.attacker)
|
|
rollData.arme.system.niveau = competence.system.niveau
|
|
rollData.arme.system.initiative = RdDInitiative.getRollInitiative(this.attacker.system.carac['melee'].value, competence.system.niveau);
|
|
}
|
|
return rollData;
|
|
}
|
|
|
|
async _onAttaque(attackerRoll) {
|
|
if (RdDCombat.isParticuliere(attackerRoll)) {
|
|
return await this._onAttaqueParticuliere(attackerRoll)
|
|
}
|
|
if (RdDCombat.isReussite(attackerRoll)) {
|
|
return await this._onAttaqueNormale(attackerRoll)
|
|
}
|
|
if (RdDCombat.isEchecTotal(attackerRoll)) {
|
|
return await this._onAttaqueEchecTotal(attackerRoll)
|
|
}
|
|
return await this._onAttaqueEchec(attackerRoll)
|
|
}
|
|
/* -------------------------------------------- */
|
|
async _onAttaqueParticuliere(rollData) {
|
|
const isMeleeDiffNegative = (rollData.competence.type == 'competencecreature' || rollData.selectedCarac.label == "Mêlée") && rollData.diffLibre < 0;
|
|
// force toujours, sauf empoignade
|
|
// finesse seulement en mélée, pour l'empoignade, ou si la difficulté libre est de -1 minimum
|
|
// rapidité seulement en mêlée, si l'arme le permet, et si la difficulté libre est de -1 minimum
|
|
const isForce = !rollData.arme.system.empoignade;
|
|
const isFinesse = rollData.tactique != 'charge' && (rollData.arme.system.empoignade || isMeleeDiffNegative);
|
|
const isRapide = rollData.tactique != 'charge' && !rollData.arme.system.empoignade && isMeleeDiffNegative && rollData.arme.system.rapide;
|
|
// si un seul choix possible, le prendre
|
|
if (isForce && !isFinesse && !isRapide) {
|
|
return await this.choixParticuliere(rollData, "force");
|
|
}
|
|
else if (!isForce && isFinesse && !isRapide) {
|
|
return await this.choixParticuliere(rollData, "finesse");
|
|
}
|
|
else if (!isForce && !isFinesse && isRapide) {
|
|
return await this.choixParticuliere(rollData, "rapidite");
|
|
}
|
|
|
|
const choixParticuliere = await ChatMessage.create({
|
|
alias: this.attacker.getAlias(),
|
|
whisper: ChatUtility.getOwners(this.attacker),
|
|
content: await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/chat-demande-attaque-particuliere.hbs', {
|
|
alias: this.attacker.getAlias(),
|
|
attackerId: this.attackerId,
|
|
attackerToken: this.attackerToken,
|
|
defenderToken: this.defenderToken,
|
|
isForce: isForce,
|
|
isFinesse: isFinesse,
|
|
isRapide: isRapide,
|
|
passeArme: rollData.passeArme
|
|
})
|
|
});
|
|
ChatUtility.setMessageData(choixParticuliere, 'rollData', rollData);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async _onAttaqueNormale(attackerRoll) {
|
|
console.log("RdDCombat.onAttaqueNormale >>>", attackerRoll);
|
|
|
|
attackerRoll.dmg = RdDBonus.dmg(attackerRoll, this.attacker);
|
|
let defenderRoll = { attackerRoll: attackerRoll, passeArme: attackerRoll.passeArme, show: {} }
|
|
attackerRoll.show = {
|
|
cible: this.defender?.getAlias() ?? 'la cible',
|
|
isRecul: (attackerRoll.particuliere == 'force' || attackerRoll.tactique == 'charge')
|
|
}
|
|
await RdDRollResult.displayRollData(attackerRoll, this.attacker, 'chat-resultat-attaque.hbs');
|
|
|
|
if (!await this.attacker.accorder(this.defender, 'avant-defense')) {
|
|
return;
|
|
}
|
|
|
|
if (this.defender) {
|
|
await this._sendMessageDefense(attackerRoll, defenderRoll);
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
isPossession(attackerRoll) {
|
|
const carac = attackerRoll.v2
|
|
? attackerRoll.current.carac?.label
|
|
: attackerRoll.selectedCarac.label
|
|
return carac?.toLowerCase() == 'possession';
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async _sendMessageDefense(attackerRoll, defenderRoll, essaisPrecedents = undefined) {
|
|
this.removeChatMessageActionsPasseArme(attackerRoll.passeArme);
|
|
if (essaisPrecedents) {
|
|
foundry.utils.mergeObject(attackerRoll.essais, essaisPrecedents, { overwrite: true });
|
|
}
|
|
|
|
// # utilisation esquive
|
|
const corpsACorps = this.defender.getCompetenceCorpsACorps({ onMessage: it => console.info(it, this.defender) });
|
|
const esquives = foundry.utils.duplicate(this.defender.getCompetencesEsquive())
|
|
esquives.forEach(e => e.nbUsage = e?._id ? this.defender.getItemUse(e._id) : 0);
|
|
|
|
const paramChatDefense = {
|
|
passeArme: attackerRoll.passeArme,
|
|
essais: attackerRoll.essais,
|
|
isPossession: this.isPossession(attackerRoll),
|
|
defender: this.defender,
|
|
attacker: this.attacker,
|
|
attackerId: this.attackerId,
|
|
esquives: esquives,
|
|
attackerToken: this.attackerToken,
|
|
defenderToken: this.defenderToken,
|
|
mainsNues: attackerRoll.dmg.mortalite != 'mortel' && corpsACorps,
|
|
armes: this._filterArmesParade(this.defender, attackerRoll.competence, attackerRoll.arme),
|
|
diffLibre: attackerRoll.ajustements?.diffLibre?.value ?? 0,
|
|
attaqueParticuliere: attackerRoll.particuliere,
|
|
attaqueCategorie: attackerRoll.competence.system.categorie,
|
|
attaqueArme: attackerRoll.arme,
|
|
surprise: this.defender.getSurprise(true),
|
|
dmg: attackerRoll.dmg,
|
|
};
|
|
|
|
if (Misc.isFirstConnectedGM()) {
|
|
await this._chatMessageDefense(paramChatDefense, defenderRoll);
|
|
}
|
|
else {
|
|
this._socketSendMessageDefense(paramChatDefense, defenderRoll);
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async _chatMessageDefense(paramDemandeDefense, defenderRoll) {
|
|
const choixDefense = await ChatMessage.create({
|
|
// message privé: du défenseur à lui même (et aux GMs)
|
|
speaker: ChatMessage.getSpeaker({ actor: this.defender, token: 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-v1.hbs', paramDemandeDefense),
|
|
});
|
|
// flag pour garder les jets d'attaque/defense
|
|
ChatUtility.setMessageData(choixDefense, 'rollData', defenderRoll);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
_socketSendMessageDefense(paramChatDefense, defenderRoll) {
|
|
// envoyer le message au destinataire
|
|
game.socket.emit(SYSTEM_SOCKET_ID, {
|
|
msg: "msg_defense", data: {
|
|
attackerId: this.attacker?.id,
|
|
attackerToken: this.attackerToken,
|
|
defenderId: this.defender?.id,
|
|
defenderToken: this.defenderToken,
|
|
defenderRoll: defenderRoll,
|
|
paramChatDefense: paramChatDefense,
|
|
rollMode: true,
|
|
}
|
|
});
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
_filterArmesParade(defender, competence, armeAttaque) {
|
|
let defenses = defender.items.filter(it => it.isParade())
|
|
defenses = foundry.utils.duplicate(defenses)
|
|
defenses.forEach(armeDefense => {
|
|
// Ajout du # d'utilisation ce round
|
|
armeDefense.nbUsage = defender.getItemUse(armeDefense.id)
|
|
armeDefense.typeParade = RdDItemArme.defenseArmeParade(armeAttaque, armeDefense)
|
|
})
|
|
|
|
switch (competence.system.categorie) {
|
|
case 'tir':
|
|
case 'lancer':
|
|
return defenses.filter(armeDefense => RdDItemArme.getCategorieParade(armeDefense) == 'boucliers')
|
|
default:
|
|
return defenses.filter(armeDefense => armeDefense.typeParade != '')
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async _onAttaqueEchecTotal(attackerRoll) {
|
|
const choixEchecTotal = await ChatMessage.create({
|
|
whisper: ChatUtility.getOwners(this.attacker),
|
|
content: await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/chat-demande-attaque-etotal.hbs', {
|
|
rolled: attackerRoll.rolled,
|
|
attackerId: this.attackerId,
|
|
attacker: this.attacker,
|
|
attackerToken: this.attackerToken,
|
|
defenderToken: this.defenderToken,
|
|
essais: attackerRoll.essais
|
|
})
|
|
});
|
|
ChatUtility.setMessageData(choixEchecTotal, 'rollData', attackerRoll);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async _onEchecTotal(rollData) {
|
|
console.log("RdDCombat._onEchecTotal >>>", rollData);
|
|
|
|
const arme = rollData.arme;
|
|
const avecArme = !['', 'sans-armes', 'armes-naturelles'].includes(arme?.system.categorie_parade ?? '');
|
|
const action = (rollData.attackerRoll ? (arme ? "la parade" : "l'esquive") : "l'attaque");
|
|
await ChatMessage.create(ChatUtility.adaptVisibility(
|
|
{ content: `<strong>Maladresse à ${action}!</strong> ` + await RdDRollTables.getMaladresse({ arme: avecArme }) },
|
|
{ actor: this.defender }))
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async _onAttaqueEchec(attackerRoll) {
|
|
console.log("RdDCombat.onAttaqueEchec >>>", attackerRoll);
|
|
await RdDRollResult.displayRollData(attackerRoll, this.attacker, 'chat-resultat-attaque.hbs');
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async choixParticuliere(rollData, choix) {
|
|
console.log("RdDCombat.choixParticuliere >>>", rollData, choix);
|
|
|
|
await this.attacker.incDecItemUse(rollData.arme.id, choix != "rapidite")
|
|
|
|
this.removeChatMessageActionsPasseArme(rollData.passeArme);
|
|
rollData.particuliere = choix;
|
|
await this._onAttaqueNormale(rollData)
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async parade(attackerRoll, armeParadeId) {
|
|
const arme = this.defender.getArmeParade(armeParadeId);
|
|
console.log("RdDCombat.parade >>>", attackerRoll, armeParadeId, arme);
|
|
const competence = arme?.system?.competence;
|
|
if (competence == undefined) {
|
|
console.error("Pas de compétence de parade associée à ", arme?.name, armeParadeId);
|
|
return;
|
|
}
|
|
|
|
let rollData = this._prepareParade(attackerRoll, arme, competence);
|
|
|
|
const dialog = await RdDRoll.create(this.defender, rollData,
|
|
{ html: 'systems/foundryvtt-reve-de-dragon/templates/dialog-roll-competence.hbs' },
|
|
{
|
|
name: 'jet-parade',
|
|
label: 'Parade: ' + (arme ? arme.name : rollData.competence.name),
|
|
callbacks: [
|
|
this.defender.createCallbackExperience(),
|
|
this.defender.createCallbackAppelAuMoral(),
|
|
{ action: r => this.removeChatMessageActionsPasseArme(r.passeArme) },
|
|
{ action: async r => await this.defender.incDecItemUse(armeParadeId, !RdDCombat.isParticuliere(r)) },
|
|
{ action: r => this._onParade(r) },
|
|
]
|
|
});
|
|
dialog.render(true);
|
|
}
|
|
|
|
async defenseV2(attackerRoll, callbacks = []) {
|
|
|
|
await this.doRollDefense({
|
|
ids: {
|
|
actorId: this.defender.id,
|
|
actorTokenId: this.defenderTokenId,
|
|
opponentTokenId: this.attackerTokenId,
|
|
opponentId: this.attackerId,
|
|
},
|
|
type: { allowed: [ROLL_TYPE_DEFENSE], current: ROLL_TYPE_DEFENSE },
|
|
attackerRoll: attackerRoll,
|
|
passeArme: attackerRoll.passeArme,
|
|
}, callbacks)
|
|
}
|
|
|
|
async doRollDefense(rollData, callbacks = []) {
|
|
return await RollDialog.create(rollData, {
|
|
onRollDone: RollDialog.onRollDoneClose,
|
|
callbacks: [
|
|
async (roll) => {
|
|
this.removeChatMessageActionsPasseArme(roll.passeArme);
|
|
// defense: esquive / arme de parade / competence de défense
|
|
if (!RdDCombat.isParticuliere(roll)) {
|
|
await roll.active.actor.incDecItemUse(roll.current[PART_DEFENSE].defense?.id);
|
|
}
|
|
await this._onDefense(roll);
|
|
},
|
|
...callbacks
|
|
]
|
|
})
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
_prepareParade(attackerRoll, armeParade, competenceParade) {
|
|
let defenderRoll = {
|
|
mode: ROLL_TYPE_DEFENSE,
|
|
alias: this.defender?.getAlias(),
|
|
passeArme: attackerRoll.passeArme,
|
|
diffLibre: attackerRoll.diffLibre,
|
|
attackerToken: this.attackerToken,
|
|
defenderToken: this.defenderToken,
|
|
attackerRoll: attackerRoll,
|
|
competence: this.defender.getCompetence(competenceParade),
|
|
arme: armeParade,
|
|
surprise: this.defender.getSurprise(true),
|
|
needParadeSignificative: ReglesOptionnelles.isUsing('categorieParade') && RdDItemArme.needParadeSignificative(attackerRoll.arme, armeParade),
|
|
needResist: RdDItemArme.needArmeResist(attackerRoll.arme, armeParade),
|
|
carac: this.defender.system.carac,
|
|
show: {}
|
|
};
|
|
|
|
if (this.defender.isCreatureOuEntite()) {
|
|
MappingCreatureArme.setRollDataCreature(defenderRoll);
|
|
}
|
|
|
|
return defenderRoll;
|
|
}
|
|
|
|
async _onDefense(rollData) {
|
|
const isEsquive = rollData.current[PART_DEFENSE].isEsquive
|
|
const isParade = !isEsquive
|
|
if (RdDCombat.isReussite(rollData)) {
|
|
if (isParade) {
|
|
await this.computeDeteriorationArme(rollData)
|
|
if (RdDCombat.isParticuliere(rollData)) {
|
|
await this.infoAttaquantDesarme(rollData)
|
|
}
|
|
}
|
|
|
|
}
|
|
this.removeChatMessageActionsPasseArme(rollData.passeArme)
|
|
}
|
|
|
|
async infoAttaquantDesarme(rollData) {
|
|
if (/*TODO: parade?*/!rollData.attackerRoll?.particuliere) {
|
|
// TODO: attaquant doit jouer résistance et peut être désarmé p132
|
|
await ChatMessage.create(ChatUtility.adaptVisibility(
|
|
{ content: `(à gérer) L'attaquant doit jouer résistance et peut être désarmé (p132)` },
|
|
{ actor: this.defender }))
|
|
}
|
|
}
|
|
|
|
async _onDefenseNormale(rollData) {
|
|
console.log("RdDCombat._onDefenseNormale >>>", rollData);
|
|
await this.computeRecul(rollData);
|
|
await this.computeDeteriorationArme(rollData);
|
|
await RdDRollResult.displayRollData(rollData, this.defender, 'chat-resultat-parade.hbs');
|
|
this.removeChatMessageActionsPasseArme(rollData.passeArme);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async _onParade(defenderRoll) {
|
|
if (RdDCombat.isReussite(defenderRoll)) {
|
|
await this._onParadeNormale(defenderRoll)
|
|
if (RdDCombat.isParticuliere(defenderRoll)) {
|
|
await this._onParadeParticuliere(defenderRoll)
|
|
}
|
|
return
|
|
}
|
|
await this._onParadeEchec(defenderRoll)
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async _onParadeParticuliere(defenderRoll) {
|
|
console.log("RdDCombat._onParadeParticuliere >>>", defenderRoll);
|
|
if (!defenderRoll.attackerRoll.isPart) {
|
|
// TODO: attaquant doit jouer résistance et peut être désarmé p132
|
|
await ChatMessage.create(ChatUtility.adaptVisibility(
|
|
{ content: `(à gérer) L'attaquant doit jouer résistance et peut être désarmé (p132)` },
|
|
{ actor: this.defender }))
|
|
}
|
|
}
|
|
/* -------------------------------------------- */
|
|
async _onParadeNormale(defenderRoll) {
|
|
console.log("RdDCombat._onParadeNormale >>>", defenderRoll);
|
|
|
|
await this.computeRecul(defenderRoll);
|
|
await this.computeDeteriorationArme(defenderRoll);
|
|
await RdDRollResult.displayRollData(defenderRoll, this.defender, 'chat-resultat-parade.hbs');
|
|
this.removeChatMessageActionsPasseArme(defenderRoll.passeArme);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async _onParadeEchec(defenderRoll) {
|
|
console.log("RdDCombat._onParadeEchec >>>", defenderRoll);
|
|
|
|
await RdDRollResult.displayRollData(defenderRoll, this.defender, 'chat-resultat-parade.hbs');
|
|
|
|
this.removeChatMessageActionsPasseArme(defenderRoll.passeArme);
|
|
this._sendMessageDefense(defenderRoll.attackerRoll, defenderRoll, { defense: true });
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async esquive(attackerRoll, compId, compName) {
|
|
const esquive = this.defender.getCompetence(compId) ?? this.defender.getCompetence(compName)
|
|
if (esquive == undefined) {
|
|
ui.notifications.error(this.defender.getAlias() + " n'a pas de compétence " + compName);
|
|
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-roll-competence.hbs' },
|
|
{
|
|
name: 'jet-esquive',
|
|
label: 'Esquiver',
|
|
callbacks: [
|
|
this.defender.createCallbackExperience(),
|
|
this.defender.createCallbackAppelAuMoral(),
|
|
{ action: async r => await this.defender.incDecItemUse(esquive._id, !RdDCombat.isParticuliere(r)) },
|
|
{ action: r => this.removeChatMessageActionsPasseArme(r.passeArme) },
|
|
{ action: r => this._onEsquive(r) },
|
|
]
|
|
});
|
|
dialog.render(true);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
_prepareEsquive(attackerRoll, competence) {
|
|
let rollData = {
|
|
mode: ROLL_TYPE_DEFENSE,
|
|
alias: this.defender.getAlias(),
|
|
passeArme: attackerRoll.passeArme,
|
|
diffLibre: attackerRoll.diffLibre,
|
|
attackerToken: this.attackerToken,
|
|
defenderToken: this.defenderToken,
|
|
attackerRoll: attackerRoll,
|
|
competence: competence,
|
|
surprise: this.defender.getSurprise(true),
|
|
surpriseDefenseur: this.defender.getSurprise(true),
|
|
carac: this.defender.system.carac,
|
|
show: {}
|
|
};
|
|
|
|
if (this.defender.isCreatureOuEntite()) {
|
|
MappingCreatureArme.setRollDataCreature(rollData);
|
|
}
|
|
return rollData;
|
|
}
|
|
|
|
async _onEsquive(defenderRoll) {
|
|
if (RdDCombat.isReussite(defenderRoll)) {
|
|
await this._onEsquiveNormale(defenderRoll)
|
|
if (RdDCombat.isParticuliere(defenderRoll)) {
|
|
await this._onEsquiveParticuliere(defenderRoll)
|
|
}
|
|
return
|
|
}
|
|
return await this._onEsquiveEchec(defenderRoll)
|
|
}
|
|
/* -------------------------------------------- */
|
|
async _onEsquiveParticuliere(defenderRoll) {
|
|
console.log("RdDCombat._onEsquiveParticuliere >>>", defenderRoll);
|
|
await ChatMessage.create(ChatUtility.adaptVisibility(
|
|
{ content: "<strong>Vous pouvez esquiver une deuxième fois!</strong>" },
|
|
{ actor: this.defender }))
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async _onEsquiveNormale(defenderRoll) {
|
|
console.log("RdDCombat._onEsquiveNormal >>>", defenderRoll);
|
|
await RdDRollResult.displayRollData(defenderRoll, this.defender, 'chat-resultat-esquive.hbs');
|
|
this.removeChatMessageActionsPasseArme(defenderRoll.passeArme);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async _onEsquiveEchec(defenderRoll) {
|
|
console.log("RdDCombat._onEsquiveEchec >>>", defenderRoll);
|
|
|
|
await RdDRollResult.displayRollData(defenderRoll, this.defender, 'chat-resultat-esquive.hbs');
|
|
|
|
this.removeChatMessageActionsPasseArme(defenderRoll.passeArme);
|
|
this._sendMessageDefense(defenderRoll.attackerRoll, defenderRoll, { defense: true })
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async computeDeteriorationArme(defenderRoll) {
|
|
if (!ReglesOptionnelles.isUsing('resistanceArmeParade')) {
|
|
return;
|
|
}
|
|
const attackerRoll = defenderRoll.attackerRoll;
|
|
// Est-ce une parade normale?
|
|
if (defenderRoll.arme && attackerRoll && !defenderRoll.rolled.isPart) {
|
|
// Est-ce que l'attaque est une particulière en force ou une charge
|
|
if (defenderRoll.needResist || this._isForceOuCharge(attackerRoll, defenderRoll.v2)) {
|
|
|
|
defenderRoll.show = defenderRoll.show || {}
|
|
|
|
const dmg = attackerRoll.dmg.dmgArme + attackerRoll.dmg.dmgActor;
|
|
let arme = defenderRoll.arme;
|
|
let resistance = Misc.toInt(arme.system.resistance);
|
|
if (arme.system.magique) {
|
|
defenderRoll.show.deteriorationArme = 'resiste'; // Par défaut
|
|
if (arme.system.resistance_magique == undefined) arme.system.resistance_magique = 0; // Quick fix
|
|
if (dmg > arme.system.resistance_magique) { // Jet uniquement si dommages supérieur à résistance magique (cf. 274)
|
|
// Jet de résistance de l'arme de parade (p.132)
|
|
let resistRoll = await RdDResolutionTable.rollData({
|
|
caracValue: resistance,
|
|
finalLevel: - dmg,
|
|
showDice: HIDE_DICE
|
|
});
|
|
if (!resistRoll.rolled.isSuccess) {
|
|
let perteResistance = (dmg - arme.system.resistance_magique)
|
|
resistance -= perteResistance;
|
|
defenderRoll.show.deteriorationArme = resistance <= 0 ? 'brise' : 'perte';
|
|
defenderRoll.show.perteResistance = perteResistance;
|
|
this.defender.updateEmbeddedDocuments('Item', [{ _id: defenderRoll.arme._id, 'system.resistance': resistance }]);
|
|
}
|
|
}
|
|
} else {
|
|
// Jet de résistance de l'arme de parade (p.132)
|
|
let resistRoll = await RdDResolutionTable.rollData({
|
|
caracValue: resistance,
|
|
finalLevel: - dmg,
|
|
showDice: HIDE_DICE
|
|
});
|
|
if (resistRoll.rolled.isSuccess) { // Perte de résistance
|
|
defenderRoll.show.deteriorationArme = 'resiste';
|
|
} else {
|
|
resistance -= dmg;
|
|
defenderRoll.show.deteriorationArme = resistance <= 0 ? 'brise' : 'perte';
|
|
defenderRoll.show.perteResistance = dmg;
|
|
this.defender.updateEmbeddedDocuments('Item', [{ _id: defenderRoll.arme._id, 'system.resistance': resistance }]);
|
|
}
|
|
}
|
|
// Si l'arme de parade n'est pas un bouclier, jet de désarmement (p.132)
|
|
if (ReglesOptionnelles.isUsing('defenseurDesarme') && resistance > 0 && RdDItemArme.getCategorieParade(defenderRoll.arme) != 'boucliers') {
|
|
let desarme = await RdDResolutionTable.rollData({
|
|
caracValue: this.defender.getForce(),
|
|
finalLevel: Misc.toInt(defenderRoll.competence.system.niveau) - dmg,
|
|
showDice: HIDE_DICE
|
|
});
|
|
defenderRoll.show.desarme = desarme.rolled.isEchec
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
async computeRecul(defenderRoll) { // Calcul du recul (p. 132)
|
|
if (!ReglesOptionnelles.isUsing('recul')) {
|
|
return
|
|
}
|
|
const attackerRoll = defenderRoll.attackerRoll;
|
|
if (this._isForceOuCharge(attackerRoll, defenderRoll.v2)) {
|
|
defenderRoll.show.recul = this.defender.encaisserRecul(this.attacker.getForce(), attackerRoll.dmg.dmgArme)
|
|
}
|
|
}
|
|
|
|
_isForceOuCharge(attaque, isRollV2 = false /* TODO: delete roll V1 */) {
|
|
return attaque.particuliere == 'force' || 'charge' == (isRollV2 ? attaque.tactique?.key : attaque.tactique)
|
|
}
|
|
|
|
|
|
/* -------------------------------------------- */
|
|
async encaisser(attackerRoll, defenderRoll) {
|
|
console.log("RdDCombat.encaisser >>>", attackerRoll, defenderRoll);
|
|
|
|
if (defenderRoll?.rolled && RdDCombat.isEchecTotal(defenderRoll)) {
|
|
this._onEchecTotal(defenderRoll);
|
|
}
|
|
|
|
await this.doRollEncaissement(attackerRoll, defenderRoll);
|
|
this.removeChatMessageActionsPasseArme(attackerRoll.passeArme);
|
|
}
|
|
|
|
async doRollEncaissement(attackerRoll, defenderRoll) {
|
|
if (Misc.isOwnerPlayer(this.defender)) {
|
|
attackerRoll.attackerId = this.attackerId;
|
|
attackerRoll.defenderTokenId = this.defenderToken.id;
|
|
await this.computeRecul(defenderRoll);
|
|
await this.defender.encaisserDommages(attackerRoll.dmg, this.attacker, defenderRoll?.show, this.attackerToken, this.defenderToken);
|
|
}
|
|
else { // envoi à un GM: les joueurs n'ont pas le droit de modifier les personnages qu'ils ne possèdent pas
|
|
game.socket.emit(SYSTEM_SOCKET_ID, {
|
|
msg: "msg_encaisser",
|
|
data: {
|
|
attackerId: this.attackerId,
|
|
attackerRoll: attackerRoll,
|
|
attackerToken: this.attackerToken,
|
|
defenderToken: this.defenderToken
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
static async displayActorCombatStatus(combat, actor, token) {
|
|
if (!actor?.isActorCombat()) {
|
|
return
|
|
}
|
|
const alias = token?.name ?? actor.getAlias();
|
|
const blessuresGraves = actor.countBlessures(it => it.isGrave());
|
|
const formData = {
|
|
combatId: combat._id,
|
|
alias: alias,
|
|
etatGeneral: actor.getEtatGeneral(),
|
|
isSonne: actor.isSonne(),
|
|
blessuresStatus: actor.computeResumeBlessure(),
|
|
SConst: actor.getSConst(),
|
|
actorId: actor.id,
|
|
actor: actor,
|
|
tokenId: token.id,
|
|
blessuresGraves: blessuresGraves,
|
|
isCritique: actor.countBlessures(it => it.isCritique()) > 0
|
|
}
|
|
await ChatMessage.create({
|
|
content: await renderTemplate(`systems/foundryvtt-reve-de-dragon/templates/chat-actor-turn-acteur.hbs`, formData),
|
|
alias: alias
|
|
})
|
|
await ChatMessage.create({
|
|
content: await renderTemplate(`systems/foundryvtt-reve-de-dragon/templates/chat-actor-turn-sante.hbs`, formData),
|
|
whisper: ChatUtility.getOwners(actor),
|
|
alias: alias
|
|
})
|
|
}
|
|
}
|
|
|