From 300719c11624bd6294310c89733dab8cc1edf389 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sun, 10 Aug 2025 01:32:12 +0200 Subject: [PATCH] [Feature] Damage-Reroll (#753) * Added rerolls for damage dice in chat * Fixed multiple dice * Added reroll icon * Fixed new style of dialog --- README.md | 4 +- lang/en.json | 5 + module/applications/dialogs/_module.mjs | 1 + module/applications/dialogs/d20RollDialog.mjs | 16 +- .../dialogs/rerollDamageDialog.mjs | 279 ++++++++++++++++++ module/applications/dialogs/rerollDialog.mjs | 279 ++++++++++++++++++ .../applications/sheets/actors/character.mjs | 21 +- .../sheets/api/application-mixin.mjs | 2 +- module/applications/ui/chatLog.mjs | 65 +++- module/config/generalConfig.mjs | 44 +-- module/data/action/baseAction.mjs | 12 +- module/data/chat-message/adversaryRoll.mjs | 57 ++-- module/data/fields/action/costField.mjs | 14 +- module/data/fields/actionField.mjs | 2 +- module/dice/d20Roll.mjs | 7 +- module/dice/damageRoll.mjs | 144 +++++++-- module/dice/dhRoll.mjs | 31 +- module/documents/actor.mjs | 9 +- module/documents/chatMessage.mjs | 27 +- module/helpers/utils.mjs | 12 +- styles/less/dialog/index.less | 2 + styles/less/dialog/reroll-dialog/sheet.less | 125 ++++++++ styles/less/global/elements.less | 6 +- styles/less/ui/chat/chat.less | 15 +- .../dialogs/rerollDialog/damage/main.hbs | 35 +++ templates/dialogs/rerollDialog/footer.hbs | 4 + templates/dialogs/rerollDialog/main.hbs | 35 +++ templates/ui/chat/parts/damage-part.hbs | 8 +- 28 files changed, 1094 insertions(+), 167 deletions(-) create mode 100644 module/applications/dialogs/rerollDamageDialog.mjs create mode 100644 module/applications/dialogs/rerollDialog.mjs create mode 100644 styles/less/dialog/reroll-dialog/sheet.less create mode 100644 templates/dialogs/rerollDialog/damage/main.hbs create mode 100644 templates/dialogs/rerollDialog/footer.hbs create mode 100644 templates/dialogs/rerollDialog/main.hbs diff --git a/README.md b/README.md index 10372b5b..c40a27a3 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,11 @@ ## Overview -This is the community repo for the Foundry VTT system *Foundryborne* 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. **recommended** Searching for *Daggerheart* or *Foundryborne* in the System Instalaltion dialgoe of the FoundryVTT admin settings. +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. diff --git a/lang/en.json b/lang/en.json index 7353a1c0..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" 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/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/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 f35ebb9f..1a7e19c1 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -344,7 +344,7 @@ export default function DHApplicationMixin(Base) { callback: async (target, event) => { const doc = await getDocFromElement(target), action = doc?.system?.attack ?? doc; - return action && action.use(event, { byPassRoll: true }) + return action && action.use(event, { byPassRoll: true }); } }); 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/config/generalConfig.mjs b/module/config/generalConfig.mjs index bd6ef44e..133dd09e 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -495,31 +495,31 @@ export const diceSetNumbers = { }; 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; - } + 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(); - } + if (system.modelFile && !system.modelLoaded) { + await system.loadModel(game.dice3d.DiceFactory.loaderGLTF); + } else { + await system.loadTextures(); + } - return { - modelFile: system.modelFile, - appearance: { - ...system.appearance, - ...type - } - }; + return { + 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); diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index 978106db..ab515e88 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -224,11 +224,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel .filter( c => (!successCost && (!c.consumeOnSuccess || config.roll?.success)) || - (successCost && c.consumeOnSuccess) + (successCost && c.consumeOnSuccess) ) .reduce((a, c) => { const resource = usefulResources[c.key]; - if( resource ) { + if (resource) { a.push({ key: c.key, value: (c.total ?? c.value) * (resource.isReversed ? 1 : -1), @@ -247,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); } } @@ -371,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/chat-message/adversaryRoll.mjs b/module/data/chat-message/adversaryRoll.mjs index 14bf9141..337acf5b 100644 --- a/module/data/chat-message/adversaryRoll.mjs +++ b/module/data/chat-message/adversaryRoll.mjs @@ -18,7 +18,6 @@ const targetsField = () => ); export default class DHActorRoll extends foundry.abstract.TypeDataModel { - static defineSchema() { return { title: new fields.StringField(), @@ -65,7 +64,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { } set targetMode(mode) { - if(!this.parent.isAuthor) return; + if (!this.parent.isAuthor) return; this.parent.targetSelection = mode; this.registerTargetHook(); this.updateTargets(); @@ -76,13 +75,14 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { } async updateTargets() { - if(!ui.chat.collection.get(this.parent.id)) return; + if (!ui.chat.collection.get(this.parent.id)) return; let targets; - if(this.targetMode) - 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)); - + targets = Array.from(game.user.targets).map(t => + game.system.api.fields.ActionFields.TargetField.formatTarget(t) + ); + await this.parent.update({ flags: { [game.system.id]: { @@ -90,16 +90,19 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { targetMode: this.targetMode } } - }) + }); } registerTargetHook() { - if(!this.parent.isAuthor) return; - if(this.targetMode && this.parent.targetHook !== null) { - Hooks.off("targetToken", this.parent.targetHook); - return this.parent.targetHook = null; + 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)); + return (this.parent.targetHook = Hooks.on( + 'targetToken', + foundry.utils.debounce(this.updateTargets.bind(this), 50) + )); } } @@ -107,13 +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(); - - 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.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.targetMode && 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; }); } 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 e87f6c20..8d865562 100644 --- a/module/data/fields/actionField.mjs +++ b/module/data/fields/actionField.mjs @@ -227,7 +227,7 @@ export function ActionMixin(Base) { } else { result = await this.item.update({ [path]: updates }, options); } - + return this.inCollection ? 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 91444b8a..63d84744 100644 --- a/module/dice/d20Roll.mjs +++ b/module/dice/d20Roll.mjs @@ -145,10 +145,9 @@ export default class D20Roll extends DHRoll { 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 e2cf6860..44794faa 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -29,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)) @@ -120,11 +122,10 @@ 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) { + 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 (modifier.beforeCrit === true && (modifier.enabled || modifier.value)) modifier.callback(part); } } @@ -142,11 +143,10 @@ 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) { + 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); + if (!modifier.beforeCrit && (modifier.enabled || modifier.value)) modifier.callback(part); } } @@ -156,11 +156,11 @@ export default class DamageRoll extends DHRoll { /* To Remove When Reaction System */ static temporaryModifierBuilder(config) { const mods = {}; - if(config.data?.parent) { - if(config.data.parent.appliedEffects) { + if (config.data?.parent) { + if (config.data.parent.appliedEffects) { // Bardic Rally mods.rally = { - label: "DAGGERHEART.CLASS.Feature.rallyDice", + 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 }); @@ -168,8 +168,10 @@ export default class DamageRoll extends DHRoll { }, []), value: null, beforeCrit: true, - callback: (part) => { - const rallyFaces = config.modifiers.rally.values.find(r => r.value === config.modifiers.rally.value)?.label; + 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}`) @@ -177,58 +179,58 @@ export default class DamageRoll extends DHRoll { } }; } - + const item = config.data.parent.items?.get(config.source.item); - if(item) { + if (item) { // Massive (Weapon Feature) - if(item.system.itemFeatures.find(f => f.value === "massive")) + if (item.system.itemFeatures.find(f => f.value === 'massive')) mods.massive = { label: CONFIG.DH.ITEM.weaponFeatures.massive.label, enabled: true, - callback: (part) => { + 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")) + if (item.system.itemFeatures.find(f => f.value === 'powerful')) mods.powerful = { label: CONFIG.DH.ITEM.weaponFeatures.powerful.label, enabled: true, - callback: (part) => { + 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")) + if (item.system.itemFeatures.find(f => f.value === 'brutal')) mods.brutal = { label: CONFIG.DH.ITEM.weaponFeatures.brutal.label, enabled: true, beforeCrit: true, - callback: (part) => { + 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")) + if (item.system.itemFeatures.find(f => f.value === 'serrated')) mods.serrated = { label: CONFIG.DH.ITEM.weaponFeatures.serrated.label, enabled: true, - callback: (part) => { + callback: part => { part.roll.terms[0].modifiers.push(`sc8`); } }; - + // Self-Correcting (Weapon Feature) - if(item.system.itemFeatures.find(f => f.value === "selfCorrecting")) + if (item.system.itemFeatures.find(f => f.value === 'selfCorrecting')) mods.selfCorrecting = { label: CONFIG.DH.ITEM.weaponFeatures.selfCorrecting.label, enabled: true, - callback: (part) => { + callback: part => { part.roll.terms[0].modifiers.push(`sc6`); } }; @@ -238,4 +240,90 @@ export default class DamageRoll extends DHRoll { 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 a785e508..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'; @@ -68,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 = {}) { @@ -97,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; @@ -217,7 +214,7 @@ export default class DHRoll extends Roll { 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/documents/actor.mjs b/module/documents/actor.mjs index bc8e2b55..8fc9b74a 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -644,7 +644,7 @@ export default class DhpActor extends Actor { ); break; case 'armor': - if(this.system.armor?.system?.marks) { + 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 @@ -652,9 +652,12 @@ export default class DhpActor extends Actor { } break; default: - if(this.system.resources?.[r.key]) { + 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), + Math.min( + this.system.resources[r.key].value + r.value, + this.system.resources[r.key].max + ), 0 ); } diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index 19f7a482..3ade8c4a 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -4,10 +4,13 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { 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 }); @@ -18,28 +21,26 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { } /* -------------------------------------------- */ - + /** @inheritDoc */ prepareData() { - if(this.isAuthor && this.targetSelection === null) - this.targetSelection = this.system.targets?.length > 0; + 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(); + if (this.system.registerTargetHook) this.system.registerTargetHook(); } /* -------------------------------------------- */ /** @inheritDoc */ async _preDelete(options, user) { - if(this.targetHook !== null) Hooks.off("targetToken", this.targetHook); + if (this.targetHook !== null) Hooks.off('targetToken', this.targetHook); return super._preDelete(options, user); } @@ -84,7 +85,7 @@ 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)); }); @@ -192,7 +193,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { onTargetSelection(event) { event.stopPropagation(); - if(!event.target.classList.contains("target-selected")) + 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 f1483e31..6124cfbe 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -172,25 +172,25 @@ Roll.replaceFormulaData = function (formula, data = {}, { missing, warn = false return nativeReplaceFormulaData(formula, data, { missing, warn }); }; -foundry.dice.terms.Die.MODIFIERS.sc = "selfCorrecting"; +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) { +foundry.dice.terms.Die.prototype.selfCorrecting = function (modifier) { const rgx = /(?:sc)([0-9]+)/i; const match = modifier.match(rgx); - if ( !match ) return false; + if (!match) return false; let [target] = match.slice(1); target = parseInt(target); - for ( const r of this.results ) { - if ( r.result === 1 ) { + 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/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/elements.less b/styles/less/global/elements.less index 688df165..4c0243f6 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -577,9 +577,9 @@ grid-template-columns: 1fr; gap: 10px; text-align: center; - display: flex; - justify-content: center; - width: 100% + display: flex; + justify-content: center; + width: 100%; } } 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/templates/dialogs/rerollDialog/damage/main.hbs b/templates/dialogs/rerollDialog/damage/main.hbs new file mode 100644 index 00000000..5b994bf6 --- /dev/null +++ b/templates/dialogs/rerollDialog/damage/main.hbs @@ -0,0 +1,35 @@ +