Merge branch 'main' of https://github.com/Foundryborne/daggerheart into feature/enhance-style-applications

This commit is contained in:
moliloo 2025-07-20 20:40:07 -03:00
commit a6c4516238
125 changed files with 2952 additions and 648 deletions

View file

@ -6,6 +6,10 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
this.configData = configData;
this.selected = null;
this.evolved = { form: null };
this.hybrid = { forms: {}, advantages: {}, features: {} };
this._dragDrop = this._createDragDropHandlers();
}
static DEFAULT_OPTIONS = {
@ -17,13 +21,16 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
},
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() {
@ -32,36 +39,217 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
/** @override */
static PARTS = {
beastform: {
template: 'systems/daggerheart/templates/dialogs/beastformDialog.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.beastformTiers = game.items.reduce((acc, x) => {
const tier = CONFIG.DH.GENERAL.tiers[x.system.tier];
if (x.type !== 'beastform' || tier.value > this.configData.tierLimit) return acc;
context.selected = this.selected;
context.selectedBeastformEffect = this.selected?.effects?.find?.(x => x.type === 'beastform');
if (!acc[tier.value]) acc[tier.value] = { label: game.i18n.localize(tier.label), values: {} };
acc[tier.value].values[x.uuid] = { selected: this.selected == x.uuid, value: x };
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;
}, {}); // Also get from compendium when added
context.canSubmit = this.selected;
}, {});
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 = [...(compendiumBeastforms ? compendiumBeastforms : []), ...game.items].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: {} };
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 selectBeastform(_, target) {
this.selected = this.selected === target.dataset.uuid ? null : target.dataset.uuid;
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();
}
@ -71,14 +259,60 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
/** @override */
_onClose(options = {}) {
if (!options.submitted) this.config = false;
if (!options.submitted) this.selected = null;
}
static async configure(configData) {
return new Promise(resolve => {
const app = new this(configData);
app.addEventListener('close', () => resolve(app.selected), { once: true });
app.addEventListener(
'close',
() => resolve({ selected: app.selected, evolved: app.evolved, hybrid: app.hybrid }),
{ 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();
}
}

View file

@ -61,7 +61,7 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application
static updateRollConfiguration(_event, _, formData) {
const { ...rest } = foundry.utils.expandObject(formData.object);
foundry.utils.mergeObject(this.config.roll, rest.roll)
foundry.utils.mergeObject(this.config.roll, rest.roll);
this.config.selectedRollMode = rest.selectedRollMode;
this.render();

View file

@ -10,7 +10,7 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap
this.reject = reject;
this.actor = actor;
this.damage = damage;
const canApplyArmor = damageType.every(t => actor.system.armorApplicableDamageTypes[t] === true);
const maxArmorMarks = canApplyArmor
? Math.min(

View file

@ -12,17 +12,18 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
);
this.nrChoices = {
shortRest: {
taken: 0,
max:
(shortrest ? this.moveData.shortRest.nrChoices : 0) +
actor.system.bonuses.rest[`${shortrest ? 'short' : 'long'}Rest`].shortMoves
},
longRest: {
taken: 0,
max:
(!shortrest ? this.moveData.longRest.nrChoices : 0) +
actor.system.bonuses.rest[`${shortrest ? 'short' : 'long'}Rest`].longMoves
}
};
this.nrChoices.total = { max: this.nrChoices.shortRest.max + this.nrChoices.longRest.max };
}
get title() {
@ -62,50 +63,41 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
);
context.selectedActivity = this.selectedActivity;
context.moveData = this.moveData;
context.nrCurrentChoices = Object.values(this.moveData).reduce((acc, category) => {
acc += Object.values(category.moves).reduce((acc, x) => acc + (x.selected ?? 0), 0);
return acc;
}, 0);
const shortRestMovesSelected = this.#nrSelectedMoves('shortRest');
const longRestMovesSelected = this.#nrSelectedMoves('longRest');
context.nrChoices = {
...this.nrChoices,
shortRest: {
...this.nrChoices.shortRest,
current: Object.values(this.moveData.shortRest.moves).reduce((acc, x) => acc + (x.selected ?? 0), 0)
current: this.nrChoices.shortRest.taken + shortRestMovesSelected
},
longRest: {
...this.nrChoices.longRest,
current: Object.values(this.moveData.longRest.moves).reduce((acc, x) => acc + (x.selected ?? 0), 0)
current: this.nrChoices.longRest.taken + longRestMovesSelected
}
};
context.nrChoices.total = {
...this.nrChoices.total,
current: context.nrChoices.shortRest.current + context.nrChoices.longRest.current
};
context.shortRestMoves = this.nrChoices.shortRest.max > 0 ? this.moveData.shortRest : null;
context.longRestMoves = this.nrChoices.longRest.max > 0 ? this.moveData.longRest : null;
context.disabledDowntime = context.nrChoices.total.current < context.nrChoices.total.max;
context.disabledDowntime = shortRestMovesSelected === 0 && longRestMovesSelected === 0;
return context;
}
static selectMove(_, target) {
const nrSelected = Object.values(this.moveData[target.dataset.category].moves).reduce(
(acc, x) => acc + (x.selected ?? 0),
0
);
const { category, move } = target.dataset;
if (nrSelected === this.nrChoices[target.dataset.category].max) {
const nrSelected = this.#nrSelectedMoves(category);
if (nrSelected + this.nrChoices[category].taken >= this.nrChoices[category].max) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.noMoreMoves'));
return;
}
const move = target.dataset.move;
this.moveData[target.dataset.category].moves[move].selected = this.moveData[target.dataset.category].moves[move]
.selected
? this.moveData[target.dataset.category].moves[move].selected + 1
this.moveData[category].moves[move].selected = this.moveData[category].moves[move].selected
? this.moveData[category].moves[move].selected + 1
: 1;
this.render();
@ -150,7 +142,7 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/ui/chat/downtime.hbs',
{
title: `${this.actor.name} - ${game.i18n.localize(`DAGGERHEART.APPLICATIONS.Downtime.${this.shortRest ? 'shortRest' : 'longRest'}.title`)}`,
title: `${this.actor.name} - ${game.i18n.localize(`DAGGERHEART.APPLICATIONS.Downtime.${this.shortrest ? 'shortRest' : 'longRest'}.title`)}`,
moves: moves
}
)
@ -158,11 +150,33 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV
cls.create(msg.toObject());
this.close();
// Reset selection and update number of taken moves
for (const [catName, category] of Object.entries(this.moveData)) {
for (const move of Object.values(category.moves)) {
if (move.selected > 0) {
this.nrChoices[catName].taken += move.selected;
move.selected = 0;
}
}
}
// We can close the window when all moves are taken
if (
this.nrChoices.shortRest.taken >= this.nrChoices.shortRest.max &&
this.nrChoices.longRest.taken >= this.nrChoices.longRest.max
) {
this.close();
} else {
this.render();
}
}
static async updateData(event, element, formData) {
this.customActivity = foundry.utils.mergeObject(this.customActivity, formData.object);
this.render();
}
#nrSelectedMoves(category) {
return Object.values(this.moveData[category].moves).reduce((acc, x) => acc + (x.selected ?? 0), 0);
}
}