diff --git a/lang/en.json b/lang/en.json index f66d9c0a..a6ef81ec 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" } }, "CONFIG": { @@ -631,6 +635,10 @@ "abbreviation": "AS" } }, + "ItemResourceType": { + "simple": "Simple", + "diceValue": "Dice Value" + }, "Range": { "self": { "name": "Self", @@ -1149,10 +1157,13 @@ "ITEMS": { "FIELDS": { "resource": { - "value": { "label": "Value" }, - "max": { "label": "Max" }, + "amount": { "label": "Amount" }, + "dieFaces": { "label": "Die Faces" }, "icon": { "label": "Icon" }, - "recovery": { "label": "Recovery" } + "max": { "label": "Max" }, + "recovery": { "label": "Recovery" }, + "type": { "label": "Type" }, + "value": { "label": "Value" } } }, "Armor": { @@ -1332,8 +1343,8 @@ }, "UI": { "Chat": { - "dualityRoll": { - "abilityCheckTitle": "{ability} Check" + "applyEffect": { + "title": "Apply Effects - {name}" }, "attackRoll": { "title": "Attack - {attack}", @@ -1349,25 +1360,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/resourceDiceDialog.mjs b/module/applications/dialogs/resourceDiceDialog.mjs new file mode 100644 index 00000000..0e93852a --- /dev/null +++ b/module/applications/dialogs/resourceDiceDialog.mjs @@ -0,0 +1,76 @@ +const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; + +export default class ResourceDiceDialog extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(name, actorName, resource, options = {}) { + super(options); + + this.name = name; + this.actorName = actorName; + this.resource = resource; + } + + static DEFAULT_OPTIONS = { + tag: 'form', + classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'resource-dice'], + window: { + icon: 'fa-solid fa-dice' + }, + actions: { + rerollDice: this.rerollDice + }, + 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.name }); + } + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.resource = this.resource; + + return context; + } + + static async rerollDice() { + const diceFormula = `${this.resource.max}d${this.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 => x.result); + + 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.actorName, + name: this.name + } + ) + }); + + cls.create(msg.toObject()); + this.close(); + } + + static async create(name, actorName, resource, options = {}) { + return new Promise(resolve => { + const app = new this(name, actorName, resource, options); + app.addEventListener('close', () => resolve(app.rollValues), { once: true }); + app.render({ force: true }); + }); + } +} diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 2003bc4c..c06086d4 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -25,6 +25,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: { @@ -668,6 +670,45 @@ 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.name, + this.document.name, + item.system.resource + ); + if (!rollValues) return; + + await item.update({ + 'system.resource.diceStates': rollValues.reduce((acc, value, index) => { + acc[index] = { value, used: false }; + return acc; + }, {}) + }); + this.render(); + } + /** * Send item to Chat * @type {ApplicationClickAction} diff --git a/module/applications/sheets/api/base-item.mjs b/module/applications/sheets/api/base-item.mjs index f6688216..2847bfc8 100644 --- a/module/applications/sheets/api/base-item.mjs +++ b/module/applications/sheets/api/base-item.mjs @@ -223,7 +223,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { */ static async #addResource() { await this.document.update({ - 'system.resource': { value: 0 } + 'system.resource': { type: 'simple', value: 0 } }); } 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/config/itemConfig.mjs b/module/config/itemConfig.mjs index cdc8a235..c576af7d 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/item/base.mjs b/module/data/item/base.mjs index f73e7814..53e5075d 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -39,6 +39,10 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { 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.NumberField({ nullable: true, initial: null }), icon: new fields.StringField(), @@ -46,7 +50,14 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { 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 } ); @@ -81,7 +92,6 @@ 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; @@ -122,6 +132,23 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { ); } + async _preUpdate(data) { + if (data.system?.resource?.max) { + const diceStatesKeys = Object.keys(this.resource.diceStates); + const resourceDiff = Math.abs(data.system.resource.max - diceStatesKeys.length); + if (!resourceDiff) return; + + const diceStates = {}; + const deleting = data.system.resource.max < diceStatesKeys.length; + [...Array(resourceDiff).keys()].forEach(nr => { + const key = deleting ? diceStatesKeys.length - 1 - nr : diceStatesKeys.length + nr; + diceStates[`${deleting ? '-=' : ''}${key}`] = deleting ? null : { value: null }; + }); + + foundry.utils.setProperty(data, 'system.resource.diceStates', diceStates); + } + } + async _preDelete() { if (!this.actor || this.actor.type !== 'character') return; diff --git a/module/systemRegistration/handlebars.mjs b/module/systemRegistration/handlebars.mjs index 133dd7ef..8456094c 100644 --- a/module/systemRegistration/handlebars.mjs +++ b/module/systemRegistration/handlebars.mjs @@ -5,6 +5,7 @@ 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', 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..6a5a3744 --- /dev/null +++ b/styles/less/dialog/resource-dice/sheet.less @@ -0,0 +1,39 @@ +.daggerheart.dialog.dh-style.views.resource-dice { + .item-resources { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + + .item-resource { + width: 38px; + height: 38px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + + label { + position: absolute; + color: light-dark(white, black); + filter: drop-shadow(0 0 1px @golden); + font-size: 24px; + z-index: 2; + } + + img { + 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; + } + } +} diff --git a/styles/less/global/inventory-item.less b/styles/less/global/inventory-item.less index beb57b16..aeaffe2b 100644 --- a/styles/less/global/inventory-item.less +++ b/styles/less/global/inventory-item.less @@ -67,22 +67,6 @@ } } - .item-resource { - display: flex; - align-items: center; - justify-content: end; - gap: 4px; - - i { - flex: none; - font-size: 14px; - } - - input { - flex: 1; - } - } - .controls { display: flex; align-items: center; @@ -109,11 +93,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; + } + } + } } } } @@ -122,6 +122,7 @@ height: 100%; width: 100%; object-fit: cover; + border-radius: 6px; } .card-label { @@ -148,15 +149,87 @@ 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 @golden); + z-index: 2; + font-size: 18px; + } + + 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/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/dialogs/dice-roll/resourceDice.hbs b/templates/dialogs/dice-roll/resourceDice.hbs new file mode 100644 index 00000000..2d4b231d --- /dev/null +++ b/templates/dialogs/dice-roll/resourceDice.hbs @@ -0,0 +1,13 @@ +