From a0fa743b8e721135a543d3f2015b4e5717c8a8df Mon Sep 17 00:00:00 2001 From: WBHarry Date: Tue, 3 Mar 2026 00:17:49 +0100 Subject: [PATCH] Initial rolls working --- lang/en.json | 4 + module/applications/dialogs/tagTeamDialog.mjs | 469 +++++++----------- .../applications/sheets/actors/character.mjs | 22 +- module/applications/sheets/actors/party.mjs | 7 +- module/config/generalConfig.mjs | 11 + module/data/_module.mjs | 1 + module/data/action/attackAction.mjs | 3 +- module/data/action/baseAction.mjs | 15 +- module/data/actor/party.mjs | 4 +- module/data/fields/action/effectsField.mjs | 2 +- module/data/fields/action/saveField.mjs | 2 +- module/data/tagTeamData.mjs | 28 ++ module/dice/dhRoll.mjs | 2 + module/documents/actor.mjs | 25 + styles/less/dialog/index.less | 2 + .../tag-team-dialog/initialization.less | 32 ++ styles/less/dialog/tag-team-dialog/sheet.less | 232 ++++----- templates/dialogs/tagTeamDialog.hbs | 110 ---- .../dialogs/tagTeamDialog/initialization.hbs | 15 + .../dialogs/tagTeamDialog/tagTeamRoll.hbs | 74 +++ 20 files changed, 483 insertions(+), 577 deletions(-) create mode 100644 module/data/tagTeamData.mjs create mode 100644 styles/less/dialog/tag-team-dialog/initialization.less delete mode 100644 templates/dialogs/tagTeamDialog.hbs create mode 100644 templates/dialogs/tagTeamDialog/initialization.hbs create mode 100644 templates/dialogs/tagTeamDialog/tagTeamRoll.hbs diff --git a/lang/en.json b/lang/en.json index 937de844..2eef200f 100755 --- a/lang/en.json +++ b/lang/en.json @@ -1850,6 +1850,10 @@ } }, "GENERAL": { + "Ability": { + "single": "Ability", + "plural": "Abilities" + }, "Action": { "single": "Action", "plural": "Actions" diff --git a/module/applications/dialogs/tagTeamDialog.mjs b/module/applications/dialogs/tagTeamDialog.mjs index d1a1e123..25016127 100644 --- a/module/applications/dialogs/tagTeamDialog.mjs +++ b/module/applications/dialogs/tagTeamDialog.mjs @@ -1,5 +1,4 @@ -import { getCritDamageBonus } from '../../helpers/utils.mjs'; -import { GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; +import Party from '../sheets/actors/party.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -7,15 +6,19 @@ 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.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'; } get title() { @@ -27,321 +30,205 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio 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 }, 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 - ); 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.memberSelection = this.partyMembers; + partContext.allSelected = partContext.memberSelection.filter(x => x.selected).length >= 2; + 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; - 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}`, { - 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 } - } - }); + 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); + const rollOptions = actor.items.reduce((acc, item) => { + if (item.system.metadata.hasActions) + acc.push( + ...item.system.actions.reduce((acc, action) => { + if (action.hasRoll) + acc.push({ + value: action.uuid, + label: action.name, + group: item.name + }); + + return acc; + }, []) + ); + + return acc; + }, []); + + acc[actorId] = { + ...data, + readyToRoll: Boolean(data.rollChoice), + hasRolled: Boolean(data.rollData), + rollOptions + }; + + return acc; + }, {}); + + break; } + + return partContext; } - 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 }; - } + static async updateData(_event, _, formData) { + const form = foundry.utils.expandObject(formData.object); + await this.party.update(form); + this.render(true); + } - await this.updateSource(update); + //#region Initialization + static #toggleSelectMember(_, button) { + const member = this.partyMembers.find(x => x.id === button.dataset.id); + member.selected = !member.selected; this.render(); } - static async #removeMember(_, button) { - const update = { [`members.-=${button.dataset.characterId}`]: null }; - if (this.data.initiator.id === button.dataset.characterId) { - update.iniator = { id: null }; + static async #startTagTeamRoll() { + await this.party.update({ + 'system.==tagTeam': new game.system.api.data.TagTeamData({ + ...this.party.system.tagTeam.toObject(), + 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; + }, {}) + }) + }); + /* Update Party data and refresh all views */ + this.tabGroups.application = 'tagTeamRoll'; + + this.render(); + } + //#endregion + //#region Tag Team Roll + + async updateRollType(event) { + await this.party.update({ + [`system.tagTeam.members.${event.target.dataset.member}`]: { + rollType: event.target.value, + rollChoice: null + } + }); + + this.render(); + } + + static async #removeRoll(_, button) { + await this.party.update({ + [`system.tagTeam.members.${button.dataset.member}`]: { + rollData: null, + rollChoice: null + } + }); + + this.render(); + } + + 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: + result = await this.makeAbilityRoll(event, member); + break; } - await this.updateSource(update); + if (!result) return; + + const rollData = result.messageRoll.toJSON(); + delete rollData.options.messageRoll; + await this.party.update({ + [`system.tagTeam.members.${member}.rollData`]: rollData + }); + this.render(); } - static async #unlinkMessage(_, button) { - await this.updateSource({ [`members.${button.id}.messageId`]: null }); - } + async makeTraitRoll(memberKey) { + const actor = game.actors.find(x => x.id === memberKey); + if (!actor) return; - 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 + const memberData = this.party.system.tagTeam.members[memberKey]; + return await actor.traitDiceRoll(memberData.rollChoice, { + skips: { + createMessage: true, + resources: true, + triggers: true } }); } - 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); + async makeAbilityRoll(event, memberKey) { + const actor = game.actors.find(x => x.id === memberKey); + if (!actor) 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) { - return ui.notifications.warn( - game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.insufficientHope') - ); + 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 } - } - - const secondaryRolls = Object.keys(this.data.members) - .filter(key => key !== mainRollId) - .map(key => game.messages.get(this.data.members[key].messageId)); - - 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); - } - } - } - - 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 }; - } - } - } - } - - 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: this.party.find(x => x.id === mainRollId) }), - system: systemData, - rolls: mainRoll.rolls, - 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]); - } - - /* 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 } - } - }); - } + }); } - 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 } diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 4ecaeb06..74d3af0b 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -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'; @@ -711,26 +710,7 @@ export default class CharacterSheet extends DHBaseActorSheet { * @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); + const result = await this.document.traitDiceRoll(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 */ diff --git a/module/applications/sheets/actors/party.mjs b/module/applications/sheets/actors/party.mjs index d78519cb..9e533572 100644 --- a/module/applications/sheets/actors/party.mjs +++ b/module/applications/sheets/actors/party.mjs @@ -6,7 +6,6 @@ import DaggerheartMenu from '../../sidebar/tabs/daggerheartMenu.mjs'; import { socketEvent } from '../../../systemRegistration/socket.mjs'; import GroupRollDialog from '../../dialogs/group-roll-dialog.mjs'; import DhpActor from '../../../documents/actor.mjs'; -import DHItem from '../../../documents/item.mjs'; export default class Party extends DHBaseActorSheet { constructor(options) { @@ -256,11 +255,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) { diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index a38d1c8a..862625cb 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -848,3 +848,14 @@ export const sceneRangeMeasurementSetting = { label: 'Custom' } }; + +export const tagTeamRollTypes = { + trait: { + id: 'trait', + label: 'DAGGERHEART.GENERAL.Trait.single' + }, + ability: { + id: 'ability', + label: 'DAGGERHEART.GENERAL.Ability.single' + } +}; diff --git a/module/data/_module.mjs b/module/data/_module.mjs index 52fa689e..2319fe10 100644 --- a/module/data/_module.mjs +++ b/module/data/_module.mjs @@ -4,6 +4,7 @@ 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'; diff --git a/module/data/action/attackAction.mjs b/module/data/action/attackAction.mjs index 60112c40..04b3aea6 100644 --- a/module/data/action/attackAction.mjs +++ b/module/data/action/attackAction.mjs @@ -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); } diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index f6ffe75f..c75cb8b8 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -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); @@ -229,7 +229,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; } @@ -239,7 +239,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} - `; @@ -266,7 +266,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); @@ -278,8 +279,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; } diff --git a/module/data/actor/party.mjs b/module/data/actor/party.mjs index 236d65db..0378e8c3 100644 --- a/module/data/actor/party.mjs +++ b/module/data/actor/party.mjs @@ -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) }; } diff --git a/module/data/fields/action/effectsField.mjs b/module/data/fields/action/effectsField.mjs index 6afd470b..1a003e2b 100644 --- a/module/data/fields/action/effectsField.mjs +++ b/module/data/fields/action/effectsField.mjs @@ -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); diff --git a/module/data/fields/action/saveField.mjs b/module/data/fields/action/saveField.mjs index c9030036..0629353e 100644 --- a/module/data/fields/action/saveField.mjs +++ b/module/data/fields/action/saveField.mjs @@ -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); diff --git a/module/data/tagTeamData.mjs b/module/data/tagTeamData.mjs new file mode 100644 index 00000000..d37e7b97 --- /dev/null +++ b/module/data/tagTeamData.mjs @@ -0,0 +1,28 @@ +export default class TagTeamData extends foundry.abstract.DataModel { + static defineSchema() { + const fields = foundry.data.fields; + + return { + members: new fields.TypedObjectField( + new fields.SchemaField({ + 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 }) + }) + ) + }; + } + + get roll() { + const roll = CONFIG.Dice.daggerheart.DualityRoll(JSON.parse(this.rollData)); + + return this.rollData ? CONFIG.Dice.daggerheart.DualityRoll(JSON.parse(this.rollData)) : null; + } +} diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index d8e5f6dd..465c5557 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -21,6 +21,8 @@ export default class DHRoll extends Roll { static async build(config = {}, message = {}) { const roll = await this.buildConfigure(config, message); if (!roll) return; + + config.messageRoll = roll; await this.buildEvaluate(roll, config, (message = {})); await this.buildPost(roll, config, (message = {})); return config; diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index e8bea0bf..6af56e00 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -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 traitDiceRoll(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']; } diff --git a/styles/less/dialog/index.less b/styles/less/dialog/index.less index 0c70df9f..73738eaa 100644 --- a/styles/less/dialog/index.less +++ b/styles/less/dialog/index.less @@ -32,6 +32,8 @@ @import './reroll-dialog/sheet.less'; @import './group-roll/group-roll.less'; + +@import './tag-team-dialog/initialization.less'; @import './tag-team-dialog/sheet.less'; @import './image-select/sheet.less'; diff --git a/styles/less/dialog/tag-team-dialog/initialization.less b/styles/less/dialog/tag-team-dialog/initialization.less new file mode 100644 index 00000000..e059d2d6 --- /dev/null +++ b/styles/less/dialog/tag-team-dialog/initialization.less @@ -0,0 +1,32 @@ +.daggerheart.dialog.dh-style.views.tag-team-dialog { + .initialization-container { + .members-container { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + gap: 8px; + + .member-container { + position: relative; + display: flex; + justify-content: center; + + &.inactive { + opacity: 0.4; + } + + .member-name { + position: absolute; + } + } + } + + footer { + margin-top: 8px; + display: flex; + + > * { + flex: 1; + } + } + } +} diff --git a/styles/less/dialog/tag-team-dialog/sheet.less b/styles/less/dialog/tag-team-dialog/sheet.less index 767c66ca..af035d7b 100644 --- a/styles/less/dialog/tag-team-dialog/sheet.less +++ b/styles/less/dialog/tag-team-dialog/sheet.less @@ -1,178 +1,136 @@ .daggerheart.dialog.dh-style.views.tag-team-dialog { - .tag-team-container { + .tag-team-roll-container { display: flex; flex-direction: column; gap: 16px; - .tag-team-data-container { + .team-container { display: flex; - align-items: center; - gap: 8px; + gap: 16px; - .form-group { - flex: 0; - - label { - white-space: nowrap; - } - - &.flex-group { - flex: 1; - } - } - } - - .title-row { - display: flex; - align-items: center; - gap: 8px; - - h2 { - text-align: start; - } - - select { - flex: 1; - } - } - - .participants-container { - margin-top: 8px; - display: flex; - flex-direction: column; - gap: 4px; - - .participant-outer-container { - padding: 8px; + .member-container { display: flex; flex-direction: column; - gap: 4px; - cursor: pointer; - border-radius: 6px; + gap: 8px; + flex: 1; - &.selected, - &:hover { - background-color: light-dark(@golden-40, @golden-40); - } - - .participant-container { + .member-info { display: flex; align-items: center; justify-content: space-between; - gap: 8px; + width: 100%; - .participant-inner-container { + img { + height: 64px; + border-radius: 6px; + border: 1px solid light-dark(@dark-blue, @golden); + } + + .member-name { flex: 1; + text-align: center; + font-size: var(--font-size-18); + } + } + + .roll-tools { + width: 100%; + display: flex; + align-items: center; + justify-content: space-evenly; + + .roll-button { + position: relative; display: flex; - align-items: center; - gap: 4px; + justify-content: center; + + .roll-label { + position: absolute; + top: 8px; + width: 60px; + text-align: center; + padding: 2px; + background: light-dark(darkblue, gold); + color: light-dark(white, black); + border-radius: 6px; + border: 1px solid light-dark(white, black); + } img { - height: 48px; - width: 48px; - border-radius: 50%; + height: 80px; } + } - .participant-labels { - display: flex; - flex-direction: column; - gap: 2px; - - .participant-label-title { - font-size: 18px; - } - - .participant-label-info { - display: flex; - gap: 4px; - - .participant-label-info-part { - border: 1px solid light-dark(white, white); - border-radius: 4px; - padding: 2px 4px; - background-color: light-dark(@beige-80, @soft-white-shadow); - color: white; - } - } - } + .delete-button i { + font-size: 40px; } } - .participant-empty-roll-container { - border: 1px dashed white; - padding: 8px 2px; - text-align: center; - font-style: italic; - } - - .participant-roll-outer-container { + .roll-data { display: flex; flex-direction: column; - gap: 2px; - color: light-dark(@dark-blue, @golden); + gap: 4px; - h4 { - text-align: center; - color: light-dark(@dark-blue, @golden); + &.hope { + --text-color: @golden; + --bg-color: @golden-40; } - .participant-roll-container { + &.fear { + --text-color: @chat-blue; + --bg-color: @chat-blue-40; + } + + &.critical { + --text-color: @chat-purple; + --bg-color: @chat-purple-40; + } + + .duality-label { + color: var(--text-color); + font-size: var(--font-size-24); + font-weight: bold; + text-align: center; + } + + .roll-dice-container { display: flex; align-items: center; justify-content: center; - white-space: nowrap; - - .participant-roll-text-container { - padding: 0 8px; - white-space: nowrap; - display: flex; - } - } - - .damage-values-container { - display: flex; - justify-content: space-around; gap: 8px; - .damage-container { - border: 1px solid light-dark(white, white); - border-radius: 6px; - padding: 0 4px; + .roll-dice { + position: relative; display: flex; - gap: 4px; + align-items: center; + justify-content: center; + + .dice-label { + position: absolute; + color: white; + font-size: 1rem; + paint-order: stroke fill; + -webkit-text-stroke: 2px black; + } + + img { + height: 32px; + } } + + .roll-operator { + font-size: var(--font-size-24); + } + } + + .roll-total { + background: var(--bg-color); + color: var(--text-color); + border-radius: 4px; + padding: 3px; } } } } - - h2 { - text-align: center; - } - - .result-container { - display: grid; - grid-template-columns: 1fr 1fr; - align-items: center; - gap: 8px; - - .result-damages-container { - display: flex; - flex-wrap: wrap; - gap: 4px; - - .result-damage-container { - border: 1px solid light-dark(white, white); - border-radius: 6px; - padding: 0 4px; - } - } - } - - .roll-leader-container { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 8px; - } } } diff --git a/templates/dialogs/tagTeamDialog.hbs b/templates/dialogs/tagTeamDialog.hbs deleted file mode 100644 index 3c96a573..00000000 --- a/templates/dialogs/tagTeamDialog.hbs +++ /dev/null @@ -1,110 +0,0 @@ -
-
-
- {{localize "DAGGERHEART.APPLICATIONS.TagTeamSelect.partyTeam"}} - -
- - -
- -
- -
- {{#each members as |member|}} -
-
-
- -
-
{{member.character.name}}
-
- {{#if member.character.system.class.value}} -
{{member.character.system.class.value.name}}
- {{/if}} - {{#if member.system.multiclass.value}} -
{{member.character.system.multiclass.value.name}}
- {{/if}} -
-
-
- -
- {{#if member.roll}} -
-
-

- - {{member.roll.system.title}} -

-
-
- -
- {{member.roll.system.roll.total}} - {{localize "DAGGERHEART.GENERAL.withThing" thing=member.roll.system.roll.result.label}} -
- -
- {{#if member.roll.system.hasDamage}} -

{{localize "DAGGERHEART.GENERAL.damage"}}

-
- {{#if member.damageValues}} - {{#each member.damageValues as |damage|}} -
-
{{damage.name}}
-
{{damage.total}}
-
- {{/each}} - {{else}} - {{localize "DAGGERHEART.APPLICATIONS.TagTeamSelect.damageNotRolled"}} - {{/if}} -
- {{/if}} -
- {{else}} -
- {{localize "DAGGERHEART.APPLICATIONS.TagTeamSelect.linkMessageHint"}} -
- {{/if}} -
- {{/each}} -
-
- -
-

- {{localize "DAGGERHEART.APPLICATIONS.TagTeamSelect.initiatingCharacter"}} - -

-

- {{localize "DAGGERHEART.GENERAL.Cost.single"}} - -

-
- {{#if showResult}} - {{#if selectedData.result}} -
-

{{localize "DAGGERHEART.APPLICATIONS.TagTeamSelect.title"}}: {{selectedData.result}}

- {{#if usesDamage}} -
- - {{#each selectedData.damageValues as |damage|}} -
- {{damage.name}} - {{damage.total}} -
- {{/each}} -
- {{/if}} -
- {{/if}} - {{/if}} - - -
-
\ No newline at end of file diff --git a/templates/dialogs/tagTeamDialog/initialization.hbs b/templates/dialogs/tagTeamDialog/initialization.hbs new file mode 100644 index 00000000..722f6414 --- /dev/null +++ b/templates/dialogs/tagTeamDialog/initialization.hbs @@ -0,0 +1,15 @@ +
+

{{localize "Select the two participants"}}

+
+ {{#each memberSelection as |member|}} + + {{member.name}} + + + {{/each}} +
+ +
+ +
+
\ No newline at end of file diff --git a/templates/dialogs/tagTeamDialog/tagTeamRoll.hbs b/templates/dialogs/tagTeamDialog/tagTeamRoll.hbs new file mode 100644 index 00000000..5a288384 --- /dev/null +++ b/templates/dialogs/tagTeamDialog/tagTeamRoll.hbs @@ -0,0 +1,74 @@ +
+
+
+ {{#each members as |member key|}} +
+
+ + {{member.name}} +
+
+
+
+ + +
+
+ + {{#if (eq member.rollType 'trait')}} +
+
+ + +
+
+ {{else}} +
+
+ + +
+
+ {{/if}} +
+ + + + {{#if member.rollData}} + {{#with member.rollData.options.roll}} +
+
{{localize "DAGGERHEART.GENERAL.withThing" thing=this.result.label}}
+
+
+ {{this.hope.value}} + +
+ + +
+ {{this.fear.value}} + +
+
+
{{../rollData.formula}}
+
+ {{/with}} + {{/if}} +
+ {{/each}} +
+
+
\ No newline at end of file