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