From 43444dfd4205a20b9b98bdc6cf8aed75de169e3a Mon Sep 17 00:00:00 2001 From: WBHarry Date: Fri, 6 Jun 2025 02:33:38 +0200 Subject: [PATCH] Rework to level up once at a time --- daggerheart.mjs | 1 + lang/en.json | 7 +- module/applications/chatMessage.mjs | 4 + module/applications/levelup.mjs | 403 ++++++++----- module/data/levelTier.mjs | 8 +- module/data/levelup.mjs | 538 ++++++------------ module/data/pc.mjs | 16 +- module/documents/actor.mjs | 280 ++------- module/helpers/utils.mjs | 18 + styles/daggerheart.css | 24 +- styles/levelup.less | 22 + .../levelup/parts/multiclass-preview-card.hbs | 15 + templates/views/levelup/tabs/advancements.hbs | 13 +- templates/views/levelup/tabs/selections.hbs | 6 +- templates/views/levelup/tabs/summary.hbs | 4 +- .../views/levelup/tabs/tab-navigation.hbs | 33 ++ 16 files changed, 653 insertions(+), 739 deletions(-) create mode 100644 templates/views/levelup/parts/multiclass-preview-card.hbs create mode 100644 templates/views/levelup/tabs/tab-navigation.hbs diff --git a/daggerheart.mjs b/daggerheart.mjs index bac9c5e1..b4bd4455 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -296,6 +296,7 @@ const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/sheets/pc/parts/advancementCard.hbs', 'systems/daggerheart/templates/components/card-preview.hbs', 'systems/daggerheart/templates/views/levelup/parts/selectable-card-preview.hbs', + 'systems/daggerheart/templates/views/levelup/parts/multiclass-preview-card.hbs', 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs' ]); }; diff --git a/lang/en.json b/lang/en.json index 7ecc4a42..9565ce5d 100755 --- a/lang/en.json +++ b/lang/en.json @@ -764,8 +764,13 @@ "selections": "Advancement Choices", "summary": "Summary" }, - "AdvanceLevel": "Continue To Level {level}", + "navigateLevel": "To Level {level}", + "navigateToSummary": "To Summary", "TakeLevelUp": "Finish Level Up", + "Delevel": { + "title": "Go back to previous level", + "content": "Returning to the previous level selection will remove all selections made for this level. Do you want to proceed?" + }, "Selections": { "emptyDomainCardHint": "Domain Card Level {level} or below" }, diff --git a/module/applications/chatMessage.mjs b/module/applications/chatMessage.mjs index a8cf6f50..256e46b3 100644 --- a/module/applications/chatMessage.mjs +++ b/module/applications/chatMessage.mjs @@ -3,6 +3,10 @@ import { DualityRollColor } from '../data/settings/Appearance.mjs'; export default class DhpChatMessage extends ChatMessage { async renderHTML() { + if (this.type === 'dualityRoll' || this.type === 'adversaryRoll' || this.type === 'abilityUse') { + this.content = await foundry.applications.handlebars.renderTemplate(this.content, this.system); + } + /* We can change to fully implementing the renderHTML function if needed, instead of augmenting it. */ const html = await super.renderHTML(); diff --git a/module/applications/levelup.mjs b/module/applications/levelup.mjs index ce7c4c7c..2dd19db9 100644 --- a/module/applications/levelup.mjs +++ b/module/applications/levelup.mjs @@ -1,7 +1,7 @@ import { abilities } from '../config/actorConfig.mjs'; import { domains } from '../config/domainConfig.mjs'; import { DhLevelup } from '../data/levelup.mjs'; -import { tagifyElement } from '../helpers/utils.mjs'; +import { getDeleteKeys, tagifyElement } from '../helpers/utils.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -16,6 +16,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) this.levelup = new DhLevelup(DhLevelup.initializeData(this.levelTiers, playerLevelupData, actor.system.level)); this._dragDrop = this._createDragDropHandlers(); + this.tabGroups.primary = 'advancements'; } get title() { @@ -33,7 +34,9 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) save: this.save, viewCompendium: this.viewCompendium, selectPreview: this.selectPreview, - selectDomain: this.selectDomain + selectDomain: this.selectDomain, + updateCurrentLevel: this.updateCurrentLevel, + activatePart: this.activatePart }, form: { handler: this.updateForm, @@ -44,7 +47,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) }; static PARTS = { - tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, + tabs: { template: 'systems/daggerheart/templates/views/levelup/tabs/tab-navigation.hbs' }, advancements: { template: 'systems/daggerheart/templates/views/levelup/tabs/advancements.hbs' }, selections: { template: 'systems/daggerheart/templates/views/levelup/tabs/selections.hbs' }, summary: { template: 'systems/daggerheart/templates/views/levelup/tabs/summary.hbs' } @@ -86,68 +89,101 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) } async _preparePartContext(partId, context) { + const currentLevel = this.levelup.levels[this.levelup.currentLevel]; switch (partId) { + case 'tabs': + const previous = + this.levelup.currentLevel === this.levelup.startLevel ? null : this.levelup.currentLevel - 1; + const next = this.levelup.currentLevel === this.levelup.endLevel ? null : this.levelup.currentLevel + 1; + context.navigate = { + previous: { + disabled: !previous, + label: previous + ? game.i18n.format('DAGGERHEART.Application.LevelUp.navigateLevel', { level: previous }) + : '', + fromSummary: this.tabGroups.primary === 'summary' + }, + next: { + disabled: !this.levelup.currentLevelFinished, + label: next + ? game.i18n.format('DAGGERHEART.Application.LevelUp.navigateLevel', { level: next }) + : '', + toSummary: !next, + show: this.tabGroups.primary !== 'summary' + } + }; + break; case 'selections': - context.advancementChoices = this.levelup.selectionData.reduce((acc, data) => { - const advancementChoice = { - ...data, - path: `tiers.${data.tier}.levels.${data.level}.optionSelections.${data.optionKey}.${data.checkboxNr}.data` - }; - if (acc[data.type]) acc[data.type].push(advancementChoice); - else acc[data.type] = [advancementChoice]; + const advancementChoices = Object.keys(currentLevel.choices).reduce((acc, choiceKey) => { + Object.keys(currentLevel.choices[choiceKey]).forEach(checkboxNr => { + const checkbox = currentLevel.choices[choiceKey][checkboxNr]; + const data = { + ...checkbox, + path: `levels.${this.levelup.currentLevel}.choices.${choiceKey}.${checkboxNr}`, + level: this.levelup.currentLevel + }; + + if (!acc[choiceKey]) acc[choiceKey] = []; + acc[choiceKey].push(data); + }); return acc; }, {}); - const traits = Object.values(context.advancementChoices.trait ?? {}); + const traits = Object.values(advancementChoices.trait ?? {}); context.traits = { - values: traits.filter(trait => !trait.locked && trait.data.length > 0).flatMap(trait => trait.data), - active: traits.length > 0 && traits.filter(trait => !trait.locked).length > 0 + values: traits.filter(trait => trait.data.length > 0).flatMap(trait => trait.data), + active: traits.length > 0 }; - const experienceIncreases = Object.values(context.advancementChoices.experience ?? {}).filter( - x => !x.locked - ); + const experienceIncreases = Object.values(advancementChoices.experience ?? {}); context.experienceIncreases = { values: experienceIncreases - .filter(trait => !trait.locked && trait.data.length > 0) - .flatMap(trait => trait.data), + .filter(exp => exp.data.length > 0) + .flatMap(exp => + exp.data.map(data => this.actor.system.experiences.find(x => x.id === data).description) + ), active: experienceIncreases.length > 0 }; - context.newExperiences = Object.keys(this.levelup.allInitialAchievements).flatMap(level => { - const achievement = this.levelup.allInitialAchievements[level]; - return Object.keys(achievement.newExperiences).map(key => ({ - ...achievement.newExperiences[key], - level: level, + context.newExperiences = Object.keys(currentLevel.achievements.experiences).map(key => { + const experience = currentLevel.achievements.experiences[key]; + return { + ...experience, + level: this.levelup.currentLevel, key: key - })); + }; }); const allDomainCards = { - ...context.advancementChoices.domainCard, - ...this.levelup.domainCards + ...advancementChoices.domainCard, + ...currentLevel.achievements.domainCards }; - const allDomainCardValues = Object.values(allDomainCards); + const allDomainCardKeys = Object.keys(allDomainCards); context.domainCards = []; - for (var domainCard of allDomainCardValues) { - if (domainCard.locked) continue; + for (var key of allDomainCardKeys) { + const domainCard = allDomainCards[key]; + if (domainCard.level > this.levelup.endLevel) continue; const uuid = domainCard.data?.length > 0 ? domainCard.data[0] : domainCard.uuid; - const card = uuid ? await foundry.utils.fromUuid(uuid) : { path: domainCard.path }; + const card = uuid ? await foundry.utils.fromUuid(uuid) : {}; + context.domainCards.push({ ...(card.toObject?.() ?? card), emptySubtext: game.i18n.format( 'DAGGERHEART.Application.LevelUp.Selections.emptyDomainCardHint', { level: domainCard.level } ), + path: domainCard.data + ? `${domainCard.path}.data` + : `levels.${domainCard.level}.achievements.domainCards.${key}.uuid`, limit: domainCard.level, compendium: 'domains' }); } - const subclassSelections = context.advancementChoices.subclass?.flatMap(x => x.data) ?? []; + const subclassSelections = advancementChoices.subclass?.flatMap(x => x.data) ?? []; const multiclassSubclass = this.actor.system.multiclass?.system?.subclasses?.[0]; const possibleSubclasses = [ @@ -156,30 +192,29 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) ]; const selectedSubclasses = possibleSubclasses.filter(x => subclassSelections.includes(x.uuid)); context.subclassCards = []; - if (context.advancementChoices.subclass?.length > 0) { + if (advancementChoices.subclass?.length > 0) { for (var subclass of possibleSubclasses) { const data = await foundry.utils.fromUuid(subclass.uuid); const selected = selectedSubclasses.some(x => x.uuid === data.uuid); context.subclassCards.push({ ...data.toObject(), uuid: data.uuid, - disabled: - !selected && subclassSelections.length === context.advancementChoices.subclass.length, + // disabled: + // !selected && subclassSelections.length === context.advancementChoices.subclass.length, selected: selected }); } } - const multiclasses = Object.values(context.advancementChoices.multiclass ?? {}); + const multiclasses = Object.values(advancementChoices.multiclass ?? {}); if (multiclasses?.[0]) { const data = multiclasses[0]; - const path = `tiers.${data.tier}.levels.${data.level}.optionSelections.${data.optionKey}.${data.checkboxNr}`; const multiclass = data.data.length > 0 ? await foundry.utils.fromUuid(data.data[0]) : {}; context.multiclass = { + ...data, ...(multiclass.toObject?.() ?? multiclass), uuid: multiclass.uuid, - path: path, domains: multiclass?.system?.domains.map(key => { const domain = domains[key]; @@ -198,36 +233,51 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) break; case 'summary': + const { current: currentActorLevel, changed: changedActorLevel } = this.actor.system.levelData.level; const actorArmor = this.actor.system.armor; - const { current: currentLevel, changed: changedLevel } = this.actor.system.levelData.level; + const levelKeys = Object.keys(this.levelup.levels); + let achivementProficiency = 0; const achievementCards = []; - for (var card of Object.values(this.levelup.domainCards)) { - if (card.uuid) { - const itemCard = await foundry.utils.fromUuid(card.uuid); - achievementCards.push(itemCard); + let achievementExperiences = []; + for (var levelKey of levelKeys) { + const level = this.levelup.levels[levelKey]; + if (Number(levelKey) < this.levelup.startLevel) continue; + + achivementProficiency += level.achievements.proficiency ?? 0; + const cards = level.achievements.domainCards ? Object.values(level.achievements.domainCards) : null; + if (cards) { + for (var card of cards) { + const itemCard = await foundry.utils.fromUuid(card.uuid); + achievementCards.push(itemCard); + } } + + achievementExperiences = level.achievements.experiences + ? Object.values(level.achievements.experiences).reduce((acc, experience) => { + if (experience.name) acc.push(experience); + return acc; + }, []) + : []; } context.achievements = { proficiency: { old: this.actor.system.proficiency.value, - new: - this.actor.system.proficiency.value + - Object.values(this.levelup.allInitialAchievements).reduce( - (acc, x) => acc + x.proficiency, - 0 - ) + new: this.actor.system.proficiency.value + achivementProficiency, + shown: achivementProficiency > 0 }, damageThresholds: { major: { old: this.actor.system.damageThresholds.major, - new: this.actor.system.damageThresholds.major + changedLevel - currentLevel + new: this.actor.system.damageThresholds.major + changedActorLevel - currentActorLevel }, severe: { old: this.actor.system.damageThresholds.severe, new: this.actor.system.damageThresholds.severe + - (actorArmor ? changedLevel - currentLevel : (changedLevel - currentLevel) * 2) + (actorArmor + ? changedActorLevel - currentActorLevel + : (changedActorLevel - currentActorLevel) * 2) }, unarmored: !actorArmor }, @@ -236,37 +286,43 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) shown: achievementCards.length > 0 }, experiences: { - values: Object.values(this.levelup.allInitialAchievements).flatMap(achievements => { - return Object.values(achievements.newExperiences).reduce((acc, experience) => { - if (experience.name) acc.push(experience); - - return acc; - }, []); - }) + values: achievementExperiences } }; - context.achievements.proficiency.shown = - context.achievements.proficiency.new > context.achievements.proficiency.old; - context.achievements.experiences.shown = context.achievements.experiences.values.length > 0; - const advancementChoices = this.levelup.selectionData.reduce((acc, data) => { - const advancementChoice = { - ...data, - path: `tiers.${data.tier}.levels.${data.level}.optionSelections.${data.optionKey}.${data.checkboxNr}.data` - }; - if (acc[data.type]) acc[data.type].push(advancementChoice); - else acc[data.type] = [advancementChoice]; + const advancement = {}; + for (var levelKey of levelKeys) { + const level = this.levelup.levels[levelKey]; + if (Number(levelKey) < this.levelup.startLevel) continue; - return acc; - }, {}); - - const advancementCards = []; - const cardChoices = advancementChoices.domainCard ?? []; - for (var card of cardChoices) { - if (card.data.length > 0) { - for (var data of card.data) { - const itemCard = await foundry.utils.fromUuid(data); - advancementCards.push(itemCard); + for (var choiceKey of Object.keys(level.choices)) { + const choice = level.choices[choiceKey]; + for (var checkbox of Object.values(choice)) { + switch (choiceKey) { + case 'proficiency': + case 'hitPoint': + case 'stress': + case 'evasion': + advancement[choiceKey] = advancement[choiceKey] + ? advancement[choiceKey] + Number(checkbox.value) + : Number(checkbox.value); + break; + case 'domainCard': + if (!advancement[choiceKey]) advancement[choiceKey] = []; + if (checkbox.data.length === 1) { + const choiceItem = await foundry.utils.fromUuid(checkbox.data[0]); + advancement[choiceKey].push(choiceItem.toObject()); + } + break; + case 'experience': + if (!advancement[choiceKey]) advancement[choiceKey] = []; + const data = checkbox.data.map( + data => + this.actor.system.experiences.find(x => x.id === data)?.description ?? '' + ); + advancement[choiceKey].push({ data: data, value: checkbox.value }); + break; + } } } } @@ -275,36 +331,29 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) statistics: { proficiency: { old: context.achievements.proficiency.new, - new: - context.achievements.proficiency.new + - Object.values(advancementChoices.proficiency ?? {}).reduce((acc, x) => acc + x.value, 0) + new: context.achievements.proficiency.new + (advancement.proficiency ?? 0) }, hitPoints: { old: this.actor.system.resources.hitPoints.max, - new: - this.actor.system.resources.hitPoints.max + - Object.values(advancementChoices.hitPoint ?? {}).reduce((acc, x) => acc + x.value, 0) + new: this.actor.system.resources.hitPoints.max + (advancement.hitPoint ?? 0) }, stress: { old: this.actor.system.resources.stress.max, - new: - this.actor.system.resources.stress.max + - Object.values(advancementChoices.stress ?? {}).reduce((acc, x) => acc + x.value, 0) + new: this.actor.system.resources.stress.max + (advancement.stress ?? 0) }, evasion: { old: this.actor.system.evasion.value, - new: - this.actor.system.evasion.value + - Object.values(advancementChoices.evasion ?? {}).reduce((acc, x) => acc + x.value, 0) + new: this.actor.system.evasion.value + (advancement.evasion ?? 0) } }, - traits: Object.values(advancementChoices.trait ?? {}).flatMap(x => - x.data.map(data => game.i18n.localize(abilities[data].label)) - ), - domainCards: advancementCards, - experiences: Object.values(advancementChoices.experience ?? {}).flatMap(x => - x.data.map(data => ({ name: data, modifier: x.value })) - ) + traits: + advancement.trait?.flatMap(x => + x.data.map(data => game.i18n.localize(abilities[data].label)) + ) ?? [], + domainCards: advancement.domainCard ?? [], + experiences: + advancement.experience?.flatMap(x => x.data.map(data => ({ name: data, modifier: x.value }))) ?? + [] }; context.advancements.statistics.proficiency.shown = @@ -375,13 +424,24 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) tagifyUpdate = type => async (_, { option, removed }) => { - const updatePath = this.levelup.selectionData.reduce((acc, data) => { - if (data.optionKey === type && removed ? data.data.includes(option) : data.data.length < data.amount) { - return `tiers.${data.tier}.levels.${data.level}.optionSelections.${data.optionKey}.${data.checkboxNr}.data`; - } + const updatePath = Object.keys(this.levelup.levels[this.levelup.currentLevel].choices).reduce( + (acc, choiceKey) => { + const choice = this.levelup.levels[this.levelup.currentLevel].choices[choiceKey]; + Object.keys(choice).forEach(checkboxNr => { + const checkbox = choice[checkboxNr]; + if ( + choiceKey === type && removed + ? checkbox.data.includes(option) + : checkbox.data.length < checkbox.amount + ) { + acc = `levels.${this.levelup.currentLevel}.choices.${choiceKey}.${checkboxNr}.data`; + } + }); - return acc; - }, null); + return acc; + }, + null + ); if (!updatePath) { ui.notifications.error( @@ -408,8 +468,10 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) if (event.target.closest('.domain-cards')) { const target = event.target.closest('.card-preview-container'); if (item.type === 'domainCard') { - if (!this.actor.system.class.system.domains.includes(item.system.domain)) { - // Also needs to check for multiclass adding a new domain + if ( + !this.actor.system.class.system.domains.includes(item.system.domain) && + this.levelup.multiclass?.domain !== item.system.domain + ) { ui.notifications.error( game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.error.domainCardWrongDomain') ); @@ -424,8 +486,18 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) } if ( - Object.values(this.levelup.domainCards).some(x => x.uuid === item.uuid) || - this.levelup.selectionData.some(x => x.type === 'domainCard' && x.data.includes(item.uuid)) + Object.values(this.levelup.levels).some(level => { + const achievementExists = Object.values(level.achievements.domainCards).some( + card => card.uuid === item.uuid + ); + const advancementExists = Object.keys(level.choices).some(choiceKey => { + if (choiceKey !== 'domainCard') return false; + const choice = level.choices[choiceKey]; + return Object.values(choice).some(checkbox => checkbox.data.includes(item.uuid)); + }); + + return achievementExists || advancementExists; + }) ) { ui.notifications.error( game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.error.domainCardDuplicate') @@ -447,7 +519,13 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) } await this.levelup.updateSource({ + multiclass: { class: item.uuid, level: this.levelup.currentLevel }, [target.dataset.path]: { + tier: Number(target.dataset.tier), + minCost: Number(target.dataset.minCost), + amount: target.dataset.amount ? Number(target.dataset.amount) : null, + value: target.dataset.value, + type: target.dataset.type, data: item.uuid, secondaryData: null } @@ -461,14 +539,12 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) event.stopPropagation(); const button = event.currentTarget; + const update = {}; if (!button.checked) { - await this.levelup.updateSource({ - [`tiers.${button.dataset.tier}.levels.${button.dataset.level}.optionSelections.${button.dataset.option}.-=${button.dataset.checkboxNr}`]: - null - }); + update[`levels.${button.dataset.level}.choices.${button.dataset.option}.-=${button.dataset.checkboxNr}`] = + null; } else { - const levelSelections = this.levelup.levelSelections; - if (levelSelections.total + Number(button.dataset.cost) > this.levelup.maxSelections) { + if (!this.levelup.levels[this.levelup.currentLevel].nrSelections.available) { ui.notifications.info( game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.info.insufficentAdvancements') ); @@ -476,38 +552,18 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) return; } - const nrTiers = Object.keys(this.levelup.tiers).length; - let lowestLevelChoice = null; - for (var tierKey = Number(button.dataset.tier); tierKey <= nrTiers + 1; tierKey++) { - const tier = this.levelup.tiers[tierKey]; - lowestLevelChoice = Object.keys(levelSelections.available).reduce((currentLowest, key) => { - const level = Number(key); - if (levelSelections.available[key] >= button.dataset.cost && tier.belongingLevels.includes(level)) { - if (!currentLowest || level < currentLowest) return level; - } - - return currentLowest; - }, null); - - if (lowestLevelChoice) break; - } - - if (!lowestLevelChoice) { - ui.notifications.info( - game.i18n.localize( - 'DAGGERHEART.Application.LevelUp.notifications.info.insufficientTierAdvancements' - ) - ); - this.render(); - return; - } - - await this.levelup.updateSource({ - [`tiers.${button.dataset.tier}.levels.${lowestLevelChoice}.optionSelections.${button.dataset.option}.${button.dataset.checkboxNr}`]: - { selected: true, minCost: button.dataset.cost } - }); + update[ + `levels.${this.levelup.currentLevel}.choices.${button.dataset.option}.${button.dataset.checkboxNr}` + ] = { + tier: Number(button.dataset.tier), + minCost: Number(button.dataset.cost), + amount: button.dataset.amount ? Number(button.dataset.amount) : null, + value: button.dataset.value, + type: button.dataset.type + }; } + await this.levelup.updateSource(update); this.render(); } @@ -529,14 +585,69 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) } static async selectDomain(_, button) { - const option = this.levelup.selectionData.find(x => x.type === 'multiclass'); - const path = `tiers.${option.tier}.levels.${option.level}.optionSelections.${option.optionKey}.${option.checkboxNr}.secondaryData`; - await this.levelup.updateSource({ [path]: option.secondaryData ? null : button.dataset.domain }); + const option = foundry.utils.getProperty(this.levelup, button.dataset.path); + const domain = option.secondaryData ? null : button.dataset.domain; + + await this.levelup.updateSource({ + multiclass: { domain }, + [`${button.dataset.path}.secondaryData`]: domain + }); + this.render(); + } + + static async updateCurrentLevel(_, button) { + if (!button.dataset.forward) { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.localize('DAGGERHEART.Application.LevelUp.Delevel.title') + }, + content: game.i18n.format('DAGGERHEART.Application.LevelUp.Delevel.content') + }); + + if (!confirmed) return; + + await this.levelup.updateSource({ + currentLevel: Math.min(this.levelup.currentLevel - 1, this.levelup.startLevel), + levels: Object.keys(this.levelup.levels).reduce((acc, key) => { + const level = this.levelup.levels[key]; + if (Number(key) === this.levelup.currentLevel) { + acc[key] = { + achievements: { + experiences: getDeleteKeys(level.achievements.experiences, 'name', ''), + domainCards: getDeleteKeys(level.achievements.domainCards, 'uuid', null) + }, + choices: getDeleteKeys(level.choices) + }; + } + return acc; + }, {}), + ...(this.levelup.multiclass?.level === this.levelup.currentLevel ? { '-=multiclass': null } : {}) + }); + } else { + await this.levelup.updateSource({ + currentLevel: Math.max(this.levelup.currentLevel + 1, this.levelup.endLevel) + }); + } + + this.tabGroups.primary = 'advancements'; + this.render(); + } + + static activatePart(_, button) { + this.tabGroups.primary = button.dataset.part; this.render(); } static async save() { - await this.actor.levelUp(this.levelup.levelupData); + const levelupData = Object.keys(this.levelup.levels).reduce((acc, level) => { + if (level >= this.levelup.startLevel) { + acc[level] = this.levelup.levels[level].toObject(); + } + + return acc; + }, {}); + + await this.actor.levelUp(levelupData); this.close(); } } diff --git a/module/data/levelTier.mjs b/module/data/levelTier.mjs index 17e150bf..6cf11252 100644 --- a/module/data/levelTier.mjs +++ b/module/data/levelTier.mjs @@ -242,14 +242,14 @@ export const defaultLevelTiers = { }, proficiency: { label: 'DAGGERHEART.LevelUp.Options.proficiency', - checkboxSelections: 1, + checkboxSelections: 2, minCost: 2, type: LevelOptionType.proficiency.id, value: 1 }, multiclass: { label: 'DAGGERHEART.LevelUp.Options.multiclass', - checkboxSelections: 1, + checkboxSelections: 2, minCost: 2, type: LevelOptionType.multiclass.id } @@ -323,14 +323,14 @@ export const defaultLevelTiers = { }, proficiency: { label: 'DAGGERHEART.LevelUp.Options.proficiency', - checkboxSelections: 1, + checkboxSelections: 2, minCost: 2, type: LevelOptionType.proficiency.id, value: 1 }, multiclass: { label: 'DAGGERHEART.LevelUp.Options.multiclass', - checkboxSelections: 1, + checkboxSelections: 2, minCost: 2, type: LevelOptionType.multiclass.id } diff --git a/module/data/levelup.mjs b/module/data/levelup.mjs index 64c4157c..9052a9b4 100644 --- a/module/data/levelup.mjs +++ b/module/data/levelup.mjs @@ -3,89 +3,60 @@ import { LevelOptionType } from './levelTier.mjs'; export class DhLevelup extends foundry.abstract.DataModel { static initializeData(levelTierData, pcLevelData) { - const availableChoicesPerLevel = levelTierData.availableChoicesPerLevel; + const startLevel = pcLevelData.level.current + 1; + const currentLevel = pcLevelData.level.current + 1; + const endLevel = pcLevelData.level.changed; + + const tiers = {}; + const levels = {}; const tierKeys = Object.keys(levelTierData.tiers); - const maxLevel = levelTierData.tiers[tierKeys[tierKeys.length - 1]].levels.end; + tierKeys.forEach(key => { + const tier = levelTierData.tiers[key]; + const belongingLevels = []; + for (var i = tier.levels.start; i <= tier.levels.end; i++) { + if (i <= endLevel) { + const initialAchievements = i === tier.levels.start ? tier.initialAchievements : {}; + const experiences = initialAchievements.experience + ? [...Array(initialAchievements.experience.nr).keys()].reduce((acc, _) => { + acc[foundry.utils.randomID()] = { + name: '', + modifier: initialAchievements.experience.modifier + }; + return acc; + }, {}) + : {}; + const domainCards = [...Array(tier.domainCardByLevel).keys()].reduce((acc, _) => { + const id = foundry.utils.randomID(); + acc[id] = { uuid: null, itemUuid: null, level: i }; + return acc; + }, {}); - const totalLevelProgression = []; - for (var level = pcLevelData.level.current + 1; level <= pcLevelData.level.changed; level++) { - totalLevelProgression.push(level); - } - - const pcSelections = Object.values(pcLevelData.levelups).flatMap(x => x.selections); - const tiers = tierKeys.reduce((acc, key) => { - acc[key] = DhLevelupTier.initializeData( - levelTierData.tiers[key], - maxLevel, - pcSelections.filter(x => x.tier === Number(key)), - pcLevelData.level.changed - ); - - return acc; - }, {}); - - const allInitialAchievements = Object.values(tiers).reduce((acc, tier) => { - const levelThreshold = Math.min(...tier.belongingLevels); - - if (totalLevelProgression.includes(levelThreshold)) { - acc[levelThreshold] = { - newExperiences: {}, - proficiency: tier.initialAchievements.proficiency - }; - [...Array(tier.initialAchievements.experience.nr).keys()].forEach(_ => { - acc[levelThreshold].newExperiences[foundry.utils.randomID()] = { - name: '', - modifier: tier.initialAchievements.experience.modifier - }; - }); - } - - return acc; - }, {}); - - const domainCards = Object.keys(tiers).reduce((acc, tierKey) => { - const tier = tiers[tierKey]; - for (var level of tier.belongingLevels) { - if (level <= pcLevelData.level.current) { - const cardId = foundry.utils.randomID(); - acc[cardId] = { - ...pcLevelData.levelups[level].domainCards[0], - path: `domainCards.${cardId}.uuid`, - locked: true, - level: level, - tier: tierKey - }; - } else if (level <= pcLevelData.level.changed) { - for (var domainCardSlot = 1; domainCardSlot <= tier.domainCardByLevel; domainCardSlot++) { - const cardId = foundry.utils.randomID(); - acc[cardId] = { - uuid: null, - tier: tierKey, - level: level, - domainCardSlot: domainCardSlot, - path: `domainCards.${cardId}.uuid` - }; - } + levels[i] = DhLevelupLevel.initializeData(pcLevelData.levelups[i], tier.availableOptions, { + ...initialAchievements, + experiences, + domainCards + }); } + + belongingLevels.push(i); } - return acc; - }, {}); + tiers[key] = { + name: tier.name, + belongingLevels: belongingLevels, + options: Object.keys(tier.options).reduce((acc, key) => { + acc[key] = tier.options[key].toObject(); + return acc; + }, {}) + }; + }); return { - tiers: tiers, - maxSelections: [...Array(pcLevelData.level.changed).keys()].reduce((acc, index) => { - const level = index + 1; - const availableChoices = availableChoicesPerLevel[level]; - if (level > 1 && availableChoices) { - acc += availableChoices; - } - - return acc; - }, 0), - allInitialAchievements: allInitialAchievements, - domainCards: domainCards, - progressionLevels: totalLevelProgression + tiers, + levels, + startLevel, + currentLevel, + endLevel }; } @@ -93,191 +64,134 @@ export class DhLevelup extends foundry.abstract.DataModel { const fields = foundry.data.fields; return { - tiers: new fields.TypedObjectField(new fields.EmbeddedDataField(DhLevelupTier)), - maxSelections: new fields.NumberField({ required: true, integer: true }), - allInitialAchievements: new fields.TypedObjectField( + tiers: new fields.TypedObjectField( new fields.SchemaField({ - newExperiences: new fields.TypedObjectField( + name: new fields.StringField({ required: true }), + belongingLevels: new fields.ArrayField(new fields.NumberField({ required: true, integer: true })), + options: new fields.TypedObjectField( new fields.SchemaField({ - name: new fields.StringField({ required: true }), - modifier: new fields.NumberField({ required: true, integer: true }) + label: new fields.StringField({ required: true }), + checkboxSelections: new fields.NumberField({ required: true, integer: true }), + minCost: new fields.NumberField({ required: true, integer: true }), + type: new fields.StringField({ required: true, choices: LevelOptionType }), + value: new fields.NumberField({ integer: true }), + amount: new fields.NumberField({ integer: true }) }) - ), - proficiency: new fields.NumberField({ required: true, integer: true }) + ) }) ), - domainCards: new fields.TypedObjectField( - new fields.SchemaField({ - uuid: new fields.StringField({ required: true, nullable: true, initial: null }), - tier: new fields.NumberField({ required: true, integer: true }), - level: new fields.NumberField({ required: true, integer: true }), - domainCardSlot: new fields.NumberField({ required: true, integer: true }), - path: new fields.StringField({ required: true }), - locked: new fields.BooleanField({ required: true, initial: false }) - }) - ), - progressionLevels: new fields.ArrayField(new fields.NumberField({ required: true, integer: true })) + levels: new fields.TypedObjectField(new fields.EmbeddedDataField(DhLevelupLevel)), + startLevel: new fields.NumberField({ required: true, integer: true }), + currentLevel: new fields.NumberField({ required: true, integer: true }), + endLevel: new fields.NumberField({ required: true, integer: true }), + multiclass: new fields.SchemaField({ + class: new fields.StringField({ required: true }), + domain: new fields.StringField(), + level: new fields.NumberField({ required: true, integer: true }) + }) }; } - get canLevelUp() { - if (this.levelSelections.total !== this.maxSelections) return false; + #levelFinished(levelKey) { + const allSelectionsMade = this.levels[levelKey].nrSelections.available === 0; + const allChoicesMade = Object.keys(this.levels[levelKey].choices).every(choiceKey => { + const choice = this.levels[levelKey].choices[choiceKey]; + return Object.values(choice).every(checkbox => { + switch (choiceKey) { + case 'trait': + case 'experience': + case 'domainCard': + case 'subclass': + return checkbox.amount ? checkbox.data.length === checkbox.amount : checkbox.data.length === 1; + case 'multiclass': + const classSelected = checkbox.data.length === 1; + const domainSelected = checkbox.secondaryData; + return classSelected && domainSelected; + default: + return true; + } + }); + }); + const experiencesSelected = !this.levels[levelKey].achievements.experiences + ? true + : Object.values(this.levels[levelKey].achievements.experiences).every(exp => exp.name); + const domainCardsSelected = Object.values(this.levels[levelKey].achievements.domainCards) + .filter(x => x.level <= this.endLevel) + .every(card => card.uuid); + const allAchievementsSelected = experiencesSelected && domainCardsSelected; - const achievementsDone = - Object.values(this.allInitialAchievements).every(achievement => - Object.values(achievement.newExperiences).every(experience => experience.name) - ) && Object.values(this.domainCards).every(card => card.uuid); - - const selectionData = this.selectionData; - let advancementsDone = true; - for (var advancement of selectionData) { - switch (advancement.type) { - case 'trait': - case 'experience': - case 'domainCard': - case 'subclass': - advancementsDone = advancement.amount - ? advancement.data.length === advancement.amount - : advancement.data.length === 1; - break; - case 'multiclass': - const classSelected = advancement.data.length === 1; - const domainSelected = advancement.secondaryData; - advancementsDone = classSelected && domainSelected; - break; - } - - if (!advancementsDone) break; - } - - return achievementsDone && advancementsDone; + return allSelectionsMade && allChoicesMade && allAchievementsSelected; } - get levelSelections() { - return Object.values(this.tiers).reduce( - (acc, tier) => { - acc.total += tier.selections.total; - for (var key in tier.selections.available) { - const availableSelections = tier.selections.available[key]; - acc.totalAvailable += availableSelections; + get currentLevelFinished() { + return this.#levelFinished(this.currentLevel); + } - if (acc.available[key]) acc.available[key] += availableSelections; - else acc.available[key] = availableSelections; - } + get allLevelsFinished() { + return Object.keys(this.levels) + .filter(level => Number(level) >= this.startLevel) + .every(this.#levelFinished.bind(this)); + } + + get tiersForRendering() { + const tierKeys = Object.keys(this.tiers); + const selections = Object.keys(this.levels).reduce( + (acc, key) => { + const level = this.levels[key]; + Object.keys(level.choices).forEach(optionKey => { + const choice = level.choices[optionKey]; + Object.keys(choice).forEach(checkboxNr => { + const checkbox = choice[checkboxNr]; + if (!acc[checkbox.tier][optionKey]) acc[checkbox.tier][optionKey] = {}; + Object.keys(choice).forEach(checkboxNr => { + acc[checkbox.tier][optionKey][checkboxNr] = { ...checkbox, level: Number(key) }; + }); + }); + }); return acc; }, - { total: 0, available: {}, totalAvailable: 0 } + tierKeys.reduce((acc, key) => { + acc[key] = {}; + return acc; + }, {}) ); - } - get selectionData() { - return Object.keys(this.tiers).flatMap(tierKey => { + return tierKeys.map(tierKey => { const tier = this.tiers[tierKey]; - return Object.keys(tier.levels).flatMap(levelKey => { - const level = tier.levels[levelKey]; - return Object.keys(level.optionSelections).flatMap(optionSelectionKey => { - const selection = level.optionSelections[optionSelectionKey]; - const optionSelect = tier.options[optionSelectionKey]; - - return Object.keys(selection).map(checkboxNr => { - const selectionObj = selection[checkboxNr]; - return { - tier: Number(tierKey), - level: Number(levelKey), - optionKey: optionSelectionKey, - type: optionSelect.type, - checkboxNr: Number(checkboxNr), - value: optionSelect.value, - amount: optionSelect.amount, - data: selectionObj.data, - secondaryData: selectionObj.secondaryData, - locked: selectionObj.locked - }; - }); - }); - }); - }); - } - - get levelupData() { - const leveledSelections = this.selectionData.reduce((acc, data) => { - if (data.type === 'domainCard' && data.locked) return acc; - if (!acc[data.level]) acc[data.level] = [data]; - else acc[data.level].push(data); - - return acc; - }, {}); - return this.progressionLevels.reduce((acc, level) => { - acc[level] = { - domainCards: Object.values(this.domainCards).filter(x => !x.locked && x.level === level), - selections: leveledSelections[level] - }; - if (this.allInitialAchievements[level]) { - acc[level].achievements = { - experiences: this.allInitialAchievements[level].newExperiences, - proficiency: this.allInitialAchievements[level].proficiency - }; - } - - return acc; - }, {}); - } - - /* Data to render all options from */ - get tierCheckboxGroups() { - const multiclassSelected = Object.values(this.tiers).some(tier => - Object.values(tier.levels).some(level => { - return Object.keys(level.optionSelections).some(option => - Object.values(level.optionSelections[option]).some(x => option === 'multiclass' && x.selected) - ); - }) - ); - return Object.keys(this.tiers).map(tierKey => { - const tier = this.tiers[tierKey]; - const subclassSelected = Object.values(tier.levels).some(level => { - return Object.keys(level.optionSelections).some(option => - Object.values(level.optionSelections[option]).some(x => option === 'subclass' && x.selected) - ); - }); - return { - tierActive: tier.active, - tierName: tier.name, + name: tier.name, + active: this.currentLevel >= Math.min(...tier.belongingLevels), groups: Object.keys(tier.options).map(optionKey => { const option = tier.options[optionKey]; - const checkboxes = [...Array(option.checkboxSelections).keys()].flatMap(checkboxNr => { - const levelId = Object.keys(tier.levels).find(levelKey => { - const optionSelect = tier.levels[levelKey].optionSelections; - return Object.keys(optionSelect) - .filter(key => key === optionKey) - .some(optionKey => optionSelect[optionKey][checkboxNr]?.selected); - }); - const selected = Boolean(levelId); - const disabled = !levelId - ? false - : tier.levels[levelId].optionSelections[optionKey][checkboxNr]?.locked; - const multiclassDisabled = - !selected && optionKey === 'multiclass' && (multiclassSelected || subclassSelected); - return [...Array(option.minCost)].map(_ => ({ - ...option, - tier: tierKey, - level: levelId, - selected: selected, - optionKey: optionKey, - checkboxNr: checkboxNr, - disabled: disabled || multiclassDisabled, - cost: option.minCost - })); + const checkboxes = [...Array(option.checkboxSelections).keys()].flatMap(index => { + const checkboxNr = index + 1; + const checkboxData = selections[tierKey]?.[optionKey]?.[checkboxNr]; + const checkbox = { ...option, checkboxNr, tier: tierKey }; + + if (checkboxData) { + checkbox.level = checkboxData.level; + checkbox.selected = true; + checkbox.disabled = checkbox.level !== this.currentLevel; + } + + return checkbox; }); - return { label: game.i18n.localize(option.label), - checkboxGroups: chunkify(checkboxes, option.minCost, chunkedBoxes => ({ - multi: option.minCost > 1, - checkboxes: chunkedBoxes - })) + checkboxGroups: chunkify(checkboxes, option.minCost, chunkedBoxes => { + const anySelected = chunkedBoxes.some(x => x.selected); + const anyDisabled = chunkedBoxes.some(x => x.disabled); + return { + multi: option.minCost > 1, + checkboxes: chunkedBoxes.map(x => ({ + ...x, + selected: anySelected, + disabled: anyDisabled + })) + }; + }) }; }) }; @@ -285,114 +199,23 @@ export class DhLevelup extends foundry.abstract.DataModel { } } -class DhLevelupTier extends foundry.abstract.DataModel { - static initializeData(levelTier, levelEndCap, pcLevelData, pcLevel) { - const levels = {}; - for (var level = levelTier.levels.start; level <= levelEndCap; level++) { - levels[level] = DhLevelupLevel.initializeData( - level <= Math.min(pcLevel, levelTier.levels.end) ? levelTier.availableOptions : 0, - levelTier.options, - pcLevelData.filter(x => x.level === level), - level < pcLevel - ); - } - - var belongingLevels = []; - for (var i = levelTier.levels.start; i <= levelTier.levels.end; i++) { - belongingLevels.push(i); - } - - return { - tier: levelTier.tier, - name: levelTier.name, - active: pcLevel >= levelTier.levels.start, - options: Object.keys(levelTier.options).reduce((acc, key) => { - acc[key] = levelTier.options[key]; - - return acc; - }, {}), - belongingLevels: belongingLevels, - initialAchievements: levelTier.initialAchievements, - domainCardByLevel: levelTier.domainCardByLevel, - levels: levels - }; - } - - static defineSchema() { - const fields = foundry.data.fields; - - return { - tier: new fields.NumberField({ required: true, integer: true }), - name: new fields.StringField({ required: true }), - active: new fields.BooleanField({ required: true, initial: true }), - options: new fields.TypedObjectField(new fields.EmbeddedDataField(DhLevelupTierOption)), - belongingLevels: new fields.ArrayField(new fields.NumberField({ required: true, integer: true })), - initialAchievements: new fields.SchemaField({ - experience: new fields.SchemaField({ - nr: new fields.NumberField({ required: true, initial: 1 }), - modifier: new fields.NumberField({ required: true, initial: 2 }) - }), - proficiency: new fields.NumberField({ integer: true, initial: 1 }) - }), - domainCardByLevel: new fields.NumberField({ required: true, integer: true }), - levels: new fields.TypedObjectField(new fields.EmbeddedDataField(DhLevelupLevel)) - }; - } - - get initialAchievementData() { - return this.active ? this.initialAchievements : null; - } - - get selections() { - const allSelections = Object.keys(this.levels).reduce( - (acc, key) => { - const { selections, available } = this.levels[key].nrSelections; - - if (acc.available[key]) acc.available[key] += available; - else acc.available[key] = available; - - acc.totalAvailable += available; - acc.total += selections; - - return acc; - }, - { available: {}, totalAvailable: 0, total: 0 } - ); - return { - available: allSelections.available, - totalAvailable: allSelections.totalAvailable, - total: allSelections.total - }; - } -} - -class DhLevelupTierOption extends foundry.abstract.DataModel { - static defineSchema() { - const fields = foundry.data.fields; - - return { - label: new fields.StringField({ required: true }), - checkboxSelections: new fields.NumberField({ required: true, integer: true }), - minCost: new fields.NumberField({ required: true, integer: true }), - type: new fields.StringField({ required: true, choices: LevelOptionType }), - value: new fields.NumberField({ integer: true }), - amount: new fields.NumberField({ integer: true }) - }; - } -} - -class DhLevelupLevel extends foundry.abstract.DataModel { - static initializeData(maxSelections, optionSelections, levelData, locked) { +export class DhLevelupLevel extends foundry.abstract.DataModel { + static initializeData(levelData = { selections: [] }, maxSelections, achievements) { return { maxSelections: maxSelections, - optionSelections: levelData.reduce((acc, data) => { + achievements: { + experiences: levelData.achievements?.experiences ?? achievements.experiences ?? {}, + domainCards: levelData.achievements?.domainCards + ? levelData.achievements.domainCards.reduce((acc, card, index) => { + acc[index] = { ...card }; + return acc; + }, {}) + : (achievements.domainCards ?? {}), + proficiency: levelData.achievements?.proficiency ?? achievements.proficiency ?? null + }, + choices: levelData.selections.reduce((acc, data) => { if (!acc[data.optionKey]) acc[data.optionKey] = {}; - acc[data.optionKey][data.checkboxNr] = { - ...data, - minCost: optionSelections[data.optionKey].minCost, - amount: optionSelections[data.optionKey].amount, - locked: locked - }; + acc[data.optionKey][data.checkboxNr] = { ...data }; return acc; }, {}) @@ -404,15 +227,32 @@ class DhLevelupLevel extends foundry.abstract.DataModel { return { maxSelections: new fields.NumberField({ required: true, integer: true }), - optionSelections: new fields.TypedObjectField( + achievements: new fields.SchemaField({ + experiences: new fields.TypedObjectField( + new fields.SchemaField({ + name: new fields.StringField({ required: true }), + modifier: new fields.NumberField({ required: true, integer: true }) + }) + ), + domainCards: new fields.TypedObjectField( + new fields.SchemaField({ + uuid: new fields.StringField({ required: true, nullable: true, initial: null }), + itemUuid: new fields.StringField({ required: true }), + level: new fields.NumberField({ required: true, integer: true }) + }) + ), + proficiency: new fields.NumberField({ integer: true }) + }), + choices: new fields.TypedObjectField( new fields.TypedObjectField( new fields.SchemaField({ - selected: new fields.BooleanField({ required: true, initial: true }), + tier: new fields.NumberField({ required: true, integer: true }), minCost: new fields.NumberField({ required: true, integer: true }), amount: new fields.NumberField({ integer: true }), - locked: new fields.BooleanField({ required: true, initial: false }), + value: new fields.StringField(), data: new fields.ArrayField(new fields.StringField()), - secondaryData: new fields.StringField() + secondaryData: new fields.StringField(), + type: new fields.StringField({ required: true }) }) ) ) @@ -420,11 +260,9 @@ class DhLevelupLevel extends foundry.abstract.DataModel { } get nrSelections() { - const selections = Object.keys(this.optionSelections).reduce((acc, optionKey) => { - const selection = this.optionSelections[optionKey]; - acc += Object.values(selection) - .filter(x => x.selected) - .reduce((acc, x) => acc + x.minCost, 0); + const selections = Object.keys(this.choices).reduce((acc, choiceKey) => { + const choice = this.choices[choiceKey]; + acc += Object.values(choice).reduce((acc, x) => acc + x.minCost, 0); return acc; }, 0); diff --git a/module/data/pc.mjs b/module/data/pc.mjs index 77530ab0..a259ee9d 100644 --- a/module/data/pc.mjs +++ b/module/data/pc.mjs @@ -375,16 +375,16 @@ class DhPCLevelData extends foundry.abstract.DataModel { modifier: new fields.NumberField({ required: true, integer: true }) }) ), + domainCards: new fields.ArrayField( + new fields.SchemaField({ + uuid: new fields.StringField({ required: true }), + itemUuid: new fields.StringField({ required: true }) + }) + ), proficiency: new fields.NumberField({ integer: true }) }, { nullable: true, initial: null } ), - domainCards: new fields.ArrayField( - new fields.SchemaField({ - uuid: new fields.StringField({ required: true }), - itemUuid: new fields.StringField({ required: true }) - }) - ), selections: new fields.ArrayField( new fields.SchemaField({ tier: new fields.NumberField({ required: true, integer: true }), @@ -393,9 +393,11 @@ class DhPCLevelData extends foundry.abstract.DataModel { type: new fields.StringField({ required: true, choices: LevelOptionType }), checkboxNr: new fields.NumberField({ required: true, integer: true }), value: new fields.NumberField({ integer: true }), + minCost: new fields.NumberField({ integer: true }), amount: new fields.NumberField({ integer: true }), data: new fields.ArrayField(new fields.StringField({ required: true })), - uuid: new fields.StringField({ required: true }) + secondaryData: new fields.StringField(), + itemUuid: new fields.StringField({ required: true }) }) ) }) diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 9a6bedd6..bc116550 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -33,247 +33,91 @@ export default class DhpActor extends Actor { if (newLevel > this.system.levelData.level.current) { await this.update({ 'system.levelData.level.changed': newLevel }); } else { - const levelTiers = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.LevelTiers); - const passedLevelTiers = Object.values(levelTiers.tiers) - .filter(x => newLevel <= x.levels.end) - .map(x => x.levels.start); - const firstPassedLevelTier = passedLevelTiers.length > 0 ? Math.min(...passedLevelTiers) : null; - - const changes = this.getLevelChangedFeatures( - newLevel, - this.system.levelData.level.changed, - this.system.levelData.levelups - ); - - for (var domainCard of changes.domainCards) { - const uuid = domainCard.itemUuid ? domainCard.itemUuid : domainCard.uuid; - const itemCard = await this.items.find(x => x.uuid === uuid); - itemCard.delete(); - } - - var traitsUpdate = changes.traits.reduce((acc, trait) => { - const currentTrait = this.system.traits[trait.data]; - acc[trait.data] = { - bonus: currentTrait.bonus - 1, - tierMarked: trait.first - ? !firstPassedLevelTier || trait.level <= firstPassedLevelTier - ? true - : false - : currentTrait.tierMarked - }; + const updatedLevelups = Object.keys(this.system.levelData.levelups).reduce((acc, level) => { + if (Number(level) > newLevel) acc[`-=${level}`] = null; return acc; }, {}); - const newExperienceKeys = Object.keys(changes.experiences); - const experienceUpdate = this.system.experiences.filter(x => !newExperienceKeys.includes(x.id)); - for (var experience of changes.experienceIncreases) { - for (var id of experience.data) { - const existingExperience = experienceUpdate.find(x => x.id === id); - existingExperience.value -= experience.value; - } + const domainCards = Object.keys(this.system.levelData.levelups) + .filter(x => x > newLevel) + .flatMap(levelKey => { + const level = this.system.levelData.levelups[levelKey]; + const achievementCards = level.achievements.domainCards.map(x => x.itemUuid); + const advancementCards = level.selections.filter(x => x.type === 'domainCard').map(x => x.itemUuid); + return [...achievementCards, ...advancementCards]; + }); + + for (var domainCard of domainCards) { + const itemCard = await this.items.find(x => x.uuid === domainCard); + itemCard.delete(); } - for (var subclass of changes.subclasses) { - /* Implemented after datamodel rework is in */ - } - - if (changes.multiclass) { - /* Implemented after datamodel rework is in */ - } - - const newLevelData = { - level: { - current: newLevel, - changed: newLevel - }, - levelups: Object.keys(this.system.levelData.levelups).reduce((acc, levelKey) => { - const level = Number(levelKey); - if (level > newLevel) acc[`-=${level}`] = null; - - return acc; - }, {}) - }; await this.update({ system: { - 'traits': traitsUpdate, - 'experiences': experienceUpdate, - 'resources': { - hitPoints: { - bonus: this.system.resources.hitPoints.bonus - changes.hitPoint + levelData: { + level: { + current: newLevel, + changed: newLevel }, - stress: { - bonus: this.system.resources.stress.bonus - changes.stress - } - }, - 'evasion.bonus': this.system.evasion.bonus - changes.evasion, - 'proficiency.bonus': this.system.proficiency.bonus - changes.proficiency, - 'levelData': newLevelData + levelups: updatedLevelups + } } }); } } - getLevelChangedFeatures(startLevel, endLevel, levelData) { - const changedFeatures = { - hitPoint: 0, - stress: 0, - evasion: 0, - proficiency: 0, - domainCards: [], - multiclass: null, - subclasses: [], - traits: [], - experiences: {}, - experienceIncreases: [] - }; - - for (var level = startLevel + 1; level <= endLevel; level++) { - if (!levelData[level]) continue; - - const achievements = levelData[level].achievements; - const selections = levelData[level].selections.reduce((acc, selection) => { - if (!acc[selection.type]) acc[selection.type] = [selection]; - else acc[selection.type].push(selection); - - return acc; - }, {}); - - changedFeatures.hitPoint += selections.hitPoint - ? selections.hitPoint.reduce((acc, hp) => acc + Number(hp.value), 0) - : 0; - changedFeatures.stress += selections.stress - ? selections.stress.reduce((acc, stress) => acc + Number(stress.value), 0) - : 0; - changedFeatures.evasion += selections.evasion - ? selections.evasion.reduce((acc, evasion) => acc + Number(evasion.value), 0) - : 0; - changedFeatures.proficiency += - (achievements?.proficiency ?? 0) + - (selections.proficiency - ? selections.proficiency.reduce((acc, proficiency) => acc + Number(proficiency.value), 0) - : 0); - changedFeatures.domainCards.push( - ...[ - ...levelData[level].domainCards, - ...(selections.domainCard?.flatMap(x => x.data.map(data => ({ ...x, data: data }))) ?? []) - ] - ); - changedFeatures.traits.push( - ...(selections.trait - ? selections.trait.flatMap(x => - x.data.map(data => ({ - level: x.level, - data: data, - first: level === startLevel, - last: level === endLevel - })) - ) - : []) - ); - changedFeatures.experiences = Object.keys(achievements?.experiences ? achievements.experiences : {}).reduce( - (acc, key) => { - acc[key] = achievements.experiences[key]; - - return acc; - }, - changedFeatures.experiences - ); - changedFeatures.experienceIncreases.push(...(selections.experience ?? [])); - changedFeatures.subclasses.push(...(selections.subclasses ? [] : [])); - changedFeatures.multiclass = selections.multiclass ? selections.multiclass[0] : null; - } - - return changedFeatures; - } - async levelUp(levelupData) { - const levelTiers = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.LevelTiers); - const passedLevelTiers = Object.values(levelTiers.tiers) - .filter(x => this.system.levelData.level.changed >= x.levels.start) - .map(x => x.levels.start); - const lastPassedLevelTier = passedLevelTiers.length > 0 ? Math.max(...passedLevelTiers) : null; - - const changes = this.getLevelChangedFeatures( - this.system.levelData.level.current, - this.system.levelData.level.changed, - levelupData - ); - - for (var card of changes.domainCards) { - const fromAchievement = Boolean(card.uuid); - const domainCard = await foundry.utils.fromUuid(fromAchievement ? card.uuid : card.data); - const createdCards = await this.createEmbeddedDocuments('Item', [domainCard]); - const newCard = createdCards[0]; - if (fromAchievement) { - const levelupCard = levelupData[card.level].domainCards.find( - x => x.tier === card.tier && x.level === card.level - ); - if (levelupCard) levelupCard.itemUuid = newCard.uuid; - } else { - const levelupCard = levelupData[card.level].selections.find( - x => - x.tier === card.tier && - x.level === card.level && - x.optionKey === card.optionKey && - x.checkboxNr === card.checkboxNr - ); - if (levelupCard) levelupCard.uuid = newCard.uuid; + const levelups = {}; + for (var levelKey of Object.keys(levelupData)) { + const level = levelupData[levelKey]; + const achievementDomainCards = []; + for (var card of Object.values(level.achievements.domainCards)) { + const item = await foundry.utils.fromUuid(card.uuid); + const embeddedItem = await this.createEmbeddedDocuments('Item', [item.toObject()]); + card.itemUuid = embeddedItem[0].uuid; + achievementDomainCards.push(card); } - } - var traitsUpdate = changes.traits.reduce((acc, trait) => { - const currentTrait = this.system.traits[trait.data]; - acc[`${trait.data}`] = { - bonus: currentTrait.bonus + 1, - tierMarked: trait.last - ? !lastPassedLevelTier || trait.level >= lastPassedLevelTier - ? true - : false - : currentTrait.tierMarked + const selections = []; + for (var optionKey of Object.keys(level.choices)) { + const selection = level.choices[optionKey]; + for (var checkboxNr of Object.keys(selection)) { + const checkbox = selection[checkboxNr]; + let itemUuid = null; + + if (checkbox.type === 'domainCard') { + const item = await foundry.utils.fromUuid(checkbox.data[0]); + const embeddedItem = await this.createEmbeddedDocuments('Item', [item.toObject()]); + itemUuid = embeddedItem[0].uuid; + } + + selections.push({ + ...checkbox, + level: Number(levelKey), + optionKey: optionKey, + checkboxNr: Number(checkboxNr), + itemUuid + }); + } + } + + levelups[levelKey] = { + achievements: { + ...level.achievements, + domainCards: achievementDomainCards + }, + selections: selections }; - return acc; - }, {}); - - const experienceUpdate = this.system.experiences; - const newExperienceKeys = Object.keys(changes.experiences); - for (var key of newExperienceKeys) { - const experience = changes.experiences[key]; - experienceUpdate.push({ id: key, description: experience.name, value: experience.modifier }); - } - - for (var experience of changes.experienceIncreases) { - for (var id of experience.data) { - const existingExperience = experienceUpdate.find(x => x.id === id); - existingExperience.value += experience.value; - } - } - - for (var subclass of changes.subclasses) { - /* Implemented after datamodel rework is in */ - } - - if (changes.multiclass) { - /* Implemented after datamodel rework is in */ } await this.update({ system: { - 'traits': traitsUpdate, - 'experiences': experienceUpdate, - 'resources': { - hitPoints: { - bonus: this.system.resources.hitPoints.bonus + changes.hitPoint + levelData: { + level: { + current: this.system.levelData.level.changed }, - stress: { - bonus: this.system.resources.stress.bonus + changes.stress - } - }, - 'evasion.bonus': this.system.evasion.bonus + changes.evasion, - 'proficiency.bonus': this.system.proficiency.bonus + changes.proficiency, - 'levelData': { - 'level.current': this.system.levelData.level.changed, - 'levelups': levelupData + levelups: levelups } } }); diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 8eaa4325..9e9caa37 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -205,3 +205,21 @@ export const tagifyElement = (element, options, onChange, tagifyOptions = {}) => }; tagifyElement.on('change', onSelect); }; + +export const getDeleteKeys = (property, innerProperty, innerPropertyDefaultValue) => { + return Object.keys(property).reduce((acc, key) => { + if (innerProperty) { + if (innerPropertyDefaultValue !== undefined) { + acc[`${key}`] = { + [innerProperty]: innerPropertyDefaultValue + }; + } else { + acc[`${key}.-=${innerProperty}`] = null; + } + } else { + acc[`-=${key}`] = null; + } + + return acc; + }, {}); +}; diff --git a/styles/daggerheart.css b/styles/daggerheart.css index 0c123587..683afd20 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -2735,6 +2735,24 @@ div.daggerheart.views.multiclass { flex-direction: column; gap: 8px; } +.daggerheart.levelup .levelup-navigation-container { + display: flex; + align-items: center; + gap: 22px; +} +.daggerheart.levelup .levelup-navigation-container nav { + flex: 1; +} +.daggerheart.levelup .levelup-navigation-container .levelup-navigation-actions { + width: 306px; + display: flex; + justify-content: end; + gap: 16px; + margin-right: 4px; +} +.daggerheart.levelup .levelup-navigation-container .levelup-navigation-actions * { + width: calc(50% - 8px); +} .daggerheart.levelup .tiers-container { display: flex; gap: 16px; @@ -3091,21 +3109,21 @@ div.daggerheart.views.multiclass { font-style: normal; font-weight: 400; font-display: swap; - src: url(https://fonts.gstatic.com/s/cinzel/v23/8vIU7ww63mVu7gtR-kwKxNvkNOjw-tbnTYo.ttf) format('truetype'); + src: url(https://fonts.gstatic.com/s/cinzel/v25/8vIU7ww63mVu7gtR-kwKxNvkNOjw-tbnTYo.ttf) format('truetype'); } @font-face { font-family: 'Cinzel'; font-style: normal; font-weight: 700; font-display: swap; - src: url(https://fonts.gstatic.com/s/cinzel/v23/8vIU7ww63mVu7gtR-kwKxNvkNOjw-jHgTYo.ttf) format('truetype'); + src: url(https://fonts.gstatic.com/s/cinzel/v25/8vIU7ww63mVu7gtR-kwKxNvkNOjw-jHgTYo.ttf) format('truetype'); } @font-face { font-family: 'Cinzel Decorative'; font-style: normal; font-weight: 700; font-display: swap; - src: url(https://fonts.gstatic.com/s/cinzeldecorative/v17/daaHSScvJGqLYhG8nNt8KPPswUAPniZoaelD.ttf) format('truetype'); + src: url(https://fonts.gstatic.com/s/cinzeldecorative/v18/daaHSScvJGqLYhG8nNt8KPPswUAPniZoaelD.ttf) format('truetype'); } @font-face { font-family: 'Montserrat'; diff --git a/styles/levelup.less b/styles/levelup.less index 64a57d7b..77183486 100644 --- a/styles/levelup.less +++ b/styles/levelup.less @@ -28,6 +28,28 @@ } } + .levelup-navigation-container { + display: flex; + align-items: center; + gap: 22px; + + nav { + flex: 1; + } + + .levelup-navigation-actions { + width: 306px; + display: flex; + justify-content: end; + gap: 16px; + margin-right: 4px; + + * { + width: calc(50% - 8px); + } + } + } + .tiers-container { display: flex; gap: 16px; diff --git a/templates/views/levelup/parts/multiclass-preview-card.hbs b/templates/views/levelup/parts/multiclass-preview-card.hbs new file mode 100644 index 00000000..c1627e1e --- /dev/null +++ b/templates/views/levelup/parts/multiclass-preview-card.hbs @@ -0,0 +1,15 @@ +
+ {{#if this.img}} + +
{{this.name}}
+ {{else}} +
+
+ +
{{this.emptySubtext}}
+
+
+ {{/if}} +
\ No newline at end of file diff --git a/templates/views/levelup/tabs/advancements.hbs b/templates/views/levelup/tabs/advancements.hbs index 022ebac1..26081e71 100644 --- a/templates/views/levelup/tabs/advancements.hbs +++ b/templates/views/levelup/tabs/advancements.hbs @@ -5,9 +5,9 @@ >
- {{#each this.levelup.tierCheckboxGroups as |tier key|}} -
- {{tier.tierName}} + {{#each this.levelup.tiersForRendering as |tier key|}} +
+ {{tier.name}} {{#each tier.groups}}
@@ -19,9 +19,12 @@ type="checkbox" class="selection-checkbox{{#if (gt this.cost 1)}} multi{{/if}}" {{checked this.selected}} {{#if this.disabled}}disabled{{/if}} data-tier="{{this.tier}}" data-level="{{this.level}}" - data-option="{{this.optionKey}}" + data-option="{{this.type}}" data-checkbox-nr="{{this.checkboxNr}}" - data-cost="{{this.cost}}" + data-cost="{{this.minCost}}" + data-amount="{{this.amount}}" + data-value="{{this.value}}" + data-type="{{this.type}}" /> {{/each}}
diff --git a/templates/views/levelup/tabs/selections.hbs b/templates/views/levelup/tabs/selections.hbs index b3eff3a6..9dffbef7 100644 --- a/templates/views/levelup/tabs/selections.hbs +++ b/templates/views/levelup/tabs/selections.hbs @@ -11,7 +11,7 @@ {{#each this.newExperiences}}
- +
{{signedNumber this.modifier}}
@@ -68,10 +68,10 @@

{{localize "DAGGERHEART.Application.LevelUp.summary.multiclass"}}

- {{> "systems/daggerheart/templates/components/card-preview.hbs" img=this.multiclass.img name=this.multiclass.name path=this.multiclass.path compendium=this.multiclass.compendium }} + {{> "systems/daggerheart/templates/views/levelup/parts/multiclass-preview-card.hbs" this.multiclass }}
{{#each this.multiclass.domains}} -
+
{{localize this.label}}
{{#if this.selected}} diff --git a/templates/views/levelup/tabs/summary.hbs b/templates/views/levelup/tabs/summary.hbs index b90211d7..77925cff 100644 --- a/templates/views/levelup/tabs/summary.hbs +++ b/templates/views/levelup/tabs/summary.hbs @@ -84,7 +84,7 @@ {{/if}} {{#if this.advancements.statistics.evasion.shown}}
- {{localize "DAGGERHEART.Application.LevelUp.summary.evasionIncrease" stress=this.advancements.statistics.evasion.old }} + {{localize "DAGGERHEART.Application.LevelUp.summary.evasionIncrease" evasion=this.advancements.statistics.evasion.old }} {{this.advancements.statistics.evasion.new}}
@@ -128,7 +128,7 @@
- +
\ No newline at end of file diff --git a/templates/views/levelup/tabs/tab-navigation.hbs b/templates/views/levelup/tabs/tab-navigation.hbs new file mode 100644 index 00000000..f788b826 --- /dev/null +++ b/templates/views/levelup/tabs/tab-navigation.hbs @@ -0,0 +1,33 @@ +
+ +
+ +
+ {{#if (not this.navigate.previous.disabled)}} + {{#if this.navigate.previous.fromSummary}} + + {{else}} + + {{/if}} + {{/if}} + {{#if this.navigate.next.show}} + {{#if this.navigate.next.toSummary}} + + {{else}} + + {{/if}} + {{else}} +
+ {{/if}} +
+
+ +
\ No newline at end of file