From e1d8f8784a624dbcc683e1793f62f1e8d4b0f99a Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Fri, 1 Aug 2025 17:16:35 +0200 Subject: [PATCH] [Feature] Manual Character Editing (#490) * Initial * Added Character-Settings * Finalized Character-Settings * Hide CharacterSetup if any part is done manually * Fixed class/subclass drag-drop * Fixed relinking of Features from items created on Character * Adding features on CharacterItems now adds them on the Character and relinks * Made suggested items inactive in the Class sheet if rendered from inside a Character * Added hope to CharacterSetting * add style to textarea element, add spellcasting and domain class into char sheet and move rest buttons to another place * Fixed characterCreation experience description --------- Co-authored-by: moliloo --- lang/en.json | 35 ++++- module/applications/dialogs/_module.mjs | 1 + .../dialogs/multiclassChoiceDialog.mjs | 73 +++++++++ module/applications/levelup/levelup.mjs | 4 +- .../applications/sheets-configs/_module.mjs | 1 + .../sheets-configs/character-settings.mjs | 143 ++++++++++++++++++ .../sheets-configs/companion-settings.mjs | 34 +++++ .../applications/sheets/actors/character.mjs | 10 +- .../sheets/api/application-mixin.mjs | 24 ++- module/applications/sheets/api/base-item.mjs | 22 ++- module/applications/sheets/items/class.mjs | 76 +++++----- module/data/actor/adversary.mjs | 3 +- module/data/actor/character.mjs | 89 ++++++----- module/data/fields/actorField.mjs | 9 +- module/data/item/base.mjs | 19 ++- module/data/item/class.mjs | 35 ++++- module/data/item/feature.mjs | 1 - module/data/item/subclass.mjs | 57 ++++--- module/data/settings/Automation.mjs | 5 + module/documents/actor.mjs | 142 +++++++++-------- .../selections-container.less | 40 +++-- styles/less/dialog/index.less | 2 + .../dialog/level-up/selections-container.less | 6 + .../less/dialog/multiclass-choice/sheet.less | 76 ++++++++++ styles/less/global/elements.less | 19 ++- .../adversary-settings/experiences.less | 19 ++- .../character-settings/sheet.less | 36 +++++ styles/less/sheets-settings/index.less | 1 + .../less/sheets/actors/character/header.less | 80 ++++++---- .../less/sheets/actors/character/sheet.less | 1 + .../less/sheets/actors/character/sidebar.less | 116 +++++++++++--- .../setupTabs/experience.hbs | 8 +- templates/dialogs/multiclassChoice.hbs | 23 +++ templates/levelup/tabs/footer.hbs | 8 + templates/levelup/tabs/tab-navigation.hbs | 74 ++++----- templates/settings/automation-settings.hbs | 1 + .../adversary-settings/experiences.hbs | 10 +- .../character-settings/details.hbs | 42 +++++ .../character-settings/experiences.hbs | 25 +++ .../character-settings/header.hbs | 3 + .../companion-settings/attack.hbs | 1 + .../companion-settings/experiences.hbs | 16 +- templates/sheets/actors/adversary/header.hbs | 2 +- templates/sheets/actors/adversary/sidebar.hbs | 20 +-- templates/sheets/actors/character/header.hbs | 44 +++--- templates/sheets/actors/character/sidebar.hbs | 39 +++-- templates/sheets/actors/companion/header.hbs | 2 +- templates/sheets/items/class/features.hbs | 68 +++++---- templates/sheets/items/class/settings.hbs | 26 ++-- 49 files changed, 1205 insertions(+), 386 deletions(-) create mode 100644 module/applications/dialogs/multiclassChoiceDialog.mjs create mode 100644 module/applications/sheets-configs/character-settings.mjs create mode 100644 styles/less/dialog/multiclass-choice/sheet.less create mode 100644 styles/less/sheets-settings/character-settings/sheet.less create mode 100644 templates/dialogs/multiclassChoice.hbs create mode 100644 templates/levelup/tabs/footer.hbs create mode 100644 templates/sheets-settings/character-settings/details.hbs create mode 100644 templates/sheets-settings/character-settings/experiences.hbs create mode 100644 templates/sheets-settings/character-settings/header.hbs diff --git a/lang/en.json b/lang/en.json index 2ed77e99..48b01ea7 100755 --- a/lang/en.json +++ b/lang/en.json @@ -162,11 +162,25 @@ }, "faith": "Faith", "levelUp": "You can level up", + "maxEvasionBonus": "Max Evasion Increase", + "maxHPBonus": "Max HP Increase", "pronouns": "Pronouns", "story": { "backgroundTitle": "Background", "characteristics": "Characteristics", "connectionsTitle": "Connections" + }, + "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": { @@ -460,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" @@ -1866,6 +1885,7 @@ "levelUp": "Level Up", "loadout": "Loadout", "max": "Max", + "maxWithThing": "Max {thing}", "multiclass": "Multiclass", "newCategory": "New Category", "none": "None", @@ -2022,6 +2042,10 @@ "gm": { "label": "GM" }, "players": { "label": "Players" } }, + "levelupAuto": { + "label": "Levelup Automation", + "hint": "When you've made your choices and finish levelup, the numerical changes are automatically applied to your character." + }, "actionPoints": { "label": "Action Points", "hint": "Automatically give and take Action Points as combatants take their turns." @@ -2205,7 +2229,9 @@ "tooHighLevel": "You cannot raise the character level past the maximum", "tooLowLevel": "You cannot lower the character level below starting level", "subclassNotInClass": "This subclass does not belong to your selected class.", + "subclassNotInMulticlass": "This subclass does not belong to your selected multiclass.", "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.", @@ -2235,7 +2261,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", @@ -2243,6 +2271,8 @@ "openItemWorld": "Open Item World", "openActorWorld": "Open Actor World", "sendToChat": "Send to Chat", + "maxEvasionClassBound": "Your Evasion base is set on your class. This is the increase ontop of that.", + "maxHPClassBound": "Your max HP base is set on your class. This is the increase ontop of that.", "moreOptions": "More Options", "equip": "Equip", "unequip": "Unequip", @@ -2252,7 +2282,8 @@ "rangeAndTarget": "Range & Target", "dragApplyEffect": "Drag effect to apply it to an actor", "appliedEvenIfSuccessful": "Applied even if save succeeded", - "diceIsRerolled": "The dice has been rerolled (x{times})" + "diceIsRerolled": "The dice has been rerolled (x{times})", + "openSheetSettings": "Open Settings" } } } 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..12f2629b --- /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(value) + }; + }); + 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/applications/levelup/levelup.mjs b/module/applications/levelup/levelup.mjs index 7820c267..f025c131 100644 --- a/module/applications/levelup/levelup.mjs +++ b/module/applications/levelup/levelup.mjs @@ -46,7 +46,8 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) tabs: { template: 'systems/daggerheart/templates/levelup/tabs/tab-navigation.hbs' }, advancements: { template: 'systems/daggerheart/templates/levelup/tabs/advancements.hbs' }, selections: { template: 'systems/daggerheart/templates/levelup/tabs/selections.hbs' }, - summary: { template: 'systems/daggerheart/templates/levelup/tabs/summary.hbs' } + summary: { template: 'systems/daggerheart/templates/levelup/tabs/summary.hbs' }, + footer: { template: 'systems/daggerheart/templates/levelup/tabs/footer.hbs' } }; static TABS = { @@ -95,6 +96,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) const context = await super._prepareContext(_options); context.levelup = this.levelup; context.tabs = this._getTabs(this.constructor.TABS); + context.levelupAuto = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto; return context; } diff --git a/module/applications/sheets-configs/_module.mjs b/module/applications/sheets-configs/_module.mjs index 49cc74b0..ed062163 100644 --- a/module/applications/sheets-configs/_module.mjs +++ b/module/applications/sheets-configs/_module.mjs @@ -1,4 +1,5 @@ export { default as ActionConfig } from './action-config.mjs'; +export { default as CharacterSettings } from './character-settings.mjs'; export { default as AdversarySettings } from './adversary-settings.mjs'; export { default as CompanionSettings } from './companion-settings.mjs'; export { default as DowntimeConfig } from './downtimeConfig.mjs'; diff --git a/module/applications/sheets-configs/character-settings.mjs b/module/applications/sheets-configs/character-settings.mjs new file mode 100644 index 00000000..20a09cfc --- /dev/null +++ b/module/applications/sheets-configs/character-settings.mjs @@ -0,0 +1,143 @@ +import DHBaseActorSettings from '../sheets/api/actor-setting.mjs'; + +/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ + +export default class DHCharacterSettings extends DHBaseActorSettings { + /**@inheritdoc */ + static DEFAULT_OPTIONS = { + classes: ['character-settings'], + position: { width: 455, height: 'auto' }, + actions: { + addExperience: DHCharacterSettings.#addExperience, + removeExperience: DHCharacterSettings.#removeExperience + }, + dragDrop: [ + { dragSelector: null, dropSelector: '.tab.features' }, + { dragSelector: '.feature-item', dropSelector: null } + ] + }; + + /**@override */ + static PARTS = { + header: { + id: 'header', + template: 'systems/daggerheart/templates/sheets-settings/character-settings/header.hbs' + }, + tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, + details: { + id: 'details', + template: 'systems/daggerheart/templates/sheets-settings/character-settings/details.hbs' + }, + experiences: { + id: 'experiences', + template: 'systems/daggerheart/templates/sheets-settings/character-settings/experiences.hbs' + } + }; + + /** @override */ + static TABS = { + primary: { + tabs: [{ id: 'details' }, { id: 'experiences' }], + initial: 'details', + labelPrefix: 'DAGGERHEART.GENERAL.Tabs' + } + }; + + /**@inheritdoc */ + async _prepareContext(options) { + const context = await super._prepareContext(options); + context.levelupAuto = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto; + + return context; + } + + /* -------------------------------------------- */ + + /** + * Adds a new experience entry to the actor. + * @type {ApplicationClickAction} + */ + static async #addExperience() { + const newExperience = { + name: 'Experience', + modifier: 0 + }; + await this.actor.update({ [`system.experiences.${foundry.utils.randomID()}`]: newExperience }); + } + + /** + * Removes an experience entry from the actor. + * @type {ApplicationClickAction} + */ + static async #removeExperience(_, target) { + const experience = this.actor.system.experiences[target.dataset.experience]; + const updates = {}; + + const relinkAchievementData = []; + const relinkSelectionData = []; + Object.keys(this.actor.system.levelData.levelups).forEach(key => { + const level = this.actor.system.levelData.levelups[key]; + + const achievementIncludesExp = level.achievements.experiences[target.dataset.experience]; + if (achievementIncludesExp) + relinkAchievementData.push({ levelKey: key, experience: target.dataset.experience }); + + const selectionIndex = level.selections.findIndex( + x => x.optionKey === 'experience' && x.data[0] === target.dataset.experience + ); + if (selectionIndex !== -1) + relinkSelectionData.push({ levelKey: key, selectionIndex, experience: target.dataset.experience }); + }); + + if (relinkAchievementData.length > 0 || relinkSelectionData.length > 0) { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.localize('DAGGERHEART.ACTORS.Character.experienceDataRemoveConfirmation.title') + }, + content: game.i18n.localize('DAGGERHEART.ACTORS.Character.experienceDataRemoveConfirmation.text') + }); + if (!confirmed) return; + } + + if (relinkAchievementData.length > 0) { + relinkAchievementData.forEach(data => { + updates[`system.levelData.levelups.${data.levelKey}.achievements.experiences.-=${data.experience}`] = + null; + }); + } else if (relinkSelectionData.length > 0) { + relinkSelectionData.forEach(data => { + updates[`system.levelData.levelups.${data.levelKey}.selections`] = this.actor.system.levelData.levelups[ + data.levelKey + ].selections.reduce((acc, selection, index) => { + if ( + index === data.selectionIndex && + selection.optionKey === 'experience' && + selection.data.includes(data.experience) + ) { + acc.push({ ...selection, data: selection.data.filter(x => x !== data.experience) }); + } else { + acc.push(selection); + } + + return acc; + }, []); + }); + } else { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { + type: game.i18n.localize(`DAGGERHEART.GENERAL.Experience.single`), + name: experience.name + }) + }, + content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: experience.name }) + }); + if (!confirmed) return; + } + + await this.actor.update({ + ...updates, + [`system.experiences.-=${target.dataset.experience}`]: null + }); + } +} diff --git a/module/applications/sheets-configs/companion-settings.mjs b/module/applications/sheets-configs/companion-settings.mjs index fed70c56..6791c4c7 100644 --- a/module/applications/sheets-configs/companion-settings.mjs +++ b/module/applications/sheets-configs/companion-settings.mjs @@ -10,6 +10,8 @@ export default class DHCompanionSettings extends DHBaseActorSettings { classes: ['companion-settings'], position: { width: 455, height: 'auto' }, actions: { + addExperience: DHCompanionSettings.#addExperience, + removeExperience: DHCompanionSettings.#removeExperience, levelUp: DHCompanionSettings.#levelUp } }; @@ -88,6 +90,38 @@ export default class DHCompanionSettings extends DHBaseActorSettings { if (!value) await this.actor.updateLevel(1); } + /** + * Adds a new experience entry to the actor. + * @type {ApplicationClickAction} + */ + static async #addExperience() { + const newExperience = { + name: 'Experience', + modifier: 0 + }; + await this.actor.update({ [`system.experiences.${foundry.utils.randomID()}`]: newExperience }); + } + + /** + * Removes an experience entry from the actor. + * @type {ApplicationClickAction} + */ + static async #removeExperience(_, target) { + const experience = this.actor.system.experiences[target.dataset.experience]; + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { + type: game.i18n.localize(`DAGGERHEART.GENERAL.Experience.single`), + name: experience.name + }) + }, + content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: experience.name }) + }); + if (!confirmed) return; + + await this.actor.update({ [`system.experiences.-=${target.dataset.experience}`]: null }); + } + /** * Opens the companion level-up dialog for the associated actor. * @type {ApplicationClickAction} diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 6bbef5b0..1ef50b84 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -716,10 +716,14 @@ export default class CharacterSheet extends DHBaseActorSheet { }); } + /** + * Open the downtime application. + * @type {ApplicationClickAction} + */ static useDowntime(_, button) { - new game.system.api.applications.dialogs.Downtime(this.document, button.dataset.type === 'shortRest').render( - true - ); + new game.system.api.applications.dialogs.Downtime(this.document, button.dataset.type === 'shortRest').render({ + force: true + }); } async _onDragStart(event) { diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index d5d0565f..15bcb87f 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -417,17 +417,29 @@ export default function DHApplicationMixin(Base) { const { documentClass, type, inVault, disabled } = target.dataset; const parentIsItem = this.document.documentName === 'Item'; const parent = - parentIsItem && documentClass === 'Item' - ? type === 'action' - ? this.document.system - : null - : this.document; + this.document.parent?.type === 'character' + ? this.document.parent + : parentIsItem && documentClass === 'Item' + ? type === 'action' + ? this.document.system + : null + : this.document; + + let systemData = {}; + if (parent?.type === 'character' && type === 'feature') { + systemData = { + originItemType: this.document.type, + originId: this.document.id, + identifier: this.document.system.isMulticlass ? 'multiclass' : null + }; + } const cls = type === 'action' ? game.system.api.models.actions.actionsTypes.base : getDocumentClass(documentClass); const data = { name: cls.defaultName({ type, parent }), - type + type, + system: systemData }; if (inVault) data['system.inVault'] = true; if (disabled) data.disabled = true; diff --git a/module/applications/sheets/api/base-item.mjs b/module/applications/sheets/api/base-item.mjs index ed63956b..b5573a0c 100644 --- a/module/applications/sheets/api/base-item.mjs +++ b/module/applications/sheets/api/base-item.mjs @@ -150,10 +150,24 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { static async #addFeature(_, target) { const { type } = target.dataset; const cls = foundry.documents.Item.implementation; - const item = await cls.create({ - type: 'feature', - name: cls.defaultName({ type: 'feature' }) - }); + + let systemData = {}; + if (this.document.parent?.type === 'character') { + systemData = { + originItemType: this.document.type, + originId: this.document.id, + identifier: this.document.system.isMulticlass ? 'multiclass' : null + }; + } + + const item = await cls.create( + { + type: 'feature', + name: cls.defaultName({ type: 'feature' }), + system: systemData + }, + { parent: this.document.parent?.type === 'character' ? this.document.parent : undefined } + ); await this.document.update({ 'system.features': [...this.document.system.features, { type, item }].map(x => ({ ...x, diff --git a/module/applications/sheets/items/class.mjs b/module/applications/sheets/items/class.mjs index c78e1de1..3dce0a11 100644 --- a/module/applications/sheets/items/class.mjs +++ b/module/applications/sheets/items/class.mjs @@ -113,45 +113,47 @@ export default class ClassSheet extends DHBaseItemSheet { }); } else if (item.type === 'feature') { super._onDrop(event); - } else if (item.type === 'weapon') { - if (target.classList.contains('primary-weapon-section')) { - if (!item.system.secondary) + } else if (this.document.parent?.type !== 'character') { + if (item.type === 'weapon') { + if (target.classList.contains('primary-weapon-section')) { + if (!item.system.secondary) + await this.document.update({ + 'system.characterGuide.suggestedPrimaryWeapon': item.uuid + }); + } else if (target.classList.contains('secondary-weapon-section')) { + if (item.system.secondary) + await this.document.update({ + 'system.characterGuide.suggestedSecondaryWeapon': item.uuid + }); + } + } else if (item.type === 'armor') { + if (target.classList.contains('armor-section')) { await this.document.update({ - 'system.characterGuide.suggestedPrimaryWeapon': item.uuid - }); - } else if (target.classList.contains('secondary-weapon-section')) { - if (item.system.secondary) - await this.document.update({ - 'system.characterGuide.suggestedSecondaryWeapon': item.uuid - }); - } - } else if (item.type === 'armor') { - if (target.classList.contains('armor-section')) { - await this.document.update({ - 'system.characterGuide.suggestedArmor': item.uuid - }); - } - } else if (target.classList.contains('choice-a-section')) { - if (item.type === 'loot' || item.type === 'consumable') { - const filteredChoiceA = this.document.system.inventory.choiceA; - if (filteredChoiceA.length < 2) - await this.document.update({ - 'system.inventory.choiceA': [...filteredChoiceA.map(x => x.uuid), item.uuid] - }); - } - } else if (item.type === 'loot') { - if (target.classList.contains('take-section')) { - const filteredTake = this.document.system.inventory.take.filter(x => x); - if (filteredTake.length < 3) - await this.document.update({ - 'system.inventory.take': [...filteredTake.map(x => x.uuid), item.uuid] - }); - } else if (target.classList.contains('choice-b-section')) { - const filteredChoiceB = this.document.system.inventory.choiceB.filter(x => x); - if (filteredChoiceB.length < 2) - await this.document.update({ - 'system.inventory.choiceB': [...filteredChoiceB.map(x => x.uuid), item.uuid] + 'system.characterGuide.suggestedArmor': item.uuid }); + } + } else if (target.classList.contains('choice-a-section')) { + if (item.type === 'loot' || item.type === 'consumable') { + const filteredChoiceA = this.document.system.inventory.choiceA; + if (filteredChoiceA.length < 2) + await this.document.update({ + 'system.inventory.choiceA': [...filteredChoiceA.map(x => x.uuid), item.uuid] + }); + } + } else if (item.type === 'loot') { + if (target.classList.contains('take-section')) { + const filteredTake = this.document.system.inventory.take.filter(x => x); + if (filteredTake.length < 3) + await this.document.update({ + 'system.inventory.take': [...filteredTake.map(x => x.uuid), item.uuid] + }); + } else if (target.classList.contains('choice-b-section')) { + const filteredChoiceB = this.document.system.inventory.choiceB.filter(x => x); + if (filteredChoiceB.length < 2) + await this.document.update({ + 'system.inventory.choiceB': [...filteredChoiceB.map(x => x.uuid), item.uuid] + }); + } } } } diff --git a/module/data/actor/adversary.mjs b/module/data/actor/adversary.mjs index 161c3324..5ad855ed 100644 --- a/module/data/actor/adversary.mjs +++ b/module/data/actor/adversary.mjs @@ -87,7 +87,8 @@ export default class DhpAdversary extends BaseDataActor { experiences: new fields.TypedObjectField( new fields.SchemaField({ name: new fields.StringField(), - value: new fields.NumberField({ required: true, integer: true, initial: 1 }) + value: new fields.NumberField({ required: true, integer: true, initial: 1 }), + description: new fields.StringField() }) ), bonuses: new fields.SchemaField({ diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 7fc7c5d7..e0bbcc03 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -4,6 +4,7 @@ import DhLevelData from '../levelData.mjs'; import BaseDataActor from './base.mjs'; import { attributeField, resourceField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs'; import { ActionField } from '../fields/actionField.mjs'; +import DHCharacterSettings from '../../applications/sheets-configs/character-settings.mjs'; export default class DhCharacter extends BaseDataActor { static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Character']; @@ -12,6 +13,7 @@ export default class DhCharacter extends BaseDataActor { return foundry.utils.mergeObject(super.metadata, { label: 'TYPES.Actor.character', type: 'character', + settingSheet: DHCharacterSettings, isNPC: false }); } @@ -22,7 +24,12 @@ export default class DhCharacter extends BaseDataActor { return { ...super.defineSchema(), resources: new fields.SchemaField({ - hitPoints: resourceField(0, 'DAGGERHEART.GENERAL.HitPoints.plural', true), + hitPoints: resourceField( + 0, + 'DAGGERHEART.GENERAL.HitPoints.plural', + true, + 'DAGGERHEART.ACTORS.Character.maxHPBonus' + ), stress: resourceField(6, 'DAGGERHEART.GENERAL.stress', true), hope: resourceField(6, 'DAGGERHEART.GENERAL.hope') }), @@ -56,7 +63,8 @@ export default class DhCharacter extends BaseDataActor { experiences: new fields.TypedObjectField( new fields.SchemaField({ name: new fields.StringField(), - value: new fields.NumberField({ integer: true, initial: 0 }) + value: new fields.NumberField({ integer: true, initial: 0 }), + description: new fields.StringField() }) ), gold: new fields.SchemaField({ @@ -312,7 +320,7 @@ export default class DhCharacter extends BaseDataActor { } get needsCharacterSetup() { - return !this.class.value || !this.class.subclass; + return !(this.class.value || this.class.subclass || this.ancestry || this.community); } get spellcastModifier() { @@ -399,11 +407,16 @@ export default class DhCharacter extends BaseDataActor { } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.subclass.id) { if (this.class.subclass) { const subclassState = this.class.subclass.system.featureState; - const subType = item.system.subType; + const subclass = + item.system.identifier === 'multiclass' ? this.multiclass.subclass : this.class.subclass; + const featureType = subclass + ? (subclass.system.features.find(x => x.item?.uuid === item.uuid)?.type ?? null) + : null; + if ( - subType === CONFIG.DH.ITEM.featureSubTypes.foundation || - (subType === CONFIG.DH.ITEM.featureSubTypes.specialization && subclassState >= 2) || - (subType === CONFIG.DH.ITEM.featureSubTypes.mastery && subclassState >= 3) + featureType === CONFIG.DH.ITEM.featureSubTypes.foundation || + (featureType === CONFIG.DH.ITEM.featureSubTypes.specialization && subclassState >= 2) || + (featureType === CONFIG.DH.ITEM.featureSubTypes.mastery && subclassState >= 3) ) { subclassFeatures.push(item); } @@ -502,7 +515,7 @@ export default class DhCharacter extends BaseDataActor { } prepareBaseData() { - this.evasion = this.class.value?.system?.evasion ?? 0; + this.evasion += this.class.value?.system?.evasion ?? 0; const currentLevel = this.levelData.level.current; const currentTier = @@ -511,37 +524,39 @@ export default class DhCharacter extends BaseDataActor { : Object.values(game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers).find( tier => currentLevel >= tier.levels.start && currentLevel <= tier.levels.end ).tier; - for (let levelKey in this.levelData.levelups) { - const level = this.levelData.levelups[levelKey]; + if (game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto) { + for (let levelKey in this.levelData.levelups) { + const level = this.levelData.levelups[levelKey]; - this.proficiency += level.achievements.proficiency; + this.proficiency += level.achievements.proficiency; - for (let selection of level.selections) { - switch (selection.type) { - case 'trait': - selection.data.forEach(data => { - this.traits[data].value += 1; - this.traits[data].tierMarked = selection.tier === currentTier; - }); - break; - case 'hitPoint': - this.resources.hitPoints.max += selection.value; - break; - case 'stress': - this.resources.stress.max += selection.value; - break; - case 'evasion': - this.evasion += selection.value; - break; - case 'proficiency': - this.proficiency += selection.value; - break; - case 'experience': - Object.keys(this.experiences).forEach(key => { - const experience = this.experiences[key]; - experience.value += selection.value; - }); - break; + for (let selection of level.selections) { + switch (selection.type) { + case 'trait': + selection.data.forEach(data => { + this.traits[data].value += 1; + this.traits[data].tierMarked = selection.tier === currentTier; + }); + break; + case 'hitPoint': + this.resources.hitPoints.max += selection.value; + break; + case 'stress': + this.resources.stress.max += selection.value; + break; + case 'evasion': + this.evasion += selection.value; + break; + case 'proficiency': + this.proficiency += selection.value; + break; + case 'experience': + selection.data.forEach(id => { + const experience = this.experiences[id]; + if (experience) experience.value += selection.value; + }); + break; + } } } } diff --git a/module/data/fields/actorField.mjs b/module/data/fields/actorField.mjs index 5fde9394..b4c7f9b9 100644 --- a/module/data/fields/actorField.mjs +++ b/module/data/fields/actorField.mjs @@ -6,10 +6,15 @@ const attributeField = label => tierMarked: new fields.BooleanField({ initial: false }) }); -const resourceField = (max = 0, label, reverse = false) => +const resourceField = (max = 0, label, reverse = false, maxLabel) => new fields.SchemaField({ value: new fields.NumberField({ initial: 0, min: 0, integer: true, label }), - max: new fields.NumberField({ initial: max, integer: true }), + max: new fields.NumberField({ + initial: max, + integer: true, + label: + maxLabel ?? game.i18n.format('DAGGERHEART.GENERAL.maxWithThing', { thing: game.i18n.localize(label) }) + }), isReversed: new fields.BooleanField({ initial: reverse }) }); diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index 8b9adab4..1f55e878 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -125,6 +125,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { } if (this.actor && this.actor.type === 'character' && this.features) { + const featureUpdates = {}; for (let f of this.features) { const fBase = f.item ?? f; const feature = fBase.system ? fBase : await foundry.utils.fromUuid(fBase.uuid); @@ -134,14 +135,26 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { system: { originItemType: this.parent.type, originId: data._id, - identifier: feature.identifier, - subType: feature.item ? feature.type : undefined + identifier: this.isMulticlass ? 'multiclass' : null } }, { inplace: false } ); - await this.actor.createEmbeddedDocuments('Item', [createData]); + const [doc] = await this.actor.createEmbeddedDocuments('Item', [createData]); + + if (!featureUpdates.features) + featureUpdates.features = this.features.map(x => (x.item ? { ...x, item: x.item.uuid } : x.uuid)); + + if (f.item) { + const existingFeature = featureUpdates.features.find(x => x.item === f.item.uuid); + existingFeature.item = doc.uuid; + } else { + const replaceIndex = featureUpdates.features.findIndex(x => x === f.uuid); + featureUpdates.features.splice(replaceIndex, 1, doc.uuid); + } } + + await this.updateSource(featureUpdates); } } 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/feature.mjs b/module/data/item/feature.mjs index 93a2c0bd..53e4d2d6 100644 --- a/module/data/item/feature.mjs +++ b/module/data/item/feature.mjs @@ -23,7 +23,6 @@ export default class DHFeature extends BaseDataItem { nullable: true, initial: null }), - subType: new fields.StringField({ choices: CONFIG.DH.ITEM.featureSubTypes, nullable: true, initial: null }), originId: new fields.StringField({ nullable: true, initial: null }), identifier: new fields.StringField() }; diff --git a/module/data/item/subclass.mjs b/module/data/item/subclass.mjs index ed6f8b45..11d42e86 100644 --- a/module/data/item/subclass.mjs +++ b/module/data/item/subclass.mjs @@ -41,27 +41,48 @@ export default class DHSubclass 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 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; + } + + if ( + this.actor.system.multiclass.value.system.subclasses.every( + x => x.uuid !== (data.uuid ?? `Item.${data._id}`) + ) + ) { + ui.notifications.error( + game.i18n.localize('DAGGERHEART.UI.Notifications.subclassNotInMulticlass') + ); + 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; + } + if ( + this.actor.system.class.value.system.subclasses.every( + x => x.uuid !== (data.uuid ?? `Item.${data._id}`) + ) + ) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassNotInClass')); + return false; + } } } + + const allowed = await super._preCreate(data, options, user); + if (allowed === false) return; } _onCreate(data, options, userId) { diff --git a/module/data/settings/Automation.mjs b/module/data/settings/Automation.mjs index d4d6a2a7..66e685d0 100644 --- a/module/data/settings/Automation.mjs +++ b/module/data/settings/Automation.mjs @@ -14,6 +14,11 @@ export default class DhAutomation extends foundry.abstract.DataModel { label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.hopeFear.players.label' }) }), + levelupAuto: new fields.BooleanField({ + required: true, + initial: true, + label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.levelupAuto.label' + }), actionPoints: new fields.BooleanField({ required: true, initial: false, diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index d7760637..dad36ec7 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -172,26 +172,30 @@ export default class DhpActor extends Actor { } async levelUp(levelupData) { + const levelupAuto = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto; + 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}`]: { - name: experience.name, - value: experience.modifier - } - }); - - if (this.system.companion) { - await this.system.companion.update({ + if (levelupAuto) { + for (var experienceKey in level.achievements.experiences) { + const experience = level.achievements.experiences[experienceKey]; + await this.update({ [`system.experiences.${experienceKey}`]: { - name: '', + name: experience.name, value: experience.modifier } }); + + if (this.system.companion) { + await this.system.companion.update({ + [`system.experiences.${experienceKey}`]: { + name: '', + value: experience.modifier + } + }); + } } } @@ -250,74 +254,86 @@ export default class DhpActor extends Actor { } for (var addition of featureAdditions) { - for (var featureData of addition.features) { - const feature = new DHFeature({ - ...featureData, - description: game.i18n.localize(featureData.description) - }); - - const document = featureData.toPartner && this.system.partner ? this.system.partner : this; - const embeddedItem = await document.createEmbeddedDocuments('Item', [ - { + if (levelupAuto) { + for (var featureData of addition.features) { + const feature = new DHFeature({ ...featureData, - name: game.i18n.localize(featureData.name), - type: 'feature', - system: feature - } - ]); - const newFeature = { - onPartner: Boolean(featureData.toPartner && this.system.partner), - id: embeddedItem[0].id - }; - addition.checkbox.features = !addition.checkbox.features - ? [newFeature] - : [...addition.checkbox.features, newFeature]; + description: game.i18n.localize(featureData.description) + }); + + const document = featureData.toPartner && this.system.partner ? this.system.partner : this; + const embeddedItem = await document.createEmbeddedDocuments('Item', [ + { + ...featureData, + name: game.i18n.localize(featureData.name), + type: 'feature', + system: feature + } + ]); + const newFeature = { + onPartner: Boolean(featureData.toPartner && this.system.partner), + id: embeddedItem[0].id + }; + addition.checkbox.features = !addition.checkbox.features + ? [newFeature] + : [...addition.checkbox.features, newFeature]; + } } selections.push(addition.checkbox); } 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(); + if (levelupAuto) { + 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 + 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 + await this.createEmbeddedDocuments('Item', [ + { + ...subclassData, + system: { + ...subclassData.system, + isMulticlass: true + } } - } - ]); - selections.push({ ...multiclass, itemUuid: embeddedItem[0].uuid }); + ]); + selections.push({ ...multiclass, itemUuid: embeddedItem[0].uuid }); + } else { + selections.push({ ...multiclass }); + } } 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 }); + if (levelupAuto) { + const item = await foundry.utils.fromUuid(domainCard.data[0]); + const embeddedItem = await this.createEmbeddedDocuments('Item', [item.toObject()]); + selections.push({ ...domainCard, itemUuid: embeddedItem[0].uuid }); + } else { + selections.push({ ...domainCard }); + } } const achievementDomainCards = []; - for (var card of Object.values(level.achievements.domainCards)) { - const item = await foundry.utils.fromUuid(card.uuid); - const embeddedItem = await this.createEmbeddedDocuments('Item', [item.toObject()]); - card.itemUuid = embeddedItem[0].uuid; - achievementDomainCards.push(card); + if (levelupAuto) { + for (var card of Object.values(level.achievements.domainCards)) { + const item = await foundry.utils.fromUuid(card.uuid); + const embeddedItem = await this.createEmbeddedDocuments('Item', [item.toObject()]); + card.itemUuid = embeddedItem[0].uuid; + achievementDomainCards.push(card); + } } if (subclassFeatureState.class) { diff --git a/styles/less/dialog/character-creation/selections-container.less b/styles/less/dialog/character-creation/selections-container.less index 0c13fae9..9f81f93b 100644 --- a/styles/less/dialog/character-creation/selections-container.less +++ b/styles/less/dialog/character-creation/selections-container.less @@ -263,24 +263,36 @@ gap: 8px; .experience-container { - position: relative; display: flex; - align-items: center; + flex-direction: column; + gap: 5px; - .experience-description { - border-color: light-dark(@dark-blue, @golden); - padding-right: 24px; - } - - .experience-value { - position: absolute; - right: 0; - width: 22px; - border-left: 1px solid light-dark(@dark-blue, @golden); - height: 100%; + .experience-inner-container { + position: relative; display: flex; align-items: center; - justify-content: center; + + .experience-description { + border-color: light-dark(@dark-blue, @golden); + padding-right: 24px; + } + + .experience-value { + position: absolute; + right: 0; + width: 22px; + border-left: 1px solid light-dark(@dark-blue, @golden); + height: 100%; + display: flex; + align-items: center; + justify-content: center; + } + } + + textarea { + width: 100%; + resize: none; + border-color: light-dark(@dark-blue, @golden); } } } 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/level-up/selections-container.less b/styles/less/dialog/level-up/selections-container.less index 10d61ed6..552f7c86 100644 --- a/styles/less/dialog/level-up/selections-container.less +++ b/styles/less/dialog/level-up/selections-container.less @@ -104,4 +104,10 @@ } } } + + .levelup-footer { + display: flex; + gap: 8px; + margin-top: 8px; + } } 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/global/elements.less b/styles/less/global/elements.less index 72723f79..5dea5e11 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -5,22 +5,26 @@ border: 1px solid light-dark(@dark-blue, @golden); input[type='text'], - input[type='number'] { + input[type='number'], + textarea { background: light-dark(transparent, transparent); border-radius: 6px; box-shadow: 0 4px 30px @soft-shadow; backdrop-filter: blur(9.5px); -webkit-backdrop-filter: blur(9.5px); - outline: none; + outline: 2px solid transparent; color: light-dark(@dark-blue, @golden); border: 1px solid light-dark(@dark, @beige); + transition: all 0.3s ease; &::placeholder { color: light-dark(@dark-40, @beige-50); } + &:hover, &:hover[type='text'], &:hover[type='number'], + &:focus, &:focus[type='text'], &:focus[type='number'] { background: light-dark(@soft-shadow, @semi-transparent-dark-blue); @@ -70,6 +74,13 @@ } } + textarea { + color: light-dark(@dark, @beige); + font-family: @font-body; + scrollbar-width: thin; + scrollbar-color: light-dark(@dark-blue, @golden) transparent; + } + button { background: light-dark(transparent, @golden); border: 1px solid light-dark(@dark-blue, @dark-blue); @@ -206,6 +217,10 @@ } } + &.inactive { + opacity: 0.5; + } + &.fit-height { height: 95%; } diff --git a/styles/less/sheets-settings/adversary-settings/experiences.less b/styles/less/sheets-settings/adversary-settings/experiences.less index 05595ed4..dab261a6 100644 --- a/styles/less/sheets-settings/adversary-settings/experiences.less +++ b/styles/less/sheets-settings/adversary-settings/experiences.less @@ -12,18 +12,33 @@ .experience-list { display: flex; flex-direction: column; - gap: 10px; + gap: 15px; + } - .experience-item { + .experience-item { + display: flex; + flex-direction: column; + gap: 5px; + + .experience-inner-item { display: grid; grid-template-columns: 3fr 1fr 30px; align-items: center; gap: 5px; + &.no-controls { + grid-template-columns: 3fr 1fr; + } + a { text-align: center; } } + + textarea { + width: 100%; + resize: none; + } } } } diff --git a/styles/less/sheets-settings/character-settings/sheet.less b/styles/less/sheets-settings/character-settings/sheet.less new file mode 100644 index 00000000..b563f2f4 --- /dev/null +++ b/styles/less/sheets-settings/character-settings/sheet.less @@ -0,0 +1,36 @@ +.application.daggerheart.dh-style.dialog { + .tab.details { + .traits-inner-container { + width: 100%; + display: flex; + align-items: center; + justify-content: space-evenly; + gap: 8px; + + .trait-container { + width: 60px; + height: 60px; + background: url(../assets/svg/trait-shield.svg) no-repeat; + display: flex; + flex-direction: column; + align-items: center; + + div { + filter: drop-shadow(0 0 3px black); + text-shadow: 0 0 3px black; + font-family: @font-body; + font-size: 12px; + } + + input { + text-align: center; + width: 32px; + height: 24px; + position: relative; + top: 2px; + padding: 0; + } + } + } + } +} diff --git a/styles/less/sheets-settings/index.less b/styles/less/sheets-settings/index.less index e7818326..f575f848 100644 --- a/styles/less/sheets-settings/index.less +++ b/styles/less/sheets-settings/index.less @@ -2,6 +2,7 @@ @import './adversary-settings/sheet.less'; @import './adversary-settings/experiences.less'; @import './adversary-settings/features.less'; +@import './character-settings/sheet.less'; @import './environment-settings/features.less'; @import './environment-settings/adversaries.less'; diff --git a/styles/less/sheets/actors/character/header.less b/styles/less/sheets/actors/character/header.less index f9511207..d25d4ebf 100644 --- a/styles/less/sheets/actors/character/header.less +++ b/styles/less/sheets/actors/character/header.less @@ -4,12 +4,24 @@ // Theme header backgrounds .appTheme({ - .character-header-sheet .trait { - background: url(../assets/svg/trait-shield.svg) no-repeat; + .character-header-sheet { + .trait { + background: url(../assets/svg/trait-shield.svg) no-repeat; + } + + .character-row .domains-section img { + filter: invert(88%) sepia(98%) saturate(1784%) hue-rotate(311deg) brightness(104%) contrast(91%); + } } }, { - .character-header-sheet .trait { + .character-header-sheet { + .trait { background: url('../assets/svg/trait-shield-light.svg') no-repeat; + } + + .character-row .domains-section img { + filter: invert(87%) sepia(15%) saturate(343%) hue-rotate(333deg) brightness(110%) contrast(87%); + } } }); @@ -88,6 +100,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; @@ -107,14 +124,27 @@ .character-row { display: flex; - gap: 20px; align-items: center; - justify-content: space-between; padding: 0; margin-bottom: 15px; + .hope-section { + margin-right: 20px; + } + + .downtime-section { + display: flex; + align-items: center; + gap: 2px; + margin-left: auto; + + button { + flex: 1; + } + } + .hope-section, - .threshold-section { + .domains-section { position: relative; display: flex; gap: 10px; @@ -132,21 +162,24 @@ font-weight: bold; text-transform: uppercase; color: light-dark(@dark-blue, @golden); - - &.threshold-value { - color: light-dark(@dark, @beige); - } } - .threshold-legend { - position: absolute; - bottom: -21px; - color: light-dark(@golden, @dark-blue); - background-color: light-dark(@dark-blue, @golden); - padding: 3px; - justify-self: anchor-center; - border-radius: 0 0 3px 3px; - text-transform: capitalize; + .domain { + display: flex; + align-items: center; + gap: 5px; + + .label { + font-size: 14px; + font-weight: bold; + text-transform: uppercase; + color: light-dark(@dark-blue, @golden); + } + + img { + height: 20px; + width: 20px; + } } .hope-value { @@ -193,14 +226,5 @@ } } } - - .character-downtime-container { - display: flex; - gap: 2px; - - button { - flex: 1; - } - } } } diff --git a/styles/less/sheets/actors/character/sheet.less b/styles/less/sheets/actors/character/sheet.less index 60fc6835..421b102f 100644 --- a/styles/less/sheets/actors/character/sheet.less +++ b/styles/less/sheets/actors/character/sheet.less @@ -18,6 +18,7 @@ } .character-header-sheet { + position: relative; grid-row: 1; grid-column: 2; } diff --git a/styles/less/sheets/actors/character/sidebar.less b/styles/less/sheets/actors/character/sidebar.less index 7cfeffac..61f8cc70 100644 --- a/styles/less/sheets/actors/character/sidebar.less +++ b/styles/less/sheets/actors/character/sidebar.less @@ -55,6 +55,62 @@ } } } + + .icons-list { + position: absolute; + display: flex; + flex-direction: column; + gap: 5px; + align-items: end; + justify-content: center; + top: 45px; + right: 10px; + + .spellcast-icon { + display: flex; + align-items: center; + justify-content: end; + text-align: center; + padding-right: 8px; + max-width: 50px; + height: 50px; + font-size: 1.2rem; + background: light-dark(@dark-blue-60, @dark-golden-80); + backdrop-filter: blur(8px); + border: 4px double light-dark(@beige, @golden); + color: light-dark(@beige, @golden); + border-radius: 999px; + transition: all 0.3s ease; + + .spellcast-label { + font-size: 14px; + opacity: 0; + margin-right: 0.3rem; + transition: all 0.3s ease; + } + + i { + height: 24px; + width: 24px; + align-content: center; + margin-right: 3px; + } + + &:not(.no-label):hover { + max-width: 300px; + padding: 0 10px; + border-radius: 60px; + + .spellcast-label { + opacity: 1; + } + + i { + margin-right: 0px; + } + } + } + } } .info-section { @@ -62,12 +118,13 @@ display: flex; flex-direction: column; top: -20px; - gap: 30px; + gap: 10px; margin-bottom: -16px; .resources-section { display: flex; justify-content: space-evenly; + margin-bottom: 20px; .status-bar { display: flex; @@ -345,6 +402,32 @@ } } } + + .threshold-section { + position: relative; + display: flex; + align-self: center; + gap: 10px; + background-color: light-dark(transparent, @dark-blue); + color: light-dark(@dark-blue, @golden); + padding: 5px 10px; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + align-items: center; + width: fit-content; + height: 30px; + + h4 { + font-size: 14px; + font-weight: bold; + text-transform: uppercase; + color: light-dark(@dark-blue, @golden); + + &.threshold-value { + color: light-dark(@dark, @beige); + } + } + } } .items-sidebar-list { @@ -360,7 +443,7 @@ .shortcut-items-section { overflow-y: hidden; max-height: 56%; - padding-top: 16px; + padding-top: 10px; padding-bottom: 20px; mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%); @@ -393,35 +476,24 @@ .experience-row { display: flex; - gap: 5px; width: 250px; align-items: center; justify-content: space-between; - input[type='text'] { - height: 32px; - width: 180px; - border: 1px solid transparent; - outline: 2px solid transparent; + .experience-value { + height: 25px; + width: 35px; font-size: 14px; font-family: @font-body; - transition: all 0.3s ease; color: light-dark(@dark, @beige); - - &:hover { - outline: 2px solid light-dark(@dark, @beige); - } + align-content: center; + text-align: center; + margin-right: 5px; } - } - .experience-value { - height: 25px; - width: 35px; - font-size: 14px; - font-family: @font-body; - color: light-dark(@dark, @beige); - align-content: center; - text-align: center; + .controls { + margin-left: auto; + } } } } diff --git a/templates/characterCreation/setupTabs/experience.hbs b/templates/characterCreation/setupTabs/experience.hbs index 46c5b754..4b70b1b9 100644 --- a/templates/characterCreation/setupTabs/experience.hbs +++ b/templates/characterCreation/setupTabs/experience.hbs @@ -9,8 +9,12 @@
{{#each experience.values as |experience id|}}
- -
{{numberFormat this.value sign=true}}
+
+ +
{{numberFormat this.value sign=true}}
+
+ +
{{/each}}
diff --git a/templates/dialogs/multiclassChoice.hbs b/templates/dialogs/multiclassChoice.hbs new file mode 100644 index 00000000..3c89ff1a --- /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/levelup/tabs/footer.hbs b/templates/levelup/tabs/footer.hbs new file mode 100644 index 00000000..85c49a91 --- /dev/null +++ b/templates/levelup/tabs/footer.hbs @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/templates/levelup/tabs/tab-navigation.hbs b/templates/levelup/tabs/tab-navigation.hbs index 82aacf1e..990e909d 100644 --- a/templates/levelup/tabs/tab-navigation.hbs +++ b/templates/levelup/tabs/tab-navigation.hbs @@ -1,40 +1,44 @@
- -
- {{#if this.showTabs}} - - {{/if}} -
- {{#if this.navigate.previous.fromSummary}} - - {{else}} - {{#if (not this.navigate.previous.disabled)}} - - {{/if}} + {{#if levelupAuto}} + +
+ {{#if this.showTabs}} + {{/if}} - {{#if this.navigate.next.show}} - {{#if this.navigate.next.toSummary}} - - {{else}} - - {{/if}} - {{else}} -
+ {{#if this.levelupAuto}} +
+ {{#if this.navigate.previous.fromSummary}} + + {{else}} + {{#if (not this.navigate.previous.disabled)}} + + {{/if}} + {{/if}} + {{#if this.navigate.next.show}} + {{#if this.navigate.next.toSummary}} + + {{else}} + + {{/if}} + {{else}} +
+ {{/if}} +
{{/if}}
-
- + + {{/if}}
\ No newline at end of file diff --git a/templates/settings/automation-settings.hbs b/templates/settings/automation-settings.hbs index a02d6f97..efc25221 100644 --- a/templates/settings/automation-settings.hbs +++ b/templates/settings/automation-settings.hbs @@ -11,6 +11,7 @@ {{formGroup settingFields.schema.fields.actionPoints value=settingFields._source.actionPoints localize=true}} {{formGroup settingFields.schema.fields.hordeDamage value=settingFields._source.hordeDamage localize=true}} {{formGroup settingFields.schema.fields.effects.fields.rangeDependent value=settingFields._source.effects.rangeDependent localize=true}} + {{formGroup settingFields.schema.fields.levelupAuto value=settingFields._source.levelupAuto localize=true}}