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:
152
module/config/d30_results_tables.json
Normal file
152
module/config/d30_results_tables.json
Normal file
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"d30_dice_results": {
|
||||
"30": {
|
||||
"melee_attack": "Possible Lethal or Vital Strike or Add D20E to Attack",
|
||||
"ranged_attack": "Possible Lethal or Vital Strike or Add D20E to Attack",
|
||||
"melee_defense": "Possible Flawless or Legendary Defense or Add D20E to Defense",
|
||||
"arcane_spell_attack": "Possible Lethal or Vital Magical Strike or Add D20E to Spell Attack",
|
||||
"arcane_spell_defense": "Possible Spell Catastrophe or adds D20E to Spell Defense",
|
||||
"skill_rolls": "Skill Succeeds Regardless of Opposing Roll / Success at highest level / Matching 30s cancel each other out"
|
||||
},
|
||||
"29": {
|
||||
"melee_attack": "Gain 1 Grit",
|
||||
"ranged_attack": "Gain 1 Grit",
|
||||
"melee_defense": "Gain 1 Grit",
|
||||
"arcane_spell_attack": "Gain 1 Grit",
|
||||
"arcane_spell_defense": "Gain 1 Grit",
|
||||
"skill_rolls": "Gain 1 Grit"
|
||||
},
|
||||
"28": {
|
||||
"melee_attack": "Shield Destruction",
|
||||
"ranged_attack": "empty",
|
||||
"melee_defense": "empty",
|
||||
"arcane_spell_attack": "empty",
|
||||
"arcane_spell_defense": "empty",
|
||||
"skill_rolls": "empty"
|
||||
},
|
||||
"27": {
|
||||
"melee_attack": "Granted D6 (1-6) Attack Modifier for This Melee Attack",
|
||||
"ranged_attack": "Granted D6 (1-6) Attack Modifier for This Ranged Attack",
|
||||
"melee_defense": "Granted 1 Luck dice for Use in This Combat Only",
|
||||
"arcane_spell_attack": "No Spell Lethargy (the Aether Approves)",
|
||||
"arcane_spell_defense": "Caster Suffers Severe pain and will be under a flash of pain for 1D6E seconds",
|
||||
"skill_rolls": "Granted D6 (1-6) Skill Modifier for this Skill Attempt"
|
||||
},
|
||||
"26": {
|
||||
"melee_attack": "Shield Destruction",
|
||||
"ranged_attack": "empty",
|
||||
"melee_defense": "empty",
|
||||
"arcane_spell_attack": "empty",
|
||||
"arcane_spell_defense": "empty",
|
||||
"skill_rolls": "empty"
|
||||
},
|
||||
"25": {
|
||||
"melee_attack": "Bleed, Knock-Back on Hit",
|
||||
"ranged_attack": "Bleed",
|
||||
"melee_defense": "Kick, Punch or Shield Bash",
|
||||
"arcane_spell_attack": "empty",
|
||||
"arcane_spell_defense": "empty",
|
||||
"skill_rolls": "Add 1 to Skill Roll"
|
||||
},
|
||||
"21": {
|
||||
"melee_attack": "Hit Inflicts Flash of Pain 1D6E seconds",
|
||||
"ranged_attack": "Hit Inflicts Flash of Pain 1D6E seconds",
|
||||
"melee_defense": "Defender Recovers or ignores any flash of pain",
|
||||
"arcane_spell_attack": "Magical Damage inflicts Flash of pain 1D6E seconds",
|
||||
"arcane_spell_defense": "Caster Suffers Severe pain and will be under a flash of pain for 1D6E seconds",
|
||||
"skill_rolls": "empty"
|
||||
},
|
||||
"20": {
|
||||
"melee_attack": "Possible Vicious Strike. Bleed, Knock-back on Hit",
|
||||
"ranged_attack": "Possible Vicious Strike. Bleeding wound inflicted on hit.",
|
||||
"melee_defense": "Possible 20/20 defense (avoids Any Attack Except a Lethal Strike). Grants a Kick, Punch or Shield Bash counter",
|
||||
"arcane_spell_attack": "Possible Vicious Application of a Magical Attack",
|
||||
"arcane_spell_defense": "Possible 20/20 Spell defense (Saves Against Any Magical Attack Except a Lethal Magical Strike)",
|
||||
"skill_rolls": "20 Added to Skill Roll"
|
||||
},
|
||||
"15": {
|
||||
"melee_attack": "Bleed, Knock-back on Hit",
|
||||
"ranged_attack": "Bleed",
|
||||
"melee_defense": "Kick, Punch or Shield Bash",
|
||||
"arcane_spell_attack": "empty",
|
||||
"arcane_spell_defense": "empty",
|
||||
"skill_rolls": "Add 1 to Skill Roll"
|
||||
},
|
||||
"13": {
|
||||
"melee_attack": "empty",
|
||||
"ranged_attack": "empty",
|
||||
"melee_defense": "empty",
|
||||
"arcane_spell_attack": "empty",
|
||||
"arcane_spell_defense": "empty",
|
||||
"skill_rolls": "empty"
|
||||
},
|
||||
"11": {
|
||||
"melee_attack": "Flurry Attack or Hit to Miss",
|
||||
"ranged_attack": "Roll 2x Damage Dice",
|
||||
"melee_defense": "empty",
|
||||
"arcane_spell_attack": "empty",
|
||||
"arcane_spell_defense": "empty",
|
||||
"skill_rolls": "empty"
|
||||
},
|
||||
"10": {
|
||||
"melee_attack": "Bleed, Knock-back on Hit",
|
||||
"ranged_attack": "Bleed",
|
||||
"melee_defense": "Kick, Punch or Shield Bash",
|
||||
"arcane_spell_attack": "empty",
|
||||
"arcane_spell_defense": "empty",
|
||||
"skill_rolls": "Add 1 to Skill Roll"
|
||||
},
|
||||
"8": {
|
||||
"melee_attack": "Mulligan, Can Choose to Re-roll This Attack",
|
||||
"ranged_attack": "Mulligan, Can Choose to Re-Roll This Attack",
|
||||
"melee_defense": "Mulligan, Can Choose to Re-Roll This Defense",
|
||||
"arcane_spell_attack": "Mulligan, Can Re-Roll This Spell Attack",
|
||||
"arcane_spell_defense": "Mulligan, Can Re-Roll This Spell Defense",
|
||||
"skill_rolls": "Mulligan, Can Re-Roll This Skill roll"
|
||||
},
|
||||
"7": {
|
||||
"melee_attack": "Flurry Attack on Hit or Miss",
|
||||
"ranged_attack": "Roll 2x Double Damage Dice",
|
||||
"melee_defense": "empty",
|
||||
"arcane_spell_attack": "empty",
|
||||
"arcane_spell_defense": "empty",
|
||||
"skill_rolls": "empty"
|
||||
},
|
||||
"5": {
|
||||
"melee_attack": "Bleed, Knock-back on Hit",
|
||||
"ranged_attack": "Bleed",
|
||||
"melee_defense": "Kick, Punch, or Shield Bash",
|
||||
"arcane_spell_attack": "empty",
|
||||
"arcane_spell_defense": "empty",
|
||||
"skill_rolls": "Add 1 to Skill Roll"
|
||||
},
|
||||
"3": {
|
||||
"melee_attack": "Triple Damage",
|
||||
"ranged_attack": "Triple Damage",
|
||||
"melee_defense": "DR Tripled including Shield",
|
||||
"arcane_spell_attack": "Triple Damage on Spell Damage",
|
||||
"arcane_spell_defense": "D12 Added to Spell Defense Modifier",
|
||||
"skill_rolls": "empty"
|
||||
},
|
||||
"2": {
|
||||
"melee_attack": "Double Damage",
|
||||
"ranged_attack": "Double Damage",
|
||||
"melee_defense": "DR Doubled including Shield",
|
||||
"arcane_spell_attack": "Double Damage on Spell Damage",
|
||||
"arcane_spell_defense": "D6 Added to Spell Defense Modifier",
|
||||
"skill_rolls": "empty"
|
||||
},
|
||||
"1": {
|
||||
"melee_attack": "empty",
|
||||
"ranged_attack": "Possible Fumble Ranged ammo is broken unrecoverable",
|
||||
"melee_defense": "empty",
|
||||
"arcane_spell_attack": "Possible Spell Calamity or Catastrophe",
|
||||
"arcane_spell_defense": "empty",
|
||||
"skill_rolls": "empty"
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"flash_of_pain": "Causes the victim to defend with disfavor. They can only walk and cannot attack, cast spells, call miracles or perform skills.",
|
||||
"shield_destruction_condition": "Occurs only if damage exceeds the shields DR."
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -273,7 +273,7 @@ export default class LethalFantasyCharacter extends foundry.abstract.TypeDataMod
|
||||
* @param {"="|"+"|"++"|"-"|"--"} rollAdvantage If there is an avantage (+), a disadvantage (-), a double advantage (++), a double disadvantage (--) or a normal roll (=).
|
||||
* @returns {Promise<null>} - A promise that resolves to null if the roll is cancelled.
|
||||
*/
|
||||
async roll(rollType, rollTarget) {
|
||||
async roll(rollType, rollTarget, defenderId) {
|
||||
const hasTarget = false
|
||||
let roll = await LethalFantasyRoll.prompt({
|
||||
rollType,
|
||||
@@ -282,7 +282,8 @@ export default class LethalFantasyCharacter extends foundry.abstract.TypeDataMod
|
||||
actorName: this.parent.name,
|
||||
actorImage: this.parent.img,
|
||||
hasTarget,
|
||||
target: false
|
||||
target: false,
|
||||
defenderId
|
||||
})
|
||||
if (!roll) return null
|
||||
|
||||
@@ -311,6 +312,13 @@ export default class LethalFantasyCharacter extends foundry.abstract.TypeDataMod
|
||||
}
|
||||
|
||||
async rollProgressionDice(combatId, combatantId, rollProgressionCount) {
|
||||
let combatant = game.combats.get(combatId)?.combatants?.get(combatantId)
|
||||
|
||||
// Don't roll if the combatant is defeated
|
||||
if (combatant?.isDefeated) {
|
||||
ui.notifications.warn(`${this.parent.name} is defeated and cannot attack.`)
|
||||
return
|
||||
}
|
||||
|
||||
// Get all weapons from the actor
|
||||
let weapons = this.parent.items.filter(i => i.type === "weapon" && i.system.weaponType === "melee")
|
||||
|
||||
@@ -122,6 +122,10 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
|
||||
shieldDamageReduction: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
|
||||
shieldDefenseDice: new fields.StringField({ required: true, nullable: false, initial: "d4" })
|
||||
})
|
||||
schema.combatHTH = new fields.SchemaField({
|
||||
attack1: attackField("1"),
|
||||
attack2: attackField("2")
|
||||
})
|
||||
|
||||
|
||||
return schema
|
||||
@@ -137,7 +141,7 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
|
||||
* @param {"="|"+"|"++"|"-"|"--"} rollAdvantage If there is an avantage (+), a disadvantage (-), a double advantage (++), a double disadvantage (--) or a normal roll (=).
|
||||
* @returns {Promise<null>} - A promise that resolves to null if the roll is cancelled.
|
||||
*/
|
||||
async roll(rollType, rollTarget) {
|
||||
async roll(rollType, rollTarget, defenderId = undefined) {
|
||||
const hasTarget = false
|
||||
let roll = await LethalFantasyRoll.prompt({
|
||||
rollType,
|
||||
@@ -146,14 +150,15 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
|
||||
actorName: this.parent.name,
|
||||
actorImage: this.parent.img,
|
||||
hasTarget,
|
||||
target: false
|
||||
target: false,
|
||||
defenderId
|
||||
})
|
||||
if (!roll) return null
|
||||
|
||||
await roll.toMessage({}, { rollMode: roll.options.rollMode })
|
||||
}
|
||||
|
||||
async prepareMonsterRoll(rollType, rollKey, rollDice = undefined, tokenId = undefined, damageModifier = undefined) {
|
||||
async prepareMonsterRoll(rollType, rollKey, rollDice = undefined, tokenId = undefined, damageModifier = undefined, defenderId = undefined) {
|
||||
let rollTarget
|
||||
switch (rollType) {
|
||||
case "monster-attack":
|
||||
@@ -166,6 +171,18 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
|
||||
rollTarget.damageModifier = damageModifier
|
||||
}
|
||||
break
|
||||
case "monster-attack-hth":
|
||||
case "monster-defense-hth":
|
||||
case "monster-damage-hth":
|
||||
rollTarget = foundry.utils.duplicate(this.combatHTH[rollKey])
|
||||
rollTarget.rollKey = rollKey
|
||||
// Si damageModifier est fourni (depuis le chat), l'utiliser au lieu de celui de la fiche
|
||||
if (damageModifier !== undefined && rollType === "monster-damage-hth") {
|
||||
rollTarget.damageModifier = damageModifier
|
||||
}
|
||||
// Convertir le type de roll pour utiliser les mêmes handlers que les attaques normales
|
||||
rollType = rollType.replace("-hth", "")
|
||||
break
|
||||
case "monster-skill":
|
||||
rollTarget = foundry.utils.duplicate(this.resists[rollKey])
|
||||
rollTarget.rollKey = rollKey
|
||||
@@ -237,7 +254,7 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
|
||||
if (rollTarget) {
|
||||
rollTarget.tokenId = tokenId
|
||||
console.log(rollTarget)
|
||||
await this.roll(rollType, rollTarget)
|
||||
await this.roll(rollType, rollTarget, defenderId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,6 +278,13 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
|
||||
}
|
||||
|
||||
async rollProgressionDice(combatId, combatantId) {
|
||||
let combatant = game.combats.get(combatId)?.combatants?.get(combatantId)
|
||||
|
||||
// Don't roll if the combatant is defeated
|
||||
if (combatant?.isDefeated) {
|
||||
ui.notifications.warn(`${this.parent.name} is defeated and cannot attack.`)
|
||||
return
|
||||
}
|
||||
|
||||
const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
|
||||
const fieldRollMode = new foundry.data.fields.StringField({
|
||||
@@ -271,7 +295,6 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
|
||||
|
||||
let roll = new Roll("1D12")
|
||||
await roll.evaluate()
|
||||
let combatant = game.combats.get(combatId)?.combatants?.get(combatantId)
|
||||
|
||||
let msg = await roll.toMessage({ flavor: `Progression Roll for ${this.parent.name}` })
|
||||
if (game?.dice3d) {
|
||||
@@ -283,8 +306,16 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
|
||||
let attack = this.attacks[key]
|
||||
if (attack.enabled && attack.attackScore > 0 && attack.attackScore === roll.total) {
|
||||
hasAttack = true
|
||||
let message = game.i18n.format("LETHALFANTASY.Notifications.messageProgressionOKMonster", { isMonster: true, name: this.parent.name, weapon: attack.name, roll: roll.total })
|
||||
ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: this.parent }) })
|
||||
const messageContent = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-lethal-fantasy/templates/progression-message.hbs",
|
||||
{
|
||||
success: true,
|
||||
actorName: this.parent.name,
|
||||
weaponName: attack.name,
|
||||
rollResult: roll.total
|
||||
}
|
||||
)
|
||||
ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: this.parent }) })
|
||||
let token = combatant?.token
|
||||
this.prepareMonsterRoll("monster-attack", key, undefined, token?.id)
|
||||
if (token?.object) {
|
||||
@@ -293,9 +324,41 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check Hand To Hand attacks as well
|
||||
if (!hasAttack) {
|
||||
let message = game.i18n.format("LETHALFANTASY.Notifications.messageProgressionKOMonster", { isMonster: true, name: this.parent.name, roll: roll.total })
|
||||
ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: this.parent }) })
|
||||
for (let key in this.combatHTH) {
|
||||
let attack = this.combatHTH[key]
|
||||
if (attack.enabled && attack.attackScore > 0 && attack.attackScore === roll.total) {
|
||||
hasAttack = true
|
||||
const messageContent = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-lethal-fantasy/templates/progression-message.hbs",
|
||||
{
|
||||
success: true,
|
||||
actorName: this.parent.name,
|
||||
weaponName: `${attack.name} (HTH)`,
|
||||
rollResult: roll.total
|
||||
}
|
||||
)
|
||||
ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: this.parent }) })
|
||||
let token = combatant?.token
|
||||
this.prepareMonsterRoll("monster-attack-hth", key, undefined, token?.id)
|
||||
if (token?.object) {
|
||||
token.object?.control({ releaseOthers: true });
|
||||
return canvas.animatePan(token.object.center);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasAttack) {
|
||||
const messageContent = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-lethal-fantasy/templates/progression-message.hbs",
|
||||
{
|
||||
success: false,
|
||||
actorName: this.parent.name,
|
||||
rollResult: roll.total
|
||||
}
|
||||
)
|
||||
ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: this.parent }) })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
481
module/utils.mjs
481
module/utils.mjs
@@ -1,3 +1,9 @@
|
||||
import { SYSTEM } from "./config/system.mjs"
|
||||
|
||||
// Map temporaire pour stocker les données d'attaque en attente de défense
|
||||
if (!globalThis.pendingDefenses) {
|
||||
globalThis.pendingDefenses = new Map()
|
||||
}
|
||||
|
||||
export default class LethalFantasyUtils {
|
||||
|
||||
@@ -75,9 +81,468 @@ export default class LethalFantasyUtils {
|
||||
actor = game.actors.get(msg.actorId)
|
||||
actor.system.rollProgressionDice(msg.combatId, msg.combatantId, msg.rollProgressionCount)
|
||||
break
|
||||
case "requestDefense":
|
||||
// Vérifier si le message est destiné à cet utilisateur
|
||||
if (msg.userId === game.user.id) {
|
||||
LethalFantasyUtils.showDefenseRequest(msg)
|
||||
}
|
||||
break
|
||||
case "offerAttackerGrit":
|
||||
// Vérifier si le message est destiné à cet utilisateur
|
||||
if (msg.userId === game.user.id) {
|
||||
LethalFantasyUtils.handleAttackerGritOffer(msg)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static async handleAttackerGritOffer(msg) {
|
||||
const { attackerId, attackRoll, defenseRoll, attackerName, defenderName, attackWeaponId, attackRollType, attackRollKey, defenderId } = msg
|
||||
|
||||
const attacker = game.actors.get(attackerId)
|
||||
if (!attacker) {
|
||||
console.warn("Attacker not found:", attackerId)
|
||||
return
|
||||
}
|
||||
|
||||
const attackBonus = await LethalFantasyUtils.offerAttackerGritBonus(
|
||||
attacker,
|
||||
attackRoll,
|
||||
defenseRoll,
|
||||
attackerName,
|
||||
defenderName
|
||||
)
|
||||
|
||||
const attackRollFinal = attackRoll + attackBonus
|
||||
|
||||
// Maintenant créer le message de comparaison
|
||||
await LethalFantasyUtils.compareAttackDefense({
|
||||
attackerName,
|
||||
attackerId,
|
||||
attackRoll: attackRollFinal,
|
||||
attackWeaponId,
|
||||
attackRollType,
|
||||
attackRollKey,
|
||||
defenderName,
|
||||
defenderId,
|
||||
defenseRoll
|
||||
})
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static async showDefenseRequest(msg) {
|
||||
const attackerName = msg.attackerName
|
||||
const attackerId = msg.attackerId
|
||||
const defenderName = msg.defenderName
|
||||
const weaponName = msg.weaponName || "attack"
|
||||
const attackRoll = msg.attackRoll
|
||||
const attackWeaponId = msg.attackWeaponId
|
||||
const attackRollType = msg.attackRollType
|
||||
const attackRollKey = msg.attackRollKey
|
||||
const combatantId = msg.combatantId
|
||||
const tokenId = msg.tokenId
|
||||
|
||||
// Récupérer le défenseur - essayer d'abord depuis le combat, puis depuis le token
|
||||
let defender = null
|
||||
|
||||
if (game.combat && combatantId) {
|
||||
const combatant = game.combat.combatants.get(combatantId)
|
||||
if (combatant) {
|
||||
defender = combatant.actor
|
||||
}
|
||||
}
|
||||
|
||||
// Si pas trouvé dans le combat, chercher le token directement
|
||||
if (!defender && tokenId) {
|
||||
const token = canvas.tokens.get(tokenId)
|
||||
if (token) {
|
||||
defender = token.actor
|
||||
}
|
||||
}
|
||||
|
||||
if (!defender) {
|
||||
ui.notifications.error("Defender actor not found")
|
||||
return
|
||||
}
|
||||
|
||||
const isMonster = defender.type === "monster"
|
||||
|
||||
// Pour les monstres, récupérer les attaques activées
|
||||
if (isMonster) {
|
||||
const enabledAttacks = Object.entries(defender.system.attacks).filter(([key, attack]) => attack.enabled)
|
||||
|
||||
if (enabledAttacks.length === 0) {
|
||||
ui.notifications.warn("No enabled attacks available for defense")
|
||||
return
|
||||
}
|
||||
|
||||
// Créer le contenu du dialogue pour monstre
|
||||
let attacksHTML = enabledAttacks.map(([key, attack]) =>
|
||||
`<option value="${key}">${attack.name}</option>`
|
||||
).join("")
|
||||
|
||||
const content = `
|
||||
<div class="defense-request-dialog">
|
||||
<div class="attack-info">
|
||||
<p><strong>${attackerName}</strong> attacks <strong>${defenderName}</strong> with <strong>${weaponName}</strong>!</p>
|
||||
<p>Attack roll: <strong>${attackRoll}</strong></p>
|
||||
</div>
|
||||
<div class="weapon-selection">
|
||||
<label for="defense-attack">Choose your defense attack:</label>
|
||||
<select id="defense-attack" name="attackKey" style="width: 100%; margin-top: 8px;">
|
||||
${attacksHTML}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// Afficher le dialogue
|
||||
const result = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Defense Roll" },
|
||||
classes: ["lethalfantasy"],
|
||||
content,
|
||||
buttons: [
|
||||
{
|
||||
label: "Roll Defense",
|
||||
icon: "fa-solid fa-shield",
|
||||
callback: (event, button, dialog) => {
|
||||
const attackKey = button.form.elements.attackKey.value
|
||||
return attackKey
|
||||
},
|
||||
},
|
||||
],
|
||||
rejectClose: false
|
||||
})
|
||||
|
||||
// Si l'utilisateur a validé, lancer le jet de défense
|
||||
if (result) {
|
||||
// Stocker temporairement les données pour le hook preCreateChatMessage
|
||||
game.lethalFantasy = game.lethalFantasy || {}
|
||||
game.lethalFantasy.nextDefenseData = {
|
||||
attackerId,
|
||||
attackRoll,
|
||||
attackerName,
|
||||
defenderName,
|
||||
attackWeaponId,
|
||||
attackRollType,
|
||||
attackRollKey,
|
||||
defenderId: defender.id
|
||||
}
|
||||
|
||||
console.log("Storing defense data for monster:", defender.id)
|
||||
|
||||
defender.system.prepareMonsterRoll("monster-defense", result)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Pour les personnages, récupérer les armes équipées
|
||||
const equippedWeapons = defender.items.filter(i =>
|
||||
i.type === "weapon" && i.system.equipped === true
|
||||
)
|
||||
|
||||
if (equippedWeapons.length === 0) {
|
||||
ui.notifications.warn("No equipped weapons for defense")
|
||||
return
|
||||
}
|
||||
|
||||
// Créer le contenu du dialogue pour personnage
|
||||
let weaponsHTML = equippedWeapons.map(w =>
|
||||
`<option value="${w.id}">${w.name}</option>`
|
||||
).join("")
|
||||
|
||||
const content = `
|
||||
<div class="defense-request-dialog">
|
||||
<div class="attack-info">
|
||||
<p><strong>${attackerName}</strong> attacks <strong>${defenderName}</strong> with <strong>${weaponName}</strong>!</p>
|
||||
<p>Attack roll: <strong>${attackRoll}</strong></p>
|
||||
</div>
|
||||
<div class="weapon-selection">
|
||||
<label for="defense-weapon">Choose your defense weapon:</label>
|
||||
<select id="defense-weapon" name="weaponId" style="width: 100%; margin-top: 8px;">
|
||||
${weaponsHTML}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// Afficher le dialogue
|
||||
const result = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Defense Roll" },
|
||||
classes: ["lethalfantasy"],
|
||||
content,
|
||||
buttons: [
|
||||
{
|
||||
label: "Roll Defense",
|
||||
icon: "fa-solid fa-shield",
|
||||
callback: (event, button, dialog) => {
|
||||
const weaponId = button.form.elements.weaponId.value
|
||||
return weaponId
|
||||
},
|
||||
},
|
||||
],
|
||||
rejectClose: false
|
||||
})
|
||||
|
||||
// Si l'utilisateur a validé, lancer le jet de défense
|
||||
if (result) {
|
||||
// Stocker temporairement les données pour le hook preCreateChatMessage
|
||||
game.lethalFantasy = game.lethalFantasy || {}
|
||||
game.lethalFantasy.nextDefenseData = {
|
||||
attackerId,
|
||||
attackRoll,
|
||||
attackerName,
|
||||
defenderName,
|
||||
attackWeaponId,
|
||||
attackRollType,
|
||||
attackRollKey,
|
||||
defenderId: defender.id
|
||||
}
|
||||
|
||||
console.log("Storing defense data for character:", defender.id)
|
||||
|
||||
defender.prepareRoll("weapon-defense", result)
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static async offerGritLuckBonus(defender, attackRoll, currentDefenseRoll, attackerName, defenderName) {
|
||||
let totalBonus = 0
|
||||
let keepOffering = true
|
||||
|
||||
while (keepOffering && currentDefenseRoll + totalBonus < attackRoll) {
|
||||
const currentGrit = defender.system.grit.current
|
||||
const currentLuck = defender.system.luck.current
|
||||
|
||||
// Si plus de points disponibles, sortir
|
||||
if (currentGrit <= 0 && currentLuck <= 0) {
|
||||
break
|
||||
}
|
||||
|
||||
const buttons = []
|
||||
|
||||
if (currentGrit > 0) {
|
||||
buttons.push({
|
||||
action: "grit",
|
||||
label: `Spend 1 Grit (+1D6) [${currentGrit} left]`,
|
||||
icon: "fa-solid fa-fist-raised",
|
||||
callback: () => "grit"
|
||||
})
|
||||
}
|
||||
|
||||
if (currentLuck > 0) {
|
||||
buttons.push({
|
||||
action: "luck",
|
||||
label: `Spend 1 Luck (+1D6) [${currentLuck} left]`,
|
||||
icon: "fa-solid fa-clover",
|
||||
callback: () => "luck"
|
||||
})
|
||||
}
|
||||
|
||||
buttons.push({
|
||||
action: "continue",
|
||||
label: "Continue (no bonus)",
|
||||
icon: "fa-solid fa-forward",
|
||||
callback: () => "continue"
|
||||
})
|
||||
|
||||
const content = `
|
||||
<div class="grit-luck-dialog">
|
||||
<div class="combat-status">
|
||||
<p><strong>${attackerName}</strong> rolled <strong>${attackRoll}</strong></p>
|
||||
<p><strong>${defenderName}</strong> currently has <strong>${currentDefenseRoll + totalBonus}</strong></p>
|
||||
${totalBonus > 0 ? `<p class="bonus-info">Bonus already added: +${totalBonus}</p>` : ''}
|
||||
</div>
|
||||
<p class="offer-text">You are losing! Spend Grit or Luck to add 1D6 to your defense?</p>
|
||||
</div>
|
||||
`
|
||||
|
||||
const choice = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Defend with Grit or Luck" },
|
||||
classes: ["lethalfantasy"],
|
||||
content,
|
||||
buttons,
|
||||
rejectClose: false
|
||||
})
|
||||
|
||||
if (!choice || choice === "continue") {
|
||||
keepOffering = false
|
||||
break
|
||||
}
|
||||
|
||||
// Lancer 1D6
|
||||
const bonusRoll = new Roll("1d6")
|
||||
await bonusRoll.evaluate()
|
||||
|
||||
if (game?.dice3d) {
|
||||
await game.dice3d.showForRoll(bonusRoll, game.user, true)
|
||||
}
|
||||
|
||||
totalBonus += bonusRoll.total
|
||||
|
||||
// Déduire le point de Grit ou Luck
|
||||
if (choice === "grit") {
|
||||
await defender.update({ "system.grit.current": currentGrit - 1 })
|
||||
await ChatMessage.create({
|
||||
content: `<p><strong>${defenderName}</strong> spends 1 Grit and rolls <strong>${bonusRoll.total}</strong>! (Total defense bonus: +${totalBonus})</p>`,
|
||||
speaker: ChatMessage.getSpeaker({ actor: defender })
|
||||
})
|
||||
} else if (choice === "luck") {
|
||||
await defender.update({ "system.luck.current": currentLuck - 1 })
|
||||
await ChatMessage.create({
|
||||
content: `<p><strong>${defenderName}</strong> spends 1 Luck and rolls <strong>${bonusRoll.total}</strong>! (Total defense bonus: +${totalBonus})</p>`,
|
||||
speaker: ChatMessage.getSpeaker({ actor: defender })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return totalBonus
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static async offerAttackerGritBonus(attacker, currentAttackRoll, defenseRoll, attackerName, defenderName) {
|
||||
let totalBonus = 0
|
||||
let keepOffering = true
|
||||
|
||||
while (keepOffering && currentAttackRoll + totalBonus <= defenseRoll) {
|
||||
const currentGrit = attacker.system.grit.current
|
||||
|
||||
// Si plus de points de Grit disponibles, sortir
|
||||
if (currentGrit <= 0) {
|
||||
break
|
||||
}
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
action: "grit",
|
||||
label: `Spend 1 Grit (+1D6) [${currentGrit} left]`,
|
||||
icon: "fa-solid fa-fist-raised",
|
||||
callback: () => "grit"
|
||||
},
|
||||
{
|
||||
action: "continue",
|
||||
label: "Continue (no bonus)",
|
||||
icon: "fa-solid fa-forward",
|
||||
callback: () => "continue"
|
||||
}
|
||||
]
|
||||
|
||||
const content = `
|
||||
<div class="grit-luck-dialog">
|
||||
<div class="combat-status">
|
||||
<p><strong>${attackerName}</strong> currently has <strong>${currentAttackRoll + totalBonus}</strong></p>
|
||||
<p><strong>${defenderName}</strong> rolled <strong>${defenseRoll}</strong></p>
|
||||
${totalBonus > 0 ? `<p class="bonus-info">Bonus already added: +${totalBonus}</p>` : ''}
|
||||
</div>
|
||||
<p class="offer-text">You are losing! Spend Grit to add 1D6 to your attack?</p>
|
||||
</div>
|
||||
`
|
||||
|
||||
const choice = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Attack with Grit" },
|
||||
classes: ["lethalfantasy"],
|
||||
content,
|
||||
buttons,
|
||||
rejectClose: false
|
||||
})
|
||||
|
||||
if (!choice || choice === "continue") {
|
||||
keepOffering = false
|
||||
break
|
||||
}
|
||||
|
||||
// Lancer 1D6
|
||||
const bonusRoll = new Roll("1d6")
|
||||
await bonusRoll.evaluate()
|
||||
|
||||
if (game?.dice3d) {
|
||||
await game.dice3d.showForRoll(bonusRoll, game.user, true)
|
||||
}
|
||||
|
||||
totalBonus += bonusRoll.total
|
||||
|
||||
// Déduire le point de Grit
|
||||
await attacker.update({ "system.grit.current": currentGrit - 1 })
|
||||
await ChatMessage.create({
|
||||
content: `<p><strong>${attackerName}</strong> spends 1 Grit and rolls <strong>${bonusRoll.total}</strong>! (Total attack bonus: +${totalBonus})</p>`,
|
||||
speaker: ChatMessage.getSpeaker({ actor: attacker })
|
||||
})
|
||||
}
|
||||
|
||||
return totalBonus
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static async compareAttackDefense(data) {
|
||||
console.log("compareAttackDefense called with:", data)
|
||||
const isAttackWin = data.attackRoll > data.defenseRoll
|
||||
console.log("isAttackWin:", isAttackWin, "attackRoll:", data.attackRoll, "defenseRoll:", data.defenseRoll)
|
||||
|
||||
let damageButton = ""
|
||||
if (isAttackWin && (data.attackWeaponId || data.attackRollKey)) {
|
||||
console.log("Creating damage button. defenderId:", data.defenderId)
|
||||
// Déterminer le type de dégâts à lancer
|
||||
if (data.attackRollType === "weapon-attack") {
|
||||
damageButton = `
|
||||
<div class="attack-result-damage">
|
||||
<button class="roll-damage-btn" data-attacker-id="${data.attackerId}" data-defender-id="${data.defenderId}" data-weapon-id="${data.attackWeaponId}" data-damage-type="small">
|
||||
<i class="fa-solid fa-dice-d6"></i> Damage (Small)
|
||||
</button>
|
||||
<button class="roll-damage-btn" data-attacker-id="${data.attackerId}" data-defender-id="${data.defenderId}" data-weapon-id="${data.attackWeaponId}" data-damage-type="medium">
|
||||
<i class="fa-solid fa-dice-d20"></i> Damage (Medium)
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
} else if (data.attackRollType === "monster-attack") {
|
||||
damageButton = `
|
||||
<div class="attack-result-damage">
|
||||
<button class="roll-damage-btn" data-attacker-id="${data.attackerId}" data-defender-id="${data.defenderId}" data-attack-key="${data.attackRollKey}" data-damage-type="monster">
|
||||
<i class="fa-solid fa-burst"></i> Damage
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
const resultMessage = `
|
||||
<div class="attack-result ${isAttackWin ? 'attack-success' : 'attack-failure'}">
|
||||
<h3><i class="fa-solid ${isAttackWin ? 'fa-sword' : 'fa-shield'}"></i> Combat Result</h3>
|
||||
<div class="combat-comparison">
|
||||
<div class="combat-side attacker ${isAttackWin ? 'winner' : 'loser'}">
|
||||
<div class="side-label">Attacker</div>
|
||||
<div class="side-info">
|
||||
<div class="side-name">${data.attackerName}</div>
|
||||
<div class="side-roll">${data.attackRoll}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="combat-vs">VS</div>
|
||||
<div class="combat-side defender ${isAttackWin ? 'loser' : 'winner'}">
|
||||
<div class="side-label">Defender</div>
|
||||
<div class="side-info">
|
||||
<div class="side-name">${data.defenderName}</div>
|
||||
<div class="side-roll">${data.defenseRoll}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="combat-result-text">
|
||||
${isAttackWin ?
|
||||
`<i class="fa-solid fa-circle-check"></i> <strong>${data.attackerName}</strong> hits <strong>${data.defenderName}</strong>!` :
|
||||
`<i class="fa-solid fa-shield-halved"></i> <strong>${data.defenderName}</strong> parries the attack!`
|
||||
}
|
||||
</div>
|
||||
${damageButton}
|
||||
</div>
|
||||
`
|
||||
|
||||
console.log("Creating combat result message...")
|
||||
await ChatMessage.create({
|
||||
content: resultMessage,
|
||||
speaker: { alias: "Combat System" }
|
||||
})
|
||||
console.log("Combat result message created!")
|
||||
}
|
||||
|
||||
static registerHandlebarsHelpers() {
|
||||
|
||||
Handlebars.registerHelper('isNull', function (val) {
|
||||
@@ -328,16 +793,26 @@ export default class LethalFantasyUtils {
|
||||
// Message de confirmation
|
||||
let drText = ""
|
||||
if (result.drType === "armor") {
|
||||
drText = ` (Armor DR: ${armorDR})`
|
||||
drText = `Armor DR: ${armorDR}`
|
||||
} else if (result.drType === "all") {
|
||||
drText = ` (Total DR: ${totalDR})`
|
||||
drText = `Total DR: ${totalDR}`
|
||||
}
|
||||
|
||||
const messageContent = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-lethal-fantasy/templates/damage-applied-message.hbs",
|
||||
{
|
||||
targetName: targetActor.name,
|
||||
damage: result.damage,
|
||||
drText: drText,
|
||||
weaponName: weaponName
|
||||
}
|
||||
)
|
||||
|
||||
ChatMessage.create({
|
||||
user: game.user.id,
|
||||
speaker: { alias: targetActor.name },
|
||||
rollMode: "gmroll",
|
||||
content: `${targetActor.name} takes ${result.damage} damage${drText} from ${weaponName}`
|
||||
content: messageContent
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user