Improve init for monsters and some fixwes around shields
Release Creation / build (release) Successful in 48s

This commit is contained in:
2026-05-17 13:22:29 +02:00
parent 54421e4a83
commit db3e8b5d35
36 changed files with 366 additions and 352 deletions
+215 -241
View File
@@ -129,310 +129,284 @@ Hooks.once("ready", function () {
}
})
// Test if version below 13
let hookName = "renderChatMessage"
if (foundry.utils.isNewerVersion(game.version, "12.0",)) {
hookName = "renderChatMessageHTML"
}
Hooks.on(hookName, (message, html, data) => {
Hooks.on("renderChatMessageHTML", (message, html, data) => {
const typeMessage = data.message.flags.lethalFantasy?.typeMessage
// Message de demande de jet de dés
if (typeMessage === "askRoll") {
// Affichage des boutons de jet de dés uniquement pour les joueurs
if (game.user.isGM) {
html.find(".ask-roll-dice").each((i, btn) => {
for (const btn of html.querySelectorAll(".ask-roll-dice")) {
btn.style.display = "none"
})
}
} else {
html.find(".ask-roll-dice").click((event) => {
const btn = $(event.currentTarget)
const type = btn.data("type")
const value = btn.data("value")
const avantage = btn.data("avantage") ?? "="
const character = game.user.character
if (type === SYSTEM.ROLL_TYPE.RESOURCE) character.rollResource(value)
else if (type === SYSTEM.ROLL_TYPE.SAVE) character.rollSave(value, avantage)
})
for (const btn of html.querySelectorAll(".ask-roll-dice")) {
btn.addEventListener("click", () => {
const type = btn.dataset.type
const value = btn.dataset.value
const avantage = btn.dataset.avantage ?? "="
const character = game.user.character
if (type === SYSTEM.ROLL_TYPE.RESOURCE) character.rollResource(value)
else if (type === SYSTEM.ROLL_TYPE.SAVE) character.rollSave(value, avantage)
})
}
}
}
// Gestion du survol et du clic sur les boutons de dégâts pour les GMs
if (game.user.isGM) {
// Show damage buttons only for GM
$(html).find(".li-apply-wounds").each((i, btn) => {
for (const btn of html.querySelectorAll(".li-apply-wounds")) {
btn.style.display = "block"
})
}
$(html).find(".apply-wounds-btn").hover(
function (event) {
// Mouse enter - select the token and pan to it
let combatantId = $(this).data("combatant-id")
for (const btn of html.querySelectorAll(".apply-wounds-btn")) {
btn.addEventListener("mouseenter", () => {
const combatantId = btn.dataset.combatantId
if (combatantId && game.combat) {
let combatant = game.combat.combatants.get(combatantId)
const combatant = game.combat.combatants.get(combatantId)
if (combatant?.token) {
let token = canvas.tokens.get(combatant.token.id)
const token = canvas.tokens.get(combatant.token.id)
if (token) {
token.control({ releaseOthers: true })
canvas.animatePan(token.center)
}
}
}
},
function (event) {
// Mouse leave - release selection
canvas.tokens.releaseAll()
}
)
$(html).find(".apply-wounds-btn").click((event) => {
LethalFantasyUtils.applyDamage(message, event)
})
})
btn.addEventListener("mouseleave", () => canvas.tokens.releaseAll())
btn.addEventListener("click", event => LethalFantasyUtils.applyDamage(message, 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")
for (const btn of html.querySelectorAll(".request-defense-btn")) {
btn.addEventListener("mouseenter", () => {
const tokenId = btn.dataset.tokenId
if (tokenId) {
let token = canvas.tokens.get(tokenId)
const token = canvas.tokens.get(tokenId)
if (token) {
token.control({ releaseOthers: true })
canvas.animatePan(token.center)
}
}
},
function (event) {
// Mouse leave - release selection
canvas.tokens.releaseAll()
}
)
})
btn.addEventListener("mouseleave", () => 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()
// Gestionnaire pour les boutons de demande de défense
btn.addEventListener("click", event => {
event.preventDefault()
event.stopPropagation()
const button = $(event.currentTarget)
const combatantId = button.data("combatant-id")
const tokenId = button.data("token-id")
const combatantId = btn.dataset.combatantId
const tokenId = btn.dataset.tokenId
// Récupérer le combattant soit du combat, soit directement du token
let combatant = null
let token = null
// 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)
}
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
// 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
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
console.log(`[LF] request-defense-btn | attackRollType=${attackRollType} defender=${defenderName} defenderType=${combatant.actor?.type}`)
const attackD30result = message.rolls[0]?.options?.D30result || null
const attackD30message = message.rolls[0]?.options?.D30message || null
const attackRerollContext = {
rollType: message.rolls[0]?.options?.rollType,
rollTarget: foundry.utils.duplicate(message.rolls[0]?.options?.rollTarget ?? {}),
actorId: message.rolls[0]?.options?.actorId,
actorName: message.rolls[0]?.options?.actorName,
actorImage: message.rolls[0]?.options?.actorImage,
defenderId: combatant.actor?.id || null,
defenderTokenId: tokenId || combatant.token?.id || null,
rollContext: foundry.utils.duplicate(message.rolls[0]?.options?.rollData ?? {})
}
// 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
console.log(`[LF] request-defense-btn | attackRollType=${attackRollType} defender=${defenderName} defenderType=${combatant.actor?.type}`)
const attackD30result = message.rolls[0]?.options?.D30result || null
const attackD30message = message.rolls[0]?.options?.D30message || null
const attackRerollContext = {
rollType: message.rolls[0]?.options?.rollType,
rollTarget: foundry.utils.duplicate(message.rolls[0]?.options?.rollTarget ?? {}),
actorId: message.rolls[0]?.options?.actorId,
actorName: message.rolls[0]?.options?.actorName,
actorImage: message.rolls[0]?.options?.actorImage,
defenderId: combatant.actor?.id || null,
defenderTokenId: tokenId || combatant.token?.id || null,
rollContext: foundry.utils.duplicate(message.rolls[0]?.options?.rollData ?? {})
}
// Préparer le message de demande de défense
// isRanged: true si le monstre était en mode ranged (via rollTarget.attackMode stocké dans le roll)
// OU si l'attaquant utilisait une arme ranged (weapon-attack avec weaponType === "ranged")
const attacker = game.actors.get(attackerId)
const rollTargetOptions = message.rolls[0]?.options?.rollTarget
const attackerWeapon = rollTargetOptions?.weapon
const isRangedAttack = (rollTargetOptions?.attackMode === "ranged")
|| (attacker?.type === "monster" && attacker.system.attackMode === "ranged")
|| (attackerWeapon?.system?.weaponType === "ranged")
// Préparer le message de demande de défense
// isRanged: true si le monstre était en mode ranged (via rollTarget.attackMode stocké dans le roll)
// OU si l'attaquant utilisait une arme ranged (weapon-attack avec weaponType === "ranged")
const attacker = game.actors.get(attackerId)
const rollTargetOptions = message.rolls[0]?.options?.rollTarget
const attackerWeapon = rollTargetOptions?.weapon
const isRangedAttack = (rollTargetOptions?.attackMode === "ranged")
|| (attacker?.type === "monster" && attacker.system.attackMode === "ranged")
|| (attackerWeapon?.system?.weaponType === "ranged")
const defenseMsg = {
type: "requestDefense",
attackerName: attackerName,
attackerId: attackerId,
defenderName: defenderName,
weaponName: weaponName,
attackRoll: attackRoll,
attackWeaponId: attackWeaponId,
attackRollType: attackRollType,
attackRollKey: attackRollKey,
attackD30result: attackD30result,
attackD30message: attackD30message,
attackRerollContext: attackRerollContext,
combatantId: combatantId,
tokenId: tokenId,
isRanged: isRangedAttack
}
const defenseMsg = {
type: "requestDefense",
attackerName,
attackerId,
defenderName,
weaponName,
attackRoll,
attackWeaponId,
attackRollType,
attackRollKey,
attackD30result,
attackD30message,
attackRerollContext,
combatantId,
tokenId,
isRanged: isRangedAttack
}
// Envoyer le message socket à l'utilisateur contrôlant le combatant
// Only consider active (online) users; fall back to any active GM for unowned/GM monsters.
let owners = game.users.filter(u =>
u.active && combatant.actor.testUserPermission(u, "OWNER")
)
if (owners.length === 0) {
owners = game.users.filter(u => u.active && u.isGM)
}
// Envoyer le message socket à l'utilisateur contrôlant le combatant
// Only consider active (online) users; fall back to any active GM for unowned/GM monsters.
let owners = game.users.filter(u => u.active && combatant.actor.testUserPermission(u, "OWNER"))
if (owners.length === 0) {
owners = game.users.filter(u => u.active && u.isGM)
}
// Récupérer l'acteur attaquant pour vérifier qui l'a lancé
const attackerOwners = attacker ? game.users.filter(u => attacker.testUserPermission(u, "OWNER")).map(u => u.id) : []
// Récupérer l'acteur attaquant pour vérifier qui l'a lancé
const attackerOwners = attacker ? game.users.filter(u => attacker.testUserPermission(u, "OWNER")).map(u => u.id) : []
// Monsters always need their owner (usually the GM) to roll a save/defense,
// even if that owner also controls the attacker. Only skip for same-player PC-vs-PC.
const defenderIsMonster = combatant.actor?.type === "monster"
// Monsters always need their owner (usually the GM) to roll a save/defense,
// even if that owner also controls the attacker. Only skip for same-player PC-vs-PC.
const defenderIsMonster = combatant.actor?.type === "monster"
let messageSent = false
owners.forEach(owner => {
// Don't let a player be both attacker and defender for their own PC, unless defending a monster.
if (attackerOwners.includes(owner.id) && owner.id === game.user.id && !defenderIsMonster) {
// Ne rien faire - on ne veut pas que l'attaquant se défende contre lui-même
let messageSent = false
owners.forEach(owner => {
// Don't let a player be both attacker and defender for their own PC, unless defending a monster.
if (attackerOwners.includes(owner.id) && owner.id === game.user.id && !defenderIsMonster) {
// 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)
for (const btn of html.querySelectorAll(".damage-roll-btn, .roll-damage-btn")) {
btn.addEventListener("click", async event => {
event.preventDefault()
event.stopPropagation()
const weaponId = btn.dataset.weaponId
const attackKey = btn.dataset.attackKey
const attackerId = btn.dataset.attackerId
const defenderId = btn.dataset.defenderId
const defenderTokenId = btn.dataset.defenderTokenId || null
const extraShieldDr = Number(btn.dataset.extraShieldDr || 0)
const damageType = btn.dataset.damageType
const damageFormula = btn.dataset.damageFormula
const damageModifier = btn.dataset.damageModifier
const isMonster = btn.dataset.isMonster
// Récupérer l'acteur (soit depuis le message, soit depuis attackerId)
const actor = attackerId ? game.actors.get(attackerId) : game.actors.get(message.rolls[0]?.actorId)
if (!actor) {
ui.notifications.error("Actor not found")
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
// Pour les sorts, rouler les dés de dégâts avec option bypass DR
if (damageType === "spell" && damageFormula) {
const bypassArmor = await foundry.applications.api.DialogV2.confirm({
window: { title: "Spell Damage" },
classes: ["lethalfantasy"],
content: "<p>Does this spell's damage bypass armor DR?</p>",
yes: { label: "Yes (ignore armor)", icon: "fa-solid fa-wand-magic-sparkles" },
no: { label: "No (apply armor DR)", icon: "fa-solid fa-shield" }
})
messageSent = true
const rollOpts = {
type: "spell-damage",
rollType: "spell-damage",
rollName: damageFormula,
isDamage: true,
rollData: { isDamage: true },
bypassArmor: bypassArmor ?? false,
defenderId,
defenderTokenId,
actorId: actor.id,
actorName: actor.name,
actorImage: actor.img
}
const roll = new LethalFantasyRoll(damageFormula, {}, rollOpts)
await roll.evaluate()
roll.options.rollTotal = roll.total
if (game?.dice3d) await game.dice3d.showForRoll(roll, game.user, true)
await roll.toMessage()
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, defenderTokenId, extraShieldDr)
return
}
// Pour les monstres, utiliser prepareMonsterRoll
if (isMonster || actor.type === "monster") {
await actor.system.prepareMonsterRoll("monster-damage", weaponId, undefined, undefined, damageModifier)
return
}
// Pour les personnages, récupérer l'arme
const weapon = actor.items.get(weaponId)
if (!weapon) {
ui.notifications.error("Weapon not found")
return
}
// 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, undefined, defenderId, defenderTokenId, extraShieldDr)
})
// 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 defenderTokenId = button.data("defender-token-id") || null
const extraShieldDr = Number(button.data("extra-shield-dr") || 0)
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 (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 sorts, rouler les dés de dégâts avec option bypass DR
if (damageType === "spell" && damageFormula) {
const bypassArmor = await foundry.applications.api.DialogV2.confirm({
window: { title: "Spell Damage" },
classes: ["lethalfantasy"],
content: "<p>Does this spell's damage bypass armor DR?</p>",
yes: { label: "Yes (ignore armor)", icon: "fa-solid fa-wand-magic-sparkles" },
no: { label: "No (apply armor DR)", icon: "fa-solid fa-shield" }
})
const rollOpts = {
type: "spell-damage",
rollType: "spell-damage",
rollName: damageFormula,
isDamage: true,
rollData: { isDamage: true },
bypassArmor: bypassArmor ?? false,
defenderId,
defenderTokenId,
actorId: actor.id,
actorName: actor.name,
actorImage: actor.img
}
const roll = new LethalFantasyRoll(damageFormula, {}, rollOpts)
await roll.evaluate()
roll.options.rollTotal = roll.total
if (game?.dice3d) await game.dice3d.showForRoll(roll, game.user, true)
await roll.toMessage()
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, defenderTokenId, extraShieldDr)
return
}
// Pour les monstres, utiliser prepareMonsterRoll
if (isMonster || actor.type === "monster") {
await actor.system.prepareMonsterRoll("monster-damage", weaponId, undefined, undefined, damageModifier)
return
}
// Pour les personnages, récupérer l'arme
const weapon = actor.items.get(weaponId)
if (!weapon) {
ui.notifications.error("Weapon not found")
return
}
// 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, undefined, defenderId, defenderTokenId, extraShieldDr)
})
}
// 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")
for (const btn of html.querySelectorAll(".roll-damage-btn")) {
const attackerId = btn.dataset.attackerId
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()
btn.style.display = "none"
}
}
})
}
})
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