Enhancements as per issue tracking sheet

This commit is contained in:
2026-03-19 15:39:25 +01:00
parent b2befe039e
commit b67d85c6be
22 changed files with 588 additions and 55 deletions

View File

@@ -229,6 +229,11 @@
font-size: calc(0.86rem * 0.9); font-size: calc(0.86rem * 0.9);
font-weight: bold; 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-rank-col select,
.oathhammer .skills-container .skill-row .skill-modifier-col input { .oathhammer .skills-container .skill-row .skill-modifier-col input {
width: 100%; width: 100%;
@@ -433,6 +438,19 @@
font-size: 0.86rem; font-size: 0.86rem;
font-weight: bold; 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 { .oathhammer .character-main .character-identity-bar .identity-slot .slot-icon {
font-size: calc(0.86rem * 0.9); font-size: calc(0.86rem * 0.9);
opacity: 0.8; opacity: 0.8;
@@ -557,6 +575,51 @@
width: 4rem; width: 4rem;
text-align: center; 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 { .oathhammer .identity-lineage-class {
gap: 8px; gap: 8px;
margin-bottom: 8px; margin-bottom: 8px;
@@ -671,6 +734,9 @@
.oathhammer .item-list-header .col-name { .oathhammer .item-list-header .col-name {
text-align: left; text-align: left;
} }
.oathhammer .item-list-header .col-oath-effect {
text-align: left;
}
.oathhammer .item-entry { .oathhammer .item-entry {
display: grid; display: grid;
align-items: center; align-items: center;
@@ -767,7 +833,7 @@
} }
.oathhammer .item-list--ammo .item-list-header, .oathhammer .item-list--ammo .item-list-header,
.oathhammer .item-list--ammo .item-entry { .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-list-header,
.oathhammer .item-list--spell .item-entry { .oathhammer .item-list--spell .item-entry {
@@ -783,7 +849,7 @@
} }
.oathhammer .item-list--equipment .item-list-header, .oathhammer .item-list--equipment .item-list-header,
.oathhammer .item-list--equipment .item-entry { .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-list-header,
.oathhammer .item-list--magic-item .item-entry { .oathhammer .item-list--magic-item .item-entry {
@@ -799,7 +865,21 @@
} }
.oathhammer .item-list--oath .item-list-header, .oathhammer .item-list--oath .item-list-header,
.oathhammer .item-list--oath .item-entry { .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 { .oathhammer .item-usage {
font-size: calc(0.86rem * 0.9); font-size: calc(0.86rem * 0.9);
@@ -1157,6 +1237,14 @@
.fvtt-oath-hammer .window-content { .fvtt-oath-hammer .window-content {
background: #f5ead0; background: #f5ead0;
padding: 6px 8px; 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 { .fvtt-oath-hammer .oh-roll-dialog {
font-family: "Calibri", "Segoe UI", sans-serif; font-family: "Calibri", "Segoe UI", sans-serif;
@@ -1325,6 +1413,27 @@
cursor: pointer; cursor: pointer;
flex: 1 1 auto; 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 { .fvtt-oath-hammer .oh-roll-dialog .roll-visibility-block select {
width: 100%; width: 100%;
padding: 4px 6px; padding: 4px 6px;
@@ -1777,3 +1886,98 @@
.item-list--armor .item-actions a[data-action="edit"] { .item-list--armor .item-actions a[data-action="edit"] {
margin-left: 6px; 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);
}

View File

@@ -256,6 +256,7 @@
"ColorDice": "Color", "ColorDice": "Color",
"Lineage": "Lineage", "Lineage": "Lineage",
"DropClass": "Drop Class Here", "DropClass": "Drop Class Here",
"OpenClass": "Click to open class details",
"Traits": "Traits", "Traits": "Traits",
"Features": "Features", "Features": "Features",
"Name": "Name", "Name": "Name",
@@ -333,6 +334,7 @@
"DamageModifier": "Damage Modifier", "DamageModifier": "Damage Modifier",
"DamageModifierHint": "extra or fewer damage dice", "DamageModifierHint": "extra or fewer damage dice",
"RangeCondition": "Range Condition", "RangeCondition": "Range Condition",
"RangeConditions": "Range Conditions",
"RangeNormal": "Normal", "RangeNormal": "Normal",
"RangeLong": "Long Range", "RangeLong": "Long Range",
"RangeMoving": "Moving Before Shot", "RangeMoving": "Moving Before Shot",
@@ -424,6 +426,17 @@
"MagicSpells": "spells" "MagicSpells": "spells"
} }
}, },
"FreeRoll": {
"Label": "Free Roll",
"PoolTitle": "Number of dice (120)",
"ColorTitle": "Dice colour",
"ColorWhite": "White (4+)",
"ColorRed": "Red (3+)",
"ColorBlack": "Black (2+)",
"ExplodeTitle": "Explode on 5+",
"Roll": "Roll",
"CardTitle": "Free Dice Roll"
},
"Character": { "Character": {
"FIELDS": { "FIELDS": {
"lineage": { "lineage": {
@@ -876,6 +889,7 @@
}, },
"UsagePeriod": { "UsagePeriod": {
"None": "Passive (always on)", "None": "Passive (always on)",
"Round": "Per Round",
"Encounter": "Per Encounter", "Encounter": "Per Encounter",
"Day": "Per Day" "Day": "Per Day"
}, },

View File

@@ -14,6 +14,17 @@ export default class OathHammerArmorDialog {
const isReinforced = [...(sys.traits ?? [])].includes("reinforced") const isReinforced = [...(sys.traits ?? [])].includes("reinforced")
const defaultColor = isReinforced ? "red" : "white" 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 // AP options — entered by the user based on the attacker's weapon
const apOptions = Array.from({ length: 9 }, (_, i) => ({ const apOptions = Array.from({ length: 9 }, (_, i) => ({
value: -i, value: -i,
@@ -45,6 +56,9 @@ export default class OathHammerArmorDialog {
colorOptions, colorOptions,
rollModes, rollModes,
visibility: game.settings.get("core", "rollMode"), visibility: game.settings.get("core", "rollMode"),
availableLuck,
isHuman,
luckOptions,
} }
const content = await foundry.applications.handlebars.renderTemplate( const content = await foundry.applications.handlebars.renderTemplate(
@@ -55,6 +69,7 @@ export default class OathHammerArmorDialog {
const result = await foundry.applications.api.DialogV2.wait({ const result = await foundry.applications.api.DialogV2.wait({
window: { title: game.i18n.format("OATHHAMMER.Dialog.ArmorRollTitle", { armor: armor.name }) }, window: { title: game.i18n.format("OATHHAMMER.Dialog.ArmorRollTitle", { armor: armor.name }) },
classes: ["fvtt-oath-hammer"], classes: ["fvtt-oath-hammer"],
position: { width: 420 },
content, content,
rejectClose: false, rejectClose: false,
buttons: [{ buttons: [{
@@ -80,6 +95,8 @@ export default class OathHammerArmorDialog {
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",
luckSpend: Math.min(Math.max(0, parseInt(result.luckSpend) || 0), availableLuck),
luckIsHuman: result.luckIsHuman === "true",
} }
} }
} }

View File

@@ -70,6 +70,7 @@ export default class OathHammerDefenseDialog {
const result = await foundry.applications.api.DialogV2.wait({ const result = await foundry.applications.api.DialogV2.wait({
window: { title: game.i18n.format("OATHHAMMER.Dialog.DefenseTitle", { actor: actor.name }) }, window: { title: game.i18n.format("OATHHAMMER.Dialog.DefenseTitle", { actor: actor.name }) },
classes: ["fvtt-oath-hammer"], classes: ["fvtt-oath-hammer"],
position: { width: 420 },
content, content,
rejectClose: false, rejectClose: false,
buttons: [{ buttons: [{

View File

@@ -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 (120 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 = `
<span class="oh-frb-label">
<i class="fa-solid fa-dice-d6"></i>
${game.i18n.localize("OATHHAMMER.FreeRoll.Label")}
</span>
<div class="oh-frb-controls">
<select class="oh-frb-pool" title="${game.i18n.localize("OATHHAMMER.FreeRoll.PoolTitle")}">
${Array.from({length: 16}, (_, i) => `<option value="${i+1}"${i+1===2?" selected":""}>${i+1}d</option>`).join("")}
</select>
<select class="oh-frb-color" title="${game.i18n.localize("OATHHAMMER.FreeRoll.ColorTitle")}">
<option value="white"> ${game.i18n.localize("OATHHAMMER.FreeRoll.ColorWhite")}</option>
<option value="red">🔴 ${game.i18n.localize("OATHHAMMER.FreeRoll.ColorRed")}</option>
<option value="black"> ${game.i18n.localize("OATHHAMMER.FreeRoll.ColorBlack")}</option>
</select>
<label class="oh-frb-explode-label" title="${game.i18n.localize("OATHHAMMER.FreeRoll.ExplodeTitle")}">
<input type="checkbox" class="oh-frb-explode">
💥 5+
</label>
<button type="button" class="oh-frb-roll-btn">
${game.i18n.localize("OATHHAMMER.FreeRoll.Roll")}
</button>
</div>
`
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 ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
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 = `
<div class="oh-roll-card">
<div class="oh-roll-header">${game.i18n.localize("OATHHAMMER.FreeRoll.CardTitle")}</div>
<div class="oh-roll-info">
<span>${colorEmoji} ${pool}d6 (${threshold}+) &mdash; ${colorLabel}</span>
</div>
${modLine}
<div class="oh-roll-dice">${diceHtml}</div>
<div class="oh-roll-result ${resultClass}">
<span class="oh-roll-successes">${successes}</span>
<span class="oh-roll-verdict">${resultLabel}</span>
</div>
</div>
`
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)
}

View File

@@ -126,6 +126,7 @@ export default class OathHammerRollDialog {
const result = await foundry.applications.api.DialogV2.wait({ const result = await foundry.applications.api.DialogV2.wait({
window: { title }, window: { title },
classes: ["fvtt-oath-hammer"], classes: ["fvtt-oath-hammer"],
position: { width: 420 },
content, content,
rejectClose: false, rejectClose: false,
buttons: [ buttons: [

View File

@@ -108,6 +108,9 @@ export default class OathHammerActorSheet extends HandlebarsApplicationMixin(fou
_onDragOver(event) {} _onDragOver(event) {}
async _onDropItem(item) { 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() const itemData = item.toObject()
// Class is unique: replace any existing item of the same type // Class is unique: replace any existing item of the same type
if (item.type === "class") { if (item.type === "class") {

View File

@@ -35,6 +35,8 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
rollArmorSave: OathHammerCharacterSheet.#onRollArmorSave, rollArmorSave: OathHammerCharacterSheet.#onRollArmorSave,
resetMiracleBlocked: OathHammerCharacterSheet.#onResetMiracleBlocked, resetMiracleBlocked: OathHammerCharacterSheet.#onResetMiracleBlocked,
rollInitiative: OathHammerCharacterSheet.#onRollInitiative, 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, 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, _typeLabel: typeEntry ? game.i18n.localize(typeEntry.label) : o.system.oathType,
_violated: o.system.violated, _violated: o.system.violated,
_boonText: _stripHtml(o.system.boon, 80),
_baneText: _stripHtml(o.system.bane, 80),
_descTooltip: _stripHtml(parts.join(" ")) _descTooltip: _stripHtml(parts.join(" "))
} }
}) })
@@ -184,9 +188,12 @@ 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 // Slot tracking: max = 10 + (Might rank × 2); 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)
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 context.slotsOver = context.slotsUsed > context.slotsMax
// Show current initiative score if actor is in an active combat // Show current initiative score if actor is in an active combat
const combatant = game.combat?.combatants.find(c => c.actor?.id === doc.id) 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) _descTooltip: _stripHtml(m.system.description)
})) }))
context.slotsMax = 10 + (doc.system.attributes.might.rank * 2) 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 context.slotsOver = context.slotsUsed > context.slotsMax
break break
case "notes": case "notes":
@@ -376,6 +386,24 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
await rollInitiativeCheck(actor) 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. */ /** Strip HTML tags and collapse whitespace for use in data-tooltip attributes. */

View File

@@ -19,7 +19,7 @@ export default class OathHammerWeaponDialog {
const sys = weapon.system const sys = weapon.system
const actorSys = actor.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 skillKey = isRanged ? "shooting" : "fighting"
const skillDef = SYSTEM.SKILLS[skillKey] const skillDef = SYSTEM.SKILLS[skillKey]
const defaultAttr = skillDef.attribute const defaultAttr = skillDef.attribute
@@ -30,6 +30,16 @@ export default class OathHammerWeaponDialog {
const hasNimble = sys.traits.has("nimble") 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 // Auto-bonuses from special properties
let autoAttackBonus = 0 let autoAttackBonus = 0
if (sys.specialProperties.has("master-crafted")) autoAttackBonus += 1 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 } return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
}) })
const rangeOptions = [ const rangeConditions = [
{ value: 0, label: game.i18n.localize("OATHHAMMER.Dialog.RangeNormal") }, { name: "range_long", penalty: -1, label: `${game.i18n.localize("OATHHAMMER.Dialog.RangeLong")} (1)` },
{ value: -1, label: game.i18n.localize("OATHHAMMER.Dialog.RangeLong") + " (1)" }, { name: "range_moving", penalty: -2, label: `${game.i18n.localize("OATHHAMMER.Dialog.RangeMoving")} (2)` },
{ value: -2, label: game.i18n.localize("OATHHAMMER.Dialog.RangeMoving") + " (2)" }, { name: "range_concealment", penalty: -2, label: `${game.i18n.localize("OATHHAMMER.Dialog.RangeConcealment")} (2)` },
{ value: -2, label: game.i18n.localize("OATHHAMMER.Dialog.RangeConcealment") + " (2)" }, { name: "range_cover", penalty: -3, label: `${game.i18n.localize("OATHHAMMER.Dialog.RangeCover")} (3)` },
{ value: -3, label: game.i18n.localize("OATHHAMMER.Dialog.RangeCover") + " (3)" },
] ]
const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes) const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
@@ -96,10 +105,13 @@ export default class OathHammerWeaponDialog {
apValue: sys.ap, apValue: sys.ap,
traits: traitLabels, traits: traitLabels,
attackBonusOptions, attackBonusOptions,
rangeOptions, rangeConditions,
colorOptions: _colorOptions(skillColor), colorOptions: _colorOptions(skillColor),
rollModes, rollModes,
visibility: game.settings.get("core", "rollMode"), visibility: game.settings.get("core", "rollMode"),
availableLuck,
isHuman,
luckOptions,
} }
const content = await foundry.applications.handlebars.renderTemplate( const content = await foundry.applications.handlebars.renderTemplate(
@@ -110,6 +122,7 @@ export default class OathHammerWeaponDialog {
const result = await foundry.applications.api.DialogV2.wait({ const result = await foundry.applications.api.DialogV2.wait({
window: { title: game.i18n.format("OATHHAMMER.Dialog.AttackTitle", { weapon: weapon.name }) }, window: { title: game.i18n.format("OATHHAMMER.Dialog.AttackTitle", { weapon: weapon.name }) },
classes: ["fvtt-oath-hammer"], classes: ["fvtt-oath-hammer"],
position: { width: 420 },
content, content,
rejectClose: false, rejectClose: false,
buttons: [{ buttons: [{
@@ -128,12 +141,17 @@ export default class OathHammerWeaponDialog {
if (!result) return null if (!result) return null
return { return {
attackBonus: parseInt(result.attackBonus) || 0, 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, attrOverride: result.attrOverride || defaultAttr,
colorOverride: result.colorOverride || skillColor, colorOverride: result.colorOverride || skillColor,
visibility: result.visibility ?? game.settings.get("core", "rollMode"), visibility: result.visibility ?? game.settings.get("core", "rollMode"),
autoAttackBonus, autoAttackBonus,
explodeOn5: result.explodeOn5 === "true", 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) .filter(i => i.type === "armor" && i.system.equipped)
.reduce((sum, a) => sum + (a.system.penalty ?? 0), 0) .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 // Pre-select attack type: block weapons default to ranged, parry to melee
const defaultAttackType = hasBlock && !hasParry ? "ranged" : "melee" const defaultAttackType = hasBlock && !hasParry ? "ranged" : "melee"
@@ -229,6 +257,9 @@ export default class OathHammerWeaponDialog {
colorOptions: _colorOptions(defaultDefenseColor), colorOptions: _colorOptions(defaultDefenseColor),
rollModes, rollModes,
visibility: game.settings.get("core", "rollMode"), visibility: game.settings.get("core", "rollMode"),
availableLuck,
isHuman,
luckOptions,
} }
const content = await foundry.applications.handlebars.renderTemplate( const content = await foundry.applications.handlebars.renderTemplate(
@@ -239,6 +270,7 @@ export default class OathHammerWeaponDialog {
const result = await foundry.applications.api.DialogV2.wait({ const result = await foundry.applications.api.DialogV2.wait({
window: { title: game.i18n.format("OATHHAMMER.Dialog.WeaponDefenseTitle", { weapon: weapon.name }) }, window: { title: game.i18n.format("OATHHAMMER.Dialog.WeaponDefenseTitle", { weapon: weapon.name }) },
classes: ["fvtt-oath-hammer"], classes: ["fvtt-oath-hammer"],
position: { width: 420 },
content, content,
rejectClose: false, rejectClose: false,
buttons: [{ buttons: [{
@@ -279,6 +311,8 @@ export default class OathHammerWeaponDialog {
bonus, bonus,
visibility: result.visibility ?? game.settings.get("core", "rollMode"), visibility: result.visibility ?? game.settings.get("core", "rollMode"),
explodeOn5: result.explodeOn5 === "true", 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({ const result = await foundry.applications.api.DialogV2.wait({
window: { title: game.i18n.format("OATHHAMMER.Dialog.DamageTitle", { weapon: weapon.name }) }, window: { title: game.i18n.format("OATHHAMMER.Dialog.DamageTitle", { weapon: weapon.name }) },
classes: ["fvtt-oath-hammer"], classes: ["fvtt-oath-hammer"],
position: { width: 420 },
content, content,
rejectClose: false, rejectClose: false,
buttons: [{ buttons: [{

View File

@@ -206,6 +206,7 @@ export const TRAIT_TYPE_CHOICES = {
// When a trait's uses reset (none = passive/always on) // When a trait's uses reset (none = passive/always on)
export const TRAIT_USAGE_PERIOD = { export const TRAIT_USAGE_PERIOD = {
none: "OATHHAMMER.UsagePeriod.None", none: "OATHHAMMER.UsagePeriod.None",
round: "OATHHAMMER.UsagePeriod.Round",
encounter: "OATHHAMMER.UsagePeriod.Encounter", encounter: "OATHHAMMER.UsagePeriod.Encounter",
day: "OATHHAMMER.UsagePeriod.Day" day: "OATHHAMMER.UsagePeriod.Day"
} }
@@ -318,7 +319,7 @@ export const SKILLS = {
diplomacy: { id: "diplomacy", attribute: "willpower", label: "OATHHAMMER.Skill.Diplomacy" }, diplomacy: { id: "diplomacy", attribute: "willpower", label: "OATHHAMMER.Skill.Diplomacy" },
discipline: { id: "discipline", attribute: "willpower", label: "OATHHAMMER.Skill.Discipline" }, discipline: { id: "discipline", attribute: "willpower", label: "OATHHAMMER.Skill.Discipline" },
fighting: { id: "fighting", attribute: "might", label: "OATHHAMMER.Skill.Fighting" }, 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" }, fortune: { id: "fortune", attribute: "fate", label: "OATHHAMMER.Skill.Fortune" },
heal: { id: "heal", attribute: "intelligence", label: "OATHHAMMER.Skill.Heal" }, heal: { id: "heal", attribute: "intelligence", label: "OATHHAMMER.Skill.Heal" },
leadership: { id: "leadership", attribute: "willpower", label: "OATHHAMMER.Skill.Leadership" }, leadership: { id: "leadership", attribute: "willpower", label: "OATHHAMMER.Skill.Leadership" },
@@ -340,9 +341,9 @@ export const SKILLS_BY_ATTRIBUTE = {
might: ["athletics", "fighting", "masonry", "smithing"], might: ["athletics", "fighting", "masonry", "smithing"],
toughness: ["resilience"], toughness: ["resilience"],
agility: ["acrobatics", "carpentry", "defense", "dexterity", "ride", "shooting", "stealth"], 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"], intelligence: ["academics", "brewing", "heal", "orientation", "tracking"],
fate: ["folklore", "fortune"], fate: ["fortune"],
} }
export const ASCII = ` export const ASCII = `

View File

@@ -13,10 +13,8 @@ export default class OathHammerWeapon extends foundry.abstract.TypeDataModel {
required: true, initial: "common", choices: SYSTEM.WEAPON_PROFICIENCY_GROUPS required: true, initial: "common", choices: SYSTEM.WEAPON_PROFICIENCY_GROUPS
}) })
// Damage: melee = Might rank + damageMod dice; bows = baseDice (fixed, no Might) // Damage: melee/throwing = Might rank + damageMod dice; bows = baseDice (fixed, no Might)
// usesMight=true → formula displayed as "M+2", "M-1", etc. // usesMight is now derived from proficiencyGroup (see getter below)
// usesMight=false → formula displayed as e.g. "6" (fixed dice for bows)
schema.usesMight = new fields.BooleanField({ required: true, initial: true })
schema.damageMod = new fields.NumberField({ ...requiredInteger, initial: 0, min: -4, max: 16 }) schema.damageMod = new fields.NumberField({ ...requiredInteger, initial: 0, min: -4, max: 16 })
// AP (Armor Penetration): penalty imposed on armor/defense rolls // 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" } const map = { 0: "common", 1: "uncommon", 2: "rare", 3: "very-rare", 4: "legendary", 5: "legendary", 6: "legendary" }
source.rarity = map[source.rarity] ?? "common" source.rarity = map[source.rarity] ?? "common"
} }
// Remove legacy usesMight field — now derived from proficiencyGroup
delete source.usesMight
return super.migrateData(source) 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" * Human-readable damage formula for display, e.g. "M+2", "M-1", "6"
*/ */

View File

@@ -183,7 +183,7 @@ export async function rollRarityCheck(actor, rarityKey, itemName) {
* @param {number} threshold Minimum value to count as a success * @param {number} threshold Minimum value to count as a success
* @returns {Promise<{roll: Roll, successes: number, diceResults: Array}>} * @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 explodeThreshold = explodeOn5 ? 5 : 6
const roll = await new Roll(`${Math.max(pool, 1)}d6`).evaluate() const roll = await new Roll(`${Math.max(pool, 1)}d6`).evaluate()
const rolls = [roll] const rolls = [roll]
@@ -216,7 +216,7 @@ async function _rollPool(pool, threshold, explodeOn5 = false) {
/** /**
* Render dice results as HTML spans. * Render dice results as HTML spans.
*/ */
function _diceHtml(diceResults, threshold) { export function _diceHtml(diceResults, threshold) {
return diceResults.map(({ val, exploded }) => { return diceResults.map(({ val, exploded }) => {
const cssClass = val >= threshold ? "die-success" : "die-fail" const cssClass = val >= threshold ? "die-success" : "die-fail"
return `<span class="oh-die ${cssClass}${exploded ? " die-exploded" : ""}" title="${exploded ? "💥" : ""}">${val}</span>` return `<span class="oh-die ${cssClass}${exploded ? " die-exploded" : ""}" title="${exploded ? "💥" : ""}">${val}</span>`
@@ -236,12 +236,12 @@ function _diceHtml(diceResults, threshold) {
* @param {object} options From OathHammerWeaponDialog.promptAttack() * @param {object} options From OathHammerWeaponDialog.promptAttack()
*/ */
export async function rollWeaponAttack(actor, weapon, options = {}) { 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 sys = weapon.system
const actorSys = actor.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 skillKey = isRanged ? "shooting" : "fighting"
const skillDef = SYSTEM.SKILLS[skillKey] const skillDef = SYSTEM.SKILLS[skillKey]
const defaultAttr = skillDef.attribute 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 threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4
const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜" 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) 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 (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 (rangeCondition !== 0) modParts.push(`${rangeCondition} ${game.i18n.localize("OATHHAMMER.Dialog.RangeCondition")}`)
if (autoAttackBonus > 0) modParts.push(`+${autoAttackBonus} auto`) 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 ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : "" const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
const content = ` const content = `
@@ -342,6 +350,8 @@ export async function rollWeaponDamage(actor, weapon, options = {}) {
if (sv > 0) modParts.push(`+${sv} SV`) if (sv > 0) modParts.push(`+${sv} SV`)
if (damageBonus !== 0) modParts.push(`${damageBonus > 0 ? "+" : ""}${damageBonus} ${game.i18n.localize("OATHHAMMER.Dialog.DamageModifier")}`) if (damageBonus !== 0) modParts.push(`${damageBonus > 0 ? "+" : ""}${damageBonus} ${game.i18n.localize("OATHHAMMER.Dialog.DamageModifier")}`)
if (autoDamageBonus > 0) modParts.push(`+${autoDamageBonus} auto`) 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 ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : "" const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
const apNote = sys.ap > 0 ? `<span class="oh-ap-note">AP ${sys.ap}</span>` : "" const apNote = sys.ap > 0 ? `<span class="oh-ap-note">AP ${sys.ap}</span>` : ""
@@ -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 (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 (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 (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 ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : "" const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
const stressLine = `<div class="oh-stress-line${isBlocked ? " stress-blocked" : ""}"> const stressLine = `<div class="oh-stress-line${isBlocked ? " stress-blocked" : ""}">
@@ -530,7 +541,8 @@ export async function rollMiracleCast(actor, miracle, options = {}) {
const modParts = [] const modParts = []
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 (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 ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : "" const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
const blockedLine = !isSuccess 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 (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 (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")}`) 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 ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : "" const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
const content = ` const content = `
@@ -685,13 +699,21 @@ export async function rollWeaponDefense(actor, weapon, options = {}) {
bonus = 0, bonus = 0,
visibility, visibility,
explodeOn5 = false, explodeOn5 = false,
luckSpend = 0,
luckIsHuman = false,
} = options } = options
const defRank = actor.system.skills.defense.rank const defRank = actor.system.skills.defense.rank
const totalDice = Math.max(attrRank + defRank + traitBonus + armorPenalty + diminishPenalty + bonus, 1) 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 threshold = colorOverride === "black" ? 2 : colorOverride === "red" ? 3 : 4
const colorEmoji = colorOverride === "black" ? "⬛" : colorOverride === "red" ? "🔴" : "⬜" 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 { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5)
const diceHtml = _diceHtml(diceResults, threshold) 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 (armorPenalty < 0) modParts.push(`${armorPenalty} ${game.i18n.localize("OATHHAMMER.Dialog.ArmorPenalty")}`)
if (diminishPenalty < 0) modParts.push(`${diminishPenalty} ${game.i18n.localize("OATHHAMMER.Dialog.DiminishingDefense")}`) 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 (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 ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : "" const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
const content = ` const content = `
@@ -761,24 +785,34 @@ export async function rollArmorSave(actor, armor, options = {}) {
bonus = 0, bonus = 0,
visibility, visibility,
explodeOn5 = false, explodeOn5 = false,
luckSpend = 0,
luckIsHuman = false,
} = options } = options
// Armor CAN be reduced to 0 dice (fully bypassed by AP) const luckDicePerPoint = luckIsHuman ? 3 : 2
const totalDice = Math.max(av + apPenalty + bonus, 0) // 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 colorType = colorOverride || (isReinforced ? "red" : "white")
const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4 const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4
const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜" 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 successes = 0
let diceHtml = `<em>${game.i18n.localize("OATHHAMMER.Roll.ArmorBypassed")}</em>` let diceHtml = `<em>${game.i18n.localize("OATHHAMMER.Roll.ArmorBypassed")}</em>`
let roll let roll
let rolls = [] let rolls = []
let armorDiceResults = []
if (totalDice > 0) { if (totalDice > 0) {
const result = await _rollPool(totalDice, threshold, explodeOn5) const result = await _rollPool(totalDice, threshold, explodeOn5)
roll = result.roll roll = result.roll
rolls = result.rolls rolls = result.rolls
successes = result.successes successes = result.successes
armorDiceResults = result.diceResults
diceHtml = _diceHtml(result.diceResults, threshold) diceHtml = _diceHtml(result.diceResults, threshold)
} else { } else {
// Zero dice — create a dummy roll with no results so Foundry can still attach it // 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 = [] const modParts = []
if (apPenalty < 0) modParts.push(`AP ${apPenalty}`) if (apPenalty < 0) modParts.push(`AP ${apPenalty}`)
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 (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 ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : "" const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
const content = ` const content = `
@@ -866,7 +902,8 @@ export async function rollInitiativeCheck(actor, options = {}) {
const modParts = [] const modParts = []
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 (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 ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : "" const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
const content = ` const content = `

View File

@@ -8,6 +8,7 @@ import OathHammerUtils from "./module/utils.mjs"
import OathHammerWeaponDialog from "./module/applications/weapon-dialog.mjs" import OathHammerWeaponDialog from "./module/applications/weapon-dialog.mjs"
import OathHammerCombat from "./module/combat.mjs" import OathHammerCombat from "./module/combat.mjs"
import { rollWeaponDamage } from "./module/rolls.mjs" import { rollWeaponDamage } from "./module/rolls.mjs"
import { injectFreeRollBar } from "./module/applications/free-roll.mjs"
Hooks.once("init", function () { Hooks.once("init", function () {
console.info(SYSTEM.ASCII) console.info(SYSTEM.ASCII)
@@ -108,3 +109,6 @@ Hooks.on("renderChatMessageHTML", (message, html) => {
if (opts) await rollWeaponDamage(actor, weapon, opts) if (opts) await rollWeaponDamage(actor, weapon, opts)
}) })
}) })
// Inject Free Roll bar into the chat sidebar
Hooks.on("renderChatLog", (_chatLog, html) => injectFreeRollBar(_chatLog, html))

View File

@@ -103,7 +103,11 @@
<li class="item-entry" data-item-id="{{ammo.id}}" data-item-uuid="{{ammo.uuid}}"> <li class="item-entry" data-item-id="{{ammo.id}}" data-item-uuid="{{ammo.uuid}}">
<img src="{{ammo.img}}" class="item-img" /> <img src="{{ammo.img}}" class="item-img" />
<span class="item-name">{{ammo.name}}</span> <span class="item-name">{{ammo.name}}</span>
<span class="item-detail">×{{ammo.system.quantity}}</span> <span class="item-qty-stepper">
<a data-action="adjustQty" data-item-id="{{ammo.id}}" data-delta="-1" class="qty-btn"></a>
<span class="qty-value">{{ammo.system.quantity}}</span>
<a data-action="adjustQty" data-item-id="{{ammo.id}}" data-delta="1" class="qty-btn">+</a>
</span>
<div class="item-actions"> <div class="item-actions">
<a data-action="edit" data-item-id="{{ammo.id}}" data-item-uuid="{{ammo.uuid}}"><i class="fa-solid fa-edit"></i></a> <a data-action="edit" data-item-id="{{ammo.id}}" data-item-uuid="{{ammo.uuid}}"><i class="fa-solid fa-edit"></i></a>
<a data-action="delete" data-item-id="{{ammo.id}}" data-item-uuid="{{ammo.uuid}}"><i class="fa-solid fa-trash"></i></a> <a data-action="delete" data-item-id="{{ammo.id}}" data-item-uuid="{{ammo.uuid}}"><i class="fa-solid fa-trash"></i></a>

View File

@@ -8,15 +8,27 @@
<div class="flexrow"> <div class="flexrow">
<div class="currency-item"> <div class="currency-item">
<label>{{localize "OATHHAMMER.Currency.GP"}}</label> <label>{{localize "OATHHAMMER.Currency.GP"}}</label>
{{formInput systemFields.currency.fields.gold value=system.currency.gold name="system.currency.gold" disabled=isPlayMode}} <div class="currency-stepper">
<a data-action="adjustCurrency" data-field="system.currency.gold" data-delta="-1" class="qty-btn"></a>
{{formInput systemFields.currency.fields.gold value=system.currency.gold name="system.currency.gold"}}
<a data-action="adjustCurrency" data-field="system.currency.gold" data-delta="1" class="qty-btn">+</a>
</div>
</div> </div>
<div class="currency-item"> <div class="currency-item">
<label>{{localize "OATHHAMMER.Currency.SP"}}</label> <label>{{localize "OATHHAMMER.Currency.SP"}}</label>
{{formInput systemFields.currency.fields.silver value=system.currency.silver name="system.currency.silver" disabled=isPlayMode}} <div class="currency-stepper">
<a data-action="adjustCurrency" data-field="system.currency.silver" data-delta="-1" class="qty-btn"></a>
{{formInput systemFields.currency.fields.silver value=system.currency.silver name="system.currency.silver"}}
<a data-action="adjustCurrency" data-field="system.currency.silver" data-delta="1" class="qty-btn">+</a>
</div>
</div> </div>
<div class="currency-item"> <div class="currency-item">
<label>{{localize "OATHHAMMER.Currency.CP"}}</label> <label>{{localize "OATHHAMMER.Currency.CP"}}</label>
{{formInput systemFields.currency.fields.copper value=system.currency.copper name="system.currency.copper" disabled=isPlayMode}} <div class="currency-stepper">
<a data-action="adjustCurrency" data-field="system.currency.copper" data-delta="-1" class="qty-btn"></a>
{{formInput systemFields.currency.fields.copper value=system.currency.copper name="system.currency.copper"}}
<a data-action="adjustCurrency" data-field="system.currency.copper" data-delta="1" class="qty-btn">+</a>
</div>
</div> </div>
</div> </div>
</fieldset> </fieldset>
@@ -30,7 +42,7 @@
<span></span> <span></span>
<span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span> <span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span>
<span>{{localize "OATHHAMMER.Label.Type"}}</span> <span>{{localize "OATHHAMMER.Label.Type"}}</span>
<span>×</span> <span>{{localize "OATHHAMMER.Label.Quantity"}}</span>
<span></span> <span></span>
</li> </li>
{{#each equipment as |equip|}} {{#each equipment as |equip|}}
@@ -38,7 +50,11 @@
<img src="{{equip.img}}" class="item-img" /> <img src="{{equip.img}}" class="item-img" />
<span class="item-name" {{#if equip._descTooltip}}data-tooltip="{{equip._descTooltip}}"{{/if}}>{{equip.name}}</span> <span class="item-name" {{#if equip._descTooltip}}data-tooltip="{{equip._descTooltip}}"{{/if}}>{{equip.name}}</span>
<span class="item-type">{{localize equip.system.itemType}}</span> <span class="item-type">{{localize equip.system.itemType}}</span>
<span class="item-detail">{{equip.system.quantity}}</span> <span class="item-qty-stepper">
<a data-action="adjustQty" data-item-id="{{equip.id}}" data-delta="-1" class="qty-btn"></a>
<span class="qty-value">{{equip.system.quantity}}</span>
<a data-action="adjustQty" data-item-id="{{equip.id}}" data-delta="1" class="qty-btn">+</a>
</span>
<div class="item-actions"> <div class="item-actions">
<a data-action="edit" data-item-id="{{equip.id}}" data-item-uuid="{{equip.uuid}}"><i class="fa-solid fa-edit"></i></a> <a data-action="edit" data-item-id="{{equip.id}}" data-item-uuid="{{equip.uuid}}"><i class="fa-solid fa-edit"></i></a>
<a data-action="delete" data-item-id="{{equip.id}}" data-item-uuid="{{equip.uuid}}"><i class="fa-solid fa-trash"></i></a> <a data-action="delete" data-item-id="{{equip.id}}" data-item-uuid="{{equip.uuid}}"><i class="fa-solid fa-trash"></i></a>

View File

@@ -10,7 +10,7 @@
<li class="item-list-header"> <li class="item-list-header">
<span></span> <span></span>
<span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span> <span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span>
<span>{{localize "OATHHAMMER.Label.Type"}}</span> <span class="col-oath-effect">{{localize "OATHHAMMER.Label.Boon"}} / {{localize "OATHHAMMER.Label.Bane"}}</span>
<span>{{localize "OATHHAMMER.Label.Violated"}}</span> <span>{{localize "OATHHAMMER.Label.Violated"}}</span>
<span></span> <span></span>
</li> </li>
@@ -18,7 +18,9 @@
<li class="item-entry {{#if oath._violated}}oath--violated{{/if}}" data-item-id="{{oath.id}}" data-item-uuid="{{oath.uuid}}"> <li class="item-entry {{#if oath._violated}}oath--violated{{/if}}" data-item-id="{{oath.id}}" data-item-uuid="{{oath.uuid}}">
<img src="{{oath.img}}" class="item-img" /> <img src="{{oath.img}}" class="item-img" />
<span class="item-name" {{#if oath._descTooltip}}data-tooltip="{{oath._descTooltip}}"{{/if}}>{{oath.name}}</span> <span class="item-name" {{#if oath._descTooltip}}data-tooltip="{{oath._descTooltip}}"{{/if}}>{{oath.name}}</span>
<span class="item-type">{{oath._typeLabel}}</span> <span class="item-oath-effect {{#if oath._violated}}oath-bane{{else}}oath-boon{{/if}}">
{{#if oath._violated}}{{oath._baneText}}{{else}}{{oath._boonText}}{{/if}}
</span>
<span class="item-violated">{{#if oath._violated}}<i class="fa-solid fa-circle-xmark"></i>{{else}}<i class="fa-regular fa-circle-check"></i>{{/if}}</span> <span class="item-violated">{{#if oath._violated}}<i class="fa-solid fa-circle-xmark"></i>{{else}}<i class="fa-regular fa-circle-check"></i>{{/if}}</span>
<div class="item-actions"> <div class="item-actions">
<a data-action="edit" data-item-id="{{oath.id}}" data-item-uuid="{{oath.uuid}}"><i class="fa-solid fa-edit"></i></a> <a data-action="edit" data-item-id="{{oath.id}}" data-item-uuid="{{oath.uuid}}"><i class="fa-solid fa-edit"></i></a>

View File

@@ -28,9 +28,8 @@
<div class="identity-slot class-slot {{#unless characterClass}}empty{{/unless}}" data-drop-type="class"> <div class="identity-slot class-slot {{#unless characterClass}}empty{{/unless}}" data-drop-type="class">
{{#if characterClass}} {{#if characterClass}}
<img src="{{characterClass.img}}" class="identity-img" data-item-id="{{characterClass.id}}" data-item-uuid="{{characterClass.uuid}}" /> <img src="{{characterClass.img}}" class="identity-img" data-item-id="{{characterClass.id}}" data-item-uuid="{{characterClass.uuid}}" />
<span class="identity-name">{{characterClass.name}}</span> <a class="identity-name class-open-link" data-action="edit" data-item-id="{{characterClass.id}}" data-item-uuid="{{characterClass.uuid}}" data-tooltip="{{localize 'OATHHAMMER.Label.OpenClass'}}">{{characterClass.name}}</a>
{{#unless isPlayMode}} {{#unless isPlayMode}}
<a data-action="edit" data-item-id="{{characterClass.id}}" data-item-uuid="{{characterClass.uuid}}"><i class="fa-solid fa-edit"></i></a>
<a data-action="delete" data-item-id="{{characterClass.id}}" data-item-uuid="{{characterClass.uuid}}"><i class="fa-solid fa-trash"></i></a> <a data-action="delete" data-item-id="{{characterClass.id}}" data-item-uuid="{{characterClass.uuid}}"><i class="fa-solid fa-trash"></i></a>
{{/unless}} {{/unless}}
{{else}} {{else}}
@@ -39,8 +38,6 @@
{{/if}} {{/if}}
</div> </div>
<div class="identity-xp"> <div class="identity-xp">
<span class="xp-label">{{localize "OATHHAMMER.Label.Level"}}</span>
{{formInput systemFields.experience.fields.level value=system.experience.level name="system.experience.level" disabled=isPlayMode}}
<span class="xp-label">{{localize "OATHHAMMER.Label.XP"}}</span> <span class="xp-label">{{localize "OATHHAMMER.Label.XP"}}</span>
{{formInput systemFields.experience.fields.current value=system.experience.current name="system.experience.current" disabled=isPlayMode}} {{formInput systemFields.experience.fields.current value=system.experience.current name="system.experience.current" disabled=isPlayMode}}
<span class="xp-sep">/</span> <span class="xp-sep">/</span>

View File

@@ -51,6 +51,18 @@
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.ExplodeOn5Hint"}}</span> <span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.ExplodeOn5Hint"}}</span>
</div> </div>
{{#if availableLuck}}
<div class="roll-option-row roll-option-luck">
<label>{{localize "OATHHAMMER.Dialog.LuckSpend"}} <i class="fa-solid fa-clover luck-icon"></i></label>
<select name="luckSpend">
{{#each luckOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
</select>
<label class="luck-human-label" for="armorLuckIsHuman">{{localize "OATHHAMMER.Dialog.LuckHuman"}}</label>
<input type="checkbox" id="armorLuckIsHuman" name="luckIsHuman" value="true" {{#if isHuman}}checked{{/if}} />
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.LuckHint"}} ({{availableLuck}} {{localize "OATHHAMMER.Dialog.Available"}})</span>
</div>
{{/if}}
</fieldset> </fieldset>
{{!-- Visibility -----------------------------------------------------------}} {{!-- Visibility -----------------------------------------------------------}}

View File

@@ -9,6 +9,7 @@
{{formField systemFields.quantity value=system.quantity name="system.quantity"}} {{formField systemFields.quantity value=system.quantity name="system.quantity"}}
{{formField systemFields.rarity value=system.rarity name="system.rarity" localize=true}} {{formField systemFields.rarity value=system.rarity name="system.rarity" localize=true}}
<a data-action="rollRarity" class="rarity-roll-btn" data-tooltip="{{localize 'OATHHAMMER.Roll.RarityCheck'}}"><i class="fa-solid fa-dice"></i> {{localize "OATHHAMMER.Roll.RarityCheck"}}</a> <a data-action="rollRarity" class="rarity-roll-btn" data-tooltip="{{localize 'OATHHAMMER.Roll.RarityCheck'}}"><i class="fa-solid fa-dice"></i> {{localize "OATHHAMMER.Roll.RarityCheck"}}</a>
{{formField systemFields.cost value=system.cost name="system.cost"}}
{{formField systemFields.currency value=system.currency name="system.currency" localize=true}} {{formField systemFields.currency value=system.currency name="system.currency" localize=true}}
</div> </div>
</div> </div>

View File

@@ -6,7 +6,6 @@
<div class="flexrow"> <div class="flexrow">
<div class="align-top"> <div class="align-top">
{{formField systemFields.proficiencyGroup value=system.proficiencyGroup name="system.proficiencyGroup" localize=true}} {{formField systemFields.proficiencyGroup value=system.proficiencyGroup name="system.proficiencyGroup" localize=true}}
{{formField systemFields.usesMight value=system.usesMight name="system.usesMight"}}
<div class="form-group"> <div class="form-group">
<label>{{localize "OATHHAMMER.Weapon.FIELDS.damageMod.label"}}</label> <label>{{localize "OATHHAMMER.Weapon.FIELDS.damageMod.label"}}</label>
<div class="form-fields"> <div class="form-fields">

View File

@@ -56,11 +56,16 @@
</div> </div>
{{#if isRanged}} {{#if isRanged}}
<div class="roll-option-row"> <div class="roll-option-block">
<label>{{localize "OATHHAMMER.Dialog.RangeCondition"}}</label> <label class="roll-option-block-label">{{localize "OATHHAMMER.Dialog.RangeConditions"}}</label>
<select name="rangeCondition"> <div class="range-conditions">
{{#each rangeOptions}}<option value="{{value}}">{{label}}</option>{{/each}} {{#each rangeConditions}}
</select> <div class="roll-option-check">
<input type="checkbox" id="{{name}}" name="{{name}}" value="true" />
<label for="{{name}}">{{label}}</label>
</div>
{{/each}}
</div>
</div> </div>
{{/if}} {{/if}}
@@ -70,6 +75,18 @@
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.ExplodeOn5Hint"}}</span> <span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.ExplodeOn5Hint"}}</span>
</div> </div>
{{#if availableLuck}}
<div class="roll-option-row roll-option-luck">
<label>{{localize "OATHHAMMER.Dialog.LuckSpend"}} <i class="fa-solid fa-clover luck-icon"></i></label>
<select name="luckSpend">
{{#each luckOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
</select>
<label class="luck-human-label" for="attackLuckIsHuman">{{localize "OATHHAMMER.Dialog.LuckHuman"}}</label>
<input type="checkbox" id="attackLuckIsHuman" name="luckIsHuman" value="true" {{#if isHuman}}checked{{/if}} />
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.LuckHint"}} ({{availableLuck}} {{localize "OATHHAMMER.Dialog.Available"}})</span>
</div>
{{/if}}
</fieldset> </fieldset>
{{!-- Visibility --}} {{!-- Visibility --}}

View File

@@ -86,6 +86,18 @@
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.ExplodeOn5Hint"}}</span> <span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.ExplodeOn5Hint"}}</span>
</div> </div>
{{#if availableLuck}}
<div class="roll-option-row roll-option-luck">
<label>{{localize "OATHHAMMER.Dialog.LuckSpend"}} <i class="fa-solid fa-clover luck-icon"></i></label>
<select name="luckSpend">
{{#each luckOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
</select>
<label class="luck-human-label" for="defenseLuckIsHuman">{{localize "OATHHAMMER.Dialog.LuckHuman"}}</label>
<input type="checkbox" id="defenseLuckIsHuman" name="luckIsHuman" value="true" {{#if isHuman}}checked{{/if}} />
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.LuckHint"}} ({{availableLuck}} {{localize "OATHHAMMER.Dialog.Available"}})</span>
</div>
{{/if}}
</fieldset> </fieldset>
{{!-- Visibility --}} {{!-- Visibility --}}