Compare commits

...

5 Commits

Author SHA1 Message Date
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
uberwald d389a85a9f Fix ranged attacks again
Release Creation / build (release) Successful in 43s
2026-05-24 09:42:07 +02:00
uberwald c217490a5b Fix ranged attacks again
Release Creation / build (release) Has been cancelled
2026-05-24 09:41:06 +02:00
uberwald 38eb1a8d3d Add ranged actions for monsters
Release Creation / build (release) Successful in 54s
2026-05-23 19:10:10 +02:00
38 changed files with 795 additions and 408 deletions
+62
View File
@@ -0,0 +1,62 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with this repository.
## Overview
FoundryVTT v13+ game system for the **Lethal Fantasy RPG**. Entry point: `lethal-fantasy.mjs`.
## Commands
```bash
# Compile LESS styles (styles/ -> css/)
npx gulp css # one-shot
npx gulp # compile + watch
# Lint
npx eslint .
# Compendium pack management (LevelDB <-> YAML source)
npm run pushLDBtoYML # export packs-system/ LevelDB -> source YAML
npm run pullYMLtoLDB # import source YAML -> packs-system/ LevelDB
```
No test suite exists.
## Architecture
Four layers in `module/`, all wired in `lethal-fantasy.mjs` via the `init` hook:
| Layer | Path | Purpose |
|---|---|---|
| Config | `module/config/` | Game constants. `SYSTEM` is `globalThis.SYSTEM` — always use `SYSTEM.*` for enumerations. |
| Models | `module/models/` | `TypeDataModel` subclasses — data schemas per document type. |
| Documents | `module/documents/` | Actor/Item/Roll/ChatMessage subclasses — game logic, roll processing, hooks. |
| Applications | `module/applications/` | `ApplicationV2` sheets + custom combat tracker. |
**Actor types**: `character`, `monster`
**Item types**: `skill`, `gift`, `vulnerability`, `weapon`, `armor`, `shield`, `spell`, `miracle`, `equipment`
Each layer has an `_module.mjs` barrel file that re-exports all classes from that layer.
Templates (`.hbs`) live in `templates/`. Styles are authored in LESS under `styles/` and compiled to `css/`.
### Key Patterns
- **Sheets**: Extend `HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2)` — imported from `foundry.applications.api`. **Not** the legacy `ActorSheet`. Child sheets (e.g. `character-sheet.mjs`) extend `base-actor-sheet.mjs` and override `static PARTS` and `DEFAULT_OPTIONS.actions`. Template paths are prefixed `systems/fvtt-lethal-fantasy/templates/`. Actor sheets have a play/edit toggle via `_sheetMode` and `SHEET_MODES`.
- **Models**: `static defineSchema()` using `foundry.data.fields.*`. Field definitions derived from SYSTEM config objects.
- **Rolls**: `LethalFantasyRoll` extends `Roll` with rich metadata via `this.options`. `D30Roll` is a separate class for D30 result tables (initialized in the `ready` hook).
- **Socket**: Events use `game.socket.on(\`system.${SYSTEM.id}\`, ...)`. Multi-player attack-defense uses a global `pendingDefenses` Map.
- **i18n**: All user-visible strings are keys in `lang/en.json` as `LETHALFANTASY.Category.Key`. Always use `game.i18n.localize(key)`.
### Compendium Packs
Five LevelDB packs in `packs-system/`: skills, equipment, gifts, vulnerabilities, spells-miracles. Use the `tools/` scripts to export/import editable YAML.
## Code Style
- No semicolons, double quotes, 2-space indent
- JSDoc `/** */` required on all functions/classes
- Max line length 180 (strings/templates exempt)
- Arrow functions: omit parens for single param
- ESLint + Prettier config in `eslint.config.mjs`
+4 -4
View File
@@ -529,16 +529,16 @@ i.lethalfantasy {
} }
.lethalfantasy .tab.character-combat .main-div .combat-details .combat-detail .armor-hp { .lethalfantasy .tab.character-combat .main-div .combat-details .combat-detail .armor-hp {
margin-right: 4px; margin-right: 4px;
min-width: 10rem; min-width: 11rem;
max-width: 10rem; max-width: 11rem;
} }
.lethalfantasy .tab.character-combat .main-div .combat-details .combat-detail .armor-hp .name { .lethalfantasy .tab.character-combat .main-div .combat-details .combat-detail .armor-hp .name {
min-width: 6rem; min-width: 6rem;
max-width: 6rem; max-width: 6rem;
} }
.lethalfantasy .tab.character-combat .main-div .combat-details .combat-detail .armor-hp .input { .lethalfantasy .tab.character-combat .main-div .combat-details .combat-detail .armor-hp .input {
min-width: 2.5rem; min-width: 3.5rem;
max-width: 2.5rem; max-width: 3.5rem;
} }
.lethalfantasy .tab.character-combat .main-div .combat-details .combat-detail .granted { .lethalfantasy .tab.character-combat .main-div .combat-details .combat-detail .granted {
min-width: 8rem; min-width: 8rem;
+2
View File
@@ -471,6 +471,8 @@
"range": "Range", "range": "Range",
"rangeDefenseDialog": "Ranged defense dialog", "rangeDefenseDialog": "Ranged defense dialog",
"rangeDefenseRoll": "Ranged defense roll", "rangeDefenseRoll": "Ranged defense roll",
"rangeAttackDialog": "Ranged attack dialog",
"rangeAttackRoll": "Ranged attack roll",
"rangedAttackDefense": "Ranged attack defense", "rangedAttackDefense": "Ranged attack defense",
"resource": "Resource", "resource": "Resource",
"resources": "Resources", "resources": "Resources",
+44 -42
View File
@@ -375,12 +375,12 @@ Hooks.on("renderChatMessageHTML", (message, html, data) => {
{ {
action: "cancel", action: "cancel",
label: game.i18n.localize("LETHALFANTASY.Combat.proceedNo"), label: game.i18n.localize("LETHALFANTASY.Combat.proceedNo"),
callback: () => null callback: () => "cancel"
} }
], ],
rejectClose: false rejectClose: false
}) })
if (manualDR === null) return if (manualDR === null || manualDR === "cancel") return
const rollOpts = { const rollOpts = {
type: "spell-damage", type: "spell-damage",
rollType: "spell-damage", rollType: "spell-damage",
@@ -394,18 +394,7 @@ Hooks.on("renderChatMessageHTML", (message, html, data) => {
actorName: actor.name, actorName: actor.name,
actorImage: actor.img actorImage: actor.img
} }
const roll = new documents.LethalFantasyRoll(damageFormula, {}, rollOpts) await documents.LethalFantasyRoll.rollSpellDamageToMessage(damageFormula, 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()
return return
} }
@@ -537,19 +526,31 @@ Hooks.on("createChatMessage", async (message) => {
}) })
} }
// Si le défenseur est un personnage qui perd, proposer Grit/Luck (seulement s'il a des points) // Reaction phase — both sides may use grit/luck/shield/mulligan before the outcome is resolved.
// Seulement si l'utilisateur actuel est le propriétaire du défenseur // After a mulligan reroll (either side), the comparison restarts so both sides can react to the new numbers.
let defenderHandledBonus = false let defenderHandledBonus = false
let attackerHandledBonus = false
let shieldReaction = null let shieldReaction = null
let shieldBlocked = false let shieldBlocked = false
const isSpellOrMiracle = attackRollType === "spell-attack" || attackRollType === "miracle-attack" const isSpellOrMiracle = attackRollType === "spell-attack" || attackRollType === "miracle-attack"
if (defender && defenseRoll < attackRoll && isPrimaryController(defender) && !isSpellOrMiracle) {
// These persist across mulligan restarts (once used they stay consumed)
const shieldData = LethalFantasyUtils.getShieldReactionData(defender) const shieldData = LethalFantasyUtils.getShieldReactionData(defender)
let canRerollDefense = LethalFantasyUtils.hasD30Reroll(defenseD30message) let canRerollDefense = LethalFantasyUtils.hasD30Reroll(defenseD30message)
let canShieldReact = !!shieldData let canShieldReact = !!shieldData
let canAdHocShield = !shieldData let canAdHocShield = !shieldData
let attackRollFinal = attackRoll
let canRerollAttack = LethalFantasyUtils.hasD30Reroll(attackD30message)
let mulliganRestart = false
while (defenseRoll < attackRoll) { do {
mulliganRestart = false
defenderHandledBonus = false
attackerHandledBonus = false
// ── Defense reaction loop ──────────────────────────────────────────────
if (defender && defenseRoll < attackRollFinal && isPrimaryController(defender) && !isSpellOrMiracle) {
while (defenseRoll < attackRollFinal) {
const currentGrit = Number(defender.system?.grit?.current) || 0 const currentGrit = Number(defender.system?.grit?.current) || 0
const currentLuck = Number(defender.system?.luck?.current) || 0 const currentLuck = Number(defender.system?.luck?.current) || 0
const buttons = [] const buttons = []
@@ -582,7 +583,7 @@ Hooks.on("createChatMessage", async (message) => {
if (canRerollDefense) { if (canRerollDefense) {
buttons.push({ buttons.push({
action: "rerollDefense", action: "rerollDefense",
label: "Re-roll defense", label: "Re-roll defense (Mulligan)",
icon: "fa-solid fa-rotate-right", icon: "fa-solid fa-rotate-right",
callback: () => "rerollDefense" callback: () => "rerollDefense"
}) })
@@ -596,7 +597,6 @@ Hooks.on("createChatMessage", async (message) => {
callback: () => "shieldReact" callback: () => "shieldReact"
}) })
} else if (canAdHocShield) { } else if (canAdHocShield) {
// No pre-configured shield — offer ad-hoc shield option (useful for monsters)
buttons.push({ buttons.push({
action: "adHocShield", action: "adHocShield",
label: "Roll ad-hoc shield (choose dice + DR)", label: "Roll ad-hoc shield (choose dice + DR)",
@@ -618,7 +618,7 @@ Hooks.on("createChatMessage", async (message) => {
content: ` content: `
<div class="grit-luck-dialog"> <div class="grit-luck-dialog">
<div class="combat-status"> <div class="combat-status">
<p><strong>${attackerName}</strong> rolled <strong>${attackRoll}</strong></p> <p><strong>${attackerName}</strong> rolled <strong>${attackRollFinal}</strong></p>
<p><strong>${defenderName}</strong> currently has <strong>${defenseRoll}</strong></p> <p><strong>${defenderName}</strong> currently has <strong>${defenseRoll}</strong></p>
${defenseD30message ? `<p class="bonus-info">D30 special: ${defenseD30message.description}</p>` : ""} ${defenseD30message ? `<p class="bonus-info">D30 special: ${defenseD30message.description}</p>` : ""}
</div> </div>
@@ -634,21 +634,21 @@ Hooks.on("createChatMessage", async (message) => {
defenderHandledBonus = true defenderHandledBonus = true
if (choice === "grit") { 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>`) const bonusRoll = await LethalFantasyUtils.rollBonusDie("1d6", defender, total => `<p><strong>${defenderName}</strong> spends 1 Grit and rolls <strong>${total}</strong> for defense.</p>`)
defenseRoll += bonusRoll defenseRoll += bonusRoll
await defender.update({ "system.grit.current": currentGrit - 1 }) await defender.update({ "system.grit.current": currentGrit - 1 })
continue continue
} }
if (choice === "luck") { 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>`) const bonusRoll = await LethalFantasyUtils.rollBonusDie("1d6", defender, total => `<p><strong>${defenderName}</strong> spends 1 Luck and rolls <strong>${total}</strong> for defense.</p>`)
defenseRoll += bonusRoll defenseRoll += bonusRoll
await defender.update({ "system.luck.current": currentLuck - 1 }) await defender.update({ "system.luck.current": currentLuck - 1 })
continue continue
} }
if (choice === "bonusDie") { if (choice === "bonusDie") {
const bonusDie = await LethalFantasyUtils.promptCombatBonusDie(defenderName, "attack", defenseRoll, attackRoll) const bonusDie = await LethalFantasyUtils.promptCombatBonusDie(defenderName, "attack", defenseRoll, attackRollFinal)
if (!bonusDie) continue if (!bonusDie) continue
const bonusRoll = await LethalFantasyUtils.rollBonusDie(bonusDie, defender, (total, formula) => `<p><strong>${defenderName}</strong> adds <strong>${formula.toUpperCase()}</strong> and rolls <strong>${total}</strong> for defense.</p>`) const bonusRoll = await LethalFantasyUtils.rollBonusDie(bonusDie, defender, (total, formula) => `<p><strong>${defenderName}</strong> adds <strong>${formula.toUpperCase()}</strong> and rolls <strong>${total}</strong> for defense.</p>`)
defenseRoll += bonusRoll defenseRoll += bonusRoll
@@ -661,8 +661,10 @@ Hooks.on("createChatMessage", async (message) => {
canRerollDefense = false canRerollDefense = false
if (!reroll) continue if (!reroll) continue
defenseRoll = reroll.options?.rollTotal || reroll.total || oldDefenseRoll defenseRoll = reroll.options?.rollTotal || reroll.total || oldDefenseRoll
await createReactionMessage(defender, `<p><strong>${defenderName}</strong> uses Mulligan and re-rolls defense: <strong>${oldDefenseRoll}</strong> → <strong>${defenseRoll}</strong>.</p>`) await createReactionMessage(defender, `<p><strong>${defenderName}</strong> uses Mulligan and re-rolls defense: <strong>${oldDefenseRoll}</strong> → <strong>${defenseRoll}</strong>. Both sides may now react to the new numbers.</p>`)
continue // Restart the full comparison so both sides can react to the new roll
mulliganRestart = true
break
} }
if (choice === "shieldReact" && canShieldReact) { if (choice === "shieldReact" && canShieldReact) {
@@ -671,7 +673,7 @@ Hooks.on("createChatMessage", async (message) => {
defenseRoll = newDefenseTotal defenseRoll = newDefenseTotal
canShieldReact = false canShieldReact = false
if (newDefenseTotal >= attackRoll) { if (newDefenseTotal >= attackRollFinal) {
shieldBlocked = true shieldBlocked = true
shieldReaction = { shieldReaction = {
damageReduction: shieldData.damageReduction, damageReduction: shieldData.damageReduction,
@@ -680,19 +682,19 @@ Hooks.on("createChatMessage", async (message) => {
} }
await createReactionMessage( await createReactionMessage(
defender, defender,
`<p><strong>${defenderName}</strong> rolls <strong>${shieldData.label}</strong> and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal}${attackRoll}). <strong>Shield blocked the attack!</strong> Both armor DR and shield DR <strong>${shieldData.damageReduction}</strong> will apply to damage.</p>` `<p><strong>${defenderName}</strong> rolls <strong>${shieldData.label}</strong> and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal}${attackRollFinal}). <strong>Shield blocked the attack!</strong> Both armor DR and shield DR <strong>${shieldData.damageReduction}</strong> will apply to damage.</p>`
) )
} else { } else {
shieldReaction = null shieldReaction = null
await createReactionMessage( await createReactionMessage(
defender, defender,
`<p><strong>${defenderName}</strong> rolls <strong>${shieldData.label}</strong> and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} < ${attackRoll}). Shield did not block — normal hit, armor DR only.</p>` `<p><strong>${defenderName}</strong> rolls <strong>${shieldData.label}</strong> and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} < ${attackRollFinal}). Shield did not block — normal hit, armor DR only.</p>`
) )
} }
} }
if (choice === "adHocShield") { if (choice === "adHocShield") {
const adHoc = await LethalFantasyUtils.promptAdHocShield(defenderName, attackRoll, defenseRoll) const adHoc = await LethalFantasyUtils.promptAdHocShield(defenderName, attackRollFinal, defenseRoll)
if (!adHoc) continue if (!adHoc) continue
const shieldBonus = await LethalFantasyUtils.rollBonusDie(adHoc.formula, defender) const shieldBonus = await LethalFantasyUtils.rollBonusDie(adHoc.formula, defender)
const newDefenseTotal = defenseRoll + shieldBonus const newDefenseTotal = defenseRoll + shieldBonus
@@ -700,7 +702,7 @@ Hooks.on("createChatMessage", async (message) => {
canShieldReact = false canShieldReact = false
canAdHocShield = false canAdHocShield = false
if (newDefenseTotal >= attackRoll) { if (newDefenseTotal >= attackRollFinal) {
shieldBlocked = true shieldBlocked = true
shieldReaction = { shieldReaction = {
damageReduction: adHoc.damageReduction, damageReduction: adHoc.damageReduction,
@@ -709,27 +711,23 @@ Hooks.on("createChatMessage", async (message) => {
} }
await createReactionMessage( await createReactionMessage(
defender, defender,
`<p><strong>${defenderName}</strong> rolls <strong>${adHoc.formula.toUpperCase()}</strong> shield and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal}${attackRoll}). <strong>Shield blocked the attack!</strong> Both armor DR and shield DR <strong>${adHoc.damageReduction}</strong> will apply to damage.</p>` `<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>`
) )
} else { } else {
shieldReaction = null shieldReaction = null
await createReactionMessage( await createReactionMessage(
defender, defender,
`<p><strong>${defenderName}</strong> rolls <strong>${adHoc.formula.toUpperCase()}</strong> shield and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} < ${attackRoll}). Shield did not block — normal hit, armor DR only.</p>` `<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>`
) )
} }
} }
} }
} }
let attackRollFinal = attackRoll if (mulliganRestart) continue
let attackerHandledBonus = false
// Si l'attaquant est un personnage qui perd et a du Grit // ── Attack reaction loop ───────────────────────────────────────────────
// Seulement si l'utilisateur actuel est le propriétaire de l'attaquant (pas le MJ)
if (!defenderHandledBonus && attacker && attackRollFinal <= defenseRoll && isPrimaryController(attacker)) { if (!defenderHandledBonus && attacker && attackRollFinal <= defenseRoll && isPrimaryController(attacker)) {
let canRerollAttack = LethalFantasyUtils.hasD30Reroll(attackD30message)
while (attackRollFinal <= defenseRoll) { while (attackRollFinal <= defenseRoll) {
const currentGrit = Number(attacker.system?.grit?.current) || 0 const currentGrit = Number(attacker.system?.grit?.current) || 0
const buttons = [] const buttons = []
@@ -753,7 +751,7 @@ Hooks.on("createChatMessage", async (message) => {
if (canRerollAttack && attackRerollContext) { if (canRerollAttack && attackRerollContext) {
buttons.push({ buttons.push({
action: "rerollAttack", action: "rerollAttack",
label: "Re-roll attack", label: "Re-roll attack (Mulligan)",
icon: "fa-solid fa-rotate-right", icon: "fa-solid fa-rotate-right",
callback: () => "rerollAttack" callback: () => "rerollAttack"
}) })
@@ -788,7 +786,7 @@ Hooks.on("createChatMessage", async (message) => {
attackerHandledBonus = true attackerHandledBonus = true
if (choice === "grit") { if (choice === "grit") {
const attackBonus = await LethalFantasyUtils.rollBonusDie("1d6", attacker, (total) => `<p><strong>${attackerName}</strong> spends 1 Grit and rolls <strong>${total}</strong> for attack.</p>`) const attackBonus = await LethalFantasyUtils.rollBonusDie("1d6", attacker, total => `<p><strong>${attackerName}</strong> spends 1 Grit and rolls <strong>${total}</strong> for attack.</p>`)
attackRollFinal += attackBonus attackRollFinal += attackBonus
await attacker.update({ "system.grit.current": currentGrit - 1 }) await attacker.update({ "system.grit.current": currentGrit - 1 })
continue continue
@@ -808,10 +806,14 @@ Hooks.on("createChatMessage", async (message) => {
canRerollAttack = false canRerollAttack = false
if (!reroll) continue if (!reroll) continue
attackRollFinal = reroll.options?.rollTotal || reroll.total || oldAttackRoll attackRollFinal = reroll.options?.rollTotal || reroll.total || oldAttackRoll
await createReactionMessage(attacker, `<p><strong>${attackerName}</strong> uses Mulligan and re-rolls attack: <strong>${oldAttackRoll}</strong> → <strong>${attackRollFinal}</strong>.</p>`) await createReactionMessage(attacker, `<p><strong>${attackerName}</strong> uses Mulligan and re-rolls attack: <strong>${oldAttackRoll}</strong> → <strong>${attackRollFinal}</strong>. Both sides may now react to the new numbers.</p>`)
// Restart the full comparison so both sides can react to the new roll
mulliganRestart = true
break
} }
} }
} }
} while (mulliganRestart)
const shieldDamageReduction = shieldBlocked ? shieldReaction.damageReduction : 0 const shieldDamageReduction = shieldBlocked ? shieldReaction.damageReduction : 0
const outcome = shieldBlocked ? "shielded-hit" : (attackRollFinal > defenseRoll ? "hit" : "miss") const outcome = shieldBlocked ? "shielded-hit" : (attackRollFinal > defenseRoll ? "hit" : "miss")
+3 -14
View File
@@ -268,12 +268,12 @@ export default class LethalFantasyCharacterSheet extends LethalFantasyActorSheet
{ {
action: "cancel", action: "cancel",
label: game.i18n.localize("LETHALFANTASY.Combat.proceedNo"), label: game.i18n.localize("LETHALFANTASY.Combat.proceedNo"),
callback: () => null callback: () => "cancel"
} }
], ],
rejectClose: false rejectClose: false
}) })
if (manualDR === null) return if (manualDR === null || manualDR === "cancel") return
const rollOpts = { const rollOpts = {
type: "spell-damage", type: "spell-damage",
@@ -286,18 +286,7 @@ export default class LethalFantasyCharacterSheet extends LethalFantasyActorSheet
actorName: this.actor.name, actorName: this.actor.name,
actorImage: this.actor.img actorImage: this.actor.img
} }
const roll = new LethalFantasyRoll(formula, {}, rollOpts) await LethalFantasyRoll.rollSpellDamageToMessage(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()
} }
static #onCreateEquipment(event, target) { static #onCreateEquipment(event, target) {
+49
View File
@@ -108,6 +108,10 @@
} }
], ],
"description": "Possible Flawless or Legendary Defense or Add D20E to Defense" "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": { "29": {
@@ -145,6 +149,11 @@
"type": "gain_grit", "type": "gain_grit",
"amount": 1, "amount": 1,
"description": "Gain 1 Grit" "description": "Gain 1 Grit"
},
"saving_throws": {
"type": "gain_grit",
"amount": 1,
"description": "Gain 1 Grit"
} }
}, },
"28": { "28": {
@@ -199,6 +208,12 @@
"amount": 1, "amount": 1,
"target": "skill", "target": "skill",
"description": "Add 1 to Skill Roll" "description": "Add 1 to Skill Roll"
},
"saving_throws": {
"type": "bonus_flat",
"amount": 1,
"target": "save",
"description": "Add 1 to Saving Throw"
} }
}, },
"21": { "21": {
@@ -239,6 +254,12 @@
"ranged_defense": { "ranged_defense": {
"type": "recover_pain", "type": "recover_pain",
"description": "Defender Recovers or ignores any flash of 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": { "20": {
@@ -349,6 +370,12 @@
} }
], ],
"description": "Possible 20/20 defense that avoids Any Attack Except a Lethal Strike or adds D12 to defense" "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": { "15": {
@@ -391,6 +418,12 @@
"shield_bash" "shield_bash"
], ],
"description": "Kick, Punch or Shield Bash" "description": "Kick, Punch or Shield Bash"
},
"saving_throws": {
"type": "bonus_flat",
"amount": 1,
"target": "save",
"description": "Add 1 to Saving Throw"
} }
}, },
"13": {}, "13": {},
@@ -445,6 +478,12 @@
"shield_bash" "shield_bash"
], ],
"description": "Kick, Punch or Shield Bash" "description": "Kick, Punch or Shield Bash"
},
"saving_throws": {
"type": "bonus_flat",
"amount": 1,
"target": "save",
"description": "Add 1 to Saving Throw"
} }
}, },
"8": { "8": {
@@ -475,6 +514,10 @@
"ranged_defense": { "ranged_defense": {
"type": "mulligan", "type": "mulligan",
"description": "Mulligan, Can Choose to Re-Roll This Defense" "description": "Mulligan, Can Choose to Re-Roll This Defense"
},
"saving_throws": {
"type": "mulligan",
"description": "Mulligan, Can Re-Roll This Saving Throw"
} }
}, },
"7": { "7": {
@@ -528,6 +571,12 @@
"shield_bash" "shield_bash"
], ],
"description": "Kick, Punch, or Shield Bash" "description": "Kick, Punch, or Shield Bash"
},
"saving_throws": {
"type": "bonus_flat",
"amount": 1,
"target": "save",
"description": "Add 1 to Saving Throw"
} }
}, },
"3": { "3": {
+8
View File
@@ -90,6 +90,13 @@ export const RANGE_CHOICES = {
"beyondskill": { label: "Beyond Skill (Blue +11)", value: "beyondskill" } "beyondskill": { label: "Beyond Skill (Blue +11)", value: "beyondskill" }
} }
export const ATTACKER_MOVEMENT_CHOICES = {
"none": { label: "None / Stationary (D20E Favor)", favor: true, value: "2D20kh" },
"walk": { label: "Walk (D20E)", value: "D20" },
"incombat": { label: "In Combat (D20E)", value: "D20" },
"run": { label: "Jog/Run/Sprint (D20E Disfavor)", disfavor: true, value: "2D20kl" }
}
export const ATTACKER_AIM_CHOICES = { export const ATTACKER_AIM_CHOICES = {
"simple": { label: "Simple (+0)", value: "0" }, "simple": { label: "Simple (+0)", value: "0" },
"careful": { label: "Careful (Red +5)", value: "+4" }, "careful": { label: "Careful (Red +5)", value: "+4" },
@@ -321,6 +328,7 @@ export const SYSTEM = {
RANGE_CHOICES, RANGE_CHOICES,
FAVOR_CHOICES, FAVOR_CHOICES,
ATTACKER_AIM_CHOICES, ATTACKER_AIM_CHOICES,
ATTACKER_MOVEMENT_CHOICES,
MORTAL_CHOICES, MORTAL_CHOICES,
SPELL_CRITICAL, SPELL_CRITICAL,
MIRACLE_TYPES, MIRACLE_TYPES,
+8 -3
View File
@@ -25,7 +25,8 @@ export default class D30Roll {
RANGED_DEFENSE: "ranged_defense", RANGED_DEFENSE: "ranged_defense",
ARCANE_SPELL_ATTACK: "arcane_spell_attack", ARCANE_SPELL_ATTACK: "arcane_spell_attack",
ARCANE_SPELL_DEFENSE: "arcane_spell_defense", 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 // Skill types
if (externalType === "skill" || externalType === "monster-skill" || if (externalType === "skill" || externalType === "monster-skill" || externalType === "challenge") {
externalType === "save" || externalType === "challenge") {
return this.ROLL_TYPES.SKILL_ROLLS 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 // If no match, return null
console.warn(`D30Roll | Unknown external roll type: ${externalType}`) console.warn(`D30Roll | Unknown external roll type: ${externalType}`)
return null return null
+215 -4
View File
@@ -338,6 +338,10 @@ export default class LethalFantasyRoll extends Roll {
let modifier = "+0" let modifier = "+0"
let targetName let targetName
// True for any ranged attack: PC weapon (ranged type) or monster attack (ranged mode)
const isRangedAttack = (options.rollType === "weapon-attack" && options.rollTarget?.weapon?.system?.weaponType === "ranged")
|| (options.rollType === "monster-attack" && options.rollTarget?.attackMode === "ranged")
let dialogContext = { let dialogContext = {
rollType: options.rollType, rollType: options.rollType,
rollTarget: options.rollTarget, rollTarget: options.rollTarget,
@@ -362,7 +366,8 @@ export default class LethalFantasyRoll extends Roll {
modifier, modifier,
saveSpell: false, saveSpell: false,
favor: "none", favor: "none",
targetName targetName,
isRangedAttack
} }
let rollContext let rollContext
if (options.rollContext) { if (options.rollContext) {
@@ -604,7 +609,7 @@ export default class LethalFantasyRoll extends Roll {
rollD30.total, rollD30.total,
options.rollType, options.rollType,
options.rollTarget?.weapon, options.rollTarget?.weapon,
{ isRanged: isRangedForD30 } { isRanged: isRangedForD30, isSpellSave: saveSpell }
) )
options.D30message = d30Message options.D30message = d30Message
} }
@@ -782,9 +787,18 @@ export default class LethalFantasyRoll extends Roll {
let buttons = [] let buttons = []
if (currentAction) { if (currentAction) {
if (currentAction.type === "weapon") { 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({ buttons.push({
action: "roll", action: "roll",
label: "Roll progression dice", label: weaponLabel,
callback: (event, button) => { callback: (event, button) => {
let pos = $('#combat-action-dialog').position() let pos = $('#combat-action-dialog').position()
game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos) game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos)
@@ -910,7 +924,8 @@ export default class LethalFantasyRoll extends Roll {
await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction))
return return
} else { } 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 }) }) ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) })
currentAction.castingTime = 1 currentAction.castingTime = 1
currentAction.spellStatus = "toBeCasted" currentAction.spellStatus = "toBeCasted"
@@ -1133,6 +1148,8 @@ export default class LethalFantasyRoll extends Roll {
options = { ...options, ...rollContext } options = { ...options, ...rollContext }
options.rollName = "Ranged Defense" options.rollName = "Ranged Defense"
options.rollType = "weapon-defense" options.rollType = "weapon-defense"
options.type = options.rollType // Required: this.type reads options.type
options.rollMode = rollContext.visibility // Required: callers pass roll.options.rollMode to toMessage
const rollBase = new this(rollContext.movement, options.data, rollData) const rollBase = new this(rollContext.movement, options.data, rollData)
const rollModifier = new Roll(modifierFormula, options.data, rollData) const rollModifier = new Roll(modifierFormula, options.data, rollData)
@@ -1200,6 +1217,161 @@ export default class LethalFantasyRoll extends Roll {
return rollBase return rollBase
} }
/**
* Prompts the GM for ranged attack context (movement, range, target size, aim) when a monster
* attacks with a ranged weapon, then evaluates an exploding D20 attack roll with the resulting modifiers.
*
* @param {Object} options Options for the roll.
* @param {string} options.actorId The attacker actor ID.
* @param {string} options.actorName The attacker actor name.
* @param {Object} options.rollTarget The rollTarget containing attackModifier and related data.
* @returns {Promise<LethalFantasyRoll|null>} The resulting roll, or null if cancelled.
*/
static async promptRangedAttack(options = {}) {
const rollModes = foundry.utils.duplicate(CONFIG.ChatMessage.modes)
const fieldRollMode = new foundry.data.fields.StringField({
choices: rollModes,
blank: false,
default: "public",
})
let dialogContext = {
attackerMovementChoices: SYSTEM.ATTACKER_MOVEMENT_CHOICES,
rangeChoices: SYSTEM.RANGE_CHOICES,
sizeChoices: SYSTEM.SIZE_CHOICES,
attackerAimChoices: SYSTEM.ATTACKER_AIM_CHOICES,
movement: "none",
range: "short",
size: "+5",
attackerAim: "simple",
fieldRollMode,
rollModes
}
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/range-attack-dialog.hbs", dialogContext)
const label = game.i18n.localize("LETHALFANTASY.Label.rangeAttackRoll")
const rollContext = await foundry.applications.api.DialogV2.wait({
window: { title: "Ranged Attack" },
classes: ["lethalfantasy"],
content,
buttons: [
{
label,
callback: (event, button) => {
const output = Array.from(button.form.elements).reduce((obj, input) => {
if (input.name) obj[input.name] = input.value
return obj
}, {})
return output
},
},
],
rejectClose: false
})
if (rollContext === null) return null
// Handle pointblank: attacker at point blank gets favor (standing still easier to aim)
if (rollContext.range === "pointblank") {
rollContext.movement = rollContext.movement.replace("kh", "")
rollContext.movement = rollContext.movement.replace("kl", "")
rollContext.movement += "kh" // Favor for attacker at point blank
rollContext.range = "0"
}
// Handle beyondskill: extreme range gives disfavor to attacker
if (rollContext.range === "beyondskill") {
rollContext.movement = rollContext.movement.replace("kh", "")
rollContext.movement = rollContext.movement.replace("kl", "")
rollContext.movement += "kl" // Disfavor for attacker beyond skill range
rollContext.range = "+11"
}
// Compute contextual penalty: range + target_size, reduced by aim bonus and attack modifier
const attackModifier = options.rollTarget?.attackModifier ?? 0
const contextualPenalty = Number(rollContext.range) + Number(rollContext.size)
const aimBonus = Number(rollContext.attackerAim || 0)
const fullModifier = contextualPenalty - aimBonus - attackModifier
let modifierFormula
if (fullModifier === 0) {
modifierFormula = "0"
} else {
const modAbs = Math.abs(fullModifier)
modifierFormula = `D${modAbs + 1} -1`
}
const rollData = { ...rollContext }
options = { ...options, ...rollContext }
options.rollName = "Ranged Attack"
options.rollType = options.rollType || "monster-attack"
options.type = options.rollType // Required: this.type reads options.type, used to build weaponDamageOptions in toHTML
options.rollMode = rollContext.visibility // Required: callers pass roll.options.rollMode to toMessage
options.isRangedAttack = true
const rollBase = new this(rollContext.movement, options.data, rollData)
const rollModifier = new Roll(modifierFormula, options.data, rollData)
rollModifier.evaluate()
await rollBase.evaluate()
const rollD30 = await new Roll("1D30").evaluate()
options.D30result = rollD30.total
options.D30message = D30Roll.getResult(rollD30.total, options.rollType, undefined, { isRanged: true })
// Determine favor from dice formula
let badResult = 0
if (rollContext.movement.includes("kh")) {
rollData.favor = "favor"
badResult = Math.min(rollBase.terms[0].results[0].result, rollBase.terms[0].results[1]?.result || 20)
}
if (rollContext.movement.includes("kl")) {
rollData.favor = "disfavor"
badResult = Math.max(rollBase.terms[0].results[0].result, rollBase.terms[0].results[1]?.result || 1)
}
const dice = rollContext.movement
const maxValue = 20
let rollTotal = -1
let diceResults = []
let diceResult = rollBase.dice[0].results[0].result
diceResults.push({ dice: `${dice.toUpperCase()}`, value: diceResult })
let diceSum = diceResult
// Exploding dice
while (diceResult === maxValue) {
const r = await new Roll(dice).evaluate()
diceResult = r.dice[0].results[0].result
diceResults.push({ dice: `${dice.toUpperCase()}-1`, value: diceResult - 1 })
diceSum += (diceResult - 1)
}
if (fullModifier !== 0) {
diceResults.push({ dice: `${rollModifier.formula.toUpperCase()}`, value: rollModifier.total })
if (fullModifier > 0) {
// Net penalty: subtract from roll
rollTotal = Math.max(diceSum - rollModifier.total, 0)
} else {
// Net bonus: add to roll
rollTotal = diceSum + rollModifier.total
}
} else {
rollTotal = diceSum
}
rollBase.options = { ...rollBase.options, ...options }
rollBase.options.resultType = undefined
rollBase.options.rollTotal = rollTotal
rollBase.options.diceResults = diceResults
rollBase.options.rollTarget = options.rollTarget
rollBase.options.titleFormula = `1D20E + ${modifierFormula}`
rollBase.options.D30result = options.D30result
rollBase.options.D30message = options.D30message
rollBase.options.rollName = "Ranged Attack"
rollBase.options.badResult = badResult
rollBase.options.rollData = foundry.utils.duplicate(rollData)
return rollBase
}
/** /**
* Creates a title based on the given type. * Creates a title based on the given type.
* *
@@ -1367,4 +1539,43 @@ 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()
if (game?.dice3d) await game.dice3d.showForRoll(xr, game.user, true)
// 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()
}
} }
+16
View File
@@ -166,6 +166,22 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
*/ */
async roll(rollType, rollTarget, defenderId = undefined, defenderTokenId = undefined, extraShieldDr = 0) { async roll(rollType, rollTarget, defenderId = undefined, defenderTokenId = undefined, extraShieldDr = 0) {
const hasTarget = false const hasTarget = false
// Ranged monster defense uses the ranged defense dialog (movement, range, size modifiers)
if (rollType === "monster-defense" && rollTarget?.isRangedDefense === true) {
let roll = await LethalFantasyRoll.promptRangedDefense({
actorId: this.parent.id,
actorName: this.parent.name,
actorImage: this.parent.img,
rollTarget,
defenderId,
defenderTokenId,
})
if (!roll) return null
await roll.toMessage({}, { messageMode: roll.options.rollMode })
return
}
let roll = await LethalFantasyRoll.prompt({ let roll = await LethalFantasyRoll.prompt({
rollType, rollType,
rollTarget, rollTarget,
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000611 MANIFEST-000623
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/20-23:09:33.153266 7f179e7ed6c0 Recovering log #609 2026/05/25-11:43:40.059603 7f88315ff6c0 Recovering log #621
2026/05/20-23:09:33.205459 7f179e7ed6c0 Delete type=3 #607 2026/05/25-11:43:40.071136 7f88315ff6c0 Delete type=3 #619
2026/05/20-23:09:33.205507 7f179e7ed6c0 Delete type=0 #609 2026/05/25-11:43:40.071202 7f88315ff6c0 Delete type=0 #621
2026/05/20-23:16:53.517842 7f179d7eb6c0 Level-0 table #614: started 2026/05/25-12:29:17.727995 7f87e2ffd6c0 Level-0 table #626: started
2026/05/20-23:16:53.517893 7f179d7eb6c0 Level-0 table #614: 0 bytes OK 2026/05/25-12:29:17.728028 7f87e2ffd6c0 Level-0 table #626: 0 bytes OK
2026/05/20-23:16:53.523991 7f179d7eb6c0 Delete type=0 #612 2026/05/25-12:29:17.733783 7f87e2ffd6c0 Delete type=0 #624
2026/05/20-23:16:53.536989 7f179d7eb6c0 Manual compaction at level-0 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end) 2026/05/25-12:29:17.740143 7f87e2ffd6c0 Manual compaction at level-0 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
2026/05/20-23:16:53.537026 7f179d7eb6c0 Manual compaction at level-1 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end) 2026/05/25-12:29:17.740167 7f87e2ffd6c0 Manual compaction at level-1 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/20-00:10:37.079181 7f0b1e7ee6c0 Recovering log #605 2026/05/24-09:22:43.041326 7fdfa95ff6c0 Recovering log #617
2026/05/20-00:10:37.089613 7f0b1e7ee6c0 Delete type=3 #603 2026/05/24-09:22:43.083988 7fdfa95ff6c0 Delete type=3 #615
2026/05/20-00:10:37.089671 7f0b1e7ee6c0 Delete type=0 #605 2026/05/24-09:22:43.084045 7fdfa95ff6c0 Delete type=0 #617
2026/05/20-10:53:40.566713 7f0b1cfeb6c0 Level-0 table #610: started 2026/05/24-09:42:00.304351 7fdf5affd6c0 Level-0 table #622: started
2026/05/20-10:53:40.566740 7f0b1cfeb6c0 Level-0 table #610: 0 bytes OK 2026/05/24-09:42:00.304423 7fdf5affd6c0 Level-0 table #622: 0 bytes OK
2026/05/20-10:53:40.572741 7f0b1cfeb6c0 Delete type=0 #608 2026/05/24-09:42:00.311333 7fdf5affd6c0 Delete type=0 #620
2026/05/20-10:53:40.587356 7f0b1cfeb6c0 Manual compaction at level-0 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end) 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/20-10:53:40.587409 7f0b1cfeb6c0 Manual compaction at level-1 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)
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000608 MANIFEST-000620
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/20-23:09:33.234730 7f179e7ed6c0 Recovering log #606 2026/05/25-11:43:40.077580 7f87e37fe6c0 Recovering log #618
2026/05/20-23:09:33.288868 7f179e7ed6c0 Delete type=3 #604 2026/05/25-11:43:40.087750 7f87e37fe6c0 Delete type=3 #616
2026/05/20-23:09:33.288920 7f179e7ed6c0 Delete type=0 #606 2026/05/25-11:43:40.087800 7f87e37fe6c0 Delete type=0 #618
2026/05/20-23:16:53.524172 7f179d7eb6c0 Level-0 table #611: started 2026/05/25-12:29:17.758832 7f87e2ffd6c0 Level-0 table #623: started
2026/05/20-23:16:53.524219 7f179d7eb6c0 Level-0 table #611: 0 bytes OK 2026/05/25-12:29:17.758858 7f87e2ffd6c0 Level-0 table #623: 0 bytes OK
2026/05/20-23:16:53.530312 7f179d7eb6c0 Delete type=0 #609 2026/05/25-12:29:17.764622 7f87e2ffd6c0 Delete type=0 #621
2026/05/20-23:16:53.537003 7f179d7eb6c0 Manual compaction at level-0 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end) 2026/05/25-12:29:17.777473 7f87e2ffd6c0 Manual compaction at level-0 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
2026/05/20-23:16:53.537059 7f179d7eb6c0 Manual compaction at level-1 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end) 2026/05/25-12:29:17.784124 7f87e2ffd6c0 Manual compaction at level-1 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/20-00:10:37.107483 7f0b1efef6c0 Recovering log #602 2026/05/24-09:22:43.091828 7fdfa8dfe6c0 Recovering log #614
2026/05/20-00:10:37.117907 7f0b1efef6c0 Delete type=3 #600 2026/05/24-09:22:43.131041 7fdfa8dfe6c0 Delete type=3 #612
2026/05/20-00:10:37.117968 7f0b1efef6c0 Delete type=0 #602 2026/05/24-09:22:43.131104 7fdfa8dfe6c0 Delete type=0 #614
2026/05/20-10:53:40.572909 7f0b1cfeb6c0 Level-0 table #607: started 2026/05/24-09:42:00.326388 7fdf5affd6c0 Level-0 table #619: started
2026/05/20-10:53:40.572948 7f0b1cfeb6c0 Level-0 table #607: 0 bytes OK 2026/05/24-09:42:00.326417 7fdf5affd6c0 Level-0 table #619: 0 bytes OK
2026/05/20-10:53:40.579001 7f0b1cfeb6c0 Delete type=0 #605 2026/05/24-09:42:00.334051 7fdf5affd6c0 Delete type=0 #617
2026/05/20-10:53:40.587370 7f0b1cfeb6c0 Manual compaction at level-0 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end) 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/20-10:53:40.587430 7f0b1cfeb6c0 Manual compaction at level-1 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)
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000613 MANIFEST-000625
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/20-23:09:33.082007 7f179dfec6c0 Recovering log #611 2026/05/25-11:43:40.044921 7f87e3fff6c0 Recovering log #623
2026/05/20-23:09:33.140910 7f179dfec6c0 Delete type=3 #609 2026/05/25-11:43:40.054992 7f87e3fff6c0 Delete type=3 #621
2026/05/20-23:09:33.140962 7f179dfec6c0 Delete type=0 #611 2026/05/25-11:43:40.055048 7f87e3fff6c0 Delete type=0 #623
2026/05/20-23:16:53.510592 7f179d7eb6c0 Level-0 table #616: started 2026/05/25-12:29:17.710484 7f87e2ffd6c0 Level-0 table #628: started
2026/05/20-23:16:53.510664 7f179d7eb6c0 Level-0 table #616: 0 bytes OK 2026/05/25-12:29:17.710568 7f87e2ffd6c0 Level-0 table #628: 0 bytes OK
2026/05/20-23:16:53.517661 7f179d7eb6c0 Delete type=0 #614 2026/05/25-12:29:17.716928 7f87e2ffd6c0 Delete type=0 #626
2026/05/20-23:16:53.536973 7f179d7eb6c0 Manual compaction at level-0 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end) 2026/05/25-12:29:17.740121 7f87e2ffd6c0 Manual compaction at level-0 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
2026/05/20-23:16:53.537037 7f179d7eb6c0 Manual compaction at level-1 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end) 2026/05/25-12:29:17.758707 7f87e2ffd6c0 Manual compaction at level-1 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/20-00:10:37.061485 7f0b1efef6c0 Recovering log #607 2026/05/24-09:22:42.993966 7fdfa8dfe6c0 Recovering log #619
2026/05/20-00:10:37.071572 7f0b1efef6c0 Delete type=3 #605 2026/05/24-09:22:43.035467 7fdfa8dfe6c0 Delete type=3 #617
2026/05/20-00:10:37.071684 7f0b1efef6c0 Delete type=0 #607 2026/05/24-09:22:43.035518 7fdfa8dfe6c0 Delete type=0 #619
2026/05/20-10:53:40.579189 7f0b1cfeb6c0 Level-0 table #612: started 2026/05/24-09:42:00.319082 7fdf5affd6c0 Level-0 table #624: started
2026/05/20-10:53:40.579229 7f0b1cfeb6c0 Level-0 table #612: 0 bytes OK 2026/05/24-09:42:00.319109 7fdf5affd6c0 Level-0 table #624: 0 bytes OK
2026/05/20-10:53:40.587176 7f0b1cfeb6c0 Delete type=0 #610 2026/05/24-09:42:00.326261 7fdf5affd6c0 Delete type=0 #622
2026/05/20-10:53:40.587386 7f0b1cfeb6c0 Manual compaction at level-0 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end) 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/20-10:53:40.587419 7f0b1cfeb6c0 Manual compaction at level-1 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)
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000308 MANIFEST-000320
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/20-23:09:33.368596 7f179e7ed6c0 Recovering log #306 2026/05/25-11:43:40.107633 7f8830dfe6c0 Recovering log #318
2026/05/20-23:09:33.422152 7f179e7ed6c0 Delete type=3 #304 2026/05/25-11:43:40.118039 7f8830dfe6c0 Delete type=3 #316
2026/05/20-23:09:33.422210 7f179e7ed6c0 Delete type=0 #306 2026/05/25-11:43:40.118104 7f8830dfe6c0 Delete type=0 #318
2026/05/20-23:16:53.560903 7f179d7eb6c0 Level-0 table #311: started 2026/05/25-12:29:17.777484 7f87e2ffd6c0 Level-0 table #323: started
2026/05/20-23:16:53.560969 7f179d7eb6c0 Level-0 table #311: 0 bytes OK 2026/05/25-12:29:17.777511 7f87e2ffd6c0 Level-0 table #323: 0 bytes OK
2026/05/20-23:16:53.568455 7f179d7eb6c0 Delete type=0 #309 2026/05/25-12:29:17.784021 7f87e2ffd6c0 Delete type=0 #321
2026/05/20-23:16:53.568616 7f179d7eb6c0 Manual compaction at level-0 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end) 2026/05/25-12:29:17.784188 7f87e2ffd6c0 Manual compaction at level-0 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
2026/05/20-23:16:53.578679 7f179d7eb6c0 Manual compaction at level-1 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end) 2026/05/25-12:29:17.803354 7f87e2ffd6c0 Manual compaction at level-1 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/20-00:10:37.136939 7f0b1efef6c0 Recovering log #302 2026/05/24-09:22:43.194643 7fdfa8dfe6c0 Recovering log #314
2026/05/20-00:10:37.146957 7f0b1efef6c0 Delete type=3 #300 2026/05/24-09:22:43.231409 7fdfa8dfe6c0 Delete type=3 #312
2026/05/20-00:10:37.147030 7f0b1efef6c0 Delete type=0 #302 2026/05/24-09:22:43.231470 7fdfa8dfe6c0 Delete type=0 #314
2026/05/20-10:53:40.615430 7f0b1cfeb6c0 Level-0 table #307: started 2026/05/24-09:42:00.341818 7fdf5affd6c0 Level-0 table #319: started
2026/05/20-10:53:40.615467 7f0b1cfeb6c0 Level-0 table #307: 0 bytes OK 2026/05/24-09:42:00.341853 7fdf5affd6c0 Level-0 table #319: 0 bytes OK
2026/05/20-10:53:40.621586 7f0b1cfeb6c0 Delete type=0 #305 2026/05/24-09:42:00.348538 7fdf5affd6c0 Delete type=0 #317
2026/05/20-10:53:40.621713 7f0b1cfeb6c0 Manual compaction at level-0 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end) 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/20-10:53:40.633723 7f0b1cfeb6c0 Manual compaction at level-1 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)
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000607 MANIFEST-000619
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/20-23:09:33.302872 7f179efee6c0 Recovering log #605 2026/05/25-11:43:40.093877 7f87e3fff6c0 Recovering log #617
2026/05/20-23:09:33.362229 7f179efee6c0 Delete type=3 #603 2026/05/25-11:43:40.103670 7f87e3fff6c0 Delete type=3 #615
2026/05/20-23:09:33.362319 7f179efee6c0 Delete type=0 #605 2026/05/25-11:43:40.103720 7f87e3fff6c0 Delete type=0 #617
2026/05/20-23:16:53.530498 7f179d7eb6c0 Level-0 table #610: started 2026/05/25-12:29:17.764689 7f87e2ffd6c0 Level-0 table #622: started
2026/05/20-23:16:53.530547 7f179d7eb6c0 Level-0 table #610: 0 bytes OK 2026/05/25-12:29:17.764705 7f87e2ffd6c0 Level-0 table #622: 0 bytes OK
2026/05/20-23:16:53.536854 7f179d7eb6c0 Delete type=0 #608 2026/05/25-12:29:17.770486 7f87e2ffd6c0 Delete type=0 #620
2026/05/20-23:16:53.537015 7f179d7eb6c0 Manual compaction at level-0 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end) 2026/05/25-12:29:17.784104 7f87e2ffd6c0 Manual compaction at level-0 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
2026/05/20-23:16:53.537048 7f179d7eb6c0 Manual compaction at level-1 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end) 2026/05/25-12:29:17.784130 7f87e2ffd6c0 Manual compaction at level-1 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/20-00:10:37.121340 7f0b1e7ee6c0 Recovering log #601 2026/05/24-09:22:43.141936 7fdf5bfff6c0 Recovering log #613
2026/05/20-00:10:37.132435 7f0b1e7ee6c0 Delete type=3 #599 2026/05/24-09:22:43.189387 7fdf5bfff6c0 Delete type=3 #611
2026/05/20-00:10:37.132491 7f0b1e7ee6c0 Delete type=0 #601 2026/05/24-09:22:43.189438 7fdf5bfff6c0 Delete type=0 #613
2026/05/20-10:53:40.559937 7f0b1cfeb6c0 Level-0 table #606: started 2026/05/24-09:42:00.311436 7fdf5affd6c0 Level-0 table #618: started
2026/05/20-10:53:40.560028 7f0b1cfeb6c0 Level-0 table #606: 0 bytes OK 2026/05/24-09:42:00.311459 7fdf5affd6c0 Level-0 table #618: 0 bytes OK
2026/05/20-10:53:40.566605 7f0b1cfeb6c0 Delete type=0 #604 2026/05/24-09:42:00.318964 7fdf5affd6c0 Delete type=0 #616
2026/05/20-10:53:40.587338 7f0b1cfeb6c0 Manual compaction at level-0 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end) 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/20-10:53:40.587398 7f0b1cfeb6c0 Manual compaction at level-1 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)
+4 -4
View File
@@ -387,11 +387,11 @@
min-width: 6rem; min-width: 6rem;
max-width: 6rem; max-width: 6rem;
} }
min-width: 10rem; min-width: 11rem;
max-width: 10rem; max-width: 11rem;
.input { .input {
min-width: 2.5rem; min-width: 3.5rem;
max-width: 2.5rem; max-width: 3.5rem;
} }
} }
.granted { .granted {
+45
View File
@@ -0,0 +1,45 @@
<div class="lethalfantasy-range-defense-dialog">
<fieldSet class="">
<legend>{{localize "LETHALFANTASY.Label.rangeAttackDialog"}}</legend>
<div class="field-section">
<span class="field-name">Attacker Movement :</span>
<select name="movement" data-tooltip-direction="UP">
{{selectOptions attackerMovementChoices selected=movement}}
</select>
</div>
<div class="field-section">
<span class="field-name">Range :</span>
<select name="range" data-tooltip-direction="UP">
{{selectOptions rangeChoices selected=range}}
</select>
</div>
<div class="field-section">
<span class="field-name">Target Size :</span>
<select name="size" data-tooltip-direction="UP">
{{selectOptions sizeChoices selected=size}}
</select>
</div>
<div class="field-section">
<span class="field-name">Aim :</span>
<select name="attackerAim" data-tooltip-direction="UP">
{{selectOptions attackerAimChoices selected=attackerAim}}
</select>
</div>
</fieldSet>
<fieldSet>
<legend>{{localize "LETHALFANTASY.Roll.visibility"}}</legend>
<span class="fieldset-centered">
<select name="visibility">
{{selectOptions rollModes selected=visibility localize=true}}
</select>
</span>
</fieldSet>
</div>
+1 -3
View File
@@ -55,8 +55,7 @@
<div class="dialog-save">Add Granted Attack Dice <div class="dialog-save">Add Granted Attack Dice
<input type="checkbox" data-action="selectGranted" name="granted" /> <input type="checkbox" data-action="selectGranted" name="granted" />
</div> </div>
{{#if rollTarget.weapon}} {{#if isRangedAttack}}
{{#if (eq rollTarget.weapon.system.weaponType "melee")}}{{else}}
<div class="dialog-save">Point Blank Range Attack <div class="dialog-save">Point Blank Range Attack
<input <input
type="checkbox" type="checkbox"
@@ -84,7 +83,6 @@
</select> </select>
</div> </div>
{{/if}} {{/if}}
{{/if}}
{{/if}} {{/if}}
{{#if (match rollType "defense")}} {{#if (match rollType "defense")}}