Fixes and enhancements, from issue list

This commit is contained in:
2026-03-27 21:21:17 +01:00
parent f1dda301d7
commit c22c3d713b
25 changed files with 531 additions and 111 deletions

View File

@@ -550,6 +550,26 @@
opacity: 0.7;
font-size: calc(0.86rem * 0.9);
}
.oathhammer .character-main .character-stats-band .character-resources .character-resource.character-resource--luck .luck-stepper {
display: flex;
align-items: center;
gap: 1px;
}
.oathhammer .character-main .character-stats-band .character-resources .character-resource.character-resource--luck .luck-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.2rem;
height: 1.4rem;
font-size: calc(0.86rem * 0.85);
font-weight: bold;
color: #535128;
cursor: pointer;
line-height: 1;
}
.oathhammer .character-main .character-stats-band .character-resources .character-resource.character-resource--luck .luck-btn:hover {
color: #2a1a0a;
}
.oathhammer .character-main .character-stats-band .character-resources .resource-label {
min-width: 4.2rem;
font-family: "BlueDragon", "Palatino Linotype", serif;
@@ -919,7 +939,7 @@
grid-template-columns: 1fr 1fr 1fr;
}
.oathhammer .npc-main .regiment-vitals-grid.regiment-row2 {
grid-template-columns: 1fr 1fr;
grid-template-columns: 1fr 1fr 1fr;
border-top: none;
margin-top: 4px;
padding-top: 4px;
@@ -1160,7 +1180,7 @@
}
.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 12rem 1fr 3.5rem 3.5rem;
}
.oathhammer .item-list--npc-skill .item-list-header,
.oathhammer .item-list--npc-skill .item-entry {
@@ -2574,9 +2594,39 @@
.oathhammer .party-main .party-treasury .party-currency-cp .currency-label {
color: #aa6633;
}
.oathhammer .party-main .party-slots {
display: flex;
align-items: center;
gap: 0.4rem;
margin-top: 0.3rem;
font-family: "BlueDragon", "Palatino Linotype", serif;
font-size: calc(0.86rem * 0.85);
}
.oathhammer .party-main .party-slots .party-slots-label {
font-weight: bold;
color: #535128;
margin-right: 0.3rem;
text-transform: uppercase;
font-size: calc(0.86rem * 0.9);
letter-spacing: 0.04em;
}
.oathhammer .party-main .party-slots .party-slots-current {
font-weight: bold;
min-width: 1.8rem;
text-align: right;
}
.oathhammer .party-main .party-slots .party-slots-sep {
color: #535128;
}
.oathhammer .party-main .party-slots .party-slots-max {
width: 3.5rem;
text-align: center;
font-family: "BlueDragon", "Palatino Linotype", serif;
font-size: calc(0.86rem * 0.85);
}
.oathhammer .item-list--party-member .item-list-header,
.oathhammer .item-list--party-member .item-entry {
grid-template-columns: 1.8rem 24px 1fr 7rem 3rem 5rem 5.5rem;
grid-template-columns: 1.8rem 24px 1fr 7rem 5rem 3rem 5.5rem;
}
.oathhammer .item-list--party-member .party-member-order {
font-family: "BlueDragon", "Palatino Linotype", serif;
@@ -2586,6 +2636,15 @@
text-align: center;
align-self: center;
}
.oathhammer .item-list--party-member .item-detail--center {
text-align: center;
}
.oathhammer .item-list--party-member .item-detail--center a {
color: #2a1a0a;
}
.oathhammer .item-list--party-member .item-detail--center .fa-faded {
opacity: 0.2;
}
.oathhammer .item-list--party-loot .item-list-header,
.oathhammer .item-list--party-loot .item-entry {
grid-template-columns: 24px 1fr 6rem 5.5rem 5rem;
@@ -2752,3 +2811,79 @@
color: #987d2e;
font-family: "BlueDragon", "Palatino Linotype", serif;
}
.oh-free-roll-bar {
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
padding: 4px 6px;
border-top: 1px solid rgba(83, 81, 40, 0.4);
background: rgba(83, 81, 40, 0.08);
flex-shrink: 0;
flex-wrap: wrap;
z-index: 1;
position: relative;
}
.oh-free-roll-bar .oh-frb-label {
font-family: "BlueDragon", "Palatino Linotype", serif;
font-size: calc(0.86rem * 0.9);
font-weight: bold;
color: #2a1a0a;
white-space: nowrap;
display: flex;
align-items: center;
gap: 4px;
}
.oh-free-roll-bar .oh-frb-controls {
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
flex-wrap: wrap;
flex: 1;
}
.oh-free-roll-bar .oh-frb-controls select {
font-size: calc(0.86rem * 0.9);
padding: 1px 2px;
height: 1.6rem;
border: 1px solid #535128;
border-radius: 3px;
background: #fff;
cursor: pointer;
}
.oh-free-roll-bar .oh-frb-controls .oh-frb-pool {
width: 3.8rem;
}
.oh-free-roll-bar .oh-frb-controls .oh-frb-color {
flex: 1;
min-width: 6rem;
}
.oh-free-roll-bar .oh-frb-controls .oh-frb-explode-label {
display: flex;
align-items: center;
gap: 3px;
font-size: calc(0.86rem * 0.9);
cursor: pointer;
white-space: nowrap;
color: #2a1a0a;
}
.oh-free-roll-bar .oh-frb-controls .oh-frb-explode-label input[type="checkbox"] {
cursor: pointer;
}
.oh-free-roll-bar .oh-frb-controls .oh-frb-roll-btn {
font-family: "BlueDragon", "Palatino Linotype", serif;
font-size: calc(0.86rem * 0.9);
font-weight: bold;
padding: 2px 8px;
height: 1.6rem;
border: 1px solid #535128;
border-radius: 3px;
background: rgba(83, 81, 40, 0.2);
color: #2a1a0a;
cursor: pointer;
white-space: nowrap;
}
.oh-free-roll-bar .oh-frb-controls .oh-frb-roll-btn:hover {
background: #c8a84b;
border-color: #ac8d34;
}

View File

@@ -190,7 +190,8 @@
"ClassTrait": "Class Trait",
"LineageTrait": "Lineage Trait",
"NpcTrait": "NPC Trait",
"CreatureTrait": "Creature Trait"
"CreatureTrait": "Creature Trait",
"RegimentTrait": "Regiment Trait"
},
"Condition": {
"Blinded": "Blinded",
@@ -355,7 +356,7 @@
"DropLeaderHint": "Drop a linked actor here",
"MarchingOrder": "Marching Order",
"NoMembers": "No members yet — drag characters here.",
"DropMemberHint": "Drag a character actor here to add them to the party.",
"DropMemberHint": "Drag a character or NPC actor here to add them to the party.",
"Loot": "Loot",
"NoLoot": "No loot yet — drag items here.",
"DropLootHint": "Drag weapons, armor or equipment here to add party loot.",
@@ -368,7 +369,11 @@
"Location": "Location",
"Regiments": "Regiments",
"DropRegimentHint": "Drag a regiment actor (must be token-linked) to add it to this army.",
"TotalSupply": "Total Supply"
"TotalSupply": "Total Supply",
"Mercenary": "Mercenary",
"CurrentXP": "XP",
"CarriesLight": "Carries Light",
"Slots": "Slots"
},
"ColorDice": {
"White": "White (4+)",

View File

@@ -168,6 +168,27 @@
}
.res-sep { opacity: 0.7; font-size: @font-size-xs; }
&.character-resource--luck {
.luck-stepper {
display: flex;
align-items: center;
gap: 1px;
}
.luck-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.2rem;
height: 1.4rem;
font-size: @font-size-sm;
font-weight: bold;
color: @color-olive;
cursor: pointer;
line-height: 1;
&:hover { color: @color-dark; }
}
}
}
.resource-label {

77
less/free-roll.less Normal file
View File

@@ -0,0 +1,77 @@
// ============================================================
// FREE ROLL BAR — injected below the chat log
// ============================================================
.oh-free-roll-bar {
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
padding: 4px 6px;
border-top: 1px solid fade(@color-olive, 40%);
background: fade(@color-olive, 8%);
flex-shrink: 0;
flex-wrap: wrap;
z-index: 1;
position: relative;
.oh-frb-label {
font-family: @font-secondary;
font-size: @font-size-xs;
font-weight: bold;
color: @color-dark;
white-space: nowrap;
display: flex;
align-items: center;
gap: 4px;
}
.oh-frb-controls {
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
flex-wrap: wrap;
flex: 1;
select {
font-size: @font-size-xs;
padding: 1px 2px;
height: 1.6rem;
border: 1px solid @color-olive;
border-radius: 3px;
background: #fff;
cursor: pointer;
}
.oh-frb-pool { width: 3.8rem; }
.oh-frb-color { flex: 1; min-width: 6rem; }
.oh-frb-explode-label {
display: flex;
align-items: center;
gap: 3px;
font-size: @font-size-xs;
cursor: pointer;
white-space: nowrap;
color: @color-dark;
input[type="checkbox"] { cursor: pointer; }
}
.oh-frb-roll-btn {
font-family: @font-secondary;
font-size: @font-size-xs;
font-weight: bold;
padding: 2px 8px;
height: 1.6rem;
border: 1px solid @color-olive;
border-radius: 3px;
background: @color-olive-faint;
color: @color-dark;
cursor: pointer;
white-space: nowrap;
&:hover { background: @color-gold; border-color: darken(@color-gold, 10%); }
}
}
}

View File

@@ -14,3 +14,4 @@
@import "settlement-sheet";
@import "party-sheet";
@import "army-sheet";
@import "free-roll";

View File

@@ -191,7 +191,7 @@
.item-list--oath {
.item-list-header, .item-entry {
grid-template-columns: @item-img-size 1fr 7rem 3.5rem 3.5rem;
grid-template-columns: @item-img-size 12rem 1fr 3.5rem 3.5rem;
}
}

View File

@@ -216,7 +216,7 @@
.oathhammer .npc-main .regiment-vitals-grid {
&.regiment-row1 { grid-template-columns: 1fr 1fr 1fr; }
&.regiment-row2 {
grid-template-columns: 1fr 1fr;
grid-template-columns: 1fr 1fr 1fr;
border-top: none;
margin-top: 4px;
padding-top: 4px;

View File

@@ -132,13 +132,43 @@
.party-currency-sp .currency-label { color: #888; }
.party-currency-cp .currency-label { color: #aa6633; }
}
.party-slots {
display: flex;
align-items: center;
gap: 0.4rem;
margin-top: 0.3rem;
font-family: @font-secondary;
font-size: @font-size-sm;
.party-slots-label {
font-weight: bold;
color: @color-olive;
margin-right: 0.3rem;
text-transform: uppercase;
font-size: @font-size-xs;
letter-spacing: 0.04em;
}
.party-slots-current {
font-weight: bold;
min-width: 1.8rem;
text-align: right;
}
.party-slots-sep { color: @color-olive; }
.party-slots-max {
width: 3.5rem;
text-align: center;
font-family: @font-secondary;
font-size: @font-size-sm;
}
}
}
// ── Member list ────────────────────────────────────────────────
.oathhammer .item-list--party-member {
.item-list-header, .item-entry {
// order# | img | name | class | level | grit | actions
grid-template-columns: 1.8rem @item-img-size 1fr 7rem 3rem 5rem 5.5rem;
// order# | img | name | lineage | grit | light | actions
grid-template-columns: 1.8rem @item-img-size 1fr 7rem 5rem 3rem 5.5rem;
}
.party-member-order {
@@ -149,6 +179,12 @@
text-align: center;
align-self: center;
}
.item-detail--center {
text-align: center;
a { color: @color-dark; }
.fa-faded { opacity: 0.2; }
}
}
// ── Loot list ──────────────────────────────────────────────────

View File

@@ -56,10 +56,11 @@ export function injectFreeRollBar(_chatLog, html) {
rollFree(pool, color, explode5)
})
// Insert between .chat-scroll and .chat-form
// Insert before the chat form — use chatForm.parentElement for AppV2 compatibility
// (in v13 parts are nested inside the app element, not direct children)
const chatForm = html.querySelector(".chat-form")
if (chatForm) {
html.insertBefore(bar, chatForm)
chatForm.parentElement.insertBefore(bar, chatForm)
} else {
html.appendChild(bar)
}

View File

@@ -37,7 +37,7 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
rollInitiative: OathHammerCharacterSheet.#onRollInitiative,
adjustQty: OathHammerCharacterSheet.#onAdjustQty,
adjustCurrency: OathHammerCharacterSheet.#onAdjustCurrency,
adjustStress: OathHammerCharacterSheet.#onAdjustStress,
adjustLuck: OathHammerCharacterSheet.#onAdjustLuck,
clearStress: OathHammerCharacterSheet.#onClearStress,
},
}
@@ -410,6 +410,13 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
await this.document.update({ [field]: Math.max(0, current + delta) })
}
static async #onAdjustLuck(event, target) {
const delta = parseInt(target.dataset.delta, 10)
const current = this.document.system.luck.value ?? 0
// No upper cap — luck can exceed max (e.g. from blessings/bonuses)
await this.document.update({ "system.luck.value": Math.max(0, current + delta) })
}
static async #onAdjustStress(event, target) {
const delta = parseInt(target.dataset.delta, 10)
const current = this.document.system.arcaneStress.value ?? 0

View File

@@ -172,6 +172,7 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet {
colorType: attack.system.colorDiceType,
threshold: attack.system.threshold,
bonusOptions,
showExplodeOn5: true,
colorChoices: Object.fromEntries(
Object.entries(SYSTEM.DICE_COLOR_TYPES).map(([k, v]) => [k, game.i18n.localize(v)])
),
@@ -194,6 +195,7 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet {
await rollNPCAttackDamage(this.document, attack, {
bonus: parseInt(getValue("bonus")) || 0,
explodeOn5: getValue("explodeOn5") === "true",
visibility: getValue("visibility"),
})
}
@@ -228,6 +230,7 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet {
itemName: spell.name, itemImg: spell.img,
dv: spell.system.difficultyValue,
poolOptions, bonusOptions, colorChoices, showColor: true,
showExplodeOn5: true,
rollModes: foundry.utils.duplicate(CONFIG.Dice.rollModes),
visibility: game.settings.get("core", "rollMode"),
}
@@ -249,6 +252,7 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet {
dicePool: parseInt(getValue("dicePool")) || 3,
bonus: parseInt(getValue("bonus")) || 0,
colorOverride: getValue("colorOverride") || null,
explodeOn5: getValue("explodeOn5") === "true",
visibility: getValue("visibility"),
})
}
@@ -272,6 +276,7 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet {
itemName: miracle.name, itemImg: miracle.img,
dv: null, showColor: false,
poolOptions, bonusOptions,
showExplodeOn5: true,
rollModes: foundry.utils.duplicate(CONFIG.Dice.rollModes),
visibility: game.settings.get("core", "rollMode"),
}
@@ -292,6 +297,7 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet {
await rollNPCMiracle(this.document, miracle, {
dicePool: parseInt(getValue("dicePool")) || 3,
bonus: parseInt(getValue("bonus")) || 0,
explodeOn5: getValue("explodeOn5") === "true",
visibility: getValue("visibility"),
})
}
@@ -323,6 +329,7 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet {
threshold,
bonusOptions,
colorChoices,
showExplodeOn5: true,
rollModes: foundry.utils.duplicate(CONFIG.Dice.rollModes),
visibility: game.settings.get("core", "rollMode"),
}
@@ -343,6 +350,7 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet {
await rollNPCArmor(actor, {
bonus: parseInt(getValue("bonus")) || 0,
colorOverride: getValue("colorOverride") || null,
explodeOn5: getValue("explodeOn5") === "true",
visibility: getValue("visibility"),
})
}
@@ -381,6 +389,7 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet {
threshold: item.system.threshold,
bonusOptions,
colorChoices,
showExplodeOn5: true,
rollModes: foundry.utils.duplicate(CONFIG.Dice.rollModes),
visibility: game.settings.get("core", "rollMode"),
}
@@ -401,6 +410,7 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet {
await rollNPCSkill(this.document, item, {
bonus: parseInt(getValue("bonus")) || 0,
colorOverride: getValue("colorOverride") || null,
explodeOn5: getValue("explodeOn5") === "true",
visibility: getValue("visibility"),
})
}

View File

@@ -10,9 +10,10 @@ export default class OathHammerPartySheet extends OathHammerActorSheet {
window: { contentClasses: ["party-content"] },
actions: {
openMember: OathHammerPartySheet.#onOpenMember,
removeMember: OathHammerPartySheet.#onRemoveMember,
moveMemberUp: OathHammerPartySheet.#onMoveMemberUp,
moveMemberDown: OathHammerPartySheet.#onMoveMemberDown,
removeMember: OathHammerPartySheet.#onRemoveMember,
moveMemberUp: OathHammerPartySheet.#onMoveMemberUp,
moveMemberDown: OathHammerPartySheet.#onMoveMemberDown,
toggleCarriesLight: OathHammerPartySheet.#onToggleCarriesLight,
adjustCurrency: OathHammerPartySheet.#onAdjustCurrency,
adjustQty: OathHammerPartySheet.#onAdjustQty,
},
@@ -53,28 +54,41 @@ export default class OathHammerPartySheet extends OathHammerActorSheet {
async _preparePartContext(partId, context) {
const doc = this.document
switch (partId) {
case "main":
case "main": {
const lootItems = doc.items.contents.filter(i => ALLOWED_LOOT_TYPES.has(i.type))
context.currentSlots = lootItems.reduce((sum, i) => {
const slots = i.system.slots ?? 0
const qty = i.system.quantity ?? 1
return sum + slots * qty
}, 0)
break
}
case "members": {
context.tab = context.tabs.members
const refs = doc.system.memberRefs ?? []
context.members = refs.map((id, idx) => {
const actor = game.actors?.get(id)
context.members = refs.map((ref, idx) => {
const actor = game.actors?.get(ref.id)
if (!actor) return null
const sys = actor.system
const classItem = actor.items?.find(i => i.type === "class")
const isNpc = actor.type === "npc"
const classItem = !isNpc ? actor.items?.find(i => i.type === "class") : null
return {
id: actor.id,
name: actor.name,
img: actor.img,
id: actor.id,
name: actor.name,
img: actor.img,
type: actor.type,
idx,
position: idx + 1,
isFirst: idx === 0,
isLast: idx === refs.length - 1,
classLabel: classItem?.name ?? "—",
level: sys.level ?? "—",
grit: sys.grit ? `${sys.grit.value}/${sys.grit.max}` : "—",
position: idx + 1,
isFirst: idx === 0,
isLast: idx === refs.length - 1,
carriesLight: ref.carriesLight ?? false,
classLabel: isNpc
? game.i18n.localize(`OATHHAMMER.NpcSubtype.${sys.subtype === "creature" ? "Creature" : "Npc"}`)
: (classItem?.name ?? "—"),
lineage: !isNpc ? (sys.lineage?.name || "—") : "—",
current: !isNpc ? (sys.experience?.current ?? "—") : "—",
grit: sys.grit ? `${sys.grit.value}/${sys.grit.max}` : "—",
}
}).filter(Boolean)
break
@@ -109,10 +123,10 @@ export default class OathHammerPartySheet extends OathHammerActorSheet {
if (data.type === "Actor") {
const actor = await fromUuid(data.uuid)
if (!actor || actor.type !== "character") return
if (!actor || !["character", "npc"].includes(actor.type)) return
const refs = foundry.utils.deepClone(this.document.system.memberRefs ?? [])
if (refs.includes(actor.id)) return
refs.push(actor.id)
if (refs.some(r => r.id === actor.id)) return
refs.push({ id: actor.id, carriesLight: false })
return this.document.update({ "system.memberRefs": refs })
}
@@ -132,7 +146,7 @@ export default class OathHammerPartySheet extends OathHammerActorSheet {
static async #onRemoveMember(event, target) {
const id = target.dataset.actorId
const refs = (this.document.system.memberRefs ?? []).filter(r => r !== id)
const refs = (this.document.system.memberRefs ?? []).filter(r => r.id !== id)
await this.document.update({ "system.memberRefs": refs })
}
@@ -152,6 +166,14 @@ export default class OathHammerPartySheet extends OathHammerActorSheet {
await this.document.update({ "system.memberRefs": refs })
}
static async #onToggleCarriesLight(event, target) {
const idx = parseInt(target.dataset.idx, 10)
const refs = foundry.utils.deepClone(this.document.system.memberRefs ?? [])
if (!refs[idx]) return
refs[idx].carriesLight = !refs[idx].carriesLight
await this.document.update({ "system.memberRefs": refs })
}
static async #onAdjustCurrency(event, target) {
const field = target.dataset.field
const delta = parseInt(target.dataset.delta, 10)

View File

@@ -56,6 +56,9 @@ export default class OathHammerRegimentSheet extends OathHammerActorSheet {
context.colorChoices = Object.fromEntries(
Object.entries(SYSTEM.DICE_COLOR_TYPES).map(([k, v]) => [k, game.i18n.localize(v)])
)
context.traitTypeLabels = Object.fromEntries(
Object.entries(SYSTEM.TRAIT_TYPE_CHOICES).map(([k, v]) => [k, v])
)
// Resolve leader actor
const leaderUuid = this.document.system.leaderUuid
if (leaderUuid) {
@@ -75,7 +78,7 @@ export default class OathHammerRegimentSheet extends OathHammerActorSheet {
break
case "skills":
context.tab = context.tabs.skills
context.skills = doc.itemTypes.skillnpc ?? []
context.skills = (doc.itemTypes.skillnpc ?? []).slice().sort((a, b) => a.name.localeCompare(b.name))
break
case "combat":
context.tab = context.tabs.combat
@@ -141,6 +144,9 @@ export default class OathHammerRegimentSheet extends OathHammerActorSheet {
const doc = this.document
const armorDice = doc.system.armorDice
if (!armorDice?.value) return ui.notifications.info("No armor dice to roll.")
const colorType = armorDice.colorDiceType || "white"
const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4
const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜"
const bonusOptions = Array.from({ length: 13 }, (_, i) => {
const v = i - 6
return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
@@ -149,9 +155,10 @@ export default class OathHammerRegimentSheet extends OathHammerActorSheet {
"systems/fvtt-oath-hammer/templates/npc-skill-dialog.hbs",
{
skillName: game.i18n.localize("OATHHAMMER.Label.ArmorDice"),
skillImg: doc.img, basePool: armorDice.value, bonusOptions,
skillImg: doc.img, dicePool: armorDice.value,
colorType, colorEmoji, threshold, bonusOptions,
colorChoices: { white: "⬜ White (4+)", red: "🔴 Red (3+)", black: "⬛ Black (2+)" },
selectedColor: armorDice.colorDiceType,
showExplodeOn5: true,
rollModes: foundry.utils.duplicate(CONFIG.Dice.rollModes),
visibility: game.settings.get("core", "rollMode")
}
@@ -165,15 +172,19 @@ export default class OathHammerRegimentSheet extends OathHammerActorSheet {
const form = new DOMParser().parseFromString(result, "text/html")
const getValue = n => form.querySelector(`[name="${n}"]`)?.value
await rollNPCArmor(doc, {
bonus: parseInt(getValue("bonus")) || 0,
bonus: parseInt(getValue("bonus")) || 0,
colorOverride: getValue("colorOverride") || null,
visibility: getValue("visibility"),
explodeOn5: getValue("explodeOn5") === "true",
visibility: getValue("visibility"),
})
}
static async #onRollSkillNPC(event, target) {
const skill = this.document.items.get(target.dataset.itemId)
if (!skill) return
const colorType = skill.system.colorDiceType || "white"
const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4
const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜"
const bonusOptions = Array.from({ length: 13 }, (_, i) => {
const v = i - 6
return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
@@ -181,9 +192,10 @@ export default class OathHammerRegimentSheet extends OathHammerActorSheet {
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-oath-hammer/templates/npc-skill-dialog.hbs",
{
skillName: skill.name, skillImg: skill.img, basePool: skill.system.dicePool, bonusOptions,
skillName: skill.name, skillImg: skill.img, dicePool: skill.system.dicePool,
colorType, colorEmoji, threshold, bonusOptions,
colorChoices: { white: "⬜ White (4+)", red: "🔴 Red (3+)", black: "⬛ Black (2+)" },
selectedColor: skill.system.colorDiceType,
showExplodeOn5: true,
rollModes: foundry.utils.duplicate(CONFIG.Dice.rollModes),
visibility: game.settings.get("core", "rollMode")
}
@@ -197,9 +209,10 @@ export default class OathHammerRegimentSheet extends OathHammerActorSheet {
const form = new DOMParser().parseFromString(result, "text/html")
const getValue = n => form.querySelector(`[name="${n}"]`)?.value
await rollNPCSkill(this.document, skill, {
bonus: parseInt(getValue("bonus")) || 0,
bonus: parseInt(getValue("bonus")) || 0,
colorOverride: getValue("colorOverride") || null,
visibility: getValue("visibility"),
explodeOn5: getValue("explodeOn5") === "true",
visibility: getValue("visibility"),
})
}
@@ -212,6 +225,9 @@ export default class OathHammerRegimentSheet extends OathHammerActorSheet {
static async #onRollNpcAttack(event, target) {
const attack = this.document.items.get(target.dataset.itemId)
if (!attack) return
const colorType = attack.system.colorDiceType || "white"
const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4
const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜"
const bonusOptions = Array.from({ length: 13 }, (_, i) => {
const v = i - 6
return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
@@ -219,9 +235,11 @@ export default class OathHammerRegimentSheet extends OathHammerActorSheet {
const content = await foundry.applications.handlebars.renderTemplate(
"systems/fvtt-oath-hammer/templates/npc-skill-dialog.hbs",
{
skillName: attack.name, skillImg: attack.img, basePool: attack.system.damageDice, bonusOptions,
skillName: attack.name, skillImg: attack.img,
dicePool: attack.system.damageDice,
colorType, colorEmoji, threshold, bonusOptions,
showExplodeOn5: true,
colorChoices: { white: "⬜ White (4+)", red: "🔴 Red (3+)", black: "⬛ Black (2+)" },
selectedColor: attack.system.colorDiceType,
rollModes: foundry.utils.duplicate(CONFIG.Dice.rollModes),
visibility: game.settings.get("core", "rollMode")
}
@@ -235,9 +253,10 @@ export default class OathHammerRegimentSheet extends OathHammerActorSheet {
const form = new DOMParser().parseFromString(result, "text/html")
const getValue = n => form.querySelector(`[name="${n}"]`)?.value
await rollNPCAttackDamage(this.document, attack, {
bonus: parseInt(getValue("bonus")) || 0,
bonus: parseInt(getValue("bonus")) || 0,
colorOverride: getValue("colorOverride") || null,
visibility: getValue("visibility"),
explodeOn5: getValue("explodeOn5") === "true",
visibility: getValue("visibility"),
})
}

View File

@@ -215,7 +215,8 @@ export const TRAIT_TYPE_CHOICES = {
"class-trait": "OATHHAMMER.TraitType.ClassTrait",
"lineage-trait": "OATHHAMMER.TraitType.LineageTrait",
"npc-trait": "OATHHAMMER.TraitType.NpcTrait",
"creature-trait": "OATHHAMMER.TraitType.CreatureTrait"
"creature-trait": "OATHHAMMER.TraitType.CreatureTrait",
"regiment-trait": "OATHHAMMER.TraitType.RegimentTrait"
}
export const NPC_SUBTYPES = {

View File

@@ -6,9 +6,12 @@ export default class OathHammerParty extends foundry.abstract.TypeDataModel {
schema.notes = new fields.HTMLField({ required: false, nullable: true, initial: "" })
// Ordered list of character actor IDs — position = marching order
// Ordered list of member entries — position = marching order
schema.memberRefs = new fields.ArrayField(
new fields.StringField({ required: true, nullable: false, blank: false })
new fields.SchemaField({
id: new fields.StringField({ required: true, nullable: false, blank: false }),
carriesLight: new fields.BooleanField({ initial: false }),
})
)
schema.treasury = new fields.SchemaField({
@@ -17,8 +20,19 @@ export default class OathHammerParty extends foundry.abstract.TypeDataModel {
cp: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
})
schema.maxSlots = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
return schema
}
static migrateData(source) {
if (Array.isArray(source.memberRefs)) {
source.memberRefs = source.memberRefs.map(r =>
typeof r === "string" ? { id: r, carriesLight: false } : r
)
}
return super.migrateData(source)
}
static LOCALIZATION_PREFIXES = ["OATHHAMMER.Party"]
}

View File

@@ -22,6 +22,7 @@ export default class OathHammerRegiment extends foundry.abstract.TypeDataModel {
schema.movement = new fields.NumberField({ ...requiredInteger, initial: 60, min: 0, max: 500 })
schema.supplyCost = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
schema.recruitmentCost = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
schema.mercenary = new fields.BooleanField({ required: true, initial: false })
schema.leaderUuid = new fields.StringField({ required: false, nullable: true, initial: null })

View File

@@ -915,7 +915,15 @@ export async function rollInitiativeCheck(actor, options = {}) {
})
}
// NPC: Fate rank + initiativeBonus
// NPC: find Leadership skillnpc item, fall back to Fate rank + initiativeBonus
const leadershipSkill = actor.items.find(
i => i.type === "skillnpc" && i.name.toLowerCase() === "leadership"
)
if (leadershipSkill) {
return rollNPCSkill(actor, leadershipSkill, { bonus, visibility })
}
// Fallback: Fate rank + initiativeBonus
const sys = actor.system
const fateRank = sys.attributes?.fate?.rank ?? 1
const initBonus = sys.initiativeBonus ?? 0
@@ -967,7 +975,7 @@ export async function rollInitiativeCheck(actor, options = {}) {
* @param {object} options
*/
export async function rollNPCSkill(actor, skillItem, options = {}) {
const { bonus = 0, colorOverride, visibility } = options
const { bonus = 0, colorOverride, visibility, explodeOn5 = false } = options
const sys = skillItem.system
const colorType = colorOverride || sys.colorDiceType
@@ -976,12 +984,14 @@ export async function rollNPCSkill(actor, skillItem, options = {}) {
const totalDice = Math.max(sys.dicePool + bonus, 1)
const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, false)
const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5)
const diceHtml = _diceHtml(diceResults, threshold)
const modLine = bonus !== 0
? `<div class="oh-roll-mods">${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}</div>`
: ""
const explodedCount = diceResults.filter(d => d.exploded).length
const modParts = []
if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
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 content = `
<div class="oh-roll-card">
@@ -1025,6 +1035,7 @@ export async function rollNPCWeaponAttack(actor, weapon, options = {}) {
const modParts = []
if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
if (diceResults.filter(d => d.exploded).length > 0) modParts.push(`💥 ${diceResults.filter(d => d.exploded).length} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
const content = `
@@ -1067,6 +1078,7 @@ export async function rollNPCWeaponDamage(actor, weapon, options = {}) {
const modParts = []
if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
if (diceResults.filter(d => d.exploded).length > 0) modParts.push(`💥 ${diceResults.filter(d => d.exploded).length} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
const content = `
@@ -1098,7 +1110,7 @@ export async function rollNPCWeaponDamage(actor, weapon, options = {}) {
* NPC armor dice roll — rolls actor's armorDice.value dice with armorDice.colorDiceType color.
*/
export async function rollNPCArmor(actor, options = {}) {
const { bonus = 0, colorOverride, visibility } = options
const { bonus = 0, colorOverride, visibility, explodeOn5 = false } = options
const sys = actor.system
const basePool = sys.armorDice?.value ?? 0
@@ -1107,12 +1119,15 @@ export async function rollNPCArmor(actor, options = {}) {
const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜"
const totalDice = Math.max(basePool + bonus, 1)
const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, false)
const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5)
const diceHtml = _diceHtml(diceResults, threshold)
const modLine = bonus !== 0
? `<div class="oh-roll-mods">${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}</div>`
: ""
const explodedCount = diceResults.filter(d => d.exploded).length
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")}`)
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 label = game.i18n.localize("OATHHAMMER.Label.ArmorDice")
const content = `
@@ -1144,20 +1159,23 @@ export async function rollNPCArmor(actor, options = {}) {
* NPC spell cast — flat dice pool, no arcane stress, posts DV success/failure to chat.
*/
export async function rollNPCSpell(actor, spell, options = {}) {
const { dicePool = 3, bonus = 0, colorOverride, visibility } = options
const { dicePool = 3, bonus = 0, colorOverride, visibility, explodeOn5 = false } = options
const dv = spell.system.difficultyValue ?? 1
const colorType = colorOverride || "white"
const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4
const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜"
const totalDice = Math.max(dicePool + bonus, 1)
const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, false)
const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5)
const diceHtml = _diceHtml(diceResults, threshold)
const isSuccess = successes >= dv
const modLine = bonus !== 0
? `<div class="oh-roll-mods">${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}</div>`
: ""
const explodedCount = diceResults.filter(d => d.exploded).length
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")}`)
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 = isSuccess ? "roll-success" : "roll-failure"
const resultLabel = isSuccess
@@ -1193,19 +1211,22 @@ export async function rollNPCSpell(actor, spell, options = {}) {
* NPC miracle invocation — flat dice pool, no blocked tracking, posts DV success/failure to chat.
*/
export async function rollNPCMiracle(actor, miracle, options = {}) {
const { dicePool = 3, bonus = 0, visibility } = options
const { dicePool = 3, bonus = 0, visibility, explodeOn5 = false } = options
const dv = 1
const threshold = 4
const colorEmoji = "⬜"
const totalDice = Math.max(dicePool + bonus, 1)
const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, false)
const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5)
const diceHtml = _diceHtml(diceResults, threshold)
const isSuccess = successes >= dv
const modLine = bonus !== 0
? `<div class="oh-roll-mods">${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}</div>`
: ""
const explodedCount = diceResults.filter(d => d.exploded).length
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")}`)
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 = isSuccess ? "roll-success" : "roll-failure"
const resultLabel = isSuccess
@@ -1241,7 +1262,7 @@ export async function rollNPCMiracle(actor, miracle, options = {}) {
* NPC attack damage roll — flat dice pool from the npcattack item, no Might.
*/
export async function rollNPCAttackDamage(actor, attack, options = {}) {
const { bonus = 0, visibility } = options
const { bonus = 0, visibility, explodeOn5 = false } = options
const sys = attack.system
const colorType = sys.colorDiceType || "white"
const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4
@@ -1249,12 +1270,15 @@ export async function rollNPCAttackDamage(actor, attack, options = {}) {
const totalDice = Math.max((sys.damageDice ?? 1) + bonus, 1)
const ap = sys.ap ?? 0
const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, false)
const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5)
const diceHtml = _diceHtml(diceResults, threshold)
const explodedCount = diceResults.filter(d => d.exploded).length
const modParts = []
if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
if (ap > 0) modParts.push(`AP ${ap}`)
if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
if (ap > 0) modParts.push(`AP ${ap}`)
if (explodeOn5) 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 content = `

View File

@@ -104,24 +104,45 @@ Hooks.once("init", function () {
Hooks.once("ready", async function () {
console.info("Oath Hammer | System Ready")
// Migration: remove orphaned items with removed types (lineage → actor field, ability → trait)
const removedTypes = new Set(["lineage", "ability"])
// Migration: remove orphaned items with removed types (lineage → actor field, ability → trait, regiment → actor type)
const removedTypes = new Set(["lineage", "ability", "regiment"])
for (const actor of game.actors) {
const invalidItems = actor._source.items?.filter(i => removedTypes.has(i.type)) ?? []
if (invalidItems.length) {
console.info(`Oath Hammer | Migrating ${actor.name}: removing ${invalidItems.length} obsolete item(s)`)
await actor.deleteEmbeddedDocuments("Item", invalidItems.map(i => i._id))
const toDelete = []
// Catch items that failed validation and landed in invalidDocumentIds
for (const id of (actor.items.invalidDocumentIds ?? [])) {
const raw = actor.items.getInvalid(id)
if (raw && removedTypes.has(raw._source?.type)) toDelete.push(id)
}
// Also catch any that slipped through in _source (belt-and-suspenders)
for (const src of (actor._source.items ?? [])) {
if (removedTypes.has(src.type) && !toDelete.includes(src._id)) toDelete.push(src._id)
}
if (toDelete.length) {
console.info(`Oath Hammer | Migrating ${actor.name}: removing ${toDelete.length} obsolete item(s)`)
await actor.deleteEmbeddedDocuments("Item", toDelete)
}
}
// Purge invalid world items of removed types
for (const id of game.items.invalidDocumentIds) {
const item = game.items.getInvalid(id)
if (item && removedTypes.has(item._source.type)) {
if (item && removedTypes.has(item._source?.type)) {
console.info(`Oath Hammer | Deleting world item: ${item._source.name} (${item._source.type})`)
await item.delete()
}
}
})
// Auto-link regiment (and army) actor tokens so they can be added to garrisons/armies
Hooks.on("preCreateActor", (actor, _data, _options, _userId) => {
if (actor.type === "regiment" || actor.type === "army") {
actor.updateSource({ "prototypeToken.actorLink": true })
}
})
// Handle "Roll Damage" button in weapon attack chat cards
Hooks.on("renderChatMessageHTML", (message, html) => {
const btn = html.querySelector("[data-action=\"rollWeaponDamage\"]")

View File

@@ -54,9 +54,13 @@
<span class="res-sep">/</span>
{{formInput systemFields.grit.fields.max value=system.grit.max name="system.grit.max" disabled=isPlayMode}}
</div>
<div class="character-resource">
<div class="character-resource character-resource--luck">
<span class="resource-label">{{localize "OATHHAMMER.Label.Luck"}}</span>
{{formInput systemFields.luck.fields.value value=system.luck.value name="system.luck.value" disabled=isPlayMode}}
<div class="luck-stepper">
<a data-action="adjustLuck" data-delta="-1" class="luck-btn"></a>
{{formInput systemFields.luck.fields.value value=system.luck.value name="system.luck.value"}}
<a data-action="adjustLuck" data-delta="1" class="luck-btn">+</a>
</div>
<span class="res-sep">/</span>
{{formInput systemFields.luck.fields.max value=system.luck.max name="system.luck.max" disabled=isPlayMode}}
</div>

View File

@@ -9,9 +9,9 @@
<span class="col-order">#</span>
<span></span>
<span class="col-name">{{localize "OATHHAMMER.Label.Name"}}</span>
<span>{{localize "OATHHAMMER.Label.Class"}}</span>
<span>{{localize "OATHHAMMER.Label.Level"}}</span>
<span>{{localize "OATHHAMMER.Label.Lineage"}}</span>
<span>{{localize "OATHHAMMER.Label.Grit"}}</span>
<span data-tooltip="{{localize 'OATHHAMMER.Label.CarriesLight'}}"><i class="fa-solid fa-fire"></i></span>
<span></span>
</li>
{{#each members as |member|}}
@@ -21,9 +21,13 @@
<span class="item-name">
<a data-action="openMember" data-actor-id="{{member.id}}">{{member.name}}</a>
</span>
<span class="item-detail item-detail--small">{{member.classLabel}}</span>
<span class="item-detail">{{member.level}}</span>
<span class="item-detail item-detail--small">{{member.lineage}}</span>
<span class="item-detail">{{member.grit}}</span>
<span class="item-detail item-detail--center">
<a data-action="toggleCarriesLight" data-idx="{{member.idx}}">
<i class="fa-solid fa-fire{{#unless member.carriesLight}} fa-faded{{/unless}}"></i>
</a>
</span>
<div class="item-actions">
{{#unless member.isFirst}}
<a data-action="moveMemberUp" data-idx="{{member.idx}}" data-tooltip="{{localize 'OATHHAMMER.Tooltip.MoveUp'}}"><i class="fa-solid fa-chevron-up"></i></a>

View File

@@ -48,6 +48,14 @@
</div>
</div><!-- /party-treasury -->
<!-- Slots -->
<div class="party-slots">
<span class="party-slots-label">{{localize "OATHHAMMER.Label.Slots"}}</span>
<span class="party-slots-current">{{currentSlots}}</span>
<span class="party-slots-sep">/</span>
<input class="party-slots-max" type="number" name="system.maxSlots" value="{{system.maxSlots}}" min="0" {{#if isPlayMode}}disabled{{/if}} />
</div>
</div><!-- /party-header-body -->
</div>
</fieldset>

View File

@@ -70,7 +70,7 @@
<span class="vital-label">{{localize "OATHHAMMER.Label.SupplyCost"}}</span>
<span class="vital-value">
<input type="number" class="npc-num-input" name="system.supplyCost" value="{{system.supplyCost}}" min="0" {{#if isPlayMode}}disabled{{/if}} />
<span class="res-sep">gp / month</span>
<span class="res-sep">gp / day</span>
</span>
</div>
@@ -83,6 +83,14 @@
</span>
</div>
<!-- Mercenary -->
<div class="npc-vital regiment-mercenary-vital">
<span class="vital-label">{{localize "OATHHAMMER.Label.Mercenary"}}</span>
<span class="vital-value">
{{formInput systemFields.mercenary value=system.mercenary name="system.mercenary" disabled=isPlayMode}}
</span>
</div>
</div><!-- /row2 -->
<!-- Leader -->

View File

@@ -1,23 +1,18 @@
<section data-tab="overview" data-group="{{tab.group}}" class="tab {{tab.cssClass}}">
<fieldset class="currency-bar">
<legend>{{localize "OATHHAMMER.Label.Treasury"}}</legend>
<div class="flexrow">
<div class="currency-item">
<label>{{localize "OATHHAMMER.Currency.GP"}}</label>
<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 class="settlement-overview-grid">
<fieldset class="currency-bar">
<legend>{{localize "OATHHAMMER.Label.Treasury"}}</legend>
<div class="flexrow">
<div class="currency-item">
<label>{{localize "OATHHAMMER.Currency.GP"}}</label>
<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>
</fieldset>
<div class="settlement-overview-grid">
<fieldset>
<legend>{{localize "OATHHAMMER.Label.Garrison"}}</legend>
{{formInput systemFields.garrison value=system.garrison name="system.garrison" disabled=isPlayMode}}
</fieldset>
<fieldset>
@@ -33,13 +28,7 @@
<fieldset>
<legend>{{localize "OATHHAMMER.Label.Description"}}</legend>
{{#if isEditMode}}
<prose-mirror name="system.description" toggled="false" collaborate="false">
{{{system.description}}}
</prose-mirror>
{{else}}
<div class="editor-content">{{{enrichedDescription}}}</div>
{{/if}}
{{formInput systemFields.description enriched=enrichedDescription value=system.description name="system.description" toggled=true}}
</fieldset>
</section>

View File

@@ -37,6 +37,11 @@
</div>
{{/if}}
<div class="roll-option-row">
<label>{{localize "OATHHAMMER.Dialog.ExplodeOn5"}}</label>
<input type="checkbox" name="explodeOn5" value="true" />
</div>
</fieldset>
<fieldset class="roll-visibility-block">

View File

@@ -28,6 +28,13 @@
</select>
</div>
{{#if showExplodeOn5}}
<div class="roll-option-row">
<label>{{localize "OATHHAMMER.Dialog.ExplodeOn5"}}</label>
<input type="checkbox" name="explodeOn5" value="true" />
</div>
{{/if}}
</fieldset>
<fieldset class="roll-visibility-block">