This commit is contained in:
WBHarry 2026-03-12 15:08:39 +01:00
parent 43114187b9
commit f78b4d1789
5 changed files with 370 additions and 57 deletions

View file

@ -684,7 +684,12 @@
"damageNotRolled": "Damage not rolled in chat message yet",
"insufficientHope": "The initiating character doesn't have enough hope",
"createTagTeam": "Create TagTeam Roll",
"chatMessageRollTitle": "Roll"
"chatMessageRollTitle": "Roll",
"hints": {
"completeRolls": "Set up and complete the rolls for the characters",
"selectRoll": "Select which roll value to be used for the Tagteam",
"totalDamage": "Total Damage: {damage}"
}
},
"TokenConfig": {
"actorSizeUsed": "Actor size is set, determining the dimensions"

View file

@ -1,3 +1,4 @@
import { getCritDamageBonus } from '../../helpers/utils.mjs';
import Party from '../sheets/actors/party.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -34,7 +35,11 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
startTagTeamRoll: TagTeamDialog.#startTagTeamRoll,
makeRoll: TagTeamDialog.#makeRoll,
removeRoll: TagTeamDialog.#removeRoll,
rerollDice: TagTeamDialog.#rerollDice
rerollDice: TagTeamDialog.#rerollDice,
makeDamageRoll: TagTeamDialog.#makeDamageRoll,
removeDamageRoll: TagTeamDialog.#removeDamageRoll,
selectRoll: TagTeamDialog.#selectRoll,
finishRoll: TagTeamDialog.#finishRoll
},
form: { handler: this.updateData, submitOnChange: true, closeOnSubmit: false }
};
@ -83,6 +88,8 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
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?.isCritical ?? false);
partContext.members = Object.keys(this.party.system.tagTeam.members).reduce((acc, actorId) => {
const data = this.party.system.tagTeam.members[actorId];
const actor = game.actors.get(actorId);
@ -104,17 +111,27 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
return acc;
}, []);
const hitPointsDamage = data.rollData?.options?.damage?.hitPoints;
const preCritHitPointsDamage = data.rollData?.options?.damage?.hitPoints?.preCritData?.hitPoints;
acc[actorId] = {
...data,
key: actorId,
readyToRoll: Boolean(data.rollChoice),
hasRolled: Boolean(data.rollData),
rollOptions
rollOptions,
damage: hitPointsDamage,
preCritDamage: preCritHitPointsDamage,
useCritFallback: selectedRoll !== data && critSelected === false
};
return acc;
}, {});
const { hint, totalDamage } = await this.getInfoTexts(this.party.system.tagTeam.members, critSelected);
partContext.hintText = hint;
partContext.totalDamage = totalDamage;
break;
}
@ -157,6 +174,38 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
//#endregion
//#region Tag Team Roll
async getInfoTexts(members, critSelected) {
let rollsAreFinished = true;
let rollIsSelected = false;
let totalDamage = null;
for (const member of Object.values(members)) {
const rollFinished = Boolean(member.rollData);
const hasDamage = member.rollData?.options?.hasDamage;
const damageFinished =
member.rollData?.options?.hasDamage !== undefined ? member.rollData.options.damage : true;
let hitPointDamage =
critSelected === false && member.rollData?.options?.isCritical
? member.rollData.options.damage?.hitPoints?.preCritData?.hitPoints?.total
: member.rollData?.options?.damage?.hitPoints?.total;
if (critSelected && member.rollData?.options?.isCritical === false) {
hitPointDamage += await getCritDamageBonus(member.rollData.options.damage?.hitPoints?.formula ?? '');
}
if (hasDamage) totalDamage = (totalDamage ?? 0) + (hitPointDamage ?? 0);
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, totalDamage };
}
async updateRollType(event) {
await this.party.update({
[`system.tagTeam.members.${event.target.dataset.member}`]: {
@ -258,5 +307,187 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
this.render();
}
static async #makeDamageRoll(_, 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 = {
source: {},
skips: {
createMessage: true,
resources: true,
triggers: true
}
};
await action.workflow.get('damage').execute(config, null, true);
if (!config.damage) return;
if (memberData.rollData.options.isCritical && config.damage.hitPoints) {
const critBonus = await getCritDamageBonus(config.damage.hitPoints.formula);
if (critBonus) {
config.damage.hitPoints.preCritData = foundry.utils.deepClone(config.damage);
config.damage.hitPoints.total += critBonus;
config.damage.hitPoints.formula = `${config.damage.hitPoints.formula} + ${critBonus}`;
config.damage.hitPoints.parts[0].total += critBonus;
config.damage.hitPoints.parts[0].formula = `${config.damage.hitPoints.parts[0].formula} + ${critBonus}`;
}
}
const current = this.party.system.tagTeam.members[memberKey].rollData;
await this.party.update({
[`system.tagTeam.members.${memberKey}.rollData`]: {
...current,
options: {
...current.options,
damage: config.damage
}
}
});
this.render();
}
static async #removeDamageRoll(_, button) {
const { memberKey } = button.dataset;
const current = this.party.system.tagTeam.members[memberKey].rollData;
await this.party.update({
[`system.tagTeam.members.${memberKey}.rollData`]: {
...current,
options: {
...current.options,
damage: null
}
}
});
this.render();
}
static async #selectRoll(_, button) {
const { memberKey } = button.dataset;
await this.party.update({
[`system.tagTeam.members`]: Object.entries(this.party.system.tagTeam.members).reduce(
(acc, [key, member]) => {
acc[key] = { selected: key === memberKey ? !member.selected : false };
return acc;
},
{}
)
});
this.render();
}
static async #finishRoll() {
// const mainRollId = Object.keys(this.data.members).find(key => this.data.members[key].selected);
// const mainRoll = game.messages.get(this.data.members[mainRollId].messageId);
// 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) {
// return ui.notifications.warn(
// game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.insufficientHope')
// );
// }
// }
let mainRoll = null;
let secondaryRoll = null;
for (const member of Object.values(this.party.system.tagTeam.members)) {
if (member.selected) mainRoll = foundry.utils.deepClone(member.rollData);
else secondaryRoll = foundry.utils.deepClone(member.rollData);
}
if (!mainRoll || !secondaryRoll) return;
const systemData = mainRoll.options;
const criticalRoll = systemData.roll.isCritical;
if (secondaryRoll.options.hasDamage && systemData.hasDamage) {
for (let key in secondaryRoll.options.damage) {
var damage = secondaryRoll.options.damage[key];
const damageTotal =
!secondaryRoll.options.isCritical && criticalRoll
? (await getCritDamageBonus(damage.formula)) + damage.total
: damage.total;
const updatedDamageParts = damage.parts;
if (systemData.damage[key]) {
if (!secondaryRoll.options.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);
}
}
} else if (
secondaryRoll.options.isCritical &&
!criticalRoll &&
secondaryRoll.options.damage.hitPoints.preCritData
) {
damage = secondaryRoll.options.damage.hitPoints.preCritData[key];
}
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 };
}
}
}
const mainActor = this.party.system.partyMembers.find(x => x.uuid === mainRoll.options.source.actor);
systemData.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: mainActor }),
system: systemData,
rolls: [mainRoll],
sound: null,
flags: { core: { RollTable: true } }
};
await cls.create(msgData);
// const fearUpdate = { key: 'fear', value: null, total: null, enabled: true };
// for (let memberId of Object.keys(this.data.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
// ? rollGivesHope
// ? 1 - this.data.initiator.cost
// : -this.data.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) {
// 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);
// }
// if (fearUpdate.value) {
// this.party.find(x => x.id === mainRollId).modifyResource([fearUpdate]);
// }
/* Clear Party tag Team Data here */
}
//#endregion
}

View file

@ -22,7 +22,8 @@ class MemberData extends foundry.abstract.DataModel {
label: 'Roll Type'
}),
rollChoice: new fields.StringField({ nullable: true, initial: null }),
rollData: new fields.JSONField({ nullable: true, initial: null })
rollData: new fields.JSONField({ nullable: true, initial: null }),
selected: new fields.BooleanField({ initial: false })
};
}

View file

@ -11,9 +11,17 @@
.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;
@ -33,6 +41,10 @@
}
}
.roll-setup {
width: 100%;
}
.roll-tools {
width: 100%;
display: flex;
@ -92,6 +104,10 @@
font-size: var(--font-size-20);
font-weight: bold;
text-align: center;
.unused-damage {
text-decoration: line-through;
}
}
.roll-dice-container {
@ -131,6 +147,34 @@
padding: 3px;
}
}
.select-roll-button i {
color: light-dark(@dark-blue, @golden);
font-size: 48px;
&.inactive {
opacity: 0.4;
}
}
}
}
.finish-container {
display: flex;
flex-direction: column;
gap: 16px;
text-align: center;
.damage-info {
font-size: var(--font-size-20);
}
.hint {
height: 18px;
}
button {
flex: 1;
}
}
}

View file

@ -3,72 +3,104 @@
<div class="team-container">
{{#each members as |member key|}}
<fieldset class="member-container">
<div class="member-info">
<img src="{{member.img}}" />
<span class="member-name">{{member.name}}</span>
</div>
<div class="roll-setup">
<div class="form-group">
<div class="form-fields">
<label>{{localize "Roll Type"}}</label>
<select class="roll-type-select" data-member="{{key}}" {{#if member.hasRolled}}disabled{{/if}}>
{{selectOptions ../rollTypes selected=member.rollType localize=true}}
</select>
</div>
<div class="data-container">
<div class="member-info">
<img src="{{member.img}}" />
<span class="member-name">{{member.name}}</span>
</div>
{{#if (eq member.rollType 'trait')}}
<div class="roll-setup">
<div class="form-group">
<div class="form-fields">
<label>{{localize "Trait"}}</label>
<select name="{{concat "system.tagTeam.members." key ".rollChoice"}}" {{#if member.hasRolled}}disabled{{/if}}>
{{selectOptions ../traitOptions selected=member.rollChoice localize=true blank=""}}
<label>{{localize "Roll Type"}}</label>
<select class="roll-type-select" data-member="{{key}}" {{#if member.hasRolled}}disabled{{/if}}>
{{selectOptions ../rollTypes selected=member.rollType localize=true}}
</select>
</div>
</div>
{{else}}
<div class="form-group">
<div class="form-fields">
<label>{{localize "Ability"}}</label>
<select name="{{concat "system.tagTeam.members." key ".rollChoice"}}" {{#if member.hasRolled}}disabled{{/if}}>
{{selectOptions member.rollOptions selected=member.rollChoice localize=true blank=""}}
</select>
{{#if (eq member.rollType 'trait')}}
<div class="form-group">
<div class="form-fields">
<label>{{localize "Trait"}}</label>
<select name="{{concat "system.tagTeam.members." key ".rollChoice"}}" {{#if member.hasRolled}}disabled{{/if}}>
{{selectOptions ../traitOptions selected=member.rollChoice localize=true blank=""}}
</select>
</div>
</div>
{{else}}
<div class="form-group">
<div class="form-fields">
<label>{{localize "Ability"}}</label>
<select name="{{concat "system.tagTeam.members." key ".rollChoice"}}" {{#if member.hasRolled}}disabled{{/if}}>
{{selectOptions member.rollOptions selected=member.rollChoice localize=true blank=""}}
</select>
</div>
</div>
{{/if}}
</div>
<div class="roll-tools">
<a class="roll-button" data-action="makeRoll" data-member="{{key}}" {{#unless member.readyToRoll}}disabled{{/unless}}>
<span class="roll-label">{{#if member.hasRolled}}{{localize "DAGGERHEART.GENERAL.reroll"}}{{else}}{{localize "DAGGERHEART.GENERAL.roll"}}{{/if}}</span>
<img src="systems/daggerheart/assets/icons/dice/duality/Daggerheart%20Foundry_g519.png" />
</a>
<a class="delete-button" data-action="removeRoll" data-member="{{key}}" {{#unless member.hasRolled}}disabled{{/unless}}><i class="fa-solid fa-trash"></i></a>
</div>
{{#if member.rollData}}
{{#with member.rollData.options.roll}}
<div class="roll-data
{{#if this.isCritical}}critical{{else}}{{#if (eq this.result.duality 1)}}hope{{else}}fear{{/if}}{{/if}}"
>
<div class="duality-label">{{this.total}} {{localize "DAGGERHEART.GENERAL.withThing" thing=this.result.label}}</div>
<div class="roll-dice-container">
<a class="roll-dice" data-action="rerollDice" data-member="{{../key}}" data-dice-type="hope">
<span class="dice-label">{{this.hope.value}}</span>
<img src="{{concat "systems/daggerheart/assets/icons/dice/hope/" this.hope.dice ".svg"}}" />
</a>
<span class="roll-operator">+</span>
<a class="roll-dice" data-action="rerollDice" data-member="{{../key}}" data-dice-type="fear">
<span class="dice-label">{{this.fear.value}}</span>
<img src="{{concat "systems/daggerheart/assets/icons/dice/fear/" this.fear.dice ".svg"}}" />
</a>
</div>
<div class="roll-total">{{this.formula}}</div>
</div>
{{/with}}
{{/if}}
{{#if member.rollData.options.hasDamage}}
<div class="roll-tools">
<a class="roll-button" data-action="makeDamageRoll" data-member-key="{{key}}" {{#unless member.readyToRoll}}disabled{{/unless}}>
<span class="roll-label">{{#if member.rollData.options.damage}}{{localize "DAGGERHEART.GENERAL.reroll"}}{{else}}{{localize "DAGGERHEART.GENERAL.roll"}}{{/if}}</span>
<img src="systems/daggerheart/assets/icons/dice/default/d20.svg" />
</a>
<a class="delete-button" data-action="removeDamageRoll" data-member-key="{{key}}" {{#unless member.rollData.options.damage}}disabled{{/unless}}><i class="fa-solid fa-trash"></i></a>
</div>
{{/if}}
{{#if damage}}
<div class="roll-data">
<div class="duality-label">
{{localize "DAGGERHEART.GENERAL.damage"}}
<span class="{{#if useCritFallback}}unused-damage{{/if}}">{{damage.total}}</span>
{{#if useCritFallback}}<span>{{preCritDamage.total}}</span>{{/if}}
</div>
<div class="roll-total">{{damage.formula}}</div>
</div>
{{/if}}
</div>
<div class="roll-tools">
<a class="roll-button" data-action="makeRoll" data-member="{{key}}" {{#unless member.readyToRoll}}disabled{{/unless}}>
<span class="roll-label">{{#if member.hasRolled}}{{localize "DAGGERHEART.GENERAL.reroll"}}{{else}}{{localize "DAGGERHEART.GENERAL.roll"}}{{/if}}</span>
<img src="systems/daggerheart/assets/icons/dice/duality/Daggerheart%20Foundry_g519.png" />
</a>
<a class="delete-button" data-action="removeRoll" data-member="{{key}}" {{#unless member.hasRolled}}disabled{{/unless}}><i class="fa-solid fa-trash"></i></a>
</div>
{{#if member.rollData}}
{{#with member.rollData.options.roll}}
<div class="roll-data
{{#if this.isCritical}}critical{{else}}{{#if (eq this.result.duality 1)}}hope{{else}}fear{{/if}}{{/if}}"
>
<div class="duality-label">{{this.total}} {{localize "DAGGERHEART.GENERAL.withThing" thing=this.result.label}}</div>
<div class="roll-dice-container">
<a class="roll-dice" data-action="rerollDice" data-member="{{../key}}" data-dice-type="hope">
<span class="dice-label">{{this.hope.value}}</span>
<img src="{{concat "systems/daggerheart/assets/icons/dice/hope/" this.hope.dice ".svg"}}" />
</a>
<span class="roll-operator">+</span>
<a class="roll-dice" data-action="rerollDice" data-member="{{../key}}" data-dice-type="fear">
<span class="dice-label">{{this.fear.value}}</span>
<img src="{{concat "systems/daggerheart/assets/icons/dice/fear/" this.fear.dice ".svg"}}" />
</a>
</div>
<div class="roll-total">{{this.formula}}</div>
</div>
{{/with}}
{{/if}}
<a class="select-roll-button" data-action="selectRoll" data-member-key="{{key}}">
<i class="{{#if member.selected}}fa-solid fa-circle-check{{else}}fa-regular fa-circle inactive{{/if}}"></i>
</a>
</fieldset>
{{/each}}
</div>
<div class="finish-container">
{{#if totalDamage includeZero=true}}<div class="damage-info">{{localize "DAGGERHEART.APPLICATIONS.TagTeamSelect.hints.totalDamage" damage=totalDamage}}</div>{{/if}}
<div class="hint">{{localize hintText}}</div>
<button type="button" data-action="finishRoll" {{#if hintText}}disabled{{/if}}>{{localize "Finish Tagteam Roll"}}</button>
</div>
</div>
</section>