Compare commits

...

4 Commits

Author SHA1 Message Date
uberwald 3b0d4e032e fix: cache results.length before explosion loop to prevent double-count
Release Creation / build (release) Successful in 50s
Pushing explosion dice to DieTerm.results made the for loop
condition (j < results.length) grow mid-iteration, re-processing
the explosion result as a normal die. This produced spurious
entries like `1D6 → 4` alongside the correct `1D6-1 → 3`.

Fixes prompt() (for loop) and rollSpellDamageToMessage()
(for...of) by caching result count / snapshotting the array
before iterating.
2026-06-16 19:34:13 +02:00
uberwald 539841c4ff fix: push explosion dice to DieTerm results so DSN displays them
Explosion rolls were evaluated as separate Roll instances but never
added to the original DieTerm's results array. Dice So Nice reads
DieTerm.results to render 3D dice, so explosions were invisible.

Now each explosion result is pushed into the DieTerm's results array
({result, active:true}), letting DSN render explosion dice in the
correct chronological order alongside the main die.

Applies to prompt(), promptRangedDefense(), promptRangedAttack(),
and rollSpellDamageToMessage().
2026-06-16 19:29:07 +02:00
uberwald ffba37b59e Fix D30 management, again 2026-06-14 23:00:39 +02:00
uberwald 1a7585e1f6 fix: merge saving_throws D30 table into arcane_spell_defense
Release Creation / build (release) Successful in 49s
saving_throws was redundant — all saves in this system are
vs spells. Removed SAVING_THROWS constant; all save rollType
lookups use ARCANE_SPELL_DEFENSE. D30=1 arcane_spell_defense
blank (no special result). Added miracle types to ARCANE_SPELL_ATTACK
mapping so they get D30 results instead of null.
2026-06-14 22:57:41 +02:00
4 changed files with 71 additions and 116 deletions
+1 -1
View File
@@ -57,7 +57,7 @@ jobs:
token: ${{ secrets.FOUNDRY_PUBLISH_KEY }}
id: "fvtt-lethal-fantasy"
version: ${{github.event.release.tag_name}}
manifest: "https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{github.event.release.tag_name}}/system.json"
manifest: "https://www.uberwald.me/gitea/uberwald/fvtt-lethal-fantasy/releases/download/latest/system.json"
notes: "https://www.uberwald.me/gitea/public/fvtt-lethal-fantasy/raw/branch/main/changelog.md"
compatibility-minimum: "14"
compatibility-verified: "14"
+54 -107
View File
@@ -73,20 +73,6 @@
],
"description": "Possible Lethal or Vital Magical Strike or Add D20E to Spell Attack"
},
"arcane_spell_defense": {
"type": "choice",
"choices": [
{
"type": "spell_calamity"
},
{
"type": "bonus_dice",
"dice": "D20E",
"target": "spell_defense"
}
],
"description": "Possible Spell Catastrophe or adds D20E to Spell Defense"
},
"skill_rolls": {
"type": "skill_auto_success",
"description": "Skill Succeeds Regardless of Opposing Roll"
@@ -109,9 +95,19 @@
],
"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"
"arcane_spell_defense": {
"type": "choice",
"choices": [
{
"type": "spell_calamity"
},
{
"type": "bonus_dice",
"dice": "D20E",
"target": "spell_defense"
}
],
"description": "Possible Spell Catastrophe or adds D20E to Spell Defense"
}
},
"29": {
@@ -135,11 +131,6 @@
"amount": 1,
"description": "Gain 1 Grit"
},
"arcane_spell_defense": {
"type": "gain_grit",
"amount": 1,
"description": "Gain 1 Grit"
},
"skill_rolls": {
"type": "gain_grit",
"amount": 1,
@@ -150,7 +141,7 @@
"amount": 1,
"description": "Gain 1 Grit"
},
"saving_throws": {
"arcane_spell_defense": {
"type": "gain_grit",
"amount": 1,
"description": "Gain 1 Grit"
@@ -184,16 +175,16 @@
"type": "no_lethargy",
"description": "No Spell Lethargy the Aether Approves of Characters Efforts"
},
"ranged_defense": {
"type": "luck_die",
"scope": "combat",
"description": "Granted 1 Luck dice for Use in This Combat Only"
},
"arcane_spell_defense": {
"type": "flash_of_pain",
"duration_dice": "1D6E",
"target": "caster",
"description": "Caster Suffers Severe pain and will be under a flash of pain for 1D6E seconds"
},
"ranged_defense": {
"type": "luck_die",
"scope": "combat",
"description": "Granted 1 Luck dice for Use in This Combat Only"
}
},
"26": {
@@ -208,12 +199,6 @@
"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,12 +224,6 @@
"target": "defender",
"description": "Magical Damage inflicts Flash of pain 1D6E seconds"
},
"arcane_spell_defense": {
"type": "flash_of_pain",
"duration_dice": "1D6E",
"target": "caster",
"description": "Caster Suffers Severe pain and will be under a flash of pain for 1D6E seconds"
},
"skill_rolls": {
"type": "bonus_dice",
"dice": "D6",
@@ -255,11 +234,11 @@
"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"
"arcane_spell_defense": {
"type": "flash_of_pain",
"duration_dice": "1D6E",
"target": "caster",
"description": "Caster Suffers Severe pain and will be under a flash of pain for 1D6E seconds"
}
},
"20": {
@@ -331,23 +310,6 @@
],
"description": "Possible Vicious Application of a Magical Attack or add D12 to attack"
},
"arcane_spell_defense": {
"type": "choice",
"choices": [
{
"type": "special_defense",
"options": [
"perfect_spell"
]
},
{
"type": "bonus_dice",
"dice": "D12",
"target": "spell_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",
"amount": 20,
@@ -371,11 +333,22 @@
],
"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"
"arcane_spell_defense": {
"type": "choice",
"choices": [
{
"type": "special_defense",
"options": [
"perfect_spell"
]
},
{
"type": "bonus_dice",
"dice": "D12",
"target": "spell_defense"
}
],
"description": "Possible 20/20 Spell defense that Saves Against Any Magical Attack Except a Lethal Magical Strike or add D12 to spell defense"
}
},
"15": {
@@ -416,12 +389,6 @@
"punch"
],
"description": "Kick or Punch"
},
"saving_throws": {
"type": "bonus_flat",
"amount": 1,
"target": "save",
"description": "Add 1 to Saving Throw"
}
},
"13": {},
@@ -474,12 +441,6 @@
"punch"
],
"description": "Kick or Punch"
},
"saving_throws": {
"type": "bonus_flat",
"amount": 1,
"target": "save",
"description": "Add 1 to Saving Throw"
}
},
"8": {
@@ -499,10 +460,6 @@
"type": "mulligan",
"description": "Mulligan, Can Re-Roll This Spell Attack"
},
"arcane_spell_defense": {
"type": "mulligan",
"description": "Mulligan, Can Re-Roll This Spell Defense"
},
"skill_rolls": {
"type": "mulligan",
"description": "Mulligan, Can Re-Roll This Skill roll"
@@ -511,9 +468,9 @@
"type": "mulligan",
"description": "Mulligan, Can Choose to Re-Roll This Defense"
},
"saving_throws": {
"arcane_spell_defense": {
"type": "mulligan",
"description": "Mulligan, Can Re-Roll This Saving Throw"
"description": "Mulligan, Can Re-Roll This Spell Defense"
}
},
"7": {
@@ -565,12 +522,6 @@
"punch"
],
"description": "Kick or Punch"
},
"saving_throws": {
"type": "bonus_flat",
"amount": 1,
"target": "save",
"description": "Add 1 to Saving Throw"
}
},
"3": {
@@ -595,17 +546,17 @@
"multiplier": 3,
"description": "Triple Damage on Spell Damage"
},
"arcane_spell_defense": {
"type": "bonus_dice",
"dice": "D12",
"target": "spell_defense",
"description": "D12 Added to Spell Defense Modifier"
},
"ranged_defense": {
"type": "dr_multiplier",
"multiplier": 3,
"includes_shield": true,
"description": "DR Tripled including Shield"
},
"arcane_spell_defense": {
"type": "bonus_dice",
"dice": "D12",
"target": "spell_defense",
"description": "D12 Added to Spell Defense Modifier"
}
},
"2": {
@@ -630,17 +581,17 @@
"multiplier": 2,
"description": "Double Damage on Spell Damage"
},
"arcane_spell_defense": {
"type": "bonus_dice",
"dice": "D6",
"target": "spell_defense",
"description": "D6 Added to Spell Defense Modifier"
},
"ranged_defense": {
"type": "dr_multiplier",
"multiplier": 2,
"includes_shield": true,
"description": "DR Doubled including Shield"
},
"arcane_spell_defense": {
"type": "bonus_dice",
"dice": "D6",
"target": "spell_defense",
"description": "D6 Added to Spell Defense Modifier"
}
},
"1": {
@@ -653,10 +604,6 @@
"type": "spell_calamity",
"description": "A possible spell calamity has occurred"
},
"arcane_spell_defense": {
"type": "spell_calamity",
"description": "Possible Spell Calamity or Catastrophe"
},
"melee_attack": {
"type": "fumble",
"detail": "melee_fumble",
+5 -5
View File
@@ -27,8 +27,7 @@ export default class D30Roll {
RANGED_DEFENSE: "ranged_defense",
ARCANE_SPELL_ATTACK: "arcane_spell_attack",
ARCANE_SPELL_DEFENSE: "arcane_spell_defense",
SKILL_ROLLS: "skill_rolls",
SAVING_THROWS: "saving_throws"
SKILL_ROLLS: "skill_rolls"
}
/**
@@ -134,8 +133,9 @@ export default class D30Roll {
return options.isRanged ? this.ROLL_TYPES.RANGED_DEFENSE : this.ROLL_TYPES.MELEE_DEFENSE
}
// Spell types
if (externalType === "spell-attack" || externalType === "spell" || externalType === "spell-power") {
// Spell/Miracle types
if (externalType === "spell-attack" || externalType === "spell" || externalType === "spell-power"
|| externalType === "miracle-attack" || externalType === "miracle" || externalType === "miracle-power") {
return this.ROLL_TYPES.ARCANE_SPELL_ATTACK
}
@@ -146,7 +146,7 @@ export default class D30Roll {
// Saving throw types
if (externalType === "save") {
return options.isSpellSave ? this.ROLL_TYPES.ARCANE_SPELL_DEFENSE : this.ROLL_TYPES.SAVING_THROWS
return this.ROLL_TYPES.ARCANE_SPELL_DEFENSE
}
// If no match, return null
+11 -3
View File
@@ -616,8 +616,10 @@ export default class LethalFantasyRoll extends Roll {
let singleDice = `1D${maxValue}`
for (let i = 0; i < rollBase.dice.length; i++) {
for (let j = 0; j < rollBase.dice[i].results.length; j++) {
let diceResult = rollBase.dice[i].results[j].result
const dieResults = rollBase.dice[i].results
const resultCount = dieResults.length
for (let j = 0; j < resultCount; j++) {
let diceResult = dieResults[j].result
diceResults.push({ dice: `${singleDice.toUpperCase()}`, value: diceResult })
diceSum += diceResult
if (hasMaxValue) {
@@ -626,6 +628,8 @@ export default class LethalFantasyRoll extends Roll {
diceResult = r.dice[0].results[0].result
diceResults.push({ dice: `${singleDice.toUpperCase()}-1`, value: diceResult - 1 })
diceSum += (diceResult - 1)
// Add to DieTerm results so DSN/Foundry display shows explosion dice
dieResults.push({ result: diceResult, active: true })
}
}
}
@@ -1214,6 +1218,7 @@ export default class LethalFantasyRoll extends Roll {
diceResult = r.dice[0].results[0].result
diceResults.push({ dice: `${dice.toUpperCase()}-1`, value: diceResult - 1 })
diceSum += (diceResult - 1)
rollBase.dice[0].results.push({ result: diceResult, active: true })
}
if (fullModifier !== 0) {
diceResults.push({ dice: `${rollModifier.formula.toUpperCase()}`, value: rollModifier.total })
@@ -1376,6 +1381,7 @@ export default class LethalFantasyRoll extends Roll {
diceResult = r.dice[0].results[0].result
diceResults.push({ dice: `${dice.toUpperCase()}-1`, value: diceResult - 1 })
diceSum += (diceResult - 1)
rollBase.dice[0].results.push({ result: diceResult, active: true })
}
if (fullModifier !== 0) {
@@ -1588,7 +1594,8 @@ export default class LethalFantasyRoll extends Roll {
let diceSum = 0
for (const term of roll.dice) {
const singleDice = `1D${term.faces}`
for (const r of term.results) {
const termResults = Array.from(term.results)
for (const r of termResults) {
let diceResult = r.result
diceResults.push({ dice: singleDice.toUpperCase(), value: diceResult })
diceSum += diceResult
@@ -1599,6 +1606,7 @@ export default class LethalFantasyRoll extends Roll {
diceResult = xr.dice?.[0]?.results?.[0]?.result ?? (term.faces - 1)
diceResults.push({ dice: `${singleDice.toUpperCase()}-1`, value: diceResult - 1 })
diceSum += (diceResult - 1)
term.results.push({ result: diceResult, active: true })
}
}
}