Main implementation

This commit is contained in:
WBHarry 2025-10-23 19:51:22 +02:00
parent 9e9c1d2ac0
commit 0511d24921
24 changed files with 1230 additions and 499 deletions

View file

@ -545,6 +545,17 @@
"ResourceDice": { "ResourceDice": {
"title": "{name} Resource", "title": "{name} Resource",
"rerollDice": "Reroll Dice" "rerollDice": "Reroll Dice"
},
"TagTeamSelect": {
"title": "Tag Team Roll",
"leaderTitle": "Initiating Character",
"membersTitle": "Participants",
"hopeCost": "Hope Cost",
"linkMessageHint": "Make a roll from your character sheet to link it to the Tag Team Roll",
"damageNotRolled": "Damage not rolled in chat message yet",
"insufficientHope": "The initiating character doesn't have enough hope",
"createTagTeam": "Create TagTeam Roll",
"chatMessageRollTitle": "Roll"
} }
}, },
"CLASS": { "CLASS": {
@ -2452,8 +2463,16 @@
}, },
"resourceRoll": { "resourceRoll": {
"playerMessage": "{user} rerolled their {name}" "playerMessage": "{user} rerolled their {name}"
},
"tagTeam": {
"title": "Tag Team",
"membersTitle": "Members"
} }
}, },
"ChatLog": {
"rerollDamage": "Reroll Damage",
"assignTagRoll": "Assign as Tag Roll"
},
"ItemBrowser": { "ItemBrowser": {
"title": "Daggerheart Compendium Browser", "title": "Daggerheart Compendium Browser",
"hint": "Select a Folder in sidebar to start browsing through the compendium", "hint": "Select a Folder in sidebar to start browsing through the compendium",

View file

@ -10,4 +10,5 @@ export { default as OwnershipSelection } from './ownershipSelection.mjs';
export { default as RerollDamageDialog } from './rerollDamageDialog.mjs'; export { default as RerollDamageDialog } from './rerollDamageDialog.mjs';
export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs'; export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs';
export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs'; export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs';
export { default as GroupRollDialog } from './group-roll-dialog.mjs'; export { default as GroupRollDialog } from './group-roll-dialog.mjs';
export { default as TagTeamDialog } from './tagTeamDialog.mjs';

View file

@ -34,6 +34,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
updateIsAdvantage: this.updateIsAdvantage, updateIsAdvantage: this.updateIsAdvantage,
selectExperience: this.selectExperience, selectExperience: this.selectExperience,
toggleReaction: this.toggleReaction, toggleReaction: this.toggleReaction,
toggleTagTeamRoll: this.toggleTagTeamRoll,
submitRoll: this.submitRoll submitRoll: this.submitRoll
}, },
form: { form: {
@ -120,6 +121,13 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
context.showReaction = !this.config.roll?.type && context.rollType === 'DualityRoll'; context.showReaction = !this.config.roll?.type && context.rollType === 'DualityRoll';
context.reactionOverride = this.reactionOverride; context.reactionOverride = this.reactionOverride;
} }
const tagTeamSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
if (tagTeamSetting.members[this.actor.id]) {
context.activeTagTeamRoll = true;
context.tagTeamSelected = this.config.tagTeamSelected;
}
return context; return context;
} }
@ -195,6 +203,11 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio
} }
} }
static toggleTagTeamRoll() {
this.config.tagTeamSelected = !this.config.tagTeamSelected;
this.render();
}
static async submitRoll() { static async submitRoll() {
await this.close({ submitted: true }); await this.close({ submitted: true });
} }

View file

@ -1,3 +1,5 @@
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) { export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) {
@ -122,6 +124,15 @@ export default class RerollDamageDialog extends HandlebarsApplicationMixin(Appli
}, {}) }, {})
}; };
await this.message.update(update); 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(); await this.close();
} }

View file

@ -0,0 +1,279 @@
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class TagTeamDialog extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(party) {
super();
this.data = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
this.party = party;
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();
}
});
}
get title() {
return game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.title');
}
static DEFAULT_OPTIONS = {
tag: 'form',
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
},
form: { handler: this.updateData, submitOnChange: true, closeOnSubmit: false }
};
static PARTS = {
application: {
id: 'tag-team-dialog',
template: 'systems/daggerheart/templates/dialogs/tagTeamDialog.hbs'
}
};
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
};
context.selectedData = Object.values(context.members).reduce(
(acc, member) => {
if (!member.roll) return acc;
if (member.selected) {
acc.result = `${member.roll.system.roll.total} ${member.roll.system.roll.result.label}`;
}
if (context.usesDamage) {
if (!acc.damageValues) acc.damageValues = {};
for (let damage of member.damageValues) {
if (acc.damageValues[damage.key]) {
acc.damageValues[damage.key].total += damage.total;
} else {
acc.damageValues[damage.key] = foundry.utils.deepClone(damage);
}
}
}
return acc;
},
{ result: null, damageValues: null }
);
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);
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
}
});
}
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}`]: null };
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
}
});
}
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);
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 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();
for (let roll of secondaryRolls) {
if (roll.system.hasDamage) {
for (let key in roll.system.damage) {
var damage = roll.system.damage[key];
if (systemData.damage[key]) {
systemData.damage[key].total += damage.total;
systemData.damage[key].parts = [...systemData.damage[key].parts, ...damage.parts];
} else {
systemData.damage[key] = damage;
}
}
}
}
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
};
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 = [];
if (systemData.roll.isCritical || systemData.roll.result.duality === 1) {
const value =
memberId !== this.data.initiator.id
? 1
: this.data.initiator.cost
? 1 - this.data.initiator.cost
: 1;
resourceUpdates.push({ key: 'hope', value: value, total: -value, 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 */
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, {
members: [],
initiator: { id: null, cost: 3 }
});
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
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 });
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
}
});
}
async close(options = {}) {
Hooks.off(socketEvent.Refresh, this.setupHooks);
await super.close(options);
}
}

View file

@ -1,475 +1,482 @@
import DHBaseActorSheet from '../api/base-actor.mjs'; import DHBaseActorSheet from '../api/base-actor.mjs';
import { getDocFromElement } from '../../../helpers/utils.mjs'; import { getDocFromElement } from '../../../helpers/utils.mjs';
import { ItemBrowser } from '../../ui/itemBrowser.mjs'; import { ItemBrowser } from '../../ui/itemBrowser.mjs';
import FilterMenu from '../../ux/filter-menu.mjs'; import FilterMenu from '../../ux/filter-menu.mjs';
import DaggerheartMenu from '../../sidebar/tabs/daggerheartMenu.mjs'; import DaggerheartMenu from '../../sidebar/tabs/daggerheartMenu.mjs';
import { socketEvent } from '../../../systemRegistration/socket.mjs'; import { socketEvent } from '../../../systemRegistration/socket.mjs';
import GroupRollDialog from '../../dialogs/group-roll-dialog.mjs'; import GroupRollDialog from '../../dialogs/group-roll-dialog.mjs';
export default class Party extends DHBaseActorSheet { export default class Party extends DHBaseActorSheet {
constructor(options) { constructor(options) {
super(options); super(options);
this.refreshSelections = DaggerheartMenu.defaultRefreshSelections(); this.refreshSelections = DaggerheartMenu.defaultRefreshSelections();
} }
/**@inheritdoc */ /**@inheritdoc */
static DEFAULT_OPTIONS = { static DEFAULT_OPTIONS = {
classes: ['party'], classes: ['party'],
position: { position: {
width: 550 width: 550
}, },
window: { window: {
resizable: true resizable: true
}, },
actions: { actions: {
deletePartyMember: Party.#deletePartyMember, deletePartyMember: Party.#deletePartyMember,
toggleHope: Party.#toggleHope, toggleHope: Party.#toggleHope,
toggleHitPoints: Party.#toggleHitPoints, toggleHitPoints: Party.#toggleHitPoints,
toggleStress: Party.#toggleStress, toggleStress: Party.#toggleStress,
toggleArmorSlot: Party.#toggleArmorSlot, toggleArmorSlot: Party.#toggleArmorSlot,
tempBrowser: Party.#tempBrowser, tempBrowser: Party.#tempBrowser,
refeshActions: Party.#refeshActions, refeshActions: Party.#refeshActions,
triggerRest: Party.#triggerRest, triggerRest: Party.#triggerRest,
groupRoll: Party.#groupRoll, tagTeamRoll: Party.#tagTeamRoll,
selectRefreshable: DaggerheartMenu.selectRefreshable, groupRoll: Party.#groupRoll,
refreshActors: DaggerheartMenu.refreshActors selectRefreshable: DaggerheartMenu.selectRefreshable,
}, refreshActors: DaggerheartMenu.refreshActors
dragDrop: [{ dragSelector: '.actors-section .inventory-item', dropSelector: null }] },
}; dragDrop: [{ dragSelector: '.actors-section .inventory-item', dropSelector: null }]
};
/**@override */
static PARTS = { /**@override */
header: { template: 'systems/daggerheart/templates/sheets/actors/party/header.hbs' }, static PARTS = {
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, header: { template: 'systems/daggerheart/templates/sheets/actors/party/header.hbs' },
partyMembers: { template: 'systems/daggerheart/templates/sheets/actors/party/party-members.hbs' }, tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
resources: { partyMembers: { template: 'systems/daggerheart/templates/sheets/actors/party/party-members.hbs' },
template: 'systems/daggerheart/templates/sheets/actors/party/resources.hbs', resources: {
scrollable: ['.resources'] template: 'systems/daggerheart/templates/sheets/actors/party/resources.hbs',
}, scrollable: ['.resources']
projects: { },
template: 'systems/daggerheart/templates/sheets/actors/party/projects.hbs', projects: {
scrollable: ['.projects'] template: 'systems/daggerheart/templates/sheets/actors/party/projects.hbs',
}, scrollable: ['.projects']
inventory: { },
template: 'systems/daggerheart/templates/sheets/actors/party/inventory.hbs', inventory: {
scrollable: ['.inventory'] template: 'systems/daggerheart/templates/sheets/actors/party/inventory.hbs',
}, scrollable: ['.inventory']
notes: { template: 'systems/daggerheart/templates/sheets/actors/party/notes.hbs' } },
}; notes: { template: 'systems/daggerheart/templates/sheets/actors/party/notes.hbs' }
};
/** @inheritdoc */
static TABS = { /** @inheritdoc */
primary: { static TABS = {
tabs: [ primary: {
{ id: 'partyMembers' }, tabs: [
{ id: 'resources' }, { id: 'partyMembers' },
{ id: 'projects' }, { id: 'resources' },
{ id: 'inventory' }, { id: 'projects' },
{ id: 'notes' } { id: 'inventory' },
], { id: 'notes' }
initial: 'partyMembers', ],
labelPrefix: 'DAGGERHEART.GENERAL.Tabs' initial: 'partyMembers',
} labelPrefix: 'DAGGERHEART.GENERAL.Tabs'
}; }
};
async _onRender(context, options) {
await super._onRender(context, options); async _onRender(context, options) {
this._createFilterMenus(); await super._onRender(context, options);
this._createSearchFilter(); this._createFilterMenus();
} this._createSearchFilter();
}
/* -------------------------------------------- */
/* Prepare Context */ /* -------------------------------------------- */
/* -------------------------------------------- */ /* Prepare Context */
/* -------------------------------------------- */
async _prepareContext(_options) {
const context = await super._prepareContext(_options); async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.inventory = {
currency: { context.inventory = {
title: game.i18n.localize('DAGGERHEART.CONFIG.Gold.title'), currency: {
coins: game.i18n.localize('DAGGERHEART.CONFIG.Gold.coins'), title: game.i18n.localize('DAGGERHEART.CONFIG.Gold.title'),
handfuls: game.i18n.localize('DAGGERHEART.CONFIG.Gold.handfuls'), coins: game.i18n.localize('DAGGERHEART.CONFIG.Gold.coins'),
bags: game.i18n.localize('DAGGERHEART.CONFIG.Gold.bags'), handfuls: game.i18n.localize('DAGGERHEART.CONFIG.Gold.handfuls'),
chests: game.i18n.localize('DAGGERHEART.CONFIG.Gold.chests') bags: game.i18n.localize('DAGGERHEART.CONFIG.Gold.bags'),
} chests: game.i18n.localize('DAGGERHEART.CONFIG.Gold.chests')
}; }
};
const homebrewCurrency = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).currency;
if (homebrewCurrency.enabled) { const homebrewCurrency = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).currency;
context.inventory.currency = homebrewCurrency; if (homebrewCurrency.enabled) {
} context.inventory.currency = homebrewCurrency;
}
if (context.inventory.length === 0) {
context.inventory = Array(1).fill(Array(5).fill([])); if (context.inventory.length === 0) {
} context.inventory = Array(1).fill(Array(5).fill([]));
}
return context;
} return context;
}
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options); async _preparePartContext(partId, context, options) {
switch (partId) { context = await super._preparePartContext(partId, context, options);
case 'header': switch (partId) {
await this._prepareHeaderContext(context, options); case 'header':
break; await this._prepareHeaderContext(context, options);
case 'notes': break;
await this._prepareNotesContext(context, options); case 'notes':
break; await this._prepareNotesContext(context, options);
} break;
return context; }
} return context;
}
/**
* Prepare render context for the Header part. /**
* @param {ApplicationRenderContext} context * Prepare render context for the Header part.
* @param {ApplicationRenderOptions} options * @param {ApplicationRenderContext} context
* @returns {Promise<void>} * @param {ApplicationRenderOptions} options
* @protected * @returns {Promise<void>}
*/ * @protected
async _prepareHeaderContext(context, _options) { */
const { system } = this.document; async _prepareHeaderContext(context, _options) {
const { TextEditor } = foundry.applications.ux; const { system } = this.document;
const { TextEditor } = foundry.applications.ux;
context.description = await TextEditor.implementation.enrichHTML(system.description, {
secrets: this.document.isOwner, context.description = await TextEditor.implementation.enrichHTML(system.description, {
relativeTo: this.document secrets: this.document.isOwner,
}); relativeTo: this.document
} });
}
/**
* Prepare render context for the Biography part. /**
* @param {ApplicationRenderContext} context * Prepare render context for the Biography part.
* @param {ApplicationRenderOptions} options * @param {ApplicationRenderContext} context
* @returns {Promise<void>} * @param {ApplicationRenderOptions} options
* @protected * @returns {Promise<void>}
*/ * @protected
async _prepareNotesContext(context, _options) { */
const { system } = this.document; async _prepareNotesContext(context, _options) {
const { TextEditor } = foundry.applications.ux; const { system } = this.document;
const { TextEditor } = foundry.applications.ux;
const paths = {
notes: 'notes' const paths = {
}; notes: 'notes'
};
for (const [key, path] of Object.entries(paths)) {
const value = foundry.utils.getProperty(system, path); for (const [key, path] of Object.entries(paths)) {
context[key] = { const value = foundry.utils.getProperty(system, path);
field: system.schema.getField(path), context[key] = {
value, field: system.schema.getField(path),
enriched: await TextEditor.implementation.enrichHTML(value, { value,
secrets: this.document.isOwner, enriched: await TextEditor.implementation.enrichHTML(value, {
relativeTo: this.document secrets: this.document.isOwner,
}) relativeTo: this.document
}; })
} };
} }
}
/**
* Toggles a hope resource value. /**
* @type {ApplicationClickAction} * Toggles a hope resource value.
*/ * @type {ApplicationClickAction}
static async #toggleHope(_, target) { */
const hopeValue = Number.parseInt(target.dataset.value); static async #toggleHope(_, target) {
const actor = await foundry.utils.fromUuid(target.dataset.actorId); const hopeValue = Number.parseInt(target.dataset.value);
const newValue = actor.system.resources.hope.value >= hopeValue ? hopeValue - 1 : hopeValue; const actor = await foundry.utils.fromUuid(target.dataset.actorId);
await actor.update({ 'system.resources.hope.value': newValue }); const newValue = actor.system.resources.hope.value >= hopeValue ? hopeValue - 1 : hopeValue;
this.render(); await actor.update({ 'system.resources.hope.value': newValue });
} this.render();
}
/**
* Toggles a hp resource value. /**
* @type {ApplicationClickAction} * Toggles a hp resource value.
*/ * @type {ApplicationClickAction}
static async #toggleHitPoints(_, target) { */
const hitPointsValue = Number.parseInt(target.dataset.value); static async #toggleHitPoints(_, target) {
const actor = await foundry.utils.fromUuid(target.dataset.actorId); const hitPointsValue = Number.parseInt(target.dataset.value);
const newValue = actor.system.resources.hitPoints.value >= hitPointsValue ? hitPointsValue - 1 : hitPointsValue; const actor = await foundry.utils.fromUuid(target.dataset.actorId);
await actor.update({ 'system.resources.hitPoints.value': newValue }); const newValue = actor.system.resources.hitPoints.value >= hitPointsValue ? hitPointsValue - 1 : hitPointsValue;
this.render(); await actor.update({ 'system.resources.hitPoints.value': newValue });
} this.render();
}
/**
* Toggles a stress resource value. /**
* @type {ApplicationClickAction} * Toggles a stress resource value.
*/ * @type {ApplicationClickAction}
static async #toggleStress(_, target) { */
const stressValue = Number.parseInt(target.dataset.value); static async #toggleStress(_, target) {
const actor = await foundry.utils.fromUuid(target.dataset.actorId); const stressValue = Number.parseInt(target.dataset.value);
const newValue = actor.system.resources.stress.value >= stressValue ? stressValue - 1 : stressValue; const actor = await foundry.utils.fromUuid(target.dataset.actorId);
await actor.update({ 'system.resources.stress.value': newValue }); const newValue = actor.system.resources.stress.value >= stressValue ? stressValue - 1 : stressValue;
this.render(); await actor.update({ 'system.resources.stress.value': newValue });
} this.render();
}
/**
* Toggles a armor slot resource value. /**
* @type {ApplicationClickAction} * Toggles a armor slot resource value.
*/ * @type {ApplicationClickAction}
static async #toggleArmorSlot(_, target, element) { */
const armorItem = await foundry.utils.fromUuid(target.dataset.itemUuid); static async #toggleArmorSlot(_, target, element) {
const armorValue = Number.parseInt(target.dataset.value); const armorItem = await foundry.utils.fromUuid(target.dataset.itemUuid);
const newValue = armorItem.system.marks.value >= armorValue ? armorValue - 1 : armorValue; const armorValue = Number.parseInt(target.dataset.value);
await armorItem.update({ 'system.marks.value': newValue }); const newValue = armorItem.system.marks.value >= armorValue ? armorValue - 1 : armorValue;
this.render(); await armorItem.update({ 'system.marks.value': newValue });
} this.render();
}
/**
* Opens Compedium Browser /**
*/ * Opens Compedium Browser
static async #tempBrowser(_, target) { */
new ItemBrowser().render({ force: true }); static async #tempBrowser(_, target) {
} new ItemBrowser().render({ force: true });
}
static async #refeshActions() {
const confirmed = await foundry.applications.api.DialogV2.confirm({ static async #refeshActions() {
window: { const confirmed = await foundry.applications.api.DialogV2.confirm({
title: 'New Section', window: {
icon: 'fa-solid fa-campground' title: 'New Section',
}, icon: 'fa-solid fa-campground'
content: await foundry.applications.handlebars.renderTemplate( },
'systems/daggerheart/templates/sidebar/daggerheart-menu/main.hbs', content: await foundry.applications.handlebars.renderTemplate(
{ 'systems/daggerheart/templates/sidebar/daggerheart-menu/main.hbs',
refreshables: DaggerheartMenu.defaultRefreshSelections() {
} refreshables: DaggerheartMenu.defaultRefreshSelections()
), }
classes: ['daggerheart', 'dialog', 'dh-style', 'tab', 'sidebar-tab', 'daggerheartMenu-sidebar'] ),
}); classes: ['daggerheart', 'dialog', 'dh-style', 'tab', 'sidebar-tab', 'daggerheartMenu-sidebar']
});
if (!confirmed) return;
} if (!confirmed) return;
}
static async #triggerRest(_, button) {
const confirmed = await foundry.applications.api.DialogV2.confirm({ static async #triggerRest(_, button) {
window: { const confirmed = await foundry.applications.api.DialogV2.confirm({
title: game.i18n.localize(`DAGGERHEART.APPLICATIONS.Downtime.${button.dataset.type}.title`), window: {
icon: button.dataset.type === 'shortRest' ? 'fa-solid fa-utensils' : 'fa-solid fa-bed' title: game.i18n.localize(`DAGGERHEART.APPLICATIONS.Downtime.${button.dataset.type}.title`),
}, icon: button.dataset.type === 'shortRest' ? 'fa-solid fa-utensils' : 'fa-solid fa-bed'
content: 'This will trigger a dialog to players make their downtime moves, are you sure?', },
classes: ['daggerheart', 'dialog', 'dh-style'] content: 'This will trigger a dialog to players make their downtime moves, are you sure?',
}); classes: ['daggerheart', 'dialog', 'dh-style']
});
if (!confirmed) return;
if (!confirmed) return;
this.document.system.partyMembers.forEach(actor => {
game.socket.emit(`system.${CONFIG.DH.id}`, { this.document.system.partyMembers.forEach(actor => {
action: socketEvent.DowntimeTrigger, game.socket.emit(`system.${CONFIG.DH.id}`, {
data: { action: socketEvent.DowntimeTrigger,
actorId: actor.uuid, data: {
downtimeType: button.dataset.type actorId: actor.uuid,
} downtimeType: button.dataset.type
}); }
}); });
} });
}
static async downtimeMoveQuery({ actorId, downtimeType }) {
const actor = await foundry.utils.fromUuid(actorId); static async downtimeMoveQuery({ actorId, downtimeType }) {
if (!actor || !actor?.isOwner) reject(); const actor = await foundry.utils.fromUuid(actorId);
new game.system.api.applications.dialogs.Downtime(actor, downtimeType === 'shortRest').render({ if (!actor || !actor?.isOwner) reject();
force: true new game.system.api.applications.dialogs.Downtime(actor, downtimeType === 'shortRest').render({
}); force: true
} });
}
static async #groupRoll(params) {
new GroupRollDialog(this.document.system.partyMembers).render({ force: true }); static async #tagTeamRoll() {
} new game.system.api.applications.dialogs.TagTeamDialog(this.document.system.partyMembers).render({
force: true
/** });
* Get the set of ContextMenu options for Consumable and Loot. }
* @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
* @this {CharacterSheet} static async #groupRoll(params) {
* @protected new GroupRollDialog(this.document.system.partyMembers).render({ force: true });
*/ }
static #getItemContextOptions() {
return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true }); /**
} * Get the set of ContextMenu options for Consumable and Loot.
/* -------------------------------------------- */ * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance
/* Filter Tracking */ * @this {CharacterSheet}
/* -------------------------------------------- */ * @protected
*/
/** static #getItemContextOptions() {
* The currently active search filter. return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true });
* @type {foundry.applications.ux.SearchFilter} }
*/ /* -------------------------------------------- */
#search = {}; /* Filter Tracking */
/* -------------------------------------------- */
/**
* The currently active search filter. /**
* @type {FilterMenu} * The currently active search filter.
*/ * @type {foundry.applications.ux.SearchFilter}
#menu = {}; */
#search = {};
/**
* Tracks which item IDs are currently displayed, organized by filter type and section. /**
* @type {{ * The currently active search filter.
* inventory: { * @type {FilterMenu}
* search: Set<string>, */
* menu: Set<string> #menu = {};
* },
* loadout: { /**
* search: Set<string>, * Tracks which item IDs are currently displayed, organized by filter type and section.
* menu: Set<string> * @type {{
* }, * inventory: {
* }} * search: Set<string>,
*/ * menu: Set<string>
#filteredItems = { * },
inventory: { * loadout: {
search: new Set(), * search: Set<string>,
menu: new Set() * menu: Set<string>
}, * },
loadout: { * }}
search: new Set(), */
menu: new Set() #filteredItems = {
} inventory: {
}; search: new Set(),
menu: new Set()
/* -------------------------------------------- */ },
/* Search Inputs */ loadout: {
/* -------------------------------------------- */ search: new Set(),
menu: new Set()
/** }
* Create and initialize search filter instances for the inventory and loadout sections. };
*
* Sets up two {@link foundry.applications.ux.SearchFilter} instances: /* -------------------------------------------- */
* - One for the inventory, which filters items in the inventory grid. /* Search Inputs */
* - One for the loadout, which filters items in the loadout/card grid. /* -------------------------------------------- */
* @private
*/ /**
_createSearchFilter() { * Create and initialize search filter instances for the inventory and loadout sections.
//Filters could be a application option if needed *
const filters = [ * Sets up two {@link foundry.applications.ux.SearchFilter} instances:
{ * - One for the inventory, which filters items in the inventory grid.
key: 'inventory', * - One for the loadout, which filters items in the loadout/card grid.
input: 'input[type="search"].search-inventory', * @private
content: '[data-application-part="inventory"] .items-section', */
callback: this._onSearchFilterInventory.bind(this) _createSearchFilter() {
} //Filters could be a application option if needed
]; const filters = [
{
for (const { key, input, content, callback } of filters) { key: 'inventory',
const filter = new foundry.applications.ux.SearchFilter({ input: 'input[type="search"].search-inventory',
inputSelector: input, content: '[data-application-part="inventory"] .items-section',
contentSelector: content, callback: this._onSearchFilterInventory.bind(this)
callback }
}); ];
filter.bind(this.element);
this.#search[key] = filter; for (const { key, input, content, callback } of filters) {
} const filter = new foundry.applications.ux.SearchFilter({
} inputSelector: input,
contentSelector: content,
/** callback
* Handle invetory items search and filtering. });
* @param {KeyboardEvent} event The keyboard input event. filter.bind(this.element);
* @param {string} query The input search string. this.#search[key] = filter;
* @param {RegExp} rgx The regular expression query that should be matched against. }
* @param {HTMLElement} html The container to filter items from. }
* @protected
*/ /**
async _onSearchFilterInventory(_event, query, rgx, html) { * Handle invetory items search and filtering.
this.#filteredItems.inventory.search.clear(); * @param {KeyboardEvent} event The keyboard input event.
* @param {string} query The input search string.
for (const li of html.querySelectorAll('.inventory-item')) { * @param {RegExp} rgx The regular expression query that should be matched against.
const item = await getDocFromElement(li); * @param {HTMLElement} html The container to filter items from.
const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name); * @protected
if (matchesSearch) this.#filteredItems.inventory.search.add(item.id); */
const { menu } = this.#filteredItems.inventory; async _onSearchFilterInventory(_event, query, rgx, html) {
li.hidden = !(menu.has(item.id) && matchesSearch); this.#filteredItems.inventory.search.clear();
}
} for (const li of html.querySelectorAll('.inventory-item')) {
const item = await getDocFromElement(li);
/* -------------------------------------------- */ const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name);
/* Filter Menus */ if (matchesSearch) this.#filteredItems.inventory.search.add(item.id);
/* -------------------------------------------- */ const { menu } = this.#filteredItems.inventory;
li.hidden = !(menu.has(item.id) && matchesSearch);
_createFilterMenus() { }
//Menus could be a application option if needed }
const menus = [
{ /* -------------------------------------------- */
key: 'inventory', /* Filter Menus */
container: '[data-application-part="inventory"]', /* -------------------------------------------- */
content: '.items-section',
callback: this._onMenuFilterInventory.bind(this), _createFilterMenus() {
target: '.filter-button', //Menus could be a application option if needed
filters: FilterMenu.invetoryFilters const menus = [
} {
]; key: 'inventory',
container: '[data-application-part="inventory"]',
menus.forEach(m => { content: '.items-section',
const container = this.element.querySelector(m.container); callback: this._onMenuFilterInventory.bind(this),
this.#menu[m.key] = new FilterMenu(container, m.target, m.filters, m.callback, { target: '.filter-button',
contentSelector: m.content filters: FilterMenu.invetoryFilters
}); }
}); ];
}
menus.forEach(m => {
/** const container = this.element.querySelector(m.container);
* Callback when filters change this.#menu[m.key] = new FilterMenu(container, m.target, m.filters, m.callback, {
* @param {PointerEvent} event contentSelector: m.content
* @param {HTMLElement} html });
* @param {import('../ux/filter-menu.mjs').FilterItem[]} filters });
*/ }
async _onMenuFilterInventory(_event, html, filters) {
this.#filteredItems.inventory.menu.clear(); /**
* Callback when filters change
for (const li of html.querySelectorAll('.inventory-item')) { * @param {PointerEvent} event
const item = await getDocFromElement(li); * @param {HTMLElement} html
* @param {import('../ux/filter-menu.mjs').FilterItem[]} filters
const matchesMenu = */
filters.length === 0 || filters.some(f => foundry.applications.ux.SearchFilter.evaluateFilter(item, f)); async _onMenuFilterInventory(_event, html, filters) {
if (matchesMenu) this.#filteredItems.inventory.menu.add(item.id); this.#filteredItems.inventory.menu.clear();
const { search } = this.#filteredItems.inventory; for (const li of html.querySelectorAll('.inventory-item')) {
li.hidden = !(search.has(item.id) && matchesMenu); const item = await getDocFromElement(li);
}
} const matchesMenu =
filters.length === 0 || filters.some(f => foundry.applications.ux.SearchFilter.evaluateFilter(item, f));
/* -------------------------------------------- */ if (matchesMenu) this.#filteredItems.inventory.menu.add(item.id);
async _onDragStart(event) { const { search } = this.#filteredItems.inventory;
const item = event.currentTarget.closest('.inventory-item'); li.hidden = !(search.has(item.id) && matchesMenu);
}
if (item) { }
const adversaryData = { type: 'Actor', uuid: item.dataset.itemUuid };
event.dataTransfer.setData('text/plain', JSON.stringify(adversaryData)); /* -------------------------------------------- */
event.dataTransfer.setDragImage(item, 60, 0);
} async _onDragStart(event) {
} const item = event.currentTarget.closest('.inventory-item');
async _onDrop(event) { if (item) {
const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); const adversaryData = { type: 'Actor', uuid: item.dataset.itemUuid };
const actor = await foundry.utils.fromUuid(data.uuid); event.dataTransfer.setData('text/plain', JSON.stringify(adversaryData));
const currentMembers = this.document.system.partyMembers.map(x => x.uuid); event.dataTransfer.setDragImage(item, 60, 0);
}
if (foundry.utils.parseUuid(data.uuid).collection instanceof CompendiumCollection) return; }
if (actor.type !== 'character') { async _onDrop(event) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.onlyCharactersInPartySheet')); const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);
} const actor = await foundry.utils.fromUuid(data.uuid);
const currentMembers = this.document.system.partyMembers.map(x => x.uuid);
if (currentMembers.includes(data.uuid)) {
return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.duplicateCharacter')); if (foundry.utils.parseUuid(data.uuid).collection instanceof CompendiumCollection) return;
}
if (actor.type !== 'character') {
await this.document.update({ 'system.partyMembers': [...currentMembers, actor.uuid] }); return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.onlyCharactersInPartySheet'));
} }
static async #deletePartyMember(_event, target) { if (currentMembers.includes(data.uuid)) {
const doc = await getDocFromElement(target.closest('.inventory-item')); return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.duplicateCharacter'));
}
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: { await this.document.update({ 'system.partyMembers': [...currentMembers, actor.uuid] });
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { }
type: game.i18n.localize('TYPES.Actor.adversary'),
name: doc.name static async #deletePartyMember(_event, target) {
}) const doc = await getDocFromElement(target.closest('.inventory-item'));
},
content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: doc.name }) const confirmed = await foundry.applications.api.DialogV2.confirm({
}); window: {
title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', {
if (!confirmed) return; type: game.i18n.localize('TYPES.Actor.adversary'),
name: doc.name
const currentMembers = this.document.system.partyMembers.map(x => x.uuid); })
const newMemberdList = currentMembers.filter(uuid => uuid !== doc.uuid); },
await this.document.update({ 'system.partyMembers': newMemberdList }); content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: doc.name })
} });
}
if (!confirmed) return;
const currentMembers = this.document.system.partyMembers.map(x => x.uuid);
const newMemberdList = currentMembers.filter(uuid => uuid !== doc.uuid);
await this.document.update({ 'system.partyMembers': newMemberdList });
}
}

View file

@ -1,3 +1,5 @@
import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog { export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog {
constructor(options) { constructor(options) {
super(options); super(options);
@ -35,7 +37,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
// } // }
// }, // },
{ {
name: 'Reroll Damage', name: game.i18n.localize('DAGGERHEART.UI.ChatLog.rerollDamage'),
icon: '<i class="fa-solid fa-dice"></i>', icon: '<i class="fa-solid fa-dice"></i>',
condition: li => { condition: li => {
const message = game.messages.get(li.dataset.messageId); const message = game.messages.get(li.dataset.messageId);
@ -164,6 +166,14 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
'system.roll': newRoll, 'system.roll': newRoll,
'rolls': [parsedRoll] '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

@ -27,7 +27,8 @@ export const gameSettings = {
}, },
LevelTiers: 'LevelTiers', LevelTiers: 'LevelTiers',
Countdowns: 'Countdowns', Countdowns: 'Countdowns',
LastMigrationVersion: 'LastMigrationVersion' LastMigrationVersion: 'LastMigrationVersion',
TagTeamRoll: 'TagTeamRoll'
}; };
export const actionAutomationChoices = { export const actionAutomationChoices = {

View file

@ -1,5 +1,6 @@
export { default as DhCombat } from './combat.mjs'; export { default as DhCombat } from './combat.mjs';
export { default as DhCombatant } from './combatant.mjs'; export { default as DhCombatant } from './combatant.mjs';
export { default as DhTagTeamRoll } from './tagTeamRoll.mjs';
export * as actions from './action/_module.mjs'; export * as actions from './action/_module.mjs';
export * as activeEffects from './activeEffect/_module.mjs'; export * as activeEffects from './activeEffect/_module.mjs';

View file

@ -0,0 +1,20 @@
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,4 +1,5 @@
import DamageDialog from '../applications/dialogs/damageDialog.mjs'; import DamageDialog from '../applications/dialogs/damageDialog.mjs';
import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs';
import DHRoll from './dhRoll.mjs'; import DHRoll from './dhRoll.mjs';
export default class DamageRoll extends DHRoll { export default class DamageRoll extends DHRoll {
@ -338,5 +339,13 @@ export default class DamageRoll extends DHRoll {
parts: damageParts parts: damageParts
} }
}); });
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll });
await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.Refresh,
data: {
refreshType: RefreshType.TagTeamRoll
}
});
} }
} }

View file

@ -29,6 +29,10 @@ export default class DHRoll extends Roll {
config.hooks = [...this.getHooks(), '']; config.hooks = [...this.getHooks(), ''];
config.dialog ??= {}; config.dialog ??= {};
const actorIdSplit = config.source.actor.split('.');
const tagTeamSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
config.tagTeamSelected = tagTeamSettings.members[actorIdSplit[actorIdSplit.length - 1]];
for (const hook of config.hooks) { for (const hook of config.hooks) {
if (Hooks.call(`${CONFIG.DH.id}.preRoll${hook.capitalize()}`, config, message) === false) return null; if (Hooks.call(`${CONFIG.DH.id}.preRoll${hook.capitalize()}`, config, message) === false) return null;
} }
@ -100,6 +104,10 @@ export default class DHRoll extends Roll {
if (roll._evaluated) { if (roll._evaluated) {
const message = await cls.create(msgData, { rollMode: config.selectedRollMode }); const message = await cls.create(msgData, { rollMode: config.selectedRollMode });
if (config.tagTeamSelected) {
game.system.api.applications.dialogs.TagTeamDialog.assignRoll(message.speakerActor, message);
}
if (game.modules.get('dice-so-nice')?.active) { if (game.modules.get('dice-so-nice')?.active) {
await game.dice3d.waitFor3DAnimationByMessageID(message.id); await game.dice3d.waitFor3DAnimationByMessageID(message.id);
} }
@ -228,7 +236,8 @@ export const registerRollDiceHooks = () => {
if ( if (
!config.source?.actor || !config.source?.actor ||
(game.user.isGM ? !hopeFearAutomation.gm : !hopeFearAutomation.players) || (game.user.isGM ? !hopeFearAutomation.gm : !hopeFearAutomation.players) ||
config.actionType === 'reaction' config.actionType === 'reaction' ||
config.tagTeamSelected
) )
return; return;

View file

@ -1,4 +1,4 @@
import { emitAsGM, GMUpdateEvent } from '../systemRegistration/socket.mjs'; import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../systemRegistration/socket.mjs';
export default class DhpChatMessage extends foundry.documents.ChatMessage { export default class DhpChatMessage extends foundry.documents.ChatMessage {
targetHook = null; targetHook = null;
@ -149,7 +149,15 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
event.stopPropagation(); event.stopPropagation();
const config = foundry.utils.deepClone(this.system); const config = foundry.utils.deepClone(this.system);
config.event = event; config.event = event;
this.system.action?.workflow.get('damage')?.execute(config, this._id, true); 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) { async onApplyDamage(event) {

View file

@ -14,7 +14,8 @@ export default class RegisterHandlebarsHelpers {
getProperty: foundry.utils.getProperty, getProperty: foundry.utils.getProperty,
setVar: this.setVar, setVar: this.setVar,
empty: this.empty, empty: this.empty,
pluralize: this.pluralize pluralize: this.pluralize,
log: this.log
}); });
} }
static add(a, b) { static add(a, b) {
@ -89,4 +90,8 @@ export default class RegisterHandlebarsHelpers {
const key = isSingular ? `${baseKey}.single` : `${baseKey}.plural`; const key = isSingular ? `${baseKey}.single` : `${baseKey}.plural`;
return game.i18n.localize(key); return game.i18n.localize(key);
} }
static log(test) {
return test;
}
} }

View file

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

View file

@ -34,7 +34,8 @@ export const GMUpdateEvent = {
}; };
export const RefreshType = { export const RefreshType = {
Countdown: 'DhCoundownRefresh' Countdown: 'DhCoundownRefresh',
TagTeamRoll: 'DhTagTeamRollRefresh'
}; };
export const registerSocketHooks = () => { export const registerSocketHooks = () => {

View file

@ -11,7 +11,14 @@
.application.daggerheart.dialog.dh-style.views.roll-selection { .application.daggerheart.dialog.dh-style.views.roll-selection {
.dialog-header { .dialog-header {
display: flex; display: flex;
justify-content: center; flex-direction: column;
align-items: center;
gap: 4px;
.dialog-header-inner {
display: flex;
justify-content: center;
}
h1 { h1 {
width: auto; width: auto;
@ -37,6 +44,29 @@
} }
} }
} }
.tag-team-controller {
display: flex;
align-items: center;
border-radius: 5px;
width: fit-content;
gap: 5px;
cursor: pointer;
padding: 5px;
background: light-dark(@dark-blue-10, @golden-10);
color: light-dark(@dark-blue, @golden);
.label {
font-style: normal;
font-weight: 400;
font-size: var(--font-size-14);
line-height: 17px;
}
&.selected {
background: light-dark(@dark-blue-40, @golden-40);
}
}
} }
.roll-dialog-container { .roll-dialog-container {

View file

@ -32,3 +32,5 @@
@import './reroll-dialog/sheet.less'; @import './reroll-dialog/sheet.less';
@import './group-roll/group-roll.less'; @import './group-roll/group-roll.less';
@import './tag-team-dialog/sheet.less';

View file

@ -0,0 +1,178 @@
.daggerheart.dialog.dh-style.views.tag-team-dialog {
.tag-team-container {
display: flex;
flex-direction: column;
gap: 16px;
.tag-team-data-container {
display: flex;
align-items: center;
gap: 8px;
.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;
display: flex;
flex-direction: column;
gap: 4px;
cursor: pointer;
border-radius: 6px;
&.selected,
&:hover {
background-color: light-dark(@golden-40, @golden-40);
}
.participant-container {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
.participant-inner-container {
flex: 1;
display: flex;
align-items: center;
gap: 4px;
img {
height: 48px;
width: 48px;
border-radius: 50%;
}
.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;
}
}
}
}
}
.participant-empty-roll-container {
border: 1px dashed white;
padding: 8px 2px;
text-align: center;
font-style: italic;
}
.participant-roll-outer-container {
display: flex;
flex-direction: column;
gap: 2px;
color: light-dark(@dark-blue, @golden);
h4 {
text-align: center;
color: light-dark(@dark-blue, @golden);
}
.participant-roll-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;
display: flex;
gap: 4px;
}
}
}
}
}
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;
}
}
}

View file

@ -4,6 +4,7 @@
@golden: #f3c267; @golden: #f3c267;
@golden-10: #f3c26710; @golden-10: #f3c26710;
@golden-40: #f3c26740; @golden-40: #f3c26740;
@golden-90: #f3c26790;
@golden-bg: #f3c2671a; @golden-bg: #f3c2671a;
@golden-secondary: #eaaf42; @golden-secondary: #eaaf42;
@golden-filter: brightness(0) saturate(100%) invert(89%) sepia(13%) saturate(2008%) hue-rotate(332deg) brightness(99%) @golden-filter: brightness(0) saturate(100%) invert(89%) sepia(13%) saturate(2008%) hue-rotate(332deg) brightness(99%)

View file

@ -5,7 +5,7 @@
"version": "1.2.0", "version": "1.2.0",
"compatibility": { "compatibility": {
"minimum": "13", "minimum": "13",
"verified": "13.347", "verified": "13.350",
"maximum": "13" "maximum": "13"
}, },
"authors": [ "authors": [
@ -269,7 +269,8 @@
"dualityRoll": {}, "dualityRoll": {},
"adversaryRoll": {}, "adversaryRoll": {},
"damageRoll": {}, "damageRoll": {},
"abilityUse": {} "abilityUse": {},
"tagTeam": {}
} }
}, },
"background": "systems/daggerheart/assets/logos/FoundrybornBackgroundLogo.png", "background": "systems/daggerheart/assets/logos/FoundrybornBackgroundLogo.png",

View file

@ -1,14 +1,22 @@
<header class="dialog-header"> <header class="dialog-header">
<h1> <div class="dialog-header-inner">
{{#if reactionOverride}} <h1>
{{localize "DAGGERHEART.CONFIG.ActionType.reaction"}} {{#if reactionOverride}}
{{else}} {{localize "DAGGERHEART.CONFIG.ActionType.reaction"}}
{{ifThen rollConfig.headerTitle rollConfig.headerTitle rollConfig.title}} {{else}}
{{/if}} {{ifThen rollConfig.headerTitle rollConfig.headerTitle rollConfig.title}}
{{#if showReaction}} {{/if}}
<button class="reaction-roll-controller {{#if reactionOverride}}active{{/if}}" data-action="toggleReaction" data-tooltip-text="{{localize "DAGGERHEART.GENERAL.reactionRoll"}}"> {{#if showReaction}}
<i class="fa-solid fa-reply"></i> <button class="reaction-roll-controller {{#if reactionOverride}}active{{/if}}" data-action="toggleReaction" data-tooltip-text="{{localize "DAGGERHEART.GENERAL.reactionRoll"}}">
</button> <i class="fa-solid fa-reply"></i>
{{/if}} </button>
</h1> {{/if}}
</h1>
</div>
{{#if (and @root.hasRoll @root.activeTagTeamRoll)}}
<div class="tag-team-controller {{#if @root.tagTeamSelected}}selected{{/if}}" data-action="toggleTagTeamRoll">
<span><i class="{{ifThen @root.tagTeamSelected "fa-solid" "fa-regular"}} fa-circle"></i></span>
<span class="label">{{localize "Tag Team Roll"}}</span>
</div>
{{/if}}
</header> </header>

View file

@ -0,0 +1,110 @@
<div>
<div class="tag-team-container">
<fieldset>
<legend>{{localize "Party Team"}}</legend>
<div class="form-group flex-group">
<label>{{localize "TYPES.Actor.character"}}</label>
<div class="form-fields">
<select name="selectedAddMember">
{{selectOptions memberOptions labelAttr="name" valueAttr="uuid" blank=""}}
</select>
</div>
<div class="participants-container">
{{#each members as |member|}}
<div class="participant-outer-container {{#if member.selected}}selected{{/if}}" data-action="selectMessage" id="{{member.character.id}}">
<div class="participant-container">
<div class="participant-inner-container">
<img src="{{member.character.img}}" />
<div class="participant-labels">
<div class="participant-label-title">{{member.character.name}}</div>
<div class="participant-label-info">
{{#if member.character.system.class.value}}
<div class="participant-label-info-part">{{member.character.system.class.value.name}}</div>
{{/if}}
{{#if member.system.multiclass.value}}
<div class="participant-label-info-part">{{member.character.system.multiclass.value.name}}</div>
{{/if}}
</div>
</div>
</div>
<a data-action="removeMember" data-character-id="{{member.character.id}}"><i class="fa-solid fa-trash"></i></a>
</div>
{{#if member.roll}}
<div class="participant-roll-outer-container">
<div class="participant-roll-container">
<h4>
<a data-action="unlinkMessage" id="{{member.character.id}}"><i class="fa-solid fa-link"></i></a>
{{member.roll.system.title}}
</h4>
</div>
<div class="participant-roll-container">
<side-line-div class="invert"></side-line-div>
<div class="participant-roll-text-container">
{{member.roll.system.roll.total}}
{{localize "DAGGERHEART.GENERAL.withThing" thing=member.roll.system.roll.result.label}}
</div>
<side-line-div></side-line-div>
</div>
{{#if member.roll.system.hasDamage}}
<h4>{{localize "DAGGERHEART.GENERAL.damage"}}</h4>
<div class="damage-values-container">
{{#if member.damageValues}}
{{#each member.damageValues as |damage|}}
<div class="damage-container">
<div>{{damage.name}}</div>
<div>{{damage.total}}</div>
</div>
{{/each}}
{{else}}
{{localize "DAGGERHEART.APPLICATIONS.TagTeamSelect.damageNotRolled"}}
{{/if}}
</div>
{{/if}}
</div>
{{else}}
<div class="participant-empty-roll-container">
{{localize "DAGGERHEART.APPLICATIONS.TagTeamSelect.linkMessageHint"}}
</div>
{{/if}}
</div>
{{/each}}
</div>
</fieldset>
<div class="roll-leader-container">
<h4>
<strong>{{localize "Initiating Character"}}</strong>
<select name="initiator.id">
{{selectOptions selectedCharacterOptions selected=initiator.character.id labelAttr="name" valueAttr="id" blank=""}}
</select>
</h4>
<h4>
<strong>{{localize "Cost"}}</strong>
<input type="text" data-dtype="Number" min="0" name="initiator.cost" value="{{initiator.cost}}" />
</h4>
</div>
{{#if showResult}}
{{#if selectedData.result}}
<div class="result-container">
<h4><strong>Tag Team Roll:</strong> {{selectedData.result}}</h4>
{{#if usesDamage}}
<div class="result-damages-container">
<label><strong>{{localize "DAGGERHEART.GENERAL.damage"}}:</strong></label>
{{#each selectedData.damageValues as |damage|}}
<div class="result-damage-container">
{{damage.name}}
{{damage.total}}
</div>
{{/each}}
</div>
{{/if}}
</div>
{{/if}}
{{/if}}
<button data-action="createTagTeam" {{disabled createDisabled}}>{{localize "DAGGERHEART.APPLICATIONS.TagTeamSelect.createTagTeam"}}</button>
</div>
</div>

View file

@ -5,7 +5,7 @@
> >
<div class="actions-section"> <div class="actions-section">
<button> <button data-action="tagTeamRoll">
<i class="fa-solid fa-user-group"></i> <i class="fa-solid fa-user-group"></i>
<span>Tag Team Roll</span> <span>Tag Team Roll</span>
</button> </button>