From 81e9bd8c1925ede4ef88cd856d8feed4b01622a6 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Sat, 31 May 2025 21:27:24 +0200 Subject: [PATCH] Added PC level/delevel benefits of leveling up --- daggerheart.mjs | 1 + lang/en.json | 15 +- module/applications/levelup.mjs | 163 ++++++++++++++++- module/applications/sheets/adversary.mjs | 2 +- module/applications/sheets/pc.mjs | 7 +- module/data/levelup.mjs | 131 ++++++++++---- module/data/pc.mjs | 44 +++-- module/documents/actor.mjs | 169 ++++++++++++++++-- module/helpers/utils.mjs | 60 +++++++ styles/daggerheart.css | 89 +++++++-- styles/less/global/elements.less | 48 ++++- styles/levelup.less | 55 +++++- templates/components/card-preview.hbs | 2 +- templates/sheets/parts/defense.hbs | 2 +- .../views/levelup/advancementSelection.hbs | 29 --- .../levelup/parts/selectable-card-preview.hbs | 11 ++ templates/views/levelup/tabs/advancements.hbs | 10 ++ templates/views/levelup/tabs/selections.hbs | 96 +++++++--- templates/views/levelup/tabs/summary.hbs | 10 +- 19 files changed, 790 insertions(+), 154 deletions(-) delete mode 100644 templates/views/levelup/advancementSelection.hbs create mode 100644 templates/views/levelup/parts/selectable-card-preview.hbs diff --git a/daggerheart.mjs b/daggerheart.mjs index 2399ccdf..9885b691 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -285,6 +285,7 @@ const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/sheets/pc/parts/advancementCard.hbs', 'systems/daggerheart/templates/views/parts/level.hbs', 'systems/daggerheart/templates/components/card-preview.hbs', + 'systems/daggerheart/templates/views/levelup/parts/selectable-card-preview.hbs', 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs' ]); }; diff --git a/lang/en.json b/lang/en.json index a18c02dd..0c7c0649 100755 --- a/lang/en.json +++ b/lang/en.json @@ -770,7 +770,11 @@ "damageThresholdSevereIncrease": "Severe: {threshold}", "newExperiences": "New Experiences", "experiencePlaceholder": "A new experience..", - "domainCards": "Domain Cards" + "domainCards": "Domain Cards", + "subclass": "Subclass", + "multiclass": "Multiclass", + "traits": "Increased Traits", + "experienceIncreases": "Experience Increases" }, "notifications": { "info": { @@ -781,7 +785,9 @@ }, "error": { "domainCardWrongDomain": "You don't have access to that Domain", - "domainCardToHighLevel": "The Domain Card is too high level to be selected" + "domainCardToHighLevel": "The Domain Card is too high level to be selected", + "noSelectionsLeft": "Nothing more to select!", + "alreadySelectedClass": "You already have that class!" } } }, @@ -944,7 +950,10 @@ }, "NewItem": "New Item", "NewScar": "New Scar", - "DeleteConfirmation": "Are you sure you want to delete the item - {item}?" + "DeleteConfirmation": "Are you sure you want to delete the item - {item}?", + "Errors": { + "missingClassOrSubclass": "The character doesn't have a class and subclass" + } }, "Adversary": { "Description": "Description", diff --git a/module/applications/levelup.mjs b/module/applications/levelup.mjs index 2aec01a6..06780fcb 100644 --- a/module/applications/levelup.mjs +++ b/module/applications/levelup.mjs @@ -1,4 +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'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -27,7 +30,9 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) resizable: true }, actions: { - save: this.save + save: this.save, + viewCompendium: this.viewCompendium, + selectPreview: this.selectPreview }, form: { handler: this.updateForm, @@ -93,7 +98,27 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) return acc; }, {}); - context.newExperiences = this.levelup.allInitialAchievements.newExperiences; + const traits = Object.values(context.advancementChoices.trait ?? {}); + context.traits = { + values: traits.filter(trait => trait.data.length > 0).flatMap(trait => trait.data), + active: traits.length > 0 + }; + + const experienceIncreases = Object.values(context.advancementChoices.experience ?? {}); + context.experienceIncreases = { + values: experienceIncreases.filter(trait => trait.data.length > 0).flatMap(trait => trait.data), + 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, + key: key + })); + }); + const allDomainCards = { ...context.advancementChoices.domainCard, ...this.levelup.domainCards @@ -102,7 +127,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) context.domainCards = []; for (var domainCard of allDomainCardValues) { - const uuid = domainCard.data ?? domainCard.uuid; + const uuid = domainCard.data?.length > 0 ? domainCard.data[0] : domainCard.uuid; const card = uuid ? await foundry.utils.fromUuid(uuid) : { path: domainCard.path }; context.domainCards.push({ ...(card.toObject?.() ?? card), @@ -114,6 +139,54 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) }); } + const subclassSelections = context.advancementChoices.subclass?.flatMap(x => x.data) ?? []; + + const multiclassSubclass = this.actor.system.multiclass?.system?.subclasses?.[0]; + const possibleSubclasses = [ + this.actor.system.subclass, + ...(multiclassSubclass ? [multiclassSubclass] : []) + ]; + const selectedSubclasses = possibleSubclasses.filter(x => subclassSelections.includes(x.uuid)); + context.subclassCards = []; + if (context.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, + selected: selected + }); + } + } + + const multiclasses = Object.values(context.advancementChoices.multiclass ?? {}); + if (multiclasses?.[0]) { + const data = multiclasses[0]; + const path = `tiers.${data.tier}.levels.${data.level}.optionSelections.${data.optionKey}.${data.checkboxNr}.data`; + const multiclass = + data.data.length > 0 ? await foundry.utils.fromUuid(data.data[0]) : { path: path }; + + context.multiclass = { + ...(multiclass.toObject?.() ?? multiclass), + domains: + multiclass?.system?.domains.map(key => { + const domain = domains[key]; + const alreadySelected = this.actor.system.class.system.domains.includes(key); + + return { + ...domain, + selected: key === data.secondaryData, + disabled: (key !== data.secondaryData && data.secondaryData) || alreadySelected + }; + }) ?? [], + compendium: 'classes', + limit: 1 + }; + } + break; case 'summary': const actorArmor = this.actor.system.armor; @@ -169,9 +242,53 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) htmlElement .querySelectorAll('.selection-checkbox') .forEach(element => element.addEventListener('change', this.selectionClick.bind(this))); + + const traitsTagify = htmlElement.querySelector('.levelup-trait-increases'); + if (traitsTagify) { + tagifyElement(traitsTagify, abilities, this.tagifyUpdate('trait').bind(this)); + } + + const experienceIncreaseTagify = htmlElement.querySelector('.levelup-experience-increases'); + if (experienceIncreaseTagify) { + tagifyElement( + experienceIncreaseTagify, + this.actor.system.experiences.reduce((acc, experience) => { + acc[experience.id] = { label: experience.description }; + + return acc; + }, {}), + this.tagifyUpdate('experience').bind(this) + ); + } + this._dragDrop.forEach(d => d.bind(htmlElement)); } + tagifyUpdate = + type => + async (_, { option, removed }) => { + /* Needs to take Amount into account to allow multiple to be stored in the same option. Array structure? */ + 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`; + } + + return acc; + }, null); + + if (!updatePath) { + ui.notifications.error( + game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.error.noSelectionsLeft') + ); + return; + } + + const currentData = foundry.utils.getProperty(this.levelup, updatePath); + const updatedData = removed ? currentData.filter(x => x !== option) : [...currentData, option]; + await this.levelup.updateSource({ [updatePath]: updatedData }); + this.render(); + }; + static async updateForm(event, _, formData) { const { levelup } = foundry.utils.expandObject(formData.object); await this.levelup.updateSource(levelup); @@ -181,7 +298,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) async _onDrop(event) { const data = foundry.applications.ux.TextEditor.getDragEventData(event); const item = await fromUuid(data.uuid); - if (event.currentTarget.parentElement?.classList?.contains('domain-cards')) { + if (event.target.parentElement?.classList?.contains('domain-cards')) { 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 @@ -191,15 +308,25 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) return; } - if (item.system.level > Number(event.currentTarget.dataset.limit)) { + if (item.system.level > Number(event.target.dataset.limit)) { ui.notifications.error( game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.error.domainCardToHighLevel') ); return; } - const achievementCard = event.currentTarget.dataset.path.startsWith('domainCards'); - await this.levelup.updateSource({ [event.currentTarget.dataset.path]: item.uuid }); + await this.levelup.updateSource({ [event.target.dataset.path]: item.uuid }); + this.render(); + } + } else if (event.target.parentElement?.classList?.contains('multiclass-cards')) { + if (item.type === 'class') { + if (item.name === this.actor.system.class.name) { + ui.notifications.error( + game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.error.alreadySelectedClass') + ); + return; + } + await this.levelup.updateSource({ [event.target.dataset.path]: item.uuid }); this.render(); } } @@ -260,9 +387,27 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) this.render(); } - static async save() { - await this.actor.levelUp(this.levelup.selectionData); + static async viewCompendium(_, button) { + (await game.packs.get(`daggerheart.${button.dataset.compendium}`))?.render(true); + } + static async selectPreview(_, button) { + const remove = button.dataset.selected; + const selectionData = Object.values(this.levelup.selectionData); + const option = remove + ? selectionData.find(x => x.type === 'subclass' && x.data.includes(button.dataset.uuid)) + : selectionData.find(x => x.type === 'subclass' && x.data.length === 0); + if (!option) { + return; // Notification? + } + + const path = `tiers.${option.tier}.levels.${option.level}.optionSelections.${option.optionKey}.${option.checkboxNr}.data`; + await this.levelup.updateSource({ [path]: remove ? [] : button.dataset.uuid }); + this.render(); + } + + static async save() { + await this.actor.levelUp(this.levelup.levelupData); this.close(); } } diff --git a/module/applications/sheets/adversary.mjs b/module/applications/sheets/adversary.mjs index 2087ea0a..b9180bf3 100644 --- a/module/applications/sheets/adversary.mjs +++ b/module/applications/sheets/adversary.mjs @@ -362,7 +362,7 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) { name: x.actor.name, img: x.actor.img, difficulty: x.actor.system.difficulty, - evasion: x.actor.system.evasion + evasion: x.actor.system.evasion.value })); const cls = getDocumentClass('ChatMessage'); diff --git a/module/applications/sheets/pc.mjs b/module/applications/sheets/pc.mjs index 2d16b470..31c98b6e 100644 --- a/module/applications/sheets/pc.mjs +++ b/module/applications/sheets/pc.mjs @@ -604,7 +604,7 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) { name: x.actor.name, img: x.actor.img, difficulty: x.actor.system.difficulty, - evasion: x.actor.system.evasion + evasion: x.actor.system.evasion.value })); const systemData = { @@ -645,6 +645,11 @@ export default class PCSheet extends DaggerheartSheet(ActorSheetV2) { } openLevelUp() { + if (!this.document.system.class || !this.document.system.subclass) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.Sheets.PC.Errors.missingClassOrSubclass')); + return; + } + new DhlevelUp(this.document).render(true); } diff --git a/module/data/levelup.mjs b/module/data/levelup.mjs index ff2f8295..b7d8f8df 100644 --- a/module/data/levelup.mjs +++ b/module/data/levelup.mjs @@ -12,35 +12,36 @@ export class DhLevelup extends foundry.abstract.DataModel { 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, - pcLevelData.selections.filter(x => x.tier === Number(key)), + 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); + const allInitialAchievements = Object.values(tiers).reduce((acc, tier) => { + const levelThreshold = Math.min(...tier.belongingLevels); - if (totalLevelProgression.includes(levelThreshold)) { - acc.proficiency += tier.initialAchievements.proficiency; - [...Array(tier.initialAchievements.experience.nr).keys()].forEach(_ => { - acc.newExperiences[foundry.utils.randomID()] = { - name: '', - modifier: tier.initialAchievements.experience.modifier - }; - }); - } + 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; - }, - { newExperiences: {}, proficiency: 0 } - ); + return acc; + }, {}); const domainCards = Object.keys(tiers).reduce((acc, tierKey) => { const tier = tiers[tierKey]; @@ -73,11 +74,9 @@ export class DhLevelup extends foundry.abstract.DataModel { return acc; }, 0), - allInitialAchievements: { - newExperiences: allInitialAchievements.newExperiences, - proficiency: allInitialAchievements.proficiency - }, - domainCards: domainCards + allInitialAchievements: allInitialAchievements, + domainCards: domainCards, + progressionLevels: totalLevelProgression }; } @@ -87,15 +86,17 @@ export class DhLevelup extends foundry.abstract.DataModel { return { tiers: new fields.TypedObjectField(new fields.EmbeddedDataField(DhLevelupTier)), maxSelections: new fields.NumberField({ required: true, integer: true }), - allInitialAchievements: new fields.SchemaField({ - newExperiences: new fields.TypedObjectField( - new fields.SchemaField({ - name: new fields.StringField({ required: true }), - modifier: new fields.NumberField({ required: true, integer: true }) - }) - ), - proficiency: new fields.NumberField({ required: true, integer: true }) - }), + allInitialAchievements: new fields.TypedObjectField( + new fields.SchemaField({ + newExperiences: new fields.TypedObjectField( + new fields.SchemaField({ + name: new fields.StringField({ required: true }), + modifier: new fields.NumberField({ required: true, 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 }), @@ -104,13 +105,44 @@ export class DhLevelup extends foundry.abstract.DataModel { domainCardSlot: new fields.NumberField({ required: true, integer: true }), path: new fields.StringField({ required: true }) }) - ) - // advancementSelections: new fields.SchemaField({ - // experiences: new fields.SetField(new fields.StringField()), - // }), + ), + progressionLevels: new fields.ArrayField(new fields.NumberField({ required: true, integer: true })) }; } + get canLevelUp() { + if (this.levelSelections.total !== this.maxSelections) return false; + + 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; + } + get levelSelections() { return Object.values(this.tiers).reduce( (acc, tier) => { @@ -148,13 +180,35 @@ export class DhLevelup extends foundry.abstract.DataModel { checkboxNr: Number(checkboxNr), value: optionSelect.value, amount: optionSelect.amount, - data: selectionObj.data + data: selectionObj.data, + secondaryData: selectionObj.secondaryData }; }); }); }); }); } + + get levelupData() { + const leveledSelections = this.selectionData.reduce((acc, data) => { + if (!acc[data.level]) acc[data.level] = [data]; + else acc[data.level].push(data); + + return acc; + }, {}); + return this.progressionLevels.reduce((acc, level) => { + acc[level] = { + achievements: { + experiences: this.allInitialAchievements[level].newExperiences, + proficiency: this.allInitialAchievements[level].proficiency + }, + domainCards: Object.values(this.domainCards).map(card => ({ ...card })), + selections: leveledSelections[level] + }; + + return acc; + }, {}); + } } class DhLevelupTier extends foundry.abstract.DataModel { @@ -295,6 +349,7 @@ class DhLevelupLevel extends foundry.abstract.DataModel { if (!acc[data.optionKey]) acc[data.optionKey] = {}; acc[data.optionKey][data.checkboxNr] = { minCost: optionSelections[data.optionKey].minCost, + minCost: optionSelections[data.optionKey].amount, locked: locked }; @@ -313,8 +368,10 @@ class DhLevelupLevel extends foundry.abstract.DataModel { new fields.SchemaField({ selected: new fields.BooleanField({ required: true, initial: true }), minCost: new fields.NumberField({ required: true, integer: true }), + amount: new fields.NumberField({ integer: true }), locked: new fields.BooleanField({ required: true, initial: false }), - data: new fields.StringField() + data: new fields.ArrayField(new fields.StringField()), + secondaryData: new fields.StringField() }) ) ) diff --git a/module/data/pc.mjs b/module/data/pc.mjs index 08763123..29c23777 100644 --- a/module/data/pc.mjs +++ b/module/data/pc.mjs @@ -50,7 +50,9 @@ export default class DhpPC extends foundry.abstract.TypeDataModel { knowledge: attributeField() }), proficiency: new fields.NumberField({ required: true, initial: 1, integer: true }), - evasion: new fields.NumberField({ initial: 0, integer: true }), + evasion: new fields.SchemaField({ + bonuses: new fields.NumberField({ initial: 0, integer: true }) + }), experiences: new fields.ArrayField( new fields.SchemaField({ id: new fields.StringField({ required: true }), @@ -338,7 +340,7 @@ export default class DhpPC extends foundry.abstract.TypeDataModel { // : attribute.data.actualValue; } - this.evasion = this.class?.system?.evasion ?? 0; + this.evasion.value = (this.class?.system?.evasion ?? 0) + this.evasion.bonuses; // this.armor.value = this.activeArmor?.baseScore ?? 0; const armor = this.armor; this.damageThresholds = { @@ -429,15 +431,37 @@ class DhPCLevelData extends foundry.abstract.DataModel { current: new fields.NumberField({ required: true, integer: true, initial: 1 }), changed: new fields.NumberField({ required: true, integer: true, initial: 1 }) }), - selections: new fields.ArrayField( + levelups: new fields.TypedObjectField( new fields.SchemaField({ - tier: new fields.NumberField({ required: true, integer: true }), - level: new fields.NumberField({ required: true, integer: true }), - optionKey: new fields.StringField({ required: true }), - type: new fields.StringField({ required: true, choices: LevelOptionType }), - checkboxNr: new fields.NumberField({ required: true, integer: true }), - value: new fields.NumberField({ integer: true }), - amount: new fields.NumberField({ integer: true }) + 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 }) + }) + ), + proficiency: new fields.NumberField({ integer: true }) + }, + { nullable: true, initial: null } + ), + domainCards: new fields.ArrayField( + new fields.SchemaField({ + uuid: new fields.StringField({ required: true }) + }) + ), + selections: new fields.ArrayField( + new fields.SchemaField({ + tier: new fields.NumberField({ required: true, integer: true }), + level: new fields.NumberField({ required: true, integer: true }), + optionKey: new fields.StringField({ required: true }), + type: new fields.StringField({ required: true, choices: LevelOptionType }), + checkboxNr: new fields.NumberField({ required: true, integer: true }), + value: new fields.NumberField({ integer: true }), + amount: new fields.NumberField({ integer: true }), + data: new fields.ArrayField(new fields.StringField({ required: true })) + }) + ) }) ) }; diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index ff193d93..012377c7 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -27,29 +27,178 @@ export default class DhpActor extends Actor { if (newLevel > this.system.levelData.level.current) { await this.update({ 'system.levelData.level.changed': newLevel }); } else { + const changes = this.getLevelChangedFeatures( + newLevel, + this.system.levelData.level.changed, + this.system.levelData.levelups + ); + + for (var domainCard of changes.domainCards) { + const uuid = domainCard.uuid ? domainCard.uuid : domainCard.data; + const itemCard = await this.items.find(x => x.uuid === uuid); + itemCard.delete(); + } + + var traitsUpdate = changes.traits.reduce((acc, trait) => { + acc[`${trait}.data.value`] = this.system.traits[trait].data.value - 1; + 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 newLevelData = { level: { current: newLevel, changed: newLevel }, - selections: Object.keys(this.system.levelData.selections).reduce((acc, key) => { - const level = this.system.levelData.selections[key]; - if (level.level <= newLevel) { - acc[key] = level; - } + 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.levelData': newLevelData }); + await this.update({ + system: { + 'traits': traitsUpdate, + 'experiences': experienceUpdate, + 'resources': { + health: { + max: this.system.resources.health.max - changes.hitPoint + }, + stress: { + max: this.system.resources.stress.max - changes.stress + } + }, + 'evasion.bonuses': this.system.evasion.bonuses - changes.evasion, + 'proficiency': this.system.proficiency - changes.proficiency, + 'levelData': newLevelData + } + }); } } + 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++) { + 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.evasion ? selections.evasion.reduce((acc, evasion) => acc + Number(evasion.value), 0) : 0); + changedFeatures.domainCards = [ + ...levelData[level].domainCards, + ...(selections.domainCard.flatMap(x => x.data.map(data => ({ ...x, data: data }))) ?? []) + ]; + changedFeatures.traits = selections.trait ? selections.trait.flatMap(x => x.data) : []; + changedFeatures.experiences = achievements?.experiences ? achievements.experiences : {}; + changedFeatures.experienceIncreases = selections.experience ?? []; + changedFeatures.subclasses = selections.subclasses ? [] : []; + changedFeatures.multiclass = selections.multiclass ? [] : []; + } + + return changedFeatures; + } + async levelUp(levelupData) { - await this.actor.update({ - 'system.levelData': { - 'level.current': this.system.levelData.level.changed, - 'selections': levelupData + 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 newCard = (await this.createEmbeddedDocuments('Item', [domainCard]))[0]; + if (fromAchievement) { + const levelupCard = levelupData[card.level].domainCards.find( + x => x.tier === card.tier && x.level === card.level + ); + if (levelupCard) levelupCard.uuid = 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.data.findSplice(x => x === card.data, newCard.uuid); + } + } + + var traitsUpdate = changes.traits.reduce((acc, trait) => { + acc[`${trait}.data.value`] = this.system.traits[trait].data.value + 1; + 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; + } + } + + await this.update({ + system: { + 'traits': traitsUpdate, + 'experiences': experienceUpdate, + 'resources': { + health: { + max: this.system.resources.health.max + changes.hitPoint + }, + stress: { + max: this.system.resources.stress.max + changes.stress + } + }, + 'evasion.bonuses': this.system.evasion.bonuses + changes.evasion, + 'proficiency': this.system.proficiency + changes.proficiency, + 'levelData': { + 'level.current': this.system.levelData.level.changed, + 'levelups': levelupData + } } }); } diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 7a0743f0..8eaa4325 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -1,4 +1,5 @@ import { getDiceSoNicePresets } from '../config/generalConfig.mjs'; +import Tagify from '@yaireo/tagify'; export const loadCompendiumOptions = async compendiums => { const compendiumValues = []; @@ -145,3 +146,62 @@ export const chunkify = (array, chunkSize, mappingFunc) => { return chunkifiedArray; }; + +export const tagifyElement = (element, options, onChange, tagifyOptions = {}) => { + const { maxTags } = tagifyOptions; + const tagifyElement = new Tagify(element, { + tagTextProp: 'name', + enforceWhitelist: true, + whitelist: Object.keys(options).map(key => { + const option = options[key]; + return { + value: key, + name: game.i18n.localize(option.label), + src: option.src + }; + }), + maxTags: maxTags, + // callbacks: { invalid: this.onAddTag }, + dropdown: { + mapValueTo: 'name', + searchKeys: ['name'], + enabled: 0, + maxItems: 20, + closeOnSelect: true, + highlightFirst: false + }, + templates: { + tag(tagData) { + return ` + +
+ ${tagData[this.settings.tagTextProp] || tagData.value} + ${tagData.src ? `` : ''} +
+
`; + } + } + }); + + const onSelect = async event => { + const inputElement = event.detail.tagify.DOM.originalInput; + const selectedOptions = event.detail?.value ? JSON.parse(event.detail.value) : []; + + const unusedDropDownItems = event.detail.tagify.suggestedListItems; + const missingOptions = Object.keys(options).filter(x => !unusedDropDownItems.find(item => item.value === x)); + const removedItem = missingOptions.find(x => !selectedOptions.find(item => item.value === x)); + const addedItem = removedItem + ? null + : selectedOptions.find(x => !missingOptions.find(item => item === x.value)); + + const changedItem = { option: removedItem ?? addedItem.value, removed: Boolean(removedItem) }; + + onChange(selectedOptions, changedItem, inputElement); + }; + tagifyElement.on('change', onSelect); +}; diff --git a/styles/daggerheart.css b/styles/daggerheart.css index e6ad610b..0540c0d5 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -2714,8 +2714,12 @@ div.daggerheart.views.multiclass { .item-button .item-icon.checked { opacity: 1; } -.theme-light { - /* Add specifics*/ +.theme-light .daggerheart.levelup .tiers-container .tier-container { + background-image: url('../assets/parchments/dh-parchment-light.png'); +} +.daggerheart.levelup .window-content { + max-height: 960px; + overflow: auto; } .daggerheart.levelup div[data-application-part='form'] { display: flex; @@ -2805,10 +2809,40 @@ div.daggerheart.views.multiclass { font-size: 12px; } .daggerheart.levelup .levelup-selections-container .levelup-card-selection { - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr; + display: flex; + flex-wrap: wrap; gap: 40px; - padding-right: 120px; +} +.daggerheart.levelup .levelup-selections-container .levelup-card-selection .card-preview-container { + width: calc(100% * (1 / 5)); +} +.daggerheart.levelup .levelup-selections-container .levelup-card-selection .levelup-domains-selection-container { + display: flex; + flex-direction: column; + gap: 8px; +} +.daggerheart.levelup .levelup-selections-container .levelup-card-selection .levelup-domains-selection-container .levelup-domain-selection-container { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; + position: relative; + cursor: pointer; +} +.daggerheart.levelup .levelup-selections-container .levelup-card-selection .levelup-domains-selection-container .levelup-domain-selection-container.disabled { + pointer-events: none; + opacity: 0.4; +} +.daggerheart.levelup .levelup-selections-container .levelup-card-selection .levelup-domains-selection-container .levelup-domain-selection-container div { + position: absolute; + text-align: center; + top: 4px; + background: grey; + padding: 0 12px; + border-radius: 6px; +} +.daggerheart.levelup .levelup-selections-container .levelup-card-selection .levelup-domains-selection-container .levelup-domain-selection-container img { + height: 124px; } .daggerheart.levelup .levelup-summary-container .level-achievements-container { display: flex; @@ -3250,13 +3284,18 @@ div.daggerheart.views.multiclass { .system-daggerheart.theme-light .tagify__dropdown .tagify__dropdown__item--active { color: #efe6d8; } -.theme-light .application .component.dh-style.card.card-preview-container { +.theme-light .application .component.dh-style.card-preview-container { background-image: url('../assets/parchments/dh-parchment-light.png'); } -.theme-light .application .component.dh-style.card.card-preview-container .preview-text-container { +.theme-light .application .component.dh-style.card-preview-container .preview-text-container { background-image: url(../assets/parchments/dh-parchment-dark.png); } +.theme-light .application .component.dh-style.card-preview-container .preview-selected-icon-container { + background-image: url(../assets/parchments/dh-parchment-dark.png); + color: var(--color-light-5); +} .application .component.dh-style.card-preview-container { + position: relative; border-radius: 6px; border: 2px solid var(--color-tabs-border); display: flex; @@ -3264,12 +3303,22 @@ div.daggerheart.views.multiclass { aspect-ratio: 0.75; background-image: url('../assets/parchments/dh-parchment-dark.png'); } -.application .component.dh-style.card-preview-container.empty { +.application .component.dh-style.card-preview-container.selectable { cursor: pointer; } +.application .component.dh-style.card-preview-container.disabled { + pointer-events: none; + opacity: 0.4; +} +.application .component.dh-style.card-preview-container .preview-image-outer-container { + position: relative; + display: flex; + align-items: center; + justify-content: center; +} .application .component.dh-style.card-preview-container .preview-image-container { flex: 1; - border-radius: 4px 4px 0; + border-radius: 4px 4px 0 0; } .application .component.dh-style.card-preview-container .preview-text-container { flex: 1; @@ -3283,13 +3332,14 @@ div.daggerheart.views.multiclass { background-image: url(../assets/parchments/dh-parchment-light.png); } .application .component.dh-style.card-preview-container .preview-empty-container { + pointer-events: none; + position: relative; display: flex; align-items: center; justify-content: center; flex: 1; } .application .component.dh-style.card-preview-container .preview-empty-container .preview-empty-inner-container { - position: relative; width: 100%; display: flex; justify-content: center; @@ -3299,11 +3349,28 @@ div.daggerheart.views.multiclass { } .application .component.dh-style.card-preview-container .preview-empty-container .preview-empty-inner-container .preview-empty-subtext { position: absolute; - bottom: -48px; + top: 10%; font-size: 18px; font-variant: small-caps; text-align: center; } +.application .component.dh-style.card-preview-container .preview-selected-icon-container { + position: absolute; + height: 54px; + width: 54px; + border-radius: 50%; + border: 2px solid; + font-size: 48px; + display: flex; + align-items: center; + justify-content: center; + background-image: url(../assets/parchments/dh-parchment-light.png); + color: var(--color-dark-5); +} +.application .component.dh-style.card-preview-container .preview-selected-icon-container i { + position: relative; + right: 2px; +} .sheet.daggerheart.dh-style .tab-navigation { margin: 5px 0; height: 40px; diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less index e82d60d3..c4a15cbb 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -275,13 +275,18 @@ .theme-light { .application { - .component.dh-style.card { + .component.dh-style { &.card-preview-container { background-image: url('../assets/parchments/dh-parchment-light.png'); .preview-text-container { background-image: url(../assets/parchments/dh-parchment-dark.png); } + + .preview-selected-icon-container { + background-image: url(../assets/parchments/dh-parchment-dark.png); + color: var(--color-light-5); + } } } } @@ -290,6 +295,7 @@ .application { .component.dh-style { &.card-preview-container { + position: relative; border-radius: 6px; border: 2px solid var(--color-tabs-border); display: flex; @@ -297,13 +303,25 @@ aspect-ratio: 0.75; background-image: url('../assets/parchments/dh-parchment-dark.png'); - &.empty { + &.selectable { cursor: pointer; } + &.disabled { + pointer-events: none; + opacity: 0.4; + } + + .preview-image-outer-container { + position: relative; + display: flex; + align-items: center; + justify-content: center; + } + .preview-image-container { flex: 1; - border-radius: 4px 4px 0; + border-radius: 4px 4px 0 0; } .preview-text-container { @@ -319,13 +337,14 @@ } .preview-empty-container { + pointer-events: none; + position: relative; display: flex; align-items: center; justify-content: center; flex: 1; .preview-empty-inner-container { - position: relative; width: 100%; display: flex; justify-content: center; @@ -336,13 +355,32 @@ .preview-empty-subtext { position: absolute; - bottom: -48px; + top: 10%; font-size: 18px; font-variant: small-caps; text-align: center; } } } + + .preview-selected-icon-container { + position: absolute; + height: 54px; + width: 54px; + border-radius: 50%; + border: 2px solid; + font-size: 48px; + display: flex; + align-items: center; + justify-content: center; + background-image: url(../assets/parchments/dh-parchment-light.png); + color: var(--color-dark-5); + + i { + position: relative; + right: 2px; + } + } } } } diff --git a/styles/levelup.less b/styles/levelup.less index 115b1d2b..80554d39 100644 --- a/styles/levelup.less +++ b/styles/levelup.less @@ -1,8 +1,19 @@ .theme-light { - /* Add specifics*/ + .daggerheart.levelup { + .tiers-container { + .tier-container { + background-image: url('../assets/parchments/dh-parchment-light.png'); + } + } + } } .daggerheart.levelup { + .window-content { + max-height: 960px; + overflow: auto; + } + div[data-application-part='form'] { display: flex; flex-direction: column; @@ -110,10 +121,46 @@ } .levelup-card-selection { - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr; + display: flex; + flex-wrap: wrap; gap: 40px; - padding-right: 120px; + + .card-preview-container { + width: calc(100% * (1 / 5)); + } + + .levelup-domains-selection-container { + display: flex; + flex-direction: column; + gap: 8px; + + .levelup-domain-selection-container { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; + position: relative; + cursor: pointer; + + &.disabled { + pointer-events: none; + opacity: 0.4; + } + + div { + position: absolute; + text-align: center; + top: 4px; + background: grey; + padding: 0 12px; + border-radius: 6px; + } + + img { + height: 124px; // Can it be dynamically sized? Won't follow any window resizing like this. + } + } + } } } diff --git a/templates/components/card-preview.hbs b/templates/components/card-preview.hbs index b62b6ab5..dc132b94 100644 --- a/templates/components/card-preview.hbs +++ b/templates/components/card-preview.hbs @@ -1,4 +1,4 @@ -
+
{{#if this.img}}
{{this.name}}
diff --git a/templates/sheets/parts/defense.hbs b/templates/sheets/parts/defense.hbs index cf5ef747..f56e5096 100644 --- a/templates/sheets/parts/defense.hbs +++ b/templates/sheets/parts/defense.hbs @@ -4,7 +4,7 @@
-
{{document.system.evasion}}
+
{{document.system.evasion.value}}
{{localize "DAGGERHEART.Sheets.PC.Defense.Evasion"}}
diff --git a/templates/views/levelup/advancementSelection.hbs b/templates/views/levelup/advancementSelection.hbs deleted file mode 100644 index 5d646693..00000000 --- a/templates/views/levelup/advancementSelection.hbs +++ /dev/null @@ -1,29 +0,0 @@ -{{#switch}} - {{#case 'trait'}} - - {{/case}} -{{/switch}} - - - -trait: { - id: 'trait', - label: 'Character Trait', - dataPath: '' -}, -experience: { - id: 'experience', - label: 'Experience' -}, -domainCard: { - id: 'domainCard', - label: 'Domain Card' -}, -subclass: { - id: 'subclass', - label: 'Subclass' -}, -multiclass: { - id: 'multiclass', - label: 'Multiclass' -} \ No newline at end of file diff --git a/templates/views/levelup/parts/selectable-card-preview.hbs b/templates/views/levelup/parts/selectable-card-preview.hbs new file mode 100644 index 00000000..039983e5 --- /dev/null +++ b/templates/views/levelup/parts/selectable-card-preview.hbs @@ -0,0 +1,11 @@ +
+
+ + {{#if this.selected}} +
+ +
+ {{/if}} +
+
{{this.name}}
+
\ No newline at end of file diff --git a/templates/views/levelup/tabs/advancements.hbs b/templates/views/levelup/tabs/advancements.hbs index 2b372db3..77c01afb 100644 --- a/templates/views/levelup/tabs/advancements.hbs +++ b/templates/views/levelup/tabs/advancements.hbs @@ -33,5 +33,15 @@ {{/each}}
+
+
+ {{localize "DAGGERHEART.Application.LevelUp.notifications.info.remainingAdvancementInfo" choices=this.levelup.levelSelections.totalAvailable}} + + {{#each this.levelup.tiers as |tier key|}} +
Tier {{tier.tier}}: {{tier.selections.totalAvailable}}
+ {{/each}} + +
+
\ No newline at end of file diff --git a/templates/views/levelup/tabs/selections.hbs b/templates/views/levelup/tabs/selections.hbs index 7b0a1ebd..804ae031 100644 --- a/templates/views/levelup/tabs/selections.hbs +++ b/templates/views/levelup/tabs/selections.hbs @@ -4,31 +4,81 @@ data-group='{{tabs.selections.group}}' >
-
-

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

-
- {{#each this.newExperiences as |experience key|}} -
-
- -
{{signedNumber experience.modifier}}
-
-
- {{#if experience.name}}{{/if}} + {{#if (gt this.newExperiences.length 0)}} +
+

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

+
+ {{#each this.newExperiences}} +
+
+ +
{{signedNumber this.modifier}}
+
+
+ {{#if this.name}}{{/if}} +
+ {{/each}} +
+
+ {{/if}} + + {{#if this.traits.active}} +
+

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

+ + +
+ {{/if}} + + {{#if this.experienceIncreases.active}} +
+

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

+ + +
+ {{/if}} + + {{#if (gt this.domainCards.length 0)}} +
+

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

+ +
+ {{#each this.domainCards}} + {{> "systems/daggerheart/templates/components/card-preview.hbs" img=this.img name=this.name path=this.path }} + {{/each}} +
+
+ {{/if}} + + {{#if (gt this.subclassCards.length 0)}} +
+

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

+ +
+ {{#each this.subclassCards}} + {{> "systems/daggerheart/templates/views/levelup/parts/selectable-card-preview.hbs" img=this.img name=this.name path=this.path selected=this.selected uuid=this.uuid disabled=this.disabled }} + {{/each}} +
+
+ {{/if}} + + {{#if this.multiclass}} +
+

{{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 }} +
+ {{#each this.multiclass.domains}} +
+
{{localize this.label}}
+ +
+ {{/each}}
- {{/each}} +
-
- -
-

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

- -
- {{#each this.domainCards}} - {{> "systems/daggerheart/templates/components/card-preview.hbs" img=this.img name=this.name path=this.path }} - {{/each}} -
-
+ {{/if}}
\ No newline at end of file diff --git a/templates/views/levelup/tabs/summary.hbs b/templates/views/levelup/tabs/summary.hbs index ddacb3f0..dbcf0486 100644 --- a/templates/views/levelup/tabs/summary.hbs +++ b/templates/views/levelup/tabs/summary.hbs @@ -46,15 +46,7 @@
\ No newline at end of file