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

@ -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."
}
},

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

View file

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

View file

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

View file

@ -7,19 +7,19 @@
{{#each this.attributes as |attribute key|}}
<div class="attribute">
<div class="attribute-banner">
<img class="attribute-roll" data-action="attributeRoll" data-attribute="{{key}}" data-value="{{attribute.value}}" src="icons/svg/d12-grey.svg" />
<img class="attribute-roll" data-action="attributeRoll" data-attribute="{{key}}" data-value="{{attribute.total}}" src="icons/svg/d12-grey.svg" />
<div class="attribute-text">{{key}}</div>
</div>
<div class="attribute-image">
{{#if ../editAttributes}}
<select class="attribute-value{{#if (lt attribute.value 0)}} negative{{/if}}{{#if (and (not attribute.value) (not ../abilityScoresFinished))}} unselected{{/if}}" data-attribute="{{key}}">
{{#if (not (eq attribute.value 0))}}<option value="">{{attribute.value}}</option>{{/if}}
<select class="attribute-value{{#if (lt attribute.total 0)}} negative{{/if}}{{#if (and (not attribute.total) (not ../abilityScoresFinished))}} unselected{{/if}}" data-attribute="{{key}}">
{{#if (not (eq attribute.total 0))}}<option value="">{{attribute.total}}</option>{{/if}}
{{#each ../abilityScoreArray as |option|}}
<option value="{{option.value}}">{{option.name}}</option>
<option value="{{option.total}}">{{option.name}}</option>
{{/each}}
</select>
{{else}}
<div class="attribute-text {{#if (lt attribute.value 0)}}negative{{/if}}">{{attribute.value}}</div>
<div class="attribute-text {{#if (lt attribute.total 0)}}negative{{/if}}">{{attribute.total}}</div>
{{/if}}
<img src="systems/daggerheart/assets/AttributeShield.svg" />
<div>

View file

@ -4,7 +4,7 @@
<div class="defense-row">
<div class="defense-section">
<div class="defense-container">
<div class="defense-value">{{document.system.evasion}}</div>
<div class="defense-value">{{document.system.evasion.total}}</div>
<img src="systems/daggerheart/assets/AttributeShield.svg" />
<div class="defense-banner">{{localize "DAGGERHEART.Sheets.PC.Defense.Evasion"}}</div>
</div>

View file

@ -3,7 +3,7 @@
{{#each document.system.experiences as |experience id|}}
<div class="experience-row">
<input name="{{concat "system.experiences." id ".description"}}" data-experience={{id}} value="{{experience.description}}" type="text" />
<div name="{{concat "system.experiences." id ".value"}}" class="experience-value">{{experience.value}}</div>
<div name="{{concat "system.experiences." id ".value"}}" class="experience-value">{{experience.total}}</div>
</div>
{{/each}}
{{#times (subtract 5 (length document.system.experiences))}}

View file

@ -21,7 +21,7 @@
<i class="fa-solid fa-caret-left"></i>
<div class="health-category">{{localize "DAGGERHEART.Sheets.PC.Health.Severe"}}</div>
</div>
<i data-action="makeDeathMove" class="fas fa-skull death-save {{#if (lt resources.hitPoints.value document.system.resources.hitPoints.max)}}disabled{{/if}}" title="{{localize "DAGGERHEART.Sheets.PC.Health.DeathMoveTooltip"}}"></i>
<i data-action="makeDeathMove" class="fas fa-skull death-save {{#if (lt resources.hitPoints.value document.system.resources.hitPoints.maxTotal)}}disabled{{/if}}" title="{{localize "DAGGERHEART.Sheets.PC.Health.DeathMoveTooltip"}}"></i>
</div>
<div class="flexrow" style="flex-wrap: nowrap; align-items: center;">
<div class="flexcol flex0">
@ -30,22 +30,22 @@
</div>
<div class="flexcol">
<div class="flexrow" style="flex-wrap: nowrap;">
{{#times document.system.resources.hitPoints.max}}
{{#times document.system.resources.hitPoints.maxTotal}}
{{#with (add this 1)}}
<input class="resource-box" type="checkbox" data-action="toggleHP" data-value="{{this}}" {{ checked (gte ../../document.system.resources.hitPoints.value this) }} />
{{/with}}
{{/times}}
{{#times (subtract 12 document.system.resources.hitPoints.max)}}
{{#times (subtract 12 document.system.resources.hitPoints.maxTotal)}}
<input class="resource-box disabled" type="checkbox" disabled />
{{/times}}
</div>
<div class="flexrow">
{{#times document.system.resources.stress.max}}
{{#times document.system.resources.stress.maxTotal}}
{{#with (add this 1)}}
<input class="resource-box" type="checkbox" data-action="toggleStress" data-value="{{this}}" {{ checked (gte ../../document.system.resources.stress.value this) }} />
{{/with}}
{{/times}}
{{#times (subtract 12 document.system.resources.stress.max)}}
{{#times (subtract 12 document.system.resources.stress.maxTotal)}}
<input class="resource-box disabled" type="checkbox" disabled />
{{/times}}
</div>

View file

@ -5,7 +5,7 @@
<div class="proficiency-container">
<span>{{localize "DAGGERHEART.Sheets.PC.Weapons.ProficiencyTitle"}}</span>
{{#times 6}}
<i class="fa-solid fa-circle proficiency-dot {{#if (gt ../proficiency this)}}marked{{/if}}"></i>
<i class="fa-solid fa-circle proficiency-dot {{#if (gt ../proficiency.total this)}}marked{{/if}}"></i>
{{/times}}
</div>
<div class="proficiency-container-visual-element"></div>

View file

@ -1,4 +1,4 @@
<div class="component dh-style card-preview-container selectable {{#if this.disabled}}disabled{{/if}}" data-action="selectPreview" data-path="{{this.path}}" data-uuid="{{this.uuid}}" {{#if selected}}data-selected="true"{{/if}}>
<div class="component dh-style card-preview-container selectable {{#if this.disabled}}disabled{{/if}}" data-action="selectPreview" data-path="{{this.path}}" data-uuid="{{this.uuid}}" data-is-multiclass="{{this.isMulticlass}}" data-feature-state="{{this.featureState}}" {{#if selected}}data-selected="true"{{/if}}>
<div class="preview-image-outer-container">
<img class="preview-image-container" src="{{this.img}}" />
{{#if this.selected}}
@ -7,5 +7,8 @@
</div>
{{/if}}
</div>
<div class="preview-text-container">{{this.name}}</div>
<div class="preview-text-container">
<div>{{this.name}}</div>
<strong>{{this.featureLabel}}</strong>
</div>
</div>

View file

@ -63,7 +63,7 @@
<div class="levelup-card-selection subclass-cards">
{{#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}}
</div>
</div>
@ -88,6 +88,20 @@
</div>
{{/each}}
</div>
<div class="levelup-domains-selection-container">
{{#each this.multiclass.subclasses}}
<div class="levelup-domain-selection-container {{#if this.disabled}}disabled{{/if}}" {{#if (not this.disabled)}}data-action="selectSubclass" data-uuid="{{../multiclass.uuid}}" data-subclass="{{this.uuid}}" data-path="{{../multiclass.path}}" {{/if}}>
<div class="levelup-domain-label">{{localize this.name}}</div>
<img src="{{this.img}}" />
{{#if this.selected}}
<div class="levelup-domain-selected">
<i class="fa-solid fa-check"></i>
</div>
{{/if}}
</div>
{{/each}}
</div>
</div>
</div>
{{/if}}

View file

@ -95,11 +95,14 @@
{{#if this.advancements.traits}}
<div>
<h5>{{localize "DAGGERHEART.Application.LevelUp.summary.traits"}}</h5>
<div class="summary-selection-container">
{{#each this.advancements.traits}}
<div class="summary-selection">{{this}}</div>
<div class="increase-container">
{{this.label}}: {{this.old}}
<i class="fa-solid fa-arrow-right-long"></i>
{{this.new}}
</div>
{{/each}}
</div>
</div>
{{/if}}
@ -124,6 +127,30 @@
</div>
</div>
{{/if}}
{{#if this.advancements.subclass}}
<div>
<h5>{{localize "DAGGERHEART.Application.LevelUp.summary.subclass"}}</h5>
<div class="summary-selection-container">
{{#each this.advancements.subclass}}
<div class="summary-selection">{{this.name}} - {{this.featureLabel}}</div>
{{/each}}
</div>
</div>
{{/if}}
{{#if this.advancements.multiclass}}
{{#with this.advancements.multiclass}}
<div>
<h5>{{localize "DAGGERHEART.Application.LevelUp.summary.multiclass"}}</h5>
<div class="summary-selection-container">
<div class="summary-selection">{{this.name}}</div>
<div class="summary-selection">{{this.domain}}</div>
<div class="summary-selection">{{this.subclass}}</div>
</div>
</div>
{{/with}}
{{/if}}
</div>
</fieldset>