import { EcrymeUtility } from "../common/ecryme-utility.js"; const { HandlebarsApplicationMixin } = foundry.applications.api /** * Main confrontation dialog — Application V2 version. * Features drag-and-drop of dice from the pool to execution/preservation slots. * All event listeners (change + drag-drop) are bound once via _listenersAdded guard. */ export class EcrymeConfrontDialog extends HandlebarsApplicationMixin(foundry.applications.api.ApplicationV2) { #dragDrop _listenersAdded = false /* -------------------------------------------- */ constructor(actor, rollData, options = {}) { super(options) this.actor = actor this.rollData = rollData this.buttonDisabled = true this.#dragDrop = this.#createDragDropHandlers() } /* -------------------------------------------- */ /** @override */ static DEFAULT_OPTIONS = { classes: ["fvtt-ecryme", "ecryme-confrontation-dialog"], position: { width: 640 }, window: { title: "ECRY.ui.confront" }, dragDrop: [{ dragSelector: ".confront-dice-container", dropSelector: null }], actions: { launchConfront: EcrymeConfrontDialog.#onLaunchConfront, cancel: EcrymeConfrontDialog.#onCancel, }, } /** @override */ static PARTS = { content: { template: "systems/fvtt-ecryme/templates/dialogs/confront-dialog.hbs" }, } /* -------------------------------------------- */ static async create(actor, rollData) { return new EcrymeConfrontDialog(actor, rollData) } /* -------------------------------------------- */ async _prepareContext() { return { ...this.rollData, config: game.system.ecryme.config, buttonDisabled: this.buttonDisabled, } } /* -------------------------------------------- */ /** Bind drag-drop and form-change listeners once; re-bind DragDrop on each render. */ _onRender(context, options) { // DragDrop must be re-bound each render because the DOM is replaced this.#dragDrop.forEach(d => d.bind(this.element)) // Form-change listener is bound once (event delegation survives DOM re-renders) if (!this._listenersAdded) { this._listenersAdded = true this.element.addEventListener('change', this.#onFormChange.bind(this)) } } /* -------------------------------------------- */ #onFormChange(event) { const target = event.target switch (target.id) { case 'bonusMalusPerso': this.rollData.bonusMalusPerso = Number(target.value) this.computeTotals() break case 'roll-specialization': this.rollData.selectedSpecs = Array.from(target.selectedOptions).map(o => o.value) this.computeTotals() break case 'roll-trait-bonus': this.rollData.traitsBonusSelected = Array.from(target.selectedOptions).map(o => o.value) this.computeTotals() break case 'roll-trait-malus': this.rollData.traitsMalusSelected = Array.from(target.selectedOptions).map(o => o.value) this.computeTotals() break case 'roll-select-transcendence': this.rollData.skillTranscendence = Number(target.value) this.computeTotals() break case 'roll-apply-transcendence': this.rollData.applyTranscendence = target.value this.computeTotals() break case 'annency-bonus': this.rollData.annencyBonus = Number(target.value) break } } // #region Drag-and-Drop #createDragDropHandlers() { return this.options.dragDrop.map(d => { d.permissions = { dragstart: () => true, drop: () => true, } d.callbacks = { dragstart: this._onDragStart.bind(this), dragover: this._onDragOver.bind(this), drop: this._onDrop.bind(this), } return new foundry.applications.ux.DragDrop.implementation(d) }) } /* -------------------------------------------- */ _onDragStart(event) { const target = event.target const dragType = target.dataset.dragType let diceData if (dragType === "dice") { diceData = { dragType: "dice", diceIndex: target.dataset.diceIdx, diceValue: target.dataset.diceValue, } } else { diceData = { dragType: "bonus", bonusIndex: target.dataset.bonusIdx, bonusValue: 1, } } event.dataTransfer.setData("text/plain", JSON.stringify(diceData)) } /* -------------------------------------------- */ _onDragOver(event) { event.preventDefault() } /* -------------------------------------------- */ _onDrop(event) { let data try { data = JSON.parse(event.dataTransfer.getData("text/plain")) } catch (e) { return } // Walk up the DOM to find a meaningful drop area const executionArea = event.target.closest('.confront-execution-area') const preservationArea = event.target.closest('.confront-preservation-area') const diceList = event.target.closest('.confrontation-dice-list') const bonusList = event.target.closest('.confrontation-bonus-list') if (data.dragType === "dice") { const idx = Number(data.diceIndex) if (executionArea && this.rollData.availableDices.filter(d => d.location === "execution").length < 2) { this.rollData.availableDices[idx].location = "execution" } else if (preservationArea && this.rollData.availableDices.filter(d => d.location === "preservation").length < 2) { this.rollData.availableDices[idx].location = "preservation" } else if (diceList) { this.rollData.availableDices[idx].location = "mainpool" } const execCount = this.rollData.availableDices.filter(d => d.location === "execution").length const presCount = this.rollData.availableDices.filter(d => d.location === "preservation").length this.buttonDisabled = !(execCount === 2 && presCount === 2) } else if (data.dragType === "bonus") { const idx = Number(data.bonusIndex) if (executionArea) this.rollData.confrontBonus[idx].location = "execution" else if (preservationArea) this.rollData.confrontBonus[idx].location = "preservation" else if (bonusList) this.rollData.confrontBonus[idx].location = "mainpool" } this.computeTotals() } // #endregion /* -------------------------------------------- */ processTranscendence() { if (this.rollData.skillTranscendence > 0) { if (this.rollData.applyTranscendence === "execution") { this.rollData.executionTotal += Number(this.rollData.skillTranscendence) } else { this.rollData.preservationTotal += Number(this.rollData.skillTranscendence) } } } /* -------------------------------------------- */ computeTotals() { const rollData = this.rollData const actor = game.actors.get(rollData.actorId) rollData.executionTotal = rollData.availableDices .filter(d => d.location === "execution") .reduce((acc, d) => acc + d.result, rollData.skill.value) rollData.executionTotal = rollData.confrontBonus .filter(d => d.location === "execution") .reduce((acc) => acc + 1, rollData.executionTotal) rollData.preservationTotal = rollData.availableDices .filter(d => d.location === "preservation") .reduce((acc, d) => acc + d.result, rollData.skill.value) rollData.preservationTotal = rollData.confrontBonus .filter(d => d.location === "preservation") .reduce((acc) => acc + 1, rollData.preservationTotal) this.processTranscendence() // Specialization if (rollData.selectedSpecs?.length > 0) { rollData.spec = foundry.utils.duplicate(actor.getSpecialization(rollData.selectedSpecs[0])) rollData.specApplied = true rollData.executionTotal += 2 rollData.preservationTotal += 2 } if (rollData.specApplied && rollData.selectedSpecs?.length === 0) { rollData.spec = undefined rollData.specApplied = false } // Traits bonus/malus rollData.bonusMalusTraits = 0 for (const t of rollData.traitsBonus) t.activated = false for (const t of rollData.traitsMalus) t.activated = false for (const id of (rollData.traitsBonusSelected ?? [])) { const trait = rollData.traitsBonus.find(t => t._id === id) if (trait) { trait.activated = true; rollData.bonusMalusTraits += Number(trait.system.level) } } for (const id of (rollData.traitsMalusSelected ?? [])) { const trait = rollData.traitsMalus.find(t => t._id === id) if (trait) { trait.activated = true; rollData.bonusMalusTraits -= Number(trait.system.level) } } rollData.executionTotal += Number(rollData.bonusMalusTraits) + Number(rollData.bonusMalusPerso) rollData.preservationTotal += Number(rollData.bonusMalusTraits) + Number(rollData.bonusMalusPerso) this.render() } /* -------------------------------------------- */ async launchConfront() { const msg = await EcrymeUtility.createChatMessage(this.rollData.alias, "blindroll", { content: await foundry.applications.handlebars.renderTemplate( `systems/fvtt-ecryme/templates/chat/chat-confrontation-pending.hbs`, this.rollData ), }) EcrymeUtility.blindMessageToGM({ rollData: this.rollData, template: "systems/fvtt-ecryme/templates/chat/chat-confrontation-pending.hbs", }) msg.setFlag("world", "ecryme-rolldata", this.rollData) } /* -------------------------------------------- */ static async #onLaunchConfront(event, target) { await this.launchConfront() this.close() } static #onCancel(event, target) { this.close() } }