const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; export default class BeastformDialog extends HandlebarsApplicationMixin(ApplicationV2) { constructor(configData, item) { super(); this.item = item; this.configData = configData; this.selected = null; this.evolved = { form: null }; this.hybrid = { forms: {}, advantages: {}, features: {} }; this._dragDrop = this._createDragDropHandlers(); } static DEFAULT_OPTIONS = { tag: 'form', classes: ['daggerheart', 'views', 'dialog', 'dh-style', 'beastform-selection'], position: { width: 600, height: 'auto' }, window: { icon: 'fa-solid fa-paw' }, actions: { selectBeastform: this.selectBeastform, toggleHybridFeature: this.toggleHybridFeature, toggleHybridAdvantage: this.toggleHybridAdvantage, submitBeastform: this.submitBeastform }, form: { handler: this.updateBeastform, submitOnChange: true, submitOnClose: false }, dragDrop: [{ dragSelector: '.beastform-container', dropSelector: '.advanced-form-container' }] }; get title() { return this.item.name; } /** @override */ static PARTS = { header: { template: 'systems/daggerheart/templates/dialogs/beastform/header.hbs' }, tabs: { template: 'systems/daggerheart/templates/dialogs/beastform/tabs.hbs' }, beastformTier: { template: 'systems/daggerheart/templates/dialogs/beastform/beastformTier.hbs' }, advanced: { template: 'systems/daggerheart/templates/dialogs/beastform/advanced.hbs' }, footer: { template: 'systems/daggerheart/templates/dialogs/beastform/footer.hbs' } }; /** @inheritdoc */ static TABS = { primary: { tabs: [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }], initial: '1', labelPrefix: 'DAGGERHEART.GENERAL.Tiers' } }; changeTab(tab, group, options) { super.changeTab(tab, group, options); this.render(); } _createDragDropHandlers() { return this.options.dragDrop.map(d => { d.callbacks = { dragstart: this._onDragStart.bind(this), drop: this._onDrop.bind(this) }; return new foundry.applications.ux.DragDrop.implementation(d); }); } _attachPartListeners(partId, htmlElement, options) { super._attachPartListeners(partId, htmlElement, options); this._dragDrop.forEach(d => d.bind(htmlElement)); } async _prepareContext(_options) { const context = await super._prepareContext(_options); context.selected = this.selected; context.selectedBeastformEffect = this.selected?.effects?.find?.(x => x.type === 'beastform'); context.evolved = this.evolved; context.hybridForms = Object.keys(this.hybrid.forms).reduce((acc, formKey) => { if (!this.hybrid.forms[formKey]) { acc[formKey] = null; } else { const data = this.hybrid.forms[formKey].toObject(); acc[formKey] = { ...data, system: { ...data.system, features: this.hybrid.forms[formKey].system.features.map(feature => ({ ...feature.toObject(), uuid: feature.uuid, selected: Boolean(this.hybrid.features?.[formKey]?.[feature.uuid]) })), advantageOn: Object.keys(data.system.advantageOn).reduce((acc, key) => { acc[key] = { ...data.system.advantageOn[key], selected: Boolean(this.hybrid.advantages?.[formKey]?.[key]) }; return acc; }, {}) } }; } return acc; }, {}); const maximumDragTier = Math.max( this.selected?.system?.evolved?.maximumTier ?? 0, this.selected?.system?.hybrid?.maximumTier ?? 0 ); const compendiumBeastforms = await game.packs.get(`daggerheart.beastforms`)?.getDocuments(); const beastformTiers = [...game.items, ...(compendiumBeastforms ? compendiumBeastforms : [])].reduce( (acc, x) => { const tier = CONFIG.DH.GENERAL.tiers[x.system.tier]; if (x.type !== 'beastform' || tier.id > this.configData.tierLimit) return acc; if (!acc[tier.id]) acc[tier.id] = { label: game.i18n.localize(tier.label), values: {} }; if (Object.values(acc[tier.id].values).find(existing => existing.value.name === x.name)) return acc; acc[tier.id].values[x.uuid] = { selected: this.selected?.uuid == x.uuid, value: x, draggable: !['evolved', 'hybrid'].includes(x.system.beastformType) && maximumDragTier ? x.system.tier <= maximumDragTier : false }; return acc; }, {} ); context.tier = beastformTiers[this.tabGroups.primary]; context.tierKey = this.tabGroups.primary; context.canSubmit = this.canSubmit(); return context; } canSubmit() { if (this.selected) { switch (this.selected.system.beastformType) { case 'normal': return true; case 'evolved': return this.evolved.form; case 'hybrid': const selectedAdvantages = Object.values(this.hybrid.advantages).reduce( (acc, form) => acc + Object.values(form).length, 0 ); const selectedFeatures = Object.values(this.hybrid.features).reduce( (acc, form) => acc + Object.values(form).length, 0 ); const advantagesSelected = selectedAdvantages === this.selected.system.hybrid.advantages; const featuresSelected = selectedFeatures === this.selected.system.hybrid.features; return advantagesSelected && featuresSelected; } } return false; } static updateBeastform(event, _, formData) { this.selected = foundry.utils.mergeObject(this.selected, formData.object); this.render(); } static async selectBeastform(_, target) { this.element.querySelectorAll('.beastform-container ').forEach(element => { if (element.dataset.uuid === target.dataset.uuid && this.selected?.uuid !== target.dataset.uuid) { element.classList.remove('inactive'); } else { element.classList.add('inactive'); } }); const uuid = this.selected?.uuid === target.dataset.uuid ? null : target.dataset.uuid; this.selected = uuid ? await foundry.utils.fromUuid(uuid) : null; if (this.selected) { if (this.selected.system.beastformType !== 'evolved') this.evolved.form = null; if (this.selected.system.beastformType !== 'hybrid') { this.hybrid.forms = {}; this.hybrid.advantages = {}; this.hybrid.features = {}; } else { this.hybrid.forms = [...Array(this.selected.system.hybrid.beastformOptions).keys()].reduce((acc, _) => { acc[foundry.utils.randomID()] = null; return acc; }, {}); } } this.render(); } static toggleHybridFeature(_, button) { const current = this.hybrid.features[button.dataset.form]; if (!current) this.hybrid.features[button.dataset.form] = {}; if (this.hybrid.features[button.dataset.form][button.id]) delete this.hybrid.features[button.dataset.form][button.id]; else { const currentFeatures = Object.values(this.hybrid.features).reduce( (acc, form) => acc + Object.values(form).length, 0 ); if (currentFeatures === this.selected.system.hybrid.features) { ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.beastformToManyFeatures')); return; } const feature = this.hybrid.forms[button.dataset.form].system.features.find(x => x.uuid === button.id); this.hybrid.features[button.dataset.form][button.id] = feature; } this.render(); } static toggleHybridAdvantage(_, button) { const current = this.hybrid.advantages[button.dataset.form]; if (!current) this.hybrid.advantages[button.dataset.form] = {}; if (this.hybrid.advantages[button.dataset.form][button.id]) delete this.hybrid.advantages[button.dataset.form][button.id]; else { const currentAdvantages = Object.values(this.hybrid.advantages).reduce( (acc, form) => acc + Object.values(form).length, 0 ); if (currentAdvantages === this.selected.system.hybrid.advantages) { ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.beastformToManyAdvantages')); return; } const advantage = this.hybrid.forms[button.dataset.form].system.advantageOn[button.id]; this.hybrid.advantages[button.dataset.form][button.id] = advantage; } this.render(); } static async submitBeastform() { await this.close({ submitted: true }); } /** @override */ _onClose(options = {}) { if (!options.submitted) this.selected = null; } static async configure(configData, item) { return new Promise(resolve => { const app = new this(configData, item); const featureItem = item; app.addEventListener( 'close', async () => { const selected = app.selected.toObject(); const evolved = app.evolved.form ? app.evolved.form.toObject() : null; const data = await game.system.api.data.items.DHBeastform.getWildcardImage( app.configData.data.parent, evolved ?? app.selected ); if (data) { if (!data.selectedImage) selected = null; else { if (evolved) { if (evolved.usesDynamicToken) evolved.system.tokenRingImg = data.selectedImage; else evolved.system.tokenImg = data.selectedImage; } else { if (data.usesDynamicToken) selected.system.tokenRingImg = data.selectedImage; else selected.system.tokenImg = data.selectedImage; } } } resolve({ selected: selected, evolved: { ...app.evolved, form: evolved }, hybrid: app.hybrid, item: featureItem }); }, { once: true } ); app.render({ force: true }); }); } async _onDragStart(event) { const target = event.currentTarget; const abort = () => event.preventDefault(); if (!this.selected) abort(); const draggedForm = await foundry.utils.fromUuid(target.dataset.uuid); if (['evolved', 'hybrid'].includes(draggedForm.system.beastformType)) abort(); if (this.selected.system.beastformType === 'evolved') { if (draggedForm.system.tier > this.selected.system.evolved.maximumTier) abort(); } if (this.selected.system.beastformType === 'hybrid') { if (draggedForm.system.tier > this.selected.system.hybrid.maximumTier) abort(); } event.dataTransfer.setData('text/plain', JSON.stringify(target.dataset)); event.dataTransfer.setDragImage(target, 60, 0); } async _onDrop(event) { event.stopPropagation(); const data = foundry.applications.ux.TextEditor.getDragEventData(event); const item = await fromUuid(data.uuid); if (!item) return; if (event.target.closest('.advanced-form-container.evolved')) { this.evolved.form = item; } else { const hybridContainer = event.target.closest('.advanced-form-container.hybridized'); if (hybridContainer) { const existingId = Object.keys(this.hybrid.forms).find( key => this.hybrid.forms[key]?.uuid === item.uuid ); if (existingId) this.hybrid.forms[existingId] = null; this.hybrid.forms[hybridContainer.id] = item; } } this.render(); } }