feat: luck bonus, inventory slots bonus, multi-enhancements, magic skill modifier
Release Creation / build (release) Successful in 1m25s

- Add luck.bonus field to character DataModel; included in prepareDerivedData
   (luck.max = fate.rank + bonus); shown as editable +bonus field next to
   luck max in edit mode (same UX as grit bonus)

 - Add inventory.slotsBonus field to character DataModel; Equipment tab now
   shows an editable "Bonus Slots" input that adds to the calculated max slots
   (10 + Might×2 + bonus)

 - Replace single Enhancement <select> in spell cast dialog with a checkbox
   list; multiple enhancements can now be selected simultaneously — stress
   costs, pool penalties, and boolean flags (redDice, noStress) are aggregated
   across all active enhancements

 - Include skills.magic.modifier in basePool for both spell and miracle dialogs
   and in baseDice in rollSpellCast / rollMiracleCast; modifier is shown in
   the dialog pool-info line and in the chat card when non-zero

 - Fix: pool-reduction indicator in rollSpellCast now compares against
   intRank + magicRank + magicMod (was missing magicMod)
This commit is contained in:
2026-05-12 08:16:57 +02:00
parent f67d9079dd
commit 0381e8e024
21 changed files with 192 additions and 39 deletions
+11 -7
View File
@@ -463,7 +463,7 @@ export async function rollWeaponDamage(actor, weapon, options = {}) {
export async function rollSpellCast(actor, spell, options = {}) {
const {
dv = spell.system.difficultyValue,
enhancement = "none",
enhancements = ["none"],
stressCost = 0,
poolPenalty = 0,
redDice = false,
@@ -484,8 +484,9 @@ export async function rollSpellCast(actor, spell, options = {}) {
const intRank = actorSys.attributes.intelligence.rank
const magicRank = actorSys.skills.magic.rank
const magicMod = actorSys.skills.magic.modifier ?? 0
const luckDicePerPoint = luckIsHuman ? 3 : 2
const baseDice = intRank + magicRank + bonus + poolPenalty + elementalBonus + grimPenalty + (luckSpend * luckDicePerPoint)
const baseDice = intRank + magicRank + magicMod + bonus + poolPenalty + elementalBonus + grimPenalty + (luckSpend * luckDicePerPoint)
// poolSize: voluntary reduction (p.101) — clamped to [1, baseDice]
const totalDice = poolSize !== null
? Math.max(1, Math.min(poolSize + bonus + poolPenalty + elementalBonus + grimPenalty + (luckSpend * luckDicePerPoint), baseDice))
@@ -523,9 +524,11 @@ export async function rollSpellCast(actor, spell, options = {}) {
: game.i18n.localize("OATHHAMMER.Roll.Failure")
const modParts = []
if (poolSize !== null && poolSize < intRank + magicRank) modParts.push(`🎲 ${poolSize}d ${game.i18n.localize("OATHHAMMER.Dialog.PoolSizeReduced")}`)
if (poolSize !== null && poolSize < intRank + magicRank + magicMod) modParts.push(`🎲 ${poolSize}d ${game.i18n.localize("OATHHAMMER.Dialog.PoolSizeReduced")}`)
if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
if (poolPenalty !== 0) modParts.push(`${poolPenalty} ${game.i18n.localize("OATHHAMMER.Enhancement." + _cap(enhancement))}`)
const activeEnhs = enhancements.filter(k => k !== "none")
if (poolPenalty !== 0 || (activeEnhs.length && !poolPenalty))
modParts.push(`${poolPenalty !== 0 ? poolPenalty + "d " : ""}[${activeEnhs.join(", ")}]`)
if (elementalBonus > 0) modParts.push(`+${elementalBonus} ${game.i18n.localize("OATHHAMMER.Dialog.ElementMet")}`)
if (grimPenalty < 0) modParts.push(`${grimPenalty} ${game.i18n.localize("OATHHAMMER.Dialog.GrimoireNo")}`)
if (luckSpend > 0) modParts.push(`+${luckSpend * luckDicePerPoint} ${game.i18n.localize("OATHHAMMER.Dialog.LuckSpend")} (${luckSpend}LP${luckIsHuman ? " 👤" : ""})`)
@@ -547,7 +550,7 @@ export async function rollSpellCast(actor, spell, options = {}) {
<span>${spell.name} (DV ${dv})</span>
</div>
<div class="oh-roll-info">
<span>${attrLabel} ${intRank} + ${skillLabel} ${magicRank}</span>
<span>${attrLabel} ${intRank} + ${skillLabel} ${magicRank}${magicMod !== 0 ? ` ${magicMod > 0 ? "+" : ""}${magicMod}` : ""}</span>
<span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
</div>
${modLine}
@@ -603,8 +606,9 @@ export async function rollMiracleCast(actor, miracle, options = {}) {
const wpRank = actorSys.attributes.willpower.rank
const magicRank = actorSys.skills.magic.rank
const magicMod = actorSys.skills.magic.modifier ?? 0
const luckDicePerPoint = luckIsHuman ? 3 : 2
const totalDice = Math.max(wpRank + magicRank + bonus + (luckSpend * luckDicePerPoint), 1)
const totalDice = Math.max(wpRank + magicRank + magicMod + bonus + (luckSpend * luckDicePerPoint), 1)
const threshold = colorOverride === "black" ? 2 : colorOverride === "red" ? 3 : 4
const colorEmoji = colorOverride === "black" ? "⬛" : colorOverride === "red" ? "🔴" : "⬜"
@@ -646,7 +650,7 @@ export async function rollMiracleCast(actor, miracle, options = {}) {
<span>${miracle.name} (${dvNote})</span>
</div>
<div class="oh-roll-info">
<span>${attrLabel} ${wpRank} + ${skillLabel} ${magicRank}</span>
<span>${attrLabel} ${wpRank} + ${skillLabel} ${magicRank}${magicMod !== 0 ? ` ${magicMod > 0 ? "+" : ""}${magicMod}` : ""}</span>
<span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
</div>
${modLine}