Fixes and enhancements, from issue list
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
11
lang/en.json
11
lang/en.json
@@ -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+)",
|
||||
|
||||
@@ -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
77
less/free-roll.less
Normal 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%); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,3 +14,4 @@
|
||||
@import "settlement-sheet";
|
||||
@import "party-sheet";
|
||||
@import "army-sheet";
|
||||
@import "free-roll";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────────
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
|
||||
|
||||
@@ -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 = `
|
||||
|
||||
@@ -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\"]")
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user