diff --git a/lang/en.json b/lang/en.json index d9717793..3f1bdcf3 100755 --- a/lang/en.json +++ b/lang/en.json @@ -173,6 +173,14 @@ "experienceDataRemoveConfirmation": { "title": "Remove Experience Data", "text": "The experience you are about to remove has levelup data linked to it (assumably because you did levelups with the 'levelupAuto' automation setting on). Removing it will remove this automation data aswell. Do you want to proceed?" + }, + "manualMulticlass": { + "title": "Multiclass", + "text": "Do you want to add this class as your multiclass?" + }, + "manualMulticlassSubclass": { + "title": "Multiclass Subclass", + "text": "Do you want to add this subclass as your multiclass subclass?" } }, "Companion": { @@ -466,6 +474,11 @@ }, "title": "{actor} Level Up" }, + "MulticlassChoice": { + "title": "Multiclassing - {actor}", + "explanation": "You are adding {class} as your multiclass", + "selectDomainPrompt": "Select your new domain" + }, "OwnershipSelection": { "title": "Ownership Selection - {name}", "default": "Default Ownership" @@ -2209,6 +2222,7 @@ "tooLowLevel": "You cannot lower the character level below starting level", "subclassNotInClass": "This subclass does not belong to your selected class.", "missingClass": "You don't have a class selected yet.", + "missingMulticlass": "Missing multiclass", "wrongDomain": "The card isn't from one of your class domains.", "cardTooHighLevel": "The card is too high level!", "duplicateCard": "You cannot select the same card more than once.", @@ -2238,7 +2252,9 @@ "beastformToManyFeatures": "You cannot select any more features.", "beastformEquipWeapon": "You cannot use weapons while in a Beastform.", "loadoutMaxReached": "You already have {max} cards in your loadout. Move atleast one to your vault before adding a new one.", - "insufficientResources": "You have insufficient resources" + "insufficientResources": "You have insufficient resources", + "multiclassAlreadyPresent": "You already have a class and multiclass", + "subclassesAlreadyPresent": "You already have a class and multiclass subclass" }, "Tooltip": { "disableEffect": "Disable Effect", diff --git a/module/applications/dialogs/_module.mjs b/module/applications/dialogs/_module.mjs index f9b40f3f..11d6dd2b 100644 --- a/module/applications/dialogs/_module.mjs +++ b/module/applications/dialogs/_module.mjs @@ -5,6 +5,7 @@ export { default as DamageReductionDialog } from './damageReductionDialog.mjs'; export { default as DamageSelectionDialog } from './damageSelectionDialog.mjs'; export { default as DeathMove } from './deathMove.mjs'; export { default as Downtime } from './downtime.mjs'; +export { default as MulticlassChoiceDialog } from './multiclassChoiceDialog.mjs'; export { default as OwnershipSelection } from './ownershipSelection.mjs'; export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs'; export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs'; diff --git a/module/applications/dialogs/multiclassChoiceDialog.mjs b/module/applications/dialogs/multiclassChoiceDialog.mjs new file mode 100644 index 00000000..9e9f90ab --- /dev/null +++ b/module/applications/dialogs/multiclassChoiceDialog.mjs @@ -0,0 +1,73 @@ +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +export default class MulticlassChoiceDialog extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(actor, multiclass, options) { + super(options); + + this.actor = actor; + this.multiclass = multiclass; + this.selectedDomain = null; + } + + get title() { + return game.i18n.format('DAGGERHEART.APPLICATIONS.MulticlassChoice.title', { actor: this.actor.name }); + } + + static DEFAULT_OPTIONS = { + classes: ['daggerheart', 'dh-style', 'dialog', 'views', 'multiclass-choice'], + position: { width: 'auto', height: 'auto' }, + window: { icon: 'fa-solid fa-person-rays' }, + actions: { + save: MulticlassChoiceDialog.#save, + selectDomain: MulticlassChoiceDialog.#selectDomain + } + }; + + static PARTS = { + application: { + id: 'multiclass-choice', + template: 'systems/daggerheart/templates/dialogs/multiclassChoice.hbs' + } + }; + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.multiclass = this.multiclass; + context.domainChoices = this.multiclass.domains.map(value => { + const domain = CONFIG.DH.DOMAIN.domains[value]; + return { + value: value, + label: game.i18n.localize(domain.label), + description: game.i18n.localize(domain.description), + src: domain.src, + selected: value === this.selectedDomain, + disabled: this.actor.system.domains.includes(domain) + }; + }); + context.multiclassDisabled = !this.selectedDomain; + + return context; + } + + /** @override */ + _onClose(options = {}) { + if (!options.submitted) this.move = null; + } + + static async configure(actor, multiclass, options = {}) { + return new Promise(resolve => { + const app = new this(actor, multiclass, options); + app.addEventListener('close', () => resolve(app.selectedDomain), { once: true }); + app.render({ force: true }); + }); + } + + static #save() { + this.close({ submitted: true }); + } + + static #selectDomain(_event, button) { + this.selectedDomain = this.selectedDomain === button.dataset.domain ? null : button.dataset.domain; + this.render(); + } +} diff --git a/module/data/item/class.mjs b/module/data/item/class.mjs index d64c77cc..bca6434c 100644 --- a/module/data/item/class.mjs +++ b/module/data/item/class.mjs @@ -62,16 +62,37 @@ export default class DHClass extends BaseDataItem { } 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.value' : 'system.class.value'; - if (foundry.utils.getProperty(this.actor, path)) { - ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.classAlreadySelected')); - return false; + const levelupAuto = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto; + if (levelupAuto) { + const path = data.system.isMulticlass ? 'system.multiclass.value' : 'system.class.value'; + if (foundry.utils.getProperty(this.actor, path)) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.classAlreadySelected')); + return false; + } + } else { + if (this.actor.system.class.value) { + if (this.actor.system.multiclass.value) { + ui.notifications.warn( + game.i18n.localize('DAGGERHEART.UI.Notifications.multiclassAlreadyPresent') + ); + return false; + } else { + const selectedDomain = + await game.system.api.applications.dialogs.MulticlassChoiceDialog.configure( + this.actor, + this + ); + if (!selectedDomain) return false; + + await this.updateSource({ isMulticlass: true, domains: [selectedDomain] }); + } + } } } + + const allowed = await super._preCreate(data, options, user); + if (allowed === false) return; } _onCreate(data, options, userId) { diff --git a/module/data/item/subclass.mjs b/module/data/item/subclass.mjs index ed6f8b45..2a58fd8a 100644 --- a/module/data/item/subclass.mjs +++ b/module/data/item/subclass.mjs @@ -45,21 +45,23 @@ export default class DHSubclass extends BaseDataItem { if (allowed === false) return; if (this.actor?.type === 'character') { - 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.UI.Notifications.missingClass')); - return false; - } else if (subclassData) { - ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassAlreadySelected')); - return false; - } else if (classData.system.subclasses.every(x => x.uuid !== (data.uuid ?? `Item.${data._id}`))) { - ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassNotInClass')); - return false; + if (this.actor.system.class.subclass) { + if (this.actor.system.multiclass.subclass) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassesAlreadyPresent')); + return false; + } else { + if (!this.actor.system.multiclass.value) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.missingMulticlass')); + return false; + } + + await this.updateSource({ isMulticlass: true }); + } + } else { + if (!this.actor.system.class.value) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.missingClass')); + return false; + } } } } diff --git a/styles/less/dialog/index.less b/styles/less/dialog/index.less index 496d09e9..c6a16527 100644 --- a/styles/less/dialog/index.less +++ b/styles/less/dialog/index.less @@ -24,3 +24,5 @@ @import './dice-roll/roll-selection.less'; @import './damage-reduction/damage-reduction-container.less'; @import './damage-reduction/sheets.less'; + +@import './multiclass-choice/sheet.less'; diff --git a/styles/less/dialog/multiclass-choice/sheet.less b/styles/less/dialog/multiclass-choice/sheet.less new file mode 100644 index 00000000..93a8540b --- /dev/null +++ b/styles/less/dialog/multiclass-choice/sheet.less @@ -0,0 +1,76 @@ +.theme-light .daggerheart.dh-style.dialog.multiclass-choice { + .multiclass-container .domain-choice-container button label { + background-image: url(../assets/parchments/dh-parchment-light.png); + } +} + +.daggerheart.dh-style.dialog.multiclass-choice { + .multiclass-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + + .multiclass-explanation { + margin-top: 0; + font-style: italic; + } + + .multiclass-domains-container { + display: flex; + gap: 16px; + + .domain-choice-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + + button { + position: relative; + display: flex; + justify-content: center; + width: 120px; + height: 120px; + background: light-dark(@dark-blue-10, @golden-10); + color: light-dark(@dark-blue, @golden); + + &.selected { + background: light-dark(@dark-blue-40, @golden-40); + } + + label { + position: absolute; + top: 4px; + font-family: @font-body; + border-radius: 6px; + border: 2px solid; + padding: 0 2px; + background-image: url(../assets/parchments/dh-parchment-dark.png); + color: light-dark(@dark, @beige); + } + } + + .domain-description { + width: 240px; + display: flex; + flex-wrap: wrap; + font-style: italic; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + padding: 4px 4px; + } + } + } + + footer { + width: 100%; + display: flex; + gap: 8px; + + button { + flex: 1; + } + } + } +} diff --git a/styles/less/sheets/actors/character/header.less b/styles/less/sheets/actors/character/header.less index 060fa59f..05a951e8 100644 --- a/styles/less/sheets/actors/character/header.less +++ b/styles/less/sheets/actors/character/header.less @@ -88,6 +88,11 @@ font-size: 12px; color: light-dark(@dark-blue, @golden); + .missing-header-feature { + opacity: 0.5; + text-decoration: line-through; + } + span { padding: 3px; border-radius: 3px; diff --git a/templates/dialogs/multiclassChoice.hbs b/templates/dialogs/multiclassChoice.hbs new file mode 100644 index 00000000..2e37544a --- /dev/null +++ b/templates/dialogs/multiclassChoice.hbs @@ -0,0 +1,23 @@ +
+
+
{{localize "DAGGERHEART.APPLICATIONS.MulticlassChoice.explanation" class=multiclass.parent.name }}
+
{{localize "DAGGERHEART.APPLICATIONS.MulticlassChoice.selectDomainPrompt" }}
+ +
+ {{#each domainChoices as | choice |}} +
+ +
{{choice.description}}
+
+ {{/each}} +
+ + +
+
\ No newline at end of file diff --git a/templates/sheets/actors/character/header.hbs b/templates/sheets/actors/character/header.hbs index 57a86d34..cfb4e079 100644 --- a/templates/sheets/actors/character/header.hbs +++ b/templates/sheets/actors/character/header.hbs @@ -31,25 +31,25 @@ {{#if document.system.class.value}} {{document.system.class.value.name}} {{else}} - {{localize 'TYPES.Item.class'}} + {{localize 'TYPES.Item.class'}} {{/if}} {{#if document.system.class.subclass}} {{document.system.class.subclass.name}} {{else}} - {{localize 'TYPES.Item.subclass'}} + {{localize 'TYPES.Item.subclass'}} {{/if}} {{#if document.system.community}} {{document.system.community.name}} {{else}} - {{localize 'TYPES.Item.community'}} + {{localize 'TYPES.Item.community'}} {{/if}} {{#if document.system.ancestry}} {{document.system.ancestry.name}} {{else}} - {{localize 'TYPES.Item.ancestry'}} + {{localize 'TYPES.Item.ancestry'}} {{/if}} @@ -64,7 +64,7 @@ {{#if document.system.multiclass.subclass}} {{document.system.multiclass.subclass.name}} {{else}} - {{localize 'TYPES.Item.subclass'}} + {{localize 'TYPES.Item.subclass'}} {{/if}} {{/if}}