From ad9e0aa558899d74882342ba3a4447ee3029d028 Mon Sep 17 00:00:00 2001 From: Dapoulp <74197441+Dapoulp@users.noreply.github.com> Date: Thu, 17 Jul 2025 00:45:53 +0200 Subject: [PATCH 1/7] Feature/344 bardic rally (#363) * 2 * Dardic Rally Dice --- lang/en.json | 6 +- module/applications/dialogs/d20RollDialog.mjs | 1 + module/data/actor/character.mjs | 10 ++- module/data/fields/actorField.mjs | 7 +- module/dice/d20Roll.mjs | 10 ++- module/dice/dualityRoll.mjs | 76 +++++++++++++------ module/documents/token.mjs | 22 ++++++ templates/dialogs/dice-roll/rollSelection.hbs | 26 +++++-- templates/ui/chat/duality-roll.hbs | 35 ++++++++- 9 files changed, 153 insertions(+), 40 deletions(-) diff --git a/lang/en.json b/lang/en.json index 4af86abe..75f2b886 100755 --- a/lang/en.json +++ b/lang/en.json @@ -407,7 +407,11 @@ "rerollDice": "Reroll Dice" } }, - + "CLASS": { + "Feature": { + "rallyDice": "Bardic Rally Dice" + } + }, "CONFIG": { "ActionType": { "passive": "Passive", diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index 7987dd6b..67ca77e6 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -89,6 +89,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio if (this.roll) { context.roll = this.roll; context.rollType = this.roll?.constructor.name; + context.rallyDie = this.roll.rallyChoices; context.experiences = Object.keys(this.config.data.experiences).map(id => ({ id, ...this.config.data.experiences[id] diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 804eabec..20bada01 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -102,7 +102,7 @@ export default class DhCharacter extends BaseDataActor { physical: bonusField('DAGGERHEART.GENERAL.Damage.physicalDamage'), magical: bonusField('DAGGERHEART.GENERAL.Damage.magicalDamage'), primaryWeapon: bonusField('DAGGERHEART.GENERAL.Damage.primaryWeapon'), - secondaryWeapon: bonusField('DAGGERHEART.GENERAL.Damage.primaryWeapon') + secondaryWeapon: bonusField('DAGGERHEART.GENERAL.Damage.secondaryWeapon') }), healing: bonusField('DAGGERHEART.GENERAL.Healing.healingAmount'), range: new fields.SchemaField({ @@ -121,7 +121,13 @@ export default class DhCharacter extends BaseDataActor { initial: 0, label: 'DAGGERHEART.GENERAL.Range.other' }) - }) + }), + rally: new fields.ArrayField( + new fields.StringField(), + { + label: 'DAGGERHEART.CLASS.Feature.rallyDice' + } + ) }), companion: new ForeignDocumentUUIDField({ type: 'Actor', nullable: true, initial: null }), rules: new fields.SchemaField({ diff --git a/module/data/fields/actorField.mjs b/module/data/fields/actorField.mjs index fe00e251..047b6f4f 100644 --- a/module/data/fields/actorField.mjs +++ b/module/data/fields/actorField.mjs @@ -25,8 +25,11 @@ const stressDamageReductionRule = localizationPath => const bonusField = label => new fields.SchemaField({ - bonus: new fields.NumberField({ integer: true, initial: 0, label }), - dice: new fields.ArrayField(new fields.StringField()) + bonus: new fields.NumberField({ integer: true, initial: 0, label: `${game.i18n.localize(label)} Value` }), + dice: new fields.ArrayField( + new fields.StringField(), + { label: `${game.i18n.localize(label)} Dice` } + ) }); export { attributeField, resourceField, stressDamageReductionRule, bonusField }; diff --git a/module/dice/d20Roll.mjs b/module/dice/d20Roll.mjs index 0c29fc42..58d45f95 100644 --- a/module/dice/d20Roll.mjs +++ b/module/dice/d20Roll.mjs @@ -39,11 +39,13 @@ export default class D20Roll extends DHRoll { } get hasAdvantage() { - return this.options.roll.advantage === this.constructor.ADV_MODE.ADVANTAGE; + const adv = this.options.roll.advantage.type ?? this.options.roll.advantage; + return adv === this.constructor.ADV_MODE.ADVANTAGE; } get hasDisadvantage() { - return this.options.roll.advantage === this.constructor.ADV_MODE.DISADVANTAGE; + const adv = this.options.roll.advantage.type ?? this.options.roll.advantage; + return adv === this.constructor.ADV_MODE.DISADVANTAGE; } static applyKeybindings(config) { @@ -90,8 +92,8 @@ export default class D20Roll extends DHRoll { configureModifiers() { this.applyAdvantage(); - - this.baseTerms = foundry.utils.deepClone(this.terms); + + this.baseTerms = foundry.utils.deepClone(this.dice); this.options.roll.modifiers = this.applyBaseBonus(); diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index d983b2d6..5fd71e6c 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -4,9 +4,12 @@ import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs'; export default class DualityRoll extends D20Roll { _advantageFaces = 6; + _advantageNumber = 1; + _rallyIndex; constructor(formula, data = {}, options = {}) { super(formula, data, options); + this.rallyChoices = this.setRallyChoices(); } static messageType = 'dualityRoll'; @@ -51,6 +54,35 @@ export default class DualityRoll extends D20Roll { this._advantageFaces = this.getFaces(faces); } + get advantageNumber() { + return this._advantageNumber; + } + + set advantageNumber(value) { + this._advantageNumber = Number(value); + } + + setRallyChoices() { + return this.data?.parent?.effects.reduce((a,c) => { + const change = c.changes.find(ch => ch.key === 'system.bonuses.rally'); + if(change) a.push({ value: c.id, label: change.value }); + return a; + }, []); + } + + get dRally() { + if(!this.rallyFaces) return null; + if(this.hasDisadvantage || this.hasAdvantage) + return this.dice[3]; + else + return this.dice[2]; + } + + get rallyFaces() { + const rallyChoice = this.rallyChoices?.find(r => r.value === this._rallyIndex)?.label; + return rallyChoice ? this.getFaces(rallyChoice) : null; + } + get isCritical() { if (!this.dHope._evaluated || !this.dFear._evaluated) return; return this.dHope.total === this.dFear.total; @@ -66,10 +98,6 @@ export default class DualityRoll extends D20Roll { return this.dHope.total < this.dFear.total; } - get hasBarRally() { - return null; - } - get totalLabel() { const label = this.withHope ? 'DAGGERHEART.GENERAL.hope' @@ -98,24 +126,20 @@ export default class DualityRoll extends D20Roll { } applyAdvantage() { - const dieFaces = this.advantageFaces, - bardRallyFaces = this.hasBarRally, - advDie = new foundry.dice.terms.Die({ faces: dieFaces }); - if (this.hasAdvantage || this.hasDisadvantage || bardRallyFaces) - this.terms.push(new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' })); - if (bardRallyFaces) { - const rallyDie = new foundry.dice.terms.Die({ faces: bardRallyFaces }); - if (this.hasAdvantage) { - this.terms.push( - new foundry.dice.terms.PoolTerm({ - terms: [advDie.formula, rallyDie.formula], - modifiers: ['kh'] - }) - ); - } else if (this.hasDisadvantage) { - this.terms.push(advDie, new foundry.dice.terms.OperatorTerm({ operator: '+' }), rallyDie); - } - } else if (this.hasAdvantage || this.hasDisadvantage) this.terms.push(advDie); + if (this.hasAdvantage || this.hasDisadvantage) { + const dieFaces = this.advantageFaces, + advDie = new foundry.dice.terms.Die({ faces: dieFaces, number: this.advantageNumber }); + if(this.advantageNumber > 1) advDie.modifiers = ['kh']; + this.terms.push( + new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }), + advDie + ); + } + if(this.rallyFaces) + this.terms.push( + new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }), + new foundry.dice.terms.Die({ faces: this.rallyFaces }) + ); } applyBaseBonus() { @@ -138,6 +162,7 @@ export default class DualityRoll extends D20Roll { static postEvaluate(roll, config = {}) { super.postEvaluate(roll, config); + config.roll.hope = { dice: roll.dHope.denomination, value: roll.dHope.total @@ -146,12 +171,19 @@ export default class DualityRoll extends D20Roll { dice: roll.dFear.denomination, value: roll.dFear.total }; + config.roll.rally = { + dice: roll.dRally?.denomination, + value: roll.dRally?.total + }; config.roll.result = { duality: roll.withHope ? 1 : roll.withFear ? -1 : 0, total: roll.dHope.total + roll.dFear.total, label: roll.totalLabel }; + if(roll._rallyIndex && roll.data?.parent) + roll.data.parent.deleteEmbeddedDocuments('ActiveEffect', [roll._rallyIndex]); + setDiceSoNiceForDualityRoll(roll, config.roll.advantage.type); } } diff --git a/module/documents/token.mjs b/module/documents/token.mjs index 4592c843..3e7b49ea 100644 --- a/module/documents/token.mjs +++ b/module/documents/token.mjs @@ -32,4 +32,26 @@ export default class DHToken extends TokenDocument { return bars.concat(values); } + + static _getTrackedAttributesFromSchema(schema, _path=[]) { + const attributes = {bar: [], value: []}; + for ( const [name, field] of Object.entries(schema.fields) ) { + const p = _path.concat([name]); + if ( field instanceof foundry.data.fields.NumberField ) attributes.value.push(p); + if ( field instanceof foundry.data.fields.ArrayField ) attributes.value.push(p); + const isSchema = field instanceof foundry.data.fields.SchemaField; + const isModel = field instanceof foundry.data.fields.EmbeddedDataField; + if ( isSchema || isModel ) { + const schema = isModel ? field.model.schema : field; + const isBar = schema.has && schema.has("value") && schema.has("max"); + if ( isBar ) attributes.bar.push(p); + else { + const inner = this.getTrackedAttributes(schema, p); + attributes.bar.push(...inner.bar); + attributes.value.push(...inner.value); + } + } + } + return attributes; + } } diff --git a/templates/dialogs/dice-roll/rollSelection.hbs b/templates/dialogs/dice-roll/rollSelection.hbs index 0d2f3f48..b4c7ccac 100644 --- a/templates/dialogs/dice-roll/rollSelection.hbs +++ b/templates/dialogs/dice-roll/rollSelection.hbs @@ -88,7 +88,7 @@ {{/if}} {{/each}} -
+
Modifiers
- {{#unless (eq @root.rollType 'D20Roll')}} - + {{#times 10}} + + {{/times}} + + - {{/unless}} -
- + + {{/unless}} + {{#if @root.rallyDie.length}} + {{localize "DAGGERHEART.CLASS.Feature.rallyDice"}} + + {{/if}} + {{#if (eq @root.rollType 'DualityRoll')}}Situational Bonus{{/if}} +
{{/unless}} Formula: {{@root.formula}} diff --git a/templates/ui/chat/duality-roll.hbs b/templates/ui/chat/duality-roll.hbs index 66d32e95..8b44039b 100644 --- a/templates/ui/chat/duality-roll.hbs +++ b/templates/ui/chat/duality-roll.hbs @@ -16,6 +16,11 @@ {{localize "DAGGERHEART.GENERAL.Disadvantage.full"}} {{/if}} + {{#if roll.rally.dice}} +
+ {{localize "DAGGERHEART.CLASS.Feature.rallyDice"}} {{roll.rally.dice}} +
+ {{/if}}
{{roll.formula}}
@@ -38,7 +43,7 @@
{{localize "DAGGERHEART.GENERAL.hope"}}
- +
{{roll.hope.value}}
@@ -49,7 +54,7 @@
{{localize "DAGGERHEART.GENERAL.fear"}}
- +
{{roll.fear.value}}
@@ -72,7 +77,7 @@
- +
{{roll.advantage.value}}
@@ -82,6 +87,30 @@
{{/if}} + {{#if roll.rally.dice}} +
+
+ + 1{{roll.rally.dice}} + + {{roll.rally.value}} +
+
+
    +
  1. +
    +
    +
    + +
    +
    {{roll.rally.value}}
    +
    +
    +
  2. +
+
+
+ {{/if}} {{#each roll.extra as | extra | }}
From d7e024be02b3b938c0dc898ed0fa8eeb2fea8954 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Thu, 17 Jul 2025 19:01:15 +0200 Subject: [PATCH 2/7] [Feature] Advantage/Disadvantage Tooltips (#362) * Added advantage/disadvantageSource to Character model. It's shown from a tooltip icon on rolls * Added support for beastform advantageOn --- module/data/actor/character.mjs | 2 ++ module/data/item/beastform.mjs | 1 + module/documents/tooltipManager.mjs | 18 ++++++++++++++++++ .../less/dialog/dice-roll/roll-selection.less | 4 ++++ templates/dialogs/dice-roll/rollSelection.hbs | 6 ++++++ templates/ui/tooltip/advantage.hbs | 5 +++++ 6 files changed, 36 insertions(+) create mode 100644 templates/ui/tooltip/advantage.hbs diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 20bada01..6fe66a53 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -87,6 +87,8 @@ export default class DhCharacter extends BaseDataActor { value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }), subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }) }), + advantageSources: new fields.ArrayField(new fields.StringField()), + disadvantageSources: new fields.ArrayField(new fields.StringField()), levelData: new fields.EmbeddedDataField(DhLevelData), bonuses: new fields.SchemaField({ roll: new fields.SchemaField({ diff --git a/module/data/item/beastform.mjs b/module/data/item/beastform.mjs index b7ea5cb9..d19521d0 100644 --- a/module/data/item/beastform.mjs +++ b/module/data/item/beastform.mjs @@ -69,6 +69,7 @@ export default class DHBeastform extends BaseDataItem { const beastformEffect = this.parent.effects.find(x => x.type === 'beastform'); await beastformEffect.updateSource({ + changes: [...beastformEffect.changes, { key: 'system.advantageSources', mode: 2, value: this.advantageOn }], system: { characterTokenData: { tokenImg: this.parent.parent.prototypeToken.texture.src, diff --git a/module/documents/tooltipManager.mjs b/module/documents/tooltipManager.mjs index d9444207..f24823f4 100644 --- a/module/documents/tooltipManager.mjs +++ b/module/documents/tooltipManager.mjs @@ -21,6 +21,24 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti this.tooltip.innerHTML = html; options.direction = this._determineItemTooltipDirection(element); } + } else { + const isAdvantage = element.dataset.tooltip?.startsWith('#advantage#'); + const isDisadvantage = element.dataset.tooltip?.startsWith('#disadvantage#'); + if (isAdvantage || isDisadvantage) { + const actorUuid = element.dataset.tooltip.slice(isAdvantage ? 11 : 14); + const actor = await foundry.utils.fromUuid(actorUuid); + + if (actor) { + html = await foundry.applications.handlebars.renderTemplate( + `systems/daggerheart/templates/ui/tooltip/advantage.hbs`, + { + sources: isAdvantage ? actor.system.advantageSources : actor.system.disadvantageSources + } + ); + + this.tooltip.innerHTML = html; + } + } } super.activate(element, { ...options, html: html }); diff --git a/styles/less/dialog/dice-roll/roll-selection.less b/styles/less/dialog/dice-roll/roll-selection.less index 55db8bb7..af6c3c20 100644 --- a/styles/less/dialog/dice-roll/roll-selection.less +++ b/styles/less/dialog/dice-roll/roll-selection.less @@ -100,6 +100,10 @@ font-size: 14px; line-height: 17px; } + + .advantage-chip-tooltip { + pointer-events: all; + } } .advantage-chip { diff --git a/templates/dialogs/dice-roll/rollSelection.hbs b/templates/dialogs/dice-roll/rollSelection.hbs index b4c7ccac..7c894fd8 100644 --- a/templates/dialogs/dice-roll/rollSelection.hbs +++ b/templates/dialogs/dice-roll/rollSelection.hbs @@ -98,6 +98,9 @@ {{/if}} {{localize "DAGGERHEART.GENERAL.Advantage.full"}} + {{#if @root.rollConfig.data.advantageSources.length}} + + {{/if}}
{{#unless (eq @root.rollType 'D20Roll')}} diff --git a/templates/ui/tooltip/advantage.hbs b/templates/ui/tooltip/advantage.hbs new file mode 100644 index 00000000..886f336d --- /dev/null +++ b/templates/ui/tooltip/advantage.hbs @@ -0,0 +1,5 @@ +
+ {{#each sources as | source |}} +
{{{source}}}
+ {{/each}} +
\ No newline at end of file From f15483c7229a782c6b3b8f9be1d2d0f79673e105 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Thu, 17 Jul 2025 19:03:54 +0200 Subject: [PATCH 3/7] Feature/349 items actions macro use (#356) * Added itemUse macro on drag to hotbar * Fixed item.type logic * Added support for actionMacro drag from items * Added MacroDrag for Attacks * Fixed so UseItem macros get the img set --- daggerheart.mjs | 1 + lang/en.json | 6 +- module/applications/sheets/api/base-actor.mjs | 25 +++- module/applications/sheets/api/base-item.mjs | 20 ++- module/applications/ui/_module.mjs | 1 + module/applications/ui/hotbar.mjs | 129 ++++++++++++++++++ module/data/countdowns.mjs | 4 +- 7 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 module/applications/ui/hotbar.mjs diff --git a/daggerheart.mjs b/daggerheart.mjs index 0d78e8fe..22586e90 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -140,6 +140,7 @@ Hooks.once('init', () => { CONFIG.Combat.documentClass = documents.DhpCombat; CONFIG.ui.combat = applications.ui.DhCombatTracker; CONFIG.ui.chat = applications.ui.DhChatLog; + CONFIG.ui.hotbar = applications.ui.DhHotbar; CONFIG.Token.rulerClass = placeables.DhTokenRuler; CONFIG.ui.resources = applications.ui.DhFearTracker; diff --git a/lang/en.json b/lang/en.json index 75f2b886..3456b6b5 100755 --- a/lang/en.json +++ b/lang/en.json @@ -1537,7 +1537,11 @@ "noAvailableArmorMarks": "You have no more available armor marks", "notEnoughStress": "You don't have enough stress", "damageIgnore": "{character} did not take damage", - "featureIsMissing": "Feature is missing" + "featureIsMissing": "Feature is missing", + "actionIsMissing": "Action is missing", + "attackIsMissing": "Attack is missing", + "unownedActionMacro": "Cannot make a Use macro for an Action not on your character", + "unownedAttackMacro": "Cannot make a Use macro for an Attack that doesn't belong to one of your characters" }, "Tooltip": { "openItemWorld": "Open Item World", diff --git a/module/applications/sheets/api/base-actor.mjs b/module/applications/sheets/api/base-actor.mjs index 7102fa1c..78df0aac 100644 --- a/module/applications/sheets/api/base-actor.mjs +++ b/module/applications/sheets/api/base-actor.mjs @@ -23,7 +23,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { actions: { openSettings: DHBaseActorSheet.#openSettings }, - dragDrop: [] + dragDrop: [{ dragSelector: '.inventory-item[data-type="attack"]', dropSelector: null }] }; /**@type {typeof DHBaseActorSettings}*/ @@ -49,4 +49,27 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { static async #openSettings() { await this.settingSheet.render({ force: true }); } + + /* -------------------------------------------- */ + /* Application Drag/Drop */ + /* -------------------------------------------- */ + + /** + * On dragStart on the item. + * @param {DragEvent} event - The drag event + */ + async _onDragStart(event) { + const attackItem = event.currentTarget.closest('.inventory-item[data-type="attack"]'); + + if (attackItem) { + const attackData = { + type: 'Attack', + actorUuid: this.document.uuid, + img: this.document.system.attack.img, + fromInternal: true + }; + event.dataTransfer.setData('text/plain', JSON.stringify(attackData)); + event.dataTransfer.setDragImage(attackItem.querySelector('img'), 60, 0); + } + } } diff --git a/module/applications/sheets/api/base-item.mjs b/module/applications/sheets/api/base-item.mjs index 0c25a44d..723b4802 100644 --- a/module/applications/sheets/api/base-item.mjs +++ b/module/applications/sheets/api/base-item.mjs @@ -30,7 +30,8 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { }, dragDrop: [ { dragSelector: null, dropSelector: '.tab.features .drop-section' }, - { dragSelector: '.feature-item', dropSelector: null } + { dragSelector: '.feature-item', dropSelector: null }, + { dragSelector: '.action-item', dropSelector: null } ] }; @@ -258,6 +259,23 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { const featureData = { type: 'Item', data: { ...feature.toObject(), _id: null }, fromInternal: true }; event.dataTransfer.setData('text/plain', JSON.stringify(featureData)); event.dataTransfer.setDragImage(featureItem.querySelector('img'), 60, 0); + } else { + const actionItem = event.currentTarget.closest('.action-item'); + if (actionItem) { + const action = this.document.system.actions[actionItem.dataset.index]; + if (!action) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.actionIsMissing')); + return; + } + + const actionData = { + type: 'Action', + data: { ...action.toObject(), id: action.id, itemUuid: this.document.uuid }, + fromInternal: true + }; + event.dataTransfer.setData('text/plain', JSON.stringify(actionData)); + event.dataTransfer.setDragImage(actionItem.querySelector('img'), 60, 0); + } } } diff --git a/module/applications/ui/_module.mjs b/module/applications/ui/_module.mjs index f1c32840..6a17a61e 100644 --- a/module/applications/ui/_module.mjs +++ b/module/applications/ui/_module.mjs @@ -2,3 +2,4 @@ export { default as DhChatLog } from './chatLog.mjs'; export { default as DhCombatTracker } from './combatTracker.mjs'; export * as DhCountdowns from './countdowns.mjs'; export { default as DhFearTracker } from './fearTracker.mjs'; +export { default as DhHotbar } from './hotbar.mjs'; diff --git a/module/applications/ui/hotbar.mjs b/module/applications/ui/hotbar.mjs new file mode 100644 index 00000000..b4ebc05c --- /dev/null +++ b/module/applications/ui/hotbar.mjs @@ -0,0 +1,129 @@ +export default class DhHotbar extends foundry.applications.ui.Hotbar { + constructor(options) { + super(options); + + this.setupHooks(); + } + + static async useItem(uuid) { + const item = await fromUuid(uuid); + if (!item) { + return ui.notifications.warn('WARNING.ObjectDoesNotExist', { + format: { + name: game.i18n.localize('Document'), + identifier: uuid + } + }); + } + + await item.use({}); + } + + static async useAction(itemUuid, actionId) { + const item = await foundry.utils.fromUuid(itemUuid); + if (!item) { + return ui.notifications.warn('WARNING.ObjectDoesNotExist', { + format: { + name: game.i18n.localize('Document'), + identifier: itemUuid + } + }); + } + + const action = item.system.actions.find(x => x.id === actionId); + if (!action) { + return ui.notifications.warn('DAGGERHEART.UI.Notifications.actionIsMissing'); + } + + await action.use({}); + } + + static async useAttack(actorUuid) { + const actor = await foundry.utils.fromUuid(actorUuid); + if (!actor) { + return ui.notifications.warn('WARNING.ObjectDoesNotExist', { + format: { + name: game.i18n.localize('Document'), + identifier: actorUuid + } + }); + } + + const attack = actor.system.attack; + if (!attack) { + return ui.notifications.warn('DAGGERHEART.UI.Notifications.attackIsMissing'); + } + + await attack.use({}); + } + + setupHooks() { + Hooks.on('hotbarDrop', (bar, data, slot) => { + if (data.type === 'Item') { + const item = foundry.utils.fromUuidSync(data.uuid); + if (item.uuid.startsWith('Compendium') || !item.isOwned || !item.isOwner) return true; + + switch (item.type) { + case 'ancestry': + case 'community': + case 'class': + case 'subclass': + return true; + default: + this.createItemMacro(item, slot); + return false; + } + } else if (data.type === 'Action') { + const item = foundry.utils.fromUuidSync(data.data.itemUuid); + if (item.uuid.startsWith('Compendium')) return true; + if (!item.isOwned || !item.isOwner) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.unownedActionMacro')); + return false; + } + + this.createActionMacro(data, slot); + return false; + } else if (data.type === 'Attack') { + const actor = foundry.utils.fromUuidSync(data.actorUuid); + if (actor.uuid.startsWith('Compendium')) return true; + if (!actor.isOwner) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.unownedAttackMacro')); + return false; + } + + this.createAttackMacro(data, slot); + return false; + } + }); + } + + async createItemMacro(data, slot) { + const macro = await Macro.implementation.create({ + name: `${game.i18n.localize('Display')} ${name}`, + type: CONST.MACRO_TYPES.SCRIPT, + img: data.img, + command: `await game.system.api.applications.ui.DhHotbar.useItem("${data.uuid}");` + }); + await game.user.assignHotbarMacro(macro, slot); + } + + async createActionMacro(data, slot) { + const macro = await Macro.implementation.create({ + name: `${game.i18n.localize('Display')} ${name}`, + type: CONST.MACRO_TYPES.SCRIPT, + img: data.data.img, + command: `await game.system.api.applications.ui.DhHotbar.useAction("${data.data.itemUuid}", "${data.data.id}");` + }); + await game.user.assignHotbarMacro(macro, slot); + } + + async createAttackMacro(data, slot) { + const macro = await Macro.implementation.create({ + name: `${game.i18n.localize('Display')} ${name}`, + type: CONST.MACRO_TYPES.SCRIPT, + img: data.img, + command: `await game.system.api.applications.ui.DhHotbar.useAttack("${data.actorUuid}");` + }); + await game.user.assignHotbarMacro(macro, slot); + } +} diff --git a/module/data/countdowns.mjs b/module/data/countdowns.mjs index 881ecf20..34e8b790 100644 --- a/module/data/countdowns.mjs +++ b/module/data/countdowns.mjs @@ -135,8 +135,8 @@ export const registerCountdownHooks = () => { if (application) { foundry.applications.instances.get(application)?.render(); } else { - foundry.applications.instances.get('narrative-countdowns').render(); - foundry.applications.instances.get('encounter-countdowns').render(); + foundry.applications.instances.get('narrative-countdowns')?.render(); + foundry.applications.instances.get('encounter-countdowns')?.render(); } return false; From 1d5e26728540b065034a4e9551dad65b28adef91 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Thu, 17 Jul 2025 19:07:11 +0200 Subject: [PATCH 4/7] Improved the datastructure some to avoid errors and simplify useage (#361) --- module/applications/dialogs/resourceDiceDialog.mjs | 2 +- module/data/actor/character.mjs | 10 +++++----- module/data/item/base.mjs | 7 +++++-- templates/dialogs/dice-roll/resourceDice.hbs | 8 ++++---- templates/sheets/global/partials/item-resource.hbs | 2 +- templates/sheets/global/partials/resource-section.hbs | 2 +- 6 files changed, 17 insertions(+), 14 deletions(-) diff --git a/module/applications/dialogs/resourceDiceDialog.mjs b/module/applications/dialogs/resourceDiceDialog.mjs index b79ff895..8205dee5 100644 --- a/module/applications/dialogs/resourceDiceDialog.mjs +++ b/module/applications/dialogs/resourceDiceDialog.mjs @@ -67,7 +67,7 @@ export default class ResourceDiceDialog extends HandlebarsApplicationMixin(Appli 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 diceFormula = `${max}${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 })); diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 6fe66a53..6b0a2fd7 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -252,13 +252,13 @@ export default class DhCharacter extends BaseDataActor { features = []; for (let item of this.parent.items) { - if (item.system.type === CONFIG.DH.ITEM.featureTypes.ancestry.id) { + if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.ancestry.id) { ancestryFeatures.push(item); - } else if (item.system.type === CONFIG.DH.ITEM.featureTypes.community.id) { + } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.community.id) { communityFeatures.push(item); - } else if (item.system.type === CONFIG.DH.ITEM.featureTypes.class.id) { + } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.class.id) { classFeatures.push(item); - } else if (item.system.type === CONFIG.DH.ITEM.featureTypes.subclass.id) { + } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.subclass.id) { const subclassState = this.class.subclass.system.featureState; const identifier = item.system.identifier; if ( @@ -268,7 +268,7 @@ export default class DhCharacter extends BaseDataActor { ) { subclassFeatures.push(item); } - } else if (item.system.type === CONFIG.DH.ITEM.featureTypes.companion.id) { + } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.companion.id) { companionFeatures.push(item); } else if (item.type === 'feature' && !item.system.type) { features.push(item); diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index e99c85c3..24e5e0cc 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -53,11 +53,14 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { }), diceStates: new fields.TypedObjectField( new fields.SchemaField({ - value: new fields.NumberField({ integer: true, nullable: true, initial: null }), + value: new fields.NumberField({ integer: true, initial: 1, min: 1 }), used: new fields.BooleanField({ initial: false }) }) ), - dieFaces: new fields.StringField({ initial: '4' }) + dieFaces: new fields.StringField({ + choices: CONFIG.DH.GENERAL.diceTypes, + initial: CONFIG.DH.GENERAL.diceTypes.d4 + }) }, { nullable: true, initial: null } ); diff --git a/templates/dialogs/dice-roll/resourceDice.hbs b/templates/dialogs/dice-roll/resourceDice.hbs index 33c93386..bebe8f4e 100644 --- a/templates/dialogs/dice-roll/resourceDice.hbs +++ b/templates/dialogs/dice-roll/resourceDice.hbs @@ -3,14 +3,14 @@ {{#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/global/partials/item-resource.hbs b/templates/sheets/global/partials/item-resource.hbs index 9b92dea0..d90f0b3f 100644 --- a/templates/sheets/global/partials/item-resource.hbs +++ b/templates/sheets/global/partials/item-resource.hbs @@ -10,7 +10,7 @@
- + {{#if state.used}}{{/if}}
diff --git a/templates/sheets/global/partials/resource-section.hbs b/templates/sheets/global/partials/resource-section.hbs index f0d322d3..ab329efc 100644 --- a/templates/sheets/global/partials/resource-section.hbs +++ b/templates/sheets/global/partials/resource-section.hbs @@ -19,7 +19,7 @@ {{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.dieFaces value=source.system.resource.dieFaces localize=true blank=false}} {{formGroup systemFields.resource.fields.max value=source.system.resource.max label="DAGGERHEART.ITEMS.FIELDS.resource.amount.label" localize=true}} {{/if}} From 0cc1597dfe1a37566180dca810f892c28556610e Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Thu, 17 Jul 2025 19:43:35 +0200 Subject: [PATCH 5/7] [Fix] Class Feature Change (#364) * Changed to just use a features field * Subclass now uses a simple Features field --- lang/en.json | 11 ++- .../characterCreation/characterCreation.mjs | 4 +- module/applications/sheets/items/ancestry.mjs | 25 ++----- module/applications/sheets/items/class.mjs | 48 ++++++++----- module/applications/sheets/items/subclass.mjs | 69 +++++++++++++++---- module/config/actorConfig.mjs | 2 +- module/config/itemConfig.mjs | 7 +- module/data/actor/character.mjs | 17 ++--- module/data/item/class.mjs | 17 +++-- module/data/item/domainCard.mjs | 1 - module/data/item/subclass.mjs | 22 +++--- .../sheets/items/domainCard/settings.hbs | 2 - templates/sheets/items/subclass/features.hbs | 30 ++++---- 13 files changed, 158 insertions(+), 97 deletions(-) diff --git a/lang/en.json b/lang/en.json index 3456b6b5..80cc67f7 100755 --- a/lang/en.json +++ b/lang/en.json @@ -1325,8 +1325,8 @@ }, "DomainCard": { "type": "Type", - "foundation": "Foundation", "recallCost": "Recall Cost", + "foundationTitle": "Foundation", "specializationTitle": "Specialization", "masteryTitle": "Mastery" }, @@ -1541,7 +1541,14 @@ "actionIsMissing": "Action is missing", "attackIsMissing": "Attack is missing", "unownedActionMacro": "Cannot make a Use macro for an Action not on your character", - "unownedAttackMacro": "Cannot make a Use macro for an Attack that doesn't belong to one of your characters" + "unownedAttackMacro": "Cannot make a Use macro for an Attack that doesn't belong to one of your characters", + "featureNotHope": "This feature is used as something else than a Hope feature and cannot be used here.", + "featureNotClass": "This feature is used as something else than a Class feature and cannot be used here.", + "featureNotPrimary": "This feature is used as something else than a Primary feature and cannot be used here.", + "featureNotSecondary": "This feature is used as something else than a Secondary feature and cannot be used here.", + "featureNotFoundation": "This feature is used as something else than a Foundation feature and cannot be used here.", + "featureNotSpecialization": "This feature is used as something else than a Specialization feature and cannot be used here.", + "featureNotMastery": "This feature is used as something else than a Mastery feature and cannot be used here." }, "Tooltip": { "openItemWorld": "Open Item World", diff --git a/module/applications/characterCreation/characterCreation.mjs b/module/applications/characterCreation/characterCreation.mjs index ed0ee5a7..b8759cc5 100644 --- a/module/applications/characterCreation/characterCreation.mjs +++ b/module/applications/characterCreation/characterCreation.mjs @@ -506,9 +506,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl name: this.setup.ancestryName ?? this.setup.primaryAncestry.name, system: { ...this.setup.primaryAncestry.system, - features: [primaryAncestryFeature.uuid, secondaryAncestryFeature.uuid], - primaryFeature: primaryAncestryFeature.uuid, - secondaryFeature: secondaryAncestryFeature.uuid + features: [primaryAncestryFeature.uuid, secondaryAncestryFeature.uuid] } }; diff --git a/module/applications/sheets/items/ancestry.mjs b/module/applications/sheets/items/ancestry.mjs index bd9e3792..de55301d 100644 --- a/module/applications/sheets/items/ancestry.mjs +++ b/module/applications/sheets/items/ancestry.mjs @@ -63,22 +63,8 @@ export default class AncestrySheet extends DHHeritageSheet { 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 }); + if (feature) await feature.update({ 'system.subType': null }); await this.document.update({ 'system.features': this.document.system.features.filter(x => x && x.uuid !== feature.uuid).map(x => x.uuid) }); @@ -94,15 +80,18 @@ export default class AncestrySheet extends DHHeritageSheet { */ 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 }); + if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes[subType]) { + const error = subType === 'primary' ? 'featureNotPrimary' : 'featureNotSecondary'; + ui.notifications.warn(game.i18n.localize(`DAGGERHEART.UI.Notifications.${error}`)); + return; + } + 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 b49105be..3a34bcec 100644 --- a/module/applications/sheets/items/class.mjs +++ b/module/applications/sheets/items/class.mjs @@ -78,6 +78,7 @@ export default class ClassSheet extends DHBaseItemSheet { /* -------------------------------------------- */ async _onDrop(event) { + event.stopPropagation(); const data = TextEditor.getDragEventData(event); const item = await fromUuid(data.uuid); const target = event.target.closest('fieldset.drop-section'); @@ -87,12 +88,24 @@ export default class ClassSheet extends DHBaseItemSheet { }); } else if (item.type === 'feature') { if (target.classList.contains('hope-feature')) { + if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes.hope) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureNotHope')); + return; + } + + await item.update({ 'system.subType': CONFIG.DH.ITEM.featureSubTypes.hope }); await this.document.update({ - 'system.hopeFeatures': [...this.document.system.hopeFeatures.map(x => x.uuid), item.uuid] + 'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid] }); } else if (target.classList.contains('class-feature')) { + if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes.class) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureNotClass')); + return; + } + + await item.update({ 'system.subType': CONFIG.DH.ITEM.featureSubTypes.class }); await this.document.update({ - 'system.classFeatures': [...this.document.system.classFeatures.map(x => x.uuid), item.uuid] + 'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid] }); } } else if (item.type === 'weapon') { @@ -177,28 +190,25 @@ export default class ClassSheet extends DHBaseItemSheet { doc.sheet.render({ force: true }); } - getActionPath(type) { - return type === 'hope' ? 'hopeFeatures' : 'classFeatures'; - } - static async addFeature(_, target) { - const actionPath = this.getActionPath(target.dataset.type); const feature = await game.items.documentClass.create({ type: 'feature', - name: game.i18n.format('DOCUMENT.New', { type: game.i18n.localize('TYPES.Item.feature') }) + name: game.i18n.format('DOCUMENT.New', { type: game.i18n.localize('TYPES.Item.feature') }), + system: { + subType: + target.dataset.type === 'hope' + ? CONFIG.DH.ITEM.featureSubTypes.hope + : CONFIG.DH.ITEM.featureSubTypes.class + } }); await this.document.update({ - [`system.${actionPath}`]: [ - ...this.document.system[actionPath].filter(x => x).map(x => x.uuid), - feature.uuid - ] + [`system.features`]: [...this.document.system.features.filter(x => x).map(x => x.uuid), feature.uuid] }); } static async editFeature(_, button) { const target = button.closest('.feature-item'); - const actionPath = this.getActionPath(button.dataset.type); - const feature = this.document.system[actionPath].find(x => x?.id === target.dataset.featureId); + const feature = this.document.system.features.find(x => x?.id === target.dataset.featureId); if (!feature) { ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing')); return; @@ -210,10 +220,16 @@ export default class ClassSheet extends DHBaseItemSheet { static async deleteFeature(event, button) { event.stopPropagation(); const target = button.closest('.feature-item'); - const actionPath = this.getActionPath(button.dataset.type); + + const feature = this.document.system.features.find( + feature => feature && feature.id === target.dataset.featureId + ); + if (feature) { + await feature.update({ 'system.subType': null }); + } await this.document.update({ - [`system.${actionPath}`]: this.document.system[actionPath] + [`system.features`]: this.document.system.features .filter(feature => feature && feature.id !== target.dataset.featureId) .map(x => x.uuid) }); diff --git a/module/applications/sheets/items/subclass.mjs b/module/applications/sheets/items/subclass.mjs index 31eca43a..fcf62f69 100644 --- a/module/applications/sheets/items/subclass.mjs +++ b/module/applications/sheets/items/subclass.mjs @@ -40,28 +40,46 @@ export default class SubclassSheet extends DHBaseItemSheet { static async addFeature(_, target) { const feature = await game.items.documentClass.create({ type: 'feature', - name: game.i18n.format('DOCUMENT.New', { type: game.i18n.localize('TYPES.Item.feature') }) + name: game.i18n.format('DOCUMENT.New', { type: game.i18n.localize('TYPES.Item.feature') }), + system: { + subType: + target.dataset.type === 'foundation' + ? CONFIG.DH.ITEM.featureSubTypes.foundation + : target.dataset.type === 'specialization' + ? CONFIG.DH.ITEM.featureSubTypes.specialization + : CONFIG.DH.ITEM.featureSubTypes.mastery + } }); await this.document.update({ - [`system.${target.dataset.type}`]: feature.uuid + [`system.features`]: [...this.document.system.features.map(x => x.uuid), feature.uuid] }); } static async editFeature(_, button) { - const feature = this.document.system[button.dataset.type]; + const feature = this.document.system.features.find(x => x.id === button.dataset.feature); if (!feature) { ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureIsMissing')); return; } + if (feature) { + await feature.update({ 'system.subType': null }); + } + feature.sheet.render(true); } - static async deleteFeature(event, button) { + static async deleteFeature(event, target) { event.stopPropagation(); + const feature = this.document.system.features.find(feature => feature.id === target.dataset.feature); + if (feature) { + await feature.update({ 'system.subType': null }); + } await this.document.update({ - [`system.${button.dataset.type}`]: null + [`system.features`]: this.document.system.features + .filter(feature => feature && feature.id !== target.dataset.feature) + .map(x => x.uuid) }); } @@ -82,18 +100,45 @@ export default class SubclassSheet extends DHBaseItemSheet { } async _onDrop(event) { + event.stopPropagation(); + const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); if (data.fromInternal) return; const item = await fromUuid(data.uuid); - if (item?.type === 'feature') { - const dropSection = event.target.closest('.drop-section'); - if (this.document.system[dropSection.dataset.type]) { - ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.notifications.featureIsFull')); - return; - } + const target = event.target.closest('fieldset.drop-section'); + if (item.type === 'feature') { + if (target.dataset.type === 'foundation') { + if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes.foundation) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureNotFoundation')); + return; + } - await this.document.update({ [`system.${dropSection.dataset.type}`]: item.uuid }); + await item.update({ 'system.subType': CONFIG.DH.ITEM.featureSubTypes.foundation }); + await this.document.update({ + 'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid] + }); + } else if (target.dataset.type === 'specialization') { + if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes.specialization) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureNotSpecialization')); + return; + } + + await item.update({ 'system.subType': CONFIG.DH.ITEM.featureSubTypes.specialization }); + await this.document.update({ + 'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid] + }); + } else if (target.dataset.type === 'mastery') { + if (item.system.subType && item.system.subType !== CONFIG.DH.ITEM.featureSubTypes.mastery) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.featureNotMastery')); + return; + } + + await item.update({ 'system.subType': CONFIG.DH.ITEM.featureSubTypes.mastery }); + await this.document.update({ + 'system.features': [...this.document.system.features.map(x => x.uuid), item.uuid] + }); + } } } } diff --git a/module/config/actorConfig.mjs b/module/config/actorConfig.mjs index 02cfd4a9..d05dc81c 100644 --- a/module/config/actorConfig.mjs +++ b/module/config/actorConfig.mjs @@ -411,7 +411,7 @@ export const levelupData = { }; export const subclassFeatureLabels = { - 1: 'DAGGERHEART.ITEMS.DomainCard.foundation', + 1: 'DAGGERHEART.ITEMS.DomainCard.foundationTitle', 2: 'DAGGERHEART.ITEMS.DomainCard.specializationTitle', 3: 'DAGGERHEART.ITEMS.DomainCard.masteryTitle' }; diff --git a/module/config/itemConfig.mjs b/module/config/itemConfig.mjs index ede5ef08..b26a26ca 100644 --- a/module/config/itemConfig.mjs +++ b/module/config/itemConfig.mjs @@ -1322,7 +1322,12 @@ export const featureTypes = { export const featureSubTypes = { primary: 'primary', - secondary: 'secondary' + secondary: 'secondary', + hope: 'hope', + class: 'class', + foundation: 'foundation', + specialization: 'specialization', + mastery: 'mastery' }; export const actionTypes = { diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 6b0a2fd7..1c15d036 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -124,12 +124,9 @@ export default class DhCharacter extends BaseDataActor { label: 'DAGGERHEART.GENERAL.Range.other' }) }), - rally: new fields.ArrayField( - new fields.StringField(), - { - label: 'DAGGERHEART.CLASS.Feature.rallyDice' - } - ) + rally: new fields.ArrayField(new fields.StringField(), { + label: 'DAGGERHEART.CLASS.Feature.rallyDice' + }) }), companion: new ForeignDocumentUUIDField({ type: 'Actor', nullable: true, initial: null }), rules: new fields.SchemaField({ @@ -260,11 +257,11 @@ export default class DhCharacter extends BaseDataActor { classFeatures.push(item); } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.subclass.id) { const subclassState = this.class.subclass.system.featureState; - const identifier = item.system.identifier; + const subType = item.system.subType; if ( - identifier === 'foundationFeature' || - (identifier === 'specializationFeature' && subclassState >= 2) || - (identifier === 'masterFeature' && subclassState >= 3) + subType === CONFIG.DH.ITEM.featureSubTypes.foundation || + (subType === CONFIG.DH.ITEM.featureSubTypes.specialization && subclassState >= 2) || + (subType === CONFIG.DH.ITEM.featureSubTypes.mastery && subclassState >= 3) ) { subclassFeatures.push(item); } diff --git a/module/data/item/class.mjs b/module/data/item/class.mjs index 281b0a48..b8a9ab81 100644 --- a/module/data/item/class.mjs +++ b/module/data/item/class.mjs @@ -27,8 +27,7 @@ export default class DHClass extends BaseDataItem { label: 'DAGGERHEART.GENERAL.hitPoints.plural' }), evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }), - hopeFeatures: new ForeignDocumentUUIDArrayField({ type: 'Item' }), - classFeatures: new ForeignDocumentUUIDArrayField({ type: 'Item' }), + features: new ForeignDocumentUUIDArrayField({ type: 'Item' }), subclasses: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }), inventory: new fields.SchemaField({ take: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }), @@ -52,12 +51,18 @@ export default class DHClass extends BaseDataItem { }; } - get hopeFeature() { - return this.hopeFeatures.length > 0 ? this.hopeFeatures[0] : null; + get hopeFeatures() { + return ( + this.features.filter(x => x?.system?.subType === CONFIG.DH.ITEM.featureSubTypes.hope) ?? + (this.features.filter(x => !x).length > 0 ? {} : null) + ); } - get features() { - return [...this.hopeFeatures.filter(x => x), ...this.classFeatures.filter(x => x)]; + get classFeatures() { + return ( + this.features.filter(x => x?.system?.subType === CONFIG.DH.ITEM.featureSubTypes.class) ?? + (this.features.filter(x => !x).length > 0 ? {} : null) + ); } async _preCreate(data, options, user) { diff --git a/module/data/item/domainCard.mjs b/module/data/item/domainCard.mjs index 89bbfb40..df60b9d1 100644 --- a/module/data/item/domainCard.mjs +++ b/module/data/item/domainCard.mjs @@ -29,7 +29,6 @@ export default class DHDomainCard extends BaseDataItem { required: true, initial: CONFIG.DH.DOMAIN.cardTypes.ability.id }), - foundation: new fields.BooleanField({ initial: false }), inVault: new fields.BooleanField({ initial: false }), actions: new fields.ArrayField(new ActionField()) }; diff --git a/module/data/item/subclass.mjs b/module/data/item/subclass.mjs index 265c2566..e0a76092 100644 --- a/module/data/item/subclass.mjs +++ b/module/data/item/subclass.mjs @@ -1,4 +1,4 @@ -import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; +import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs'; import BaseDataItem from './base.mjs'; export default class DHSubclass extends BaseDataItem { @@ -22,20 +22,22 @@ export default class DHSubclass extends BaseDataItem { nullable: true, initial: null }), - foundationFeature: new ForeignDocumentUUIDField({ type: 'Item' }), - specializationFeature: new ForeignDocumentUUIDField({ type: 'Item' }), - masteryFeature: new ForeignDocumentUUIDField({ type: 'Item' }), + features: new ForeignDocumentUUIDArrayField({ type: 'Item' }), featureState: new fields.NumberField({ required: true, initial: 1, min: 1 }), isMulticlass: new fields.BooleanField({ initial: false }) }; } - get features() { - return [ - { ...this.foundationFeature?.toObject(), identifier: 'foundationFeature' }, - { ...this.specializationFeature?.toObject(), identifier: 'specializationFeature' }, - { ...this.masteryFeature?.toObject(), identifier: 'masteryFeature' } - ]; + get foundationFeatures() { + return this.features.filter(x => x.system.subType === CONFIG.DH.ITEM.featureSubTypes.foundation); + } + + get specializationFeatures() { + return this.features.filter(x => x.system.subType === CONFIG.DH.ITEM.featureSubTypes.specialization); + } + + get masteryFeatures() { + return this.features.filter(x => x.system.subType === CONFIG.DH.ITEM.featureSubTypes.mastery); } async _preCreate(data, options, user) { diff --git a/templates/sheets/items/domainCard/settings.hbs b/templates/sheets/items/domainCard/settings.hbs index 2faa6934..5518b4c3 100644 --- a/templates/sheets/items/domainCard/settings.hbs +++ b/templates/sheets/items/domainCard/settings.hbs @@ -8,8 +8,6 @@ {{localize "DAGGERHEART.GENERAL.type"}} {{formField systemFields.type value=source.system.type localize=true}} - {{localize "DAGGERHEART.ITEMS.DomainCard.foundation"}} - {{formField systemFields.foundation value=source.system.foundation }} {{localize "DAGGERHEART.GENERAL.Domain.single"}} {{formField systemFields.domain value=source.system.domain localize=true}} {{localize "DAGGERHEART.GENERAL.level"}} diff --git a/templates/sheets/items/subclass/features.hbs b/templates/sheets/items/subclass/features.hbs index d2424d01..1a75974e 100644 --- a/templates/sheets/items/subclass/features.hbs +++ b/templates/sheets/items/subclass/features.hbs @@ -3,42 +3,42 @@ data-tab='{{tabs.features.id}}' data-group='{{tabs.features.group}}' > -
+
{{localize "DAGGERHEART.GENERAL.Tabs.foundation"}} - +
- {{#if source.system.foundationFeature}} - {{> 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs' type='foundationFeature' feature=source.system.foundationFeature}} - {{/if}} + {{#each source.system.foundationFeatures as | feature | }} + {{> 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs' type='foundation' feature=feature}} + {{/each}}
-
+
{{localize "DAGGERHEART.GENERAL.Tabs.specialization"}} - +
- {{#if source.system.specializationFeature}} - {{> 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs' type='specializationFeature' feature=source.system.specializationFeature}} - {{/if}} + {{#each source.system.specializationFeatures as | feature |}} + {{> 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs' type='specialization' feature=feature}} + {{/each}}
-
+
{{localize "DAGGERHEART.GENERAL.Tabs.mastery"}} - +
- {{#if source.system.masteryFeature}} - {{> 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs' type='masteryFeature' feature=source.system.masteryFeature}} - {{/if}} + {{#each source.system.masteryFeatures as | feature |}} + {{> 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs' type='mastery' feature=feature}} + {{/each}}
\ No newline at end of file From 6e87e4dad066173cd4cd47fbb135881725f82330 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Fri, 18 Jul 2025 00:48:59 +0200 Subject: [PATCH 6/7] [Fix] Downtime Rework (#367) * Fixed so that the dropdown for activeEffectAutocomplete never ends up behind dialog * Downtime can now display both ShortRest and LongRest options depending on character rules * Initial downtime layout rework * Fixed styling for downtime tooltip * Added icon to homebrew menu for DowntimeActions * Fixed columns if both types of moves are not available * Changed the lightmode to darkmode * Added downtime buttons * . * Moved extra rest options from rules to bonuses * Improved dialog width --- lang/en.json | 32 +++++- module/applications/dialogs/downtime.mjs | 87 +++++++++++---- .../components/settingsActionsView.mjs | 10 +- .../settings/homebrewSettings.mjs | 2 + .../sheets-configs/activeEffectConfig.mjs | 3 + .../applications/sheets/actors/character.mjs | 9 +- module/config/generalConfig.mjs | 19 ++-- module/data/actor/character.mjs | 38 +++++++ module/data/settings/Homebrew.mjs | 2 + module/documents/token.mjs | 20 ++-- module/documents/tooltipManager.mjs | 51 ++++++++- module/systemRegistration/handlebars.mjs | 3 +- .../dialog/downtime/downtime-container.less | 102 +++++++++--------- styles/less/global/tab-navigation.less | 20 +++- .../less/sheets/actors/character/header.less | 9 ++ styles/less/ux/autocomplete/autocomplete.less | 1 - styles/less/ux/tooltip/tooltip.less | 14 +++ templates/dialogs/downtime.hbs | 24 ----- templates/dialogs/downtime/activities.hbs | 18 ++++ templates/dialogs/downtime/downtime.hbs | 14 +++ templates/settings/automation-settings.hbs | 2 +- templates/settings/components/action-view.hbs | 19 ++-- templates/sheets/actors/character/header.hbs | 11 +- .../sheets/global/tabs/tab-navigation.hbs | 25 +++-- templates/ui/tooltip/downtime.hbs | 7 ++ 25 files changed, 390 insertions(+), 152 deletions(-) delete mode 100644 templates/dialogs/downtime.hbs create mode 100644 templates/dialogs/downtime/activities.hbs create mode 100644 templates/dialogs/downtime/downtime.hbs create mode 100644 templates/ui/tooltip/downtime.hbs diff --git a/lang/en.json b/lang/en.json index 80cc67f7..39e49b67 100755 --- a/lang/en.json +++ b/lang/en.json @@ -249,8 +249,9 @@ "title": "{actor} - Death Move" }, "Downtime": { - "downtimeHeader": "Downtime Moves ({current}/{max})", "longRest": { + "title": "Long Rest", + "moves": "Long Rest Moves ({current}/{max})", "clearStress": { "description": "Describe how you blow off steam or pull yourself together, and clear all marked Stress.", "name": "Clear Stress" @@ -267,7 +268,6 @@ "description": "Describe how you patch yourself up and remove all marked Hit Points. You may also do this on an ally instead.", "name": "Tend to Wounds" }, - "title": "Long Rest", "workOnAProject": { "description": "Establish or continue work on a project.", "name": "Work on a Project" @@ -275,6 +275,7 @@ }, "shortRest": { "title": "Short Rest", + "moves": "Short Rest Moves ({current}/{max})", "tendToWounds": { "name": "Tend to Wounds", "description": "Describe how you hastily patch yourself up, then clear a number of Hit Points equal to 1d4 + your tier. You can do this to an ally instead." @@ -291,7 +292,8 @@ "name": "Prepare", "description": "Describe how you prepare yourself for the path ahead, then gain a Hope. If you choose to Prepare with one or more members of your party, you each gain 2 Hope." } - } + }, + "takeDowntime": "Take Downtime" }, "HUD": { "tokenHUD": { @@ -1012,6 +1014,30 @@ "singular": "Adversary", "plural": "Adversaries" }, + "Bonuses": { + "rest": { + "shortRest": { + "shortRestMoves": { + "label": "Short Rest: Bonus Short Rest Moves", + "hint": "The number of extra Short Rest Moves the character can take during a Short Rest." + }, + "longRestMoves": { + "label": "Short Rest: Bonus Long Rest Moves", + "hint": "The number of extra Long Rest Moves the character can take during a Short Rest." + } + }, + "longRest": { + "shortRestMoves": { + "label": "Long Rest: Bonus Short Rest Moves", + "hint": "The number of extra Short Rest Moves the character can take during a Long Rest." + }, + "longRestMoves": { + "label": "Long Rest: Bonus Long Rest Moves", + "hint": "The number of extra Long Rest Moves the character can take during a Long Rest." + } + } + } + }, "Character": { "singular": "Character", "plural": "Characters" diff --git a/module/applications/dialogs/downtime.mjs b/module/applications/dialogs/downtime.mjs index 3966f7e4..15add3ad 100644 --- a/module/applications/dialogs/downtime.mjs +++ b/module/applications/dialogs/downtime.mjs @@ -7,8 +7,22 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV this.actor = actor; this.shortrest = shortrest; - const options = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).restMoves; - this.moveData = shortrest ? options.shortRest : options.longRest; + this.moveData = foundry.utils.deepClone( + game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).restMoves + ); + this.nrChoices = { + shortRest: { + max: + (shortrest ? this.moveData.shortRest.nrChoices : 0) + + actor.system.bonuses.rest[`${shortrest ? 'short' : 'long'}Rest`].shortMoves + }, + longRest: { + max: + (!shortrest ? this.moveData.longRest.nrChoices : 0) + + actor.system.bonuses.rest[`${shortrest ? 'short' : 'long'}Rest`].longMoves + } + }; + this.nrChoices.total = { max: this.nrChoices.shortRest.max + this.nrChoices.longRest.max }; } get title() { @@ -17,8 +31,8 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV static DEFAULT_OPTIONS = { tag: 'form', - classes: ['daggerheart', 'views', 'downtime'], - position: { width: 680, height: 'auto' }, + classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'downtime'], + position: { width: 'auto', height: 'auto' }, actions: { selectMove: this.selectMove, takeDowntime: this.takeDowntime @@ -29,7 +43,7 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV static PARTS = { application: { id: 'downtime', - template: 'systems/daggerheart/templates/dialogs/downtime.hbs' + template: 'systems/daggerheart/templates/dialogs/downtime/downtime.hbs' } }; @@ -37,46 +51,83 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV super._attachPartListeners(partId, htmlElement, options); htmlElement - .querySelectorAll('.activity-image') + .querySelectorAll('.activity-container') .forEach(element => element.addEventListener('contextmenu', this.deselectMove.bind(this))); } async _prepareContext(_options) { const context = await super._prepareContext(_options); + context.title = game.i18n.localize( + `DAGGERHEART.APPLICATIONS.Downtime.${this.shortrest ? 'shortRest' : 'longRest'}.title` + ); context.selectedActivity = this.selectedActivity; context.moveData = this.moveData; - context.nrCurrentChoices = Object.values(this.moveData.moves).reduce((acc, x) => acc + (x.selected ?? 0), 0); - context.disabledDowntime = context.nrCurrentChoices < context.moveData.nrChoices; + context.nrCurrentChoices = Object.values(this.moveData).reduce((acc, category) => { + acc += Object.values(category.moves).reduce((acc, x) => acc + (x.selected ?? 0), 0); + return acc; + }, 0); + + context.nrChoices = { + ...this.nrChoices, + shortRest: { + ...this.nrChoices.shortRest, + current: Object.values(this.moveData.shortRest.moves).reduce((acc, x) => acc + (x.selected ?? 0), 0) + }, + longRest: { + ...this.nrChoices.longRest, + current: Object.values(this.moveData.longRest.moves).reduce((acc, x) => acc + (x.selected ?? 0), 0) + } + }; + context.nrChoices.total = { + ...this.nrChoices.total, + current: context.nrChoices.shortRest.current + context.nrChoices.longRest.current + }; + + context.shortRestMoves = this.nrChoices.shortRest.max > 0 ? this.moveData.shortRest : null; + context.longRestMoves = this.nrChoices.longRest.max > 0 ? this.moveData.longRest : null; + + context.disabledDowntime = context.nrChoices.total.current < context.nrChoices.total.max; return context; } - static selectMove(_, button) { - const nrSelected = Object.values(this.moveData.moves).reduce((acc, x) => acc + (x.selected ?? 0), 0); - if (nrSelected === this.moveData.nrChoices) { + static selectMove(_, target) { + const nrSelected = Object.values(this.moveData[target.dataset.category].moves).reduce( + (acc, x) => acc + (x.selected ?? 0), + 0 + ); + + if (nrSelected === this.nrChoices[target.dataset.category].max) { ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.noMoreMoves')); return; } - const move = button.dataset.move; - this.moveData.moves[move].selected = this.moveData.moves[move].selected - ? this.moveData.moves[move].selected + 1 + const move = target.dataset.move; + this.moveData[target.dataset.category].moves[move].selected = this.moveData[target.dataset.category].moves[move] + .selected + ? this.moveData[target.dataset.category].moves[move].selected + 1 : 1; this.render(); } deselectMove(event) { - const move = event.currentTarget.dataset.move; - this.moveData.moves[move].selected = this.moveData.moves[move].selected - ? this.moveData.moves[move].selected - 1 + const button = event.target.closest('.activity-container'); + const move = button.dataset.move; + this.moveData[button.dataset.category].moves[move].selected = this.moveData[button.dataset.category].moves[move] + .selected + ? this.moveData[button.dataset.category].moves[move].selected - 1 : 0; this.render(); } static async takeDowntime() { - const moves = Object.values(this.moveData.moves).filter(x => x.selected); + const moves = Object.values(this.moveData).flatMap(category => { + return Object.values(category.moves) + .filter(x => x.selected) + .flatMap(move => [...Array(move.selected).keys()].map(_ => move)); + }); const cls = getDocumentClass('ChatMessage'); const msg = new cls({ diff --git a/module/applications/settings/components/settingsActionsView.mjs b/module/applications/settings/components/settingsActionsView.mjs index a905e824..f77c5fce 100644 --- a/module/applications/settings/components/settingsActionsView.mjs +++ b/module/applications/settings/components/settingsActionsView.mjs @@ -4,13 +4,14 @@ import DHActionConfig from '../../sheets-configs/action-config.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; export default class DhSettingsActionView extends HandlebarsApplicationMixin(ApplicationV2) { - constructor(resolve, reject, title, name, img, description, actions) { + constructor(resolve, reject, title, name, icon, img, description, actions) { super({}); this.resolve = resolve; this.reject = reject; this.viewTitle = title; this.name = name; + this.icon = icon; this.img = img; this.description = description; this.actions = actions; @@ -23,7 +24,7 @@ export default class DhSettingsActionView extends HandlebarsApplicationMixin(App static DEFAULT_OPTIONS = { tag: 'form', classes: ['daggerheart', 'setting', 'dh-style'], - position: { width: '400', height: 'auto' }, + position: { width: 440, height: 'auto' }, actions: { editImage: this.onEditImage, addItem: this.addItem, @@ -46,6 +47,7 @@ export default class DhSettingsActionView extends HandlebarsApplicationMixin(App async _prepareContext(_options) { const context = await super._prepareContext(_options); context.name = this.name; + context.icon = this.icon; context.img = this.img; context.description = this.description; context.enrichedDescription = await foundry.applications.ux.TextEditor.enrichHTML(context.description); @@ -55,8 +57,9 @@ export default class DhSettingsActionView extends HandlebarsApplicationMixin(App } static async updateData(event, element, formData) { - const { name, img, description } = foundry.utils.expandObject(formData.object); + const { name, icon, description } = foundry.utils.expandObject(formData.object); this.name = name; + this.icon = icon; this.description = description; this.render(); @@ -65,6 +68,7 @@ export default class DhSettingsActionView extends HandlebarsApplicationMixin(App static async saveForm(event) { this.resolve({ name: this.name, + icon: this.icon, img: this.img, description: this.description, actions: this.actions diff --git a/module/applications/settings/homebrewSettings.mjs b/module/applications/settings/homebrewSettings.mjs index 08b4cf4e..e516be03 100644 --- a/module/applications/settings/homebrewSettings.mjs +++ b/module/applications/settings/homebrewSettings.mjs @@ -76,6 +76,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli reject, game.i18n.localize('DAGGERHEART.SETTINGS.Homebrew.downtimeMoves'), move.name, + move.icon, move.img, move.description, move.actions @@ -87,6 +88,7 @@ export default class DhHomebrewSettings extends HandlebarsApplicationMixin(Appli await this.settings.updateSource({ [`restMoves.${type}.moves.${id}`]: { name: data.name, + icon: data.icon, img: data.img, description: data.description } diff --git a/module/applications/sheets-configs/activeEffectConfig.mjs b/module/applications/sheets-configs/activeEffectConfig.mjs index 8574a5db..087b5b08 100644 --- a/module/applications/sheets-configs/activeEffectConfig.mjs +++ b/module/applications/sheets-configs/activeEffectConfig.mjs @@ -88,6 +88,9 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac element.value = `system.${item.value}`; }, click: e => e.fetch(), + customize: function (_input, _inputRect, container) { + container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ; + }, minLength: 0 }); }); diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 139a1369..0f6f8284 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -28,7 +28,8 @@ export default class CharacterSheet extends DHBaseActorSheet { useAction: this.useAction, toggleResourceDice: this.toggleResourceDice, handleResourceDice: this.handleResourceDice, - toChat: this.toChat + toChat: this.toChat, + useDowntime: this.useDowntime }, window: { resizable: true @@ -752,6 +753,12 @@ export default class CharacterSheet extends DHBaseActorSheet { } } + static useDowntime(_, button) { + new game.system.api.applications.dialogs.Downtime(this.document, button.dataset.type === 'shortRest').render( + true + ); + } + async _onDragStart(event) { const item = this.getItem(event); diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 704a5401..632e5448 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -130,6 +130,7 @@ export const defaultRestOptions = { tendToWounds: { id: 'tendToWounds', name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.tendToWounds.name'), + icon: 'fa-solid fa-bandage', img: 'icons/magic/life/cross-worn-green.webp', description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.tendToWounds.description'), actions: [ @@ -153,6 +154,7 @@ export const defaultRestOptions = { clearStress: { id: 'clearStress', name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.clearStress.name'), + icon: 'fa-regular fa-face-surprise', img: 'icons/magic/perception/eye-ringed-green.webp', description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.clearStress.description'), actions: [ @@ -176,6 +178,7 @@ export const defaultRestOptions = { repairArmor: { id: 'repairArmor', name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.repairArmor.name'), + icon: 'fa-solid fa-hammer', img: 'icons/skills/trades/smithing-anvil-silver-red.webp', description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.repairArmor.description'), actions: [] @@ -183,6 +186,7 @@ export const defaultRestOptions = { prepare: { id: 'prepare', name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepare.name'), + icon: 'fa-solid fa-dumbbell', img: 'icons/skills/trades/academics-merchant-scribe.webp', description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.shortRest.prepare.description'), actions: [] @@ -192,6 +196,7 @@ export const defaultRestOptions = { tendToWounds: { id: 'tendToWounds', name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.tendToWounds.name'), + icon: 'fa-solid fa-bandage', img: 'icons/magic/life/cross-worn-green.webp', description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.tendToWounds.description'), actions: [] @@ -199,6 +204,7 @@ export const defaultRestOptions = { clearStress: { id: 'clearStress', name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.clearStress.name'), + icon: 'fa-regular fa-face-surprise', img: 'icons/magic/perception/eye-ringed-green.webp', description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.clearStress.description'), actions: [] @@ -206,6 +212,7 @@ export const defaultRestOptions = { repairArmor: { id: 'repairArmor', name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.repairArmor.name'), + icon: 'fa-solid fa-hammer', img: 'icons/skills/trades/smithing-anvil-silver-red.webp', description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.repairArmor.description'), actions: [] @@ -213,6 +220,7 @@ export const defaultRestOptions = { prepare: { id: 'prepare', name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepare.name'), + icon: 'fa-solid fa-dumbbell', img: 'icons/skills/trades/academics-merchant-scribe.webp', description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.prepare.description'), actions: [] @@ -220,19 +228,12 @@ export const defaultRestOptions = { workOnAProject: { id: 'workOnAProject', name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.workOnAProject.name'), + icon: 'fa-solid fa-diagram-project', img: 'icons/skills/social/thumbsup-approval-like.webp', description: game.i18n.localize('DAGGERHEART.APPLICATIONS.Downtime.longRest.workOnAProject.description'), actions: [] } - }), - custom: { - id: 'customActivity', - name: '', - img: 'icons/skills/trades/academics-investigation-puzzles.webp', - description: '', - namePlaceholder: 'DAGGERHEART.APPLICATIONS.Downtime.custom.namePlaceholder', - placeholder: 'DAGGERHEART.APPLICATIONS.Downtime.custom.placeholder' - } + }) }; export const deathMoves = { diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 1c15d036..c15d2221 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -126,6 +126,44 @@ export default class DhCharacter extends BaseDataActor { }), rally: new fields.ArrayField(new fields.StringField(), { label: 'DAGGERHEART.CLASS.Feature.rallyDice' + }), + rest: new fields.SchemaField({ + shortRest: new fields.SchemaField({ + shortMoves: new fields.NumberField({ + required: true, + integer: true, + min: 0, + initial: 0, + label: 'DAGGERHEART.GENERAL.Bonuses.rest.shortRest.shortRestMoves.label', + hint: 'DAGGERHEART.GENERAL.Bonuses.rest.shortRest.shortRestMoves.hint' + }), + longMoves: new fields.NumberField({ + required: true, + integer: true, + min: 0, + initial: 0, + label: 'DAGGERHEART.GENERAL.Bonuses.rest.shortRest.longRestMoves.label', + hint: 'DAGGERHEART.GENERAL.Bonuses.rest.shortRest.longRestMoves.hint' + }) + }), + longRest: new fields.SchemaField({ + shortMoves: new fields.NumberField({ + required: true, + integer: true, + min: 0, + initial: 0, + label: 'DAGGERHEART.GENERAL.Bonuses.rest.longRest.shortRestMoves.label', + hint: 'DAGGERHEART.GENERAL.Bonuses.rest.longRest.shortRestMoves.hint' + }), + longMoves: new fields.NumberField({ + required: true, + integer: true, + min: 0, + initial: 0, + label: 'DAGGERHEART.GENERAL.Bonuses.rest.longRest.longRestMoves.label', + hint: 'DAGGERHEART.GENERAL.Bonuses.rest.longRest.longRestMoves.hint' + }) + }) }) }), companion: new ForeignDocumentUUIDField({ type: 'Actor', nullable: true, initial: null }), diff --git a/module/data/settings/Homebrew.mjs b/module/data/settings/Homebrew.mjs index ead5a09f..008cd73c 100644 --- a/module/data/settings/Homebrew.mjs +++ b/module/data/settings/Homebrew.mjs @@ -54,6 +54,7 @@ export default class DhHomebrew extends foundry.abstract.DataModel { moves: new fields.TypedObjectField( new fields.SchemaField({ name: new fields.StringField({ required: true }), + icon: new fields.StringField({ required: true }), img: new fields.FilePathField({ initial: 'icons/magic/life/cross-worn-green.webp', categories: ['IMAGE'], @@ -70,6 +71,7 @@ export default class DhHomebrew extends foundry.abstract.DataModel { moves: new fields.TypedObjectField( new fields.SchemaField({ name: new fields.StringField({ required: true }), + icon: new fields.StringField({ required: true }), img: new fields.FilePathField({ initial: 'icons/magic/life/cross-worn-green.webp', categories: ['IMAGE'], diff --git a/module/documents/token.mjs b/module/documents/token.mjs index 3e7b49ea..89305128 100644 --- a/module/documents/token.mjs +++ b/module/documents/token.mjs @@ -16,7 +16,7 @@ export default class DHToken extends TokenDocument { }); bars.sort((a, b) => a.label.compare(b.label)); - const invalidAttributes = ['gold', 'levelData', 'rules.damageReduction.maxArmorMarked.value']; + const invalidAttributes = ['gold', 'levelData', 'actions', 'rules.damageReduction.maxArmorMarked.value']; const values = attributes.value.reduce((acc, v) => { const a = v.join('.'); if (invalidAttributes.some(x => a.startsWith(x))) return acc; @@ -32,19 +32,19 @@ export default class DHToken extends TokenDocument { return bars.concat(values); } - - static _getTrackedAttributesFromSchema(schema, _path=[]) { - const attributes = {bar: [], value: []}; - for ( const [name, field] of Object.entries(schema.fields) ) { + + static _getTrackedAttributesFromSchema(schema, _path = []) { + const attributes = { bar: [], value: [] }; + for (const [name, field] of Object.entries(schema.fields)) { const p = _path.concat([name]); - if ( field instanceof foundry.data.fields.NumberField ) attributes.value.push(p); - if ( field instanceof foundry.data.fields.ArrayField ) attributes.value.push(p); + if (field instanceof foundry.data.fields.NumberField) attributes.value.push(p); + if (field instanceof foundry.data.fields.ArrayField) attributes.value.push(p); const isSchema = field instanceof foundry.data.fields.SchemaField; const isModel = field instanceof foundry.data.fields.EmbeddedDataField; - if ( isSchema || isModel ) { + if (isSchema || isModel) { const schema = isModel ? field.model.schema : field; - const isBar = schema.has && schema.has("value") && schema.has("max"); - if ( isBar ) attributes.bar.push(p); + const isBar = schema.has && schema.has('value') && schema.has('max'); + if (isBar) attributes.bar.push(p); else { const inner = this.getTrackedAttributes(schema, p); attributes.bar.push(...inner.bar); diff --git a/module/documents/tooltipManager.mjs b/module/documents/tooltipManager.mjs index f24823f4..e622059d 100644 --- a/module/documents/tooltipManager.mjs +++ b/module/documents/tooltipManager.mjs @@ -22,6 +22,28 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti options.direction = this._determineItemTooltipDirection(element); } } else { + const shortRest = element.dataset.tooltip?.startsWith('#shortRest#'); + const longRest = element.dataset.tooltip?.startsWith('#longRest#'); + if (shortRest || longRest) { + const key = element.dataset.tooltip.slice(shortRest ? 11 : 10); + const downtimeOptions = shortRest + ? CONFIG.DH.GENERAL.defaultRestOptions.shortRest() + : CONFIG.DH.GENERAL.defaultRestOptions.longRest(); + const move = downtimeOptions[key]; + html = await foundry.applications.handlebars.renderTemplate( + `systems/daggerheart/templates/ui/tooltip/downtime.hbs`, + { + move: move + } + ); + + this.tooltip.innerHTML = html; + options.direction = this._determineItemTooltipDirection( + element, + this.constructor.TOOLTIP_DIRECTIONS.UP + ); + } + const isAdvantage = element.dataset.tooltip?.startsWith('#advantage#'); const isDisadvantage = element.dataset.tooltip?.startsWith('#disadvantage#'); if (isAdvantage || isDisadvantage) { @@ -44,9 +66,34 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti super.activate(element, { ...options, html: html }); } - _determineItemTooltipDirection(element) { + _determineItemTooltipDirection(element, prefered = this.constructor.TOOLTIP_DIRECTIONS.LEFT) { const pos = element.getBoundingClientRect(); const dirs = this.constructor.TOOLTIP_DIRECTIONS; - return dirs[pos.x - this.tooltip.offsetWidth < 0 ? 'DOWN' : 'LEFT']; + switch (prefered) { + case this.constructor.TOOLTIP_DIRECTIONS.LEFT: + return dirs[ + pos.x - this.tooltip.offsetWidth < 0 + ? this.constructor.TOOLTIP_DIRECTIONS.DOWN + : this.constructor.TOOLTIP_DIRECTIONS.LEFT + ]; + case this.constructor.TOOLTIP_DIRECTIONS.UP: + return dirs[ + pos.y - this.tooltip.offsetHeight < 0 + ? this.constructor.TOOLTIP_DIRECTIONS.RIGHT + : this.constructor.TOOLTIP_DIRECTIONS.UP + ]; + case this.constructor.TOOLTIP_DIRECTIONS.RIGHT: + return dirs[ + pos.x + this.tooltip.offsetWidth > document.body.clientWidth + ? this.constructor.TOOLTIP_DIRECTIONS.DOWN + : this.constructor.TOOLTIP_DIRECTIONS.RIGHT + ]; + case this.constructor.TOOLTIP_DIRECTIONS.DOWN: + return dirs[ + pos.y + this.tooltip.offsetHeight > document.body.clientHeight + ? this.constructor.TOOLTIP_DIRECTIONS.LEFT + : this.constructor.TOOLTIP_DIRECTIONS.DOWN + ]; + } } } diff --git a/module/systemRegistration/handlebars.mjs b/module/systemRegistration/handlebars.mjs index 8456094c..0c455750 100644 --- a/module/systemRegistration/handlebars.mjs +++ b/module/systemRegistration/handlebars.mjs @@ -25,6 +25,7 @@ export const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/settings/components/settings-item-line.hbs', 'systems/daggerheart/templates/ui/chat/parts/damage-chat.hbs', 'systems/daggerheart/templates/ui/chat/parts/target-chat.hbs', - 'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs' + 'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs', + 'systems/daggerheart/templates/dialogs/downtime/activities.hbs' ]); }; diff --git a/styles/less/dialog/downtime/downtime-container.less b/styles/less/dialog/downtime/downtime-container.less index 4e785e7b..0f803d9b 100644 --- a/styles/less/dialog/downtime/downtime-container.less +++ b/styles/less/dialog/downtime/downtime-container.less @@ -1,81 +1,75 @@ @import '../../utils/spacing.less'; @import '../../utils/colors.less'; -.daggerheart.views { +.theme-light .daggerheart.dh-style.views.downtime { + .downtime-container .activity-container .activity-selected-marker { + background-image: url(../assets/parchments/dh-parchment-light.png); + } +} + +.daggerheart.dh-style.views.downtime { + font-family: @font-body; + .downtime-container { - .downtime-header { - margin: 0; - color: light-dark(@dark-blue, @golden); - text-align: center; + .activities-grouping { + width: 280px; } - .activity-container { - display: flex; - align-items: center; - padding: 8px; + .activities-container { + width: 100%; - .activity-title { - flex: 1; + .activity-container { display: flex; align-items: center; + justify-content: space-between; + padding: 8px; - .activity-title-text { - font-size: 24px; - font-weight: bold; - } - - .activity-image { - width: 80px; - position: relative; + .activity-inner-container { display: flex; - justify-content: center; - margin-right: 8px; - border: 2px solid black; - border-radius: 50%; - cursor: pointer; + align-items: center; + gap: 4px; - .activity-select-label { - position: absolute; - top: -9px; - font-size: 14px; - border: 1px solid light-dark(@dark-blue, @golden); - border-radius: 6px; - color: light-dark(@beige, @dark); - background-image: url(../assets/parchments/dh-parchment-light.png); - padding: 0 8px; - line-height: 1; - font-weight: bold; + .activity-marker { + font-size: 8px; + flex: none; + color: light-dark(#18162e, #f3c267); + margin-right: 4px; } - img { - border-radius: 50%; - } + .activity-select-section { + display: flex; + align-items: center; + gap: 4px; - &:hover, - &.selected { - filter: drop-shadow(0 0 6px gold); + .activity-icon { + min-width: 24px; + text-align: center; + } } } - .custom-name-input { - font-size: 24px; + .activity-selected-marker { + font-size: 14px; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + color: light-dark(@dark, @beige); + background-image: url(../assets/parchments/dh-parchment-dark.png); + padding: 0 8px; + line-height: 1; font-weight: bold; - padding: 0; - background: transparent; - color: rgb(239, 230, 216); } } - - .activity-body { - flex: 1; - font-style: italic; - } } } - &.downtime { - .activity-text-area { - resize: none; + footer { + margin-top: 8px; + display: flex; + gap: 8px; + + button { + flex: 1; + font-family: 'Montserrat', sans-serif; } } } diff --git a/styles/less/global/tab-navigation.less b/styles/less/global/tab-navigation.less index 2880711d..014da89f 100755 --- a/styles/less/global/tab-navigation.less +++ b/styles/less/global/tab-navigation.less @@ -7,12 +7,22 @@ height: 40px; width: 100%; - .feature-tab { - border: none; + .navigation-container { + display: flex; + align-items: center; + gap: 8px; - a { - color: light-dark(@dark-blue, @golden); - font-family: @font-body; + .navigation-inner-container { + flex: 1; + + .feature-tab { + border: none; + + a { + color: light-dark(@dark-blue, @golden); + font-family: @font-body; + } + } } } } diff --git a/styles/less/sheets/actors/character/header.less b/styles/less/sheets/actors/character/header.less index 6110fcc6..b80da83d 100644 --- a/styles/less/sheets/actors/character/header.less +++ b/styles/less/sheets/actors/character/header.less @@ -193,5 +193,14 @@ } } } + + .character-downtime-container { + display: flex; + gap: 2px; + + button { + flex: 1; + } + } } } diff --git a/styles/less/ux/autocomplete/autocomplete.less b/styles/less/ux/autocomplete/autocomplete.less index 06cabf5a..868b4f43 100644 --- a/styles/less/ux/autocomplete/autocomplete.less +++ b/styles/less/ux/autocomplete/autocomplete.less @@ -10,7 +10,6 @@ border-color: light-dark(@dark, @beige); border-radius: 6px; background-image: url('../assets/parchments/dh-parchment-dark.png'); - z-index: 200; max-height: 400px !important; width: fit-content !important; overflow-y: auto; diff --git a/styles/less/ux/tooltip/tooltip.less b/styles/less/ux/tooltip/tooltip.less index 38502d09..0060f74b 100644 --- a/styles/less/ux/tooltip/tooltip.less +++ b/styles/less/ux/tooltip/tooltip.less @@ -4,6 +4,20 @@ align-items: center; gap: 4px; + .tooltip-title-container { + width: 100%; + display: flex; + align-items: center; + gap: 16px; + + .tooltip-image { + height: 40px; + width: 40px; + border-radius: 6px; + border: 1px solid @golden; + } + } + .tooltip-title { margin: 0; text-align: center; diff --git a/templates/dialogs/downtime.hbs b/templates/dialogs/downtime.hbs deleted file mode 100644 index fd5fa405..00000000 --- a/templates/dialogs/downtime.hbs +++ /dev/null @@ -1,24 +0,0 @@ -
-
-

{{localize "DAGGERHEART.APPLICATIONS.Downtime.downtimeHeader" current=nrCurrentChoices max=moveData.nrChoices}}

- {{#each moveData.moves as |move key|}} -
-
-
- {{#if this.selected}}
{{move.selected}}
{{/if}} - -
- - {{localize this.name}} -
-
- {{localize this.description}} -
-
- {{/each}} -
-
- - -
-
\ No newline at end of file diff --git a/templates/dialogs/downtime/activities.hbs b/templates/dialogs/downtime/activities.hbs new file mode 100644 index 00000000..f67e8a10 --- /dev/null +++ b/templates/dialogs/downtime/activities.hbs @@ -0,0 +1,18 @@ +
+ {{localize (concat "DAGGERHEART.APPLICATIONS.Downtime." category ".moves") max=nrChoices.max current=nrChoices.current}} + + +
\ No newline at end of file diff --git a/templates/dialogs/downtime/downtime.hbs b/templates/dialogs/downtime/downtime.hbs new file mode 100644 index 00000000..c8e44e5d --- /dev/null +++ b/templates/dialogs/downtime/downtime.hbs @@ -0,0 +1,14 @@ +
+
+

{{title}}

+
+ +
+ {{#if shortRestMoves.moves}}{{> "systems/daggerheart/templates/dialogs/downtime/activities.hbs" moves=shortRestMoves.moves category='shortRest' nrChoices=nrChoices.shortRest}}{{/if}} + {{#if longRestMoves.moves}}{{> "systems/daggerheart/templates/dialogs/downtime/activities.hbs" moves=longRestMoves.moves category='longRest' nrChoices=nrChoices.longRest}}{{/if}} +
+
+ + +
+
\ No newline at end of file diff --git a/templates/settings/automation-settings.hbs b/templates/settings/automation-settings.hbs index 0e158ab6..910ace56 100644 --- a/templates/settings/automation-settings.hbs +++ b/templates/settings/automation-settings.hbs @@ -6,7 +6,7 @@ {{formGroup settingFields.schema.fields.actionPoints value=settingFields._source.actionPoints localize=true}} -s {{formGroup settingFields.schema.fields.hordeDamage value=settingFields._source.hordeDamage localize=true}} + {{formGroup settingFields.schema.fields.hordeDamage value=settingFields._source.hordeDamage localize=true}}