diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..b472f383 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "js/ts.preferGoToSourceDefinition": true +} \ No newline at end of file diff --git a/assets/icons/documents/actors/capybara.svg b/assets/icons/documents/actors/capybara.svg index 90deb64a..8fbe2c06 100644 --- a/assets/icons/documents/actors/capybara.svg +++ b/assets/icons/documents/actors/capybara.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/actors/dark-squad.svg b/assets/icons/documents/actors/dark-squad.svg index f21b4c5b..39ca794a 100644 --- a/assets/icons/documents/actors/dark-squad.svg +++ b/assets/icons/documents/actors/dark-squad.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/actors/dragon-head.svg b/assets/icons/documents/actors/dragon-head.svg index d9e008f5..324ffbbb 100644 --- a/assets/icons/documents/actors/dragon-head.svg +++ b/assets/icons/documents/actors/dragon-head.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/actors/drama-masks.svg b/assets/icons/documents/actors/drama-masks.svg index 84307da0..98b9b68b 100644 --- a/assets/icons/documents/actors/drama-masks.svg +++ b/assets/icons/documents/actors/drama-masks.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/actors/forest.svg b/assets/icons/documents/actors/forest.svg index 8f7117e8..fcdb8a9d 100644 --- a/assets/icons/documents/actors/forest.svg +++ b/assets/icons/documents/actors/forest.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/items/battered-axe.svg b/assets/icons/documents/items/battered-axe.svg index 5d7be27d..436884bd 100644 --- a/assets/icons/documents/items/battered-axe.svg +++ b/assets/icons/documents/items/battered-axe.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/items/card-play.svg b/assets/icons/documents/items/card-play.svg index 587cb1c1..5c29fc43 100644 --- a/assets/icons/documents/items/card-play.svg +++ b/assets/icons/documents/items/card-play.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/items/chest-armor.svg b/assets/icons/documents/items/chest-armor.svg index 2cef80a6..03b82d48 100644 --- a/assets/icons/documents/items/chest-armor.svg +++ b/assets/icons/documents/items/chest-armor.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/items/family-tree.svg b/assets/icons/documents/items/family-tree.svg index d95c935d..031f23da 100644 --- a/assets/icons/documents/items/family-tree.svg +++ b/assets/icons/documents/items/family-tree.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/items/laurel-crown.svg b/assets/icons/documents/items/laurel-crown.svg index 34a54d2a..38dbcd04 100644 --- a/assets/icons/documents/items/laurel-crown.svg +++ b/assets/icons/documents/items/laurel-crown.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/items/laurels.svg b/assets/icons/documents/items/laurels.svg index 2c3cdf63..ae7398ec 100644 --- a/assets/icons/documents/items/laurels.svg +++ b/assets/icons/documents/items/laurels.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/items/open-treasure-chest.svg b/assets/icons/documents/items/open-treasure-chest.svg index 172a8003..d66399f4 100644 --- a/assets/icons/documents/items/open-treasure-chest.svg +++ b/assets/icons/documents/items/open-treasure-chest.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/items/round-potion.svg b/assets/icons/documents/items/round-potion.svg index 7f981914..0c235732 100644 --- a/assets/icons/documents/items/round-potion.svg +++ b/assets/icons/documents/items/round-potion.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/items/stars-stack.svg b/assets/icons/documents/items/stars-stack.svg index 19c197f6..2d446683 100644 --- a/assets/icons/documents/items/stars-stack.svg +++ b/assets/icons/documents/items/stars-stack.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/items/village.svg b/assets/icons/documents/items/village.svg index c28d742b..b7c776a1 100644 --- a/assets/icons/documents/items/village.svg +++ b/assets/icons/documents/items/village.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/documents/items/wolf-head.svg b/assets/icons/documents/items/wolf-head.svg index 2be500c1..7e57bbad 100644 --- a/assets/icons/documents/items/wolf-head.svg +++ b/assets/icons/documents/items/wolf-head.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/daggerheart.d.ts b/daggerheart.d.ts index 7ff7fd59..1641faa8 100644 --- a/daggerheart.d.ts +++ b/daggerheart.d.ts @@ -2,6 +2,7 @@ import '@client/global.mjs'; import '@common/global.mjs'; import '@common/primitives/global.mjs'; import Canvas from '@client/canvas/board.mjs'; +import { ResourceUpdateMap } from './module/data/action/baseAction.mjs'; // Foundry's use of `Object.assign(globalThis) means many globally available objects are not read as such // This declare global hopefully fixes that @@ -39,4 +40,43 @@ declare global { const Collection: foundry.utils.Collection; const FormDataExtended: foundry.applications.ux.FormDataExtended; const TextEditor: foundry.applications.ux.TextEditor; + + /** + * Data used to build rolls such as duality rolls. The definition is incomplete and likely incorrect. + * Objects will often accept a Partial and spit out a non-partial. Those that are not guaranteed should be marked optional. + */ + interface RollConfig { + // unverified, check which ones are used and optional/not optional + event: Event; + title: string; + roll: { + modifier: number; + simple: boolean; + type: string; + difficulty: number; + }; + hasDamage: boolean; + hasEffect: boolean; + hasRoll: boolean; + chatMessage: { + template: string; + mute: boolean; + }; + targets: object; + costs: object; + + // verified + source?: { + /** uuid of the actor this roll is coming from */ + actor: string; + }; + /** Roll data associated with the actor or item */ + data: object; + resourceUpdates: ResourceUpdateMap; + hooks: string[]; + dialog: { + configure: boolean; + }; + damageOptions: object; + } } diff --git a/eslint.config.mjs b/eslint.config.mjs index 3c9b8fd9..74141cc8 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -90,12 +90,26 @@ export default defineConfig([ }, rules: { 'no-undef': 'error', - // 'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], + 'no-unused-vars': [ + 'error', + { + args: 'none', + destructuredArrayIgnorePattern: '^_', + varsIgnorePattern: '^_[A-Z]', + ignoreRestSiblings: true + } + ], ...stylisticRules } }, { files: ['**/*.ts'], - extends: [js.configs.recommended, tseslint.configs.recommended] + extends: [js.configs.recommended, tseslint.configs.recommended], + plugins: { + '@stylistic': stylistic + }, + rules: { + ...stylisticRules + } } ]); diff --git a/jsconfig.json b/jsconfig.json index a0d51d0b..42c4b404 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -8,7 +8,7 @@ } }, "exclude": ["node_modules", "**/node_modules/*"], - "include": ["daggerheart.mjs", "foundry/client/client.mjs", "daggerheart.d.ts"], + "include": ["daggerheart.mjs", "foundry/client/client.mjs", "daggerheart.d.ts", "module/**/*.d.ts"], "typeAcquisition": { "include": ["jquery"] } diff --git a/lang/en.json b/lang/en.json index 9f16828e..a4a5edf0 100755 --- a/lang/en.json +++ b/lang/en.json @@ -484,7 +484,6 @@ "startFormula": "Randomized Start Value Formula", "currentCountdownCurrent": "Current: {value}", "currentCountdownStart": "Start: {value}", - "category": "Category", "progressionType": "Progression", "decreasing": "Decreasing", "looping": "Looping", @@ -1162,7 +1161,7 @@ "autoAppliedByLabel": "Max Stress" } }, - "CountdownType": { + "CountdownProgressType": { "actionRoll": "Action Roll", "characterAttack": "Character Attack", "characterSpotlight": "Character Spotlight", @@ -1170,6 +1169,10 @@ "fear": "Fear", "spotlight": "Spotlight" }, + "CountdownType": { + "encounter": { "label": "Short Term", "shortLabel": "Short" }, + "narrative": { "label": "Long Term", "shortLabel": "Long" } + }, "DaggerheartDiceAnimationEvents": { "critical": { "name": "Critical" }, "higher": { "name": "Highest Roll" } @@ -1448,7 +1451,7 @@ }, "triggerType": "Trigger Type", "triggeringActorType": "Triggering Actor Type", - "triggerError": "{trigger} trigger failed for {actor}. It's probably configured wrong." + "triggerError": "{trigger} trigger on {item}({itemUuid}) failed for {actor}. It's probably configured wrong." }, "WeaponFeature": { "barrier": { diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index e9140b56..048797d8 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -160,7 +160,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio } if (rest.hasOwnProperty('trait')) { this.config.roll.trait = rest.trait; - if (!this.config.source.item) + if (!this.config.source.item && this.config.roll.trait) this.config.title = game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { ability: game.i18n.localize(abilities[this.config.roll.trait]?.label) }); @@ -225,7 +225,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio } static async submitRoll(event) { - event.preventDefault(); await this.close({ submitted: true }); } diff --git a/module/applications/dialogs/damageReductionDialog.mjs b/module/applications/dialogs/damageReductionDialog.mjs index e5108e34..8cb026f0 100644 --- a/module/applications/dialogs/damageReductionDialog.mjs +++ b/module/applications/dialogs/damageReductionDialog.mjs @@ -176,7 +176,6 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap } static updateData(event, _, formData) { - const form = foundry.utils.expandObject(formData.object); this.render(true); } diff --git a/module/applications/dialogs/groupRollDialog.mjs b/module/applications/dialogs/groupRollDialog.mjs index 7196d848..ebc80d39 100644 --- a/module/applications/dialogs/groupRollDialog.mjs +++ b/module/applications/dialogs/groupRollDialog.mjs @@ -1,4 +1,5 @@ import { ResourceUpdateMap } from '../../data/action/baseAction.mjs'; +import { shouldUseHopeFearAutomation } from '../../helpers/utils.mjs'; import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import Party from '../sheets/actors/party.mjs'; @@ -480,19 +481,22 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat await cls.create(msgData); - const resourceMap = new ResourceUpdateMap(actor); - if (totalRoll.isCritical) { - resourceMap.addResources([ - { key: 'stress', value: -1, total: 1 }, - { key: 'hope', value: 1, total: 1 } - ]); - } else if (totalRoll.withHope) { - resourceMap.addResources([{ key: 'hope', value: 1, total: 1 }]); - } else { - resourceMap.addResources([{ key: 'fear', value: 1, total: 1 }]); - } + /* Handle resource updates for the finished GroupRoll */ + if (shouldUseHopeFearAutomation({ gmAsPlayer: true })) { + const resourceMap = new ResourceUpdateMap(actor); + if (totalRoll.isCritical) { + resourceMap.addResources([ + { key: 'stress', value: -1 }, + { key: 'hope', value: 1 } + ]); + } else if (totalRoll.withHope) { + resourceMap.addResources([{ key: 'hope', value: 1 }]); + } else { + resourceMap.addResources([{ key: 'fear', value: 1 }]); + } - resourceMap.updateResources(); + resourceMap.updateResources(); + } /* Fin */ this.cancelRoll({ confirm: false }); diff --git a/module/applications/dialogs/tagTeamDialog.mjs b/module/applications/dialogs/tagTeamDialog.mjs index b2ce0258..19869a00 100644 --- a/module/applications/dialogs/tagTeamDialog.mjs +++ b/module/applications/dialogs/tagTeamDialog.mjs @@ -1,5 +1,6 @@ +import { ResourceUpdateMap } from '../../data/action/baseAction.mjs'; import { MemberData } from '../../data/tagTeamData.mjs'; -import { getCritDamageBonus } from '../../helpers/utils.mjs'; +import { getCritDamageBonus, shouldUseHopeFearAutomation } from '../../helpers/utils.mjs'; import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import Party from '../sheets/actors/party.mjs'; @@ -9,6 +10,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio constructor(party) { super({ id: `TagTeamDialog-${party.id}` }); + this.usesTagTeamHopeCost = true; this.party = party; this.partyMembers = party.system.partyMembers .filter(x => Party.DICE_ROLL_ACTOR_TYPES.includes(x.type)) @@ -20,7 +22,9 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio owned: member.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER) })); - this.initiator = { cost: 3 }; + this.initiator = { cost: + this.party.system.schema.fields.tagTeam.fields.initiator.fields.cost.initial + }; this.openForAllPlayers = true; this.tabGroups.application = Object.keys(party.system.tagTeam.members).length @@ -94,9 +98,13 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio ?.addEventListener('input', this.updateInitiatorMemberField.bind(this)); htmlElement - .querySelector('.initiator-cost-field') + .querySelector('.initiator-cost-input') ?.addEventListener('input', this.updateInitiatorCostField.bind(this)); + htmlElement + .querySelector('.initiator-cost-enabled-checkbox') + ?.addEventListener('change', this.toggleInitiatorCostEnabled.bind(this)); + htmlElement .querySelector('.openforall-field') ?.addEventListener('change', this.updateOpenForAllField.bind(this)); @@ -156,6 +164,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio .map(x => ({ value: x.id, label: x.name })); partContext.initiatorDisabled = !selectedMembers.length; partContext.openForAllPlayers = this.openForAllPlayers; + partContext.usesTagTeamHopeCost = this.usesTagTeamHopeCost; break; case 'tagTeamRoll': @@ -397,6 +406,13 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio this.render(); } + toggleInitiatorCostEnabled(_event) { + this.usesTagTeamHopeCost = !this.usesTagTeamHopeCost; + this.initiator.cost = this.usesTagTeamHopeCost ? + this.party.system.schema.fields.tagTeam.fields.initiator.fields.cost.initial : 0; + this.render(); + } + updateOpenForAllField(event) { this.openForAllPlayers = event.target.checked; this.render(); @@ -752,32 +768,37 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio /* Handle resource updates from the finished TagTeamRoll */ const tagTeamData = this.party.system.tagTeam; - const fearUpdate = { key: 'fear', value: null, total: null, enabled: true }; - for (let memberId in tagTeamData.members) { - const resourceUpdates = []; - const rollGivesHope = finalRoll.isCritical || finalRoll.withHope; - if (memberId === tagTeamData.initiator.memberId) { - const value = tagTeamData.initiator.cost - ? rollGivesHope - ? 1 - tagTeamData.initiator.cost - : -tagTeamData.initiator.cost - : 1; - resourceUpdates.push({ key: 'hope', value: value, total: -value, enabled: true }); - } else if (rollGivesHope) { - resourceUpdates.push({ key: 'hope', value: 1, total: -1, enabled: true }); - } - if (finalRoll.isCritical) resourceUpdates.push({ key: 'stress', value: -1, total: 1, enabled: true }); - if (finalRoll.withFear) { - fearUpdate.value = fearUpdate.value === null ? 1 : fearUpdate.value + 1; - fearUpdate.total = fearUpdate.total === null ? -1 : fearUpdate.total - 1; - } - game.actors.get(memberId).modifyResource(resourceUpdates); + const actorResourceMaps = Object.keys(tagTeamData.members).reduce((acc, key) => { + acc[key] = new ResourceUpdateMap(game.actors.get(key)); + return acc; + }, {}); + + if (shouldUseHopeFearAutomation({ gmAsPlayer: true })) { + const fearResourceMap = actorResourceMaps[tagTeamData.initiator.memberId]; + for (const memberId in tagTeamData.members) { + const resourceMap = actorResourceMaps[memberId]; + if (finalRoll.isCritical) { + resourceMap.addResources([ + { key: 'stress', value: -1, enabled: true }, + { key: 'hope', value: 1, enabled: true } + ]); + } else if (finalRoll.withHope) { + resourceMap.addResources([{ key: 'hope', value: 1, enabled: true }]); + } else if (finalRoll.withFear) { + fearResourceMap.addResources([{ key: 'fear', value: 1, enabled: true }]); + } + } + } + + /* Even with Hope/Fear automation off, the hope cost of performing the TagTeamRoll can still optionally be subtracted */ + if (tagTeamData.initiator.cost) { + const resourceMap = actorResourceMaps[tagTeamData.initiator.memberId]; + resourceMap.addResources([{ key: 'hope', value: -tagTeamData.initiator.cost, enabled: true }]); } - if (fearUpdate.value) { - mainActor.modifyResource([fearUpdate]); - } + for (const resourceMap of Object.values(actorResourceMaps)) + resourceMap.updateResources(); /* Fin */ this.cancelRoll({ confirm: false }); diff --git a/module/applications/levelup/levelup.mjs b/module/applications/levelup/levelup.mjs index cafc5c89..f40609a4 100644 --- a/module/applications/levelup/levelup.mjs +++ b/module/applications/levelup/levelup.mjs @@ -1,4 +1,3 @@ -import { abilities, subclassFeatureLabels } from '../../config/actorConfig.mjs'; import { getDeleteKeys, tagifyElement } from '../../helpers/utils.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; diff --git a/module/applications/settings/appearanceSettings.mjs b/module/applications/settings/appearanceSettings.mjs index 9de9e752..f2defffb 100644 --- a/module/applications/settings/appearanceSettings.mjs +++ b/module/applications/settings/appearanceSettings.mjs @@ -1,12 +1,13 @@ -import DhAppearance from '../../data/settings/Appearance.mjs'; import { getDiceSoNicePreset } from '../../config/generalConfig.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; /** + * @import DhAppearance from '../../data/settings/Appearance.mjs'; * @import {ApplicationClickAction} from "@client/applications/_types.mjs" */ +/** Settings menu for appearance settings */ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(ApplicationV2) { /**@inheritdoc */ static DEFAULT_OPTIONS = { diff --git a/module/applications/sheets-configs/adversary-settings.mjs b/module/applications/sheets-configs/adversary-settings.mjs index 57405675..ff3f3039 100644 --- a/module/applications/sheets-configs/adversary-settings.mjs +++ b/module/applications/sheets-configs/adversary-settings.mjs @@ -10,11 +10,7 @@ export default class DHAdversarySettings extends DHBaseActorSettings { actions: { addExperience: DHAdversarySettings.#addExperience, removeExperience: DHAdversarySettings.#removeExperience - }, - dragDrop: [ - { dragSelector: null, dropSelector: '.tab.features' }, - { dragSelector: '.feature-item', dropSelector: null } - ] + } }; /**@override */ @@ -38,7 +34,8 @@ export default class DHAdversarySettings extends DHBaseActorSettings { }, features: { id: 'features', - template: 'systems/daggerheart/templates/sheets-settings/adversary-settings/features.hbs' + template: 'systems/daggerheart/templates/sheets-settings/adversary-settings/features.hbs', + scrollable: [''] } }; @@ -54,12 +51,16 @@ export default class DHAdversarySettings extends DHBaseActorSettings { async _prepareContext(options) { const context = await super._prepareContext(options); - const featureForms = ['passive', 'action', 'reaction']; - context.features = context.document.system.features.sort((a, b) => - a.system.featureForm !== b.system.featureForm - ? featureForms.indexOf(a.system.featureForm) - featureForms.indexOf(b.system.featureForm) - : a.sort - b.sort - ); + // Get feature groups. Uncategorized go to actions + const featureFormsTypes = ['passive', 'action', 'reaction']; + const features = this.document.system.features.sort((a, b) => a.sort - b.sort); + const featureGroups = featureFormsTypes.map(t => ({ + featureForm: t, + label: _loc(CONFIG.DH.ITEM.featureForm[t]), + features: features.filter(f => f.system.featureForm === t) + })); + featureGroups[1].features.push(...features.filter(f => !featureFormsTypes.includes(f.system.featureForm))); + context.featureGroups = featureGroups; return context; } @@ -97,32 +98,4 @@ export default class DHAdversarySettings extends DHBaseActorSettings { await this.actor.update({ [`system.experiences.${target.dataset.experience}`]: _del }); } - - async _onDragStart(event) { - const featureItem = event.currentTarget.closest('.feature-item'); - - if (featureItem) { - const feature = this.actor.items.get(featureItem.id); - const featureData = { type: 'Item', uuid: feature.uuid, fromInternal: true }; - event.dataTransfer.setData('text/plain', JSON.stringify(featureData)); - event.dataTransfer.setDragImage(featureItem.querySelector('img'), 60, 0); - } - } - - async _onDrop(event) { - event.stopPropagation(); - const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); - - const item = await fromUuid(data.uuid); - if (item?.type === 'feature') { - if (data.fromInternal && item.parent?.uuid === this.actor.uuid) { - return; - } - - const itemData = item.toObject(); - delete itemData._id; - - await this.actor.createEmbeddedDocuments('Item', [itemData]); - } - } } diff --git a/module/applications/sheets-configs/environment-settings.mjs b/module/applications/sheets-configs/environment-settings.mjs index 6d74f9c6..954d6381 100644 --- a/module/applications/sheets-configs/environment-settings.mjs +++ b/module/applications/sheets-configs/environment-settings.mjs @@ -15,7 +15,7 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings { dragDrop: [ { dragSelector: null, dropSelector: '.category-container' }, { dragSelector: null, dropSelector: '.tab.features' }, - { dragSelector: '.feature-item', dropSelector: null } + { dragSelector: '.feature-item, .inventory-item[data-type="adversary"]', dropSelector: null } ] }; @@ -32,11 +32,13 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings { }, features: { id: 'features', - template: 'systems/daggerheart/templates/sheets-settings/environment-settings/features.hbs' + template: 'systems/daggerheart/templates/sheets-settings/environment-settings/features.hbs', + scrollable: [''] }, adversaries: { id: 'adversaries', - template: 'systems/daggerheart/templates/sheets-settings/environment-settings/adversaries.hbs' + template: 'systems/daggerheart/templates/sheets-settings/environment-settings/adversaries.hbs', + scrollable: [''] } }; @@ -52,12 +54,16 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings { async _prepareContext(options) { const context = await super._prepareContext(options); - const featureForms = ['passive', 'action', 'reaction']; - context.features = context.document.system.features.sort((a, b) => - a.system.featureForm !== b.system.featureForm - ? featureForms.indexOf(a.system.featureForm) - featureForms.indexOf(b.system.featureForm) - : a.sort - b.sort - ); + // Get feature groups. Uncategorized go to actions + const featureFormsTypes = ['passive', 'action', 'reaction']; + const features = this.document.system.features.sort((a, b) => a.sort - b.sort); + const featureGroups = featureFormsTypes.map(t => ({ + featureForm: t, + label: _loc(CONFIG.DH.ITEM.featureForm[t]), + features: features.filter(f => f.system.featureForm === t) + })); + featureGroups[1].features.push(...features.filter(f => !featureFormsTypes.includes(f.system.featureForm))); + context.featureGroups = featureGroups; return context; } @@ -110,33 +116,30 @@ export default class DHEnvironmentSettings extends DHBaseActorSettings { } async _onDragStart(event) { - const featureItem = event.currentTarget.closest('.feature-item'); - - if (featureItem) { - const feature = this.actor.items.get(featureItem.id); - const featureData = { type: 'Item', uuid: feature.uuid, fromInternal: true }; - event.dataTransfer.setData('text/plain', JSON.stringify(featureData)); - event.dataTransfer.setDragImage(featureItem.querySelector('img'), 60, 0); + const element = event.currentTarget.closest('.inventory-item[data-type=adversary]'); + if (element) { + const adversaryData = { type: 'Actor', uuid: element.dataset.itemUuid }; + event.dataTransfer.setData('text/plain', JSON.stringify(adversaryData)); + event.dataTransfer.setDragImage(element, 60, 0); + } else { + return super._onDragStart(event); } } async _onDrop(event) { event.stopPropagation(); const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); - const item = await fromUuid(data.uuid); - if (data.fromInternal && item?.parent?.uuid === this.actor.uuid) return; - - if (item.type === 'adversary' && event.target.closest('.category-container')) { + const doc = await fromUuid(data.uuid); + if (doc?.type === 'adversary' && event.target.closest('.category-container')) { const target = event.target.closest('.category-container'); const path = `system.potentialAdversaries.${target.dataset.potentialAdversary}.adversaries`; const current = foundry.utils.getProperty(this.actor, path).map(x => x.uuid); - await this.actor.update({ - [path]: [...current, item.uuid] - }); - this.render(); - } else if (item.type === 'feature' && event.target.closest('.tab.features')) { - await this.actor.createEmbeddedDocuments('Item', [item]); - this.render(); + if (!current.includes(doc.uuid)) { + await this.actor.update({ [path]: [...current, doc.uuid] }); + } + return; } + + return super._onDrop(event); } } diff --git a/module/applications/sheets-configs/npc-settings.mjs b/module/applications/sheets-configs/npc-settings.mjs index c187877c..33cbc270 100644 --- a/module/applications/sheets-configs/npc-settings.mjs +++ b/module/applications/sheets-configs/npc-settings.mjs @@ -27,7 +27,8 @@ export default class DHNPCSettings extends DHBaseActorSettings { }, features: { id: 'features', - template: 'systems/daggerheart/templates/sheets-settings/npc-settings/features.hbs' + template: 'systems/daggerheart/templates/sheets-settings/npc-settings/features.hbs', + scrollable: [''] } }; @@ -43,43 +44,17 @@ export default class DHNPCSettings extends DHBaseActorSettings { async _prepareContext(options) { const context = await super._prepareContext(options); - const featureForms = ['passive', 'action', 'reaction']; - context.features = context.document.system.features.sort((a, b) => - a.system.featureForm !== b.system.featureForm - ? featureForms.indexOf(a.system.featureForm) - featureForms.indexOf(b.system.featureForm) - : a.sort - b.sort - ); + // Get feature groups. Uncategorized go to actions + const featureFormsTypes = ['passive', 'action', 'reaction']; + const features = this.document.system.features.sort((a, b) => a.sort - b.sort); + const featureGroups = featureFormsTypes.map(t => ({ + featureForm: t, + label: _loc(CONFIG.DH.ITEM.featureForm[t]), + features: features.filter(f => f.system.featureForm === t) + })); + featureGroups[1].features.push(...features.filter(f => !featureFormsTypes.includes(f.system.featureForm))); + context.featureGroups = featureGroups; return context; } - - /* -------------------------------------------- */ - - async _onDragStart(event) { - const featureItem = event.currentTarget.closest('.feature-item'); - - if (featureItem) { - const feature = this.actor.items.get(featureItem.id); - const featureData = { type: 'Item', uuid: feature.uuid, fromInternal: true }; - event.dataTransfer.setData('text/plain', JSON.stringify(featureData)); - event.dataTransfer.setDragImage(featureItem.querySelector('img'), 60, 0); - } - } - - async _onDrop(event) { - event.stopPropagation(); - const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); - - const item = await fromUuid(data.uuid); - if (item?.type === 'feature') { - if (data.fromInternal && item.parent?.uuid === this.actor.uuid) { - return; - } - - const itemData = item.toObject(); - delete itemData._id; - - await this.actor.createEmbeddedDocuments('Item', [itemData]); - } - } } diff --git a/module/applications/sheets/actors/_types.d.ts b/module/applications/sheets/actors/_types.d.ts new file mode 100644 index 00000000..89adc57b --- /dev/null +++ b/module/applications/sheets/actors/_types.d.ts @@ -0,0 +1,8 @@ +import DhCompanion from '../../../data/actor/companion.mjs'; +import DhpActor from '../../../documents/actor.mjs'; + +declare module './companion.mjs' { + export default interface DhCompanionSheet { + actor: DhpActor; + } +} diff --git a/module/applications/sheets/actors/adversary.mjs b/module/applications/sheets/actors/adversary.mjs index c8d5a299..85380392 100644 --- a/module/applications/sheets/actors/adversary.mjs +++ b/module/applications/sheets/actors/adversary.mjs @@ -103,7 +103,7 @@ export default class AdversarySheet extends DHBaseActorSheet { context.resources.stress.emptyPips = context.resources.stress.max < maxResource ? maxResource - context.resources.stress.max : 0; - const featureForms = ['passive', 'action', 'reaction']; + const featureForms = Object.keys(CONFIG.DH.ITEM.featureForm); context.features = this.document.system.features.sort((a, b) => a.system.featureForm !== b.system.featureForm ? featureForms.indexOf(a.system.featureForm) - featureForms.indexOf(b.system.featureForm) @@ -131,6 +131,15 @@ export default class AdversarySheet extends DHBaseActorSheet { return context; } + /** @inheritdoc */ + _prepareTabs(group) { + const result = super._prepareTabs(group); + if (group === 'primary') { + result.notes.empty = !this.document.system.notes?.trim(); + } + return result; + } + /**@inheritdoc */ _attachPartListeners(partId, htmlElement, options) { super._attachPartListeners(partId, htmlElement, options); diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index f0f8326f..3a60e7ca 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -809,7 +809,7 @@ export default class CharacterSheet extends DHBaseActorSheet { /* This could be avoided by baking config.costs into config.resourceUpdates. Didn't feel like messing with it at the time */ const costResources = - result.costs?.filter(x => x.enabled).map(cost => ({ ...cost, value: -cost.value, total: -cost.total })) || + result.costs?.filter(x => x.enabled).map(cost => ({ ...cost, value: -cost.value })) || {}; result.resourceUpdates.addResources(costResources); await result.resourceUpdates.updateResources(); diff --git a/module/applications/sheets/actors/environment.mjs b/module/applications/sheets/actors/environment.mjs index f8ff74a6..60fb9071 100644 --- a/module/applications/sheets/actors/environment.mjs +++ b/module/applications/sheets/actors/environment.mjs @@ -72,13 +72,22 @@ export default class DhpEnvironment extends DHBaseActorSheet { return applicationOptions; } + /** @inheritdoc */ + _prepareTabs(group) { + const result = super._prepareTabs(group); + if (group === 'primary') { + result.potentialAdversaries.empty = foundry.utils.isEmpty(this.document.system.potentialAdversaries); + result.notes.empty = !this.document.system.notes?.trim(); + } + return result; + } + /**@inheritdoc */ async _preparePartContext(partId, context, options) { context = await super._preparePartContext(partId, context, options); switch (partId) { case 'header': await this._prepareHeaderContext(context, options); - break; case 'features': await this._prepareFeaturesContext(context, options); diff --git a/module/applications/sheets/api/actor-setting.mjs b/module/applications/sheets/api/actor-setting.mjs index 738f7002..fa434034 100644 --- a/module/applications/sheets/api/actor-setting.mjs +++ b/module/applications/sheets/api/actor-setting.mjs @@ -1,13 +1,15 @@ import DHApplicationMixin from './application-mixin.mjs'; -const { DocumentSheetV2 } = foundry.applications.api; +const { ActorSheetV2 } = foundry.applications.sheets; -/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ +/** + * @typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction + */ /** * Base settings sheet for Daggerheart actors. - * @extends {DHApplicationMixin} + * @extends {DHApplicationMixin} */ -export default class DHBaseActorSettings extends DHApplicationMixin(DocumentSheetV2) { +export default class DHBaseActorSettings extends DHApplicationMixin(ActorSheetV2) { /**@inheritdoc */ static DEFAULT_OPTIONS = { classes: ['dialog'], @@ -23,7 +25,7 @@ export default class DHBaseActorSettings extends DHApplicationMixin(DocumentShee }, dragDrop: [ { dragSelector: null, dropSelector: '.tab.features' }, - { dragSelector: '.feature-item', dropSelector: null } + { dragSelector: '.feature-item, .inventory-item[data-type="feature"]', dropSelector: null } ] }; @@ -34,7 +36,7 @@ export default class DHBaseActorSettings extends DHApplicationMixin(DocumentShee return options; } - /**@returns {foundry.documents.Actor} */ + /** @returns {foundry.documents.Actor} */ get actor() { return this.document; } @@ -73,4 +75,32 @@ export default class DHBaseActorSettings extends DHApplicationMixin(DocumentShee return context; } + + async _onDragStart(event) { + const featureItemEl = event.currentTarget.closest('.feature-item, .inventory-item[data-type="feature"]'); + const feature = this.actor.items.get(featureItemEl?.dataset.itemId); + if (feature && event.target.closest('.tab.features')) { + const featureData = { ...feature.toDragData(), fromInternal: true }; + event.dataTransfer.setData('text/plain', JSON.stringify(featureData)); + event.dataTransfer.setDragImage(featureItemEl.querySelector('img'), 60, 0); + } + } + + async _onDrop(event) { + event.stopPropagation(); + const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); + const item = await fromUuid(data.uuid); + if (item?.type === 'feature') { + if (data.fromInternal && item.parent?.uuid === this.actor.uuid) { + return super._onDrop(event); + } + + const itemData = item.toObject(); + delete itemData._id; + await this.actor.createEmbeddedDocuments('Item', [itemData]); + } + } + + /** Setting sheets do not auto extend */ + async _autoExpandDescriptions() {} } diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index 752dc80b..98f38f03 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -4,6 +4,7 @@ import { getDocFromElement, getDocFromElementSync, tagifyElement } from '../../. const typeSettingsMap = { character: 'extendCharacterDescriptions', adversary: 'extendAdversaryDescriptions', + npc: 'extendAdversaryDescriptions', environment: 'extendEnvironmentDescriptions', ancestry: 'extendItemDescriptions', community: 'extendItemDescriptions', @@ -262,7 +263,7 @@ export default function DHApplicationMixin(Base) { if (!!this.options.contextMenus.length) this._createContextMenus(); - this.#autoExtendDescriptions(context); + this._autoExpandDescriptions(context); } /** @inheritDoc */ @@ -381,12 +382,35 @@ export default function DHApplicationMixin(Base) { * @protected */ _onDrop(event) { - // Fallback to super, but note that config sheets don't have this option - // We still need this to avoid setting apps having issues + // Potentially handle subclasses that dont descend from actor/item sheet. event.stopPropagation(); return super._onDrop?.(event); } + /** @inheritdoc */ + _onSortItem(event, item) { + // If we are dragging a feature past its allowed feature form, put it in the front or in the back + const doc = this.actor.items.get(item.id); + const dropTargetEl = event.target.closest('[data-item-id]'); + const dropTarget = this.actor.items.get(dropTargetEl?.dataset.itemId); + if (doc?.type === 'feature' && dropTarget?.type === 'feature' && doc.system.featureForm !== dropTarget.system.featureForm) { + const siblings = this.actor.itemTypes.feature + .filter(f => f.system.featureForm === doc.system.featureForm) + .sort((a, b) => a.sort - b.sort); + if (siblings.length > 1) { + const featureForms = Object.keys(CONFIG.DH.ITEM.featureForm); + const thisFeatureIdx = featureForms.indexOf(doc.system.featureForm); + const targetFeatureIdx = featureForms.indexOf(dropTarget.system.featureForm); + const target = targetFeatureIdx < thisFeatureIdx ? siblings[0] : siblings.at(-1); + const sortUpdates = foundry.utils.performIntegerSort(doc, { target, siblings }); + const updateData = sortUpdates.map(u => ({ ...u.update, _id: u.target._id })); + return this.actor.updateEmbeddedDocuments('Item', updateData); + } + } + + return super._onSortItem?.(event, item); + } + /* -------------------------------------------- */ /* Context Menu */ /* -------------------------------------------- */ @@ -494,7 +518,7 @@ export default function DHApplicationMixin(Base) { const doc = await getDocFromElement(target), action = doc?.system?.attack ?? doc; const config = action.prepareConfig(event); - config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects( + config.effects = await game.system.api.data.actions.actionsTypes.base.getActionRelevantEffects( this.document, doc ); @@ -607,8 +631,9 @@ export default function DHApplicationMixin(Base) { /** * Extend inventory description when enabled in settings. * @returns {Promise} + * @protected */ - async #autoExtendDescriptions(context) { + async _autoExpandDescriptions(context) { const inventoryItems = this.element.querySelectorAll('.inventory-item[data-item-uuid]'); for (const el of inventoryItems) { // Get the doc uuid from the element @@ -711,7 +736,7 @@ export default function DHApplicationMixin(Base) { * @type {ApplicationClickAction} */ static async #onCreateDoc(event, target) { - const { documentClass, type, inVault, disabled } = target.dataset; + const { documentClass, type, inVault, disabled, featureForm } = target.dataset; const parentIsItem = this.document.documentName === 'Item'; const featureOnCharacter = this.document.parent?.type === 'character' && type === 'feature'; const parent = featureOnCharacter @@ -729,6 +754,7 @@ export default function DHApplicationMixin(Base) { identifier: this.document.system.isMulticlass ? 'multiclass' : null }; } + if (featureForm) systemData.featureForm = featureForm; const cls = type === 'action' ? game.system.api.models.actions.actionsTypes.base : getDocumentClass(documentClass); diff --git a/module/applications/sheets/api/base-actor.mjs b/module/applications/sheets/api/base-actor.mjs index 027dd397..007b641b 100644 --- a/module/applications/sheets/api/base-actor.mjs +++ b/module/applications/sheets/api/base-actor.mjs @@ -1,10 +1,12 @@ import { getDocFromElement, itemIsIdentical } from '../../../helpers/utils.mjs'; -import DHBaseActorSettings from './actor-setting.mjs'; import DHApplicationMixin from './application-mixin.mjs'; const { ActorSheetV2 } = foundry.applications.sheets; -/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ +/** + * @import DHBaseActorSettings from './actor-setting.mjs'; + * @typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction + */ /** * A base actor sheet extending {@link ActorSheetV2} via {@link DHApplicationMixin} @@ -210,7 +212,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { const doc = await getDocFromElement(target), action = doc?.system?.attack ?? doc; const config = action.prepareConfig(event); - config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects( + config.effects = await game.system.api.data.actions.actionsTypes.base.getActionRelevantEffects( this.document, doc ); diff --git a/module/applications/sidebar/tabs/actorDirectory.mjs b/module/applications/sidebar/tabs/actorDirectory.mjs index a6f48b54..4d00afa2 100644 --- a/module/applications/sidebar/tabs/actorDirectory.mjs +++ b/module/applications/sidebar/tabs/actorDirectory.mjs @@ -1,6 +1,6 @@ export default class DhActorDirectory extends foundry.applications.sidebar.tabs.ActorDirectory { static DEFAULT_OPTIONS = { - renderUpdateKeys: ['system.levelData.level.current', 'system.partner', 'system.tier'] + renderUpdateKeys: ['system.levelData.level.current', 'system.partner', 'system.tier', 'system.type'] }; static _entryPartial = 'systems/daggerheart/templates/ui/sidebar/actor-document-partial.hbs'; diff --git a/module/applications/ui/countdownEdit.mjs b/module/applications/ui/countdownEdit.mjs index 5974e1f2..1dbc56be 100644 --- a/module/applications/ui/countdownEdit.mjs +++ b/module/applications/ui/countdownEdit.mjs @@ -35,7 +35,7 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio static PARTS = { countdowns: { - template: 'systems/daggerheart/templates/ui/countdown-edit.hbs', + template: 'systems/daggerheart/templates/ui/countdowns/countdown-edit.hbs', scrollable: ['.expanded-view', '.edit-content'] } }; @@ -45,7 +45,7 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio context.isGM = game.user.isGM; context.ownershipDefaultOptions = CONFIG.DH.GENERAL.basicOwnershiplevels; context.defaultOwnership = this.data.defaultOwnership; - context.countdownBaseTypes = CONFIG.DH.GENERAL.countdownBaseTypes; + context.countdownTypes = CONFIG.DH.GENERAL.countdownTypes; context.countdownProgressionTypes = CONFIG.DH.GENERAL.countdownProgressionTypes; context.countdownLoopingTypes = CONFIG.DH.GENERAL.countdownLoopingTypes; context.hideNewCountdowns = this.hideNewCountdowns; @@ -62,7 +62,7 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio const randomizeValid = !new Roll(countdown.progress.startFormula ?? '').isDeterministic; acc[key] = { ...countdown, - typeName: game.i18n.localize(CONFIG.DH.GENERAL.countdownBaseTypes[countdown.type].label), + typeName: game.i18n.localize(CONFIG.DH.GENERAL.countdownTypes[countdown.type].label), progress: { ...countdown.progress, typeName: game.i18n.localize( diff --git a/module/applications/ui/countdowns.mjs b/module/applications/ui/countdowns.mjs index 2a2a113e..5cf79100 100644 --- a/module/applications/ui/countdowns.mjs +++ b/module/applications/ui/countdowns.mjs @@ -11,9 +11,14 @@ const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; */ export default class DhCountdowns extends HandlebarsApplicationMixin(ApplicationV2) { + previousCountdownData = null; + changedCountdownsForAnimation = new Set(); + constructor(options = {}) { super(options); + this.previousCountdownData = + game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns).countdowns; this.setupHooks(); } @@ -32,6 +37,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application }, actions: { toggleViewMode: DhCountdowns.#onToggleViewMode, + toggleCountdownTypes: DhCountdowns.#onToggleCountdownTypes, editCountdowns: DhCountdowns.#onEditCountdowns, loopCountdown: DhCountdowns.#onLoopCountdown, decreaseCountdown: (_, target) => this.editCountdown(false, target), @@ -48,11 +54,20 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application static PARTS = { resources: { root: true, - template: 'systems/daggerheart/templates/ui/countdowns.hbs' + template: 'systems/daggerheart/templates/ui/countdowns/countdowns-view.hbs' } }; - /**@inheritdoc */ + /** + * Returns all visible countdown types + * @returns {string[]} + */ + get visibleCountdownTypes() { + const { encounter, narrative } = CONFIG.DH.GENERAL.countdownTypes; + return game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownTypeModes) + ?? [encounter.id, narrative.id]; + } + async _renderFrame(options) { const frame = await super._renderFrame(options); @@ -65,6 +80,57 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application return frame; } + + async _onRender(context, options) { + await super._onRender(context, options); + + /* Handle rendering/hiding/positioning of the countdown UI */ + this.element.hidden = !game.user.isGM && this.#getCountdowns().length === 0; + if (options?.force) { + document.getElementById('ui-right-column-1')?.appendChild(this.element); + } + + this.previousCountdownData = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns) + .countdowns; + + /* Handle animations to draw attention to countdown values changing */ + const typesToAnimate = new Set(); + for (const countdownKey of this.changedCountdownsForAnimation) { + const shimmerAnimation = [ + { backgroundPositionX: '98%' }, + { backgroundPositionX: '0%' } + ]; + const shimmerTiming = { + duration: 1000, + iterations: 1 + }; + + const element = this.element.querySelector(`.countdown-container[data-countdown="${countdownKey}"]`); + element?.animate(shimmerAnimation, shimmerTiming); + + const countdown = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns) + .countdowns[countdownKey]; + if (!this.visibleCountdownTypes.includes(countdown?.type)) + typesToAnimate.add(countdown.type); + } + + for (const type of typesToAnimate) { + const pulseAnimation = [ + { boxShadow: '0 0 1px 1px var(--golden)' }, + { boxShadow: '0 0 2px 2px var(--golden)' } + ]; + const pulseTiming = { + duration: 1000, + iterations: 3 + }; + + const element = this.element.querySelector(`.header-type-toggles .header-type[data-type="${type}"]`); + element?.animate(pulseAnimation, pulseTiming); + } + + this.changedCountdownsForAnimation.clear(); + } + /** Returns countdown data filtered by ownership */ #getCountdowns() { const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); @@ -76,16 +142,10 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application return values.filter(v => v.ownership !== CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE); } - /** @override */ - async _prepareContext(options) { - const context = await super._prepareContext(options); - context.isGM = game.user.isGM; - - context.iconOnly = - game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode) === - CONFIG.DH.GENERAL.countdownAppMode.iconOnly; + _getCountdownData() { const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); - context.countdowns = this.#getCountdowns().reduce((acc, { key, countdown, ownership }) => { + + return this.#getCountdowns().reduce((acc, { key, countdown, ownership }) => { const playersWithAccess = game.users.reduce((acc, user) => { const ownership = DhCountdowns.#getPlayerOwnership(user, setting, countdown); if (!user.isGM && ownership && ownership !== CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE) { @@ -108,7 +168,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application !countdownEditable || (isLooping && (countdown.progress.current > 0 || countdown.progress.start === '0')); - acc[key] = { + acc[countdown.type][key] = { ...countdown, editable: countdownEditable, noPlayerAccess: nonGmPlayers.length && playersWithAccess.length === 0, @@ -117,7 +177,38 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application loopTooltip: isLooping && game.i18n.localize(loopTooltip) }; return acc; - }, {}); + }, Object.keys(CONFIG.DH.GENERAL.countdownTypes).reduce((acc, key) => { + acc[key] = {}; + return acc; + }, {})); + } + + /** @override */ + async _prepareContext(options) { + const context = await super._prepareContext(options); + context.isGM = game.user.isGM; + + context.iconOnly = + game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode) + === CONFIG.DH.GENERAL.countdownAppMode.iconOnly; + + context.userCountdownTypes = this.visibleCountdownTypes; + + context.typeToggles = + Object.values(CONFIG.DH.GENERAL.countdownTypes).map(type => ({ + type: type.id, + label: game.i18n.localize(type.shortLabel), + active: context.userCountdownTypes.includes(type.id) + })); + + context.countdowns = this._getCountdownData(); + context.countdownTypesWithVisibleEntries = this.#getCountdowns().reduce((acc, data) => { + if (context.userCountdownTypes.includes(data.countdown.type) && !acc.includes(data.countdown.type)) + acc.push(data.countdown.type); + + return acc; + }, []); + return context; } @@ -147,6 +238,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application return true; } + /** @this {DhCountdowns} */ static async #onToggleViewMode() { const currentMode = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownMode); const appMode = CONFIG.DH.GENERAL.countdownAppMode; @@ -158,10 +250,24 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application this.render(); } + /** @this {DhCountdowns} */ + static async #onToggleCountdownTypes(event, target) { + const currentTypes = this.visibleCountdownTypes; + const { type } = target.dataset; + const newTypes = event.shiftKey ? + [type] : + currentTypes.includes(type) ? currentTypes.filter(x => x !== type) : [...currentTypes, type]; + await game.user.setFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.userFlags.countdownTypeModes, newTypes); + + this.render(); + } + + /** @this {DhCountdowns} */ static async #onEditCountdowns() { new game.system.api.applications.ui.CountdownEdit().render(true); } + /** @this {DhCountdowns} */ static async #onLoopCountdown(_, target) { if (!DhCountdowns.canPerformEdit()) return; @@ -236,29 +342,31 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application * Sends updates of the countdowns to the GM player. Since this is asynchronous, be sure to * update all the countdowns at the same time. * - * @param {...any} progressTypes Countdowns to be updated + * @param {...(string | { type: string; undo?: boolean })} progressTypes Countdowns to be updated */ static async updateCountdowns(...progressTypes) { + progressTypes = progressTypes.map(p => typeof p === 'string' ? { type: p } : p); const { countdownAutomation } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation); if (!countdownAutomation) return; const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); const updatedCountdowns = Object.keys(countdownSetting.countdowns).reduce((acc, key) => { const countdown = countdownSetting.countdowns[key]; - if (progressTypes.indexOf(countdown.progress.type) !== -1 && countdown.progress.current > 0) { - acc.push(key); + const progressData = progressTypes.find(x => x.type === countdown.progress.type); + if (progressData && countdown.progress.current > 0) { + acc[key] = { value: progressData.undo ? 1 : -1 }; } return acc; - }, []); + }, {}); const countdownData = countdownSetting.toObject(); const settings = { ...countdownData, countdowns: Object.keys(countdownData.countdowns).reduce((acc, key) => { const countdown = foundry.utils.deepClone(countdownData.countdowns[key]); - if (updatedCountdowns.includes(key)) { - countdown.progress.current -= 1; + if (updatedCountdowns[key]) { + countdown.progress.current += updatedCountdowns[key].value; } acc[key] = countdown; @@ -269,12 +377,4 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application refreshType: RefreshType.Countdown }); } - - async _onRender(context, options) { - await super._onRender(context, options); - this.element.hidden = !game.user.isGM && this.#getCountdowns().length === 0; - if (options?.force) { - document.getElementById('ui-right-column-1')?.appendChild(this.element); - } - } } diff --git a/module/config/flagsConfig.mjs b/module/config/flagsConfig.mjs index 817ac89d..cb05bd72 100644 --- a/module/config/flagsConfig.mjs +++ b/module/config/flagsConfig.mjs @@ -24,7 +24,8 @@ export const itemAttachmentSource = 'attachmentSource'; export const userFlags = { welcomeMessage: 'welcome-message', - countdownMode: 'countdown-mode' + countdownMode: 'countdown-mode', + countdownTypeModes: 'countdown-type-modes' }; export const combatToggle = 'combat-toggle-origin'; diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 6fc85ec5..188efafb 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -867,27 +867,27 @@ export const abilityCosts = { export const countdownProgressionTypes = { actionRoll: { id: 'actionRoll', - label: 'DAGGERHEART.CONFIG.CountdownType.actionRoll' + label: 'DAGGERHEART.CONFIG.CountdownProgressType.actionRoll' }, characterAttack: { id: 'characterAttack', - label: 'DAGGERHEART.CONFIG.CountdownType.characterAttack' + label: 'DAGGERHEART.CONFIG.CountdownProgressType.characterAttack' }, characterSpotlight: { id: 'characterSpotlight', - label: 'DAGGERHEART.CONFIG.CountdownType.characterSpotlight' + label: 'DAGGERHEART.CONFIG.CountdownProgressType.characterSpotlight' }, custom: { id: 'custom', - label: 'DAGGERHEART.CONFIG.CountdownType.custom' + label: 'DAGGERHEART.CONFIG.CountdownProgressType.custom' }, fear: { id: 'fear', - label: 'DAGGERHEART.CONFIG.CountdownType.fear' + label: 'DAGGERHEART.CONFIG.CountdownProgressType.fear' }, spotlight: { id: 'spotlight', - label: 'DAGGERHEART.CONFIG.CountdownType.spotlight' + label: 'DAGGERHEART.CONFIG.CountdownProgressType.spotlight' } }; export const rollTypes = { @@ -935,17 +935,6 @@ export const simpleOwnershiplevels = { ...basicOwnershiplevels }; -export const countdownBaseTypes = { - narrative: { - id: 'narrative', - label: 'DAGGERHEART.APPLICATIONS.Countdown.types.narrative' - }, - encounter: { - id: 'encounter', - label: 'DAGGERHEART.APPLICATIONS.Countdown.types.encounter' - } -}; - export const countdownLoopingTypes = { noLooping: { id: 'noLooping', @@ -970,6 +959,19 @@ export const countdownAppMode = { iconOnly: 'icon-only' }; +export const countdownTypes = { + encounter: { + id: 'encounter', + label: 'DAGGERHEART.CONFIG.CountdownType.encounter.label', + shortLabel: 'DAGGERHEART.CONFIG.CountdownType.encounter.shortLabel' + }, + narrative: { + id: 'narrative', + label: 'DAGGERHEART.CONFIG.CountdownType.narrative.label', + shortLabel: 'DAGGERHEART.CONFIG.CountdownType.narrative.shortLabel' + } +}; + export const sceneRangeMeasurementSetting = { disable: { id: 'disable', diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index c71f5ef9..f568436e 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -228,7 +228,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel let config = this.prepareConfig(event, configOptions); if (!config) return; - config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(this.actor, this.item); + config.effects = + await game.system.api.data.actions.actionsTypes.base.getActionRelevantEffects(this.actor, this.item); if (Hooks.call(`${CONFIG.DH.id}.preUseAction`, this, config) === false) return; @@ -333,27 +334,43 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel } /** - * Get the all potentially applicable effects on the actor + * Get the all potentially applicable effects on the actor for the action's RollDialog * @param {DHActor} actor The actor performing the action * @param {DHItem|DhActor} effectParent The parent of the effect * @returns {DhActiveEffect[]} */ - static async getEffects(actor, effectParent) { + static async getActionRelevantEffects(actor, effectParent) { if (!actor) return []; - return Array.from(await actor.allApplicableEffects({ noTransferArmor: true, noSelfArmor: true })).filter( - effect => { - /* Effects on weapons only ever apply for the weapon itself */ + // Changes on weapon effects are not typically only applicable to show in the roll dialog for the weapon itself + // The exemptions to this rule are listed below + const weaponTransferredEffectKeys = [ + 'system.bonuses.roll.spellcast.bonus' + ]; + + return Array.from(await actor.allApplicableEffects({ noTransferArmor: true, noSelfArmor: true })).reduce( + (acc, effect) => { + const effectData = effect.toObject(); + /* Effects on weapons only ever apply for the weapon itself, with a few defined exceptions */ if (effect.parent.type === 'weapon') { /* Unless they're secondary - then they apply only to other primary weapons */ if (effect.parent.system.secondary) { - if (effectParent?.type !== 'weapon' || effectParent?.system.secondary) return false; - } else if (effectParent?.id !== effect.parent.id) return false; + if (effectParent?.type !== 'weapon' || effectParent?.system.secondary) { + effectData.system.changes = + effectData.system.changes.filter(x => weaponTransferredEffectKeys.includes(x.key)); + } + } else if (effectParent?.id !== effect.parent.id) { + effectData.system.changes = + effectData.system.changes.filter(x => weaponTransferredEffectKeys.includes(x.key)); + } } - return !effect.isSuppressed; - } - ); + if (!effect.isSuppressed) { + acc.push(effectData); + } + + return acc; + }, []); } /** @@ -459,8 +476,7 @@ export class ResourceUpdateMap extends Map { } else if (!existing?.clear) { this.set(resource.key, { ...existing, - value: existing.value + (resource.value ?? 0), - total: existing.total + (resource.total ?? 0) + value: existing.value + (resource.value ?? 0) }); } } diff --git a/module/data/actor/_types.d.ts b/module/data/actor/_types.d.ts new file mode 100644 index 00000000..fec6e638 --- /dev/null +++ b/module/data/actor/_types.d.ts @@ -0,0 +1,14 @@ +import DhpActor from '../../documents/actor.mjs' +import DhCharacter from './character.mjs'; + +declare module './base.mjs' { + export default interface BaseDataActor { + parent: DhpActor; + } +} + +declare module './companion.mjs' { + export default interface DhCompanion { + partner: DhpActor; + } +} diff --git a/module/data/actor/base.mjs b/module/data/actor/base.mjs index 9efbe7d7..995708eb 100644 --- a/module/data/actor/base.mjs +++ b/module/data/actor/base.mjs @@ -1,9 +1,13 @@ -import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs'; -import DHItem from '../../documents/item.mjs'; import { createShallowProxy, getScrollTextData } from '../../helpers/utils.mjs'; const fields = foundry.data.fields; +/** + * @import DHItem from '../../documents/item.mjs'; + * @import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs'; + */ + +/** Function to generate resistance fields for damage types */ const resistanceField = (resistanceLabel, immunityLabel, reductionLabel) => new fields.SchemaField({ resistance: new fields.BooleanField({ @@ -96,6 +100,8 @@ export const commonActorRules = (extendedData = { damageReduction: {}, attack: { * @property {Boolean} isNPC - This data model represents a NPC? * @property {typeof DHBaseActorSettings} settingSheet - The sheet class used to render the settings UI for this actor type. */ + +/** Base actor type data model for all actors in Daggerheart */ export default class BaseDataActor extends foundry.abstract.TypeDataModel { /** @returns {ActorDataModelMetadata}*/ static get metadata() { diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 0cae662f..3b12da6f 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -315,7 +315,12 @@ export default class DhCharacter extends DhCreature { label: 'DAGGERHEART.ACTORS.Character.defaultDisadvantageDice' }) }) - }) + }), + /** Accumulated armor score from all sources */ + armorScore: new fields.SchemaField({ + value: new fields.NumberField(), + max: new fields.NumberField() + }, { persisted: false }) }; } @@ -411,8 +416,8 @@ export default class DhCharacter extends DhCreature { get domainCards() { const domainCards = this.parent.items.filter(x => x.type === 'domainCard'); - const loadout = domainCards.filter(x => !x.system.inVault); - const vault = domainCards.filter(x => x.system.inVault); + const loadout = domainCards.filter(x => !x.system.inVault).sort((a, b) => a.sort - b.sort); + const vault = domainCards.filter(x => x.system.inVault).sort((a, b) => a.sort - b.sort); return { loadout: loadout, diff --git a/module/data/countdowns.mjs b/module/data/countdowns.mjs index 54971d34..8e55ed31 100644 --- a/module/data/countdowns.mjs +++ b/module/data/countdowns.mjs @@ -13,6 +13,21 @@ export default class DhCountdowns extends foundry.abstract.DataModel { }) }; } + + handleChange() { + const previousCountdowns = foundry.ui.countdowns.previousCountdownData; + const changedCountdowns = Object.entries(this.countdowns).reduce((acc, [key, countdown]) => { + const previousCountdown = previousCountdowns[key]; + if (!previousCountdown || (previousCountdown.progress.current !== countdown.progress.current)) { + acc.push(key); + } + + return acc; + }, []); + + for (const countdownKey of changedCountdowns) + foundry.ui.countdowns.changedCountdownsForAnimation.add(countdownKey); + } } export class DhCountdown extends foundry.abstract.DataModel { @@ -21,7 +36,8 @@ export class DhCountdown extends foundry.abstract.DataModel { return { type: new fields.StringField({ required: true, - choices: CONFIG.DH.GENERAL.countdownBaseTypes, + choices: CONFIG.DH.GENERAL.countdownTypes, + initial: CONFIG.DH.GENERAL.countdownTypes.encounter.id, label: 'DAGGERHEART.GENERAL.type' }), name: new fields.StringField({ @@ -85,7 +101,7 @@ export class DhCountdown extends foundry.abstract.DataModel { : undefined; return { - type: type ?? CONFIG.DH.GENERAL.countdownBaseTypes.narrative.id, + type: type ?? CONFIG.DH.GENERAL.countdownTypes.encounter.id, name: game.i18n.localize('DAGGERHEART.APPLICATIONS.Countdown.newCountdown'), img: 'icons/magic/time/hourglass-yellow-green.webp', ownership: ownership, diff --git a/module/data/fields/action/countdownField.mjs b/module/data/fields/action/countdownField.mjs index 96d9dd91..53e132c9 100644 --- a/module/data/fields/action/countdownField.mjs +++ b/module/data/fields/action/countdownField.mjs @@ -8,8 +8,8 @@ export default class CountdownField extends fields.ArrayField { ...game.system.api.data.countdowns.DhCountdown.defineSchema(), type: new fields.StringField({ required: true, - choices: CONFIG.DH.GENERAL.countdownBaseTypes, - initial: CONFIG.DH.GENERAL.countdownBaseTypes.encounter.id, + choices: CONFIG.DH.GENERAL.countdownTypes, + initial: CONFIG.DH.GENERAL.countdownTypes.encounter.id, label: 'DAGGERHEART.GENERAL.type' }), name: new fields.StringField({ diff --git a/module/data/fields/formulaField.mjs b/module/data/fields/formulaField.mjs index 85922f1f..c465bec3 100644 --- a/module/data/fields/formulaField.mjs +++ b/module/data/fields/formulaField.mjs @@ -39,7 +39,7 @@ export default class FormulaField extends foundry.data.fields.StringField { let roll = null; try { roll = new Roll(value.replace(/@([a-z.0-9_-]+)/gi, '1')); - } catch (_) { + } catch { roll = new Roll(value.replace(/@([a-z.0-9_-]+)/gi, 'd6')); } roll.evaluateSync({ strict: false }); diff --git a/module/data/item/_types.d.ts b/module/data/item/_types.d.ts new file mode 100644 index 00000000..6004e13b --- /dev/null +++ b/module/data/item/_types.d.ts @@ -0,0 +1,8 @@ +import DHItem from '../../documents/item.mjs'; + +declare module './base.mjs' { + export default interface BaseDataItem { + parent: DHItem; + actor: DhpActor; + } +} \ No newline at end of file diff --git a/module/data/registeredTriggers.mjs b/module/data/registeredTriggers.mjs index ab86351c..64e7f93e 100644 --- a/module/data/registeredTriggers.mjs +++ b/module/data/registeredTriggers.mjs @@ -149,12 +149,15 @@ export default class RegisteredTriggers extends Map { const result = await command(...args); if (result?.updates?.length) updates.push(...result.updates); - } catch (_) { + } catch { + const item = await foundry.utils.fromUuid(itemUuid); const triggerName = game.i18n.localize(triggerData.label); - ui.notifications.error( + console.error( game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerError', { trigger: triggerName, - actor: currentActor?.name + actor: currentActor?.name, + item: item?.name ?? '', + itemUuid: item?.uuid ?? '' }) ); } diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index c28db98f..1c6ec829 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -23,6 +23,10 @@ export default class DHRoll extends Roll { static DefaultDialog = D20RollDialog; + /** + * @param {Partial} config + * @returns {Promise} + */ static async build(config = {}, message = {}) { const roll = await this.buildConfigure(config, message); if (!roll) return; @@ -34,6 +38,10 @@ export default class DHRoll extends Roll { return config; } + /** + * @param {Partial} config + * @returns {Promise} + */ static async buildConfigure(config = {}, message = {}) { config.hooks = [...this.getHooks(), '']; config.dialog ??= {}; diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index 1cfed094..e2d967a3 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -1,6 +1,6 @@ import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; import D20Roll from './d20Roll.mjs'; -import { parseRallyDice, setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs'; +import { parseRallyDice, setDiceSoNiceForDualityRoll, shouldUseHopeFearAutomation } from '../helpers/utils.mjs'; import { getDiceSoNicePresets } from '../config/generalConfig.mjs'; import { updateResourcesForDualityReroll } from './helpers.mjs'; @@ -312,11 +312,9 @@ export default class DualityRoll extends D20Roll { } static async addDualityResourceUpdates(config) { - const automationSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation); - const hopeFearAutomation = automationSettings.hopeFear; if ( !config.source?.actor || - (game.user.isGM ? !hopeFearAutomation.gm : !hopeFearAutomation.players) || + !shouldUseHopeFearAutomation() || config.actionType === 'reaction' || config.skips?.resources ) @@ -334,15 +332,15 @@ export default class DualityRoll extends D20Roll { const fear = (config.roll.result.duality === -1 ? 1 : 0) - (config.rerolledRoll.result.duality === -1 ? 1 : 0); - if (hope !== 0) updates.push({ key: 'hope', value: hope, total: -1 * hope, enabled: true }); - if (stress !== 0) updates.push({ key: 'stress', value: -1 * stress, total: stress, enabled: true }); - if (fear !== 0) updates.push({ key: 'fear', value: fear, total: -1 * fear, enabled: true }); + if (hope !== 0) updates.push({ key: 'hope', value: hope, enabled: true }); + if (stress !== 0) updates.push({ key: 'stress', value: -1 * stress, enabled: true }); + if (fear !== 0) updates.push({ key: 'fear', value: fear, enabled: true }); } } else { if (config.roll.isCritical || config.roll.result.duality === 1) - updates.push({ key: 'hope', value: 1, total: -1, enabled: true }); - if (config.roll.isCritical) updates.push({ key: 'stress', value: -1, total: 1, enabled: true }); - if (config.roll.result.duality === -1) updates.push({ key: 'fear', value: 1, total: -1, enabled: true }); + updates.push({ key: 'hope', value: 1, enabled: true }); + if (config.roll.isCritical) updates.push({ key: 'stress', value: -1, enabled: true }); + if (config.roll.result.duality === -1) updates.push({ key: 'fear', value: 1, enabled: true }); } if (updates.length) { diff --git a/module/dice/helpers.mjs b/module/dice/helpers.mjs index 35adb8b7..d03970d0 100644 --- a/module/dice/helpers.mjs +++ b/module/dice/helpers.mjs @@ -1,19 +1,28 @@ import { ResourceUpdateMap } from '../data/action/baseAction.mjs'; export function updateResourcesForDualityReroll(oldDuality, newDuality, actor) { - const { hopeFear } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation); - if (game.user.isGM ? !hopeFear.gm : !hopeFear.players) return; - - const updates = []; const hope = (newDuality >= 0 ? 1 : 0) - (oldDuality >= 0 ? 1 : 0); const stress = (newDuality === 0 ? 1 : 0) - (oldDuality === 0 ? 1 : 0); const fear = (newDuality === -1 ? 1 : 0) - (oldDuality === -1 ? 1 : 0); - if (hope !== 0) updates.push({ key: 'hope', value: hope, total: -1 * hope, enabled: true }); - if (stress !== 0) updates.push({ key: 'stress', value: -1 * stress, total: stress, enabled: true }); - if (fear !== 0) updates.push({ key: 'fear', value: fear, total: -1 * fear, enabled: true }); + const { hopeFear, countdownAutomation } = + game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation); - const resourceUpdates = new ResourceUpdateMap(actor); - resourceUpdates.addResources(updates); - resourceUpdates.updateResources(); + if (game.user.isGM ? hopeFear.gm : hopeFear.players) { + const updates = []; + if (hope !== 0) updates.push({ key: 'hope', value: hope, enabled: true }); + if (stress !== 0) updates.push({ key: 'stress', value: -1 * stress, enabled: true }); + if (fear !== 0) updates.push({ key: 'fear', value: fear, enabled: true }) + + const resourceUpdates = new ResourceUpdateMap(actor); + resourceUpdates.addResources(updates); + resourceUpdates.updateResources(); + } + + if (countdownAutomation && fear !== 0) { + game.system.api.applications.ui.DhCountdowns.updateCountdowns({ + type: CONFIG.DH.GENERAL.countdownProgressionTypes.fear.id, + undo: fear === 1 ? false : true + }); + } } diff --git a/module/documents/_types.d.ts b/module/documents/_types.d.ts new file mode 100644 index 00000000..a94d6395 --- /dev/null +++ b/module/documents/_types.d.ts @@ -0,0 +1,22 @@ +import BaseDataActor from '../data/actor/base.mjs' +import DHItem from './item.mjs'; +import BaseDataItem from '../data/item/base.mjs'; +import DhActiveEffect from './activeEffect.mjs'; +import EmbeddedCollection from '@common/abstract/embedded-collection.mjs'; + +declare module './actor.mjs' { + export default interface DhpActor { + system: T; + items: EmbeddedCollection; + effects: EmbeddedCollection; + } +} + +declare module './item.mjs' { + export default interface DHItem { + parent: DhpActor; + actor: DhpActor; + system: T; + effects: EmbeddedCollection; + } +} \ No newline at end of file diff --git a/module/documents/activeEffect.mjs b/module/documents/activeEffect.mjs index 3518210b..0e7f5d1e 100644 --- a/module/documents/activeEffect.mjs +++ b/module/documents/activeEffect.mjs @@ -212,7 +212,7 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { try { const evl = new Function('sandbox', `with (sandbox) { return ${expression}}`); result = evl(Roll.MATH_PROXY); - } catch (err) { + } catch { return expression; } diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index fb10435f..8ef64f65 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -531,21 +531,7 @@ export default class DhpActor extends Actor { } /** - * @param {object} config - * @param {Event} config.event - * @param {string} config.title - * @param {object} config.roll - * @param {number} config.roll.modifier - * @param {boolean} [config.roll.simple=false] - * @param {string} [config.roll.type] - * @param {number} [config.roll.difficulty] - * @param {boolean} [config.hasDamage] - * @param {boolean} [config.hasEffect] - * @param {object} [config.chatMessage] - * @param {string} config.chatMessage.template - * @param {boolean} [config.chatMessage.mute] - * @param {object} [config.targets] - * @param {object} [config.costs] + * @param {Partial} config */ async diceRoll(config) { config.source = { ...(config.source ?? {}), actor: this.uuid }; @@ -563,7 +549,7 @@ export default class DhpActor extends Actor { headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { ability: abilityLabel }), - effects: await game.system.api.data.actions.actionsTypes.base.getEffects(this), + effects: await game.system.api.data.actions.actionsTypes.base.getActionRelevantEffects(this), roll: { trait: trait, type: 'trait' diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index 480f8c69..b555dfca 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -167,7 +167,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { if (this.system.action) { const actor = await foundry.utils.fromUuid(config.source.actor); const item = actor?.items.get(config.source.item) ?? null; - config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(actor, item); + config.effects = await game.system.api.data.actions.actionsTypes.base.getActionRelevantEffects(actor, item); await this.system.action.workflow.get('damage')?.execute(config, this._id, true); } } diff --git a/module/enrichers/FateRollEnricher.mjs b/module/enrichers/FateRollEnricher.mjs index c82bbcb2..8513ed94 100644 --- a/module/enrichers/FateRollEnricher.mjs +++ b/module/enrichers/FateRollEnricher.mjs @@ -49,7 +49,7 @@ export const renderFateButton = async event => { const fateTypeData = getFateTypeData(button.dataset?.fatetype); if (!fateTypeData) ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing')); - const { value: fateType, label: fateTypeLabel } = fateTypeData; + const { value: fateType } = fateTypeData; await enrichedFateRoll( { diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 90a5c29d..6467edd7 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -318,7 +318,7 @@ export function getDocFromElementSync(element) { const target = element.closest('[data-item-uuid]'); try { return foundry.utils.fromUuidSync(target.dataset.itemUuid) ?? null; - } catch (_) { + } catch { return null; } } @@ -377,7 +377,7 @@ export const itemAbleRollParse = (value, actor, item) => { try { return Roll.replaceFormulaData(slicedValue, rollData); - } catch (_) { + } catch { return ''; } }; @@ -885,3 +885,8 @@ export async function triggerChatRollFx(rolls, options = { whisper: false, blind foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice }); } } + +export function shouldUseHopeFearAutomation(options = { gmAsPlayer: true }) { + const { hopeFear } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation); + return (!game.user.isGM || options.gmAsPlayer) ? hopeFear.players : hopeFear.gm; +} \ No newline at end of file diff --git a/module/systemRegistration/handlebars.mjs b/module/systemRegistration/handlebars.mjs index 1b08e0ad..bf315358 100644 --- a/module/systemRegistration/handlebars.mjs +++ b/module/systemRegistration/handlebars.mjs @@ -50,6 +50,7 @@ export const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/ui/chat/parts/target-part.hbs', 'systems/daggerheart/templates/ui/chat/parts/button-part.hbs', 'systems/daggerheart/templates/ui/itemBrowser/itemContainer.hbs', + 'systems/daggerheart/templates/ui/countdowns/parts/countdowns.hbs', 'systems/daggerheart/templates/scene/dh-config.hbs', 'systems/daggerheart/templates/settings/appearance-settings/diceSoNiceTab.hbs', 'systems/daggerheart/templates/sheets/activeEffect/typeChanges/armorChange.hbs' diff --git a/module/systemRegistration/migrations.mjs b/module/systemRegistration/migrations.mjs index b718a127..ec546c92 100644 --- a/module/systemRegistration/migrations.mjs +++ b/module/systemRegistration/migrations.mjs @@ -178,8 +178,8 @@ export async function runMigrations() { await countdownSettings.updateSource({ countdowns: { - ...getCountdowns(countdownSettings.narrative, CONFIG.DH.GENERAL.countdownBaseTypes.narrative.id), - ...getCountdowns(countdownSettings.encounter, CONFIG.DH.GENERAL.countdownBaseTypes.encounter.id) + ...getCountdowns(countdownSettings.narrative, 'narrative'), + ...getCountdowns(countdownSettings.encounter, 'encounter') } }); await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, countdownSettings); diff --git a/module/systemRegistration/settings.mjs b/module/systemRegistration/settings.mjs index a66323c7..1a985274 100644 --- a/module/systemRegistration/settings.mjs +++ b/module/systemRegistration/settings.mjs @@ -199,7 +199,10 @@ const registerNonConfigSettings = () => { game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, { scope: 'world', config: false, - type: DhCountdowns + type: DhCountdowns, + onChange: value => { + value.handleChange(); + } }); game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings, { diff --git a/styles/less/dialog/tag-team-dialog/initialization.less b/styles/less/dialog/tag-team-dialog/initialization.less index d6f7ad29..e79ed65c 100644 --- a/styles/less/dialog/tag-team-dialog/initialization.less +++ b/styles/less/dialog/tag-team-dialog/initialization.less @@ -88,9 +88,21 @@ grid-template-columns: 1fr 1fr; gap: 8px; - &.inactive { + .inactive { opacity: 0.4; } + + .initiator-cost-fields { + display: flex; + flex-direction: column; + align-items: flex-start; + + .initiator-cost-inputs { + display: grid; + grid-template-columns: auto 1fr; + align-items: center; + } + } } footer { diff --git a/styles/less/sheets/actors/actor-sheet-shared.less b/styles/less/sheets/actors/actor-sheet-shared.less index b3eb0469..89617103 100644 --- a/styles/less/sheets/actors/actor-sheet-shared.less +++ b/styles/less/sheets/actors/actor-sheet-shared.less @@ -37,6 +37,10 @@ color: @color-text-subtle; } + .window-header > .attribution-header-label { + margin-right: var(--spacer-4); + } + .tab.inventory { .gold-section { display: grid; diff --git a/styles/less/sheets/actors/adversary/header.less b/styles/less/sheets/actors/adversary/header.less index 1e5e4fa5..549a219d 100644 --- a/styles/less/sheets/actors/adversary/header.less +++ b/styles/less/sheets/actors/adversary/header.less @@ -3,16 +3,21 @@ .application.sheet.daggerheart.actor.dh-style.adversary { .adversary-header-sheet { - padding: 0 15px; padding-top: var(--header-height); width: 100%; + > *:not(line-div, .tab-navigation) { + padding-left: 15px; + padding-right: 15px; + } + .name-row { display: flex; gap: 5px; align-items: center; justify-content: space-between; - padding: 8px 0; + padding-top: var(--spacer-4); + padding-bottom: var(--spacer-4); flex: 1; h1 { @@ -34,8 +39,8 @@ .tags { display: flex; - gap: 10px; - padding-bottom: 8px; + gap: 8px; + padding-bottom: var(--spacer-12); .tag { display: flex; @@ -44,8 +49,7 @@ justify-content: center; align-items: center; padding: 3px 5px; - font-size: var(--font-size-12); - font: @font-body; + font: var(--font-size-12) @font-body; background: light-dark(@dark-15, @beige-15); border: 1px solid light-dark(@dark, @beige); @@ -64,8 +68,16 @@ .adversary-info { display: flex; flex-direction: column; - gap: 12px; - padding: 16px 0; + gap: var(--spacer-8); + padding-top: var(--spacer-12); + padding-bottom: var(--spacer-12); + } + + .tab-navigation { + margin-top: 0; + button[data-action="openSettings"] { + margin-right: 12px; + } } } } diff --git a/styles/less/ui/countdown/countdown.less b/styles/less/ui/countdown/countdown.less index 63e539ba..96e01ffd 100644 --- a/styles/less/ui/countdown/countdown.less +++ b/styles/less/ui/countdown/countdown.less @@ -22,7 +22,6 @@ pointer-events: all; align-self: flex-end; transition: 0.3s right ease-in-out; - display: flex; flex-direction: column; @@ -65,12 +64,31 @@ padding: 0 0.5rem; overflow: hidden; font-size: var(--font-size-13); + .window-title { font-family: @font-body; - flex: 1; } - .header-control + .header-control { - margin-left: 8px; + + + .header-type-toggles { + flex: 1; + display: flex; + align-items: center; + gap: 4px; + + .header-type { + flex: 1; + border: 1px solid @beige; + border-radius: 12px; + padding: 2px 4px; + text-align: center; + background-color: @dark-blue; + color: @beige; + + &.inactive { + opacity: 0.4; + } + } } } @@ -82,9 +100,23 @@ overflow: auto; max-height: 312px; + .countdown-category-container { + display: flex; + flex-direction: column; + gap: 8px; + + &.hidden { + display: none; + } + } + .countdown-container { display: flex; width: 100%; + border-radius: 6px; + background: linear-gradient(-45deg, transparent 30%, light-dark(@dark-blue-40, @golden-40) 35%, transparent 40%); + background-size: 300%; + background-position-x: 100%; &.icon-only { gap: 8px; @@ -105,6 +137,7 @@ display: flex; align-items: center; gap: 16px; + border-radius: 6px; img { width: 2.75rem; diff --git a/styles/less/ui/game-pause/game-pause.less b/styles/less/ui/game-pause/game-pause.less index 60d08bec..daf9a1e4 100644 --- a/styles/less/ui/game-pause/game-pause.less +++ b/styles/less/ui/game-pause/game-pause.less @@ -1,23 +1,34 @@ @import '../../utils/mixin.less'; #pause.dh-style { + animation: none; + + img { + width: 125px; + height: 125px; + } + figcaption { + --base-shadow: drop-shadow(2px 2px 2px black); position: absolute; margin-top: 24px; animation: pause-pulse 3s ease-in-out infinite; + font-size: var(--font-size-30); + letter-spacing: 0.18em; + filter: var(--base-shadow); } @keyframes pause-pulse { 0% { - filter: drop-shadow(0 0 5px @secondary-blue); + filter: var(--base-shadow) drop-shadow(0 0 5px @secondary-blue); } 50% { - filter: drop-shadow(0 0 5px @golden); + filter: var(--base-shadow) drop-shadow(0 0 5px @golden-secondary); } 100% { - filter: drop-shadow(0 0 5px @secondary-blue); + filter: var(--base-shadow) drop-shadow(0 0 5px @secondary-blue); } } } \ No newline at end of file diff --git a/styles/less/utils/fonts.less b/styles/less/utils/fonts.less index 07da3389..72ee1a85 100755 --- a/styles/less/utils/fonts.less +++ b/styles/less/utils/fonts.less @@ -10,6 +10,7 @@ --font-size-8: 0.5rem; --font-size-9: 0.5625rem; --font-size-22: 1.375rem; + --font-size-30: 1.875rem; } @font-title: ~"var(--dh-font-title, 'Cinzel Decorative'), serif"; diff --git a/system.json b/system.json index f5e13a62..08693074 100644 --- a/system.json +++ b/system.json @@ -2,7 +2,7 @@ "id": "daggerheart", "title": "Daggerheart", "description": "An unofficial implementation of the Daggerheart system", - "version": "2.3.4", + "version": "2.4.0", "compatibility": { "minimum": "14.364", "verified": "14.364", @@ -10,7 +10,7 @@ }, "url": "https://github.com/Foundryborne/daggerheart", "manifest": "https://raw.githubusercontent.com/Foundryborne/daggerheart/v14/system.json", - "download": "https://github.com/Foundryborne/daggerheart/releases/download/2.3.4/system.zip", + "download": "https://github.com/Foundryborne/daggerheart/releases/download/2.4.0/system.zip", "authors": [ { "name": "WBHarry" diff --git a/templates/dialogs/tagTeamDialog/initialization.hbs b/templates/dialogs/tagTeamDialog/initialization.hbs index 0b92e68e..40491e1b 100644 --- a/templates/dialogs/tagTeamDialog/initialization.hbs +++ b/templates/dialogs/tagTeamDialog/initialization.hbs @@ -26,11 +26,16 @@ -
+
-
- -
+
+ + +
diff --git a/templates/sheets-settings/adversary-settings/features.hbs b/templates/sheets-settings/adversary-settings/features.hbs index 2f2f5f47..1cab8cfe 100644 --- a/templates/sheets-settings/adversary-settings/features.hbs +++ b/templates/sheets-settings/adversary-settings/features.hbs @@ -3,27 +3,31 @@ data-tab='{{tabs.features.id}}' data-group='{{tabs.features.group}}' > - -
- {{localize tabs.features.label}} -
    - {{#each @root.features as |feature|}} -
  • - -
    - {{feature.name}} -
    -
    - - -
    -
  • - {{/each}} -
-
- {{localize "DAGGERHEART.GENERAL.dropFeaturesHere"}} -
-
+ {{#each featureGroups as |group|}} +
+ + {{group.label}} + + + + +
    + {{#each group.features as |feature|}} + {{> 'daggerheart.inventory-item' + item=feature + type='feature' + actorType=@root.document.type + hideTags=true + hideContextMenu=true + hideResources=true + showActions=false + hideTooltip=true + }} + {{/each}} +
+
+ {{/each}} +
+ {{localize "DAGGERHEART.GENERAL.dropFeaturesHere"}} +
\ No newline at end of file diff --git a/templates/sheets-settings/environment-settings/features.hbs b/templates/sheets-settings/environment-settings/features.hbs index 579fe74e..1cab8cfe 100644 --- a/templates/sheets-settings/environment-settings/features.hbs +++ b/templates/sheets-settings/environment-settings/features.hbs @@ -3,24 +3,31 @@ data-tab='{{tabs.features.id}}' data-group='{{tabs.features.group}}' > - -
- {{localize tabs.features.label}} -
    - {{#each @root.features as |feature|}} -
  • - -
    - {{feature.name}} -
    -
    - - -
    -
  • - {{/each}} -
-
+ {{#each featureGroups as |group|}} +
+ + {{group.label}} + + + + +
    + {{#each group.features as |feature|}} + {{> 'daggerheart.inventory-item' + item=feature + type='feature' + actorType=@root.document.type + hideTags=true + hideContextMenu=true + hideResources=true + showActions=false + hideTooltip=true + }} + {{/each}} +
+
+ {{/each}} +
+ {{localize "DAGGERHEART.GENERAL.dropFeaturesHere"}} +
\ No newline at end of file diff --git a/templates/sheets-settings/npc-settings/features.hbs b/templates/sheets-settings/npc-settings/features.hbs index 2f2f5f47..1cab8cfe 100644 --- a/templates/sheets-settings/npc-settings/features.hbs +++ b/templates/sheets-settings/npc-settings/features.hbs @@ -3,27 +3,31 @@ data-tab='{{tabs.features.id}}' data-group='{{tabs.features.group}}' > - -
- {{localize tabs.features.label}} -
    - {{#each @root.features as |feature|}} -
  • - -
    - {{feature.name}} -
    -
    - - -
    -
  • - {{/each}} -
-
- {{localize "DAGGERHEART.GENERAL.dropFeaturesHere"}} -
-
+ {{#each featureGroups as |group|}} +
+ + {{group.label}} + + + + +
    + {{#each group.features as |feature|}} + {{> 'daggerheart.inventory-item' + item=feature + type='feature' + actorType=@root.document.type + hideTags=true + hideContextMenu=true + hideResources=true + showActions=false + hideTooltip=true + }} + {{/each}} +
+
+ {{/each}} +
+ {{localize "DAGGERHEART.GENERAL.dropFeaturesHere"}} +
\ 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 54e44e64..ca584e57 100644 --- a/templates/sheets/global/partials/domain-card-item.hbs +++ b/templates/sheets/global/partials/domain-card-item.hbs @@ -1,4 +1,4 @@ -
  • +
  • {{item.system.recallCost}} diff --git a/templates/sheets/global/partials/inventory-item-V2.hbs b/templates/sheets/global/partials/inventory-item-V2.hbs index 523e9304..f7d22a30 100644 --- a/templates/sheets/global/partials/inventory-item-V2.hbs +++ b/templates/sheets/global/partials/inventory-item-V2.hbs @@ -27,17 +27,17 @@ Parameters: >
    {{!-- Image --}} -
    - - {{#if item.usable}} - {{#if @root.isNPC}} - d20 - {{else}} - 2d12 +
    + + {{#if (and item.usable (ne showActions false))}} + {{#if @root.isNPC}} + d20 + {{else}} + 2d12 + {{/if}} {{/if}} - {{/if}}
    {{!-- Name & Tags --}} @@ -46,10 +46,10 @@ Parameters: {{localize item.name}} {{#unless (or noExtensible (not item.system.description))}}{{/unless}} {{!-- Tags Start --}} - {{#if (not ../hideTags)}} + {{#if (not hideTags)}} {{#> "systems/daggerheart/templates/sheets/global/partials/item-tags.hbs" item}} {{#if (eq ../type 'feature')}} - {{#if (and system.featureForm (or (eq @root.document.type "adversary") (eq @root.document.type "environment")))}} + {{#if (and system.featureForm (ne @root.document.type "character"))}}
    {{localize (concat "DAGGERHEART.CONFIG.FeatureForm." system.featureForm)}}
    diff --git a/templates/ui/countdown-edit.hbs b/templates/ui/countdowns/countdown-edit.hbs similarity index 97% rename from templates/ui/countdown-edit.hbs rename to templates/ui/countdowns/countdown-edit.hbs index 6b7d22d4..eebefe11 100644 --- a/templates/ui/countdown-edit.hbs +++ b/templates/ui/countdowns/countdown-edit.hbs @@ -78,9 +78,9 @@
    - +
    diff --git a/templates/ui/countdowns/countdowns-view.hbs b/templates/ui/countdowns/countdowns-view.hbs new file mode 100644 index 00000000..7727c4b6 --- /dev/null +++ b/templates/ui/countdowns/countdowns-view.hbs @@ -0,0 +1,24 @@ +
    +
    + + {{#unless iconOnly}} + {{localize "DAGGERHEART.UI.Countdowns.title"}} + {{/unless}} +
    + {{#each typeToggles as |type|}} + {{localize type.label}} + {{/each}} +
    + {{#if isGM}} + + + + {{/if}} + + + +
    +
    + {{> "systems/daggerheart/templates/ui/countdowns/parts/countdowns.hbs" }} +
    +
    \ No newline at end of file diff --git a/templates/ui/countdowns.hbs b/templates/ui/countdowns/parts/countdowns.hbs similarity index 70% rename from templates/ui/countdowns.hbs rename to templates/ui/countdowns/parts/countdowns.hbs index 4a77dfd7..4f0ee086 100644 --- a/templates/ui/countdowns.hbs +++ b/templates/ui/countdowns/parts/countdowns.hbs @@ -1,23 +1,11 @@ -
    -
    - - {{localize "DAGGERHEART.UI.Countdowns.title"}} - {{#if isGM}} - - - - {{/if}} - - - -
    -
    - {{#each countdowns as | countdown id |}} -
    +{{#each countdowns as | category key |}} +
    + {{#each category as |countdown id|}} +
    - +
    - {{#unless ../iconOnly}} + {{#unless @root.iconOnly}}
    {{countdown.name}}
    {{/unless}}
    @@ -29,7 +17,7 @@ {{#if countdown.editable}}{{/if}}
    - {{#if (not ../iconOnly)}} + {{#if (not @root.iconOnly)}} {{#if (and countdown.noPlayerAccess @root.isGM)}} {{/if}} @@ -53,4 +41,4 @@
    {{/each}}
    -
    \ No newline at end of file +{{/each}} \ No newline at end of file