Add luck option after roll, attributes above 6, fix miracle icon and grit bonus
All checks were successful
Release Creation / build (release) Successful in 1m36s
All checks were successful
Release Creation / build (release) Successful in 1m36s
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -2,3 +2,8 @@ _docs_private/
|
|||||||
node_modules/
|
node_modules/
|
||||||
package-lock.json
|
package-lock.json
|
||||||
.history/
|
.history/
|
||||||
|
.github/
|
||||||
|
# LevelDB runtime files (lock/log are regenerated on open)
|
||||||
|
packs/*/LOCK
|
||||||
|
packs/*/LOG
|
||||||
|
packs/*/LOG.old
|
||||||
|
|||||||
@@ -565,6 +565,21 @@
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
font-size: calc(0.86rem * 0.9);
|
font-size: calc(0.86rem * 0.9);
|
||||||
}
|
}
|
||||||
|
.oathhammer .character-main .character-stats-band .character-resources .character-resource .grit-max-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
.oathhammer .character-main .character-stats-band .character-resources .character-resource .grit-max-group .res-bonus-label {
|
||||||
|
opacity: 0.6;
|
||||||
|
font-size: calc(0.86rem * 0.9);
|
||||||
|
}
|
||||||
|
.oathhammer .character-main .character-stats-band .character-resources .character-resource .grit-max-group .res-bonus-input {
|
||||||
|
width: 2.2rem;
|
||||||
|
opacity: 0.85;
|
||||||
|
border-left: 1px dashed #535128;
|
||||||
|
padding-left: 2px;
|
||||||
|
}
|
||||||
.oathhammer .character-main .character-stats-band .character-resources .character-resource.character-resource--luck .luck-stepper {
|
.oathhammer .character-main .character-stats-band .character-resources .character-resource.character-resource--luck .luck-stepper {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1775,6 +1790,45 @@
|
|||||||
.oathhammer .rarity-roll-btn i {
|
.oathhammer .rarity-roll-btn i {
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
}
|
}
|
||||||
|
.oh-luck-btn-row {
|
||||||
|
margin-top: 6px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.oh-post-luck-btn {
|
||||||
|
background: linear-gradient(135deg, #2d6a2d 0%, #4a8c4a 100%);
|
||||||
|
color: #e8d97a;
|
||||||
|
border: 1px solid #3a7a3a;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
cursor: pointer;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
transition: filter 0.15s;
|
||||||
|
}
|
||||||
|
.oh-post-luck-btn:hover {
|
||||||
|
filter: brightness(1.15);
|
||||||
|
}
|
||||||
|
.oh-luck-result {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 6px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: rgba(45, 106, 45, 0.15);
|
||||||
|
border-left: 3px solid #4a8c4a;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #2d6a2d;
|
||||||
|
}
|
||||||
|
.oh-luck-result .oh-luck-result-icon {
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
.oh-luck-result .oh-luck-dice {
|
||||||
|
display: flex;
|
||||||
|
gap: 3px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
.fvtt-oath-hammer .window-content {
|
.fvtt-oath-hammer .window-content {
|
||||||
background: #f5ead0;
|
background: #f5ead0;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
@@ -2196,6 +2250,16 @@
|
|||||||
.item-list--weapon .item-entry .item-actions a[data-action="edit"] {
|
.item-list--weapon .item-entry .item-actions a[data-action="edit"] {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
|
.oh-spell-dialog .spell-header .weapon-img-sm,
|
||||||
|
.oh-miracle-dialog .spell-header .weapon-img-sm {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
-o-object-fit: contain;
|
||||||
|
object-fit: contain;
|
||||||
|
border: 1px solid rgba(83, 81, 40, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
.oh-spell-dialog .dv-badge,
|
.oh-spell-dialog .dv-badge,
|
||||||
.oh-miracle-dialog .dv-badge {
|
.oh-miracle-dialog .dv-badge {
|
||||||
background: #3a0e6b;
|
background: #3a0e6b;
|
||||||
|
|||||||
11
lang/en.json
11
lang/en.json
@@ -230,6 +230,7 @@
|
|||||||
"NPC": "NPC",
|
"NPC": "NPC",
|
||||||
"Grit": "Grit",
|
"Grit": "Grit",
|
||||||
"Luck": "Luck",
|
"Luck": "Luck",
|
||||||
|
"LuckAvailable": "Available Luck",
|
||||||
"Defense": "Defense",
|
"Defense": "Defense",
|
||||||
"DefenseValue": "Defense Value",
|
"DefenseValue": "Defense Value",
|
||||||
"ArmorRating": "Armor Rating",
|
"ArmorRating": "Armor Rating",
|
||||||
@@ -433,6 +434,10 @@
|
|||||||
"LuckHint": "+2 dice each",
|
"LuckHint": "+2 dice each",
|
||||||
"LuckHuman": "Human (+3d)",
|
"LuckHuman": "Human (+3d)",
|
||||||
"Available": "available",
|
"Available": "available",
|
||||||
|
"LuckPoints": "Luck Points to Spend",
|
||||||
|
"LuckPostRollTitle": "Luck Roll — {name}",
|
||||||
|
"LuckPostRollConfirm": "Roll Luck Dice",
|
||||||
|
"LuckPostRollHint": "Spend Luck Points to roll extra dice and add successes",
|
||||||
"Visibility": "Visibility",
|
"Visibility": "Visibility",
|
||||||
"Attribute": "Attribute",
|
"Attribute": "Attribute",
|
||||||
"RollSkill": "Click to roll skill check",
|
"RollSkill": "Click to roll skill check",
|
||||||
@@ -544,7 +549,11 @@
|
|||||||
"DefenseMelee": "melee defense",
|
"DefenseMelee": "melee defense",
|
||||||
"FightingNimble": "nimble weapon",
|
"FightingNimble": "nimble weapon",
|
||||||
"MagicSpells": "spells"
|
"MagicSpells": "spells"
|
||||||
}
|
},
|
||||||
|
"NoLuckLeft": "No Luck Points remaining!",
|
||||||
|
"LuckRollPost": "Spend Luck",
|
||||||
|
"LuckResult": "Luck bonus:",
|
||||||
|
"NoBonus": "no bonus successes"
|
||||||
},
|
},
|
||||||
"Rune": {
|
"Rune": {
|
||||||
"Attached": "Rune \"{name}\" attached.",
|
"Attached": "Rune \"{name}\" attached.",
|
||||||
|
|||||||
@@ -169,6 +169,24 @@
|
|||||||
|
|
||||||
.res-sep { opacity: 0.7; font-size: @font-size-xs; }
|
.res-sep { opacity: 0.7; font-size: @font-size-xs; }
|
||||||
|
|
||||||
|
.grit-max-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
|
||||||
|
.res-bonus-label {
|
||||||
|
opacity: 0.6;
|
||||||
|
font-size: @font-size-xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.res-bonus-input {
|
||||||
|
width: 2.2rem;
|
||||||
|
opacity: 0.85;
|
||||||
|
border-left: 1px dashed @color-olive;
|
||||||
|
padding-left: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.character-resource--luck {
|
&.character-resource--luck {
|
||||||
.luck-stepper {
|
.luck-stepper {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -450,6 +450,17 @@
|
|||||||
.oh-miracle-dialog {
|
.oh-miracle-dialog {
|
||||||
|
|
||||||
// Spell/miracle header (reuses .spell-header, .weapon-img-sm, .weapon-name-lg, .weapon-badges)
|
// Spell/miracle header (reuses .spell-header, .weapon-img-sm, .weapon-name-lg, .weapon-badges)
|
||||||
|
.spell-header {
|
||||||
|
.weapon-img-sm {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
object-fit: contain;
|
||||||
|
border: 1px solid @color-olive-faint;
|
||||||
|
border-radius: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.dv-badge {
|
.dv-badge {
|
||||||
background: #3a0e6b;
|
background: #3a0e6b;
|
||||||
color: #e8d9ff;
|
color: #e8d9ff;
|
||||||
|
|||||||
@@ -115,3 +115,47 @@
|
|||||||
i { font-size: 0.85em; }
|
i { font-size: 0.85em; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Post-roll luck button and result
|
||||||
|
.oh-luck-btn-row {
|
||||||
|
margin-top: 6px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oh-post-luck-btn {
|
||||||
|
background: linear-gradient(135deg, #2d6a2d 0%, #4a8c4a 100%);
|
||||||
|
color: #e8d97a;
|
||||||
|
border: 1px solid #3a7a3a;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
cursor: pointer;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
transition: filter 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
filter: brightness(1.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.oh-luck-result {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 6px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: fade(#2d6a2d, 15%);
|
||||||
|
border-left: 3px solid #4a8c4a;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #2d6a2d;
|
||||||
|
|
||||||
|
.oh-luck-result-icon { font-size: 1.1em; }
|
||||||
|
|
||||||
|
.oh-luck-dice {
|
||||||
|
display: flex;
|
||||||
|
gap: 3px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
52
module/applications/luck-roll-dialog.mjs
Normal file
52
module/applications/luck-roll-dialog.mjs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* Dialog for spending Luck Points after a roll result is known.
|
||||||
|
* Called from the "🍀 Luck" button on chat cards.
|
||||||
|
*/
|
||||||
|
export default class OathHammerLuckRollDialog {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompt the actor's owner to spend luck points after seeing a roll result.
|
||||||
|
* @param {Actor} actor
|
||||||
|
* @returns {Promise<{luckSpend: number, luckIsHuman: boolean}|null>}
|
||||||
|
*/
|
||||||
|
static async prompt(actor) {
|
||||||
|
const actorSys = actor.system
|
||||||
|
const availableLuck = actorSys.luck?.value ?? 0
|
||||||
|
if (availableLuck <= 0) {
|
||||||
|
ui.notifications.warn(game.i18n.localize("OATHHAMMER.Roll.NoLuckLeft"))
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHuman = (actorSys.lineage?.name ?? "").toLowerCase() === "human"
|
||||||
|
const luckDicePerPoint = isHuman ? 3 : 2
|
||||||
|
|
||||||
|
const luckOptions = Array.from({ length: availableLuck + 1 }, (_, i) => ({
|
||||||
|
value: i,
|
||||||
|
label: i === 0 ? "0" : `${i} LP (+${i * luckDicePerPoint}d)`,
|
||||||
|
selected: i === 1,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const content = await foundry.applications.handlebars.renderTemplate(
|
||||||
|
"systems/fvtt-oath-hammer/templates/luck-roll-dialog.hbs",
|
||||||
|
{ availableLuck, isHuman, luckOptions }
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = await foundry.applications.api.DialogV2.wait({
|
||||||
|
window: { title: game.i18n.format("OATHHAMMER.Dialog.LuckPostRollTitle", { name: actor.name }) },
|
||||||
|
classes: ["fvtt-oath-hammer"],
|
||||||
|
content,
|
||||||
|
rejectClose: false,
|
||||||
|
buttons: [{
|
||||||
|
label: game.i18n.localize("OATHHAMMER.Dialog.LuckPostRollConfirm"),
|
||||||
|
callback: (_ev, btn) => {
|
||||||
|
const spend = parseInt(btn.form.elements.luckSpend?.value) || 0
|
||||||
|
const isH = btn.form.elements.luckIsHuman?.checked ?? false
|
||||||
|
return { luckSpend: spend, luckIsHuman: isH }
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result || result.luckSpend <= 0) return null
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -79,7 +79,7 @@ export default class OathHammerItemSheet extends HandlebarsApplicationMixin(foun
|
|||||||
)
|
)
|
||||||
// Weapon-specific numeric selects
|
// Weapon-specific numeric selects
|
||||||
context.damageModChoices = Object.fromEntries(
|
context.damageModChoices = Object.fromEntries(
|
||||||
Array.from({ length: 10 }, (_, i) => [i - 4, i - 4 >= 0 ? `+${i - 4}` : String(i - 4)])
|
Array.from({ length: 20 }, (_, i) => [i - 4, i - 4 >= 0 ? `+${i - 4}` : String(i - 4)])
|
||||||
)
|
)
|
||||||
context.apChoices = Object.fromEntries(
|
context.apChoices = Object.fromEntries(
|
||||||
Array.from({ length: 7 }, (_, i) => [i, String(i)])
|
Array.from({ length: 7 }, (_, i) => [i, String(i)])
|
||||||
|
|||||||
@@ -348,7 +348,7 @@ export default class OathHammerWeaponDialog {
|
|||||||
selected: i === defaultSV,
|
selected: i === defaultSV,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const damageBonusOptions = Array.from({ length: 9 }, (_, i) => {
|
const damageBonusOptions = Array.from({ length: 20 }, (_, i) => {
|
||||||
const v = i - 4
|
const v = i - 4
|
||||||
return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
|
return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default class OathHammerCharacter extends foundry.abstract.TypeDataModel
|
|||||||
})
|
})
|
||||||
|
|
||||||
const attributeField = () => new fields.SchemaField({
|
const attributeField = () => new fields.SchemaField({
|
||||||
rank: new fields.NumberField({ ...requiredInteger, initial: 1, min: 1, max: 4 })
|
rank: new fields.NumberField({ ...requiredInteger, initial: 1, min: 1 })
|
||||||
})
|
})
|
||||||
schema.attributes = new fields.SchemaField({
|
schema.attributes = new fields.SchemaField({
|
||||||
might: attributeField(),
|
might: attributeField(),
|
||||||
@@ -68,7 +68,8 @@ export default class OathHammerCharacter extends foundry.abstract.TypeDataModel
|
|||||||
|
|
||||||
schema.grit = new fields.SchemaField({
|
schema.grit = new fields.SchemaField({
|
||||||
value: new fields.NumberField({ ...requiredInteger, initial: 2, min: 0 }),
|
value: new fields.NumberField({ ...requiredInteger, initial: 2, min: 0 }),
|
||||||
max: new fields.NumberField({ ...requiredInteger, initial: 2, min: 0 })
|
max: new fields.NumberField({ ...requiredInteger, initial: 2, min: 0 }),
|
||||||
|
bonus: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Luck.max is derived from fate.rank; resets at session start.
|
// Luck.max is derived from fate.rank; resets at session start.
|
||||||
@@ -125,8 +126,8 @@ export default class OathHammerCharacter extends foundry.abstract.TypeDataModel
|
|||||||
|
|
||||||
prepareDerivedData() {
|
prepareDerivedData() {
|
||||||
super.prepareDerivedData()
|
super.prepareDerivedData()
|
||||||
// Grit max = Resilience skill rank + Toughness attribute rank (rulebook p.5)
|
// Grit max = Resilience skill rank + Toughness attribute rank + bonus (rulebook p.5)
|
||||||
this.grit.max = this.skills.resilience.rank + this.attributes.toughness.rank
|
this.grit.max = this.skills.resilience.rank + this.attributes.toughness.rank + (this.grit.bonus ?? 0)
|
||||||
// Luck max = Fate rank; restores at session start
|
// Luck max = Fate rank; restores at session start
|
||||||
this.luck.max = this.attributes.fate.rank
|
this.luck.max = this.attributes.fate.rank
|
||||||
// Defense score = 10 + Agility + Armor Rating + bonus
|
// Defense score = 10 + Agility + Armor Rating + bonus
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default class OathHammerWeapon extends foundry.abstract.TypeDataModel {
|
|||||||
|
|
||||||
// Damage: melee/throwing = Might rank + damageMod dice; bows = baseDice (fixed, no Might)
|
// Damage: melee/throwing = Might rank + damageMod dice; bows = baseDice (fixed, no Might)
|
||||||
// usesMight is now derived from proficiencyGroup (see getter below)
|
// usesMight is now derived from proficiencyGroup (see getter below)
|
||||||
schema.damageMod = new fields.NumberField({ ...requiredInteger, initial: 0, min: -4, max: 16 })
|
schema.damageMod = new fields.NumberField({ ...requiredInteger, initial: 0, min: -4, max: 15 })
|
||||||
|
|
||||||
// AP (Armor Penetration): penalty imposed on armor/defense rolls
|
// AP (Armor Penetration): penalty imposed on armor/defense rolls
|
||||||
schema.ap = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0, max: 16 })
|
schema.ap = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0, max: 16 })
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ export async function rollSkillCheck(actor, skillKey, dv, options = {}) {
|
|||||||
content,
|
content,
|
||||||
rolls: allRolls,
|
rolls: allRolls,
|
||||||
sound: CONFIG.sounds.dice,
|
sound: CONFIG.sounds.dice,
|
||||||
|
flags: { "fvtt-oath-hammer": { luckRoll: _luckFlagData(actor, threshold, colorType, dv, isOpposed, explodeOn5) } },
|
||||||
}
|
}
|
||||||
ChatMessage.applyRollMode(msgData, rollMode)
|
ChatMessage.applyRollMode(msgData, rollMode)
|
||||||
await ChatMessage.create(msgData)
|
await ChatMessage.create(msgData)
|
||||||
@@ -224,6 +225,59 @@ export function _diceHtml(diceResults, threshold) {
|
|||||||
}).join(" ")
|
}).join(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the luck flag data to store on a chat message, enabling post-roll luck spending.
|
||||||
|
* @param {Actor} actor
|
||||||
|
* @param {number} threshold Dice success threshold (2/3/4)
|
||||||
|
* @param {string} colorType "white"|"red"|"black"
|
||||||
|
* @param {number} dv Difficulty value (0 = opposed)
|
||||||
|
* @param {boolean} isOpposed
|
||||||
|
* @param {boolean} explodeOn5
|
||||||
|
*/
|
||||||
|
export function _luckFlagData(actor, threshold, colorType, dv, isOpposed, explodeOn5) {
|
||||||
|
return { actorUuid: actor.uuid, threshold, colorType, dv, isOpposed, explodeOn5 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a post-roll luck spend: roll extra dice and update the chat message.
|
||||||
|
* @param {ChatMessage} message
|
||||||
|
* @param {number} luckSpend
|
||||||
|
* @param {boolean} luckIsHuman
|
||||||
|
*/
|
||||||
|
export async function rollPostRollLuck(message, luckSpend, luckIsHuman) {
|
||||||
|
const flag = message.getFlag("fvtt-oath-hammer", "luckRoll")
|
||||||
|
if (!flag || flag.used || luckSpend <= 0) return
|
||||||
|
|
||||||
|
const actor = await fromUuid(flag.actorUuid)
|
||||||
|
if (!actor) return
|
||||||
|
|
||||||
|
const currentLuck = actor.system.luck?.value ?? 0
|
||||||
|
const safeSpend = Math.min(luckSpend, currentLuck)
|
||||||
|
if (safeSpend <= 0) {
|
||||||
|
ui.notifications.warn(game.i18n.localize("OATHHAMMER.Roll.NoLuckLeft"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const luckDicePerPoint = luckIsHuman ? 3 : 2
|
||||||
|
const extraDice = safeSpend * luckDicePerPoint
|
||||||
|
|
||||||
|
const { successes, diceResults } = await _rollPool(extraDice, flag.threshold, flag.explodeOn5)
|
||||||
|
|
||||||
|
await actor.update({ "system.luck.value": Math.max(0, currentLuck - safeSpend) })
|
||||||
|
|
||||||
|
const luckDiceHtml = _diceHtml(diceResults, flag.threshold)
|
||||||
|
|
||||||
|
await message.setFlag("fvtt-oath-hammer", "luckRoll", {
|
||||||
|
...flag,
|
||||||
|
used: true,
|
||||||
|
luckSpend: safeSpend,
|
||||||
|
luckIsHuman,
|
||||||
|
bonusSuccesses: successes,
|
||||||
|
extraDiceResults: diceResults,
|
||||||
|
luckDiceHtml,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// WEAPON ATTACK ROLL
|
// WEAPON ATTACK ROLL
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -309,7 +363,7 @@ export async function rollWeaponAttack(actor, weapon, options = {}) {
|
|||||||
content,
|
content,
|
||||||
rolls: rolls,
|
rolls: rolls,
|
||||||
sound: CONFIG.sounds.dice,
|
sound: CONFIG.sounds.dice,
|
||||||
flags: { "fvtt-oath-hammer": { weaponAttack: flagData } },
|
flags: { "fvtt-oath-hammer": { weaponAttack: flagData, luckRoll: _luckFlagData(actor, threshold, colorType, 0, true, explodeOn5) } },
|
||||||
}
|
}
|
||||||
ChatMessage.applyRollMode(msgData, rollMode)
|
ChatMessage.applyRollMode(msgData, rollMode)
|
||||||
await ChatMessage.create(msgData)
|
await ChatMessage.create(msgData)
|
||||||
@@ -506,11 +560,13 @@ export async function rollSpellCast(actor, spell, options = {}) {
|
|||||||
`
|
`
|
||||||
|
|
||||||
const rollMode = visibility ?? game.settings.get("core", "rollMode")
|
const rollMode = visibility ?? game.settings.get("core", "rollMode")
|
||||||
|
const spellColorType = redDice ? "red" : "white"
|
||||||
const msgData = {
|
const msgData = {
|
||||||
speaker: ChatMessage.getSpeaker({ actor }),
|
speaker: ChatMessage.getSpeaker({ actor }),
|
||||||
content,
|
content,
|
||||||
rolls: rolls,
|
rolls: rolls,
|
||||||
sound: CONFIG.sounds.dice,
|
sound: CONFIG.sounds.dice,
|
||||||
|
flags: { "fvtt-oath-hammer": { luckRoll: _luckFlagData(actor, threshold, spellColorType, dv, false, explodeOn5) } },
|
||||||
}
|
}
|
||||||
ChatMessage.applyRollMode(msgData, rollMode)
|
ChatMessage.applyRollMode(msgData, rollMode)
|
||||||
await ChatMessage.create(msgData)
|
await ChatMessage.create(msgData)
|
||||||
@@ -608,6 +664,7 @@ export async function rollMiracleCast(actor, miracle, options = {}) {
|
|||||||
content,
|
content,
|
||||||
rolls: rolls,
|
rolls: rolls,
|
||||||
sound: CONFIG.sounds.dice,
|
sound: CONFIG.sounds.dice,
|
||||||
|
flags: { "fvtt-oath-hammer": { luckRoll: _luckFlagData(actor, threshold, "white", dv, false, explodeOn5) } },
|
||||||
}
|
}
|
||||||
ChatMessage.applyRollMode(msgData, rollMode)
|
ChatMessage.applyRollMode(msgData, rollMode)
|
||||||
await ChatMessage.create(msgData)
|
await ChatMessage.create(msgData)
|
||||||
@@ -683,11 +740,13 @@ export async function rollDefense(actor, options = {}) {
|
|||||||
`
|
`
|
||||||
|
|
||||||
const rollMode = visibility ?? game.settings.get("core", "rollMode")
|
const rollMode = visibility ?? game.settings.get("core", "rollMode")
|
||||||
|
const defColorType = redDice ? "red" : "white"
|
||||||
const msgData = {
|
const msgData = {
|
||||||
speaker: ChatMessage.getSpeaker({ actor }),
|
speaker: ChatMessage.getSpeaker({ actor }),
|
||||||
content,
|
content,
|
||||||
rolls: rolls,
|
rolls: rolls,
|
||||||
sound: CONFIG.sounds.dice,
|
sound: CONFIG.sounds.dice,
|
||||||
|
flags: { "fvtt-oath-hammer": { luckRoll: _luckFlagData(actor, threshold, defColorType, 0, true, false) } },
|
||||||
}
|
}
|
||||||
ChatMessage.applyRollMode(msgData, rollMode)
|
ChatMessage.applyRollMode(msgData, rollMode)
|
||||||
await ChatMessage.create(msgData)
|
await ChatMessage.create(msgData)
|
||||||
@@ -786,6 +845,7 @@ export async function rollWeaponDefense(actor, weapon, options = {}) {
|
|||||||
content,
|
content,
|
||||||
rolls: rolls,
|
rolls: rolls,
|
||||||
sound: CONFIG.sounds.dice,
|
sound: CONFIG.sounds.dice,
|
||||||
|
flags: { "fvtt-oath-hammer": { luckRoll: _luckFlagData(actor, threshold, colorOverride, 0, true, explodeOn5) } },
|
||||||
}
|
}
|
||||||
ChatMessage.applyRollMode(msgData, rollMode)
|
ChatMessage.applyRollMode(msgData, rollMode)
|
||||||
await ChatMessage.create(msgData)
|
await ChatMessage.create(msgData)
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import OathHammerUtils from "./module/utils.mjs"
|
|||||||
import OathHammerWeaponDialog from "./module/applications/weapon-dialog.mjs"
|
import OathHammerWeaponDialog from "./module/applications/weapon-dialog.mjs"
|
||||||
import OathHammerCombat from "./module/combat.mjs"
|
import OathHammerCombat from "./module/combat.mjs"
|
||||||
import { rollWeaponDamage } from "./module/rolls.mjs"
|
import { rollWeaponDamage } from "./module/rolls.mjs"
|
||||||
|
import { rollPostRollLuck } from "./module/rolls.mjs"
|
||||||
import { injectFreeRollBar } from "./module/applications/free-roll.mjs"
|
import { injectFreeRollBar } from "./module/applications/free-roll.mjs"
|
||||||
|
import OathHammerLuckRollDialog from "./module/applications/luck-roll-dialog.mjs"
|
||||||
|
|
||||||
Hooks.once("init", function () {
|
Hooks.once("init", function () {
|
||||||
console.info(SYSTEM.ASCII)
|
console.info(SYSTEM.ASCII)
|
||||||
@@ -152,19 +154,64 @@ Hooks.on("preCreateActor", (actor, _data, _options, _userId) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Handle "Roll Damage" button in weapon attack chat cards
|
// Handle "Roll Damage" button in weapon attack chat cards
|
||||||
Hooks.on("renderChatMessageHTML", (message, html) => {
|
Hooks.on("renderChatMessageHTML", async (message, html) => {
|
||||||
|
// Weapon damage button
|
||||||
const btn = html.querySelector("[data-action=\"rollWeaponDamage\"]")
|
const btn = html.querySelector("[data-action=\"rollWeaponDamage\"]")
|
||||||
if (!btn) return
|
if (btn) {
|
||||||
btn.addEventListener("click", async () => {
|
btn.addEventListener("click", async () => {
|
||||||
const flagData = message.getFlag("fvtt-oath-hammer", "weaponAttack")
|
const flagData = message.getFlag("fvtt-oath-hammer", "weaponAttack")
|
||||||
if (!flagData) return
|
if (!flagData) return
|
||||||
const { actorUuid, weaponUuid, attackSuccesses } = flagData
|
const { actorUuid, weaponUuid, attackSuccesses } = flagData
|
||||||
const actor = await fromUuid(actorUuid)
|
const actor = await fromUuid(actorUuid)
|
||||||
const weapon = await fromUuid(weaponUuid)
|
const weapon = await fromUuid(weaponUuid)
|
||||||
if (!actor || !weapon) return ui.notifications.warn(game.i18n.localize("OATHHAMMER.Roll.NoActor"))
|
if (!actor || !weapon) return ui.notifications.warn(game.i18n.localize("OATHHAMMER.Roll.NoActor"))
|
||||||
const opts = await OathHammerWeaponDialog.promptDamage(actor, weapon, attackSuccesses ?? 0)
|
const opts = await OathHammerWeaponDialog.promptDamage(actor, weapon, attackSuccesses ?? 0)
|
||||||
if (opts) await rollWeaponDamage(actor, weapon, opts)
|
if (opts) await rollWeaponDamage(actor, weapon, opts)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Luck post-roll button
|
||||||
|
const luckFlag = message.getFlag("fvtt-oath-hammer", "luckRoll")
|
||||||
|
if (!luckFlag) return
|
||||||
|
|
||||||
|
const resultDiv = html.querySelector(".oh-roll-result")
|
||||||
|
if (!resultDiv) return
|
||||||
|
|
||||||
|
if (luckFlag.used) {
|
||||||
|
// Show luck result section
|
||||||
|
const bonusLabel = luckFlag.bonusSuccesses > 0
|
||||||
|
? `+${luckFlag.bonusSuccesses} ${game.i18n.localize("OATHHAMMER.Roll.Successes")}`
|
||||||
|
: game.i18n.localize("OATHHAMMER.Roll.NoBonus")
|
||||||
|
const resultHtml = `
|
||||||
|
<div class="oh-luck-result">
|
||||||
|
<span class="oh-luck-result-icon">🍀</span>
|
||||||
|
<span>${game.i18n.localize("OATHHAMMER.Roll.LuckResult")} ${bonusLabel}</span>
|
||||||
|
<span class="oh-luck-dice">${luckFlag.luckDiceHtml ?? ""}</span>
|
||||||
|
</div>`
|
||||||
|
resultDiv.insertAdjacentHTML("afterend", resultHtml)
|
||||||
|
} else {
|
||||||
|
// Show "Spend Luck" button if actor owns the message and has luck left
|
||||||
|
const actor = await fromUuid(luckFlag.actorUuid).catch(() => null)
|
||||||
|
if (!actor?.isOwner) return
|
||||||
|
const availableLuck = actor.system.luck?.value ?? 0
|
||||||
|
if (availableLuck <= 0) return
|
||||||
|
|
||||||
|
const btnHtml = `
|
||||||
|
<div class="oh-luck-btn-row">
|
||||||
|
<button type="button" class="oh-post-luck-btn" data-action="postRollLuck">
|
||||||
|
🍀 ${game.i18n.localize("OATHHAMMER.Roll.LuckRollPost")}
|
||||||
|
</button>
|
||||||
|
</div>`
|
||||||
|
resultDiv.insertAdjacentHTML("afterend", btnHtml)
|
||||||
|
|
||||||
|
html.querySelector("[data-action=\"postRollLuck\"]")?.addEventListener("click", async () => {
|
||||||
|
const actor = await fromUuid(luckFlag.actorUuid).catch(() => null)
|
||||||
|
if (!actor) return
|
||||||
|
const opts = await OathHammerLuckRollDialog.prompt(actor)
|
||||||
|
if (!opts) return
|
||||||
|
await rollPostRollLuck(message, opts.luckSpend, opts.luckIsHuman)
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Inject Free Roll bar into the chat sidebar
|
// Inject Free Roll bar into the chat sidebar
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
MANIFEST-000017
|
MANIFEST-000021
|
||||||
|
|||||||
Binary file not shown.
@@ -2,7 +2,7 @@
|
|||||||
"id": "fvtt-oath-hammer",
|
"id": "fvtt-oath-hammer",
|
||||||
"title": "Oath Hammer RPG",
|
"title": "Oath Hammer RPG",
|
||||||
"description": "Oath Hammer RPG System for FoundryVTT",
|
"description": "Oath Hammer RPG System for FoundryVTT",
|
||||||
"version": "14.0.0",
|
"version": "14.0.1",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"name": "Uberwald",
|
"name": "Uberwald",
|
||||||
@@ -156,8 +156,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"url": "https://www.uberwald.me/gitea/uberwald/fvtt-oath-hammer",
|
"url": "https://www.uberwald.me/gitea/uberwald/fvtt-oath-hammer",
|
||||||
"manifest": "https://www.uberwald.me/gitea/public/fvtt-oath-hammer/releases/download/latest/system.json",
|
"manifest": "https://www.uberwald.me/gitea/uberwald/fvtt-oath-hammer/releases/download/latest/system.json",
|
||||||
"download": "https://www.uberwald.me/gitea/public/fvtt-oath-hammer/releases/download/latest/fvtt-oath-hammer.zip",
|
"download": "https://www.uberwald.me/gitea/uberwald/fvtt-oath-hammer/releases/download/14.0.1/fvtt-oath-hammer.zip",
|
||||||
"packs": [
|
"packs": [
|
||||||
{
|
{
|
||||||
"label": "Scenes",
|
"label": "Scenes",
|
||||||
|
|||||||
@@ -56,7 +56,13 @@
|
|||||||
<a data-action="adjustGrit" data-delta="1" class="grit-btn">+</a>
|
<a data-action="adjustGrit" data-delta="1" class="grit-btn">+</a>
|
||||||
</div>
|
</div>
|
||||||
<span class="res-sep">/</span>
|
<span class="res-sep">/</span>
|
||||||
{{formInput systemFields.grit.fields.max value=system.grit.max name="system.grit.max" disabled=isPlayMode}}
|
<span class="grit-max-group">
|
||||||
|
{{formInput systemFields.grit.fields.max value=system.grit.max name="system.grit.max" disabled=true}}
|
||||||
|
{{#unless isPlayMode}}
|
||||||
|
<span class="res-bonus-label">+</span>
|
||||||
|
{{formInput systemFields.grit.fields.bonus value=system.grit.bonus name="system.grit.bonus" class="res-bonus-input"}}
|
||||||
|
{{/unless}}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="character-resource character-resource--luck">
|
<div class="character-resource character-resource--luck">
|
||||||
<span class="resource-label">{{localize "OATHHAMMER.Label.Luck"}}</span>
|
<span class="resource-label">{{localize "OATHHAMMER.Label.Luck"}}</span>
|
||||||
|
|||||||
30
templates/luck-roll-dialog.hbs
Normal file
30
templates/luck-roll-dialog.hbs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<div class="oh-roll-dialog oh-luck-dialog">
|
||||||
|
|
||||||
|
<div class="luck-dialog-info">
|
||||||
|
<i class="fa-solid fa-clover luck-icon"></i>
|
||||||
|
<span>{{localize "OATHHAMMER.Dialog.LuckPostRollHint"}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<fieldset class="attack-options-block">
|
||||||
|
<legend>{{localize "OATHHAMMER.Dialog.LuckSpend"}}</legend>
|
||||||
|
|
||||||
|
<div class="pool-info-line">
|
||||||
|
{{localize "OATHHAMMER.Label.LuckAvailable"}}: <strong>{{availableLuck}}</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="roll-option-row">
|
||||||
|
<label>{{localize "OATHHAMMER.Dialog.LuckPoints"}}</label>
|
||||||
|
<select name="luckSpend">
|
||||||
|
{{#each luckOptions}}<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>{{/each}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="roll-option-row roll-option-check">
|
||||||
|
<label for="postLuckIsHuman">{{localize "OATHHAMMER.Dialog.LuckHuman"}} <i class="fa-solid fa-clover luck-icon"></i></label>
|
||||||
|
<input type="checkbox" id="postLuckIsHuman" name="luckIsHuman" value="true" {{#if isHuman}}checked{{/if}} />
|
||||||
|
<span class="roll-option-hint">{{localize "OATHHAMMER.Dialog.LuckHint"}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user