From 3ddfb7e75e070503b11e332de7e9ae665136fc61 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Thu, 9 Apr 2026 00:14:44 +0200 Subject: [PATCH] . --- daggerheart.mjs | 11 + lang/en.json | 11 + .../applications/dialogs/groupRollDialog.mjs | 420 +++++++++++++++++- module/config/hooksConfig.mjs | 3 +- module/data/_module.mjs | 1 + module/data/actor/party.mjs | 4 +- module/data/groupRollData.mjs | 40 ++ module/systemRegistration/socket.mjs | 5 +- .../group-roll-dialog/initialization.less | 59 +++ .../group-roll-dialog/mainCharacter.less | 30 ++ .../less/dialog/group-roll-dialog/sheet.less | 243 ++++++++++ styles/less/dialog/index.less | 4 + templates/dialogs/groupRollDialog/footer.hbs | 6 + .../dialogs/groupRollDialog/groupRoll.hbs | 19 +- .../groupRollMainCharacter.hbs | 71 +++ .../groupRollDialog/groupRollMember.hbs | 85 ++++ .../groupRollDialog/initialization.hbs | 32 ++ .../dialogs/tagTeamDialog/initialization.hbs | 1 - 18 files changed, 1021 insertions(+), 24 deletions(-) create mode 100644 module/data/groupRollData.mjs create mode 100644 styles/less/dialog/group-roll-dialog/initialization.less create mode 100644 styles/less/dialog/group-roll-dialog/mainCharacter.less create mode 100644 styles/less/dialog/group-roll-dialog/sheet.less create mode 100644 templates/dialogs/groupRollDialog/footer.hbs create mode 100644 templates/dialogs/groupRollDialog/groupRollMainCharacter.hbs create mode 100644 templates/dialogs/groupRollDialog/initialization.hbs 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 07cdb35b..13c21915 100755 --- a/lang/en.json +++ b/lang/en.json @@ -729,6 +729,16 @@ "selectRoll": "Select which roll value to be used for the Tag Team" } }, + "GroupRollSelect": { + "title": "Group Roll", + "mainCharacter": "Main Character", + "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" } @@ -2386,6 +2396,7 @@ }, "maxWithThing": "Max {thing}", "missingDragDropThing": "Drop {thing} here", + "modifier": "Modifier", "multiclass": "Multiclass", "newCategory": "New Category", "newThing": "New {thing}", diff --git a/module/applications/dialogs/groupRollDialog.mjs b/module/applications/dialogs/groupRollDialog.mjs index 06ac3191..b5169443 100644 --- a/module/applications/dialogs/groupRollDialog.mjs +++ b/module/applications/dialogs/groupRollDialog.mjs @@ -1,4 +1,4 @@ -import { RefreshType } 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; @@ -14,10 +14,17 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat ...member.toObject(), uuid: member.uuid, id: member.id, - selected: false, + 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()); } @@ -30,54 +37,186 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat id: 'GroupRollDialog', classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'group-roll-dialog'], position: { width: 550, height: 'auto' }, - actions: {}, + 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 { groupRoll } = super._configureRenderParts(options); - const augmentedParts = { groupRoll }; - for (const memberKey of Object.keys(this.party.system.tagTeam.members)) { + 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/tagTeamDialog/tagTeamMember.hbs' + template: 'systems/daggerheart/templates/dialogs/groupRollDialog/groupRollMember.hbs' }; } - augmentedParts.rollSelection = rollSelection; - augmentedParts.tagTeamRoll = tagTeamRoll; + + 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; + 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; + partContext.openForAllPlayers = this.openForAllPlayers; + break; + case 'mainCharacter': + partContext.mainCharacter = this.getRollCharacterData(this.party.system.groupRoll.mainCharacter); + break; case 'groupRoll': + 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 = this.party.system.groupRoll.mainCharacter?.rollData + ? this.party.system.groupRoll.mainCharacter.roll.total + : null; + partContext.groupRoll = { + totalLabel: this.party.system.groupRoll.mainCharacter?.rollData + ? game.i18n.format('DAGGERHEART.GENERAL.withThing', { + thing: this.party.system.groupRoll.mainCharacter.roll.totalLabel + }) + : null, + 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.tagTeam.members).includes(partId)) { - const data = this.party.system.tagTeam.members[partId]; - const actor = game.actors.get(partId); + 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); @@ -93,7 +232,7 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat this.render({ parts: updatingParts }); game.socket.emit(`system.${CONFIG.DH.id}`, { action: socketEvent.Refresh, - data: { refreshType: RefreshType.TagTeamRoll, action: 'refresh', parts: updatingParts } + data: { refreshType: RefreshType.GroupRoll, action: 'refresh', parts: updatingParts } }); }; @@ -102,22 +241,38 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat gmUpdate, update, this.party.uuid, - options.render - ? { refreshType: RefreshType.TagTeamRoll, action: 'refresh', parts: updatingParts } - : undefined + 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 [...(updatingMember ? [updatingMember] : []), rollSelection.id]; + 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; @@ -134,4 +289,235 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat 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 + + static async #makeRoll(_event, button) { + const { member } = button.dataset; + + const actor = game.actors.find(x => x.id === member); + if (!actor) return; + + const characterData = this.party.system.groupRoll.aidingCharacters[member]; + 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( + { + [`system.groupRoll.aidingCharacters.${member}.rollData`]: rollData + }, + this.getUpdatingParts(button) + ); + } + + static async #removeRoll(_, button) { + this.updatePartyData( + { + [`system.groupRoll.aidingCharacters.${button.dataset.member}`]: { + rollData: null, + rollChoice: null, + selected: false + } + }, + 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`); + } + + 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 #makeMainCharacterRoll(_event, button) { + const actor = game.actors.find(x => x.id === this.party.system.groupRoll.mainCharacter.id); + if (!actor) return; + + const characterData = this.party.system.groupRoll.mainCharacter; + 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( + { + [`system.groupRoll.mainCharacter.rollData`]: rollData + }, + this.getUpdatingParts(button) + ); + } + + static async #removeMainCharacterRoll(_event, button) { + this.updatePartyData( + { + [`system.groupRoll.mainCharacter`]: { + rollData: null, + rollChoice: null, + selected: false + } + }, + this.getUpdatingParts(button) + ); + } + + 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.TagTeamSelect.title'), + speaker: cls.getSpeaker({ actor }), + system: systemData, + rolls: [JSON.stringify(totalRoll)], + sound: null, + flags: { core: { RollTable: true } } + }; + + await cls.create(msgData); + + /* Fin */ + this.cancelRoll({ confirm: false }); + } } 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/groupRollData.mjs b/module/data/groupRollData.mjs new file mode 100644 index 00000000..5047c4e2 --- /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 { + mainCharacter: new fields.EmbeddedDataField(CharacterData, { nullable: true, initial: null }), + aidingCharacters: new fields.TypedObjectField(new fields.EmbeddedDataField(CharacterData)) + }; + } + + get participants() { + return { + ...(this.mainCharacter ? { [this.mainCharacter.id]: this.mainCharacter } : {}), + ...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 027b6245..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 = { 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..211495ee --- /dev/null +++ b/styles/less/dialog/group-roll-dialog/initialization.less @@ -0,0 +1,59 @@ +.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; + } + } + } + + .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/mainCharacter.less b/styles/less/dialog/group-roll-dialog/mainCharacter.less new file mode 100644 index 00000000..019d0e1c --- /dev/null +++ b/styles/less/dialog/group-roll-dialog/mainCharacter.less @@ -0,0 +1,30 @@ +.daggerheart.dialog.dh-style.views.group-roll-dialog { + .main-character-outer-container { + .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..571d0d38 --- /dev/null +++ b/styles/less/dialog/group-roll-dialog/sheet.less @@ -0,0 +1,243 @@ +.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; + + .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; + + &::before, + &::after { + color: light-dark(@dark-blue, @golden); + 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..fa59d0f5 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/mainCharacter.less'; +@import './group-roll-dialog/sheet.less'; + @import './image-select/sheet.less'; @import './item-transfer/sheet.less'; 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 index 09768e7d..24f317a2 100644 --- a/templates/dialogs/groupRollDialog/groupRoll.hbs +++ b/templates/dialogs/groupRollDialog/groupRoll.hbs @@ -1,3 +1,16 @@ -
- Test -
\ No newline at end of file +
+
+ {{localize "Result"}} + +
+ {{groupRoll.total}} {{groupRoll.totalLabel}} +
+ {{#if groupRoll.mainCharacterTotal includeZero=true}}{{groupRoll.mainCharacterTotal}}{{else}}{{localize "
"}}{{/if}} + {{#each groupRoll.modifiers as |modifier|}} + {{#if (gte modifier 0)}}+{{else}}-{{/if}} + {{positive modifier}} + {{/each}} +
+
+
+
\ No newline at end of file diff --git a/templates/dialogs/groupRollDialog/groupRollMainCharacter.hbs b/templates/dialogs/groupRollDialog/groupRollMainCharacter.hbs new file mode 100644 index 00000000..0a090acf --- /dev/null +++ b/templates/dialogs/groupRollDialog/groupRollMainCharacter.hbs @@ -0,0 +1,71 @@ +{{#with mainCharacter}} +
+
{{localize "Main Character"}}
+
+
+
+ +
+ {{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/groupRollDialog/groupRollMember.hbs b/templates/dialogs/groupRollDialog/groupRollMember.hbs index e69de29b..af1e7909 100644 --- a/templates/dialogs/groupRollDialog/groupRollMember.hbs +++ 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"}}{{#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..06741363 --- /dev/null +++ b/templates/dialogs/groupRollDialog/initialization.hbs @@ -0,0 +1,32 @@ +
+
+ {{#each memberSelection as |member|}} + + {{member.name}} + + + {{/each}} +
+ +
+
+ +
+ +
+
+
+ +
+ +
+ {{localize "DAGGERHEART.APPLICATIONS.GroupRollSelect.openDialogForAll"}} + +
+
+
\ 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|}}