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 @@
+
+
+ {{localize "DAGGERHEART.APPLICATIONS.GroupRollSelect.cancelGroupRoll"}}
+ {{localize "DAGGERHEART.APPLICATIONS.GroupRollSelect.finishGroupRoll"}}
+
+
\ 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"}}
+
+
+
+
+
+
+
+
+ {{#if readyToRoll}}
+
+
+ {{localize "DAGGERHEART.GENERAL.roll"}}
+
+
+
+ {{#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)}}
+
+
+
+
+
+
+ {{#if readyToRoll}}
+
+
+ {{localize "DAGGERHEART.GENERAL.roll"}}
+
+
+
+ {{#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 @@
+
\ 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|}}