diff --git a/assets/svg/trait-shield-light.svg b/assets/svg/trait-shield-light.svg index c4d1824d..dc6f77c3 100644 --- a/assets/svg/trait-shield-light.svg +++ b/assets/svg/trait-shield-light.svg @@ -1,3 +1,3 @@ diff --git a/assets/svg/trait-shield.svg b/assets/svg/trait-shield.svg index dcb87cf4..87c18dd7 100644 --- a/assets/svg/trait-shield.svg +++ b/assets/svg/trait-shield.svg @@ -1,3 +1,3 @@ diff --git a/daggerheart.mjs b/daggerheart.mjs index 240d8704..0bbbd274 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -9,7 +9,10 @@ import * as dice from './module/dice/_module.mjs'; import * as fields from './module/data/fields/_module.mjs'; import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs'; import { enricherConfig, enricherRenderSetup } from './module/enrichers/_module.mjs'; +import { getCommandTarget, rollCommandToJSON } from './module/helpers/utils.mjs'; import { BaseRoll, DHRoll, DualityRoll, D20Roll, DamageRoll, FateRoll } from './module/dice/_module.mjs'; +import { enrichedDualityRoll } from './module/enrichers/DualityRollEnricher.mjs'; +import { enrichedFateRoll, getFateTypeData } from './module/enrichers/FateRollEnricher.mjs'; import { handlebarsRegistration, runMigrations, @@ -32,8 +35,6 @@ CONFIG.Dice.daggerheart = { FateRoll: FateRoll }; -Object.assign(CONFIG.Dice.termTypes, dice.diceTypes); - CONFIG.Actor.documentClass = documents.DhpActor; CONFIG.Actor.dataModels = models.actors.config; CONFIG.Actor.collection = collections.DhActorCollection; @@ -332,6 +333,78 @@ Hooks.on('renderHandlebarsApplication', (_, element) => { enricherRenderSetup(element); }); +Hooks.on('chatMessage', (_, message) => { + if (message.startsWith('/dr')) { + const result = + message.trim().toLowerCase() === '/dr' ? { result: {} } : rollCommandToJSON(message.replace(/\/dr\s?/, '')); + if (!result) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.dualityParsing')); + return false; + } + + const { result: rollCommand, flavor } = result; + + const reaction = rollCommand.reaction; + const traitValue = rollCommand.trait?.toLowerCase(); + const advantage = rollCommand.advantage + ? CONFIG.DH.ACTIONS.advantageState.advantage.value + : rollCommand.disadvantage + ? CONFIG.DH.ACTIONS.advantageState.disadvantage.value + : undefined; + const difficulty = rollCommand.difficulty; + const grantResources = rollCommand.grantResources; + + const target = getCommandTarget({ allowNull: true }); + const title = + (flavor ?? traitValue) + ? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { + ability: game.i18n.localize(SYSTEM.ACTOR.abilities[traitValue].label) + }) + : game.i18n.localize('DAGGERHEART.GENERAL.duality'); + + enrichedDualityRoll({ + reaction, + traitValue, + target, + difficulty, + title, + label: game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll'), + actionType: null, + advantage, + grantResources + }); + return false; + } + + if (message.startsWith('/fr')) { + const result = + message.trim().toLowerCase() === '/fr' ? { result: {} } : rollCommandToJSON(message.replace(/\/fr\s?/, '')); + + if (!result) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateParsing')); + return false; + } + + const { result: rollCommand, flavor } = result; + const fateTypeData = getFateTypeData(rollCommand?.type); + + if (!fateTypeData) + return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing')); + + const { value: fateType, label: fateTypeLabel } = fateTypeData; + const target = getCommandTarget({ allowNull: true }); + const title = flavor ?? game.i18n.localize('DAGGERHEART.GENERAL.fateRoll'); + + enrichedFateRoll({ + target, + title, + label: fateTypeLabel, + fateType + }); + return false; + } +}); + Hooks.on(CONFIG.DH.HOOKS.hooksConfig.tagTeamStart, async data => { if (data.openForAllPlayers && data.partyId) { const party = game.actors.get(data.partyId); diff --git a/module/applications/dialogs/tagTeamDialog.mjs b/module/applications/dialogs/tagTeamDialog.mjs index 2d363471..5fdefcbd 100644 --- a/module/applications/dialogs/tagTeamDialog.mjs +++ b/module/applications/dialogs/tagTeamDialog.mjs @@ -200,7 +200,6 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio partContext.members[partId] = { ...data, - roll: data.roll, isEditable: actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER), key: partId, readyToRoll: Boolean(data.rollChoice), @@ -449,19 +448,24 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio const { member, diceType } = button.dataset; const memberData = this.party.system.tagTeam.members[member]; - const dieIndex = diceType === 'hope' ? 0 : diceType === 'fear' ? 1 : 2; - const newRoll = game.system.api.dice.DualityRoll.fromData(memberData.rollData); - const dice = newRoll.dice[dieIndex]; - await dice.reroll(`/r1=${dice.total}`, { - liveRoll: { - roll: newRoll, - isReaction: true - } - }); - const rollData = newRoll.toJSON(); + const dieIndex = diceType === 'hope' ? 0 : diceType === 'fear' ? 2 : 4; + + const { parsedRoll, newRoll } = await game.system.api.dice.DualityRoll.reroll( + memberData.rollData, + dieIndex, + diceType, + false + ); + const rollData = parsedRoll.toJSON(); this.updatePartyData( { - [`system.tagTeam.members.${member}.rollData`]: rollData + [`system.tagTeam.members.${member}.rollData`]: { + ...rollData, + options: { + ...rollData.options, + roll: newRoll + } + } }, this.getUpdatingParts(button) ); @@ -696,9 +700,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio const error = this.checkInitiatorHopeError(this.party.system.tagTeam.initiator); if (error) return error; - const joinedRoll = await this.getJoinedRoll(); - const mainRoll = joinedRoll.rollData; - const finalRoll = foundry.utils.deepClone(joinedRoll.roll); + const mainRoll = (await this.getJoinedRoll()).rollData; const mainActor = this.party.system.partyMembers.find(x => x.uuid === mainRoll.options.source.actor); mainRoll.options.title = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.chatMessageRollTitle'); @@ -709,7 +711,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio title: game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.title'), speaker: cls.getSpeaker({ actor: mainActor }), system: mainRoll.options, - rolls: [JSON.stringify(joinedRoll.roll)], + rolls: [mainRoll], sound: null, flags: { core: { RollTable: true } } }; @@ -721,7 +723,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio const fearUpdate = { key: 'fear', value: null, total: null, enabled: true }; for (let memberId in tagTeamData.members) { const resourceUpdates = []; - const rollGivesHope = finalRoll.isCritical || finalRoll.withHope; + const rollGivesHope = mainRoll.options.roll.isCritical || mainRoll.options.roll.result.duality === 1; if (memberId === tagTeamData.initiator.memberId) { const value = tagTeamData.initiator.cost ? rollGivesHope @@ -732,8 +734,9 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio } else if (rollGivesHope) { resourceUpdates.push({ key: 'hope', value: 1, total: -1, enabled: true }); } - if (finalRoll.isCritical) resourceUpdates.push({ key: 'stress', value: -1, total: 1, enabled: true }); - if (finalRoll.withFear) { + if (mainRoll.options.roll.isCritical) + resourceUpdates.push({ key: 'stress', value: -1, total: 1, enabled: true }); + if (mainRoll.options.roll.result.duality === -1) { fearUpdate.value = fearUpdate.value === null ? 1 : fearUpdate.value + 1; fearUpdate.total = fearUpdate.total === null ? -1 : fearUpdate.total - 1; } diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index 8cbacb09..e29498e6 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -1,8 +1,5 @@ import { abilities } from '../../config/actorConfig.mjs'; -import { enrichedDualityRoll } from '../../enrichers/DualityRollEnricher.mjs'; -import { enrichedFateRoll, getFateTypeData } from '../../enrichers/FateRollEnricher.mjs'; -import { getCommandTarget, rollCommandToJSON } from '../../helpers/utils.mjs'; -import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs'; +import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog { constructor(options) { @@ -24,84 +21,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo classes: ['daggerheart'] }; - static CHAT_COMMANDS = { - ...super.CHAT_COMMANDS, - dr: { - rgx: /^(?:\/dr)((?:\s)[^]*)?/, - fn: (_, match) => { - const argString = match[1]?.trim(); - const result = argString ? rollCommandToJSON(argString) : { result: {} }; - if (!result) { - ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.dualityParsing')); - return false; - } - - const { result: rollCommand, flavor } = result; - - const reaction = rollCommand.reaction; - const traitValue = rollCommand.trait?.toLowerCase(); - const advantage = rollCommand.advantage - ? CONFIG.DH.ACTIONS.advantageState.advantage.value - : rollCommand.disadvantage - ? CONFIG.DH.ACTIONS.advantageState.disadvantage.value - : undefined; - const difficulty = rollCommand.difficulty; - const grantResources = rollCommand.grantResources; - - const target = getCommandTarget({ allowNull: true }); - const title = - (flavor ?? traitValue) - ? game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { - ability: game.i18n.localize(SYSTEM.ACTOR.abilities[traitValue].label) - }) - : game.i18n.localize('DAGGERHEART.GENERAL.duality'); - - enrichedDualityRoll({ - reaction, - traitValue, - target, - difficulty, - title, - label: game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll'), - actionType: null, - advantage, - grantResources - }); - return false; - } - }, - fr: { - rgx: /^(?:\/fr)((?:\s)[^]*)?/, - fn: (_, match) => { - const argString = match[1]?.trim(); - const result = argString ? rollCommandToJSON(argString) : { result: {} }; - - if (!result) { - ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateParsing')); - return false; - } - - const { result: rollCommand, flavor } = result; - const fateTypeData = getFateTypeData(rollCommand?.type); - - if (!fateTypeData) - return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.fateTypeParsing')); - - const { value: fateType, label: fateTypeLabel } = fateTypeData; - const target = getCommandTarget({ allowNull: true }); - const title = flavor ?? game.i18n.localize('DAGGERHEART.GENERAL.fateRoll'); - - enrichedFateRoll({ - target, - title, - label: fateTypeLabel, - fateType - }); - return false; - } - } - }; - _getEntryContextOptions() { return [ ...super._getEntryContextOptions(), @@ -256,7 +175,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo action.use(event); } - async rerollEvent(event, messageData) { + async rerollEvent(event, message) { event.stopPropagation(); if (!event.shiftKey) { const confirmed = await foundry.applications.api.DialogV2.confirm({ @@ -268,7 +187,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo if (!confirmed) return; } - const message = game.messages.get(messageData._id); const target = event.target.closest('[data-die-index]'); if (target.dataset.type === 'damage') { @@ -291,16 +209,27 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo } }); } else { - const rerollDice = message.system.roll.dice[target.dataset.dieIndex]; - await rerollDice.reroll(`/r1=${rerollDice.total}`, { - liveRoll: { - roll: message.system.roll, - actor: message.system.actionActor, - isReaction: message.system.roll.options.actionType === 'reaction' - } - }); - await message.update({ - rolls: [message.system.roll.toJSON()] + 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 }); + + const { newRoll, parsedRoll } = await rollClass.reroll( + originalRoll_parsed, + target.dataset.dieIndex, + target.dataset.type + ); + + await game.messages.get(message._id).update({ + 'system.roll': newRoll, + 'rolls': [parsedRoll] }); } } diff --git a/module/data/chat-message/actorRoll.mjs b/module/data/chat-message/actorRoll.mjs index e601f86d..1ea7ff93 100644 --- a/module/data/chat-message/actorRoll.mjs +++ b/module/data/chat-message/actorRoll.mjs @@ -32,6 +32,7 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { return { title: new fields.StringField(), actionDescription: new fields.HTMLField(), + roll: new fields.ObjectField(), targets: targetsField(), hasRoll: new fields.BooleanField({ initial: false }), hasDamage: new fields.BooleanField({ initial: false }), @@ -54,16 +55,6 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { }; } - get roll() { - if (this.parent.type === 'dualityRoll') - return this.parent.rolls.find(x => x instanceof game.system.api.dice.DualityRoll); - - if (this.parent.type === 'fateRoll') - return this.parent.rolls.find(x => x instanceof game.system.api.dice.FateRoll); - - return null; - } - get actionActor() { if (!this.source.actor) return null; return fromUuidSync(this.source.actor); diff --git a/module/dice/_module.mjs b/module/dice/_module.mjs index e1206f82..b9339d87 100644 --- a/module/dice/_module.mjs +++ b/module/dice/_module.mjs @@ -4,4 +4,3 @@ export { default as DamageRoll } from './damageRoll.mjs'; export { default as DHRoll } from './dhRoll.mjs'; export { default as DualityRoll } from './dualityRoll.mjs'; export { default as FateRoll } from './fateRoll.mjs'; -export { diceTypes } from './die/_module.mjs'; diff --git a/module/dice/d20Roll.mjs b/module/dice/d20Roll.mjs index 6c1082f0..f117ff65 100644 --- a/module/dice/d20Roll.mjs +++ b/module/dice/d20Roll.mjs @@ -36,7 +36,7 @@ export default class D20Roll extends DHRoll { get isCritical() { if (!this.d20._evaluated) return; - const criticalThreshold = this.options.actionType === 'reaction' ? 20 : this.data.criticalThreshold; + const criticalThreshold = this.options.actionType === 'reaction' ? 20 : this.data.system.criticalThreshold; return this.d20.total >= criticalThreshold; } @@ -217,11 +217,49 @@ export default class D20Roll extends DHRoll { results: d.results }; }); - data.modifierTotal = roll.modifierTotal; + data.modifierTotal = this.calculateTotalModifiers(roll); return data; } resetFormula() { return (this._formula = this.constructor.getFormula(this.terms)); } + + static async reroll(rollString, _target, message) { + let parsedRoll = game.system.api.dice.D20Roll.fromData(rollString); + parsedRoll = await parsedRoll.reroll(); + const newRoll = game.system.api.dice.D20Roll.postEvaluate(parsedRoll, { + targets: message.system.targets, + roll: { + advantage: message.system.roll.advantage?.type, + difficulty: message.system.roll.difficulty ? Number(message.system.roll.difficulty) : null + } + }); + + if (game.modules.get('dice-so-nice')?.active) { + await game.dice3d.showForRoll(parsedRoll, game.user, true); + } + + const rerolled = { + any: true, + rerolls: [ + ...(message.system.roll.dice[0].rerolled?.rerolls?.length > 0 + ? [message.system.roll.dice[0].rerolled?.rerolls] + : []), + rollString.terms[0].results + ] + }; + return { + newRoll: { + ...newRoll, + dice: [ + { + ...newRoll.dice[0], + rerolled: rerolled + } + ] + }, + parsedRoll + }; + } } diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index e4a34bd4..3310b9ca 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -12,10 +12,6 @@ export default class DHRoll extends Roll { return game.i18n.localize('DAGGERHEART.GENERAL.Roll.basic'); } - get modifierTotal() { - return this.constructor.calculateTotalModifiers(this); - } - static messageType = 'adversaryRoll'; static CHAT_TEMPLATE = 'systems/daggerheart/templates/ui/chat/roll.hbs'; @@ -142,7 +138,6 @@ export default class DHRoll extends Roll { const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options }); return foundry.applications.handlebars.renderTemplate(template, { ...chatData, - roll: this, parent: chatData.parent, targetMode: chatData.targetMode, metagamingSettings @@ -246,21 +241,16 @@ export default class DHRoll extends Roll { return (this._formula = this.constructor.getFormula(this.terms)); } - /** - * Calculate total modifiers of any rolls, including non-dh rolls. - * This exists because damage rolls still may receive base roll classes - */ static calculateTotalModifiers(roll) { let modifierTotal = 0; for (let i = 0; i < roll.terms.length; i++) { - if (!roll.terms[i].isDeterministic) continue; - const termTotal = roll.terms[i].total; - if (typeof termTotal === 'number') { - const multiplier = roll.terms[i - 1]?.operator === " - " ? -1 : 1; - modifierTotal += multiplier * termTotal; - } + if ( + roll.terms[i] instanceof foundry.dice.terms.NumericTerm && + !!roll.terms[i - 1] && + roll.terms[i - 1] instanceof foundry.dice.terms.OperatorTerm + ) + modifierTotal += Number(`${roll.terms[i - 1].operator}${roll.terms[i].total}`); } - return modifierTotal; } diff --git a/module/dice/die/_module.mjs b/module/dice/die/_module.mjs deleted file mode 100644 index ed892f6a..00000000 --- a/module/dice/die/_module.mjs +++ /dev/null @@ -1,9 +0,0 @@ -import DualityDie from './dualityDie.mjs'; -import AdvantageDie from './advantageDie.mjs'; -import DisadvantageDie from './disadvantageDie.mjs'; - -export const diceTypes = { - DualityDie, - AdvantageDie, - DisadvantageDie -}; diff --git a/module/dice/die/advantageDie.mjs b/module/dice/die/advantageDie.mjs deleted file mode 100644 index 9c2f0b03..00000000 --- a/module/dice/die/advantageDie.mjs +++ /dev/null @@ -1,7 +0,0 @@ -export default class AdvantageDie extends foundry.dice.terms.Die { - constructor(options) { - super(options); - - this.modifiers = []; - } -} diff --git a/module/dice/die/disadvantageDie.mjs b/module/dice/die/disadvantageDie.mjs deleted file mode 100644 index f56ebe96..00000000 --- a/module/dice/die/disadvantageDie.mjs +++ /dev/null @@ -1,7 +0,0 @@ -export default class DisadvantageDie extends foundry.dice.terms.Die { - constructor(options) { - super(options); - - this.modifiers = []; - } -} diff --git a/module/dice/die/dualityDie.mjs b/module/dice/die/dualityDie.mjs deleted file mode 100644 index e9deb77f..00000000 --- a/module/dice/die/dualityDie.mjs +++ /dev/null @@ -1,62 +0,0 @@ -import { ResourceUpdateMap } from '../../data/action/baseAction.mjs'; - -export default class DualityDie extends foundry.dice.terms.Die { - constructor(options) { - super(options); - - this.modifiers = []; - } - - #getDualityState(roll) { - if (!roll) return null; - return roll.withHope ? 1 : roll.withFear ? -1 : 0; - } - - #updateResources(oldDuality, newDuality, actor) { - const { hopeFear } = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation); - if (game.user.isGM ? !hopeFear.gm : !hopeFear.players) return; - - const updates = []; - const hope = (newDuality >= 0 ? 1 : 0) - (oldDuality >= 0 ? 1 : 0); - const stress = (newDuality === 0 ? 1 : 0) - (oldDuality === 0 ? 1 : 0); - const fear = (newDuality === -1 ? 1 : 0) - (oldDuality === -1 ? 1 : 0); - - if (hope !== 0) updates.push({ key: 'hope', value: hope, total: -1 * hope, enabled: true }); - if (stress !== 0) updates.push({ key: 'stress', value: -1 * stress, total: stress, enabled: true }); - if (fear !== 0) updates.push({ key: 'fear', value: fear, total: -1 * fear, enabled: true }); - - const resourceUpdates = new ResourceUpdateMap(actor); - resourceUpdates.addResources(updates); - resourceUpdates.updateResources(); - } - - async reroll(modifier, options) { - const oldDuality = this.#getDualityState(options.liveRoll.roll); - await super.reroll(modifier, options); - - if (options?.liveRoll) { - /* Can't currently test since DiceSoNice is not v14. Might need to set the appearance earlier if a roll is triggered by super.reroll */ - if (game.modules.get('dice-so-nice')?.active) { - const diceSoNiceRoll = { - _evaluated: true, - dice: [this], - options: { appearance: {} } - }; - - const preset = await getDiceSoNicePreset(diceSoNice[key], faces); - diceSoNiceRoll.dice[0].options.appearance = preset.appearance; - diceSoNiceRoll.dice[0].options.modelFile = preset.modelFile; - - await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true); - } else { - foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice }); - } - - await options.liveRoll.roll._evaluate(); - if (options.liveRoll.isReaction) return; - - const newDuality = this.#getDualityState(options.liveRoll.roll); - this.#updateResources(oldDuality, newDuality, options.liveRoll.actor); - } - } -} diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index bc381f07..84e0b493 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -1,6 +1,8 @@ import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs'; import D20Roll from './d20Roll.mjs'; import { parseRallyDice, setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs'; +import { getDiceSoNicePresets } from '../config/generalConfig.mjs'; +import { ResourceUpdateMap } from '../data/action/baseAction.mjs'; export default class DualityRoll extends D20Roll { _advantageFaces = 6; @@ -24,31 +26,27 @@ export default class DualityRoll extends D20Roll { } get dHope() { - if (!(this.dice[0] instanceof game.system.api.dice.diceTypes.DualityDie)) this.createBaseDice(); + if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice(); return this.dice[0]; } set dHope(faces) { - // TODO this should not be asymmetrical with the getter. updateRollConfiguration() should use dHope.faces - this.dHope.faces = this.getFaces(faces); + if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice(); + this.dice[0].faces = this.getFaces(faces); } get dFear() { - if (!(this.dice[1] instanceof game.system.api.dice.diceTypes.DualityDie)) this.createBaseDice(); + if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice(); return this.dice[1]; } set dFear(faces) { - // TODO this should not be asymmetrical with the getter. updateRollConfiguration() should use dFear.faces - this.dFear.faces = this.getFaces(faces); + if (!(this.dice[1] instanceof foundry.dice.terms.Die)) this.createBaseDice(); + this.dice[1].faces = this.getFaces(faces); } get dAdvantage() { - return this.dice[2] instanceof game.system.api.dice.diceTypes.AdvantageDie ? this.dice[2] : null; - } - - get dDisadvantage() { - return this.dice[2] instanceof game.system.api.dice.diceTypes.DisadvantageDie ? this.dice[2] : null; + return this.dice[2]; } get advantageFaces() { @@ -67,11 +65,6 @@ export default class DualityRoll extends D20Roll { this._advantageNumber = Number(value); } - get extraDice() { - const { DualityDie, AdvantageDie, DisadvantageDie } = game.system.api.dice.diceTypes; - return this.dice.filter(x => ![DualityDie, AdvantageDie, DisadvantageDie].some(die => x instanceof die)); - } - setRallyChoices() { return this.data?.parent?.appliedEffects.reduce((a, c) => { const change = c.system.changes.find(ch => ch.key === 'system.bonuses.rally'); @@ -125,28 +118,22 @@ export default class DualityRoll extends D20Roll { /** @inheritDoc */ static fromData(data) { - data.terms[0].class = 'DualityDie'; - data.terms[2].class = 'DualityDie'; - if (data.options.roll.advantage?.type && data.terms[4]?.faces) { - data.terms[4].class = data.options.roll.advantage.type === 1 ? 'AdvantageDie' : 'DisadvantageDie'; - } + data.terms[0].class = foundry.dice.terms.Die.name; + data.terms[2].class = foundry.dice.terms.Die.name; return super.fromData(data); } createBaseDice() { - if ( - this.dice[0] instanceof game.system.api.dice.diceTypes.DualityDie && - this.dice[1] instanceof game.system.api.dice.diceTypes.DualityDie - ) { + if (this.dice[0] instanceof foundry.dice.terms.Die && this.dice[1] instanceof foundry.dice.terms.Die) { this.terms = [this.terms[0], this.terms[1], this.terms[2]]; return; } - this.terms[0] = new game.system.api.dice.diceTypes.DualityDie({ + this.terms[0] = new foundry.dice.terms.Die({ faces: this.data.rules.dualityRoll?.defaultHopeDice ?? 12 }); this.terms[1] = new foundry.dice.terms.OperatorTerm({ operator: '+' }); - this.terms[2] = new game.system.api.dice.diceTypes.DualityDie({ + this.terms[2] = new foundry.dice.terms.Die({ faces: this.data.rules.dualityRoll?.defaultFearDice ?? 12 }); } @@ -384,4 +371,63 @@ export default class DualityRoll extends D20Roll { if (currentCombatant?.actorId == config.data.id) ui.combat.setCombatantSpotlight(currentCombatant.id); } } + + static async reroll(rollBase, dieIndex, diceType, updateResources = true) { + let parsedRoll = game.system.api.dice.DualityRoll.fromData({ ...rollBase, evaluated: false }); + const term = parsedRoll.terms[dieIndex]; + await term.reroll(`/r1=${term.total}`); + const result = await parsedRoll.evaluate(); + + if (game.modules.get('dice-so-nice')?.active) { + const diceSoNiceRoll = { + _evaluated: true, + dice: [ + new foundry.dice.terms.Die({ + ...term, + faces: term._faces, + results: term.results.filter(x => !x.rerolled) + }) + ], + options: { appearance: {} } + }; + + const diceSoNicePresets = await getDiceSoNicePresets(`d${term._faces}`, `d${term._faces}`); + if (diceSoNicePresets[diceType]) { + diceSoNiceRoll.dice[0].options = diceSoNicePresets[diceType]; + } + + await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true); + } else { + foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice }); + } + + const newRoll = game.system.api.dice.DualityRoll.postEvaluate(parsedRoll, { + targets: parsedRoll.options.targets ?? [], + roll: { + advantage: parsedRoll.options.roll.advantage?.type, + difficulty: parsedRoll.options.roll.difficulty ? Number(parsedRoll.options.roll.difficulty) : null + } + }); + + const extraIndex = newRoll.advantage ? 3 : 2; + newRoll.extra = newRoll.extra.slice(extraIndex); + + const actor = parsedRoll.options.source.actor + ? await foundry.utils.fromUuid(parsedRoll.options.source.actor) + : null; + const config = { + source: { actor: parsedRoll.options.source.actor ?? '' }, + targets: parsedRoll.targets, + roll: newRoll, + rerolledRoll: parsedRoll.options.roll, + resourceUpdates: new ResourceUpdateMap(actor) + }; + + if (updateResources) { + await DualityRoll.addDualityResourceUpdates(config); + await config.resourceUpdates.updateResources(); + } + + return { newRoll, parsedRoll }; + } } diff --git a/module/dice/fateRoll.mjs b/module/dice/fateRoll.mjs index 114fad59..418c8465 100644 --- a/module/dice/fateRoll.mjs +++ b/module/dice/fateRoll.mjs @@ -21,8 +21,8 @@ export default class FateRoll extends D20Roll { } set dHope(faces) { - // TODO this should not be asymmetrical with the getter. updateRollConfiguration() should use dHope.faces - this.dHope.faces = this.getFaces(faces); + if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice(); + this.dice[0].faces = this.getFaces(faces); } get dFear() { @@ -31,8 +31,8 @@ export default class FateRoll extends D20Roll { } set dFear(faces) { - // TODO this should not be asymmetrical with the getter. updateRollConfiguration() should use dFear.faces - this.dFear.faces = this.getFaces(faces); + if (!(this.dice[0] instanceof foundry.dice.terms.Die)) this.createBaseDice(); + this.dice[0].faces = this.getFaces(faces); } get isCritical() { @@ -43,20 +43,6 @@ export default class FateRoll extends D20Roll { return this.data.fateType; } - get withHope() { - return this.data.fateType === 'Hope'; - } - - get withFear() { - return this.data.fateType === 'Fear'; - } - - get totalLabel() { - const label = this.withHope ? 'DAGGERHEART.GENERAL.hope' : 'DAGGERHEART.GENERAL.fear'; - - return game.i18n.localize(label); - } - static getHooks(hooks) { return [...(hooks ?? []), 'Fate']; } diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index 307677bb..8b094678 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -1,4 +1,4 @@ -import { emitAsGM, GMUpdateEvent } from '../systemRegistration/socket.mjs'; +import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../systemRegistration/socket.mjs'; export default class DhpChatMessage extends foundry.documents.ChatMessage { targetHook = null; @@ -78,14 +78,25 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { if (this.isContentVisible) { if (this.type === 'dualityRoll') { html.classList.add('duality'); - if (this.system.roll.withHope) html.classList.add('hope'); - else if (this.system.roll.withFear) html.classList.add('fear'); - else html.classList.add('critical'); + switch (this.system.roll?.result?.duality) { + case 1: + html.classList.add('hope'); + break; + case -1: + html.classList.add('fear'); + break; + default: + html.classList.add('critical'); + break; + } } if (this.type === 'fateRoll') { html.classList.add('fate'); - if (this.system.roll?.fateDie) { - html.classList.add(this.system.roll.fateDie.toLowerCase()); + if (this.system.roll?.fate.fateDie == 'Hope') { + html.classList.add('hope'); + } + if (this.system.roll?.fate.fateDie == 'Fear') { + html.classList.add('fear'); } } diff --git a/styles/less/dialog/character-creation/selections-container.less b/styles/less/dialog/character-creation/selections-container.less index f9569fca..1de3d870 100644 --- a/styles/less/dialog/character-creation/selections-container.less +++ b/styles/less/dialog/character-creation/selections-container.less @@ -240,16 +240,12 @@ display: flex; align-items: center; justify-content: space-evenly; - gap: 2px; + gap: 8px; .trait-container { - span { - font-size: var(--font-size-10); - } - width: 65px; - height: 65px; + width: 60px; + height: 60px; background: url(../assets/svg/trait-shield.svg) no-repeat; - background-size: 100%; div { filter: drop-shadow(0 0 3px black); diff --git a/styles/less/sheets-settings/character-settings/sheet.less b/styles/less/sheets-settings/character-settings/sheet.less index f7f16df4..f0c7c94e 100644 --- a/styles/less/sheets-settings/character-settings/sheet.less +++ b/styles/less/sheets-settings/character-settings/sheet.less @@ -20,22 +20,16 @@ display: flex; align-items: center; justify-content: space-evenly; - gap: 2px; + gap: 8px; .trait-container { - width: 65px; - height: 65px; + width: 60px; + height: 60px; background: url(../assets/svg/trait-shield.svg) no-repeat; - background-size: 100%; - padding-top: 4px; display: flex; flex-direction: column; align-items: center; - span { - font-size: var(--font-size-10); - } - div { filter: drop-shadow(0 0 3px black); text-shadow: 0 0 3px black; diff --git a/styles/less/ui/chat/chat.less b/styles/less/ui/chat/chat.less index db6dceb9..e9ef9147 100644 --- a/styles/less/ui/chat/chat.less +++ b/styles/less/ui/chat/chat.less @@ -384,15 +384,6 @@ justify-content: center; width: 15px; } - &.has-minus:before { - content: '-'; - font-size: var(--font-size-20); - grid-area: c; - display: flex; - align-items: center; - justify-content: center; - width: 15px; - } } } diff --git a/templates/dialogs/dice-roll/rollSelection.hbs b/templates/dialogs/dice-roll/rollSelection.hbs index 64a3cdcb..4451160b 100644 --- a/templates/dialogs/dice-roll/rollSelection.hbs +++ b/templates/dialogs/dice-roll/rollSelection.hbs @@ -53,14 +53,14 @@ {{#if @root.advantage}} {{#if (eq @root.advantage 1)}}