import SelectDialog from '../dialogs/selectDialog.mjs'; import { getTier } from '../helpers/utils.mjs'; import DhpMulticlassDialog from './multiclassDialog.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; export default class DhpLevelup extends HandlebarsApplicationMixin(ApplicationV2) { constructor(actor) { super({}); this.actor = actor; this.data = foundry.utils.deepClone(actor.system.levelData); this.activeLevel = actor.system.levelData.currentLevel + 1; } get title() { return `${this.actor.name} - Level Up`; } static DEFAULT_OPTIONS = { id: 'daggerheart-levelup', classes: ['daggerheart', 'views', 'levelup'], position: { width: 1200, height: 'auto' }, window: { resizable: true }, actions: { toggleBox: this.toggleBox, advanceLevel: this.advanceLevel, finishLevelup: this.finishLevelup } }; static PARTS = { form: { id: 'levelup', template: 'systems/daggerheart/templates/views/levelup.hbs' } }; async _prepareContext(_options) { let selectedChoices = 0, multiclassing = {}, subclassing = {}; const leveledTiers = Object.keys(this.data.levelups).reduce( (acc, levelKey) => { const levelData = this.data.levelups[levelKey]; ['tier1', 'tier2', 'tier3'].forEach(tierKey => { let tierUpdate = {}; const tierData = levelData[tierKey]; if (tierData) { tierUpdate = Object.keys(tierData).reduce((acc, propertyKey) => { const values = tierData[propertyKey]; const level = Number.parseInt(levelKey); acc[propertyKey] = Object.values(values).map(value => { if (value && level === this.activeLevel) selectedChoices++; if (propertyKey === 'multiclass') multiclassing[levelKey] = true; if (propertyKey === 'subclass') subclassing[tierKey] = true; return { level: level, value: value }; }); return acc; }, {}); } Object.keys(tierUpdate).forEach(propertyKey => { const property = tierUpdate[propertyKey]; const propertyValues = foundry.utils.getProperty(acc, `${tierKey}.${propertyKey}`) ?? []; foundry.utils.setProperty(acc, `${tierKey}.${propertyKey}`, [...propertyValues, ...property]); }); }); return acc; }, { tier1: {}, tier2: {}, tier3: {} } ); const activeTier = getTier(this.activeLevel); const data = Object.keys(SYSTEM.ACTOR.levelupData).reduce((acc, tierKey) => { const tier = SYSTEM.ACTOR.levelupData[tierKey]; acc[tierKey] = { label: game.i18n.localize(tier.label), info: game.i18n.localize(tier.info), pretext: game.i18n.localize(tier.pretext), postext: game.i18n.localize(tier.posttext), active: tierKey <= activeTier, choices: Object.keys(tier.choices).reduce((acc, propertyKey) => { const property = tier.choices[propertyKey]; acc[propertyKey] = { description: property.description, cost: property.cost ?? 1, values: [] }; for (var i = 0; i < property.maxChoices; i++) { const leveledValue = leveledTiers[tierKey][propertyKey]?.[i]; const subclassLock = propertyKey === 'subclass' && Object.keys(multiclassing).find(x => getTier(Number.parseInt(x)) === tierKey); const subclassMulticlassLock = propertyKey === 'multiclass' && subclassing[tierKey]; const multiclassLock = propertyKey === 'multiclass' && Object.keys(multiclassing).length > 0 && !( leveledValue && Object.keys(multiclassing).find(x => Number.parseInt(x) === leveledValue.level) ); const locked = (leveledValue && leveledValue.level !== this.activeLevel) || subclassLock || subclassMulticlassLock || multiclassLock; const disabled = tierKey > activeTier || (selectedChoices === 2 && !(leveledValue && leveledValue.level === this.activeLevel)) || locked; acc[propertyKey].values.push({ selected: leveledValue?.value !== undefined, path: `levelups.${this.activeLevel}.${tierKey}.${propertyKey}.${i}`, description: game.i18n.localize(property.description), disabled: disabled, locked: locked }); } return acc; }, {}) }; return acc; }, {}); return { data: data, activeLevel: this.activeLevel, changedLevel: this.actor.system.levelData.changedLevel, completedSelection: selectedChoices === 2 }; } static async toggleBox(_, button) { const path = button.dataset.path; if (foundry.utils.getProperty(this.data, path)) { const pathParts = path.split('.'); const arrayPart = pathParts.slice(0, pathParts.length - 1).join('.'); let array = foundry.utils.getProperty(this.data, arrayPart); if (button.dataset.levelAttribute === 'multiclass') { array = []; } else { delete array[Number.parseInt(pathParts[pathParts.length - 1])]; } foundry.utils.setProperty(this.data, arrayPart, array); } else { const updates = [{ path: path, value: { level: this.activeLevel } }]; const levelChoices = SYSTEM.ACTOR.levelChoices[button.dataset.levelAttribute]; if (button.dataset.levelAttribute === 'subclass') { if (!this.actor.system.multiclassSubclass) { updates[0].value.value = { multiclass: false, feature: this.actor.system.subclass.system.specializationFeature.unlocked ? 'mastery' : 'specialization' }; } else { const choices = [ { name: this.actor.system.subclass.name, value: this.actor.system.subclass.uuid }, { name: this.actor.system.multiclassSubclass.name, value: this.actor.system.multiclassSubclass.uuid } ]; const indexes = await SelectDialog.selectItem({ actor: this.actor, choices: choices, title: levelChoices.title, nrChoices: 1 }); if (indexes.length === 0) { this.render(); return; } const multiclassSubclass = choices[indexes[0]].name === this.actor.system.multiclassSubclass.name; updates[0].value.value = { multiclass: multiclassSubclass, feature: this.actor.system.multiclassSubclass.system.specializationFeature.unlocked ? 'mastery' : 'specialization' }; } } else if (button.dataset.levelAttribute === 'multiclass') { const multiclassAwait = new Promise(resolve => { new DhpMulticlassDialog(this.actor.name, this.actor.system.class, resolve).render(true); }); const multiclassData = await multiclassAwait; if (!multiclassData) { this.render(); return; } const pathParts = path.split('.'); const arrayPart = pathParts.slice(0, pathParts.length - 1).join('.'); updates[0] = { path: [arrayPart, '0'].join('.'), value: { level: this.activeLevel, value: { class: multiclassData.class, subclass: multiclassData.subclass, domain: multiclassData.domain, level: this.activeLevel } } }; updates[1] = { path: [arrayPart, '1'].join('.'), value: { level: this.activeLevel, value: { class: multiclassData.class, subclass: multiclassData.subclass, domain: multiclassData.domain, level: this.activeLevel } } }; } else { if (levelChoices.choices.length > 0) { if (typeof levelChoices.choices === 'string') { const choices = foundry.utils .getProperty(this.actor, levelChoices.choices) .map(x => ({ name: x.description, value: x.id })); const indexes = await SelectDialog.selectItem({ actor: this.actor, choices: choices, title: levelChoices.title, nrChoices: levelChoices.nrChoices }); if (indexes.length === 0) { this.render(); return; } updates[0].value.value = choices .filter((_, index) => indexes.includes(index)) .map(x => x.value); } else { const indexes = await SelectDialog.selectItem({ actor: this.actor, choices: levelChoices.choices, title: levelChoices.title, nrChoices: levelChoices.nrChoices }); if (indexes.length === 0) { this.render(); return; } updates[0].value.value = levelChoices.choices[indexes[0]].path; } } } const update = updates.reduce((acc, x) => { acc[x.path] = x.value; return acc; }, {}); this.data = foundry.utils.mergeObject(this.data, update); } this.render(); } static advanceLevel() { this.activeLevel += 1; this.render(); } static async finishLevelup() { this.data.currentLevel = this.data.changedLevel; let multiclass = null; for (var level in this.data.levelups) { for (var tier in this.data.levelups[level]) { for (var category in this.data.levelups[level][tier]) { for (var value in this.data.levelups[level][tier][category]) { if (category === 'multiclass') { multiclass = this.data.levelups[level][tier][category][value].value; this.data.levelups[level][tier][category][value] = true; } else { this.data.levelups[level][tier][category][value] = this.data.levelups[level][tier][category][value].value ?? true; } } } } } const tiersMoved = getTier(this.actor.system.levelData.changedLevel, true) - getTier(this.actor.system.levelData.currentLevel, true); const experiences = Array.from(Array(tiersMoved), (_, index) => ({ id: foundry.utils.randomID(), level: this.actor.system.experiences.length + index * 3, description: '', value: 1 })); await this.actor.update( { system: { levelData: this.data, experiences: [...this.actor.system.experiences, ...experiences] } }, { diff: false } ); if (!this.actor.multiclass && multiclass) { const multiclassClass = (await fromUuid(multiclass.class.uuid)).toObject(); multiclassClass.system.domains = [multiclass.domain.id]; multiclassClass.system.multiclass = multiclass.level; const multiclassFeatures = []; for (var i = 0; i < multiclassClass.system.features.length; i++) { const feature = (await fromUuid(multiclassClass.system.features[i].uuid)).toObject(); feature.system.multiclass = multiclass.level; multiclassFeatures.push(feature); } const multiclassSubclass = (await fromUuid(multiclass.subclass.uuid)).toObject(); multiclassSubclass.system.multiclass = multiclass.level; const multiclassSubclassFeatures = {}; const features = [ multiclassSubclass.system.foundationFeature, multiclassSubclass.system.specializationFeature, multiclassSubclass.system.masteryFeature ]; for (var i = 0; i < features.length; i++) { const path = i === 0 ? 'foundationFeature' : i === 1 ? 'specializationFeature' : 'masteryFeature'; const feature = features[i]; for (var ability of feature.abilities) { const data = (await fromUuid(ability.uuid)).toObject(); if (i > 0) data.system.disabled = true; data.system.multiclass = multiclass.level; if (!multiclassSubclassFeatures[path]) multiclassSubclassFeatures[path] = [data]; else multiclassSubclassFeatures[path].push(data); // data.uuid = feature.uuid; // const abilityData = await this._onDropItemCreate(data); // ability.uuid = abilityData[0].uuid; // createdItems.push(abilityData); } } for (let subclassFeaturesKey in multiclassSubclassFeatures) { const values = multiclassSubclassFeatures[subclassFeaturesKey]; const abilityResults = await this.actor.createEmbeddedDocuments('Item', values); for (var i = 0; i < abilityResults.length; i++) { multiclassSubclass.system[subclassFeaturesKey].abilities[i].uuid = abilityResults[i].uuid; } } await this.actor.createEmbeddedDocuments('Item', [ multiclassClass, ...multiclassFeatures, multiclassSubclass ]); } this.close(); } }