${f("MGNE.RollDialog.OmenMaximizePrompt", { omens: actorOmens })}
`,
buttons: [
{ action: "roll", label: t("MGNE.Common.Roll"), icon: "fa-solid fa-dice", callback: () => "roll" },
{ action: "maximize", label: t("MGNE.RollDialog.SpendOmenMaximize"), icon: "fa-solid fa-star", callback: () => "maximize" },
],
rejectClose: false,
})
if (choice === null) return null
maximize = choice === "maximize"
}
const roll = await (new Roll(formula)).evaluate(maximize ? { maximize: true } : {})
if (maximize) {
// Re-read omens after dialog to avoid overwriting concurrent changes
const currentOmens = actor.system.omens?.current ?? 0
await actor.update({ "system.omens.current": Math.max(0, currentOmens - 1) })
}
const isCritical = multiplier > 1
const contentHtml = await renderCard({
mode: "damage",
actorName: actor.name,
actorImg: actor.img,
label: f("MGNE.Roll.ItemDamageLabel", { item: item.name }),
subtitle: null,
formula: roll.formula,
total: roll.total,
outcome: t("MGNE.Roll.OutcomeRolled"),
specialText: isCritical ? t("MGNE.Roll.CriticalDamageApplied") : "",
omenMaximized: maximize,
showApplyButton: true,
damageTotal: roll.total,
damageCritical: isCritical,
damageTargetActorId: targetActor?.id ?? null,
_roll: roll,
})
await ChatMessage.create({
speaker: ChatMessage.getSpeaker({ actor }),
rolls: [roll],
content: contentHtml,
})
return { roll }
}
static async rollFlatDamage({ actor, label, formula, targetActor = null }) {
const damageBonus = await actor.consumePendingDamageBonus("profile-attack")
const multiplier = damageBonus?.multiplier ?? 1
const baseFormula = formula || "1"
const resolvedFormula = multiplier > 1 ? `${multiplier} * (${baseFormula})` : baseFormula
const roll = await (new Roll(resolvedFormula)).evaluate()
const isCritical = multiplier > 1
const contentHtml = await renderCard({
mode: "damage",
actorName: actor.name,
actorImg: actor.img,
label,
subtitle: null,
formula: roll.formula,
total: roll.total,
outcome: t("MGNE.Roll.OutcomeRolled"),
specialText: isCritical ? t("MGNE.Roll.CriticalDamageApplied") : "",
showApplyButton: true,
damageTotal: roll.total,
damageCritical: isCritical,
damageTargetActorId: targetActor?.id ?? null,
_roll: roll,
})
await ChatMessage.create({
speaker: ChatMessage.getSpeaker({ actor }),
rolls: [roll],
content: contentHtml,
})
return { roll }
}
static async rollArmorSave(actor) {
const formula = actor.getArmorRollFormula()
const items = actor.getEquippedArmorItems()
if (formula === "0") {
ui.notifications.warn(t("MGNE.Notification.NoArmorEquipped"))
return null
}
const roll = await (new Roll(formula)).evaluate()
const armorNames = items.map(i => i.name).join(" + ")
const contentHtml = await renderCard({
mode: "armor",
actorName: actor.name,
actorImg: actor.img,
label: t("MGNE.Roll.ArmorSave"),
subtitle: armorNames,
formula: roll.formula,
total: roll.total,
outcome: f("MGNE.Roll.ArmorAbsorbed", { amount: roll.total }),
_roll: roll,
})
await ChatMessage.create({
speaker: ChatMessage.getSpeaker({ actor }),
rolls: [roll],
content: contentHtml,
})
return roll
}
static async rollUsage(item) {
const currentDie = item.system.usageDie
if (!currentDie || currentDie === "depleted") {
ui.notifications.warn(f("MGNE.Notification.ItemDepleted", { item: item.name }))
return null
}
const roll = await (new Roll(`1${currentDie}`)).evaluate()
const depleted = roll.total <= 2
const nextDie = depleted ? stepDownDie(currentDie) : currentDie
const updates = { "system.usageDie": nextDie }
if (item.type === "resonance-core" && nextDie === "depleted") updates["system.burnedOut"] = true
await item.update(updates)
const contentHtml = await renderCard({
mode: "usage",
actorName: item.parent?.name ?? item.name,
actorImg: item.img,
label: f("MGNE.Roll.ItemUsageLabel", { item: item.name }),
subtitle: f("MGNE.Roll.CurrentDie", { die: currentDie.toUpperCase() }),
formula: roll.formula,
total: roll.total,
outcome: depleted ? f("MGNE.Roll.DowngradedTo", { die: nextDie.toUpperCase() }) : t("MGNE.Roll.NoChange"),
specialText: depleted && nextDie === "depleted" ? t("MGNE.Roll.ItemNowDepleted") : "",
_roll: roll,
})
await ChatMessage.create({
speaker: ChatMessage.getSpeaker({ actor: item.parent ?? null }),
rolls: [roll],
content: contentHtml,
})
return { roll, depleted, nextDie }
}
static async rollDurability(item) {
const currentDie = item.system.durabilityDie
if (!currentDie || currentDie === "depleted") {
ui.notifications.warn(f("MGNE.Notification.ItemDurabilityDepleted", { item: item.name }))
return null
}
const roll = await (new Roll(`1${currentDie}`)).evaluate()
const degraded = roll.total <= 2
const nextDie = degraded ? stepDownDie(currentDie) : currentDie
const nowBroken = degraded && nextDie === "depleted"
const updates = { "system.durabilityDie": nextDie }
if (nowBroken) {
updates["system.broken"] = true
if ("equipped" in (item.system ?? {})) updates["system.equipped"] = false
}
await item.update(updates)
const contentHtml = await renderCard({
mode: "durability",
actorName: item.parent?.name ?? item.name,
actorImg: item.img,
label: f("MGNE.Roll.DurabilityLabel", { item: item.name }),
subtitle: f("MGNE.Roll.CurrentDie", { die: currentDie.toUpperCase() }),
formula: roll.formula,
total: roll.total,
outcome: degraded
? f("MGNE.Roll.DowngradedTo", { die: nextDie.toUpperCase() })
: t("MGNE.Roll.NoChange"),
specialText: nowBroken ? t("MGNE.Roll.ItemNowBroken") : "",
_roll: roll,
})
await ChatMessage.create({
speaker: ChatMessage.getSpeaker({ actor: item.parent ?? null }),
rolls: [roll],
content: contentHtml,
})
return { roll, degraded, nextDie, nowBroken }
}
static async applyDamageCard({ actor, sourceActor = null, sourceItem = null, amount, armorRoll = null, appliedDamage, newHp, breakText = "", defenseFumbleText = "", criticalArmorText = "" }) {
const contentHtml = await renderCard({
mode: "apply-damage",
actorName: actor.name,
actorImg: actor.img,
label: f("MGNE.Roll.TakesDamageLabel", { actor: actor.name }),
subtitle: sourceItem
? (sourceActor
? f("MGNE.Roll.DamageSourceWithActor", { item: sourceItem.name, actor: sourceActor.name })
: f("MGNE.Roll.DamageSourceItem", { item: sourceItem.name }))
: t("MGNE.Roll.DirectDamage"),
formula: armorRoll?.formula ?? "",
total: amount,
outcome: f("MGNE.Roll.HPNow", { hp: newHp }),
specialText: joinParts([
defenseFumbleText,
armorRoll ? f("MGNE.Roll.ArmorAbsorbed", { amount: armorRoll.total }) : "",
f("MGNE.Roll.AppliedDamageText", { amount: appliedDamage }),
criticalArmorText,
breakText ? f("MGNE.Roll.BreakText", { text: breakText }) : "",
]),
_roll: armorRoll ?? null,
})
await ChatMessage.create({
speaker: ChatMessage.getSpeaker({ actor }),
rolls: armorRoll ? [armorRoll] : [],
content: contentHtml,
})
}
static async createRestCard({ actor, label, subtitle, roll, outcome, specialText = "" }) {
return this.createActionCard({ mode: "rest", actor, label, subtitle, roll, outcome, specialText })
}
static async createActionCard({ mode = "action", actor = null, label, subtitle = "", roll, outcome, specialText = "" }) {
const contentHtml = await renderCard({
mode,
actorName: actor?.name ?? "",
actorImg: actor?.img ?? "",
label,
subtitle,
formula: roll.formula,
total: roll.total,
outcome,
specialText,
_roll: roll,
})
await ChatMessage.create({
speaker: ChatMessage.getSpeaker({ actor }),
rolls: [roll],
content: contentHtml,
})
}
static getFirstTargetActor() {
return getFirstTargetActor()
}
static stepDownDie(die, steps = 1, track = SYSTEM.usageDice) {
return stepDownDie(die, steps, track)
}
}