140 lines
4.7 KiB
JavaScript
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 }
|
|
}
|
|
}
|