6 Commits

Author SHA1 Message Date
uberwald 0381e8e024 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)
2026-05-12 08:16:57 +02:00
uberwald f67d9079dd FIx for spells/miracles
Release Creation / build (release) Successful in 2m33s
2026-04-30 23:10:13 +02:00
uberwald ecc2c22372 Merge branch 'main' of https://www.uberwald.me/gitea/uberwald/fvtt-oath-hammer 2026-04-30 23:09:04 +02:00
uberwald 1641be6516 FIx for spells/miracles 2026-04-30 23:08:23 +02:00
uberwald ef41116006 Actualiser .gitea/workflows/release.yaml
Release Creation / build (release) Successful in 1m16s
2026-04-20 15:51:51 +02:00
uberwald 83566ba8fb Actualiser .gitea/workflows/release.yaml 2026-04-20 14:32:02 +02:00
22 changed files with 251 additions and 63 deletions
+13 -13
View File
@@ -26,8 +26,8 @@ jobs:
env: env:
version: ${{steps.get_version.outputs.version-without-v}} version: ${{steps.get_version.outputs.version-without-v}}
url: https://www.uberwald.me/gitea/${{gitea.repository}} url: https://www.uberwald.me/gitea/${{gitea.repository}}
manifest: https://www.uberwald.me/gitea/public/fvtt-oath-hammer/releases/download/latest/system.json manifest: https://www.uberwald.me/gitea/uberwald/fvtt-oath-hammer/releases/download/latest/system.json
download: https://www.uberwald.me/gitea/public/fvtt-oath-hammer/releases/download/${{github.event.release.tag_name}}/fvtt-oath-hammer.zip download: https://www.uberwald.me/gitea/uberwald/fvtt-oath-hammer/releases/download/${{github.event.release.tag_name}}/fvtt-oath-hammer.zip
# Build CSS from LESS sources # Build CSS from LESS sources
- name: Setup Node.js - name: Setup Node.js
@@ -45,7 +45,7 @@ jobs:
apt update -y apt update -y
apt install -y zip apt install -y zip
- run: zip -r ./fvtt-oath-hammer.zip system.json README.md LICENSE.md assets/ css/ lang/ module/ oath-hammer.mjs templates/ - run: zip -r ./fvtt-oath-hammer.zip system.json README.md LICENSE.md assets/ css/ lang/ module/ oath-hammer.mjs packs/ templates/
- name: setup go - name: setup go
uses: https://github.com/actions/setup-go@v4 uses: https://github.com/actions/setup-go@v4
@@ -61,13 +61,13 @@ jobs:
system.json system.json
api_key: "${{secrets.ALLOW_PUSH_RELEASE}}" api_key: "${{secrets.ALLOW_PUSH_RELEASE}}"
#- name: Publish to Foundry server - name: Publish to Foundry server
#uses: https://github.com/djlechuck/foundryvtt-publish-package-action@v1 uses: https://github.com/djlechuck/foundryvtt-publish-package-action@v1
#with: with:
#token: ${{ secrets.FOUNDRYVTT_RELEASE_TOKEN }} token: ${{ secrets.FOUNDRYVTT_RELEASE_TOKEN }}
#id: 'fvtt-oath-hammer' id: "fvtt-oath-hammer"
#version: ${{github.event.release.tag_name}} version: ${{github.event.release.tag_name}}
#manifest: 'https://www.uberwald.me/gitea/public/fvtt-oath-hammer/releases/download/latest/system.json' manifest: "https://www.uberwald.me/gitea/uberwald/fvtt-oath-hammer/releases/download/latest/system.json"
#notes: 'https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{github.event.release.tag_name}}/fvtt-oath-hammer.zip' notes: "https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{github.event.release.tag_name}}/fvtt-oath-hammer.zip"
#compatibility-minimum: '13' compatibility-minimum: "13"
#compatibility-verified: '14' compatibility-verified: "14"
+57 -3
View File
@@ -565,16 +565,19 @@
opacity: 0.7; opacity: 0.7;
font-size: calc(0.86rem * 0.9); font-size: calc(0.86rem * 0.9);
} }
.oathhammer .character-main .character-stats-band .character-resources .character-resource .grit-max-group { .oathhammer .character-main .character-stats-band .character-resources .character-resource .grit-max-group,
.oathhammer .character-main .character-stats-band .character-resources .character-resource .luck-max-group {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 2px; gap: 2px;
} }
.oathhammer .character-main .character-stats-band .character-resources .character-resource .grit-max-group .res-bonus-label { .oathhammer .character-main .character-stats-band .character-resources .character-resource .grit-max-group .res-bonus-label,
.oathhammer .character-main .character-stats-band .character-resources .character-resource .luck-max-group .res-bonus-label {
opacity: 0.6; opacity: 0.6;
font-size: calc(0.86rem * 0.9); font-size: calc(0.86rem * 0.9);
} }
.oathhammer .character-main .character-stats-band .character-resources .character-resource .grit-max-group .res-bonus-input { .oathhammer .character-main .character-stats-band .character-resources .character-resource .grit-max-group .res-bonus-input,
.oathhammer .character-main .character-stats-band .character-resources .character-resource .luck-max-group .res-bonus-input {
width: 2.2rem; width: 2.2rem;
opacity: 0.85; opacity: 0.85;
border-left: 1px dashed #535128; border-left: 1px dashed #535128;
@@ -1327,6 +1330,7 @@
} }
.slots-counter { .slots-counter {
display: flex; display: flex;
align-items: center;
gap: 6px; gap: 6px;
padding: 2px 6px 4px; padding: 2px 6px 4px;
} }
@@ -1353,6 +1357,20 @@
background: rgba(192, 57, 43, 0.1); background: rgba(192, 57, 43, 0.1);
border-color: rgba(192, 57, 43, 0.4); border-color: rgba(192, 57, 43, 0.4);
} }
.slots-counter .slots-bonus-label {
font-size: calc(0.86rem * 0.9);
color: rgba(42, 26, 10, 0.6);
margin-left: 6px;
}
.slots-counter .slots-bonus-input {
width: 3rem;
font-size: calc(0.86rem * 0.9);
text-align: center;
padding: 1px 4px;
border: 1px solid rgba(200, 168, 75, 0.4);
border-radius: 4px;
background: rgba(200, 168, 75, 0.08);
}
.oathhammer .item-list--regiment .item-list-header, .oathhammer .item-list--regiment .item-list-header,
.oathhammer .item-list--regiment .item-entry { .oathhammer .item-list--regiment .item-entry {
grid-template-columns: 24px 1fr 4rem 5rem 4rem 4.5rem; grid-template-columns: 24px 1fr 4rem 5rem 4rem 4.5rem;
@@ -2360,6 +2378,42 @@
.oh-miracle-dialog select.enhancement-select { .oh-miracle-dialog select.enhancement-select {
min-width: 220px; min-width: 220px;
} }
.oh-spell-dialog .roll-option-enhancements,
.oh-miracle-dialog .roll-option-enhancements {
align-items: flex-start;
}
.oh-spell-dialog .roll-option-enhancements .enhancements-header,
.oh-miracle-dialog .roll-option-enhancements .enhancements-header {
padding-top: 3px;
flex-shrink: 0;
}
.oh-spell-dialog .roll-option-enhancements .enhancements-list,
.oh-miracle-dialog .roll-option-enhancements .enhancements-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.oh-spell-dialog .roll-option-enhancements .enhancement-item,
.oh-miracle-dialog .roll-option-enhancements .enhancement-item {
display: flex;
align-items: center;
gap: 6px;
}
.oh-spell-dialog .roll-option-enhancements .enhancement-item input[type="checkbox"],
.oh-miracle-dialog .roll-option-enhancements .enhancement-item input[type="checkbox"] {
flex-shrink: 0;
width: 14px;
height: 14px;
cursor: pointer;
}
.oh-spell-dialog .roll-option-enhancements .enhancement-item .enh-name,
.oh-miracle-dialog .roll-option-enhancements .enhancement-item .enh-name {
font-size: calc(0.86rem * 0.85);
color: #2a1a0a;
cursor: pointer;
margin: 0;
font-weight: normal;
}
.oh-spell-card .oh-stress-line, .oh-spell-card .oh-stress-line,
.oh-miracle-card .oh-stress-line { .oh-miracle-card .oh-stress-line {
margin-top: 6px; margin-top: 6px;
+3 -2
View File
@@ -250,9 +250,10 @@
"Oaths": "Oaths", "Oaths": "Oaths",
"Weapons": "Weapons", "Weapons": "Weapons",
"Attacks": "Attacks", "Attacks": "Attacks",
"Armor": "Armor & Shields", "Armor": "Armor",
"Ammunition": "Ammunition", "Ammunition": "Ammunition",
"ItemSlots": "Item Slots", "ItemSlots": "Item Slots",
"SlotsBonus": "Bonus Slots",
"Spells": "Spells", "Spells": "Spells",
"Miracles": "Miracles", "Miracles": "Miracles",
"Equipment": "Equipment", "Equipment": "Equipment",
@@ -444,7 +445,7 @@
"ExplodeOn5": "Explode on 5+", "ExplodeOn5": "Explode on 5+",
"ExplodeOn5Hint": "trait bonus — 5s & 6s explode", "ExplodeOn5Hint": "trait bonus — 5s & 6s explode",
"DiceColor": "Dice Color", "DiceColor": "Dice Color",
"DiceColorHint": "equipment traits can upgrade dice", "DiceColorHint": "White (4+), Red (3+), or Black (2+) thresholds",
"AttackTitle": "Attack: {weapon}", "AttackTitle": "Attack: {weapon}",
"DamageTitle": "Damage: {weapon}", "DamageTitle": "Damage: {weapon}",
"Attack": "Attack", "Attack": "Attack",
+2 -1
View File
@@ -169,7 +169,8 @@
.res-sep { opacity: 0.7; font-size: @font-size-xs; } .res-sep { opacity: 0.7; font-size: @font-size-xs; }
.grit-max-group { .grit-max-group,
.luck-max-group {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 2px; gap: 2px;
+17
View File
@@ -302,6 +302,7 @@
// Slots counter on the Combat tab // Slots counter on the Combat tab
.slots-counter { .slots-counter {
display: flex; display: flex;
align-items: center;
gap: 6px; gap: 6px;
padding: 2px 6px 4px; padding: 2px 6px 4px;
@@ -330,6 +331,22 @@
border-color: fade(#c0392b, 40%); border-color: fade(#c0392b, 40%);
} }
} }
.slots-bonus-label {
font-size: @font-size-xs;
color: fade(@color-dark, 60%);
margin-left: 6px;
}
.slots-bonus-input {
width: 3rem;
font-size: @font-size-xs;
text-align: center;
padding: 1px 4px;
border: 1px solid fade(@color-gold, 40%);
border-radius: 4px;
background: fade(@color-gold, 8%);
}
} }
// ── Regiment list (settlement garrison tab) ────────────────────────────────── // ── Regiment list (settlement garrison tab) ──────────────────────────────────
+38 -1
View File
@@ -553,10 +553,47 @@
i { flex-shrink: 0; } i { flex-shrink: 0; }
} }
// Wide select for enhancement list // Wide select for enhancement list (legacy, kept for miracle dialog)
select.enhancement-select { select.enhancement-select {
min-width: 220px; min-width: 220px;
} }
// Multi-enhancement checkbox list
.roll-option-enhancements {
align-items: flex-start;
.enhancements-header {
padding-top: 3px;
flex-shrink: 0;
}
.enhancements-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.enhancement-item {
display: flex;
align-items: center;
gap: 6px;
input[type="checkbox"] {
flex-shrink: 0;
width: 14px;
height: 14px;
cursor: pointer;
}
.enh-name {
font-size: @font-size-sm;
color: @color-dark;
cursor: pointer;
margin: 0;
font-weight: normal;
}
}
}
} }
// Chat card additions for spell/miracle // Chat card additions for spell/miracle
+16 -3
View File
@@ -6,9 +6,12 @@ export default class OathHammerMiracleDialog {
const sys = miracle.system const sys = miracle.system
const actorSys = actor.system const actorSys = actor.system
const wpRank = actorSys.attributes.willpower.rank const wpRank = actorSys.attributes.willpower.rank
const magicRank = actorSys.skills.magic.rank const magicRank = actorSys.skills.magic.rank
const basePool = wpRank + magicRank const magicMod = actorSys.skills.magic.modifier ?? 0
const magicColor = actorSys.skills.magic.colorDiceType ?? "white"
const basePool = wpRank + magicRank + magicMod
const magicModDisplay = magicMod > 0 ? `+${magicMod}` : magicMod < 0 ? `${magicMod}` : ""
const isRitual = sys.isRitual const isRitual = sys.isRitual
const dv = isRitual ? (sys.difficultyValue || 1) : null const dv = isRitual ? (sys.difficultyValue || 1) : null
@@ -18,6 +21,12 @@ export default class OathHammerMiracleDialog {
return key ? game.i18n.localize(key) : sys.divineTradition return key ? game.i18n.localize(key) : sys.divineTradition
})() })()
const colorOptions = [
{ value: "white", label: game.i18n.localize("OATHHAMMER.ColorDice.White"), selected: magicColor === "white" },
{ value: "red", label: game.i18n.localize("OATHHAMMER.ColorDice.Red"), selected: magicColor === "red" },
{ value: "black", label: game.i18n.localize("OATHHAMMER.ColorDice.Black"), selected: magicColor === "black" },
]
// Miracle count options — DV = miracle number today // Miracle count options — DV = miracle number today
const miracleCountOptions = Array.from({ length: 10 }, (_, i) => ({ const miracleCountOptions = Array.from({ length: 10 }, (_, i) => ({
value: i + 1, value: i + 1,
@@ -53,8 +62,11 @@ export default class OathHammerMiracleDialog {
spellSave: sys.spellSave, spellSave: sys.spellSave,
wpRank, wpRank,
magicRank, magicRank,
magicMod,
magicModDisplay,
basePool, basePool,
miracleCountOptions, miracleCountOptions,
colorOptions,
bonusOptions, bonusOptions,
availableLuck, availableLuck,
isHuman, isHuman,
@@ -93,6 +105,7 @@ export default class OathHammerMiracleDialog {
return { return {
dv: computedDV, dv: computedDV,
isRitual, isRitual,
colorOverride: result.colorOverride ?? magicColor,
bonus: parseInt(result.bonus) || 0, bonus: parseInt(result.bonus) || 0,
visibility: result.visibility ?? game.settings.get("core", "rollMode"), visibility: result.visibility ?? game.settings.get("core", "rollMode"),
explodeOn5: result.explodeOn5 === "true", explodeOn5: result.explodeOn5 === "true",
@@ -39,6 +39,7 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
adjustCurrency: OathHammerCharacterSheet.#onAdjustCurrency, adjustCurrency: OathHammerCharacterSheet.#onAdjustCurrency,
adjustLuck: OathHammerCharacterSheet.#onAdjustLuck, adjustLuck: OathHammerCharacterSheet.#onAdjustLuck,
adjustGrit: OathHammerCharacterSheet.#onAdjustGrit, adjustGrit: OathHammerCharacterSheet.#onAdjustGrit,
adjustStress: OathHammerCharacterSheet.#onAdjustStress,
clearStress: OathHammerCharacterSheet.#onClearStress, clearStress: OathHammerCharacterSheet.#onClearStress,
}, },
} }
@@ -190,8 +191,8 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
} }
}) })
context.ammunition = doc.itemTypes.ammunition context.ammunition = doc.itemTypes.ammunition
// Slot tracking: max = 10 + (Might rank × 2); used = sum of all items' slots × quantity // Slot tracking: max = 10 + (Might rank × 2) + bonus; used = sum of all items' slots × quantity
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) => { context.slotsUsed = doc.items.reduce((sum, item) => {
const qty = item.system.quantity ?? 1 const qty = item.system.quantity ?? 1
return sum + (item.system.slots ?? 0) * Math.max(qty, 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, id: m.id, uuid: m.uuid, img: m.img, name: m.name, system: m.system,
_descTooltip: _stripHtml(m.system.description) _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) => { context.slotsUsed = doc.items.reduce((sum, item) => {
const qty = item.system.quantity ?? 1 const qty = item.system.quantity ?? 1
return sum + (item.system.slots ?? 0) * Math.max(qty, 1) return sum + (item.system.slots ?? 0) * Math.max(qty, 1)
+38 -15
View File
@@ -25,9 +25,12 @@ export default class OathHammerSpellDialog {
const sys = spell.system const sys = spell.system
const actorSys = actor.system const actorSys = actor.system
const intRank = actorSys.attributes.intelligence.rank const intRank = actorSys.attributes.intelligence.rank
const magicRank = actorSys.skills.magic.rank const magicRank = actorSys.skills.magic.rank
const basePool = intRank + magicRank const magicMod = actorSys.skills.magic.modifier ?? 0
const magicColor = actorSys.skills.magic.colorDiceType ?? "white"
const basePool = intRank + magicRank + magicMod
const magicModDisplay = magicMod > 0 ? `+${magicMod}` : magicMod < 0 ? `${magicMod}` : ""
const currentStress = actorSys.arcaneStress.value const currentStress = actorSys.arcaneStress.value
const stressThreshold = actorSys.arcaneStress.threshold const stressThreshold = actorSys.arcaneStress.threshold
@@ -41,11 +44,18 @@ export default class OathHammerSpellDialog {
return entry ? game.i18n.localize(entry.label) : (sys.tradition ?? "") return entry ? game.i18n.localize(entry.label) : (sys.tradition ?? "")
})() })()
const enhancementOptions = Object.entries(SPELL_ENHANCEMENTS).map(([key, def]) => ({ const colorOptions = [
value: key, { value: "white", label: game.i18n.localize("OATHHAMMER.ColorDice.White"), selected: magicColor === "white" },
label: game.i18n.localize(def.label), { value: "red", label: game.i18n.localize("OATHHAMMER.ColorDice.Red"), selected: magicColor === "red" },
selected: key === "none", { value: "black", label: game.i18n.localize("OATHHAMMER.ColorDice.Black"), selected: magicColor === "black" },
})) ]
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 bonusOptions = Array.from({ length: 13 }, (_, i) => {
const v = i - 6 const v = i - 6
@@ -85,8 +95,11 @@ export default class OathHammerSpellDialog {
element: sys.element, element: sys.element,
intRank, intRank,
magicRank, magicRank,
magicMod,
magicModDisplay,
basePool, basePool,
poolSizeOptions, poolSizeOptions,
colorOptions,
currentStress, currentStress,
stressThreshold, stressThreshold,
isOverThreshold, isOverThreshold,
@@ -124,16 +137,26 @@ export default class OathHammerSpellDialog {
if (!result) return null if (!result) return null
const enhKey = result.enhancement ?? "none" // Collect all checked enhancements and aggregate their effects
const enh = SPELL_ENHANCEMENTS[enhKey] ?? SPELL_ENHANCEMENTS.none 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 { return {
dv, dv,
enhancement: enhKey, enhancements: selectedEnhs.length ? selectedEnhs : ["none"],
stressCost: enh.stress, stressCost: aggregated.stress,
poolPenalty: enh.penalty, poolPenalty: aggregated.penalty,
redDice: enh.redDice, redDice: aggregated.redDice,
noStress: enh.noStress, noStress: aggregated.noStress,
colorOverride: result.colorOverride ?? magicColor,
elementalBonus: parseInt(result.elementalBonus) || 0, elementalBonus: parseInt(result.elementalBonus) || 0,
bonus: parseInt(result.bonus) || 0, bonus: parseInt(result.bonus) || 0,
poolSize: Math.min(Math.max(1, parseInt(result.poolSize) || basePool), basePool), poolSize: Math.min(Math.max(1, parseInt(result.poolSize) || basePool), basePool),
+8 -3
View File
@@ -75,7 +75,8 @@ export default class OathHammerCharacter extends foundry.abstract.TypeDataModel
// Luck.max is derived from fate.rank; resets at session start. // Luck.max is derived from fate.rank; resets at session start.
schema.luck = new fields.SchemaField({ schema.luck = new fields.SchemaField({
value: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }), 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({ 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 }) 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 return schema
} }
@@ -128,8 +133,8 @@ export default class OathHammerCharacter extends foundry.abstract.TypeDataModel
super.prepareDerivedData() super.prepareDerivedData()
// Grit max = Resilience skill rank + Toughness attribute rank + bonus (rulebook p.5) // 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) this.grit.max = this.skills.resilience.rank + this.attributes.toughness.rank + (this.grit.bonus ?? 0)
// Luck max = Fate rank; restores at session start // Luck max = Fate rank + bonus; restores at session start
this.luck.max = this.attributes.fate.rank this.luck.max = this.attributes.fate.rank + (this.luck.bonus ?? 0)
// Defense score = 10 + Agility + Armor Rating + bonus // Defense score = 10 + Agility + Armor Rating + bonus
this.defense.value = 10 + this.attributes.agility.rank + this.defense.armorRating + this.defense.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) // Stress Threshold = Willpower rank + Magic rank + bonus (rulebook p.101)
+19 -14
View File
@@ -463,11 +463,12 @@ export async function rollWeaponDamage(actor, weapon, options = {}) {
export async function rollSpellCast(actor, spell, options = {}) { export async function rollSpellCast(actor, spell, options = {}) {
const { const {
dv = spell.system.difficultyValue, dv = spell.system.difficultyValue,
enhancement = "none", enhancements = ["none"],
stressCost = 0, stressCost = 0,
poolPenalty = 0, poolPenalty = 0,
redDice = false, redDice = false,
noStress = false, noStress = false,
colorOverride = "white",
elementalBonus = 0, elementalBonus = 0,
bonus = 0, bonus = 0,
poolSize = null, poolSize = null,
@@ -483,14 +484,15 @@ export async function rollSpellCast(actor, spell, options = {}) {
const intRank = actorSys.attributes.intelligence.rank const intRank = actorSys.attributes.intelligence.rank
const magicRank = actorSys.skills.magic.rank const magicRank = actorSys.skills.magic.rank
const magicMod = actorSys.skills.magic.modifier ?? 0
const luckDicePerPoint = luckIsHuman ? 3 : 2 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] // poolSize: voluntary reduction (p.101) — clamped to [1, baseDice]
const totalDice = poolSize !== null const totalDice = poolSize !== null
? Math.max(1, Math.min(poolSize + bonus + poolPenalty + elementalBonus + grimPenalty + (luckSpend * luckDicePerPoint), baseDice)) ? Math.max(1, Math.min(poolSize + bonus + poolPenalty + elementalBonus + grimPenalty + (luckSpend * luckDicePerPoint), baseDice))
: Math.max(baseDice, 1) : Math.max(baseDice, 1)
const threshold = redDice ? 3 : 4 const threshold = colorOverride === "black" ? 2 : colorOverride === "red" ? 3 : 4
const colorEmoji = redDice ? "🔴" : "⬜" const colorEmoji = colorOverride === "black" ? "⬛" : colorOverride === "red" ? "🔴" : "⬜"
const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5) const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5)
const diceHtml = _diceHtml(diceResults, threshold) const diceHtml = _diceHtml(diceResults, threshold)
@@ -522,9 +524,11 @@ export async function rollSpellCast(actor, spell, options = {}) {
: game.i18n.localize("OATHHAMMER.Roll.Failure") : game.i18n.localize("OATHHAMMER.Roll.Failure")
const modParts = [] 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 (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 (elementalBonus > 0) modParts.push(`+${elementalBonus} ${game.i18n.localize("OATHHAMMER.Dialog.ElementMet")}`)
if (grimPenalty < 0) modParts.push(`${grimPenalty} ${game.i18n.localize("OATHHAMMER.Dialog.GrimoireNo")}`) 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 ? " 👤" : ""})`) if (luckSpend > 0) modParts.push(`+${luckSpend * luckDicePerPoint} ${game.i18n.localize("OATHHAMMER.Dialog.LuckSpend")} (${luckSpend}LP${luckIsHuman ? " 👤" : ""})`)
@@ -546,7 +550,7 @@ export async function rollSpellCast(actor, spell, options = {}) {
<span>${spell.name} (DV ${dv})</span> <span>${spell.name} (DV ${dv})</span>
</div> </div>
<div class="oh-roll-info"> <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> <span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
</div> </div>
${modLine} ${modLine}
@@ -560,13 +564,12 @@ export async function rollSpellCast(actor, spell, options = {}) {
` `
const rollMode = visibility ?? game.settings.get("core", "rollMode") const rollMode = visibility ?? game.settings.get("core", "rollMode")
const spellColorType = redDice ? "red" : "white"
const msgData = { const msgData = {
speaker: ChatMessage.getSpeaker({ actor }), speaker: ChatMessage.getSpeaker({ actor }),
content, content,
rolls: rolls, rolls: rolls,
sound: CONFIG.sounds.dice, sound: CONFIG.sounds.dice,
flags: { "fvtt-oath-hammer": { luckRoll: _luckFlagData(actor, threshold, spellColorType, dv, false, explodeOn5) } }, flags: { "fvtt-oath-hammer": { luckRoll: _luckFlagData(actor, threshold, colorOverride, dv, false, explodeOn5) } },
} }
ChatMessage.applyRollMode(msgData, rollMode) ChatMessage.applyRollMode(msgData, rollMode)
await ChatMessage.create(msgData) await ChatMessage.create(msgData)
@@ -590,6 +593,7 @@ export async function rollMiracleCast(actor, miracle, options = {}) {
const { const {
dv = 1, dv = 1,
isRitual = false, isRitual = false,
colorOverride = "white",
bonus = 0, bonus = 0,
visibility, visibility,
explodeOn5 = false, explodeOn5 = false,
@@ -602,10 +606,11 @@ export async function rollMiracleCast(actor, miracle, options = {}) {
const wpRank = actorSys.attributes.willpower.rank const wpRank = actorSys.attributes.willpower.rank
const magicRank = actorSys.skills.magic.rank const magicRank = actorSys.skills.magic.rank
const magicMod = actorSys.skills.magic.modifier ?? 0
const luckDicePerPoint = luckIsHuman ? 3 : 2 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 = 4 const threshold = colorOverride === "black" ? 2 : colorOverride === "red" ? 3 : 4
const colorEmoji = "⬜" const colorEmoji = colorOverride === "black" ? "⬛" : colorOverride === "red" ? "🔴" : "⬜"
const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5) const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5)
const diceHtml = _diceHtml(diceResults, threshold) const diceHtml = _diceHtml(diceResults, threshold)
@@ -645,7 +650,7 @@ export async function rollMiracleCast(actor, miracle, options = {}) {
<span>${miracle.name} (${dvNote})</span> <span>${miracle.name} (${dvNote})</span>
</div> </div>
<div class="oh-roll-info"> <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> <span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
</div> </div>
${modLine} ${modLine}
@@ -664,7 +669,7 @@ export async function rollMiracleCast(actor, miracle, options = {}) {
content, content,
rolls: rolls, rolls: rolls,
sound: CONFIG.sounds.dice, sound: CONFIG.sounds.dice,
flags: { "fvtt-oath-hammer": { luckRoll: _luckFlagData(actor, threshold, "white", dv, false, explodeOn5) } }, flags: { "fvtt-oath-hammer": { luckRoll: _luckFlagData(actor, threshold, colorOverride, dv, false, explodeOn5) } },
} }
ChatMessage.applyRollMode(msgData, rollMode) ChatMessage.applyRollMode(msgData, rollMode)
await ChatMessage.create(msgData) await ChatMessage.create(msgData)
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000021 MANIFEST-000030
Binary file not shown.
View File
View File
+2
View File
@@ -2,6 +2,8 @@
<div class="slots-counter"> <div class="slots-counter">
<span class="slots-label">{{localize "OATHHAMMER.Label.ItemSlots"}}</span> <span class="slots-label">{{localize "OATHHAMMER.Label.ItemSlots"}}</span>
<span class="slots-value {{#if slotsOver}}slots-over{{/if}}">{{slotsUsed}} / {{slotsMax}}</span> <span class="slots-value {{#if slotsOver}}slots-over{{/if}}">{{slotsUsed}} / {{slotsMax}}</span>
<span class="slots-bonus-label">{{localize "OATHHAMMER.Label.SlotsBonus"}}</span>
<input type="number" name="system.inventory.slotsBonus" value="{{system.inventory.slotsBonus}}" class="slots-bonus-input">
</div> </div>
<fieldset class="currency-bar"> <fieldset class="currency-bar">
<legend>{{localize "OATHHAMMER.Label.Currency"}}</legend> <legend>{{localize "OATHHAMMER.Label.Currency"}}</legend>
+7 -1
View File
@@ -72,7 +72,13 @@
<a data-action="adjustLuck" data-delta="1" class="luck-btn">+</a> <a data-action="adjustLuck" data-delta="1" class="luck-btn">+</a>
</div> </div>
<span class="res-sep">/</span> <span class="res-sep">/</span>
{{formInput systemFields.luck.fields.max value=system.luck.max name="system.luck.max" disabled=isPlayMode}} <span class="luck-max-group">
{{formInput systemFields.luck.fields.max value=system.luck.max name="system.luck.max" disabled=true}}
{{#unless isPlayMode}}
<span class="res-bonus-label">+</span>
{{formInput systemFields.luck.fields.bonus value=system.luck.bonus name="system.luck.bonus" class="res-bonus-input"}}
{{/unless}}
</span>
</div> </div>
<div class="character-resource"> <div class="character-resource">
<span class="resource-label">{{localize "OATHHAMMER.Label.Movement"}}</span> <span class="resource-label">{{localize "OATHHAMMER.Label.Movement"}}</span>
+9
View File
@@ -32,6 +32,7 @@
<div class="pool-info-line"> <div class="pool-info-line">
{{localize "OATHHAMMER.Skill.Magic"}} ({{localize "OATHHAMMER.Attribute.Willpower"}} {{wpRank}}) {{localize "OATHHAMMER.Skill.Magic"}} ({{localize "OATHHAMMER.Attribute.Willpower"}} {{wpRank}})
+ {{localize "OATHHAMMER.Label.SkillRank"}} {{magicRank}} + {{localize "OATHHAMMER.Label.SkillRank"}} {{magicRank}}
{{#if magicModDisplay}} {{localize "OATHHAMMER.Label.SkillModifier"}} {{magicModDisplay}}{{/if}}
= <strong>{{basePool}}d6</strong> = <strong>{{basePool}}d6</strong>
</div> </div>
@@ -45,6 +46,14 @@
</div> </div>
{{/unless}} {{/unless}}
<div class="roll-option-row">
<label>{{localize "OATHHAMMER.Dialog.DiceColor"}}</label>
<select name="colorOverride">
{{#each colorOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
</select>
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.DiceColorHint"}}</span>
</div>
<div class="roll-option-row"> <div class="roll-option-row">
<label>{{localize "OATHHAMMER.Dialog.Modifier"}}</label> <label>{{localize "OATHHAMMER.Dialog.Modifier"}}</label>
<select name="bonus"> <select name="bonus">
+17 -3
View File
@@ -31,6 +31,7 @@
<div class="pool-info-line"> <div class="pool-info-line">
{{localize "OATHHAMMER.Skill.Magic"}} ({{localize "OATHHAMMER.Attribute.Intelligence"}} {{intRank}}) {{localize "OATHHAMMER.Skill.Magic"}} ({{localize "OATHHAMMER.Attribute.Intelligence"}} {{intRank}})
+ {{localize "OATHHAMMER.Label.SkillRank"}} {{magicRank}} + {{localize "OATHHAMMER.Label.SkillRank"}} {{magicRank}}
{{#if magicModDisplay}} {{localize "OATHHAMMER.Label.SkillModifier"}} {{magicModDisplay}}{{/if}}
= <strong>{{basePool}}d6</strong> = <strong>{{basePool}}d6</strong>
</div> </div>
@@ -45,10 +46,23 @@
{{/if}} {{/if}}
<div class="roll-option-row"> <div class="roll-option-row">
<label>{{localize "OATHHAMMER.Dialog.Enhancement"}}</label> <label>{{localize "OATHHAMMER.Dialog.DiceColor"}}</label>
<select name="enhancement" class="enhancement-select"> <select name="colorOverride">
{{#each enhancementOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}} {{#each colorOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
</select> </select>
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.DiceColorHint"}}</span>
</div>
<div class="roll-option-row roll-option-enhancements">
<label class="enhancements-header">{{localize "OATHHAMMER.Dialog.Enhancement"}}</label>
<div class="enhancements-list">
{{#each enhancementOptions}}
<div class="enhancement-item">
<input type="checkbox" id="enh_{{value}}" name="enh_{{value}}" value="true">
<label for="enh_{{value}}" class="enh-name">{{label}}</label>
</div>
{{/each}}
</div>
</div> </div>
{{#if isElemental}} {{#if isElemental}}