Files
2026-05-05 13:55:42 +02:00

140 lines
4.7 KiB
JavaScript

import MGNERoll from "./roll.mjs"
import { SYSTEM_ID } from "../config/system.mjs"
const t = key => game.i18n.localize(key)
const f = (key, data = {}) => game.i18n.format(key, data)
const INITIATIVE_FLAG = "initiativeState"
const SIDE_BONUS = 20
const FLEE_BASE_TARGET = 5
const FLEE_MIN_TARGET = 2
function isPlayerSide(combatant) {
return Boolean(combatant?.actor?.hasPlayerOwner)
}
function fleeDialogContent() {
const options = Array.from({ length: FLEE_BASE_TARGET - FLEE_MIN_TARGET + 1 }, (_, index) => {
const value = index
return `<option value="${value}">${value}</option>`
}).join("")
return `
<form class="mgne-flee-dialog">
<div class="form-group">
<label>${t("MGNE.Combat.StudyActions")}</label>
<select name="studyActions">${options}</select>
<p class="notes">${t("MGNE.Combat.StudyHelp")}</p>
</div>
</form>
`
}
export default class MGNECombat extends Combat {
getFleeCombatant(combatantId = null) {
if (combatantId) return this.combatants.get(combatantId) ?? null
const controlledCombatant = canvas.tokens?.controlled?.[0]?.combatant ?? null
return this.combatant ?? controlledCombatant
}
async getInitiativeState() {
const existing = this.getFlag(SYSTEM_ID, INITIATIVE_FLAG)
if (existing && Number.isFinite(existing.sideRoll)) return existing
const sideRoll = await (new Roll("1d6")).evaluate()
const initiativeState = {
sideRoll: sideRoll.total,
playersActFirst: sideRoll.total >= 4,
}
await this.setFlag(SYSTEM_ID, INITIATIVE_FLAG, initiativeState)
await ChatMessage.create({
speaker: ChatMessage.getSpeaker(),
content: `<p>${f("MGNE.Initiative.SideRoll", { roll: initiativeState.sideRoll })}</p><p>${t(initiativeState.playersActFirst ? "MGNE.Initiative.PlayersFirst" : "MGNE.Initiative.EnemiesFirst")}</p><p>${t("MGNE.Initiative.TieBreak")}</p>`,
})
return initiativeState
}
async rollInitiative(ids, { updateTurn = true } = {}) {
const combatantIds = typeof ids === "string" ? [ids] : Array.from(ids ?? [])
if (!combatantIds.length) return this
const currentCombatantId = this.combatant?.id ?? null
const initiativeState = await this.getInitiativeState()
const updates = []
for (const id of combatantIds) {
const combatant = this.combatants.get(id)
if (!combatant) continue
const agility = combatant.actor?.system?.abilities?.agility?.value ?? 0
const roll = await (new Roll("1d6 + @agility", { agility })).evaluate()
const actsWithPriority = isPlayerSide(combatant) === initiativeState.playersActFirst
updates.push({
_id: id,
initiative: roll.total + (actsWithPriority ? SIDE_BONUS : 0),
})
}
if (updates.length) await this.updateEmbeddedDocuments("Combatant", updates)
if (updateTurn && currentCombatantId) {
const turn = this.turns.findIndex(combatant => combatant.id === currentCombatantId)
if (turn >= 0) await this.update({ turn })
}
return this
}
async rollFlee({ combatantId = null } = {}) {
const combatant = this.getFleeCombatant(combatantId)
if (!combatant?.actor) {
ui.notifications.warn(t("MGNE.Combat.FleeNoCombatant"))
return null
}
const dialogData = await foundry.applications.api.DialogV2.wait({
window: { title: t("MGNE.Combat.Flee") },
classes: ["mgne", "roll-dialog"],
content: fleeDialogContent(),
buttons: [{
label: t("MGNE.Combat.Flee"),
icon: "fa-solid fa-person-running",
callback: (_event, button) => {
const value = Number.parseInt(button.form?.elements.studyActions?.value ?? "0", 10)
return { studyActions: Math.max(0, Math.min(FLEE_BASE_TARGET - FLEE_MIN_TARGET, value || 0)) }
},
}],
rejectClose: false,
})
if (!dialogData) return null
const studyActions = dialogData.studyActions ?? 0
const target = Math.max(FLEE_MIN_TARGET, FLEE_BASE_TARGET - studyActions)
const roll = await (new Roll("1d6")).evaluate()
const escaped = roll.total >= target
if (escaped) {
await combatant.delete()
} else {
await combatant.actor.update({ "system.hp.value": 0 })
await combatant.update({ defeated: true })
}
await MGNERoll.createActionCard({
mode: "flee",
actor: combatant.actor,
label: f("MGNE.Roll.FleeLabel", { actor: combatant.name }),
subtitle: f("MGNE.Roll.FleeSubtitle", { target }),
roll,
outcome: t(escaped ? "MGNE.Roll.FleeEscaped" : "MGNE.Roll.FleeKilled"),
specialText: studyActions > 0
? f("MGNE.Roll.FleeStudyActions", { count: studyActions })
: t("MGNE.Roll.FleeNoStudyActions"),
})
return { combatant, roll, escaped, target, studyActions }
}
}