273 lines
9.5 KiB
JavaScript
273 lines
9.5 KiB
JavaScript
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()
|
|
}
|
|
}
|