This commit is contained in:
WBHarry 2026-04-09 00:14:44 +02:00
parent 6c761b1840
commit 3ddfb7e75e
18 changed files with 1021 additions and 24 deletions

View file

@ -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', '<div class="team-container"></div>');
initializationPart.insertAdjacentHTML(
'afterend',
`<div class="section-title">${game.i18n.localize('Aiding Characters')}</div>`
);
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 });
}
}