import {ActionManager} from "../sheets/actions/action-manager.mjs"; const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api /** * @typedef TokenDistance * @property {Number} x * @property {Number} y * @property {Number} d * @property {Token} token */ export class CombatActionDialog extends HandlebarsApplicationMixin(ApplicationV2) { static DEFAULT_OPTIONS = { classes: ['dsa41', 'dialog', 'combat'], tag: "form", position: { width: 480, height: 640 }, window: { resizable: false, }, form: { submitOnChange: false, closeOnSubmit: true, handler: CombatActionDialog.#onSubmitForm }, actions: { selectTarget: CombatActionDialog.#onSelectTarget, selectWeaponAndSkill: CombatActionDialog.#onSelectWeaponAndSkill, selectManeuver: CombatActionDialog.#onSelectManeuver, } } static PARTS = { form: { template: 'systems/DSA_4-1/templates/dialog/combat-action.hbs', } } /** * @type {Actor} * @private */ _actor = null constructor(actor) { super(); this._actor = actor this._targetId = null this._skillId = null this._weaponId = null this._defenseManeuverId = null this._actionManager = new ActionManager(this._actor) CombatActionDialog._instance = this } static _instance = null async #processOnSelectTarget(event, target) { const {targetId} = target.dataset this._targetId = this._targetId === targetId ? null : targetId this.render({parts: ["form"]}) } static async #onSelectTarget(event, target) { event.preventDefault() CombatActionDialog._instance.#processOnSelectTarget(event, target) } async #processOnSelectManeuver(event, target) { const {maneuverId} = target.dataset this._defenseManeuverId = this._defenseManeuverId === maneuverId ? null : maneuverId this.render({parts: ["form"]}) } static async #onSelectManeuver(event, target) { event.preventDefault() CombatActionDialog._instance.#processOnSelectManeuver(event, target) } async #processOnSelectWeaponAndSkill(event, target) { const {weaponId, skillId} = target.dataset this._weaponId = this._weaponId === weaponId ? null : weaponId this._skillId = this._skillId === skillId ? null : skillId this.render({parts: ["form"]}) } static async #onSelectWeaponAndSkill(event, target) { event.preventDefault() CombatActionDialog._instance.#processOnSelectWeaponAndSkill(event, target) } async #processOnSubmitForm(event, form, formData) { const maneuver = CombatActionDialog._instance.#evaluateManeuvers().find(p => p.id === this._defenseManeuverId) const weapon = this._actor.itemTypes["Equipment"].find(p => p._id === this._weaponId) const skill = this._actor.itemTypes["Skill"].find(p => p._id === this._skillId) const target = game.actors.get(game.scenes.current.tokens.find(p => p._id === this._targetId).actorId) if (maneuver.cost !== ActionManager.CONTINUING) { this._actor.rollAttack({ weapon: this._weaponId, skill: this._skillId, target: this._targetId, title: maneuver.name, maneuver: maneuver.activate.toString(), mod: this._mod, circumstance: this._circumstance, penalty: this._penalty, targetNumber: this._targetNumber, modDescription: maneuver?.modDescription?.replace("{}", "" + this._mod) ?? "" }) return true } else { // push into cooldown queue const cooldowns = this._actor.system.cooldowns /** @type Cooldown */ const newCooldown = { start: maneuver.cooldown({weapon, skill, target, mod: this._mod}), current: maneuver.cooldown({weapon, skill, target, mod: this._mod}), data: { cssClass: "Kampf", weapon: this._weaponId, skill: this._skillId, target: this._targetId, title: maneuver.name, maneuver: maneuver.activate.toString(), mod: this._mod, circumstance: this._circumstance, penalty: this._penalty, targetNumber: this._targetNumber, modDescription: maneuver?.modDescription?.replace("{}", "" + this._mod) ?? "" } } cooldowns.push(newCooldown) await this._actor.update({"system.cooldowns": cooldowns}) ui.notifications.info(`Neue Aktion für ${maneuver.name} mit Abklingzeit von ${maneuver.cooldown({ weapon, skill, target, mod: this._mod })} Aktionen hinzugefügt`) } } static async #onSubmitForm(event, form, formData) { event.preventDefault() CombatActionDialog._instance.#processOnSubmitForm(event, form, formData) } _configureRenderOptions(options) { super._configureRenderOptions(options) if (options.window) { options.window.title = `Mit ${this._actor.name} angreifen` } return options } #getDistanceBetween(originToken, targetToken) { const distance = game.scenes.current.dimensions.distancePixels // get distances between of tokens and thisTokenRepresentative const actorOfToken = game.actors.get(targetToken.actorId) return { id: targetToken._id, x: targetToken.x, y: targetToken.y, actor: actorOfToken, token: targetToken, d: ((Math.sqrt(Math.pow(targetToken.x - originToken.x, 2) + Math.pow(targetToken.y - originToken.y, 2))) / distance).toFixed(2), } } #evaluateDistances() { // get tokens in range on the current scene const scene = game.scenes.current const tokens = scene.tokens const tokenId = this._actor.getActiveTokens()[0].id const thisTokenRepresentative = tokens.get(tokenId) /** * * @type {[TokenDistance]} */ return tokens.map(token => { return { isSelected: this._targetId === token.id, ...this.#getDistanceBetween(thisTokenRepresentative, token) } }).sort((a, b) => a.d - b.d).sort((a, b) => (a.isSelected ? 0 : 1) - (b.isSelected ? 0 : 1)) } #evaluateWeapons() { // get equipped weapons and adjust AT/PA values by basis values from actor TODO: and W/M of weapons const equips = this._actor.system.heldenausruestung[this._actor.system.setEquipped] const weapons = [] const equippedWeapons = ["links", "rechts", "fernkampf"] const baseAt = { links: this._actor.system.at.links.aktuell, // TODO hook Beidhändigerkampf/linkhand rechts: this._actor.system.at.rechts.aktuell, fernkampf: this._actor.system.fk.aktuell, } const basePa = { links: this._actor.system.pa.links.aktuell, // TODO hook Beidhändigerkampf/linkhand rechts: this._actor.system.pa.rechts.aktuell, fernkampf: 0, } equippedWeapons.forEach(slot => { const equip = equips[slot] const weapon = this._actor.itemTypes["Equipment"].find(p => p._id === equip) if (weapon) { const variantWeaponSkills = [...weapon.system.rangedSkills, ...weapon.system.meleeSkills] variantWeaponSkills.forEach(weaponSkill => { const skill = this._actor.itemTypes["Skill"].find(p => p.name === weaponSkill) if (skill) { const skillAt = skill.system.at const skillPa = skill.system.pa weapons.push({ isSelected: this._skillId === skill._id && this._weaponId === weapon._id, weaponId: weapon._id, skillId: skill._id, name: weapon.name, skillName: skill.name, img: weapon.img, combatStatistics: { at: baseAt[slot] + weapon.system.attackModifier + skillAt, pa: basePa[slot] + weapon.system.parryModifier + skillPa } }) } }) } }) this._weapons = weapons.sort((a, b) => (a.isSelected ? 0 : 1) - (b.isSelected ? 0 : 1)) return this._weapons } #evaluateManeuvers() { const manager = this._actionManager const weapon = this._actor.itemTypes["Equipment"].find(p => p._id === this._weaponId) const skill = this._actor.itemTypes["Skill"].find(p => p._id === this._skillId) const target = game.actors.get(game.scenes.current.tokens.find(p => p._id === this._targetId).actorId) this._maneuvers = manager.evaluate({ target, weapon, skill }).filter(p => p.type === ActionManager.ATTACK).map(action => { return { isSelected: this._defenseManeuverId === action.name, id: action.name, name: action.name, type: action.type, source: action.source, cost: action.cost, penalty: action.eval?.mod ?? 0, mod: action.mod, modDescription: action.modDescription, cooldown: action.cooldown, activate: action.activate, } }).sort((a, b) => (a.isSelected ? 0 : 1) - (b.isSelected ? 0 : 1)) return this._maneuvers } async _prepareContext(options) { const context = await super._prepareContext(options) context.actor = this._actor context.distanceUnit = game.scenes.current.grid.units if (this._actor.getActiveTokens()[0]?.id) { context.tokenDistances = this.#evaluateDistances() context.weapons = this.#evaluateWeapons() if (this._targetId && this._weaponId && this._skillId) { context.maneuver = this.#evaluateManeuvers() } const maneuver = this._maneuvers?.find(p => p.id === this._defenseManeuverId) if (maneuver) { context.canMod = maneuver.mod != undefined } context.targetNumber = context.weapons.find(p => p.weaponId === this._weaponId && p.skillId === this._skillId)?.combatStatistics.at // TODO get W/M of weapon NOW context.ready = this._targetId && this._weaponId && this._skillId && this._defenseManeuverId return context } else { ui.notifications.error(`Feature funktioniert nur wenn der Akteur ein Token auf der aktuellen Szene hat`); } } #update(context) { const target = this.element.querySelector(".actions button .value") const targetDescription = this.element.querySelector(".modResult") const at = Number(context.targetNumber) const maneuver = this._maneuvers?.find(p => p.id === this._defenseManeuverId) const mod = Number(this.element.querySelector('[name="mod"]').value) const penalty = 0 - (maneuver?.penalty ?? 0) + (maneuver?.mod?.(mod) ?? 0) ?? 0 const circumstance = Number(this.element.querySelector('[name="circumstance"]').value) this.element.querySelector('[name="penalty"]').value = penalty + circumstance const result = (at + circumstance + penalty) this._circumstance = circumstance this._penalty = penalty this._targetNumber = result this._mod = mod this._modDescription = maneuver?.modDescription?.replace("{}", "" + mod) ?? "" target.textContent = `(${result})` targetDescription.textContent = this._modDescription if (result <= 0) { context.ready = false this.element.querySelector(".actions button").classList.remove("ready") this.element.querySelector(".actions button").setAttribute("disabled", true) } else { context.ready = true this.element.querySelector(".actions button").classList.add("ready") this.element.querySelector(".actions button").removeAttribute("disabled") } } _onRender(context, options) { this.#update(context) this.element.querySelectorAll('[name="mod"], [name="malus"], [name="circumstance"]').forEach(e => e.addEventListener('change', (event) => { this.#update(context) })) } }