Files
foundryvtt-reve-de-dragon/module/combat/rdd-combat.js
T
VincentVk d8cfd02807 Regrouper les classes de combat
Elles sont souvent touchées ensemble

Préparation aux changements pour utiliser les Combatants
(pour gérer les empoignades/possessions en cours)

Renommages:
- RdDCombat -> PasseArmes
- RdDCombatManager -> RdDCombat
2026-07-26 22:58:25 +02:00

299 lines
11 KiB
JavaScript

import { ChatUtility } from "../chat-utility.js";
import { Grammar } from "../grammar.js";
import { Misc } from "../misc.js";
import { RdDEmpoignade } from "./rdd-empoignade.js";
import { RdDInitiative } from "./initiative.mjs";
import { PasseArme } from "./passe-arme.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 RdDCombat extends Combat {
static init() {
/* -------------------------------------------- */
Hooks.on("getCombatTrackerEntryContext", (html, options) => { RdDCombat.pushInitiativeOptions(html, options); });
Hooks.on("updateCombat", (combat, change, options, userId) => { PasseArme.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.actorId == actorId &&
it.tokenId == tokenId
)
}
static getRangInitiativeCombatant(actorId, tokenId) {
const combatant = RdDCombat.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 => RdDCombat.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 = RdDCombat.getActorCombatant(combatant)
if (actor) {
formule = formule ?? RdDCombat.getFirstInitRollFormula(actor)
const init = await RdDInitiative.roll(formule)
await this.updateEmbeddedDocuments("Combatant", [{
_id: combatant.id,
initiative: init.init, 'system.init': init
}])
// Send a chat message
let rollMode = messageOptions.rollMode || game.settings.get("core", "messageMode");
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 });
RdDCombat.processPremierRoundInit();
}
return this;
}
static getFirstInitRollFormula(actor) {
const actions = actor.listActions()
if (actions.length > 0) {
const action = actions[0]
const init = RdDCombat.getInitData(actor, action)
const ajustement = RdDCombat.bonusArme(action.arme) + RdDCombat.etatGeneral(actor)
return RdDInitiative.formule(init.phase, init.carac, init.niveau, ajustement);
}
return RdDInitiative.formule('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 RdDCombat.incDecInit(combatantId, commandValue);
case 'autre': return RdDCombat.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 => {
RdDCombat.displayInitiativeMenu(html, target.data('combatant-id'))
}
}
}
options = [
{ name: "Incrémenter initiative", condition: true, icon: '<i class="fa-solid fa-plus"></i>', callback: target => { RdDCombat.incDecInit(target.data('combatant-id'), +1); } },
{ name: "Décrémenter initiative", condition: true, icon: '<i class="fa-solid fa-minus"></i>', callback: target => { RdDCombat.incDecInit(target.data('combatant-id'), -1); } }
].concat(options);
}
/* -------------------------------------------- */
static async rollInitiativeAction(combatantId, action) {
const combatant = game.combat.combatants.get(combatantId)
const actor = RdDCombat.getActorCombatant(combatant)
if (actor == undefined) { return }
const init = RdDCombat.getInitData(actor, action)
const ajustement = RdDCombat.bonusArme(actor, action.arme) + RdDCombat.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: RdDInitiative.PHASES.totale, info: "Surprise Totale", carac: 0, niveau: 0 } }
if (actor.getSurprise() == "demi") { return { phase: RdDInitiative.PHASES.demi, info: "Demi Surprise", carac: 0, niveau: 0 } }
if (action.action == 'autre') { return { phase: RdDInitiative.PHASES.autre, info: "Autre Action", carac: 0, niveau: 0 } }
if (action.action == 'possession') { return { phase: RdDInitiative.PHASES.possession, info: "Possession", carac: actor.getReveActuel(), niveau: 0 } }
if (action.action == 'haut-reve') { return { phase: RdDInitiative.PHASES.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 = RdDCombat.getActorCombatant(combatant, { warning: false })
if (actor) {
const actions = RdDCombat.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 => { RdDCombat.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()
return Misc.indexed(actions)
}
}