import { ResourceUpdateMap } from '../../data/action/baseAction.mjs'; import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import Party from '../sheets/actors/party.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; export default class GroupRollDialog extends HandlebarsApplicationMixin(ApplicationV2) { constructor(party) { super(); 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: true, owned: member.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER) })); this.mainCharacter = null; this.openForAllPlayers = true; this.tabGroups.application = Object.keys(party.system.groupRoll.participants).length ? 'groupRoll' : 'initialization'; Hooks.on(socketEvent.Refresh, this.groupRollRefresh.bind()); } get title() { return game.i18n.localize('DAGGERHEART.APPLICATIONS.GroupRoll.title'); } static DEFAULT_OPTIONS = { tag: 'form', id: 'GroupRollDialog', classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'group-roll-dialog'], position: { width: 550, height: 'auto' }, actions: { toggleSelectMember: this.#toggleSelectMember, startGroupRoll: this.#startGroupRoll, makeRoll: this.#makeRoll, removeRoll: this.#removeRoll, rerollDice: this.#rerollDice, makeMainCharacterRoll: this.#makeMainCharacterRoll, removeMainCharacterRoll: this.#removeMainCharacterRoll, rerollMainCharacterDice: this.#rerollMainCharacterDice, markSuccessfull: this.#markSuccessfull, cancelRoll: this.#onCancelRoll, finishRoll: this.#finishRoll }, form: { handler: this.updateData, submitOnChange: true, closeOnSubmit: false } }; static PARTS = { initialization: { id: 'initialization', template: 'systems/daggerheart/templates/dialogs/groupRollDialog/initialization.hbs' }, mainCharacter: { id: 'mainCharacter', template: 'systems/daggerheart/templates/dialogs/groupRollDialog/groupRollMainCharacter.hbs' }, groupRoll: { id: 'groupRoll', template: 'systems/daggerheart/templates/dialogs/groupRollDialog/groupRoll.hbs' }, footer: { id: 'footer', template: 'systems/daggerheart/templates/dialogs/groupRollDialog/footer.hbs' } }; /** @inheritdoc */ static TABS = { application: { tabs: [{ id: 'initialization' }, { id: 'groupRoll' }] } }; _attachPartListeners(partId, htmlElement, options) { super._attachPartListeners(partId, htmlElement, options); htmlElement .querySelector('.main-character-field') ?.addEventListener('input', this.updateMainCharacterField.bind(this)); } _configureRenderParts(options) { const { initialization, mainCharacter, groupRoll, footer } = super._configureRenderParts(options); const augmentedParts = { initialization }; for (const memberKey of Object.keys(this.party.system.groupRoll.aidingCharacters)) { augmentedParts[memberKey] = { id: memberKey, template: 'systems/daggerheart/templates/dialogs/groupRollDialog/groupRollMember.hbs' }; } augmentedParts.mainCharacter = mainCharacter; augmentedParts.groupRoll = groupRoll; augmentedParts.footer = footer; return augmentedParts; } /**@inheritdoc */ async _onRender(context, options) { await super._onRender(context, options); if (this.element.querySelector('.team-container')) return; if (this.tabGroups.application !== this.constructor.PARTS.initialization.id) { const initializationPart = this.element.querySelector('.initialization-container'); initializationPart.insertAdjacentHTML('afterend', '
'); initializationPart.insertAdjacentHTML( 'afterend', `
${game.i18n.localize('Aiding Characters')}
` ); const teamContainer = this.element.querySelector('.team-container'); for (const memberContainer of this.element.querySelectorAll('.team-member-container')) teamContainer.appendChild(memberContainer); } } async _prepareContext(_options) { const context = await super._prepareContext(_options); context.isGM = game.user.isGM; context.isEditable = this.getIsEditable(); context.fields = this.party.system.schema.fields.groupRoll.fields; context.data = this.party.system.groupRoll; context.traitOptions = CONFIG.DH.ACTOR.abilities; context.members = {}; context.allHaveRolled = Object.keys(context.data.participants).every(key => { const data = context.data.participants[key]; return Boolean(data.rollData); }); return context; } async _preparePartContext(partId, context, options) { const partContext = await super._preparePartContext(partId, context, options); partContext.partId = partId; switch (partId) { case 'initialization': partContext.groupRollFields = this.party.system.schema.fields.groupRoll.fields; partContext.memberSelection = this.partyMembers; const selectedMembers = partContext.memberSelection.filter(x => x.selected); partContext.selectedMainCharacter = this.mainCharacter; partContext.selectedMainCharacterOptions = selectedMembers .filter(actor => actor.owned) .map(x => ({ value: x.id, label: x.name })); partContext.selectedMainCharacterDisabled = !selectedMembers.length; partContext.canStartGroupRoll = selectedMembers.length > 1 && this.mainCharacter?.memberId; partContext.openForAllPlayers = this.openForAllPlayers; break; case 'mainCharacter': partContext.mainCharacter = this.getRollCharacterData(this.party.system.groupRoll.mainCharacter); break; case 'groupRoll': const leader = this.party.system.groupRoll.mainCharacter; partContext.hasRolled = leader?.rollData || Object.values(this.party.system.groupRoll?.aidingCharacters ?? {}).some( x => x.successfull !== null ); const { modifierTotal, modifiers } = Object.values(this.party.system.groupRoll.aidingCharacters).reduce( (acc, curr) => { const modifier = curr.successfull === true ? 1 : curr.successfull === false ? -1 : null; if (modifier) { acc.modifierTotal += modifier; acc.modifiers.push(modifier); } return acc; }, { modifierTotal: 0, modifiers: [] } ); const mainCharacterTotal = leader?.rollData ? leader.roll.total : null; partContext.groupRoll = { totalLabel: leader?.rollData ? game.i18n.format('DAGGERHEART.GENERAL.withThing', { thing: leader.roll.totalLabel }) : null, totalDualityClass: leader?.roll?.isCritical ? 'critical' : leader?.roll?.withHope ? 'hope' : 'fear', total: mainCharacterTotal + modifierTotal, mainCharacterTotal, modifiers }; break; case 'footer': partContext.canFinishRoll = Boolean(this.party.system.groupRoll.mainCharacter?.rollData) && Object.values(this.party.system.groupRoll.aidingCharacters).every(x => x.successfull !== null); break; } if (Object.keys(this.party.system.groupRoll.aidingCharacters).includes(partId)) { const characterData = this.party.system.groupRoll.aidingCharacters[partId]; partContext.members[partId] = this.getRollCharacterData(characterData, partId); } return partContext; } getRollCharacterData(data, partId) { if (!data) return {}; const actor = game.actors.get(data.id); return { ...data, roll: data.roll, isEditable: actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER), key: partId, readyToRoll: Boolean(data.rollChoice), hasRolled: Boolean(data.rollData) }; } static async updateData(event, _, formData) { const partyData = foundry.utils.expandObject(formData.object); this.updatePartyData(partyData, this.getUpdatingParts(event.target)); } async updatePartyData(update, updatingParts, options = { render: true }) { if (!game.users.activeGM) return ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.gmRequired')); const gmUpdate = async update => { await this.party.update(update); this.render({ parts: updatingParts }); game.socket.emit(`system.${CONFIG.DH.id}`, { action: socketEvent.Refresh, data: { refreshType: RefreshType.GroupRoll, action: 'refresh', parts: updatingParts } }); }; await emitAsGM( GMUpdateEvent.UpdateDocument, gmUpdate, update, this.party.uuid, options.render ? { refreshType: RefreshType.GroupRoll, action: 'refresh', parts: updatingParts } : undefined ); } getUpdatingParts(target) { const { initialization, mainCharacter, groupRoll, footer } = this.constructor.PARTS; const isInitialization = this.tabGroups.application === initialization.id; const updatingMember = target.closest('.team-member-container')?.dataset?.memberKey; const updatingMainCharacter = target.closest('.main-character-outer-container'); return [ ...(isInitialization ? [initialization.id] : []), ...(updatingMember ? [updatingMember] : []), ...(updatingMainCharacter ? [mainCharacter.id] : []), ...(!isInitialization ? [groupRoll.id, footer.id] : []) ]; } getIsEditable() { return this.party.system.partyMembers.some(actor => { const selected = Boolean(this.party.system.groupRoll.participants[actor.id]); return selected && actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER); }); } groupRollRefresh = ({ refreshType, action, parts }) => { if (refreshType !== RefreshType.GroupRoll) return; switch (action) { case 'startGroupRoll': this.tabGroups.application = 'groupRoll'; break; case 'refresh': this.render({ parts }); 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.groupRollRefresh); return super.close(options); } //#region Initialization static #toggleSelectMember(_, button) { const member = this.partyMembers.find(x => x.id === button.dataset.id); member.selected = !member.selected; this.render(); } updateMainCharacterField(event) { if (!this.mainCharacter) this.mainCharacter = {}; this.mainCharacter.memberId = event.target.value; this.render(); } static async #startGroupRoll() { const mainCharacter = this.partyMembers.find(x => x.id === this.mainCharacter.memberId); const aidingCharacters = this.partyMembers.reduce((acc, curr) => { if (curr.selected && curr.id !== this.mainCharacter.memberId) acc[curr.id] = { id: curr.id, name: curr.name, img: curr.img }; return acc; }, {}); await this.party.update({ 'system.groupRoll': _replace( new game.system.api.data.GroupRollData({ ...this.party.system.groupRoll.toObject(), mainCharacter: { id: mainCharacter.id, name: mainCharacter.name, img: mainCharacter.img }, aidingCharacters }) ) }); const hookData = { openForAllPlayers: this.openForAllPlayers, partyId: this.party.id }; Hooks.callAll(CONFIG.DH.HOOKS.hooksConfig.groupRollStart, hookData); game.socket.emit(`system.${CONFIG.DH.id}`, { action: socketEvent.GroupRollStart, data: hookData }); this.render(); } //#endregion async makeRoll(button, characterData, path) { const actor = game.actors.find(x => x.id === characterData.id); if (!actor) return; const result = await actor.rollTrait(characterData.rollChoice, { skips: { createMessage: true, resources: true, triggers: true } }); 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( { [path]: rollData }, this.getUpdatingParts(button) ); } static async #makeRoll(_event, button) { const { member } = button.dataset; const character = this.party.system.groupRoll.aidingCharacters[member]; this.makeRoll(button, character, `system.groupRoll.aidingCharacters.${member}.rollData`); } static async #makeMainCharacterRoll(_event, button) { const character = this.party.system.groupRoll.mainCharacter; this.makeRoll(button, character, 'system.groupRoll.mainCharacter.rollData'); } async removeRoll(button, path) { this.updatePartyData( { [path]: { rollData: null, rollChoice: null, selected: false, successfull: null } }, this.getUpdatingParts(button) ); } static async #removeRoll(_event, button) { this.removeRoll(button, `system.groupRoll.aidingCharacters.${button.dataset.member}`); } static async #removeMainCharacterRoll(_event, button) { this.removeRoll(button, 'system.groupRoll.mainCharacter'); } async rerollDice(button, data, path) { const { diceType } = button.dataset; const dieIndex = diceType === 'hope' ? 0 : diceType === 'fear' ? 1 : 2; const newRoll = game.system.api.dice.DualityRoll.fromData(data.rollData); const dice = newRoll.dice[dieIndex]; await dice.reroll(`/r1=${dice.total}`, { liveRoll: { roll: newRoll, isReaction: true } }); const rollData = newRoll.toJSON(); this.updatePartyData( { [path]: rollData }, this.getUpdatingParts(button) ); } static async #rerollDice(_, button) { const { member } = button.dataset; this.rerollDice( button, this.party.system.groupRoll.aidingCharacters[member], `system.groupRoll.aidingCharacters.${member}.rollData` ); } static async #rerollMainCharacterDice(_, button) { this.rerollDice(button, this.party.system.groupRoll.mainCharacter, `system.groupRoll.mainCharacter.rollData`); } static #markSuccessfull(_event, button) { const previousValue = this.party.system.groupRoll.aidingCharacters[button.dataset.member].successfull; const newValue = Boolean(button.dataset.successfull === 'true'); this.updatePartyData( { [`system.groupRoll.aidingCharacters.${button.dataset.member}.successfull`]: previousValue === newValue ? null : newValue }, this.getUpdatingParts(button) ); } 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.GroupRollSelect.cancelConfirmTitle') }, content: game.i18n.localize('DAGGERHEART.APPLICATIONS.GroupRollSelect.cancelConfirmText') }); if (!confirmed) return; } await this.updatePartyData( { 'system.groupRoll': { mainCharacter: null, aidingCharacters: _replace({}) } }, [], { render: false } ); this.close(); game.socket.emit(`system.${CONFIG.DH.id}`, { action: socketEvent.Refresh, data: { refreshType: RefreshType.GroupRoll, action: 'close' } }); } static async #finishRoll() { const totalRoll = this.party.system.groupRoll.mainCharacter.roll; for (const character of Object.values(this.party.system.groupRoll.aidingCharacters)) { totalRoll.terms.push(new foundry.dice.terms.OperatorTerm({ operator: character.successfull ? '+' : '-' })); totalRoll.terms.push(new foundry.dice.terms.NumericTerm({ number: 1 })); } await totalRoll._evaluate(); const systemData = totalRoll.options; const actor = game.actors.get(this.party.system.groupRoll.mainCharacter.id); const cls = getDocumentClass('ChatMessage'), msgData = { type: 'dualityRoll', user: game.user.id, title: game.i18n.localize('DAGGERHEART.APPLICATIONS.GroupRollSelect.title'), speaker: cls.getSpeaker({ actor }), system: systemData, rolls: [JSON.stringify(totalRoll)], sound: null, flags: { core: { RollTable: true } } }; await cls.create(msgData); const resourceMap = new ResourceUpdateMap(actor); if (totalRoll.isCritical) { resourceMap.addResources([ { key: 'stress', value: -1, total: 1 }, { key: 'hope', value: 1, total: 1 } ]); } else if (totalRoll.withHope) { resourceMap.addResources([{ key: 'hope', value: 1, total: 1 }]); } else { resourceMap.addResources([{ key: 'fear', value: 1, total: 1 }]); } resourceMap.updateResources(); /* Fin */ this.cancelRoll({ confirm: false }); } }