diff --git a/README.md b/README.md index 5e2bbcec..c40a27a3 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,26 @@ -# Daggerheart +# Foundryborne Daggerheart ## Table of Contents - [Overview](#overview) - [User Install Guide](#user-install) +- [Documentation](#documentation) - [Developer Setup](#development-setup) - [Contribution Info](#contributing) ## Overview -This is a community repo for a Foundry VTT implementation of Daggerheart. It is not associated with Critical Role or Darrington Press. +This is the community repo for the Foundry VTT system _Foundryborne_ Daggerheart. It is not associated with Critical Role or Darrington Press. ## User Install -1. **(Not Yet Supported - No Releases Yet)** Pasting `https://raw.githubusercontent.com/Foundryborne/daggerheart/refs/heads/main/system.json` into the Install System dialog on the Setup menu of the application. -2. **(Not Yet Supported - No Releases Yet)** Browsing the repository's Releases page, where you can copy any system.json link for use in the Install System dialog. -3. **(Not Yet Supported - No Releases Yet)** Downloading one of the .zip archives from the Releases page and extracting it into your foundry Data folder, under Data/systems/daggerheart. +1. **recommended** Searching for _Daggerheart_ or _Foundryborne_ in the System Instalaltion dialgoe of the FoundryVTT admin settings. +2. Pasting `https://raw.githubusercontent.com/Foundryborne/daggerheart/refs/heads/main/system.json` into the Install System dialog on the Setup menu of the application. +3. Downloading one of the .zip archives from the Releases page and extracting it into your foundry Data folder, under Data/systems/daggerheart. + +## Documentation + +You can find the documentation here: https://github.com/Foundryborne/daggerheart/wiki ## Development Setup diff --git a/lang/en.json b/lang/en.json index fd278e89..867558d7 100755 --- a/lang/en.json +++ b/lang/en.json @@ -498,6 +498,11 @@ "ReactionRoll": { "title": "Reaction Roll: {trait}" }, + "RerollDialog": { + "title": "Reroll", + "deselectDiceNotification": "Deselect one of the selected dice first", + "acceptCurrentRolls": "Accept Current Rolls" + }, "ResourceDice": { "title": "{name} Resource", "rerollDice": "Reroll Dice" @@ -505,7 +510,8 @@ }, "CLASS": { "Feature": { - "rallyDice": "Bardic Rally Dice" + "rallyDice": "Bardic Rally Dice", + "short": "Rally" } }, "CONFIG": { diff --git a/module/applications/dialogs/_module.mjs b/module/applications/dialogs/_module.mjs index 520c90b6..8908ae2b 100644 --- a/module/applications/dialogs/_module.mjs +++ b/module/applications/dialogs/_module.mjs @@ -6,5 +6,6 @@ export { default as DeathMove } from './deathMove.mjs'; export { default as Downtime } from './downtime.mjs'; export { default as MulticlassChoiceDialog } from './multiclassChoiceDialog.mjs'; export { default as OwnershipSelection } from './ownershipSelection.mjs'; +export { default as RerollDamageDialog } from './rerollDamageDialog.mjs'; export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs'; export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs'; diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index a06708e6..0f1cd946 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -151,11 +151,19 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio this.config.experiences.indexOf(button.dataset.key) > -1 ? this.config.experiences.filter(x => x !== button.dataset.key) : [...this.config.experiences, button.dataset.key]; - if(this.config?.data?.parent?.type === 'character' || this.config?.data?.parent?.type === 'companion') { + if (this.config?.data?.parent?.type === 'character' || this.config?.data?.parent?.type === 'companion') { this.config.costs = this.config.costs.indexOf(this.config.costs.find(c => c.extKey === button.dataset.key)) > -1 ? this.config.costs.filter(x => x.extKey !== button.dataset.key) - : [...this.config.costs, { extKey: button.dataset.key, key: 'hope', value: 1, name: this.config.data?.experiences?.[button.dataset.key]?.name }]; + : [ + ...this.config.costs, + { + extKey: button.dataset.key, + key: 'hope', + value: 1, + name: this.config.data?.experiences?.[button.dataset.key]?.name + } + ]; } this.render(); } @@ -166,8 +174,8 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio this.config.roll.type = this.reactionOverride ? CONFIG.DH.ITEM.actionTypes.reaction.id : this.config.roll.type === CONFIG.DH.ITEM.actionTypes.reaction.id - ? null - : this.config.roll.type; + ? null + : this.config.roll.type; this.render(); } } diff --git a/module/applications/dialogs/damageDialog.mjs b/module/applications/dialogs/damageDialog.mjs index 2d372725..fbc584e4 100644 --- a/module/applications/dialogs/damageDialog.mjs +++ b/module/applications/dialogs/damageDialog.mjs @@ -56,12 +56,14 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application label, icon })); + context.modifiers = this.config.modifiers; return context; } static updateRollConfiguration(_event, _, formData) { const { ...rest } = foundry.utils.expandObject(formData.object); foundry.utils.mergeObject(this.config.roll, rest.roll); + foundry.utils.mergeObject(this.config.modifiers, rest.modifiers); this.config.selectedRollMode = rest.selectedRollMode; this.render(); diff --git a/module/applications/dialogs/rerollDamageDialog.mjs b/module/applications/dialogs/rerollDamageDialog.mjs new file mode 100644 index 00000000..0c2ea0e1 --- /dev/null +++ b/module/applications/dialogs/rerollDamageDialog.mjs @@ -0,0 +1,279 @@ +const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; + +export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(message, options = {}) { + super(options); + + this.message = message; + this.damage = Object.keys(message.system.damage).reduce((acc, typeKey) => { + const type = message.system.damage[typeKey]; + acc[typeKey] = Object.keys(type.parts).reduce((acc, partKey) => { + const part = type.parts[partKey]; + acc[partKey] = Object.keys(part.dice).reduce((acc, diceKey) => { + const dice = part.dice[diceKey]; + const activeResults = dice.results.filter(x => x.active); + acc[diceKey] = { + dice: dice.dice, + selectedResults: activeResults.length, + maxSelected: activeResults.length, + results: activeResults.map(x => ({ ...x, selected: true })) + }; + + return acc; + }, {}); + + return acc; + }, {}); + + return acc; + }, {}); + } + + static DEFAULT_OPTIONS = { + id: 'reroll-dialog', + classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'reroll-dialog'], + window: { + icon: 'fa-solid fa-dice' + }, + actions: { + toggleResult: RerollDamageDialog.#toggleResult, + selectRoll: RerollDamageDialog.#selectRoll, + doReroll: RerollDamageDialog.#doReroll, + save: RerollDamageDialog.#save + } + }; + + /** @override */ + static PARTS = { + main: { + id: 'main', + template: 'systems/daggerheart/templates/dialogs/rerollDialog/damage/main.hbs' + }, + footer: { + id: 'footer', + template: 'systems/daggerheart/templates/dialogs/rerollDialog/footer.hbs' + } + }; + + get title() { + return game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.damageTitle'); + } + + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + + htmlElement.querySelectorAll('.to-reroll-input').forEach(element => { + element.addEventListener('change', this.toggleDice.bind(this)); + }); + } + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.damage = this.damage; + context.disabledReroll = !this.getRerollDice().length; + context.saveDisabled = !this.isSelectionDone(); + + return context; + } + + static async #save() { + const update = { + 'system.damage': Object.keys(this.damage).reduce((acc, typeKey) => { + const type = this.damage[typeKey]; + let typeTotal = 0; + const messageType = this.message.system.damage[typeKey]; + const parts = Object.keys(type).map(partKey => { + const part = type[partKey]; + const messagePart = messageType.parts[partKey]; + let partTotal = messagePart.modifierTotal; + const dice = Object.keys(part).map(diceKey => { + const dice = part[diceKey]; + const total = dice.results.reduce((acc, result) => { + if (result.active) acc += result.result; + return acc; + }, 0); + partTotal += total; + const messageDice = messagePart.dice[diceKey]; + return { + ...messageDice, + total: total, + results: dice.results.map(x => ({ + ...x, + hasRerolls: dice.results.length > 1 + })) + }; + }); + + typeTotal += partTotal; + return { + ...messagePart, + total: partTotal, + dice: dice + }; + }); + + acc[typeKey] = { + ...messageType, + total: typeTotal, + parts: parts + }; + + return acc; + }, {}) + }; + await this.message.update(update); + await this.close(); + } + + getRerollDice() { + const rerollDice = []; + Object.keys(this.damage).forEach(typeKey => { + const type = this.damage[typeKey]; + Object.keys(type).forEach(partKey => { + const part = type[partKey]; + Object.keys(part).forEach(diceKey => { + const dice = part[diceKey]; + Object.keys(dice.results).forEach(resultKey => { + const result = dice.results[resultKey]; + if (result.toReroll) { + rerollDice.push({ + ...result, + dice: dice.dice, + type: typeKey, + part: partKey, + dice: diceKey, + result: resultKey + }); + } + }); + }); + }); + }); + + return rerollDice; + } + + isSelectionDone() { + const diceFinishedData = []; + Object.keys(this.damage).forEach(typeKey => { + const type = this.damage[typeKey]; + Object.keys(type).forEach(partKey => { + const part = type[partKey]; + Object.keys(part).forEach(diceKey => { + const dice = part[diceKey]; + const selected = dice.results.reduce((acc, result) => acc + (result.active ? 1 : 0), 0); + diceFinishedData.push(selected === dice.maxSelected); + }); + }); + }); + + return diceFinishedData.every(x => x); + } + + toggleDice(event) { + const target = event.target; + const { type, part, dice } = target.dataset; + const toggleDice = this.damage[type][part][dice]; + + const existingDiceRerolls = this.getRerollDice().filter( + x => x.type === type && x.part === part && x.dice === dice + ); + + const allRerolled = existingDiceRerolls.length === toggleDice.results.filter(x => x.active).length; + + toggleDice.toReroll = !allRerolled; + toggleDice.results.forEach(result => { + if (result.active) { + result.toReroll = !allRerolled; + } + }); + + this.render(); + } + + static #toggleResult(event) { + event.stopPropagation(); + + const target = event.target.closest('.to-reroll-result'); + const { type, part, dice, result } = target.dataset; + const toggleDice = this.damage[type][part][dice]; + const toggleResult = toggleDice.results[result]; + toggleResult.toReroll = !toggleResult.toReroll; + + const existingDiceRerolls = this.getRerollDice().filter( + x => x.type === type && x.part === part && x.dice === dice + ); + + const allToReroll = existingDiceRerolls.length === toggleDice.results.filter(x => x.active).length; + toggleDice.toReroll = allToReroll; + + this.render(); + } + + static async #selectRoll(_, button) { + const { type, part, dice, result } = button.dataset; + + const diceVal = this.damage[type][part][dice]; + const diceResult = diceVal.results[result]; + if (!diceResult.active && diceVal.results.filter(x => x.active).length === diceVal.maxSelected) { + return ui.notifications.warn( + game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.deselectDiceNotification') + ); + } + + if (diceResult.active) { + diceVal.toReroll = false; + diceResult.toReroll = false; + } + + diceVal.selectedResults += diceResult.active ? -1 : 1; + diceResult.active = !diceResult.active; + + this.render(); + } + + static async #doReroll() { + const toReroll = this.getRerollDice().map(x => { + const { type, part, dice, result } = x; + const diceData = this.damage[type][part][dice].results[result]; + return { + ...diceData, + dice: this.damage[type][part][dice].dice, + typeKey: type, + partKey: part, + diceKey: dice, + resultsIndex: result + }; + }); + + const roll = await new Roll(toReroll.map(x => `1${x.dice}`).join(' + ')).evaluate(); + + if (game.modules.get('dice-so-nice')?.active) { + const diceSoNiceRoll = { + _evaluated: true, + dice: roll.dice, + options: { appearance: {} } + }; + + await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true); + } + + toReroll.forEach((data, index) => { + const { typeKey, partKey, diceKey, resultsIndex } = data; + const rerolledDice = roll.dice[index]; + + const dice = this.damage[typeKey][partKey][diceKey]; + dice.toReroll = false; + dice.results[resultsIndex].active = false; + dice.results[resultsIndex].discarded = true; + dice.results[resultsIndex].toReroll = false; + dice.results.splice(dice.results.length, 0, { + ...rerolledDice.results[0], + toReroll: false, + selected: true + }); + }); + + this.render(); + } +} diff --git a/module/applications/dialogs/rerollDialog.mjs b/module/applications/dialogs/rerollDialog.mjs new file mode 100644 index 00000000..cae4e53a --- /dev/null +++ b/module/applications/dialogs/rerollDialog.mjs @@ -0,0 +1,279 @@ +const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; + +export default class RerollDialog extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(message, options = {}) { + super(options); + + this.message = message; + this.damage = Object.keys(message.system.damage).reduce((acc, typeKey) => { + const type = message.system.damage[typeKey]; + acc[typeKey] = Object.keys(type.parts).reduce((acc, partKey) => { + const part = type.parts[partKey]; + acc[partKey] = Object.keys(part.dice).reduce((acc, diceKey) => { + const dice = part.dice[diceKey]; + const activeResults = dice.results.filter(x => x.active); + acc[diceKey] = { + dice: dice.dice, + selectedResults: activeResults.length, + maxSelected: activeResults.length, + results: activeResults.map(x => ({ ...x, selected: true })) + }; + + return acc; + }, {}); + + return acc; + }, {}); + + return acc; + }, {}); + } + + static DEFAULT_OPTIONS = { + id: 'reroll-dialog', + classes: ['daggerheart', 'dialog', 'dh-style', 'views', 'reroll-dialog'], + window: { + icon: 'fa-solid fa-dice' + }, + actions: { + toggleResult: RerollDialog.#toggleResult, + selectRoll: RerollDialog.#selectRoll, + doReroll: RerollDialog.#doReroll, + save: RerollDialog.#save + } + }; + + /** @override */ + static PARTS = { + main: { + id: 'main', + template: 'systems/daggerheart/templates/dialogs/rerollDialog/main.hbs' + }, + footer: { + id: 'footer', + template: 'systems/daggerheart/templates/dialogs/rerollDialog/footer.hbs' + } + }; + + get title() { + return game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.title'); + } + + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + + htmlElement.querySelectorAll('.to-reroll-input').forEach(element => { + element.addEventListener('change', this.toggleDice.bind(this)); + }); + } + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.damage = this.damage; + context.disabledReroll = !this.getRerollDice().length; + context.saveDisabled = !this.isSelectionDone(); + + return context; + } + + static async #save() { + const update = { + 'system.damage': Object.keys(this.damage).reduce((acc, typeKey) => { + const type = this.damage[typeKey]; + let typeTotal = 0; + const messageType = this.message.system.damage[typeKey]; + const parts = Object.keys(type).map(partKey => { + const part = type[partKey]; + const messagePart = messageType.parts[partKey]; + let partTotal = messagePart.modifierTotal; + const dice = Object.keys(part).map(diceKey => { + const dice = part[diceKey]; + const total = dice.results.reduce((acc, result) => { + if (result.active) acc += result.result; + return acc; + }, 0); + partTotal += total; + const messageDice = messagePart.dice[diceKey]; + return { + ...messageDice, + total: total, + results: dice.results.map(x => ({ + ...x, + hasRerolls: dice.results.length > 1 + })) + }; + }); + + typeTotal += partTotal; + return { + ...messagePart, + total: partTotal, + dice: dice + }; + }); + + acc[typeKey] = { + ...messageType, + total: typeTotal, + parts: parts + }; + + return acc; + }, {}) + }; + await this.message.update(update); + await this.close(); + } + + getRerollDice() { + const rerollDice = []; + Object.keys(this.damage).forEach(typeKey => { + const type = this.damage[typeKey]; + Object.keys(type).forEach(partKey => { + const part = type[partKey]; + Object.keys(part).forEach(diceKey => { + const dice = part[diceKey]; + Object.keys(dice.results).forEach(resultKey => { + const result = dice.results[resultKey]; + if (result.toReroll) { + rerollDice.push({ + ...result, + dice: dice.dice, + type: typeKey, + part: partKey, + dice: diceKey, + result: resultKey + }); + } + }); + }); + }); + }); + + return rerollDice; + } + + isSelectionDone() { + const diceFinishedData = []; + Object.keys(this.damage).forEach(typeKey => { + const type = this.damage[typeKey]; + Object.keys(type).forEach(partKey => { + const part = type[partKey]; + Object.keys(part).forEach(diceKey => { + const dice = part[diceKey]; + const selected = dice.results.reduce((acc, result) => acc + (result.active ? 1 : 0), 0); + diceFinishedData.push(selected === dice.maxSelected); + }); + }); + }); + + return diceFinishedData.every(x => x); + } + + toggleDice(event) { + const target = event.target; + const { type, part, dice } = target.dataset; + const toggleDice = this.damage[type][part][dice]; + + const existingDiceRerolls = this.getRerollDice().filter( + x => x.type === type && x.part === part && x.dice === dice + ); + + const allRerolled = existingDiceRerolls.length === toggleDice.results.filter(x => x.active).length; + + toggleDice.toReroll = !allRerolled; + toggleDice.results.forEach(result => { + if (result.active) { + result.toReroll = !allRerolled; + } + }); + + this.render(); + } + + static #toggleResult(event) { + event.stopPropagation(); + + const target = event.target.closest('.to-reroll-result'); + const { type, part, dice, result } = target.dataset; + const toggleDice = this.damage[type][part][dice]; + const toggleResult = toggleDice.results[result]; + toggleResult.toReroll = !toggleResult.toReroll; + + const existingDiceRerolls = this.getRerollDice().filter( + x => x.type === type && x.part === part && x.dice === dice + ); + + const allToReroll = existingDiceRerolls.length === toggleDice.results.length; + toggleDice.toReroll = allToReroll; + + this.render(); + } + + static async #selectRoll(_, button) { + const { type, part, dice, result } = button.dataset; + + const diceVal = this.damage[type][part][dice]; + const diceResult = diceVal.results[result]; + if (!diceResult.active && diceVal.results.filter(x => x.active).length === diceVal.maxSelected) { + return ui.notifications.warn( + game.i18n.localize('DAGGERHEART.APPLICATIONS.RerollDialog.deselectDiceNotification') + ); + } + + if (diceResult.active) { + diceVal.toReroll = false; + diceResult.toReroll = false; + } + + diceVal.selectedResults += diceResult.active ? -1 : 1; + diceResult.active = !diceResult.active; + + this.render(); + } + + static async #doReroll() { + const toReroll = this.getRerollDice().map(x => { + const { type, part, dice, result } = x; + const diceData = this.damage[type][part][dice].results[result]; + return { + ...diceData, + dice: this.damage[type][part][dice].dice, + typeKey: type, + partKey: part, + diceKey: dice, + resultsIndex: result + }; + }); + + const roll = await new Roll(toReroll.map(x => `1${x.dice}`).join(' + ')).evaluate(); + + if (game.modules.get('dice-so-nice')?.active) { + const diceSoNiceRoll = { + _evaluated: true, + dice: roll.dice, + options: { appearance: {} } + }; + + await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true); + } + + toReroll.forEach((data, index) => { + const { typeKey, partKey, diceKey, resultsIndex } = data; + const rerolledDice = roll.dice[index]; + + const dice = this.damage[typeKey][partKey][diceKey]; + dice.toReroll = false; + dice.results[resultsIndex].active = false; + dice.results[resultsIndex].discarded = true; + dice.results[resultsIndex].toReroll = false; + dice.results.splice(dice.results.length, 0, { + ...rerolledDice.results[0], + toReroll: false, + selected: true + }); + }); + + this.render(); + } +} diff --git a/module/applications/settings/appearanceSettings.mjs b/module/applications/settings/appearanceSettings.mjs index 491c9799..f0310477 100644 --- a/module/applications/settings/appearanceSettings.mjs +++ b/module/applications/settings/appearanceSettings.mjs @@ -1,4 +1,5 @@ import DhAppearance from '../../data/settings/Appearance.mjs'; +import { getDiceSoNicePreset } from '../../config/generalConfig.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -25,7 +26,8 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App }, actions: { reset: this.reset, - save: this.save + save: this.save, + preview: this.preview }, form: { handler: this.updateData, submitOnChange: true } }; @@ -89,6 +91,22 @@ export default class DHAppearanceSettings extends HandlebarsApplicationMixin(App this.render(); } + static async preview() { + const source = this.settings._source.diceSoNice[this.tabGroups.diceSoNice]; + let faces = 'd12'; + switch (this.tabGroups.diceSoNice) { + case 'advantage': + case 'disadvantage': + faces = 'd6'; + } + const preset = await getDiceSoNicePreset(source, faces); + const diceSoNiceRoll = await new Roll(`1${faces}`).evaluate(); + diceSoNiceRoll.dice[0].options.appearance = preset.appearance; + diceSoNiceRoll.dice[0].options.modelFile = preset.modelFile; + + await game.dice3d.showForRoll(diceSoNiceRoll, game.user, false); + } + static async reset() { this.settings = new DhAppearance(); this.render(); diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index e93de7ed..ae597270 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -645,18 +645,17 @@ export default class CharacterSheet extends DHBaseActorSheet { } async consumeResource(costs) { - if(!costs?.length) return; + if (!costs?.length) return; const usefulResources = foundry.utils.deepClone(this.actor.system.resources); - const resources = game.system.api.fields.ActionFields.CostField.getRealCosts(costs) - .map(c => { - const resource = usefulResources[c.key]; - return { - key: c.key, - value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1), - target: resource.target, - keyIsID: resource.keyIsID - }; - }); + const resources = game.system.api.fields.ActionFields.CostField.getRealCosts(costs).map(c => { + 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); } diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index 7f646460..1a7e19c1 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -333,7 +333,7 @@ export default function DHApplicationMixin(Base) { } ]; - if (usable) + if (usable) { options.unshift({ name: 'DAGGERHEART.GENERAL.damage', icon: 'fa-solid fa-explosion', @@ -341,20 +341,6 @@ export default function DHApplicationMixin(Base) { const doc = getDocFromElementSync(target); return doc?.system?.attack?.damage.parts.length || doc?.damage?.parts.length; }, - callback: async (target, event) => { - const doc = await getDocFromElement(target), - action = doc?.system?.attack ?? doc; - return action && action.use(event, { byPassRoll: true }) - } - }); - - options.unshift({ - name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem', - icon: 'fa-solid fa-burst', - condition: target => { - const doc = getDocFromElementSync(target); - return doc?.system?.attack?.damage.parts.length || doc?.damage?.parts.length; - }, callback: async (target, event) => { const doc = await getDocFromElement(target), action = doc?.system?.attack ?? doc; @@ -362,15 +348,16 @@ export default function DHApplicationMixin(Base) { } }); - options.unshift({ - name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem', - icon: 'fa-solid fa-burst', - condition: target => { - const doc = getDocFromElementSync(target); - return doc && !(doc.type === 'domainCard' && doc.system.inVault); - }, - callback: async (target, event) => (await getDocFromElement(target)).use(event) - }); + options.unshift({ + name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem', + icon: 'fa-solid fa-burst', + condition: target => { + const doc = getDocFromElementSync(target); + return doc && !(doc.type === 'domainCard' && doc.system.inVault); + }, + callback: async (target, event) => (await getDocFromElement(target)).use(event) + }); + } if (toChat) options.push({ diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index 79d5e468..8b4b12d3 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -20,6 +20,40 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo classes: ['daggerheart'] }; + _getEntryContextOptions() { + return [ + ...super._getEntryContextOptions(), + // { + // name: 'Reroll', + // icon: '', + // condition: li => { + // const message = game.messages.get(li.dataset.messageId); + + // return (game.user.isGM || message.isAuthor) && message.rolls.length > 0; + // }, + // callback: li => { + // const message = game.messages.get(li.dataset.messageId); + // new game.system.api.applications.dialogs.RerollDialog(message).render({ force: true }); + // } + // }, + { + name: 'Reroll Damage', + icon: '', + condition: li => { + const message = game.messages.get(li.dataset.messageId); + const hasRolledDamage = message.system.hasDamage + ? Object.keys(message.system.damage).length > 0 + : false; + return (game.user.isGM || message.isAuthor) && hasRolledDamage; + }, + callback: li => { + const message = game.messages.get(li.dataset.messageId); + new game.system.api.applications.dialogs.RerollDamageDialog(message).render({ force: true }); + } + } + ]; + } + addChatListeners = async (app, html, data) => { html.querySelectorAll('.duality-action-damage').forEach(element => element.addEventListener('click', event => this.onRollDamage(event, data.message)) @@ -193,19 +227,28 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo } const target = event.target.closest('[data-die-index]'); - let originalRoll_parsed = message.rolls.map(roll => JSON.parse(roll))[0]; - const rollClass = - game.system.api.dice[ - message.type === 'dualityRoll' ? 'DualityRoll' : target.dataset.type === 'damage' ? 'DHRoll' : 'D20Roll' - ]; - if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice }); + if (target.dataset.type === 'damage') { + game.system.api.dice.DamageRoll.reroll(target, message); + } else { + let originalRoll_parsed = message.rolls.map(roll => JSON.parse(roll))[0]; + const rollClass = + game.system.api.dice[ + message.type === 'dualityRoll' + ? 'DualityRoll' + : target.dataset.type === 'damage' + ? 'DHRoll' + : 'D20Roll' + ]; - const { newRoll, parsedRoll } = await rollClass.reroll(originalRoll_parsed, target, message); + if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice }); - await game.messages.get(message._id).update({ - 'system.roll': newRoll, - 'rolls': [parsedRoll] - }); + const { newRoll, parsedRoll } = await rollClass.reroll(originalRoll_parsed, target, message); + + await game.messages.get(message._id).update({ + 'system.roll': newRoll, + 'rolls': [parsedRoll] + }); + } } } diff --git a/module/applications/ui/itemBrowser.mjs b/module/applications/ui/itemBrowser.mjs index fabb354b..a15e0b65 100644 --- a/module/applications/ui/itemBrowser.mjs +++ b/module/applications/ui/itemBrowser.mjs @@ -17,7 +17,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { this.config = CONFIG.DH.ITEMBROWSER.compendiumConfig; this.presets = options.presets; - if(this.presets?.compendium && this.presets?.folder) + if (this.presets?.compendium && this.presets?.folder) ItemBrowser.selectFolder.call(this, null, null, this.presets.compendium, this.presets.folder); } @@ -26,7 +26,6 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { id: 'itemBrowser', classes: ['daggerheart', 'dh-style', 'dialog', 'compendium-browser'], tag: 'div', - // title: 'Item Browser', window: { frame: true, title: 'Compendium Browser', @@ -41,9 +40,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { sortList: this.sortList }, position: { - top: 330, - left: 120, - width: 800, + left: 100, + width: 850, height: 600 } }; @@ -88,16 +86,14 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { /** @inheritDoc */ async _preFirstRender(context, options) { - if(context.presets?.render?.noFolder || context.presets?.render?.lite) - options.position.width = 600; + if (context.presets?.render?.noFolder || context.presets?.render?.lite) options.position.width = 600; await super._preFirstRender(context, options); } /** @inheritDoc */ async _preRender(context, options) { - - if(context.presets?.render?.noFolder || context.presets?.render?.lite) + if (context.presets?.render?.noFolder || context.presets?.render?.lite) options.parts.splice(options.parts.indexOf('sidebar'), 1); await super._preRender(context, options); @@ -111,17 +107,16 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { this._createFilterInputs(); this._createDragProcess(); - if(context.presets?.render?.lite) - this.element.classList.add('lite'); + if (context.presets?.render?.lite) this.element.classList.add('lite'); - if(context.presets?.render?.noFolder) - this.element.classList.add('no-folder'); + if (context.presets?.render?.noFolder) this.element.classList.add('no-folder'); - if(context.presets?.render?.noFilter) - this.element.classList.add('no-filter'); + if (context.presets?.render?.noFilter) this.element.classList.add('no-filter'); - if(this.presets?.filter) { - Object.entries(this.presets.filter).forEach(([k,v]) => this.fieldFilter.find(c => c.name === k).value = v.value); + if (this.presets?.filter) { + Object.entries(this.presets.filter).forEach( + ([k, v]) => (this.fieldFilter.find(c => c.name === k).value = v.value) + ); await this._onInputFilterBrowser(); } } @@ -203,6 +198,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { formatLabel(item, field) { const property = foundry.utils.getProperty(item, field.key); + if (Array.isArray(property)) property.join(', '); if (typeof field.format !== 'function') return property ?? '-'; return field.format(property); } @@ -320,19 +316,18 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { async _onInputFilterBrowser(event) { this.#filteredItems.browser.input.clear(); - if(event) this.fieldFilter.find(f => f.name === event.target.name).value = event.target.value; + if (event) this.fieldFilter.find(f => f.name === event.target.name).value = event.target.value; for (const li of this.element.querySelectorAll('.item-container')) { const itemUUID = li.dataset.itemUuid, item = this.items.find(i => i.uuid === itemUUID); - if(!item) continue; + if (!item) continue; const matchesMenu = this.fieldFilter.length === 0 || - this.fieldFilter.every(f => ( - !f.value && f.value !== false) || - ItemBrowser.evaluateFilter(item, this.createFilterData(f)) + this.fieldFilter.every( + f => (!f.value && f.value !== false) || ItemBrowser.evaluateFilter(item, this.createFilterData(f)) ); if (matchesMenu) this.#filteredItems.browser.input.add(item.id); @@ -350,11 +345,11 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { let docValue = foundry.utils.getProperty(obj, filter.field); let filterValue = filter.value; switch (filter.operator) { - case "contains2": + case 'contains2': filterValue = Array.isArray(filterValue) ? filterValue : [filterValue]; docValue = Array.isArray(docValue) ? docValue : [docValue]; return docValue.some(dv => filterValue.includes(dv)); - case "contains3": + case 'contains3': return docValue.some(f => f.value === filterValue); default: return foundry.applications.ux.SearchFilter.evaluateFilter(obj, filter); @@ -378,24 +373,27 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { this.render({ force: true }); } - static getFolderConfig(folder, property = "columns") { - if(!folder) return []; + static getFolderConfig(folder, property = 'columns') { + if (!folder) return []; return folder[property] ?? CONFIG.DH.ITEMBROWSER.typeConfig[folder.listType]?.[property] ?? []; } static sortList(_, target) { const key = target.dataset.sortKey, - type = !target.dataset.sortType || target.dataset.sortType === "DESC" ? "ASC" : "DESC", - itemListContainer = target.closest(".compendium-results").querySelector(".item-list"), - itemList = itemListContainer.querySelectorAll(".item-container"); + type = !target.dataset.sortType || target.dataset.sortType === 'DESC' ? 'ASC' : 'DESC', + itemListContainer = target.closest('.compendium-results').querySelector('.item-list'), + itemList = itemListContainer.querySelectorAll('.item-container'); - target.closest(".item-list-header").querySelectorAll('[data-sort-key]').forEach(b => b.dataset.sortType = ""); + target + .closest('.item-list-header') + .querySelectorAll('[data-sort-key]') + .forEach(b => (b.dataset.sortType = '')); target.dataset.sortType = type; const newOrder = [...itemList].reverse().sort((a, b) => { const aProp = a.querySelector(`[data-item-key="${key}"]`), - bProp = b.querySelector(`[data-item-key="${key}"]`) - if(type === "DESC") { + bProp = b.querySelector(`[data-item-key="${key}"]`); + if (type === 'DESC') { return aProp.innerText < bProp.innerText ? 1 : -1; } else { return aProp.innerText > bProp.innerText ? 1 : -1; diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 7785b6a6..133dd09e 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -494,40 +494,41 @@ export const diceSetNumbers = { flat: 'Flat' }; -export const getDiceSoNicePresets = async (hopeFaces, fearFaces, advantageFaces = 'd6', disadvantageFaces = 'd6') => { - const { diceSoNice } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance); - const getPreset = async (type, faces) => { - const system = game.dice3d.DiceFactory.systems.get(type.system).dice.get(faces); - if (!system) { - ui.notifications.error( - game.i18n.format('DAGGERHEART.UI.Notifications.noDiceSystem', { - system: game.dice3d.DiceFactory.systems.get(type.system).name, - faces: faces - }) - ); - return; - } +export const getDiceSoNicePreset = async (type, faces) => { + const system = game.dice3d.DiceFactory.systems.get(type.system).dice.get(faces); + if (!system) { + ui.notifications.error( + game.i18n.format('DAGGERHEART.UI.Notifications.noDiceSystem', { + system: game.dice3d.DiceFactory.systems.get(type.system).name, + faces: faces + }) + ); + return; + } - if (system.modelFile && !system.modelLoaded) { - await system.loadModel(game.dice3d.DiceFactory.loaderGLTF); - } else { - await system.loadTextures(); - } - - return { - modelFile: system.modelFile, - appearance: { - ...system.appearance, - ...type - } - }; - }; + if (system.modelFile && !system.modelLoaded) { + await system.loadModel(game.dice3d.DiceFactory.loaderGLTF); + } else { + await system.loadTextures(); + } return { - hope: await getPreset(diceSoNice.hope, hopeFaces), - fear: await getPreset(diceSoNice.fear, fearFaces), - advantage: await getPreset(diceSoNice.advantage, advantageFaces), - disadvantage: await getPreset(diceSoNice.disadvantage, disadvantageFaces) + modelFile: system.modelFile, + appearance: { + ...system.appearance, + ...type + } + }; +}; + +export const getDiceSoNicePresets = async (hopeFaces, fearFaces, advantageFaces = 'd6', disadvantageFaces = 'd6') => { + const { diceSoNice } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance); + + return { + hope: await getDiceSoNicePreset(diceSoNice.hope, hopeFaces), + fear: await getDiceSoNicePreset(diceSoNice.fear, fearFaces), + advantage: await getDiceSoNicePreset(diceSoNice.advantage, advantageFaces), + disadvantage: await getDiceSoNicePreset(diceSoNice.disadvantage, disadvantageFaces) }; }; diff --git a/module/config/itemConfig.mjs b/module/config/itemConfig.mjs index e9d8de4c..296dc927 100644 --- a/module/config/itemConfig.mjs +++ b/module/config/itemConfig.mjs @@ -1040,16 +1040,6 @@ export const weaponFeatures = { key: 'system.evasion', mode: 2, value: '-1' - }, - { - key: 'system.bonuses.damage.primaryWeapon.extraDice', - mode: 2, - value: '1' - }, - { - key: 'system.rules.weapon.dropLowestDamageDice', - mode: 5, - value: '1' } ] } @@ -1166,18 +1156,7 @@ export const weaponFeatures = { name: 'DAGGERHEART.CONFIG.WeaponFeature.powerful.effects.powerful.name', description: 'DAGGERHEART.CONFIG.WeaponFeature.powerful.effects.powerful.description', img: 'icons/magic/control/buff-flight-wings-runes-red-yellow.webp', - changes: [ - { - key: 'system.bonuses.damage.primaryWeapon.extraDice', - mode: 2, - value: '1' - }, - { - key: 'system.rules.weapon.dropLowestDamageDice', - mode: 5, - value: '1' - } - ] + changes: [] } ] }, @@ -1301,13 +1280,7 @@ export const weaponFeatures = { name: 'DAGGERHEART.CONFIG.WeaponFeature.selfCorrecting.effects.selfCorrecting.name', description: 'DAGGERHEART.CONFIG.WeaponFeature.selfCorrecting.effects.selfCorrecting.description', img: 'icons/weapons/ammunition/arrow-broadhead-glowing-orange.webp', - changes: [ - { - key: 'system.rules.damage.flipMinDiceValue', - mode: 5, - value: 1 - } - ] + changes: [] } ] }, @@ -1319,13 +1292,7 @@ export const weaponFeatures = { name: 'DAGGERHEART.CONFIG.WeaponFeature.serrated.effects.serrated.name', description: 'DAGGERHEART.CONFIG.WeaponFeature.serrated.effects.serrated.description', img: 'icons/weapons/ammunition/arrow-broadhead-glowing-orange.webp', - changes: [ - { - key: 'system.rules.damage.flipMinDiceValue', - mode: 5, - value: 1 - } - ] + changes: [] } ] }, diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index 954e5328..ab515e88 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -146,7 +146,6 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel else if (this.hasSave || this.hasEffect) { const roll = new CONFIG.Dice.daggerheart.DHRoll(''); roll._evaluated = true; - if(config.hasTarget) config.targetSelection = config.targets.length > 0; await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config); } } @@ -225,17 +224,20 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel .filter( c => (!successCost && (!c.consumeOnSuccess || config.roll?.success)) || - (successCost && c.consumeOnSuccess) + (successCost && c.consumeOnSuccess) ) - .map(c => { + .reduce((a, c) => { const resource = usefulResources[c.key]; - return { - key: c.key, - value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1), - target: resource.target, - keyIsID: resource.keyIsID - }; - }); + if (resource) { + a.push({ + key: c.key, + value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1), + target: resource.target, + keyIsID: resource.keyIsID + }); + return a; + } + }, []); await (this.actor.system.partner ?? this.actor).modifyResource(resources); if ( @@ -245,9 +247,9 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel ) this.update({ 'uses.value': this.uses.value + 1 }); - if(config.roll?.success || successCost) { + if (config.roll?.success || successCost) { setTimeout(() => { - (config.message ?? config.parent).update({'system.successConsumed': true}) + (config.message ?? config.parent).update({ 'system.successConsumed': true }); }, 50); } } @@ -369,11 +371,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel async updateChatMessage(message, targetId, changes, chain = true) { setTimeout(async () => { const chatMessage = ui.chat.collection.get(message._id); - + await chatMessage.update({ flags: { [game.system.id]: { - "reactionRolls": { + reactionRolls: { [targetId]: changes } } diff --git a/module/data/action/damageAction.mjs b/module/data/action/damageAction.mjs index 88eec481..1627f8e8 100644 --- a/module/data/action/damageAction.mjs +++ b/module/data/action/damageAction.mjs @@ -49,8 +49,7 @@ export default class DHDamageAction extends DHBaseAction { ...systemData, roll: formulas, dialog: {}, - data: this.getRollData(), - targetSelection: systemData.targets.length > 0 + data: this.getRollData() }; if (this.hasSave) config.onSave = this.save.damageMod; if (data.system) { diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index d4544d2c..c40e7e5d 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -287,18 +287,6 @@ export default class DhCharacter extends BaseDataActor { }) }) }), - weapon: new fields.SchemaField({ - /* Unimplemented - -> Should remove the lowest damage dice from weapon damage - -> Reflect this in the chat message somehow so players get feedback that their choice is helping them. - */ - dropLowestDamageDice: new fields.BooleanField({ initial: false }), - /* Unimplemented - -> Should flip any lowest possible dice rolls for weapon damage to highest - -> Reflect this in the chat message somehow so players get feedback that their choice is helping them. - */ - flipMinDiceValue: new fields.BooleanField({ intial: false }) - }), runeWard: new fields.BooleanField({ initial: false }), burden: new fields.SchemaField({ ignore: new fields.BooleanField() diff --git a/module/data/chat-message/adversaryRoll.mjs b/module/data/chat-message/adversaryRoll.mjs index 5e1835c0..337acf5b 100644 --- a/module/data/chat-message/adversaryRoll.mjs +++ b/module/data/chat-message/adversaryRoll.mjs @@ -18,14 +18,11 @@ const targetsField = () => ); export default class DHActorRoll extends foundry.abstract.TypeDataModel { - targetHook = null; - static defineSchema() { return { title: new fields.StringField(), roll: new fields.ObjectField(), targets: targetsField(), - targetSelection: new fields.BooleanField({ initial: false }), hasRoll: new fields.BooleanField({ initial: false }), hasDamage: new fields.BooleanField({ initial: false }), hasHealing: new fields.BooleanField({ initial: false }), @@ -63,42 +60,49 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { } get targetMode() { - return this.targetSelection; + return this.parent.targetSelection; } set targetMode(mode) { - this.targetSelection = mode; + if (!this.parent.isAuthor) return; + this.parent.targetSelection = mode; this.registerTargetHook(); this.updateTargets(); } get hitTargets() { - return this.currentTargets.filter(t => t.hit || !this.hasRoll || !this.targetSelection); + return this.currentTargets.filter(t => t.hit || !this.hasRoll || !this.targetMode); } async updateTargets() { - if(!ui.chat.collection.get(this.parent.id)) return; + if (!ui.chat.collection.get(this.parent.id)) return; let targets; - if(this.targetSelection) - targets = this.targets; + if (this.targetMode) targets = this.targets; else - targets = Array.from(game.user.targets).map(t => game.system.api.fields.ActionFields.TargetField.formatTarget(t)); - - this.parent.setFlag(game.system.id, "targets", targets); - await this.parent.updateSource({ - system: { - targetSelection: this.targetSelection + targets = Array.from(game.user.targets).map(t => + game.system.api.fields.ActionFields.TargetField.formatTarget(t) + ); + + await this.parent.update({ + flags: { + [game.system.id]: { + targets: targets, + targetMode: this.targetMode + } } }); } registerTargetHook() { - if(!this.parent.isAuthor) return; - if(this.targetSelection && this.targetHook !== null) { - Hooks.off("targetToken", this.targetHook); - this.targetHook = null; - } else if (!this.targetSelection && this.targetHook === null) { - this.targetHook = Hooks.on('targetToken', foundry.utils.debounce(this.updateTargets.bind(this), 50)); + if (!this.parent.isAuthor) return; + if (this.targetMode && this.parent.targetHook !== null) { + Hooks.off('targetToken', this.parent.targetHook); + return (this.parent.targetHook = null); + } else if (!this.targetMode && this.parent.targetHook === null) { + return (this.parent.targetHook = Hooks.on( + 'targetToken', + foundry.utils.debounce(this.updateTargets.bind(this), 50) + )); } } @@ -106,14 +110,16 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { if (this.hasTarget) { this.hasHitTarget = this.targets.filter(t => t.hit === true).length > 0; this.currentTargets = this.getTargetList(); - this. registerTargetHook(); - - if(this.targetSelection === true && this.hasRoll) { - this.targetShort = this.targets.reduce((a,c) => { - if(c.hit) a.hit += 1; - else a.miss += 1; - return a; - }, {hit: 0, miss: 0}) + + if (this.targetMode === true && this.hasRoll) { + this.targetShort = this.targets.reduce( + (a, c) => { + if (c.hit) a.hit += 1; + else a.miss += 1; + return a; + }, + { hit: 0, miss: 0 } + ); } if (this.hasSave) this.setPendingSaves(); } @@ -123,13 +129,16 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { } getTargetList() { - const targets = this.targetSelection && this.parent.isAuthor ? this.targets : (this.parent.getFlag(game.system.id, "targets") ?? this.targets), - reactionRolls = this.parent.getFlag(game.system.id, "reactionRolls"); + const targets = + this.targetMode && this.parent.isAuthor + ? this.targets + : (this.parent.getFlag(game.system.id, 'targets') ?? this.targets), + reactionRolls = this.parent.getFlag(game.system.id, 'reactionRolls'); - if(reactionRolls) { + if (reactionRolls) { Object.entries(reactionRolls).forEach(([k, r]) => { const target = targets.find(t => t.id === k); - if(target) target.saved = r; + if (target) target.saved = r; }); } @@ -137,7 +146,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { } setPendingSaves() { - this.pendingSaves = this.targetSelection + this.pendingSaves = this.targetMode ? this.targets.filter(target => target.hit && target.saved.success === null).length > 0 : this.currentTargets.filter(target => target.saved.success === null).length > 0; } diff --git a/module/data/fields/action/costField.mjs b/module/data/fields/action/costField.mjs index 4972a9a0..f4d942b1 100644 --- a/module/data/fields/action/costField.mjs +++ b/module/data/fields/action/costField.mjs @@ -12,7 +12,10 @@ export default class CostField extends fields.ArrayField { value: new fields.NumberField({ nullable: true, initial: 1, min: 0 }), scalable: new fields.BooleanField({ initial: false }), step: new fields.NumberField({ nullable: true, initial: null }), - consumeOnSuccess: new fields.BooleanField({ initial: false, label: "DAGGERHEART.ACTIONS.Settings.consumeOnSuccess.label" }) + consumeOnSuccess: new fields.BooleanField({ + initial: false, + label: 'DAGGERHEART.ACTIONS.Settings.consumeOnSuccess.label' + }) }); super(element, options, context); } @@ -47,7 +50,7 @@ export default class CostField extends fields.ArrayField { static hasCost(costs) { const realCosts = CostField.getRealCosts.call(this, costs), hasFearCost = realCosts.findIndex(c => c.key === 'fear'); - + if (hasFearCost > -1) { const fearCost = realCosts.splice(hasFearCost, 1)[0]; if ( @@ -72,7 +75,8 @@ export default class CostField extends fields.ArrayField { static getResources(costs) { const actorResources = foundry.utils.deepClone(this.actor.system.resources); - if(this.actor.system.partner) actorResources.hope = foundry.utils.deepClone(this.actor.system.partner.system.resources.hope); + if (this.actor.system.partner) + actorResources.hope = foundry.utils.deepClone(this.actor.system.partner.system.resources.hope); const itemResources = {}; for (let itemResource of costs) { if (itemResource.keyIsID) { @@ -92,9 +96,9 @@ export default class CostField extends fields.ArrayField { static getRealCosts(costs) { const realCosts = costs?.length ? costs.filter(c => c.enabled) : []; let mergedCosts = []; - realCosts.forEach(c => { + realCosts.forEach(c => { const getCost = Object.values(mergedCosts).find(gc => gc.key === c.key); - if(getCost) getCost.total += c.total; + if (getCost) getCost.total += c.total; else mergedCosts.push(c); }); return mergedCosts; diff --git a/module/data/fields/actionField.mjs b/module/data/fields/actionField.mjs index 9300a02e..8d865562 100644 --- a/module/data/fields/actionField.mjs +++ b/module/data/fields/actionField.mjs @@ -229,7 +229,7 @@ export function ActionMixin(Base) { } return this.inCollection - ? foundry.utils.getProperty(result, basePath).get(this.id) + ? foundry.utils.getProperty(result, basePath)?.get(this.id) : foundry.utils.getProperty(result, basePath); } diff --git a/module/dice/d20Roll.mjs b/module/dice/d20Roll.mjs index d636fd27..63d84744 100644 --- a/module/dice/d20Roll.mjs +++ b/module/dice/d20Roll.mjs @@ -141,15 +141,13 @@ export default class D20Roll extends DHRoll { data.type = config.roll?.type; data.difficulty = config.roll.difficulty; if (config.targets?.length) { - config.targetSelection = true; config.targets.forEach(target => { const difficulty = config.roll.difficulty ?? target.difficulty ?? target.evasion; target.hit = roll.isCritical || roll.total >= difficulty; }); - data.success = config.targets.some(target => target.hit) - } else if (config.roll.difficulty) - data.success = roll.isCritical || roll.total >= config.roll.difficulty; - + data.success = config.targets.some(target => target.hit); + } else if (config.roll.difficulty) data.success = roll.isCritical || roll.total >= config.roll.difficulty; + data.advantage = { type: config.roll.advantage, dice: roll.dAdvantage?.denomination, diff --git a/module/dice/damageRoll.mjs b/module/dice/damageRoll.mjs index 31458516..44794faa 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -15,7 +15,6 @@ export default class DamageRoll extends DHRoll { const parts = config.roll.map(r => this.postEvaluate(r)); config.damage = this.unifyDamageRoll(parts); - // config.targetSelection = config.targets?.length } static postEvaluate(roll, config = {}) { @@ -30,7 +29,9 @@ export default class DamageRoll extends DHRoll { } static async buildPost(roll, config, message) { - const chatMessage = config.source?.message ? ui.chat.collection.get(config.source.message) : getDocumentClass('ChatMessage').applyRollMode({}, config.rollMode); + const chatMessage = config.source?.message + ? ui.chat.collection.get(config.source.message) + : getDocumentClass('ChatMessage').applyRollMode({}, config.rollMode); if (game.modules.get('dice-so-nice')?.active) { const pool = foundry.dice.terms.PoolTerm.fromRolls( Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll)) @@ -102,14 +103,14 @@ export default class DamageRoll extends DHRoll { } constructFormula(config) { - this.options.roll.forEach(part => { + this.options.roll.forEach((part, index) => { part.roll = new Roll(Roll.replaceFormulaData(part.formula, config.data)); - this.constructFormulaPart(config, part); + this.constructFormulaPart(config, part, index); }); return this.options.roll; } - constructFormulaPart(config, part) { + constructFormulaPart(config, part, index) { part.roll.terms = Roll.parse(part.roll.formula, config.data); if (part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) { @@ -120,6 +121,14 @@ export default class DamageRoll extends DHRoll { }); } + /* To Remove When Reaction System */ + if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) { + for (const mod in config.modifiers) { + const modifier = config.modifiers[mod]; + if (modifier.beforeCrit === true && (modifier.enabled || modifier.value)) modifier.callback(part); + } + } + if (part.extraFormula) { part.roll.terms.push( new foundry.dice.terms.OperatorTerm({ operator: '+' }), @@ -132,6 +141,189 @@ export default class DamageRoll extends DHRoll { criticalBonus = tmpRoll.total - this.constructor.calculateTotalModifiers(tmpRoll); part.roll.terms.push(...this.formatModifier(criticalBonus)); } + + /* To Remove When Reaction System */ + if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) { + for (const mod in config.modifiers) { + const modifier = config.modifiers[mod]; + if (!modifier.beforeCrit && (modifier.enabled || modifier.value)) modifier.callback(part); + } + } + return (part.roll._formula = this.constructor.getFormula(part.roll.terms)); } + + /* To Remove When Reaction System */ + static temporaryModifierBuilder(config) { + const mods = {}; + if (config.data?.parent) { + if (config.data.parent.appliedEffects) { + // Bardic Rally + mods.rally = { + label: 'DAGGERHEART.CLASS.Feature.rallyDice', + values: config.data?.parent?.appliedEffects.reduce((a, c) => { + const change = c.changes.find(ch => ch.key === 'system.bonuses.rally'); + if (change) a.push({ value: c.id, label: change.value }); + return a; + }, []), + value: null, + beforeCrit: true, + callback: part => { + const rallyFaces = config.modifiers.rally.values.find( + r => r.value === config.modifiers.rally.value + )?.label; + part.roll.terms.push( + new foundry.dice.terms.OperatorTerm({ operator: '+' }), + ...this.parse(`1${rallyFaces}`) + ); + } + }; + } + + const item = config.data.parent.items?.get(config.source.item); + if (item) { + // Massive (Weapon Feature) + if (item.system.itemFeatures.find(f => f.value === 'massive')) + mods.massive = { + label: CONFIG.DH.ITEM.weaponFeatures.massive.label, + enabled: true, + callback: part => { + part.roll.terms[0].modifiers.push(`kh${part.roll.terms[0].number}`); + part.roll.terms[0].number += 1; + } + }; + + // Powerful (Weapon Feature) + if (item.system.itemFeatures.find(f => f.value === 'powerful')) + mods.powerful = { + label: CONFIG.DH.ITEM.weaponFeatures.powerful.label, + enabled: true, + callback: part => { + part.roll.terms[0].modifiers.push(`kh${part.roll.terms[0].number}`); + part.roll.terms[0].number += 1; + } + }; + + // Brutal (Weapon Feature) + if (item.system.itemFeatures.find(f => f.value === 'brutal')) + mods.brutal = { + label: CONFIG.DH.ITEM.weaponFeatures.brutal.label, + enabled: true, + beforeCrit: true, + callback: part => { + part.roll.terms[0].modifiers.push(`x${part.roll.terms[0].faces}`); + } + }; + + // Serrated (Weapon Feature) + if (item.system.itemFeatures.find(f => f.value === 'serrated')) + mods.serrated = { + label: CONFIG.DH.ITEM.weaponFeatures.serrated.label, + enabled: true, + callback: part => { + part.roll.terms[0].modifiers.push(`sc8`); + } + }; + + // Self-Correcting (Weapon Feature) + if (item.system.itemFeatures.find(f => f.value === 'selfCorrecting')) + mods.selfCorrecting = { + label: CONFIG.DH.ITEM.weaponFeatures.selfCorrecting.label, + enabled: true, + callback: part => { + part.roll.terms[0].modifiers.push(`sc6`); + } + }; + } + } + + config.modifiers = mods; + return mods; + } + + static async reroll(target, message) { + const { damageType, part, dice, result } = target.dataset; + const rollPart = message.system.damage[damageType].parts[part]; + + let diceIndex = 0; + let parsedRoll = game.system.api.dice.DamageRoll.fromData({ + ...rollPart.roll, + terms: rollPart.roll.terms.map(term => { + const isDie = term.class === 'Die'; + const fixedTerm = { + ...term, + ...(isDie ? { results: rollPart.dice[diceIndex].results } : {}) + }; + + if (isDie) diceIndex++; + return fixedTerm; + }), + class: 'DamageRoll', + evaluated: false + }); + + const parsedDiceTerms = Object.keys(parsedRoll.terms).reduce((acc, key) => { + const term = parsedRoll.terms[key]; + if (term instanceof CONFIG.Dice.termTypes.DiceTerm) acc[Object.keys(acc).length] = term; + return acc; + }, {}); + const term = parsedDiceTerms[dice]; + const termResult = parsedDiceTerms[dice].results[result]; + + const newIndex = parsedDiceTerms[dice].results.length; + await term.reroll(`/r1=${termResult.result}`); + + if (game.modules.get('dice-so-nice')?.active) { + const newResult = parsedDiceTerms[dice].results[newIndex]; + const diceSoNiceRoll = { + _evaluated: true, + dice: [ + new foundry.dice.terms.Die({ + ...term, + total: newResult.result, + faces: term._faces, + results: [newResult] + }) + ], + options: { appearance: {} } + }; + + await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true); + } + + await parsedRoll.evaluate(); + + const results = parsedRoll.dice[dice].results.map(result => ({ + ...result, + discarded: !result.active + })); + const newResult = results.splice(results.length - 1, 1); + results.splice(Number(result) + 1, 0, newResult[0]); + + const rerolledDice = parsedRoll.dice.map((x, index) => { + const isRerollDice = index === Number(dice); + if (!isRerollDice) return { ...x, dice: x.denomination }; + return { + dice: parsedRoll.dice[dice].denomination, + total: parsedRoll.dice[dice].total, + results: results.map(result => ({ + ...result, + hasRerolls: result.hasRerolls || isRerollDice + })) + }; + }); + + const updateMessage = game.messages.get(message._id); + await updateMessage.update({ + [`system.damage.${damageType}`]: { + ...updateMessage, + total: parsedRoll.total, + [`parts.${part}`]: { + ...rollPart, + total: parsedRoll.total, + dice: rerolledDice + } + } + }); + } } diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index 889b24b3..504a9c02 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -8,9 +8,7 @@ export default class DHRoll extends Roll { } get title() { - return game.i18n.localize( - "DAGGERHEART.GENERAL.Roll.basic" - ); + return game.i18n.localize('DAGGERHEART.GENERAL.Roll.basic'); } static messageType = 'adversaryRoll'; @@ -36,6 +34,8 @@ export default class DHRoll extends Roll { this.applyKeybindings(config); + this.temporaryModifierBuilder(config); + let roll = new this(config.roll.formula, config.data, config); if (config.dialog.configure !== false) { // Open Roll Dialog @@ -66,8 +66,7 @@ export default class DHRoll extends Roll { } // Create Chat Message - if (!config.source?.message) - config.message = await this.toMessage(roll, config); + if (!config.source?.message) config.message = await this.toMessage(roll, config); } static postEvaluate(roll, config = {}) { @@ -95,30 +94,30 @@ export default class DHRoll extends Roll { rolls: [roll] }; config.selectedRollMode ??= game.settings.get('core', 'rollMode'); - if(roll._evaluated) return await cls.create(msg, { rollMode: config.selectedRollMode }); + if (roll._evaluated) return await cls.create(msg, { rollMode: config.selectedRollMode }); return msg; } - + /** @inheritDoc */ - async render({flavor, template=this.constructor.CHAT_TEMPLATE, isPrivate=false, ...options}={}) { - if ( !this._evaluated ) return; - const chatData = await this._prepareChatRenderContext({flavor, isPrivate, ...options}); + async render({ flavor, template = this.constructor.CHAT_TEMPLATE, isPrivate = false, ...options } = {}) { + if (!this._evaluated) return; + const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options }); return foundry.applications.handlebars.renderTemplate(template, chatData); } - + /** @inheritDoc */ - async _prepareChatRenderContext({flavor, isPrivate=false, ...options}={}) { - if(isPrivate) { + async _prepareChatRenderContext({ flavor, isPrivate = false, ...options } = {}) { + if (isPrivate) { return { user: game.user.id, flavor: null, - title: "???", + title: '???', roll: { - total: "??" + total: '??' }, hasRoll: true, isPrivate - } + }; } else { options.message.system.user = game.user.id; return options.message.system; @@ -207,11 +206,15 @@ export default class DHRoll extends Roll { } return modifierTotal; } + + static temporaryModifierBuilder(config) { + return {}; + } } export const registerRollDiceHooks = () => { Hooks.on(`${CONFIG.DH.id}.postRollDuality`, async (config, message) => { - const hopeFearAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).hopeFear; + const hopeFearAutomation = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).hopeFear; if ( !config.source?.actor || (game.user.isGM ? !hopeFearAutomation.gm : !hopeFearAutomation.players) || diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index 030b4df2..35bae725 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -149,7 +149,7 @@ export default class DualityRoll extends D20Roll { } if (this.rallyFaces) this.terms.push( - new foundry.dice.terms.OperatorTerm({ operator: '+' }), + new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' }), new foundry.dice.terms.Die({ faces: this.rallyFaces }) ); } diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 910ec4f1..8fc9b74a 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -644,16 +644,23 @@ export default class DhpActor extends Actor { ); break; case 'armor': - updates.armor.resources['system.marks.value'] = Math.max( - Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore), - 0 - ); + if (this.system.armor?.system?.marks) { + 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 - ); + if (this.system.resources?.[r.key]) { + 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; } } diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index 06a9f147..3ade8c4a 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -1,10 +1,16 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { + targetHook = null; + targetSelection = null; + async renderHTML() { const actor = game.actors.get(this.speaker.actor); - const actorData = actor && this.isContentVisible ? actor : { - img: this.author.avatar ? this.author.avatar : 'icons/svg/mystery-man.svg', - name: '' - }; + const actorData = + actor && this.isContentVisible + ? actor + : { + img: this.author.avatar ? this.author.avatar : 'icons/svg/mystery-man.svg', + name: '' + }; /* We can change to fully implementing the renderHTML function if needed, instead of augmenting it. */ const html = await super.renderHTML({ actor: actorData, author: this.author }); @@ -14,6 +20,30 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { return html; } + /* -------------------------------------------- */ + + /** @inheritDoc */ + prepareData() { + if (this.isAuthor && this.targetSelection === null) this.targetSelection = this.system.targets?.length > 0; + super.prepareData(); + } + + /* -------------------------------------------- */ + + /** @inheritDoc */ + _onCreate(data, options, userId) { + super._onCreate(data, options, userId); + if (this.system.registerTargetHook) this.system.registerTargetHook(); + } + + /* -------------------------------------------- */ + + /** @inheritDoc */ + async _preDelete(options, user) { + if (this.targetHook !== null) Hooks.off('targetToken', this.targetHook); + return super._preDelete(options, user); + } + enrichChatMessage(html) { const elements = html.querySelectorAll('[data-perm-id]'); elements.forEach(e => { @@ -55,14 +85,14 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { element.addEventListener('mouseleave', this.unhoverTarget); element.addEventListener('click', this.clickTarget); }); - + html.querySelectorAll('.button-target-selection').forEach(element => { element.addEventListener('click', this.onTargetSelection.bind(this)); }); } getTargetList() { - const targets = this.system.hitTargets; + const targets = this.system.hitTargets ?? []; return targets.map(target => game.canvas.tokens.documentCollection.find(t => t.actor?.uuid === target.actorId)); } @@ -134,7 +164,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { } consumeOnSuccess() { - if (!this.system.successConsumed && !this.system.targetSelection) { + if (!this.system.successConsumed && !this.targetSelection) { const action = this.system.action; if (action) action.consume(this.system, true); } @@ -143,12 +173,12 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { hoverTarget(event) { event.stopPropagation(); const token = canvas.tokens.get(event.currentTarget.dataset.token); - if (!token?.controlled) token._onHoverIn(event, { hoverOutOthers: true }); + if (token && !token?.controlled) token._onHoverIn(event, { hoverOutOthers: true }); } unhoverTarget(event) { const token = canvas.tokens.get(event.currentTarget.dataset.token); - if (!token?.controlled) token._onHoverOut(event); + if (token && !token?.controlled) token._onHoverOut(event); } clickTarget(event) { @@ -163,6 +193,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { onTargetSelection(event) { event.stopPropagation(); - this.system.targetMode = Boolean(event.target.dataset.targetHit); + if (!event.target.classList.contains('target-selected')) + this.system.targetMode = Boolean(event.target.dataset.targetHit); } } diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 7b588fc7..6124cfbe 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -172,6 +172,26 @@ Roll.replaceFormulaData = function (formula, data = {}, { missing, warn = false return nativeReplaceFormulaData(formula, data, { missing, warn }); }; +foundry.dice.terms.Die.MODIFIERS.sc = 'selfCorrecting'; + +/** + * Return the configured value as result if 1 is rolled + * Example: 6d6sc6 Roll 6d6, each result of 1 will be changed into 6 + * @param {string} modifier The matched modifier query + */ +foundry.dice.terms.Die.prototype.selfCorrecting = function (modifier) { + const rgx = /(?:sc)([0-9]+)/i; + const match = modifier.match(rgx); + if (!match) return false; + let [target] = match.slice(1); + target = parseInt(target); + for (const r of this.results) { + if (r.result === 1) { + r.result = target; + } + } +}; + export const getDamageKey = damage => { return ['none', 'minor', 'major', 'severe'][damage]; }; diff --git a/src/packs/items/weapons/weapon_Advanced_Longsword_9xkB3MWXahrsVP4N.json b/src/packs/items/weapons/weapon_Advanced_Longsword_9xkB3MWXahrsVP4N.json index 79ce8416..8b44c759 100644 --- a/src/packs/items/weapons/weapon_Advanced_Longsword_9xkB3MWXahrsVP4N.json +++ b/src/packs/items/weapons/weapon_Advanced_Longsword_9xkB3MWXahrsVP4N.json @@ -43,7 +43,7 @@ "parts": [ { "value": { - "dice": "d8", + "dice": "d10", "bonus": 9, "multiplier": "prof", "flatMultiplier": 1, diff --git a/src/packs/items/weapons/weapon_Advanced_Spear_pK6dsNABKKp1CIGN.json b/src/packs/items/weapons/weapon_Advanced_Spear_pK6dsNABKKp1CIGN.json index 433533e3..0bfb4a8f 100644 --- a/src/packs/items/weapons/weapon_Advanced_Spear_pK6dsNABKKp1CIGN.json +++ b/src/packs/items/weapons/weapon_Advanced_Spear_pK6dsNABKKp1CIGN.json @@ -12,15 +12,7 @@ "equipped": false, "secondary": false, "burden": "twoHanded", - "weaponFeatures": [ - { - "value": "cumbersome", - "effectIds": [ - "hl0S2LrBY5Mg69q6" - ], - "actionIds": [] - } - ], + "weaponFeatures": [], "attack": { "name": "Attack", "img": "icons/skills/melee/blood-slash-foam-red.webp", @@ -51,8 +43,8 @@ "parts": [ { "value": { - "dice": "d10", - "bonus": 8, + "dice": "d8", + "bonus": 9, "multiplier": "prof", "flatMultiplier": 1, "custom": { diff --git a/src/packs/items/weapons/weapon_Improved_Longsword_QyBZ5NxM8F9nCL9s.json b/src/packs/items/weapons/weapon_Improved_Longsword_QyBZ5NxM8F9nCL9s.json index 867a563f..b064b1c2 100644 --- a/src/packs/items/weapons/weapon_Improved_Longsword_QyBZ5NxM8F9nCL9s.json +++ b/src/packs/items/weapons/weapon_Improved_Longsword_QyBZ5NxM8F9nCL9s.json @@ -43,7 +43,7 @@ "parts": [ { "value": { - "dice": "d8", + "dice": "d10", "bonus": 6, "multiplier": "prof", "flatMultiplier": 1, diff --git a/src/packs/items/weapons/weapon_Improved_Spear_j5Pt1thLfcvopBij.json b/src/packs/items/weapons/weapon_Improved_Spear_j5Pt1thLfcvopBij.json index beb295d6..367f80ad 100644 --- a/src/packs/items/weapons/weapon_Improved_Spear_j5Pt1thLfcvopBij.json +++ b/src/packs/items/weapons/weapon_Improved_Spear_j5Pt1thLfcvopBij.json @@ -12,15 +12,7 @@ "equipped": false, "secondary": false, "burden": "twoHanded", - "weaponFeatures": [ - { - "value": "cumbersome", - "effectIds": [ - "8twXPJELZpvFWA5K" - ], - "actionIds": [] - } - ], + "weaponFeatures": [], "attack": { "name": "Attack", "img": "icons/skills/melee/blood-slash-foam-red.webp", @@ -51,8 +43,8 @@ "parts": [ { "value": { - "dice": "d10", - "bonus": 5, + "dice": "d8", + "bonus": 6, "multiplier": "prof", "flatMultiplier": 1, "custom": { diff --git a/src/packs/items/weapons/weapon_Legendary_Longsword_14abPqQcROJfDChR.json b/src/packs/items/weapons/weapon_Legendary_Longsword_14abPqQcROJfDChR.json index 186cd1c1..636dd7a5 100644 --- a/src/packs/items/weapons/weapon_Legendary_Longsword_14abPqQcROJfDChR.json +++ b/src/packs/items/weapons/weapon_Legendary_Longsword_14abPqQcROJfDChR.json @@ -43,7 +43,7 @@ "parts": [ { "value": { - "dice": "d8", + "dice": "d10", "bonus": 12, "multiplier": "prof", "flatMultiplier": 1, diff --git a/src/packs/items/weapons/weapon_Legendary_Spear_4e5pWxi2qohuGsWh.json b/src/packs/items/weapons/weapon_Legendary_Spear_4e5pWxi2qohuGsWh.json index c1322de0..bfdcd4eb 100644 --- a/src/packs/items/weapons/weapon_Legendary_Spear_4e5pWxi2qohuGsWh.json +++ b/src/packs/items/weapons/weapon_Legendary_Spear_4e5pWxi2qohuGsWh.json @@ -12,15 +12,7 @@ "equipped": false, "secondary": false, "burden": "twoHanded", - "weaponFeatures": [ - { - "value": "cumbersome", - "effectIds": [ - "f44KWDgCQeKYfccr" - ], - "actionIds": [] - } - ], + "weaponFeatures": [], "attack": { "name": "Attack", "img": "icons/skills/melee/blood-slash-foam-red.webp", @@ -51,8 +43,8 @@ "parts": [ { "value": { - "dice": "d10", - "bonus": 11, + "dice": "d8", + "bonus": 12, "multiplier": "prof", "flatMultiplier": 1, "custom": { diff --git a/src/packs/items/weapons/weapon_Longsword_Iv8BZM1R24QMT72M.json b/src/packs/items/weapons/weapon_Longsword_Iv8BZM1R24QMT72M.json index 54d18b78..c5d9070b 100644 --- a/src/packs/items/weapons/weapon_Longsword_Iv8BZM1R24QMT72M.json +++ b/src/packs/items/weapons/weapon_Longsword_Iv8BZM1R24QMT72M.json @@ -43,7 +43,7 @@ "parts": [ { "value": { - "dice": "d8", + "dice": "d10", "bonus": 3, "multiplier": "prof", "flatMultiplier": 1, diff --git a/src/packs/items/weapons/weapon_Spear_TF85tKJetUjLwh54.json b/src/packs/items/weapons/weapon_Spear_TF85tKJetUjLwh54.json index 6aa8fe8c..77ba9a93 100644 --- a/src/packs/items/weapons/weapon_Spear_TF85tKJetUjLwh54.json +++ b/src/packs/items/weapons/weapon_Spear_TF85tKJetUjLwh54.json @@ -12,15 +12,7 @@ "equipped": false, "secondary": false, "burden": "twoHanded", - "weaponFeatures": [ - { - "value": "cumbersome", - "effectIds": [ - "Z5MnVI8EOOgzRdXC" - ], - "actionIds": [] - } - ], + "weaponFeatures": [], "attack": { "name": "Attack", "img": "icons/skills/melee/blood-slash-foam-red.webp", @@ -51,8 +43,8 @@ "parts": [ { "value": { - "dice": "d10", - "bonus": 2, + "dice": "d8", + "bonus": 3, "multiplier": "prof", "flatMultiplier": 1, "custom": { diff --git a/styles/less/dialog/index.less b/styles/less/dialog/index.less index 942016e0..05593d44 100644 --- a/styles/less/dialog/index.less +++ b/styles/less/dialog/index.less @@ -27,3 +27,5 @@ @import './damage-reduction/sheets.less'; @import './multiclass-choice/sheet.less'; + +@import './reroll-dialog/sheet.less'; diff --git a/styles/less/dialog/reroll-dialog/sheet.less b/styles/less/dialog/reroll-dialog/sheet.less new file mode 100644 index 00000000..f8687009 --- /dev/null +++ b/styles/less/dialog/reroll-dialog/sheet.less @@ -0,0 +1,125 @@ +.daggerheart.dialog.dh-style.views.reroll-dialog { + .window-content { + max-width: 648px; + } + + .reroll-outer-container { + h2 { + margin: 0; + } + + .dices-container { + display: flex; + flex-wrap: wrap; + gap: 8px; + } + + .dice-outer-container { + width: 300px; + + legend { + display: flex; + align-items: center; + gap: 4px; + + i { + margin-right: 4px; + } + } + + .dice-container { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr; + + .result-container { + position: relative; + aspect-ratio: 1; + display: flex; + align-items: center; + justify-content: center; + font-size: 22px; + opacity: 0.8; + + &.selected { + opacity: 1; + border: 1px solid; + border-radius: 6px; + border-color: light-dark(@dark-blue, @golden); + filter: drop-shadow(0 0 3px @golden); + } + + &:before { + content: ' '; + position: absolute; + width: 100%; + height: 100%; + z-index: -1; + mask: var(--svg-die) no-repeat center; + mask-size: contain; + background: linear-gradient(139.01deg, #efe6d8 3.51%, #372e1f 96.49%); + } + + &.d4:before { + --svg-die: url(../assets/icons/dice/default/d4.svg); + } + &.d6:before { + --svg-die: url(../assets/icons/dice/default/d6.svg); + } + &.d8:before { + --svg-die: url(../assets/icons/dice/default/d8.svg); + } + &.d10:before { + --svg-die: url(../assets/icons/dice/default/d10.svg); + } + &.d12:before { + --svg-die: url('../assets/icons/dice/default/d12.svg'); + } + &.d20:before { + --svg-die: url(../assets/icons/dice/default/d20.svg); + } + + .to-reroll-result { + position: absolute; + bottom: -7px; + gap: 2px; + border: 1px solid; + border-radius: 6px; + background-image: url(../assets/parchments/dh-parchment-dark.png); + display: flex; + align-items: center; + padding: 2px 6px; + + input { + margin: 0; + height: 12px; + line-height: 0px; + position: relative; + top: 1px; + + &:before, + &:after { + line-height: 12px; + font-size: 12px; + } + } + + i { + font-size: 10px; + } + } + } + } + } + } + + footer { + margin-top: 8px; + display: flex; + justify-content: space-between; + + .controls { + display: flex; + gap: 8px; + } + } +} diff --git a/styles/less/global/dialog.less b/styles/less/global/dialog.less index aaa5c812..701d5025 100644 --- a/styles/less/global/dialog.less +++ b/styles/less/global/dialog.less @@ -53,7 +53,7 @@ font-weight: 500; font-size: 14px; line-height: 17px; - + white-space: nowrap; color: light-dark(@dark, @beige); } diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less index 7f8cdd94..4c0243f6 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -571,6 +571,16 @@ white-space: nowrap; } } + + .button-container { + display: grid; + grid-template-columns: 1fr; + gap: 10px; + text-align: center; + display: flex; + justify-content: center; + width: 100%; + } } footer { diff --git a/styles/less/ui/chat/chat.less b/styles/less/ui/chat/chat.less index 6ffd00cf..81af3d23 100644 --- a/styles/less/ui/chat/chat.less +++ b/styles/less/ui/chat/chat.less @@ -35,7 +35,8 @@ border-color: transparent; } [data-view-perm='false'] { - &[data-perm-hidden='true'], > * { + &[data-perm-hidden='true'], + > * { display: none; } &::after { @@ -161,7 +162,7 @@ color: var(--text-color); font-weight: 700; font-family: 'Cinzel', sans-serif; - line-height: .75; + line-height: 0.75; .roll-result-value { font-size: var(--font-size-24); @@ -191,10 +192,14 @@ .roll-die { display: grid; grid-template-areas: - ". a a" - "c b b"; + '. a a' + 'c b b'; gap: 3px; + .reroll-button:hover { + filter: drop-shadow(0 0 3px @golden); + } + label { text-align: center; height: var(--font-size-12); @@ -272,7 +277,7 @@ border-radius: 3px; &:hover { - background-color: rgba(255,255,255,.1); + background-color: rgba(255, 255, 255, 0.1); } .target-img { diff --git a/styles/less/ui/item-browser/item-browser.less b/styles/less/ui/item-browser/item-browser.less index 3b13056b..e7ff3b12 100644 --- a/styles/less/ui/item-browser/item-browser.less +++ b/styles/less/ui/item-browser/item-browser.less @@ -227,14 +227,16 @@ display: flex; > * { - flex: 1; + flex: 2.5; + text-align: center; } .item-list-img { width: 40px; flex: unset; } .item-list-name { - flex-grow: 3 !important; + flex-grow: 3; + text-align: start; } } diff --git a/templates/dialogs/dice-roll/damageSelection.hbs b/templates/dialogs/dice-roll/damageSelection.hbs index 2967b675..be49906b 100644 --- a/templates/dialogs/dice-roll/damageSelection.hbs +++ b/templates/dialogs/dice-roll/damageSelection.hbs @@ -24,6 +24,22 @@ {{/each}} + {{#if @root.modifiers}} +
+ {{/if}}