New combat management and various improvments
All checks were successful
Release Creation / build (release) Successful in 48s
All checks were successful
Release Creation / build (release) Successful in 48s
This commit is contained in:
@@ -2,3 +2,4 @@ export { default as LethalFantasyActor } from "./actor.mjs"
|
||||
export { default as LethalFantasyItem } from "./item.mjs"
|
||||
export { default as LethalFantasyRoll } from "./roll.mjs"
|
||||
export { default as LethalFantasyChatMessage } from "./chat-message.mjs"
|
||||
export { default as D30Roll } from "./d30-roll.mjs"
|
||||
|
||||
@@ -153,8 +153,8 @@ export default class LethalFantasyActor extends Actor {
|
||||
}
|
||||
|
||||
/* *************************************************/
|
||||
async prepareRoll(rollType, rollKey, rollDice) {
|
||||
console.log("Preparing roll", rollType, rollKey, rollDice)
|
||||
async prepareRoll(rollType, rollKey, rollDice, defenderId) {
|
||||
console.log("Preparing roll", rollType, rollKey, rollDice, defenderId)
|
||||
let rollTarget
|
||||
switch (rollType) {
|
||||
case "granted":
|
||||
@@ -268,7 +268,7 @@ export default class LethalFantasyActor extends Actor {
|
||||
rollTarget.magicUser = this.system.biodata.magicUser
|
||||
rollTarget.actorModifiers = foundry.utils.duplicate(this.system.modifiers)
|
||||
rollTarget.actorLevel = this.system.biodata.level
|
||||
await this.system.roll(rollType, rollTarget)
|
||||
await this.system.roll(rollType, rollTarget, defenderId)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
215
module/documents/d30-roll.mjs
Normal file
215
module/documents/d30-roll.mjs
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* Classe pour gérer les résultats du D30 dans Lethal Fantasy
|
||||
*/
|
||||
export default class D30Roll {
|
||||
/**
|
||||
* Table des résultats D30 chargée depuis le fichier JSON
|
||||
* @type {Object}
|
||||
*/
|
||||
static resultsTable = null
|
||||
|
||||
/**
|
||||
* Définitions des conditions spéciales
|
||||
* @type {Object}
|
||||
*/
|
||||
static definitions = null
|
||||
|
||||
/**
|
||||
* Types de jets supportés
|
||||
* @type {Object}
|
||||
*/
|
||||
static ROLL_TYPES = {
|
||||
MELEE_ATTACK: "melee_attack",
|
||||
RANGED_ATTACK: "ranged_attack",
|
||||
MELEE_DEFENSE: "melee_defense",
|
||||
ARCANE_SPELL_ATTACK: "arcane_spell_attack",
|
||||
ARCANE_SPELL_DEFENSE: "arcane_spell_defense",
|
||||
SKILL_ROLLS: "skill_rolls"
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise la classe en chargeant la table des résultats
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async initialize() {
|
||||
try {
|
||||
const response = await fetch("systems/fvtt-lethal-fantasy/module/config/d30_results_tables.json")
|
||||
const data = await response.json()
|
||||
|
||||
this.resultsTable = data.d30_dice_results
|
||||
this.definitions = data.definitions
|
||||
|
||||
console.log("D30Roll | D30 results table loaded successfully")
|
||||
} catch (error) {
|
||||
console.error("D30Roll | Error loading D30 table:", error)
|
||||
ui.notifications.error("Unable to load D30 results table")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le résultat d'un jet de D30
|
||||
* @param {number} diceValue La valeur du dé (1-30)
|
||||
* @param {string} rollType Le type de jet externe (ex: "weapon-attack", "spell-attack", etc.)
|
||||
* @param {Object} weapon L'arme ou l'objet utilisé (optionnel, nécessaire pour certains types)
|
||||
* @returns {string|null} Le résultat correspondant ou null si vide/non trouvé
|
||||
*/
|
||||
static getResult(diceValue, rollType, weapon = null) {
|
||||
if (!this.resultsTable) {
|
||||
console.warn("D30Roll | Results table is not initialized. Call D30Roll.initialize() first.")
|
||||
return null
|
||||
}
|
||||
|
||||
// Validation des paramètres
|
||||
if (diceValue < 1 || diceValue > 30) {
|
||||
console.warn(`D30Roll | Invalid dice value: ${diceValue}. Must be between 1 and 30.`)
|
||||
return null
|
||||
}
|
||||
|
||||
// Convert external rollType to internal rollType
|
||||
const internalType = this.convertToInternalType(rollType, weapon)
|
||||
|
||||
if (!internalType) {
|
||||
console.warn(`D30Roll | Could not convert roll type: ${rollType}`)
|
||||
return null
|
||||
}
|
||||
|
||||
if (!Object.values(this.ROLL_TYPES).includes(internalType)) {
|
||||
console.warn(`D30Roll | Invalid internal roll type: ${internalType}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const resultEntry = this.resultsTable[diceValue]
|
||||
if (!resultEntry) {
|
||||
console.warn(`D30Roll | No entry found for value ${diceValue}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const result = resultEntry[internalType]
|
||||
|
||||
// Retourne null si le résultat est "empty"
|
||||
if (result === "empty" || !result) {
|
||||
return null
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un rollType externe en rollType interne
|
||||
* @param {string} externalType Le type de jet externe (ex: "weapon-attack")
|
||||
* @param {Object} weapon L'arme ou l'objet utilisé (optionnel)
|
||||
* @returns {string|null} Le type interne correspondant ou null
|
||||
*/
|
||||
static convertToInternalType(externalType, weapon = null) {
|
||||
// Attack types - need weapon to determine if melee or ranged
|
||||
if (externalType === "weapon-attack") {
|
||||
if (!weapon) {
|
||||
console.warn("D30Roll | Weapon object required for weapon-attack type")
|
||||
return this.ROLL_TYPES.MELEE_ATTACK // Default to melee
|
||||
}
|
||||
return weapon.system?.weaponType === "ranged"
|
||||
? this.ROLL_TYPES.RANGED_ATTACK
|
||||
: this.ROLL_TYPES.MELEE_ATTACK
|
||||
}
|
||||
|
||||
// Monster attacks - default to melee
|
||||
if (externalType === "monster-attack") {
|
||||
// Check if weapon object has range information
|
||||
if (weapon?.system?.weaponType === "ranged") {
|
||||
return this.ROLL_TYPES.RANGED_ATTACK
|
||||
}
|
||||
return this.ROLL_TYPES.MELEE_ATTACK
|
||||
}
|
||||
|
||||
// Defense types
|
||||
if (externalType === "weapon-defense" || externalType === "monster-defense") {
|
||||
return this.ROLL_TYPES.MELEE_DEFENSE
|
||||
}
|
||||
|
||||
// Spell types
|
||||
if (externalType === "spell-attack" || externalType === "spell" || externalType === "spell-power") {
|
||||
return this.ROLL_TYPES.ARCANE_SPELL_ATTACK
|
||||
}
|
||||
|
||||
// Skill types
|
||||
if (externalType === "skill" || externalType === "monster-skill" ||
|
||||
externalType === "save" || externalType === "challenge") {
|
||||
return this.ROLL_TYPES.SKILL_ROLLS
|
||||
}
|
||||
|
||||
// If no match, return null
|
||||
console.warn(`D30Roll | Unknown external roll type: ${externalType}`)
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère toutes les informations pour une valeur de dé donnée
|
||||
* @param {number} diceValue La valeur du dé (1-30)
|
||||
* @returns {Object|null} Tous les résultats pour cette valeur ou null
|
||||
*/
|
||||
static getAllResultsForValue(diceValue) {
|
||||
if (!this.resultsTable) {
|
||||
console.warn("D30Roll | Results table is not initialized.")
|
||||
return null
|
||||
}
|
||||
|
||||
if (diceValue < 1 || diceValue > 30) {
|
||||
console.warn(`D30Roll | Invalid dice value: ${diceValue}`)
|
||||
return null
|
||||
}
|
||||
|
||||
return this.resultsTable[diceValue]
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère la définition d'une condition spéciale
|
||||
* @param {string} definitionKey La clé de la définition (ex: "flash_of_pain")
|
||||
* @returns {string|null} La définition ou null
|
||||
*/
|
||||
static getDefinition(definitionKey) {
|
||||
if (!this.definitions) {
|
||||
console.warn("D30Roll | Definitions are not initialized.")
|
||||
return null
|
||||
}
|
||||
|
||||
return this.definitions[definitionKey] || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si un résultat est vide
|
||||
* @param {string} result Le résultat à vérifier
|
||||
* @returns {boolean} True si le résultat est vide
|
||||
*/
|
||||
static isEmptyResult(result) {
|
||||
return !result || result === "empty"
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère un résultat formaté pour l'affichage
|
||||
* @param {number} diceValue La valeur du dé (1-30)
|
||||
* @param {string} rollType Le type de jet externe
|
||||
* @param {Object} weapon L'arme ou l'objet utilisé (optionnel)
|
||||
* @returns {Object} Un objet avec le résultat et des informations de formatage
|
||||
*/
|
||||
static getFormattedResult(diceValue, rollType, weapon = null) {
|
||||
const result = this.getResult(diceValue, rollType, weapon)
|
||||
const internalType = this.convertToInternalType(rollType, weapon)
|
||||
|
||||
return {
|
||||
value: diceValue,
|
||||
rollType: rollType,
|
||||
internalType: internalType,
|
||||
result: result,
|
||||
isEmpty: this.isEmptyResult(result),
|
||||
hasResult: !this.isEmptyResult(result)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si la table est chargée
|
||||
* @returns {boolean} True si la table est chargée
|
||||
*/
|
||||
static isInitialized() {
|
||||
return this.resultsTable !== null && this.definitions !== null
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SYSTEM } from "../config/system.mjs"
|
||||
import LethalFantasyUtils from "../utils.mjs"
|
||||
import D30Roll from "./d30-roll.mjs"
|
||||
|
||||
export default class LethalFantasyRoll extends Roll {
|
||||
/**
|
||||
@@ -92,6 +93,10 @@ export default class LethalFantasyRoll extends Roll {
|
||||
return this.options.D30result
|
||||
}
|
||||
|
||||
get D30message() {
|
||||
return this.options.D30message
|
||||
}
|
||||
|
||||
get badResult() {
|
||||
return this.options.badResult
|
||||
}
|
||||
@@ -100,6 +105,10 @@ export default class LethalFantasyRoll extends Roll {
|
||||
return this.options.rollData
|
||||
}
|
||||
|
||||
get defenderId() {
|
||||
return this.options.defenderId
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt the user with a dialog to configure and execute a roll.
|
||||
*
|
||||
@@ -544,6 +553,14 @@ export default class LethalFantasyRoll extends Roll {
|
||||
game.dice3d.showForRoll(rollD30, game.user, true)
|
||||
}
|
||||
options.D30result = rollD30.total
|
||||
|
||||
// Récupérer le message D30 correspondant
|
||||
const d30Message = D30Roll.getResult(
|
||||
rollD30.total,
|
||||
options.rollType,
|
||||
options.rollTarget?.weapon
|
||||
)
|
||||
options.D30message = d30Message
|
||||
}
|
||||
|
||||
let rollTotal = 0
|
||||
@@ -599,8 +616,10 @@ export default class LethalFantasyRoll extends Roll {
|
||||
rollBase.options.rollTarget = options.rollTarget
|
||||
rollBase.options.titleFormula = titleFormula
|
||||
rollBase.options.D30result = options.D30result
|
||||
rollBase.options.D30message = options.D30message
|
||||
rollBase.options.badResult = badResult
|
||||
rollBase.options.rollData = foundry.utils.duplicate(rollData)
|
||||
rollBase.options.defenderId = options.defenderId
|
||||
|
||||
/**
|
||||
* A hook event that fires after the roll has been made.
|
||||
@@ -862,8 +881,17 @@ export default class LethalFantasyRoll extends Roll {
|
||||
let toCompare = Math.min(currentAction.progressionCount, max)
|
||||
if (roll.total <= toCompare) {
|
||||
// Notify that the player can act now with a chat message
|
||||
let message = game.i18n.format("LETHALFANTASY.Notifications.messageLethargyOK", { name: combatant.actor.name, spellName: currentAction.name, roll: roll.total })
|
||||
ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
|
||||
const messageContent = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-lethal-fantasy/templates/progression-message.hbs",
|
||||
{
|
||||
success: true,
|
||||
actorName: combatant.actor.name,
|
||||
weaponName: currentAction.name,
|
||||
rollResult: roll.total,
|
||||
isLethargy: true
|
||||
}
|
||||
)
|
||||
ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
|
||||
// Update the combatant progression count
|
||||
await combatant.setFlag(SYSTEM.id, "currentAction", "")
|
||||
// Display the action selection window again
|
||||
@@ -872,8 +900,18 @@ export default class LethalFantasyRoll extends Roll {
|
||||
// Notify that the player cannot act now with a chat message
|
||||
currentAction.progressionCount += 1
|
||||
await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction))
|
||||
let message = game.i18n.format("LETHALFANTASY.Notifications.messageLethargyKO", { name: combatant.actor.name, spellName: currentAction.name, roll: roll.total })
|
||||
ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
|
||||
const messageContent = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-lethal-fantasy/templates/progression-message.hbs",
|
||||
{
|
||||
success: false,
|
||||
actorName: combatant.actor.name,
|
||||
weaponName: currentAction.name,
|
||||
rollResult: roll.total,
|
||||
progressionCount: currentAction.progressionCount,
|
||||
isLethargy: true
|
||||
}
|
||||
)
|
||||
ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -919,16 +957,33 @@ export default class LethalFantasyRoll extends Roll {
|
||||
|
||||
if (roll.total <= max) {
|
||||
// Notify that the player can act now with a chat message
|
||||
let message = game.i18n.format("LETHALFANTASY.Notifications.messageProgressionOK", { isMonster, name: combatant.actor.name, weapon: currentAction.name, roll: roll.total })
|
||||
ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
|
||||
const messageContent = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-lethal-fantasy/templates/progression-message.hbs",
|
||||
{
|
||||
success: true,
|
||||
actorName: combatant.actor.name,
|
||||
weaponName: currentAction.name,
|
||||
rollResult: roll.total
|
||||
}
|
||||
)
|
||||
ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
|
||||
await combatant.setFlag(SYSTEM.id, "currentAction", "")
|
||||
combatant.actor.prepareRoll(currentAction.type === "weapon" ? "weapon-attack" : "spell-attack", currentAction._id)
|
||||
} else {
|
||||
// Notify that the player cannot act now with a chat message
|
||||
currentAction.progressionCount += 1
|
||||
combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction))
|
||||
let message = game.i18n.format("LETHALFANTASY.Notifications.messageProgressionKO", { isMonster, name: combatant.actor.name, weapon: currentAction.name, roll: roll.total })
|
||||
ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
|
||||
const messageContent = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-lethal-fantasy/templates/progression-message.hbs",
|
||||
{
|
||||
success: false,
|
||||
actorName: combatant.actor.name,
|
||||
weaponName: currentAction.name,
|
||||
rollResult: roll.total,
|
||||
progressionCount: currentAction.progressionCount
|
||||
}
|
||||
)
|
||||
ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1132,10 +1187,28 @@ export default class LethalFantasyRoll extends Roll {
|
||||
async _getChatCardData(isPrivate) {
|
||||
// Générer la liste des combatants de la scène
|
||||
let combatants = []
|
||||
if (game?.combat?.combatants && this.rollData?.isDamage) {
|
||||
for (let c of game.combat.combatants) {
|
||||
if (c.actorId !== this.actorId) {
|
||||
combatants.push({ id: c.id, name: c.name })
|
||||
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
|
||||
if (game?.combat?.combatants) {
|
||||
for (let c of game.combat.combatants) {
|
||||
if (c.actorId !== this.actorId) {
|
||||
combatants.push({ id: c.id, name: c.name, tokenId: 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)) {
|
||||
combatants.push({
|
||||
id: token.id,
|
||||
name: token.name,
|
||||
tokenId: token.id
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1184,11 +1257,16 @@ export default class LethalFantasyRoll extends Roll {
|
||||
targetName: this.targetName,
|
||||
targetArmor: this.targetArmor,
|
||||
D30result: this.D30result,
|
||||
D30message: this.D30message,
|
||||
badResult: this.badResult,
|
||||
rollData: this.rollData,
|
||||
isPrivate: isPrivate,
|
||||
combatants: combatants,
|
||||
weaponDamageOptions: weaponDamageOptions
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user