[Fix] finishing levelup with a multiclass (#1906)
Some checks are pending
Project CI / build (24.x) (push) Waiting to run

* Fix finishing levelup with a multiclass

* Fix removal when de-leveling

* Also delete multiclass related stuff if reducing below the minimum multiclass level
This commit is contained in:
Carlos Fernandez 2026-05-20 12:45:42 -04:00 committed by GitHub
parent 2f589c1b8e
commit b23095cb2f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 47 additions and 51 deletions

View file

@ -718,7 +718,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
? { ? {
'system.linkedClass.uuid': { 'system.linkedClass.uuid': {
key: 'system.linkedClass.uuid', key: 'system.linkedClass.uuid',
value: this.document.system.class.value._stats.compendiumSource value: this.document.system.class.value?._stats.compendiumSource
} }
} }
: undefined, : undefined,

View file

@ -56,39 +56,31 @@ export default class DHSubclass extends BaseDataItem {
if (allowed === false) return; if (allowed === false) return;
if (this.actor?.type === 'character') { if (this.actor?.type === 'character') {
const dataUuid = data.uuid ?? data._stats.compendiumSource ?? `Item.${data._id}`; const { value: actorClass, subclass: existingSubclass } = this.actor.system.class;
if (this.actor.system.class.subclass) { const { value: multiclass, subclass: existingMultisubclass } = this.actor.system.multiclass;
if (this.actor.system.multiclass.subclass) { if (!actorClass && !multiclass) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassesAlreadyPresent')); ui.notifications.warn('DAGGERHEART.UI.Notifications.missingClass', { localize: true });
return false; return false;
} else { }
const multiclass = this.actor.items.find(x => x.type === 'class' && x.system.isMulticlass); if (existingSubclass && existingMultisubclass) {
if (!multiclass) { ui.notifications.warn('DAGGERHEART.UI.Notifications.subclassesAlreadyPresent', { localize: true });
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.missingMulticlass')); return false;
}
if (existingSubclass && !multiclass) {
ui.notifications.warn('DAGGERHEART.UI.Notifications.missingMulticlass', { localize: true });
return false; return false;
} }
if (multiclass.system.subclasses.every(x => x.uuid !== dataUuid)) { const match = [multiclass, actorClass].find(
ui.notifications.error( c => c && (c._stats.compendiumSource ?? c.uuid) === this.linkedClass
game.i18n.localize('DAGGERHEART.UI.Notifications.subclassNotInMulticlass')
); );
if (!match) {
const key = multiclass ? 'subclassNotInMulticlass' : 'subclassNotInClass';
ui.notifications.warn(`DAGGERHEART.UI.Notifications.${key}`, { localize: true });
return false; return false;
} } else if (match.system.isMulticlass) {
await this.updateSource({ isMulticlass: true }); await this.updateSource({ isMulticlass: true });
} }
} else {
const actorClass = this.actor.items.find(x => x.type === 'class' && !x.system.isMulticlass);
if (!actorClass) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.missingClass'));
return false;
}
if ((await actorClass.system.fetchSubclasses()).every(x => x.uuid !== dataUuid)) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassNotInClass'));
return false;
}
}
} }
} }

View file

@ -153,10 +153,13 @@ export default class DhpActor extends Actor {
async updateLevel(newLevel) { async updateLevel(newLevel) {
if (!['character', 'companion'].includes(this.type) || newLevel === this.system.levelData.level.changed) return; if (!['character', 'companion'].includes(this.type) || newLevel === this.system.levelData.level.changed) return;
const tiers = Object.values(game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers);
const maxLevel = tiers.reduce((acc, tier) => Math.max(acc, tier.levels.end), 0);
const multiclassMinLevel = Math.min(
maxLevel,
...tiers.filter(t => t.options.multiclass).map(t => t.levels.start)
);
if (newLevel > this.system.levelData.level.current) { if (newLevel > this.system.levelData.level.current) {
const maxLevel = Object.values(
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers
).reduce((acc, tier) => Math.max(acc, tier.levels.end), 0);
if (newLevel > maxLevel) { if (newLevel > maxLevel) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.tooHighLevel')); ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.tooHighLevel'));
} }
@ -231,18 +234,19 @@ export default class DhpActor extends Actor {
this.system.multiclass.subclass.update({ 'system.featureState': subclassFeatureState.multiclass }); this.system.multiclass.subclass.update({ 'system.featureState': subclassFeatureState.multiclass });
} }
if (multiclass) { // Remove multiclass if we're removing a multiclass feature or if we're below the multiclass minimum level
const multiclassItem = this.items.find(x => x.uuid === multiclass.itemUuid); // Multclasses cannot be manually removed on the sheet, so this allows recovering in the case of errors
const multiclassFeatures = this.items.filter( if (multiclass || newLevel < multiclassMinLevel) {
x => x.system.originItemType === 'class' && x.system.multiclassOrigin const multiclassItems = this.items.filter(
); x =>
const subclassFeatures = this.items.filter( x.uuid === multiclass?.itemUuid ||
x => x.system.originItemType === 'subclass' && x.system.multiclassOrigin x.system.isMulticlass ||
(['class', 'subclass'].includes(x.system.originItemType) && x.system.multiclassOrigin)
); );
this.deleteEmbeddedDocuments( this.deleteEmbeddedDocuments(
'Item', 'Item',
[multiclassItem, ...multiclassFeatures, ...subclassFeatures].map(x => x.id) multiclassItems.map(x => x.id)
); );
this.update({ this.update({
@ -281,6 +285,7 @@ export default class DhpActor extends Actor {
async levelUp(levelupData) { async levelUp(levelupData) {
const levelupAuto = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto; const levelupAuto = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto;
const getStatsWithSource = document => ({ ...(document._stats ?? {}), compendiumSource: document.uuid });
const levelups = {}; const levelups = {};
for (var levelKey of Object.keys(levelupData)) { for (var levelKey of Object.keys(levelupData)) {
@ -393,8 +398,8 @@ export default class DhpActor extends Actor {
const embeddedItem = await this.createEmbeddedDocuments('Item', [ const embeddedItem = await this.createEmbeddedDocuments('Item', [
{ {
...multiclassData, ...multiclassData,
uuid: multiclassItem.uuid, uuid: multiclassItem.uuid, // todo: replace with setting an id and using keepId
_stats: multiclassItem._stats, _stats: getStatsWithSource(multiclassItem),
system: { system: {
...multiclassData.system, ...multiclassData.system,
features: multiclassData.system.features.filter(x => x.type !== 'hope'), features: multiclassData.system.features.filter(x => x.type !== 'hope'),
@ -407,8 +412,8 @@ export default class DhpActor extends Actor {
await this.createEmbeddedDocuments('Item', [ await this.createEmbeddedDocuments('Item', [
{ {
...subclassData, ...subclassData,
uuid: subclassItem.uuid, uuid: subclassItem.uuid, // todo: replace with setting an id and using keepId
_stats: subclassItem._stats, _stats: getStatsWithSource(subclassItem),
system: { system: {
...subclassData.system, ...subclassData.system,
isMulticlass: true isMulticlass: true
@ -428,8 +433,8 @@ export default class DhpActor extends Actor {
const embeddedItem = await this.createEmbeddedDocuments('Item', [ const embeddedItem = await this.createEmbeddedDocuments('Item', [
{ {
...cardData, ...cardData,
uuid: cardItem.uuid, uuid: cardItem.uuid, // todo: replace with setting an id and using keepId
_stats: cardItem._stats, _stats: getStatsWithSource(cardItem),
system: { system: {
...cardData.system, ...cardData.system,
inVault: true inVault: true
@ -450,8 +455,7 @@ export default class DhpActor extends Actor {
const embeddedItem = await this.createEmbeddedDocuments('Item', [ const embeddedItem = await this.createEmbeddedDocuments('Item', [
{ {
...cardData, ...cardData,
uuid: cardItem.uuid, _stats: getStatsWithSource(cardItem),
_stats: cardItem._stats,
system: { system: {
...cardData.system, ...cardData.system,
inVault: true inVault: true