Files
fvtt-lethal-fantasy/module/documents/roll-base.mjs
T
uberwald fc5d38deff
Release Creation / build (release) Successful in 54s
fix: dead combatants reappearing as targets via scene token fallback
The combat loop correctly skipped defeated combatants, but their token
IDs were absent from existingTokenIds, so the scene token loop re-added
them. Now building a defeatedTokenIds set from defeated combatants and
checking it in the scene token loop. Also switched from
game.actors.get() to token.actor for the monster HP check so unlinked
monster tokens (synthetic actors) are covered.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-08-18 18:38:10 +02:00

292 lines
8.7 KiB
JavaScript

import { SYSTEM } from "../config/system.mjs"
import { prompt } from "./roll-prompt.mjs"
import { promptInitiative, promptCombatAction, promptRangedDefense, promptRangedAttack } from "./roll-combat.mjs"
import { rollSpellDamageToMessage } from "./roll-damage.mjs"
export default class LethalFantasyRoll extends Roll {
/**
* The HTML template path used to render dice checks of this type
* @type {string}
*/
static CHAT_TEMPLATE = "systems/fvtt-lethal-fantasy/templates/chat-message.hbs"
get type() {
return this.options.type
}
get titleFormula() {
return this.options.titleFormula
}
get rollName() {
return this.options.rollName
}
get target() {
return this.options.target
}
get value() {
return this.options.value
}
get treshold() {
return this.options.treshold
}
get actorId() {
return this.options.actorId
}
get actorName() {
return this.options.actorName
}
get actorImage() {
return this.options.actorImage
}
get modifier() {
return this.options.modifier
}
get resultType() {
return this.options.resultType
}
get isFailure() {
return this.resultType === "failure"
}
get hasTarget() {
return this.options.hasTarget
}
get targetName() {
return this.options.targetName
}
get targetArmor() {
return this.options.targetArmor
}
get targetMalus() {
return this.options.targetMalus
}
get realDamage() {
return this.options.realDamage
}
get rollTotal() {
return this.options.rollTotal
}
get diceResults() {
return this.options.diceResults
}
get rollTarget() {
return this.options.rollTarget
}
get D30result() {
return this.options.D30result
}
get D30message() {
return this.options.D30message
}
get badResult() {
return this.options.badResult
}
get rollData() {
return this.options.rollData
}
get defenderId() {
return this.options.defenderId
}
/**
* Creates a title based on the given type.
*
* @param {string} type The type of the roll.
* @param {string} target The target of the roll.
* @returns {string} The generated title.
*/
static createTitle(type, target) {
switch (type) {
case "challenge":
return `${game.i18n.localize("LETHALFANTASY.Label.titleChallenge")}`
case "save":
return `${game.i18n.localize("LETHALFANTASY.Label.titleSave")}`
case "monster-skill":
case "skill":
return `${game.i18n.localize("LETHALFANTASY.Label.titleSkill")}`
case "weapon-attack":
return `${game.i18n.localize("LETHALFANTASY.Label.weapon-attack")}`
case "weapon-defense":
return `${game.i18n.localize("LETHALFANTASY.Label.weapon-defense")}`
case "weapon-damage":
return `${game.i18n.localize("LETHALFANTASY.Label.weapon-damage")}`
case "spell":
case "spell-attack":
case "spell-power":
return `${game.i18n.localize("LETHALFANTASY.Label.spell")}`
case "miracle":
case "miracle-attack":
case "miracle-power":
return `${game.i18n.localize("LETHALFANTASY.Label.miracle")}`
default:
return game.i18n.localize("LETHALFANTASY.Label.titleStandard")
}
}
/** @override */
async render(chatOptions = {}) {
let chatData = await this._getChatCardData(chatOptions.isPrivate)
log("ChatData", chatData)
return await foundry.applications.handlebars.renderTemplate(this.constructor.CHAT_TEMPLATE, chatData)
}
/*
* Generates the data required for rendering a roll chat card.
*/
async _getChatCardData(isPrivate) {
// Générer la liste des combatants de la scène
let combatants = []
let isAttack = this.type === "weapon-attack" || this.type === "monster-attack" || this.type === "spell-attack" || this.type === "miracle-attack"
if (this.rollData?.isDamage || isAttack) {
// D'abord, ajouter les combattants du combat actif
const defeatedTokenIds = new Set()
if (game?.combat?.combatants) {
for (let c of game.combat.combatants) {
if (c.actorId !== this.actorId && !c.isDefeated) {
combatants.push({ id: c.id, name: c.name, tokenId: c.token.id })
}
if (c.isDefeated && c.token?.id) {
defeatedTokenIds.add(c.token.id)
}
}
}
// Ensuite, ajouter tous les tokens de la scène active qui ne sont pas déjà dans la liste
if (canvas?.scene?.tokens) {
const existingTokenIds = new Set(combatants.map(c => c.tokenId))
for (let token of canvas.scene.tokens) {
if (token.actorId !== this.actorId && !existingTokenIds.has(token.id) && !defeatedTokenIds.has(token.id)) {
// Skip dead monsters (0 HP). Use token.actor for unlinked tokens.
const tokenActor = token.actor ?? game.actors.get(token.actorId)
if (tokenActor?.type === "monster" && (Number(tokenActor?.system?.hp?.value) || 0) <= 0) continue
combatants.push({
id: token.id,
name: token.name,
tokenId: token.id
})
}
}
}
}
// Récupérer les informations de l'arme pour les attaques réussies
let weaponDamageOptions = null
log("Roll type:", this.type, "rollTarget:", this.rollTarget, "Has weapon:", !!this.rollTarget?.weapon)
if (this.type === "weapon-attack" && this.rollTarget?.weapon) {
const weapon = this.rollTarget.weapon
weaponDamageOptions = {
weaponId: weapon._id || weapon.id,
weaponName: weapon.name,
damageM: weapon.system?.damage?.damageM
}
log("Weapon damage options:", weaponDamageOptions)
} else if (this.type === "monster-attack" && this.rollTarget) {
weaponDamageOptions = {
weaponId: this.rollTarget.rollKey,
weaponName: this.rollTarget.name,
damageFormula: this.rollTarget.damageDice,
damageModifier: this.rollTarget.damageModifier,
isMonster: true
}
log("Monster damage options:", weaponDamageOptions)
}
const cardData = {
css: [SYSTEM.id, "dice-roll"],
data: this.data,
diceTotal: this.dice.reduce((t, d) => t + d.total, 0),
isGM: game.user.isGM,
formula: this.formula,
titleFormula: this.titleFormula,
rollName: this.rollName,
rollType: this.type,
rollTarget: this.rollTarget,
total: this.rollTotal,
isFailure: this.isFailure,
actorId: this.actorId,
diceResults: this.diceResults,
actingCharName: this.actorName,
actingCharImg: this.actorImage,
resultType: this.resultType,
hasTarget: this.hasTarget,
targetName: this.targetName,
targetArmor: this.targetArmor,
D30result: this.D30result,
D30message: this.D30message,
badResult: this.badResult,
rollData: this.rollData,
isPrivate: isPrivate,
combatants: combatants,
weaponDamageOptions: weaponDamageOptions,
isAttack: isAttack,
defenderId: this.defenderId,
// Vérifier si l'utilisateur peut sélectionner une cible (est GM ou possède l'acteur)
canSelectTarget: game.user.isGM || game.actors.get(this.actorId)?.testUserPermission(game.user, "OWNER")
}
cardData.cssClass = cardData.css.join(" ")
cardData.tooltip = isPrivate ? "" : await this.getTooltip()
return cardData
}
/**
* Converts the roll result to a chat message.
*
* @param {Object} [messageData={}] Additional data to include in the message.
* @param {Object} options Options for message creation.
* @param {string} options.messageMode The mode of the roll (e.g., public, private).
* @param {boolean} [options.create=true] Whether to create the message.
* @returns {Promise} - A promise that resolves when the message is created.
*/
async toMessage(messageData = {}, { messageMode, create = true } = {}) {
return await super.toMessage(
{
isSave: this.isSave,
isChallenge: this.isChallenge,
isFailure: this.resultType === "failure",
rollType: this.type,
rollTarget: this.rollTarget,
actingCharName: this.actorName,
actingCharImg: this.actorImage,
hasTarget: this.hasTarget,
targetName: this.targetName,
targetArmor: this.targetArmor,
targetMalus: this.targetMalus,
realDamage: this.realDamage,
rollData: this.rollData,
...messageData,
},
{ messageMode, create },
)
}
}
// Attach imported prompt methods
LethalFantasyRoll.prompt = prompt
LethalFantasyRoll.promptInitiative = promptInitiative
LethalFantasyRoll.promptCombatAction = promptCombatAction
LethalFantasyRoll.promptRangedDefense = promptRangedDefense
LethalFantasyRoll.promptRangedAttack = promptRangedAttack
LethalFantasyRoll.rollSpellDamageToMessage = rollSpellDamageToMessage