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:
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