Release Creation / build (release) Successful in 54s
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>
292 lines
8.7 KiB
JavaScript
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
|