From eefa116d9a15eab0b1ac84fa8d2b80818b76b351 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Mon, 14 Jul 2025 00:48:59 +0200 Subject: [PATCH 1/5] 321 - ChatLog Popout (#331) * Fixed chatlog popout * PR Fix --- module/applications/ui/chatLog.mjs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index e8f699e4..f295d2a6 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -1,6 +1,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog { - constructor() { - super(); + constructor(options) { + super(options); this.targetTemplate = { activeLayer: undefined, @@ -14,6 +14,8 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo } addChatListeners = async (app, html, data) => { + super.addChatListeners(app, html, data); + html.querySelectorAll('.duality-action-damage').forEach(element => element.addEventListener('click', event => this.onRollDamage(event, data.message)) ); From 4be3e6179c91019fd3078e07e8e602d2d154cef2 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Mon, 14 Jul 2025 01:12:32 +0200 Subject: [PATCH 2/5] [Feature] Item Resource Support (#328) * Initial * Resource setup finished * Fixed so that costs can be used * Corrected standard resources * Actions can only use item resources from their parent item * Fixed up dice * Fixed resource dice positioning * Fixed parsing of resource.max * Fixed styling on settings tab * Added manual input for Dice Resources * Lightmode fixes * Fixed Feature spellcasting modifier * Bugfix for item input to resourceDiceDialog * Item fix for TokenInput * PR Fixes --- lang/en.json | 46 +++- module/applications/dialogs/_module.mjs | 1 + module/applications/dialogs/d20RollDialog.mjs | 9 +- .../dialogs/resourceDiceDialog.mjs | 99 ++++++++ .../sheets-configs/action-config.mjs | 30 ++- .../applications/sheets/actors/character.mjs | 78 +++++- module/applications/sheets/api/base-item.mjs | 24 +- .../sheets/api/item-attachment-sheet.mjs | 23 +- module/applications/sheets/items/armor.mjs | 2 +- module/applications/sheets/items/class.mjs | 12 +- module/applications/sheets/items/feature.mjs | 4 +- module/applications/sheets/items/weapon.mjs | 2 +- module/config/flagsConfig.mjs | 2 +- module/config/generalConfig.mjs | 25 -- module/config/itemConfig.mjs | 11 + module/data/action/actionDice.mjs | 2 +- module/data/action/baseAction.mjs | 63 +++-- module/data/action/damageAction.mjs | 6 +- module/data/actor/base.mjs | 9 +- module/data/actor/character.mjs | 8 +- module/data/item/armor.mjs | 1 - module/data/item/attachableItem.mjs | 20 +- module/data/item/base.mjs | 67 ++++-- module/data/item/domainCard.mjs | 3 +- module/data/item/feature.mjs | 27 ++- module/data/item/weapon.mjs | 3 +- module/dice/dhRoll.mjs | 2 +- module/documents/activeEffect.mjs | 22 +- module/documents/actor.mjs | 101 +++++--- module/helpers/handlebarsHelper.mjs | 11 +- module/helpers/utils.mjs | 20 +- module/systemRegistration/handlebars.mjs | 2 + styles/less/dialog/index.less | 2 + styles/less/dialog/resource-dice/sheet.less | 58 +++++ styles/less/global/elements.less | 6 +- styles/less/global/index.less | 1 + styles/less/global/inventory-item.less | 118 ++++++++- styles/less/global/tab-attachments.less | 2 +- styles/less/global/tab-settings.less | 8 + styles/less/ui/chat/chat.less | 8 + templates/actionTypes/cost.hbs | 2 +- templates/dialogs/dice-roll/costSelection.hbs | 2 +- templates/dialogs/dice-roll/resourceDice.hbs | 16 ++ .../action-settings/configuration.hbs | 2 +- .../global/partials/domain-card-item.hbs | 48 ++-- .../sheets/global/partials/inventory-item.hbs | 225 +++++++++--------- .../sheets/global/partials/item-resource.hbs | 21 ++ .../global/partials/resource-section.hbs | 29 +++ templates/sheets/items/class/features.hbs | 4 +- .../sheets/items/domainCard/settings.hbs | 2 + templates/sheets/items/feature/settings.hbs | 7 + templates/ui/chat/resource-roll.hbs | 3 + templates/ui/tooltip/parts/tooltipTags.hbs | 2 +- 53 files changed, 972 insertions(+), 329 deletions(-) create mode 100644 module/applications/dialogs/resourceDiceDialog.mjs create mode 100644 styles/less/dialog/resource-dice/sheet.less create mode 100644 styles/less/global/tab-settings.less create mode 100644 templates/dialogs/dice-roll/resourceDice.hbs create mode 100644 templates/sheets/global/partials/item-resource.hbs create mode 100644 templates/sheets/global/partials/resource-section.hbs create mode 100644 templates/sheets/items/feature/settings.hbs create mode 100644 templates/ui/chat/resource-roll.hbs diff --git a/lang/en.json b/lang/en.json index bb67b6c2..c0dc1c3c 100755 --- a/lang/en.json +++ b/lang/en.json @@ -387,6 +387,10 @@ "OwnershipSelection": { "title": "Ownership Selection - {name}", "default": "Default Ownership" + }, + "ResourceDice": { + "title": "{name} Resource", + "rerollDice": "Reroll Dice" } }, @@ -632,6 +636,10 @@ "abbreviation": "AS" } }, + "ItemResourceType": { + "simple": "Simple", + "diceValue": "Dice Value" + }, "Range": { "self": { "name": "Self", @@ -1068,6 +1076,10 @@ "shortrest": "Short Rest", "longrest": "Long Rest" }, + "Resource": { + "single": "Resource", + "plural": "Resources" + }, "Tabs": { "details": "Details", "attack": "Attack", @@ -1154,6 +1166,17 @@ "value": "Value" }, "ITEMS": { + "FIELDS": { + "resource": { + "amount": { "label": "Amount" }, + "dieFaces": { "label": "Die Faces" }, + "icon": { "label": "Icon" }, + "max": { "label": "Max" }, + "recovery": { "label": "Recovery" }, + "type": { "label": "Type" }, + "value": { "label": "Value" } + } + }, "Armor": { "baseScore": "Base Score", "baseThresholds": { @@ -1331,8 +1354,8 @@ }, "UI": { "Chat": { - "dualityRoll": { - "abilityCheckTitle": "{ability} Check" + "applyEffect": { + "title": "Apply Effects - {name}" }, "attackRoll": { "title": "Attack - {attack}", @@ -1348,25 +1371,28 @@ "hitTarget": "Hit Targets", "selectedTarget": "Selected" }, - "applyEffect": { - "title": "Apply Effects - {name}" - }, - "healingRoll": { - "title": "Heal - {healing}", - "heal": "Heal" - }, "deathMove": { "title": "Death Move" }, "domainCard": { "title": "Domain Card" }, + "dualityRoll": { + "abilityCheckTitle": "{ability} Check" + }, + "featureTitle": "Class Feature", "foundationCard": { "ancestryTitle": "Ancestry Card", "communityTitle": "Community Card", "subclassFeatureTitle": "Subclass Feature" }, - "featureTitle": "Class Feature" + "healingRoll": { + "title": "Heal - {healing}", + "heal": "Heal" + }, + "resourceRoll": { + "playerMessage": "{user} rerolled their {name}" + } }, "Notifications": { "adversaryMissing": "The linked adversary doesn't exist in the world.", diff --git a/module/applications/dialogs/_module.mjs b/module/applications/dialogs/_module.mjs index 82c6f17d..0722c747 100644 --- a/module/applications/dialogs/_module.mjs +++ b/module/applications/dialogs/_module.mjs @@ -7,3 +7,4 @@ export { default as DamageSelectionDialog } from './damageSelectionDialog.mjs'; export { default as DeathMove } from './deathMove.mjs'; export { default as Downtime } from './downtime.mjs'; export { default as OwnershipSelection } from './ownershipSelection.mjs'; +export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs'; diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index d1196459..6cb0761c 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -66,7 +66,12 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio context.canRoll = true; if (this.config.costs?.length) { const updatedCosts = this.action.calcCosts(this.config.costs); - context.costs = updatedCosts; + context.costs = updatedCosts.map(x => ({ + ...x, + label: x.keyIsID + ? this.action.parent.parent.name + : game.i18n.localize(CONFIG.DH.GENERAL.abilityCosts[x.key].label) + })); context.canRoll = this.action.hasCost(updatedCosts); this.config.data.scale = this.config.costs[0].total; } @@ -74,7 +79,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio context.uses = this.action.calcUses(this.config.uses); context.canRoll = context.canRoll && this.action.hasUses(context.uses); } - if(this.roll) { + if (this.roll) { context.roll = this.roll; context.rollType = this.roll?.constructor.name; context.experiences = Object.keys(this.config.data.experiences).map(id => ({ diff --git a/module/applications/dialogs/resourceDiceDialog.mjs b/module/applications/dialogs/resourceDiceDialog.mjs new file mode 100644 index 00000000..b79ff895 --- /dev/null +++ b/module/applications/dialogs/resourceDiceDialog.mjs @@ -0,0 +1,99 @@ +import { itemAbleRollParse } from '../../helpers/utils.mjs'; + +const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; + +export default class ResourceDiceDialog extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(item, actor, options = {}) { + super(options); + + this.item = item; + this.actor = actor; + this.diceStates = foundry.utils.deepClone(item.system.resource.diceStates); + } + + static DEFAULT_OPTIONS = { + tag: 'form', + classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'resource-dice'], + window: { + icon: 'fa-solid fa-dice' + }, + actions: { + rerollDice: this.rerollDice, + save: this.save + }, + form: { + handler: this.updateResourceDice, + submitOnChange: true, + submitOnClose: false + } + }; + + /** @override */ + static PARTS = { + resourceDice: { + id: 'resourceDice', + template: 'systems/daggerheart/templates/dialogs/dice-roll/resourceDice.hbs' + } + }; + + get title() { + return game.i18n.format('DAGGERHEART.APPLICATIONS.ResourceDice.title', { name: this.item.name }); + } + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.item = this.item; + context.actor = this.actor; + context.diceStates = this.diceStates; + + return context; + } + + static async updateResourceDice(event, _, formData) { + const { diceStates } = foundry.utils.expandObject(formData.object); + this.diceStates = Object.keys(diceStates).reduce((acc, key) => { + const resourceState = this.item.system.resource.diceStates[key]; + acc[key] = { ...diceStates[key], used: Boolean(resourceState?.used) }; + return acc; + }, {}); + + this.render(); + } + + static async save() { + this.rollValues = Object.values(this.diceStates); + this.close(); + } + + static async rerollDice() { + const max = itemAbleRollParse(this.item.system.resource.max, this.actor, this.item); + const diceFormula = `${max}d${this.item.system.resource.dieFaces}`; + const roll = await new Roll(diceFormula).evaluate(); + if (game.modules.get('dice-so-nice')?.active) await game.dice3d.showForRoll(roll, game.user, true); + this.rollValues = roll.terms[0].results.map(x => ({ value: x.result, used: false })); + this.resetUsed = true; + + const cls = getDocumentClass('ChatMessage'); + const msg = new cls({ + user: game.user.id, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/ui/chat/resource-roll.hbs', + { + user: this.actor.name, + name: this.item.name + } + ) + }); + + cls.create(msg.toObject()); + this.close(); + } + + static async create(item, actor, options = {}) { + return new Promise(resolve => { + const app = new this(item, actor, options); + app.addEventListener('close', () => resolve(app.rollValues), { once: true }); + app.render({ force: true }); + }); + } +} diff --git a/module/applications/sheets-configs/action-config.mjs b/module/applications/sheets-configs/action-config.mjs index 8823fa07..e1a62e0d 100644 --- a/module/applications/sheets-configs/action-config.mjs +++ b/module/applications/sheets-configs/action-config.mjs @@ -107,6 +107,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { context.hasBaseDamage = !!this.action.parent.attack; context.getRealIndex = this.getRealIndex.bind(this); context.getEffectDetails = this.getEffectDetails.bind(this); + context.costOptions = this.getCostOptions(); context.disableOption = this.disableOption.bind(this); context.isNPC = this.action.actor && this.action.actor.type !== 'character'; context.hasRoll = this.action.hasRoll; @@ -125,8 +126,21 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { this.render(true); } - disableOption(index, options, choices) { - const filtered = foundry.utils.deepClone(options); + getCostOptions() { + const options = foundry.utils.deepClone(CONFIG.DH.GENERAL.abilityCosts); + const resource = this.action.parent.resource; + if (resource) { + options[this.action.parent.parent.id] = { + label: this.action.parent.parent.name, + group: 'TYPES.Actor.character' + }; + } + + return options; + } + + disableOption(index, costOptions, choices) { + const filtered = foundry.utils.deepClone(costOptions); Object.keys(filtered).forEach(o => { if (choices.find((c, idx) => c.type === o && index !== idx)) filtered[o].disabled = true; }); @@ -142,11 +156,19 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { return this.action.item.effects.get(id); } - _prepareSubmitData(event, formData) { + _prepareSubmitData(_event, formData) { const submitData = foundry.utils.expandObject(formData.object); for (const keyPath of this.constructor.CLEAN_ARRAYS) { const data = foundry.utils.getProperty(submitData, keyPath); - if (data) foundry.utils.setProperty(submitData, keyPath, Object.values(data)); + const dataValues = data ? Object.values(data) : []; + if (keyPath === 'cost') { + for (var value of dataValues) { + const item = this.action.parent.parent.id === value.key; + value.keyIsID = Boolean(item); + } + } + + if (data) foundry.utils.setProperty(submitData, keyPath, dataValues); } return submitData; } diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 8a18dd22..eae7a223 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -4,6 +4,7 @@ import { abilities } from '../../../config/actorConfig.mjs'; import DhCharacterlevelUp from '../../levelup/characterLevelup.mjs'; import DhCharacterCreation from '../../characterCreation/characterCreation.mjs'; import FilterMenu from '../../ux/filter-menu.mjs'; +import { itemAbleRollParse } from '../../../helpers/utils.mjs'; /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ @@ -25,6 +26,8 @@ export default class CharacterSheet extends DHBaseActorSheet { toggleEquipItem: CharacterSheet.#toggleEquipItem, useItem: this.useItem, //TODO Fix this useAction: this.useAction, + toggleResourceDice: this.toggleResourceDice, + handleResourceDice: this.handleResourceDice, toChat: this.toChat }, window: { @@ -91,6 +94,17 @@ export default class CharacterSheet extends DHBaseActorSheet { } }; + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + + htmlElement.querySelectorAll('.inventory-item-resource').forEach(element => { + element.addEventListener('change', this.updateItemResource.bind(this)); + }); + htmlElement.querySelectorAll('.inventory-item-quantity').forEach(element => { + element.addEventListener('change', this.updateItemQuantity.bind(this)); + }); + } + /** @inheritDoc */ async _onRender(context, options) { await super._onRender(context, options); @@ -480,6 +494,27 @@ export default class CharacterSheet extends DHBaseActorSheet { } } + /* -------------------------------------------- */ + /* Application Listener Actions */ + /* -------------------------------------------- */ + async updateItemResource(event) { + const item = this.getItem(event.currentTarget); + if (!item) return; + + const max = item.system.resource.max ? itemAbleRollParse(item.system.resource.max, this.document, item) : null; + const value = max ? Math.min(Number(event.currentTarget.value), max) : event.currentTarget.value; + await item.update({ 'system.resource.value': value }); + this.render(); + } + + async updateItemQuantity(event) { + const item = this.getItem(event.currentTarget); + if (!item) return; + + await item.update({ 'system.quantity': event.currentTarget.value }); + this.render(); + } + /* -------------------------------------------- */ /* Application Clicks Actions */ /* -------------------------------------------- */ @@ -640,6 +675,41 @@ export default class CharacterSheet extends DHBaseActorSheet { action.use(event); } + /** + * Toggle the used state of a resource dice. + * @type {ApplicationClickAction} + */ + static async toggleResourceDice(event) { + const target = event.target.closest('.item-resource'); + const item = this.getItem(event); + if (!item) return; + + const diceState = item.system.resource.diceStates[target.dataset.dice]; + await item.update({ + [`system.resource.diceStates.${target.dataset.dice}.used`]: diceState?.used ? !diceState.used : true + }); + } + + /** + * Handle the roll values of resource dice. + * @type {ApplicationClickAction} + */ + static async handleResourceDice(event) { + const item = this.getItem(event); + if (!item) return; + + const rollValues = await game.system.api.applications.dialogs.ResourceDiceDialog.create(item, this.document); + if (!rollValues) return; + + await item.update({ + 'system.resource.diceStates': rollValues.reduce((acc, state, index) => { + acc[index] = { value: state.value, used: state.used }; + return acc; + }, {}) + }); + this.render(); + } + /** * Send item to Chat * @type {ApplicationClickAction} @@ -672,14 +742,14 @@ export default class CharacterSheet extends DHBaseActorSheet { async _onDragStart(event) { const item = this.getItem(event); - + const dragData = { type: item.documentName, uuid: item.uuid }; - + event.dataTransfer.setData('text/plain', JSON.stringify(dragData)); - + super._onDragStart(event); } @@ -687,7 +757,7 @@ export default class CharacterSheet extends DHBaseActorSheet { // Prevent event bubbling to avoid duplicate handling event.preventDefault(); event.stopPropagation(); - + super._onDrop(event); this._onDropItem(event, TextEditor.getDragEventData(event)); } diff --git a/module/applications/sheets/api/base-item.mjs b/module/applications/sheets/api/base-item.mjs index d624acc6..2847bfc8 100644 --- a/module/applications/sheets/api/base-item.mjs +++ b/module/applications/sheets/api/base-item.mjs @@ -24,7 +24,9 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { removeAction: DHBaseItemSheet.#removeAction, addFeature: DHBaseItemSheet.#addFeature, editFeature: DHBaseItemSheet.#editFeature, - removeFeature: DHBaseItemSheet.#removeFeature + removeFeature: DHBaseItemSheet.#removeFeature, + addResource: DHBaseItemSheet.#addResource, + removeResource: DHBaseItemSheet.#removeResource }, dragDrop: [ { dragSelector: null, dropSelector: '.tab.features .drop-section' }, @@ -215,6 +217,26 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { }); } + /** + * Add a resource to the item. + * @type {ApplicationClickAction} + */ + static async #addResource() { + await this.document.update({ + 'system.resource': { type: 'simple', value: 0 } + }); + } + + /** + * Remove the resource from the item. + * @type {ApplicationClickAction} + */ + static async #removeResource() { + await this.document.update({ + 'system.resource': null + }); + } + /* -------------------------------------------- */ /* Application Drag/Drop */ /* -------------------------------------------- */ diff --git a/module/applications/sheets/api/item-attachment-sheet.mjs b/module/applications/sheets/api/item-attachment-sheet.mjs index e89c7cba..e9c3994c 100644 --- a/module/applications/sheets/api/item-attachment-sheet.mjs +++ b/module/applications/sheets/api/item-attachment-sheet.mjs @@ -1,4 +1,3 @@ - export default function ItemAttachmentSheet(Base) { return class extends Base { static DEFAULT_OPTIONS = { @@ -25,10 +24,7 @@ export default function ItemAttachmentSheet(Base) { ...super.TABS, primary: { ...super.TABS?.primary, - tabs: [ - ...(super.TABS?.primary?.tabs || []), - { id: 'attachments' } - ], + tabs: [...(super.TABS?.primary?.tabs || []), { id: 'attachments' }], initial: super.TABS?.primary?.initial || 'description', labelPrefix: super.TABS?.primary?.labelPrefix || 'DAGGERHEART.GENERAL.Tabs' } @@ -46,29 +42,28 @@ export default function ItemAttachmentSheet(Base) { async _onDrop(event) { const data = TextEditor.getDragEventData(event); - + const attachmentsSection = event.target.closest('.attachments-section'); if (!attachmentsSection) return super._onDrop(event); - + event.preventDefault(); event.stopPropagation(); - + const item = await Item.implementation.fromDropData(data); if (!item) return; - + // Call the data model's public method await this.document.system.addAttachment(item); } - static async #removeAttachment(event, target) { // Call the data model's public method await this.document.system.removeAttachment(target.dataset.uuid); -} + } async _preparePartContext(partId, context) { await super._preparePartContext(partId, context); - + if (partId === 'attachments') { // Keep this simple UI preparation in the mixin const attachedUUIDs = this.document.system.attached; @@ -83,8 +78,8 @@ export default function ItemAttachmentSheet(Base) { }) ); } - + return context; } }; -} \ No newline at end of file +} diff --git a/module/applications/sheets/items/armor.mjs b/module/applications/sheets/items/armor.mjs index a6413cf0..f3a38068 100644 --- a/module/applications/sheets/items/armor.mjs +++ b/module/applications/sheets/items/armor.mjs @@ -27,7 +27,7 @@ export default class ArmorSheet extends ItemAttachmentSheet(DHBaseItemSheet) { template: 'systems/daggerheart/templates/sheets/items/armor/settings.hbs', scrollable: ['.settings'] }, - ...super.PARTS, + ...super.PARTS }; /**@inheritdoc */ diff --git a/module/applications/sheets/items/class.mjs b/module/applications/sheets/items/class.mjs index c7b84340..55295455 100644 --- a/module/applications/sheets/items/class.mjs +++ b/module/applications/sheets/items/class.mjs @@ -85,6 +85,16 @@ export default class ClassSheet extends DHBaseItemSheet { await this.document.update({ 'system.subclasses': [...this.document.system.subclasses.map(x => x.uuid), item.uuid] }); + } else if (item.type === 'feature') { + if (target.classList.contains('hope-feature')) { + await this.document.update({ + 'system.hopeFeatures': [...this.document.system.hopeFeatures.map(x => x.uuid), item.uuid] + }); + } else if (target.classList.contains('class-feature')) { + await this.document.update({ + 'system.classFeatures': [...this.document.system.classFeatures.map(x => x.uuid), item.uuid] + }); + } } else if (item.type === 'weapon') { if (target.classList.contains('primary-weapon-section')) { if (!this.document.system.characterGuide.suggestedPrimaryWeapon && !item.system.secondary) @@ -144,7 +154,7 @@ export default class ClassSheet extends DHBaseItemSheet { static async #removeItemFromCollection(_event, element) { const { uuid, target } = element.dataset; const prop = foundry.utils.getProperty(this.document.system, target); - await this.document.update({ [target]: prop.filter(i => i.uuid !== uuid) }); + await this.document.update({ [`system.${target}`]: prop.filter(i => i.uuid !== uuid) }); } /** diff --git a/module/applications/sheets/items/feature.mjs b/module/applications/sheets/items/feature.mjs index 3d13d7de..ca375fc7 100644 --- a/module/applications/sheets/items/feature.mjs +++ b/module/applications/sheets/items/feature.mjs @@ -21,6 +21,7 @@ export default class FeatureSheet extends DHBaseItemSheet { header: { template: 'systems/daggerheart/templates/sheets/items/feature/header.hbs' }, tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' }, + settings: { template: 'systems/daggerheart/templates/sheets/items/feature/settings.hbs' }, actions: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-actions.hbs', scrollable: ['.actions'] @@ -34,7 +35,7 @@ export default class FeatureSheet extends DHBaseItemSheet { /**@override */ static TABS = { primary: { - tabs: [{ id: 'description' }, { id: 'actions' }, { id: 'effects' }], + tabs: [{ id: 'description' }, { id: 'settings' }, { id: 'actions' }, { id: 'effects' }], initial: 'description', labelPrefix: 'DAGGERHEART.GENERAL.Tabs' } @@ -67,7 +68,6 @@ export default class FeatureSheet extends DHBaseItemSheet { } ), title = game.i18n.localize('DAGGERHEART.CONFIG.SelectAction.selectType'); - console.log(this.document); return foundry.applications.api.DialogV2.prompt({ window: { title }, diff --git a/module/applications/sheets/items/weapon.mjs b/module/applications/sheets/items/weapon.mjs index e89c3dce..c601f926 100644 --- a/module/applications/sheets/items/weapon.mjs +++ b/module/applications/sheets/items/weapon.mjs @@ -27,7 +27,7 @@ export default class WeaponSheet extends ItemAttachmentSheet(DHBaseItemSheet) { template: 'systems/daggerheart/templates/sheets/items/weapon/settings.hbs', scrollable: ['.settings'] }, - ...super.PARTS, + ...super.PARTS }; /**@inheritdoc */ diff --git a/module/config/flagsConfig.mjs b/module/config/flagsConfig.mjs index 0c112231..64a36fdf 100644 --- a/module/config/flagsConfig.mjs +++ b/module/config/flagsConfig.mjs @@ -8,4 +8,4 @@ export const encounterCountdown = { position: 'countdown-encounter-position' }; -export const itemAttachmentSource = 'attachmentSource'; \ No newline at end of file +export const itemAttachmentSource = 'attachmentSource'; diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 94efec1b..4216f631 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -366,31 +366,6 @@ export const abilityCosts = { label: 'Armor Stack', group: 'TYPES.Actor.character' }, - prayer: { - id: 'prayer', - label: 'Prayer Dice', - group: 'TYPES.Actor.character' - }, - favor: { - id: 'favor', - label: 'Favor Points', - group: 'TYPES.Actor.character' - }, - slayer: { - id: 'slayer', - label: 'Slayer Dice', - group: 'TYPES.Actor.character' - }, - tide: { - id: 'tide', - label: 'Tide', - group: 'TYPES.Actor.character' - }, - chaos: { - id: 'chaos', - label: 'Chaos', - group: 'TYPES.Actor.character' - }, fear: { id: 'fear', label: 'Fear', diff --git a/module/config/itemConfig.mjs b/module/config/itemConfig.mjs index 1d6b96b8..c63a0008 100644 --- a/module/config/itemConfig.mjs +++ b/module/config/itemConfig.mjs @@ -1334,3 +1334,14 @@ export const actionTypes = { label: 'DAGGERHEART.CONFIG.ActionType.reaction' } }; + +export const itemResourceTypes = { + simple: { + id: 'simple', + label: 'DAGGERHEART.CONFIG.ItemResourceType.simple' + }, + diceValue: { + id: 'diceValue', + label: 'DAGGERHEART.CONFIG.ItemResourceType.diceValue' + } +}; diff --git a/module/data/action/actionDice.mjs b/module/data/action/actionDice.mjs index 5cc2d10a..d71b390a 100644 --- a/module/data/action/actionDice.mjs +++ b/module/data/action/actionDice.mjs @@ -107,7 +107,7 @@ export class DHDamageData extends foundry.abstract.DataModel { }), { label: 'Type', - initial: 'physical', + initial: 'physical' } ), resultBased: new fields.BooleanField({ diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index ae70bf21..6fdadfe2 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -1,4 +1,4 @@ -import { DHActionDiceData, DHActionRollData, DHDamageData, DHDamageField } from './actionDice.mjs'; +import { DHActionDiceData, DHActionRollData, DHDamageField } from './actionDice.mjs'; import DhpActor from '../../documents/actor.mjs'; import D20RollDialog from '../../applications/dialogs/d20RollDialog.mjs'; @@ -35,12 +35,12 @@ export default class DHBaseAction extends foundry.abstract.DataModel { }), cost: new fields.ArrayField( new fields.SchemaField({ - type: new fields.StringField({ - choices: CONFIG.DH.GENERAL.abilityCosts, + key: new fields.StringField({ nullable: false, required: true, initial: 'hope' }), + keyIsID: new fields.BooleanField(), value: new fields.NumberField({ nullable: true, initial: 1 }), scalable: new fields.BooleanField({ initial: false }), step: new fields.NumberField({ nullable: true, initial: null }) @@ -181,7 +181,7 @@ export default class DHBaseAction extends foundry.abstract.DataModel { // Add Roll results to RollDatas actorData.result = data.roll?.total ?? 1; - + actorData.scale = data.costs?.length // Right now only return the first scalable cost. ? (data.costs.find(c => c.scalable)?.total ?? 1) : 1; @@ -204,7 +204,7 @@ export default class DHBaseAction extends foundry.abstract.DataModel { // Prepare Costs const costsConfig = this.prepareCost(); - if (isFastForward && !this.hasCost(costsConfig)) + if (isFastForward && !(await this.hasCost(costsConfig))) return ui.notifications.warn("You don't have the resources to use that action."); // Prepare Uses @@ -278,7 +278,7 @@ export default class DHBaseAction extends foundry.abstract.DataModel { prepareCost() { const costs = this.cost?.length ? foundry.utils.deepClone(this.cost) : []; - return costs; + return this.calcCosts(costs); } prepareUse() { @@ -327,11 +327,26 @@ export default class DHBaseAction extends foundry.abstract.DataModel { } async consume(config) { + const usefulResources = foundry.utils.deepClone(this.actor.system.resources); + for (var cost of config.costs) { + if (cost.keyIsID) { + usefulResources[cost.key] = { + value: cost.value, + target: this.parent.parent, + keyIsID: true + }; + } + } const resources = config.costs .filter(c => c.enabled !== false) .map(c => { - const resource = this.actor.system.resources[c.type]; - return { type: c.type, value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1) }; + const resource = usefulResources[c.key]; + return { + key: c.key, + value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1), + target: resource.target, + keyIsID: resource.keyIsID + }; }); await this.actor.modifyResource(resources); @@ -372,9 +387,27 @@ export default class DHBaseAction extends foundry.abstract.DataModel { }); } - hasCost(costs) { + async getResources(costs) { + const actorResources = this.actor.system.resources; + const itemResources = {}; + for (var itemResource of costs) { + if (itemResource.keyIsID) { + itemResources[itemResource.key] = { + value: this.parent.resource.value ?? 0 + }; + } + } + + return { + ...actorResources, + ...itemResources + }; + } + + /* COST */ + async hasCost(costs) { const realCosts = this.getRealCosts(costs), - hasFearCost = realCosts.findIndex(c => c.type === 'fear'); + hasFearCost = realCosts.findIndex(c => c.key === 'fear'); if (hasFearCost > -1) { const fearCost = realCosts.splice(hasFearCost, 1)[0]; if ( @@ -385,16 +418,15 @@ export default class DHBaseAction extends foundry.abstract.DataModel { } /* isReversed is a sign that the resource is inverted, IE it counts upwards instead of down */ - const resources = this.actor.system.resources; + const resources = await this.getResources(realCosts); return realCosts.reduce( (a, c) => - a && resources[c.type].isReversed - ? resources[c.type].value + (c.total ?? c.value) <= resources[c.type].max - : resources[c.type]?.value >= (c.total ?? c.value), + a && resources[c.key].isReversed + ? resources[c.key].value + (c.total ?? c.value) <= resources[c.key].max + : resources[c.key]?.value >= (c.total ?? c.value), true ); } - /* COST */ /* USES */ calcUses(uses) { @@ -409,7 +441,6 @@ export default class DHBaseAction extends foundry.abstract.DataModel { if (!uses) return true; return (uses.hasOwnProperty('enabled') && !uses.enabled) || uses.value + 1 <= uses.max; } - /* USES */ /* TARGET */ isTargetFriendly(target) { diff --git a/module/data/action/damageAction.mjs b/module/data/action/damageAction.mjs index 3e840e60..7f7faeee 100644 --- a/module/data/action/damageAction.mjs +++ b/module/data/action/damageAction.mjs @@ -11,8 +11,8 @@ export default class DHDamageAction extends DHBaseAction { async rollDamage(event, data) { let formula = this.damage.parts.map(p => this.getFormulaValue(p, data).getFormula(this.actor)).join(' + '), - damageTypes = [...new Set(this.damage.parts.reduce((a,c) => a.concat([...c.type]), []))]; - + damageTypes = [...new Set(this.damage.parts.reduce((a, c) => a.concat([...c.type]), []))]; + damageTypes = !damageTypes.length ? ['physical'] : damageTypes; if (!formula || formula == '') return; @@ -36,7 +36,7 @@ export default class DHDamageAction extends DHBaseAction { config.source.message = data._id; config.directDamage = false; } - + roll = CONFIG.Dice.daggerheart.DamageRoll.build(config); } } diff --git a/module/data/actor/base.mjs b/module/data/actor/base.mjs index d7c7213e..cf1eebb1 100644 --- a/module/data/actor/base.mjs +++ b/module/data/actor/base.mjs @@ -1,4 +1,4 @@ -import DHBaseActorSettings from "../../applications/sheets/api/actor-setting.mjs"; +import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs'; const resistanceField = () => new foundry.data.fields.SchemaField({ @@ -37,13 +37,12 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel { const fields = foundry.data.fields; const schema = {}; - if(this.metadata.isNPC) - schema.description = new fields.HTMLField({ required: true, nullable: true }); - if(this.metadata.hasResistances) + if (this.metadata.isNPC) schema.description = new fields.HTMLField({ required: true, nullable: true }); + if (this.metadata.hasResistances) schema.resistance = new fields.SchemaField({ physical: resistanceField(), magical: resistanceField() - }) + }); return schema; } diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index d8fc2b5c..3ee7c980 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -39,9 +39,7 @@ export default class DhCharacter extends BaseDataActor { resources: new fields.SchemaField({ hitPoints: resourceField(0, true), stress: resourceField(6, true), - hope: resourceField(6), - tokens: new fields.ObjectField(), - dice: new fields.ObjectField() + hope: resourceField(6) }), traits: new fields.SchemaField({ agility: attributeField(), @@ -292,9 +290,7 @@ export default class DhCharacter extends BaseDataActor { } get deathMoveViable() { - return ( - this.resources.hitPoints.max > 0 && this.resources.hitPoints.value >= this.resources.hitPoints.max - ); + return this.resources.hitPoints.max > 0 && this.resources.hitPoints.value >= this.resources.hitPoints.max; } get armorApplicableDamageTypes() { diff --git a/module/data/item/armor.mjs b/module/data/item/armor.mjs index 7a8b06c0..8522c8fc 100644 --- a/module/data/item/armor.mjs +++ b/module/data/item/armor.mjs @@ -10,7 +10,6 @@ export default class DHArmor extends AttachableItem { label: 'TYPES.Item.armor', type: 'armor', hasDescription: true, - isQuantifiable: true, isInventoryItem: true }); } diff --git a/module/data/item/attachableItem.mjs b/module/data/item/attachableItem.mjs index 2b0608eb..d2ef8d22 100644 --- a/module/data/item/attachableItem.mjs +++ b/module/data/item/attachableItem.mjs @@ -5,7 +5,7 @@ export default class AttachableItem extends BaseDataItem { const fields = foundry.data.fields; return { ...super.defineSchema(), - attached: new fields.ArrayField(new fields.DocumentUUIDField({ type: "Item", nullable: true })) + attached: new fields.ArrayField(new fields.DocumentUUIDField({ type: 'Item', nullable: true })) }; } @@ -90,7 +90,10 @@ export default class AttachableItem extends BaseDataItem { }); if (effectsToRemove.length > 0) { - await actor.deleteEmbeddedDocuments('ActiveEffect', effectsToRemove.map(e => e.id)); + await actor.deleteEmbeddedDocuments( + 'ActiveEffect', + effectsToRemove.map(e => e.id) + ); } } @@ -140,13 +143,18 @@ export default class AttachableItem extends BaseDataItem { const parentUuidProperty = `${parentType}Uuid`; const effectsToRemove = actor.effects.filter(effect => { const attachmentSource = effect.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.itemAttachmentSource); - return attachmentSource && + return ( + attachmentSource && attachmentSource[parentUuidProperty] === this.parent.uuid && - attachmentSource.itemUuid === attachedUuid; + attachmentSource.itemUuid === attachedUuid + ); }); if (effectsToRemove.length > 0) { - await actor.deleteEmbeddedDocuments('ActiveEffect', effectsToRemove.map(e => e.id)); + await actor.deleteEmbeddedDocuments( + 'ActiveEffect', + effectsToRemove.map(e => e.id) + ); } } -} \ No newline at end of file +} diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index 9358a6b3..e99c85c3 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -11,12 +11,15 @@ const fields = foundry.data.fields; export default class BaseDataItem extends foundry.abstract.TypeDataModel { + static LOCALIZATION_PREFIXES = ['DAGGERHEART.ITEMS']; + /** @returns {ItemDataModelMetadata}*/ static get metadata() { return { label: 'Base Item', type: 'base', hasDescription: false, + hasResource: false, isQuantifiable: false, isInventoryItem: false }; @@ -33,6 +36,33 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { if (this.metadata.hasDescription) schema.description = new fields.HTMLField({ required: true, nullable: true }); + if (this.metadata.hasResource) { + schema.resource = new fields.SchemaField( + { + type: new fields.StringField({ + choices: CONFIG.DH.ITEM.itemResourceTypes, + initial: CONFIG.DH.ITEM.itemResourceTypes.simple + }), + value: new fields.NumberField({ integer: true, min: 0, initial: 0 }), + max: new fields.StringField({ nullable: true, initial: null }), + icon: new fields.StringField(), + recovery: new fields.StringField({ + choices: CONFIG.DH.GENERAL.refreshTypes, + initial: null, + nullable: true + }), + diceStates: new fields.TypedObjectField( + new fields.SchemaField({ + value: new fields.NumberField({ integer: true, nullable: true, initial: null }), + used: new fields.BooleanField({ initial: false }) + }) + ), + dieFaces: new fields.StringField({ initial: '4' }) + }, + { nullable: true, initial: null } + ); + } + if (this.metadata.isQuantifiable) schema.quantity = new fields.NumberField({ integer: true, initial: 1, min: 0, required: true }); @@ -62,28 +92,27 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { return data; } - /**@inheritdoc */ async _preCreate(data, options, user) { // Skip if no initial action is required or actions already exist - if (!this.metadata.hasInitialAction || !foundry.utils.isEmpty(this.actions)) return; + if (this.metadata.hasInitialAction && foundry.utils.isEmpty(this.actions)) { + const metadataType = this.metadata.type; + const actionType = { weapon: 'attack' }[metadataType]; + const ActionClass = game.system.api.models.actions.actionsTypes[actionType]; - const metadataType = this.metadata.type; - const actionType = { weapon: 'attack' }[metadataType]; - const ActionClass = game.system.api.models.actions.actionsTypes[actionType]; + const action = new ActionClass( + { + _id: foundry.utils.randomID(), + type: actionType, + name: game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[actionType].name), + ...ActionClass.getSourceConfig(this.parent) + }, + { + parent: this.parent + } + ); - const action = new ActionClass( - { - _id: foundry.utils.randomID(), - type: actionType, - name: game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[actionType].name), - ...ActionClass.getSourceConfig(this.parent) - }, - { - parent: this.parent - } - ); - - this.updateSource({ actions: [action] }); + this.updateSource({ actions: [action] }); + } } _onCreate(data) { @@ -95,7 +124,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { ...feature, system: { ...feature.system, - type: this.parent.type, + originItemType: this.parent.type, originId: data._id, identifier: feature.identifier } diff --git a/module/data/item/domainCard.mjs b/module/data/item/domainCard.mjs index 3451a14a..89bbfb40 100644 --- a/module/data/item/domainCard.mjs +++ b/module/data/item/domainCard.mjs @@ -7,7 +7,8 @@ export default class DHDomainCard extends BaseDataItem { return foundry.utils.mergeObject(super.metadata, { label: 'TYPES.Item.domainCard', type: 'domainCard', - hasDescription: true + hasDescription: true, + hasResource: true }); } diff --git a/module/data/item/feature.mjs b/module/data/item/feature.mjs index a5b5cb3c..061772c8 100644 --- a/module/data/item/feature.mjs +++ b/module/data/item/feature.mjs @@ -7,7 +7,8 @@ export default class DHFeature extends BaseDataItem { return foundry.utils.mergeObject(super.metadata, { label: 'TYPES.Item.feature', type: 'feature', - hasDescription: true + hasDescription: true, + hasResource: true }); } @@ -16,10 +17,32 @@ export default class DHFeature extends BaseDataItem { const fields = foundry.data.fields; return { ...super.defineSchema(), - type: new fields.StringField({ choices: CONFIG.DH.ITEM.featureTypes, nullable: true, initial: null }), + originItemType: new fields.StringField({ + choices: CONFIG.DH.ITEM.featureTypes, + nullable: true, + initial: null + }), originId: new fields.StringField({ nullable: true, initial: null }), identifier: new fields.StringField(), actions: new fields.ArrayField(new ActionField()) }; } + + get spellcastingModifier() { + let traitValue = 0; + if (this.actor && this.originId && ['class', 'subclass'].includes(this.originItemType)) { + if (this.originItemType === 'subclass') { + traitValue = + this.actor.system.traits[this.actor.items.get(this.originId).system.spellcastingTrait]?.value ?? 0; + } else { + const subclass = + this.actor.system.multiclass.value?.id === this.originId + ? this.actor.system.multiclass.subclass + : this.actor.system.class.subclass; + traitValue = this.actor.system.traits[subclass.system.spellcastingTrait]?.value ?? 0; + } + } + + return traitValue; + } } diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index 82b4ba80..9fbb3eba 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -9,7 +9,6 @@ export default class DHWeapon extends AttachableItem { label: 'TYPES.Item.weapon', type: 'weapon', hasDescription: true, - isQuantifiable: true, isInventoryItem: true // hasInitialAction: true }); @@ -37,7 +36,7 @@ export default class DHWeapon extends AttachableItem { actionIds: new fields.ArrayField(new fields.StringField({ required: true })) }) ), - attack: new ActionField({ + attack: new ActionField({ initial: { name: 'Attack', img: 'icons/skills/melee/blood-slash-foam-red.webp', diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index 87c9401e..8744223a 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -56,7 +56,7 @@ export default class DHRoll extends Roll { // Create Chat Message if (config.source?.message) { - if(game.modules.get('dice-so-nice')?.active) await game.dice3d.showForRoll(roll, game.user, true); + if (game.modules.get('dice-so-nice')?.active) await game.dice3d.showForRoll(roll, game.user, true); } else { config.message = await this.toMessage(roll, config); } diff --git a/module/documents/activeEffect.mjs b/module/documents/activeEffect.mjs index 03ac73bc..aa1a5b0d 100644 --- a/module/documents/activeEffect.mjs +++ b/module/documents/activeEffect.mjs @@ -1,3 +1,5 @@ +import { itemAbleRollParse } from '../helpers/utils.mjs'; + export default class DhActiveEffect extends ActiveEffect { get isSuppressed() { // If this is a copied effect from an attachment, never suppress it @@ -24,16 +26,18 @@ export default class DhActiveEffect extends ActiveEffect { */ get isAttached() { if (!this.parent || !this.parent.parent) return false; - + // Check if this item's UUID is in any actor's armor or weapon attachment lists const actor = this.parent.parent; if (!actor || !actor.items) return false; - + return actor.items.some(item => { - return (item.type === 'armor' || item.type === 'weapon') && - item.system?.attached && - Array.isArray(item.system.attached) && - item.system.attached.includes(this.parent.uuid); + return ( + (item.type === 'armor' || item.type === 'weapon') && + item.system?.attached && + Array.isArray(item.system.attached) && + item.system.attached.includes(this.parent.uuid) + ); }); } @@ -51,11 +55,7 @@ export default class DhActiveEffect extends ActiveEffect { } static applyField(model, change, field) { - const isItemTarget = change.value.toLowerCase().startsWith('item.'); - change.value = isItemTarget ? change.value.slice(5) : change.value; - change.value = Roll.safeEval( - Roll.replaceFormulaData(change.value, isItemTarget ? change.effect.parent : model) - ); + change.value = itemAbleRollParse(change.value, model, change.effect.parent); super.applyField(model, change, field); } diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index d8d75461..bfeb103e 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -396,7 +396,7 @@ export default class DhpActor extends Actor { if (Hooks.call(`${CONFIG.DH.id}.preTakeDamage`, this, baseDamage, type) === false) return null; if (this.type === 'companion') { - await this.modifyResource([{ value: 1, type: 'stress' }]); + await this.modifyResource([{ value: 1, key: 'stress' }]); return; } @@ -418,8 +418,8 @@ export default class DhpActor extends Actor { const { modifiedDamage, armorSpent, stressSpent } = armorStackResult; updates.find(u => u.type === 'hitPoints').value = modifiedDamage; updates.push( - ...(armorSpent ? [{ value: armorSpent, type: 'armorStack' }] : []), - ...(stressSpent ? [{ value: stressSpent, type: 'stress' }] : []) + ...(armorSpent ? [{ value: armorSpent, key: 'armorStack' }] : []), + ...(stressSpent ? [{ value: stressSpent, key: 'stress' }] : []) ); } } @@ -434,8 +434,8 @@ export default class DhpActor extends Actor { /* if(this.system.resistance[type]?.immunity) return 0; if(this.system.resistance[type]?.resistance) baseDamage = Math.ceil(baseDamage / 2); */ - if(this.canResist(type, 'immunity')) return 0; - if(this.canResist(type, 'resistance')) baseDamage = Math.ceil(baseDamage / 2); + if (this.canResist(type, 'immunity')) return 0; + if (this.canResist(type, 'resistance')) baseDamage = Math.ceil(baseDamage / 2); // const flatReduction = this.system.resistance[type].reduction; const flatReduction = this.getDamageTypeReduction(type); @@ -448,13 +448,16 @@ export default class DhpActor extends Actor { } canResist(type, resistance) { - if(!type) return 0; + if (!type) return 0; return type.every(t => this.system.resistance[t]?.[resistance] === true); } getDamageTypeReduction(type) { - if(!type) return 0; - const reduction = Object.entries(this.system.resistance).reduce((a, [index, value]) => type.includes(index) ? Math.min(value.reduction, a) : a, Infinity); + if (!type) return 0; + const reduction = Object.entries(this.system.resistance).reduce( + (a, [index, value]) => (type.includes(index) ? Math.min(value.reduction, a) : a), + Infinity + ); return reduction === Infinity ? 0 : reduction; } @@ -467,39 +470,61 @@ export default class DhpActor extends Actor { if (!resources.length) return; if (resources.find(r => r.type === 'stress')) this.convertStressDamageToHP(resources); - let updates = { actor: { target: this, resources: {} }, armor: { target: this.system.armor, resources: {} } }; + let updates = { + actor: { target: this, resources: {} }, + armor: { target: this.system.armor, resources: {} }, + items: {} + }; resources.forEach(r => { - switch (r.type) { - case 'fear': - ui.resources.updateFear( - game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear) + r.value - ); - break; - case 'armorStack': - updates.armor.resources['system.marks.value'] = Math.max( - Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore), - 0 - ); - break; - default: - updates.actor.resources[`system.resources.${r.type}.value`] = Math.max( - Math.min( - this.system.resources[r.type].value + r.value, - this.system.resources[r.type].max - ), - 0 - ); - break; + if (r.keyIsID) { + updates.items[r.key] = { + target: r.target, + resources: { + 'system.resource.value': r.target.system.resource.value + r.value + } + }; + } else { + switch (r.key) { + case 'fear': + ui.resources.updateFear( + game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear) + r.value + ); + break; + case 'armorStack': + updates.armor.resources['system.marks.value'] = Math.max( + Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore), + 0 + ); + break; + default: + updates.actor.resources[`system.resources.${r.key}.value`] = Math.max( + Math.min(this.system.resources[r.key].value + r.value, this.system.resources[r.key].max), + 0 + ); + break; + } } }); - Object.values(updates).forEach(async u => { - if (Object.keys(u.resources).length > 0) { - await emitAsGM( - GMUpdateEvent.UpdateDocument, - u.target.update.bind(u.target), - u.resources, - u.target.uuid - ); + Object.keys(updates).forEach(async key => { + const u = updates[key]; + if (key === 'items') { + Object.values(u).forEach(async item => { + await emitAsGM( + GMUpdateEvent.UpdateDocument, + item.target.update.bind(item.target), + item.resources, + item.target.uuid + ); + }); + } else { + if (Object.keys(u.resources).length > 0) { + await emitAsGM( + GMUpdateEvent.UpdateDocument, + u.target.update.bind(u.target), + u.resources, + u.target.uuid + ); + } } }); } diff --git a/module/helpers/handlebarsHelper.mjs b/module/helpers/handlebarsHelper.mjs index b3b46dcb..407f065e 100644 --- a/module/helpers/handlebarsHelper.mjs +++ b/module/helpers/handlebarsHelper.mjs @@ -1,3 +1,5 @@ +import { itemAbleRollParse } from './utils.mjs'; + export default class RegisterHandlebarsHelpers { static registerHelpers() { Handlebars.registerHelper({ @@ -6,8 +8,7 @@ export default class RegisterHandlebarsHelpers { times: this.times, damageFormula: this.damageFormula, damageSymbols: this.damageSymbols, - tertiary: this.tertiary, - signedNumber: this.signedNumber + rollParsed: this.rollParsed }); } @@ -43,7 +44,9 @@ export default class RegisterHandlebarsHelpers { return new Handlebars.SafeString(Array.from(symbols).map(symbol => ``)); } - static tertiary(a, b) { - return a ?? b; + static rollParsed(value, actor, item, numerical) { + const isNumerical = typeof numerical === 'boolean' ? numerical : false; + const result = itemAbleRollParse(value, actor, item); + return isNumerical && !result ? 0 : result; } } diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 40a71ac7..1e694aad 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -245,10 +245,10 @@ export const getDamageLabel = damage => { export const damageKeyToNumber = key => { return { - 'none': 0, - 'minor': 1, - 'major': 2, - 'severe': 3 + none: 0, + minor: 1, + major: 2, + severe: 3 }[key]; }; @@ -299,3 +299,15 @@ export const updateActorTokens = async (actor, update) => { } } }; + +export const itemAbleRollParse = (value, actor, item) => { + if (!value) return value; + + const isItemTarget = value.toLowerCase().startsWith('item.'); + const slicedValue = isItemTarget ? value.slice(5) : value; + try { + return Roll.safeEval(Roll.replaceFormulaData(slicedValue, isItemTarget ? item : actor)); + } catch (_) { + return ''; + } +}; diff --git a/module/systemRegistration/handlebars.mjs b/module/systemRegistration/handlebars.mjs index e4cc1a2c..8456094c 100644 --- a/module/systemRegistration/handlebars.mjs +++ b/module/systemRegistration/handlebars.mjs @@ -5,6 +5,8 @@ export const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/sheets/global/partials/action-item.hbs', 'systems/daggerheart/templates/sheets/global/partials/domain-card-item.hbs', 'systems/daggerheart/templates/sheets/global/partials/inventory-fieldset-items.hbs', + 'systems/daggerheart/templates/sheets/global/partials/item-resource.hbs', + 'systems/daggerheart/templates/sheets/global/partials/resource-section.hbs', 'systems/daggerheart/templates/components/card-preview.hbs', 'systems/daggerheart/templates/levelup/parts/selectable-card-preview.hbs', 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs', diff --git a/styles/less/dialog/index.less b/styles/less/dialog/index.less index 545ce2e1..f3e86518 100644 --- a/styles/less/dialog/index.less +++ b/styles/less/dialog/index.less @@ -4,6 +4,8 @@ @import './level-up/summary-container.less'; @import './level-up/tiers-container.less'; +@import './resource-dice/sheet.less'; + @import './actions/action-list.less'; @import './damage-selection/sheet.less'; diff --git a/styles/less/dialog/resource-dice/sheet.less b/styles/less/dialog/resource-dice/sheet.less new file mode 100644 index 00000000..7ddb53ca --- /dev/null +++ b/styles/less/dialog/resource-dice/sheet.less @@ -0,0 +1,58 @@ +.theme-light .daggerheart.dialog.dh-style.views.resource-dice { + .resource-items .resource-item { + input { + background-image: url('../assets/parchments/dh-parchment-light.png'); + } + + img { + filter: brightness(0) saturate(100%); + } + } +} + +.daggerheart.dialog.dh-style.views.resource-dice { + .reroll-confirmation { + margin-bottom: 8px; + } + + .resource-items { + display: flex; + justify-content: center; + gap: 8px; + + .resource-item { + position: relative; + display: flex; + align-items: center; + justify-content: center; + + input { + position: absolute; + border-color: light-dark(@dark-blue, @golden); + color: light-dark(black, white); + background-image: url('../assets/parchments/dh-parchment-dark.png'); + z-index: 2; + line-height: 22px; + height: unset; + text-align: center; + } + + img { + width: 48px; + height: 48px; + filter: brightness(0) saturate(100%) invert(97%) sepia(7%) saturate(580%) hue-rotate(332deg) + brightness(96%) contrast(95%); + } + } + } + + footer { + display: flex; + gap: 8px; + + button { + flex: 1; + white-space: nowrap; + } + } +} diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less index ab519a1c..39f0d1f1 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -112,7 +112,7 @@ margin: 5px; height: inherit; .tag { - box-shadow: 0 0 0 1.1em #E5E5E5 inset; + box-shadow: 0 0 0 1.1em @beige inset; vertical-align: top; box-sizing: border-box; max-width: 100%; @@ -120,9 +120,9 @@ color: black; border-radius: 3px; white-space: nowrap; - transition: .13s ease-out; + transition: 0.13s ease-out; height: 22px; - font-size: .9rem; + font-size: 0.9rem; gap: 0.5em; z-index: 1; .remove { diff --git a/styles/less/global/index.less b/styles/less/global/index.less index 932e48ab..c37de0c7 100644 --- a/styles/less/global/index.less +++ b/styles/less/global/index.less @@ -6,6 +6,7 @@ @import './tab-actions.less'; @import './tab-features.less'; @import './tab-effects.less'; +@import './tab-settings.less'; @import './item-header.less'; @import './feature-section.less'; @import './inventory-item.less'; diff --git a/styles/less/global/inventory-item.less b/styles/less/global/inventory-item.less index 24e53165..30fe920c 100644 --- a/styles/less/global/inventory-item.less +++ b/styles/less/global/inventory-item.less @@ -1,6 +1,17 @@ @import '../utils/colors.less'; @import '../utils/fonts.less'; +.theme-light .application.daggerheart.dh-style { + .inventory-item, + .card-item { + .item-resource .item-dice-resource { + img { + filter: brightness(0) saturate(100%); + } + } + } +} + .application.daggerheart.dh-style { .inventory-item { display: grid; @@ -21,10 +32,19 @@ } } + .item-label-wrapper { + display: grid; + grid-template-columns: 1fr 60px; + } + .item-label { font-family: @font-body; align-self: center; + &.fullWidth { + grid-column: span 2; + } + .item-name { font-size: 14px; } @@ -84,11 +104,27 @@ &:hover { .card-label { padding-top: 15px; - .controls { + .menu { opacity: 1; visibility: visible; transition: all 0.3s ease; max-height: 16px; + + &.resource-menu { + max-height: 55px; + + &.dice-menu { + max-height: 118px; + + .item-resources { + flex-wrap: wrap; + } + + .item-resource { + width: unset; + } + } + } } } } @@ -97,6 +133,7 @@ height: 100%; width: 100%; object-fit: cover; + border-radius: 6px; } .card-label { @@ -123,15 +160,88 @@ color: @beige; } - .controls { + .menu { display: flex; - gap: 15px; - align-items: center; + flex-direction: column; + gap: 8px; max-height: 0px; opacity: 0; visibility: collapse; transition: all 0.3s ease; color: @beige; + + .controls { + display: flex; + gap: 2px; + gap: 15px; + justify-content: center; + } + } + } + + .item-resources { + width: 92px; + } + + .item-resource { + width: 92px; + } + } + + .inventory-item, + .card-item { + .item-resources { + display: flex; + gap: 4px; + + .resource-edit { + font-size: 14px; + } + } + + .item-resource { + display: flex; + align-items: center; + justify-content: end; + gap: 4px; + + i { + flex: none; + font-size: 14px; + } + + input { + flex: 1; + } + + .item-dice-resource { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 26px; + + label { + position: absolute; + color: light-dark(white, black); + filter: drop-shadow(0 0 1px light-dark(@dark-blue, @golden)); + z-index: 2; + font-size: 18px; + cursor: pointer; + } + + img { + filter: brightness(0) saturate(100%) invert(97%) sepia(7%) saturate(580%) hue-rotate(332deg) + brightness(96%) contrast(95%); + } + + i { + position: absolute; + text-shadow: 0 0 3px white; + filter: drop-shadow(0 1px white); + color: black; + font-size: 26px; + } } } } diff --git a/styles/less/global/tab-attachments.less b/styles/less/global/tab-attachments.less index c283269e..04a9f49e 100644 --- a/styles/less/global/tab-attachments.less +++ b/styles/less/global/tab-attachments.less @@ -4,4 +4,4 @@ width: 100%; } } -} \ No newline at end of file +} diff --git a/styles/less/global/tab-settings.less b/styles/less/global/tab-settings.less new file mode 100644 index 00000000..3d5248be --- /dev/null +++ b/styles/less/global/tab-settings.less @@ -0,0 +1,8 @@ +@import '../utils/colors.less'; +@import '../utils/fonts.less'; + +.sheet.daggerheart.dh-style { + .tab.settings { + margin-bottom: 36px; + } +} diff --git a/styles/less/ui/chat/chat.less b/styles/less/ui/chat/chat.less index 945bda3f..12e8ba0c 100644 --- a/styles/less/ui/chat/chat.less +++ b/styles/less/ui/chat/chat.less @@ -31,6 +31,14 @@ } } + &.resource-roll { + .reroll-message { + text-align: center; + font-size: 18px; + margin-bottom: 0; + } + } + &.roll { .dice-flavor { text-align: center; diff --git a/templates/actionTypes/cost.hbs b/templates/actionTypes/cost.hbs index 515f4968..116fc631 100644 --- a/templates/actionTypes/cost.hbs +++ b/templates/actionTypes/cost.hbs @@ -6,7 +6,7 @@ {{#each source as |cost index|}}
{{formField ../fields.scalable label="Scalable" value=cost.scalable name=(concat "cost." index ".scalable") classes="checkbox"}} - {{formField ../fields.type choices=(@root.disableOption index ../fields.type.choices ../source) label="Resource" value=cost.type name=(concat "cost." index ".type") localize=true}} + {{formField ../fields.key choices=(@root.disableOption index @root.costOptions ../source) label="Resource" value=cost.key name=(concat "cost." index ".key") localize=true}} {{formField ../fields.value label="Amount" value=cost.value name=(concat "cost." index ".value")}} {{formField ../fields.step label="Step" value=cost.step name=(concat "cost." index ".step") disabled=(not cost.scalable)}} diff --git a/templates/dialogs/dice-roll/costSelection.hbs b/templates/dialogs/dice-roll/costSelection.hbs index 39ef7cf7..48ff90a0 100644 --- a/templates/dialogs/dice-roll/costSelection.hbs +++ b/templates/dialogs/dice-roll/costSelection.hbs @@ -10,7 +10,7 @@ {{#each costs as | cost index |}}
- + {{#if scalable}} diff --git a/templates/dialogs/dice-roll/resourceDice.hbs b/templates/dialogs/dice-roll/resourceDice.hbs new file mode 100644 index 00000000..33c93386 --- /dev/null +++ b/templates/dialogs/dice-roll/resourceDice.hbs @@ -0,0 +1,16 @@ +
+
+ {{#times (rollParsed item.system.resource.max actor item numerical=true)}} + {{#with (ifThen (lookup ../diceStates this) (lookup ../diceStates this) this) as | state |}} +
+ + +
+ {{/with}} + {{/times}} +
+
+ + +
+
\ No newline at end of file diff --git a/templates/sheets-settings/action-settings/configuration.hbs b/templates/sheets-settings/action-settings/configuration.hbs index 74d5fcc1..51b2a72b 100644 --- a/templates/sheets-settings/action-settings/configuration.hbs +++ b/templates/sheets-settings/action-settings/configuration.hbs @@ -4,6 +4,6 @@ data-tab="config" > {{> 'systems/daggerheart/templates/actionTypes/uses.hbs' fields=fields.uses.fields source=source.uses}} - {{> 'systems/daggerheart/templates/actionTypes/cost.hbs' fields=fields.cost.element.fields source=source.cost}} + {{> 'systems/daggerheart/templates/actionTypes/cost.hbs' fields=fields.cost.element.fields source=source.cost costOptions=costOptions}} {{> 'systems/daggerheart/templates/actionTypes/range-target.hbs' fields=(object range=fields.range target=fields.target.fields) source=(object target=source.target range=source.range)}} \ No newline at end of file diff --git a/templates/sheets/global/partials/domain-card-item.hbs b/templates/sheets/global/partials/domain-card-item.hbs index 3f2fe146..0ff7e8f5 100644 --- a/templates/sheets/global/partials/domain-card-item.hbs +++ b/templates/sheets/global/partials/domain-card-item.hbs @@ -1,31 +1,35 @@
  • -
    - {{#if (eq type 'weapon')}} - - - +
    {{item.name}}
    diff --git a/templates/sheets/global/partials/inventory-item.hbs b/templates/sheets/global/partials/inventory-item.hbs index 21b7de61..a6373c08 100644 --- a/templates/sheets/global/partials/inventory-item.hbs +++ b/templates/sheets/global/partials/inventory-item.hbs @@ -1,128 +1,137 @@
  • -
    - {{#if isCompanion}} - {{item.name}} - {{else}} -
    {{item.name}}
    - {{/if}} - {{#if (eq type 'weapon')}} -
    +
    +
    + {{#if isCompanion}} + {{item.name}} + {{else}} +
    {{item.name}}
    + {{/if}} + {{#if (eq type 'weapon')}} +
    + {{#if isSidebar}} +
    +
    + {{localize (concat 'DAGGERHEART.CONFIG.Traits.' item.system.attack.roll.trait '.short')}} + {{localize (concat 'DAGGERHEART.CONFIG.Range.' item.system.attack.range '.short')}} + - + {{item.system.attack.damage.parts.0.value.dice}}{{#if item.system.attack.damage.parts.0.value.bonus}} + {{item.system.attack.damage.parts.0.value.bonus}}{{/if}} + {{#each item.system.attack.damage.parts.0.type as | type | }} + {{#with (lookup @root.config.GENERAL.damageTypes type)}} + + {{/with}} + {{/each}} +
    +
    + {{else}} +
    + {{localize (concat 'DAGGERHEART.CONFIG.Traits.' item.system.attack.roll.trait '.name')}} +
    +
    + {{localize (concat 'DAGGERHEART.CONFIG.Range.' item.system.attack.range '.name')}} +
    +
    + {{item.system.attack.damage.parts.0.value.dice}}{{#if item.system.attack.damage.parts.0.value.bonus}} + {{item.system.attack.damage.parts.0.value.bonus}}{{/if}} + ( + {{#each item.system.attack.damage.parts.0.type}} + {{localize (concat 'DAGGERHEART.CONFIG.DamageType.' this '.abbreviation')}} + {{/each}} + ) +
    +
    + {{localize (concat 'DAGGERHEART.CONFIG.Burden.' item.system.burden)}} +
    + {{/if}} +
    + {{/if}} + {{#if (eq type 'armor')}} {{#if isSidebar}}
    - {{!-- {{localize (concat 'DAGGERHEART.CONFIG.Traits.' item.system.attack.roll.trait '.short')}} --}} - {{localize (concat 'DAGGERHEART.CONFIG.Range.' item.system.attack.range '.short')}} - - - {{item.system.attack.damage.parts.0.value.dice}}{{#if item.system.attack.damage.parts.0.value.bonus}} + {{item.system.attack.damage.parts.0.value.bonus}}{{/if}} - {{!-- ({{localize (concat 'DAGGERHEART.CONFIG.DamageType.' item.system.attack.damage.parts.0.type '.abbreviation')}}) --}} - {{#each item.system.attack.damage.parts.0.type as | type | }} - {{#with (lookup @root.config.GENERAL.damageTypes type)}} - - {{/with}} - {{/each}} + {{localize "DAGGERHEART.ITEMS.Armor.baseScore"}}: + {{item.system.baseScore}}
    {{else}} -
    - {{localize (concat 'DAGGERHEART.CONFIG.Traits.' item.system.attack.roll.trait '.name')}} -
    -
    - {{localize (concat 'DAGGERHEART.CONFIG.Range.' item.system.attack.range '.name')}} -
    -
    - {{item.system.attack.damage.parts.0.value.dice}}{{#if item.system.attack.damage.parts.0.value.bonus}} + {{item.system.attack.damage.parts.0.value.bonus}}{{/if}} - ( - {{#each item.system.attack.damage.parts.0.type}} - {{localize (concat 'DAGGERHEART.CONFIG.DamageType.' this '.abbreviation')}} - {{/each}} - ) -
    -
    - {{localize (concat 'DAGGERHEART.CONFIG.Burden.' item.system.burden)}} +
    +
    + {{localize "DAGGERHEART.ITEMS.Armor.baseScore"}}: + {{item.system.baseScore}} +
    +
    + {{localize "DAGGERHEART.ITEMS.Armor.baseThresholds.base"}}: + {{item.system.baseThresholds.major}} + / + {{item.system.baseThresholds.severe}} +
    {{/if}} -
    - {{/if}} - {{#if (eq type 'armor')}} - {{#if isSidebar}} -
    -
    - {{localize "DAGGERHEART.ITEMS.Armor.baseScore"}}: - {{item.system.baseScore}} + {{/if}} + {{#if (eq type 'domainCard')}} + {{#if isSidebar}} +
    +
    + {{localize (concat 'DAGGERHEART.CONFIG.DomainCardTypes.' item.system.type)}} + - + {{localize (concat 'DAGGERHEART.GENERAL.Domain.' item.system.domain '.label')}} + - + {{item.system.recallCost}} + +
    -
    - {{else}} + {{else}} +
    +
    + {{localize (concat 'DAGGERHEART.CONFIG.DomainCardTypes.' item.system.type)}} +
    +
    + {{localize (concat 'DAGGERHEART.GENERAL.Domain.' item.system.domain '.label')}} +
    +
    + {{localize "DAGGERHEART.ITEMS.DomainCard.recallCost"}}: + {{item.system.recallCost}} +
    +
    + {{/if}} + {{/if}} + {{#if (eq type 'effect')}}
    - {{localize "DAGGERHEART.ITEMS.Armor.baseScore"}}: - {{item.system.baseScore}} + {{localize (concat 'TYPES.Item.' item.parent.type)}} + : + {{item.parent.name}}
    - {{localize "DAGGERHEART.ITEMS.Armor.baseThresholds.base"}}: - {{item.system.baseThresholds.major}} - / - {{item.system.baseThresholds.severe}} + {{#if item.duration.duration}} + {{localize 'DAGGERHEART.EFFECTS.Duration.temporary'}} + {{else}} + {{localize 'DAGGERHEART.EFFECTS.Duration.passive'}} + {{/if}} +
    + {{#each item.statuses as |status|}} +
    + {{localize (concat 'DAGGERHEART.CONFIG.Condition.' status '.name')}} +
    + {{/each}} +
    + {{/if}} + {{#if (eq type 'action')}} +
    +
    + {{localize (concat 'DAGGERHEART.ACTIONS.TYPES.' item.type '.name')}} +
    +
    + {{localize (concat 'DAGGERHEART.CONFIG.ActionType.' item.actionType)}}
    {{/if}} +
    + {{#if (and (not isSidebar) (eq item.system.resource.type 'simple'))}} + {{> "systems/daggerheart/templates/sheets/global/partials/item-resource.hbs"}} {{/if}} - {{#if (eq type 'domainCard')}} - {{#if isSidebar}} -
    -
    - {{localize (concat 'DAGGERHEART.CONFIG.DomainCardTypes.' item.system.type)}} - - - {{localize (concat 'DAGGERHEART.GENERAL.Domain.' item.system.domain '.label')}} - - - {{item.system.recallCost}} - -
    -
    - {{else}} -
    -
    - {{localize (concat 'DAGGERHEART.CONFIG.DomainCardTypes.' item.system.type)}} -
    -
    - {{localize (concat 'DAGGERHEART.GENERAL.Domain.' item.system.domain '.label')}} -
    -
    - {{localize "DAGGERHEART.ITEMS.DomainCard.recallCost"}}: - {{item.system.recallCost}} -
    -
    - {{/if}} - {{/if}} - {{#if (eq type 'effect')}} -
    -
    - {{localize (concat 'TYPES.Item.' item.parent.type)}} - : - {{item.parent.name}} -
    -
    - {{#if item.duration.duration}} - {{localize 'DAGGERHEART.EFFECTS.Duration.temporary'}} - {{else}} - {{localize 'DAGGERHEART.EFFECTS.Duration.passive'}} - {{/if}} -
    - {{#each item.statuses as |status|}} -
    - {{localize (concat 'DAGGERHEART.CONFIG.Condition.' status '.name')}} -
    - {{/each}} -
    - {{/if}} - {{#if (eq type 'action')}} -
    -
    - {{localize (concat 'DAGGERHEART.ACTIONS.TYPES.' item.type '.name')}} -
    -
    - {{localize (concat 'DAGGERHEART.CONFIG.ActionType.' item.actionType)}} -
    + {{#if (and (not isSidebar) item.system.quantity)}} +
    +
    {{/if}}
    @@ -175,7 +184,9 @@ {{/unless}}
    {{#unless isSidebar}}{{{item.system.description}}}{{/unless}}
    - + {{#if (and (not isSidebar) (eq item.system.resource.type 'diceValue'))}} + {{> "systems/daggerheart/templates/sheets/global/partials/item-resource.hbs"}} + {{/if}} {{#if featureType}}
    {{#each item.system.actions as | action |}} diff --git a/templates/sheets/global/partials/item-resource.hbs b/templates/sheets/global/partials/item-resource.hbs new file mode 100644 index 00000000..9b92dea0 --- /dev/null +++ b/templates/sheets/global/partials/item-resource.hbs @@ -0,0 +1,21 @@ +{{#if (eq item.system.resource.type 'simple')}} +
    + + +
    +{{else}} +
    + {{#times (rollParsed item.system.resource.max item.parent item numerical=true)}} + {{#with (ifThen (lookup ../item.system.resource.diceStates this) (lookup ../item.system.resource.diceStates this) this) as | state |}} + +
    + + + {{#if state.used}}{{/if}} +
    +
    + {{/with}} + {{/times}} + +
    +{{/if}} \ No newline at end of file diff --git a/templates/sheets/global/partials/resource-section.hbs b/templates/sheets/global/partials/resource-section.hbs new file mode 100644 index 00000000..f0d322d3 --- /dev/null +++ b/templates/sheets/global/partials/resource-section.hbs @@ -0,0 +1,29 @@ +
    + + {{localize "DAGGERHEART.GENERAL.Resource.single"}} + {{#unless source.system.resource}} + + {{else}} + + {{/unless}} + + + {{#if source.system.resource}} +
    + {{formGroup systemFields.resource.fields.type value=source.system.resource.type localize=true blank=false}} + {{formGroup systemFields.resource.fields.recovery value=source.system.resource.recovery localize=true}} +
    + +
    + {{#if (eq source.system.resource.type 'simple')}} + {{formGroup systemFields.resource.fields.value value=source.system.resource.value localize=true}} + {{formGroup systemFields.resource.fields.max value=source.system.resource.max localize=true}} + {{else}} + {{formGroup systemFields.resource.fields.dieFaces value=source.system.resource.dieFaces localize=true}} + {{formGroup systemFields.resource.fields.max value=source.system.resource.max label="DAGGERHEART.ITEMS.FIELDS.resource.amount.label" localize=true}} + {{/if}} +
    + + {{#if (eq source.system.resource.type 'simple')}}{{formGroup systemFields.resource.fields.icon value=source.system.resource.icon localize=true placeholder="fa-solid fa-hashtag"}}{{/if}} + {{/if}} +
    \ No newline at end of file diff --git a/templates/sheets/items/class/features.hbs b/templates/sheets/items/class/features.hbs index 96ce2655..5431e54b 100644 --- a/templates/sheets/items/class/features.hbs +++ b/templates/sheets/items/class/features.hbs @@ -4,7 +4,7 @@ data-group='{{tabs.features.group}}' >
    -
    +
    {{localize "DAGGERHEART.ITEMS.Class.hopeFeatures"}}
    {{#each source.system.hopeFeatures as |feature|}} @@ -13,7 +13,7 @@
    -
    +
    {{localize "DAGGERHEART.ITEMS.Class.classFeatures"}}
    {{#each source.system.classFeatures as |feature|}} diff --git a/templates/sheets/items/domainCard/settings.hbs b/templates/sheets/items/domainCard/settings.hbs index 5c653fdc..2faa6934 100644 --- a/templates/sheets/items/domainCard/settings.hbs +++ b/templates/sheets/items/domainCard/settings.hbs @@ -17,4 +17,6 @@ {{localize "DAGGERHEART.ITEMS.DomainCard.recallCost"}} {{formField systemFields.recallCost value=source.system.recallCost data-dtype="Number"}}
    + + {{> "systems/daggerheart/templates/sheets/global/partials/resource-section.hbs" }} \ No newline at end of file diff --git a/templates/sheets/items/feature/settings.hbs b/templates/sheets/items/feature/settings.hbs new file mode 100644 index 00000000..24e6f8ef --- /dev/null +++ b/templates/sheets/items/feature/settings.hbs @@ -0,0 +1,7 @@ +
    + {{> "systems/daggerheart/templates/sheets/global/partials/resource-section.hbs" }} +
    \ No newline at end of file diff --git a/templates/ui/chat/resource-roll.hbs b/templates/ui/chat/resource-roll.hbs new file mode 100644 index 00000000..b6e1f3b0 --- /dev/null +++ b/templates/ui/chat/resource-roll.hbs @@ -0,0 +1,3 @@ +
    +
    {{localize "DAGGERHEART.UI.Chat.resourceRoll.playerMessage" user=user name=name }}
    +
    \ No newline at end of file diff --git a/templates/ui/tooltip/parts/tooltipTags.hbs b/templates/ui/tooltip/parts/tooltipTags.hbs index a02a729a..ba4e875f 100644 --- a/templates/ui/tooltip/parts/tooltipTags.hbs +++ b/templates/ui/tooltip/parts/tooltipTags.hbs @@ -6,7 +6,7 @@
    {{localize feature.name}}
    {{#if feature.img}}{{/if}}
    -
    {{{localize (tertiary feature.description feature.system.description)}}}
    +
    {{{localize (ifThen feature.description feature.description feature.system.description)}}}
    {{/each}}
    \ No newline at end of file From a768b1dfdba58d3c87f69c35a939608e5c78bc3a Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Mon, 14 Jul 2025 01:58:16 +0200 Subject: [PATCH 3/5] Character Setup Rework (#315) * Updated to make use of setup tabs. Ancestry now has primary/secondary features * Changed so ancestry uses a single Features field * Revert "Changed so ancestry uses a single Features field" This reverts commit 0bda6b5dbeaf719c84c0733fef16583407784118. * Reapply "Changed so ancestry uses a single Features field" This reverts commit 1febafd4415c87c47e279e0575f603effa8f267a. * Made it work again the bad way \._./ * Changed so that Feature(Item) has a primary field * Feature(Item) now has subtype instead of primary as a field * Fixed experience/evasion * Moved light styling to appTheme mixing * Added svg and style rules --- lang/en.json | 20 +- .../characterCreation/characterCreation.mjs | 222 ++++++++++++++++-- module/applications/sheets/api/base-item.mjs | 4 +- .../sheets/api/heritage-sheet.mjs | 4 - module/applications/sheets/items/ancestry.mjs | 101 +++++++- module/applications/sheets/items/class.mjs | 2 +- .../applications/sheets/items/community.mjs | 6 +- module/applications/sheets/items/subclass.mjs | 4 +- module/config/itemConfig.mjs | 5 + module/data/item/ancestry.mjs | 16 ++ module/data/item/feature.mjs | 1 + module/documents/actor.mjs | 5 +- .../selections-container.less | 167 ++++++++++++- templates/characterCreation/footer.hbs | 6 +- .../characterCreation/setupTabs/ancestry.hbs | 63 +++++ .../characterCreation/setupTabs/class.hbs | 24 ++ .../characterCreation/setupTabs/community.hbs | 18 ++ .../setupTabs/domainCards.hbs | 22 ++ .../setupTabs/experience.hbs | 19 ++ .../characterCreation/setupTabs/traits.hbs | 32 +++ templates/characterCreation/tabs/setup.hbs | 102 +------- templates/components/card-preview.hbs | 8 +- templates/sheets/actors/character/header.hbs | 2 +- templates/sheets/items/ancestry/features.hbs | 47 ++++ 24 files changed, 756 insertions(+), 144 deletions(-) create mode 100644 templates/characterCreation/setupTabs/ancestry.hbs create mode 100644 templates/characterCreation/setupTabs/class.hbs create mode 100644 templates/characterCreation/setupTabs/community.hbs create mode 100644 templates/characterCreation/setupTabs/domainCards.hbs create mode 100644 templates/characterCreation/setupTabs/experience.hbs create mode 100644 templates/characterCreation/setupTabs/traits.hbs create mode 100644 templates/sheets/items/ancestry/features.hbs diff --git a/lang/en.json b/lang/en.json index c0dc1c3c..96f5a9dd 100755 --- a/lang/en.json +++ b/lang/en.json @@ -162,12 +162,25 @@ }, "APPLICATIONS": { "CharacterCreation": { + "setupTabs": { + "ancestry": "Ancestry", + "community": "Community", + "class": "Class", + "experience": "Experience", + "traits": "Traits", + "domainCards": "Domain Cards" + }, + "ancestryNamePlaceholder": "Your ancestry's name", + "buttonTitle": "Character Setup", "choice": "Choice", "finishCreation": "Finish Character Setup", "heritage": "Heritage", "initialExperiences": "Initial Experiences", + "mixedAncestry": "Mixed Ancestry", "newExperience": "New Experience..", "selectAncestry": "Select Ancestry", + "selectPrimaryAncestry": "Select Primary Ancestry", + "selectSecondaryAncestry": "Select Secondary Ancestry", "selectArmor": "Select Armor", "selectClass": "Select Class", "selectCommunity": "Select Community", @@ -1177,6 +1190,10 @@ "value": { "label": "Value" } } }, + "Ancestry": { + "primaryFeature": "Primary Feature", + "secondaryFeature": "Secondary Feature" + }, "Armor": { "baseScore": "Base Score", "baseThresholds": { @@ -1432,7 +1449,8 @@ "damageAlreadyNone": "The damage has already been reduced to none", "noAvailableArmorMarks": "You have no more available armor marks", "notEnoughStress": "You don't have enough stress", - "damageIgnore": "{character} did not take damage" + "damageIgnore": "{character} did not take damage", + "featureIsMissing": "Feature is missing" }, "Tooltip": { "openItemWorld": "Open Item World", diff --git a/module/applications/characterCreation/characterCreation.mjs b/module/applications/characterCreation/characterCreation.mjs index 71849f56..ed0ee5a7 100644 --- a/module/applications/characterCreation/characterCreation.mjs +++ b/module/applications/characterCreation/characterCreation.mjs @@ -11,13 +11,16 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl this.setup = { traits: this.character.system.traits, - ancestry: this.character.system.ancestry ?? {}, + ancestryName: '', + mixedAncestry: false, + primaryAncestry: this.character.system.ancestry ?? {}, + secondaryAncestry: {}, community: this.character.system.community ?? {}, class: this.character.system.class?.value ?? {}, subclass: this.character.system.class?.subclass ?? {}, experiences: { - [foundry.utils.randomID()]: { description: '', value: 2 }, - [foundry.utils.randomID()]: { description: '', value: 2 } + [foundry.utils.randomID()]: { name: '', value: 2 }, + [foundry.utils.randomID()]: { name: '', value: 2 } }, domainCards: { [foundry.utils.randomID()]: {}, @@ -47,12 +50,13 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl static DEFAULT_OPTIONS = { tag: 'form', classes: ['daggerheart', 'dialog', 'dh-style', 'character-creation'], - position: { width: 800, height: 'auto' }, + position: { width: 700, height: 'auto' }, actions: { viewCompendium: this.viewCompendium, viewItem: this.viewItem, useSuggestedTraits: this.useSuggestedTraits, equipmentChoice: this.equipmentChoice, + setupGoNext: this.setupGoNext, finish: this.finish }, form: { @@ -76,6 +80,12 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl static PARTS = { tabs: { template: 'systems/daggerheart/templates/characterCreation/tabs.hbs' }, setup: { template: 'systems/daggerheart/templates/characterCreation/tabs/setup.hbs' }, + ancestry: { template: 'systems/daggerheart/templates/characterCreation/setupTabs/ancestry.hbs' }, + community: { template: 'systems/daggerheart/templates/characterCreation/setupTabs/community.hbs' }, + class: { template: 'systems/daggerheart/templates/characterCreation/setupTabs/class.hbs' }, + traits: { template: 'systems/daggerheart/templates/characterCreation/setupTabs/traits.hbs' }, + experience: { template: 'systems/daggerheart/templates/characterCreation/setupTabs/experience.hbs' }, + domainCards: { template: 'systems/daggerheart/templates/characterCreation/setupTabs/domainCards.hbs' }, equipment: { template: 'systems/daggerheart/templates/characterCreation/tabs/equipment.hbs' }, // story: { template: 'systems/daggerheart/templates/characterCreation/tabs/story.hbs' }, footer: { template: 'systems/daggerheart/templates/characterCreation/footer.hbs' } @@ -107,6 +117,51 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl // } }; + static SETUPTABS = { + ancestry: { + active: true, + cssClass: '', + group: 'setup', + id: 'ancestry', + label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.setupTabs.ancestry' + }, + community: { + active: false, + cssClass: '', + group: 'setup', + id: 'community', + label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.setupTabs.community' + }, + class: { + active: false, + cssClass: '', + group: 'setup', + id: 'class', + label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.setupTabs.class' + }, + traits: { + active: false, + cssClass: '', + group: 'setup', + id: 'traits', + label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.setupTabs.traits' + }, + experience: { + active: false, + cssClass: '', + group: 'setup', + id: 'experience', + label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.setupTabs.experience' + }, + domainCards: { + active: false, + cssClass: '', + group: 'setup', + id: 'domainCards', + label: 'DAGGERHEART.APPLICATIONS.CharacterCreation.setupTabs.domainCards' + } + }; + _getTabs(tabs) { for (const v of Object.values(tabs)) { v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active; @@ -114,14 +169,16 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl switch (v.id) { case 'setup': + const ancestryFinished = this.setup.primaryAncestry.uuid; + const communityFinished = this.setup.community.uuid; const classFinished = this.setup.class.uuid && this.setup.subclass.uuid; - const heritageFinished = this.setup.ancestry.uuid && this.setup.community.uuid; const traitsFinished = Object.values(this.setup.traits).every(x => x.value !== null); - const experiencesFinished = Object.values(this.setup.experiences).every(x => x.description); + const experiencesFinished = Object.values(this.setup.experiences).every(x => x.name); const domainCardsFinished = Object.values(this.setup.domainCards).every(x => x.uuid); v.finished = + ancestryFinished && + communityFinished && classFinished && - heritageFinished && traitsFinished && experiencesFinished && domainCardsFinished; @@ -146,15 +203,44 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl return tabs; } + _getSetupTabs(tabs) { + for (const v of Object.values(tabs)) { + v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active; + v.cssClass = v.active ? 'active' : ''; + + switch (v.id) { + case 'community': + v.disabled = this.setup.visibility < 2; + break; + case 'class': + v.disabled = this.setup.visibility < 3; + break; + case 'traits': + v.disabled = this.setup.visibility < 4; + break; + case 'experience': + v.disabled = this.setup.visibility < 5; + break; + case 'domainCards': + v.disabled = this.setup.visibility < 6; + break; + } + } + + return tabs; + } + changeTab(tab, group, options) { super.changeTab(tab, group, options); - for (var listTab of Object.keys(this.constructor.TABS)) { - const marker = options.navElement.querySelector(`a[data-action="tab"].${listTab} .finish-marker`); - if (listTab === tab) { - marker.classList.add('active'); - } else { - marker.classList.remove('active'); + if (group === 'primary') { + for (var listTab of Object.keys(this.constructor.TABS)) { + const marker = options.navElement.querySelector(`a[data-action="tab"].${listTab} .finish-marker`); + if (listTab === tab) { + marker.classList.add('active'); + } else { + marker.classList.remove('active'); + } } } } @@ -163,6 +249,11 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl super._attachPartListeners(partId, htmlElement, options); this._dragDrop.forEach(d => d.bind(htmlElement)); + + htmlElement.querySelectorAll('.mixed-ancestry-slider').forEach(element => { + element.addEventListener('input', this.mixedAncestryToggle.bind(this)); + element.addEventListener('click', this.mixedAncestryToggle.bind(this)); + }); } async _prepareContext(_options) { @@ -174,7 +265,30 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl async _preparePartContext(partId, context) { switch (partId) { + case 'footer': + context.isLastTab = this.tabGroups.setup === 'domainCards'; + switch (this.tabGroups.setup) { + case null: + case 'ancestry': + context.nextDisabled = this.setup.visibility === 1; + break; + case 'community': + context.nextDisabled = this.setup.visibility === 2; + break; + case 'class': + context.nextDisabled = this.setup.visibility === 3; + break; + case 'traits': + context.nextDisabled = this.setup.visibility === 4; + break; + case 'experience': + context.nextDisabled = this.setup.visibility === 5; + break; + } + + break; case 'setup': + context.setupTabs = this._getSetupTabs(this.constructor.SETUPTABS); const availableTraitModifiers = game.settings .get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew) .traitArray.map(trait => ({ key: trait, name: trait })); @@ -215,13 +329,13 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl context.experience = { values: this.setup.experiences, nrTotal: Object.keys(this.setup.experiences).length, - nrSelected: Object.values(this.setup.experiences).reduce( - (acc, exp) => acc + (exp.description ? 1 : 0), - 0 - ) + nrSelected: Object.values(this.setup.experiences).reduce((acc, exp) => acc + (exp.name ? 1 : 0), 0) }; - context.ancestry = { ...this.setup.ancestry, compendium: 'ancestries' }; + context.mixedAncestry = Number(this.setup.mixedAncestry); + context.ancestryName = this.setup.ancestryName; + context.primaryAncestry = { ...this.setup.primaryAncestry, compendium: 'ancestries' }; + context.secondaryAncestry = { ...this.setup.secondaryAncestry, compendium: 'ancestries' }; context.community = { ...this.setup.community, compendium: 'communities' }; context.class = { ...this.setup.class, compendium: 'classes' }; context.subclass = { ...this.setup.subclass, compendium: 'subclasses' }; @@ -278,18 +392,29 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl this.render(); } + mixedAncestryToggle(event) { + event.preventDefault(); + event.stopPropagation(); + this.setup.mixedAncestry = !this.setup.mixedAncestry; + if (!this.setup.mixedAncestry) this.setup.secondaryAncestry = {}; + + this.render(); + } + getUpdateVisibility() { switch (this.setup.visibility) { + case 6: + return 6; case 5: - return 5; + return Object.values(this.setup.experiences).every(x => x.name) ? 6 : 5; case 4: - return Object.values(this.setup.experiences).every(x => x.description) ? 5 : 4; + return Object.values(this.setup.traits).every(x => x.value !== null) ? 5 : 4; case 3: - return Object.values(this.setup.traits).every(x => x.value !== null) ? 4 : 3; + return this.setup.class.uuid && this.setup.subclass.uuid ? 4 : 3; case 2: - return this.setup.ancestry.uuid && this.setup.community.uuid ? 3 : 2; + return this.setup.community.uuid ? 3 : 2; case 1: - return this.setup.class.uuid && this.setup.subclass.uuid ? 2 : 1; + return this.setup.primaryAncestry.uuid ? 2 : 1; } } @@ -348,8 +473,46 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl this.render(); } + static setupGoNext() { + switch (this.setup.visibility) { + case 2: + this.tabGroups.setup = 'community'; + break; + case 3: + this.tabGroups.setup = 'class'; + break; + case 4: + this.tabGroups.setup = 'traits'; + break; + case 5: + this.tabGroups.setup = 'experience'; + break; + case 6: + this.tabGroups.setup = 'domainCards'; + break; + } + + this.render(); + } + static async finish() { - await this.character.createEmbeddedDocuments('Item', [this.setup.ancestry]); + const primaryAncestryFeature = this.setup.primaryAncestry.system.primaryFeature; + const secondaryAncestryFeature = this.setup.secondaryAncestry?.uuid + ? this.setup.secondaryAncestry.system.secondaryFeature + : this.setup.primaryAncestry.system.secondaryFeature; + + const ancestry = { + ...this.setup.primaryAncestry, + name: this.setup.ancestryName ?? this.setup.primaryAncestry.name, + system: { + ...this.setup.primaryAncestry.system, + features: [primaryAncestryFeature.uuid, secondaryAncestryFeature.uuid], + primaryFeature: primaryAncestryFeature.uuid, + secondaryFeature: secondaryAncestryFeature.uuid + } + }; + + await this.character.createEmbeddedDocuments('Item', [ancestry]); await this.character.createEmbeddedDocuments('Item', [this.setup.community]); await this.character.createEmbeddedDocuments('Item', [this.setup.class]); await this.character.createEmbeddedDocuments('Item', [this.setup.subclass]); @@ -396,8 +559,15 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl async _onDrop(event) { const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); const item = await foundry.utils.fromUuid(data.uuid); - if (item.type === 'ancestry' && event.target.closest('.ancestry-card')) { - this.setup.ancestry = { + if (item.type === 'ancestry' && event.target.closest('.primary-ancestry-card')) { + this.setup.ancestryName = item.name; + this.setup.primaryAncestry = { + ...item, + effects: Array.from(item.effects).map(x => x.toObject()), + uuid: item.uuid + }; + } else if (item.type === 'ancestry' && event.target.closest('.secondary-ancestry-card')) { + this.setup.secondaryAncestry = { ...item, effects: Array.from(item.effects).map(x => x.toObject()), uuid: item.uuid diff --git a/module/applications/sheets/api/base-item.mjs b/module/applications/sheets/api/base-item.mjs index 2847bfc8..0c25a44d 100644 --- a/module/applications/sheets/api/base-item.mjs +++ b/module/applications/sheets/api/base-item.mjs @@ -181,7 +181,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { const target = button.closest('.feature-item'); const feature = this.document.system.features.find(x => x?.id === target.id); if (!feature) { - ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.notifications.featureIsMissing')); + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing')); return; } @@ -251,7 +251,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { if (featureItem) { const feature = this.document.system.features.find(x => x?.id === featureItem.id); if (!feature) { - ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.notifications.featureIsMissing')); + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing')); return; } diff --git a/module/applications/sheets/api/heritage-sheet.mjs b/module/applications/sheets/api/heritage-sheet.mjs index 349da194..8ac8d7d6 100644 --- a/module/applications/sheets/api/heritage-sheet.mjs +++ b/module/applications/sheets/api/heritage-sheet.mjs @@ -10,10 +10,6 @@ export default class DHHeritageSheet extends DHBaseItemSheet { static PARTS = { tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' }, - feature: { - template: 'systems/daggerheart/templates/sheets/global/tabs/tab-features.hbs', - scrollable: ['.feature'] - }, effects: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs', scrollable: ['.effects'] diff --git a/module/applications/sheets/items/ancestry.mjs b/module/applications/sheets/items/ancestry.mjs index 3636ea62..bd9e3792 100644 --- a/module/applications/sheets/items/ancestry.mjs +++ b/module/applications/sheets/items/ancestry.mjs @@ -3,12 +3,109 @@ import DHHeritageSheet from '../api/heritage-sheet.mjs'; export default class AncestrySheet extends DHHeritageSheet { /**@inheritdoc */ static DEFAULT_OPTIONS = { - classes: ['ancestry'] + classes: ['ancestry'], + actions: { + addFeature: AncestrySheet.#addFeature, + editFeature: AncestrySheet.#editFeature, + removeFeature: AncestrySheet.#removeFeature + }, + dragDrop: [{ dragSelector: null, dropSelector: '.tab.features .drop-section' }] }; /**@inheritdoc */ static PARTS = { header: { template: 'systems/daggerheart/templates/sheets/items/ancestry/header.hbs' }, - ...super.PARTS + ...super.PARTS, + features: { template: 'systems/daggerheart/templates/sheets/items/ancestry/features.hbs' } }; + + /* -------------------------------------------- */ + /* Application Clicks Actions */ + /* -------------------------------------------- */ + + /** + * Add a new feature to the item, prompting the user for its type. + * @type {ApplicationClickAction} + */ + static async #addFeature(_event, button) { + const feature = await game.items.documentClass.create({ + type: 'feature', + name: game.i18n.format('DOCUMENT.New', { type: game.i18n.localize('TYPES.Item.feature') }), + system: { + subType: button.dataset.type + } + }); + await this.document.update({ + 'system.features': [...this.document.system.features.map(x => x.uuid), feature.uuid] + }); + } + + /** + * Edit an existing feature on the item + * @type {ApplicationClickAction} + */ + static async #editFeature(_event, button) { + const target = button.closest('.feature-item'); + const feature = this.document.system[`${target.dataset.type}Feature`]; + if (!feature || Object.keys(feature).length === 0) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing')); + return; + } + + feature.sheet.render(true); + } + + /** + * Remove a feature from the item. + * @type {ApplicationClickAction} + */ + static async #removeFeature(event, button) { + event.stopPropagation(); + const target = button.closest('.feature-item'); + const feature = this.document.system[`${target.dataset.type}Feature`]; + const featureExists = feature && Object.keys(feature).length > 0; + + if (featureExists) { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { + type: game.i18n.localize(`TYPES.Item.feature`), + name: feature.name + }) + }, + content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: feature.name }) + }); + if (!confirmed) return; + } + + if (featureExists && target.dataset.type === 'primary') await feature.update({ 'system.primary': null }); + await this.document.update({ + 'system.features': this.document.system.features.filter(x => x && x.uuid !== feature.uuid).map(x => x.uuid) + }); + } + + /* -------------------------------------------- */ + /* Application Drag/Drop */ + /* -------------------------------------------- */ + + /** + * On drop on the item. + * @param {DragEvent} event - The drag event + */ + async _onDrop(event) { + event.stopPropagation(); + event.preventDefault(); + + const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); + + const item = await fromUuid(data.uuid); + if (item?.type === 'feature') { + const subType = event.target.closest('.primary-feature') ? 'primary' : 'secondary'; + await item.update({ 'system.subType': subType }); + + await this.document.update({ + 'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid] + }); + } + } } diff --git a/module/applications/sheets/items/class.mjs b/module/applications/sheets/items/class.mjs index 55295455..b49105be 100644 --- a/module/applications/sheets/items/class.mjs +++ b/module/applications/sheets/items/class.mjs @@ -200,7 +200,7 @@ export default class ClassSheet extends DHBaseItemSheet { const actionPath = this.getActionPath(button.dataset.type); const feature = this.document.system[actionPath].find(x => x?.id === target.dataset.featureId); if (!feature) { - ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.notifications.featureIsMissing')); + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing')); return; } diff --git a/module/applications/sheets/items/community.mjs b/module/applications/sheets/items/community.mjs index 92c3731e..387556d9 100644 --- a/module/applications/sheets/items/community.mjs +++ b/module/applications/sheets/items/community.mjs @@ -9,6 +9,10 @@ export default class CommunitySheet extends DHHeritageSheet { /**@inheritdoc */ static PARTS = { header: { template: 'systems/daggerheart/templates/sheets/items/community/header.hbs' }, - ...super.PARTS + ...super.PARTS, + feature: { + template: 'systems/daggerheart/templates/sheets/global/tabs/tab-features.hbs', + scrollable: ['.feature'] + } }; } diff --git a/module/applications/sheets/items/subclass.mjs b/module/applications/sheets/items/subclass.mjs index 7761dcbc..31eca43a 100644 --- a/module/applications/sheets/items/subclass.mjs +++ b/module/applications/sheets/items/subclass.mjs @@ -50,7 +50,7 @@ export default class SubclassSheet extends DHBaseItemSheet { static async editFeature(_, button) { const feature = this.document.system[button.dataset.type]; if (!feature) { - ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.notifications.featureIsMissing')); + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing')); return; } @@ -71,7 +71,7 @@ export default class SubclassSheet extends DHBaseItemSheet { if (featureItem) { const feature = this.document.system[featureItem.dataset.type]; if (!feature) { - ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.notifications.featureIsMissing')); + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing')); return; } diff --git a/module/config/itemConfig.mjs b/module/config/itemConfig.mjs index c63a0008..ede5ef08 100644 --- a/module/config/itemConfig.mjs +++ b/module/config/itemConfig.mjs @@ -1320,6 +1320,11 @@ export const featureTypes = { } }; +export const featureSubTypes = { + primary: 'primary', + secondary: 'secondary' +}; + export const actionTypes = { passive: { id: 'passive', diff --git a/module/data/item/ancestry.mjs b/module/data/item/ancestry.mjs index 463fe6c1..71b7683d 100644 --- a/module/data/item/ancestry.mjs +++ b/module/data/item/ancestry.mjs @@ -18,4 +18,20 @@ export default class DHAncestry extends BaseDataItem { features: new ForeignDocumentUUIDArrayField({ type: 'Item' }) }; } + + get primaryFeature() { + return ( + this.features.find(x => x?.system?.subType === CONFIG.DH.ITEM.featureSubTypes.primary) ?? + (this.features.filter(x => !x).length > 0 ? {} : null) + ); + } + + get secondaryFeature() { + return ( + this.features.find(x => x?.system?.subType === CONFIG.DH.ITEM.featureSubTypes.secondary) ?? + (this.features.filter(x => !x || x.system.subType === CONFIG.DH.ITEM.featureSubTypes.primary).length > 1 + ? {} + : null) + ); + } } diff --git a/module/data/item/feature.mjs b/module/data/item/feature.mjs index 061772c8..62b955e9 100644 --- a/module/data/item/feature.mjs +++ b/module/data/item/feature.mjs @@ -22,6 +22,7 @@ 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(), actions: new fields.ArrayField(new ActionField()) diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index bfeb103e..c789064b 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -1,9 +1,8 @@ -import DamageSelectionDialog from '../applications/dialogs/damageSelectionDialog.mjs'; -import { emitAsGM, emitAsOwner, GMUpdateEvent, socketEvent } from '../systemRegistration/socket.mjs'; +import { emitAsGM, GMUpdateEvent } from '../systemRegistration/socket.mjs'; import DamageReductionDialog from '../applications/dialogs/damageReductionDialog.mjs'; import { LevelOptionType } from '../data/levelTier.mjs'; import DHFeature from '../data/item/feature.mjs'; -import { damageKeyToNumber, getDamageKey } from '../helpers/utils.mjs'; +import { damageKeyToNumber } from '../helpers/utils.mjs'; export default class DhpActor extends Actor { /** diff --git a/styles/less/dialog/character-creation/selections-container.less b/styles/less/dialog/character-creation/selections-container.less index 643a5c29..0c13fae9 100644 --- a/styles/less/dialog/character-creation/selections-container.less +++ b/styles/less/dialog/character-creation/selections-container.less @@ -1,11 +1,111 @@ @import '../../utils/colors.less'; +.appTheme({}, { + &.daggerheart.dh-style.dialog.character-creation { + .setup-tabs button { + background-image: url(../assets/parchments/dh-parchment-dark.png); + } + + nav a .descriptor { + background-image: url(../assets/parchments/dh-parchment-dark.png); + } + + .main-selections-container { + .ancestry-mixed-controller label { + background-image: url(../assets/parchments/dh-parchment-dark.png); + } + + .selections-container + .ancestry-preview-info-container + .ancestry-preview-features + .ancestry-preview-feature { + background-image: url(../assets/parchments/dh-parchment-light.png); + } + + .traits-container { + .suggested-traits-container .suggested-trait-container { + background-image: url('../assets/parchments/dh-parchment-dark.png'); + } + + .traits-inner-container .trait-container { + background: url('../assets/svg/trait-shield-light.svg') no-repeat; + + div { + filter: none; + text-shadow: none; + } + } + } + } + } +}); + .daggerheart.dh-style.dialog.character-creation { + .setup-tabs { + display: flex; + justify-content: center; + + button { + background-image: url(../assets/parchments/dh-parchment-light.png); + border-radius: 6px; + border-color: light-dark(@dark-blue, @golden); + color: light-dark(@beige, @dark); + } + } + .main-selections-container { display: flex; flex-direction: column; gap: 4px; + .ancestry-mixed-controller { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + gap: 4px; + margin-bottom: 8px; + + &.active { + label { + opacity: 1; + } + } + + label { + position: absolute; + font-size: 18px; + font-weight: bold; + padding: 0 2px; + background-image: url(../assets/parchments/dh-parchment-light.png); + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + color: light-dark(@beige, @dark); + opacity: 0.4; + cursor: pointer; + display: flex; + align-items: center; + gap: 4px; + } + + input { + width: 50%; + } + } + + .ancestry-name { + display: flex; + justify-content: center; + width: 100%; + margin-bottom: 8px; + + input { + width: 50%; + text-align: center; + } + } + .selections-container { width: 140px; display: flex; @@ -15,6 +115,43 @@ .card-preview-container { border-color: light-dark(@dark-blue, @golden); } + + .ancestry-preview-info-container { + display: flex; + flex-direction: column; + gap: 4px; + width: 100%; + + .ancestry-preview-label { + text-align: center; + font-weight: bold; + } + + .ancestry-preview-features { + display: flex; + flex-direction: column; + justify-content: end; + gap: 2px; + width: 100%; + padding: 0 2px 2px 2px; + + .ancestry-preview-feature { + flex: 1; + font-size: 14px; + white-space: wrap; + padding: 0 2px; + border: 1px solid light-dark(@golden, @dark-blue); + border-radius: 6px; + background-image: url(../assets/parchments/dh-parchment-dark.png); + color: light-dark(@dark, @beige); + height: min-content; + + &.inactive { + opacity: 0.2; + } + } + } + } } .selections-outer-container { @@ -27,6 +164,10 @@ border-radius: 8px; border-color: light-dark(@dark-blue, @golden); + &.inactive { + opacity: 0.2; + } + legend { margin-left: auto; margin-right: auto; @@ -44,6 +185,7 @@ legend { font-size: 20px; + white-space: nowrap; } .action-button { @@ -87,21 +229,38 @@ } .traits-inner-container { + width: 100%; display: flex; + align-items: center; justify-content: space-evenly; gap: 8px; .trait-container { - border: 1px solid light-dark(@dark-blue, @golden); - padding: 0 4px; + width: 60px; + height: 60px; + background: url(../assets/svg/trait-shield.svg) no-repeat; + + div { + filter: drop-shadow(0 0 3px black); + text-shadow: 0 0 3px black; + } + + select { + text-align: center; + width: 32px; + height: 24px; + position: relative; + top: 2px; + padding: 0; + } } } } .experiences-inner-container { display: flex; - justify-content: space-evenly; - text-align: center; + flex-direction: column; + gap: 8px; .experience-container { position: relative; diff --git a/templates/characterCreation/footer.hbs b/templates/characterCreation/footer.hbs index fb9b6a29..47321c7a 100644 --- a/templates/characterCreation/footer.hbs +++ b/templates/characterCreation/footer.hbs @@ -1,4 +1,8 @@ \ No newline at end of file diff --git a/templates/characterCreation/setupTabs/ancestry.hbs b/templates/characterCreation/setupTabs/ancestry.hbs new file mode 100644 index 00000000..1c87ca47 --- /dev/null +++ b/templates/characterCreation/setupTabs/ancestry.hbs @@ -0,0 +1,63 @@ +
    +
    +
    + {{localize "TYPES.Item.ancestry"}} + +
    + +
    + +
    + + +
    + +
    +
    + {{#> "systems/daggerheart/templates/components/card-preview.hbs" primaryAncestry altPartialBlock=true secondaryDisabled=secondaryAncestry.uuid mixedAncestry=mixedAncestry }} + {{#if uuid}} +
    +
    {{name}}
    +
    +
    {{system.primaryFeature.name}}
    +
    {{system.secondaryFeature.name}}
    +
    +
    + {{else}} + {{#if mixedAncestry}} + {{localize "DAGGERHEART.APPLICATIONS.CharacterCreation.selectPrimaryAncestry"}} + {{else}} + {{localize "DAGGERHEART.APPLICATIONS.CharacterCreation.selectAncestry"}} + {{/if}} + {{/if}} + {{/"systems/daggerheart/templates/components/card-preview.hbs"}} +
    + + {{#if mixedAncestry}} +
    + {{#> "systems/daggerheart/templates/components/card-preview.hbs" secondaryAncestry altPartialBlock=true }} + {{#if uuid}} +
    +
    {{name}}
    +
    +
    {{system.primaryFeature.name}}
    +
    {{system.secondaryFeature.name}}
    +
    +
    + {{else}} + {{localize "DAGGERHEART.APPLICATIONS.CharacterCreation.selectSecondaryAncestry"}} + {{/if}} + {{/"systems/daggerheart/templates/components/card-preview.hbs"}} +
    + {{/if}} +
    +
    +
    +
    \ No newline at end of file diff --git a/templates/characterCreation/setupTabs/class.hbs b/templates/characterCreation/setupTabs/class.hbs new file mode 100644 index 00000000..d2215b80 --- /dev/null +++ b/templates/characterCreation/setupTabs/class.hbs @@ -0,0 +1,24 @@ +
    +
    +
    + {{localize "TYPES.Item.class"}} +
    +
    + {{#> "systems/daggerheart/templates/components/card-preview.hbs" class }} + {{localize "DAGGERHEART.APPLICATIONS.CharacterCreation.selectClass"}} + {{/"systems/daggerheart/templates/components/card-preview.hbs"}} +
    + +
    + {{#> "systems/daggerheart/templates/components/card-preview.hbs" subclass disabled=(not class.img) }} + {{localize "DAGGERHEART.APPLICATIONS.CharacterCreation.selectSubclass"}} + {{/"systems/daggerheart/templates/components/card-preview.hbs"}} +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/templates/characterCreation/setupTabs/community.hbs b/templates/characterCreation/setupTabs/community.hbs new file mode 100644 index 00000000..32a0844c --- /dev/null +++ b/templates/characterCreation/setupTabs/community.hbs @@ -0,0 +1,18 @@ +
    +
    +
    + {{localize "TYPES.Item.community"}} +
    +
    + {{#> "systems/daggerheart/templates/components/card-preview.hbs" community }} + {{localize "DAGGERHEART.APPLICATIONS.CharacterCreation.selectCommunity"}} + {{/"systems/daggerheart/templates/components/card-preview.hbs"}} +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/templates/characterCreation/setupTabs/domainCards.hbs b/templates/characterCreation/setupTabs/domainCards.hbs new file mode 100644 index 00000000..298d8859 --- /dev/null +++ b/templates/characterCreation/setupTabs/domainCards.hbs @@ -0,0 +1,22 @@ +
    +
    +
    + {{localize "TYPES.Item.domainCard"}} +
    + {{#each domainCards as |domainCard id|}} +
    + {{#> "systems/daggerheart/templates/components/card-preview.hbs" domainCard }} + {{#each @root.class.system.domains }} +
    {{localize (concat "DAGGERHEART.GENERAL.Domain." this ".label")}}
    + {{/each}} + {{/"systems/daggerheart/templates/components/card-preview.hbs"}} +
    + {{/each}} +
    +
    +
    +
    \ No newline at end of file diff --git a/templates/characterCreation/setupTabs/experience.hbs b/templates/characterCreation/setupTabs/experience.hbs new file mode 100644 index 00000000..46c5b754 --- /dev/null +++ b/templates/characterCreation/setupTabs/experience.hbs @@ -0,0 +1,19 @@ +
    +
    +
    + {{localize "DAGGERHEART.APPLICATIONS.CharacterCreation.initialExperiences"}} {{experience.nrSelected}}/{{experience.nrTotal}} +
    + {{#each experience.values as |experience id|}} +
    + +
    {{numberFormat this.value sign=true}}
    +
    + {{/each}} +
    +
    +
    +
    \ No newline at end of file diff --git a/templates/characterCreation/setupTabs/traits.hbs b/templates/characterCreation/setupTabs/traits.hbs new file mode 100644 index 00000000..94f66452 --- /dev/null +++ b/templates/characterCreation/setupTabs/traits.hbs @@ -0,0 +1,32 @@ +
    +
    +
    + {{localize "DAGGERHEART.APPLICATIONS.CharacterCreation.traitIncreases"}} {{traits.nrSelected}}/{{traits.nrTotal}} +
    +
    + {{localize "DAGGERHEART.APPLICATIONS.CharacterCreation.suggestedTraits"}} +
    + {{#each suggestedTraits}} +
    {{this}}
    + {{/each}} +
    + +
    +
    + {{#each traits.values}} +
    +
    {{this.name}}
    + +
    + {{/each}} +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/templates/characterCreation/tabs/setup.hbs b/templates/characterCreation/tabs/setup.hbs index 3f21fbb4..1504898d 100644 --- a/templates/characterCreation/tabs/setup.hbs +++ b/templates/characterCreation/tabs/setup.hbs @@ -3,99 +3,11 @@ data-tab='{{tabs.setup.id}}' data-group='{{tabs.setup.group}}' > -
    -
    - {{localize "TYPES.Item.class"}} -
    -
    - {{#> "systems/daggerheart/templates/components/card-preview.hbs" class }} - {{localize "DAGGERHEART.APPLICATIONS.CharacterCreation.selectClass"}} - {{/"systems/daggerheart/templates/components/card-preview.hbs"}} -
    - -
    - {{#> "systems/daggerheart/templates/components/card-preview.hbs" subclass disabled=(not class.img) }} - {{localize "DAGGERHEART.APPLICATIONS.CharacterCreation.selectSubclass"}} - {{/"systems/daggerheart/templates/components/card-preview.hbs"}} -
    -
    -
    - - {{#if (gte visibility 2)}} -
    - {{localize "DAGGERHEART.APPLICATIONS.CharacterCreation.heritage"}} -
    -
    - {{#> "systems/daggerheart/templates/components/card-preview.hbs" ancestry }} - {{localize "DAGGERHEART.APPLICATIONS.CharacterCreation.selectAncestry"}} - {{/"systems/daggerheart/templates/components/card-preview.hbs"}} -
    - -
    - {{#> "systems/daggerheart/templates/components/card-preview.hbs" community }} - {{localize "DAGGERHEART.APPLICATIONS.CharacterCreation.selectCommunity"}} - {{/"systems/daggerheart/templates/components/card-preview.hbs"}} -
    -
    -
    - {{/if}} - - {{#if (gte visibility 3)}} -
    - {{localize "DAGGERHEART.APPLICATIONS.CharacterCreation.traitIncreases"}} {{traits.nrSelected}}/{{traits.nrTotal}} -
    -
    - {{localize "DAGGERHEART.APPLICATIONS.CharacterCreation.suggestedTraits"}} -
    - {{#each suggestedTraits}} -
    {{this}}
    - {{/each}} -
    - -
    -
    - {{#each traits.values}} -
    -
    {{this.name}}
    - -
    - {{/each}} -
    -
    -
    - {{/if}} - - {{#if (gte visibility 4)}} -
    - {{localize "DAGGERHEART.APPLICATIONS.CharacterCreation.initialExperiences"}} {{experience.nrSelected}}/{{experience.nrTotal}} -
    - {{#each experience.values as |experience id|}} -
    - -
    {{numberFormat this.value sign=true}}
    -
    - {{/each}} -
    -
    - {{/if}} - - {{#if (gte visibility 5)}} -
    - {{localize "TYPES.Item.domainCard"}} -
    - {{#each domainCards as |domainCard id|}} -
    - {{#> "systems/daggerheart/templates/components/card-preview.hbs" domainCard }} - {{#each @root.class.system.domains }} -
    {{localize (concat "DAGGERHEART.GENERAL.Domain." this ".label")}}
    - {{/each}} - {{/"systems/daggerheart/templates/components/card-preview.hbs"}} -
    - {{/each}} -
    -
    - {{/if}} -
    + \ No newline at end of file diff --git a/templates/components/card-preview.hbs b/templates/components/card-preview.hbs index da2abb77..08d4ca01 100644 --- a/templates/components/card-preview.hbs +++ b/templates/components/card-preview.hbs @@ -4,7 +4,13 @@ > {{#if this.img}} -
    {{this.name}}
    +
    + {{#if altPartialBlock}} + {{> @partial-block }} + {{else}} + {{this.name}} + {{/if}} +
    {{else}}
    diff --git a/templates/sheets/actors/character/header.hbs b/templates/sheets/actors/character/header.hbs index 9c72526e..a09063d4 100644 --- a/templates/sheets/actors/character/header.hbs +++ b/templates/sheets/actors/character/header.hbs @@ -15,7 +15,7 @@ {{#if (or document.system.needsCharacterSetup document.system.levelData.canLevelUp)}}
    -
    -
    - {{#if document.system.armor.system.marks}} -

    {{document.system.armor.system.marks.value}}/{{document.system.armorScore}}

    - {{else}} + {{#if document.system.armor.system.marks}} +
    +
    +

    +

    /

    +

    {{document.system.armorScore}}

    +
    + +
    +

    {{localize "DAGGERHEART.GENERAL.armorSlots"}}

    +
    +
    + {{else}} +
    +

    -

    - {{/if}} +
    +
    +

    {{localize "DAGGERHEART.GENERAL.armorSlots"}}

    +
    -
    -

    Armor Slots

    -
    -
    + {{/if}}