diff --git a/css/fvtt-lethal-fantasy.css b/css/fvtt-lethal-fantasy.css index d7af336..4ba2a24 100644 --- a/css/fvtt-lethal-fantasy.css +++ b/css/fvtt-lethal-fantasy.css @@ -4139,6 +4139,59 @@ i.lethalfantasy { padding-left: 8px; font-size: 0.9rem; } +/* Luck/Grit Styles */ +#token-hud .luck-grit-wrap { + position: absolute; + left: 75px; + display: none; + top: 50%; + width: 80px; + text-align: start; + background: rgba(0, 0, 0, 0.8); + border: 1px solid rgba(139, 69, 19, 0.5); + border-radius: 4px; + padding: 4px 6px; + transform: translate(-100%, -50%); +} +#token-hud .luck-grit-hud-active { + display: block; +} +#token-hud .luck-grit-hud-disabled { + display: none; +} +#token-hud .luck-grit-row { + display: flex; + align-items: center; + gap: 4px; + margin: 2px 0; +} +#token-hud .luck-grit-label { + flex: 1; + color: #c9b896; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; +} +#token-hud .luck-grit-btn { + width: 28px; + height: 22px; + padding: 0; + background: rgba(139, 69, 19, 0.25); + border: 1px solid rgba(139, 69, 19, 0.4); + border-radius: 3px; + color: #d4c5a9; + font-size: 12px; + font-weight: 700; + cursor: pointer; + line-height: 1; + display: flex; + align-items: center; + justify-content: center; +} +#token-hud .luck-grit-btn:hover { + background: rgba(139, 69, 19, 0.5); + border-color: rgba(139, 69, 19, 0.7); +} /* -------------------------------------------------- */ /* Dice Tray — injected into the Foundry chat sidebar */ /* -------------------------------------------------- */ diff --git a/lang/en.json b/lang/en.json index 9571b7c..67c28da 100644 --- a/lang/en.json +++ b/lang/en.json @@ -505,8 +505,6 @@ "monster-defense": "Monster defense", "weapons": "Weapons", "wis": "WIS", - "weapon-damage-medium": "Weapon damage medium", - "weapon-damage-small": "Weapon damage small", "combatProgressionStart": "Combat start threshold", "miracle": "Miracle", "titleStandard": "Standard Roll", @@ -828,6 +826,12 @@ "cost": { "label": "Cost" }, + "costOverpowered": { + "label": "Cost (Overpowered)" + }, + "costOverpowered2": { + "label": "Cost (Overpowered 2)" + }, "description": { "label": "Description" }, diff --git a/lethal-fantasy.mjs b/lethal-fantasy.mjs index 1d4a145..c182f37 100644 --- a/lethal-fantasy.mjs +++ b/lethal-fantasy.mjs @@ -242,6 +242,9 @@ Hooks.on("renderChatMessageHTML", (message, html, data) => { console.log(`[LF] request-defense-btn | attackRollType=${attackRollType} defender=${defenderName} defenderType=${combatant.actor?.type}`) const attackD30result = message.rolls[0]?.options?.D30result || null const attackD30message = message.rolls[0]?.options?.D30message || null + const attackDiceResults = message.rolls[0]?.options?.diceResults || null + const attackNaturalRoll = attackDiceResults?.[0]?.value || null + const damageTier = message.rolls[0]?.options?.damageTier || "standard" const attackRerollContext = { rollType: message.rolls[0]?.options?.rollType, rollTarget: foundry.utils.duplicate(message.rolls[0]?.options?.rollTarget ?? {}), @@ -277,6 +280,8 @@ Hooks.on("renderChatMessageHTML", (message, html, data) => { attackD30result, attackD30message, attackRerollContext, + attackNaturalRoll, + damageTier, combatantId, tokenId, isRanged: isRangedAttack @@ -338,6 +343,9 @@ Hooks.on("renderChatMessageHTML", (message, html, data) => { const damageFormula = btn.dataset.damageFormula const damageModifier = btn.dataset.damageModifier const isMonster = btn.dataset.isMonster + const d30Bleed = btn.dataset.d30Bleed === "true" + const d30DamageMultiplier = Number(btn.dataset.d30DamageMult) || 1 + const d30DrMultiplier = Number(btn.dataset.d30DrMult) || 1 // Récupérer l'acteur (soit depuis le message, soit depuis attackerId) const actor = attackerId ? game.actors.get(attackerId) : game.actors.get(message.rolls[0]?.actorId) @@ -392,7 +400,10 @@ Hooks.on("renderChatMessageHTML", (message, html, data) => { defenderTokenId, actorId: actor.id, actorName: actor.name, - actorImage: actor.img + actorImage: actor.img, + d30Bleed, + d30DamageMultiplier, + d30DrMultiplier } await documents.LethalFantasyRoll.rollSpellDamageToMessage(damageFormula, rollOpts) return @@ -400,13 +411,13 @@ Hooks.on("renderChatMessageHTML", (message, html, data) => { // 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) + await actor.system.prepareMonsterRoll("monster-damage", attackKey, undefined, undefined, undefined, defenderId, defenderTokenId, extraShieldDr, { d30Bleed, d30DamageMultiplier, d30DrMultiplier }) return } // Pour les monstres, utiliser prepareMonsterRoll if (isMonster === "true" || actor.type === "monster") { - await actor.system.prepareMonsterRoll("monster-damage", weaponId, undefined, undefined, damageModifier) + await actor.system.prepareMonsterRoll("monster-damage", weaponId, undefined, undefined, damageModifier, defenderId, defenderTokenId, extraShieldDr, { d30Bleed, d30DamageMultiplier, d30DrMultiplier }) return } @@ -417,9 +428,9 @@ Hooks.on("renderChatMessageHTML", (message, html, data) => { return } - // Lancer les dégâts avec la bonne méthode - const rollType = damageType === "small" ? "weapon-damage-small" : "weapon-damage-medium" - await actor.prepareRoll(rollType, weaponId, undefined, defenderId, defenderTokenId, extraShieldDr) + // Lancer les dégâts + const rollType = "weapon-damage" + await actor.prepareRoll(rollType, weaponId, undefined, defenderId, defenderTokenId, extraShieldDr, { d30Bleed, d30DamageMultiplier, d30DrMultiplier }) }) } @@ -483,6 +494,8 @@ Hooks.on("createChatMessage", async (message) => { attackRollKey, attackD30message, attackRerollContext, + attackNaturalRoll, + damageTier, defenderId, defenderTokenId } = attackData @@ -542,12 +555,50 @@ Hooks.on("createChatMessage", async (message) => { let attackRollFinal = attackRoll let canRerollAttack = LethalFantasyUtils.hasD30Reroll(attackD30message) let mulliganRestart = false + // These persist across mulligan restarts (D30 bonus only applied once) + let defenseD30Processed = false + let attackD30Processed = false + // D30 combat effects for damage application + let d30Bleed = false + let d30DamageMultiplier = 1 + let d30DrMultiplier = 1 do { mulliganRestart = false defenderHandledBonus = false attackerHandledBonus = false + // ── D30 bonus dice (defense) — resolved before grit/luck/shield ─────── + if (defenseD30message && !defenseD30Processed && isPrimaryController(defender)) { + const d30Result = await LethalFantasyUtils.processD30BonusDice(defenseD30message, "defense", null, defender) + if (d30Result.modifier) { + defenseRoll += d30Result.modifier + if (d30Result.modifier > 0) { + await createReactionMessage(defender, + `

${defenderName} gains +${d30Result.modifier} from D30 bonus die for defense.

` + ) + } + } + if (d30Result.specialEffect === "auto") { + defenseRoll = attackRollFinal + 1 // auto-block + await createReactionMessage(defender, + `

${defenderName} uses ${d30Result.specialName || "Special Defense"} from D30 — defense automatically succeeds!

` + ) + } + if (d30Result.specialEffect === "flag") { + await createReactionMessage(defender, + `

D30 — ${d30Result.specialName || "Special Effect"} triggered for ${defenderName}!

` + ) + } + if (d30Result.specialEffect === "drMultiplier") { + d30DrMultiplier = d30Result.multiplier + await createReactionMessage(defender, + `

D30 — Defense grants x${d30Result.multiplier} DR (choose which DR types to multiply when damage is applied)

` + ) + } + defenseD30Processed = true + } + // ── Defense reaction loop ────────────────────────────────────────────── if (defender && defenseRoll < attackRollFinal && isPrimaryController(defender) && !isSpellOrMiracle) { while (defenseRoll < attackRollFinal) { @@ -726,6 +777,43 @@ Hooks.on("createChatMessage", async (message) => { if (mulliganRestart) continue + // ── D30 bonus dice (attack) — resolved before grit/luck ──────────────── + if (attackD30message && !attackD30Processed && isPrimaryController(attacker)) { + const d30Result = await LethalFantasyUtils.processD30BonusDice(attackD30message, "attack", attackNaturalRoll, attacker) + if (d30Result.modifier) { + attackRollFinal += d30Result.modifier + if (d30Result.modifier > 0) { + await createReactionMessage(attacker, + `

${attackerName} gains +${d30Result.modifier} from D30 bonus die for attack.

` + ) + } + } + if (d30Result.specialEffect === "auto") { + attackRollFinal = defenseRoll + 1 // auto-hit + await createReactionMessage(attacker, + `

${attackerName} uses ${d30Result.specialName || "Special Strike"} from D30 — attack automatically hits!

` + ) + } + if (d30Result.specialEffect === "flag") { + await createReactionMessage(attacker, + `

D30 — ${d30Result.specialName || "Special Effect"} triggered for ${attackerName}!

` + ) + } + if (d30Result.specialEffect === "bleed") { + d30Bleed = true + await createReactionMessage(attacker, + `

D30 — Bleeding/Internal Injury on hit! Damage past DR will cause a bleeding wound.

` + ) + } + if (d30Result.specialEffect === "damageMultiplier") { + d30DamageMultiplier = d30Result.multiplier + await createReactionMessage(attacker, + `

D30 — x${d30Result.multiplier} damage before damage reduction!

` + ) + } + attackD30Processed = true + } + // ── Attack reaction loop ─────────────────────────────────────────────── if (!defenderHandledBonus && attacker && attackRollFinal <= defenseRoll && isPrimaryController(attacker)) { while (attackRollFinal <= defenseRoll) { @@ -837,7 +925,11 @@ Hooks.on("createChatMessage", async (message) => { defenderTokenId, defenseRoll, outcome, - shieldDamageReduction + shieldDamageReduction, + d30Bleed: d30Bleed ? "true" : "", + d30DamageMultiplier: d30DamageMultiplier, + d30DrMultiplier: d30DrMultiplier, + damageTier: damageTier || "standard" }) } else { console.log("Skipping message creation", { attackerHandledBonus, defenderHandledBonus }) @@ -865,15 +957,19 @@ Hooks.on("createChatMessage", async (message) => { const spell = spellId ? actor.items.get(spellId) : null if (!spell || spell.type !== "spell") return - const cost = Number(spell.system?.cost) || 0 + const damageTier = message.rolls[0]?.options?.damageTier || "standard" + const tierCostMap = { standard: "cost", overpowered: "costOverpowered", overpowered2: "costOverpowered2" } + const costField = tierCostMap[damageTier] || "cost" + const cost = Number(spell.system?.[costField]) || 0 if (cost <= 0) return const currentAether = Number(actor.system.aetherPoints?.value) || 0 const newAether = Math.max(0, currentAether - cost) await actor.update({ "system.aetherPoints.value": newAether }) + const tierLabel = damageTier === "standard" ? "" : ` (${damageTier})` await ChatMessage.create({ - content: `

🔮 ${actor.name} casts ${spell.name} — spends ${cost} Aether (${currentAether} → ${newAether}).

`, + content: `

🔮 ${actor.name} casts ${spell.name}${tierLabel} — spends ${cost} Aether (${currentAether} → ${newAether}).

`, speaker: ChatMessage.getSpeaker({ actor }) }) }) @@ -966,13 +1062,94 @@ Hooks.on("createChatMessage", async (message) => { const attackerName = message.rolls[0]?.options?.actorName || "Unknown Attacker" const rollType = message.rolls[0]?.options?.rollType + // Lire les effets D30 + const d30Bleed = message.rolls[0]?.options?.d30Bleed || false + const d30DamageMultiplier = message.rolls[0]?.options?.d30DamageMultiplier || 1 + const d30DrMultiplier = message.rolls[0]?.options?.d30DrMultiplier || 1 + + // Appliquer le multiplicateur de dégâts D30 au total AVANT DR + const rawDamage = damageTotal * d30DamageMultiplier + // Calculer les DR — les sorts utilisent une DR manuelle saisie par l'utilisateur const isSpellDamage = rollType === "spell-damage" const manualDR = message.rolls[0]?.options?.manualDR ?? 0 const extraShieldDr = Number(message.rolls[0]?.options?.extraShieldDr) || 0 - const armorDR = isSpellDamage ? manualDR : (defender.computeDamageReduction() || 0) - const totalDR = isSpellDamage ? manualDR : armorDR + extraShieldDr - const finalDamage = Math.max(0, damageTotal - totalDR) + + // Décomposer les DR en composants + let baseDR = 0 + let shieldDR = 0 + let magicDR = 0 + + if (isSpellDamage) { + baseDR = manualDR + } else { + const totalDefDR = defender.computeDamageReduction() || 0 + magicDR = defender.getMagicDR() || 0 + baseDR = totalDefDR - magicDR // naturalDR + armorDR (ou hpDR + combatDR pour les monstres) + shieldDR = extraShieldDr + } + + // Appliquer le multiplicateur de DR D30 si actif — boîte de dialogue + let appliedBaseDR = baseDR + let appliedShieldDR = shieldDR + let appliedMagicDR = magicDR + + if (d30DrMultiplier > 1) { + const drResult = await (async () => { + const checks = { + base: true, + shield: shieldDR > 0, + magic: magicDR > 0 + } + const html = ` +
+

D30 DR Multiplier ×${d30DrMultiplier}

+

Choose which DR types to multiply:

+ + + +
+ ` + const result = await foundry.applications.api.DialogV2.wait({ + window: { title: "Apply D30 DR Multiplier" }, + classes: ["lethalfantasy"], + content: html, + buttons: [ + { + action: "apply", + label: "Apply Damage", + icon: "fa-solid fa-check", + callback: (event, button) => { + const form = button.form || button.closest("form") + return { + applyBase: form.querySelector("#d30-dr-base")?.checked || false, + applyShield: form.querySelector("#d30-dr-shield")?.checked || false, + applyMagic: form.querySelector("#d30-dr-magic")?.checked || false + } + } + } + ], + rejectClose: false + }) + return result || { applyBase: false, applyShield: false, applyMagic: false } + })() + + appliedBaseDR = drResult.applyBase ? baseDR * d30DrMultiplier : baseDR + appliedShieldDR = drResult.applyShield ? shieldDR * d30DrMultiplier : shieldDR + appliedMagicDR = drResult.applyMagic ? magicDR * d30DrMultiplier : magicDR + } + + const totalDR = appliedBaseDR + appliedShieldDR + appliedMagicDR + const finalDamage = Math.max(0, rawDamage - 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 @@ -987,34 +1164,70 @@ Hooks.on("createChatMessage", async (message) => { // 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. + const applyDamageToActor = async (actor) => { + await actor.applyDamage(-finalDamage) + // Create bleeding wound if D30 triggered it + if (d30Bleed && finalDamage > 0 && actor.system.hp?.wounds) { + const wounds = foundry.utils.duplicate(actor.system.hp.wounds) + const slot = wounds.findIndex(w => !w.value && !w.duration) + if (slot !== -1) { + wounds[slot] = { value: finalDamage, duration: finalDamage, description: "Bleeding" } + await actor.update({ "system.hp.wounds": wounds }) + } + } + } + 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) + await applyDamageToActor(tokenActor) } else { game.socket.emit(`system.${SYSTEM.id}`, { type: "applyDamage", actorId: defender.id, tokenId: defenderTokenId, damage: -finalDamage }) + // Also emit wound creation for bleeding + if (d30Bleed && finalDamage > 0 && defender.system.hp?.wounds) { + game.socket.emit(`system.${SYSTEM.id}`, { type: "applyBleeding", actorId: defender.id, tokenId: defenderTokenId, damage: finalDamage }) + } } + // Build DR text for confirmation message + let drText = "" + if (isSpellDamage) { + drText = manualDR > 0 ? `Spell DR: ${manualDR}` : "No DR applied" + } else { + const parts = [] + if (appliedBaseDR > 0) parts.push(`Base DR: ${appliedBaseDR}${d30DrMultiplier > 1 && appliedBaseDR !== baseDR ? ` (×${d30DrMultiplier})` : ""}`) + if (appliedShieldDR > 0) parts.push(`Shield DR: ${appliedShieldDR}${d30DrMultiplier > 1 && appliedShieldDR !== shieldDR ? ` (×${d30DrMultiplier})` : ""}`) + if (appliedMagicDR > 0) parts.push(`Magic DR: ${appliedMagicDR}${d30DrMultiplier > 1 && appliedMagicDR !== magicDR ? ` (×${d30DrMultiplier})` : ""}`) + drText = parts.length > 0 ? parts.join(" + ") : "No DR applied" + } + + // Build raw damage text showing D30 multiplier if active + const rawDamageText = d30DamageMultiplier > 1 + ? `${damageTotal} × ${d30DamageMultiplier} = ${rawDamage}` + : String(damageTotal) + // 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: isSpellDamage - ? (manualDR > 0 ? `Spell DR: ${manualDR}` : "No DR applied") - : (totalDR > 0 ? `Armor DR: ${armorDR}${extraShieldDr > 0 ? ` + Shield DR: ${extraShieldDr}` : ""}` : ""), + drText, weaponName: weaponName, attackerName: attackerName, - rawDamage: damageTotal + rawDamage: rawDamageText } ) + // Add bleeding notification + let bleedContent = "" + if (d30Bleed && finalDamage > 0) { + bleedContent = `

Bleeding: Wound of ${finalDamage} HP for ${finalDamage} seconds.

` + } + await ChatMessage.create({ - content: messageContent, + content: messageContent + bleedContent, speaker: ChatMessage.getSpeaker({ actor: defender }), whisper: ChatMessage.getWhisperRecipients("GM") }) diff --git a/module/config/d30_results_tables.json b/module/config/d30_results_tables.json index afc93ca..8d04f70 100644 --- a/module/config/d30_results_tables.json +++ b/module/config/d30_results_tables.json @@ -346,7 +346,7 @@ "target": "spell_defense" } ], - "description": "Possible 20/20 Spell defense that Saves Against Any Magical Attack Except a Lethal Magical Strike or add D12 to defense" + "description": "Possible 20/20 Spell defense that Saves Against Any Magical Attack Except a Lethal Magical Strike or add D12 to spell defense" }, "skill_rolls": { "type": "bonus_flat", @@ -386,10 +386,10 @@ "type": "bleed" }, { - "type": "knockback" + "type": "internal_injury" } ], - "description": "Bleed, Knock-back on Hit" + "description": "Bleed, Internal Injury on Hit" }, "ranged_attack": { "type": "bleed", @@ -399,10 +399,9 @@ "type": "counter_attack", "options": [ "kick", - "punch", - "shield_bash" + "punch" ], - "description": "Kick, Punch or Shield Bash" + "description": "Kick or Punch" }, "skill_rolls": { "type": "bonus_flat", @@ -414,10 +413,9 @@ "type": "counter_attack", "options": [ "kick", - "punch", - "shield_bash" + "punch" ], - "description": "Kick, Punch or Shield Bash" + "description": "Kick or Punch" }, "saving_throws": { "type": "bonus_flat", @@ -446,10 +444,10 @@ "type": "bleed" }, { - "type": "knockback" + "type": "internal_injury" } ], - "description": "Bleed, Knock-back on Hit" + "description": "Bleed, Internal Injury on Hit" }, "ranged_attack": { "type": "bleed", @@ -459,10 +457,9 @@ "type": "counter_attack", "options": [ "kick", - "punch", - "shield_bash" + "punch" ], - "description": "Kick, Punch or Shield Bash" + "description": "Kick or Punch" }, "skill_rolls": { "type": "bonus_flat", @@ -474,10 +471,9 @@ "type": "counter_attack", "options": [ "kick", - "punch", - "shield_bash" + "punch" ], - "description": "Kick, Punch or Shield Bash" + "description": "Kick or Punch" }, "saving_throws": { "type": "bonus_flat", @@ -539,10 +535,10 @@ "type": "bleed" }, { - "type": "knockback" + "type": "internal_injury" } ], - "description": "Bleed, Knock-back on Hit" + "description": "Bleed, Internal Injury on Hit" }, "ranged_attack": { "type": "bleed", @@ -552,10 +548,9 @@ "type": "counter_attack", "options": [ "kick", - "punch", - "shield_bash" + "punch" ], - "description": "Kick, Punch, or Shield Bash" + "description": "Kick or Punch" }, "skill_rolls": { "type": "bonus_flat", @@ -567,10 +562,9 @@ "type": "counter_attack", "options": [ "kick", - "punch", - "shield_bash" + "punch" ], - "description": "Kick, Punch, or Shield Bash" + "description": "Kick or Punch" }, "saving_throws": { "type": "bonus_flat", diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 2355b09..77ff631 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -79,16 +79,28 @@ export default class LethalFantasyActor extends Actor { } /* *************************************************/ - computeDamageReduction() { - // Pour les monstres, utiliser hp.damageResistance et combat.damageReduction + getNaturalDR() { if (this.type === "monster") { - let hpDR = Number(this.system.hp?.damageResistance) || 0 + return Number(this.system.hp?.damageResistance) || 0 + } + return Number(this.system.biodata?.naturalDR) || 0 + } + + /* *************************************************/ + getMagicDR() { + if (this.type === "monster") return 0 + return Number(this.system.biodata?.magicDR) || 0 + } + + /* *************************************************/ + computeDamageReduction() { + if (this.type === "monster") { + let hpDR = this.getNaturalDR() let combatDR = Number(this.system.combat?.damageReduction) || 0 return hpDR + combatDR } - // Pour les personnages, utiliser biodata et items - let naturalDR = Number(this.system.biodata?.naturalDR) || 0 - let magicDR = Number(this.system.biodata?.magicDR) || 0 + let naturalDR = this.getNaturalDR() + let magicDR = this.getMagicDR() let armorDR = this.getArmorDR() return naturalDR + magicDR + armorDR } @@ -153,7 +165,7 @@ export default class LethalFantasyActor extends Actor { } /* *************************************************/ - async prepareRoll(rollType, rollKey, rollDice, defenderId, defenderTokenId, extraShieldDr = 0) { + async prepareRoll(rollType, rollKey, rollDice, defenderId, defenderTokenId, extraShieldDr = 0, d30Effects = {}) { console.log("Preparing roll", rollType, rollKey, rollDice, defenderId) let rollTarget switch (rollType) { @@ -198,8 +210,15 @@ export default class LethalFantasyActor extends Actor { case "miracle-power": rollTarget = this.items.find((i) => (i.type === "miracle" || i.type == "spell") && i.id === rollKey) rollTarget.rollKey = rollKey + // Read damage tier from combatant currentAction if available + const activeCombatant = game.combat?.combatants?.find(c => c.actorId === this.id) + const currentAction = activeCombatant?.getFlag(SYSTEM.id, "currentAction") + const damageTier = currentAction?.damageTier || "standard" + rollTarget.damageTier = damageTier if (rollType === "spell-attack" || rollType === "spell-power") { - const cost = Number(rollTarget.system?.cost) || 0 + const tierCostMap = { standard: "cost", overpowered: "costOverpowered", overpowered2: "costOverpowered2" } + const costField = tierCostMap[damageTier] || "cost" + const cost = Number(rollTarget.system?.[costField]) || 0 const currentAether = Number(this.system.aetherPoints?.value) || 0 if (cost > currentAether) { ui.notifications.warn(`${this.name} cannot cast ${rollTarget.name}: insufficient Aether (needs ${cost}, has ${currentAether}).`) @@ -222,8 +241,7 @@ export default class LethalFantasyActor extends Actor { rollTarget.rollKey = rollKey } break; - case "weapon-damage-small": - case "weapon-damage-medium": + case "weapon-damage": case "weapon-attack": case "weapon-defense": { let weapon = this.items.find((i) => i.type === "weapon" && i.id === rollKey) @@ -264,7 +282,7 @@ export default class LethalFantasyActor extends Actor { combat: foundry.utils.duplicate(this.system.combat), isRangedAttack: weapon.system.weaponType === "ranged" } - if (rollType === "weapon-damage-small" || rollType === "weapon-damage-medium") { + if (rollType === "weapon-damage") { rollTarget.grantedDice = this.system.granted.damageDice } if (rollType === "weapon-attack") { @@ -287,7 +305,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, defenderId, defenderTokenId, extraShieldDr) + await this.system.roll(rollType, rollTarget, defenderId, defenderTokenId, extraShieldDr, d30Effects) } } diff --git a/module/documents/roll.mjs b/module/documents/roll.mjs index 0d84bf4..6280c45 100644 --- a/module/documents/roll.mjs +++ b/module/documents/roll.mjs @@ -285,13 +285,7 @@ export default class LethalFantasyRoll extends Roll { let damageBonus = (options.rollTarget.weapon.system.applyStrengthDamageBonus) ? options.rollTarget.combat.damageModifier : 0 options.rollTarget.value = damageBonus + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.damageBonus options.rollTarget.charModifier = damageBonus - if (options.rollType.includes("small")) { - options.damageSmall = true - dice = options.rollTarget.weapon.system.damage.damageS - } else { - options.damageMedium = true - dice = options.rollTarget.weapon.system.damage.damageM - } + dice = options.rollTarget.weapon.system.damage.damageM if (/NE$/i.test(dice)) { hasMaxValue = false hasExplode = false @@ -529,8 +523,6 @@ export default class LethalFantasyRoll extends Roll { rollMode: rollContext.visibility, hasTarget: options.hasTarget, isDamage: options.isDamage, - damageSmall: options.damageSmall, - damageMedium: options.damageMedium, pointBlank, beyondSkill, letItFly, @@ -614,6 +606,11 @@ export default class LethalFantasyRoll extends Roll { options.D30message = d30Message } + // Show main roll before explosion dice so Dice So Nice animates in correct order + if (game?.dice3d && rollContext.favor !== "favor" && rollContext.favor !== "disfavor") { + await game.dice3d.showForRoll(rollBase, game.user, true) + } + let rollTotal = 0 let diceResults = [] let resultType @@ -673,6 +670,10 @@ export default class LethalFantasyRoll extends Roll { rollBase.options.defenderId = options.defenderId rollBase.options.defenderTokenId = options.defenderTokenId rollBase.options.extraShieldDr = options.extraShieldDr || 0 + rollBase.options.damageTier = options.damageTier || "standard" + rollBase.options.d30Bleed = options.d30Bleed || false + rollBase.options.d30DamageMultiplier = options.d30DamageMultiplier || 1 + rollBase.options.d30DrMultiplier = options.d30DrMultiplier || 1 /** * A hook event that fires after the roll has been made. @@ -903,6 +904,29 @@ export default class LethalFantasyRoll extends Roll { actionItem.progressionCount = firstActionTaken ? 1 : (combatant.actor.system.combat?.combatProgressionStart ?? 1) if (!firstActionTaken) await combatant.setFlag(SYSTEM.id, "firstActionTaken", true) actionItem.rangedMode = rangedMode + // If this is a spell/miracle with multiple damage tiers, prompt tier choice + if (actionItem.system?.damageDice) { + const tiers = [ + { id: "standard", label: "Standard", dice: actionItem.system.damageDice }, + { id: "overpowered", label: "Overpowered", dice: actionItem.system.damageDiceOverpowered }, + { id: "overpowered2", label: "Overpowered 2", dice: actionItem.system.damageDiceOverpowered2 }, + ].filter(t => t.dice) + if (tiers.length > 1) { + const tierChoice = await foundry.applications.api.DialogV2.wait({ + window: { title: "Choose Damage Tier" }, + classes: ["lethalfantasy"], + content: `

${selectedItem.name} has multiple damage tiers.

Choose which damage to use when the attack lands:

`, + buttons: tiers.map(t => ({ + action: t.id, + label: `${t.label} (${t.dice.toUpperCase()})`, + icon: "fa-solid fa-wand-magic-sparkles", + callback: () => t.id + })), + rejectClose: false + }) + actionItem.damageTier = tierChoice || "standard" + } + } actionItem.castingTime = 1 actionItem.spellStatus = "castingTime" // Set the flag on the combatant @@ -1392,10 +1416,8 @@ export default class LethalFantasyRoll extends Roll { return `${game.i18n.localize("LETHALFANTASY.Label.weapon-attack")}` case "weapon-defense": return `${game.i18n.localize("LETHALFANTASY.Label.weapon-defense")}` - case "weapon-damage-small": - return `${game.i18n.localize("LETHALFANTASY.Label.weapon-damage-small")}` - case "weapon-damage-medium": - return `${game.i18n.localize("LETHALFANTASY.Label.weapon-damage-medium")}` + case "weapon-damage": + return `${game.i18n.localize("LETHALFANTASY.Label.weapon-damage")}` case "spell": case "spell-attack": case "spell-power": @@ -1456,7 +1478,6 @@ export default class LethalFantasyRoll extends Roll { weaponDamageOptions = { weaponId: weapon._id || weapon.id, weaponName: weapon.name, - damageS: weapon.system?.damage?.damageS, damageM: weapon.system?.damage?.damageM } console.log("Weapon damage options:", weaponDamageOptions) diff --git a/module/models/character.mjs b/module/models/character.mjs index ef4175e..cb3e611 100644 --- a/module/models/character.mjs +++ b/module/models/character.mjs @@ -281,7 +281,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} - A promise that resolves to null if the roll is cancelled. */ - async roll(rollType, rollTarget, defenderId, defenderTokenId, extraShieldDr = 0) { + async roll(rollType, rollTarget, defenderId, defenderTokenId, extraShieldDr = 0, d30Effects = {}) { const hasTarget = false let roll = await LethalFantasyRoll.prompt({ rollType, @@ -293,7 +293,11 @@ export default class LethalFantasyCharacter extends foundry.abstract.TypeDataMod target: false, defenderId, defenderTokenId, - extraShieldDr + extraShieldDr, + damageTier: rollTarget.damageTier || "standard", + d30Bleed: d30Effects.d30Bleed || false, + d30DamageMultiplier: d30Effects.d30DamageMultiplier || 1, + d30DrMultiplier: d30Effects.d30DrMultiplier || 1 }) if (!roll) return null diff --git a/module/models/monster.mjs b/module/models/monster.mjs index be07716..5935a97 100644 --- a/module/models/monster.mjs +++ b/module/models/monster.mjs @@ -56,10 +56,28 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel }, {}), ) + const woundFieldSchema = { + value: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), + duration: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), + description: new fields.StringField({ initial: "", required: false, nullable: true }), + } + schema.hp = new fields.SchemaField({ value: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }), average: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }), max: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }), + wounds: new fields.ArrayField(new fields.SchemaField(woundFieldSchema), { + initial: [ + { description: "", value: 0, duration: 0 }, + { description: "", value: 0, duration: 0 }, + { description: "", value: 0, duration: 0 }, + { description: "", value: 0, duration: 0 }, + { description: "", value: 0, duration: 0 }, + { description: "", value: 0, duration: 0 }, + { description: "", value: 0, duration: 0 }, + { description: "", value: 0, duration: 0 } + ], min: 8 + }), damageResistance: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), painDamage: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }) }) @@ -164,7 +182,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} - A promise that resolves to null if the roll is cancelled. */ - async roll(rollType, rollTarget, defenderId = undefined, defenderTokenId = undefined, extraShieldDr = 0) { + async roll(rollType, rollTarget, defenderId = undefined, defenderTokenId = undefined, extraShieldDr = 0, d30Effects = {}) { const hasTarget = false // Ranged monster defense uses the ranged defense dialog (movement, range, size modifiers) @@ -192,14 +210,18 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel target: false, defenderId, defenderTokenId, - extraShieldDr + extraShieldDr, + damageTier: rollTarget.damageTier || "standard", + d30Bleed: d30Effects.d30Bleed || false, + d30DamageMultiplier: d30Effects.d30DamageMultiplier || 1, + d30DrMultiplier: d30Effects.d30DrMultiplier || 1 }) if (!roll) return null await roll.toMessage({}, { messageMode: roll.options.rollMode }) } - async prepareMonsterRoll(rollType, rollKey, rollDice = undefined, tokenId = undefined, damageModifier = undefined, defenderId = undefined, defenderTokenId = undefined, extraShieldDr = 0) { + async prepareMonsterRoll(rollType, rollKey, rollDice = undefined, tokenId = undefined, damageModifier = undefined, defenderId = undefined, defenderTokenId = undefined, extraShieldDr = 0, d30Effects = {}) { let rollTarget switch (rollType) { case "monster-attack": @@ -252,8 +274,7 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel }, { messageMode: roll.options.rollMode ?? game.settings.get("core", "rollMode") }) return } - case "weapon-damage-small": - case "weapon-damage-medium": + case "weapon-damage": case "weapon-attack": case "weapon-defense": { let weapon = this.actor.items.find((i) => i.type === "weapon" && i.id === rollKey) @@ -300,7 +321,7 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel // In all cases if (rollTarget) { rollTarget.tokenId = tokenId - await this.roll(rollType, rollTarget, defenderId, defenderTokenId, extraShieldDr) + await this.roll(rollType, rollTarget, defenderId, defenderTokenId, extraShieldDr, d30Effects) } } diff --git a/module/models/spell.mjs b/module/models/spell.mjs index c068c67..eeed377 100644 --- a/module/models/spell.mjs +++ b/module/models/spell.mjs @@ -19,6 +19,8 @@ export default class LethalFantasySpell extends foundry.abstract.TypeDataModel { }) schema.cost = new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 }) + schema.costOverpowered = new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 }) + schema.costOverpowered2 = new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 }) schema.memorized = new fields.BooleanField({ required: true, initial: false }) schema.components = new fields.SchemaField({ diff --git a/module/utils.mjs b/module/utils.mjs index 81d7850..7b674ec 100644 --- a/module/utils.mjs +++ b/module/utils.mjs @@ -105,6 +105,35 @@ export default class LethalFantasyUtils { $(html).find('.hp-gain-wrap')[2].classList.add('hp-gain-hud-disabled'); } }) + + // Luck/Grit Buttons + const luckGritButton = await foundry.applications.handlebars.renderTemplate('systems/fvtt-lethal-fantasy/templates/luck-grit-hud.hbs', {}) + $(html).find('div.left').append(luckGritButton); + $(html).find('.lethal-luck-grit-hud').click((event) => { + event.preventDefault(); + let wrap = $(html).find('.luck-grit-wrap')[0] + if (wrap.classList.contains("luck-grit-hud-disabled")) { + wrap.classList.add('luck-grit-hud-active'); + wrap.classList.remove('luck-grit-hud-disabled'); + } else { + wrap.classList.remove('luck-grit-hud-active'); + wrap.classList.add('luck-grit-hud-disabled'); + } + }) + $(html).find('.luck-grit-btn').click((event) => { + event.preventDefault(); + if (token) { + let tokenFull = canvas.tokens.placeables.find(t => t.id === token._id); + let actor = tokenFull.actor; + const resource = event.currentTarget.dataset.resource; + const amount = Number(event.currentTarget.dataset.amount); + const current = Number(foundry.utils.getProperty(actor.system, `${resource}.current`)) || 0; + const newValue = Math.max(0, current + amount); + actor.update({ [`system.${resource}.current`]: newValue }); + $(html).find('.luck-grit-wrap')[0].classList.remove('luck-grit-hud-active'); + $(html).find('.luck-grit-wrap')[0].classList.add('luck-grit-hud-disabled'); + } + }) }) } @@ -145,6 +174,21 @@ export default class LethalFantasyUtils { LethalFantasyUtils.handleAttackerGritOffer(msg) } break + case "applyBleeding": + if (game.user.isGM) { + actor = msg.tokenId + ? canvas.tokens?.placeables?.find(t => t.id === msg.tokenId)?.actor + : game.actors.get(msg.actorId) + if (actor && actor.system.hp?.wounds && msg.damage > 0) { + const wounds = foundry.utils.duplicate(actor.system.hp.wounds) + const slot = wounds.findIndex(w => !w.value && !w.duration) + if (slot !== -1) { + wounds[slot] = { value: msg.damage, duration: msg.damage, description: "Bleeding" } + actor.update({ "system.hp.wounds": wounds }) + } + } + } + break } } @@ -290,6 +334,8 @@ export default class LethalFantasyUtils { attackD30result, attackD30message, attackRerollContext, + attackNaturalRoll: msg.attackNaturalRoll, + damageTier: msg.damageTier, defenderId: defender.id, defenderTokenId } @@ -365,6 +411,8 @@ export default class LethalFantasyUtils { attackD30result, attackD30message, attackRerollContext, + attackNaturalRoll: msg.attackNaturalRoll, + damageTier: msg.damageTier, defenderId: defender.id, defenderTokenId, isRanged: msg.isRanged @@ -397,6 +445,7 @@ export default class LethalFantasyUtils { attackD30result, attackD30message, attackRerollContext, + damageTier: msg.damageTier, defenderId: defender.id, defenderTokenId, isRanged: true @@ -467,9 +516,11 @@ export default class LethalFantasyUtils { attackRollType, attackRollKey, attackD30result, - attackD30message, - attackRerollContext, - defenderId: defender.id, + attackD30message, + attackRerollContext, + attackNaturalRoll: msg.attackNaturalRoll, + damageTier: msg.damageTier, + defenderId: defender.id, defenderTokenId, isRanged: msg.isRanged } @@ -485,6 +536,180 @@ export default class LethalFantasyUtils { return d30Message?.type === "mulligan" } + /* -------------------------------------------- */ + /** + * Process D30 bonus dice for attack or defense. + * Rolls and applies bonus dice BEFORE grit/luck/shield decisions. + * For `choice` type results (D30=20, 30), shows dialog to choose between bonus dice and special effect. + * For `bonus_dice` type results (D30=27, 2, 3), auto-rolls the dice. + * @param {Object|null} d30Message The D30 result object + * @param {"attack"|"defense"} side Whether processing the attack or defense side + * @param {number|null} naturalRoll The natural D20 roll (for special strike type detection) + * @param {Object} actor The actor (for dice3d display) + * @returns {Promise<{modifier: number, specialEffect: string|null, specialName: string|null}>} + */ + static async processD30BonusDice(d30Message, side, naturalRoll = null, actor = null) { + if (!d30Message) return { modifier: 0, specialEffect: null, specialName: null } + + const validTargets = side === "attack" ? ["attack", "spell_attack"] : ["defense", "spell_defense"] + + // ── Simple bonus_dice type ── auto-roll if target matches + if (d30Message.type === "bonus_dice") { + if (!validTargets.includes(d30Message.target)) return { modifier: 0, specialEffect: null, specialName: null } + const modifier = await this._rollD30BonusDie(d30Message.dice, actor) + return { modifier, specialEffect: null, specialName: null } + } + + // ── Choice type ── present all options to the player + if (d30Message.type === "choice") { + const buttons = d30Message.choices.map(c => { + let label + let icon + if (c.type === "bonus_dice") { + label = `Roll ${c.dice.toUpperCase()} and add to ${side}` + icon = "fa-solid fa-dice" + } else if (c.type === "special_strike") { + label = this._buildSpecialLabel(c, naturalRoll) + icon = "fa-solid fa-star" + } else if (c.type === "special_defense") { + label = this._buildSpecialLabel(c, naturalRoll) + icon = "fa-solid fa-shield-halved" + } else { + label = c.type.replace(/_/g, " ").replace(/\b\w/g, l => l.toUpperCase()) + icon = "fa-solid fa-question" + } + return { + action: c.type, + label, + icon, + callback: () => c + } + }) + + const choice = await foundry.applications.api.DialogV2.wait({ + window: { title: "D30 Special — Choose Effect" }, + classes: ["lethalfantasy"], + content: ` +
+

D30 result: ${d30Message.description}

+

Choose how to use this result:

+
+ `, + buttons, + rejectClose: false + }) + + if (!choice) return { modifier: 0, specialEffect: null, specialName: null } + + if (choice.type === "bonus_dice") { + const modifier = await this._rollD30BonusDie(choice.dice, actor) + return { modifier, specialEffect: null, specialName: null } + } + + if (choice.type === "special_strike" || choice.type === "special_defense") { + return { modifier: 0, specialEffect: "auto", specialName: this._buildSpecialName(choice, naturalRoll) } + } + + // Non-standard choice (spell_calamity, etc.) — report it + return { modifier: 0, specialEffect: "flag", specialName: choice.type } + } + + // ── Combo type (bleed / internal injury) — flag for wound creation + if (d30Message.type === "combo") { + const hasBleed = d30Message.effects?.some(e => e.type === "bleed" || e.type === "internal_injury") + if (hasBleed) { + return { modifier: 0, specialEffect: "bleed", specialName: "Bleeding/Internal Injury" } + } + } + + // ── Damage multiplier type (2x/3x damage before DR) + if (d30Message.type === "damage_multiplier") { + return { modifier: 0, specialEffect: "damageMultiplier", specialName: `x${d30Message.multiplier} Damage`, multiplier: d30Message.multiplier } + } + + // ── DR multiplier type (2x/3x DR including shield) + if (d30Message.type === "dr_multiplier") { + return { modifier: 0, specialEffect: "drMultiplier", specialName: `x${d30Message.multiplier} DR`, multiplier: d30Message.multiplier } + } + + return { modifier: 0, specialEffect: null, specialName: null } + } + + /* -------------------------------------------- */ + /** + * Roll a D30 bonus die and show with 3D dice if available. + * @param {string} formula Dice formula (e.g. "D6", "D12", "D20E") + * @param {Object} actor Actor for chat message speaker + * @returns {Promise} The roll total + */ + static async _rollD30BonusDie(formula, actor) { + const cleaned = formula.replace(/NE$/i, "").replace("E", "") + const roll = new Roll(cleaned) + await roll.evaluate() + if (game?.dice3d) { + await game.dice3d.showForRoll(roll, game.user, true) + } + await ChatMessage.create({ + content: `

D30 bonus: rolled ${cleaned.toUpperCase()} = ${roll.total}

`, + speaker: ChatMessage.getSpeaker({ actor }) + }) + return roll.total + } + + /* -------------------------------------------- */ + /** + * Build a human-readable label for a special strike/defense choice in the D30 prompt. + * @param {Object} specialChoice The choice object with type and options + * @param {number|null} naturalRoll The natural D20 roll + * @returns {string} Display label + */ + static _buildSpecialLabel(specialChoice, naturalRoll) { + if (specialChoice.type === "special_strike") { + if (specialChoice.options.includes("lethal")) { + if (naturalRoll === 20) return "Lethal Strike (auto-hit)" + if (naturalRoll >= 16 && naturalRoll <= 19) return "Vital Strike (auto-hit)" + return "Lethal/Vital Strike (auto-hit)" + } + if (specialChoice.options.includes("vicious")) return "Vicious Strike (auto-hit)" + return "Special Strike (auto-hit)" + } + if (specialChoice.type === "special_defense") { + if (specialChoice.options.includes("perfect_spell")) return "Perfect Spell Defense (auto-block)" + if (specialChoice.options.includes("flawless")) return "Flawless Defense (auto-block)" + if (specialChoice.options.includes("legendary")) return "Legendary Defense (auto-block)" + if (specialChoice.options.includes("perfect")) return "Perfect Defense (auto-block)" + return "Special Defense (auto-block)" + } + return "Special Effect" + } + + /* -------------------------------------------- */ + /** + * Build the special effect name based on the D30 result and natural roll. + * @param {Object} specialChoice The choice object with type and options + * @param {number|null} naturalRoll The natural D20 roll + * @returns {string} The special effect name + */ + static _buildSpecialName(specialChoice, naturalRoll) { + if (specialChoice.type === "special_strike") { + if (specialChoice.options.includes("lethal")) { + if (naturalRoll === 20) return "Lethal Strike" + if (naturalRoll >= 16 && naturalRoll <= 19) return "Vital Strike" + return "Lethal/Vital Strike" + } + if (specialChoice.options.includes("vicious")) return "Vicious Strike" + return "Special Strike" + } + if (specialChoice.type === "special_defense") { + if (specialChoice.options.includes("perfect_spell")) return "Perfect Spell Defense" + if (specialChoice.options.includes("flawless")) return "Flawless Defense" + if (specialChoice.options.includes("legendary")) return "Legendary Defense" + if (specialChoice.options.includes("perfect")) return "Perfect Defense" + return "Special Defense" + } + return "Special Effect" + } + /* -------------------------------------------- */ static getCombatBonusDiceChoices() { return ["1d4", "1d6", "1d8", "1d10", "1d12", "1d20", "1d20e"] @@ -843,19 +1068,16 @@ export default class LethalFantasyUtils { // Déterminer le type de dégâts à lancer if (data.attackRollType === "weapon-attack") { damageButton = ` -
- -
` } else if (data.attackRollType === "monster-attack") { damageButton = `
-
@@ -863,11 +1085,13 @@ export default class LethalFantasyUtils { } else if (data.attackRollType === "spell-attack" || data.attackRollType === "miracle-attack") { const attacker = game.actors.get(data.attackerId) const spell = attacker?.items.get(data.attackWeaponId || data.attackRollKey) - const tiers = [ - { formula: spell?.system?.damageDice, label: "Standard" }, - { formula: spell?.system?.damageDiceOverpowered, label: "Overpowered" }, - { formula: spell?.system?.damageDiceOverpowered2, label: "Overpowered 2" }, - ].filter(t => t.formula) + const chosenTier = data.damageTier || "standard" + const allTiers = [ + { id: "standard", formula: spell?.system?.damageDice, label: "Standard" }, + { id: "overpowered", formula: spell?.system?.damageDiceOverpowered, label: "Overpowered" }, + { id: "overpowered2", formula: spell?.system?.damageDiceOverpowered2, label: "Overpowered 2" }, + ] + const tiers = allTiers.filter(t => t.id === chosenTier && t.formula) if (tiers.length) { const buttons = tiers.map(t => { const escapedFormula = Handlebars.escapeExpression(t.formula) @@ -877,7 +1101,10 @@ export default class LethalFantasyUtils { data-defender-id="${data.defenderId}" data-defender-token-id="${data.defenderTokenId || ""}" data-damage-type="spell" - data-damage-formula="${escapedFormula}"> + data-damage-formula="${escapedFormula}" + data-d30-bleed="${data.d30Bleed || ""}" + data-d30-damage-mult="${data.d30DamageMultiplier || 1}" + data-d30-dr-mult="${data.d30DrMultiplier || 1}"> ${t.label} (${escapedFormula}) ` }).join("") diff --git a/styles/hud.less b/styles/hud.less index 68be8fd..0fbf441 100644 --- a/styles/hud.less +++ b/styles/hud.less @@ -89,6 +89,66 @@ font-size: 0.9rem; } +/* Luck/Grit Styles */ +#token-hud .luck-grit-wrap { + position: absolute; + left: 75px; + display: none; + top: 50%; + width: 80px; + text-align: start; + background: rgba(0, 0, 0, 0.8); + border: 1px solid rgba(139, 69, 19, 0.5); + border-radius: 4px; + padding: 4px 6px; + transform: translate(-100%, -50%); +} + +#token-hud .luck-grit-hud-active { + display: block; +} + +#token-hud .luck-grit-hud-disabled { + display: none; +} + +#token-hud .luck-grit-row { + display: flex; + align-items: center; + gap: 4px; + margin: 2px 0; +} + +#token-hud .luck-grit-label { + flex: 1; + color: #c9b896; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; +} + +#token-hud .luck-grit-btn { + width: 28px; + height: 22px; + padding: 0; + background: rgba(139, 69, 19, 0.25); + border: 1px solid rgba(139, 69, 19, 0.4); + border-radius: 3px; + color: #d4c5a9; + font-size: 12px; + font-weight: 700; + cursor: pointer; + line-height: 1; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + background: rgba(139, 69, 19, 0.5); + border-color: rgba(139, 69, 19, 0.7); + } +} + /* -------------------------------------------------- */ /* Dice Tray — injected into the Foundry chat sidebar */ /* -------------------------------------------------- */ diff --git a/templates/character-combat.hbs b/templates/character-combat.hbs index a70418a..7f6e1c0 100644 --- a/templates/character-combat.hbs +++ b/templates/character-combat.hbs @@ -87,16 +87,10 @@ - - S - - - - M + + diff --git a/templates/chat-message.hbs b/templates/chat-message.hbs index 58137c6..961e994 100644 --- a/templates/chat-message.hbs +++ b/templates/chat-message.hbs @@ -69,16 +69,10 @@ {{/if}} - {{#if rollData.damageSmall}} -
- - {{localize "LETHALFANTASY.Label.weapon-damage-small"}} -
- {{/if}} - {{#if rollData.damageMedium}} + {{#if rollData.isDamage}}
- {{localize "LETHALFANTASY.Label.weapon-damage-medium"}} + {{localize "LETHALFANTASY.Label.weapon-damage"}}
{{/if}} @@ -202,19 +196,6 @@ }}+{{weaponDamageOptions.damageModifier}}{{/if}} {{else}} - {{#if weaponDamageOptions.damageS}} - - {{/if}} {{#if weaponDamageOptions.damageM}} {{/if}} {{/if}} diff --git a/templates/luck-grit-hud.hbs b/templates/luck-grit-hud.hbs new file mode 100644 index 0000000..c457555 --- /dev/null +++ b/templates/luck-grit-hud.hbs @@ -0,0 +1,16 @@ +
+ + +
+
+ Luck + + +
+
+ Grit + + +
+
+
diff --git a/templates/roll-dialog.hbs b/templates/roll-dialog.hbs index 10dd2f8..c6f4c7b 100644 --- a/templates/roll-dialog.hbs +++ b/templates/roll-dialog.hbs @@ -123,17 +123,15 @@ {{#if (eq rollType "save")}} - {{#if rollTarget.magicUser}} -
- Save against spell (+{{rollTarget.actorModifiers.saveModifier}}) - ? - -
- {{/if}} +
+ Save against spell (+{{rollTarget.actorModifiers.saveModifier}}) + ? + +
{{/if}} {{/if}} diff --git a/templates/spell.hbs b/templates/spell.hbs index 300e5ff..f9c2685 100644 --- a/templates/spell.hbs +++ b/templates/spell.hbs @@ -6,6 +6,8 @@ {{formField systemFields.level value=system.level}} {{formField systemFields.cost value=system.cost}} + {{formField systemFields.costOverpowered value=system.costOverpowered}} + {{formField systemFields.costOverpowered2 value=system.costOverpowered2}}
diff --git a/templates/weapon.hbs b/templates/weapon.hbs index 56ed3e1..57e48dd 100644 --- a/templates/weapon.hbs +++ b/templates/weapon.hbs @@ -17,11 +17,7 @@ {{formField systemFields.damageType.fields.typeS value=system.damageType.typeS}}
- -
- {{formField systemFields.damage.fields.damageS value=system.damage.damageS}} - {{formField systemFields.damage.fields.damageM value=system.damage.damageM}} -
+ {{formField systemFields.damage.fields.damageM value=system.damage.damageM label="LETHALFANTASY.Label.damage"}} {{formField systemFields.applyStrengthDamageBonus value=system.applyStrengthDamageBonus localize=true}}