feat: luck bonus, inventory slots bonus, multi-enhancements, magic skill modifier
Release Creation / build (release) Successful in 1m25s
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:
@@ -8,8 +8,10 @@ export default class OathHammerMiracleDialog {
|
||||
|
||||
const wpRank = actorSys.attributes.willpower.rank
|
||||
const magicRank = actorSys.skills.magic.rank
|
||||
const magicMod = actorSys.skills.magic.modifier ?? 0
|
||||
const magicColor = actorSys.skills.magic.colorDiceType ?? "white"
|
||||
const basePool = wpRank + magicRank
|
||||
const basePool = wpRank + magicRank + magicMod
|
||||
const magicModDisplay = magicMod > 0 ? `+${magicMod}` : magicMod < 0 ? `${magicMod}` : ""
|
||||
|
||||
const isRitual = sys.isRitual
|
||||
const dv = isRitual ? (sys.difficultyValue || 1) : null
|
||||
@@ -60,6 +62,8 @@ export default class OathHammerMiracleDialog {
|
||||
spellSave: sys.spellSave,
|
||||
wpRank,
|
||||
magicRank,
|
||||
magicMod,
|
||||
magicModDisplay,
|
||||
basePool,
|
||||
miracleCountOptions,
|
||||
colorOptions,
|
||||
|
||||
@@ -39,6 +39,7 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
|
||||
adjustCurrency: OathHammerCharacterSheet.#onAdjustCurrency,
|
||||
adjustLuck: OathHammerCharacterSheet.#onAdjustLuck,
|
||||
adjustGrit: OathHammerCharacterSheet.#onAdjustGrit,
|
||||
adjustStress: OathHammerCharacterSheet.#onAdjustStress,
|
||||
clearStress: OathHammerCharacterSheet.#onClearStress,
|
||||
},
|
||||
}
|
||||
@@ -190,8 +191,8 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
|
||||
}
|
||||
})
|
||||
context.ammunition = doc.itemTypes.ammunition
|
||||
// Slot tracking: max = 10 + (Might rank × 2); used = sum of all items' slots × quantity
|
||||
context.slotsMax = 10 + (doc.system.attributes.might.rank * 2)
|
||||
// Slot tracking: max = 10 + (Might rank × 2) + bonus; used = sum of all items' slots × quantity
|
||||
context.slotsMax = 10 + (doc.system.attributes.might.rank * 2) + (doc.system.inventory?.slotsBonus ?? 0)
|
||||
context.slotsUsed = doc.items.reduce((sum, item) => {
|
||||
const qty = item.system.quantity ?? 1
|
||||
return sum + (item.system.slots ?? 0) * Math.max(qty, 1)
|
||||
@@ -225,7 +226,7 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
|
||||
id: m.id, uuid: m.uuid, img: m.img, name: m.name, system: m.system,
|
||||
_descTooltip: _stripHtml(m.system.description)
|
||||
}))
|
||||
context.slotsMax = 10 + (doc.system.attributes.might.rank * 2)
|
||||
context.slotsMax = 10 + (doc.system.attributes.might.rank * 2) + (doc.system.inventory?.slotsBonus ?? 0)
|
||||
context.slotsUsed = doc.items.reduce((sum, item) => {
|
||||
const qty = item.system.quantity ?? 1
|
||||
return sum + (item.system.slots ?? 0) * Math.max(qty, 1)
|
||||
|
||||
@@ -27,8 +27,10 @@ export default class OathHammerSpellDialog {
|
||||
|
||||
const intRank = actorSys.attributes.intelligence.rank
|
||||
const magicRank = actorSys.skills.magic.rank
|
||||
const magicMod = actorSys.skills.magic.modifier ?? 0
|
||||
const magicColor = actorSys.skills.magic.colorDiceType ?? "white"
|
||||
const basePool = intRank + magicRank
|
||||
const basePool = intRank + magicRank + magicMod
|
||||
const magicModDisplay = magicMod > 0 ? `+${magicMod}` : magicMod < 0 ? `${magicMod}` : ""
|
||||
|
||||
const currentStress = actorSys.arcaneStress.value
|
||||
const stressThreshold = actorSys.arcaneStress.threshold
|
||||
@@ -48,11 +50,12 @@ export default class OathHammerSpellDialog {
|
||||
{ value: "black", label: game.i18n.localize("OATHHAMMER.ColorDice.Black"), selected: magicColor === "black" },
|
||||
]
|
||||
|
||||
const enhancementOptions = Object.entries(SPELL_ENHANCEMENTS).map(([key, def]) => ({
|
||||
value: key,
|
||||
label: game.i18n.localize(def.label),
|
||||
selected: key === "none",
|
||||
}))
|
||||
const enhancementOptions = Object.entries(SPELL_ENHANCEMENTS)
|
||||
.filter(([key]) => key !== "none")
|
||||
.map(([key, def]) => ({
|
||||
value: key,
|
||||
label: game.i18n.localize(def.label),
|
||||
}))
|
||||
|
||||
const bonusOptions = Array.from({ length: 13 }, (_, i) => {
|
||||
const v = i - 6
|
||||
@@ -92,6 +95,8 @@ export default class OathHammerSpellDialog {
|
||||
element: sys.element,
|
||||
intRank,
|
||||
magicRank,
|
||||
magicMod,
|
||||
magicModDisplay,
|
||||
basePool,
|
||||
poolSizeOptions,
|
||||
colorOptions,
|
||||
@@ -132,16 +137,25 @@ export default class OathHammerSpellDialog {
|
||||
|
||||
if (!result) return null
|
||||
|
||||
const enhKey = result.enhancement ?? "none"
|
||||
const enh = SPELL_ENHANCEMENTS[enhKey] ?? SPELL_ENHANCEMENTS.none
|
||||
// Collect all checked enhancements and aggregate their effects
|
||||
const selectedEnhs = Object.keys(SPELL_ENHANCEMENTS)
|
||||
.filter(k => k !== "none" && result[`enh_${k}`] === "true")
|
||||
const aggregated = selectedEnhs.reduce((acc, key) => {
|
||||
const def = SPELL_ENHANCEMENTS[key]
|
||||
acc.stress += def.stress
|
||||
acc.penalty += def.penalty
|
||||
if (def.redDice) acc.redDice = true
|
||||
if (def.noStress) acc.noStress = true
|
||||
return acc
|
||||
}, { stress: 0, penalty: 0, redDice: false, noStress: false })
|
||||
|
||||
return {
|
||||
dv,
|
||||
enhancement: enhKey,
|
||||
stressCost: enh.stress,
|
||||
poolPenalty: enh.penalty,
|
||||
redDice: enh.redDice,
|
||||
noStress: enh.noStress,
|
||||
enhancements: selectedEnhs.length ? selectedEnhs : ["none"],
|
||||
stressCost: aggregated.stress,
|
||||
poolPenalty: aggregated.penalty,
|
||||
redDice: aggregated.redDice,
|
||||
noStress: aggregated.noStress,
|
||||
colorOverride: result.colorOverride ?? magicColor,
|
||||
elementalBonus: parseInt(result.elementalBonus) || 0,
|
||||
bonus: parseInt(result.bonus) || 0,
|
||||
|
||||
@@ -75,7 +75,8 @@ export default class OathHammerCharacter extends foundry.abstract.TypeDataModel
|
||||
// Luck.max is derived from fate.rank; resets at session start.
|
||||
schema.luck = new fields.SchemaField({
|
||||
value: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }),
|
||||
max: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 })
|
||||
max: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }),
|
||||
bonus: new fields.NumberField({ ...requiredInteger, initial: 0 })
|
||||
})
|
||||
|
||||
schema.arcaneStress = new fields.SchemaField({
|
||||
@@ -119,6 +120,10 @@ export default class OathHammerCharacter extends foundry.abstract.TypeDataModel
|
||||
copper: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
|
||||
})
|
||||
|
||||
schema.inventory = new fields.SchemaField({
|
||||
slotsBonus: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
|
||||
})
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
@@ -128,8 +133,8 @@ export default class OathHammerCharacter extends foundry.abstract.TypeDataModel
|
||||
super.prepareDerivedData()
|
||||
// Grit max = Resilience skill rank + Toughness attribute rank + bonus (rulebook p.5)
|
||||
this.grit.max = this.skills.resilience.rank + this.attributes.toughness.rank + (this.grit.bonus ?? 0)
|
||||
// Luck max = Fate rank; restores at session start
|
||||
this.luck.max = this.attributes.fate.rank
|
||||
// Luck max = Fate rank + bonus; restores at session start
|
||||
this.luck.max = this.attributes.fate.rank + (this.luck.bonus ?? 0)
|
||||
// Defense score = 10 + Agility + Armor Rating + bonus
|
||||
this.defense.value = 10 + this.attributes.agility.rank + this.defense.armorRating + this.defense.bonus
|
||||
// Stress Threshold = Willpower rank + Magic rank + bonus (rulebook p.101)
|
||||
|
||||
+11
-7
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user