From 3717e2ddd46893afd540acd9e73f7ae55e057477 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Wed, 11 Jun 2025 22:52:48 +0200 Subject: [PATCH] Levelup applies bonuses to character --- lang/en.json | 9 + module/applications/levelup.mjs | 165 ++++++++++++----- module/applications/sheets/items/class.mjs | 50 +---- module/config/actorConfig.mjs | 6 + module/config/domainConfig.mjs | 18 +- module/data/actor/character.mjs | 70 ++++++- module/data/item/domainCard.mjs | 9 +- module/data/item/subclass.mjs | 23 ++- module/data/levelup.mjs | 24 +-- module/documents/actor.mjs | 173 +++++++++++++++--- styles/daggerheart.css | 1 + styles/less/global/elements.less | 1 + templates/sheets/parts/attributes.hbs | 10 +- templates/sheets/parts/defense.hbs | 2 +- templates/sheets/parts/experience.hbs | 2 +- templates/sheets/parts/health.hbs | 10 +- templates/sheets/parts/weapons.hbs | 2 +- .../levelup/parts/selectable-card-preview.hbs | 7 +- templates/views/levelup/tabs/selections.hbs | 16 +- templates/views/levelup/tabs/summary.hbs | 33 +++- 20 files changed, 457 insertions(+), 174 deletions(-) diff --git a/lang/en.json b/lang/en.json index 363a4e63..1339b500 100755 --- a/lang/en.json +++ b/lang/en.json @@ -351,30 +351,39 @@ }, "Domains": { "Arcana": { + "label": "Arcana", "Description": "This is the domain of the innate or instinctual use of magic. Those who walk this path tap into the raw, enigmatic forces of the realms to manipulate both the elements and their own energy. Arcana offers wielders a volatile power, but it is incredibly potent when correctly channeled." }, "Blade": { + "label": "Blade", "Description": "This is the domain of those who dedicate their lives to the mastery of weapons. Whether by blade, bow, or perhaps a more specialized arm, those who follow this path have the skill to cut short the lives of others. Blade requires study and dedication from its followers, in exchange for inexorable power over death." }, "Bone": { + "label": "Bone", "Description": "This is the domain of mastery of swiftness and tactical mastery. Practitioners of this domain have an uncanny control over their own physical abilities, and an eye for predicting the behaviors of others in combat. Bone grants its adherents unparalleled understanding of bodies and their movements in exchange for diligent training." }, "Codex": { + "label": "Codex", "Description": "This is the domain of intensive magical study. Those who seek magical knowledge turn to the recipes of power recorded in books, on scrolls, etched into walls, or tattooed on bodies. Codex offers a commanding and versatile understanding of magic to those devotees who are willing to seek beyond the common knowledge." }, "Grace": { + "label": "Grace", "Description": "This is the domain of charisma. Through rapturous storytelling, clever charm, or a shroud of lies, those who channel this power define the realities of their adversaries, bending perception to their will. Grace offers its wielders raw magnetism and mastery over language." }, "Midnight": { + "label": "Midnight", "Description": "This is the domain of shadows and secrecy. Whether by clever tricks, or cloak of night those who channel these forces are practiced in that art of obscurity and there is nothing hidden they cannot reach. Midnight offers practitioners the incredible power to control and create enigmas." }, "Sage": { + "label": "Sage", "Description": "This is the domain of the natural world. Those who walk this path tap into the unfettered power of the earth and its creatures to unleash raw magic. Sage grants its adherents the vitality of a blooming flower and ferocity of a hungry predator." }, "Splendor": { + "label": "Splendor", "Description": "This is the domain of life. Through this magic, followers gain the ability to heal, though such power also grants the wielder some control over death. Splendor offers its disciples the magnificent ability to both give and end life." }, "Valor": { + "label": "Valor", "Description": "This is the domain of protection. Whether through attack or defense, those who choose this discipline channel formidable strength to protect their allies in battle. Valor offers great power to those who raise their shield in defense of others." } }, diff --git a/module/applications/levelup.mjs b/module/applications/levelup.mjs index 61a85677..e0e215f4 100644 --- a/module/applications/levelup.mjs +++ b/module/applications/levelup.mjs @@ -1,4 +1,4 @@ -import { abilities } from '../config/actorConfig.mjs'; +import { abilities, subclassFeatureLabels } from '../config/actorConfig.mjs'; import { domains } from '../config/domainConfig.mjs'; import { DhLevelup } from '../data/levelup.mjs'; import { getDeleteKeys, tagifyElement } from '../helpers/utils.mjs'; @@ -35,6 +35,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) viewCompendium: this.viewCompendium, selectPreview: this.selectPreview, selectDomain: this.selectDomain, + selectSubclass: this.selectSubclass, updateCurrentLevel: this.updateCurrentLevel, activatePart: this.activatePart }, @@ -201,22 +202,38 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) } const subclassSelections = advancementChoices.subclass?.flatMap(x => x.data) ?? []; + const possibleSubclasses = [this.actor.system.class.subclass]; + if (this.actor.system.multiclass?.subclass) { + possibleSubclasses.push(this.actor.system.multiclass.subclass); + } - const multiclassSubclass = this.actor.system.multiclass?.system?.subclasses?.[0]; - const possibleSubclasses = [ - this.actor.system.class.subclass, - ...(multiclassSubclass ? [multiclassSubclass] : []) - ]; - const selectedSubclasses = possibleSubclasses.filter(x => subclassSelections.includes(x.uuid)); + // const featureStateIncrease = advancementChoices.subclass?.reduce((acc, subclass) => { + // if(subclass.secondaryData.featureState) acc += 1; + // return acc; + // }, 0) ?? 0; context.subclassCards = []; if (advancementChoices.subclass?.length > 0) { + const featureStateIncrease = Object.values(this.levelup.levels).reduce((acc, level) => { + acc += Object.values(level.choices).filter(choice => { + return Object.values(choice).every(checkbox => checkbox.type === 'subclass'); + }).length; + return acc; + }, 0); + for (var subclass of possibleSubclasses) { + const choice = + advancementChoices.subclass.find(x => x.data[0] === subclass.uuid) ?? + advancementChoices.subclass.find(x => x.data.length === 0); + const featureState = subclass.system.featureState + featureStateIncrease; const data = await foundry.utils.fromUuid(subclass.uuid); - const selected = selectedSubclasses.some(x => x.uuid === data.uuid); context.subclassCards.push({ ...data.toObject(), + path: choice?.path, uuid: data.uuid, - selected: selected + selected: subclassSelections.includes(subclass.uuid), + featureState: featureState, + featureLabel: game.i18n.localize(subclassFeatureLabels[featureState]), + isMulticlass: subclass.system.isMulticlass ? 'true' : 'false' }); } } @@ -237,10 +254,19 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) return { ...domain, - selected: key === data.secondaryData, - disabled: (data.secondaryData && key !== data.secondaryData) || alreadySelected + selected: key === data.secondaryData.domain, + disabled: + (data.secondaryData.domain && key !== data.secondaryData.domain) || + alreadySelected }; }) ?? [], + subclasses: + multiclass?.system?.subclasses.map(subclass => ({ + ...subclass, + uuid: subclass.uuid, + selected: data.secondaryData.subclass === subclass.uuid, + disabled: data.secondaryData.subclass && data.secondaryData.subclass !== subclass.uuid + })) ?? [], compendium: 'classes', limit: 1 }; @@ -277,8 +303,8 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) context.achievements = { proficiency: { - old: this.actor.system.proficiency, - new: this.actor.system.proficiency + achivementProficiency, + old: this.actor.system.proficiency.total, + new: this.actor.system.proficiency.total + achivementProficiency, shown: achivementProficiency > 0 }, damageThresholds: { @@ -322,6 +348,13 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) ? advancement[choiceKey] + Number(checkbox.value) : Number(checkbox.value); break; + case 'trait': + if (!advancement[choiceKey]) advancement[choiceKey] = {}; + for (var traitKey of checkbox.data) { + if (!advancement[choiceKey][traitKey]) advancement[choiceKey][traitKey] = 0; + advancement[choiceKey][traitKey] += 1; + } + break; case 'domainCard': if (!advancement[choiceKey]) advancement[choiceKey] = []; if (checkbox.data.length === 1) { @@ -339,6 +372,33 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) }); advancement[choiceKey].push({ data: data, value: checkbox.value }); break; + case 'subclass': + if (checkbox.data[0]) { + const subclassItem = await foundry.utils.fromUuid(checkbox.data[0]); + if (!advancement[choiceKey]) advancement[choiceKey] = []; + advancement[choiceKey].push({ + ...subclassItem.toObject(), + featureLabel: game.i18n.localize( + subclassFeatureLabels[Number(checkbox.secondaryData.featureState)] + ) + }); + } + break; + case 'multiclass': + const multiclassItem = await foundry.utils.fromUuid(checkbox.data[0]); + const subclass = multiclassItem + ? await foundry.utils.fromUuid(checkbox.secondaryData.subclass) + : null; + advancement[choiceKey] = multiclassItem + ? { + ...multiclassItem.toObject(), + domain: checkbox.secondaryData.domain + ? game.i18n.localize(domains[checkbox.secondaryData.domain].label) + : null, + subclass: subclass ? subclass.name : null + } + : {}; + break; } } } @@ -351,26 +411,35 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) new: context.achievements.proficiency.new + (advancement.proficiency ?? 0) }, hitPoints: { - old: this.actor.system.resources.hitPoints.max, - new: this.actor.system.resources.hitPoints.max + (advancement.hitPoint ?? 0) + old: this.actor.system.resources.hitPoints.maxTotal, + new: this.actor.system.resources.hitPoints.maxTotal + (advancement.hitPoint ?? 0) }, stress: { - old: this.actor.system.resources.stress.max, - new: this.actor.system.resources.stress.max + (advancement.stress ?? 0) + old: this.actor.system.resources.stress.maxTotal, + new: this.actor.system.resources.stress.maxTotal + (advancement.stress ?? 0) }, evasion: { - old: this.actor.system.evasion, - new: this.actor.system.evasion + (advancement.evasion ?? 0) + old: this.actor.system.evasion.total, + new: this.actor.system.evasion.total + (advancement.evasion ?? 0) } }, - traits: - advancement.trait?.flatMap(x => - x.data.map(data => game.i18n.localize(abilities[data].label)) - ) ?? [], + traits: Object.keys(this.actor.system.traits).reduce((acc, traitKey) => { + if (advancement.trait?.[traitKey]) { + if (!acc) acc = {}; + acc[traitKey] = { + label: game.i18n.localize(abilities[traitKey].label), + old: this.actor.system.traits[traitKey].total, + new: this.actor.system.traits[traitKey].total + advancement.trait[traitKey] + }; + } + return acc; + }, null), domainCards: advancement.domainCard ?? [], experiences: advancement.experience?.flatMap(x => x.data.map(data => ({ name: data, modifier: x.value }))) ?? - [] + [], + multiclass: advancement.multiclass, + subclass: advancement.subclass }; context.advancements.statistics.proficiency.shown = @@ -486,7 +555,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) const target = event.target.closest('.card-preview-container'); if (item.type === 'domainCard') { if ( - !this.actor.system.class.value.system.domains.includes(item.system.domain) && + !this.actor.system.domains.includes(item.system.domain) && this.levelup.classUpgradeChoices?.multiclass?.domain !== item.system.domain ) { ui.notifications.error( @@ -547,8 +616,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) amount: target.dataset.amount ? Number(target.dataset.amount) : null, value: target.dataset.value, type: target.dataset.type, - data: item.uuid, - secondaryData: null + data: item.uuid } }); this.render(); @@ -562,16 +630,16 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) const update = {}; if (!button.checked) { - if (button.dataset.cost > 1) { + const basePath = `levels.${this.levelup.currentLevel}.choices`; + const current = foundry.utils.getProperty(this.levelup, `${basePath}.${button.dataset.option}`); + if (Number(button.dataset.cost) > 1 || Object.keys(current).length === 1) { // Simple handling that doesn't cover potential Custom LevelTiers. - update[`levels.${this.levelup.currentLevel}.choices.-=${button.dataset.option}`] = null; + update[`${basePath}.-=${button.dataset.option}`] = null; } else { - update[ - `levels.${this.levelup.currentLevel}.choices.${button.dataset.option}.-=${button.dataset.checkboxNr}` - ] = null; + update[`${basePath}.${button.dataset.option}.-=${button.dataset.checkboxNr}`] = null; } } else { - if (!this.levelup.levels[this.levelup.currentLevel].nrSelections.available) { + if (this.levelup.levels[this.levelup.currentLevel].nrSelections.available < Number(button.dataset.cost)) { ui.notifications.info( game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.info.insufficentAdvancements') ); @@ -600,24 +668,35 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) 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; + await this.levelup.updateSource({ + [`${button.dataset.path}`]: { + data: remove ? [] : [button.dataset.uuid], + secondaryData: { + featureState: button.dataset.featureState, + isMulticlass: button.dataset.isMulticlass + } + } + }); - 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 selectDomain(_, button) { const option = foundry.utils.getProperty(this.levelup, button.dataset.path); - const domain = option.secondaryData ? null : button.dataset.domain; + const domain = option.secondaryData.domain ? null : button.dataset.domain; await this.levelup.updateSource({ - multiclass: { domain }, - [`${button.dataset.path}.secondaryData`]: domain + [`${button.dataset.path}.secondaryData.domain`]: domain + }); + this.render(); + } + + static async selectSubclass(_, button) { + const option = foundry.utils.getProperty(this.levelup, button.dataset.path); + const subclass = option.secondaryData.subclass ? null : button.dataset.subclass; + + await this.levelup.updateSource({ + [`${button.dataset.path}.secondaryData.subclass`]: subclass }); this.render(); } diff --git a/module/applications/sheets/items/class.mjs b/module/applications/sheets/items/class.mjs index b0d8bd1d..c8f5c1e1 100644 --- a/module/applications/sheets/items/class.mjs +++ b/module/applications/sheets/items/class.mjs @@ -1,5 +1,5 @@ +import { tagifyElement } from '../../../helpers/utils.mjs'; import DaggerheartSheet from '../daggerheart-sheet.mjs'; -import Tagify from '@yaireo/tagify'; const { ItemSheetV2 } = foundry.applications.sheets; const { TextEditor } = foundry.applications.ux; @@ -72,55 +72,14 @@ export default class ClassSheet extends DaggerheartSheet(ItemSheetV2) { super._attachPartListeners(partId, htmlElement, options); const domainInput = htmlElement.querySelector('.domain-input'); - const domainTagify = new Tagify(domainInput, { - tagTextProp: 'name', - enforceWhitelist: true, - whitelist: Object.keys(SYSTEM.DOMAIN.domains).map(key => { - const domain = SYSTEM.DOMAIN.domains[key]; - return { - value: key, - name: game.i18n.localize(domain.label), - src: domain.src, - background: domain.background - }; - }), - maxTags: 2, - callbacks: { invalid: this.onAddTag }, - dropdown: { - mapValueTo: 'name', - searchKeys: ['name'], - enabled: 0, - maxItems: 20, - closeOnSelect: true, - highlightFirst: false - }, - templates: { - tag(tagData) { - //z-index: unset; background-image: ${tagData.background}; Maybe a domain specific background for the chips? - return ` - -
- ${tagData[this.settings.tagTextProp] || tagData.value} - -
-
`; - } - } - }); - - domainTagify.on('change', this.onDomainSelect.bind(this)); + tagifyElement(domainInput, SYSTEM.DOMAIN.domains, this.onDomainSelect.bind(this)); } async _prepareContext(_options) { const context = await super._prepareContext(_options); context.document = this.document; context.tabs = super._getTabs(this.constructor.TABS); - context.domains = this.document.system.domains.map(x => SYSTEM.DOMAIN.domains[x].label); + context.domains = this.document.system.domains; return context; } @@ -136,8 +95,7 @@ export default class ClassSheet extends DaggerheartSheet(ItemSheetV2) { } } - async onDomainSelect(event) { - const domains = event.detail?.value ? JSON.parse(event.detail.value) : []; + async onDomainSelect(domains) { await this.document.update({ 'system.domains': domains.map(x => x.value) }); this.render(true); } diff --git a/module/config/actorConfig.mjs b/module/config/actorConfig.mjs index 3698d19e..6af480d7 100644 --- a/module/config/actorConfig.mjs +++ b/module/config/actorConfig.mjs @@ -409,3 +409,9 @@ export const levelupData = { } } }; + +export const subclassFeatureLabels = { + 1: 'DAGGERHEART.Sheets.PC.DomainCard.FoundationTitle', + 2: 'DAGGERHEART.Sheets.PC.DomainCard.SpecializationTitle', + 3: 'DAGGERHEART.Sheets.PC.DomainCard.MasteryTitle' +}; diff --git a/module/config/domainConfig.mjs b/module/config/domainConfig.mjs index 394f6fb3..a32091c2 100644 --- a/module/config/domainConfig.mjs +++ b/module/config/domainConfig.mjs @@ -1,56 +1,56 @@ export const domains = { arcana: { id: 'arcana', - label: 'Arcana', + label: 'DAGGERHEART.Domains.Arcana.label', src: 'icons/magic/symbols/circled-gem-pink.webp', description: 'DAGGERHEART.Domains.Arcana' }, blade: { id: 'blade', - label: 'Blade', + label: 'DAGGERHEART.Domains.Blade.label', src: 'icons/weapons/swords/sword-broad-crystal-paired.webp', description: 'DAGGERHEART.Domains.Blade' }, bone: { id: 'bone', - label: 'Bone', + label: 'DAGGERHEART.Domains.Bone.label', src: 'icons/skills/wounds/bone-broken-marrow-red.webp', description: 'DAGGERHEART.Domains.Bone' }, codex: { id: 'codex', - label: 'Codex', + label: 'DAGGERHEART.Domains.Codex.label', src: 'icons/sundries/books/book-embossed-jewel-gold-purple.webp', description: 'DAGGERHEART.Domains.Codex' }, grace: { id: 'grace', - label: 'Grace', + label: 'DAGGERHEART.Domains.Grace.label', src: 'icons/skills/movement/feet-winged-boots-glowing-yellow.webp', description: 'DAGGERHEART.Domains.Grace' }, midnight: { id: 'midnight', - label: 'Midnight', + label: 'DAGGERHEART.Domains.Midnight.label', src: 'icons/environment/settlement/watchtower-castle-night.webp', background: 'systems/daggerheart/assets/backgrounds/MidnightBackground.webp', description: 'DAGGERHEART.Domains.Midnight' }, sage: { id: 'sage', - label: 'Sage', + label: 'DAGGERHEART.Domains.Sage.label', src: 'icons/sundries/misc/pipe-wooden-straight-brown.webp', description: 'DAGGERHEART.Domains.Sage' }, splendor: { id: 'splendor', - label: 'Splendor', + label: 'DAGGERHEART.Domains.Splendor.label', src: 'icons/magic/control/control-influence-crown-gold.webp', description: 'DAGGERHEART.Domains.Splendor' }, valor: { id: 'valor', - label: 'Valor', + label: 'DAGGERHEART.Domains.Valor.label', src: 'icons/magic/control/control-influence-rally-purple.webp', description: 'DAGGERHEART.Domains.Valor' } diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index def502b5..54a7a3e1 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -6,12 +6,14 @@ import BaseDataActor from './base.mjs'; const attributeField = () => new foundry.data.fields.SchemaField({ value: new foundry.data.fields.NumberField({ initial: 0, integer: true }), + bonus: new foundry.data.fields.NumberField({ initial: 0, integer: true }), tierMarked: new foundry.data.fields.BooleanField({ initial: false }) }); const resourceField = max => new foundry.data.fields.SchemaField({ value: new foundry.data.fields.NumberField({ initial: 0, integer: true }), + bonus: new foundry.data.fields.NumberField({ initial: 0, integer: true }), max: new foundry.data.fields.NumberField({ initial: max, integer: true }) }); @@ -40,12 +42,18 @@ export default class DhCharacter extends BaseDataActor { presence: attributeField(), knowledge: attributeField() }), - proficiency: new fields.NumberField({ initial: 1, integer: true }), - evasion: new fields.NumberField({ initial: 0, integer: true }), + proficiency: new fields.SchemaField({ + value: new fields.NumberField({ initial: 1, integer: true }), + bonus: new fields.NumberField({ initial: 0, integer: true }) + }), + evasion: new fields.SchemaField({ + bonus: new fields.NumberField({ initial: 0, integer: true }) + }), experiences: new fields.TypedObjectField( new fields.SchemaField({ description: new fields.StringField({}), - value: new fields.NumberField({ integer: true, nullable: true, initial: null }) + value: new fields.NumberField({ integer: true, initial: 0 }), + bonus: new fields.NumberField({ integer: true, initial: 0 }) }), { initial: { @@ -89,8 +97,8 @@ export default class DhCharacter extends BaseDataActor { } get domains() { - const classDomains = this.class ? this.class.system.domains : []; - const multiclassDomains = this.multiclass ? this.multiclass.system.domains : []; + const classDomains = this.class.value ? this.class.value.system.domains : []; + const multiclassDomains = this.multiclass.value ? this.multiclass.value.system.domains : []; return [...classDomains, ...multiclassDomains]; } @@ -163,9 +171,38 @@ export default class DhCharacter extends BaseDataActor { } prepareBaseData() { - for (var attributeKey in this.traits) { - const attribute = this.traits[attributeKey]; - /* Levleup handling */ + for (let levelKey in this.levelData.levelups) { + const level = this.levelData.levelups[levelKey]; + + this.proficiency.bonus += level.achievements.proficiency; + + for (let selection of level.selections) { + switch (selection.type) { + case 'trait': + selection.data.forEach(data => { + this.traits[data].bonus += 1; + }); + break; + case 'hitPoint': + this.resources.hitPoints.bonus += selection.value; + break; + case 'stress': + this.resources.stress.bonus += selection.value; + break; + case 'evasion': + this.evasion.bonus += selection.value; + break; + case 'proficiency': + this.proficiency.bonus = selection.value; + break; + case 'experience': + Object.keys(this.experiences).forEach(key => { + const experience = this.experiences[key]; + experience.bonus += selection.value; + }); + break; + } + } } const armor = this.armor; @@ -182,6 +219,21 @@ export default class DhCharacter extends BaseDataActor { prepareDerivedData() { this.resources.hope.max -= Object.keys(this.scars).length; this.resources.hope.value = Math.min(this.resources.hope.value, this.resources.hope.max); + + for (var traitKey in this.traits) { + var trait = this.traits[traitKey]; + trait.total = trait.value + trait.bonus; + } + + for (var experienceKey in this.experiences) { + var experience = this.experiences[experienceKey]; + experience.total = experience.value + experience.bonus; + } + + this.resources.hitPoints.maxTotal = this.resources.hitPoints.max + this.resources.hitPoints.bonus; + this.resources.stress.maxTotal = this.resources.stress.max + this.resources.stress.bonus; + this.evasion.total = (this.class?.evasion ?? 0) + this.evasion.bonus; + this.proficiency.total = this.proficiency.value + this.proficiency.bonus; } } @@ -225,7 +277,7 @@ class DhPCLevelData extends foundry.abstract.DataModel { minCost: new fields.NumberField({ integer: true }), amount: new fields.NumberField({ integer: true }), data: new fields.ArrayField(new fields.StringField({ required: true })), - secondaryData: new fields.StringField(), + secondaryData: new fields.TypedObjectField(new fields.StringField({ required: true })), itemUuid: new fields.StringField({ required: true }) }) ) diff --git a/module/data/item/domainCard.mjs b/module/data/item/domainCard.mjs index b5880aad..e11fc5a7 100644 --- a/module/data/item/domainCard.mjs +++ b/module/data/item/domainCard.mjs @@ -36,17 +36,12 @@ export default class DHDomainCard extends BaseDataItem { return false; } - if (!this.actor.system.domains.find(x => x === item.system.domain)) { + if (!this.actor.system.domains.find(x => x === this.domain)) { ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.LacksDomain')); return false; } - if (this.actor.system.domainCards.total.length === 5) { - ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.MaxLoadoutReached')); - return false; - } - - if (this.actor.system.domainCards.total.find(x => x.name === item.name)) { + if (this.actor.system.domainCards.total.find(x => x.name === this.parent.name)) { ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.DuplicateDomainCard')); return false; } diff --git a/module/data/item/subclass.mjs b/module/data/item/subclass.mjs index ea506efa..bb315fcd 100644 --- a/module/data/item/subclass.mjs +++ b/module/data/item/subclass.mjs @@ -25,24 +25,37 @@ export default class DHSubclass extends BaseDataItem { foundationFeature: new ForeignDocumentUUIDField({ type: 'Item' }), specializationFeature: new ForeignDocumentUUIDField({ type: 'Item' }), masteryFeature: new ForeignDocumentUUIDField({ type: 'Item' }), + featureState: new fields.NumberField({ required: true, initial: 1, min: 1 }), isMulticlass: new fields.BooleanField({ initial: false }) }; } + get features() { + return { + foundation: this.foundationFeature, + specialization: this.featureState >= 2 ? this.specializationFeature : null, + mastery: this.featureState === 3 ? this.masteryFeature : null + }; + } + async _preCreate(data, options, user) { const allowed = await super._preCreate(data, options, user); if (allowed === false) return; if (this.actor?.type === 'character') { - const path = data.system.isMulticlass ? 'system.multiclass' : 'system.class'; - const classData = foundry.utils.getProperty(this.actor, path); - if (!classData.value) { + const classData = this.actor.items.find( + x => x.type === 'class' && x.system.isMulticlass === data.system.isMulticlass + ); + const subclassData = this.actor.items.find( + x => x.type === 'subclass' && x.system.isMulticlass === data.system.isMulticlass + ); + if (!classData) { ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.MissingClass')); return false; - } else if (classData.subclass) { + } else if (subclassData) { ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.SubclassAlreadySelected')); return false; - } else if (classData.value.system.subclasses.every(x => x.uuid !== `Item.${data._id}`)) { + } else if (classData.system.subclasses.every(x => x.uuid !== `Item.${data._id}`)) { ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.SubclassNotInClass')); return false; } diff --git a/module/data/levelup.mjs b/module/data/levelup.mjs index 0f9204e0..29956401 100644 --- a/module/data/levelup.mjs +++ b/module/data/levelup.mjs @@ -97,11 +97,12 @@ export class DhLevelup extends foundry.abstract.DataModel { case 'experience': case 'domainCard': case 'subclass': - return checkbox.amount ? checkbox.data.length === checkbox.amount : checkbox.data.length === 1; + return checkbox.data.length === (checkbox.amount ?? 1); case 'multiclass': const classSelected = checkbox.data.length === 1; - const domainSelected = checkbox.secondaryData; - return classSelected && domainSelected; + const domainSelected = checkbox.secondaryData.domain; + const subclassSelected = checkbox.secondaryData.subclass; + return classSelected && domainSelected && subclassSelected; default: return true; } @@ -129,7 +130,7 @@ export class DhLevelup extends foundry.abstract.DataModel { } get classUpgradeChoices() { - let subclass = null; + let subclasses = []; let multiclass = null; Object.keys(this.levels).forEach(levelKey => { const level = this.levels[levelKey]; @@ -138,21 +139,22 @@ export class DhLevelup extends foundry.abstract.DataModel { if (checkbox.type === 'multiclass') { multiclass = { class: checkbox.data.length > 0 ? checkbox.data[0] : null, - domain: checkbox.secondaryData ?? null, + domain: checkbox.secondaryData.domain ?? null, + subclass: checkbox.secondaryData.subclass ?? null, tier: checkbox.tier, level: levelKey }; } if (checkbox.type === 'subclass') { - subclass = { + subclasses.push({ tier: checkbox.tier, level: levelKey - }; + }); } }); }); }); - return { subclass, multiclass }; + return { subclasses, multiclass }; } get tiersForRendering() { @@ -179,11 +181,11 @@ export class DhLevelup extends foundry.abstract.DataModel { }, {}) ); - const { multiclass, subclass } = this.classUpgradeChoices; + const { multiclass, subclasses } = this.classUpgradeChoices; return tierKeys.map(tierKey => { const tier = this.tiers[tierKey]; const multiclassInTier = multiclass?.tier === Number(tierKey); - const subclassInTier = subclass?.tier === Number(tierKey); + const subclassInTier = subclasses.some(x => x.tier === Number(tierKey)); return { name: tier.name, @@ -287,7 +289,7 @@ export class DhLevelupLevel extends foundry.abstract.DataModel { amount: new fields.NumberField({ integer: true }), value: new fields.StringField(), data: new fields.ArrayField(new fields.StringField()), - secondaryData: new fields.StringField(), + secondaryData: new fields.TypedObjectField(new fields.StringField()), type: new fields.StringField({ required: true }) }) ) diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 55b00634..14639224 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -39,17 +39,65 @@ export default class DhpActor extends Actor { return acc; }, {}); - const domainCards = Object.keys(this.system.levelData.levelups) + const domainCards = []; + const experiences = []; + const subclassFeatureState = { class: null, multiclass: null }; + let multiclass = null; + Object.keys(this.system.levelData.levelups) .filter(x => x > newLevel) - .flatMap(levelKey => { + .forEach(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]; + domainCards.push(...achievementCards, ...advancementCards); + experiences.push(...Object.keys(level.achievements.experiences)); + + const subclass = level.selections.find(x => x.type === 'subclass'); + if (subclass) { + const path = subclass.secondaryData.isMulticlass === 'true' ? 'multiclass' : 'class'; + const subclassState = Number(subclass.secondaryData.featureState) - 1; + subclassFeatureState[path] = subclassFeatureState[path] + ? Math.min(subclassState, subclassFeatureState[path]) + : subclassState; + } + + multiclass = level.selections.find(x => x.type === 'multiclass'); }); - for (var domainCard of domainCards) { - const itemCard = await this.items.find(x => x.uuid === domainCard); + if (experiences.length > 0) { + this.update({ + 'system.experiences': experiences.reduce((acc, key) => { + acc[`-=${key}`] = null; + return acc; + }, {}) + }); + } + + if (subclassFeatureState.class) { + this.system.class.subclass.update({ 'system.featureState': subclassFeatureState.class }); + } + + if (subclassFeatureState.multiclass) { + this.system.multiclass.subclass.update({ 'system.featureState': subclassFeatureState.multiclass }); + } + + if (multiclass) { + const multiclassSubclass = this.items.find(x => x.type === 'subclass' && x.system.isMulticlass); + const multiclassItem = this.items.find(x => x.uuid === multiclass.itemUuid); + + multiclassSubclass.delete(); + multiclassItem.delete(); + + this.update({ + 'system.multiclass': { + value: null, + subclass: null + } + }); + } + + for (let domainCard of domainCards) { + const itemCard = this.items.find(x => x.uuid === domainCard); itemCard.delete(); } @@ -71,6 +119,94 @@ export default class DhpActor extends Actor { const levelups = {}; for (var levelKey of Object.keys(levelupData)) { const level = levelupData[levelKey]; + + for (var experienceKey in level.achievements.experiences) { + const experience = level.achievements.experiences[experienceKey]; + await this.update({ + [`system.experiences.${experienceKey}`]: { + description: experience.name, + value: experience.modifier + } + }); + } + + let multiclass = null; + const domainCards = []; + const subclassFeatureState = { class: null, multiclass: null }; + 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]; + + if (checkbox.type === 'multiclass') { + multiclass = { + ...checkbox, + level: Number(levelKey), + optionKey: optionKey, + checkboxNr: Number(checkboxNr) + }; + } else if (checkbox.type === 'domainCard') { + domainCards.push({ + ...checkbox, + level: Number(levelKey), + optionKey: optionKey, + checkboxNr: Number(checkboxNr) + }); + } else { + if (checkbox.type === 'subclass') { + const path = checkbox.secondaryData.isMulticlass === 'true' ? 'multiclass' : 'class'; + subclassFeatureState[path] = Math.max( + Number(checkbox.secondaryData.featureState), + subclassFeatureState[path] + ); + } + + selections.push({ + ...checkbox, + level: Number(levelKey), + optionKey: optionKey, + checkboxNr: Number(checkboxNr) + }); + } + } + } + + if (multiclass) { + const subclassItem = await foundry.utils.fromUuid(multiclass.secondaryData.subclass); + const subclassData = subclassItem.toObject(); + const multiclassItem = await foundry.utils.fromUuid(multiclass.data[0]); + const multiclassData = multiclassItem.toObject(); + + const embeddedItem = await this.createEmbeddedDocuments('Item', [ + { + ...multiclassData, + system: { + ...multiclassData.system, + domains: [multiclass.secondaryData.domain], + isMulticlass: true + } + } + ]); + + await this.createEmbeddedDocuments('Item', [ + { + ...subclassData, + system: { + ...subclassData.system, + isMulticlass: true + } + } + ]); + selections.push({ ...multiclass, itemUuid: embeddedItem[0].uuid }); + } + + for (var domainCard of domainCards) { + const item = await foundry.utils.fromUuid(domainCard.data[0]); + const embeddedItem = await this.createEmbeddedDocuments('Item', [item.toObject()]); + selections.push({ ...domainCard, itemUuid: embeddedItem[0].uuid }); + } + const achievementDomainCards = []; for (var card of Object.values(level.achievements.domainCards)) { const item = await foundry.utils.fromUuid(card.uuid); @@ -79,27 +215,14 @@ export default class DhpActor extends Actor { achievementDomainCards.push(card); } - 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 (subclassFeatureState.class) { + await this.system.class.subclass.update({ 'system.featureState': subclassFeatureState.class }); + } - 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 - }); - } + if (subclassFeatureState.multiclass) { + await this.system.multiclass.subclass.update({ + 'system.featureState': subclassFeatureState.multiclass + }); } levelups[levelKey] = { diff --git a/styles/daggerheart.css b/styles/daggerheart.css index 46415c6e..8262d33c 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -3340,6 +3340,7 @@ div.daggerheart.views.multiclass { flex: 1; border-radius: 0 0 4px 4px; display: flex; + flex-direction: column; align-items: center; justify-content: center; font-size: 18px; diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less index 797b2328..ae1f15e1 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -344,6 +344,7 @@ flex: 1; border-radius: 0 0 4px 4px; display: flex; + flex-direction: column; align-items: center; justify-content: center; font-size: 18px; diff --git a/templates/sheets/parts/attributes.hbs b/templates/sheets/parts/attributes.hbs index 1ebec896..08e414cd 100644 --- a/templates/sheets/parts/attributes.hbs +++ b/templates/sheets/parts/attributes.hbs @@ -7,19 +7,19 @@ {{#each this.attributes as |attribute key|}}
- +
{{key}}
{{#if ../editAttributes}} - + {{#if (not (eq attribute.total 0))}}{{/if}} {{#each ../abilityScoreArray as |option|}} - + {{/each}} {{else}} -
{{attribute.value}}
+
{{attribute.total}}
{{/if}}
diff --git a/templates/sheets/parts/defense.hbs b/templates/sheets/parts/defense.hbs index 2041e9a6..7553373c 100644 --- a/templates/sheets/parts/defense.hbs +++ b/templates/sheets/parts/defense.hbs @@ -4,7 +4,7 @@
-
{{document.system.evasion}}
+
{{document.system.evasion.total}}
{{localize "DAGGERHEART.Sheets.PC.Defense.Evasion"}}
diff --git a/templates/sheets/parts/experience.hbs b/templates/sheets/parts/experience.hbs index ace6970e..5bc7e1c2 100644 --- a/templates/sheets/parts/experience.hbs +++ b/templates/sheets/parts/experience.hbs @@ -3,7 +3,7 @@ {{#each document.system.experiences as |experience id|}}
-
{{experience.value}}
+
{{experience.total}}
{{/each}} {{#times (subtract 5 (length document.system.experiences))}} diff --git a/templates/sheets/parts/health.hbs b/templates/sheets/parts/health.hbs index 4b5a9fac..18419461 100644 --- a/templates/sheets/parts/health.hbs +++ b/templates/sheets/parts/health.hbs @@ -21,7 +21,7 @@
{{localize "DAGGERHEART.Sheets.PC.Health.Severe"}}
- +
@@ -30,22 +30,22 @@
- {{#times document.system.resources.hitPoints.max}} + {{#times document.system.resources.hitPoints.maxTotal}} {{#with (add this 1)}} {{/with}} {{/times}} - {{#times (subtract 12 document.system.resources.hitPoints.max)}} + {{#times (subtract 12 document.system.resources.hitPoints.maxTotal)}} {{/times}}
- {{#times document.system.resources.stress.max}} + {{#times document.system.resources.stress.maxTotal}} {{#with (add this 1)}} {{/with}} {{/times}} - {{#times (subtract 12 document.system.resources.stress.max)}} + {{#times (subtract 12 document.system.resources.stress.maxTotal)}} {{/times}}
diff --git a/templates/sheets/parts/weapons.hbs b/templates/sheets/parts/weapons.hbs index a53fffcb..a020d55f 100644 --- a/templates/sheets/parts/weapons.hbs +++ b/templates/sheets/parts/weapons.hbs @@ -5,7 +5,7 @@
{{localize "DAGGERHEART.Sheets.PC.Weapons.ProficiencyTitle"}} {{#times 6}} - + {{/times}}
diff --git a/templates/views/levelup/parts/selectable-card-preview.hbs b/templates/views/levelup/parts/selectable-card-preview.hbs index 039983e5..6be03b37 100644 --- a/templates/views/levelup/parts/selectable-card-preview.hbs +++ b/templates/views/levelup/parts/selectable-card-preview.hbs @@ -1,4 +1,4 @@ -
+
{{#if this.selected}} @@ -7,5 +7,8 @@
{{/if}}
-
{{this.name}}
+
+
{{this.name}}
+ {{this.featureLabel}} +
\ No newline at end of file diff --git a/templates/views/levelup/tabs/selections.hbs b/templates/views/levelup/tabs/selections.hbs index 1f8b2b21..49cd8860 100644 --- a/templates/views/levelup/tabs/selections.hbs +++ b/templates/views/levelup/tabs/selections.hbs @@ -63,7 +63,7 @@
{{#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 }} + {{> "systems/daggerheart/templates/views/levelup/parts/selectable-card-preview.hbs" img=this.img header=this.featureLabel name=this.name path=this.path selected=this.selected uuid=this.uuid isMulticlass=this.isMulticlass featureState=this.featureState disabled=this.disabled }} {{/each}}
@@ -88,6 +88,20 @@
{{/each}}
+ +
+ {{#each this.multiclass.subclasses}} +
+
{{localize this.name}}
+ + {{#if this.selected}} +
+ +
+ {{/if}} +
+ {{/each}} +
{{/if}} diff --git a/templates/views/levelup/tabs/summary.hbs b/templates/views/levelup/tabs/summary.hbs index 77925cff..c1c1d13f 100644 --- a/templates/views/levelup/tabs/summary.hbs +++ b/templates/views/levelup/tabs/summary.hbs @@ -95,11 +95,14 @@ {{#if this.advancements.traits}}
{{localize "DAGGERHEART.Application.LevelUp.summary.traits"}}
-
+ {{#each this.advancements.traits}} -
{{this}}
+
+ {{this.label}}: {{this.old}} + + {{this.new}} +
{{/each}} -
{{/if}} @@ -124,6 +127,30 @@ {{/if}} + + {{#if this.advancements.subclass}} +
+
{{localize "DAGGERHEART.Application.LevelUp.summary.subclass"}}
+
+ {{#each this.advancements.subclass}} +
{{this.name}} - {{this.featureLabel}}
+ {{/each}} +
+
+ {{/if}} + + {{#if this.advancements.multiclass}} + {{#with this.advancements.multiclass}} +
+
{{localize "DAGGERHEART.Application.LevelUp.summary.multiclass"}}
+
+
{{this.name}}
+
{{this.domain}}
+
{{this.subclass}}
+
+
+ {{/with}} + {{/if}}