From b67d85c6be3f70b1c95ce9c44541933cc2973c96 Mon Sep 17 00:00:00 2001 From: LeRatierBretonnier Date: Thu, 19 Mar 2026 15:39:25 +0100 Subject: [PATCH] Enhancements as per issue tracking sheet --- css/fvtt-oath-hammer.css | 210 +++++++++++++++++- lang/en.json | 14 ++ module/applications/armor-dialog.mjs | 17 ++ module/applications/defense-dialog.mjs | 1 + module/applications/free-roll.mjs | 123 ++++++++++ module/applications/roll-dialog.mjs | 1 + .../applications/sheets/base-actor-sheet.mjs | 3 + .../applications/sheets/character-sheet.mjs | 34 ++- module/applications/weapon-dialog.mjs | 53 ++++- module/config/system.mjs | 7 +- module/models/weapon.mjs | 13 +- module/rolls.mjs | 67 ++++-- oath-hammer.mjs | 4 + templates/actor/character-combat.hbs | 6 +- templates/actor/character-equipment.hbs | 26 ++- templates/actor/character-identity.hbs | 6 +- templates/actor/character-sheet.hbs | 5 +- templates/armor-roll-dialog.hbs | 12 + templates/item/ammunition-sheet.hbs | 1 + templates/item/weapon-sheet.hbs | 1 - templates/weapon-attack-dialog.hbs | 27 ++- templates/weapon-defense-dialog.hbs | 12 + 22 files changed, 588 insertions(+), 55 deletions(-) create mode 100644 module/applications/free-roll.mjs diff --git a/css/fvtt-oath-hammer.css b/css/fvtt-oath-hammer.css index 38d6fc4..6f3ac70 100644 --- a/css/fvtt-oath-hammer.css +++ b/css/fvtt-oath-hammer.css @@ -229,6 +229,11 @@ font-size: calc(0.86rem * 0.9); font-weight: bold; } +.oathhammer .skills-container .skill-row a.skill-name-col { + display: inline-flex; + align-items: center; + gap: 5px; +} .oathhammer .skills-container .skill-row .skill-rank-col select, .oathhammer .skills-container .skill-row .skill-modifier-col input { width: 100%; @@ -433,6 +438,19 @@ font-size: 0.86rem; font-weight: bold; } +.oathhammer .character-main .character-identity-bar .identity-slot a.class-open-link { + flex: 1; + font-family: "BlueDragon", "Palatino Linotype", serif; + font-size: 0.86rem; + font-weight: bold; + opacity: 1; + text-decoration: none; + cursor: pointer; +} +.oathhammer .character-main .character-identity-bar .identity-slot a.class-open-link:hover { + text-decoration: underline; + opacity: 1; +} .oathhammer .character-main .character-identity-bar .identity-slot .slot-icon { font-size: calc(0.86rem * 0.9); opacity: 0.8; @@ -557,6 +575,51 @@ width: 4rem; text-align: center; } +.oathhammer .currency-stepper { + display: flex; + align-items: center; + gap: 2px; +} +.oathhammer .currency-stepper input { + width: 3.5rem; + text-align: center; +} +/* Shared +/- button style */ +.oathhammer .qty-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + font-size: 0.9rem; + font-weight: bold; + line-height: 1; + color: #2a1a0a; + background: rgba(200, 168, 75, 0.2); + border: 1px solid rgba(200, 168, 75, 0.5); + border-radius: 3px; + cursor: pointer; + user-select: none; + flex-shrink: 0; + text-decoration: none; +} +.oathhammer .qty-btn:hover { + background: rgba(200, 168, 75, 0.45); + color: #2a1a0a; +} +/* Quantity stepper in item rows */ +.oathhammer .item-qty-stepper { + display: flex; + align-items: center; + gap: 3px; + justify-content: center; +} +.oathhammer .item-qty-stepper .qty-value { + min-width: 1.6rem; + text-align: center; + font-size: calc(0.86rem * 0.9); + font-weight: bold; +} .oathhammer .identity-lineage-class { gap: 8px; margin-bottom: 8px; @@ -671,6 +734,9 @@ .oathhammer .item-list-header .col-name { text-align: left; } +.oathhammer .item-list-header .col-oath-effect { + text-align: left; +} .oathhammer .item-entry { display: grid; align-items: center; @@ -767,7 +833,7 @@ } .oathhammer .item-list--ammo .item-list-header, .oathhammer .item-list--ammo .item-entry { - grid-template-columns: 24px 1fr 4rem 3.5rem; + grid-template-columns: 24px 1fr 5.5rem 3.5rem; } .oathhammer .item-list--spell .item-list-header, .oathhammer .item-list--spell .item-entry { @@ -783,7 +849,7 @@ } .oathhammer .item-list--equipment .item-list-header, .oathhammer .item-list--equipment .item-entry { - grid-template-columns: 24px 1fr 5rem 3rem 3.5rem; + grid-template-columns: 24px 1fr 5rem 5.5rem 3.5rem; } .oathhammer .item-list--magic-item .item-list-header, .oathhammer .item-list--magic-item .item-entry { @@ -799,7 +865,21 @@ } .oathhammer .item-list--oath .item-list-header, .oathhammer .item-list--oath .item-entry { - grid-template-columns: 24px 1fr 7rem 3.5rem 3.5rem; + grid-template-columns: 24px 1fr 10rem 3.5rem 3.5rem; +} +.oathhammer .item-oath-effect { + font-size: calc(0.86rem * 0.88); + color: #2a1a0a; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.oathhammer .item-oath-effect.oath-boon { + color: #1a5c2a; +} +.oathhammer .item-oath-effect.oath-bane { + color: #c0392b; + font-style: italic; } .oathhammer .item-usage { font-size: calc(0.86rem * 0.9); @@ -1157,6 +1237,14 @@ .fvtt-oath-hammer .window-content { background: #f5ead0; padding: 6px 8px; + /* iOS Safari flex-overflow fix: prevent flex child collapse */ + overflow-y: auto; + -webkit-overflow-scrolling: touch; +} +/* iOS Safari: ensure dialog content is not hidden due to flex-height collapse */ +.fvtt-oath-hammer.dialog .window-content { + min-height: 80px; + height: auto; } .fvtt-oath-hammer .oh-roll-dialog { font-family: "Calibri", "Segoe UI", sans-serif; @@ -1325,6 +1413,27 @@ cursor: pointer; flex: 1 1 auto; } +/* Range conditions stacked checkbox block */ +.fvtt-oath-hammer .oh-roll-dialog .roll-option-block { + display: flex; + flex-direction: column; + gap: 4px; + padding: 4px 0; + border-top: 1px solid rgba(83, 81, 40, 0.15); +} +.fvtt-oath-hammer .oh-roll-dialog .roll-option-block-label { + font-size: calc(0.86rem * 0.85); + font-weight: bold; + color: #5a3e1b; + text-transform: uppercase; + letter-spacing: 0.03em; +} +.fvtt-oath-hammer .oh-roll-dialog .range-conditions { + display: flex; + flex-direction: column; + gap: 2px; + padding-left: 4px; +} .fvtt-oath-hammer .oh-roll-dialog .roll-visibility-block select { width: 100%; padding: 4px 6px; @@ -1777,3 +1886,98 @@ .item-list--armor .item-actions a[data-action="edit"] { margin-left: 6px; } + +/* ============================================================ + FREE ROLL BAR — Chat sidebar widget + ============================================================ */ +.oh-free-roll-bar { + pointer-events: all; + display: flex; + align-items: center; + gap: 6px; + padding: 5px 8px; + background: linear-gradient(135deg, #2a1a0a 0%, #3d2812 100%); + border-top: 1px solid rgba(255, 200, 80, 0.25); + border-bottom: 1px solid rgba(0, 0, 0, 0.3); + flex-wrap: wrap; +} +.oh-free-roll-bar .oh-frb-label { + color: #f5d78e; + font-family: "BlueDragon", "Palatino Linotype", serif; + font-size: calc(0.86rem * 0.9); + white-space: nowrap; + display: flex; + align-items: center; + gap: 4px; + opacity: 0.9; +} +.oh-free-roll-bar .oh-frb-controls { + display: flex; + align-items: center; + gap: 5px; + flex: 1; + flex-wrap: wrap; +} +.oh-free-roll-bar .oh-frb-pool { + height: 1.6rem; + font-size: 0.8rem; + background: rgba(255, 255, 255, 0.12); + border: 1px solid rgba(255, 200, 80, 0.3); + border-radius: 3px; + color: #f5ead0; + cursor: pointer; + padding: 0 3px; +} +.oh-free-roll-bar .oh-frb-pool option { + background: #2a1a0a; + color: #f5ead0; +} +.oh-free-roll-bar .oh-frb-color { + height: 1.6rem; + font-size: 0.8rem; + background: rgba(255, 255, 255, 0.12); + border: 1px solid rgba(255, 200, 80, 0.3); + border-radius: 3px; + color: #f5ead0; + cursor: pointer; + padding: 0 3px; +} +.oh-free-roll-bar .oh-frb-color option { + background: #2a1a0a; + color: #f5ead0; +} +.oh-free-roll-bar .oh-frb-explode-label { + display: flex; + align-items: center; + gap: 3px; + color: #f5d78e; + font-size: 0.78rem; + cursor: pointer; + white-space: nowrap; + opacity: 0.85; +} +.oh-free-roll-bar .oh-frb-explode-label input[type="checkbox"] { + cursor: pointer; + accent-color: #c9a227; +} +.oh-free-roll-bar .oh-frb-roll-btn { + margin-left: auto; + height: 1.7rem; + padding: 0 10px; + background: linear-gradient(135deg, #c9a227 0%, #8a6a10 100%); + border: 1px solid rgba(255, 200, 80, 0.4); + border-radius: 4px; + color: #1a0e00; + font-family: "BlueDragon", "Palatino Linotype", serif; + font-size: calc(0.86rem * 0.85); + font-weight: bold; + cursor: pointer; + white-space: nowrap; + transition: filter 0.15s; +} +.oh-free-roll-bar .oh-frb-roll-btn:hover { + filter: brightness(1.15); +} +.oh-free-roll-bar .oh-frb-roll-btn:active { + filter: brightness(0.9); +} diff --git a/lang/en.json b/lang/en.json index a6a9240..d53b51b 100644 --- a/lang/en.json +++ b/lang/en.json @@ -256,6 +256,7 @@ "ColorDice": "Color", "Lineage": "Lineage", "DropClass": "Drop Class Here", + "OpenClass": "Click to open class details", "Traits": "Traits", "Features": "Features", "Name": "Name", @@ -333,6 +334,7 @@ "DamageModifier": "Damage Modifier", "DamageModifierHint": "extra or fewer damage dice", "RangeCondition": "Range Condition", + "RangeConditions": "Range Conditions", "RangeNormal": "Normal", "RangeLong": "Long Range", "RangeMoving": "Moving Before Shot", @@ -424,6 +426,17 @@ "MagicSpells": "spells" } }, + "FreeRoll": { + "Label": "Free Roll", + "PoolTitle": "Number of dice (1–20)", + "ColorTitle": "Dice colour", + "ColorWhite": "White (4+)", + "ColorRed": "Red (3+)", + "ColorBlack": "Black (2+)", + "ExplodeTitle": "Explode on 5+", + "Roll": "Roll", + "CardTitle": "Free Dice Roll" + }, "Character": { "FIELDS": { "lineage": { @@ -876,6 +889,7 @@ }, "UsagePeriod": { "None": "Passive (always on)", + "Round": "Per Round", "Encounter": "Per Encounter", "Day": "Per Day" }, diff --git a/module/applications/armor-dialog.mjs b/module/applications/armor-dialog.mjs index efa6f0c..21f9339 100644 --- a/module/applications/armor-dialog.mjs +++ b/module/applications/armor-dialog.mjs @@ -14,6 +14,17 @@ export default class OathHammerArmorDialog { const isReinforced = [...(sys.traits ?? [])].includes("reinforced") const defaultColor = isReinforced ? "red" : "white" + // Luck + const actorSys = actor.system + const availableLuck = actorSys.luck?.value ?? 0 + const isHuman = (actorSys.lineage?.name ?? "").toLowerCase() === "human" + const luckDicePerPoint = isHuman ? 3 : 2 + const luckOptions = Array.from({ length: availableLuck + 1 }, (_, i) => ({ + value: i, + label: i === 0 ? "0" : `${i} (+${i * luckDicePerPoint}d)`, + selected: i === 0, + })) + // AP options — entered by the user based on the attacker's weapon const apOptions = Array.from({ length: 9 }, (_, i) => ({ value: -i, @@ -45,6 +56,9 @@ export default class OathHammerArmorDialog { colorOptions, rollModes, visibility: game.settings.get("core", "rollMode"), + availableLuck, + isHuman, + luckOptions, } const content = await foundry.applications.handlebars.renderTemplate( @@ -55,6 +69,7 @@ export default class OathHammerArmorDialog { const result = await foundry.applications.api.DialogV2.wait({ window: { title: game.i18n.format("OATHHAMMER.Dialog.ArmorRollTitle", { armor: armor.name }) }, classes: ["fvtt-oath-hammer"], + position: { width: 420 }, content, rejectClose: false, buttons: [{ @@ -80,6 +95,8 @@ export default class OathHammerArmorDialog { bonus: parseInt(result.bonus) || 0, visibility: result.visibility ?? game.settings.get("core", "rollMode"), explodeOn5: result.explodeOn5 === "true", + luckSpend: Math.min(Math.max(0, parseInt(result.luckSpend) || 0), availableLuck), + luckIsHuman: result.luckIsHuman === "true", } } } diff --git a/module/applications/defense-dialog.mjs b/module/applications/defense-dialog.mjs index 3aa5207..74e02df 100644 --- a/module/applications/defense-dialog.mjs +++ b/module/applications/defense-dialog.mjs @@ -70,6 +70,7 @@ export default class OathHammerDefenseDialog { const result = await foundry.applications.api.DialogV2.wait({ window: { title: game.i18n.format("OATHHAMMER.Dialog.DefenseTitle", { actor: actor.name }) }, classes: ["fvtt-oath-hammer"], + position: { width: 420 }, content, rejectClose: false, buttons: [{ diff --git a/module/applications/free-roll.mjs b/module/applications/free-roll.mjs new file mode 100644 index 0000000..cd55794 --- /dev/null +++ b/module/applications/free-roll.mjs @@ -0,0 +1,123 @@ +/** + * Free Dice Roll widget injected into the Foundry chat sidebar. + * + * Provides a compact bar for GM and players to roll any dice pool without + * needing an actor — useful for quick checks, table rolls, narration, etc. + * + * Features: + * - Pool size (1–20 dice) + * - Color: White (4+), Red (3+), Black (2+) + * - Explode on 5+ checkbox + */ + +import { _rollPool, _diceHtml } from "../rolls.mjs" + +/** + * Inject the Free Roll bar into the ChatLog HTML. + * Called from `Hooks.on("renderChatLog", ...)`. + * + * @param {Application} _chatLog + * @param {HTMLElement} html + */ +export function injectFreeRollBar(_chatLog, html) { + // Avoid double-injection on re-renders + if (html.querySelector(".oh-free-roll-bar")) return + + const bar = document.createElement("div") + bar.className = "oh-free-roll-bar" + bar.innerHTML = ` + + + ${game.i18n.localize("OATHHAMMER.FreeRoll.Label")} + +
+ + + + +
+ ` + + bar.querySelector(".oh-frb-roll-btn").addEventListener("click", () => { + const pool = parseInt(bar.querySelector(".oh-frb-pool").value) || 2 + const color = bar.querySelector(".oh-frb-color").value + const explode5 = bar.querySelector(".oh-frb-explode").checked + rollFree(pool, color, explode5) + }) + + // Insert between .chat-scroll and .chat-form + const chatForm = html.querySelector(".chat-form") + if (chatForm) { + html.insertBefore(bar, chatForm) + } else { + html.appendChild(bar) + } +} + +/** + * Execute a free dice roll and post the result to chat. + * + * @param {number} pool Number of d6 to roll + * @param {string} colorType "white" | "red" | "black" + * @param {boolean} explode5 True to explode on 5+ + */ +export async function rollFree(pool, colorType, explode5 = false) { + const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4 + const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜" + const colorLabel = colorType === "black" + ? game.i18n.localize("OATHHAMMER.FreeRoll.ColorBlack") + : colorType === "red" + ? game.i18n.localize("OATHHAMMER.FreeRoll.ColorRed") + : game.i18n.localize("OATHHAMMER.FreeRoll.ColorWhite") + + const { rolls, successes, diceResults } = await _rollPool(pool, threshold, explode5) + + const explodedCount = diceResults.filter(d => d.exploded).length + const diceHtml = _diceHtml(diceResults, threshold) + + const modParts = [] + if (explode5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`) + if (explodedCount > 0) modParts.push(`💥 ${explodedCount} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`) + const modLine = modParts.length ? `
${modParts.join(" · ")}
` : "" + + const resultClass = successes > 0 ? "roll-success" : "roll-failure" + const resultLabel = successes > 0 + ? game.i18n.localize("OATHHAMMER.Roll.Success") + : game.i18n.localize("OATHHAMMER.Roll.Failure") + + const content = ` +
+
${game.i18n.localize("OATHHAMMER.FreeRoll.CardTitle")}
+
+ ${colorEmoji} ${pool}d6 (${threshold}+) — ${colorLabel} +
+ ${modLine} +
${diceHtml}
+
+ ${successes} + ${resultLabel} +
+
+ ` + + const rollMode = game.settings.get("core", "rollMode") + const msgData = { + speaker: ChatMessage.getSpeaker(), + content, + rolls, + sound: CONFIG.sounds.dice, + } + ChatMessage.applyRollMode(msgData, rollMode) + await ChatMessage.create(msgData) +} diff --git a/module/applications/roll-dialog.mjs b/module/applications/roll-dialog.mjs index 1f20bf7..3cc45b6 100644 --- a/module/applications/roll-dialog.mjs +++ b/module/applications/roll-dialog.mjs @@ -126,6 +126,7 @@ export default class OathHammerRollDialog { const result = await foundry.applications.api.DialogV2.wait({ window: { title }, classes: ["fvtt-oath-hammer"], + position: { width: 420 }, content, rejectClose: false, buttons: [ diff --git a/module/applications/sheets/base-actor-sheet.mjs b/module/applications/sheets/base-actor-sheet.mjs index c3869d8..92f9d39 100644 --- a/module/applications/sheets/base-actor-sheet.mjs +++ b/module/applications/sheets/base-actor-sheet.mjs @@ -108,6 +108,9 @@ export default class OathHammerActorSheet extends HandlebarsApplicationMixin(fou _onDragOver(event) {} async _onDropItem(item) { + // Ignore drops of items already owned by this actor (internal drag = no-op) + if (item.parent?.id === this.document.id) return + const itemData = item.toObject() // Class is unique: replace any existing item of the same type if (item.type === "class") { diff --git a/module/applications/sheets/character-sheet.mjs b/module/applications/sheets/character-sheet.mjs index 210a712..ca7f4c4 100644 --- a/module/applications/sheets/character-sheet.mjs +++ b/module/applications/sheets/character-sheet.mjs @@ -35,6 +35,8 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet { rollArmorSave: OathHammerCharacterSheet.#onRollArmorSave, resetMiracleBlocked: OathHammerCharacterSheet.#onResetMiracleBlocked, rollInitiative: OathHammerCharacterSheet.#onRollInitiative, + adjustQty: OathHammerCharacterSheet.#onAdjustQty, + adjustCurrency: OathHammerCharacterSheet.#onAdjustCurrency, }, } @@ -109,6 +111,8 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet { id: o.id, uuid: o.uuid, img: o.img, name: o.name, system: o.system, _typeLabel: typeEntry ? game.i18n.localize(typeEntry.label) : o.system.oathType, _violated: o.system.violated, + _boonText: _stripHtml(o.system.boon, 80), + _baneText: _stripHtml(o.system.bane, 80), _descTooltip: _stripHtml(parts.join(" ")) } }) @@ -184,9 +188,12 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet { } }) context.ammunition = doc.itemTypes.ammunition - // Slot tracking: max = 10 + (Might rank × 2); used = sum of all items' slots + // Slot tracking: max = 10 + (Might rank × 2); used = sum of all items' slots × quantity context.slotsMax = 10 + (doc.system.attributes.might.rank * 2) - context.slotsUsed = doc.items.reduce((sum, item) => sum + (item.system.slots ?? 0), 0) + context.slotsUsed = doc.items.reduce((sum, item) => { + const qty = item.system.quantity ?? 1 + return sum + (item.system.slots ?? 0) * Math.max(qty, 1) + }, 0) context.slotsOver = context.slotsUsed > context.slotsMax // Show current initiative score if actor is in an active combat const combatant = game.combat?.combatants.find(c => c.actor?.id === doc.id) @@ -214,7 +221,10 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet { _descTooltip: _stripHtml(m.system.description) })) context.slotsMax = 10 + (doc.system.attributes.might.rank * 2) - context.slotsUsed = doc.items.reduce((sum, item) => sum + (item.system.slots ?? 0), 0) + context.slotsUsed = doc.items.reduce((sum, item) => { + const qty = item.system.quantity ?? 1 + return sum + (item.system.slots ?? 0) * Math.max(qty, 1) + }, 0) context.slotsOver = context.slotsUsed > context.slotsMax break case "notes": @@ -376,6 +386,24 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet { await rollInitiativeCheck(actor) } } + + static async #onAdjustQty(event, target) { + const itemId = target.dataset.itemId + const delta = parseInt(target.dataset.delta, 10) + if (!itemId || isNaN(delta)) return + const item = this.document.items.get(itemId) + if (!item) return + const current = item.system.quantity ?? 0 + await item.update({ "system.quantity": Math.max(0, current + delta) }) + } + + static async #onAdjustCurrency(event, target) { + const field = target.dataset.field + const delta = parseInt(target.dataset.delta, 10) + if (!field || isNaN(delta)) return + const current = foundry.utils.getProperty(this.document, field) ?? 0 + await this.document.update({ [field]: Math.max(0, current + delta) }) + } } /** Strip HTML tags and collapse whitespace for use in data-tooltip attributes. */ diff --git a/module/applications/weapon-dialog.mjs b/module/applications/weapon-dialog.mjs index 26987ad..a3cb5f0 100644 --- a/module/applications/weapon-dialog.mjs +++ b/module/applications/weapon-dialog.mjs @@ -19,7 +19,7 @@ export default class OathHammerWeaponDialog { const sys = weapon.system const actorSys = actor.system - const isRanged = !sys.usesMight && (sys.shortRange > 0 || sys.longRange > 0) + const isRanged = sys.proficiencyGroup === "bows" || sys.proficiencyGroup === "throwing" const skillKey = isRanged ? "shooting" : "fighting" const skillDef = SYSTEM.SKILLS[skillKey] const defaultAttr = skillDef.attribute @@ -30,6 +30,16 @@ export default class OathHammerWeaponDialog { const hasNimble = sys.traits.has("nimble") + // Luck + const availableLuck = actorSys.luck?.value ?? 0 + const isHuman = (actorSys.lineage?.name ?? "").toLowerCase() === "human" + const luckDicePerPoint = isHuman ? 3 : 2 + const luckOptions = Array.from({ length: availableLuck + 1 }, (_, i) => ({ + value: i, + label: i === 0 ? "0" : `${i} (+${i * luckDicePerPoint}d)`, + selected: i === 0, + })) + // Auto-bonuses from special properties let autoAttackBonus = 0 if (sys.specialProperties.has("master-crafted")) autoAttackBonus += 1 @@ -56,12 +66,11 @@ export default class OathHammerWeaponDialog { return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 } }) - const rangeOptions = [ - { value: 0, label: game.i18n.localize("OATHHAMMER.Dialog.RangeNormal") }, - { value: -1, label: game.i18n.localize("OATHHAMMER.Dialog.RangeLong") + " (−1)" }, - { value: -2, label: game.i18n.localize("OATHHAMMER.Dialog.RangeMoving") + " (−2)" }, - { value: -2, label: game.i18n.localize("OATHHAMMER.Dialog.RangeConcealment") + " (−2)" }, - { value: -3, label: game.i18n.localize("OATHHAMMER.Dialog.RangeCover") + " (−3)" }, + const rangeConditions = [ + { name: "range_long", penalty: -1, label: `${game.i18n.localize("OATHHAMMER.Dialog.RangeLong")} (−1)` }, + { name: "range_moving", penalty: -2, label: `${game.i18n.localize("OATHHAMMER.Dialog.RangeMoving")} (−2)` }, + { name: "range_concealment", penalty: -2, label: `${game.i18n.localize("OATHHAMMER.Dialog.RangeConcealment")} (−2)` }, + { name: "range_cover", penalty: -3, label: `${game.i18n.localize("OATHHAMMER.Dialog.RangeCover")} (−3)` }, ] const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes) @@ -96,10 +105,13 @@ export default class OathHammerWeaponDialog { apValue: sys.ap, traits: traitLabels, attackBonusOptions, - rangeOptions, + rangeConditions, colorOptions: _colorOptions(skillColor), rollModes, visibility: game.settings.get("core", "rollMode"), + availableLuck, + isHuman, + luckOptions, } const content = await foundry.applications.handlebars.renderTemplate( @@ -110,6 +122,7 @@ export default class OathHammerWeaponDialog { const result = await foundry.applications.api.DialogV2.wait({ window: { title: game.i18n.format("OATHHAMMER.Dialog.AttackTitle", { weapon: weapon.name }) }, classes: ["fvtt-oath-hammer"], + position: { width: 420 }, content, rejectClose: false, buttons: [{ @@ -128,12 +141,17 @@ export default class OathHammerWeaponDialog { if (!result) return null return { attackBonus: parseInt(result.attackBonus) || 0, - rangeCondition: parseInt(result.rangeCondition) || 0, + rangeCondition: (result.range_long === "true" ? -1 : 0) + + (result.range_moving === "true" ? -2 : 0) + + (result.range_concealment === "true" ? -2 : 0) + + (result.range_cover === "true" ? -3 : 0), attrOverride: result.attrOverride || defaultAttr, colorOverride: result.colorOverride || skillColor, visibility: result.visibility ?? game.settings.get("core", "rollMode"), autoAttackBonus, explodeOn5: result.explodeOn5 === "true", + luckSpend: Math.min(Math.max(0, parseInt(result.luckSpend) || 0), availableLuck), + luckIsHuman: result.luckIsHuman === "true", } } @@ -172,6 +190,16 @@ export default class OathHammerWeaponDialog { .filter(i => i.type === "armor" && i.system.equipped) .reduce((sum, a) => sum + (a.system.penalty ?? 0), 0) + // Luck + const availableLuck = actorSys.luck?.value ?? 0 + const isHuman = (actorSys.lineage?.name ?? "").toLowerCase() === "human" + const luckDicePerPoint = isHuman ? 3 : 2 + const luckOptions = Array.from({ length: availableLuck + 1 }, (_, i) => ({ + value: i, + label: i === 0 ? "0" : `${i} (+${i * luckDicePerPoint}d)`, + selected: i === 0, + })) + // Pre-select attack type: block weapons default to ranged, parry to melee const defaultAttackType = hasBlock && !hasParry ? "ranged" : "melee" @@ -229,6 +257,9 @@ export default class OathHammerWeaponDialog { colorOptions: _colorOptions(defaultDefenseColor), rollModes, visibility: game.settings.get("core", "rollMode"), + availableLuck, + isHuman, + luckOptions, } const content = await foundry.applications.handlebars.renderTemplate( @@ -239,6 +270,7 @@ export default class OathHammerWeaponDialog { const result = await foundry.applications.api.DialogV2.wait({ window: { title: game.i18n.format("OATHHAMMER.Dialog.WeaponDefenseTitle", { weapon: weapon.name }) }, classes: ["fvtt-oath-hammer"], + position: { width: 420 }, content, rejectClose: false, buttons: [{ @@ -279,6 +311,8 @@ export default class OathHammerWeaponDialog { bonus, visibility: result.visibility ?? game.settings.get("core", "rollMode"), explodeOn5: result.explodeOn5 === "true", + luckSpend: Math.min(Math.max(0, parseInt(result.luckSpend) || 0), availableLuck), + luckIsHuman: result.luckIsHuman === "true", } } @@ -343,6 +377,7 @@ export default class OathHammerWeaponDialog { const result = await foundry.applications.api.DialogV2.wait({ window: { title: game.i18n.format("OATHHAMMER.Dialog.DamageTitle", { weapon: weapon.name }) }, classes: ["fvtt-oath-hammer"], + position: { width: 420 }, content, rejectClose: false, buttons: [{ diff --git a/module/config/system.mjs b/module/config/system.mjs index 6a6b855..258c7ed 100644 --- a/module/config/system.mjs +++ b/module/config/system.mjs @@ -206,6 +206,7 @@ export const TRAIT_TYPE_CHOICES = { // When a trait's uses reset (none = passive/always on) export const TRAIT_USAGE_PERIOD = { none: "OATHHAMMER.UsagePeriod.None", + round: "OATHHAMMER.UsagePeriod.Round", encounter: "OATHHAMMER.UsagePeriod.Encounter", day: "OATHHAMMER.UsagePeriod.Day" } @@ -318,7 +319,7 @@ export const SKILLS = { diplomacy: { id: "diplomacy", attribute: "willpower", label: "OATHHAMMER.Skill.Diplomacy" }, discipline: { id: "discipline", attribute: "willpower", label: "OATHHAMMER.Skill.Discipline" }, fighting: { id: "fighting", attribute: "might", label: "OATHHAMMER.Skill.Fighting" }, - folklore: { id: "folklore", attribute: "fate", label: "OATHHAMMER.Skill.Folklore" }, + folklore: { id: "folklore", attribute: "willpower", label: "OATHHAMMER.Skill.Folklore" }, fortune: { id: "fortune", attribute: "fate", label: "OATHHAMMER.Skill.Fortune" }, heal: { id: "heal", attribute: "intelligence", label: "OATHHAMMER.Skill.Heal" }, leadership: { id: "leadership", attribute: "willpower", label: "OATHHAMMER.Skill.Leadership" }, @@ -340,9 +341,9 @@ export const SKILLS_BY_ATTRIBUTE = { might: ["athletics", "fighting", "masonry", "smithing"], toughness: ["resilience"], agility: ["acrobatics", "carpentry", "defense", "dexterity", "ride", "shooting", "stealth"], - willpower: ["animalHandling", "diplomacy", "discipline", "leadership", "magic", "perception", "survival"], + willpower: ["animalHandling", "diplomacy", "discipline", "folklore", "leadership", "magic", "perception", "survival"], intelligence: ["academics", "brewing", "heal", "orientation", "tracking"], - fate: ["folklore", "fortune"], + fate: ["fortune"], } export const ASCII = ` diff --git a/module/models/weapon.mjs b/module/models/weapon.mjs index ffed064..33d7dbd 100644 --- a/module/models/weapon.mjs +++ b/module/models/weapon.mjs @@ -13,10 +13,8 @@ export default class OathHammerWeapon extends foundry.abstract.TypeDataModel { required: true, initial: "common", choices: SYSTEM.WEAPON_PROFICIENCY_GROUPS }) - // Damage: melee = Might rank + damageMod dice; bows = baseDice (fixed, no Might) - // usesMight=true → formula displayed as "M+2", "M-1", etc. - // usesMight=false → formula displayed as e.g. "6" (fixed dice for bows) - schema.usesMight = new fields.BooleanField({ required: true, initial: true }) + // Damage: melee/throwing = Might rank + damageMod dice; bows = baseDice (fixed, no Might) + // usesMight is now derived from proficiencyGroup (see getter below) schema.damageMod = new fields.NumberField({ ...requiredInteger, initial: 0, min: -4, max: 16 }) // AP (Armor Penetration): penalty imposed on armor/defense rolls @@ -71,9 +69,16 @@ export default class OathHammerWeapon extends foundry.abstract.TypeDataModel { const map = { 0: "common", 1: "uncommon", 2: "rare", 3: "very-rare", 4: "legendary", 5: "legendary", 6: "legendary" } source.rarity = map[source.rarity] ?? "common" } + // Remove legacy usesMight field — now derived from proficiencyGroup + delete source.usesMight return super.migrateData(source) } + /** Derived: only bows skip Might for damage. Throwing weapons keep Might (arm strength). */ + get usesMight() { + return this.proficiencyGroup !== "bows" + } + /** * Human-readable damage formula for display, e.g. "M+2", "M-1", "6" */ diff --git a/module/rolls.mjs b/module/rolls.mjs index 82cb587..69bb0dd 100644 --- a/module/rolls.mjs +++ b/module/rolls.mjs @@ -183,7 +183,7 @@ export async function rollRarityCheck(actor, rarityKey, itemName) { * @param {number} threshold Minimum value to count as a success * @returns {Promise<{roll: Roll, successes: number, diceResults: Array}>} */ -async function _rollPool(pool, threshold, explodeOn5 = false) { +export async function _rollPool(pool, threshold, explodeOn5 = false) { const explodeThreshold = explodeOn5 ? 5 : 6 const roll = await new Roll(`${Math.max(pool, 1)}d6`).evaluate() const rolls = [roll] @@ -216,7 +216,7 @@ async function _rollPool(pool, threshold, explodeOn5 = false) { /** * Render dice results as HTML spans. */ -function _diceHtml(diceResults, threshold) { +export function _diceHtml(diceResults, threshold) { return diceResults.map(({ val, exploded }) => { const cssClass = val >= threshold ? "die-success" : "die-fail" return `${val}` @@ -236,12 +236,12 @@ function _diceHtml(diceResults, threshold) { * @param {object} options From OathHammerWeaponDialog.promptAttack() */ export async function rollWeaponAttack(actor, weapon, options = {}) { - const { attackBonus = 0, rangeCondition = 0, attrOverride, colorOverride, visibility, autoAttackBonus = 0, explodeOn5 = false } = options + const { attackBonus = 0, rangeCondition = 0, attrOverride, colorOverride, visibility, autoAttackBonus = 0, explodeOn5 = false, luckSpend = 0, luckIsHuman = false } = options const sys = weapon.system const actorSys = actor.system - const isRanged = !sys.usesMight && (sys.shortRange > 0 || sys.longRange > 0) + const isRanged = sys.proficiencyGroup === "bows" || sys.proficiencyGroup === "throwing" const skillKey = isRanged ? "shooting" : "fighting" const skillDef = SYSTEM.SKILLS[skillKey] const defaultAttr = skillDef.attribute @@ -254,7 +254,13 @@ export async function rollWeaponAttack(actor, weapon, options = {}) { const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4 const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜" - const totalDice = Math.max(attrRank + skillRank + attackBonus + rangeCondition + autoAttackBonus, 1) + const luckDicePerPoint = luckIsHuman ? 3 : 2 + const totalDice = Math.max(attrRank + skillRank + attackBonus + rangeCondition + autoAttackBonus + (luckSpend * luckDicePerPoint), 1) + + if (luckSpend > 0) { + const currentLuck = actorSys.luck?.value ?? 0 + await actor.update({ "system.luck.value": Math.max(0, currentLuck - luckSpend) }) + } const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5) @@ -266,7 +272,9 @@ export async function rollWeaponAttack(actor, weapon, options = {}) { if (attackBonus !== 0) modParts.push(`${attackBonus > 0 ? "+" : ""}${attackBonus} ${game.i18n.localize("OATHHAMMER.Dialog.AttackModifier")}`) if (rangeCondition !== 0) modParts.push(`${rangeCondition} ${game.i18n.localize("OATHHAMMER.Dialog.RangeCondition")}`) if (autoAttackBonus > 0) modParts.push(`+${autoAttackBonus} auto`) - if (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`) + if (luckSpend > 0) modParts.push(`+${luckSpend * luckDicePerPoint} ${game.i18n.localize("OATHHAMMER.Dialog.LuckSpend")} (${luckSpend}LP${luckIsHuman ? " 👤" : ""})`) + const explodedCount = diceResults.filter(d => d.exploded).length + if (explodedCount > 0) modParts.push(`💥 ${explodedCount} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`) const modLine = modParts.length ? `
${modParts.join(" · ")}
` : "" const content = ` @@ -342,6 +350,8 @@ export async function rollWeaponDamage(actor, weapon, options = {}) { if (sv > 0) modParts.push(`+${sv} SV`) if (damageBonus !== 0) modParts.push(`${damageBonus > 0 ? "+" : ""}${damageBonus} ${game.i18n.localize("OATHHAMMER.Dialog.DamageModifier")}`) if (autoDamageBonus > 0) modParts.push(`+${autoDamageBonus} auto`) + const explodedCount = diceResults.filter(d => d.exploded).length + if (explodedCount > 0) modParts.push(`💥 ${explodedCount} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`) const modLine = modParts.length ? `
${modParts.join(" · ")}
` : "" const apNote = sys.ap > 0 ? `AP ${sys.ap}` : "" @@ -445,7 +455,8 @@ export async function rollSpellCast(actor, spell, options = {}) { if (poolPenalty !== 0) modParts.push(`${poolPenalty} ${game.i18n.localize("OATHHAMMER.Enhancement." + _cap(enhancement))}`) 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 (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`) + const explodedCountSpell = diceResults.filter(d => d.exploded).length + if (explodedCountSpell > 0) modParts.push(`💥 ${explodedCountSpell} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`) const modLine = modParts.length ? `
${modParts.join(" · ")}
` : "" const stressLine = `
@@ -530,7 +541,8 @@ export async function rollMiracleCast(actor, miracle, options = {}) { const modParts = [] if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`) - if (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`) + const explodedCountMiracle = diceResults.filter(d => d.exploded).length + if (explodedCountMiracle > 0) modParts.push(`💥 ${explodedCountMiracle} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`) const modLine = modParts.length ? `
${modParts.join(" · ")}
` : "" const blockedLine = !isSuccess @@ -618,6 +630,8 @@ export async function rollDefense(actor, options = {}) { if (traitBonus > 0) modParts.push(`+${traitBonus} ${game.i18n.localize(attackType === "melee" ? "OATHHAMMER.WeaponTrait.Parry" : "OATHHAMMER.WeaponTrait.Block")}`) if (armorPenalty < 0) modParts.push(`${armorPenalty} ${game.i18n.localize("OATHHAMMER.Dialog.ArmorPenalty")}`) if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`) + const explodedCountDef = diceResults.filter(d => d.exploded).length + if (explodedCountDef > 0) modParts.push(`💥 ${explodedCountDef} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`) const modLine = modParts.length ? `
${modParts.join(" · ")}
` : "" const content = ` @@ -685,13 +699,21 @@ export async function rollWeaponDefense(actor, weapon, options = {}) { bonus = 0, visibility, explodeOn5 = false, + luckSpend = 0, + luckIsHuman = false, } = options - const defRank = actor.system.skills.defense.rank - const totalDice = Math.max(attrRank + defRank + traitBonus + armorPenalty + diminishPenalty + bonus, 1) + const defRank = actor.system.skills.defense.rank + const luckDicePerPoint = luckIsHuman ? 3 : 2 + const totalDice = Math.max(attrRank + defRank + traitBonus + armorPenalty + diminishPenalty + bonus + (luckSpend * luckDicePerPoint), 1) const threshold = colorOverride === "black" ? 2 : colorOverride === "red" ? 3 : 4 const colorEmoji = colorOverride === "black" ? "⬛" : colorOverride === "red" ? "🔴" : "⬜" + if (luckSpend > 0) { + const currentLuck = actor.system.luck?.value ?? 0 + await actor.update({ "system.luck.value": Math.max(0, currentLuck - luckSpend) }) + } + const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5) const diceHtml = _diceHtml(diceResults, threshold) @@ -704,7 +726,9 @@ export async function rollWeaponDefense(actor, weapon, options = {}) { if (armorPenalty < 0) modParts.push(`${armorPenalty} ${game.i18n.localize("OATHHAMMER.Dialog.ArmorPenalty")}`) if (diminishPenalty < 0) modParts.push(`${diminishPenalty} ${game.i18n.localize("OATHHAMMER.Dialog.DiminishingDefense")}`) if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`) - if (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`) + if (luckSpend > 0) modParts.push(`+${luckSpend * luckDicePerPoint} ${game.i18n.localize("OATHHAMMER.Dialog.LuckSpend")} (${luckSpend}LP${luckIsHuman ? " 👤" : ""})`) + const explodedCountWDef = diceResults.filter(d => d.exploded).length + if (explodedCountWDef > 0) modParts.push(`💥 ${explodedCountWDef} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`) const modLine = modParts.length ? `
${modParts.join(" · ")}
` : "" const content = ` @@ -761,24 +785,34 @@ export async function rollArmorSave(actor, armor, options = {}) { bonus = 0, visibility, explodeOn5 = false, + luckSpend = 0, + luckIsHuman = false, } = options - // Armor CAN be reduced to 0 dice (fully bypassed by AP) - const totalDice = Math.max(av + apPenalty + bonus, 0) + const luckDicePerPoint = luckIsHuman ? 3 : 2 + // Armor CAN be reduced to 0 dice (fully bypassed by AP) — luck can still rescue + const totalDice = Math.max(av + apPenalty + bonus + (luckSpend * luckDicePerPoint), 0) const colorType = colorOverride || (isReinforced ? "red" : "white") const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4 const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜" + if (luckSpend > 0) { + const currentLuck = actor.system.luck?.value ?? 0 + await actor.update({ "system.luck.value": Math.max(0, currentLuck - luckSpend) }) + } + let successes = 0 let diceHtml = `${game.i18n.localize("OATHHAMMER.Roll.ArmorBypassed")}` let roll let rolls = [] + let armorDiceResults = [] if (totalDice > 0) { const result = await _rollPool(totalDice, threshold, explodeOn5) roll = result.roll rolls = result.rolls successes = result.successes + armorDiceResults = result.diceResults diceHtml = _diceHtml(result.diceResults, threshold) } else { // Zero dice — create a dummy roll with no results so Foundry can still attach it @@ -790,7 +824,9 @@ export async function rollArmorSave(actor, armor, options = {}) { const modParts = [] if (apPenalty < 0) modParts.push(`AP ${apPenalty}`) if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`) - if (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`) + if (luckSpend > 0) modParts.push(`+${luckSpend * luckDicePerPoint} ${game.i18n.localize("OATHHAMMER.Dialog.LuckSpend")} (${luckSpend}LP${luckIsHuman ? " 👤" : ""})`) + const explodedCountArmor = armorDiceResults.filter(d => d.exploded).length + if (explodedCountArmor > 0) modParts.push(`💥 ${explodedCountArmor} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`) const modLine = modParts.length ? `
${modParts.join(" · ")}
` : "" const content = ` @@ -866,7 +902,8 @@ export async function rollInitiativeCheck(actor, options = {}) { const modParts = [] if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`) - if (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`) + const explodedCountInit = diceResults.filter(d => d.exploded).length + if (explodedCountInit > 0) modParts.push(`💥 ${explodedCountInit} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`) const modLine = modParts.length ? `
${modParts.join(" · ")}
` : "" const content = ` diff --git a/oath-hammer.mjs b/oath-hammer.mjs index e713fae..2daf31d 100644 --- a/oath-hammer.mjs +++ b/oath-hammer.mjs @@ -8,6 +8,7 @@ import OathHammerUtils from "./module/utils.mjs" import OathHammerWeaponDialog from "./module/applications/weapon-dialog.mjs" import OathHammerCombat from "./module/combat.mjs" import { rollWeaponDamage } from "./module/rolls.mjs" +import { injectFreeRollBar } from "./module/applications/free-roll.mjs" Hooks.once("init", function () { console.info(SYSTEM.ASCII) @@ -108,3 +109,6 @@ Hooks.on("renderChatMessageHTML", (message, html) => { if (opts) await rollWeaponDamage(actor, weapon, opts) }) }) + +// Inject Free Roll bar into the chat sidebar +Hooks.on("renderChatLog", (_chatLog, html) => injectFreeRollBar(_chatLog, html)) diff --git a/templates/actor/character-combat.hbs b/templates/actor/character-combat.hbs index 5e681f8..64db82a 100644 --- a/templates/actor/character-combat.hbs +++ b/templates/actor/character-combat.hbs @@ -103,7 +103,11 @@
  • {{ammo.name}} - ×{{ammo.system.quantity}} + + + {{ammo.system.quantity}} + + +
    diff --git a/templates/actor/character-equipment.hbs b/templates/actor/character-equipment.hbs index fcdf268..59b101d 100644 --- a/templates/actor/character-equipment.hbs +++ b/templates/actor/character-equipment.hbs @@ -8,15 +8,27 @@
    - {{formInput systemFields.currency.fields.gold value=system.currency.gold name="system.currency.gold" disabled=isPlayMode}} +
    + + {{formInput systemFields.currency.fields.gold value=system.currency.gold name="system.currency.gold"}} + + +
    - {{formInput systemFields.currency.fields.silver value=system.currency.silver name="system.currency.silver" disabled=isPlayMode}} +
    + + {{formInput systemFields.currency.fields.silver value=system.currency.silver name="system.currency.silver"}} + + +
    - {{formInput systemFields.currency.fields.copper value=system.currency.copper name="system.currency.copper" disabled=isPlayMode}} +
    + + {{formInput systemFields.currency.fields.copper value=system.currency.copper name="system.currency.copper"}} + + +
    @@ -30,7 +42,7 @@ {{localize "OATHHAMMER.Label.Name"}} {{localize "OATHHAMMER.Label.Type"}} - × + {{localize "OATHHAMMER.Label.Quantity"}}
  • {{#each equipment as |equip|}} @@ -38,7 +50,11 @@ {{equip.name}} {{localize equip.system.itemType}} - {{equip.system.quantity}} + + + {{equip.system.quantity}} + + +
    diff --git a/templates/actor/character-identity.hbs b/templates/actor/character-identity.hbs index 9c117b6..0e45496 100644 --- a/templates/actor/character-identity.hbs +++ b/templates/actor/character-identity.hbs @@ -10,7 +10,7 @@
  • {{localize "OATHHAMMER.Label.Name"}} - {{localize "OATHHAMMER.Label.Type"}} + {{localize "OATHHAMMER.Label.Boon"}} / {{localize "OATHHAMMER.Label.Bane"}} {{localize "OATHHAMMER.Label.Violated"}}
  • @@ -18,7 +18,9 @@
  • {{oath.name}} - {{oath._typeLabel}} + + {{#if oath._violated}}{{oath._baneText}}{{else}}{{oath._boonText}}{{/if}} + {{#if oath._violated}}{{else}}{{/if}}
    diff --git a/templates/actor/character-sheet.hbs b/templates/actor/character-sheet.hbs index 5612531..3d9e2f3 100644 --- a/templates/actor/character-sheet.hbs +++ b/templates/actor/character-sheet.hbs @@ -28,9 +28,8 @@
    {{#if characterClass}} - {{characterClass.name}} + {{characterClass.name}} {{#unless isPlayMode}} - {{/unless}} {{else}} @@ -39,8 +38,6 @@ {{/if}}
    - {{localize "OATHHAMMER.Label.Level"}} - {{formInput systemFields.experience.fields.level value=system.experience.level name="system.experience.level" disabled=isPlayMode}} {{localize "OATHHAMMER.Label.XP"}} {{formInput systemFields.experience.fields.current value=system.experience.current name="system.experience.current" disabled=isPlayMode}} / diff --git a/templates/armor-roll-dialog.hbs b/templates/armor-roll-dialog.hbs index d71413c..30349bc 100644 --- a/templates/armor-roll-dialog.hbs +++ b/templates/armor-roll-dialog.hbs @@ -51,6 +51,18 @@ {{localize "OATHHAMMER.Dialog.ExplodeOn5Hint"}}
    + {{#if availableLuck}} +
    + + + + + {{localize "OATHHAMMER.Dialog.LuckHint"}} ({{availableLuck}} {{localize "OATHHAMMER.Dialog.Available"}}) +
    + {{/if}} + {{!-- Visibility -----------------------------------------------------------}} diff --git a/templates/item/ammunition-sheet.hbs b/templates/item/ammunition-sheet.hbs index 281833b..fcc71dc 100644 --- a/templates/item/ammunition-sheet.hbs +++ b/templates/item/ammunition-sheet.hbs @@ -9,6 +9,7 @@ {{formField systemFields.quantity value=system.quantity name="system.quantity"}} {{formField systemFields.rarity value=system.rarity name="system.rarity" localize=true}} {{localize "OATHHAMMER.Roll.RarityCheck"}} + {{formField systemFields.cost value=system.cost name="system.cost"}} {{formField systemFields.currency value=system.currency name="system.currency" localize=true}}
  • diff --git a/templates/item/weapon-sheet.hbs b/templates/item/weapon-sheet.hbs index 485210f..290bc60 100644 --- a/templates/item/weapon-sheet.hbs +++ b/templates/item/weapon-sheet.hbs @@ -6,7 +6,6 @@
    {{formField systemFields.proficiencyGroup value=system.proficiencyGroup name="system.proficiencyGroup" localize=true}} - {{formField systemFields.usesMight value=system.usesMight name="system.usesMight"}}
    diff --git a/templates/weapon-attack-dialog.hbs b/templates/weapon-attack-dialog.hbs index 6c14e5c..f543f59 100644 --- a/templates/weapon-attack-dialog.hbs +++ b/templates/weapon-attack-dialog.hbs @@ -56,11 +56,16 @@
    {{#if isRanged}} -
    - - +
    + +
    + {{#each rangeConditions}} +
    + + +
    + {{/each}} +
    {{/if}} @@ -70,6 +75,18 @@ {{localize "OATHHAMMER.Dialog.ExplodeOn5Hint"}}
    + {{#if availableLuck}} +
    + + + + + {{localize "OATHHAMMER.Dialog.LuckHint"}} ({{availableLuck}} {{localize "OATHHAMMER.Dialog.Available"}}) +
    + {{/if}} + {{!-- Visibility --}} diff --git a/templates/weapon-defense-dialog.hbs b/templates/weapon-defense-dialog.hbs index 2553514..c69f15a 100644 --- a/templates/weapon-defense-dialog.hbs +++ b/templates/weapon-defense-dialog.hbs @@ -86,6 +86,18 @@ {{localize "OATHHAMMER.Dialog.ExplodeOn5Hint"}}
    + {{#if availableLuck}} +
    + + + + + {{localize "OATHHAMMER.Dialog.LuckHint"}} ({{availableLuck}} {{localize "OATHHAMMER.Dialog.Available"}}) +
    + {{/if}} + {{!-- Visibility --}}