Add roll windows from actor sheet
This commit is contained in:
@@ -9,6 +9,9 @@ export { default as OathHammerMiracleSheet } from "./sheets/miracle-sheet.mjs"
|
||||
export { default as OathHammerMagicItemSheet } from "./sheets/magic-item-sheet.mjs"
|
||||
export { default as OathHammerTraitSheet } from "./sheets/trait-sheet.mjs"
|
||||
export { default as OathHammerOathSheet } from "./sheets/oath-sheet.mjs"
|
||||
export { default as OathHammerLineageSheet } from "./sheets/lineage-sheet.mjs"
|
||||
export { default as OathHammerClassSheet } from "./sheets/class-sheet.mjs"
|
||||
export { default as OathHammerBuildingSheet } from "./sheets/building-sheet.mjs"
|
||||
export { default as OathHammerRollDialog } from "./roll-dialog.mjs"
|
||||
export { default as OathHammerWeaponDialog } from "./weapon-dialog.mjs"
|
||||
export { default as OathHammerSpellDialog } from "./spell-dialog.mjs"
|
||||
export { default as OathHammerMiracleDialog } from "./miracle-dialog.mjs"
|
||||
|
||||
89
module/applications/miracle-dialog.mjs
Normal file
89
module/applications/miracle-dialog.mjs
Normal file
@@ -0,0 +1,89 @@
|
||||
import { SYSTEM } from "../config/system.mjs"
|
||||
|
||||
export default class OathHammerMiracleDialog {
|
||||
|
||||
static async prompt(actor, miracle) {
|
||||
const sys = miracle.system
|
||||
const actorSys = actor.system
|
||||
|
||||
const wpRank = actorSys.attributes.willpower.rank
|
||||
const magicRank = actorSys.skills.magic.rank
|
||||
const basePool = wpRank + magicRank
|
||||
|
||||
const isRitual = sys.isRitual
|
||||
const dv = isRitual ? (sys.difficultyValue || 1) : null
|
||||
|
||||
const traditionLabel = (() => {
|
||||
const key = SYSTEM.DIVINE_TRADITIONS?.[sys.divineTradition]
|
||||
return key ? game.i18n.localize(key) : sys.divineTradition
|
||||
})()
|
||||
|
||||
// Miracle count options — DV = miracle number today
|
||||
const miracleCountOptions = Array.from({ length: 10 }, (_, i) => ({
|
||||
value: i + 1,
|
||||
label: `${i + 1}${_ordinal(i + 1)} — DV${i + 1}`,
|
||||
selected: i === 0,
|
||||
}))
|
||||
|
||||
const bonusOptions = Array.from({ length: 13 }, (_, i) => {
|
||||
const v = i - 6
|
||||
return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
|
||||
})
|
||||
|
||||
const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
|
||||
|
||||
const context = {
|
||||
actorName: actor.name,
|
||||
miracleName: miracle.name,
|
||||
miracleImg: miracle.img,
|
||||
dv,
|
||||
traditionLabel,
|
||||
isRitual,
|
||||
range: sys.range,
|
||||
duration: sys.duration,
|
||||
spellSave: sys.spellSave,
|
||||
wpRank,
|
||||
magicRank,
|
||||
basePool,
|
||||
miracleCountOptions,
|
||||
bonusOptions,
|
||||
rollModes,
|
||||
visibility: game.settings.get("core", "rollMode"),
|
||||
}
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-oath-hammer/templates/miracle-cast-dialog.hbs",
|
||||
context
|
||||
)
|
||||
|
||||
const result = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: game.i18n.format("OATHHAMMER.Dialog.MiracleCastTitle", { miracle: miracle.name }) },
|
||||
classes: ["fvtt-oath-hammer"],
|
||||
content,
|
||||
rejectClose: false,
|
||||
buttons: [{
|
||||
label: game.i18n.localize("OATHHAMMER.Dialog.InvokeMiracle"),
|
||||
callback: (_ev, btn) => Object.fromEntries(
|
||||
[...btn.form.elements].filter(e => e.name).map(e => [e.name, e.value])
|
||||
),
|
||||
}],
|
||||
})
|
||||
|
||||
if (!result) return null
|
||||
|
||||
const computedDV = isRitual ? dv : (parseInt(result.miracleCount) || 1)
|
||||
|
||||
return {
|
||||
dv: computedDV,
|
||||
isRitual,
|
||||
bonus: parseInt(result.bonus) || 0,
|
||||
visibility: result.visibility ?? game.settings.get("core", "rollMode"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _ordinal(n) {
|
||||
const s = ["th", "st", "nd", "rd"]
|
||||
const v = n % 100
|
||||
return s[(v - 20) % 10] ?? s[v] ?? s[0]
|
||||
}
|
||||
154
module/applications/roll-dialog.mjs
Normal file
154
module/applications/roll-dialog.mjs
Normal file
@@ -0,0 +1,154 @@
|
||||
import { SYSTEM } from "../config/system.mjs"
|
||||
|
||||
/**
|
||||
* Roll configuration dialog for Oath Hammer skill checks.
|
||||
* Uses DialogV2.wait() — pattern from fvtt-hellborn / fvtt-lethal-fantasy.
|
||||
*
|
||||
* Dice rules (from rulebook):
|
||||
* Pool = Attribute rank + Skill rank + per-skill modifier + bonus + (luckSpend × 2) + supporters
|
||||
* White dice: succeed on 4+ | Red: 3+ | Black: 2+
|
||||
* All dice explode on 6 (roll extra die).
|
||||
* Luck Points: spending 1 LP adds +2 dice; LP restored each session.
|
||||
* Supporters: each ally with ranks in the skill adds +1 die.
|
||||
*/
|
||||
export default class OathHammerRollDialog {
|
||||
/**
|
||||
* Dual-attribute skills: show an alternate attribute option in the dialog.
|
||||
* key → { primary, alt, altLabel i18n key }
|
||||
*/
|
||||
static DUAL_ATTRIBUTE_SKILLS = {
|
||||
defense: { alt: "might", altLabelKey: "OATHHAMMER.Roll.DualAttr.DefenseMelee" },
|
||||
fighting: { alt: "agility", altLabelKey: "OATHHAMMER.Roll.DualAttr.FightingNimble" },
|
||||
magic: { alt: "intelligence", altLabelKey: "OATHHAMMER.Roll.DualAttr.MagicSpells" },
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a skill check dialog and return the user's choices.
|
||||
*
|
||||
* @param {Actor} actor Actor performing the check
|
||||
* @param {string} skillKey SYSTEM.SKILLS key (e.g. "fortune", "fighting")
|
||||
* @returns {Promise<{dv, bonus, luckSpend, supporters, attrOverride, visibility}|null>}
|
||||
* Resolved options, or null if the dialog was cancelled.
|
||||
*/
|
||||
static async prompt(actor, skillKey) {
|
||||
const sys = actor.system
|
||||
const skillDef = SYSTEM.SKILLS[skillKey]
|
||||
if (!skillDef) throw new Error(`Unknown skill: ${skillKey}`)
|
||||
|
||||
const defaultAttrKey = skillDef.attribute
|
||||
const attrRank = sys.attributes[defaultAttrKey].rank
|
||||
const skill = sys.skills[skillKey]
|
||||
const skillRank = skill.rank
|
||||
const skillMod = skill.modifier ?? 0
|
||||
const baseTotal = attrRank + skillRank + skillMod
|
||||
const colorType = skill.colorDiceType ?? "white"
|
||||
const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4
|
||||
const colorLabel = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜"
|
||||
|
||||
const dualDef = this.DUAL_ATTRIBUTE_SKILLS[skillKey]
|
||||
// Build attribute options for dual-attribute skills
|
||||
let attrOptions = null
|
||||
if (dualDef) {
|
||||
const altRank = sys.attributes[dualDef.alt].rank
|
||||
attrOptions = [
|
||||
{
|
||||
value: defaultAttrKey,
|
||||
label: `${game.i18n.localize(`OATHHAMMER.Attribute.${defaultAttrKey.charAt(0).toUpperCase()}${defaultAttrKey.slice(1)}`)} (${attrRank}) — default`,
|
||||
selected: true,
|
||||
},
|
||||
{
|
||||
value: dualDef.alt,
|
||||
label: `${game.i18n.localize(`OATHHAMMER.Attribute.${dualDef.alt.charAt(0).toUpperCase()}${dualDef.alt.slice(1)}`)} (${altRank}) — ${game.i18n.localize(dualDef.altLabelKey)}`,
|
||||
selected: false,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const availableLuck = sys.luck?.value ?? 0
|
||||
const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
|
||||
|
||||
// Build select option arrays
|
||||
const dvOptions = Array.from({ length: 10 }, (_, i) => {
|
||||
const v = i + 1
|
||||
return { value: v, label: String(v), selected: v === 2 }
|
||||
})
|
||||
|
||||
const bonusOptions = Array.from({ length: 13 }, (_, i) => {
|
||||
const v = i - 6
|
||||
return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
|
||||
})
|
||||
|
||||
const supportersOptions = Array.from({ length: 7 }, (_, i) => ({
|
||||
value: i, label: String(i), selected: i === 0,
|
||||
}))
|
||||
|
||||
const luckOptions = Array.from({ length: availableLuck + 1 }, (_, i) => ({
|
||||
value: i,
|
||||
label: i === 0 ? `0` : `${i} (+${i * 2}d)`,
|
||||
selected: i === 0,
|
||||
}))
|
||||
|
||||
const context = {
|
||||
actorName: actor.name,
|
||||
skillKey,
|
||||
skillLabel: game.i18n.localize(skillDef.label),
|
||||
attrKey: defaultAttrKey,
|
||||
attrLabel: game.i18n.localize(`OATHHAMMER.Attribute.${defaultAttrKey.charAt(0).toUpperCase()}${defaultAttrKey.slice(1)}`),
|
||||
attrRank,
|
||||
skillRank,
|
||||
skillMod,
|
||||
baseTotal,
|
||||
colorType,
|
||||
colorLabel,
|
||||
threshold,
|
||||
availableLuck,
|
||||
attrOptions,
|
||||
isDualAttr: !!dualDef,
|
||||
rollModes,
|
||||
visibility: game.settings.get("core", "rollMode"),
|
||||
dvOptions,
|
||||
bonusOptions,
|
||||
supportersOptions,
|
||||
luckOptions,
|
||||
}
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-oath-hammer/templates/roll-dialog.hbs",
|
||||
context
|
||||
)
|
||||
|
||||
const title = game.i18n.format("OATHHAMMER.Dialog.SkillCheckTitle", { skill: context.skillLabel })
|
||||
|
||||
const result = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title },
|
||||
classes: ["fvtt-oath-hammer"],
|
||||
content,
|
||||
rejectClose: false,
|
||||
buttons: [
|
||||
{
|
||||
label: game.i18n.localize("OATHHAMMER.Dialog.Roll"),
|
||||
callback: (_event, button) => {
|
||||
const out = {}
|
||||
for (const el of button.form.elements) {
|
||||
if (el.name) out[el.name] = el.value
|
||||
}
|
||||
return out
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (!result) return null
|
||||
|
||||
const attrOverride = result.attrOverride || defaultAttrKey
|
||||
|
||||
return {
|
||||
dv: Math.max(1, parseInt(result.dv) || 2),
|
||||
bonus: parseInt(result.bonus) || 0,
|
||||
luckSpend: Math.min(Math.max(0, parseInt(result.luckSpend) || 0), availableLuck),
|
||||
supporters: Math.max(0, parseInt(result.supporters) || 0),
|
||||
attrOverride,
|
||||
visibility: result.visibility ?? game.settings.get("core", "rollMode"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,8 +95,8 @@ export default class OathHammerActorSheet extends HandlebarsApplicationMixin(fou
|
||||
|
||||
async _onDropItem(item) {
|
||||
const itemData = item.toObject()
|
||||
// Lineage and class are unique: replace any existing item of the same type
|
||||
if (item.type === "lineage" || item.type === "class") {
|
||||
// Class is unique: replace any existing item of the same type
|
||||
if (item.type === "class") {
|
||||
const existing = this.document.itemTypes[item.type]
|
||||
if (existing.length > 0) {
|
||||
await this.document.deleteEmbeddedDocuments("Item", existing.map(i => i.id))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const { HandlebarsApplicationMixin } = foundry.applications.api
|
||||
import { ARMOR_TYPE_CHOICES, WEAPON_PROFICIENCY_GROUPS } from "../../config/system.mjs"
|
||||
import { rollRarityCheck } from "../../rolls.mjs"
|
||||
|
||||
export default class OathHammerItemSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ItemSheetV2) {
|
||||
static SHEET_MODES = { EDIT: 0, PLAY: 1 }
|
||||
@@ -28,6 +29,7 @@ export default class OathHammerItemSheet extends HandlebarsApplicationMixin(foun
|
||||
actions: {
|
||||
toggleSheet: OathHammerItemSheet.#onToggleSheet,
|
||||
editImage: OathHammerItemSheet.#onEditImage,
|
||||
rollRarity: OathHammerItemSheet.#onRollRarity,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -134,4 +136,20 @@ export default class OathHammerItemSheet extends HandlebarsApplicationMixin(foun
|
||||
})
|
||||
return fp.browse()
|
||||
}
|
||||
|
||||
static async #onRollRarity(event, target) {
|
||||
const rarity = this.document.system.rarity
|
||||
if (!rarity) return
|
||||
// Find the owning actor (embedded item) or prompt user to select a character
|
||||
let actor = this.document.parent
|
||||
if (!actor || actor.documentName !== "Actor") {
|
||||
// Item not embedded — use the user's selected character
|
||||
actor = game.user.character
|
||||
if (!actor) {
|
||||
ui.notifications.warn(game.i18n.localize("OATHHAMMER.Roll.NoActor"))
|
||||
return
|
||||
}
|
||||
}
|
||||
await rollRarityCheck(actor, rarity, this.document.name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import OathHammerActorSheet from "./base-actor-sheet.mjs"
|
||||
import { SYSTEM } from "../../config/system.mjs"
|
||||
import OathHammerRollDialog from "../roll-dialog.mjs"
|
||||
import OathHammerWeaponDialog from "../weapon-dialog.mjs"
|
||||
import OathHammerSpellDialog from "../spell-dialog.mjs"
|
||||
import OathHammerMiracleDialog from "../miracle-dialog.mjs"
|
||||
import { rollSkillCheck, rollWeaponAttack, rollWeaponDamage, rollSpellCast, rollMiracleCast } from "../../rolls.mjs"
|
||||
|
||||
export default class OathHammerCharacterSheet extends OathHammerActorSheet {
|
||||
/** @override */
|
||||
@@ -13,10 +18,15 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
|
||||
contentClasses: ["character-content"],
|
||||
},
|
||||
actions: {
|
||||
createWeapon: OathHammerCharacterSheet.#onCreateWeapon,
|
||||
createSpell: OathHammerCharacterSheet.#onCreateSpell,
|
||||
createMiracle: OathHammerCharacterSheet.#onCreateMiracle,
|
||||
createWeapon: OathHammerCharacterSheet.#onCreateWeapon,
|
||||
createSpell: OathHammerCharacterSheet.#onCreateSpell,
|
||||
createMiracle: OathHammerCharacterSheet.#onCreateMiracle,
|
||||
createEquipment: OathHammerCharacterSheet.#onCreateEquipment,
|
||||
rollSkill: OathHammerCharacterSheet.#onRollSkill,
|
||||
attackWeapon: OathHammerCharacterSheet.#onAttackWeapon,
|
||||
damageWeapon: OathHammerCharacterSheet.#onDamageWeapon,
|
||||
castSpell: OathHammerCharacterSheet.#onCastSpell,
|
||||
castMiracle: OathHammerCharacterSheet.#onCastMiracle,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -57,9 +67,8 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.tabs = this.#getTabs()
|
||||
// lineage/class/experience available to all parts (header + identity tab)
|
||||
// class/experience available to all parts (header + identity tab)
|
||||
const doc = this.document
|
||||
context.lineage = doc.itemTypes.lineage?.[0] ?? null
|
||||
context.characterClass = doc.itemTypes["class"]?.[0] ?? null
|
||||
return context
|
||||
}
|
||||
@@ -236,4 +245,52 @@ export default class OathHammerCharacterSheet extends OathHammerActorSheet {
|
||||
static #onCreateEquipment(event, target) {
|
||||
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("OATHHAMMER.NewItem.Equipment"), type: "equipment" }])
|
||||
}
|
||||
|
||||
static async #onRollSkill(event, target) {
|
||||
const skillKey = target.dataset.skill
|
||||
if (!skillKey) return
|
||||
const result = await OathHammerRollDialog.prompt(this.document, skillKey)
|
||||
if (!result) return
|
||||
await rollSkillCheck(this.document, skillKey, result.dv, result)
|
||||
}
|
||||
|
||||
static async #onAttackWeapon(event, target) {
|
||||
const weaponId = target.dataset.itemId
|
||||
if (!weaponId) return
|
||||
const weapon = this.document.items.get(weaponId)
|
||||
if (!weapon) return
|
||||
const opts = await OathHammerWeaponDialog.promptAttack(this.document, weapon)
|
||||
if (!opts) return
|
||||
await rollWeaponAttack(this.document, weapon, opts)
|
||||
}
|
||||
|
||||
static async #onDamageWeapon(event, target) {
|
||||
const weaponId = target.dataset.itemId
|
||||
if (!weaponId) return
|
||||
const weapon = this.document.items.get(weaponId)
|
||||
if (!weapon) return
|
||||
const opts = await OathHammerWeaponDialog.promptDamage(this.document, weapon, 0)
|
||||
if (!opts) return
|
||||
await rollWeaponDamage(this.document, weapon, opts)
|
||||
}
|
||||
|
||||
static async #onCastSpell(event, target) {
|
||||
const spellId = target.dataset.itemId
|
||||
if (!spellId) return
|
||||
const spell = this.document.items.get(spellId)
|
||||
if (!spell) return
|
||||
const opts = await OathHammerSpellDialog.prompt(this.document, spell)
|
||||
if (!opts) return
|
||||
await rollSpellCast(this.document, spell, opts)
|
||||
}
|
||||
|
||||
static async #onCastMiracle(event, target) {
|
||||
const miracleId = target.dataset.itemId
|
||||
if (!miracleId) return
|
||||
const miracle = this.document.items.get(miracleId)
|
||||
if (!miracle) return
|
||||
const opts = await OathHammerMiracleDialog.prompt(this.document, miracle)
|
||||
if (!opts) return
|
||||
await rollMiracleCast(this.document, miracle, opts)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import OathHammerItemSheet from "./base-item-sheet.mjs"
|
||||
|
||||
export default class OathHammerLineageSheet extends OathHammerItemSheet {
|
||||
/** @override */
|
||||
static DEFAULT_OPTIONS = {
|
||||
classes: ["lineage"],
|
||||
position: {
|
||||
width: 640,
|
||||
},
|
||||
window: {
|
||||
contentClasses: ["lineage-content"],
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static PARTS = {
|
||||
main: {
|
||||
template: "systems/fvtt-oath-hammer/templates/item/lineage-sheet.hbs",
|
||||
},
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.enrichedTraits = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||
this.document.system.traits ?? "", { async: true }
|
||||
)
|
||||
return context
|
||||
}
|
||||
}
|
||||
118
module/applications/spell-dialog.mjs
Normal file
118
module/applications/spell-dialog.mjs
Normal file
@@ -0,0 +1,118 @@
|
||||
import { SYSTEM } from "../config/system.mjs"
|
||||
|
||||
/**
|
||||
* Spell enhancements — applied before casting (p.96-97).
|
||||
* stress: Arcane Stress cost regardless of roll result
|
||||
* penalty: dice pool penalty
|
||||
* redDice: true = roll red dice (3+ threshold) on the check
|
||||
* noStress: true = 1s rolled do NOT add Arcane Stress (Safe Spell)
|
||||
*/
|
||||
export const SPELL_ENHANCEMENTS = {
|
||||
none: { label: "OATHHAMMER.Enhancement.None", stress: 0, penalty: 0, redDice: false, noStress: false },
|
||||
focused: { label: "OATHHAMMER.Enhancement.Focused", stress: 1, penalty: 0, redDice: true, noStress: false },
|
||||
controlled: { label: "OATHHAMMER.Enhancement.Controlled", stress: 1, penalty: 0, redDice: false, noStress: false },
|
||||
empowered: { label: "OATHHAMMER.Enhancement.Empowered", stress: 2, penalty: 0, redDice: false, noStress: false },
|
||||
extended: { label: "OATHHAMMER.Enhancement.Extended", stress: 1, penalty: -1, redDice: false, noStress: false },
|
||||
penetrating: { label: "OATHHAMMER.Enhancement.Penetrating", stress: 1, penalty: -1, redDice: false, noStress: false },
|
||||
lethal: { label: "OATHHAMMER.Enhancement.Lethal", stress: 2, penalty: -2, redDice: false, noStress: false },
|
||||
hastened: { label: "OATHHAMMER.Enhancement.Hastened", stress: 3, penalty: -3, redDice: false, noStress: false },
|
||||
safe: { label: "OATHHAMMER.Enhancement.Safe", stress: 0, penalty: -3, redDice: false, noStress: true },
|
||||
}
|
||||
|
||||
export default class OathHammerSpellDialog {
|
||||
|
||||
static async prompt(actor, spell) {
|
||||
const sys = spell.system
|
||||
const actorSys = actor.system
|
||||
|
||||
const intRank = actorSys.attributes.intelligence.rank
|
||||
const magicRank = actorSys.skills.magic.rank
|
||||
const basePool = intRank + magicRank
|
||||
|
||||
const currentStress = actorSys.arcaneStress.value
|
||||
const stressThreshold = actorSys.arcaneStress.threshold
|
||||
const isOverThreshold = currentStress >= stressThreshold
|
||||
|
||||
const isElemental = sys.tradition === "elemental"
|
||||
const dv = sys.difficultyValue
|
||||
|
||||
const traditionLabel = (() => {
|
||||
const entry = SYSTEM.SORCEROUS_TRADITIONS?.[sys.tradition]
|
||||
return entry ? game.i18n.localize(entry.label) : (sys.tradition ?? "")
|
||||
})()
|
||||
|
||||
const enhancementOptions = Object.entries(SPELL_ENHANCEMENTS).map(([key, def]) => ({
|
||||
value: key,
|
||||
label: game.i18n.localize(def.label),
|
||||
selected: key === "none",
|
||||
}))
|
||||
|
||||
const bonusOptions = Array.from({ length: 13 }, (_, i) => {
|
||||
const v = i - 6
|
||||
return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
|
||||
})
|
||||
|
||||
const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
|
||||
|
||||
const context = {
|
||||
actorName: actor.name,
|
||||
spellName: spell.name,
|
||||
spellImg: spell.img,
|
||||
dv,
|
||||
traditionLabel,
|
||||
isRitual: sys.isRitual,
|
||||
isMagicMissile: sys.isMagicMissile,
|
||||
range: sys.range,
|
||||
duration: sys.duration,
|
||||
spellSave: sys.spellSave,
|
||||
isElemental,
|
||||
element: sys.element,
|
||||
intRank,
|
||||
magicRank,
|
||||
basePool,
|
||||
currentStress,
|
||||
stressThreshold,
|
||||
isOverThreshold,
|
||||
enhancementOptions,
|
||||
bonusOptions,
|
||||
rollModes,
|
||||
visibility: game.settings.get("core", "rollMode"),
|
||||
}
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-oath-hammer/templates/spell-cast-dialog.hbs",
|
||||
context
|
||||
)
|
||||
|
||||
const result = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: game.i18n.format("OATHHAMMER.Dialog.SpellCastTitle", { spell: spell.name }) },
|
||||
classes: ["fvtt-oath-hammer"],
|
||||
content,
|
||||
rejectClose: false,
|
||||
buttons: [{
|
||||
label: game.i18n.localize("OATHHAMMER.Dialog.CastSpell"),
|
||||
callback: (_ev, btn) => Object.fromEntries(
|
||||
[...btn.form.elements].filter(e => e.name).map(e => [e.name, e.value])
|
||||
),
|
||||
}],
|
||||
})
|
||||
|
||||
if (!result) return null
|
||||
|
||||
const enhKey = result.enhancement ?? "none"
|
||||
const enh = SPELL_ENHANCEMENTS[enhKey] ?? SPELL_ENHANCEMENTS.none
|
||||
|
||||
return {
|
||||
dv,
|
||||
enhancement: enhKey,
|
||||
stressCost: enh.stress,
|
||||
poolPenalty: enh.penalty,
|
||||
redDice: enh.redDice,
|
||||
noStress: enh.noStress,
|
||||
elementalBonus: parseInt(result.elementalBonus) || 0,
|
||||
bonus: parseInt(result.bonus) || 0,
|
||||
grimPenalty: parseInt(result.noGrimoire) || 0,
|
||||
visibility: result.visibility ?? game.settings.get("core", "rollMode"),
|
||||
}
|
||||
}
|
||||
}
|
||||
215
module/applications/weapon-dialog.mjs
Normal file
215
module/applications/weapon-dialog.mjs
Normal file
@@ -0,0 +1,215 @@
|
||||
import { SYSTEM } from "../config/system.mjs"
|
||||
|
||||
/**
|
||||
* Roll dialogs for weapon attacks and damage.
|
||||
*
|
||||
* Attack flow:
|
||||
* 1. promptAttack(actor, weapon) → options
|
||||
* 2. rollWeaponAttack posts a chat card with a "Roll Damage" button
|
||||
* 3. Clicking the button calls promptDamage with attackSuccesses pre-filled
|
||||
* 4. rollWeaponDamage posts the damage chat card
|
||||
*/
|
||||
export default class OathHammerWeaponDialog {
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// ATTACK DIALOG
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
static async promptAttack(actor, weapon) {
|
||||
const sys = weapon.system
|
||||
const actorSys = actor.system
|
||||
|
||||
const isRanged = !sys.usesMight && (sys.shortRange > 0 || sys.longRange > 0)
|
||||
const skillKey = isRanged ? "shooting" : "fighting"
|
||||
const skillDef = SYSTEM.SKILLS[skillKey]
|
||||
const defaultAttr = skillDef.attribute
|
||||
const attrRank = actorSys.attributes[defaultAttr].rank
|
||||
const skillRank = actorSys.skills[skillKey].rank
|
||||
const skillColor = actorSys.skills[skillKey].colorDiceType ?? "white"
|
||||
const threshold = skillColor === "black" ? 2 : skillColor === "red" ? 3 : 4
|
||||
|
||||
const hasNimble = sys.traits.has("nimble")
|
||||
|
||||
// Auto-bonuses from special properties
|
||||
let autoAttackBonus = 0
|
||||
if (sys.specialProperties.has("master-crafted")) autoAttackBonus += 1
|
||||
if (sys.specialProperties.has("accurate")) autoAttackBonus += 1 // bows
|
||||
if (sys.specialProperties.has("balanced")) autoAttackBonus += 1 // grants Fast
|
||||
|
||||
// Damage info for reference
|
||||
const hasBrutal = sys.traits.has("brutal")
|
||||
const hasDeadly = sys.traits.has("deadly")
|
||||
const damageColorType = hasDeadly ? "black" : hasBrutal ? "red" : "white"
|
||||
const damageThreshold = hasDeadly ? 2 : hasBrutal ? 3 : 4
|
||||
const damageColorLabel = hasDeadly ? "⬛" : hasBrutal ? "🔴" : "⬜"
|
||||
const mightRank = actorSys.attributes.might.rank
|
||||
const baseDamageDice = sys.usesMight ? Math.max(mightRank + sys.damageMod, 1) : Math.max(sys.damageMod, 1)
|
||||
|
||||
const traitLabels = [...sys.traits].map(t => {
|
||||
const key = SYSTEM.WEAPON_TRAITS[t]
|
||||
return key ? game.i18n.localize(key) : t
|
||||
})
|
||||
|
||||
// Option arrays
|
||||
const attackBonusOptions = Array.from({ length: 13 }, (_, i) => {
|
||||
const v = i - 6
|
||||
return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
|
||||
})
|
||||
|
||||
const rangeOptions = [
|
||||
{ value: 0, label: game.i18n.localize("OATHHAMMER.Dialog.RangeNormal") },
|
||||
{ value: -1, label: game.i18n.localize("OATHHAMMER.Dialog.RangeLong") + " (−1)" },
|
||||
{ value: -2, label: game.i18n.localize("OATHHAMMER.Dialog.RangeMoving") + " (−2)" },
|
||||
{ value: -2, label: game.i18n.localize("OATHHAMMER.Dialog.RangeConcealment") + " (−2)" },
|
||||
{ value: -3, label: game.i18n.localize("OATHHAMMER.Dialog.RangeCover") + " (−3)" },
|
||||
]
|
||||
|
||||
const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
|
||||
|
||||
const context = {
|
||||
actorName: actor.name,
|
||||
weaponName: weapon.name,
|
||||
weaponImg: weapon.img,
|
||||
skillKey,
|
||||
skillLabel: game.i18n.localize(skillDef.label),
|
||||
attrKey: defaultAttr,
|
||||
attrLabel: game.i18n.localize(`OATHHAMMER.Attribute.${_cap(defaultAttr)}`),
|
||||
attrRank,
|
||||
skillRank,
|
||||
colorType: skillColor,
|
||||
threshold,
|
||||
baseAttackPool: attrRank + skillRank,
|
||||
autoAttackBonus,
|
||||
hasNimble,
|
||||
mightLabel: game.i18n.localize("OATHHAMMER.Attribute.Might"),
|
||||
mightRank,
|
||||
agilityLabel: game.i18n.localize("OATHHAMMER.Attribute.Agility"),
|
||||
agilityRank: actorSys.attributes.agility.rank,
|
||||
isRanged,
|
||||
shortRange: sys.shortRange,
|
||||
longRange: sys.longRange,
|
||||
damageLabel: sys.damageLabel,
|
||||
damageColorType,
|
||||
damageThreshold,
|
||||
damageColorLabel,
|
||||
baseDamageDice,
|
||||
apValue: sys.ap,
|
||||
traits: traitLabels,
|
||||
attackBonusOptions,
|
||||
rangeOptions,
|
||||
rollModes,
|
||||
visibility: game.settings.get("core", "rollMode"),
|
||||
}
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-oath-hammer/templates/weapon-attack-dialog.hbs",
|
||||
context
|
||||
)
|
||||
|
||||
const result = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: game.i18n.format("OATHHAMMER.Dialog.AttackTitle", { weapon: weapon.name }) },
|
||||
classes: ["fvtt-oath-hammer"],
|
||||
content,
|
||||
rejectClose: false,
|
||||
buttons: [{
|
||||
label: game.i18n.localize("OATHHAMMER.Dialog.RollAttack"),
|
||||
callback: (_ev, btn) => Object.fromEntries(
|
||||
[...btn.form.elements].filter(e => e.name).map(e => [e.name, e.value])
|
||||
),
|
||||
}],
|
||||
})
|
||||
|
||||
if (!result) return null
|
||||
return {
|
||||
attackBonus: parseInt(result.attackBonus) || 0,
|
||||
rangeCondition: parseInt(result.rangeCondition) || 0,
|
||||
attrOverride: result.attrOverride || defaultAttr,
|
||||
visibility: result.visibility ?? game.settings.get("core", "rollMode"),
|
||||
autoAttackBonus,
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// DAMAGE DIALOG
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
static async promptDamage(actor, weapon, defaultSV = 0) {
|
||||
const sys = weapon.system
|
||||
const actorSys = actor.system
|
||||
|
||||
const hasBrutal = sys.traits.has("brutal")
|
||||
const hasDeadly = sys.traits.has("deadly")
|
||||
const damageColorType = hasDeadly ? "black" : hasBrutal ? "red" : "white"
|
||||
const damageThreshold = hasDeadly ? 2 : hasBrutal ? 3 : 4
|
||||
const damageColorLabel = hasDeadly ? "⬛" : hasBrutal ? "🔴" : "⬜"
|
||||
const mightRank = actorSys.attributes.might.rank
|
||||
const baseDamageDice = sys.usesMight ? Math.max(mightRank + sys.damageMod, 1) : Math.max(sys.damageMod, 1)
|
||||
|
||||
// Auto-bonuses from special properties
|
||||
let autoDamageBonus = 0
|
||||
if (sys.specialProperties.has("master-crafted")) autoDamageBonus += 1
|
||||
if (sys.specialProperties.has("tempered")) autoDamageBonus += 1
|
||||
if (sys.specialProperties.has("heavy-draw")) autoDamageBonus += 1
|
||||
|
||||
const svOptions = Array.from({ length: 11 }, (_, i) => ({
|
||||
value: i,
|
||||
label: i === 0 ? "0" : `+${i}d`,
|
||||
selected: i === defaultSV,
|
||||
}))
|
||||
|
||||
const damageBonusOptions = Array.from({ length: 9 }, (_, i) => {
|
||||
const v = i - 4
|
||||
return { value: v, label: v > 0 ? `+${v}` : String(v), selected: v === 0 }
|
||||
})
|
||||
|
||||
const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes)
|
||||
|
||||
const context = {
|
||||
actorName: actor.name,
|
||||
weaponName: weapon.name,
|
||||
weaponImg: weapon.img,
|
||||
damageLabel: sys.damageLabel,
|
||||
damageColorType,
|
||||
damageThreshold,
|
||||
damageColorLabel,
|
||||
baseDamageDice,
|
||||
autoDamageBonus,
|
||||
apValue: sys.ap,
|
||||
defaultSV,
|
||||
svOptions,
|
||||
damageBonusOptions,
|
||||
rollModes,
|
||||
visibility: game.settings.get("core", "rollMode"),
|
||||
}
|
||||
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-oath-hammer/templates/weapon-damage-dialog.hbs",
|
||||
context
|
||||
)
|
||||
|
||||
const result = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: game.i18n.format("OATHHAMMER.Dialog.DamageTitle", { weapon: weapon.name }) },
|
||||
classes: ["fvtt-oath-hammer"],
|
||||
content,
|
||||
rejectClose: false,
|
||||
buttons: [{
|
||||
label: game.i18n.localize("OATHHAMMER.Dialog.RollDamage"),
|
||||
callback: (_ev, btn) => Object.fromEntries(
|
||||
[...btn.form.elements].filter(e => e.name).map(e => [e.name, e.value])
|
||||
),
|
||||
}],
|
||||
})
|
||||
|
||||
if (!result) return null
|
||||
return {
|
||||
sv: parseInt(result.sv) || 0,
|
||||
damageBonus: parseInt(result.damageBonus) || 0,
|
||||
visibility: result.visibility ?? game.settings.get("core", "rollMode"),
|
||||
autoDamageBonus,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _cap(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
Reference in New Issue
Block a user