Levelup applies bonuses to character

This commit is contained in:
WBHarry 2025-06-11 22:52:48 +02:00
parent 044d0a5b21
commit 3717e2ddd4
20 changed files with 457 additions and 174 deletions

View file

@ -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();
}

View file

@ -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 `<tag title="${tagData.title || tagData.value}"
contenteditable='false'
spellcheck='false'
tabIndex="${this.settings.a11y.focusableTags ? 0 : -1}"
class="${this.settings.classNames.tag} ${tagData.class ? tagData.class : ''}"
${this.getAttributes(tagData)}>
<x class="${this.settings.classNames.tagX}" role='button' aria-label='remove tag'></x>
<div>
<span class="${this.settings.classNames.tagText}">${tagData[this.settings.tagTextProp] || tagData.value}</span>
<img src="${tagData.src}"></i>
</div>
</tag>`;
}
}
});
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);
}

View file

@ -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'
};

View file

@ -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'
}

View file

@ -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 })
})
)

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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 })
})
)

View file

@ -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] = {