diff --git a/lang/en.json b/lang/en.json
index 00064585..0b43ee8f 100755
--- a/lang/en.json
+++ b/lang/en.json
@@ -352,30 +352,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."
}
},
@@ -834,7 +843,7 @@
"content": "Returning to the previous level selection will remove all selections made for this level. Do you want to proceed?"
},
"Selections": {
- "emptyDomainCardHint": "Domain Card Level {level} or below"
+ "emptyDomainCardHint": "{domain} level {level} or below"
},
"summary": {
"levelAchievements": "Level Achievements",
diff --git a/module/applications/levelup.mjs b/module/applications/levelup.mjs
index 61a85677..3094c5bb 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
},
@@ -178,6 +179,20 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
};
const allDomainCardKeys = Object.keys(allDomainCards);
+ const classDomainsData = this.actor.system.class.value.system.domains.map(domain => ({
+ domain,
+ multiclass: false
+ }));
+ const multiclassDomainsData = (this.actor.system.multiclass?.value?.system?.domains ?? []).map(
+ domain => ({ domain, multiclass: true })
+ );
+ const domainsData = [...classDomainsData, ...multiclassDomainsData];
+ const multiclassDomain = this.levelup.classUpgradeChoices?.multiclass?.domain;
+ if (multiclassDomain) {
+ if (!domainsData.some(x => x.domain === multiclassDomain))
+ domainsData.push({ domain: multiclassDomain, multiclass: true });
+ }
+
context.domainCards = [];
for (var key of allDomainCardKeys) {
const domainCard = allDomainCards[key];
@@ -188,35 +203,56 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
context.domainCards.push({
...(card.toObject?.() ?? card),
- emptySubtext: game.i18n.format(
- 'DAGGERHEART.Application.LevelUp.Selections.emptyDomainCardHint',
- { level: domainCard.level }
- ),
+ emptySubtexts: domainsData.map(domain => {
+ const levelBase = domain.multiclass
+ ? Math.ceil(this.levelup.currentLevel / 2)
+ : this.levelup.currentLevel;
+ const levelMax = domainCard.secondaryData?.limit
+ ? Math.min(domainCard.secondaryData.limit, levelBase)
+ : levelBase;
+
+ return game.i18n.format('DAGGERHEART.Application.LevelUp.Selections.emptyDomainCardHint', {
+ domain: game.i18n.localize(domains[domain.domain].label),
+ level: levelMax
+ });
+ }),
path: domainCard.data
? `${domainCard.path}.data`
: `levels.${domainCard.level}.achievements.domainCards.${key}.uuid`,
- limit: domainCard.level,
+ limit: domainCard.secondaryData?.limit ?? null,
compendium: 'domains'
});
}
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));
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 +273,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 +322,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 +367,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 +391,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 +430,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 =
@@ -419,7 +507,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
const traitsTagify = htmlElement.querySelector('.levelup-trait-increases');
if (traitsTagify) {
- tagifyElement(traitsTagify, abilities, this.tagifyUpdate('trait').bind(this));
+ tagifyElement(traitsTagify, this.levelup.unmarkedTraits, this.tagifyUpdate('trait').bind(this));
}
const experienceIncreaseTagify = htmlElement.querySelector('.levelup-experience-increases');
@@ -485,8 +573,10 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
if (event.target.closest('.domain-cards')) {
const target = event.target.closest('.card-preview-container');
if (item.type === 'domainCard') {
+ const { multiclass } = this.levelup.classUpgradeChoices;
+ const isMulticlass = !multiclass ? false : item.system.domain === multiclass.domain;
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(
@@ -495,7 +585,9 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
return;
}
- if (item.system.level > Number(target.dataset.limit)) {
+ const levelBase = isMulticlass ? Math.ceil(this.levelup.currentLevel / 2) : this.levelup.currentLevel;
+ const levelMax = target.dataset.limit ? Math.min(Number(target.dataset.limit), levelBase) : levelBase;
+ if (levelMax < item.system.level) {
ui.notifications.error(
game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.error.domainCardToHighLevel')
);
@@ -547,8 +639,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 +653,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')
);
@@ -579,15 +670,23 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2)
return;
}
- update[
- `levels.${this.levelup.currentLevel}.choices.${button.dataset.option}.${button.dataset.checkboxNr}`
- ] = {
+ const updateData = {
tier: Number(button.dataset.tier),
minCost: Number(button.dataset.cost),
amount: button.dataset.amount ? Number(button.dataset.amount) : null,
value: button.dataset.value,
type: button.dataset.type
};
+
+ if (button.dataset.type === 'domainCard') {
+ updateData.secondaryData = {
+ limit: Math.max(...this.levelup.tiers[button.dataset.tier].belongingLevels)
+ };
+ }
+
+ update[
+ `levels.${this.levelup.currentLevel}.choices.${button.dataset.option}.${button.dataset.checkboxNr}`
+ ] = updateData;
}
await this.levelup.updateSource(update);
@@ -600,24 +699,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 `
-
` : ''}
-