diff --git a/lang/en.json b/lang/en.json index 9c74a351..68c9aa65 100755 --- a/lang/en.json +++ b/lang/en.json @@ -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" diff --git a/module/applications/dialogs/tagTeamDialog.mjs b/module/applications/dialogs/tagTeamDialog.mjs index 9c76302a..b15f3cfe 100644 --- a/module/applications/dialogs/tagTeamDialog.mjs +++ b/module/applications/dialogs/tagTeamDialog.mjs @@ -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 } diff --git a/module/data/tagTeamData.mjs b/module/data/tagTeamData.mjs index e7e32ad7..84c4eeb4 100644 --- a/module/data/tagTeamData.mjs +++ b/module/data/tagTeamData.mjs @@ -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 }) }; } diff --git a/styles/less/dialog/tag-team-dialog/sheet.less b/styles/less/dialog/tag-team-dialog/sheet.less index 06db0335..f7683698 100644 --- a/styles/less/dialog/tag-team-dialog/sheet.less +++ b/styles/less/dialog/tag-team-dialog/sheet.less @@ -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; } } } diff --git a/templates/dialogs/tagTeamDialog/tagTeamRoll.hbs b/templates/dialogs/tagTeamDialog/tagTeamRoll.hbs index e5931a08..c160307e 100644 --- a/templates/dialogs/tagTeamDialog/tagTeamRoll.hbs +++ b/templates/dialogs/tagTeamDialog/tagTeamRoll.hbs @@ -3,72 +3,104 @@