diff --git a/daggerheart.mjs b/daggerheart.mjs index 240d8704..43aafce4 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -343,6 +343,17 @@ Hooks.on(CONFIG.DH.HOOKS.hooksConfig.tagTeamStart, async data => { } }); +Hooks.on(CONFIG.DH.HOOKS.hooksConfig.groupRollStart, 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.GroupRollDialog(party); + dialog.tabGroups.application = 'groupRoll'; + 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 f94a7325..65323790 100755 --- a/lang/en.json +++ b/lang/en.json @@ -733,6 +733,17 @@ "selectRoll": "Select which roll value to be used for the Tag Team" } }, + "GroupRollSelect": { + "title": "Group Roll", + "leader": "Leader", + "leaderRoll": "Leader Roll", + "openDialogForAll": "Open Dialog For All", + "startGroupRoll": "Start Group Roll", + "cancelGroupRoll": "Cancel", + "finishGroupRoll": "Finish Group Roll", + "cancelConfirmTitle": "Cancel Group Roll", + "cancelConfirmText": "Are you sure you want to cancel the Group Roll? This will close it for all other players too." + }, "TokenConfig": { "actorSizeUsed": "Actor size is set, determining the dimensions" } @@ -2982,18 +2993,6 @@ "immunityTo": "Immunity: {immunities}" }, "featureTitle": "Class Feature", - "groupRoll": { - "title": "Group Roll", - "leader": "Leader", - "partyTeam": "Party Team", - "team": "Team", - "selectLeader": "Select a Leader", - "selectMember": "Select a Member", - "rerollTitle": "Reroll Group Roll", - "rerollContent": "Are you sure you want to reroll your {trait} roll?", - "rerollTooltip": "Reroll", - "wholePartySelected": "The whole party is selected" - }, "healingRoll": { "title": "Heal - {damage}", "heal": "Heal", diff --git a/module/applications/dialogs/_module.mjs b/module/applications/dialogs/_module.mjs index a479100a..c866f1cd 100644 --- a/module/applications/dialogs/_module.mjs +++ b/module/applications/dialogs/_module.mjs @@ -13,7 +13,7 @@ export { default as OwnershipSelection } from './ownershipSelection.mjs'; export { default as RerollDamageDialog } from './rerollDamageDialog.mjs'; export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs'; export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs'; -export { default as GroupRollDialog } from './group-roll-dialog.mjs'; export { default as TagTeamDialog } from './tagTeamDialog.mjs'; +export { default as GroupRollDialog } from './groupRollDialog.mjs'; export { default as RiskItAllDialog } from './riskItAllDialog.mjs'; export { default as CompendiumBrowserSettingsDialog } from './CompendiumBrowserSettings.mjs'; diff --git a/module/applications/dialogs/group-roll-dialog.mjs b/module/applications/dialogs/group-roll-dialog.mjs deleted file mode 100644 index 8a3c43d6..00000000 --- a/module/applications/dialogs/group-roll-dialog.mjs +++ /dev/null @@ -1,204 +0,0 @@ -import autocomplete from 'autocompleter'; -import { abilities } from '../../config/actorConfig.mjs'; - -const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; - -export default class GroupRollDialog extends HandlebarsApplicationMixin(ApplicationV2) { - constructor(actors) { - super(); - this.actors = actors; - this.actorLeader = {}; - this.actorsMembers = []; - } - - get title() { - return 'Group Roll'; - } - - static DEFAULT_OPTIONS = { - tag: 'form', - classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'group-roll'], - position: { width: 'auto', height: 'auto' }, - window: { - title: 'DAGGERHEART.UI.Chat.groupRoll.title' - }, - actions: { - roll: GroupRollDialog.#roll, - removeLeader: GroupRollDialog.#removeLeader, - removeMember: GroupRollDialog.#removeMember - }, - form: { handler: this.updateData, submitOnChange: true, closeOnSubmit: false } - }; - - static PARTS = { - application: { - id: 'group-roll', - template: 'systems/daggerheart/templates/dialogs/group-roll/group-roll.hbs' - } - }; - - _attachPartListeners(partId, htmlElement, options) { - super._attachPartListeners(partId, htmlElement, options); - const leaderChoices = this.actors.filter(x => this.actorsMembers.every(member => member.actor?.id !== x.id)); - const memberChoices = this.actors.filter( - x => this.actorLeader?.actor?.id !== x.id && this.actorsMembers.every(member => member.actor?.id !== x.id) - ); - - htmlElement.querySelectorAll('.leader-change-input').forEach(element => { - autocomplete({ - input: element, - fetch: function (text, update) { - if (!text) { - update(leaderChoices); - } else { - text = text.toLowerCase(); - var suggestions = leaderChoices.filter(n => n.name.toLowerCase().includes(text)); - update(suggestions); - } - }, - render: function (actor, search) { - const actorName = game.i18n.localize(actor.name); - const matchIndex = actorName.toLowerCase().indexOf(search); - - const beforeText = actorName.slice(0, matchIndex); - const matchText = actorName.slice(matchIndex, matchIndex + search.length); - const after = actorName.slice(matchIndex + search.length, actorName.length); - const img = document.createElement('img'); - img.src = actor.img; - - const element = document.createElement('li'); - element.appendChild(img); - - const label = document.createElement('span'); - label.innerHTML = - `${beforeText}${matchText ? `${matchText}` : ''}${after}`.replaceAll( - ' ', - ' ' - ); - element.appendChild(label); - - return element; - }, - renderGroup: function (label) { - const itemElement = document.createElement('div'); - itemElement.textContent = game.i18n.localize(label); - return itemElement; - }, - onSelect: actor => { - element.value = actor.uuid; - this.actorLeader = { actor: actor, trait: 'agility', difficulty: 0 }; - this.render(); - }, - click: e => e.fetch(), - customize: function (_input, _inputRect, container) { - container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ; - }, - minLength: 0 - }); - }); - - htmlElement.querySelectorAll('.team-push-input').forEach(element => { - autocomplete({ - input: element, - fetch: function (text, update) { - if (!text) { - update(memberChoices); - } else { - text = text.toLowerCase(); - var suggestions = memberChoices.filter(n => n.name.toLowerCase().includes(text)); - update(suggestions); - } - }, - render: function (actor, search) { - const actorName = game.i18n.localize(actor.name); - const matchIndex = actorName.toLowerCase().indexOf(search); - - const beforeText = actorName.slice(0, matchIndex); - const matchText = actorName.slice(matchIndex, matchIndex + search.length); - const after = actorName.slice(matchIndex + search.length, actorName.length); - const img = document.createElement('img'); - img.src = actor.img; - - const element = document.createElement('li'); - element.appendChild(img); - - const label = document.createElement('span'); - label.innerHTML = - `${beforeText}${matchText ? `${matchText}` : ''}${after}`.replaceAll( - ' ', - ' ' - ); - element.appendChild(label); - - return element; - }, - renderGroup: function (label) { - const itemElement = document.createElement('div'); - itemElement.textContent = game.i18n.localize(label); - return itemElement; - }, - onSelect: actor => { - element.value = actor.uuid; - this.actorsMembers.push({ actor: actor, trait: 'agility', difficulty: 0 }); - this.render({ force: true }); - }, - click: e => e.fetch(), - customize: function (_input, _inputRect, container) { - container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ; - }, - minLength: 0 - }); - }); - } - - async _prepareContext(_options) { - const context = await super._prepareContext(_options); - context.leader = this.actorLeader; - context.members = this.actorsMembers; - context.traitList = abilities; - - context.allSelected = this.actorsMembers.length + (this.actorLeader?.actor ? 1 : 0) === this.actors.length; - context.rollDisabled = context.members.length === 0 || !this.actorLeader?.actor; - - return context; - } - - static updateData(event, _, formData) { - const { actorLeader, actorsMembers } = foundry.utils.expandObject(formData.object); - this.actorLeader = foundry.utils.mergeObject(this.actorLeader, actorLeader); - this.actorsMembers = foundry.utils.mergeObject(this.actorsMembers, actorsMembers); - this.render(true); - } - - static async #removeLeader(_, button) { - this.actorLeader = null; - this.render(); - } - - static async #removeMember(_, button) { - this.actorsMembers = this.actorsMembers.filter(m => m.actor.uuid !== button.dataset.memberUuid); - this.render(); - } - - static async #roll() { - const cls = getDocumentClass('ChatMessage'); - const systemData = { - leader: this.actorLeader, - members: this.actorsMembers - }; - const msg = { - type: 'groupRoll', - user: game.user.id, - speaker: cls.getSpeaker(), - title: game.i18n.localize('DAGGERHEART.UI.Chat.groupRoll.title'), - system: systemData, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/ui/chat/groupRoll.hbs', - { system: systemData } - ) - }; - - cls.create(msg); - this.close(); - } -} diff --git a/module/applications/dialogs/groupRollDialog.mjs b/module/applications/dialogs/groupRollDialog.mjs new file mode 100644 index 00000000..2a7be791 --- /dev/null +++ b/module/applications/dialogs/groupRollDialog.mjs @@ -0,0 +1,527 @@ +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.leader = 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.GroupRollSelect.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, + makeLeaderRoll: this.#makeLeaderRoll, + removeLeaderRoll: this.#removeLeaderRoll, + rerollLeaderDice: this.#rerollLeaderDice, + 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' + }, + leader: { + id: 'leader', + template: 'systems/daggerheart/templates/dialogs/groupRollDialog/leader.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.updateLeaderField.bind(this)); + } + + _configureRenderParts(options) { + const { initialization, leader, 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.leader = leader; + 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.selectedLeader = this.leader; + partContext.selectedLeaderOptions = selectedMembers + .filter(actor => actor.owned) + .map(x => ({ value: x.id, label: x.name })); + partContext.selectedLeaderDisabled = !selectedMembers.length; + + partContext.canStartGroupRoll = selectedMembers.length > 1 && this.leader?.memberId; + partContext.openForAllPlayers = this.openForAllPlayers; + break; + case 'leader': + partContext.leader = this.getRollCharacterData(this.party.system.groupRoll.leader); + break; + case 'groupRoll': + const leader = this.party.system.groupRoll.leader; + 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 leaderTotal = 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: leaderTotal + modifierTotal, + leaderTotal: leaderTotal, + modifiers + }; + break; + case 'footer': + partContext.canFinishRoll = + Boolean(this.party.system.groupRoll.leader?.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, leader, groupRoll, footer } = this.constructor.PARTS; + const isInitialization = this.tabGroups.application === initialization.id; + const updatingMember = target.closest('.team-member-container')?.dataset?.memberKey; + const updatingLeader = target.closest('.main-character-outer-container'); + + return [ + ...(isInitialization ? [initialization.id] : []), + ...(updatingMember ? [updatingMember] : []), + ...(updatingLeader ? [leader.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(); + } + + updateLeaderField(event) { + if (!this.leader) this.leader = {}; + this.leader.memberId = event.target.value; + this.render(); + } + + static async #startGroupRoll() { + const leader = this.partyMembers.find(x => x.id === this.leader.memberId); + const aidingCharacters = this.partyMembers.reduce((acc, curr) => { + if (curr.selected && curr.id !== this.leader.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(), + leader: { id: leader.id, name: leader.name, img: leader.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 (!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( + { + [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 #makeLeaderRoll(_event, button) { + const character = this.party.system.groupRoll.leader; + this.makeRoll(button, character, 'system.groupRoll.leader.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 #removeLeaderRoll(_event, button) { + this.removeRoll(button, 'system.groupRoll.leader'); + } + + 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 #rerollLeaderDice(_, button) { + this.rerollDice(button, this.party.system.groupRoll.leader, `system.groupRoll.leader.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': { + leader: 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.leader.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.leader.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 }); + } +} diff --git a/module/applications/sheets/actors/party.mjs b/module/applications/sheets/actors/party.mjs index 7c8c2338..d4545f63 100644 --- a/module/applications/sheets/actors/party.mjs +++ b/module/applications/sheets/actors/party.mjs @@ -4,7 +4,6 @@ import { ItemBrowser } from '../../ui/itemBrowser.mjs'; import FilterMenu from '../../ux/filter-menu.mjs'; import DaggerheartMenu from '../../sidebar/tabs/daggerheartMenu.mjs'; import { socketEvent } from '../../../systemRegistration/socket.mjs'; -import GroupRollDialog from '../../dialogs/group-roll-dialog.mjs'; import DhpActor from '../../../documents/actor.mjs'; export default class Party extends DHBaseActorSheet { @@ -117,6 +116,7 @@ export default class Party extends DHBaseActorSheet { relativeTo: this.document }); context.tagTeamActive = Boolean(this.document.system.tagTeam.initiator); + context.groupRollActive = Boolean(this.document.system.groupRoll.leader); } async _prepareMembersContext(context, _options) { @@ -318,9 +318,7 @@ export default class Party extends DHBaseActorSheet { } static async #groupRoll(_params) { - new GroupRollDialog( - this.document.system.partyMembers.filter(x => Party.DICE_ROLL_ACTOR_TYPES.includes(x.type)) - ).render({ force: true }); + new game.system.api.applications.dialogs.GroupRollDialog(this.document).render({ force: true }); } /* -------------------------------------------- */ diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index 8cbacb09..59939963 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -1,8 +1,6 @@ -import { abilities } from '../../config/actorConfig.mjs'; import { enrichedDualityRoll } from '../../enrichers/DualityRollEnricher.mjs'; import { enrichedFateRoll, getFateTypeData } from '../../enrichers/FateRollEnricher.mjs'; import { getCommandTarget, rollCommandToJSON } from '../../helpers/utils.mjs'; -import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs'; export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog { constructor(options) { @@ -150,18 +148,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo html.querySelectorAll('.reroll-button').forEach(element => element.addEventListener('click', event => this.rerollEvent(event, message)) ); - html.querySelectorAll('.group-roll-button').forEach(element => - element.addEventListener('click', event => this.groupRollButton(event, message)) - ); - html.querySelectorAll('.group-roll-reroll').forEach(element => - element.addEventListener('click', event => this.groupRollReroll(event, message)) - ); - html.querySelectorAll('.group-roll-success').forEach(element => - element.addEventListener('click', event => this.groupRollSuccessEvent(event, message)) - ); - html.querySelectorAll('.group-roll-header-expand-section').forEach(element => - element.addEventListener('click', this.groupRollExpandSection) - ); html.querySelectorAll('.risk-it-all-button').forEach(element => element.addEventListener('click', event => this.riskItAllClearStressAndHitPoints(event, data)) ); @@ -305,174 +291,6 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo } } - async groupRollButton(event, message) { - const path = event.currentTarget.dataset.path; - const isLeader = path === 'leader'; - const { actor: actorData, trait } = foundry.utils.getProperty(message.system, path); - const actor = game.actors.get(actorData._id); - - if (!actor) { - return ui.notifications.error( - game.i18n.format('DAGGERHEART.UI.Notifications.documentIsMissing', { - documentType: game.i18n.localize('TYPES.Actor.character') - }) - ); - } - - if (!actor.testUserPermission(game.user, 'OWNER')) { - return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.noActorOwnership')); - } - - const traitLabel = game.i18n.localize(abilities[trait].label); - const config = { - event: event, - title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`, - headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { - ability: traitLabel - }), - roll: { - trait: trait, - advantage: 0, - modifiers: [{ label: traitLabel, value: actor.system.traits[trait].value }] - }, - hasRoll: true, - skips: { - createMessage: true, - resources: !isLeader, - updateCountdowns: !isLeader - } - }; - const result = await actor.diceRoll({ - ...config, - headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`, - title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { - ability: traitLabel - }) - }); - - if (!result) return; - - const newMessageData = foundry.utils.deepClone(message.system); - foundry.utils.setProperty(newMessageData, `${path}.result`, result.roll); - const renderData = { system: new game.system.api.models.chatMessages.config.groupRoll(newMessageData) }; - - const updatedContent = await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/ui/chat/groupRoll.hbs', - { ...renderData, user: game.user } - ); - const mess = game.messages.get(message._id); - - await emitAsGM( - GMUpdateEvent.UpdateDocument, - mess.update.bind(mess), - { - ...renderData, - content: updatedContent - }, - mess.uuid - ); - } - - async groupRollReroll(event, message) { - const path = event.currentTarget.dataset.path; - const { actor: actorData, trait } = foundry.utils.getProperty(message.system, path); - const actor = game.actors.get(actorData._id); - - if (!actor.testUserPermission(game.user, 'OWNER')) { - return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.noActorOwnership')); - } - - const traitLabel = game.i18n.localize(abilities[trait].label); - - const config = { - event: event, - title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`, - headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { - ability: traitLabel - }), - roll: { - trait: trait, - advantage: 0, - modifiers: [{ label: traitLabel, value: actor.system.traits[trait].value }] - }, - hasRoll: true, - skips: { - createMessage: true, - updateCountdowns: true - } - }; - const result = await actor.diceRoll({ - ...config, - headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`, - title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { - ability: traitLabel - }) - }); - - const newMessageData = foundry.utils.deepClone(message.system); - foundry.utils.setProperty(newMessageData, `${path}.result`, { ...result.roll, rerolled: true }); - const renderData = { system: new game.system.api.models.chatMessages.config.groupRoll(newMessageData) }; - - const updatedContent = await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/ui/chat/groupRoll.hbs', - { ...renderData, user: game.user } - ); - const mess = game.messages.get(message._id); - await emitAsGM( - GMUpdateEvent.UpdateDocument, - mess.update.bind(mess), - { - ...renderData, - content: updatedContent - }, - mess.uuid - ); - } - - async groupRollSuccessEvent(event, message) { - if (!game.user.isGM) { - return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmOnly')); - } - - const { path, success } = event.currentTarget.dataset; - const { actor: actorData } = foundry.utils.getProperty(message.system, path); - const actor = game.actors.get(actorData._id); - - if (!actor.testUserPermission(game.user, 'OWNER')) { - return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.noActorOwnership')); - } - - const newMessageData = foundry.utils.deepClone(message.system); - foundry.utils.setProperty(newMessageData, `${path}.manualSuccess`, Boolean(success)); - const renderData = { system: new game.system.api.models.chatMessages.config.groupRoll(newMessageData) }; - - const updatedContent = await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/ui/chat/groupRoll.hbs', - { ...renderData, user: game.user } - ); - const mess = game.messages.get(message._id); - await emitAsGM( - GMUpdateEvent.UpdateDocument, - mess.update.bind(mess), - { - ...renderData, - content: updatedContent - }, - mess.uuid - ); - } - - async groupRollExpandSection(event) { - event.target - .closest('.group-roll-header-expand-section') - .querySelectorAll('i') - .forEach(element => { - element.classList.toggle('fa-angle-up'); - element.classList.toggle('fa-angle-down'); - }); - event.target.closest('.group-roll-section').querySelector('.group-roll-content').classList.toggle('closed'); - } - async riskItAllClearStressAndHitPoints(event, data) { const resourceValue = event.target.dataset.resourceValue; const actor = game.actors.get(event.target.dataset.actorId); diff --git a/module/config/hooksConfig.mjs b/module/config/hooksConfig.mjs index 8d04be6d..c0930d90 100644 --- a/module/config/hooksConfig.mjs +++ b/module/config/hooksConfig.mjs @@ -1,5 +1,6 @@ export const hooksConfig = { effectDisplayToggle: 'DHEffectDisplayToggle', lockedTooltipDismissed: 'DHLockedTooltipDismissed', - tagTeamStart: 'DHTagTeamRollStart' + tagTeamStart: 'DHTagTeamRollStart', + groupRollStart: 'DHGroupRollStart' }; diff --git a/module/data/_module.mjs b/module/data/_module.mjs index 0e7e295e..cd691ee1 100644 --- a/module/data/_module.mjs +++ b/module/data/_module.mjs @@ -4,6 +4,7 @@ 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 { default as GroupRollData } from './groupRollData.mjs'; export { default as SpotlightTracker } from './spotlightTracker.mjs'; export * as countdowns from './countdowns.mjs'; diff --git a/module/data/actor/party.mjs b/module/data/actor/party.mjs index 2c797803..ec1beb99 100644 --- a/module/data/actor/party.mjs +++ b/module/data/actor/party.mjs @@ -1,6 +1,7 @@ import BaseDataActor from './base.mjs'; import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs'; import TagTeamData from '../tagTeamData.mjs'; +import GroupRollData from '../groupRollData.mjs'; export default class DhParty extends BaseDataActor { /**@inheritdoc */ @@ -16,7 +17,8 @@ export default class DhParty extends BaseDataActor { bags: new fields.NumberField({ initial: 0, integer: true }), chests: new fields.NumberField({ initial: 0, integer: true }) }), - tagTeam: new fields.EmbeddedDataField(TagTeamData) + tagTeam: new fields.EmbeddedDataField(TagTeamData), + groupRoll: new fields.EmbeddedDataField(GroupRollData) }; } diff --git a/module/data/chat-message/_modules.mjs b/module/data/chat-message/_modules.mjs index c671de31..450d1ba2 100644 --- a/module/data/chat-message/_modules.mjs +++ b/module/data/chat-message/_modules.mjs @@ -1,6 +1,5 @@ import DHAbilityUse from './abilityUse.mjs'; import DHActorRoll from './actorRoll.mjs'; -import DHGroupRoll from './groupRoll.mjs'; import DHSystemMessage from './systemMessage.mjs'; export const config = { @@ -9,6 +8,5 @@ export const config = { damageRoll: DHActorRoll, dualityRoll: DHActorRoll, fateRoll: DHActorRoll, - groupRoll: DHGroupRoll, systemMessage: DHSystemMessage }; diff --git a/module/data/chat-message/groupRoll.mjs b/module/data/chat-message/groupRoll.mjs deleted file mode 100644 index a5308323..00000000 --- a/module/data/chat-message/groupRoll.mjs +++ /dev/null @@ -1,39 +0,0 @@ -import { abilities } from '../../config/actorConfig.mjs'; - -export default class DHGroupRoll extends foundry.abstract.TypeDataModel { - static defineSchema() { - const fields = foundry.data.fields; - - return { - leader: new fields.EmbeddedDataField(GroupRollMemberField), - members: new fields.ArrayField(new fields.EmbeddedDataField(GroupRollMemberField)) - }; - } - - get totalModifier() { - return this.members.reduce((acc, m) => { - if (m.manualSuccess === null) return acc; - - return acc + (m.manualSuccess ? 1 : -1); - }, 0); - } -} - -class GroupRollMemberField extends foundry.abstract.DataModel { - static defineSchema() { - const fields = foundry.data.fields; - - return { - actor: new fields.ObjectField(), - trait: new fields.StringField({ choices: abilities }), - difficulty: new fields.StringField(), - result: new fields.ObjectField({ nullable: true, initial: null }), - manualSuccess: new fields.BooleanField({ nullable: true, initial: null }) - }; - } - - /* Can be expanded if we handle automation of success/failure */ - get success() { - return manualSuccess; - } -} diff --git a/module/data/groupRollData.mjs b/module/data/groupRollData.mjs new file mode 100644 index 00000000..78a06b13 --- /dev/null +++ b/module/data/groupRollData.mjs @@ -0,0 +1,40 @@ +export default class GroupRollData extends foundry.abstract.DataModel { + static defineSchema() { + const fields = foundry.data.fields; + + return { + leader: new fields.EmbeddedDataField(CharacterData, { nullable: true, initial: null }), + aidingCharacters: new fields.TypedObjectField(new fields.EmbeddedDataField(CharacterData)) + }; + } + + get participants() { + return { + ...(this.leader ? { [this.leader.id]: this.leader } : {}), + ...this.aidingCharacters + }; + } +} + +export class CharacterData extends foundry.abstract.DataModel { + static defineSchema() { + const fields = foundry.data.fields; + + return { + id: new fields.StringField({ required: true }), + name: new fields.StringField({ required: true }), + img: new fields.StringField({ required: true }), + rollChoice: new fields.StringField({ + choices: CONFIG.DH.ACTOR.abilities, + initial: CONFIG.DH.ACTOR.abilities.agility.id + }), + rollData: new fields.JSONField({ nullable: true, initial: null }), + selected: new fields.BooleanField({ initial: false }), + successfull: new fields.BooleanField({ nullable: true, initial: null }) + }; + } + + get roll() { + return this.rollData ? CONFIG.Dice.daggerheart.DualityRoll.fromData(this.rollData) : null; + } +} diff --git a/module/systemRegistration/socket.mjs b/module/systemRegistration/socket.mjs index fb152959..8fed346d 100644 --- a/module/systemRegistration/socket.mjs +++ b/module/systemRegistration/socket.mjs @@ -18,6 +18,8 @@ export function handleSocketEvent({ action = null, data = {} } = {}) { case socketEvent.TagTeamStart: Hooks.callAll(CONFIG.DH.HOOKS.hooksConfig.tagTeamStart, data); break; + case socketEvent.GroupRollStart: + Hooks.callAll(CONFIG.DH.HOOKS.hooksConfig.groupRollStart, data); } } @@ -26,7 +28,8 @@ export const socketEvent = { Refresh: 'DhRefresh', DhpFearUpdate: 'DhFearUpdate', DowntimeTrigger: 'DowntimeTrigger', - TagTeamStart: 'DhTagTeamStart' + TagTeamStart: 'DhTagTeamStart', + GroupRollStart: 'DhGroupRollStart' }; export const GMUpdateEvent = { @@ -41,6 +44,7 @@ export const GMUpdateEvent = { export const RefreshType = { Countdown: 'DhCoundownRefresh', TagTeamRoll: 'DhTagTeamRollRefresh', + GroupRoll: 'DhGroupRollRefresh', EffectsDisplay: 'DhEffectsDisplayRefresh', Scene: 'DhSceneRefresh', CompendiumBrowser: 'DhCompendiumBrowserRefresh' diff --git a/styles/less/dialog/group-roll-dialog/initialization.less b/styles/less/dialog/group-roll-dialog/initialization.less new file mode 100644 index 00000000..96990339 --- /dev/null +++ b/styles/less/dialog/group-roll-dialog/initialization.less @@ -0,0 +1,78 @@ +.theme-light .daggerheart.dialog.dh-style.views.group-roll-dialog { + .initialization-container .members-container .member-container { + .member-name { + background-image: url('../assets/parchments/dh-parchment-light.png'); + } + } +} + +.daggerheart.dialog.dh-style.views.group-roll-dialog { + .initialization-container { + h2 { + text-align: center; + } + + .members-container { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + gap: 8px; + + .member-container { + position: relative; + display: flex; + justify-content: center; + + &.inactive { + opacity: 0.4; + } + + .member-name { + position: absolute; + padding: 0 2px; + border: 1px solid; + border-radius: 6px; + margin-top: 4px; + color: light-dark(@dark, @beige); + background-image: url('../assets/parchments/dh-parchment-dark.png'); + } + + img { + border-radius: 6px; + border: 1px solid light-dark(@dark-blue, @golden); + } + } + } + + .main-roll { + margin-top: 8px; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + + &.inactive { + opacity: 0.4; + } + } + + footer { + margin-top: 8px; + display: flex; + gap: 8px; + + button { + flex: 1; + } + + .finish-tools { + flex: none; + display: flex; + align-items: center; + gap: 4px; + + &.inactive { + opacity: 0.4; + } + } + } + } +} diff --git a/styles/less/dialog/group-roll-dialog/leader.less b/styles/less/dialog/group-roll-dialog/leader.less new file mode 100644 index 00000000..b3fa3a3b --- /dev/null +++ b/styles/less/dialog/group-roll-dialog/leader.less @@ -0,0 +1,35 @@ +.daggerheart.dialog.dh-style.views.group-roll-dialog { + .main-character-outer-container { + &.inactive { + opacity: 0.3; + pointer-events: none; + } + + .main-character-container { + .character-info { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + height: 64px; + + img { + height: 64px; + border-radius: 6px; + border: 1px solid light-dark(@dark-blue, @golden); + } + + .character-data { + padding-left: 0.75rem; + flex: 1; + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; + text-align: left; + font-size: var(--font-size-18); + } + } + } + } +} diff --git a/styles/less/dialog/group-roll-dialog/sheet.less b/styles/less/dialog/group-roll-dialog/sheet.less new file mode 100644 index 00000000..823f6cbf --- /dev/null +++ b/styles/less/dialog/group-roll-dialog/sheet.less @@ -0,0 +1,266 @@ +.daggerheart.dialog.dh-style.views.group-roll-dialog { + .team-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + margin-bottom: 16px; + + .team-member-container { + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 8px; + flex: 1; + + &.inactive { + opacity: 0.3; + pointer-events: none; + } + + .data-container { + display: flex; + flex-direction: column; + gap: 8px; + width: 100%; + } + + .member-info { + display: flex; + align-items: center; + width: 100%; + height: 64px; + + img { + height: 64px; + border-radius: 6px; + border: 1px solid light-dark(@dark-blue, @golden); + } + + .member-data { + padding-left: 0.75rem; + flex: 1; + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; + text-align: left; + font-size: var(--font-size-18); + } + } + } + } + + .roll-container { + display: flex; + flex-direction: column; + } + + .roll-title { + font-size: var(--font-size-20); + font-weight: bold; + color: light-dark(@dark-blue, @golden); + text-align: center; + display: flex; + align-items: center; + gap: 8px; + + &.hope, + &.fear, + &.critical { + color: var(--text-color); + } + + &.hope { + --text-color: @golden; + } + + &.fear { + --text-color: @chat-blue; + } + + &.critical { + --text-color: @chat-purple; + } + + &::before, + &::after { + color: var(--text-color); + content: ''; + flex: 1; + height: 2px; + } + + &::before { + background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, light-dark(@dark-blue, @golden) 100%); + } + + &::after { + background: linear-gradient(90deg, light-dark(@dark-blue, @golden) 0%, rgba(0, 0, 0, 0) 100%); + } + } + + .roll-tools { + display: flex; + gap: 4px; + align-items: center; + + img { + height: 16px; + } + + a { + display: flex; + font-size: 16px; + + &:hover { + text-shadow: none; + filter: drop-shadow(0 0 8px var(--golden)); + } + } + } + + .roll-data { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + + &.hope { + --text-color: @golden; + --bg-color: @golden-40; + } + + &.fear { + --text-color: @chat-blue; + --bg-color: @chat-blue-40; + } + + &.critical { + --text-color: @chat-purple; + --bg-color: @chat-purple-40; + } + + .duality-label { + color: var(--text-color); + font-size: var(--font-size-20); + font-weight: bold; + text-align: center; + + .unused-damage { + text-decoration: line-through; + } + } + + .roll-dice-container { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 8px; + + .roll-dice { + position: relative; + display: flex; + align-items: center; + justify-content: center; + + .dice-label { + position: absolute; + color: white; + font-size: 1rem; + paint-order: stroke fill; + -webkit-text-stroke: 2px black; + } + + img { + height: 32px; + } + } + + .roll-operator { + font-size: var(--font-size-24); + } + + .roll-value { + font-size: 18px; + } + } + + .roll-total { + background: var(--bg-color); + color: var(--text-color); + border-radius: 4px; + padding: 3px; + } + } + + .roll-success-container { + display: flex; + align-items: center; + justify-content: space-around; + + .roll-success-tools { + display: flex; + align-items: center; + gap: 4px; + color: light-dark(@dark-blue, @golden); + + i { + font-size: 24px; + } + } + + .roll-success-modifier { + display: flex; + align-items: center; + justify-content: right; + gap: 2px; + font-size: var(--font-size-20); + padding: 0px 4px; + + &.success { + background: @green-10; + color: @green; + } + + &.failure { + background: @red-10; + color: @red; + } + } + } + + .section-title { + font-size: var(--font-size-18); + font-weight: bold; + } + + .group-roll-results { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + font-size: var(--font-size-20); + + .group-roll-container { + display: flex; + align-items: center; + gap: 2px; + } + } + + .finish-container { + margin-top: 16px; + gap: 16px; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + + .finish-button { + grid-column: span 2; + } + } + + .hint { + text-align: center; + } +} diff --git a/styles/less/dialog/index.less b/styles/less/dialog/index.less index 73738eaa..947142ff 100644 --- a/styles/less/dialog/index.less +++ b/styles/less/dialog/index.less @@ -36,6 +36,10 @@ @import './tag-team-dialog/initialization.less'; @import './tag-team-dialog/sheet.less'; +@import './group-roll-dialog/initialization.less'; +@import './group-roll-dialog/leader.less'; +@import './group-roll-dialog/sheet.less'; + @import './image-select/sheet.less'; @import './item-transfer/sheet.less'; diff --git a/styles/less/dialog/tag-team-dialog/initialization.less b/styles/less/dialog/tag-team-dialog/initialization.less index 30676f82..0d16aa3b 100644 --- a/styles/less/dialog/tag-team-dialog/initialization.less +++ b/styles/less/dialog/tag-team-dialog/initialization.less @@ -20,6 +20,17 @@ .member-name { position: absolute; + padding: 0 2px; + border: 1px solid; + border-radius: 6px; + margin-top: 4px; + color: light-dark(@dark, @beige); + background-image: url('../assets/parchments/dh-parchment-dark.png'); + } + + img { + border-radius: 6px; + border: 1px solid light-dark(@dark-blue, @golden); } } } diff --git a/system.json b/system.json index 300b1042..fed7d02d 100644 --- a/system.json +++ b/system.json @@ -290,7 +290,6 @@ "damageRoll": {}, "abilityUse": {}, "tagTeam": {}, - "groupRoll": {}, "systemMessage": {} } }, diff --git a/templates/dialogs/groupRollDialog/footer.hbs b/templates/dialogs/groupRollDialog/footer.hbs new file mode 100644 index 00000000..cb041247 --- /dev/null +++ b/templates/dialogs/groupRollDialog/footer.hbs @@ -0,0 +1,6 @@ +
+
+ + +
+
\ No newline at end of file diff --git a/templates/dialogs/groupRollDialog/groupRoll.hbs b/templates/dialogs/groupRollDialog/groupRoll.hbs new file mode 100644 index 00000000..edf1c980 --- /dev/null +++ b/templates/dialogs/groupRollDialog/groupRoll.hbs @@ -0,0 +1,20 @@ +
+
+ {{localize "DAGGERHEART.GENERAL.result.single"}} + +
+ {{#if hasRolled}}{{groupRoll.total}} {{groupRoll.totalLabel}}{{/if}} +
+ {{#if groupRoll.leaderTotal includeZero=true}}{{groupRoll.leaderTotal}}{{else}}{{localize "DAGGERHEART.APPLICATIONS.GroupRollSelect.leaderRoll"}}{{/if}} + {{#each groupRoll.modifiers as |modifier|}} + {{#if (gte modifier 0)}}+{{else}}-{{/if}} + {{positive modifier}} + {{/each}} + {{#unless groupRoll.modifiers.length}} + + + {{localize "DAGGERHEART.GENERAL.Modifier.plural"}} + {{/unless}} +
+
+
+
\ No newline at end of file diff --git a/templates/dialogs/groupRollDialog/groupRollMember.hbs b/templates/dialogs/groupRollDialog/groupRollMember.hbs new file mode 100644 index 00000000..acf8e8f1 --- /dev/null +++ b/templates/dialogs/groupRollDialog/groupRollMember.hbs @@ -0,0 +1,85 @@ +{{#with (lookup members partId)}} +
+
+
+ +
+ {{name}} +
+
+
+ {{!-- --}} + +
+
+
+
+
+ {{#if readyToRoll}} +
+ + {{localize "DAGGERHEART.GENERAL.roll"}} +
+ + + + + {{#if hasRolled}} + + + + {{/if}} +
+
+ + {{#if roll}} +
+
{{roll.total}} {{localize "DAGGERHEART.GENERAL.withThing" thing=roll.totalLabel}}
+
+ + {{roll.dHope.total}} + + + + + + {{roll.dFear.total}} + + + {{#if roll.advantage.type}} + {{#if (eq roll.advantage.type 1)}}+{{else}}-{{/if}} + + {{roll.advantage.value}} + + + {{/if}} + {{#if (gte roll.modifierTotal 0)}}+{{else}}-{{/if}} + {{positive roll.modifierTotal}} +
+
+ {{else}} + {{localize "DAGGERHEART.APPLICATIONS.TagTeamSelect.makeYourRoll"}} + {{/if}} +
+ {{/if}} + {{#if hasRolled}} +
+ {{#if ../isGM}} + + {{/if}} +
+ {{localize "DAGGERHEART.GENERAL.Modifier.single"}}{{#if successfull}} + 1{{else if (isNullish successfull)}} + ?{{else}} - 1{{/if}} +
+
+ {{/if}} +
+
+{{/with}} \ No newline at end of file diff --git a/templates/dialogs/groupRollDialog/initialization.hbs b/templates/dialogs/groupRollDialog/initialization.hbs new file mode 100644 index 00000000..a520b8bd --- /dev/null +++ b/templates/dialogs/groupRollDialog/initialization.hbs @@ -0,0 +1,32 @@ +
+
+ {{#each memberSelection as |member|}} + + {{member.name}} + + + {{/each}} +
+ +
+
+ +
+ +
+
+
+ + +
\ No newline at end of file diff --git a/templates/dialogs/groupRollDialog/leader.hbs b/templates/dialogs/groupRollDialog/leader.hbs new file mode 100644 index 00000000..3d5db3f7 --- /dev/null +++ b/templates/dialogs/groupRollDialog/leader.hbs @@ -0,0 +1,73 @@ +
+ {{#with leader}} +
+
{{localize "DAGGERHEART.APPLICATIONS.GroupRollSelect.leader"}}
+
+
+
+ +
+ {{name}} +
+
+
+ +
+
+
+
+
+
+ + {{#if readyToRoll}} +
+ + {{localize "DAGGERHEART.GENERAL.roll"}} +
+ + + + + {{#if hasRolled}} + + + + {{/if}} +
+
+ + {{#if roll}} +
+
{{roll.total}} {{localize "DAGGERHEART.GENERAL.withThing" thing=roll.totalLabel}}
+
+ + {{roll.dHope.total}} + + + + + + {{roll.dFear.total}} + + + {{#if roll.advantage.type}} + {{#if (eq roll.advantage.type 1)}}+{{else}}-{{/if}} + + {{roll.advantage.value}} + + + {{/if}} + {{#if (gte roll.modifierTotal 0)}}+{{else}}-{{/if}} + {{positive roll.modifierTotal}} +
+
+ {{else}} + {{localize "DAGGERHEART.APPLICATIONS.TagTeamSelect.makeYourRoll"}} + {{/if}} +
+ {{/if}} +
+
+ {{/with}} +
\ No newline at end of file diff --git a/templates/dialogs/tagTeamDialog/initialization.hbs b/templates/dialogs/tagTeamDialog/initialization.hbs index d25e8f6c..7ccdf566 100644 --- a/templates/dialogs/tagTeamDialog/initialization.hbs +++ b/templates/dialogs/tagTeamDialog/initialization.hbs @@ -1,5 +1,4 @@
- {{partId}}

{{localize "DAGGERHEART.APPLICATIONS.TagTeamSelect.selectParticipants"}}

{{#each memberSelection as |member|}} diff --git a/templates/sheets/actors/party/party-members.hbs b/templates/sheets/actors/party/party-members.hbs index 8a113ac8..bc0c6672 100644 --- a/templates/sheets/actors/party/party-members.hbs +++ b/templates/sheets/actors/party/party-members.hbs @@ -9,15 +9,10 @@ Tag Team Roll - - {{!-- NOT YET IMPLEMENTED --}} - {{!-- --}}