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:
@@ -110,6 +110,9 @@ function preLocalizeConfig() {
|
||||
Hooks.once("ready", function () {
|
||||
console.info("LETHAL FANTASY | Ready")
|
||||
|
||||
// Initialiser la table des résultats D30
|
||||
documents.D30Roll.initialize()
|
||||
|
||||
if (!SYSTEM.DEV_MODE) {
|
||||
registerWorldCount("lethalFantasy")
|
||||
}
|
||||
@@ -186,22 +189,148 @@ Hooks.on(hookName, (message, html, data) => {
|
||||
})
|
||||
}
|
||||
|
||||
// Gestionnaire pour les boutons de jet de dégâts
|
||||
$(html).find(".damage-roll-btn").click(async (event) => {
|
||||
// Gestion du survol et du clic sur les boutons de défense
|
||||
$(html).find(".request-defense-btn").hover(
|
||||
function (event) {
|
||||
// Mouse enter - select the token and pan to it
|
||||
let tokenId = $(this).data("token-id")
|
||||
if (tokenId) {
|
||||
let token = canvas.tokens.get(tokenId)
|
||||
if (token) {
|
||||
token.control({ releaseOthers: true })
|
||||
canvas.animatePan(token.center)
|
||||
}
|
||||
}
|
||||
},
|
||||
function (event) {
|
||||
// Mouse leave - release selection
|
||||
canvas.tokens.releaseAll()
|
||||
}
|
||||
)
|
||||
|
||||
// Gestionnaire pour les boutons de demande de défense
|
||||
$(html).find(".request-defense-btn").off("click").on("click", (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const button = $(event.currentTarget)
|
||||
const combatantId = button.data("combatant-id")
|
||||
const tokenId = button.data("token-id")
|
||||
|
||||
// Récupérer le combattant soit du combat, soit directement du token
|
||||
let combatant = null
|
||||
let token = null
|
||||
|
||||
if (game.combat && combatantId) {
|
||||
combatant = game.combat.combatants.get(combatantId)
|
||||
}
|
||||
|
||||
// Si pas de combattant trouvé, chercher le token directement
|
||||
if (!combatant && tokenId) {
|
||||
token = canvas.tokens.get(tokenId)
|
||||
if (token) {
|
||||
// Créer un pseudo-combattant avec les infos du token
|
||||
combatant = {
|
||||
actor: token.actor,
|
||||
name: token.name,
|
||||
token: token,
|
||||
actorId: token.actorId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!combatant) return
|
||||
|
||||
// Récupérer les informations de l'attaquant depuis le message
|
||||
const attackerName = message.rolls[0]?.actorName || "Unknown"
|
||||
const attackerId = message.rolls[0]?.actorId
|
||||
const weaponName = message.rolls[0]?.rollName || "weapon"
|
||||
const attackRoll = message.rolls[0]?.rollTotal || 0
|
||||
const defenderName = combatant.name
|
||||
const attackWeaponId = message.rolls[0]?.rollTarget?.weapon?.id || message.rolls[0]?.rollTarget?.weapon?._id
|
||||
const attackRollType = message.rolls[0]?.type
|
||||
const attackRollKey = message.rolls[0]?.rollTarget?.rollKey
|
||||
|
||||
// Préparer le message de demande de défense
|
||||
const defenseMsg = {
|
||||
type: "requestDefense",
|
||||
attackerName: attackerName,
|
||||
attackerId: attackerId,
|
||||
defenderName: defenderName,
|
||||
weaponName: weaponName,
|
||||
attackRoll: attackRoll,
|
||||
attackWeaponId: attackWeaponId,
|
||||
attackRollType: attackRollType,
|
||||
attackRollKey: attackRollKey,
|
||||
combatantId: combatantId,
|
||||
tokenId: tokenId
|
||||
}
|
||||
|
||||
// Envoyer le message socket à l'utilisateur contrôlant le combatant
|
||||
const owners = game.users.filter(u =>
|
||||
combatant.actor.testUserPermission(u, "OWNER")
|
||||
)
|
||||
|
||||
// Récupérer l'acteur attaquant pour vérifier qui l'a lancé
|
||||
const attacker = game.actors.get(attackerId)
|
||||
const attackerOwners = attacker ? game.users.filter(u => attacker.testUserPermission(u, "OWNER")).map(u => u.id) : []
|
||||
|
||||
let messageSent = false
|
||||
owners.forEach(owner => {
|
||||
// Ne pas afficher le dialogue à l'attaquant lui-même s'il contrôle aussi le défenseur
|
||||
if (attackerOwners.includes(owner.id) && owner.id === game.user.id) {
|
||||
// Ne rien faire - on ne veut pas que l'attaquant se défende contre lui-même
|
||||
return
|
||||
}
|
||||
|
||||
if (owner.id === game.user.id) {
|
||||
// Si l'utilisateur actuel est le propriétaire du défenseur (mais pas l'attaquant), appeler directement
|
||||
LethalFantasyUtils.showDefenseRequest({ ...defenseMsg, userId: owner.id })
|
||||
messageSent = true
|
||||
} else {
|
||||
// Sinon, envoyer via socket
|
||||
game.socket.emit(`system.${SYSTEM.id}`, {
|
||||
...defenseMsg,
|
||||
userId: owner.id
|
||||
})
|
||||
messageSent = true
|
||||
}
|
||||
})
|
||||
|
||||
// Notification pour l'attaquant
|
||||
if (messageSent) {
|
||||
ui.notifications.info(`Defense request sent to ${defenderName}'s controller`)
|
||||
}
|
||||
})
|
||||
|
||||
// Gestionnaire pour les boutons de jet de dégâts (armes et résultats de combat)
|
||||
$(html).find(".damage-roll-btn, .roll-damage-btn").off("click").on("click", async (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const button = $(event.currentTarget)
|
||||
const weaponId = button.data("weapon-id")
|
||||
const attackKey = button.data("attack-key")
|
||||
let attackerId = button.data("attacker-id")
|
||||
const defenderId = button.data("defender-id")
|
||||
const damageType = button.data("damage-type")
|
||||
const damageFormula = button.data("damage-formula")
|
||||
const damageModifier = button.data("damage-modifier")
|
||||
const isMonster = button.data("is-monster")
|
||||
|
||||
// Récupérer l'acteur qui a fait le jet initial
|
||||
const actor = game.actors.get(message.rolls[0]?.actorId)
|
||||
// Récupérer l'acteur (soit depuis le message, soit depuis attackerId)
|
||||
let actor = attackerId ? game.actors.get(attackerId) : game.actors.get(message.rolls[0]?.actorId)
|
||||
if (!actor) {
|
||||
ui.notifications.error("Actor not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Pour les boutons de résultat de combat (monster damage)
|
||||
if (damageType === "monster" && attackKey) {
|
||||
await actor.system.prepareMonsterRoll("monster-damage", attackKey, undefined, undefined, undefined, defenderId)
|
||||
return
|
||||
}
|
||||
|
||||
// Pour les monstres, utiliser prepareMonsterRoll
|
||||
if (isMonster || actor.type === "monster") {
|
||||
await actor.system.prepareMonsterRoll("monster-damage", weaponId, undefined, undefined, damageModifier)
|
||||
@@ -217,7 +346,21 @@ Hooks.on(hookName, (message, html, data) => {
|
||||
|
||||
// Lancer les dégâts avec la bonne méthode
|
||||
const rollType = damageType === "small" ? "weapon-damage-small" : "weapon-damage-medium"
|
||||
await actor.prepareRoll(rollType, weaponId)
|
||||
await actor.prepareRoll(rollType, weaponId, undefined, defenderId)
|
||||
})
|
||||
|
||||
// Masquer les boutons de dommages dans les messages de résultat de combat si l'utilisateur n'est pas l'attaquant
|
||||
$(html).find(".roll-damage-btn").each(function() {
|
||||
const button = $(this)
|
||||
const attackerId = button.data("attacker-id")
|
||||
|
||||
if (attackerId) {
|
||||
const attacker = game.actors.get(attackerId)
|
||||
// Masquer le bouton si l'utilisateur n'est pas GM et ne possède pas l'attaquant
|
||||
if (!game.user.isGM && !attacker?.testUserPermission(game.user, "OWNER")) {
|
||||
button.hide()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -225,6 +368,209 @@ Hooks.on("getCombatTrackerEntryContext", (html, options) => {
|
||||
LethalFantasyUtils.pushCombatOptions(html, options);
|
||||
});
|
||||
|
||||
// Hook pour ajouter les données d'attaque au message de défense
|
||||
Hooks.on("preCreateChatMessage", (message) => {
|
||||
const rollType = message.rolls[0]?.options?.rollType
|
||||
|
||||
// Si c'est un message de défense et qu'on a des données en attente
|
||||
if ((rollType === "weapon-defense" || rollType === "monster-defense") && game.lethalFantasy?.nextDefenseData) {
|
||||
// Ajouter les données dans les flags du message
|
||||
message.updateSource({
|
||||
[`flags.${SYSTEM.id}.attackData`]: game.lethalFantasy.nextDefenseData
|
||||
})
|
||||
|
||||
console.log("Added attack data to defense message:", game.lethalFantasy.nextDefenseData)
|
||||
|
||||
// Nettoyer
|
||||
delete game.lethalFantasy.nextDefenseData
|
||||
}
|
||||
})
|
||||
|
||||
// Hook global pour gérer l'offre de Grit à l'attaquant après une défense
|
||||
Hooks.on("createChatMessage", async (message) => {
|
||||
const rollType = message.rolls[0]?.options?.rollType
|
||||
|
||||
console.log("Defense hook checking message, rollType:", rollType)
|
||||
|
||||
// Vérifier si c'est un message de défense
|
||||
if (rollType !== "weapon-defense" && rollType !== "monster-defense") return
|
||||
|
||||
// Récupérer les données d'attaque depuis les flags
|
||||
const attackData = message.flags?.[SYSTEM.id]?.attackData
|
||||
|
||||
console.log("Defense message confirmed, attackData:", attackData)
|
||||
|
||||
if (!attackData) {
|
||||
console.log("No attack data found in message flags")
|
||||
return
|
||||
}
|
||||
|
||||
const { attackerId, attackRoll, attackerName, defenderName, attackWeaponId, attackRollType, attackRollKey, defenderId } = attackData
|
||||
let defenseRoll = message.rolls[0]?.options?.rollTotal || message.rolls[0]?.total || 0
|
||||
|
||||
console.log("Processing defense:", { attackRoll, defenseRoll, attackerId, defenderId })
|
||||
|
||||
// Attendre l'animation 3D
|
||||
if (game?.dice3d) {
|
||||
await game.dice3d.waitFor3DAnimationByMessageID(message.id)
|
||||
}
|
||||
|
||||
// Récupérer le défenseur et l'attaquant
|
||||
const defender = game.actors.get(defenderId)
|
||||
const attacker = game.actors.get(attackerId)
|
||||
|
||||
// Si le défenseur est un personnage qui perd, proposer Grit/Luck (seulement s'il a des points)
|
||||
// Seulement si l'utilisateur actuel est le propriétaire du défenseur
|
||||
let defenderHandledBonus = false
|
||||
if (defender?.type === "character" && defenseRoll < attackRoll && !game.user.isGM && defender.isOwner) {
|
||||
const hasGritOrLuck = (defender.system.grit.current > 0) || (defender.system.luck.current > 0)
|
||||
|
||||
if (hasGritOrLuck) {
|
||||
const bonusRoll = await LethalFantasyUtils.offerGritLuckBonus(
|
||||
defender,
|
||||
attackRoll,
|
||||
defenseRoll,
|
||||
attackerName,
|
||||
defenderName
|
||||
)
|
||||
if (bonusRoll > 0) {
|
||||
defenseRoll += bonusRoll
|
||||
}
|
||||
}
|
||||
defenderHandledBonus = true
|
||||
}
|
||||
|
||||
let attackRollFinal = attackRoll
|
||||
let attackerHandledBonus = false
|
||||
|
||||
// Si l'attaquant est un personnage qui perd et a du Grit
|
||||
// Seulement si l'utilisateur actuel est le propriétaire de l'attaquant (pas le MJ)
|
||||
if (attacker?.type === "character" && attackRollFinal <= defenseRoll && attacker.system.grit.current > 0) {
|
||||
// Vérifier si l'utilisateur est un propriétaire non-GM de l'attaquant
|
||||
const isAttackerOwner = !game.user.isGM && attacker.testUserPermission(game.user, "OWNER")
|
||||
|
||||
if (isAttackerOwner) {
|
||||
console.log("Offering Grit to attacker")
|
||||
|
||||
const attackBonus = await LethalFantasyUtils.offerAttackerGritBonus(
|
||||
attacker,
|
||||
attackRollFinal,
|
||||
defenseRoll,
|
||||
attackerName,
|
||||
defenderName
|
||||
)
|
||||
|
||||
attackRollFinal += attackBonus
|
||||
attackerHandledBonus = true
|
||||
} else {
|
||||
console.log("Not attacker owner or is GM, skipping Grit offer")
|
||||
}
|
||||
}
|
||||
|
||||
// Créer le message de comparaison - uniquement par le client qui a géré le dernier bonus
|
||||
// Priorité: attaquant si il a géré le bonus, sinon défenseur si il a géré le bonus, sinon défenseur
|
||||
const shouldCreateMessage = attackerHandledBonus || (!attackerHandledBonus && defenderHandledBonus) || (!attackerHandledBonus && !defenderHandledBonus && defender.isOwner)
|
||||
|
||||
if (shouldCreateMessage) {
|
||||
console.log("Creating comparison message", { attackerHandledBonus, defenderHandledBonus, isDefenderOwner: defender.isOwner })
|
||||
|
||||
await LethalFantasyUtils.compareAttackDefense({
|
||||
attackerName,
|
||||
attackerId,
|
||||
attackRoll: attackRollFinal,
|
||||
attackWeaponId,
|
||||
attackRollType,
|
||||
attackRollKey,
|
||||
defenderName,
|
||||
defenderId,
|
||||
defenseRoll
|
||||
})
|
||||
} else {
|
||||
console.log("Skipping message creation", { attackerHandledBonus, defenderHandledBonus })
|
||||
}
|
||||
})
|
||||
|
||||
// Hook pour appliquer automatiquement les dégâts si une cible est définie
|
||||
Hooks.on("createChatMessage", async (message) => {
|
||||
// Vérifier si c'est un message de dégâts avec un defenderId
|
||||
const defenderId = message.rolls[0]?.options?.defenderId
|
||||
const isDamage = message.rolls[0]?.options?.rollData?.isDamage
|
||||
|
||||
console.log("Auto-damage hook:", { defenderId, isDamage, rollType: message.rolls[0]?.options?.rollType })
|
||||
|
||||
if (!defenderId || !isDamage) return
|
||||
|
||||
// Récupérer l'attaquant depuis le roll
|
||||
const attackerId = message.rolls[0]?.options?.actorId
|
||||
const attacker = attackerId ? game.actors.get(attackerId) : null
|
||||
|
||||
// Déterminer qui doit appliquer les dégâts :
|
||||
// 1. Si l'attaquant a un propriétaire joueur, seul ce joueur applique
|
||||
// 2. Si l'attaquant n'a que le MJ comme propriétaire (monstre), seul le MJ applique
|
||||
const attackerOwners = attacker ? game.users.filter(u =>
|
||||
!u.isGM && attacker.testUserPermission(u, "OWNER")
|
||||
) : []
|
||||
|
||||
let shouldApplyDamage = false
|
||||
if (attackerOwners.length > 0) {
|
||||
// L'attaquant a des propriétaires joueurs, seul le premier propriétaire applique
|
||||
shouldApplyDamage = attackerOwners[0].id === game.user.id
|
||||
} else {
|
||||
// L'attaquant n'a que le MJ, seul le MJ applique
|
||||
shouldApplyDamage = game.user.isGM
|
||||
}
|
||||
|
||||
if (!shouldApplyDamage) {
|
||||
console.log("Auto-damage hook: Not responsible for applying damage, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
console.log("Auto-damage hook: Applying damage as responsible user")
|
||||
|
||||
// Attendre l'animation 3D avant d'appliquer les dégâts
|
||||
if (game?.dice3d) {
|
||||
await game.dice3d.waitFor3DAnimationByMessageID(message.id)
|
||||
}
|
||||
|
||||
// Récupérer le défenseur
|
||||
const defender = game.actors.get(defenderId)
|
||||
if (!defender) {
|
||||
console.warn("Defender not found:", defenderId)
|
||||
return
|
||||
}
|
||||
|
||||
// Récupérer les dégâts (utiliser rollTotal qui contient le total calculé)
|
||||
const damageTotal = message.rolls[0]?.options?.rollTotal || message.rolls[0]?.total || 0
|
||||
const weaponName = message.rolls[0]?.options?.rollName || "Unknown Weapon"
|
||||
const attackerName = message.rolls[0]?.options?.actorName || "Unknown Attacker"
|
||||
|
||||
// Calculer les DR
|
||||
const armorDR = defender.computeDamageReduction() || 0
|
||||
|
||||
// Appliquer les dégâts avec armure DR par défaut
|
||||
const finalDamage = Math.max(0, damageTotal - armorDR)
|
||||
await defender.applyDamage(-finalDamage)
|
||||
|
||||
// Créer un message de confirmation
|
||||
const messageContent = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-lethal-fantasy/templates/damage-applied-message.hbs",
|
||||
{
|
||||
targetName: defender.name,
|
||||
damage: finalDamage,
|
||||
drText: armorDR > 0 ? `Armor DR: ${armorDR}` : "",
|
||||
weaponName: weaponName,
|
||||
attackerName: attackerName,
|
||||
rawDamage: damageTotal
|
||||
}
|
||||
)
|
||||
|
||||
await ChatMessage.create({
|
||||
content: messageContent,
|
||||
speaker: ChatMessage.getSpeaker({ actor: defender })
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* Create a macro when dropping an entity on the hotbar
|
||||
* Item - open roll dialog
|
||||
|
||||
Reference in New Issue
Block a user