[V14] TagTeamRoll Rework (#1732)

* Initial rolls working

* Fixed reroll

* more

* More work

* Added results section

* .

* Visual improvements

* .

* Removed traces of old TagTeamRoll

* Added initiator handling

* Added updating for other players

* Fixed sync start

* Completed finish method

* Damage reroll

* Fixed localization

* Fixed crit damage

* Fixes

* Added visual of advantage and disadvantage dice
This commit is contained in:
WBHarry 2026-03-16 09:31:15 +01:00 committed by GitHub
parent a7eda31aec
commit 3031531b14
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 1301 additions and 738 deletions

View file

@ -35,7 +35,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
updateIsAdvantage: this.updateIsAdvantage,
selectExperience: this.selectExperience,
toggleReaction: this.toggleReaction,
toggleTagTeamRoll: this.toggleTagTeamRoll,
toggleSelectedEffect: this.toggleSelectedEffect,
submitRoll: this.submitRoll
},
@ -133,12 +132,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.reactionOverride = this.reactionOverride;
}
const tagTeamSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
if (this.actor && tagTeamSetting.members[this.actor.id] && !this.config.skips?.createMessage) {
context.activeTagTeamRoll = true;
context.tagTeamSelected = this.config.tagTeamSelected;
}
return context;
}
@ -215,11 +208,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
}
}
static toggleTagTeamRoll() {
this.config.tagTeamSelected = !this.config.tagTeamSelected;
this.render();
}
static toggleSelectedEffect(_event, button) {
this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected;
this.render();

View file

@ -1,5 +1,3 @@
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) {
@ -123,16 +121,8 @@ export default class RerollDamageDialog extends HandlebarsApplicationMixin(Appli
return acc;
}, {})
};
await this.message.update(update);
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
}
});
await this.close();
}

View file

@ -1,5 +1,7 @@
import { MemberData } from '../../data/tagTeamData.mjs';
import { getCritDamageBonus } from '../../helpers/utils.mjs';
import { GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
import Party from '../sheets/actors/party.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -7,15 +9,23 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
constructor(party) {
super();
this.data = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
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: false
}));
this.intiator = null;
this.openForAllPlayers = true;
this.setupHooks = Hooks.on(socketEvent.Refresh, ({ refreshType }) => {
if (refreshType === RefreshType.TagTeamRoll) {
this.data = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
this.render();
}
});
this.tabGroups.application = Object.keys(party.system.tagTeam.members).length
? 'tagTeamRoll'
: 'initialization';
Hooks.on(socketEvent.Refresh, this.tagTeamRefresh.bind());
}
get title() {
@ -24,324 +34,633 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
static DEFAULT_OPTIONS = {
tag: 'form',
id: 'TagTeamDialog',
classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'tag-team-dialog'],
position: { width: 550, height: 'auto' },
actions: {
removeMember: TagTeamDialog.#removeMember,
unlinkMessage: TagTeamDialog.#unlinkMessage,
selectMessage: TagTeamDialog.#selectMessage,
createTagTeam: TagTeamDialog.#createTagTeam
toggleSelectMember: TagTeamDialog.#toggleSelectMember,
startTagTeamRoll: TagTeamDialog.#startTagTeamRoll,
makeRoll: TagTeamDialog.#makeRoll,
removeRoll: TagTeamDialog.#removeRoll,
rerollDice: TagTeamDialog.#rerollDice,
makeDamageRoll: TagTeamDialog.#makeDamageRoll,
removeDamageRoll: TagTeamDialog.#removeDamageRoll,
rerollDamageDice: TagTeamDialog.#rerollDamageDice,
selectRoll: TagTeamDialog.#selectRoll,
cancelRoll: TagTeamDialog.#onCancelRoll,
finishRoll: TagTeamDialog.#finishRoll
},
form: { handler: this.updateData, submitOnChange: true, closeOnSubmit: false }
};
static PARTS = {
application: {
id: 'tag-team-dialog',
template: 'systems/daggerheart/templates/dialogs/tagTeamDialog.hbs'
initialization: {
id: 'initialization',
template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/initialization.hbs'
},
tagTeamRoll: {
id: 'tagTeamRoll',
template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/tagTeamRoll.hbs'
}
};
/** @inheritdoc */
static TABS = {
application: {
tabs: [{ id: 'initialization' }, { id: 'tagTeamRoll' }]
}
};
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
for (const element of htmlElement.querySelectorAll('.roll-type-select'))
element.addEventListener('change', this.updateRollType.bind(this));
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.hopeCost = this.hopeCost;
context.data = this.data;
context.memberOptions = this.party.filter(c => !this.data.members[c.id]);
context.selectedCharacterOptions = this.party.filter(c => this.data.members[c.id]);
context.members = Object.keys(this.data.members).map(id => {
const roll = this.data.members[id].messageId ? game.messages.get(this.data.members[id].messageId) : null;
context.usesDamage =
context.usesDamage === undefined
? roll?.system.hasDamage
: context.usesDamage && roll?.system.hasDamage;
return {
character: this.party.find(x => x.id === id),
selected: this.data.members[id].selected,
roll: roll,
damageValues: roll
? Object.keys(roll.system.damage).map(key => ({
key: key,
name: game.i18n.localize(CONFIG.DH.GENERAL.healingTypes[key].label),
total: roll.system.damage[key].total
}))
: null
};
});
const initiatorChar = this.party.find(x => x.id === this.data.initiator.id);
context.initiator = {
character: initiatorChar,
cost: this.data.initiator.cost
};
const selectedMember = Object.values(context.members).find(x => x.selected && x.roll);
const selectedIsCritical = selectedMember?.roll?.system?.isCritical;
context.selectedData = {
result: selectedMember
? `${selectedMember.roll.system.roll.total} ${selectedMember.roll.system.roll.result.label}`
: null,
damageValues: null
};
for (const member of Object.values(context.members)) {
if (!member.roll) continue;
if (context.usesDamage) {
if (!context.selectedData.damageValues) context.selectedData.damageValues = {};
for (let damage of member.damageValues) {
const damageTotal = member.roll.system.isCritical
? damage.total
: selectedIsCritical
? damage.total + (await getCritDamageBonus(member.roll.system.damage[damage.key].formula))
: damage.total;
if (context.selectedData.damageValues[damage.key]) {
context.selectedData.damageValues[damage.key].total += damageTotal;
} else {
context.selectedData.damageValues[damage.key] = {
...foundry.utils.deepClone(damage),
total: damageTotal
};
}
}
}
}
context.showResult = Object.values(context.members).reduce((enabled, member) => {
if (!member.roll) return enabled;
if (context.usesDamage) {
enabled = enabled === null ? member.damageValues.length > 0 : enabled && member.damageValues.length > 0;
} else {
enabled = enabled === null ? Boolean(member.roll) : enabled && Boolean(member.roll);
}
return enabled;
}, null);
context.createDisabled =
!context.selectedData.result ||
!this.data.initiator.id ||
Object.keys(this.data.members).length === 0 ||
Object.values(context.members).some(x =>
context.usesDamage ? !x.damageValues || x.damageValues.length === 0 : !x.roll
);
context.isEditable = this.getIsEditable();
return context;
}
async updateSource(update) {
await this.data.updateSource(update);
async _preparePartContext(partId, context, options) {
const partContext = await super._preparePartContext(partId, context, options);
switch (partId) {
case 'initialization':
partContext.tagTeamFields = this.party.system.schema.fields.tagTeam.fields;
partContext.memberSelection = this.partyMembers;
const selectedMembers = partContext.memberSelection.filter(x => x.selected);
if (game.user.isGM) {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, this.data.toObject());
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
partContext.allSelected = selectedMembers.length === 2;
partContext.canStartTagTeam = partContext.allSelected && this.initiator;
partContext.initiator = this.initiator;
partContext.initiatorOptions = selectedMembers.map(x => ({ value: x.id, label: x.name }));
partContext.initiatorDisabled = !selectedMembers.length;
partContext.openForAllPlayers = this.openForAllPlayers;
break;
case 'tagTeamRoll':
partContext.fields = this.party.system.schema.fields.tagTeam.fields;
partContext.data = this.party.system.tagTeam;
partContext.rollTypes = CONFIG.DH.GENERAL.tagTeamRollTypes;
partContext.traitOptions = CONFIG.DH.ACTOR.abilities;
const selectedRoll = Object.values(this.party.system.tagTeam.members).find(member => member.selected);
const critSelected = !selectedRoll
? undefined
: (selectedRoll?.rollData?.options?.roll?.isCritical ?? false);
partContext.members = {};
for (const actorId in this.party.system.tagTeam.members) {
const data = this.party.system.tagTeam.members[actorId];
const actor = game.actors.get(actorId);
const rollOptions = [];
const damageRollOptions = [];
for (const item of actor.items) {
if (item.system.metadata.hasActions) {
const actions = [
...item.system.actions,
...(item.system.attack ? [item.system.attack] : [])
];
for (const action of actions) {
if (action.hasRoll) {
const actionItem = {
value: action.uuid,
label: action.name,
group: item.name,
baseAction: action.baseAction
};
rollOptions.push(actionItem);
if (action.hasDamage) damageRollOptions.push(actionItem);
}
}
}
}
const damage = data.rollData?.options?.damage;
partContext.hasDamage |= Boolean(damage);
const critHitPointsDamage = await this.getCriticalDamage(damage);
partContext.members[actorId] = {
...data,
isEditable: actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER),
key: actorId,
readyToRoll: Boolean(data.rollChoice),
hasRolled: Boolean(data.rollData),
rollOptions,
damageRollOptions,
damage: damage,
critDamage: critHitPointsDamage,
useCritDamage:
critSelected || (critSelected === undefined && data.rollData?.options?.roll?.isCritical)
};
}
partContext.hintText = await this.getInfoTexts(this.party.system.tagTeam.members);
partContext.joinedRoll = await this.getJoinedRoll({
overrideIsCritical: critSelected,
displayVersion: true
});
break;
}
return partContext;
}
static async updateData(_event, _, formData) {
const { initiator, openForAllPlayers, ...partyData } = foundry.utils.expandObject(formData.object);
this.initiator = initiator;
this.openForAllPlayers = openForAllPlayers !== undefined ? openForAllPlayers : this.openForAllPlayers;
this.updatePartyData(partyData);
}
async updatePartyData(updata, options = { render: true }) {
await this.party.update(updata);
if (options.render) {
this.render(true);
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
}
});
} else {
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateSetting,
uuid: CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll,
update: this.data.toObject(),
refresh: { refreshType: RefreshType.TagTeamRoll }
}
data: { refreshType: RefreshType.TagTeamRoll, action: 'refresh' }
});
}
}
static async updateData(_event, _element, formData) {
const { selectedAddMember, initiator } = foundry.utils.expandObject(formData.object);
const update = { initiator: initiator };
if (selectedAddMember) {
const member = await foundry.utils.fromUuid(selectedAddMember);
update[`members.${member.id}`] = { messageId: null };
}
await this.updateSource(update);
this.render();
}
static async #removeMember(_, button) {
const update = { [`members.${button.dataset.characterId}`]: _del };
if (this.data.initiator.id === button.dataset.characterId) {
update.iniator = { id: null };
}
await this.updateSource(update);
}
static async #unlinkMessage(_, button) {
await this.updateSource({ [`members.${button.id}.messageId`]: null });
}
static async #selectMessage(_, button) {
const member = this.data.members[button.id];
const currentSelected = Object.keys(this.data.members).find(key => this.data.members[key].selected);
const curretSelectedUpdate =
currentSelected && currentSelected !== button.id ? { [`${currentSelected}`]: { selected: false } } : {};
await this.updateSource({
members: {
[`${button.id}`]: { selected: !member.selected },
...curretSelectedUpdate
}
getIsEditable() {
return this.party.system.partyMembers.some(actor => {
const selected = Boolean(this.party.system.tagTeam.members[actor.id]);
return selected && actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER);
});
}
static async #createTagTeam() {
const mainRollId = Object.keys(this.data.members).find(key => this.data.members[key].selected);
const mainRoll = game.messages.get(this.data.members[mainRollId].messageId);
tagTeamRefresh = ({ refreshType, action }) => {
if (refreshType !== RefreshType.TagTeamRoll) return;
if (this.data.initiator.cost) {
const initiator = this.party.find(x => x.id === this.data.initiator.id);
if (initiator.system.resources.hope.value < this.data.initiator.cost) {
switch (action) {
case 'startTagTeamRoll':
this.tabGroups.application = 'tagTeamRoll';
break;
case 'refresh':
this.render();
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.tagTeamRefresh);
return super.close(options);
}
checkInitiatorHopeError(initiator) {
if (initiator.cost && initiator.memberId) {
const actor = game.actors.get(initiator.memberId);
if (actor.system.resources.hope.value < initiator.cost) {
return ui.notifications.warn(
game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.insufficientHope')
);
}
}
}
const secondaryRolls = Object.keys(this.data.members)
.filter(key => key !== mainRollId)
.map(key => game.messages.get(this.data.members[key].messageId));
//#region Initialization
static #toggleSelectMember(_, button) {
const member = this.partyMembers.find(x => x.id === button.dataset.id);
if (member.selected && this.initiator?.memberId === member.id) this.initiator = null;
const systemData = foundry.utils.deepClone(mainRoll).system.toObject();
const criticalRoll = systemData.roll.isCritical;
for (let roll of secondaryRolls) {
if (roll.system.hasDamage) {
for (let key in roll.system.damage) {
var damage = roll.system.damage[key];
const damageTotal =
!roll.system.isCritical && criticalRoll
? (await getCritDamageBonus(damage.formula)) + damage.total
: damage.total;
const updatedDamageParts = damage.parts;
if (systemData.damage[key]) {
if (!roll.system.isCritical && criticalRoll) {
for (let part of updatedDamageParts) {
const criticalDamage = await getCritDamageBonus(part.formula);
if (criticalDamage) {
damage.formula = `${damage.formula} + ${criticalDamage}`;
part.formula = `${part.formula} + ${criticalDamage}`;
part.modifierTotal = part.modifierTotal + criticalDamage;
part.total += criticalDamage;
part.roll = new Roll(part.formula);
}
}
}
member.selected = !member.selected;
this.render();
}
systemData.damage[key].formula = `${systemData.damage[key].formula} + ${damage.formula}`;
systemData.damage[key].total += damageTotal;
systemData.damage[key].parts = [...systemData.damage[key].parts, ...updatedDamageParts];
} else {
systemData.damage[key] = { ...damage, total: damageTotal, parts: updatedDamageParts };
}
static async #startTagTeamRoll() {
const error = this.checkInitiatorHopeError(this.initiator);
if (error) return error;
await this.party.update({
'system.tagTeam': _replace(
new game.system.api.data.TagTeamData({
...this.party.system.tagTeam.toObject(),
initiator: this.initiator,
members: this.partyMembers.reduce((acc, member) => {
if (member.selected)
acc[member.id] = {
name: member.name,
img: member.img,
rollType: CONFIG.DH.GENERAL.tagTeamRollTypes.trait.id
};
return acc;
}, {})
})
)
});
const hookData = { openForAllPlayers: this.openForAllPlayers, partyId: this.party.id };
Hooks.callAll(CONFIG.DH.HOOKS.hooksConfig.tagTeamStart, hookData);
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.TagTeamStart,
data: hookData
});
this.render();
}
//#endregion
//#region Tag Team Roll
async getInfoTexts(members) {
let rollsAreFinished = true;
let rollIsSelected = false;
for (const member of Object.values(members)) {
const rollFinished = Boolean(member.rollData);
const damageFinished =
member.rollData?.options?.hasDamage !== undefined ? member.rollData.options.damage : true;
rollsAreFinished = rollsAreFinished && rollFinished && damageFinished;
rollIsSelected = rollIsSelected || member.selected;
}
let hint = null;
if (!rollsAreFinished) hint = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.hints.completeRolls');
else if (!rollIsSelected) hint = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.hints.selectRoll');
return hint;
}
async updateRollType(event) {
this.updatePartyData({
[`system.tagTeam.members.${event.target.dataset.member}`]: {
rollType: event.target.value,
rollChoice: null
}
});
}
static async #removeRoll(_, button) {
this.updatePartyData({
[`system.tagTeam.members.${button.dataset.member}`]: {
rollData: null,
rollChoice: null,
selected: false
}
});
}
static async #makeRoll(event, button) {
const { member } = button.dataset;
let result = null;
switch (this.party.system.tagTeam.members[member].rollType) {
case CONFIG.DH.GENERAL.tagTeamRollTypes.trait.id:
result = await this.makeTraitRoll(member);
break;
case CONFIG.DH.GENERAL.tagTeamRollTypes.ability.id:
case CONFIG.DH.GENERAL.tagTeamRollTypes.damageAbility.id:
result = await this.makeAbilityRoll(event, member);
break;
}
if (!result) return;
if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
const rollData = result.messageRoll.toJSON();
delete rollData.options.messageRoll;
this.updatePartyData({
[`system.tagTeam.members.${member}.rollData`]: rollData
});
}
async makeTraitRoll(memberKey) {
const actor = game.actors.find(x => x.id === memberKey);
if (!actor) return;
const memberData = this.party.system.tagTeam.members[memberKey];
return await actor.rollTrait(memberData.rollChoice, {
skips: {
createMessage: true,
resources: true,
triggers: true
}
});
}
async makeAbilityRoll(event, memberKey) {
const actor = game.actors.find(x => x.id === memberKey);
if (!actor) return;
const memberData = this.party.system.tagTeam.members[memberKey];
const action = await foundry.utils.fromUuid(memberData.rollChoice);
return await action.use(event, {
skips: {
createMessage: true,
resources: true,
triggers: true
}
});
}
static async #rerollDice(_, button) {
const { member, diceType } = button.dataset;
const memberData = this.party.system.tagTeam.members[member];
const dieIndex = diceType === 'hope' ? 0 : diceType === 'fear' ? 2 : 4;
const { parsedRoll, newRoll } = await game.system.api.dice.DualityRoll.reroll(
memberData.rollData,
dieIndex,
diceType
);
const rollData = parsedRoll.toJSON();
this.updatePartyData({
[`system.tagTeam.members.${member}.rollData`]: {
...rollData,
options: {
...rollData.options,
roll: newRoll
}
}
});
}
static async #makeDamageRoll(event, button) {
const { memberKey } = button.dataset;
const actor = game.actors.find(x => x.id === memberKey);
if (!actor) return;
const memberData = this.party.system.tagTeam.members[memberKey];
const action = await foundry.utils.fromUuid(memberData.rollChoice);
const config = {
...memberData.rollData.options,
dialog: {
configure: !event.shiftKey
},
skips: {
createMessage: true,
resources: true,
triggers: true
}
};
await action.workflow.get('damage').execute(config, null, true);
if (!config.damage) return;
// const damage = config.roll.isCritical ? await this.getNonCriticalDamage(config, actor) : config.damage;
const current = this.party.system.tagTeam.members[memberKey].rollData;
await this.updatePartyData({
[`system.tagTeam.members.${memberKey}.rollData`]: {
...current,
options: {
...current.options,
damage: config.damage
}
}
});
}
static async #removeDamageRoll(_, button) {
const { memberKey } = button.dataset;
const current = this.party.system.tagTeam.members[memberKey].rollData;
this.updatePartyData({
[`system.tagTeam.members.${memberKey}.rollData`]: {
...current,
options: {
...current.options,
damage: null
}
}
});
}
static async #rerollDamageDice(_, button) {
const { memberKey, damageKey, part, dice } = button.dataset;
const memberData = this.party.system.tagTeam.members[memberKey];
const partData = memberData.rollData.options.damage[damageKey].parts[part];
const activeDiceResultKey = Object.keys(partData.dice[dice].results).find(
index => partData.dice[dice].results[index].active
);
const { parsedRoll, rerolledDice } = await game.system.api.dice.DamageRoll.reroll(
partData,
dice,
activeDiceResultKey
);
const rollData = this.party.system.tagTeam.members[memberKey].rollData;
rollData.options.damage[damageKey].parts = rollData.options.damage[damageKey].parts.map((damagePart, index) => {
if (index !== Number.parseInt(part)) return damagePart;
return {
...damagePart,
total: parsedRoll.total,
dice: rerolledDice
};
});
rollData.options.damage[damageKey].total = rollData.options.damage[damageKey].parts.reduce((acc, part) => {
acc += part.total;
return acc;
}, 0);
this.updatePartyData({
[`system.tagTeam.members.${memberKey}.rollData`]: rollData
});
}
async getCriticalDamage(damage) {
const newDamage = foundry.utils.deepClone(damage);
for (let key in newDamage) {
var damage = newDamage[key];
damage.formula = '';
damage.total = 0;
for (let part of damage.parts) {
const criticalDamage = await getCritDamageBonus(part.formula);
if (criticalDamage) {
part.modifierTotal += criticalDamage;
part.total += criticalDamage;
part.formula = `${part.dice.map(x => x.formula).join(' + ')} + ${part.modifierTotal}`;
part.roll = new Roll(part.formula);
}
damage.formula = [damage.formula, part.formula].filter(x => x).join(' + ');
damage.total += part.total;
}
}
systemData.title = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.chatMessageRollTitle');
return newDamage;
}
async getNonCriticalDamage(config) {
const newDamage = foundry.utils.deepClone(config.damage);
for (let key in newDamage) {
var damage = newDamage[key];
damage.formula = '';
damage.total = 0;
for (let part of damage.parts) {
const critDamageBonus = await getCritDamageBonus(part.formula);
part.modifierTotal -= critDamageBonus;
part.total -= critDamageBonus;
part.formula = `${part.dice.map(x => x.formula).join(' + ')} + ${part.modifierTotal}`;
part.roll = new Roll(part.formula);
damage.formula = [damage.formula, part.formula].filter(x => x).join(' + ');
damage.total += part.total;
}
}
return newDamage;
}
static async #selectRoll(_, button) {
const { memberKey } = button.dataset;
this.updatePartyData({
[`system.tagTeam.members`]: Object.entries(this.party.system.tagTeam.members).reduce(
(acc, [key, member]) => {
acc[key] = { selected: key === memberKey ? !member.selected : false };
return acc;
},
{}
)
});
}
async getJoinedRoll({ overrideIsCritical, displayVersion } = {}) {
const memberValues = Object.values(this.party.system.tagTeam.members);
const selectedRoll = memberValues.find(x => x.selected);
let baseMainRoll = selectedRoll ?? memberValues[0];
let baseSecondaryRoll = selectedRoll
? memberValues.find(x => !x.selected)
: memberValues.length > 1
? memberValues[1]
: null;
if (!baseMainRoll?.rollData || !baseSecondaryRoll) return null;
const mainRoll = new MemberData(baseMainRoll.toObject());
const secondaryRollData = new MemberData(baseSecondaryRoll.toObject()).rollData;
const systemData = mainRoll.rollData.options;
const isCritical = overrideIsCritical ?? systemData.roll.isCritical;
if (isCritical) systemData.damage = await this.getCriticalDamage(systemData.damage);
if (secondaryRollData?.options.hasDamage) {
const secondaryDamage = (displayVersion ? overrideIsCritical : isCritical)
? await this.getCriticalDamage(secondaryRollData.options.damage)
: secondaryRollData.options.damage;
if (systemData.damage) {
for (const key in secondaryDamage) {
const damage = secondaryDamage[key];
systemData.damage[key].formula = [systemData.damage[key].formula, damage.formula]
.filter(x => x)
.join(' + ');
systemData.damage[key].total += damage.total;
systemData.damage[key].parts.push(...damage.parts);
}
} else {
systemData.damage = secondaryDamage;
}
}
return mainRoll;
}
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.TagTeamSelect.cancelConfirmTitle')
},
content: game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.cancelConfirmText')
});
if (!confirmed) return;
}
await this.updatePartyData(
{
'system.tagTeam': {
initiator: null,
members: _replace({})
}
},
{ render: false }
);
this.close();
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: { refreshType: RefreshType.TagTeamRoll, action: 'close' }
});
}
static async #finishRoll() {
const error = this.checkInitiatorHopeError(this.party.system.tagTeam.initiator);
if (error) return error;
const mainRoll = (await this.getJoinedRoll()).rollData;
const mainActor = this.party.system.partyMembers.find(x => x.uuid === mainRoll.options.source.actor);
mainRoll.options.title = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.chatMessageRollTitle');
const cls = getDocumentClass('ChatMessage'),
msgData = {
type: 'dualityRoll',
user: game.user.id,
title: game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.title'),
speaker: cls.getSpeaker({ actor: this.party.find(x => x.id === mainRollId) }),
system: systemData,
rolls: mainRoll.rolls,
speaker: cls.getSpeaker({ actor: mainActor }),
system: mainRoll.options,
rolls: [mainRoll],
sound: null,
flags: { core: { RollTable: true } }
};
await cls.create(msgData);
/* Handle resource updates from the finished TagTeamRoll */
const tagTeamData = this.party.system.tagTeam;
const fearUpdate = { key: 'fear', value: null, total: null, enabled: true };
for (let memberId of Object.keys(this.data.members)) {
for (let memberId in tagTeamData.members) {
const resourceUpdates = [];
const rollGivesHope = systemData.roll.isCritical || systemData.roll.result.duality === 1;
if (memberId === this.data.initiator.id) {
const value = this.data.initiator.cost
const rollGivesHope = mainRoll.options.roll.isCritical || mainRoll.options.roll.result.duality === 1;
if (memberId === tagTeamData.initiator.memberId) {
const value = tagTeamData.initiator.cost
? rollGivesHope
? 1 - this.data.initiator.cost
: -this.data.initiator.cost
? 1 - tagTeamData.initiator.cost
: -tagTeamData.initiator.cost
: 1;
resourceUpdates.push({ key: 'hope', value: value, total: -value, enabled: true });
} else if (rollGivesHope) {
resourceUpdates.push({ key: 'hope', value: 1, total: -1, enabled: true });
}
if (systemData.roll.isCritical) resourceUpdates.push({ key: 'stress', value: -1, total: 1, enabled: true });
if (systemData.roll.result.duality === -1) {
if (mainRoll.options.roll.isCritical)
resourceUpdates.push({ key: 'stress', value: -1, total: 1, enabled: true });
if (mainRoll.options.roll.result.duality === -1) {
fearUpdate.value = fearUpdate.value === null ? 1 : fearUpdate.value + 1;
fearUpdate.total = fearUpdate.total === null ? -1 : fearUpdate.total - 1;
}
this.party.find(x => x.id === memberId).modifyResource(resourceUpdates);
game.actors.get(memberId).modifyResource(resourceUpdates);
}
if (fearUpdate.value) {
this.party.find(x => x.id === mainRollId).modifyResource([fearUpdate]);
mainActor.modifyResource([fearUpdate]);
}
/* Improve by fetching default from schema */
const update = { members: [], initiator: { id: null, cost: 3 } };
if (game.user.isGM) {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, update);
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
}
});
} else {
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateSetting,
uuid: CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll,
update: update,
refresh: { refreshType: RefreshType.TagTeamRoll }
}
});
}
/* Fin */
this.cancelRoll({ confirm: false });
}
static async assignRoll(char, message) {
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
const character = settings.members[char.id];
if (!character) return;
await settings.updateSource({ [`members.${char.id}.messageId`]: message.id });
if (game.user.isGM) {
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, settings);
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
}
});
} else {
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateSetting,
uuid: CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll,
update: settings,
refresh: { refreshType: RefreshType.TagTeamRoll }
}
});
}
}
async close(options = {}) {
Hooks.off(socketEvent.Refresh, this.setupHooks);
await super.close(options);
}
//#endregion
}

View file

@ -1,6 +1,5 @@
import DHBaseActorSheet from '../api/base-actor.mjs';
import DhDeathMove from '../../dialogs/deathMove.mjs';
import { abilities } from '../../../config/actorConfig.mjs';
import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs';
import DhCharacterCreation from '../../characterCreation/characterCreation.mjs';
import FilterMenu from '../../ux/filter-menu.mjs';
@ -720,35 +719,16 @@ export default class CharacterSheet extends DHBaseActorSheet {
* Rolls an attribute check based on the clicked button's dataset attribute.
* @type {ApplicationClickAction}
*/
static async #rollAttribute(event, button) {
const abilityLabel = game.i18n.localize(abilities[button.dataset.attribute].label);
const config = {
event: event,
title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.actor.name}`,
headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: abilityLabel
}),
effects: await game.system.api.data.actions.actionsTypes.base.getEffects(this.document),
roll: {
trait: button.dataset.attribute,
type: 'trait'
},
hasRoll: true,
actionType: 'action',
headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.actor.name}`,
title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: abilityLabel
})
};
const result = await this.document.diceRoll(config);
static async #rollAttribute(_event, button) {
const result = await this.document.rollTrait(button.dataset.attribute);
if (!result) return;
/* This could be avoided by baking config.costs into config.resourceUpdates. Didn't feel like messing with it at the time */
const costResources =
result.costs?.filter(x => x.enabled).map(cost => ({ ...cost, value: -cost.value, total: -cost.total })) ||
{};
config.resourceUpdates.addResources(costResources);
await config.resourceUpdates.updateResources();
result.resourceUpdates.addResources(costResources);
await result.resourceUpdates.updateResources();
}
//TODO: redo toggleEquipItem method

View file

@ -35,9 +35,7 @@ export default class Party extends DHBaseActorSheet {
refeshActions: Party.#refeshActions,
triggerRest: Party.#triggerRest,
tagTeamRoll: Party.#tagTeamRoll,
groupRoll: Party.#groupRoll,
selectRefreshable: DaggerheartMenu.selectRefreshable,
refreshActors: DaggerheartMenu.refreshActors
groupRoll: Party.#groupRoll
},
dragDrop: [{ dragSelector: '[data-item-id]', dropSelector: null }]
};
@ -120,6 +118,7 @@ export default class Party extends DHBaseActorSheet {
secrets: this.document.isOwner,
relativeTo: this.document
});
context.tagTeamActive = Boolean(this.document.system.tagTeam.initiator);
}
/**
@ -255,11 +254,7 @@ export default class Party extends DHBaseActorSheet {
}
static async #tagTeamRoll() {
new game.system.api.applications.dialogs.TagTeamDialog(
this.document.system.partyMembers.filter(x => Party.DICE_ROLL_ACTOR_TYPES.includes(x.type))
).render({
force: true
});
new game.system.api.applications.dialogs.TagTeamDialog(this.document).render({ force: true });
}
static async #groupRoll(_params) {

View file

@ -190,7 +190,24 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
const target = event.target.closest('[data-die-index]');
if (target.dataset.type === 'damage') {
game.system.api.dice.DamageRoll.reroll(target, message);
const { damageType, part, dice, result } = target.dataset;
const damagePart = message.system.damage[damageType].parts[part];
const { parsedRoll, rerolledDice } = await game.system.api.dice.DamageRoll.reroll(damagePart, dice, result);
const damageParts = message.system.damage[damageType].parts.map((damagePart, index) => {
if (index !== Number(part)) return damagePart;
return {
...damagePart,
total: parsedRoll.total,
dice: rerolledDice
};
});
const updateMessage = game.messages.get(message._id);
await updateMessage.update({
[`system.damage.${damageType}`]: {
total: parsedRoll.total,
parts: damageParts
}
});
} else {
let originalRoll_parsed = message.rolls.map(roll => JSON.parse(roll))[0];
const rollClass =
@ -204,20 +221,16 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
const { newRoll, parsedRoll } = await rollClass.reroll(originalRoll_parsed, target, message);
const { newRoll, parsedRoll } = await rollClass.reroll(
originalRoll_parsed,
target.dataset.dieIndex,
target.dataset.type
);
await game.messages.get(message._id).update({
'system.roll': newRoll,
'rolls': [parsedRoll]
});
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
}
});
}
}

View file

@ -44,7 +44,10 @@ export default class DhRegionLayer extends foundry.canvas.layers.RegionLayer {
if (game.activeTool === 'inFront') return { type: 'cone', x: 0, y: 0, radius: 0, angle: 180, hole };
const shape = super._createDragShapeData(event);
const token = shape?.type === 'emanation' && shape.base?.type === 'token' ? this.#findTokenInBounds(event.interactionData.origin) : null;
const token =
shape?.type === 'emanation' && shape.base?.type === 'token'
? this.#findTokenInBounds(event.interactionData.origin)
: null;
if (token) {
shape.base.width = token.width;
shape.base.height = token.height;

View file

@ -959,6 +959,21 @@ export const sceneRangeMeasurementSetting = {
}
};
export const tagTeamRollTypes = {
trait: {
id: 'trait',
label: 'DAGGERHEART.CONFIG.TagTeamRollTypes.trait'
},
ability: {
id: 'ability',
label: 'DAGGERHEART.CONFIG.TagTeamRollTypes.ability'
},
damageAbility: {
id: 'damageAbility',
label: 'DAGGERHEART.CONFIG.TagTeamRollTypes.damageAbility'
}
};
export const activeEffectModes = {
custom: {
id: 'custom',

View file

@ -1,4 +1,5 @@
export const hooksConfig = {
effectDisplayToggle: 'DHEffectDisplayToggle',
lockedTooltipDismissed: 'DHLockedTooltipDismissed'
lockedTooltipDismissed: 'DHLockedTooltipDismissed',
tagTeamStart: 'DHTagTeamRollStart'
};

View file

@ -34,7 +34,6 @@ export const gameSettings = {
LevelTiers: 'LevelTiers',
Countdowns: 'Countdowns',
LastMigrationVersion: 'LastMigrationVersion',
TagTeamRoll: 'TagTeamRoll',
SpotlightRequestQueue: 'SpotlightRequestQueue',
CompendiumBrowserSettings: 'CompendiumBrowserSettings'
};

View file

@ -1,9 +1,9 @@
export { default as DhCombat } from './combat.mjs';
export { default as DhCombatant } from './combatant.mjs';
export { default as DhTagTeamRoll } from './tagTeamRoll.mjs';
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 * as countdowns from './countdowns.mjs';
export * as actions from './action/_module.mjs';

View file

@ -50,9 +50,8 @@ export default class DHAttackAction extends DHDamageAction {
async use(event, options) {
const result = await super.use(event, options);
if (!result.message) return;
if (result.message.system.action.roll?.type === 'attack') {
if (result.message?.system.action.roll?.type === 'attack') {
const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;
await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.characterAttack.id);
}

View file

@ -207,10 +207,10 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
* @param {Event} event Event from the button used to trigger the Action
* @returns {object}
*/
async use(event) {
async use(event, configOptions = {}) {
if (!this.actor) throw new Error("An Action can't be used outside of an Actor context.");
let config = this.prepareConfig(event);
let config = this.prepareConfig(event, configOptions);
if (!config) return;
config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(this.actor, this.item);
@ -231,7 +231,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return;
if (this.chatDisplay && !config.actionChatMessageHandled) await this.toChat();
if (this.chatDisplay && !config.skips.createMessage && !config.actionChatMessageHandled) await this.toChat();
return config;
}
@ -241,7 +241,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
* @param {Event} event Event from the button used to trigger the Action
* @returns {object}
*/
prepareBaseConfig(event) {
prepareBaseConfig(event, configOptions = {}) {
const isActor = this.item instanceof CONFIG.Actor.documentClass;
const actionTitle = game.i18n.localize(this.name);
const itemTitle = isActor || this.item.name === actionTitle ? '' : `${this.item.name} - `;
@ -268,7 +268,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
data: this.getRollData(),
evaluate: this.hasRoll,
resourceUpdates: new ResourceUpdateMap(this.actor),
targetUuid: this.targetUuid
targetUuid: this.targetUuid,
...configOptions
};
DHBaseAction.applyKeybindings(config);
@ -280,8 +281,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
* @param {Event} event Event from the button used to trigger the Action
* @returns {object}
*/
prepareConfig(event) {
const config = this.prepareBaseConfig(event);
prepareConfig(event, configOptions = {}) {
const config = this.prepareBaseConfig(event, configOptions);
for (const clsField of Object.values(this.schema.fields)) {
if (clsField?.prepareConfig) if (clsField.prepareConfig.call(this, config) === false) return false;
}

View file

@ -189,19 +189,6 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
return true;
}
async _preDelete() {
/* Clear all partyMembers from tagTeam setting.*/
/* Revisit this when tagTeam is improved for many parties */
if (this.parent.parties.size > 0) {
const tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
await tagTeam.updateSource({
initiator: this.parent.id === tagTeam.initiator ? null : tagTeam.initiator,
members: Object.keys(tagTeam.members).find(x => x === this.parent.id) ? { [this.parent.id]: _del } : {}
});
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, tagTeam);
}
}
async _preUpdate(changes, options, userId) {
const allowed = await super._preUpdate(changes, options, userId);
if (allowed === false) return;

View file

@ -75,10 +75,6 @@ export default class DhEnvironment extends BaseDataActor {
);
scene.update({ 'flags.daggerheart.sceneEnvironments': newSceneEnvironments }).then(() => {
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Scene });
game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: { refreshType: RefreshType.TagTeamRoll }
});
});
}
}

View file

@ -1,5 +1,6 @@
import BaseDataActor from './base.mjs';
import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs';
import TagTeamData from '../tagTeamData.mjs';
export default class DhParty extends BaseDataActor {
/**@inheritdoc */
@ -14,7 +15,8 @@ export default class DhParty extends BaseDataActor {
handfuls: new fields.NumberField({ initial: 1, integer: true }),
bags: new fields.NumberField({ initial: 0, integer: true }),
chests: new fields.NumberField({ initial: 0, integer: true })
})
}),
tagTeam: new fields.EmbeddedDataField(TagTeamData)
};
}
@ -40,23 +42,6 @@ export default class DhParty extends BaseDataActor {
}
}
async _preDelete() {
/* Clear all partyMembers from tagTeam setting.*/
/* Revisit this when tagTeam is improved for many parties */
const tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
await tagTeam.updateSource({
initiator: this.partyMembers.some(x => x.id === tagTeam.initiator) ? null : tagTeam.initiator,
members: Object.keys(tagTeam.members).reduce((acc, key) => {
if (this.partyMembers.find(x => x.id === key)) {
acc[key] = _del;
}
return acc;
}, {})
});
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, tagTeam);
}
_onDelete(options, userId) {
super._onDelete(options, userId);

View file

@ -50,9 +50,9 @@ export default class DamageField extends fields.SchemaField {
formulas = DamageField.formatFormulas.call(this, formulas, config);
const damageConfig = {
dialog: {},
...config,
roll: formulas,
dialog: {},
data: this.getRollData()
};
delete damageConfig.evaluate;

View file

@ -27,7 +27,7 @@ export default class EffectsField extends fields.ArrayField {
static async execute(config, targets = null, force = false) {
if (!config.hasEffect) return;
let message = config.message ?? ui.chat.collection.get(config.parent?._id);
if (!message) {
if (!message && !config.skips.createMessage) {
const roll = new CONFIG.Dice.daggerheart.DHRoll('');
roll._evaluated = true;
message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);

View file

@ -38,7 +38,7 @@ export default class SaveField extends fields.SchemaField {
if (!config.hasSave) return;
let message = config.message ?? ui.chat.collection.get(config.parent?._id);
if (!message) {
if (!message && !config.skips.createMessage) {
const roll = new CONFIG.Dice.daggerheart.DHRoll('');
roll._evaluated = true;
message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config);

View file

@ -16,12 +16,12 @@ export default class IterableTypedObjectField extends foundry.data.fields.TypedO
}
}
/**
* The prototype of an iterable object.
/**
* The prototype of an iterable object.
* This allows the functionality of a class but also allows foundry.utils.getType() to return "Object" instead of "Unknown".
*/
const IterableObjectPrototype = {
[Symbol.iterator]: function*() {
[Symbol.iterator]: function* () {
for (const value of Object.values(this)) {
yield value;
}
@ -29,4 +29,4 @@ const IterableObjectPrototype = {
map: function (func) {
return Array.from(this, func);
}
};
};

View file

@ -0,0 +1,47 @@
export default class TagTeamData extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
initiator: new fields.SchemaField(
{
memberId: new fields.StringField({
required: true,
label: 'DAGGERHEART.APPLICATIONS.TagTeamSelect.FIELDS.initiator.memberId.label'
}),
cost: new fields.NumberField({
integer: true,
initial: 3,
label: 'DAGGERHEART.APPLICATIONS.TagTeamSelect.FIELDS.initiator.cost.label'
})
},
{ nullable: true, initial: null }
),
members: new fields.TypedObjectField(new fields.EmbeddedDataField(MemberData))
};
}
}
export class MemberData extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
name: new fields.StringField({ required: true }),
img: new fields.StringField({ required: true }),
rollType: new fields.StringField({
required: true,
choices: CONFIG.DH.GENERAL.tagTeamRollTypes,
initial: CONFIG.DH.GENERAL.tagTeamRollTypes.trait.id,
label: 'Roll Type'
}),
rollChoice: new fields.StringField({ nullable: true, initial: null }),
rollData: new fields.JSONField({ nullable: true, initial: null }),
selected: new fields.BooleanField({ initial: false })
};
}
get roll() {
return this.rollData ? CONFIG.Dice.daggerheart.DualityRoll.fromData(this.rollData) : null;
}
}

View file

@ -1,20 +0,0 @@
import { DhCharacter } from './actor/_module.mjs';
export default class DhTagTeamRoll extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
initiator: new fields.SchemaField({
id: new fields.StringField({ nullable: true, initial: null }),
cost: new fields.NumberField({ integer: true, min: 0, initial: 3 })
}),
members: new fields.TypedObjectField(
new fields.SchemaField({
messageId: new fields.StringField({ required: true, nullable: true, initial: null }),
selected: new fields.BooleanField({ required: true, initial: false })
})
)
};
}
}

View file

@ -1,6 +1,5 @@
import DamageDialog from '../applications/dialogs/damageDialog.mjs';
import { parseRallyDice } from '../helpers/utils.mjs';
import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs';
import DHRoll from './dhRoll.mjs';
export default class DamageRoll extends DHRoll {
@ -281,10 +280,7 @@ export default class DamageRoll extends DHRoll {
return mods;
}
static async reroll(target, message) {
const { damageType, part, dice, result } = target.dataset;
const rollPart = message.system.damage[damageType].parts[part];
static async reroll(rollPart, dice, result) {
let diceIndex = 0;
let parsedRoll = game.system.api.dice.DamageRoll.fromData({
...rollPart.roll,
@ -353,29 +349,6 @@ export default class DamageRoll extends DHRoll {
};
});
const updateMessage = game.messages.get(message._id);
const damageParts = updateMessage.system.damage[damageType].parts.map((damagePart, index) => {
if (index !== Number(part)) return damagePart;
return {
...rollPart,
total: parsedRoll.total,
dice: rerolledDice
};
});
await updateMessage.update({
[`system.damage.${damageType}`]: {
...updateMessage,
total: parsedRoll.total,
parts: damageParts
}
});
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
}
});
return { parsedRoll, rerolledDice };
}
}

View file

@ -21,6 +21,9 @@ export default class DHRoll extends Roll {
static async build(config = {}, message = {}) {
const roll = await this.buildConfigure(config, message);
if (!roll) return;
if (config.skips?.createMessage) config.messageRoll = roll;
await this.buildEvaluate(roll, config, (message = {}));
await this.buildPost(roll, config, (message = {}));
return config;
@ -30,12 +33,6 @@ export default class DHRoll extends Roll {
config.hooks = [...this.getHooks(), ''];
config.dialog ??= {};
const actorIdSplit = config.source?.actor?.split('.');
if (actorIdSplit) {
const tagTeamSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
config.tagTeamSelected = Boolean(tagTeamSettings.members[actorIdSplit[actorIdSplit.length - 1]]);
}
for (const hook of config.hooks) {
if (Hooks.call(`${CONFIG.DH.id}.preRoll${hook.capitalize()}`, config, message) === false) return null;
}

View file

@ -374,9 +374,9 @@ export default class DualityRoll extends D20Roll {
}
}
static async reroll(rollString, target, message) {
let parsedRoll = game.system.api.dice.DualityRoll.fromData({ ...rollString, evaluated: false });
const term = parsedRoll.terms[target.dataset.dieIndex];
static async reroll(rollBase, dieIndex, diceType) {
let parsedRoll = game.system.api.dice.DualityRoll.fromData({ ...rollBase, evaluated: false });
const term = parsedRoll.terms[dieIndex];
await term.reroll(`/r1=${term.total}`);
const result = await parsedRoll.evaluate();
@ -393,35 +393,35 @@ export default class DualityRoll extends D20Roll {
options: { appearance: {} }
};
const diceSoNicePresets = await getDiceSoNicePresets(result, `d${term._faces}`, `d${term._faces}`);
const type = target.dataset.type;
if (diceSoNicePresets[type]) {
diceSoNiceRoll.dice[0].options = diceSoNicePresets[type];
const diceSoNicePresets = await getDiceSoNicePresets(`d${term._faces}`, `d${term._faces}`);
if (diceSoNicePresets[diceType]) {
diceSoNiceRoll.dice[0].options = diceSoNicePresets[diceType];
}
await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true);
} else {
foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice });
}
const newRoll = game.system.api.dice.DualityRoll.postEvaluate(parsedRoll, {
targets: message.system.targets,
targets: parsedRoll.options.targets ?? [],
roll: {
advantage: message.system.roll.advantage?.type,
difficulty: message.system.roll.difficulty ? Number(message.system.roll.difficulty) : null
advantage: parsedRoll.options.roll.advantage?.type,
difficulty: parsedRoll.options.roll.difficulty ? Number(parsedRoll.options.roll.difficulty) : null
}
});
const extraIndex = newRoll.advantage ? 3 : 2;
newRoll.extra = newRoll.extra.slice(extraIndex);
const tagTeamSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
const actor = message.system.source.actor ? await foundry.utils.fromUuid(message.system.source.actor) : null;
const actor = parsedRoll.options.source.actor
? await foundry.utils.fromUuid(parsedRoll.options.source.actor)
: null;
const config = {
source: { actor: message.system.source.actor ?? '' },
targets: message.system.targets,
tagTeamSelected: Object.values(tagTeamSettings.members).some(x => x.messageId === message._id),
source: { actor: parsedRoll.options.source.actor ?? '' },
targets: parsedRoll.targets,
roll: newRoll,
rerolledRoll: message.system.roll,
rerolledRoll: parsedRoll.roll,
resourceUpdates: new ResourceUpdateMap(actor)
};

View file

@ -4,6 +4,7 @@ import DHFeature from '../data/item/feature.mjs';
import { createScrollText, damageKeyToNumber, getDamageKey } from '../helpers/utils.mjs';
import DhCompanionLevelUp from '../applications/levelup/companionLevelup.mjs';
import { ResourceUpdateMap } from '../data/action/baseAction.mjs';
import { abilities } from '../config/actorConfig.mjs';
export default class DhpActor extends Actor {
parties = new Set();
@ -509,6 +510,30 @@ export default class DhpActor extends Actor {
return await rollClass.build(config);
}
async rollTrait(trait, options = {}) {
const abilityLabel = game.i18n.localize(abilities[trait].label);
const config = {
event: event,
title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.name}`,
headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: abilityLabel
}),
effects: await game.system.api.data.actions.actionsTypes.base.getEffects(this),
roll: {
trait: trait,
type: 'trait'
},
hasRoll: true,
actionType: 'action',
headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.name}`,
title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', {
ability: abilityLabel
}),
...options
};
return await this.diceRoll(config);
}
get rollClass() {
return CONFIG.Dice.daggerheart[['character', 'companion'].includes(this.type) ? 'DualityRoll' : 'D20Roll'];
}

View file

@ -177,14 +177,6 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(actor, item);
await this.system.action.workflow.get('damage')?.execute(config, this._id, true);
}
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
}
});
}
async onApplyDamage(event) {

View file

@ -528,7 +528,8 @@ export function expireActiveEffects(actor, allowedTypes = null) {
export async function getCritDamageBonus(formula) {
const critRoll = new Roll(formula);
return critRoll.dice.reduce((acc, dice) => acc + dice.faces * dice.number, 0);
await critRoll.evaluate();
return critRoll.dice.reduce((acc, dice) => acc + dice.faces * dice.results.filter(r => r.active).length, 0);
}
export function htmlToText(html) {

View file

@ -39,6 +39,7 @@ export const preloadHandlebarsTemplates = async function () {
'systems/daggerheart/templates/ui/tooltip/parts/tooltipChips.hbs',
'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs',
'systems/daggerheart/templates/dialogs/downtime/activities.hbs',
'systems/daggerheart/templates/dialogs/tagTeamDialog/parts/tagTeamDamageParts.hbs',
'systems/daggerheart/templates/dialogs/dice-roll/costSelection.hbs',
'systems/daggerheart/templates/ui/chat/parts/roll-part.hbs',
'systems/daggerheart/templates/ui/chat/parts/description-part.hbs',

View file

@ -193,7 +193,7 @@ export async function runMigrations() {
}
if (foundry.utils.isNewerVersion('1.2.7', lastMigrationVersion)) {
const tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
const tagTeam = game.settings.get(CONFIG.DH.id, 'TagTeamRoll');
const initatorMissing = tagTeam.initiator && !game.actors.some(actor => actor.id === tagTeam.initiator);
const missingMembers = Object.keys(tagTeam.members).reduce((acc, id) => {
if (!game.actors.some(actor => actor.id === id)) {
@ -206,7 +206,7 @@ export async function runMigrations() {
initiator: initatorMissing ? null : tagTeam.initiator,
members: missingMembers
});
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, tagTeam);
await game.settings.set(CONFIG.DH.id, 'TagTeamRoll', tagTeam);
lastMigrationVersion = '1.2.7';
}

View file

@ -15,7 +15,7 @@ import {
DhMetagamingSettings,
DhVariantRuleSettings
} from '../applications/settings/_module.mjs';
import { CompendiumBrowserSettings, DhTagTeamRoll } from '../data/_module.mjs';
import { CompendiumBrowserSettings } from '../data/_module.mjs';
export const registerDHSettings = () => {
registerMenuSettings();
@ -157,12 +157,6 @@ const registerNonConfigSettings = () => {
type: DhCountdowns
});
game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, {
scope: 'world',
config: false,
type: DhTagTeamRoll
});
game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings, {
scope: 'world',
config: false,

View file

@ -15,6 +15,9 @@ export function handleSocketEvent({ action = null, data = {} } = {}) {
case socketEvent.DowntimeTrigger:
Party.downtimeMoveQuery(data);
break;
case socketEvent.TagTeamStart:
Hooks.callAll(CONFIG.DH.HOOKS.hooksConfig.tagTeamStart, data);
break;
}
}
@ -22,7 +25,8 @@ export const socketEvent = {
GMUpdate: 'DhGMUpdate',
Refresh: 'DhRefresh',
DhpFearUpdate: 'DhFearUpdate',
DowntimeTrigger: 'DowntimeTrigger'
DowntimeTrigger: 'DowntimeTrigger',
TagTeamStart: 'DhTagTeamStart'
};
export const GMUpdateEvent = {