Compare commits

..

14 Commits

Author SHA1 Message Date
uberwald 53f9c33419 chore: gitignore LevelDB internal files, stop tracking auto-generated LDB bookkeeping
Release Creation / build (release) Successful in 46s
2026-06-11 23:00:45 +02:00
uberwald 06eba5f835 fix: show spell tier dialog on character sheet cast; duplicate rollTarget to prevent Item mutation 2026-06-11 22:56:54 +02:00
uberwald 46fa2d15a3 fix: allow defender to react when attacker boosts past defense via cross-client socket 2026-06-11 22:41:54 +02:00
uberwald 8aae7bada0 fix: pre-check 'Save against spell' checkbox when defense originates from spell attack 2026-06-11 22:17:37 +02:00
uberwald ceb62bca3f fix: add missing class lethal-luck-grit-hud to template so JS selector matches 2026-06-11 22:03:56 +02:00
uberwald 110ac65ba5 fix: replace hardcoded French bleeding notifications with i18n keys 2026-06-11 21:50:19 +02:00
uberwald 9b75fd4d96 feat: combat-tracker-driven bleeding (HP loss per wound per round) 2026-06-11 21:49:35 +02:00
uberwald 141d6048e0 Fix triple damage issue 2026-06-11 21:32:26 +02:00
uberwald ea7acf6bf8 Fix hp < 0 and D30 with D20 bonus roll 2026-06-11 20:48:46 +02:00
uberwald c20750caa7 Minor fixes regarding rolls and chat messages
Release Creation / build (release) Successful in 48s
2026-06-10 20:17:45 +02:00
uberwald ce630feb51 feat: D30 combat effects, spell tiers, small damage removal, token HUD luck/grit
- Replace Knockback with Internal Injury on D30 (5, 10, 15); remove Shield Bash from D30 counter-attacks
- Eliminate small weapon damage: keep only medium damage labelled Damage in sheets, rolls, and chat
- D30 bonus dice (20, 27, 30) auto-resolved before grit/luck/shield decisions; choice dialogs for special strikes
- D30 combat effects: bleeding wounds, damage ×2/×3 before DR, DR ×2/×3 with component picker dialog
- Add hp.wounds to monster schema for bleeding support
- Show Save against spell? checkbox for all save rolls (not just magic users)
- Fix mulligan restart: persistent D30 process flags prevent double-application and allow both sides to react
- For Dice So Nice, show main roll animation before explosion dice for correct ordering
- Spell tier selection: force Standard/Overpowered choice at cast time, tier-specific aether cost, only chosen damage button shown
- Add +1/−1 luck and grit controls to Token HUD
- Fix inconsistent indentation, remove duplicate i18n key, remove unused includesShield return
2026-06-10 07:53:51 +02:00
uberwald b35b684d50 NEgative values for HP and weapon bonuses 2026-06-06 16:11:36 +02:00
uberwald f6fb0b68b8 Fix spell rolls again
Release Creation / build (release) Successful in 47s
2026-05-25 20:41:00 +02:00
uberwald e45edd60c4 FIx spell order and dual rollll for spell damages
Release Creation / build (release) Successful in 1m6s
2026-05-25 12:29:39 +02:00
55 changed files with 1525 additions and 556 deletions
+1
View File
@@ -0,0 +1 @@
packs/** filter=lfs diff=lfs merge=lfs -text
+8
View File
@@ -9,3 +9,11 @@ node_modules/
.history
.github/
# LevelDB internals (auto-generated, churn on every open)
packs-system/**/*.log
packs-system/**/LOG
packs-system/**/LOG.old
packs-system/**/CURRENT
packs-system/**/LOCK
packs-system/**/MANIFEST-*
+6
View File
@@ -0,0 +1,6 @@
{
"cSpell.words": [
"biodata",
"LETHALFANTASY"
]
}
+59 -11
View File
@@ -529,16 +529,16 @@ i.lethalfantasy {
}
.lethalfantasy .tab.character-combat .main-div .combat-details .combat-detail .armor-hp {
margin-right: 4px;
min-width: 10rem;
max-width: 10rem;
min-width: 11rem;
max-width: 11rem;
}
.lethalfantasy .tab.character-combat .main-div .combat-details .combat-detail .armor-hp .name {
min-width: 6rem;
max-width: 6rem;
}
.lethalfantasy .tab.character-combat .main-div .combat-details .combat-detail .armor-hp .input {
min-width: 2.5rem;
max-width: 2.5rem;
min-width: 3.5rem;
max-width: 3.5rem;
}
.lethalfantasy .tab.character-combat .main-div .combat-details .combat-detail .granted {
min-width: 8rem;
@@ -3280,16 +3280,10 @@ i.lethalfantasy {
.message.lethalfantasy.dice-roll .damage-buttons .damage-buttons-grid,
.message.fvtt-lethal-fantasy.dice-roll .damage-buttons .damage-buttons-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.lethalfantasy.dice-roll .damage-buttons .damage-buttons-grid.monster-damage,
.fvtt-lethal-fantasy.dice-roll .damage-buttons .damage-buttons-grid.monster-damage,
.message.lethalfantasy.dice-roll .damage-buttons .damage-buttons-grid.monster-damage,
.message.fvtt-lethal-fantasy.dice-roll .damage-buttons .damage-buttons-grid.monster-damage {
grid-template-columns: 1fr;
max-width: 280px;
margin: 0 auto;
gap: 8px;
}
.lethalfantasy.dice-roll .damage-buttons .damage-buttons-grid .damage-roll-btn,
.fvtt-lethal-fantasy.dice-roll .damage-buttons .damage-buttons-grid .damage-roll-btn,
@@ -3549,6 +3543,7 @@ i.lethalfantasy {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
justify-items: center;
}
.lethalfantasy.dice-roll .d30-message,
.fvtt-lethal-fantasy.dice-roll .d30-message,
@@ -4139,6 +4134,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 */
/* -------------------------------------------------- */
+9 -3
View File
@@ -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",
@@ -626,7 +624,9 @@
"messageLethargyKO": "{spellName} : Lethargy still ongoing ... ( dice result : {roll} )",
"messageProgressionKO": "{name} can't attack this second.",
"messageProgressionOKMonster": "{name} can attack this second with {weapon}.",
"messageProgressionKOMonster": "{name} can't attack this second (dice result {roll})."
"messageProgressionKOMonster": "{name} can't attack this second (dice result {roll}).",
"bleedingCombatEnd": "Bleeding active out of combat: {names}",
"bleedingCombatStart": "Bleeding still active on: {names}"
},
"Opponent": {
"FIELDS": {}
@@ -828,6 +828,12 @@
"cost": {
"label": "Cost"
},
"costOverpowered": {
"label": "Cost (Overpowered)"
},
"costOverpowered2": {
"label": "Cost (Overpowered 2)"
},
"description": {
"label": "Description"
},
+635 -290
View File
File diff suppressed because it is too large Load Diff
+3 -14
View File
@@ -268,12 +268,12 @@ export default class LethalFantasyCharacterSheet extends LethalFantasyActorSheet
{
action: "cancel",
label: game.i18n.localize("LETHALFANTASY.Combat.proceedNo"),
callback: () => null
callback: () => "cancel"
}
],
rejectClose: false
})
if (manualDR === null) return
if (manualDR === null || manualDR === "cancel") return
const rollOpts = {
type: "spell-damage",
@@ -286,18 +286,7 @@ export default class LethalFantasyCharacterSheet extends LethalFantasyActorSheet
actorName: this.actor.name,
actorImage: this.actor.img
}
const roll = new LethalFantasyRoll(formula, {}, rollOpts)
await roll.evaluate()
const diceResults = []
for (const term of roll.dice) {
for (const r of term.results) {
diceResults.push({ dice: `1D${term.faces}`, value: r.result })
}
}
roll.options.diceResults = diceResults
roll.options.rollTotal = roll.total
if (game?.dice3d) await game.dice3d.showForRoll(roll, game.user, true)
await roll.toMessage()
await LethalFantasyRoll.rollSpellDamageToMessage(formula, rollOpts)
}
static #onCreateEquipment(event, target) {
+68 -25
View File
@@ -108,6 +108,10 @@
}
],
"description": "Possible Flawless or Legendary Defense or Add D20E to Defense"
},
"saving_throws": {
"type": "save_auto_success",
"description": "Saving Throw Succeeds Regardless of Opposing Roll"
}
},
"29": {
@@ -145,6 +149,11 @@
"type": "gain_grit",
"amount": 1,
"description": "Gain 1 Grit"
},
"saving_throws": {
"type": "gain_grit",
"amount": 1,
"description": "Gain 1 Grit"
}
},
"28": {
@@ -199,6 +208,12 @@
"amount": 1,
"target": "skill",
"description": "Add 1 to Skill Roll"
},
"saving_throws": {
"type": "bonus_flat",
"amount": 1,
"target": "save",
"description": "Add 1 to Saving Throw"
}
},
"21": {
@@ -239,6 +254,12 @@
"ranged_defense": {
"type": "recover_pain",
"description": "Defender Recovers or ignores any flash of pain"
},
"saving_throws": {
"type": "bonus_dice",
"dice": "D6",
"target": "save",
"description": "Granted D6 (1-6) Saving Throw Modifier for this Saving Throw Attempt"
}
},
"20": {
@@ -325,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",
@@ -349,6 +370,12 @@
}
],
"description": "Possible 20/20 defense that avoids Any Attack Except a Lethal Strike or adds D12 to defense"
},
"saving_throws": {
"type": "bonus_flat",
"amount": 20,
"target": "save",
"description": "20 Added to Saving Throw"
}
},
"15": {
@@ -359,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",
@@ -372,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",
@@ -387,10 +413,15 @@
"type": "counter_attack",
"options": [
"kick",
"punch",
"shield_bash"
"punch"
],
"description": "Kick, Punch or Shield Bash"
"description": "Kick or Punch"
},
"saving_throws": {
"type": "bonus_flat",
"amount": 1,
"target": "save",
"description": "Add 1 to Saving Throw"
}
},
"13": {},
@@ -413,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",
@@ -426,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",
@@ -441,10 +471,15 @@
"type": "counter_attack",
"options": [
"kick",
"punch",
"shield_bash"
"punch"
],
"description": "Kick, Punch or Shield Bash"
"description": "Kick or Punch"
},
"saving_throws": {
"type": "bonus_flat",
"amount": 1,
"target": "save",
"description": "Add 1 to Saving Throw"
}
},
"8": {
@@ -475,6 +510,10 @@
"ranged_defense": {
"type": "mulligan",
"description": "Mulligan, Can Choose to Re-Roll This Defense"
},
"saving_throws": {
"type": "mulligan",
"description": "Mulligan, Can Re-Roll This Saving Throw"
}
},
"7": {
@@ -496,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",
@@ -509,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",
@@ -524,10 +562,15 @@
"type": "counter_attack",
"options": [
"kick",
"punch",
"shield_bash"
"punch"
],
"description": "Kick, Punch, or Shield Bash"
"description": "Kick or Punch"
},
"saving_throws": {
"type": "bonus_flat",
"amount": 1,
"target": "save",
"description": "Add 1 to Saving Throw"
}
},
"3": {
+52 -16
View File
@@ -72,23 +72,32 @@ export default class LethalFantasyActor extends Actor {
/* *************************************************/
async applyDamage(hpLoss) {
let hp = this.system.hp.value + hpLoss
if (hp < 0) {
hp = 0
}
this.update({ "system.hp.value": hp })
}
/* *************************************************/
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 +162,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) {
@@ -196,10 +205,38 @@ export default class LethalFantasyActor extends Actor {
case "spell-power":
case "miracle-attack":
case "miracle-power":
rollTarget = this.items.find((i) => (i.type === "miracle" || i.type == "spell") && i.id === rollKey)
rollTarget = foundry.utils.duplicate(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")
let damageTier = currentAction?.damageTier
// No tier from combat action — prompt the user if multiple tiers exist
if (!damageTier) {
const tierMap = { standard: "damageDice", overpowered: "damageDiceOverpowered", overpowered2: "damageDiceOverpowered2" }
const available = Object.entries(tierMap).filter(([k, v]) => rollTarget.system?.[v])
if (available.length > 1) {
const buttons = available.map(([id]) => ({
action: id,
label: id.charAt(0).toUpperCase() + id.slice(1),
callback: () => id,
}))
damageTier = await foundry.applications.api.DialogV2.wait({
window: { title: "Choose spell tier" },
classes: ["lethalfantasy"],
content: `<p>Select the power level for <strong>${rollTarget.name}</strong>:</p>`,
buttons,
rejectClose: false,
}) || "standard"
} else {
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 +259,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 +300,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 +323,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)
}
}
+8 -3
View File
@@ -25,7 +25,8 @@ export default class D30Roll {
RANGED_DEFENSE: "ranged_defense",
ARCANE_SPELL_ATTACK: "arcane_spell_attack",
ARCANE_SPELL_DEFENSE: "arcane_spell_defense",
SKILL_ROLLS: "skill_rolls"
SKILL_ROLLS: "skill_rolls",
SAVING_THROWS: "saving_throws"
}
/**
@@ -137,11 +138,15 @@ export default class D30Roll {
}
// Skill types
if (externalType === "skill" || externalType === "monster-skill" ||
externalType === "save" || externalType === "challenge") {
if (externalType === "skill" || externalType === "monster-skill" || externalType === "challenge") {
return this.ROLL_TYPES.SKILL_ROLLS
}
// Saving throw types
if (externalType === "save") {
return options.isSpellSave ? this.ROLL_TYPES.ARCANE_SPELL_DEFENSE : this.ROLL_TYPES.SAVING_THROWS
}
// If no match, return null
console.warn(`D30Roll | Unknown external roll type: ${externalType}`)
return null
+82 -21
View File
@@ -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
@@ -364,7 +358,7 @@ export default class LethalFantasyRoll extends Roll {
dice,
hasTarget: options.hasTarget,
modifier,
saveSpell: false,
saveSpell: game.lethalFantasy?.spellDefense ?? false,
favor: "none",
targetName,
isRangedAttack
@@ -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,
@@ -609,7 +601,7 @@ export default class LethalFantasyRoll extends Roll {
rollD30.total,
options.rollType,
options.rollTarget?.weapon,
{ isRanged: isRangedForD30 }
{ isRanged: isRangedForD30, isSpellSave: saveSpell }
)
options.D30message = d30Message
}
@@ -628,9 +620,6 @@ export default class LethalFantasyRoll extends Roll {
if (hasMaxValue) {
while (diceResult === maxValue) {
let r = await new Roll(baseFormula).evaluate()
if (game?.dice3d) {
await game.dice3d.showForRoll(r, game.user, true)
}
diceResult = r.dice[0].results[0].result
diceResults.push({ dice: `${singleDice.toUpperCase()}-1`, value: diceResult - 1 })
diceSum += (diceResult - 1)
@@ -673,6 +662,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.
@@ -787,9 +780,18 @@ export default class LethalFantasyRoll extends Roll {
let buttons = []
if (currentAction) {
if (currentAction.type === "weapon") {
let weaponLabel = "Roll progression dice"
if (currentAction.rangedMode) {
// Compute loading count from the speed formula (e.g. "3+1d6" → load=3)
const speedStr = currentAction.system?.speed?.[currentAction.rangedMode] ?? ""
const rangedLoad = currentAction.rangedLoad ?? (Number(speedStr.split("+")[0]) || 0)
if (rangedLoad > 0 && !currentAction.weaponLoaded) {
weaponLabel = "Load weapon"
}
}
buttons.push({
action: "roll",
label: "Roll progression dice",
label: weaponLabel,
callback: (event, button) => {
let pos = $('#combat-action-dialog').position()
game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos)
@@ -894,6 +896,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: `<div class="grit-luck-dialog"><p><strong>${selectedItem.name}</strong> has multiple damage tiers.</p><p>Choose which damage to use when the attack lands:</p></div>`,
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
@@ -915,7 +940,8 @@ export default class LethalFantasyRoll extends Roll {
await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction))
return
} else {
let message = `Spell/Miracle ${currentAction.name} ready to be cast on next second !`
// Last counting second — announce ready and transition immediately (no extra second consumed)
let message = `Casting time : ${currentAction.name}, count : ${time}/${time} — ready to cast next second !`
ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
currentAction.castingTime = 1
currentAction.spellStatus = "toBeCasted"
@@ -1382,10 +1408,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":
@@ -1446,7 +1470,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)
@@ -1529,4 +1552,42 @@ export default class LethalFantasyRoll extends Roll {
)
}
/**
* Evaluate a spell/miracle damage formula with per-die explosion, then post to chat.
* Explosion dice are shown manually via showForRoll; the main roll is shown automatically
* by toMessage() (which triggers Dice So Nice via its createChatMessage hook).
* Append "NE" to the formula to disable explosion.
*
* @param {string} formula Dice formula, e.g. "1d8", "2d6", "1d8NE"
* @param {Object} rollOpts Options for LethalFantasyRoll (rollType, actorId, defenderId, etc.)
* @returns {Promise<ChatMessage>}
*/
static async rollSpellDamageToMessage(formula, rollOpts) {
const roll = new LethalFantasyRoll(formula, {}, rollOpts)
await roll.evaluate()
const shouldExplode = !/NE$/i.test(formula)
const diceResults = []
let diceSum = 0
for (const term of roll.dice) {
const singleDice = `1D${term.faces}`
for (const r of term.results) {
let diceResult = r.result
diceResults.push({ dice: singleDice.toUpperCase(), value: diceResult })
diceSum += diceResult
if (shouldExplode && term.faces > 0) {
while (diceResult === term.faces) {
const xr = await new Roll(singleDice).evaluate()
// Optional chaining guards against unexpected roll structure
diceResult = xr.dice?.[0]?.results?.[0]?.result ?? (term.faces - 1)
diceResults.push({ dice: `${singleDice.toUpperCase()}-1`, value: diceResult - 1 })
diceSum += (diceResult - 1)
}
}
}
}
roll.options.diceResults = diceResults
roll.options.rollTotal = diceSum
return roll.toMessage()
}
}
+7 -3
View File
@@ -65,7 +65,7 @@ export default class LethalFantasyCharacter extends foundry.abstract.TypeDataMod
}
schema.hp = new fields.SchemaField({
value: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }),
value: new fields.NumberField({ ...requiredInteger, initial: 1 }),
max: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }),
painDamage: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
wounds: new fields.ArrayField(new fields.SchemaField(woundFieldSchema), {
@@ -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<null>} - 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
+28 -7
View File
@@ -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 }),
value: new fields.NumberField({ ...requiredInteger, initial: 1 }),
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<null>} - 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)
}
}
+2
View File
@@ -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({
+3 -3
View File
@@ -45,9 +45,9 @@ export default class LethalFantasySkill extends foundry.abstract.TypeDataModel {
})
schema.bonuses = new fields.SchemaField({
attackBonus: new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 }),
damageBonus: new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 }),
defenseBonus: new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 })
attackBonus: new fields.NumberField({ ...requiredInteger, required: true, initial: 0 }),
damageBonus: new fields.NumberField({ ...requiredInteger, required: true, initial: 0 }),
defenseBonus: new fields.NumberField({ ...requiredInteger, required: true, initial: 0 })
})
schema.encLoad = new fields.NumberField({ required: true, initial: 0, min: 0 })
+451 -16
View File
@@ -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,26 @@ 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
case "attackBoosted":
if (msg.userId === game.user.id) {
LethalFantasyUtils.handleAttackBoosted(msg)
}
break
}
}
@@ -182,6 +231,180 @@ export default class LethalFantasyUtils {
})
}
/* -------------------------------------------- */
static async handleAttackBoosted(msg) {
const {
attackerName, attackerId, defenderName, defenderId, defenderTokenId,
attackRollFinal, defenseRoll, attackWeaponId, attackRollType, attackRollKey,
shieldDamageReduction: initialShieldDR,
d30Bleed, d30DamageMultiplier, d30DrMultiplier,
damageTier, attackD30message,
hasShield, shieldLabel, shieldFormula, shieldDr, canAdHocShield
} = msg
const defender = game.actors.get(defenderId)
if (!defender) return
let updatedDefenseRoll = defenseRoll
let shieldBlocked = false
let shieldReaction = null
let canShieldReact = hasShield
let canAdHoc = canAdHocShield
// Show the defense reaction dialog
if (defender && updatedDefenseRoll < attackRollFinal) {
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 (canShieldReact) {
buttons.push({
action: "shieldReact",
label: `Roll shield (${shieldLabel})`,
icon: "fa-solid fa-shield",
callback: () => "shieldReact"
})
} else if (canAdHoc) {
buttons.push({
action: "adHocShield",
label: "Roll ad-hoc shield (choose dice + DR)",
icon: "fa-solid fa-shield-halved",
callback: () => "adHocShield"
})
}
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 — attack boosted" },
classes: ["lethalfantasy"],
content: `
<div class="grit-luck-dialog">
<div class="combat-status">
<p><strong>${attackerName}</strong> boosted attack to <strong>${attackRollFinal}</strong></p>
<p><strong>${defenderName}</strong> currently has <strong>${updatedDefenseRoll}</strong></p>
</div>
<p class="offer-text">The attack was boosted! Choose how to improve the defense.</p>
</div>
`,
buttons,
rejectClose: false
})
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>`)
updatedDefenseRoll += bonusRoll
await defender.update({ "system.grit.current": currentGrit - 1 })
} else 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>`)
updatedDefenseRoll += bonusRoll
await defender.update({ "system.luck.current": currentLuck - 1 })
} else if (choice === "bonusDie") {
const bonusDie = await LethalFantasyUtils.promptCombatBonusDie(defenderName, "attack", updatedDefenseRoll, attackRollFinal)
if (bonusDie) {
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>`)
updatedDefenseRoll += bonusRoll
}
} else if (choice === "shieldReact" && canShieldReact) {
const shieldBonus = await LethalFantasyUtils.rollBonusDie(shieldFormula, defender)
const newDefenseTotal = updatedDefenseRoll + shieldBonus
updatedDefenseRoll = newDefenseTotal
canShieldReact = false
if (newDefenseTotal >= attackRollFinal) {
shieldBlocked = true
shieldReaction = { damageReduction: shieldDr, label: shieldLabel, bonus: shieldBonus }
await ChatMessage.create({
content: `<p><strong>${defenderName}</strong> rolls <strong>${shieldLabel}</strong> and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal}${attackRollFinal}). <strong>Shield blocked the attack!</strong> Both armor DR and shield DR <strong>${shieldDr}</strong> will apply to damage.</p>`,
speaker: ChatMessage.getSpeaker({ actor: defender })
})
} else {
await ChatMessage.create({
content: `<p><strong>${defenderName}</strong> rolls <strong>${shieldLabel}</strong> and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} < ${attackRollFinal}). Shield did not block — normal hit, armor DR only.</p>`,
speaker: ChatMessage.getSpeaker({ actor: defender })
})
}
} else if (choice === "adHocShield" && canAdHoc) {
const adHoc = await LethalFantasyUtils.promptAdHocShield(defenderName, attackRollFinal, updatedDefenseRoll)
if (adHoc) {
const shieldBonus = await LethalFantasyUtils.rollBonusDie(adHoc.formula, defender)
const newDefenseTotal = updatedDefenseRoll + shieldBonus
updatedDefenseRoll = newDefenseTotal
canShieldReact = false
canAdHoc = false
if (newDefenseTotal >= attackRollFinal) {
shieldBlocked = true
shieldReaction = { damageReduction: adHoc.damageReduction, label: `${adHoc.formula.toUpperCase()} shield`, bonus: shieldBonus }
await ChatMessage.create({
content: `<p><strong>${defenderName}</strong> rolls <strong>${adHoc.formula.toUpperCase()}</strong> shield and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal}${attackRollFinal}). <strong>Shield blocked the attack!</strong> Both armor DR and shield DR <strong>${adHoc.damageReduction}</strong> will apply to damage.</p>`,
speaker: ChatMessage.getSpeaker({ actor: defender })
})
} else {
await ChatMessage.create({
content: `<p><strong>${defenderName}</strong> rolls <strong>${adHoc.formula.toUpperCase()}</strong> shield and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} < ${attackRollFinal}). Shield did not block — normal hit, armor DR only.</p>`,
speaker: ChatMessage.getSpeaker({ actor: defender })
})
}
}
}
}
const finalShieldDR = shieldBlocked ? (shieldReaction?.damageReduction || 0) : 0
const outcome = shieldBlocked ? "shielded-hit" : (updatedDefenseRoll > attackRollFinal ? "miss" : "hit")
await LethalFantasyUtils.compareAttackDefense({
attackerName,
attackerId,
attackRoll: attackRollFinal,
attackWeaponId,
attackRollType,
attackRollKey,
defenderName,
defenderId,
defenderTokenId,
defenseRoll: updatedDefenseRoll,
outcome,
shieldDamageReduction: finalShieldDR,
d30Bleed: d30Bleed || "",
d30DamageMultiplier: d30DamageMultiplier || 1,
d30DrMultiplier: d30DrMultiplier || 1,
damageTier: damageTier || "standard",
attackD30message
})
}
/* -------------------------------------------- */
static async showDefenseRequest(msg) {
const attackerName = msg.attackerName
@@ -279,6 +502,7 @@ export default class LethalFantasyUtils {
if (result) {
game.lethalFantasy = game.lethalFantasy || {}
game.lethalFantasy.spellDefense = true // pré-cocher "Save against spell" dans le dialog
game.lethalFantasy.nextDefenseData = {
attackerId,
attackRoll,
@@ -290,6 +514,8 @@ export default class LethalFantasyUtils {
attackD30result,
attackD30message,
attackRerollContext,
attackNaturalRoll: msg.attackNaturalRoll,
damageTier: msg.damageTier,
defenderId: defender.id,
defenderTokenId
}
@@ -365,6 +591,8 @@ export default class LethalFantasyUtils {
attackD30result,
attackD30message,
attackRerollContext,
attackNaturalRoll: msg.attackNaturalRoll,
damageTier: msg.damageTier,
defenderId: defender.id,
defenderTokenId,
isRanged: msg.isRanged
@@ -397,6 +625,7 @@ export default class LethalFantasyUtils {
attackD30result,
attackD30message,
attackRerollContext,
damageTier: msg.damageTier,
defenderId: defender.id,
defenderTokenId,
isRanged: true
@@ -467,9 +696,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 +716,196 @@ 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, canDialog = true) {
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, !canDialog)
return { modifier, specialEffect: null, specialName: null }
}
// ── Choice type ── present all options to the player
if (d30Message.type === "choice") {
// Try to find a bonus_dice option matching this side
const autoBonus = d30Message.choices.find(c => c.type === "bonus_dice" && validTargets.includes(c.target))
// If we can't show dialogs (wrong client), auto-roll bonus dice if available
if (!canDialog) {
if (autoBonus) {
const modifier = await this._rollD30BonusDie(autoBonus.dice, actor, true)
return { modifier, specialEffect: null, specialName: null }
}
// No bonus dice available on this side — just report as flag
const first = d30Message.choices[0]
return { modifier: 0, specialEffect: "flag", specialName: first?.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: `
<div class="grit-luck-dialog">
<p><strong>D30 result:</strong> ${d30Message.description}</p>
<p>Choose how to use this result:</p>
</div>
`,
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<number>} The roll total
*/
static async _rollD30BonusDie(formula, actor, silent = false) {
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)
}
if (!silent) {
await ChatMessage.create({
content: `<p>D30 bonus: rolled <strong>${cleaned.toUpperCase()}</strong> = <strong>${roll.total}</strong></p>`,
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"]
@@ -833,6 +1254,18 @@ export default class LethalFantasyUtils {
/* -------------------------------------------- */
static async compareAttackDefense(data) {
console.log("compareAttackDefense called with:", data)
// Compute D30 effects from the attack D30 message directly.
// This is more reliable than depending on the caller-provided values, which are
// computed per-client and may differ between clients due to cross-client processing order.
const d30DamageMultiplier = data.attackD30message?.type === "damage_multiplier"
? data.attackD30message.multiplier
: (data.d30DamageMultiplier || 1)
const d30Bleed = data.attackD30message?.type === "combo"
? (data.attackD30message.effects?.some(e => e.type === "bleed" || e.type === "internal_injury") ? "true" : "")
: data.attackD30message?.type === "bleed" ? "true" : (data.d30Bleed || "")
const d30DrMultiplier = data.d30DrMultiplier || 1
const outcome = data.outcome || (data.attackRoll > data.defenseRoll ? "hit" : "miss")
const isAttackWin = outcome !== "miss"
console.log("isAttackWin:", isAttackWin, "attackRoll:", data.attackRoll, "defenseRoll:", data.defenseRoll)
@@ -843,19 +1276,16 @@ export default class LethalFantasyUtils {
// 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)
<div class="attack-result-damage single-btn">
<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" data-d30-bleed="${d30Bleed}" data-d30-damage-mult="${d30DamageMultiplier}" data-d30-dr-mult="${d30DrMultiplier}">
<i class="fa-solid fa-dice-d20"></i> Damage
</button>
</div>
`
} else if (data.attackRollType === "monster-attack") {
damageButton = `
<div class="attack-result-damage single-btn">
<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">
<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" data-d30-bleed="${d30Bleed}" data-d30-damage-mult="${d30DamageMultiplier}" data-d30-dr-mult="${d30DrMultiplier}">
<i class="fa-solid fa-burst"></i> Damage
</button>
</div>
@@ -863,11 +1293,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 +1309,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="${d30Bleed}"
data-d30-damage-mult="${d30DamageMultiplier}"
data-d30-dr-mult="${d30DrMultiplier}">
<i class="fa-solid fa-wand-magic-sparkles"></i> ${t.label} (${escapedFormula})
</button>`
}).join("")
-1
View File
@@ -1 +0,0 @@
MANIFEST-000619
View File
-8
View File
@@ -1,8 +0,0 @@
2026/05/24-09:22:43.041326 7fdfa95ff6c0 Recovering log #617
2026/05/24-09:22:43.083988 7fdfa95ff6c0 Delete type=3 #615
2026/05/24-09:22:43.084045 7fdfa95ff6c0 Delete type=0 #617
2026/05/24-09:42:00.304351 7fdf5affd6c0 Level-0 table #622: started
2026/05/24-09:42:00.304423 7fdf5affd6c0 Level-0 table #622: 0 bytes OK
2026/05/24-09:42:00.311333 7fdf5affd6c0 Delete type=0 #620
2026/05/24-09:42:00.334170 7fdf5affd6c0 Manual compaction at level-0 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
2026/05/24-09:42:00.334370 7fdf5affd6c0 Manual compaction at level-1 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
-8
View File
@@ -1,8 +0,0 @@
2026/05/23-18:45:10.214922 7fa233fff6c0 Recovering log #613
2026/05/23-18:45:10.223944 7fa233fff6c0 Delete type=3 #611
2026/05/23-18:45:10.223997 7fa233fff6c0 Delete type=0 #613
2026/05/23-19:09:56.190877 7fa2327fc6c0 Level-0 table #618: started
2026/05/23-19:09:56.190922 7fa2327fc6c0 Level-0 table #618: 0 bytes OK
2026/05/23-19:09:56.224359 7fa2327fc6c0 Delete type=0 #616
2026/05/23-19:09:56.339160 7fa2327fc6c0 Manual compaction at level-0 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
2026/05/23-19:09:56.339194 7fa2327fc6c0 Manual compaction at level-1 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
Binary file not shown.
View File
-1
View File
@@ -1 +0,0 @@
MANIFEST-000616
View File
-8
View File
@@ -1,8 +0,0 @@
2026/05/24-09:22:43.091828 7fdfa8dfe6c0 Recovering log #614
2026/05/24-09:22:43.131041 7fdfa8dfe6c0 Delete type=3 #612
2026/05/24-09:22:43.131104 7fdfa8dfe6c0 Delete type=0 #614
2026/05/24-09:42:00.326388 7fdf5affd6c0 Level-0 table #619: started
2026/05/24-09:42:00.326417 7fdf5affd6c0 Level-0 table #619: 0 bytes OK
2026/05/24-09:42:00.334051 7fdf5affd6c0 Delete type=0 #617
2026/05/24-09:42:00.334205 7fdf5affd6c0 Manual compaction at level-0 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
2026/05/24-09:42:00.334364 7fdf5affd6c0 Manual compaction at level-1 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
-8
View File
@@ -1,8 +0,0 @@
2026/05/23-18:45:10.228083 7fa232ffd6c0 Recovering log #610
2026/05/23-18:45:10.236935 7fa232ffd6c0 Delete type=3 #608
2026/05/23-18:45:10.236958 7fa232ffd6c0 Delete type=0 #610
2026/05/23-19:09:56.261648 7fa2327fc6c0 Level-0 table #615: started
2026/05/23-19:09:56.261672 7fa2327fc6c0 Level-0 table #615: 0 bytes OK
2026/05/23-19:09:56.304021 7fa2327fc6c0 Delete type=0 #613
2026/05/23-19:09:56.339181 7fa2327fc6c0 Manual compaction at level-0 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
2026/05/23-19:09:56.339208 7fa2327fc6c0 Manual compaction at level-1 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
Binary file not shown.
View File
-1
View File
@@ -1 +0,0 @@
MANIFEST-000621
View File
-8
View File
@@ -1,8 +0,0 @@
2026/05/24-09:22:42.993966 7fdfa8dfe6c0 Recovering log #619
2026/05/24-09:22:43.035467 7fdfa8dfe6c0 Delete type=3 #617
2026/05/24-09:22:43.035518 7fdfa8dfe6c0 Delete type=0 #619
2026/05/24-09:42:00.319082 7fdf5affd6c0 Level-0 table #624: started
2026/05/24-09:42:00.319109 7fdf5affd6c0 Level-0 table #624: 0 bytes OK
2026/05/24-09:42:00.326261 7fdf5affd6c0 Delete type=0 #622
2026/05/24-09:42:00.334196 7fdf5affd6c0 Manual compaction at level-0 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
2026/05/24-09:42:00.334355 7fdf5affd6c0 Manual compaction at level-1 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
-8
View File
@@ -1,8 +0,0 @@
2026/05/23-18:45:10.200781 7fa2337fe6c0 Recovering log #615
2026/05/23-18:45:10.211345 7fa2337fe6c0 Delete type=3 #613
2026/05/23-18:45:10.211416 7fa2337fe6c0 Delete type=0 #615
2026/05/23-19:09:56.304132 7fa2327fc6c0 Level-0 table #620: started
2026/05/23-19:09:56.304162 7fa2327fc6c0 Level-0 table #620: 0 bytes OK
2026/05/23-19:09:56.339034 7fa2327fc6c0 Delete type=0 #618
2026/05/23-19:09:56.339189 7fa2327fc6c0 Manual compaction at level-0 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
2026/05/23-19:09:56.339203 7fa2327fc6c0 Manual compaction at level-1 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
MANIFEST-000316
-8
View File
@@ -1,8 +0,0 @@
2026/05/24-09:22:43.194643 7fdfa8dfe6c0 Recovering log #314
2026/05/24-09:22:43.231409 7fdfa8dfe6c0 Delete type=3 #312
2026/05/24-09:22:43.231470 7fdfa8dfe6c0 Delete type=0 #314
2026/05/24-09:42:00.341818 7fdf5affd6c0 Level-0 table #319: started
2026/05/24-09:42:00.341853 7fdf5affd6c0 Level-0 table #319: 0 bytes OK
2026/05/24-09:42:00.348538 7fdf5affd6c0 Delete type=0 #317
2026/05/24-09:42:00.367125 7fdf5affd6c0 Manual compaction at level-0 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
2026/05/24-09:42:00.367348 7fdf5affd6c0 Manual compaction at level-1 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
-8
View File
@@ -1,8 +0,0 @@
2026/05/23-18:45:10.252202 7fa2337fe6c0 Recovering log #310
2026/05/23-18:45:10.261300 7fa2337fe6c0 Delete type=3 #308
2026/05/23-18:45:10.261343 7fa2337fe6c0 Delete type=0 #310
2026/05/23-19:09:56.467263 7fa2327fc6c0 Level-0 table #315: started
2026/05/23-19:09:56.467308 7fa2327fc6c0 Level-0 table #315: 0 bytes OK
2026/05/23-19:09:56.509084 7fa2327fc6c0 Delete type=0 #313
2026/05/23-19:09:56.608544 7fa2327fc6c0 Manual compaction at level-0 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
2026/05/23-19:09:56.665018 7fa2327fc6c0 Manual compaction at level-1 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
MANIFEST-000615
-8
View File
@@ -1,8 +0,0 @@
2026/05/24-09:22:43.141936 7fdf5bfff6c0 Recovering log #613
2026/05/24-09:22:43.189387 7fdf5bfff6c0 Delete type=3 #611
2026/05/24-09:22:43.189438 7fdf5bfff6c0 Delete type=0 #613
2026/05/24-09:42:00.311436 7fdf5affd6c0 Level-0 table #618: started
2026/05/24-09:42:00.311459 7fdf5affd6c0 Level-0 table #618: 0 bytes OK
2026/05/24-09:42:00.318964 7fdf5affd6c0 Delete type=0 #616
2026/05/24-09:42:00.334187 7fdf5affd6c0 Manual compaction at level-0 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
2026/05/24-09:42:00.334334 7fdf5affd6c0 Manual compaction at level-1 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
-8
View File
@@ -1,8 +0,0 @@
2026/05/23-18:45:10.238924 7fa233fff6c0 Recovering log #609
2026/05/23-18:45:10.250309 7fa233fff6c0 Delete type=3 #607
2026/05/23-18:45:10.250371 7fa233fff6c0 Delete type=0 #609
2026/05/23-19:09:56.224507 7fa2327fc6c0 Level-0 table #614: started
2026/05/23-19:09:56.224539 7fa2327fc6c0 Level-0 table #614: 0 bytes OK
2026/05/23-19:09:56.261516 7fa2327fc6c0 Delete type=0 #612
2026/05/23-19:09:56.339172 7fa2327fc6c0 Manual compaction at level-0 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
2026/05/23-19:09:56.339199 7fa2327fc6c0 Manual compaction at level-1 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
Binary file not shown.
+4 -4
View File
@@ -387,11 +387,11 @@
min-width: 6rem;
max-width: 6rem;
}
min-width: 10rem;
max-width: 10rem;
min-width: 11rem;
max-width: 11rem;
.input {
min-width: 2.5rem;
max-width: 2.5rem;
min-width: 3.5rem;
max-width: 3.5rem;
}
}
.granted {
+60
View File
@@ -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 */
/* -------------------------------------------------- */
+4 -7
View File
@@ -647,15 +647,11 @@
.damage-buttons-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-columns: 1fr;
max-width: 280px;
margin: 0 auto;
gap: 8px;
&.monster-damage {
grid-template-columns: 1fr;
max-width: 280px;
margin: 0 auto;
}
.damage-roll-btn {
padding: 10px 14px;
background: linear-gradient(to bottom, #8b4513 0%, #6b3410 100%);
@@ -847,6 +843,7 @@
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
justify-items: center;
}
}
}
+4 -10
View File
@@ -87,16 +87,10 @@
<i class="fa-solid fa-shield-halved" data-roll-type="weapon-defense" data-roll-key="{{item.id}}"></i>
</a>
<a class="rollable" data-roll-type="weapon-damage-small" data-roll-key="{{item.id}}"
data-tooltip="Roll Damage (Small)">
<i class="fa-regular fa-face-head-bandage" data-roll-type="weapon-damage-small"
data-roll-key="{{item.id}}"></i>S
</a>
<a class="rollable" data-roll-type="weapon-damage-medium" data-roll-key="{{item.id}}"
data-tooltip="Roll Damage (Medium)">
<i class="fa-regular fa-face-head-bandage" data-roll-type="weapon-damage-medium"
data-roll-key="{{item.id}}"></i>M
<a class="rollable" data-roll-type="weapon-damage" data-roll-key="{{item.id}}"
data-tooltip="Roll Damage">
<i class="fa-regular fa-face-head-bandage" data-roll-type="weapon-damage"
data-roll-key="{{item.id}}"></i>
</a>
</div>
+3 -22
View File
@@ -69,16 +69,10 @@
</div>
{{/if}}
{{#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}}
{{#if rollData.isDamage}}
<div class="detail-badge damage-badge">
<i class="fa-solid fa-dice-d20"></i>
<span>{{localize "LETHALFANTASY.Label.weapon-damage-medium"}}</span>
<span>{{localize "LETHALFANTASY.Label.weapon-damage"}}</span>
</div>
{{/if}}
@@ -202,19 +196,6 @@
}}+{{weaponDamageOptions.damageModifier}}{{/if}}
</button>
{{else}}
{{#if weaponDamageOptions.damageS}}
<button
class="damage-roll-btn"
data-weapon-id="{{weaponDamageOptions.weaponId}}"
data-damage-type="small"
data-damage-formula="{{weaponDamageOptions.damageS}}"
data-is-monster="false"
title="{{localize 'LETHALFANTASY.Label.rollDamage'}}"
>
<i class="fa-solid fa-dice-d6"></i>
Damage Small
</button>
{{/if}}
{{#if weaponDamageOptions.damageM}}
<button
class="damage-roll-btn"
@@ -225,7 +206,7 @@
title="{{localize 'LETHALFANTASY.Label.rollDamage'}}"
>
<i class="fa-solid fa-dice-d20"></i>
Damage Medium
{{localize "LETHALFANTASY.Label.damage"}}
</button>
{{/if}}
{{/if}}
+16
View File
@@ -0,0 +1,16 @@
<div class="control-icon lethal-luck-grit-hud" data-action="lethal-luck-grit-hud">
<i class="fa-solid fa-dice" title="Adjust Luck/Grit" style="width:36px;height:36px;display:flex;align-items:center;justify-content:center;font-size:18px;cursor:pointer"></i>
<div class="luck-grit-wrap luck-grit-hud-disabled">
<div class="luck-grit-row">
<span class="luck-grit-label">Luck</span>
<button class="luck-grit-btn" data-resource="luck" data-amount="-1">1</button>
<button class="luck-grit-btn" data-resource="luck" data-amount="1">+1</button>
</div>
<div class="luck-grit-row">
<span class="luck-grit-label">Grit</span>
<button class="luck-grit-btn" data-resource="grit" data-amount="-1">1</button>
<button class="luck-grit-btn" data-resource="grit" data-amount="1">+1</button>
</div>
</div>
</div>
+9 -11
View File
@@ -123,17 +123,15 @@
</select>
{{#if (eq rollType "save")}}
{{#if rollTarget.magicUser}}
<div>
<span>Save against spell (+{{rollTarget.actorModifiers.saveModifier}})
?</span>
<input
type="checkbox"
name="saveSpellCheck"
data-action="saveSpellCheck"
/>
</div>
{{/if}}
<div>
<span>Save against spell (+{{rollTarget.actorModifiers.saveModifier}})
?</span>
<input
type="checkbox"
name="saveSpellCheck"
data-action="saveSpellCheck"
/>
</div>
{{/if}}
</fieldSet>
{{/if}}
+2
View File
@@ -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}}
<label>{{localize "LETHALFANTASY.Label.components"}}</label>
<div class="shift-right">
+1 -5
View File
@@ -17,11 +17,7 @@
{{formField systemFields.damageType.fields.typeS value=system.damageType.typeS}}
</div>
<label>{{localize "LETHALFANTASY.Label.damage"}}</label>
<div class="shift-right">
{{formField systemFields.damage.fields.damageS value=system.damage.damageS}}
{{formField systemFields.damage.fields.damageM value=system.damage.damageM}}
</div>
{{formField systemFields.damage.fields.damageM value=system.damage.damageM label="LETHALFANTASY.Label.damage"}}
{{formField systemFields.applyStrengthDamageBonus value=system.applyStrengthDamageBonus localize=true}}