Compare commits
11 Commits
13.0.17
...
6c6c473147
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c6c473147 | |||
| 2e2a917a45 | |||
| 343abc32e2 | |||
| c37d92af25 | |||
| 42945d33db | |||
| 1bf88bac06 | |||
| 3ad5681539 | |||
| df6f8e5710 | |||
| 52877e3a68 | |||
| a06dfa0ae9 | |||
| 0836cada75 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,4 +7,5 @@ styles/*.css
|
||||
node_modules/
|
||||
|
||||
.history
|
||||
.github/
|
||||
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><g class="" style="" transform="translate(0,0)"><path d="M373.47 25.5c-33.475-.064-67.614 13.444-94.44 43.156l37.22 145.156-33.437.032 35.343 132.093-116.718-188.375 50.03 5.375L202.5 47.312C120.437-1.43 4.756 40.396 8.5 158.156c4.402 138.44 191.196 184.6 247.406 331.625 59.376-147.035 251.26-184.33 246.656-331.624-2.564-82.042-64.6-132.532-129.093-132.656z" fill="#fff" fill-opacity="1"></path></g></svg>
|
||||
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<g class="" style="" transform="translate(0,0)">
|
||||
<path
|
||||
d="M373.47 25.5c-33.475-.064-67.614 13.444-94.44 43.156l37.22 145.156-33.437.032 35.343 132.093-116.718-188.375 50.03 5.375L202.5 47.312C120.437-1.43 4.756 40.396 8.5 158.156c4.402 138.44 191.196 184.6 247.406 331.625 59.376-147.035 251.26-184.33 246.656-331.624-2.564-82.042-64.6-132.532-129.093-132.656z"
|
||||
fill="#dc2626" fill-opacity="1"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 506 B After Width: | Height: | Size: 533 B |
10
assets/icons/health-increase.svg
Normal file
10
assets/icons/health-increase.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!-- Heart shape -->
|
||||
<path
|
||||
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"
|
||||
fill="#4ade80" stroke="#22c55e" />
|
||||
<!-- Plus sign inside heart -->
|
||||
<line x1="12" y1="8" x2="12" y2="14" stroke="#ffffff" stroke-width="2.5" />
|
||||
<line x1="9" y1="11" x2="15" y2="11" stroke="#ffffff" stroke-width="2.5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 572 B |
File diff suppressed because it is too large
Load Diff
37
lang/en.json
37
lang/en.json
@@ -283,6 +283,7 @@
|
||||
"Label": {
|
||||
"agility": "Dexterity",
|
||||
"applyDamage": "Apply damage to:",
|
||||
"selectTarget": "Select target for attack:",
|
||||
"rollDamage": "Roll Damage",
|
||||
"gotoToken": "Go to token",
|
||||
"combatAction": "Combat action",
|
||||
@@ -293,13 +294,13 @@
|
||||
"miracle-power": "Miracle - Power",
|
||||
"miracle-attack": "Miracle - Attack",
|
||||
"spell": "Spell",
|
||||
"will":"Will",
|
||||
"dodge":"Dodge",
|
||||
"toughness":"Toughness",
|
||||
"contagion":"Contagion",
|
||||
"poison":"Poison",
|
||||
"pain":"Pain",
|
||||
"paincourage":"Pain/Courage",
|
||||
"will": "Will",
|
||||
"dodge": "Dodge",
|
||||
"toughness": "Toughness",
|
||||
"contagion": "Contagion",
|
||||
"poison": "Poison",
|
||||
"pain": "Pain",
|
||||
"paincourage": "Pain/Courage",
|
||||
"granted": "Granted Dice",
|
||||
"shields": "Shields",
|
||||
"armorHitPoints": "Armor hit points",
|
||||
@@ -319,7 +320,7 @@
|
||||
"aetherPoints": "Aether points",
|
||||
"attacks": "Attacks",
|
||||
"monster": "Monster",
|
||||
"Resist" :"Resist",
|
||||
"Resist": "Resist",
|
||||
"resist": "Resist",
|
||||
"resistTorture": "Resist torture",
|
||||
"resistPerformance": "Resist performance",
|
||||
@@ -427,7 +428,8 @@
|
||||
"weapons": "Weapons",
|
||||
"wis": "WIS",
|
||||
"weapon-damage-medium": "Weapon damage medium",
|
||||
"weapon-damage-small": "Weapon damage small"
|
||||
"weapon-damage-small": "Weapon damage small",
|
||||
"combatProgressionStart": "Combat start threshold"
|
||||
},
|
||||
"Miracle": {
|
||||
"FIELDS": {
|
||||
@@ -721,6 +723,9 @@
|
||||
},
|
||||
"catalyst": {
|
||||
"label": "Catalyst"
|
||||
},
|
||||
"damageDice": {
|
||||
"label": "Damage dice"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -728,7 +733,8 @@
|
||||
"Tooltip": {
|
||||
"addEquipment": "New equipment",
|
||||
"addSpell": "New spells",
|
||||
"skill": "Skills list"
|
||||
"skill": "Skills list",
|
||||
"combatProgressionStart": "First attack of combat can succeed on a roll of 1 through this value. Resets to 1 for subsequent attacks."
|
||||
},
|
||||
"Vulnerability": {
|
||||
"FIELDS": {
|
||||
@@ -876,6 +882,17 @@
|
||||
"withArmor": "With Armor DR only",
|
||||
"withAll": "With Armor + Shield DR",
|
||||
"damage": "damage"
|
||||
},
|
||||
"DamageApplied": {
|
||||
"subtitle": "suffered damage",
|
||||
"damageDealt": "Damage dealt",
|
||||
"from": "from"
|
||||
},
|
||||
"ProgressionMessage": {
|
||||
"canAct": "ready to act!",
|
||||
"cannotAct": "cannot act this second",
|
||||
"diceResult": "Dice result",
|
||||
"progressionCount": "Progression count:"
|
||||
}
|
||||
},
|
||||
"TYPES": {
|
||||
|
||||
@@ -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,199 @@ 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
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
// 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 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 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 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 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)
|
||||
@@ -217,7 +397,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, 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")
|
||||
|
||||
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 +419,471 @@ 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" || rollType === "save") && 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" && rollType !== "save") 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,
|
||||
attackD30message,
|
||||
attackRerollContext,
|
||||
defenderId,
|
||||
defenderTokenId
|
||||
} = attackData
|
||||
let defenseRoll = message.rolls[0]?.options?.rollTotal || message.rolls[0]?.total || 0
|
||||
const defenseD30message = message.rolls[0]?.options?.D30message || null
|
||||
|
||||
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)
|
||||
const defenseRerollContext = {
|
||||
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,
|
||||
defenderTokenId,
|
||||
rollContext: foundry.utils.duplicate(message.rolls[0]?.options?.rollData ?? {})
|
||||
}
|
||||
|
||||
const isPrimaryController = actor => {
|
||||
if (!actor) return false
|
||||
const activePlayerOwners = game.users.filter(u => u.active && !u.isGM && actor.testUserPermission(u, "OWNER"))
|
||||
if (activePlayerOwners.length > 0) {
|
||||
return activePlayerOwners[0].id === game.user.id
|
||||
}
|
||||
return game.user.isGM
|
||||
}
|
||||
|
||||
const createReactionMessage = async (actorDocument, content) => {
|
||||
await ChatMessage.create({
|
||||
content,
|
||||
speaker: ChatMessage.getSpeaker({ actor: actorDocument })
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
let shieldReaction = null
|
||||
let shieldBlocked = false
|
||||
const isSpellOrMiracle = attackRollType === "spell-attack" || attackRollType === "miracle-attack"
|
||||
if (defender && defenseRoll < attackRoll && isPrimaryController(defender) && !isSpellOrMiracle) {
|
||||
const shieldData = LethalFantasyUtils.getShieldReactionData(defender)
|
||||
let canRerollDefense = LethalFantasyUtils.hasD30Reroll(defenseD30message)
|
||||
let canShieldReact = !!shieldData
|
||||
|
||||
while (defenseRoll < attackRoll) {
|
||||
const currentGrit = Number(defender.system?.grit?.current) || 0
|
||||
const currentLuck = Number(defender.system?.luck?.current) || 0
|
||||
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: "bonusDie",
|
||||
label: "Add bonus die",
|
||||
icon: "fa-solid fa-dice",
|
||||
callback: () => "bonusDie"
|
||||
})
|
||||
|
||||
if (canRerollDefense) {
|
||||
buttons.push({
|
||||
action: "rerollDefense",
|
||||
label: "Re-roll defense",
|
||||
icon: "fa-solid fa-rotate-right",
|
||||
callback: () => "rerollDefense"
|
||||
})
|
||||
}
|
||||
|
||||
if (canShieldReact) {
|
||||
buttons.push({
|
||||
action: "shieldReact",
|
||||
label: `Roll shield (${shieldData.label})`,
|
||||
icon: "fa-solid fa-shield",
|
||||
callback: () => "shieldReact"
|
||||
})
|
||||
}
|
||||
|
||||
buttons.push({
|
||||
action: "continue",
|
||||
label: "Continue (no defense bonus)",
|
||||
icon: "fa-solid fa-forward",
|
||||
callback: () => "continue"
|
||||
})
|
||||
|
||||
const choice = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Defense reactions" },
|
||||
classes: ["lethalfantasy"],
|
||||
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>${defenseRoll}</strong></p>
|
||||
${defenseD30message ? `<p class="bonus-info">D30 special: ${defenseD30message}</p>` : ""}
|
||||
</div>
|
||||
<p class="offer-text">Choose how to improve the defense before resolving the hit.</p>
|
||||
</div>
|
||||
`,
|
||||
buttons,
|
||||
rejectClose: false
|
||||
})
|
||||
|
||||
if (!choice || choice === "continue") break
|
||||
|
||||
defenderHandledBonus = true
|
||||
|
||||
if (choice === "grit") {
|
||||
const bonusRoll = await LethalFantasyUtils.rollBonusDie("1d6", defender, (total) => `<p><strong>${defenderName}</strong> spends 1 Grit and rolls <strong>${total}</strong> for defense.</p>`)
|
||||
defenseRoll += bonusRoll
|
||||
await defender.update({ "system.grit.current": currentGrit - 1 })
|
||||
continue
|
||||
}
|
||||
|
||||
if (choice === "luck") {
|
||||
const bonusRoll = await LethalFantasyUtils.rollBonusDie("1d6", defender, (total) => `<p><strong>${defenderName}</strong> spends 1 Luck and rolls <strong>${total}</strong> for defense.</p>`)
|
||||
defenseRoll += bonusRoll
|
||||
await defender.update({ "system.luck.current": currentLuck - 1 })
|
||||
continue
|
||||
}
|
||||
|
||||
if (choice === "bonusDie") {
|
||||
const bonusDie = await LethalFantasyUtils.promptCombatBonusDie(defenderName, "attack", defenseRoll, attackRoll)
|
||||
if (!bonusDie) continue
|
||||
const bonusRoll = await LethalFantasyUtils.rollBonusDie(bonusDie, defender, (total, formula) => `<p><strong>${defenderName}</strong> adds <strong>${formula.toUpperCase()}</strong> and rolls <strong>${total}</strong> for defense.</p>`)
|
||||
defenseRoll += bonusRoll
|
||||
continue
|
||||
}
|
||||
|
||||
if (choice === "rerollDefense" && canRerollDefense) {
|
||||
const oldDefenseRoll = defenseRoll
|
||||
const reroll = await LethalFantasyUtils.rerollConfiguredRoll(defenseRerollContext)
|
||||
canRerollDefense = false
|
||||
if (!reroll) continue
|
||||
defenseRoll = reroll.options?.rollTotal || reroll.total || oldDefenseRoll
|
||||
await createReactionMessage(defender, `<p><strong>${defenderName}</strong> uses Mulligan and re-rolls defense: <strong>${oldDefenseRoll}</strong> → <strong>${defenseRoll}</strong>.</p>`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (choice === "shieldReact" && canShieldReact) {
|
||||
const shieldBonus = await LethalFantasyUtils.rollBonusDie(shieldData.formula, defender)
|
||||
const newDefenseTotal = defenseRoll + shieldBonus
|
||||
defenseRoll = newDefenseTotal
|
||||
canShieldReact = false
|
||||
|
||||
if (newDefenseTotal >= attackRoll) {
|
||||
// Shield roll tied or exceeded the attack — shield blocked
|
||||
shieldBlocked = true
|
||||
shieldReaction = {
|
||||
damageReduction: shieldData.damageReduction,
|
||||
label: shieldData.label,
|
||||
bonus: shieldBonus
|
||||
}
|
||||
await createReactionMessage(
|
||||
defender,
|
||||
`<p><strong>${defenderName}</strong> rolls <strong>${shieldData.label}</strong> and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} ≥ ${attackRoll}). <strong>Shield blocked the attack!</strong> Both armor DR and shield DR <strong>${shieldData.damageReduction}</strong> will apply to damage.</p>`
|
||||
)
|
||||
} else {
|
||||
// Shield roll not enough — hit still lands, armor DR only
|
||||
shieldReaction = null
|
||||
await createReactionMessage(
|
||||
defender,
|
||||
`<p><strong>${defenderName}</strong> rolls <strong>${shieldData.label}</strong> and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} < ${attackRoll}). Shield did not block — normal hit, armor DR only.</p>`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 (!defenderHandledBonus && attacker && attackRollFinal <= defenseRoll && isPrimaryController(attacker)) {
|
||||
let canRerollAttack = LethalFantasyUtils.hasD30Reroll(attackD30message)
|
||||
|
||||
while (attackRollFinal <= defenseRoll) {
|
||||
const currentGrit = Number(attacker.system?.grit?.current) || 0
|
||||
const buttons = []
|
||||
|
||||
if (currentGrit > 0) {
|
||||
buttons.push({
|
||||
action: "grit",
|
||||
label: `Spend 1 Grit (+1D6) [${currentGrit} left]`,
|
||||
icon: "fa-solid fa-fist-raised",
|
||||
callback: () => "grit"
|
||||
})
|
||||
}
|
||||
|
||||
buttons.push({
|
||||
action: "bonusDie",
|
||||
label: "Add bonus die",
|
||||
icon: "fa-solid fa-dice",
|
||||
callback: () => "bonusDie"
|
||||
})
|
||||
|
||||
if (canRerollAttack && attackRerollContext) {
|
||||
buttons.push({
|
||||
action: "rerollAttack",
|
||||
label: "Re-roll attack",
|
||||
icon: "fa-solid fa-rotate-right",
|
||||
callback: () => "rerollAttack"
|
||||
})
|
||||
}
|
||||
|
||||
buttons.push({
|
||||
action: "continue",
|
||||
label: "Continue (no attack bonus)",
|
||||
icon: "fa-solid fa-forward",
|
||||
callback: () => "continue"
|
||||
})
|
||||
|
||||
const choice = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Attack reactions" },
|
||||
classes: ["lethalfantasy"],
|
||||
content: `
|
||||
<div class="grit-luck-dialog">
|
||||
<div class="combat-status">
|
||||
<p><strong>${attackerName}</strong> currently has <strong>${attackRollFinal}</strong></p>
|
||||
<p><strong>${defenderName}</strong> rolled <strong>${defenseRoll}</strong></p>
|
||||
${attackD30message ? `<p class="bonus-info">D30 special: ${attackD30message}</p>` : ""}
|
||||
</div>
|
||||
<p class="offer-text">Choose how to improve the attack before resolving the combat result.</p>
|
||||
</div>
|
||||
`,
|
||||
buttons,
|
||||
rejectClose: false
|
||||
})
|
||||
|
||||
if (!choice || choice === "continue") break
|
||||
|
||||
attackerHandledBonus = true
|
||||
|
||||
if (choice === "grit") {
|
||||
const attackBonus = await LethalFantasyUtils.rollBonusDie("1d6", attacker, (total) => `<p><strong>${attackerName}</strong> spends 1 Grit and rolls <strong>${total}</strong> for attack.</p>`)
|
||||
attackRollFinal += attackBonus
|
||||
await attacker.update({ "system.grit.current": currentGrit - 1 })
|
||||
continue
|
||||
}
|
||||
|
||||
if (choice === "bonusDie") {
|
||||
const bonusDie = await LethalFantasyUtils.promptCombatBonusDie(attackerName, "defense", attackRollFinal, defenseRoll)
|
||||
if (!bonusDie) continue
|
||||
const attackBonus = await LethalFantasyUtils.rollBonusDie(bonusDie, attacker, (total, formula) => `<p><strong>${attackerName}</strong> adds <strong>${formula.toUpperCase()}</strong> and rolls <strong>${total}</strong> for attack.</p>`)
|
||||
attackRollFinal += attackBonus
|
||||
continue
|
||||
}
|
||||
|
||||
if (choice === "rerollAttack" && canRerollAttack && attackRerollContext) {
|
||||
const oldAttackRoll = attackRollFinal
|
||||
const reroll = await LethalFantasyUtils.rerollConfiguredRoll(attackRerollContext)
|
||||
canRerollAttack = false
|
||||
if (!reroll) continue
|
||||
attackRollFinal = reroll.options?.rollTotal || reroll.total || oldAttackRoll
|
||||
await createReactionMessage(attacker, `<p><strong>${attackerName}</strong> uses Mulligan and re-rolls attack: <strong>${oldAttackRoll}</strong> → <strong>${attackRollFinal}</strong>.</p>`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const shieldDamageReduction = shieldBlocked ? shieldReaction.damageReduction : 0
|
||||
const outcome = shieldBlocked ? "shielded-hit" : (attackRollFinal > defenseRoll ? "hit" : "miss")
|
||||
|
||||
// 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 && isPrimaryController(defender))
|
||||
|
||||
if (shouldCreateMessage) {
|
||||
console.log("Creating comparison message", { attackerHandledBonus, defenderHandledBonus, isDefenderOwner: defender.isOwner })
|
||||
|
||||
await LethalFantasyUtils.compareAttackDefense({
|
||||
attackerName,
|
||||
attackerId,
|
||||
attackRoll: attackRollFinal,
|
||||
attackWeaponId,
|
||||
attackRollType,
|
||||
attackRollKey,
|
||||
defenderName,
|
||||
defenderId,
|
||||
defenderTokenId,
|
||||
defenseRoll,
|
||||
outcome,
|
||||
shieldDamageReduction
|
||||
})
|
||||
} 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.active && !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 bypassArmor = message.rolls[0]?.options?.bypassArmor || false
|
||||
const armorDR = bypassArmor ? 0 : (defender.computeDamageReduction() || 0)
|
||||
const extraShieldDr = Number(message.rolls[0]?.options?.extraShieldDr) || 0
|
||||
const totalDR = armorDR + extraShieldDr
|
||||
const finalDamage = Math.max(0, damageTotal - totalDR)
|
||||
|
||||
// Prefer the token ID stored in roll options (set at attack time when the exact token is known).
|
||||
// For unlinked tokens (default for monsters), this ensures we target the right instance even
|
||||
// when multiple unlinked copies of the same monster type are in combat.
|
||||
const rollDefenderTokenId = message.rolls[0]?.options?.defenderTokenId
|
||||
const defenderCombatant = game.combat?.combatants?.find(c => c.actorId === defender.id)
|
||||
const defenderTokenId = rollDefenderTokenId
|
||||
?? defenderCombatant?.token?.id
|
||||
?? canvas.tokens?.placeables?.find(t => t.actor?.id === defender.id)?.id
|
||||
?? null
|
||||
|
||||
// Apply damage. If the current user does not own the defender (e.g. player hitting a GM monster),
|
||||
// route the HP update to the GM via socket. The confirmation message is still created here
|
||||
// since all users can create chat messages.
|
||||
if (defender.isOwner) {
|
||||
// Resolve the token actor: prefer lookup by token ID (exact match for unlinked monsters),
|
||||
// fall back to combatant actor, then base world actor.
|
||||
const tokenActor = (defenderTokenId
|
||||
? canvas.tokens?.placeables?.find(t => t.id === defenderTokenId)?.actor
|
||||
: defenderCombatant?.actor) ?? defender
|
||||
await tokenActor.applyDamage(-finalDamage)
|
||||
} else {
|
||||
game.socket.emit(`system.${SYSTEM.id}`, { type: "applyDamage", actorId: defender.id, tokenId: defenderTokenId, damage: -finalDamage })
|
||||
}
|
||||
|
||||
// Créer un message de confirmation (visible to GM only)
|
||||
const messageContent = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-lethal-fantasy/templates/damage-applied-message.hbs",
|
||||
{
|
||||
targetName: defender.name,
|
||||
damage: finalDamage,
|
||||
drText: bypassArmor ? "Armor DR bypassed (spell)" : (totalDR > 0 ? `Armor DR: ${armorDR}${extraShieldDr > 0 ? ` + Shield DR: ${extraShieldDr}` : ""}` : ""),
|
||||
weaponName: weaponName,
|
||||
attackerName: attackerName,
|
||||
rawDamage: damageTotal
|
||||
}
|
||||
)
|
||||
|
||||
await ChatMessage.create({
|
||||
content: messageContent,
|
||||
speaker: ChatMessage.getSpeaker({ actor: defender }),
|
||||
whisper: ChatMessage.getWhisperRecipients("GM")
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* Create a macro when dropping an entity on the hotbar
|
||||
* Item - open roll dialog
|
||||
|
||||
@@ -100,6 +100,14 @@ export class LethalFantasyCombat extends Combat {
|
||||
return this.turns = turns;
|
||||
}
|
||||
|
||||
async startCombat() {
|
||||
this._playCombatSound("startEncounter")
|
||||
const updateData = { round: 0, turn: 0 }
|
||||
Hooks.callAll("combatStart", this, updateData)
|
||||
await this.update(updateData)
|
||||
return this
|
||||
}
|
||||
|
||||
async rollInitiative(ids, options) {
|
||||
console.log("%%%%%%%%% Roll Initiative", ids, options);
|
||||
|
||||
@@ -110,13 +118,12 @@ export class LethalFantasyCombat extends Combat {
|
||||
let updates = [];
|
||||
for (let cId of ids) {
|
||||
const c = this.combatants.get(cId);
|
||||
let user = game.users.find(u => u.active && u.character && u.character.id === c.actor.id);
|
||||
if (user?.hasPlayerOwner) {
|
||||
const playerOwner = game.users.find(u => u.active && !u.isGM && u.character?.id === c.actor.id);
|
||||
if (game.user.isGM && playerOwner) {
|
||||
console.log("Rolling initiative for", c.actor.name);
|
||||
game.socket.emit(`system.${SYSTEM.id}`, { type: "rollInitiative", actorId: c.actor.id, combatId: this.id, combatantId: c.id });
|
||||
game.socket.emit(`system.${SYSTEM.id}`, { type: "rollInitiative", userId: playerOwner.id, actorId: c.actor.id, combatId: this.id, combatantId: c.id });
|
||||
} else {
|
||||
user = game.users.find(u => u.active && u.isGM);
|
||||
c.actor.system.rollInitiative(this.id, c.id);
|
||||
await c.actor.system.rollInitiative(this.id, c.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,12 +203,11 @@ export class LethalFantasyCombat extends Combat {
|
||||
|
||||
for (let c of this.combatants) {
|
||||
if (nextRound >= c.initiative) {
|
||||
let user = game.users.find(u => u.active && u.character && u.character.id === c.actor.id);
|
||||
if (user?.hasPlayerOwner) {
|
||||
game.socket.emit(`system.${SYSTEM.id}`, { type: "rollProgressionDice", progressionCount: c.system.progressionCount + 1, actorId: c.actor.id, combatId: this.id, combatantId: c.id });
|
||||
const playerOwner = game.users.find(u => u.active && !u.isGM && u.character?.id === c.actor.id);
|
||||
if (game.user.isGM && playerOwner) {
|
||||
game.socket.emit(`system.${SYSTEM.id}`, { type: "rollProgressionDice", userId: playerOwner.id, progressionCount: c.system.progressionCount + 1, actorId: c.actor.id, combatId: this.id, combatantId: c.id });
|
||||
} else {
|
||||
user = game.users.find(u => u.active && u.isGM);
|
||||
c.actor.system.rollProgressionDice(this.id, c.id);
|
||||
await c.actor.system.rollProgressionDice(this.id, c.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,12 +99,22 @@ export default class LethalFantasyCharacterSheet extends LethalFantasyActorSheet
|
||||
context.damageReduction = this.actor.computeDamageReduction()
|
||||
context.damageReductionShield = this.actor.getShieldDR()
|
||||
break
|
||||
case "skills":
|
||||
case "skills": {
|
||||
context.tab = context.tabs.skills
|
||||
context.skills = doc.itemTypes.skill
|
||||
// Organiser les skills par catégorie
|
||||
const categories = ['layperson', 'professional', 'weapon', 'armor', 'resist']
|
||||
context.skillsByCategory = categories.map(cat => {
|
||||
return {
|
||||
category: cat,
|
||||
label: `LETHALFANTASY.Skill.Category.${cat}`,
|
||||
skills: context.skills.filter(s => s.system.category === cat)
|
||||
}
|
||||
}).filter(catData => catData.skills.length > 0)
|
||||
context.gifts = doc.itemTypes.gift
|
||||
context.vulnerabilities = doc.itemTypes.vulnerability
|
||||
break
|
||||
}
|
||||
case "spells":
|
||||
context.tab = context.tabs.spells
|
||||
context.spells = doc.itemTypes.spell
|
||||
|
||||
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, defenderTokenId, extraShieldDr = 0) {
|
||||
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, defenderTokenId, extraShieldDr)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
@@ -337,11 +346,25 @@ export default class LethalFantasyRoll extends Roll {
|
||||
favor: "none",
|
||||
targetName
|
||||
}
|
||||
let rollContext
|
||||
if (options.rollContext) {
|
||||
rollContext = foundry.utils.duplicate(options.rollContext)
|
||||
hasGrantedDice = !!rollContext.hasGrantedDice
|
||||
pointBlank = !!rollContext.pointBlank
|
||||
beyondSkill = !!rollContext.beyondSkill
|
||||
letItFly = !!rollContext.letItFly
|
||||
saveSpell = !!rollContext.saveSpell
|
||||
rollContext.visibility ||= rollContext.rollMode || game.settings.get("core", "rollMode")
|
||||
rollContext.modifier ||= modifier
|
||||
rollContext.favor ||= "none"
|
||||
rollContext.changeDice ||= `${dice}`
|
||||
rollContext.attackerAim ||= "0"
|
||||
} else {
|
||||
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/roll-dialog.hbs", dialogContext)
|
||||
|
||||
let position = game.user.getFlag(SYSTEM.id, "roll-dialog-pos") || { top: -1, left: -1 }
|
||||
const label = game.i18n.localize("LETHALFANTASY.Roll.roll")
|
||||
const rollContext = await foundry.applications.api.DialogV2.wait({
|
||||
rollContext = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Roll dialog" },
|
||||
classes: ["lethalfantasy"],
|
||||
content,
|
||||
@@ -382,13 +405,14 @@ export default class LethalFantasyRoll extends Roll {
|
||||
let token = canvas.tokens?.get(tokenId)
|
||||
if (token) {
|
||||
canvas.animatePan({ x: token.x, y: token.y, duration: 200 })
|
||||
canvas.tokens.releaseAll();
|
||||
token.control({ releaseOthers: true });
|
||||
canvas.tokens.releaseAll()
|
||||
token.control({ releaseOthers: true })
|
||||
}
|
||||
}
|
||||
},
|
||||
rejectClose: false // Click on Close button will not launch an error
|
||||
})
|
||||
}
|
||||
|
||||
// If the user cancels the dialog, exit
|
||||
if (rollContext === null) return
|
||||
@@ -538,12 +562,24 @@ export default class LethalFantasyRoll extends Roll {
|
||||
rollFavor = null
|
||||
}
|
||||
|
||||
if (options.forceNoD30) {
|
||||
hasD30 = false
|
||||
}
|
||||
|
||||
if (hasD30) {
|
||||
let rollD30 = await new Roll("1D30").evaluate()
|
||||
if (game?.dice3d) {
|
||||
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 +635,12 @@ 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
|
||||
rollBase.options.defenderTokenId = options.defenderTokenId
|
||||
rollBase.options.extraShieldDr = options.extraShieldDr || 0
|
||||
|
||||
/**
|
||||
* A hook event that fires after the roll has been made.
|
||||
@@ -662,16 +702,24 @@ export default class LethalFantasyRoll extends Roll {
|
||||
rejectClose: false // Click on Close button will not launch an error
|
||||
})
|
||||
|
||||
let initRoll = new Roll(`min(${rollContext.initiativeDice}, ${options.maxInit})`, options.data, rollContext)
|
||||
if (!rollContext) return
|
||||
|
||||
// When the value is a plain number (e.g. "1" for Declared Ready on Alert), wrapping it in
|
||||
// min(1, maxInit) produces a dice-less formula that FoundryVTT cannot evaluate to a valid
|
||||
// total. Use the constant directly; min() is only needed for actual dice expressions.
|
||||
const isDiceFormula = /[dD]/.test(rollContext.initiativeDice)
|
||||
const formula = isDiceFormula ? `min(${rollContext.initiativeDice}, ${options.maxInit})` : rollContext.initiativeDice
|
||||
|
||||
let initRoll = new Roll(formula, options.data)
|
||||
await initRoll.evaluate()
|
||||
let msg = await initRoll.toMessage({ flavor: `Initiative for ${options.actorName}` }, { rollMode: rollContext.visibility })
|
||||
if (game?.dice3d) {
|
||||
if (game?.dice3d && initRoll.dice?.length) {
|
||||
await game.dice3d.waitFor3DAnimationByMessageID(msg.id)
|
||||
}
|
||||
|
||||
if (options.combatId && options.combatantId) {
|
||||
let combat = game.combats.get(options.combatId)
|
||||
combat.updateEmbeddedDocuments("Combatant", [{ _id: options.combatantId, initiative: initRoll.total, 'system.progressionCount': 0 }]);
|
||||
await combat.updateEmbeddedDocuments("Combatant", [{ _id: options.combatantId, initiative: initRoll.total, 'system.progressionCount': 0, [`flags.${SYSTEM.id}.firstActionTaken`]: false }])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -808,7 +856,11 @@ export default class LethalFantasyRoll extends Roll {
|
||||
let selectedItem = combatant.actor.items.find(i => i.id === selectedChoice)
|
||||
// Setup flag for combat action usage
|
||||
let actionItem = foundry.utils.duplicate(selectedItem)
|
||||
actionItem.progressionCount = 1
|
||||
// First action of this combat: use the class-based starting threshold;
|
||||
// all subsequent actions reset to 1 (normal progression).
|
||||
const firstActionTaken = combatant.getFlag(SYSTEM.id, "firstActionTaken")
|
||||
actionItem.progressionCount = firstActionTaken ? 1 : (combatant.actor.system.combat?.combatProgressionStart ?? 1)
|
||||
if (!firstActionTaken) await combatant.setFlag(SYSTEM.id, "firstActionTaken", true)
|
||||
actionItem.rangedMode = rangedMode
|
||||
actionItem.castingTime = 1
|
||||
actionItem.spellStatus = "castingTime"
|
||||
@@ -862,8 +914,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 +933,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 +990,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 +1220,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) {
|
||||
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 })
|
||||
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 +1290,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()
|
||||
@@ -1205,7 +1316,7 @@ export default class LethalFantasyRoll extends Roll {
|
||||
* @returns {Promise} - A promise that resolves when the message is created.
|
||||
*/
|
||||
async toMessage(messageData = {}, { rollMode, create = true } = {}) {
|
||||
super.toMessage(
|
||||
return await super.toMessage(
|
||||
{
|
||||
isSave: this.isSave,
|
||||
isChallenge: this.isChallenge,
|
||||
@@ -1222,7 +1333,7 @@ export default class LethalFantasyRoll extends Roll {
|
||||
rollData: this.rollData,
|
||||
...messageData,
|
||||
},
|
||||
{ rollMode: rollMode },
|
||||
{ rollMode, create },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -155,6 +155,7 @@ export default class LethalFantasyCharacter extends foundry.abstract.TypeDataMod
|
||||
defenseBonus: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
|
||||
damageModifier: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
|
||||
armorHitPoints: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
|
||||
combatProgressionStart: new fields.NumberField({ ...requiredInteger, initial: 1, min: 1 }),
|
||||
})
|
||||
|
||||
const moneyField = (label) => {
|
||||
@@ -273,7 +274,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, defenderTokenId, extraShieldDr = 0) {
|
||||
const hasTarget = false
|
||||
let roll = await LethalFantasyRoll.prompt({
|
||||
rollType,
|
||||
@@ -282,7 +283,10 @@ export default class LethalFantasyCharacter extends foundry.abstract.TypeDataMod
|
||||
actorName: this.parent.name,
|
||||
actorImage: this.parent.img,
|
||||
hasTarget,
|
||||
target: false
|
||||
target: false,
|
||||
defenderId,
|
||||
defenderTokenId,
|
||||
extraShieldDr
|
||||
})
|
||||
if (!roll) return null
|
||||
|
||||
@@ -311,6 +315,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, defenderTokenId = undefined, extraShieldDr = 0) {
|
||||
const hasTarget = false
|
||||
let roll = await LethalFantasyRoll.prompt({
|
||||
rollType,
|
||||
@@ -146,14 +150,17 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
|
||||
actorName: this.parent.name,
|
||||
actorImage: this.parent.img,
|
||||
hasTarget,
|
||||
target: false
|
||||
target: false,
|
||||
defenderId,
|
||||
defenderTokenId,
|
||||
extraShieldDr
|
||||
})
|
||||
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, defenderTokenId = undefined, extraShieldDr = 0) {
|
||||
let rollTarget
|
||||
switch (rollType) {
|
||||
case "monster-attack":
|
||||
@@ -166,6 +173,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 +256,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, defenderTokenId, extraShieldDr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,6 +280,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 +297,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 +308,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 +326,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 }) })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ export default class LethalFantasySpell extends foundry.abstract.TypeDataModel {
|
||||
|
||||
schema.attackRoll = new fields.StringField({ required: true, initial: "" })
|
||||
schema.powerRoll = new fields.StringField({ required: true, initial: "" })
|
||||
schema.damageDice = new fields.StringField({ required: false, initial: "" })
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
752
module/utils.mjs
752
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 {
|
||||
|
||||
@@ -22,6 +28,7 @@ export default class LethalFantasyUtils {
|
||||
static setHookListeners() {
|
||||
|
||||
Hooks.on('renderTokenHUD', async (hud, html, token) => {
|
||||
// HP Loss Button (existing)
|
||||
const lossHPButton = await foundry.applications.handlebars.renderTemplate('systems/fvtt-lethal-fantasy/templates/loss-hp-hud.hbs', {})
|
||||
$(html).find('div.left').append(lossHPButton);
|
||||
$(html).find('img.lethal-hp-loss-hud').click((event) => {
|
||||
@@ -59,6 +66,45 @@ export default class LethalFantasyUtils {
|
||||
$(html).find('.hp-loss-wrap')[2].classList.add('hp-loss-hud-disabled');
|
||||
}
|
||||
})
|
||||
|
||||
// HP Gain Button (new)
|
||||
const gainHPButton = await foundry.applications.handlebars.renderTemplate('systems/fvtt-lethal-fantasy/templates/gain-hp-hud.hbs', {})
|
||||
$(html).find('div.left').append(gainHPButton);
|
||||
$(html).find('img.lethal-hp-gain-hud').click((event) => {
|
||||
event.preventDefault();
|
||||
let hpMenu = $(html).find('.hp-gain-wrap')[0]
|
||||
if (hpMenu.classList.contains("hp-gain-hud-disabled")) {
|
||||
$(html).find('.hp-gain-wrap')[0].classList.add('hp-gain-hud-active');
|
||||
$(html).find('.hp-gain-wrap')[0].classList.remove('hp-gain-hud-disabled');
|
||||
$(html).find('.hp-gain-wrap')[1].classList.add('hp-gain-hud-active');
|
||||
$(html).find('.hp-gain-wrap')[1].classList.remove('hp-gain-hud-disabled');
|
||||
$(html).find('.hp-gain-wrap')[2].classList.add('hp-gain-hud-active');
|
||||
$(html).find('.hp-gain-wrap')[2].classList.remove('hp-gain-hud-disabled');
|
||||
} else {
|
||||
$(html).find('.hp-gain-wrap')[0].classList.remove('hp-gain-hud-active');
|
||||
$(html).find('.hp-gain-wrap')[0].classList.add('hp-gain-hud-disabled');
|
||||
$(html).find('.hp-gain-wrap')[1].classList.remove('hp-gain-hud-active');
|
||||
$(html).find('.hp-gain-wrap')[1].classList.add('hp-gain-hud-disabled');
|
||||
$(html).find('.hp-gain-wrap')[2].classList.remove('hp-gain-hud-active');
|
||||
$(html).find('.hp-gain-wrap')[2].classList.add('hp-gain-hud-disabled');
|
||||
}
|
||||
})
|
||||
$(html).find('.gain-hp-hud-click').click((event) => {
|
||||
event.preventDefault();
|
||||
let hpGain = event.currentTarget.dataset.hpValue;
|
||||
if (token) {
|
||||
let tokenFull = canvas.tokens.placeables.find(t => t.id === token._id);
|
||||
console.log(tokenFull, token)
|
||||
let actor = tokenFull.actor;
|
||||
actor.applyDamage(Number(hpGain)); // Positive value to add HP
|
||||
$(html).find('.hp-gain-wrap')[0].classList.remove('hp-gain-hud-active');
|
||||
$(html).find('.hp-gain-wrap')[0].classList.add('hp-gain-hud-disabled');
|
||||
$(html).find('.hp-gain-wrap')[1].classList.remove('hp-gain-hud-active');
|
||||
$(html).find('.hp-gain-wrap')[1].classList.add('hp-gain-hud-disabled');
|
||||
$(html).find('.hp-gain-wrap')[2].classList.remove('hp-gain-hud-active');
|
||||
$(html).find('.hp-gain-wrap')[2].classList.add('hp-gain-hud-disabled');
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -67,15 +113,705 @@ export default class LethalFantasyUtils {
|
||||
console.log(`handleSocketEvent !`, msg)
|
||||
let actor
|
||||
switch (msg.type) {
|
||||
case "applyDamage":
|
||||
if (game.user.isGM) {
|
||||
// Prefer the specific token actor (correct for unlinked monsters); fall back to world actor.
|
||||
actor = msg.tokenId
|
||||
? canvas.tokens?.placeables?.find(t => t.id === msg.tokenId)?.actor
|
||||
: (game.combat?.combatants?.find(c => c.actorId === msg.actorId)?.actor
|
||||
?? game.actors.get(msg.actorId))
|
||||
if (actor) actor.applyDamage(msg.damage)
|
||||
}
|
||||
break
|
||||
case "rollInitiative":
|
||||
if (msg.userId && msg.userId !== game.user.id) break
|
||||
actor = game.actors.get(msg.actorId)
|
||||
actor.system.rollInitiative(msg.combatId, msg.combatantId)
|
||||
break
|
||||
case "rollProgressionDice":
|
||||
if (msg.userId && msg.userId !== game.user.id) break
|
||||
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 attackD30result = msg.attackD30result
|
||||
const attackD30message = msg.attackD30message
|
||||
const attackRerollContext = msg.attackRerollContext
|
||||
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
|
||||
}
|
||||
|
||||
// Resolve the specific token ID now while we still have combatant/token context.
|
||||
// This is passed through to the damage roll so the GM-side socket handler can find the
|
||||
// correct synthetic actor for unlinked tokens (avoids wrong-instance damage with multiple
|
||||
// unlinked copies of the same monster).
|
||||
const defenderTokenId = (() => {
|
||||
if (game.combat && combatantId) {
|
||||
const cbt = game.combat.combatants.get(combatantId)
|
||||
if (cbt?.token?.id) return cbt.token.id
|
||||
}
|
||||
return tokenId ?? canvas.tokens?.placeables?.find(t => t.actor?.id === defender.id)?.id ?? null
|
||||
})()
|
||||
|
||||
const isMonster = defender.type === "monster"
|
||||
|
||||
// Spell/miracle attacks use saving throws instead of weapon defense
|
||||
const isSpellAttack = attackRollType === "spell-attack" || attackRollType === "miracle-attack"
|
||||
if (isSpellAttack) {
|
||||
const savesConfig = isMonster ? SYSTEM.MONSTER_SAVES : SYSTEM.SAVES
|
||||
const combatSaves = ["will", "dodge", "toughness"]
|
||||
const savesHTML = Object.values(savesConfig)
|
||||
.filter(s => combatSaves.includes(s.id))
|
||||
.map(s => `<option value="${s.id}">${game.i18n.localize(s.label)}</option>`)
|
||||
.join("")
|
||||
|
||||
const content = `
|
||||
<div class="defense-request-dialog">
|
||||
<div class="attack-info">
|
||||
<p><strong>${attackerName}</strong> targets <strong>${defenderName}</strong> with <strong>${weaponName}</strong>!</p>
|
||||
<p>Attack roll: <strong>${attackRoll}</strong></p>
|
||||
</div>
|
||||
<div class="weapon-selection">
|
||||
<label for="save-type">Choose saving throw:</label>
|
||||
<select id="save-type" name="saveKey" style="width: 100%; margin-top: 8px;">
|
||||
${savesHTML}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
const result = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Saving Throw vs Spell" },
|
||||
classes: ["lethalfantasy"],
|
||||
content,
|
||||
buttons: [
|
||||
{
|
||||
label: "Roll Save",
|
||||
icon: "fa-solid fa-person-running",
|
||||
callback: (event, button) => button.form.elements.saveKey.value,
|
||||
},
|
||||
],
|
||||
rejectClose: false
|
||||
})
|
||||
|
||||
if (result) {
|
||||
game.lethalFantasy = game.lethalFantasy || {}
|
||||
game.lethalFantasy.nextDefenseData = {
|
||||
attackerId,
|
||||
attackRoll,
|
||||
attackerName,
|
||||
defenderName,
|
||||
attackWeaponId,
|
||||
attackRollType,
|
||||
attackRollKey,
|
||||
attackD30result,
|
||||
attackD30message,
|
||||
attackRerollContext,
|
||||
defenderId: defender.id,
|
||||
defenderTokenId
|
||||
}
|
||||
if (isMonster) {
|
||||
defender.system.prepareMonsterRoll("save", result)
|
||||
} else {
|
||||
defender.prepareRoll("save", result)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 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,
|
||||
attackD30result,
|
||||
attackD30message,
|
||||
attackRerollContext,
|
||||
defenderId: defender.id,
|
||||
defenderTokenId
|
||||
}
|
||||
|
||||
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,
|
||||
attackD30result,
|
||||
attackD30message,
|
||||
attackRerollContext,
|
||||
defenderId: defender.id,
|
||||
defenderTokenId
|
||||
}
|
||||
|
||||
console.log("Storing defense data for character:", defender.id)
|
||||
|
||||
defender.prepareRoll("weapon-defense", result)
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static hasD30Reroll(d30Message) {
|
||||
return /mulligan|re-?roll/i.test(d30Message || "")
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static getCombatBonusDiceChoices() {
|
||||
return ["1d4", "1d6", "1d8", "1d10", "1d12", "1d20", "1d20e"]
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static getShieldReactionData(actor) {
|
||||
if (!actor) return null
|
||||
if (actor.type === "monster") {
|
||||
const formula = actor.system.combat?.shieldDefenseDice
|
||||
const damageReduction = actor.getShieldDR()
|
||||
if (!formula || damageReduction <= 0) return null
|
||||
return {
|
||||
label: game.i18n.localize("LETHALFANTASY.Label.shieldDefenseDice"),
|
||||
formula,
|
||||
damageReduction
|
||||
}
|
||||
}
|
||||
|
||||
const equippedShields = actor.items.filter(item => item.type === "shield" && item.system.equipped)
|
||||
if (equippedShields.length === 0) return null
|
||||
|
||||
const shield = equippedShields[0]
|
||||
return {
|
||||
label: shield.name,
|
||||
formula: shield.system.defense,
|
||||
damageReduction: actor.getShieldDR(),
|
||||
shieldId: shield.id
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static async promptCombatBonusDie(actorName, sideLabel, currentRoll, opposingRoll) {
|
||||
const choices = this.getCombatBonusDiceChoices()
|
||||
const optionsHtml = choices.map(choice => `<option value="${choice}">${choice.toUpperCase()}</option>`).join("")
|
||||
const content = `
|
||||
<div class="grit-luck-dialog">
|
||||
<div class="combat-status">
|
||||
<p><strong>${actorName}</strong> currently has <strong>${currentRoll}</strong></p>
|
||||
<p>Opposing ${sideLabel} roll: <strong>${opposingRoll}</strong></p>
|
||||
</div>
|
||||
<div class="weapon-selection">
|
||||
<label for="bonus-die">Choose a bonus die:</label>
|
||||
<select id="bonus-die" name="bonusDie" style="width: 100%; margin-top: 8px;">
|
||||
${optionsHtml}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
return await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Add Bonus Die" },
|
||||
classes: ["lethalfantasy"],
|
||||
content,
|
||||
buttons: [
|
||||
{
|
||||
action: "roll",
|
||||
label: "Roll Bonus Die",
|
||||
icon: "fa-solid fa-dice",
|
||||
callback: (event, button) => {
|
||||
const sel = button.form?.elements?.bonusDie ?? button.closest("form")?.elements?.bonusDie
|
||||
return sel?.value ?? choices[0]
|
||||
}
|
||||
},
|
||||
{
|
||||
action: "cancel",
|
||||
label: "Cancel",
|
||||
icon: "fa-solid fa-xmark",
|
||||
callback: () => null
|
||||
}
|
||||
],
|
||||
rejectClose: false
|
||||
})
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static async rollBonusDie(formula, actor, messageContent) {
|
||||
const roll = new Roll(formula)
|
||||
await roll.evaluate()
|
||||
if (game?.dice3d) {
|
||||
await game.dice3d.showForRoll(roll, game.user, true)
|
||||
}
|
||||
if (messageContent) {
|
||||
await ChatMessage.create({
|
||||
content: messageContent(roll.total, formula),
|
||||
speaker: ChatMessage.getSpeaker({ actor })
|
||||
})
|
||||
}
|
||||
return roll.total
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
static async rerollConfiguredRoll(rerollContext = {}) {
|
||||
const RollClass = CONFIG.Dice.rolls.find(r => r.name === "LethalFantasyRoll")
|
||||
if (typeof RollClass?.prompt !== "function") {
|
||||
ui.notifications.error("Lethal Fantasy roll class not available for reroll")
|
||||
return null
|
||||
}
|
||||
|
||||
return await RollClass.prompt({
|
||||
...foundry.utils.duplicate(rerollContext),
|
||||
rollContext: foundry.utils.duplicate(rerollContext.rollContext || {}),
|
||||
forceNoD30: true,
|
||||
hasTarget: false,
|
||||
target: false
|
||||
})
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
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 outcome = data.outcome || (data.attackRoll > data.defenseRoll ? "hit" : "miss")
|
||||
const isAttackWin = outcome !== "miss"
|
||||
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-defender-token-id="${data.defenderTokenId || ""}" data-extra-shield-dr="${data.shieldDamageReduction || 0}" 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-defender-token-id="${data.defenderTokenId || ""}" data-extra-shield-dr="${data.shieldDamageReduction || 0}" 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-defender-token-id="${data.defenderTokenId || ""}" data-extra-shield-dr="${data.shieldDamageReduction || 0}" data-attack-key="${data.attackRollKey}" data-damage-type="monster">
|
||||
<i class="fa-solid fa-burst"></i> Damage
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
} else if (data.attackRollType === "spell-attack" || data.attackRollType === "miracle-attack") {
|
||||
const attacker = game.actors.get(data.attackerId)
|
||||
const spell = attacker?.items.get(data.attackWeaponId)
|
||||
const damageDice = spell?.system?.damageDice
|
||||
if (damageDice) {
|
||||
damageButton = `
|
||||
<div class="attack-result-damage">
|
||||
<button class="roll-damage-btn"
|
||||
data-attacker-id="${data.attackerId}"
|
||||
data-defender-id="${data.defenderId}"
|
||||
data-defender-token-id="${data.defenderTokenId || ""}"
|
||||
data-damage-type="spell"
|
||||
data-damage-formula="${damageDice}">
|
||||
<i class="fa-solid fa-wand-magic-sparkles"></i> Spell Damage (${damageDice})
|
||||
</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">
|
||||
${outcome === "shielded-hit"
|
||||
? `<i class="fa-solid fa-shield"></i> <strong>${data.attackerName}</strong> hits <strong>${data.defenderName}</strong>, but the shield blocked — apply armor DR + shield DR <strong>${data.shieldDamageReduction || 0}</strong>.`
|
||||
: 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() {
|
||||
@@ -328,16 +1064,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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
MANIFEST-000477
|
||||
MANIFEST-000559
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
2025/12/19-14:37:50.298740 7f24377fe6c0 Recovering log #475
|
||||
2025/12/19-14:37:50.308335 7f24377fe6c0 Delete type=3 #473
|
||||
2025/12/19-14:37:50.308381 7f24377fe6c0 Delete type=0 #475
|
||||
2025/12/19-15:40:35.866244 7f2436ffd6c0 Level-0 table #480: started
|
||||
2025/12/19-15:40:35.866281 7f2436ffd6c0 Level-0 table #480: 0 bytes OK
|
||||
2025/12/19-15:40:35.872420 7f2436ffd6c0 Delete type=0 #478
|
||||
2025/12/19-15:40:35.879555 7f2436ffd6c0 Manual compaction at level-0 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
|
||||
2025/12/19-15:40:35.879588 7f2436ffd6c0 Manual compaction at level-1 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
|
||||
2026/04/14-20:54:14.172569 7fc1dafbf6c0 Recovering log #557
|
||||
2026/04/14-20:54:14.184089 7fc1dafbf6c0 Delete type=3 #555
|
||||
2026/04/14-20:54:14.184186 7fc1dafbf6c0 Delete type=0 #557
|
||||
2026/04/14-21:31:09.240081 7fc1d8fbb6c0 Level-0 table #562: started
|
||||
2026/04/14-21:31:09.240104 7fc1d8fbb6c0 Level-0 table #562: 0 bytes OK
|
||||
2026/04/14-21:31:09.273756 7fc1d8fbb6c0 Delete type=0 #560
|
||||
2026/04/14-21:31:09.274050 7fc1d8fbb6c0 Manual compaction at level-0 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
|
||||
2026/04/14-21:31:09.274107 7fc1d8fbb6c0 Manual compaction at level-1 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
2025/12/15-21:20:57.107042 7f17053ff6c0 Recovering log #471
|
||||
2025/12/15-21:20:57.117492 7f17053ff6c0 Delete type=3 #469
|
||||
2025/12/15-21:20:57.117544 7f17053ff6c0 Delete type=0 #471
|
||||
2025/12/15-22:37:50.650614 7f16eeffd6c0 Level-0 table #476: started
|
||||
2025/12/15-22:37:50.650646 7f16eeffd6c0 Level-0 table #476: 0 bytes OK
|
||||
2025/12/15-22:37:50.656552 7f16eeffd6c0 Delete type=0 #474
|
||||
2025/12/15-22:37:50.669781 7f16eeffd6c0 Manual compaction at level-0 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
|
||||
2025/12/15-22:37:50.669815 7f16eeffd6c0 Manual compaction at level-1 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
|
||||
2026/04/12-10:10:03.851531 7f20ed3fd6c0 Recovering log #553
|
||||
2026/04/12-10:10:03.861282 7f20ed3fd6c0 Delete type=3 #551
|
||||
2026/04/12-10:10:03.861341 7f20ed3fd6c0 Delete type=0 #553
|
||||
2026/04/12-11:08:03.389691 7f1e4ffff6c0 Level-0 table #558: started
|
||||
2026/04/12-11:08:03.389742 7f1e4ffff6c0 Level-0 table #558: 0 bytes OK
|
||||
2026/04/12-11:08:03.395757 7f1e4ffff6c0 Delete type=0 #556
|
||||
2026/04/12-11:08:03.415391 7f1e4ffff6c0 Manual compaction at level-0 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
|
||||
2026/04/12-11:08:03.415438 7f1e4ffff6c0 Manual compaction at level-1 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
|
||||
|
||||
Binary file not shown.
BIN
packs-system/lf-equipment/MANIFEST-000559
Normal file
BIN
packs-system/lf-equipment/MANIFEST-000559
Normal file
Binary file not shown.
@@ -1 +1 @@
|
||||
MANIFEST-000476
|
||||
MANIFEST-000556
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
2025/12/19-14:37:50.313071 7f2437fff6c0 Recovering log #474
|
||||
2025/12/19-14:37:50.323320 7f2437fff6c0 Delete type=3 #472
|
||||
2025/12/19-14:37:50.323373 7f2437fff6c0 Delete type=0 #474
|
||||
2025/12/19-15:40:35.872568 7f2436ffd6c0 Level-0 table #479: started
|
||||
2025/12/19-15:40:35.872597 7f2436ffd6c0 Level-0 table #479: 0 bytes OK
|
||||
2025/12/19-15:40:35.879415 7f2436ffd6c0 Delete type=0 #477
|
||||
2025/12/19-15:40:35.879563 7f2436ffd6c0 Manual compaction at level-0 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
|
||||
2025/12/19-15:40:35.879582 7f2436ffd6c0 Manual compaction at level-1 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
|
||||
2026/04/14-20:54:14.189613 7fc1d9fbd6c0 Recovering log #554
|
||||
2026/04/14-20:54:14.199454 7fc1d9fbd6c0 Delete type=3 #552
|
||||
2026/04/14-20:54:14.199515 7fc1d9fbd6c0 Delete type=0 #554
|
||||
2026/04/14-21:31:09.274309 7fc1d8fbb6c0 Level-0 table #559: started
|
||||
2026/04/14-21:31:09.274344 7fc1d8fbb6c0 Level-0 table #559: 0 bytes OK
|
||||
2026/04/14-21:31:09.316123 7fc1d8fbb6c0 Delete type=0 #557
|
||||
2026/04/14-21:31:09.426109 7fc1d8fbb6c0 Manual compaction at level-0 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
|
||||
2026/04/14-21:31:09.426138 7fc1d8fbb6c0 Manual compaction at level-1 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
2025/12/15-21:20:57.122365 7f16effff6c0 Recovering log #470
|
||||
2025/12/15-21:20:57.134320 7f16effff6c0 Delete type=3 #468
|
||||
2025/12/15-21:20:57.134393 7f16effff6c0 Delete type=0 #470
|
||||
2025/12/15-22:37:50.644221 7f16eeffd6c0 Level-0 table #475: started
|
||||
2025/12/15-22:37:50.644288 7f16eeffd6c0 Level-0 table #475: 0 bytes OK
|
||||
2025/12/15-22:37:50.650497 7f16eeffd6c0 Delete type=0 #473
|
||||
2025/12/15-22:37:50.669770 7f16eeffd6c0 Manual compaction at level-0 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
|
||||
2025/12/15-22:37:50.669829 7f16eeffd6c0 Manual compaction at level-1 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
|
||||
2026/04/12-10:10:03.864565 7f20ecbfc6c0 Recovering log #550
|
||||
2026/04/12-10:10:03.875268 7f20ecbfc6c0 Delete type=3 #548
|
||||
2026/04/12-10:10:03.875332 7f20ecbfc6c0 Delete type=0 #550
|
||||
2026/04/12-11:08:03.395874 7f1e4ffff6c0 Level-0 table #555: started
|
||||
2026/04/12-11:08:03.395896 7f1e4ffff6c0 Level-0 table #555: 0 bytes OK
|
||||
2026/04/12-11:08:03.401955 7f1e4ffff6c0 Delete type=0 #553
|
||||
2026/04/12-11:08:03.415404 7f1e4ffff6c0 Manual compaction at level-0 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
|
||||
2026/04/12-11:08:03.415431 7f1e4ffff6c0 Manual compaction at level-1 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1 +1 @@
|
||||
MANIFEST-000478
|
||||
MANIFEST-000561
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
2025/12/19-14:37:50.285216 7f244c9fe6c0 Recovering log #476
|
||||
2025/12/19-14:37:50.295747 7f244c9fe6c0 Delete type=3 #474
|
||||
2025/12/19-14:37:50.295805 7f244c9fe6c0 Delete type=0 #476
|
||||
2025/12/19-15:40:35.852984 7f2436ffd6c0 Level-0 table #481: started
|
||||
2025/12/19-15:40:35.853042 7f2436ffd6c0 Level-0 table #481: 0 bytes OK
|
||||
2025/12/19-15:40:35.859968 7f2436ffd6c0 Delete type=0 #479
|
||||
2025/12/19-15:40:35.879535 7f2436ffd6c0 Manual compaction at level-0 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
|
||||
2025/12/19-15:40:35.879576 7f2436ffd6c0 Manual compaction at level-1 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
|
||||
2026/04/14-20:54:14.156150 7fc1d97bc6c0 Recovering log #559
|
||||
2026/04/14-20:54:14.166976 7fc1d97bc6c0 Delete type=3 #557
|
||||
2026/04/14-20:54:14.167099 7fc1d97bc6c0 Delete type=0 #559
|
||||
2026/04/14-21:31:09.203242 7fc1d8fbb6c0 Level-0 table #564: started
|
||||
2026/04/14-21:31:09.203283 7fc1d8fbb6c0 Level-0 table #564: 0 bytes OK
|
||||
2026/04/14-21:31:09.239957 7fc1d8fbb6c0 Delete type=0 #562
|
||||
2026/04/14-21:31:09.274032 7fc1d8fbb6c0 Manual compaction at level-0 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
|
||||
2026/04/14-21:31:09.274121 7fc1d8fbb6c0 Manual compaction at level-1 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
2025/12/15-21:20:57.092288 7f16ef7fe6c0 Recovering log #472
|
||||
2025/12/15-21:20:57.103016 7f16ef7fe6c0 Delete type=3 #470
|
||||
2025/12/15-21:20:57.103072 7f16ef7fe6c0 Delete type=0 #472
|
||||
2025/12/15-22:37:50.663349 7f16eeffd6c0 Level-0 table #477: started
|
||||
2025/12/15-22:37:50.663375 7f16eeffd6c0 Level-0 table #477: 0 bytes OK
|
||||
2025/12/15-22:37:50.669678 7f16eeffd6c0 Delete type=0 #475
|
||||
2025/12/15-22:37:50.669808 7f16eeffd6c0 Manual compaction at level-0 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
|
||||
2025/12/15-22:37:50.669842 7f16eeffd6c0 Manual compaction at level-1 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
|
||||
2026/04/12-10:10:03.838632 7f20ee3ff6c0 Recovering log #555
|
||||
2026/04/12-10:10:03.848360 7f20ee3ff6c0 Delete type=3 #553
|
||||
2026/04/12-10:10:03.848406 7f20ee3ff6c0 Delete type=0 #555
|
||||
2026/04/12-11:08:03.402029 7f1e4ffff6c0 Level-0 table #560: started
|
||||
2026/04/12-11:08:03.402048 7f1e4ffff6c0 Level-0 table #560: 0 bytes OK
|
||||
2026/04/12-11:08:03.408790 7f1e4ffff6c0 Delete type=0 #558
|
||||
2026/04/12-11:08:03.415414 7f1e4ffff6c0 Manual compaction at level-0 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
|
||||
2026/04/12-11:08:03.415454 7f1e4ffff6c0 Manual compaction at level-1 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
|
||||
|
||||
Binary file not shown.
BIN
packs-system/lf-skills/MANIFEST-000561
Normal file
BIN
packs-system/lf-skills/MANIFEST-000561
Normal file
Binary file not shown.
@@ -1 +1 @@
|
||||
MANIFEST-000176
|
||||
MANIFEST-000256
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
2025/12/19-14:37:50.338179 7f244d1ff6c0 Recovering log #174
|
||||
2025/12/19-14:37:50.348596 7f244d1ff6c0 Delete type=3 #172
|
||||
2025/12/19-14:37:50.348672 7f244d1ff6c0 Delete type=0 #174
|
||||
2025/12/19-15:40:35.860117 7f2436ffd6c0 Level-0 table #179: started
|
||||
2025/12/19-15:40:35.860149 7f2436ffd6c0 Level-0 table #179: 0 bytes OK
|
||||
2025/12/19-15:40:35.866102 7f2436ffd6c0 Delete type=0 #177
|
||||
2025/12/19-15:40:35.879546 7f2436ffd6c0 Manual compaction at level-0 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
|
||||
2025/12/19-15:40:35.879569 7f2436ffd6c0 Manual compaction at level-1 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
|
||||
2026/04/14-20:54:14.216969 7fc1d97bc6c0 Recovering log #254
|
||||
2026/04/14-20:54:14.226830 7fc1d97bc6c0 Delete type=3 #252
|
||||
2026/04/14-20:54:14.226876 7fc1d97bc6c0 Delete type=0 #254
|
||||
2026/04/14-21:31:09.316377 7fc1d8fbb6c0 Level-0 table #259: started
|
||||
2026/04/14-21:31:09.316424 7fc1d8fbb6c0 Level-0 table #259: 0 bytes OK
|
||||
2026/04/14-21:31:09.353574 7fc1d8fbb6c0 Delete type=0 #257
|
||||
2026/04/14-21:31:09.426120 7fc1d8fbb6c0 Manual compaction at level-0 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
|
||||
2026/04/14-21:31:09.426154 7fc1d8fbb6c0 Manual compaction at level-1 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
2025/12/15-21:20:57.154335 7f1704bfe6c0 Recovering log #170
|
||||
2025/12/15-21:20:57.164315 7f1704bfe6c0 Delete type=3 #168
|
||||
2025/12/15-21:20:57.164371 7f1704bfe6c0 Delete type=0 #170
|
||||
2025/12/15-22:37:50.692660 7f16eeffd6c0 Level-0 table #175: started
|
||||
2025/12/15-22:37:50.692695 7f16eeffd6c0 Level-0 table #175: 0 bytes OK
|
||||
2025/12/15-22:37:50.698767 7f16eeffd6c0 Delete type=0 #173
|
||||
2025/12/15-22:37:50.710785 7f16eeffd6c0 Manual compaction at level-0 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
|
||||
2025/12/15-22:37:50.710818 7f16eeffd6c0 Manual compaction at level-1 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
|
||||
2026/04/12-10:10:03.889781 7f20ed3fd6c0 Recovering log #250
|
||||
2026/04/12-10:10:03.899549 7f20ed3fd6c0 Delete type=3 #248
|
||||
2026/04/12-10:10:03.899595 7f20ed3fd6c0 Delete type=0 #250
|
||||
2026/04/12-11:08:03.421462 7f1e4ffff6c0 Level-0 table #255: started
|
||||
2026/04/12-11:08:03.421486 7f1e4ffff6c0 Level-0 table #255: 0 bytes OK
|
||||
2026/04/12-11:08:03.427527 7f1e4ffff6c0 Delete type=0 #253
|
||||
2026/04/12-11:08:03.443589 7f1e4ffff6c0 Manual compaction at level-0 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
|
||||
2026/04/12-11:08:03.443624 7f1e4ffff6c0 Manual compaction at level-1 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
MANIFEST-000475
|
||||
MANIFEST-000555
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
2025/12/19-14:37:50.326173 7f244c9fe6c0 Recovering log #473
|
||||
2025/12/19-14:37:50.335692 7f244c9fe6c0 Delete type=3 #471
|
||||
2025/12/19-14:37:50.335763 7f244c9fe6c0 Delete type=0 #473
|
||||
2025/12/19-15:40:35.879641 7f2436ffd6c0 Level-0 table #478: started
|
||||
2025/12/19-15:40:35.879661 7f2436ffd6c0 Level-0 table #478: 0 bytes OK
|
||||
2025/12/19-15:40:35.886005 7f2436ffd6c0 Delete type=0 #476
|
||||
2025/12/19-15:40:35.909027 7f2436ffd6c0 Manual compaction at level-0 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
|
||||
2025/12/19-15:40:35.927361 7f2436ffd6c0 Manual compaction at level-1 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
|
||||
2026/04/14-20:54:14.202677 7fc1da7be6c0 Recovering log #553
|
||||
2026/04/14-20:54:14.213358 7fc1da7be6c0 Delete type=3 #551
|
||||
2026/04/14-20:54:14.213482 7fc1da7be6c0 Delete type=0 #553
|
||||
2026/04/14-21:31:09.160202 7fc1d8fbb6c0 Level-0 table #558: started
|
||||
2026/04/14-21:31:09.160283 7fc1d8fbb6c0 Level-0 table #558: 0 bytes OK
|
||||
2026/04/14-21:31:09.203034 7fc1d8fbb6c0 Delete type=0 #556
|
||||
2026/04/14-21:31:09.274014 7fc1d8fbb6c0 Manual compaction at level-0 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
|
||||
2026/04/14-21:31:09.274091 7fc1d8fbb6c0 Manual compaction at level-1 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
2025/12/15-21:20:57.138258 7f16ef7fe6c0 Recovering log #469
|
||||
2025/12/15-21:20:57.151734 7f16ef7fe6c0 Delete type=3 #467
|
||||
2025/12/15-21:20:57.151800 7f16ef7fe6c0 Delete type=0 #469
|
||||
2025/12/15-22:37:50.656644 7f16eeffd6c0 Level-0 table #474: started
|
||||
2025/12/15-22:37:50.656668 7f16eeffd6c0 Level-0 table #474: 0 bytes OK
|
||||
2025/12/15-22:37:50.663254 7f16eeffd6c0 Delete type=0 #472
|
||||
2025/12/15-22:37:50.669789 7f16eeffd6c0 Manual compaction at level-0 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
|
||||
2025/12/15-22:37:50.669836 7f16eeffd6c0 Manual compaction at level-1 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
|
||||
2026/04/12-10:10:03.877445 7f20ee3ff6c0 Recovering log #549
|
||||
2026/04/12-10:10:03.887615 7f20ee3ff6c0 Delete type=3 #547
|
||||
2026/04/12-10:10:03.887678 7f20ee3ff6c0 Delete type=0 #549
|
||||
2026/04/12-11:08:03.408913 7f1e4ffff6c0 Level-0 table #554: started
|
||||
2026/04/12-11:08:03.408942 7f1e4ffff6c0 Level-0 table #554: 0 bytes OK
|
||||
2026/04/12-11:08:03.415303 7f1e4ffff6c0 Delete type=0 #552
|
||||
2026/04/12-11:08:03.415422 7f1e4ffff6c0 Manual compaction at level-0 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
|
||||
2026/04/12-11:08:03.415446 7f1e4ffff6c0 Manual compaction at level-1 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
|
||||
|
||||
Binary file not shown.
275
styles/chat.less
275
styles/chat.less
@@ -39,3 +39,278 @@
|
||||
font-family: var(--font-secondary);
|
||||
font-size: calc(var(--font-size-standard) * 1.2);
|
||||
}
|
||||
.defense-request {
|
||||
padding: 12px;
|
||||
background: linear-gradient(to bottom, #3a3930 0%, #2a2920 100%);
|
||||
border: 2px solid #d4af37;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
|
||||
|
||||
h3 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #d4af37;
|
||||
font-size: calc(var(--font-size-standard) * 1.1);
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
|
||||
i {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 8px 0;
|
||||
color: #f0e6d2;
|
||||
font-size: calc(var(--font-size-standard) * 0.95);
|
||||
line-height: 1.4;
|
||||
|
||||
strong {
|
||||
color: #d4af37;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.defense-prompt {
|
||||
margin-top: 12px;
|
||||
padding: 8px;
|
||||
background: rgba(212, 175, 55, 0.1);
|
||||
border-left: 3px solid #d4af37;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
color: #d4af37;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.defense-request-dialog {
|
||||
.attack-info {
|
||||
padding: 12px;
|
||||
background: linear-gradient(to bottom, rgba(42, 41, 32, 0.8) 0%, rgba(26, 25, 16, 0.9) 100%);
|
||||
border: 1px solid rgba(212, 175, 55, 0.5);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
p {
|
||||
margin: 6px 0;
|
||||
color: #f0e6d2;
|
||||
font-size: calc(var(--font-size-standard) * 0.95);
|
||||
|
||||
strong {
|
||||
color: #d4af37;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.weapon-selection {
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #d4af37;
|
||||
font-weight: 600;
|
||||
font-size: calc(var(--font-size-standard) * 0.95);
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: #3a3930 !important;
|
||||
border: 1px solid #d4af37;
|
||||
border-radius: 4px;
|
||||
color: #ffffff !important;
|
||||
font-size: calc(var(--font-size-standard) * 0.95);
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #f0e6d2;
|
||||
box-shadow: 0 0 0 2px rgba(212, 175, 55, 0.3);
|
||||
}
|
||||
|
||||
option {
|
||||
background: #3a3930 !important;
|
||||
color: #ffffff !important;
|
||||
padding: 6px;
|
||||
|
||||
&:checked,
|
||||
&:hover {
|
||||
background: #4a4940 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grit-luck-dialog {
|
||||
.combat-status {
|
||||
padding: 12px;
|
||||
background: linear-gradient(to bottom, rgba(42, 41, 32, 0.8) 0%, rgba(26, 25, 16, 0.9) 100%);
|
||||
border: 1px solid rgba(212, 175, 55, 0.5);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
p {
|
||||
margin: 6px 0;
|
||||
color: #f0e6d2;
|
||||
font-size: calc(var(--font-size-standard) * 0.95);
|
||||
|
||||
strong {
|
||||
color: #d4af37;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.bonus-info {
|
||||
color: #90EE90;
|
||||
font-style: italic;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.offer-text {
|
||||
color: #f0e6d2;
|
||||
font-size: calc(var(--font-size-standard) * 1);
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.attack-result {
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
|
||||
margin: 8px 0;
|
||||
|
||||
&.attack-success {
|
||||
background: linear-gradient(to bottom, rgba(139, 0, 0, 0.2) 0%, rgba(100, 0, 0, 0.3) 100%);
|
||||
border: 2px solid rgba(220, 20, 60, 0.6);
|
||||
}
|
||||
|
||||
&.attack-failure {
|
||||
background: linear-gradient(to bottom, rgba(0, 100, 139, 0.2) 0%, rgba(0, 70, 100, 0.3) 100%);
|
||||
border: 2px solid rgba(70, 130, 180, 0.6);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 16px 0;
|
||||
color: #d4af37;
|
||||
font-size: calc(var(--font-size-standard) * 1.2);
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
|
||||
i {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.combat-comparison {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 6px;
|
||||
|
||||
.combat-side {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
|
||||
&.winner {
|
||||
background: rgba(0, 255, 0, 0.1);
|
||||
border: 2px solid rgba(0, 255, 0, 0.4);
|
||||
}
|
||||
|
||||
&.loser {
|
||||
background: rgba(255, 0, 0, 0.1);
|
||||
border: 2px solid rgba(255, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.side-label {
|
||||
font-size: calc(var(--font-size-standard) * 0.8);
|
||||
color: #aaa;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.side-name {
|
||||
font-size: calc(var(--font-size-standard) * 1);
|
||||
color: #f0e6d2;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.side-roll {
|
||||
font-size: calc(var(--font-size-standard) * 1.5);
|
||||
color: #d4af37;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.combat-vs {
|
||||
font-size: calc(var(--font-size-standard) * 1.2);
|
||||
color: #d4af37;
|
||||
font-weight: 700;
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.combat-result-text {
|
||||
text-align: center;
|
||||
font-size: calc(var(--font-size-standard) * 1.1);
|
||||
color: #f0e6d2;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 6px;
|
||||
|
||||
i {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #d4af37;
|
||||
}
|
||||
}
|
||||
|
||||
.attack-result-damage {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
|
||||
.roll-damage-btn {
|
||||
padding: 10px 16px;
|
||||
background: linear-gradient(to bottom, #8b0000 0%, #660000 100%);
|
||||
border: 1px solid #ff0000;
|
||||
border-radius: 6px;
|
||||
color: #f0e6d2;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(to bottom, #a00000 0%, #7b0000 100%);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
i {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ i.lethalfantasy {
|
||||
|
||||
.application.dialog.lethalfantasy {
|
||||
font-family: var(--font-primary);
|
||||
font-size: calc(var(--font-size-standard) * 1.0);
|
||||
font-size: calc(var(--font-size-standard) * 1);
|
||||
background-image: var(--background-image-base);
|
||||
button:hover {
|
||||
background: var(--color-dark-6);
|
||||
@@ -70,12 +70,12 @@ i.lethalfantasy {
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.combat-sidebar li.combatant .token-initiative .initiative{
|
||||
.combat-sidebar li.combatant .token-initiative .initiative {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.combat-sidebar li.combatant .token-initiative {
|
||||
flex:none;
|
||||
flex: none;
|
||||
}
|
||||
.initiative-minus {
|
||||
margin-right: 8px;
|
||||
|
||||
@@ -35,10 +35,56 @@
|
||||
padding-bottom: 0;
|
||||
width: max-content;
|
||||
margin: 0;
|
||||
color:#252424;
|
||||
color: #252424;
|
||||
}
|
||||
|
||||
#token-hud .hp-loss-wrap .hud-loss-hp-button-select {
|
||||
padding-left: 8px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* HP Gain Styles */
|
||||
#token-hud .hp-gain-wrap {
|
||||
position: absolute;
|
||||
left: 75px;
|
||||
display: none;
|
||||
top: 50%;
|
||||
width: 48px;
|
||||
text-align: start;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#token-hud .hp-gain-wrap-col1 {
|
||||
transform: translate(-200%, -50%);
|
||||
}
|
||||
|
||||
#token-hud .hp-gain-wrap-col2 {
|
||||
transform: translate(-300%, -50%);
|
||||
}
|
||||
|
||||
#token-hud .hp-gain-wrap-col3 {
|
||||
transform: translate(-400%, -50%);
|
||||
}
|
||||
|
||||
#token-hud .hp-gain-hud-active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#token-hud .hp-gain-hud-disabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#token-hud .hud-gain-hp-button-select {
|
||||
max-width: 40px;
|
||||
background-image: var(--background-image-base);
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
width: max-content;
|
||||
margin: 0;
|
||||
color: #252424;
|
||||
}
|
||||
|
||||
#token-hud .hp-gain-wrap .hud-gain-hp-button-select {
|
||||
padding-left: 8px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
1092
styles/roll.less
1092
styles/roll.less
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,11 @@
|
||||
<a data-action="armorHitPointsMinus"><i class="fa-solid fa-hexagon-minus"></i></a>
|
||||
</div>
|
||||
|
||||
<div class="flexrow armor-hp">
|
||||
<span class="name" data-tooltip="{{localize 'LETHALFANTASY.Tooltip.combatProgressionStart'}}">{{localize "LETHALFANTASY.Label.combatProgressionStart"}}</span>
|
||||
{{formInput systemFields.combat.fields.combatProgressionStart value=system.combat.combatProgressionStart disabled=isPlayMode }}
|
||||
</div>
|
||||
|
||||
<div class="flexrow granted">
|
||||
<span class="">{{localize
|
||||
"LETHALFANTASY.Label.grantedAttackDice"}}</a></span>
|
||||
|
||||
@@ -1,15 +1,34 @@
|
||||
<section class="tab character-{{tab.id}} {{tab.cssClass}}" data-tab="skills" data-group="sheet">
|
||||
<section
|
||||
class="tab character-{{tab.id}} {{tab.cssClass}}"
|
||||
data-tab="skills"
|
||||
data-group="sheet"
|
||||
>
|
||||
<div class="main-div">
|
||||
|
||||
{{#each skillsByCategory as |categoryData|}}
|
||||
<fieldset>
|
||||
<legend data-tooltip="{{localize " LETHALFANTASY.Tooltip.skills"}}" data-tooltip-direction="UP">{{localize
|
||||
"LETHALFANTASY.Label.skills"}}</legend>
|
||||
<legend
|
||||
data-tooltip="{{localize ' LETHALFANTASY.Tooltip.skills'}}"
|
||||
data-tooltip-direction="UP"
|
||||
>{{localize categoryData.label}}</legend>
|
||||
<div class="skills">
|
||||
{{#each skills as |item|}}
|
||||
<div class="skill " data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}">
|
||||
<img class="item-img" src="{{item.img}}" data-tooltip="{{item.name}}" />
|
||||
{{#each categoryData.skills as |item|}}
|
||||
<div
|
||||
class="skill"
|
||||
data-item-id="{{item.id}}"
|
||||
data-item-uuid="{{item.uuid}}"
|
||||
>
|
||||
<img
|
||||
class="item-img"
|
||||
src="{{item.img}}"
|
||||
data-tooltip="{{item.name}}"
|
||||
/>
|
||||
<div class="name">
|
||||
<a class="rollable" data-roll-type="skill" data-roll-key="{{item.id}}">
|
||||
<a
|
||||
class="rollable"
|
||||
data-roll-type="skill"
|
||||
data-roll-key="{{item.id}}"
|
||||
>
|
||||
<i class="lf-roll-small fa-duotone fa-solid fa-dice-d10"></i>
|
||||
{{item.name}}
|
||||
</a>
|
||||
@@ -18,31 +37,62 @@
|
||||
+{{item.system.skillTotal}}
|
||||
</div>
|
||||
<div class="controls">
|
||||
<a data-tooltip="{{localize 'LETHALFANTASY.Edit'}}" data-action="edit" data-item-id="{{item.id}}"
|
||||
data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
|
||||
<a data-tooltip="{{localize 'LETHALFANTASY.Delete'}}" data-action="delete" data-item-id="{{item.id}}"
|
||||
data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>
|
||||
<a
|
||||
data-tooltip="{{localize 'LETHALFANTASY.Edit'}}"
|
||||
data-action="edit"
|
||||
data-item-id="{{item.id}}"
|
||||
data-item-uuid="{{item.uuid}}"
|
||||
><i class="fas fa-edit"></i></a>
|
||||
<a
|
||||
data-tooltip="{{localize 'LETHALFANTASY.Delete'}}"
|
||||
data-action="delete"
|
||||
data-item-id="{{item.id}}"
|
||||
data-item-uuid="{{item.uuid}}"
|
||||
><i class="fas fa-trash"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</fieldset>
|
||||
{{/each}}
|
||||
|
||||
<fieldset>
|
||||
<legend data-tooltip="{{localize " LETHALFANTASY.Tooltip.gifts"}}" data-tooltip-direction="UP">{{localize
|
||||
"LETHALFANTASY.Label.gifts"}}</legend>
|
||||
<legend
|
||||
data-tooltip="{{localize ' LETHALFANTASY.Tooltip.gifts'}}"
|
||||
data-tooltip-direction="UP"
|
||||
>{{localize "LETHALFANTASY.Label.gifts"}}</legend>
|
||||
<div class="gifts">
|
||||
{{#each gifts as |item|}}
|
||||
<div class="gift " data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}">
|
||||
<img class="item-img" src="{{item.img}}" data-tooltip="{{item.name}}" />
|
||||
<div class="name" data-tooltip="{{{item.description}}}<br><br>{{item.path}}" data-tooltip-direction="UP">
|
||||
<div
|
||||
class="gift"
|
||||
data-item-id="{{item.id}}"
|
||||
data-item-uuid="{{item.uuid}}"
|
||||
>
|
||||
<img
|
||||
class="item-img"
|
||||
src="{{item.img}}"
|
||||
data-tooltip="{{item.name}}"
|
||||
/>
|
||||
<div
|
||||
class="name"
|
||||
data-tooltip="{{{item.description}}}<br><br>{{item.path}}"
|
||||
data-tooltip-direction="UP"
|
||||
>
|
||||
{{item.name}}
|
||||
</div>
|
||||
<div class="controls">
|
||||
<a data-tooltip="{{localize 'LETHALFANTASY.Edit'}}" data-action="edit" data-item-id="{{item.id}}"
|
||||
data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
|
||||
<a data-tooltip="{{localize 'LETHALFANTASY.Delete'}}" data-action="delete" data-item-id="{{item.id}}"
|
||||
data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>
|
||||
<a
|
||||
data-tooltip="{{localize 'LETHALFANTASY.Edit'}}"
|
||||
data-action="edit"
|
||||
data-item-id="{{item.id}}"
|
||||
data-item-uuid="{{item.uuid}}"
|
||||
><i class="fas fa-edit"></i></a>
|
||||
<a
|
||||
data-tooltip="{{localize 'LETHALFANTASY.Delete'}}"
|
||||
data-action="delete"
|
||||
data-item-id="{{item.id}}"
|
||||
data-item-uuid="{{item.uuid}}"
|
||||
><i class="fas fa-trash"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
@@ -50,24 +100,46 @@
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend data-tooltip="{{localize " LETHALFANTASY.Tooltip.vulnerabilities"}}" data-tooltip-direction="UP">{{localize
|
||||
"LETHALFANTASY.Label.vulnerabilities"}}</legend>
|
||||
<legend
|
||||
data-tooltip="{{localize ' LETHALFANTASY.Tooltip.vulnerabilities'}}"
|
||||
data-tooltip-direction="UP"
|
||||
>{{localize "LETHALFANTASY.Label.vulnerabilities"}}</legend>
|
||||
<div class="vulnerabilities">
|
||||
{{#each vulnerabilities as |item|}}
|
||||
<div class="vulnerability " data-item-id="{{item.id}}" data-item-uuid="{{item.uuid}}">
|
||||
<img class="item-img" src="{{item.img}}" data-tooltip="{{item.name}}" />
|
||||
<div class="name" data-tooltip="{{{item.description}}}<br><br>{{item.path}}" data-tooltip-direction="UP">
|
||||
<div
|
||||
class="vulnerability"
|
||||
data-item-id="{{item.id}}"
|
||||
data-item-uuid="{{item.uuid}}"
|
||||
>
|
||||
<img
|
||||
class="item-img"
|
||||
src="{{item.img}}"
|
||||
data-tooltip="{{item.name}}"
|
||||
/>
|
||||
<div
|
||||
class="name"
|
||||
data-tooltip="{{{item.description}}}<br><br>{{item.path}}"
|
||||
data-tooltip-direction="UP"
|
||||
>
|
||||
{{item.name}}
|
||||
</div>
|
||||
<div class="controls">
|
||||
<a data-tooltip="{{localize 'LETHALFANTASY.Edit'}}" data-action="edit" data-item-id="{{item.id}}"
|
||||
data-item-uuid="{{item.uuid}}"><i class="fas fa-edit"></i></a>
|
||||
<a data-tooltip="{{localize 'LETHALFANTASY.Delete'}}" data-action="delete" data-item-id="{{item.id}}"
|
||||
data-item-uuid="{{item.uuid}}"><i class="fas fa-trash"></i></a>
|
||||
<a
|
||||
data-tooltip="{{localize 'LETHALFANTASY.Edit'}}"
|
||||
data-action="edit"
|
||||
data-item-id="{{item.id}}"
|
||||
data-item-uuid="{{item.uuid}}"
|
||||
><i class="fas fa-edit"></i></a>
|
||||
<a
|
||||
data-tooltip="{{localize 'LETHALFANTASY.Delete'}}"
|
||||
data-action="delete"
|
||||
data-item-id="{{item.id}}"
|
||||
data-item-uuid="{{item.uuid}}"
|
||||
><i class="fas fa-trash"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,125 +1,189 @@
|
||||
{{!log 'chat-message' this}}
|
||||
{{!log 'weaponDamageOptions' weaponDamageOptions}}
|
||||
<div class="{{cssClass}}">
|
||||
<div class="intro-chat">
|
||||
<div class="intro-img">
|
||||
<!-- Header with character info -->
|
||||
<div class="chat-header">
|
||||
<div class="character-info">
|
||||
<div class="character-avatar">
|
||||
<img src="{{actingCharImg}}" data-tooltip="{{actingCharName}}" />
|
||||
</div>
|
||||
|
||||
<div class="intro-right">
|
||||
<span><STRONG>{{actingCharName}} - {{upperFirst rollName}}</STRONG></span>
|
||||
|
||||
<div class="character-details">
|
||||
<div class="character-name">{{actingCharName}}</div>
|
||||
<div class="roll-type-badge">
|
||||
{{#if (match rollType "attack")}}
|
||||
<span>Attack roll !</span>
|
||||
<i class="fa-solid fa-swords"></i>
|
||||
{{/if}}
|
||||
{{#if (match rollType "defense")}}
|
||||
<span>Defense roll !</span>
|
||||
<i class="fa-solid fa-shield"></i>
|
||||
{{/if}}
|
||||
{{#if rollData.isDamage}}
|
||||
<i class="fa-solid fa-burst"></i>
|
||||
{{/if}}
|
||||
{{#if (eq rollType "skill")}}
|
||||
<i class="fa-solid fa-hand-sparkles"></i>
|
||||
{{/if}}
|
||||
{{upperFirst rollName}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Roll details and modifiers -->
|
||||
<div class="roll-details">
|
||||
{{#if rollTarget.weapon}}
|
||||
<div class="detail-item weapon-name">
|
||||
<i class="fa-solid fa-sword"></i>
|
||||
<span>{{rollTarget.weapon.name}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if (eq rollData.favor "favor")}}
|
||||
<span><strong>Favor roll</strong></span>
|
||||
<div class="detail-badge favor-badge">
|
||||
<i class="fa-solid fa-sparkles"></i>
|
||||
<span>Favor</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if (eq rollData.favor "disfavor")}}
|
||||
<span><strong>Disfavor roll</strong></span>
|
||||
{{/if}}
|
||||
{{#if badResult}}
|
||||
<span><strong>{{localize "LETHALFANTASY.Label.otherResult"}}</strong>
|
||||
:
|
||||
{{badResult}}</span>
|
||||
{{/if}}
|
||||
|
||||
{{#if rollTarget.weapon}}
|
||||
<span>{{rollTarget.weapon.name}}</span>
|
||||
{{/if}}
|
||||
|
||||
{{#if rollData.isDamage}}
|
||||
<span><strong>Damage Roll</strong></span>
|
||||
{{/if}}
|
||||
{{#if rollData.damageSmall}}
|
||||
<span><strong>{{localize
|
||||
"LETHALFANTASY.Label.weapon-damage-small"
|
||||
}}</strong></span>
|
||||
{{/if}}
|
||||
{{#if rollData.damageMedium}}
|
||||
<span><strong>{{localize
|
||||
"LETHALFANTASY.Label.weapon-damage-medium"
|
||||
}}</strong></span>
|
||||
<div class="detail-badge disfavor-badge">
|
||||
<i class="fa-solid fa-skull"></i>
|
||||
<span>Disfavor</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if rollData.letItFly}}
|
||||
<span>Let It Fly attack ! </span>
|
||||
<div class="detail-badge special-badge">
|
||||
<i class="fa-solid fa-bow-arrow"></i>
|
||||
<span>Let It Fly!</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if rollData.pointBlank}}
|
||||
<span>Point Blank Range Attack !</span>
|
||||
<div class="detail-badge special-badge">
|
||||
<i class="fa-solid fa-bullseye-arrow"></i>
|
||||
<span>Point Blank</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if rollData.beyondSkill}}
|
||||
<span>Beyond Skill Range Attack !</span>
|
||||
<div class="detail-badge special-badge">
|
||||
<i class="fa-solid fa-target-lock"></i>
|
||||
<span>Beyond Skill</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<span><strong>Formula</strong> : {{titleFormula}}</span>
|
||||
{{#if rollData.damageSmall}}
|
||||
<div class="detail-badge damage-badge">
|
||||
<i class="fa-solid fa-dice-d6"></i>
|
||||
<span>{{localize "LETHALFANTASY.Label.weapon-damage-small"}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if rollData.damageMedium}}
|
||||
<div class="detail-badge damage-badge">
|
||||
<i class="fa-solid fa-dice-d20"></i>
|
||||
<span>{{localize "LETHALFANTASY.Label.weapon-damage-medium"}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if badResult}}
|
||||
<div class="detail-item other-result">
|
||||
<strong>{{localize "LETHALFANTASY.Label.otherResult"}}</strong>: {{badResult}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<!-- Formula and dice results -->
|
||||
<div class="formula-section">
|
||||
<div class="formula-display">
|
||||
<i class="fa-solid fa-dice-d20"></i>
|
||||
<span class="formula-text">{{titleFormula}}</span>
|
||||
</div>
|
||||
{{#if diceResults}}
|
||||
<div class="dice-breakdown">
|
||||
{{#each diceResults as |result|}}
|
||||
<span>{{result.dice}} : {{result.value}}</span>
|
||||
<div class="dice-item">
|
||||
<span class="dice-type">{{result.dice}}</span>
|
||||
<span class="dice-separator">→</span>
|
||||
<span class="dice-value">{{result.value}}</span>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<!-- Result section -->
|
||||
{{#unless isPrivate}}
|
||||
<div class="result-section">
|
||||
<div class="main-result">
|
||||
<div class="result-label">Total</div>
|
||||
<div class="result-value {{#if (eq resultType 'success')}}success{{else}}failure{{/if}}">
|
||||
{{total}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if D30result}}
|
||||
<div class="d30-result">
|
||||
<div class="d30-header">
|
||||
<i class="fa-solid fa-dice-d20"></i>
|
||||
<span class="d30-label">D30 Special</span>
|
||||
<span class="d30-value">{{D30result}}</span>
|
||||
</div>
|
||||
{{#if D30message}}
|
||||
<div class="d30-message">
|
||||
<i class="fa-solid fa-wand-magic-sparkles"></i>
|
||||
<span>{{D30message}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if isSave}}
|
||||
<div class="result">
|
||||
<div class="outcome-badge {{#if (eq resultType 'success')}}success-outcome{{else}}failure-outcome{{/if}}">
|
||||
{{#if (eq resultType "success")}}
|
||||
{{#if isPrivate}}?{{else}}{{localize
|
||||
"LETHALFANTASY.Roll.success"
|
||||
}}{{/if}}
|
||||
<i class="fa-solid fa-circle-check"></i>
|
||||
{{localize "LETHALFANTASY.Roll.success"}}
|
||||
{{else}}
|
||||
{{#if isPrivate}}?{{else}}{{localize
|
||||
"LETHALFANTASY.Roll.failure"
|
||||
}}{{/if}}
|
||||
<i class="fa-solid fa-circle-xmark"></i>
|
||||
{{localize "LETHALFANTASY.Roll.failure"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if isResource}}
|
||||
<div class="result">
|
||||
<div class="outcome-badge {{#if (eq resultType 'success')}}success-outcome{{else}}failure-outcome{{/if}}">
|
||||
{{#if (eq resultType "success")}}
|
||||
{{#if isPrivate}}?{{else}}{{localize
|
||||
"LETHALFANTASY.Roll.success"
|
||||
}}{{/if}}
|
||||
<i class="fa-solid fa-circle-check"></i>
|
||||
{{localize "LETHALFANTASY.Roll.success"}}
|
||||
{{else}}
|
||||
{{#if isPrivate}}?{{else}}{{localize "LETHALFANTASY.Roll.failure"}}{{#if
|
||||
isFailure
|
||||
}} ({{localize "LETHALFANTASY.Roll.resourceLost"}}){{/if}}{{/if}}
|
||||
<i class="fa-solid fa-circle-xmark"></i>
|
||||
{{localize "LETHALFANTASY.Roll.failure"}}
|
||||
{{#if isFailure}}
|
||||
<span class="resource-lost">({{localize "LETHALFANTASY.Roll.resourceLost"}})</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if isDamage}}
|
||||
<div>
|
||||
{{#if (and isGM hasTarget)}}
|
||||
<div class="target-info">
|
||||
<i class="fa-solid fa-crosshairs"></i>
|
||||
{{{localize
|
||||
"LETHALFANTASY.Roll.displayArmor"
|
||||
targetName=targetName
|
||||
targetArmor=targetArmor
|
||||
realDamage=realDamage
|
||||
}}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#unless isPrivate}}
|
||||
<div class="dice-result">
|
||||
<h4 class="dice-total">{{total}}</h4>
|
||||
</div>
|
||||
{{#if D30result}}
|
||||
<div class="dice-result">
|
||||
<h4 class="dice-total">D30 result: {{D30result}}</h4>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{else}}
|
||||
<div class="private-result">
|
||||
<i class="fa-solid fa-eye-slash"></i>
|
||||
<span>Private Roll</span>
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
{{#if weaponDamageOptions}}
|
||||
{{#if canSelectTarget}}
|
||||
<div class="damage-buttons">
|
||||
<div class="damage-buttons-title">{{localize
|
||||
"LETHALFANTASY.Label.rollDamage"
|
||||
}}</div>
|
||||
<div class="damage-buttons-grid {{#if weaponDamageOptions.isMonster}}monster-damage{{/if}}">
|
||||
{{#if weaponDamageOptions.isMonster}}
|
||||
<button
|
||||
@@ -129,7 +193,7 @@
|
||||
data-damage-formula="{{weaponDamageOptions.damageFormula}}"
|
||||
data-damage-modifier="{{weaponDamageOptions.damageModifier}}"
|
||||
data-is-monster="true"
|
||||
title="{{weaponDamageOptions.weaponName}}"
|
||||
title="{{localize 'LETHALFANTASY.Label.rollDamage'}} - {{weaponDamageOptions.weaponName}}"
|
||||
>
|
||||
<i class="fa-solid fa-dice"></i>
|
||||
Damage:
|
||||
@@ -145,10 +209,10 @@
|
||||
data-damage-type="small"
|
||||
data-damage-formula="{{weaponDamageOptions.damageS}}"
|
||||
data-is-monster="false"
|
||||
title="{{localize 'LETHALFANTASY.Label.weapon-damage-small'}}"
|
||||
title="{{localize 'LETHALFANTASY.Label.rollDamage'}}"
|
||||
>
|
||||
<i class="fa-solid fa-dice-d6"></i>
|
||||
{{localize "LETHALFANTASY.Label.weapon-damage-small"}}
|
||||
Damage Small
|
||||
</button>
|
||||
{{/if}}
|
||||
{{#if weaponDamageOptions.damageM}}
|
||||
@@ -158,18 +222,27 @@
|
||||
data-damage-type="medium"
|
||||
data-damage-formula="{{weaponDamageOptions.damageM}}"
|
||||
data-is-monster="false"
|
||||
title="{{localize 'LETHALFANTASY.Label.weapon-damage-medium'}}"
|
||||
title="{{localize 'LETHALFANTASY.Label.rollDamage'}}"
|
||||
>
|
||||
<i class="fa-solid fa-dice-d20"></i>
|
||||
{{localize "LETHALFANTASY.Label.weapon-damage-medium"}}
|
||||
Damage Medium
|
||||
</button>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if rollData.isDamage}}
|
||||
{{#if defenderId}}
|
||||
<div class="damage-result auto-applied">
|
||||
<div class="auto-damage-notice">
|
||||
<i class="fa-solid fa-check-circle"></i>
|
||||
<span>Damage automatically applied to defender (with damage reduction)</span>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="damage-result">
|
||||
<ul>
|
||||
<li class="li-apply-wounds">
|
||||
@@ -189,5 +262,30 @@
|
||||
</ul>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if isAttack}}
|
||||
{{#if canSelectTarget}}
|
||||
<div class="attack-targets">
|
||||
<ul>
|
||||
<li class="li-select-target">
|
||||
<div>{{localize "LETHALFANTASY.Label.selectTarget"}}</div>
|
||||
<div class="combatants-grid">
|
||||
{{#each combatants}}
|
||||
<button
|
||||
class="request-defense-btn"
|
||||
data-combatant-id="{{this.id}}"
|
||||
data-token-id="{{this.tokenId}}"
|
||||
title="{{this.name}}"
|
||||
>
|
||||
{{this.name}}
|
||||
</button>
|
||||
{{/each}}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
{{!-- GM Controls --}}
|
||||
{{#if user.isGM}}
|
||||
{{#if combat.round}}
|
||||
{{#if (or combat.round (eq combat.turn 0))}}
|
||||
<!-- <button type="button" class="inline-control combat-control icon fa-solid fa-backward-step"
|
||||
data-action="previousRound" data-tooltip aria-label="{{ localize "COMBAT.RoundPrev" }}"></button>
|
||||
<button type="button" class="inline-control combat-control icon fa-solid fa-arrow-left" data-action="previousTurn"
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
{{!-- Combat Status --}}
|
||||
<strong class="encounter-title">
|
||||
{{#if combats.length}}
|
||||
{{#if combat.round}}
|
||||
{{#if (or combat.round (eq combat.turn 0))}}
|
||||
{{ localize "COMBAT.Round" round=combat.round }}
|
||||
{{else}}
|
||||
{{ localize "COMBAT.NotStarted" }}
|
||||
|
||||
36
templates/damage-applied-message.hbs
Normal file
36
templates/damage-applied-message.hbs
Normal file
@@ -0,0 +1,36 @@
|
||||
<div class="fvtt-lethal-fantasy damage-applied-message">
|
||||
<div class="damage-header">
|
||||
<div class="damage-icon">
|
||||
<i class="fa-solid fa-heart-crack"></i>
|
||||
</div>
|
||||
<div class="damage-info">
|
||||
<div class="target-name">{{targetName}}</div>
|
||||
<div class="damage-subtitle">{{localize "LETHALFANTASY.DamageApplied.subtitle"}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="damage-details">
|
||||
<div class="damage-amount">
|
||||
<span class="damage-label">{{localize "LETHALFANTASY.DamageApplied.damageDealt"}}</span>
|
||||
<span class="damage-value">{{damage}}</span>
|
||||
{{#if rawDamage}}
|
||||
<span class="damage-raw">({{rawDamage}} raw)</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if drText}}
|
||||
<div class="damage-reduction">
|
||||
<i class="fa-solid fa-shield"></i>
|
||||
<span>{{drText}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="damage-source">
|
||||
<i class="fa-solid fa-crosshairs"></i>
|
||||
<span>{{localize "LETHALFANTASY.DamageApplied.from"}}
|
||||
{{#if attackerName}}<strong>{{attackerName}}</strong> with {{/if}}
|
||||
<strong>{{weaponName}}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
43
templates/gain-hp-hud.hbs
Normal file
43
templates/gain-hp-hud.hbs
Normal file
@@ -0,0 +1,43 @@
|
||||
<div class="control-icon" data-action="lethal-gain-hp-hud">
|
||||
<img
|
||||
class="lethal-hp-gain-hud"
|
||||
src="systems/fvtt-lethal-fantasy/assets/icons/health-increase.svg"
|
||||
width="36"
|
||||
height="36"
|
||||
title="Restore HP"
|
||||
/>
|
||||
|
||||
<div class="hp-gain-wrap hp-gain-wrap-col1 hp-gain-hud-disabled">
|
||||
{{#for 1 11 1}}
|
||||
<button
|
||||
class="hud-gain-hp-button-select gain-hp-hud-click"
|
||||
data-hp-value="{{this}}"
|
||||
>
|
||||
<span class="">+{{this}}</span>
|
||||
</button>
|
||||
{{/for}}
|
||||
</div>
|
||||
|
||||
<div class="hp-gain-wrap hp-gain-wrap-col2 hp-gain-hud-disabled">
|
||||
{{#for 11 21 1}}
|
||||
<button
|
||||
class="hud-gain-hp-button-select gain-hp-hud-click"
|
||||
data-hp-value="{{this}}"
|
||||
>
|
||||
<span class="">+{{this}}</span>
|
||||
</button>
|
||||
{{/for}}
|
||||
</div>
|
||||
|
||||
<div class="hp-gain-wrap hp-gain-wrap-col3 hp-gain-hud-disabled">
|
||||
{{#for 21 31 1}}
|
||||
<button
|
||||
class="hud-gain-hp-button-select gain-hp-hud-click"
|
||||
data-hp-value="{{this}}"
|
||||
>
|
||||
<span class="">+{{this}}</span>
|
||||
</button>
|
||||
{{/for}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -22,7 +22,7 @@
|
||||
{{#each system.attacks as |item key|}}
|
||||
<div class="attack" data-attack-key="{{key}}" >
|
||||
<div class="">
|
||||
<input type="checkbox" name="system.attacks.{{item.key}}.enabled" value="{{item.enabled}}" data-tooltip="Attack enabled/disabled" />
|
||||
<input type="checkbox" name="system.attacks.{{item.key}}.enabled" {{checked item.enabled}} data-tooltip="Attack enabled/disabled" />
|
||||
</div>
|
||||
<div class="name">
|
||||
<input type="text" name="system.attacks.{{item.key}}.name" value="{{item.name}}" data-tooltip="Attack name" />
|
||||
@@ -65,4 +65,54 @@
|
||||
{{/each}}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Hand To Hand Attacks</legend>
|
||||
<div class="attacks">
|
||||
{{#each system.combatHTH as |item key|}}
|
||||
<div class="attack" data-attack-key="{{key}}" >
|
||||
<div class="">
|
||||
<input type="checkbox" name="system.combatHTH.{{item.key}}.enabled" {{checked item.enabled}} data-tooltip="HTH Attack enabled/disabled" />
|
||||
</div>
|
||||
<div class="name">
|
||||
<input type="text" name="system.combatHTH.{{item.key}}.name" value="{{item.name}}" data-tooltip="HTH Attack name" />
|
||||
</div>
|
||||
<div class="numeric">
|
||||
<input type="number" name="system.combatHTH.{{item.key}}.attackScore" value="{{item.attackScore}}" data-tooltip="Progression number" />
|
||||
</div>
|
||||
<div class="numeric">
|
||||
<input type="number" name="system.combatHTH.{{item.key}}.attackModifier" value="{{item.attackModifier}}" data-tooltip="Attack modifier" />
|
||||
</div>
|
||||
<div class="numeric">
|
||||
<input type="number" name="system.combatHTH.{{item.key}}.defenseModifier" value="{{item.defenseModifier}}" data-tooltip="Defense modifier"/>
|
||||
</div>
|
||||
<div class="damage-dice">
|
||||
<input type="text" name="system.combatHTH.{{item.key}}.damageDice" value="{{item.damageDice}}" data-tooltip="Damage dice"/>
|
||||
</div>
|
||||
<div class="numeric">
|
||||
<input type="number" name="system.combatHTH.{{item.key}}.damageModifier" value="{{item.damageModifier}}" data-tooltip="Damage modifier"/>
|
||||
</div>
|
||||
|
||||
<div class="attack-icons">
|
||||
|
||||
<a class="rollable" data-roll-type="monster-attack-hth" data-roll-key="{{item.key}}" data-tooltip="Roll HTH Attack">
|
||||
<i class="lf-roll-small fa-solid fa-hand-fist" data-roll-type="monster-attack-hth" data-roll-key="{{item.key}}"></i>
|
||||
</a>
|
||||
|
||||
<a class="rollable" data-roll-type="monster-defense-hth" data-roll-key="{{item.key}}" data-tooltip="Roll HTH Defense">
|
||||
<i class="fa-solid fa-shield-halved" data-roll-type="monster-defense-hth" data-roll-key="{{item.key}}"></i>
|
||||
</a>
|
||||
|
||||
<a class="rollable" data-roll-type="monster-damage-hth" data-roll-key="{{item.key}}"
|
||||
data-tooltip="Roll HTH Damage">
|
||||
<i class="fa-regular fa-face-head-bandage" data-roll-type="monster-damage-hth"
|
||||
data-roll-key="{{item.key}}"></i>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</fieldset>
|
||||
</section>
|
||||
@@ -291,13 +291,6 @@
|
||||
disabled=isPlayMode
|
||||
data-char-id="int"
|
||||
}}
|
||||
|
||||
{{formField
|
||||
systemFields.characteristics.fields.int.fields.percent
|
||||
value=system.characteristics.int.percent
|
||||
disabled=isPlayMode
|
||||
type="number"
|
||||
}}
|
||||
</div>
|
||||
<div class="monster-characteristic">
|
||||
<span>{{localize "LETHALFANTASY.Label.dex"}}</span>
|
||||
@@ -307,13 +300,6 @@
|
||||
disabled=isPlayMode
|
||||
data-char-id="wis"
|
||||
}}
|
||||
|
||||
{{formField
|
||||
systemFields.characteristics.fields.dex.fields.percent
|
||||
value=system.characteristics.dex.percent
|
||||
disabled=isPlayMode
|
||||
type="number"
|
||||
}}
|
||||
</div>
|
||||
|
||||
<legend>{{localize "LETHALFANTASY.Label.Movement"}}</legend>
|
||||
|
||||
44
templates/progression-message.hbs
Normal file
44
templates/progression-message.hbs
Normal file
@@ -0,0 +1,44 @@
|
||||
<div class="fvtt-lethal-fantasy progression-message {{#if success}}progression-success{{else}}progression-failure{{/if}}">
|
||||
<div class="progression-header">
|
||||
<div class="progression-icon">
|
||||
{{#if success}}
|
||||
<i class="fa-solid fa-check-circle"></i>
|
||||
{{else}}
|
||||
<i class="fa-solid fa-hourglass-half"></i>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="progression-info">
|
||||
<div class="actor-name">{{actorName}}</div>
|
||||
<div class="progression-subtitle">
|
||||
{{#if success}}
|
||||
{{localize "LETHALFANTASY.ProgressionMessage.canAct"}}
|
||||
{{else}}
|
||||
{{localize "LETHALFANTASY.ProgressionMessage.cannotAct"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progression-details">
|
||||
{{#if weaponName}}
|
||||
<div class="progression-weapon">
|
||||
<i class="fa-solid fa-sword"></i>
|
||||
<span><strong>{{weaponName}}</strong></span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if rollResult}}
|
||||
<div class="progression-roll">
|
||||
<span class="roll-label">{{localize "LETHALFANTASY.ProgressionMessage.diceResult"}}</span>
|
||||
<span class="roll-value">{{rollResult}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if progressionCount}}
|
||||
<div class="progression-count">
|
||||
<i class="fa-solid fa-dice-d20"></i>
|
||||
<span>{{localize "LETHALFANTASY.ProgressionMessage.progressionCount"}} <strong>{{progressionCount}}</strong></span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
@@ -30,6 +30,7 @@
|
||||
{{formField systemFields.savingThrow value=system.savingThrow}}
|
||||
{{formField systemFields.extraAetherPoints value=system.extraAetherPoints}}
|
||||
{{formField systemFields.criticalType value=system.criticalType}}
|
||||
{{formField systemFields.damageDice value=system.damageDice}}
|
||||
|
||||
|
||||
<fieldset>
|
||||
|
||||
Reference in New Issue
Block a user