diff --git a/daggerheart.mjs b/daggerheart.mjs index 6c7fb17e..2eb5109f 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -403,6 +403,17 @@ Hooks.on('chatMessage', (_, message) => { } }); +Hooks.on(CONFIG.DH.HOOKS.hooksConfig.tagTeamStart, async data => { + if (data.openForAllPlayers && data.partyId) { + const party = game.actors.get(data.partyId); + if (!party) return; + + const dialog = new game.system.api.applications.dialogs.TagTeamDialog(party); + dialog.tabGroups.application = 'tagTeamRoll'; + await dialog.render({ force: true }); + } +}); + const updateActorsRangeDependentEffects = async token => { const rangeMeasurement = game.settings.get( CONFIG.DH.id, diff --git a/lang/en.json b/lang/en.json index 056c7964..f7d49a04 100755 --- a/lang/en.json +++ b/lang/en.json @@ -677,16 +677,35 @@ }, "TagTeamSelect": { "title": "Tag Team Roll", + "FIELDS": { + "initiator": { + "memberId": { "label": "Initiating Character" }, + "cost": { "label": "Initiation Cost" } + } + }, "leaderTitle": "Initiating Character", "membersTitle": "Participants", "partyTeam": "Party Team", "hopeCost": "Hope Cost", "initiatingCharacter": "Initiating Character", + "selectParticipants": "Select the two participants", + "startTagTeamRoll": "Start Tag Team Roll", + "openDialogForAll": "Open Dialog For All", + "rollType": "Roll Type", + "makeYourRoll": "Make your roll", + "cancelTagTeamRoll": "Cancel Tag Team Roll", + "finishTagTeamRoll": "Finish Tag Team Roll", "linkMessageHint": "Make a roll from your character sheet to link it to the Tag Team Roll", "damageNotRolled": "Damage not rolled in chat message yet", "insufficientHope": "The initiating character doesn't have enough hope", - "createTagTeam": "Create TagTeam Roll", - "chatMessageRollTitle": "Roll" + "createTagTeam": "Create Tag Team Roll", + "chatMessageRollTitle": "Roll", + "cancelConfirmTitle": "Cancel Tag Team Roll", + "cancelConfirmText": "Are you sure you want to cancel the Tag Team Roll? This will close it for all other players too.", + "hints": { + "completeRolls": "Set up and complete the rolls for the characters", + "selectRoll": "Select which roll value to be used for the Tag Team" + } }, "TokenConfig": { "actorSizeUsed": "Actor size is set, determining the dimensions" @@ -1235,6 +1254,11 @@ "selectType": "Select Action Type", "selectAction": "Action Selection" }, + "TagTeamRollTypes": { + "trait": "Trait", + "ability": "Ability", + "damageAbility": "Damage Ability" + }, "TargetTypes": { "any": "Any", "friendly": "Friendly", @@ -1898,6 +1922,10 @@ } }, "GENERAL": { + "Ability": { + "single": "Ability", + "plural": "Abilities" + }, "Action": { "single": "Action", "plural": "Actions" @@ -2352,6 +2380,10 @@ "rerolled": "Rerolled", "rerollThing": "Reroll {thing}", "resource": "Resource", + "result": { + "single": "Result", + "plural": "Results" + }, "roll": "Roll", "rollAll": "Roll All", "rollDamage": "Roll Damage", diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index 8e79ba58..0fdb1896 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -35,7 +35,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio updateIsAdvantage: this.updateIsAdvantage, selectExperience: this.selectExperience, toggleReaction: this.toggleReaction, - toggleTagTeamRoll: this.toggleTagTeamRoll, toggleSelectedEffect: this.toggleSelectedEffect, submitRoll: this.submitRoll }, @@ -133,12 +132,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio context.reactionOverride = this.reactionOverride; } - const tagTeamSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); - if (this.actor && tagTeamSetting.members[this.actor.id] && !this.config.skips?.createMessage) { - context.activeTagTeamRoll = true; - context.tagTeamSelected = this.config.tagTeamSelected; - } - return context; } @@ -215,11 +208,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio } } - static toggleTagTeamRoll() { - this.config.tagTeamSelected = !this.config.tagTeamSelected; - this.render(); - } - static toggleSelectedEffect(_event, button) { this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected; this.render(); diff --git a/module/applications/dialogs/rerollDamageDialog.mjs b/module/applications/dialogs/rerollDamageDialog.mjs index e1b75eb7..b821bd24 100644 --- a/module/applications/dialogs/rerollDamageDialog.mjs +++ b/module/applications/dialogs/rerollDamageDialog.mjs @@ -1,5 +1,3 @@ -import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; - const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) { @@ -123,16 +121,8 @@ export default class RerollDamageDialog extends HandlebarsApplicationMixin(Appli return acc; }, {}) }; + await this.message.update(update); - - Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll }); - await game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.Refresh, - data: { - refreshType: RefreshType.TagTeamRoll - } - }); - await this.close(); } diff --git a/module/applications/dialogs/tagTeamDialog.mjs b/module/applications/dialogs/tagTeamDialog.mjs index c28b773c..ddaabcb4 100644 --- a/module/applications/dialogs/tagTeamDialog.mjs +++ b/module/applications/dialogs/tagTeamDialog.mjs @@ -1,5 +1,7 @@ +import { MemberData } from '../../data/tagTeamData.mjs'; import { getCritDamageBonus } from '../../helpers/utils.mjs'; -import { GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; +import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; +import Party from '../sheets/actors/party.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -7,15 +9,23 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio constructor(party) { super(); - this.data = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); this.party = party; + this.partyMembers = party.system.partyMembers + .filter(x => Party.DICE_ROLL_ACTOR_TYPES.includes(x.type)) + .map(member => ({ + ...member.toObject(), + uuid: member.uuid, + id: member.id, + selected: false + })); + this.intiator = null; + this.openForAllPlayers = true; - this.setupHooks = Hooks.on(socketEvent.Refresh, ({ refreshType }) => { - if (refreshType === RefreshType.TagTeamRoll) { - this.data = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); - this.render(); - } - }); + this.tabGroups.application = Object.keys(party.system.tagTeam.members).length + ? 'tagTeamRoll' + : 'initialization'; + + Hooks.on(socketEvent.Refresh, this.tagTeamRefresh.bind()); } get title() { @@ -24,324 +34,639 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio static DEFAULT_OPTIONS = { tag: 'form', + id: 'TagTeamDialog', classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'tag-team-dialog'], position: { width: 550, height: 'auto' }, actions: { - removeMember: TagTeamDialog.#removeMember, - unlinkMessage: TagTeamDialog.#unlinkMessage, - selectMessage: TagTeamDialog.#selectMessage, - createTagTeam: TagTeamDialog.#createTagTeam + toggleSelectMember: TagTeamDialog.#toggleSelectMember, + startTagTeamRoll: TagTeamDialog.#startTagTeamRoll, + makeRoll: TagTeamDialog.#makeRoll, + removeRoll: TagTeamDialog.#removeRoll, + rerollDice: TagTeamDialog.#rerollDice, + makeDamageRoll: TagTeamDialog.#makeDamageRoll, + removeDamageRoll: TagTeamDialog.#removeDamageRoll, + rerollDamageDice: TagTeamDialog.#rerollDamageDice, + selectRoll: TagTeamDialog.#selectRoll, + cancelRoll: TagTeamDialog.#onCancelRoll, + finishRoll: TagTeamDialog.#finishRoll }, form: { handler: this.updateData, submitOnChange: true, closeOnSubmit: false } }; static PARTS = { - application: { - id: 'tag-team-dialog', - template: 'systems/daggerheart/templates/dialogs/tagTeamDialog.hbs' + initialization: { + id: 'initialization', + template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/initialization.hbs' + }, + tagTeamRoll: { + id: 'tagTeamRoll', + template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/tagTeamRoll.hbs' } }; + /** @inheritdoc */ + static TABS = { + application: { + tabs: [{ id: 'initialization' }, { id: 'tagTeamRoll' }] + } + }; + + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + + for (const element of htmlElement.querySelectorAll('.roll-type-select')) + element.addEventListener('change', this.updateRollType.bind(this)); + } + async _prepareContext(_options) { const context = await super._prepareContext(_options); - context.hopeCost = this.hopeCost; - context.data = this.data; - - context.memberOptions = this.party.filter(c => !this.data.members[c.id]); - context.selectedCharacterOptions = this.party.filter(c => this.data.members[c.id]); - - context.members = Object.keys(this.data.members).map(id => { - const roll = this.data.members[id].messageId ? game.messages.get(this.data.members[id].messageId) : null; - - context.usesDamage = - context.usesDamage === undefined - ? roll?.system.hasDamage - : context.usesDamage && roll?.system.hasDamage; - return { - character: this.party.find(x => x.id === id), - selected: this.data.members[id].selected, - roll: roll, - damageValues: roll - ? Object.keys(roll.system.damage).map(key => ({ - key: key, - name: game.i18n.localize(CONFIG.DH.GENERAL.healingTypes[key].label), - total: roll.system.damage[key].total - })) - : null - }; - }); - - const initiatorChar = this.party.find(x => x.id === this.data.initiator.id); - context.initiator = { - character: initiatorChar, - cost: this.data.initiator.cost - }; - - const selectedMember = Object.values(context.members).find(x => x.selected && x.roll); - const selectedIsCritical = selectedMember?.roll?.system?.isCritical; - context.selectedData = { - result: selectedMember - ? `${selectedMember.roll.system.roll.total} ${selectedMember.roll.system.roll.result.label}` - : null, - damageValues: null - }; - - for (const member of Object.values(context.members)) { - if (!member.roll) continue; - if (context.usesDamage) { - if (!context.selectedData.damageValues) context.selectedData.damageValues = {}; - for (let damage of member.damageValues) { - const damageTotal = member.roll.system.isCritical - ? damage.total - : selectedIsCritical - ? damage.total + (await getCritDamageBonus(member.roll.system.damage[damage.key].formula)) - : damage.total; - if (context.selectedData.damageValues[damage.key]) { - context.selectedData.damageValues[damage.key].total += damageTotal; - } else { - context.selectedData.damageValues[damage.key] = { - ...foundry.utils.deepClone(damage), - total: damageTotal - }; - } - } - } - } - - context.showResult = Object.values(context.members).reduce((enabled, member) => { - if (!member.roll) return enabled; - if (context.usesDamage) { - enabled = enabled === null ? member.damageValues.length > 0 : enabled && member.damageValues.length > 0; - } else { - enabled = enabled === null ? Boolean(member.roll) : enabled && Boolean(member.roll); - } - - return enabled; - }, null); - - context.createDisabled = - !context.selectedData.result || - !this.data.initiator.id || - Object.keys(this.data.members).length === 0 || - Object.values(context.members).some(x => - context.usesDamage ? !x.damageValues || x.damageValues.length === 0 : !x.roll - ); + context.isEditable = this.getIsEditable(); return context; } - async updateSource(update) { - await this.data.updateSource(update); + async _preparePartContext(partId, context, options) { + const partContext = await super._preparePartContext(partId, context, options); + switch (partId) { + case 'initialization': + partContext.tagTeamFields = this.party.system.schema.fields.tagTeam.fields; + partContext.memberSelection = this.partyMembers; + const selectedMembers = partContext.memberSelection.filter(x => x.selected); - if (game.user.isGM) { - await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, this.data.toObject()); - Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll }); - await game.socket.emit(`system.${CONFIG.DH.id}`, { + partContext.allSelected = selectedMembers.length === 2; + partContext.canStartTagTeam = partContext.allSelected && this.initiator; + partContext.initiator = this.initiator; + partContext.initiatorOptions = selectedMembers.map(x => ({ value: x.id, label: x.name })); + partContext.initiatorDisabled = !selectedMembers.length; + partContext.openForAllPlayers = this.openForAllPlayers; + + break; + case 'tagTeamRoll': + partContext.fields = this.party.system.schema.fields.tagTeam.fields; + partContext.data = this.party.system.tagTeam; + partContext.rollTypes = CONFIG.DH.GENERAL.tagTeamRollTypes; + partContext.traitOptions = CONFIG.DH.ACTOR.abilities; + + const selectedRoll = Object.values(this.party.system.tagTeam.members).find(member => member.selected); + const critSelected = !selectedRoll + ? undefined + : (selectedRoll?.rollData?.options?.roll?.isCritical ?? false); + + partContext.members = {}; + for (const actorId in this.party.system.tagTeam.members) { + const data = this.party.system.tagTeam.members[actorId]; + const actor = game.actors.get(actorId); + + const rollOptions = []; + const damageRollOptions = []; + for (const item of actor.items) { + if (item.system.metadata.hasActions) { + const actions = [ + ...item.system.actions, + ...(item.system.attack ? [item.system.attack] : []) + ]; + for (const action of actions) { + if (action.hasRoll) { + const actionItem = { + value: action.uuid, + label: action.name, + group: item.name, + baseAction: action.baseAction + }; + + if (action.hasDamage) damageRollOptions.push(actionItem); + else rollOptions.push(actionItem); + } + } + } + } + + const damage = data.rollData?.options?.damage; + partContext.hasDamage |= Boolean(damage); + const critHitPointsDamage = await this.getCriticalDamage(damage); + + partContext.members[actorId] = { + ...data, + isEditable: actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER), + key: actorId, + readyToRoll: Boolean(data.rollChoice), + hasRolled: Boolean(data.rollData), + rollOptions, + damageRollOptions, + damage: damage, + critDamage: critHitPointsDamage, + useCritDamage: + critSelected || (critSelected === undefined && data.rollData?.options?.roll?.isCritical) + }; + } + + partContext.hintText = await this.getInfoTexts(this.party.system.tagTeam.members); + partContext.joinedRoll = await this.getJoinedRoll({ + overrideIsCritical: critSelected, + displayVersion: true + }); + + break; + } + + return partContext; + } + + static async updateData(_event, _, formData) { + const { initiator, openForAllPlayers, ...partyData } = foundry.utils.expandObject(formData.object); + this.initiator = initiator; + this.openForAllPlayers = openForAllPlayers !== undefined ? openForAllPlayers : this.openForAllPlayers; + + this.updatePartyData(partyData); + } + + async updatePartyData(update, options = { render: true }) { + const gmUpdate = async update => { + await this.party.update(update); + this.render(); + game.socket.emit(`system.${CONFIG.DH.id}`, { action: socketEvent.Refresh, - data: { - refreshType: RefreshType.TagTeamRoll - } + data: { refreshType: RefreshType.TagTeamRoll, action: 'refresh' } }); - } else { - await game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.GMUpdate, - data: { - action: GMUpdateEvent.UpdateSetting, - uuid: CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, - update: this.data.toObject(), - refresh: { refreshType: RefreshType.TagTeamRoll } - } - }); - } + }; + + await emitAsGM( + GMUpdateEvent.UpdateDocument, + gmUpdate, + update, + this.party.uuid, + options.render ? { refreshType: RefreshType.TagTeamRoll, action: 'refresh' } : undefined + ); } - static async updateData(_event, _element, formData) { - const { selectedAddMember, initiator } = foundry.utils.expandObject(formData.object); - const update = { initiator: initiator }; - if (selectedAddMember) { - const member = await foundry.utils.fromUuid(selectedAddMember); - update[`members.${member.id}`] = { messageId: null }; - } - - await this.updateSource(update); - this.render(); - } - - static async #removeMember(_, button) { - const update = { [`members.${button.dataset.characterId}`]: _del }; - if (this.data.initiator.id === button.dataset.characterId) { - update.iniator = { id: null }; - } - - await this.updateSource(update); - } - - static async #unlinkMessage(_, button) { - await this.updateSource({ [`members.${button.id}.messageId`]: null }); - } - - static async #selectMessage(_, button) { - const member = this.data.members[button.id]; - const currentSelected = Object.keys(this.data.members).find(key => this.data.members[key].selected); - const curretSelectedUpdate = - currentSelected && currentSelected !== button.id ? { [`${currentSelected}`]: { selected: false } } : {}; - await this.updateSource({ - members: { - [`${button.id}`]: { selected: !member.selected }, - ...curretSelectedUpdate - } + getIsEditable() { + return this.party.system.partyMembers.some(actor => { + const selected = Boolean(this.party.system.tagTeam.members[actor.id]); + return selected && actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER); }); } - static async #createTagTeam() { - const mainRollId = Object.keys(this.data.members).find(key => this.data.members[key].selected); - const mainRoll = game.messages.get(this.data.members[mainRollId].messageId); + tagTeamRefresh = ({ refreshType, action }) => { + if (refreshType !== RefreshType.TagTeamRoll) return; - if (this.data.initiator.cost) { - const initiator = this.party.find(x => x.id === this.data.initiator.id); - if (initiator.system.resources.hope.value < this.data.initiator.cost) { + switch (action) { + case 'startTagTeamRoll': + this.tabGroups.application = 'tagTeamRoll'; + break; + case 'refresh': + this.render(); + break; + case 'close': + this.close(); + break; + } + }; + + async close(options = {}) { + /* Opt out of Foundry's standard behavior of closing all application windows marked as UI when Escape is pressed */ + if (options.closeKey) return; + + Hooks.off(socketEvent.Refresh, this.tagTeamRefresh); + return super.close(options); + } + + checkInitiatorHopeError(initiator) { + if (initiator.cost && initiator.memberId) { + const actor = game.actors.get(initiator.memberId); + if (actor.system.resources.hope.value < initiator.cost) { return ui.notifications.warn( game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.insufficientHope') ); } } + } - const secondaryRolls = Object.keys(this.data.members) - .filter(key => key !== mainRollId) - .map(key => game.messages.get(this.data.members[key].messageId)); + //#region Initialization + static #toggleSelectMember(_, button) { + const member = this.partyMembers.find(x => x.id === button.dataset.id); + if (member.selected && this.initiator?.memberId === member.id) this.initiator = null; - const systemData = foundry.utils.deepClone(mainRoll).system.toObject(); - const criticalRoll = systemData.roll.isCritical; - for (let roll of secondaryRolls) { - if (roll.system.hasDamage) { - for (let key in roll.system.damage) { - var damage = roll.system.damage[key]; - const damageTotal = - !roll.system.isCritical && criticalRoll - ? (await getCritDamageBonus(damage.formula)) + damage.total - : damage.total; - const updatedDamageParts = damage.parts; - if (systemData.damage[key]) { - if (!roll.system.isCritical && criticalRoll) { - for (let part of updatedDamageParts) { - const criticalDamage = await getCritDamageBonus(part.formula); - if (criticalDamage) { - damage.formula = `${damage.formula} + ${criticalDamage}`; - part.formula = `${part.formula} + ${criticalDamage}`; - part.modifierTotal = part.modifierTotal + criticalDamage; - part.total += criticalDamage; - part.roll = new Roll(part.formula); - } - } - } + member.selected = !member.selected; + this.render(); + } - systemData.damage[key].formula = `${systemData.damage[key].formula} + ${damage.formula}`; - systemData.damage[key].total += damageTotal; - systemData.damage[key].parts = [...systemData.damage[key].parts, ...updatedDamageParts]; - } else { - systemData.damage[key] = { ...damage, total: damageTotal, parts: updatedDamageParts }; - } + static async #startTagTeamRoll() { + const error = this.checkInitiatorHopeError(this.initiator); + if (error) return error; + + await this.party.update({ + 'system.tagTeam': _replace( + new game.system.api.data.TagTeamData({ + ...this.party.system.tagTeam.toObject(), + initiator: this.initiator, + members: this.partyMembers.reduce((acc, member) => { + if (member.selected) + acc[member.id] = { + name: member.name, + img: member.img, + rollType: CONFIG.DH.GENERAL.tagTeamRollTypes.trait.id + }; + return acc; + }, {}) + }) + ) + }); + + const hookData = { openForAllPlayers: this.openForAllPlayers, partyId: this.party.id }; + Hooks.callAll(CONFIG.DH.HOOKS.hooksConfig.tagTeamStart, hookData); + game.socket.emit(`system.${CONFIG.DH.id}`, { + action: socketEvent.TagTeamStart, + data: hookData + }); + + this.render(); + } + //#endregion + //#region Tag Team Roll + + async getInfoTexts(members) { + let rollsAreFinished = true; + let rollIsSelected = false; + for (const member of Object.values(members)) { + const rollFinished = Boolean(member.rollData); + const damageFinished = + member.rollData?.options?.hasDamage !== undefined ? member.rollData.options.damage : true; + + rollsAreFinished = rollsAreFinished && rollFinished && damageFinished; + rollIsSelected = rollIsSelected || member.selected; + } + + let hint = null; + if (!rollsAreFinished) hint = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.hints.completeRolls'); + else if (!rollIsSelected) hint = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.hints.selectRoll'); + + return hint; + } + + async updateRollType(event) { + this.updatePartyData({ + [`system.tagTeam.members.${event.target.dataset.member}`]: { + rollType: event.target.value, + rollChoice: null + } + }); + } + + static async #removeRoll(_, button) { + this.updatePartyData({ + [`system.tagTeam.members.${button.dataset.member}`]: { + rollData: null, + rollChoice: null, + selected: false + } + }); + } + + static async #makeRoll(event, button) { + const { member } = button.dataset; + + let result = null; + switch (this.party.system.tagTeam.members[member].rollType) { + case CONFIG.DH.GENERAL.tagTeamRollTypes.trait.id: + result = await this.makeTraitRoll(member); + break; + case CONFIG.DH.GENERAL.tagTeamRollTypes.ability.id: + case CONFIG.DH.GENERAL.tagTeamRollTypes.damageAbility.id: + result = await this.makeAbilityRoll(event, member); + break; + } + + if (!result) return; + + if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice }); + + const rollData = result.messageRoll.toJSON(); + delete rollData.options.messageRoll; + this.updatePartyData({ + [`system.tagTeam.members.${member}.rollData`]: rollData + }); + } + + async makeTraitRoll(memberKey) { + const actor = game.actors.find(x => x.id === memberKey); + if (!actor) return; + + const memberData = this.party.system.tagTeam.members[memberKey]; + return await actor.rollTrait(memberData.rollChoice, { + skips: { + createMessage: true, + resources: true, + triggers: true + } + }); + } + + async makeAbilityRoll(event, memberKey) { + const actor = game.actors.find(x => x.id === memberKey); + if (!actor) return; + + const memberData = this.party.system.tagTeam.members[memberKey]; + const action = await foundry.utils.fromUuid(memberData.rollChoice); + + return await action.use(event, { + skips: { + createMessage: true, + resources: true, + triggers: true + } + }); + } + + static async #rerollDice(_, button) { + const { member, diceType } = button.dataset; + const memberData = this.party.system.tagTeam.members[member]; + + const dieIndex = diceType === 'hope' ? 0 : diceType === 'fear' ? 2 : 4; + + const { parsedRoll, newRoll } = await game.system.api.dice.DualityRoll.reroll( + memberData.rollData, + dieIndex, + diceType + ); + const rollData = parsedRoll.toJSON(); + this.updatePartyData({ + [`system.tagTeam.members.${member}.rollData`]: { + ...rollData, + options: { + ...rollData.options, + roll: newRoll } } + }); + } + + static async #makeDamageRoll(event, button) { + const { memberKey } = button.dataset; + const actor = game.actors.find(x => x.id === memberKey); + if (!actor) return; + + const memberData = this.party.system.tagTeam.members[memberKey]; + const action = await foundry.utils.fromUuid(memberData.rollChoice); + const config = { + ...memberData.rollData.options, + dialog: { + configure: !event.shiftKey + }, + skips: { + createMessage: true, + resources: true, + triggers: true + } + }; + + await action.workflow.get('damage').execute(config, null, true); + if (!config.damage) return; + + const current = this.party.system.tagTeam.members[memberKey].rollData; + await this.updatePartyData({ + [`system.tagTeam.members.${memberKey}.rollData`]: { + ...current, + options: { + ...current.options, + damage: config.damage + } + } + }); + } + + static async #removeDamageRoll(_, button) { + const { memberKey } = button.dataset; + const current = this.party.system.tagTeam.members[memberKey].rollData; + this.updatePartyData({ + [`system.tagTeam.members.${memberKey}.rollData`]: { + ...current, + options: { + ...current.options, + damage: null + } + } + }); + } + + static async #rerollDamageDice(_, button) { + const { memberKey, damageKey, part, dice } = button.dataset; + const memberData = this.party.system.tagTeam.members[memberKey]; + const partData = memberData.rollData.options.damage[damageKey].parts[part]; + const activeDiceResultKey = Object.keys(partData.dice[dice].results).find( + index => partData.dice[dice].results[index].active + ); + const { parsedRoll, rerolledDice } = await game.system.api.dice.DamageRoll.reroll( + partData, + dice, + activeDiceResultKey + ); + + const rollData = this.party.system.tagTeam.members[memberKey].rollData; + rollData.options.damage[damageKey].parts = rollData.options.damage[damageKey].parts.map((damagePart, index) => { + if (index !== Number.parseInt(part)) return damagePart; + + return { + ...damagePart, + total: parsedRoll.total, + dice: rerolledDice + }; + }); + rollData.options.damage[damageKey].total = rollData.options.damage[damageKey].parts.reduce((acc, part) => { + acc += part.total; + return acc; + }, 0); + + this.updatePartyData({ + [`system.tagTeam.members.${memberKey}.rollData`]: rollData + }); + } + + async getCriticalDamage(damage) { + const newDamage = foundry.utils.deepClone(damage); + for (let key in newDamage) { + var damage = newDamage[key]; + damage.formula = ''; + damage.total = 0; + + for (let part of damage.parts) { + const criticalDamage = await getCritDamageBonus(part.formula); + if (criticalDamage) { + part.modifierTotal += criticalDamage; + part.total += criticalDamage; + part.formula = `${part.dice.map(x => x.formula).join(' + ')} + ${part.modifierTotal}`; + part.roll = new Roll(part.formula); + } + + damage.formula = [damage.formula, part.formula].filter(x => x).join(' + '); + damage.total += part.total; + } } - systemData.title = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.chatMessageRollTitle'); + return newDamage; + } + + async getNonCriticalDamage(config) { + const newDamage = foundry.utils.deepClone(config.damage); + for (let key in newDamage) { + var damage = newDamage[key]; + damage.formula = ''; + damage.total = 0; + + for (let part of damage.parts) { + const critDamageBonus = await getCritDamageBonus(part.formula); + part.modifierTotal -= critDamageBonus; + part.total -= critDamageBonus; + part.formula = `${part.dice.map(x => x.formula).join(' + ')} + ${part.modifierTotal}`; + part.roll = new Roll(part.formula); + + damage.formula = [damage.formula, part.formula].filter(x => x).join(' + '); + damage.total += part.total; + } + } + + return newDamage; + } + + static async #selectRoll(_, button) { + const { memberKey } = button.dataset; + this.updatePartyData({ + [`system.tagTeam.members`]: Object.entries(this.party.system.tagTeam.members).reduce( + (acc, [key, member]) => { + acc[key] = { selected: key === memberKey ? !member.selected : false }; + return acc; + }, + {} + ) + }); + } + + async getJoinedRoll({ overrideIsCritical, displayVersion } = {}) { + const memberValues = Object.values(this.party.system.tagTeam.members); + const selectedRoll = memberValues.find(x => x.selected); + let baseMainRoll = selectedRoll ?? memberValues[0]; + let baseSecondaryRoll = selectedRoll + ? memberValues.find(x => !x.selected) + : memberValues.length > 1 + ? memberValues[1] + : null; + + if (!baseMainRoll?.rollData || !baseSecondaryRoll) return null; + + const mainRoll = new MemberData(baseMainRoll.toObject()); + const secondaryRollData = new MemberData(baseSecondaryRoll.toObject()).rollData; + const systemData = mainRoll.rollData.options; + const isCritical = overrideIsCritical ?? systemData.roll.isCritical; + if (isCritical) systemData.damage = await this.getCriticalDamage(systemData.damage); + + if (secondaryRollData?.options.hasDamage) { + const secondaryDamage = (displayVersion ? overrideIsCritical : isCritical) + ? await this.getCriticalDamage(secondaryRollData.options.damage) + : secondaryRollData.options.damage; + if (systemData.damage) { + for (const key in secondaryDamage) { + const damage = secondaryDamage[key]; + systemData.damage[key].formula = [systemData.damage[key].formula, damage.formula] + .filter(x => x) + .join(' + '); + systemData.damage[key].total += damage.total; + systemData.damage[key].parts.push(...damage.parts); + } + } else { + systemData.damage = secondaryDamage; + } + } + + return mainRoll; + } + + static async #onCancelRoll(_event, _button, options = { confirm: true }) { + this.cancelRoll(options); + } + + async cancelRoll(options = { confirm: true }) { + if (options.confirm) { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.cancelConfirmTitle') + }, + content: game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.cancelConfirmText') + }); + + if (!confirmed) return; + } + + await this.updatePartyData( + { + 'system.tagTeam': { + initiator: null, + members: _replace({}) + } + }, + { render: false } + ); + + this.close(); + game.socket.emit(`system.${CONFIG.DH.id}`, { + action: socketEvent.Refresh, + data: { refreshType: RefreshType.TagTeamRoll, action: 'close' } + }); + } + + static async #finishRoll() { + const error = this.checkInitiatorHopeError(this.party.system.tagTeam.initiator); + if (error) return error; + + 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'); const cls = getDocumentClass('ChatMessage'), msgData = { type: 'dualityRoll', user: game.user.id, title: game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.title'), - speaker: cls.getSpeaker({ actor: this.party.find(x => x.id === mainRollId) }), - system: systemData, - rolls: mainRoll.rolls, + speaker: cls.getSpeaker({ actor: mainActor }), + system: mainRoll.options, + rolls: [mainRoll], sound: null, flags: { core: { RollTable: true } } }; await cls.create(msgData); + /* Handle resource updates from the finished TagTeamRoll */ + const tagTeamData = this.party.system.tagTeam; const fearUpdate = { key: 'fear', value: null, total: null, enabled: true }; - for (let memberId of Object.keys(this.data.members)) { + for (let memberId in tagTeamData.members) { const resourceUpdates = []; - const rollGivesHope = systemData.roll.isCritical || systemData.roll.result.duality === 1; - if (memberId === this.data.initiator.id) { - const value = this.data.initiator.cost + const rollGivesHope = mainRoll.options.roll.isCritical || mainRoll.options.roll.result.duality === 1; + if (memberId === tagTeamData.initiator.memberId) { + const value = tagTeamData.initiator.cost ? rollGivesHope - ? 1 - this.data.initiator.cost - : -this.data.initiator.cost + ? 1 - tagTeamData.initiator.cost + : -tagTeamData.initiator.cost : 1; resourceUpdates.push({ key: 'hope', value: value, total: -value, enabled: true }); } else if (rollGivesHope) { resourceUpdates.push({ key: 'hope', value: 1, total: -1, enabled: true }); } - if (systemData.roll.isCritical) resourceUpdates.push({ key: 'stress', value: -1, total: 1, enabled: true }); - if (systemData.roll.result.duality === -1) { + 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; } - this.party.find(x => x.id === memberId).modifyResource(resourceUpdates); + game.actors.get(memberId).modifyResource(resourceUpdates); } if (fearUpdate.value) { - this.party.find(x => x.id === mainRollId).modifyResource([fearUpdate]); + mainActor.modifyResource([fearUpdate]); } - /* Improve by fetching default from schema */ - const update = { members: [], initiator: { id: null, cost: 3 } }; - if (game.user.isGM) { - await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, update); - Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll }); - await game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.Refresh, - data: { - refreshType: RefreshType.TagTeamRoll - } - }); - } else { - await game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.GMUpdate, - data: { - action: GMUpdateEvent.UpdateSetting, - uuid: CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, - update: update, - refresh: { refreshType: RefreshType.TagTeamRoll } - } - }); - } + /* Fin */ + this.cancelRoll({ confirm: false }); } - static async assignRoll(char, message) { - const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); - const character = settings.members[char.id]; - if (!character) return; - - await settings.updateSource({ [`members.${char.id}.messageId`]: message.id }); - - if (game.user.isGM) { - await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, settings); - Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll }); - await game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.Refresh, - data: { - refreshType: RefreshType.TagTeamRoll - } - }); - } else { - await game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.GMUpdate, - data: { - action: GMUpdateEvent.UpdateSetting, - uuid: CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, - update: settings, - refresh: { refreshType: RefreshType.TagTeamRoll } - } - }); - } - } - - async close(options = {}) { - Hooks.off(socketEvent.Refresh, this.setupHooks); - await super.close(options); - } + //#endregion } diff --git a/module/applications/sheets-configs/_module.mjs b/module/applications/sheets-configs/_module.mjs index d3fb3c39..4b83a042 100644 --- a/module/applications/sheets-configs/_module.mjs +++ b/module/applications/sheets-configs/_module.mjs @@ -3,7 +3,6 @@ export { default as ActionSettingsConfig } from './action-settings-config.mjs'; export { default as CharacterSettings } from './character-settings.mjs'; export { default as AdversarySettings } from './adversary-settings.mjs'; export { default as CompanionSettings } from './companion-settings.mjs'; -export { default as SettingActiveEffectConfig } from './setting-active-effect-config.mjs'; export { default as SettingFeatureConfig } from './setting-feature-config.mjs'; export { default as EnvironmentSettings } from './environment-settings.mjs'; export { default as ActiveEffectConfig } from './activeEffectConfig.mjs'; diff --git a/module/applications/sheets-configs/action-settings-config.mjs b/module/applications/sheets-configs/action-settings-config.mjs index 91b85802..9cb866bc 100644 --- a/module/applications/sheets-configs/action-settings-config.mjs +++ b/module/applications/sheets-configs/action-settings-config.mjs @@ -55,7 +55,7 @@ export default class DHActionSettingsConfig extends DHActionBaseConfig { static async editEffect(event) { const id = event.target.closest('[data-effect-id]')?.dataset?.effectId; - const updatedEffect = await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure( + const updatedEffect = await game.system.api.applications.sheetConfigs.ActiveEffectConfig.configureSetting( this.getEffectDetails(id) ); if (!updatedEffect) return; diff --git a/module/applications/sheets-configs/activeEffectConfig.mjs b/module/applications/sheets-configs/activeEffectConfig.mjs index 339c2c86..e4a18831 100644 --- a/module/applications/sheets-configs/activeEffectConfig.mjs +++ b/module/applications/sheets-configs/activeEffectConfig.mjs @@ -287,4 +287,34 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac return submitData; } + + /** @inheritDoc */ + _processSubmitData(event, form, submitData, options) { + if (this.options.isSetting) { + // Settings should update source instead + this.document.updateSource(submitData); + this.render(); + } else { + return super._processSubmitData(event, form, submitData, options); + } + } + + /** Creates an active effect config for a setting */ + static async configureSetting(effect, options = {}) { + const document = new CONFIG.ActiveEffect.documentClass({ ...foundry.utils.duplicate(effect), _id: effect.id }); + return new Promise(resolve => { + const app = new this({ document, ...options, isSetting: true }); + app.addEventListener( + 'close', + () => { + const newEffect = app.document.toObject(true); + newEffect.id = newEffect._id; + delete newEffect._id; + resolve(newEffect); + }, + { once: true } + ); + app.render({ force: true }); + }); + } } diff --git a/module/applications/sheets-configs/setting-active-effect-config.mjs b/module/applications/sheets-configs/setting-active-effect-config.mjs deleted file mode 100644 index 12ac90d1..00000000 --- a/module/applications/sheets-configs/setting-active-effect-config.mjs +++ /dev/null @@ -1,223 +0,0 @@ -import autocomplete from 'autocompleter'; - -const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; - -export default class SettingActiveEffectConfig extends HandlebarsApplicationMixin(ApplicationV2) { - constructor(effect) { - super({}); - - this.effect = foundry.utils.deepClone(effect); - this.changeChoices = game.system.api.applications.sheetConfigs.ActiveEffectConfig.getChangeChoices(); - } - - static DEFAULT_OPTIONS = { - classes: ['daggerheart', 'sheet', 'dh-style', 'active-effect-config', 'standard-form'], - tag: 'form', - position: { - width: 560 - }, - form: { - submitOnChange: false, - closeOnSubmit: false, - handler: SettingActiveEffectConfig.#onSubmit - }, - actions: { - editImage: SettingActiveEffectConfig.#editImage, - addChange: SettingActiveEffectConfig.#addChange, - deleteChange: SettingActiveEffectConfig.#deleteChange - } - }; - - static PARTS = { - header: { template: 'systems/daggerheart/templates/sheets/activeEffect/header.hbs' }, - tabs: { template: 'templates/generic/tab-navigation.hbs' }, - details: { template: 'systems/daggerheart/templates/sheets/activeEffect/details.hbs', scrollable: [''] }, - settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' }, - changes: { - template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs', - scrollable: ['ol[data-changes]'] - }, - footer: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-form-footer.hbs' } - }; - - static TABS = { - sheet: { - tabs: [ - { id: 'details', icon: 'fa-solid fa-book' }, - { id: 'settings', icon: 'fa-solid fa-bars', label: 'DAGGERHEART.GENERAL.Tabs.settings' }, - { id: 'changes', icon: 'fa-solid fa-gears' } - ], - initial: 'details', - labelPrefix: 'EFFECT.TABS' - } - }; - - /**@inheritdoc */ - async _onFirstRender(context, options) { - await super._onFirstRender(context, options); - } - - async _prepareContext(_options) { - const context = await super._prepareContext(_options); - context.source = this.effect; - context.fields = game.system.api.documents.DhActiveEffect.schema.fields; - context.systemFields = game.system.api.data.activeEffects.BaseEffect._schema.fields; - - return context; - } - - _attachPartListeners(partId, htmlElement, options) { - super._attachPartListeners(partId, htmlElement, options); - const changeChoices = this.changeChoices; - - htmlElement.querySelectorAll('.effect-change-input').forEach(element => { - autocomplete({ - input: element, - fetch: function (text, update) { - if (!text) { - update(changeChoices); - } else { - text = text.toLowerCase(); - var suggestions = changeChoices.filter(n => n.label.toLowerCase().includes(text)); - update(suggestions); - } - }, - render: function (item, search) { - const label = game.i18n.localize(item.label); - const matchIndex = label.toLowerCase().indexOf(search); - - const beforeText = label.slice(0, matchIndex); - const matchText = label.slice(matchIndex, matchIndex + search.length); - const after = label.slice(matchIndex + search.length, label.length); - - const element = document.createElement('li'); - element.innerHTML = - `${beforeText}${matchText ? `${matchText}` : ''}${after}`.replaceAll( - ' ', - ' ' - ); - if (item.hint) { - element.dataset.tooltip = game.i18n.localize(item.hint); - } - - return element; - }, - renderGroup: function (label) { - const itemElement = document.createElement('div'); - itemElement.textContent = game.i18n.localize(label); - return itemElement; - }, - onSelect: function (item) { - element.value = `system.${item.value}`; - }, - click: e => e.fetch(), - customize: function (_input, _inputRect, container) { - container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ; - }, - minLength: 0 - }); - }); - } - - async _preparePartContext(partId, context) { - if (partId in context.tabs) context.tab = context.tabs[partId]; - switch (partId) { - case 'details': - context.statuses = CONFIG.statusEffects.map(s => ({ value: s.id, label: game.i18n.localize(s.name) })); - context.isActorEffect = false; - context.isItemEffect = true; - const useGeneric = game.settings.get( - CONFIG.DH.id, - CONFIG.DH.SETTINGS.gameSettings.appearance - ).showGenericStatusEffects; - if (!useGeneric) { - context.statuses = [ - ...context.statuses, - Object.values(CONFIG.DH.GENERAL.conditions).map(status => ({ - value: status.id, - label: game.i18n.localize(status.name) - })) - ]; - } - break; - case 'changes': - context.modes = Object.entries(CONST.ACTIVE_EFFECT_MODES).reduce((modes, [key, value]) => { - modes[value] = game.i18n.localize(`EFFECT.MODE_${key}`); - return modes; - }, {}); - - context.priorities = ActiveEffectConfig.DEFAULT_PRIORITIES; - break; - } - - return context; - } - - static async #onSubmit(_event, _form, formData) { - this.data = foundry.utils.expandObject(formData.object); - this.close(); - } - - /** - * Edit a Document image. - * @this {DocumentSheetV2} - * @type {ApplicationClickAction} - */ - static async #editImage(_event, target) { - if (target.nodeName !== 'IMG') { - throw new Error('The editImage action is available only for IMG elements.'); - } - - const attr = target.dataset.edit; - const current = foundry.utils.getProperty(this.effect, attr); - const fp = new FilePicker.implementation({ - current, - type: 'image', - callback: path => (target.src = path), - position: { - top: this.position.top + 40, - left: this.position.left + 10 - } - }); - - await fp.browse(); - } - - /** - * Add a new change to the effect's changes array. - * @this {ActiveEffectConfig} - * @type {ApplicationClickAction} - */ - static async #addChange() { - const { changes, ...rest } = foundry.utils.expandObject(new FormDataExtended(this.form).object); - const updatedChanges = Object.values(changes ?? {}); - updatedChanges.push({}); - - this.effect = { ...rest, changes: updatedChanges }; - this.render(); - } - - /** - * Delete a change from the effect's changes array. - * @this {ActiveEffectConfig} - * @type {ApplicationClickAction} - */ - static async #deleteChange(event) { - const submitData = foundry.utils.expandObject(new FormDataExtended(this.form).object); - const updatedChanges = Object.values(submitData.changes); - const row = event.target.closest('li'); - const index = Number(row.dataset.index) || 0; - updatedChanges.splice(index, 1); - - this.effect = { ...submitData, changes: updatedChanges }; - this.render(); - } - - static async configure(effect, options = {}) { - return new Promise(resolve => { - const app = new this(effect, options); - app.addEventListener('close', () => resolve(app.data), { once: true }); - app.render({ force: true }); - }); - } -} diff --git a/module/applications/sheets-configs/setting-feature-config.mjs b/module/applications/sheets-configs/setting-feature-config.mjs index fb790f7f..f90bb52f 100644 --- a/module/applications/sheets-configs/setting-feature-config.mjs +++ b/module/applications/sheets-configs/setting-feature-config.mjs @@ -147,7 +147,7 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App const effectIndex = this.move.effects.findIndex(x => x.id === id); const effect = this.move.effects[effectIndex]; const updatedEffect = - await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(effect); + await game.system.api.applications.sheetConfigs.ActiveEffectConfig.configureSetting(effect); if (!updatedEffect) return; await this.updateMove({ diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 15e83645..bddbca62 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -1,6 +1,5 @@ import DHBaseActorSheet from '../api/base-actor.mjs'; import DhDeathMove from '../../dialogs/deathMove.mjs'; -import { abilities } from '../../../config/actorConfig.mjs'; import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs'; import DhCharacterCreation from '../../characterCreation/characterCreation.mjs'; import FilterMenu from '../../ux/filter-menu.mjs'; @@ -721,35 +720,16 @@ export default class CharacterSheet extends DHBaseActorSheet { * Rolls an attribute check based on the clicked button's dataset attribute. * @type {ApplicationClickAction} */ - static async #rollAttribute(event, button) { - const abilityLabel = game.i18n.localize(abilities[button.dataset.attribute].label); - const config = { - event: event, - title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.actor.name}`, - headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { - ability: abilityLabel - }), - effects: await game.system.api.data.actions.actionsTypes.base.getEffects(this.document), - roll: { - trait: button.dataset.attribute, - type: 'trait' - }, - hasRoll: true, - actionType: 'action', - headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.actor.name}`, - title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { - ability: abilityLabel - }) - }; - const result = await this.document.diceRoll(config); + static async #rollAttribute(_event, button) { + const result = await this.document.rollTrait(button.dataset.attribute); if (!result) return; /* This could be avoided by baking config.costs into config.resourceUpdates. Didn't feel like messing with it at the time */ const costResources = result.costs?.filter(x => x.enabled).map(cost => ({ ...cost, value: -cost.value, total: -cost.total })) || {}; - config.resourceUpdates.addResources(costResources); - await config.resourceUpdates.updateResources(); + result.resourceUpdates.addResources(costResources); + await result.resourceUpdates.updateResources(); } //TODO: redo toggleEquipItem method diff --git a/module/applications/sheets/actors/party.mjs b/module/applications/sheets/actors/party.mjs index 20792141..c5e77112 100644 --- a/module/applications/sheets/actors/party.mjs +++ b/module/applications/sheets/actors/party.mjs @@ -35,9 +35,7 @@ export default class Party extends DHBaseActorSheet { refeshActions: Party.#refeshActions, triggerRest: Party.#triggerRest, tagTeamRoll: Party.#tagTeamRoll, - groupRoll: Party.#groupRoll, - selectRefreshable: DaggerheartMenu.selectRefreshable, - refreshActors: DaggerheartMenu.refreshActors + groupRoll: Party.#groupRoll }, dragDrop: [{ dragSelector: '[data-item-id]', dropSelector: null }] }; @@ -120,6 +118,7 @@ export default class Party extends DHBaseActorSheet { secrets: this.document.isOwner, relativeTo: this.document }); + context.tagTeamActive = Boolean(this.document.system.tagTeam.initiator); } /** @@ -258,11 +257,7 @@ export default class Party extends DHBaseActorSheet { } static async #tagTeamRoll() { - new game.system.api.applications.dialogs.TagTeamDialog( - this.document.system.partyMembers.filter(x => Party.DICE_ROLL_ACTOR_TYPES.includes(x.type)) - ).render({ - force: true - }); + new game.system.api.applications.dialogs.TagTeamDialog(this.document).render({ force: true }); } static async #groupRoll(_params) { diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index 2b489f58..e29498e6 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -190,7 +190,24 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo const target = event.target.closest('[data-die-index]'); if (target.dataset.type === 'damage') { - game.system.api.dice.DamageRoll.reroll(target, message); + const { damageType, part, dice, result } = target.dataset; + const damagePart = message.system.damage[damageType].parts[part]; + const { parsedRoll, rerolledDice } = await game.system.api.dice.DamageRoll.reroll(damagePart, dice, result); + const damageParts = message.system.damage[damageType].parts.map((damagePart, index) => { + if (index !== Number(part)) return damagePart; + return { + ...damagePart, + total: parsedRoll.total, + dice: rerolledDice + }; + }); + const updateMessage = game.messages.get(message._id); + await updateMessage.update({ + [`system.damage.${damageType}`]: { + total: parsedRoll.total, + parts: damageParts + } + }); } else { let originalRoll_parsed = message.rolls.map(roll => JSON.parse(roll))[0]; const rollClass = @@ -204,20 +221,16 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo 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, message); + 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] }); - - Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll }); - await game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.Refresh, - data: { - refreshType: RefreshType.TagTeamRoll - } - }); } } diff --git a/module/applications/ui/itemBrowser.mjs b/module/applications/ui/itemBrowser.mjs index 0c3a77fa..2d2e8cdc 100644 --- a/module/applications/ui/itemBrowser.mjs +++ b/module/applications/ui/itemBrowser.mjs @@ -253,8 +253,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { for (const item of this.items) { if (['weapon', 'armor'].includes(item.type)) { item.system.enrichedTags = await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/sheets/global/partials/item-tags.hbs', - item.system + 'systems/daggerheart/templates/ui/itemBrowser/item-tags.hbs', + { item: item.system } ); } item.system.enrichedDescription = diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 391f0699..c15cae56 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -959,6 +959,21 @@ export const sceneRangeMeasurementSetting = { } }; +export const tagTeamRollTypes = { + trait: { + id: 'trait', + label: 'DAGGERHEART.CONFIG.TagTeamRollTypes.trait' + }, + ability: { + id: 'ability', + label: 'DAGGERHEART.CONFIG.TagTeamRollTypes.ability' + }, + damageAbility: { + id: 'damageAbility', + label: 'DAGGERHEART.CONFIG.TagTeamRollTypes.damageAbility' + } +}; + export const baseActiveEffectModes = { custom: { id: 'custom', diff --git a/module/config/hooksConfig.mjs b/module/config/hooksConfig.mjs index 61ba594c..8d04be6d 100644 --- a/module/config/hooksConfig.mjs +++ b/module/config/hooksConfig.mjs @@ -1,4 +1,5 @@ export const hooksConfig = { effectDisplayToggle: 'DHEffectDisplayToggle', - lockedTooltipDismissed: 'DHLockedTooltipDismissed' + lockedTooltipDismissed: 'DHLockedTooltipDismissed', + tagTeamStart: 'DHTagTeamRollStart' }; diff --git a/module/config/settingsConfig.mjs b/module/config/settingsConfig.mjs index 0b28f0ab..52a316cf 100644 --- a/module/config/settingsConfig.mjs +++ b/module/config/settingsConfig.mjs @@ -34,7 +34,6 @@ export const gameSettings = { LevelTiers: 'LevelTiers', Countdowns: 'Countdowns', LastMigrationVersion: 'LastMigrationVersion', - TagTeamRoll: 'TagTeamRoll', SpotlightRequestQueue: 'SpotlightRequestQueue', CompendiumBrowserSettings: 'CompendiumBrowserSettings' }; diff --git a/module/data/_module.mjs b/module/data/_module.mjs index 52fa689e..43ff7807 100644 --- a/module/data/_module.mjs +++ b/module/data/_module.mjs @@ -1,9 +1,9 @@ export { default as DhCombat } from './combat.mjs'; export { default as DhCombatant } from './combatant.mjs'; -export { default as DhTagTeamRoll } from './tagTeamRoll.mjs'; export { default as DhRollTable } from './rollTable.mjs'; export { default as RegisteredTriggers } from './registeredTriggers.mjs'; export { default as CompendiumBrowserSettings } from './compendiumBrowserSettings.mjs'; +export { default as TagTeamData } from './tagTeamData.mjs'; export * as countdowns from './countdowns.mjs'; export * as actions from './action/_module.mjs'; diff --git a/module/data/action/attackAction.mjs b/module/data/action/attackAction.mjs index 3671613d..a2d47309 100644 --- a/module/data/action/attackAction.mjs +++ b/module/data/action/attackAction.mjs @@ -50,9 +50,8 @@ export default class DHAttackAction extends DHDamageAction { async use(event, options) { const result = await super.use(event, options); - if (!result.message) return; - if (result.message.system.action.roll?.type === 'attack') { + if (result.message?.system.action.roll?.type === 'attack') { const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns; await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.characterAttack.id); } diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index 57e34d11..01139b30 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -207,10 +207,10 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel * @param {Event} event Event from the button used to trigger the Action * @returns {object} */ - async use(event) { + async use(event, configOptions = {}) { if (!this.actor) throw new Error("An Action can't be used outside of an Actor context."); - let config = this.prepareConfig(event); + let config = this.prepareConfig(event, configOptions); if (!config) return; config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(this.actor, this.item); @@ -231,7 +231,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return; - if (this.chatDisplay && !config.actionChatMessageHandled) await this.toChat(); + if (this.chatDisplay && !config.skips.createMessage && !config.actionChatMessageHandled) await this.toChat(); return config; } @@ -241,7 +241,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel * @param {Event} event Event from the button used to trigger the Action * @returns {object} */ - prepareBaseConfig(event) { + prepareBaseConfig(event, configOptions = {}) { const isActor = this.item instanceof CONFIG.Actor.documentClass; const actionTitle = game.i18n.localize(this.name); const itemTitle = isActor || this.item.name === actionTitle ? '' : `${this.item.name} - `; @@ -268,7 +268,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel data: this.getRollData(), evaluate: this.hasRoll, resourceUpdates: new ResourceUpdateMap(this.actor), - targetUuid: this.targetUuid + targetUuid: this.targetUuid, + ...configOptions }; DHBaseAction.applyKeybindings(config); @@ -280,8 +281,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel * @param {Event} event Event from the button used to trigger the Action * @returns {object} */ - prepareConfig(event) { - const config = this.prepareBaseConfig(event); + prepareConfig(event, configOptions = {}) { + const config = this.prepareBaseConfig(event, configOptions); for (const clsField of Object.values(this.schema.fields)) { if (clsField?.prepareConfig) if (clsField.prepareConfig.call(this, config) === false) return false; } @@ -356,11 +357,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel } get hasDamage() { - return !foundry.utils.isEmpty(this.damage?.parts) && this.type !== 'healing'; + return Boolean(Object.keys(this.damage?.parts ?? {}).length) && this.type !== 'healing'; } get hasHealing() { - return !foundry.utils.isEmpty(this.damage?.parts) && this.type === 'healing'; + return Boolean(Object.keys(this.damage?.parts ?? {}).length) && this.type === 'healing'; } get hasSave() { diff --git a/module/data/actor/base.mjs b/module/data/actor/base.mjs index 9f571edb..89ba5db2 100644 --- a/module/data/actor/base.mjs +++ b/module/data/actor/base.mjs @@ -189,19 +189,6 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel { return true; } - async _preDelete() { - /* Clear all partyMembers from tagTeam setting.*/ - /* Revisit this when tagTeam is improved for many parties */ - if (this.parent.parties.size > 0) { - const tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); - await tagTeam.updateSource({ - initiator: this.parent.id === tagTeam.initiator ? null : tagTeam.initiator, - members: Object.keys(tagTeam.members).find(x => x === this.parent.id) ? { [this.parent.id]: _del } : {} - }); - await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, tagTeam); - } - } - async _preUpdate(changes, options, userId) { const allowed = await super._preUpdate(changes, options, userId); if (allowed === false) return; diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 3d8580f0..e69c4dc1 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -822,7 +822,6 @@ export default class DhCharacter extends DhCreature { static migrateData(source) { if (typeof source.scars === 'object') source.scars = 0; - if (source.resources?.hope?.max) source.scars = Math.max(6 - source.resources.hope.max, 0); return super.migrateData(source); } diff --git a/module/data/actor/environment.mjs b/module/data/actor/environment.mjs index 0aaf8eb0..e06f038c 100644 --- a/module/data/actor/environment.mjs +++ b/module/data/actor/environment.mjs @@ -75,10 +75,6 @@ export default class DhEnvironment extends BaseDataActor { ); scene.update({ 'flags.daggerheart.sceneEnvironments': newSceneEnvironments }).then(() => { Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Scene }); - game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.Refresh, - data: { refreshType: RefreshType.TagTeamRoll } - }); }); } } diff --git a/module/data/actor/party.mjs b/module/data/actor/party.mjs index 3eddf235..2c797803 100644 --- a/module/data/actor/party.mjs +++ b/module/data/actor/party.mjs @@ -1,5 +1,6 @@ import BaseDataActor from './base.mjs'; import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs'; +import TagTeamData from '../tagTeamData.mjs'; export default class DhParty extends BaseDataActor { /**@inheritdoc */ @@ -14,7 +15,8 @@ export default class DhParty extends BaseDataActor { handfuls: new fields.NumberField({ initial: 1, integer: true }), bags: new fields.NumberField({ initial: 0, integer: true }), chests: new fields.NumberField({ initial: 0, integer: true }) - }) + }), + tagTeam: new fields.EmbeddedDataField(TagTeamData) }; } @@ -40,23 +42,6 @@ export default class DhParty extends BaseDataActor { } } - async _preDelete() { - /* Clear all partyMembers from tagTeam setting.*/ - /* Revisit this when tagTeam is improved for many parties */ - const tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); - await tagTeam.updateSource({ - initiator: this.partyMembers.some(x => x.id === tagTeam.initiator) ? null : tagTeam.initiator, - members: Object.keys(tagTeam.members).reduce((acc, key) => { - if (this.partyMembers.find(x => x.id === key)) { - acc[key] = _del; - } - - return acc; - }, {}) - }); - await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, tagTeam); - } - _onDelete(options, userId) { super._onDelete(options, userId); diff --git a/module/data/fields/action/damageField.mjs b/module/data/fields/action/damageField.mjs index e79a91a2..b7ef852e 100644 --- a/module/data/fields/action/damageField.mjs +++ b/module/data/fields/action/damageField.mjs @@ -50,9 +50,9 @@ export default class DamageField extends fields.SchemaField { formulas = DamageField.formatFormulas.call(this, formulas, config); const damageConfig = { + dialog: {}, ...config, roll: formulas, - dialog: {}, data: this.getRollData() }; delete damageConfig.evaluate; diff --git a/module/data/fields/action/effectsField.mjs b/module/data/fields/action/effectsField.mjs index 6afd470b..1a003e2b 100644 --- a/module/data/fields/action/effectsField.mjs +++ b/module/data/fields/action/effectsField.mjs @@ -27,7 +27,7 @@ export default class EffectsField extends fields.ArrayField { static async execute(config, targets = null, force = false) { if (!config.hasEffect) return; let message = config.message ?? ui.chat.collection.get(config.parent?._id); - if (!message) { + if (!message && !config.skips.createMessage) { const roll = new CONFIG.Dice.daggerheart.DHRoll(''); roll._evaluated = true; message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config); diff --git a/module/data/fields/action/saveField.mjs b/module/data/fields/action/saveField.mjs index c9030036..0629353e 100644 --- a/module/data/fields/action/saveField.mjs +++ b/module/data/fields/action/saveField.mjs @@ -38,7 +38,7 @@ export default class SaveField extends fields.SchemaField { if (!config.hasSave) return; let message = config.message ?? ui.chat.collection.get(config.parent?._id); - if (!message) { + if (!message && !config.skips.createMessage) { const roll = new CONFIG.Dice.daggerheart.DHRoll(''); roll._evaluated = true; message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config); diff --git a/module/data/fields/actorField.mjs b/module/data/fields/actorField.mjs index 69ba6bf1..25e04317 100644 --- a/module/data/fields/actorField.mjs +++ b/module/data/fields/actorField.mjs @@ -82,6 +82,24 @@ class ResourcesField extends fields.TypedObjectField { } return data; } + + /** + * Foundry bar attributes are unable to handle finding the schema field nor the label normally. + * This returns the element if its a valid resource key and overwrites the element's label for that retrieval. + */ + _getField(path) { + if (path.length === 0) return this; + const first = path.shift(); + if (first === this.element.name) return this.element_getField(path); + + const resources = CONFIG.DH.RESOURCE[this.actorType].all; + if (first in resources) { + this.element.label = resources[first].label; + return this.element._getField(path); + } + + return undefined; + } } export { attributeField, ResourcesField, stressDamageReductionRule, bonusField }; diff --git a/module/data/tagTeamData.mjs b/module/data/tagTeamData.mjs new file mode 100644 index 00000000..25158606 --- /dev/null +++ b/module/data/tagTeamData.mjs @@ -0,0 +1,47 @@ +export default class TagTeamData extends foundry.abstract.DataModel { + static defineSchema() { + const fields = foundry.data.fields; + + return { + initiator: new fields.SchemaField( + { + memberId: new fields.StringField({ + required: true, + label: 'DAGGERHEART.APPLICATIONS.TagTeamSelect.FIELDS.initiator.memberId.label' + }), + cost: new fields.NumberField({ + integer: true, + initial: 3, + label: 'DAGGERHEART.APPLICATIONS.TagTeamSelect.FIELDS.initiator.cost.label' + }) + }, + { nullable: true, initial: null } + ), + members: new fields.TypedObjectField(new fields.EmbeddedDataField(MemberData)) + }; + } +} + +export class MemberData extends foundry.abstract.DataModel { + static defineSchema() { + const fields = foundry.data.fields; + + return { + name: new fields.StringField({ required: true }), + img: new fields.StringField({ required: true }), + rollType: new fields.StringField({ + required: true, + choices: CONFIG.DH.GENERAL.tagTeamRollTypes, + initial: CONFIG.DH.GENERAL.tagTeamRollTypes.trait.id, + label: 'Roll Type' + }), + rollChoice: new fields.StringField({ nullable: true, initial: null }), + rollData: new fields.JSONField({ nullable: true, initial: null }), + selected: new fields.BooleanField({ initial: false }) + }; + } + + get roll() { + return this.rollData ? CONFIG.Dice.daggerheart.DualityRoll.fromData(this.rollData) : null; + } +} diff --git a/module/data/tagTeamRoll.mjs b/module/data/tagTeamRoll.mjs deleted file mode 100644 index de71a11b..00000000 --- a/module/data/tagTeamRoll.mjs +++ /dev/null @@ -1,20 +0,0 @@ -import { DhCharacter } from './actor/_module.mjs'; - -export default class DhTagTeamRoll extends foundry.abstract.DataModel { - static defineSchema() { - const fields = foundry.data.fields; - - return { - initiator: new fields.SchemaField({ - id: new fields.StringField({ nullable: true, initial: null }), - cost: new fields.NumberField({ integer: true, min: 0, initial: 3 }) - }), - members: new fields.TypedObjectField( - new fields.SchemaField({ - messageId: new fields.StringField({ required: true, nullable: true, initial: null }), - selected: new fields.BooleanField({ required: true, initial: false }) - }) - ) - }; - } -} diff --git a/module/dice/damageRoll.mjs b/module/dice/damageRoll.mjs index b322aae7..8cd3caac 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -1,6 +1,5 @@ import DamageDialog from '../applications/dialogs/damageDialog.mjs'; import { parseRallyDice } from '../helpers/utils.mjs'; -import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs'; import DHRoll from './dhRoll.mjs'; export default class DamageRoll extends DHRoll { @@ -281,10 +280,7 @@ export default class DamageRoll extends DHRoll { return mods; } - static async reroll(target, message) { - const { damageType, part, dice, result } = target.dataset; - const rollPart = message.system.damage[damageType].parts[part]; - + static async reroll(rollPart, dice, result) { let diceIndex = 0; let parsedRoll = game.system.api.dice.DamageRoll.fromData({ ...rollPart.roll, @@ -353,29 +349,6 @@ export default class DamageRoll extends DHRoll { }; }); - const updateMessage = game.messages.get(message._id); - const damageParts = updateMessage.system.damage[damageType].parts.map((damagePart, index) => { - if (index !== Number(part)) return damagePart; - return { - ...rollPart, - total: parsedRoll.total, - dice: rerolledDice - }; - }); - await updateMessage.update({ - [`system.damage.${damageType}`]: { - ...updateMessage, - total: parsedRoll.total, - parts: damageParts - } - }); - - Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll }); - await game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.Refresh, - data: { - refreshType: RefreshType.TagTeamRoll - } - }); + return { parsedRoll, rerolledDice }; } } diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index ec2ea0c7..b74adaf3 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -21,6 +21,9 @@ export default class DHRoll extends Roll { static async build(config = {}, message = {}) { const roll = await this.buildConfigure(config, message); if (!roll) return; + + if (config.skips?.createMessage) config.messageRoll = roll; + await this.buildEvaluate(roll, config, (message = {})); await this.buildPost(roll, config, (message = {})); return config; @@ -30,12 +33,6 @@ export default class DHRoll extends Roll { config.hooks = [...this.getHooks(), '']; config.dialog ??= {}; - const actorIdSplit = config.source?.actor?.split('.'); - if (actorIdSplit) { - const tagTeamSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); - config.tagTeamSelected = Boolean(tagTeamSettings.members[actorIdSplit[actorIdSplit.length - 1]]); - } - for (const hook of config.hooks) { if (Hooks.call(`${CONFIG.DH.id}.preRoll${hook.capitalize()}`, config, message) === false) return null; } @@ -146,6 +143,7 @@ export default class DHRoll extends Roll { return foundry.applications.handlebars.renderTemplate(template, { ...chatData, parent: chatData.parent, + targetMode: chatData.targetMode, metagamingSettings }); } diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index eb15fa5e..03035f68 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -374,9 +374,9 @@ export default class DualityRoll extends D20Roll { } } - static async reroll(rollString, target, message) { - let parsedRoll = game.system.api.dice.DualityRoll.fromData({ ...rollString, evaluated: false }); - const term = parsedRoll.terms[target.dataset.dieIndex]; + static async reroll(rollBase, dieIndex, diceType) { + 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(); @@ -393,35 +393,35 @@ export default class DualityRoll extends D20Roll { options: { appearance: {} } }; - const diceSoNicePresets = await getDiceSoNicePresets(result, `d${term._faces}`, `d${term._faces}`); - const type = target.dataset.type; - if (diceSoNicePresets[type]) { - diceSoNiceRoll.dice[0].options = diceSoNicePresets[type]; + 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: message.system.targets, + targets: parsedRoll.options.targets ?? [], roll: { - advantage: message.system.roll.advantage?.type, - difficulty: message.system.roll.difficulty ? Number(message.system.roll.difficulty) : null + 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 tagTeamSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); - - const actor = message.system.source.actor ? await foundry.utils.fromUuid(message.system.source.actor) : null; + const actor = parsedRoll.options.source.actor + ? await foundry.utils.fromUuid(parsedRoll.options.source.actor) + : null; const config = { - source: { actor: message.system.source.actor ?? '' }, - targets: message.system.targets, - tagTeamSelected: Object.values(tagTeamSettings.members).some(x => x.messageId === message._id), + source: { actor: parsedRoll.options.source.actor ?? '' }, + targets: parsedRoll.targets, roll: newRoll, - rerolledRoll: message.system.roll, + rerolledRoll: parsedRoll.roll, resourceUpdates: new ResourceUpdateMap(actor) }; diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 7326995f..023beaa0 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -4,6 +4,7 @@ import DHFeature from '../data/item/feature.mjs'; import { createScrollText, damageKeyToNumber, getDamageKey } from '../helpers/utils.mjs'; import DhCompanionLevelUp from '../applications/levelup/companionLevelup.mjs'; import { ResourceUpdateMap } from '../data/action/baseAction.mjs'; +import { abilities } from '../config/actorConfig.mjs'; export default class DhpActor extends Actor { parties = new Set(); @@ -509,6 +510,30 @@ export default class DhpActor extends Actor { return await rollClass.build(config); } + async rollTrait(trait, options = {}) { + const abilityLabel = game.i18n.localize(abilities[trait].label); + const config = { + event: event, + title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.name}`, + headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { + ability: abilityLabel + }), + effects: await game.system.api.data.actions.actionsTypes.base.getEffects(this), + roll: { + trait: trait, + type: 'trait' + }, + hasRoll: true, + actionType: 'action', + headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.name}`, + title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { + ability: abilityLabel + }), + ...options + }; + return await this.diceRoll(config); + } + get rollClass() { return CONFIG.Dice.daggerheart[['character', 'companion'].includes(this.type) ? 'DualityRoll' : 'D20Roll']; } diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index 53921329..8b094678 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -177,14 +177,6 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(actor, item); await this.system.action.workflow.get('damage')?.execute(config, this._id, true); } - - Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll }); - await game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.Refresh, - data: { - refreshType: RefreshType.TagTeamRoll - } - }); } async onApplyDamage(event) { diff --git a/module/documents/tooltipManager.mjs b/module/documents/tooltipManager.mjs index 4793f1f7..6cfecb00 100644 --- a/module/documents/tooltipManager.mjs +++ b/module/documents/tooltipManager.mjs @@ -224,7 +224,7 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti if (locked || element.dataset.hasOwnProperty('locked')) this.lockTooltip(); } - _setAnchor(direction, options) { + _setAnchor(direction, options = {}) { const directions = this.constructor.TOOLTIP_DIRECTIONS; const pad = this.constructor.TOOLTIP_MARGIN_PX; const pos = this.element.getBoundingClientRect(); diff --git a/module/helpers/handlebarsHelper.mjs b/module/helpers/handlebarsHelper.mjs index 1c47f8dc..7f30d970 100644 --- a/module/helpers/handlebarsHelper.mjs +++ b/module/helpers/handlebarsHelper.mjs @@ -49,7 +49,8 @@ export default class RegisterHandlebarsHelpers { } static damageSymbols(damageParts) { - const symbols = [...new Set(damageParts.map(x => x.type))].map(p => CONFIG.DH.GENERAL.damageTypes[p].icon); + const allTypes = [...new Set([...damageParts].flatMap(x => Array.from(x.type)))]; + const symbols = allTypes.map(p => CONFIG.DH.GENERAL.damageTypes[p].icon); return new Handlebars.SafeString(Array.from(symbols).map(symbol => ``)); } diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index bb05fc39..59ebbbb3 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -528,7 +528,8 @@ export function expireActiveEffects(actor, allowedTypes = null) { export async function getCritDamageBonus(formula) { const critRoll = new Roll(formula); - return critRoll.dice.reduce((acc, dice) => acc + dice.faces * dice.number, 0); + await critRoll.evaluate(); + return critRoll.dice.reduce((acc, dice) => acc + dice.faces * dice.results.filter(r => r.active).length, 0); } export function htmlToText(html) { diff --git a/module/systemRegistration/handlebars.mjs b/module/systemRegistration/handlebars.mjs index 058b1b56..36df8b54 100644 --- a/module/systemRegistration/handlebars.mjs +++ b/module/systemRegistration/handlebars.mjs @@ -39,6 +39,7 @@ export const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/ui/tooltip/parts/tooltipChips.hbs', 'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs', 'systems/daggerheart/templates/dialogs/downtime/activities.hbs', + 'systems/daggerheart/templates/dialogs/tagTeamDialog/parts/tagTeamDamageParts.hbs', 'systems/daggerheart/templates/dialogs/dice-roll/costSelection.hbs', 'systems/daggerheart/templates/ui/chat/parts/roll-part.hbs', 'systems/daggerheart/templates/ui/chat/parts/description-part.hbs', diff --git a/module/systemRegistration/migrations.mjs b/module/systemRegistration/migrations.mjs index 7426cff4..df837ef4 100644 --- a/module/systemRegistration/migrations.mjs +++ b/module/systemRegistration/migrations.mjs @@ -193,7 +193,7 @@ export async function runMigrations() { } if (foundry.utils.isNewerVersion('1.2.7', lastMigrationVersion)) { - const tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); + const tagTeam = game.settings.get(CONFIG.DH.id, 'TagTeamRoll'); const initatorMissing = tagTeam.initiator && !game.actors.some(actor => actor.id === tagTeam.initiator); const missingMembers = Object.keys(tagTeam.members).reduce((acc, id) => { if (!game.actors.some(actor => actor.id === id)) { @@ -206,7 +206,7 @@ export async function runMigrations() { initiator: initatorMissing ? null : tagTeam.initiator, members: missingMembers }); - await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, tagTeam); + await game.settings.set(CONFIG.DH.id, 'TagTeamRoll', tagTeam); lastMigrationVersion = '1.2.7'; } diff --git a/module/systemRegistration/settings.mjs b/module/systemRegistration/settings.mjs index e7ec37f5..658d2bd1 100644 --- a/module/systemRegistration/settings.mjs +++ b/module/systemRegistration/settings.mjs @@ -15,7 +15,7 @@ import { DhMetagamingSettings, DhVariantRuleSettings } from '../applications/settings/_module.mjs'; -import { CompendiumBrowserSettings, DhTagTeamRoll } from '../data/_module.mjs'; +import { CompendiumBrowserSettings } from '../data/_module.mjs'; export const registerDHSettings = () => { registerMenuSettings(); @@ -157,12 +157,6 @@ const registerNonConfigSettings = () => { type: DhCountdowns }); - game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, { - scope: 'world', - config: false, - type: DhTagTeamRoll - }); - game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings, { scope: 'world', config: false, diff --git a/module/systemRegistration/socket.mjs b/module/systemRegistration/socket.mjs index 173ef02b..fb152959 100644 --- a/module/systemRegistration/socket.mjs +++ b/module/systemRegistration/socket.mjs @@ -15,6 +15,9 @@ export function handleSocketEvent({ action = null, data = {} } = {}) { case socketEvent.DowntimeTrigger: Party.downtimeMoveQuery(data); break; + case socketEvent.TagTeamStart: + Hooks.callAll(CONFIG.DH.HOOKS.hooksConfig.tagTeamStart, data); + break; } } @@ -22,7 +25,8 @@ export const socketEvent = { GMUpdate: 'DhGMUpdate', Refresh: 'DhRefresh', DhpFearUpdate: 'DhFearUpdate', - DowntimeTrigger: 'DowntimeTrigger' + DowntimeTrigger: 'DowntimeTrigger', + TagTeamStart: 'DhTagTeamStart' }; export const GMUpdateEvent = { diff --git a/src/packs/classes/feature_Strange_Patterns_6YsfFjmCGuFYVhT4.json b/src/packs/classes/feature_Strange_Patterns_6YsfFjmCGuFYVhT4.json index ce1f499f..6939ff7f 100644 --- a/src/packs/classes/feature_Strange_Patterns_6YsfFjmCGuFYVhT4.json +++ b/src/packs/classes/feature_Strange_Patterns_6YsfFjmCGuFYVhT4.json @@ -85,7 +85,7 @@ { "trigger": "dualityRoll", "triggeringActorType": "self", - "command": "/* Ignore if it's a TagTeam roll */\nconst tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);\nif (tagTeam.members[actor.id]) return;\n\n/* Check if there's a Strange Pattern match */\nconst dice = [roll.dFear.total, roll.dHope.total];\nconst resource = this.parent.resource?.diceStates ? Object.values(this.parent.resource.diceStates).map(x => x.value)[0] : null;\nconst nrMatches = dice.filter(x => x === resource).length;\n\nif (!nrMatches) return;\n\n/* Create a dialog to choose Hope or Stress - or to cancel*/\nconst content = `\n