diff --git a/lang/en.json b/lang/en.json index bb67b6c2..c0dc1c3c 100755 --- a/lang/en.json +++ b/lang/en.json @@ -387,6 +387,10 @@ "OwnershipSelection": { "title": "Ownership Selection - {name}", "default": "Default Ownership" + }, + "ResourceDice": { + "title": "{name} Resource", + "rerollDice": "Reroll Dice" } }, @@ -632,6 +636,10 @@ "abbreviation": "AS" } }, + "ItemResourceType": { + "simple": "Simple", + "diceValue": "Dice Value" + }, "Range": { "self": { "name": "Self", @@ -1068,6 +1076,10 @@ "shortrest": "Short Rest", "longrest": "Long Rest" }, + "Resource": { + "single": "Resource", + "plural": "Resources" + }, "Tabs": { "details": "Details", "attack": "Attack", @@ -1154,6 +1166,17 @@ "value": "Value" }, "ITEMS": { + "FIELDS": { + "resource": { + "amount": { "label": "Amount" }, + "dieFaces": { "label": "Die Faces" }, + "icon": { "label": "Icon" }, + "max": { "label": "Max" }, + "recovery": { "label": "Recovery" }, + "type": { "label": "Type" }, + "value": { "label": "Value" } + } + }, "Armor": { "baseScore": "Base Score", "baseThresholds": { @@ -1331,8 +1354,8 @@ }, "UI": { "Chat": { - "dualityRoll": { - "abilityCheckTitle": "{ability} Check" + "applyEffect": { + "title": "Apply Effects - {name}" }, "attackRoll": { "title": "Attack - {attack}", @@ -1348,25 +1371,28 @@ "hitTarget": "Hit Targets", "selectedTarget": "Selected" }, - "applyEffect": { - "title": "Apply Effects - {name}" - }, - "healingRoll": { - "title": "Heal - {healing}", - "heal": "Heal" - }, "deathMove": { "title": "Death Move" }, "domainCard": { "title": "Domain Card" }, + "dualityRoll": { + "abilityCheckTitle": "{ability} Check" + }, + "featureTitle": "Class Feature", "foundationCard": { "ancestryTitle": "Ancestry Card", "communityTitle": "Community Card", "subclassFeatureTitle": "Subclass Feature" }, - "featureTitle": "Class Feature" + "healingRoll": { + "title": "Heal - {healing}", + "heal": "Heal" + }, + "resourceRoll": { + "playerMessage": "{user} rerolled their {name}" + } }, "Notifications": { "adversaryMissing": "The linked adversary doesn't exist in the world.", diff --git a/module/applications/dialogs/_module.mjs b/module/applications/dialogs/_module.mjs index 82c6f17d..0722c747 100644 --- a/module/applications/dialogs/_module.mjs +++ b/module/applications/dialogs/_module.mjs @@ -7,3 +7,4 @@ export { default as DamageSelectionDialog } from './damageSelectionDialog.mjs'; export { default as DeathMove } from './deathMove.mjs'; export { default as Downtime } from './downtime.mjs'; export { default as OwnershipSelection } from './ownershipSelection.mjs'; +export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs'; diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index d1196459..6cb0761c 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -66,7 +66,12 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio context.canRoll = true; if (this.config.costs?.length) { const updatedCosts = this.action.calcCosts(this.config.costs); - context.costs = updatedCosts; + context.costs = updatedCosts.map(x => ({ + ...x, + label: x.keyIsID + ? this.action.parent.parent.name + : game.i18n.localize(CONFIG.DH.GENERAL.abilityCosts[x.key].label) + })); context.canRoll = this.action.hasCost(updatedCosts); this.config.data.scale = this.config.costs[0].total; } @@ -74,7 +79,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio context.uses = this.action.calcUses(this.config.uses); context.canRoll = context.canRoll && this.action.hasUses(context.uses); } - if(this.roll) { + if (this.roll) { context.roll = this.roll; context.rollType = this.roll?.constructor.name; context.experiences = Object.keys(this.config.data.experiences).map(id => ({ diff --git a/module/applications/dialogs/resourceDiceDialog.mjs b/module/applications/dialogs/resourceDiceDialog.mjs new file mode 100644 index 00000000..b79ff895 --- /dev/null +++ b/module/applications/dialogs/resourceDiceDialog.mjs @@ -0,0 +1,99 @@ +import { itemAbleRollParse } from '../../helpers/utils.mjs'; + +const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; + +export default class ResourceDiceDialog extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(item, actor, options = {}) { + super(options); + + this.item = item; + this.actor = actor; + this.diceStates = foundry.utils.deepClone(item.system.resource.diceStates); + } + + static DEFAULT_OPTIONS = { + tag: 'form', + classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'resource-dice'], + window: { + icon: 'fa-solid fa-dice' + }, + actions: { + rerollDice: this.rerollDice, + save: this.save + }, + form: { + handler: this.updateResourceDice, + submitOnChange: true, + submitOnClose: false + } + }; + + /** @override */ + static PARTS = { + resourceDice: { + id: 'resourceDice', + template: 'systems/daggerheart/templates/dialogs/dice-roll/resourceDice.hbs' + } + }; + + get title() { + return game.i18n.format('DAGGERHEART.APPLICATIONS.ResourceDice.title', { name: this.item.name }); + } + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.item = this.item; + context.actor = this.actor; + context.diceStates = this.diceStates; + + return context; + } + + static async updateResourceDice(event, _, formData) { + const { diceStates } = foundry.utils.expandObject(formData.object); + this.diceStates = Object.keys(diceStates).reduce((acc, key) => { + const resourceState = this.item.system.resource.diceStates[key]; + acc[key] = { ...diceStates[key], used: Boolean(resourceState?.used) }; + return acc; + }, {}); + + this.render(); + } + + static async save() { + this.rollValues = Object.values(this.diceStates); + this.close(); + } + + static async rerollDice() { + const max = itemAbleRollParse(this.item.system.resource.max, this.actor, this.item); + const diceFormula = `${max}d${this.item.system.resource.dieFaces}`; + const roll = await new Roll(diceFormula).evaluate(); + if (game.modules.get('dice-so-nice')?.active) await game.dice3d.showForRoll(roll, game.user, true); + this.rollValues = roll.terms[0].results.map(x => ({ value: x.result, used: false })); + this.resetUsed = true; + + const cls = getDocumentClass('ChatMessage'); + const msg = new cls({ + user: game.user.id, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/ui/chat/resource-roll.hbs', + { + user: this.actor.name, + name: this.item.name + } + ) + }); + + cls.create(msg.toObject()); + this.close(); + } + + static async create(item, actor, options = {}) { + return new Promise(resolve => { + const app = new this(item, actor, options); + app.addEventListener('close', () => resolve(app.rollValues), { once: true }); + app.render({ force: true }); + }); + } +} diff --git a/module/applications/sheets-configs/action-config.mjs b/module/applications/sheets-configs/action-config.mjs index 8823fa07..e1a62e0d 100644 --- a/module/applications/sheets-configs/action-config.mjs +++ b/module/applications/sheets-configs/action-config.mjs @@ -107,6 +107,7 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { context.hasBaseDamage = !!this.action.parent.attack; context.getRealIndex = this.getRealIndex.bind(this); context.getEffectDetails = this.getEffectDetails.bind(this); + context.costOptions = this.getCostOptions(); context.disableOption = this.disableOption.bind(this); context.isNPC = this.action.actor && this.action.actor.type !== 'character'; context.hasRoll = this.action.hasRoll; @@ -125,8 +126,21 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { this.render(true); } - disableOption(index, options, choices) { - const filtered = foundry.utils.deepClone(options); + getCostOptions() { + const options = foundry.utils.deepClone(CONFIG.DH.GENERAL.abilityCosts); + const resource = this.action.parent.resource; + if (resource) { + options[this.action.parent.parent.id] = { + label: this.action.parent.parent.name, + group: 'TYPES.Actor.character' + }; + } + + return options; + } + + disableOption(index, costOptions, choices) { + const filtered = foundry.utils.deepClone(costOptions); Object.keys(filtered).forEach(o => { if (choices.find((c, idx) => c.type === o && index !== idx)) filtered[o].disabled = true; }); @@ -142,11 +156,19 @@ export default class DHActionConfig extends DaggerheartSheet(ApplicationV2) { return this.action.item.effects.get(id); } - _prepareSubmitData(event, formData) { + _prepareSubmitData(_event, formData) { const submitData = foundry.utils.expandObject(formData.object); for (const keyPath of this.constructor.CLEAN_ARRAYS) { const data = foundry.utils.getProperty(submitData, keyPath); - if (data) foundry.utils.setProperty(submitData, keyPath, Object.values(data)); + const dataValues = data ? Object.values(data) : []; + if (keyPath === 'cost') { + for (var value of dataValues) { + const item = this.action.parent.parent.id === value.key; + value.keyIsID = Boolean(item); + } + } + + if (data) foundry.utils.setProperty(submitData, keyPath, dataValues); } return submitData; } diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 8a18dd22..eae7a223 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -4,6 +4,7 @@ import { abilities } from '../../../config/actorConfig.mjs'; import DhCharacterlevelUp from '../../levelup/characterLevelup.mjs'; import DhCharacterCreation from '../../characterCreation/characterCreation.mjs'; import FilterMenu from '../../ux/filter-menu.mjs'; +import { itemAbleRollParse } from '../../../helpers/utils.mjs'; /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ @@ -25,6 +26,8 @@ export default class CharacterSheet extends DHBaseActorSheet { toggleEquipItem: CharacterSheet.#toggleEquipItem, useItem: this.useItem, //TODO Fix this useAction: this.useAction, + toggleResourceDice: this.toggleResourceDice, + handleResourceDice: this.handleResourceDice, toChat: this.toChat }, window: { @@ -91,6 +94,17 @@ export default class CharacterSheet extends DHBaseActorSheet { } }; + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + + htmlElement.querySelectorAll('.inventory-item-resource').forEach(element => { + element.addEventListener('change', this.updateItemResource.bind(this)); + }); + htmlElement.querySelectorAll('.inventory-item-quantity').forEach(element => { + element.addEventListener('change', this.updateItemQuantity.bind(this)); + }); + } + /** @inheritDoc */ async _onRender(context, options) { await super._onRender(context, options); @@ -480,6 +494,27 @@ export default class CharacterSheet extends DHBaseActorSheet { } } + /* -------------------------------------------- */ + /* Application Listener Actions */ + /* -------------------------------------------- */ + async updateItemResource(event) { + const item = this.getItem(event.currentTarget); + if (!item) return; + + const max = item.system.resource.max ? itemAbleRollParse(item.system.resource.max, this.document, item) : null; + const value = max ? Math.min(Number(event.currentTarget.value), max) : event.currentTarget.value; + await item.update({ 'system.resource.value': value }); + this.render(); + } + + async updateItemQuantity(event) { + const item = this.getItem(event.currentTarget); + if (!item) return; + + await item.update({ 'system.quantity': event.currentTarget.value }); + this.render(); + } + /* -------------------------------------------- */ /* Application Clicks Actions */ /* -------------------------------------------- */ @@ -640,6 +675,41 @@ export default class CharacterSheet extends DHBaseActorSheet { action.use(event); } + /** + * Toggle the used state of a resource dice. + * @type {ApplicationClickAction} + */ + static async toggleResourceDice(event) { + const target = event.target.closest('.item-resource'); + const item = this.getItem(event); + if (!item) return; + + const diceState = item.system.resource.diceStates[target.dataset.dice]; + await item.update({ + [`system.resource.diceStates.${target.dataset.dice}.used`]: diceState?.used ? !diceState.used : true + }); + } + + /** + * Handle the roll values of resource dice. + * @type {ApplicationClickAction} + */ + static async handleResourceDice(event) { + const item = this.getItem(event); + if (!item) return; + + const rollValues = await game.system.api.applications.dialogs.ResourceDiceDialog.create(item, this.document); + if (!rollValues) return; + + await item.update({ + 'system.resource.diceStates': rollValues.reduce((acc, state, index) => { + acc[index] = { value: state.value, used: state.used }; + return acc; + }, {}) + }); + this.render(); + } + /** * Send item to Chat * @type {ApplicationClickAction} @@ -672,14 +742,14 @@ export default class CharacterSheet extends DHBaseActorSheet { async _onDragStart(event) { const item = this.getItem(event); - + const dragData = { type: item.documentName, uuid: item.uuid }; - + event.dataTransfer.setData('text/plain', JSON.stringify(dragData)); - + super._onDragStart(event); } @@ -687,7 +757,7 @@ export default class CharacterSheet extends DHBaseActorSheet { // Prevent event bubbling to avoid duplicate handling event.preventDefault(); event.stopPropagation(); - + super._onDrop(event); this._onDropItem(event, TextEditor.getDragEventData(event)); } diff --git a/module/applications/sheets/api/base-item.mjs b/module/applications/sheets/api/base-item.mjs index d624acc6..2847bfc8 100644 --- a/module/applications/sheets/api/base-item.mjs +++ b/module/applications/sheets/api/base-item.mjs @@ -24,7 +24,9 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { removeAction: DHBaseItemSheet.#removeAction, addFeature: DHBaseItemSheet.#addFeature, editFeature: DHBaseItemSheet.#editFeature, - removeFeature: DHBaseItemSheet.#removeFeature + removeFeature: DHBaseItemSheet.#removeFeature, + addResource: DHBaseItemSheet.#addResource, + removeResource: DHBaseItemSheet.#removeResource }, dragDrop: [ { dragSelector: null, dropSelector: '.tab.features .drop-section' }, @@ -215,6 +217,26 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { }); } + /** + * Add a resource to the item. + * @type {ApplicationClickAction} + */ + static async #addResource() { + await this.document.update({ + 'system.resource': { type: 'simple', value: 0 } + }); + } + + /** + * Remove the resource from the item. + * @type {ApplicationClickAction} + */ + static async #removeResource() { + await this.document.update({ + 'system.resource': null + }); + } + /* -------------------------------------------- */ /* Application Drag/Drop */ /* -------------------------------------------- */ diff --git a/module/applications/sheets/api/item-attachment-sheet.mjs b/module/applications/sheets/api/item-attachment-sheet.mjs index e89c7cba..e9c3994c 100644 --- a/module/applications/sheets/api/item-attachment-sheet.mjs +++ b/module/applications/sheets/api/item-attachment-sheet.mjs @@ -1,4 +1,3 @@ - export default function ItemAttachmentSheet(Base) { return class extends Base { static DEFAULT_OPTIONS = { @@ -25,10 +24,7 @@ export default function ItemAttachmentSheet(Base) { ...super.TABS, primary: { ...super.TABS?.primary, - tabs: [ - ...(super.TABS?.primary?.tabs || []), - { id: 'attachments' } - ], + tabs: [...(super.TABS?.primary?.tabs || []), { id: 'attachments' }], initial: super.TABS?.primary?.initial || 'description', labelPrefix: super.TABS?.primary?.labelPrefix || 'DAGGERHEART.GENERAL.Tabs' } @@ -46,29 +42,28 @@ export default function ItemAttachmentSheet(Base) { async _onDrop(event) { const data = TextEditor.getDragEventData(event); - + const attachmentsSection = event.target.closest('.attachments-section'); if (!attachmentsSection) return super._onDrop(event); - + event.preventDefault(); event.stopPropagation(); - + const item = await Item.implementation.fromDropData(data); if (!item) return; - + // Call the data model's public method await this.document.system.addAttachment(item); } - static async #removeAttachment(event, target) { // Call the data model's public method await this.document.system.removeAttachment(target.dataset.uuid); -} + } async _preparePartContext(partId, context) { await super._preparePartContext(partId, context); - + if (partId === 'attachments') { // Keep this simple UI preparation in the mixin const attachedUUIDs = this.document.system.attached; @@ -83,8 +78,8 @@ export default function ItemAttachmentSheet(Base) { }) ); } - + return context; } }; -} \ No newline at end of file +} diff --git a/module/applications/sheets/items/armor.mjs b/module/applications/sheets/items/armor.mjs index a6413cf0..f3a38068 100644 --- a/module/applications/sheets/items/armor.mjs +++ b/module/applications/sheets/items/armor.mjs @@ -27,7 +27,7 @@ export default class ArmorSheet extends ItemAttachmentSheet(DHBaseItemSheet) { template: 'systems/daggerheart/templates/sheets/items/armor/settings.hbs', scrollable: ['.settings'] }, - ...super.PARTS, + ...super.PARTS }; /**@inheritdoc */ diff --git a/module/applications/sheets/items/class.mjs b/module/applications/sheets/items/class.mjs index c7b84340..55295455 100644 --- a/module/applications/sheets/items/class.mjs +++ b/module/applications/sheets/items/class.mjs @@ -85,6 +85,16 @@ export default class ClassSheet extends DHBaseItemSheet { await this.document.update({ 'system.subclasses': [...this.document.system.subclasses.map(x => x.uuid), item.uuid] }); + } else if (item.type === 'feature') { + if (target.classList.contains('hope-feature')) { + await this.document.update({ + 'system.hopeFeatures': [...this.document.system.hopeFeatures.map(x => x.uuid), item.uuid] + }); + } else if (target.classList.contains('class-feature')) { + await this.document.update({ + 'system.classFeatures': [...this.document.system.classFeatures.map(x => x.uuid), item.uuid] + }); + } } else if (item.type === 'weapon') { if (target.classList.contains('primary-weapon-section')) { if (!this.document.system.characterGuide.suggestedPrimaryWeapon && !item.system.secondary) @@ -144,7 +154,7 @@ export default class ClassSheet extends DHBaseItemSheet { static async #removeItemFromCollection(_event, element) { const { uuid, target } = element.dataset; const prop = foundry.utils.getProperty(this.document.system, target); - await this.document.update({ [target]: prop.filter(i => i.uuid !== uuid) }); + await this.document.update({ [`system.${target}`]: prop.filter(i => i.uuid !== uuid) }); } /** diff --git a/module/applications/sheets/items/feature.mjs b/module/applications/sheets/items/feature.mjs index 3d13d7de..ca375fc7 100644 --- a/module/applications/sheets/items/feature.mjs +++ b/module/applications/sheets/items/feature.mjs @@ -21,6 +21,7 @@ export default class FeatureSheet extends DHBaseItemSheet { header: { template: 'systems/daggerheart/templates/sheets/items/feature/header.hbs' }, tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' }, + settings: { template: 'systems/daggerheart/templates/sheets/items/feature/settings.hbs' }, actions: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-actions.hbs', scrollable: ['.actions'] @@ -34,7 +35,7 @@ export default class FeatureSheet extends DHBaseItemSheet { /**@override */ static TABS = { primary: { - tabs: [{ id: 'description' }, { id: 'actions' }, { id: 'effects' }], + tabs: [{ id: 'description' }, { id: 'settings' }, { id: 'actions' }, { id: 'effects' }], initial: 'description', labelPrefix: 'DAGGERHEART.GENERAL.Tabs' } @@ -67,7 +68,6 @@ export default class FeatureSheet extends DHBaseItemSheet { } ), title = game.i18n.localize('DAGGERHEART.CONFIG.SelectAction.selectType'); - console.log(this.document); return foundry.applications.api.DialogV2.prompt({ window: { title }, diff --git a/module/applications/sheets/items/weapon.mjs b/module/applications/sheets/items/weapon.mjs index e89c3dce..c601f926 100644 --- a/module/applications/sheets/items/weapon.mjs +++ b/module/applications/sheets/items/weapon.mjs @@ -27,7 +27,7 @@ export default class WeaponSheet extends ItemAttachmentSheet(DHBaseItemSheet) { template: 'systems/daggerheart/templates/sheets/items/weapon/settings.hbs', scrollable: ['.settings'] }, - ...super.PARTS, + ...super.PARTS }; /**@inheritdoc */ diff --git a/module/config/flagsConfig.mjs b/module/config/flagsConfig.mjs index 0c112231..64a36fdf 100644 --- a/module/config/flagsConfig.mjs +++ b/module/config/flagsConfig.mjs @@ -8,4 +8,4 @@ export const encounterCountdown = { position: 'countdown-encounter-position' }; -export const itemAttachmentSource = 'attachmentSource'; \ No newline at end of file +export const itemAttachmentSource = 'attachmentSource'; diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 94efec1b..4216f631 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -366,31 +366,6 @@ export const abilityCosts = { label: 'Armor Stack', group: 'TYPES.Actor.character' }, - prayer: { - id: 'prayer', - label: 'Prayer Dice', - group: 'TYPES.Actor.character' - }, - favor: { - id: 'favor', - label: 'Favor Points', - group: 'TYPES.Actor.character' - }, - slayer: { - id: 'slayer', - label: 'Slayer Dice', - group: 'TYPES.Actor.character' - }, - tide: { - id: 'tide', - label: 'Tide', - group: 'TYPES.Actor.character' - }, - chaos: { - id: 'chaos', - label: 'Chaos', - group: 'TYPES.Actor.character' - }, fear: { id: 'fear', label: 'Fear', diff --git a/module/config/itemConfig.mjs b/module/config/itemConfig.mjs index 1d6b96b8..c63a0008 100644 --- a/module/config/itemConfig.mjs +++ b/module/config/itemConfig.mjs @@ -1334,3 +1334,14 @@ export const actionTypes = { label: 'DAGGERHEART.CONFIG.ActionType.reaction' } }; + +export const itemResourceTypes = { + simple: { + id: 'simple', + label: 'DAGGERHEART.CONFIG.ItemResourceType.simple' + }, + diceValue: { + id: 'diceValue', + label: 'DAGGERHEART.CONFIG.ItemResourceType.diceValue' + } +}; diff --git a/module/data/action/actionDice.mjs b/module/data/action/actionDice.mjs index 5cc2d10a..d71b390a 100644 --- a/module/data/action/actionDice.mjs +++ b/module/data/action/actionDice.mjs @@ -107,7 +107,7 @@ export class DHDamageData extends foundry.abstract.DataModel { }), { label: 'Type', - initial: 'physical', + initial: 'physical' } ), resultBased: new fields.BooleanField({ diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index ae70bf21..6fdadfe2 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -1,4 +1,4 @@ -import { DHActionDiceData, DHActionRollData, DHDamageData, DHDamageField } from './actionDice.mjs'; +import { DHActionDiceData, DHActionRollData, DHDamageField } from './actionDice.mjs'; import DhpActor from '../../documents/actor.mjs'; import D20RollDialog from '../../applications/dialogs/d20RollDialog.mjs'; @@ -35,12 +35,12 @@ export default class DHBaseAction extends foundry.abstract.DataModel { }), cost: new fields.ArrayField( new fields.SchemaField({ - type: new fields.StringField({ - choices: CONFIG.DH.GENERAL.abilityCosts, + key: new fields.StringField({ nullable: false, required: true, initial: 'hope' }), + keyIsID: new fields.BooleanField(), value: new fields.NumberField({ nullable: true, initial: 1 }), scalable: new fields.BooleanField({ initial: false }), step: new fields.NumberField({ nullable: true, initial: null }) @@ -181,7 +181,7 @@ export default class DHBaseAction extends foundry.abstract.DataModel { // Add Roll results to RollDatas actorData.result = data.roll?.total ?? 1; - + actorData.scale = data.costs?.length // Right now only return the first scalable cost. ? (data.costs.find(c => c.scalable)?.total ?? 1) : 1; @@ -204,7 +204,7 @@ export default class DHBaseAction extends foundry.abstract.DataModel { // Prepare Costs const costsConfig = this.prepareCost(); - if (isFastForward && !this.hasCost(costsConfig)) + if (isFastForward && !(await this.hasCost(costsConfig))) return ui.notifications.warn("You don't have the resources to use that action."); // Prepare Uses @@ -278,7 +278,7 @@ export default class DHBaseAction extends foundry.abstract.DataModel { prepareCost() { const costs = this.cost?.length ? foundry.utils.deepClone(this.cost) : []; - return costs; + return this.calcCosts(costs); } prepareUse() { @@ -327,11 +327,26 @@ export default class DHBaseAction extends foundry.abstract.DataModel { } async consume(config) { + const usefulResources = foundry.utils.deepClone(this.actor.system.resources); + for (var cost of config.costs) { + if (cost.keyIsID) { + usefulResources[cost.key] = { + value: cost.value, + target: this.parent.parent, + keyIsID: true + }; + } + } const resources = config.costs .filter(c => c.enabled !== false) .map(c => { - const resource = this.actor.system.resources[c.type]; - return { type: c.type, value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1) }; + const resource = usefulResources[c.key]; + return { + key: c.key, + value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1), + target: resource.target, + keyIsID: resource.keyIsID + }; }); await this.actor.modifyResource(resources); @@ -372,9 +387,27 @@ export default class DHBaseAction extends foundry.abstract.DataModel { }); } - hasCost(costs) { + async getResources(costs) { + const actorResources = this.actor.system.resources; + const itemResources = {}; + for (var itemResource of costs) { + if (itemResource.keyIsID) { + itemResources[itemResource.key] = { + value: this.parent.resource.value ?? 0 + }; + } + } + + return { + ...actorResources, + ...itemResources + }; + } + + /* COST */ + async hasCost(costs) { const realCosts = this.getRealCosts(costs), - hasFearCost = realCosts.findIndex(c => c.type === 'fear'); + hasFearCost = realCosts.findIndex(c => c.key === 'fear'); if (hasFearCost > -1) { const fearCost = realCosts.splice(hasFearCost, 1)[0]; if ( @@ -385,16 +418,15 @@ export default class DHBaseAction extends foundry.abstract.DataModel { } /* isReversed is a sign that the resource is inverted, IE it counts upwards instead of down */ - const resources = this.actor.system.resources; + const resources = await this.getResources(realCosts); return realCosts.reduce( (a, c) => - a && resources[c.type].isReversed - ? resources[c.type].value + (c.total ?? c.value) <= resources[c.type].max - : resources[c.type]?.value >= (c.total ?? c.value), + a && resources[c.key].isReversed + ? resources[c.key].value + (c.total ?? c.value) <= resources[c.key].max + : resources[c.key]?.value >= (c.total ?? c.value), true ); } - /* COST */ /* USES */ calcUses(uses) { @@ -409,7 +441,6 @@ export default class DHBaseAction extends foundry.abstract.DataModel { if (!uses) return true; return (uses.hasOwnProperty('enabled') && !uses.enabled) || uses.value + 1 <= uses.max; } - /* USES */ /* TARGET */ isTargetFriendly(target) { diff --git a/module/data/action/damageAction.mjs b/module/data/action/damageAction.mjs index 3e840e60..7f7faeee 100644 --- a/module/data/action/damageAction.mjs +++ b/module/data/action/damageAction.mjs @@ -11,8 +11,8 @@ export default class DHDamageAction extends DHBaseAction { async rollDamage(event, data) { let formula = this.damage.parts.map(p => this.getFormulaValue(p, data).getFormula(this.actor)).join(' + '), - damageTypes = [...new Set(this.damage.parts.reduce((a,c) => a.concat([...c.type]), []))]; - + damageTypes = [...new Set(this.damage.parts.reduce((a, c) => a.concat([...c.type]), []))]; + damageTypes = !damageTypes.length ? ['physical'] : damageTypes; if (!formula || formula == '') return; @@ -36,7 +36,7 @@ export default class DHDamageAction extends DHBaseAction { config.source.message = data._id; config.directDamage = false; } - + roll = CONFIG.Dice.daggerheart.DamageRoll.build(config); } } diff --git a/module/data/actor/base.mjs b/module/data/actor/base.mjs index d7c7213e..cf1eebb1 100644 --- a/module/data/actor/base.mjs +++ b/module/data/actor/base.mjs @@ -1,4 +1,4 @@ -import DHBaseActorSettings from "../../applications/sheets/api/actor-setting.mjs"; +import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs'; const resistanceField = () => new foundry.data.fields.SchemaField({ @@ -37,13 +37,12 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel { const fields = foundry.data.fields; const schema = {}; - if(this.metadata.isNPC) - schema.description = new fields.HTMLField({ required: true, nullable: true }); - if(this.metadata.hasResistances) + if (this.metadata.isNPC) schema.description = new fields.HTMLField({ required: true, nullable: true }); + if (this.metadata.hasResistances) schema.resistance = new fields.SchemaField({ physical: resistanceField(), magical: resistanceField() - }) + }); return schema; } diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index d8fc2b5c..3ee7c980 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -39,9 +39,7 @@ export default class DhCharacter extends BaseDataActor { resources: new fields.SchemaField({ hitPoints: resourceField(0, true), stress: resourceField(6, true), - hope: resourceField(6), - tokens: new fields.ObjectField(), - dice: new fields.ObjectField() + hope: resourceField(6) }), traits: new fields.SchemaField({ agility: attributeField(), @@ -292,9 +290,7 @@ export default class DhCharacter extends BaseDataActor { } get deathMoveViable() { - return ( - this.resources.hitPoints.max > 0 && this.resources.hitPoints.value >= this.resources.hitPoints.max - ); + return this.resources.hitPoints.max > 0 && this.resources.hitPoints.value >= this.resources.hitPoints.max; } get armorApplicableDamageTypes() { diff --git a/module/data/item/armor.mjs b/module/data/item/armor.mjs index 7a8b06c0..8522c8fc 100644 --- a/module/data/item/armor.mjs +++ b/module/data/item/armor.mjs @@ -10,7 +10,6 @@ export default class DHArmor extends AttachableItem { label: 'TYPES.Item.armor', type: 'armor', hasDescription: true, - isQuantifiable: true, isInventoryItem: true }); } diff --git a/module/data/item/attachableItem.mjs b/module/data/item/attachableItem.mjs index 2b0608eb..d2ef8d22 100644 --- a/module/data/item/attachableItem.mjs +++ b/module/data/item/attachableItem.mjs @@ -5,7 +5,7 @@ export default class AttachableItem extends BaseDataItem { const fields = foundry.data.fields; return { ...super.defineSchema(), - attached: new fields.ArrayField(new fields.DocumentUUIDField({ type: "Item", nullable: true })) + attached: new fields.ArrayField(new fields.DocumentUUIDField({ type: 'Item', nullable: true })) }; } @@ -90,7 +90,10 @@ export default class AttachableItem extends BaseDataItem { }); if (effectsToRemove.length > 0) { - await actor.deleteEmbeddedDocuments('ActiveEffect', effectsToRemove.map(e => e.id)); + await actor.deleteEmbeddedDocuments( + 'ActiveEffect', + effectsToRemove.map(e => e.id) + ); } } @@ -140,13 +143,18 @@ export default class AttachableItem extends BaseDataItem { const parentUuidProperty = `${parentType}Uuid`; const effectsToRemove = actor.effects.filter(effect => { const attachmentSource = effect.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS.itemAttachmentSource); - return attachmentSource && + return ( + attachmentSource && attachmentSource[parentUuidProperty] === this.parent.uuid && - attachmentSource.itemUuid === attachedUuid; + attachmentSource.itemUuid === attachedUuid + ); }); if (effectsToRemove.length > 0) { - await actor.deleteEmbeddedDocuments('ActiveEffect', effectsToRemove.map(e => e.id)); + await actor.deleteEmbeddedDocuments( + 'ActiveEffect', + effectsToRemove.map(e => e.id) + ); } } -} \ No newline at end of file +} diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index 9358a6b3..e99c85c3 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -11,12 +11,15 @@ const fields = foundry.data.fields; export default class BaseDataItem extends foundry.abstract.TypeDataModel { + static LOCALIZATION_PREFIXES = ['DAGGERHEART.ITEMS']; + /** @returns {ItemDataModelMetadata}*/ static get metadata() { return { label: 'Base Item', type: 'base', hasDescription: false, + hasResource: false, isQuantifiable: false, isInventoryItem: false }; @@ -33,6 +36,33 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { if (this.metadata.hasDescription) schema.description = new fields.HTMLField({ required: true, nullable: true }); + if (this.metadata.hasResource) { + schema.resource = new fields.SchemaField( + { + type: new fields.StringField({ + choices: CONFIG.DH.ITEM.itemResourceTypes, + initial: CONFIG.DH.ITEM.itemResourceTypes.simple + }), + value: new fields.NumberField({ integer: true, min: 0, initial: 0 }), + max: new fields.StringField({ nullable: true, initial: null }), + icon: new fields.StringField(), + recovery: new fields.StringField({ + choices: CONFIG.DH.GENERAL.refreshTypes, + initial: null, + nullable: true + }), + diceStates: new fields.TypedObjectField( + new fields.SchemaField({ + value: new fields.NumberField({ integer: true, nullable: true, initial: null }), + used: new fields.BooleanField({ initial: false }) + }) + ), + dieFaces: new fields.StringField({ initial: '4' }) + }, + { nullable: true, initial: null } + ); + } + if (this.metadata.isQuantifiable) schema.quantity = new fields.NumberField({ integer: true, initial: 1, min: 0, required: true }); @@ -62,28 +92,27 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { return data; } - /**@inheritdoc */ async _preCreate(data, options, user) { // Skip if no initial action is required or actions already exist - if (!this.metadata.hasInitialAction || !foundry.utils.isEmpty(this.actions)) return; + if (this.metadata.hasInitialAction && foundry.utils.isEmpty(this.actions)) { + const metadataType = this.metadata.type; + const actionType = { weapon: 'attack' }[metadataType]; + const ActionClass = game.system.api.models.actions.actionsTypes[actionType]; - const metadataType = this.metadata.type; - const actionType = { weapon: 'attack' }[metadataType]; - const ActionClass = game.system.api.models.actions.actionsTypes[actionType]; + const action = new ActionClass( + { + _id: foundry.utils.randomID(), + type: actionType, + name: game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[actionType].name), + ...ActionClass.getSourceConfig(this.parent) + }, + { + parent: this.parent + } + ); - const action = new ActionClass( - { - _id: foundry.utils.randomID(), - type: actionType, - name: game.i18n.localize(CONFIG.DH.ACTIONS.actionTypes[actionType].name), - ...ActionClass.getSourceConfig(this.parent) - }, - { - parent: this.parent - } - ); - - this.updateSource({ actions: [action] }); + this.updateSource({ actions: [action] }); + } } _onCreate(data) { @@ -95,7 +124,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { ...feature, system: { ...feature.system, - type: this.parent.type, + originItemType: this.parent.type, originId: data._id, identifier: feature.identifier } diff --git a/module/data/item/domainCard.mjs b/module/data/item/domainCard.mjs index 3451a14a..89bbfb40 100644 --- a/module/data/item/domainCard.mjs +++ b/module/data/item/domainCard.mjs @@ -7,7 +7,8 @@ export default class DHDomainCard extends BaseDataItem { return foundry.utils.mergeObject(super.metadata, { label: 'TYPES.Item.domainCard', type: 'domainCard', - hasDescription: true + hasDescription: true, + hasResource: true }); } diff --git a/module/data/item/feature.mjs b/module/data/item/feature.mjs index a5b5cb3c..061772c8 100644 --- a/module/data/item/feature.mjs +++ b/module/data/item/feature.mjs @@ -7,7 +7,8 @@ export default class DHFeature extends BaseDataItem { return foundry.utils.mergeObject(super.metadata, { label: 'TYPES.Item.feature', type: 'feature', - hasDescription: true + hasDescription: true, + hasResource: true }); } @@ -16,10 +17,32 @@ export default class DHFeature extends BaseDataItem { const fields = foundry.data.fields; return { ...super.defineSchema(), - type: new fields.StringField({ choices: CONFIG.DH.ITEM.featureTypes, nullable: true, initial: null }), + originItemType: new fields.StringField({ + choices: CONFIG.DH.ITEM.featureTypes, + nullable: true, + initial: null + }), originId: new fields.StringField({ nullable: true, initial: null }), identifier: new fields.StringField(), actions: new fields.ArrayField(new ActionField()) }; } + + get spellcastingModifier() { + let traitValue = 0; + if (this.actor && this.originId && ['class', 'subclass'].includes(this.originItemType)) { + if (this.originItemType === 'subclass') { + traitValue = + this.actor.system.traits[this.actor.items.get(this.originId).system.spellcastingTrait]?.value ?? 0; + } else { + const subclass = + this.actor.system.multiclass.value?.id === this.originId + ? this.actor.system.multiclass.subclass + : this.actor.system.class.subclass; + traitValue = this.actor.system.traits[subclass.system.spellcastingTrait]?.value ?? 0; + } + } + + return traitValue; + } } diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index 82b4ba80..9fbb3eba 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -9,7 +9,6 @@ export default class DHWeapon extends AttachableItem { label: 'TYPES.Item.weapon', type: 'weapon', hasDescription: true, - isQuantifiable: true, isInventoryItem: true // hasInitialAction: true }); @@ -37,7 +36,7 @@ export default class DHWeapon extends AttachableItem { actionIds: new fields.ArrayField(new fields.StringField({ required: true })) }) ), - attack: new ActionField({ + attack: new ActionField({ initial: { name: 'Attack', img: 'icons/skills/melee/blood-slash-foam-red.webp', diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index 87c9401e..8744223a 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -56,7 +56,7 @@ export default class DHRoll extends Roll { // Create Chat Message if (config.source?.message) { - if(game.modules.get('dice-so-nice')?.active) await game.dice3d.showForRoll(roll, game.user, true); + if (game.modules.get('dice-so-nice')?.active) await game.dice3d.showForRoll(roll, game.user, true); } else { config.message = await this.toMessage(roll, config); } diff --git a/module/documents/activeEffect.mjs b/module/documents/activeEffect.mjs index 03ac73bc..aa1a5b0d 100644 --- a/module/documents/activeEffect.mjs +++ b/module/documents/activeEffect.mjs @@ -1,3 +1,5 @@ +import { itemAbleRollParse } from '../helpers/utils.mjs'; + export default class DhActiveEffect extends ActiveEffect { get isSuppressed() { // If this is a copied effect from an attachment, never suppress it @@ -24,16 +26,18 @@ export default class DhActiveEffect extends ActiveEffect { */ get isAttached() { if (!this.parent || !this.parent.parent) return false; - + // Check if this item's UUID is in any actor's armor or weapon attachment lists const actor = this.parent.parent; if (!actor || !actor.items) return false; - + return actor.items.some(item => { - return (item.type === 'armor' || item.type === 'weapon') && - item.system?.attached && - Array.isArray(item.system.attached) && - item.system.attached.includes(this.parent.uuid); + return ( + (item.type === 'armor' || item.type === 'weapon') && + item.system?.attached && + Array.isArray(item.system.attached) && + item.system.attached.includes(this.parent.uuid) + ); }); } @@ -51,11 +55,7 @@ export default class DhActiveEffect extends ActiveEffect { } static applyField(model, change, field) { - const isItemTarget = change.value.toLowerCase().startsWith('item.'); - change.value = isItemTarget ? change.value.slice(5) : change.value; - change.value = Roll.safeEval( - Roll.replaceFormulaData(change.value, isItemTarget ? change.effect.parent : model) - ); + change.value = itemAbleRollParse(change.value, model, change.effect.parent); super.applyField(model, change, field); } diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index d8d75461..bfeb103e 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -396,7 +396,7 @@ export default class DhpActor extends Actor { if (Hooks.call(`${CONFIG.DH.id}.preTakeDamage`, this, baseDamage, type) === false) return null; if (this.type === 'companion') { - await this.modifyResource([{ value: 1, type: 'stress' }]); + await this.modifyResource([{ value: 1, key: 'stress' }]); return; } @@ -418,8 +418,8 @@ export default class DhpActor extends Actor { const { modifiedDamage, armorSpent, stressSpent } = armorStackResult; updates.find(u => u.type === 'hitPoints').value = modifiedDamage; updates.push( - ...(armorSpent ? [{ value: armorSpent, type: 'armorStack' }] : []), - ...(stressSpent ? [{ value: stressSpent, type: 'stress' }] : []) + ...(armorSpent ? [{ value: armorSpent, key: 'armorStack' }] : []), + ...(stressSpent ? [{ value: stressSpent, key: 'stress' }] : []) ); } } @@ -434,8 +434,8 @@ export default class DhpActor extends Actor { /* if(this.system.resistance[type]?.immunity) return 0; if(this.system.resistance[type]?.resistance) baseDamage = Math.ceil(baseDamage / 2); */ - if(this.canResist(type, 'immunity')) return 0; - if(this.canResist(type, 'resistance')) baseDamage = Math.ceil(baseDamage / 2); + if (this.canResist(type, 'immunity')) return 0; + if (this.canResist(type, 'resistance')) baseDamage = Math.ceil(baseDamage / 2); // const flatReduction = this.system.resistance[type].reduction; const flatReduction = this.getDamageTypeReduction(type); @@ -448,13 +448,16 @@ export default class DhpActor extends Actor { } canResist(type, resistance) { - if(!type) return 0; + if (!type) return 0; return type.every(t => this.system.resistance[t]?.[resistance] === true); } getDamageTypeReduction(type) { - if(!type) return 0; - const reduction = Object.entries(this.system.resistance).reduce((a, [index, value]) => type.includes(index) ? Math.min(value.reduction, a) : a, Infinity); + if (!type) return 0; + const reduction = Object.entries(this.system.resistance).reduce( + (a, [index, value]) => (type.includes(index) ? Math.min(value.reduction, a) : a), + Infinity + ); return reduction === Infinity ? 0 : reduction; } @@ -467,39 +470,61 @@ export default class DhpActor extends Actor { if (!resources.length) return; if (resources.find(r => r.type === 'stress')) this.convertStressDamageToHP(resources); - let updates = { actor: { target: this, resources: {} }, armor: { target: this.system.armor, resources: {} } }; + let updates = { + actor: { target: this, resources: {} }, + armor: { target: this.system.armor, resources: {} }, + items: {} + }; resources.forEach(r => { - switch (r.type) { - case 'fear': - ui.resources.updateFear( - game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear) + r.value - ); - break; - case 'armorStack': - updates.armor.resources['system.marks.value'] = Math.max( - Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore), - 0 - ); - break; - default: - updates.actor.resources[`system.resources.${r.type}.value`] = Math.max( - Math.min( - this.system.resources[r.type].value + r.value, - this.system.resources[r.type].max - ), - 0 - ); - break; + if (r.keyIsID) { + updates.items[r.key] = { + target: r.target, + resources: { + 'system.resource.value': r.target.system.resource.value + r.value + } + }; + } else { + switch (r.key) { + case 'fear': + ui.resources.updateFear( + game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear) + r.value + ); + break; + case 'armorStack': + updates.armor.resources['system.marks.value'] = Math.max( + Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore), + 0 + ); + break; + default: + updates.actor.resources[`system.resources.${r.key}.value`] = Math.max( + Math.min(this.system.resources[r.key].value + r.value, this.system.resources[r.key].max), + 0 + ); + break; + } } }); - Object.values(updates).forEach(async u => { - if (Object.keys(u.resources).length > 0) { - await emitAsGM( - GMUpdateEvent.UpdateDocument, - u.target.update.bind(u.target), - u.resources, - u.target.uuid - ); + Object.keys(updates).forEach(async key => { + const u = updates[key]; + if (key === 'items') { + Object.values(u).forEach(async item => { + await emitAsGM( + GMUpdateEvent.UpdateDocument, + item.target.update.bind(item.target), + item.resources, + item.target.uuid + ); + }); + } else { + if (Object.keys(u.resources).length > 0) { + await emitAsGM( + GMUpdateEvent.UpdateDocument, + u.target.update.bind(u.target), + u.resources, + u.target.uuid + ); + } } }); } diff --git a/module/helpers/handlebarsHelper.mjs b/module/helpers/handlebarsHelper.mjs index b3b46dcb..407f065e 100644 --- a/module/helpers/handlebarsHelper.mjs +++ b/module/helpers/handlebarsHelper.mjs @@ -1,3 +1,5 @@ +import { itemAbleRollParse } from './utils.mjs'; + export default class RegisterHandlebarsHelpers { static registerHelpers() { Handlebars.registerHelper({ @@ -6,8 +8,7 @@ export default class RegisterHandlebarsHelpers { times: this.times, damageFormula: this.damageFormula, damageSymbols: this.damageSymbols, - tertiary: this.tertiary, - signedNumber: this.signedNumber + rollParsed: this.rollParsed }); } @@ -43,7 +44,9 @@ export default class RegisterHandlebarsHelpers { return new Handlebars.SafeString(Array.from(symbols).map(symbol => ``)); } - static tertiary(a, b) { - return a ?? b; + static rollParsed(value, actor, item, numerical) { + const isNumerical = typeof numerical === 'boolean' ? numerical : false; + const result = itemAbleRollParse(value, actor, item); + return isNumerical && !result ? 0 : result; } } diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 40a71ac7..1e694aad 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -245,10 +245,10 @@ export const getDamageLabel = damage => { export const damageKeyToNumber = key => { return { - 'none': 0, - 'minor': 1, - 'major': 2, - 'severe': 3 + none: 0, + minor: 1, + major: 2, + severe: 3 }[key]; }; @@ -299,3 +299,15 @@ export const updateActorTokens = async (actor, update) => { } } }; + +export const itemAbleRollParse = (value, actor, item) => { + if (!value) return value; + + const isItemTarget = value.toLowerCase().startsWith('item.'); + const slicedValue = isItemTarget ? value.slice(5) : value; + try { + return Roll.safeEval(Roll.replaceFormulaData(slicedValue, isItemTarget ? item : actor)); + } catch (_) { + return ''; + } +}; diff --git a/module/systemRegistration/handlebars.mjs b/module/systemRegistration/handlebars.mjs index e4cc1a2c..8456094c 100644 --- a/module/systemRegistration/handlebars.mjs +++ b/module/systemRegistration/handlebars.mjs @@ -5,6 +5,8 @@ export const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/sheets/global/partials/action-item.hbs', 'systems/daggerheart/templates/sheets/global/partials/domain-card-item.hbs', 'systems/daggerheart/templates/sheets/global/partials/inventory-fieldset-items.hbs', + 'systems/daggerheart/templates/sheets/global/partials/item-resource.hbs', + 'systems/daggerheart/templates/sheets/global/partials/resource-section.hbs', 'systems/daggerheart/templates/components/card-preview.hbs', 'systems/daggerheart/templates/levelup/parts/selectable-card-preview.hbs', 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs', diff --git a/styles/less/dialog/index.less b/styles/less/dialog/index.less index 545ce2e1..f3e86518 100644 --- a/styles/less/dialog/index.less +++ b/styles/less/dialog/index.less @@ -4,6 +4,8 @@ @import './level-up/summary-container.less'; @import './level-up/tiers-container.less'; +@import './resource-dice/sheet.less'; + @import './actions/action-list.less'; @import './damage-selection/sheet.less'; diff --git a/styles/less/dialog/resource-dice/sheet.less b/styles/less/dialog/resource-dice/sheet.less new file mode 100644 index 00000000..7ddb53ca --- /dev/null +++ b/styles/less/dialog/resource-dice/sheet.less @@ -0,0 +1,58 @@ +.theme-light .daggerheart.dialog.dh-style.views.resource-dice { + .resource-items .resource-item { + input { + background-image: url('../assets/parchments/dh-parchment-light.png'); + } + + img { + filter: brightness(0) saturate(100%); + } + } +} + +.daggerheart.dialog.dh-style.views.resource-dice { + .reroll-confirmation { + margin-bottom: 8px; + } + + .resource-items { + display: flex; + justify-content: center; + gap: 8px; + + .resource-item { + position: relative; + display: flex; + align-items: center; + justify-content: center; + + input { + position: absolute; + border-color: light-dark(@dark-blue, @golden); + color: light-dark(black, white); + background-image: url('../assets/parchments/dh-parchment-dark.png'); + z-index: 2; + line-height: 22px; + height: unset; + text-align: center; + } + + img { + width: 48px; + height: 48px; + filter: brightness(0) saturate(100%) invert(97%) sepia(7%) saturate(580%) hue-rotate(332deg) + brightness(96%) contrast(95%); + } + } + } + + footer { + display: flex; + gap: 8px; + + button { + flex: 1; + white-space: nowrap; + } + } +} diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less index ab519a1c..39f0d1f1 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -112,7 +112,7 @@ margin: 5px; height: inherit; .tag { - box-shadow: 0 0 0 1.1em #E5E5E5 inset; + box-shadow: 0 0 0 1.1em @beige inset; vertical-align: top; box-sizing: border-box; max-width: 100%; @@ -120,9 +120,9 @@ color: black; border-radius: 3px; white-space: nowrap; - transition: .13s ease-out; + transition: 0.13s ease-out; height: 22px; - font-size: .9rem; + font-size: 0.9rem; gap: 0.5em; z-index: 1; .remove { diff --git a/styles/less/global/index.less b/styles/less/global/index.less index 932e48ab..c37de0c7 100644 --- a/styles/less/global/index.less +++ b/styles/less/global/index.less @@ -6,6 +6,7 @@ @import './tab-actions.less'; @import './tab-features.less'; @import './tab-effects.less'; +@import './tab-settings.less'; @import './item-header.less'; @import './feature-section.less'; @import './inventory-item.less'; diff --git a/styles/less/global/inventory-item.less b/styles/less/global/inventory-item.less index 24e53165..30fe920c 100644 --- a/styles/less/global/inventory-item.less +++ b/styles/less/global/inventory-item.less @@ -1,6 +1,17 @@ @import '../utils/colors.less'; @import '../utils/fonts.less'; +.theme-light .application.daggerheart.dh-style { + .inventory-item, + .card-item { + .item-resource .item-dice-resource { + img { + filter: brightness(0) saturate(100%); + } + } + } +} + .application.daggerheart.dh-style { .inventory-item { display: grid; @@ -21,10 +32,19 @@ } } + .item-label-wrapper { + display: grid; + grid-template-columns: 1fr 60px; + } + .item-label { font-family: @font-body; align-self: center; + &.fullWidth { + grid-column: span 2; + } + .item-name { font-size: 14px; } @@ -84,11 +104,27 @@ &:hover { .card-label { padding-top: 15px; - .controls { + .menu { opacity: 1; visibility: visible; transition: all 0.3s ease; max-height: 16px; + + &.resource-menu { + max-height: 55px; + + &.dice-menu { + max-height: 118px; + + .item-resources { + flex-wrap: wrap; + } + + .item-resource { + width: unset; + } + } + } } } } @@ -97,6 +133,7 @@ height: 100%; width: 100%; object-fit: cover; + border-radius: 6px; } .card-label { @@ -123,15 +160,88 @@ color: @beige; } - .controls { + .menu { display: flex; - gap: 15px; - align-items: center; + flex-direction: column; + gap: 8px; max-height: 0px; opacity: 0; visibility: collapse; transition: all 0.3s ease; color: @beige; + + .controls { + display: flex; + gap: 2px; + gap: 15px; + justify-content: center; + } + } + } + + .item-resources { + width: 92px; + } + + .item-resource { + width: 92px; + } + } + + .inventory-item, + .card-item { + .item-resources { + display: flex; + gap: 4px; + + .resource-edit { + font-size: 14px; + } + } + + .item-resource { + display: flex; + align-items: center; + justify-content: end; + gap: 4px; + + i { + flex: none; + font-size: 14px; + } + + input { + flex: 1; + } + + .item-dice-resource { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 26px; + + label { + position: absolute; + color: light-dark(white, black); + filter: drop-shadow(0 0 1px light-dark(@dark-blue, @golden)); + z-index: 2; + font-size: 18px; + cursor: pointer; + } + + img { + filter: brightness(0) saturate(100%) invert(97%) sepia(7%) saturate(580%) hue-rotate(332deg) + brightness(96%) contrast(95%); + } + + i { + position: absolute; + text-shadow: 0 0 3px white; + filter: drop-shadow(0 1px white); + color: black; + font-size: 26px; + } } } } diff --git a/styles/less/global/tab-attachments.less b/styles/less/global/tab-attachments.less index c283269e..04a9f49e 100644 --- a/styles/less/global/tab-attachments.less +++ b/styles/less/global/tab-attachments.less @@ -4,4 +4,4 @@ width: 100%; } } -} \ No newline at end of file +} diff --git a/styles/less/global/tab-settings.less b/styles/less/global/tab-settings.less new file mode 100644 index 00000000..3d5248be --- /dev/null +++ b/styles/less/global/tab-settings.less @@ -0,0 +1,8 @@ +@import '../utils/colors.less'; +@import '../utils/fonts.less'; + +.sheet.daggerheart.dh-style { + .tab.settings { + margin-bottom: 36px; + } +} diff --git a/styles/less/ui/chat/chat.less b/styles/less/ui/chat/chat.less index 945bda3f..12e8ba0c 100644 --- a/styles/less/ui/chat/chat.less +++ b/styles/less/ui/chat/chat.less @@ -31,6 +31,14 @@ } } + &.resource-roll { + .reroll-message { + text-align: center; + font-size: 18px; + margin-bottom: 0; + } + } + &.roll { .dice-flavor { text-align: center; diff --git a/templates/actionTypes/cost.hbs b/templates/actionTypes/cost.hbs index 515f4968..116fc631 100644 --- a/templates/actionTypes/cost.hbs +++ b/templates/actionTypes/cost.hbs @@ -6,7 +6,7 @@ {{#each source as |cost index|}}
{{formField ../fields.scalable label="Scalable" value=cost.scalable name=(concat "cost." index ".scalable") classes="checkbox"}} - {{formField ../fields.type choices=(@root.disableOption index ../fields.type.choices ../source) label="Resource" value=cost.type name=(concat "cost." index ".type") localize=true}} + {{formField ../fields.key choices=(@root.disableOption index @root.costOptions ../source) label="Resource" value=cost.key name=(concat "cost." index ".key") localize=true}} {{formField ../fields.value label="Amount" value=cost.value name=(concat "cost." index ".value")}} {{formField ../fields.step label="Step" value=cost.step name=(concat "cost." index ".step") disabled=(not cost.scalable)}} diff --git a/templates/dialogs/dice-roll/costSelection.hbs b/templates/dialogs/dice-roll/costSelection.hbs index 39ef7cf7..48ff90a0 100644 --- a/templates/dialogs/dice-roll/costSelection.hbs +++ b/templates/dialogs/dice-roll/costSelection.hbs @@ -10,7 +10,7 @@ {{#each costs as | cost index |}}
- + {{#if scalable}} diff --git a/templates/dialogs/dice-roll/resourceDice.hbs b/templates/dialogs/dice-roll/resourceDice.hbs new file mode 100644 index 00000000..33c93386 --- /dev/null +++ b/templates/dialogs/dice-roll/resourceDice.hbs @@ -0,0 +1,16 @@ +
+
+ {{#times (rollParsed item.system.resource.max actor item numerical=true)}} + {{#with (ifThen (lookup ../diceStates this) (lookup ../diceStates this) this) as | state |}} +
+ + +
+ {{/with}} + {{/times}} +
+
+ + +
+
\ No newline at end of file diff --git a/templates/sheets-settings/action-settings/configuration.hbs b/templates/sheets-settings/action-settings/configuration.hbs index 74d5fcc1..51b2a72b 100644 --- a/templates/sheets-settings/action-settings/configuration.hbs +++ b/templates/sheets-settings/action-settings/configuration.hbs @@ -4,6 +4,6 @@ data-tab="config" > {{> 'systems/daggerheart/templates/actionTypes/uses.hbs' fields=fields.uses.fields source=source.uses}} - {{> 'systems/daggerheart/templates/actionTypes/cost.hbs' fields=fields.cost.element.fields source=source.cost}} + {{> 'systems/daggerheart/templates/actionTypes/cost.hbs' fields=fields.cost.element.fields source=source.cost costOptions=costOptions}} {{> 'systems/daggerheart/templates/actionTypes/range-target.hbs' fields=(object range=fields.range target=fields.target.fields) source=(object target=source.target range=source.range)}} \ No newline at end of file diff --git a/templates/sheets/global/partials/domain-card-item.hbs b/templates/sheets/global/partials/domain-card-item.hbs index 3f2fe146..0ff7e8f5 100644 --- a/templates/sheets/global/partials/domain-card-item.hbs +++ b/templates/sheets/global/partials/domain-card-item.hbs @@ -1,31 +1,35 @@
  • -
    - {{#if (eq type 'weapon')}} - - - +
    {{item.name}}
    diff --git a/templates/sheets/global/partials/inventory-item.hbs b/templates/sheets/global/partials/inventory-item.hbs index 21b7de61..a6373c08 100644 --- a/templates/sheets/global/partials/inventory-item.hbs +++ b/templates/sheets/global/partials/inventory-item.hbs @@ -1,128 +1,137 @@
  • -
    - {{#if isCompanion}} - {{item.name}} - {{else}} -
    {{item.name}}
    - {{/if}} - {{#if (eq type 'weapon')}} -
    +
    +
    + {{#if isCompanion}} + {{item.name}} + {{else}} +
    {{item.name}}
    + {{/if}} + {{#if (eq type 'weapon')}} +
    + {{#if isSidebar}} +
    +
    + {{localize (concat 'DAGGERHEART.CONFIG.Traits.' item.system.attack.roll.trait '.short')}} + {{localize (concat 'DAGGERHEART.CONFIG.Range.' item.system.attack.range '.short')}} + - + {{item.system.attack.damage.parts.0.value.dice}}{{#if item.system.attack.damage.parts.0.value.bonus}} + {{item.system.attack.damage.parts.0.value.bonus}}{{/if}} + {{#each item.system.attack.damage.parts.0.type as | type | }} + {{#with (lookup @root.config.GENERAL.damageTypes type)}} + + {{/with}} + {{/each}} +
    +
    + {{else}} +
    + {{localize (concat 'DAGGERHEART.CONFIG.Traits.' item.system.attack.roll.trait '.name')}} +
    +
    + {{localize (concat 'DAGGERHEART.CONFIG.Range.' item.system.attack.range '.name')}} +
    +
    + {{item.system.attack.damage.parts.0.value.dice}}{{#if item.system.attack.damage.parts.0.value.bonus}} + {{item.system.attack.damage.parts.0.value.bonus}}{{/if}} + ( + {{#each item.system.attack.damage.parts.0.type}} + {{localize (concat 'DAGGERHEART.CONFIG.DamageType.' this '.abbreviation')}} + {{/each}} + ) +
    +
    + {{localize (concat 'DAGGERHEART.CONFIG.Burden.' item.system.burden)}} +
    + {{/if}} +
    + {{/if}} + {{#if (eq type 'armor')}} {{#if isSidebar}}
    - {{!-- {{localize (concat 'DAGGERHEART.CONFIG.Traits.' item.system.attack.roll.trait '.short')}} --}} - {{localize (concat 'DAGGERHEART.CONFIG.Range.' item.system.attack.range '.short')}} - - - {{item.system.attack.damage.parts.0.value.dice}}{{#if item.system.attack.damage.parts.0.value.bonus}} + {{item.system.attack.damage.parts.0.value.bonus}}{{/if}} - {{!-- ({{localize (concat 'DAGGERHEART.CONFIG.DamageType.' item.system.attack.damage.parts.0.type '.abbreviation')}}) --}} - {{#each item.system.attack.damage.parts.0.type as | type | }} - {{#with (lookup @root.config.GENERAL.damageTypes type)}} - - {{/with}} - {{/each}} + {{localize "DAGGERHEART.ITEMS.Armor.baseScore"}}: + {{item.system.baseScore}}
    {{else}} -
    - {{localize (concat 'DAGGERHEART.CONFIG.Traits.' item.system.attack.roll.trait '.name')}} -
    -
    - {{localize (concat 'DAGGERHEART.CONFIG.Range.' item.system.attack.range '.name')}} -
    -
    - {{item.system.attack.damage.parts.0.value.dice}}{{#if item.system.attack.damage.parts.0.value.bonus}} + {{item.system.attack.damage.parts.0.value.bonus}}{{/if}} - ( - {{#each item.system.attack.damage.parts.0.type}} - {{localize (concat 'DAGGERHEART.CONFIG.DamageType.' this '.abbreviation')}} - {{/each}} - ) -
    -
    - {{localize (concat 'DAGGERHEART.CONFIG.Burden.' item.system.burden)}} +
    +
    + {{localize "DAGGERHEART.ITEMS.Armor.baseScore"}}: + {{item.system.baseScore}} +
    +
    + {{localize "DAGGERHEART.ITEMS.Armor.baseThresholds.base"}}: + {{item.system.baseThresholds.major}} + / + {{item.system.baseThresholds.severe}} +
    {{/if}} -
    - {{/if}} - {{#if (eq type 'armor')}} - {{#if isSidebar}} -
    -
    - {{localize "DAGGERHEART.ITEMS.Armor.baseScore"}}: - {{item.system.baseScore}} + {{/if}} + {{#if (eq type 'domainCard')}} + {{#if isSidebar}} +
    +
    + {{localize (concat 'DAGGERHEART.CONFIG.DomainCardTypes.' item.system.type)}} + - + {{localize (concat 'DAGGERHEART.GENERAL.Domain.' item.system.domain '.label')}} + - + {{item.system.recallCost}} + +
    -
    - {{else}} + {{else}} +
    +
    + {{localize (concat 'DAGGERHEART.CONFIG.DomainCardTypes.' item.system.type)}} +
    +
    + {{localize (concat 'DAGGERHEART.GENERAL.Domain.' item.system.domain '.label')}} +
    +
    + {{localize "DAGGERHEART.ITEMS.DomainCard.recallCost"}}: + {{item.system.recallCost}} +
    +
    + {{/if}} + {{/if}} + {{#if (eq type 'effect')}}
    - {{localize "DAGGERHEART.ITEMS.Armor.baseScore"}}: - {{item.system.baseScore}} + {{localize (concat 'TYPES.Item.' item.parent.type)}} + : + {{item.parent.name}}
    - {{localize "DAGGERHEART.ITEMS.Armor.baseThresholds.base"}}: - {{item.system.baseThresholds.major}} - / - {{item.system.baseThresholds.severe}} + {{#if item.duration.duration}} + {{localize 'DAGGERHEART.EFFECTS.Duration.temporary'}} + {{else}} + {{localize 'DAGGERHEART.EFFECTS.Duration.passive'}} + {{/if}} +
    + {{#each item.statuses as |status|}} +
    + {{localize (concat 'DAGGERHEART.CONFIG.Condition.' status '.name')}} +
    + {{/each}} +
    + {{/if}} + {{#if (eq type 'action')}} +
    +
    + {{localize (concat 'DAGGERHEART.ACTIONS.TYPES.' item.type '.name')}} +
    +
    + {{localize (concat 'DAGGERHEART.CONFIG.ActionType.' item.actionType)}}
    {{/if}} +
    + {{#if (and (not isSidebar) (eq item.system.resource.type 'simple'))}} + {{> "systems/daggerheart/templates/sheets/global/partials/item-resource.hbs"}} {{/if}} - {{#if (eq type 'domainCard')}} - {{#if isSidebar}} -
    -
    - {{localize (concat 'DAGGERHEART.CONFIG.DomainCardTypes.' item.system.type)}} - - - {{localize (concat 'DAGGERHEART.GENERAL.Domain.' item.system.domain '.label')}} - - - {{item.system.recallCost}} - -
    -
    - {{else}} -
    -
    - {{localize (concat 'DAGGERHEART.CONFIG.DomainCardTypes.' item.system.type)}} -
    -
    - {{localize (concat 'DAGGERHEART.GENERAL.Domain.' item.system.domain '.label')}} -
    -
    - {{localize "DAGGERHEART.ITEMS.DomainCard.recallCost"}}: - {{item.system.recallCost}} -
    -
    - {{/if}} - {{/if}} - {{#if (eq type 'effect')}} -
    -
    - {{localize (concat 'TYPES.Item.' item.parent.type)}} - : - {{item.parent.name}} -
    -
    - {{#if item.duration.duration}} - {{localize 'DAGGERHEART.EFFECTS.Duration.temporary'}} - {{else}} - {{localize 'DAGGERHEART.EFFECTS.Duration.passive'}} - {{/if}} -
    - {{#each item.statuses as |status|}} -
    - {{localize (concat 'DAGGERHEART.CONFIG.Condition.' status '.name')}} -
    - {{/each}} -
    - {{/if}} - {{#if (eq type 'action')}} -
    -
    - {{localize (concat 'DAGGERHEART.ACTIONS.TYPES.' item.type '.name')}} -
    -
    - {{localize (concat 'DAGGERHEART.CONFIG.ActionType.' item.actionType)}} -
    + {{#if (and (not isSidebar) item.system.quantity)}} +
    +
    {{/if}}
    @@ -175,7 +184,9 @@ {{/unless}}
    {{#unless isSidebar}}{{{item.system.description}}}{{/unless}}
    - + {{#if (and (not isSidebar) (eq item.system.resource.type 'diceValue'))}} + {{> "systems/daggerheart/templates/sheets/global/partials/item-resource.hbs"}} + {{/if}} {{#if featureType}}
    {{#each item.system.actions as | action |}} diff --git a/templates/sheets/global/partials/item-resource.hbs b/templates/sheets/global/partials/item-resource.hbs new file mode 100644 index 00000000..9b92dea0 --- /dev/null +++ b/templates/sheets/global/partials/item-resource.hbs @@ -0,0 +1,21 @@ +{{#if (eq item.system.resource.type 'simple')}} +
    + + +
    +{{else}} +
    + {{#times (rollParsed item.system.resource.max item.parent item numerical=true)}} + {{#with (ifThen (lookup ../item.system.resource.diceStates this) (lookup ../item.system.resource.diceStates this) this) as | state |}} + +
    + + + {{#if state.used}}{{/if}} +
    +
    + {{/with}} + {{/times}} + +
    +{{/if}} \ No newline at end of file diff --git a/templates/sheets/global/partials/resource-section.hbs b/templates/sheets/global/partials/resource-section.hbs new file mode 100644 index 00000000..f0d322d3 --- /dev/null +++ b/templates/sheets/global/partials/resource-section.hbs @@ -0,0 +1,29 @@ +
    + + {{localize "DAGGERHEART.GENERAL.Resource.single"}} + {{#unless source.system.resource}} + + {{else}} + + {{/unless}} + + + {{#if source.system.resource}} +
    + {{formGroup systemFields.resource.fields.type value=source.system.resource.type localize=true blank=false}} + {{formGroup systemFields.resource.fields.recovery value=source.system.resource.recovery localize=true}} +
    + +
    + {{#if (eq source.system.resource.type 'simple')}} + {{formGroup systemFields.resource.fields.value value=source.system.resource.value localize=true}} + {{formGroup systemFields.resource.fields.max value=source.system.resource.max localize=true}} + {{else}} + {{formGroup systemFields.resource.fields.dieFaces value=source.system.resource.dieFaces localize=true}} + {{formGroup systemFields.resource.fields.max value=source.system.resource.max label="DAGGERHEART.ITEMS.FIELDS.resource.amount.label" localize=true}} + {{/if}} +
    + + {{#if (eq source.system.resource.type 'simple')}}{{formGroup systemFields.resource.fields.icon value=source.system.resource.icon localize=true placeholder="fa-solid fa-hashtag"}}{{/if}} + {{/if}} +
    \ No newline at end of file diff --git a/templates/sheets/items/class/features.hbs b/templates/sheets/items/class/features.hbs index 96ce2655..5431e54b 100644 --- a/templates/sheets/items/class/features.hbs +++ b/templates/sheets/items/class/features.hbs @@ -4,7 +4,7 @@ data-group='{{tabs.features.group}}' >
    -
    +
    {{localize "DAGGERHEART.ITEMS.Class.hopeFeatures"}}
    {{#each source.system.hopeFeatures as |feature|}} @@ -13,7 +13,7 @@
    -
    +
    {{localize "DAGGERHEART.ITEMS.Class.classFeatures"}}
    {{#each source.system.classFeatures as |feature|}} diff --git a/templates/sheets/items/domainCard/settings.hbs b/templates/sheets/items/domainCard/settings.hbs index 5c653fdc..2faa6934 100644 --- a/templates/sheets/items/domainCard/settings.hbs +++ b/templates/sheets/items/domainCard/settings.hbs @@ -17,4 +17,6 @@ {{localize "DAGGERHEART.ITEMS.DomainCard.recallCost"}} {{formField systemFields.recallCost value=source.system.recallCost data-dtype="Number"}}
    + + {{> "systems/daggerheart/templates/sheets/global/partials/resource-section.hbs" }} \ No newline at end of file diff --git a/templates/sheets/items/feature/settings.hbs b/templates/sheets/items/feature/settings.hbs new file mode 100644 index 00000000..24e6f8ef --- /dev/null +++ b/templates/sheets/items/feature/settings.hbs @@ -0,0 +1,7 @@ +
    + {{> "systems/daggerheart/templates/sheets/global/partials/resource-section.hbs" }} +
    \ No newline at end of file diff --git a/templates/ui/chat/resource-roll.hbs b/templates/ui/chat/resource-roll.hbs new file mode 100644 index 00000000..b6e1f3b0 --- /dev/null +++ b/templates/ui/chat/resource-roll.hbs @@ -0,0 +1,3 @@ +
    +
    {{localize "DAGGERHEART.UI.Chat.resourceRoll.playerMessage" user=user name=name }}
    +
    \ No newline at end of file diff --git a/templates/ui/tooltip/parts/tooltipTags.hbs b/templates/ui/tooltip/parts/tooltipTags.hbs index a02a729a..ba4e875f 100644 --- a/templates/ui/tooltip/parts/tooltipTags.hbs +++ b/templates/ui/tooltip/parts/tooltipTags.hbs @@ -6,7 +6,7 @@
    {{localize feature.name}}
    {{#if feature.img}}{{/if}}
    -
    {{{localize (tertiary feature.description feature.system.description)}}}
    +
    {{{localize (ifThen feature.description feature.description feature.system.description)}}}
    {{/each}}
    \ No newline at end of file