From 261a3a68b028889fe028887902e1d89a182b5314 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:02:45 +0100 Subject: [PATCH] [PR] [Feature] Party Sheet (#1230) * start development * finish party members tab * start resources tab * finish resources tab * finish inventory tab and add inital template to projects tab * add resource buttons actions methods * add group roll dialog * Main implementation * Fixed costs * Minor fixes and tweaks for the party sheet (#1239) * Minor fixes and tweaks for the party sheet * Fix scroll restoration for party sheet tabs * Finished GroupRoll * Removed/commented-out not yet implemented things * Commented out Difficulty since it's not used yet * Re-render party when members update (#1242) * Fixed so style applies in preview chat message * Added the clown car * Fixed so items can be dropped into the Party sheet * Added delete icon to inventory * Fixed TokenHUD token property useage. Fixed skipping roll message * Added visible modifier to GroupRoll leader result * Leader roll displays the large result display right away after rolling * Corrected tokenHUD for non-player-tokens * Fixed clowncar tokenData * Fixed TagTeam roll message and sound * Removed final TagTeamRoll roll sound * [PR] [Party Sheets] Sidebar character sheet changes (#1249) * Something experimenting * I am silly (wearning Dunce hat) * Stressful task * Armor functional to be hit * CSS Changes to accomadate pip boy * last minute change to resource section for better visual feeling * restoring old css for toggle * Added setting to toggle pip/number display * toggle functionality added * Fixed light-mode in characterSheet * Fixed multi-row resource pips display for character * Fixed separators * Added pip-display to Adversary and Companion. Some fixing on armor display --------- Co-authored-by: WBHarry * Fixed party height and resource armor update * Fixed deletebutton padding * Only showing expand-me icon on InventoryItem if there is a description to show * . * Fixed menu icon to be beige instead of white in dark mode --------- Co-authored-by: moliloo Co-authored-by: Carlos Fernandez Co-authored-by: Nikhil Nagarajan --- assets/icons/arrow-dunk.png | Bin 0 -> 26971 bytes assets/icons/documents/actors/dark-squad.svg | 1 + daggerheart.mjs | 4 + lang/en.json | 54 +- module/applications/dialogs/_module.mjs | 2 + module/applications/dialogs/d20RollDialog.mjs | 13 + .../dialogs/group-roll-dialog.mjs | 196 +++++++ .../dialogs/rerollDamageDialog.mjs | 11 + module/applications/dialogs/tagTeamDialog.mjs | 315 +++++++++++ module/applications/hud/tokenHUD.mjs | 110 +++- module/applications/sheets/actors/_module.mjs | 1 + .../applications/sheets/actors/adversary.mjs | 33 ++ .../applications/sheets/actors/character.mjs | 44 ++ .../applications/sheets/actors/companion.mjs | 11 + .../sheets/actors/environment.mjs | 2 - module/applications/sheets/actors/party.mjs | 512 ++++++++++++++++++ module/applications/sheets/api/base-actor.mjs | 4 + .../sidebar/tabs/daggerheartMenu.mjs | 6 +- module/applications/ui/chatLog.mjs | 180 +++++- module/applications/ui/fearTracker.mjs | 2 +- module/applications/ui/itemBrowser.mjs | 15 +- module/config/generalConfig.mjs | 2 +- module/config/settingsConfig.mjs | 3 +- module/data/_module.mjs | 1 + module/data/actor/_module.mjs | 6 +- module/data/actor/adversary.mjs | 9 + module/data/actor/character.mjs | 4 + module/data/actor/party.mjs | 48 ++ module/data/chat-message/_modules.mjs | 2 + module/data/chat-message/groupRoll.mjs | 39 ++ module/data/item/armor.mjs | 6 + module/data/settings/Appearance.mjs | 1 + module/data/settings/Automation.mjs | 6 +- module/data/tagTeamRoll.mjs | 20 + module/dice/damageRoll.mjs | 9 + module/dice/dhRoll.mjs | 22 +- module/dice/dualityRoll.mjs | 2 + module/documents/actor.mjs | 28 +- module/documents/chatMessage.mjs | 14 +- module/enrichers/LookupEnricher.mjs | 2 +- module/enrichers/parser.mjs | 2 +- module/helpers/handlebarsHelper.mjs | 7 +- module/helpers/utils.mjs | 12 + module/systemRegistration/handlebars.mjs | 1 + module/systemRegistration/settings.mjs | 7 + module/systemRegistration/socket.mjs | 10 +- .../less/dialog/dice-roll/roll-selection.less | 32 +- styles/less/dialog/group-roll/group-roll.less | 50 ++ styles/less/dialog/index.less | 3 + styles/less/dialog/tag-team-dialog/sheet.less | 178 ++++++ styles/less/global/dialog.less | 4 + styles/less/global/index.less | 1 + styles/less/global/inventory-item.less | 13 +- styles/less/global/resource-bar.less | 178 ++++++ styles/less/global/sheet.less | 6 +- styles/less/global/tab-description.less | 32 +- styles/less/hud/token-hud/token-hud.less | 8 + .../less/sheets/actors/adversary/sidebar.less | 8 + .../less/sheets/actors/character/sidebar.less | 116 +++- .../less/sheets/actors/companion/header.less | 191 ++++--- styles/less/sheets/actors/party/header.less | 42 ++ .../less/sheets/actors/party/inventory.less | 73 +++ .../sheets/actors/party/party-members.less | 28 + .../less/sheets/actors/party/resources.less | 196 +++++++ styles/less/sheets/actors/party/sheet.less | 45 ++ styles/less/sheets/index.less | 6 + styles/less/ui/chat/group-roll.less | 210 +++++++ styles/less/ui/countdown/countdown-edit.less | 2 +- styles/less/ui/index.less | 1 + styles/less/ui/sidebar/tabs.less | 1 + styles/less/utils/colors.less | 2 + styles/less/ux/autocomplete/autocomplete.less | 22 +- system.json | 7 +- templates/dialogs/dice-roll/header.hbs | 32 +- templates/dialogs/group-roll/group-roll.hbs | 84 +++ templates/dialogs/tagTeamDialog.hbs | 110 ++++ templates/hud/tokenHUD.hbs | 8 + .../settings/appearance-settings/main.hbs | 4 + templates/sheets/actors/adversary/sidebar.hbs | 31 +- templates/sheets/actors/character/sidebar.hbs | 66 +-- templates/sheets/actors/companion/header.hbs | 25 +- templates/sheets/actors/party/header.hbs | 9 + templates/sheets/actors/party/inventory.hbs | 82 +++ templates/sheets/actors/party/notes.hbs | 10 + .../sheets/actors/party/party-members.hbs | 40 ++ templates/sheets/actors/party/projects.hbs | 4 + templates/sheets/actors/party/resources.hbs | 99 ++++ .../partials/inventory-fieldset-items-V2.hbs | 2 + .../global/partials/inventory-item-V2.hbs | 41 +- .../sheets/global/partials/resource-bar.hbs | 35 ++ templates/ui/chat/groupRoll.hbs | 124 +++++ 91 files changed, 3769 insertions(+), 271 deletions(-) create mode 100644 assets/icons/arrow-dunk.png create mode 100644 assets/icons/documents/actors/dark-squad.svg create mode 100644 module/applications/dialogs/group-roll-dialog.mjs create mode 100644 module/applications/dialogs/tagTeamDialog.mjs create mode 100644 module/applications/sheets/actors/party.mjs create mode 100644 module/data/actor/party.mjs create mode 100644 module/data/chat-message/groupRoll.mjs create mode 100644 module/data/tagTeamRoll.mjs create mode 100644 styles/less/dialog/group-roll/group-roll.less create mode 100644 styles/less/dialog/tag-team-dialog/sheet.less create mode 100644 styles/less/global/resource-bar.less create mode 100644 styles/less/sheets/actors/party/header.less create mode 100644 styles/less/sheets/actors/party/inventory.less create mode 100644 styles/less/sheets/actors/party/party-members.less create mode 100644 styles/less/sheets/actors/party/resources.less create mode 100644 styles/less/sheets/actors/party/sheet.less create mode 100644 styles/less/ui/chat/group-roll.less create mode 100644 templates/dialogs/group-roll/group-roll.hbs create mode 100644 templates/dialogs/tagTeamDialog.hbs create mode 100644 templates/sheets/actors/party/header.hbs create mode 100644 templates/sheets/actors/party/inventory.hbs create mode 100644 templates/sheets/actors/party/notes.hbs create mode 100644 templates/sheets/actors/party/party-members.hbs create mode 100644 templates/sheets/actors/party/projects.hbs create mode 100644 templates/sheets/actors/party/resources.hbs create mode 100644 templates/sheets/global/partials/resource-bar.hbs create mode 100644 templates/ui/chat/groupRoll.hbs diff --git a/assets/icons/arrow-dunk.png b/assets/icons/arrow-dunk.png new file mode 100644 index 0000000000000000000000000000000000000000..1958713e9d40e7a63e35555439ae04a0118774b3 GIT binary patch literal 26971 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 literal 0 HcmV?d00001 diff --git a/assets/icons/documents/actors/dark-squad.svg b/assets/icons/documents/actors/dark-squad.svg new file mode 100644 index 00000000..f21b4c5b --- /dev/null +++ b/assets/icons/documents/actors/dark-squad.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/daggerheart.mjs b/daggerheart.mjs index b6c4415f..651736a4 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -85,6 +85,10 @@ Hooks.once('init', () => { types: ['environment'], makeDefault: true }); + Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Party, { + types: ['party'], + makeDefault: true + }); CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect; CONFIG.ActiveEffect.dataModels = models.activeEffects.config; diff --git a/lang/en.json b/lang/en.json index db3f2564..b63a3aac 100755 --- a/lang/en.json +++ b/lang/en.json @@ -20,7 +20,8 @@ "character": "Character", "companion": "Companion", "adversary": "Adversary", - "environment": "Environment" + "environment": "Environment", + "party": "Party" } }, "CONTROLS": { @@ -437,7 +438,9 @@ }, "HUD": { "tokenHUD": { - "genericEffects": "Foundry Effects" + "genericEffects": "Foundry Effects", + "depositPartyTokens": "Deposit Party Tokens", + "retrievePartyTokens": "Retrieve Party Tokens" } }, "ImageSelect": { @@ -568,6 +571,19 @@ "ResourceDice": { "title": "{name} Resource", "rerollDice": "Reroll Dice" + }, + "TagTeamSelect": { + "title": "Tag Team Roll", + "leaderTitle": "Initiating Character", + "membersTitle": "Participants", + "partyTeam": "Party Team", + "hopeCost": "Hope Cost", + "initiatingCharacter": "Initiating Character", + "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": { @@ -1936,6 +1952,7 @@ "story": "Story", "biography": "Biography", "general": "General", + "resources": "Resources", "foundation": "Foundation", "specialization": "Specialization", "mastery": "Mastery", @@ -1953,6 +1970,8 @@ "downtime": "Downtime", "roll": "Roll", "rules": "Rules", + "partyMembers": "Party Members", + "projects": "Projects", "types": "Types", "itemFeatures": "Item Features", "questions": "Questions", @@ -2233,7 +2252,8 @@ "target": { "label": "Target" } - } + }, + "useResourcePips": { "label": "Pip Display For Resources" } }, "fearDisplay": { "token": "Tokens", @@ -2480,6 +2500,17 @@ "title": "Effects Applied" }, "featureTitle": "Class Feature", + "groupRoll": { + "title": "Group Roll", + "leader": "Leader", + "partyTeam": "Party Team", + "team": "Team", + "selectLeader": "Select a Leader", + "selectMember": "Select a Member", + "rerollTitle": "Reroll Group Roll", + "rerollContent": "Are you sure you want to reroll your {trait} check?", + "rerollTooltip": "Reroll" + }, "healingRoll": { "title": "Heal - {damage}", "heal": "Heal", @@ -2496,8 +2527,16 @@ }, "resourceRoll": { "playerMessage": "{user} rerolled their {name}" + }, + "tagTeam": { + "title": "Tag Team", + "membersTitle": "Members" } }, + "ChatLog": { + "rerollDamage": "Reroll Damage", + "assignTagRoll": "Assign as Tag Roll" + }, "Countdowns": { "title": "Countdowns", "toggleIconMode": "Toggle Icon Only", @@ -2577,6 +2616,8 @@ "wrongDomain": "The card isn't from one of your class domains.", "cardTooHighLevel": "The card is too high level!", "duplicateCard": "You cannot select the same card more than once.", + "duplicateCharacter": "This actor is already registered in the party members list.", + "onlyCharactersInPartySheet": "You can drag only characters to a party sheet.", "notPrimary": "The weapon is not a primary weapon!", "notSecondary": "The weapon is not a secondary weapon!", "itemTooHighTier": "The item must be from Tier1", @@ -2611,7 +2652,9 @@ "noDiceSystem": "Your selected dice {system} does not have a {faces} dice", "gmMenuRefresh": "You refreshed all actions and resources {types}", "subclassAlreadyLinked": "{name} is already a subclass in the class {class}. Remove it from there if you want it to be a subclass to this class.", - "gmRequired": "This action requires an online GM" + "gmRequired": "This action requires an online GM", + "gmOnly": "This can only be accessed by the GM", + "noActorOwnership": "You do not have permissions for this character" }, "Sidebar": { "daggerheartMenu": { @@ -2646,7 +2689,8 @@ "remainingUses": "Uses refresh on {type}", "rightClickExtand": "Right-Click to extand", "companionPartnerLevelBlock": "The companion needs an assigned partner to level up.", - "configureAttribution": "Configure Attribution" + "configureAttribution": "Configure Attribution", + "deleteItem": "Delete Item" } } } diff --git a/module/applications/dialogs/_module.mjs b/module/applications/dialogs/_module.mjs index b8e764c9..43faa68a 100644 --- a/module/applications/dialogs/_module.mjs +++ b/module/applications/dialogs/_module.mjs @@ -11,3 +11,5 @@ export { default as OwnershipSelection } from './ownershipSelection.mjs'; export { default as RerollDamageDialog } from './rerollDamageDialog.mjs'; export { default as ResourceDiceDialog } from './resourceDiceDialog.mjs'; export { default as ActionSelectionDialog } from './actionSelectionDialog.mjs'; +export { default as GroupRollDialog } from './group-roll-dialog.mjs'; +export { default as TagTeamDialog } from './tagTeamDialog.mjs'; diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index c57dda12..2534a2b8 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -34,6 +34,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio updateIsAdvantage: this.updateIsAdvantage, selectExperience: this.selectExperience, toggleReaction: this.toggleReaction, + toggleTagTeamRoll: this.toggleTagTeamRoll, submitRoll: this.submitRoll }, form: { @@ -120,6 +121,13 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio context.showReaction = !this.config.roll?.type && context.rollType === 'DualityRoll'; context.reactionOverride = this.reactionOverride; } + + const tagTeamSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); + if (tagTeamSetting.members[this.actor.id] && !this.config.skips?.createMessage) { + context.activeTagTeamRoll = true; + context.tagTeamSelected = this.config.tagTeamSelected; + } + 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() { await this.close({ submitted: true }); } diff --git a/module/applications/dialogs/group-roll-dialog.mjs b/module/applications/dialogs/group-roll-dialog.mjs new file mode 100644 index 00000000..2cb79563 --- /dev/null +++ b/module/applications/dialogs/group-roll-dialog.mjs @@ -0,0 +1,196 @@ +import autocomplete from 'autocompleter'; +import { abilities } from '../../config/actorConfig.mjs'; + +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +export default class GroupRollDialog extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(actors) { + super(); + this.actors = actors; + this.actorLeader = {}; + this.actorsMembers = []; + } + + get title() { + return 'Group Roll'; + } + + static DEFAULT_OPTIONS = { + tag: 'form', + classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'group-roll'], + position: { width: 'auto', height: 'auto' }, + window: { + title: 'DAGGERHEART.UI.Chat.groupRoll.title' + }, + actions: { + roll: GroupRollDialog.#roll, + removeLeader: GroupRollDialog.#removeLeader, + removeMember: GroupRollDialog.#removeMember + }, + form: { handler: this.updateData, submitOnChange: true, closeOnSubmit: false } + }; + + static PARTS = { + application: { + id: 'group-roll', + template: 'systems/daggerheart/templates/dialogs/group-roll/group-roll.hbs' + } + }; + + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + const leaderChoices = this.actors.filter(x => this.actorsMembers.every(member => member.actor?.id !== x.id)); + const memberChoices = this.actors.filter( + x => this.actorLeader?.actor?.id !== x.id && this.actorsMembers.every(member => member.actor?.id !== x.id) + ); + + htmlElement.querySelectorAll('.leader-change-input').forEach(element => { + autocomplete({ + input: element, + fetch: function (text, update) { + if (!text) { + update(leaderChoices); + } else { + text = text.toLowerCase(); + var suggestions = leaderChoices.filter(n => n.name.toLowerCase().includes(text)); + update(suggestions); + } + }, + render: function (actor, search) { + const actorName = game.i18n.localize(actor.name); + const matchIndex = actorName.toLowerCase().indexOf(search); + + const beforeText = actorName.slice(0, matchIndex); + const matchText = actorName.slice(matchIndex, matchIndex + search.length); + const after = actorName.slice(matchIndex + search.length, actorName.length); + const img = document.createElement('img'); + img.src = actor.img; + + const element = document.createElement('li'); + element.appendChild(img); + + const label = document.createElement('span'); + label.innerHTML = `${beforeText}${matchText ? `${matchText}` : ''}${after}`; + element.appendChild(label); + + return element; + }, + renderGroup: function (label) { + const itemElement = document.createElement('div'); + itemElement.textContent = game.i18n.localize(label); + return itemElement; + }, + onSelect: actor => { + element.value = actor.uuid; + this.actorLeader = { actor: actor, trait: 'agility', difficulty: 0 }; + this.render(); + }, + click: e => e.fetch(), + customize: function (_input, _inputRect, container) { + container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ; + }, + minLength: 0 + }); + }); + + htmlElement.querySelectorAll('.team-push-input').forEach(element => { + autocomplete({ + input: element, + fetch: function (text, update) { + if (!text) { + update(memberChoices); + } else { + text = text.toLowerCase(); + var suggestions = memberChoices.filter(n => n.name.toLowerCase().includes(text)); + update(suggestions); + } + }, + render: function (actor, search) { + const actorName = game.i18n.localize(actor.name); + const matchIndex = actorName.toLowerCase().indexOf(search); + + const beforeText = actorName.slice(0, matchIndex); + const matchText = actorName.slice(matchIndex, matchIndex + search.length); + const after = actorName.slice(matchIndex + search.length, actorName.length); + const img = document.createElement('img'); + img.src = actor.img; + + const element = document.createElement('li'); + element.appendChild(img); + + const label = document.createElement('span'); + label.innerHTML = `${beforeText}${matchText ? `${matchText}` : ''}${after}`; + element.appendChild(label); + + return element; + }, + renderGroup: function (label) { + const itemElement = document.createElement('div'); + itemElement.textContent = game.i18n.localize(label); + return itemElement; + }, + onSelect: actor => { + element.value = actor.uuid; + this.actorsMembers.push({ actor: actor, trait: 'agility', difficulty: 0 }); + this.render({ force: true }); + }, + click: e => e.fetch(), + customize: function (_input, _inputRect, container) { + container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ; + }, + minLength: 0 + }); + }); + } + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.leader = this.actorLeader; + context.members = this.actorsMembers; + context.traitList = abilities; + + context.allSelected = this.actorsMembers.length + (this.actorLeader?.actor ? 1 : 0) === this.actors.length; + context.rollDisabled = context.members.length === 0 || !this.actorLeader?.actor; + + return context; + } + + static updateData(event, _, formData) { + const { actorLeader, actorsMembers } = foundry.utils.expandObject(formData.object); + this.actorLeader = foundry.utils.mergeObject(this.actorLeader, actorLeader); + this.actorsMembers = foundry.utils.mergeObject(this.actorsMembers, actorsMembers); + this.render(true); + } + + static async #removeLeader(_, button) { + this.actorLeader = null; + this.render(); + } + + static async #removeMember(_, button) { + this.actorsMembers = this.actorsMembers.filter(m => m.actor.uuid !== button.dataset.memberUuid); + this.render(); + } + + static async #roll() { + const cls = getDocumentClass('ChatMessage'); + const systemData = { + leader: this.actorLeader, + members: this.actorsMembers + }; + const msg = { + type: 'groupRoll', + user: game.user.id, + speaker: cls.getSpeaker(), + title: game.i18n.localize('DAGGERHEART.UI.Chat.groupRoll.title'), + system: systemData, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/ui/chat/groupRoll.hbs', + { system: systemData } + ) + }; + + cls.create(msg); + this.close(); + } +} diff --git a/module/applications/dialogs/rerollDamageDialog.mjs b/module/applications/dialogs/rerollDamageDialog.mjs index 0c2ea0e1..e1b75eb7 100644 --- a/module/applications/dialogs/rerollDamageDialog.mjs +++ b/module/applications/dialogs/rerollDamageDialog.mjs @@ -1,3 +1,5 @@ +import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; + const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) { @@ -122,6 +124,15 @@ export default class RerollDamageDialog extends HandlebarsApplicationMixin(Appli }, {}) }; 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(); } diff --git a/module/applications/dialogs/tagTeamDialog.mjs b/module/applications/dialogs/tagTeamDialog.mjs new file mode 100644 index 00000000..e7290f1c --- /dev/null +++ b/module/applications/dialogs/tagTeamDialog.mjs @@ -0,0 +1,315 @@ +import { GMUpdateEvent, 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); + + 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 } + } + }); + } + } + + 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, + 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 = []; + 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 */ + 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); + } +} diff --git a/module/applications/hud/tokenHUD.mjs b/module/applications/hud/tokenHUD.mjs index 48d5ac89..030eca59 100644 --- a/module/applications/hud/tokenHUD.mjs +++ b/module/applications/hud/tokenHUD.mjs @@ -1,8 +1,11 @@ +import { shuffleArray } from '../../helpers/utils.mjs'; + export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { static DEFAULT_OPTIONS = { classes: ['daggerheart'], actions: { - combat: DHTokenHUD.#onToggleCombat + combat: DHTokenHUD.#onToggleCombat, + togglePartyTokens: DHTokenHUD.#togglePartyTokens } }; @@ -19,6 +22,12 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { async _prepareContext(options) { const context = await super._prepareContext(options); + context.partyOnCanvas = + this.actor.type === 'party' && + this.actor.system.partyMembers.some(member => member.getActiveTokens().length > 0); + context.icons.toggleParty = 'systems/daggerheart/assets/icons/arrow-dunk.png'; + context.actorType = this.actor.type; + context.usesEffects = this.actor.type !== 'party'; context.canToggleCombat = DHTokenHUD.#nonCombatTypes.includes(this.actor.type) ? false : context.canToggleCombat; @@ -59,6 +68,105 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD { } } + static async #togglePartyTokens(_, button) { + const icon = button.querySelector('img'); + icon.classList.toggle('flipped'); + button.dataset.tooltip = game.i18n.localize( + icon.classList.contains('flipped') + ? 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.retrievePartyTokens' + : 'DAGGERHEART.APPLICATIONS.HUD.tokenHUD.depositPartyTokens' + ); + + const animationDuration = 500; + const activeTokens = this.actor.system.partyMembers.flatMap(member => member.getActiveTokens()); + const { x: actorX, y: actorY } = this.document; + if (activeTokens.length > 0) { + for (let token of activeTokens) { + await token.document.update( + { x: actorX, y: actorY, alpha: 0 }, + { animation: { duration: animationDuration } } + ); + setTimeout(() => token.document.delete(), animationDuration); + } + } else { + const activeScene = game.scenes.find(x => x.active); + const partyTokenData = []; + for (let member of this.actor.system.partyMembers) { + const data = await member.getTokenDocument(); + partyTokenData.push(data.toObject()); + } + const newTokens = await activeScene.createEmbeddedDocuments( + 'Token', + partyTokenData.map(tokenData => ({ + ...tokenData, + alpha: 0, + x: actorX, + y: actorY + })) + ); + + const { sizeX, sizeY } = activeScene.grid; + const nrRandomPositions = Math.ceil(newTokens.length / 8) * 8; + /* This is an overcomplicated mess, but I'm stupid */ + const positions = shuffleArray( + [...Array(nrRandomPositions).keys()].map((_, index) => { + const nonZeroIndex = index + 1; + const indexFloor = Math.floor(index / 8); + const distanceCoefficient = indexFloor + 1; + const side = 3 + indexFloor * 2; + const sideMiddle = Math.ceil(side / 2); + const inbetween = 1 + indexFloor * 2; + const inbetweenMiddle = Math.ceil(inbetween / 2); + + if (index < side) { + const distance = + nonZeroIndex === sideMiddle + ? 0 + : nonZeroIndex < sideMiddle + ? -nonZeroIndex + : nonZeroIndex - sideMiddle; + return { x: actorX - sizeX * distance, y: actorY - sizeY * distanceCoefficient }; + } else if (index < side + inbetween) { + const inbetweenIndex = nonZeroIndex - side; + const distance = + inbetweenIndex === inbetweenMiddle + ? 0 + : inbetweenIndex < inbetweenMiddle + ? -inbetweenIndex + : inbetweenIndex - inbetweenMiddle; + return { x: actorX + sizeX * distanceCoefficient, y: actorY + sizeY * distance }; + } else if (index < 2 * side + inbetween) { + const sideIndex = nonZeroIndex - side - inbetween; + const distance = + sideIndex === sideMiddle + ? 0 + : sideIndex < sideMiddle + ? sideIndex + : -(sideIndex - sideMiddle); + return { x: actorX + sizeX * distance, y: actorY + sizeY * distanceCoefficient }; + } else { + const inbetweenIndex = nonZeroIndex - 2 * side - inbetween; + const distance = + inbetweenIndex === inbetweenMiddle + ? 0 + : inbetweenIndex < inbetweenMiddle + ? inbetweenIndex + : -(inbetweenIndex - inbetweenMiddle); + return { x: actorX - sizeX * distanceCoefficient, y: actorY + sizeY * distance }; + } + }) + ); + + for (let token of newTokens) { + const position = positions.pop(); + token.update( + { x: position.x, y: position.y, alpha: 1 }, + { animation: { duration: animationDuration } } + ); + } + } + } + _getStatusEffectChoices() { // Include all HUD-enabled status effects const choices = {}; diff --git a/module/applications/sheets/actors/_module.mjs b/module/applications/sheets/actors/_module.mjs index 9998733c..c4ea2d94 100644 --- a/module/applications/sheets/actors/_module.mjs +++ b/module/applications/sheets/actors/_module.mjs @@ -2,3 +2,4 @@ export { default as Adversary } from './adversary.mjs'; export { default as Character } from './character.mjs'; export { default as Companion } from './companion.mjs'; export { default as Environment } from './environment.mjs'; +export { default as Party } from './party.mjs'; diff --git a/module/applications/sheets/actors/adversary.mjs b/module/applications/sheets/actors/adversary.mjs index 6b57565c..95d77787 100644 --- a/module/applications/sheets/actors/adversary.mjs +++ b/module/applications/sheets/actors/adversary.mjs @@ -10,6 +10,8 @@ export default class AdversarySheet extends DHBaseActorSheet { position: { width: 660, height: 766 }, window: { resizable: true }, actions: { + toggleHitPoints: AdversarySheet.#toggleHitPoints, + toggleStress: AdversarySheet.#toggleStress, reactionRoll: AdversarySheet.#reactionRoll, toggleResourceDice: AdversarySheet.#toggleResourceDice, handleResourceDice: AdversarySheet.#handleResourceDice @@ -75,6 +77,16 @@ export default class AdversarySheet extends DHBaseActorSheet { const context = await super._prepareContext(options); context.systemFields.attack.fields = this.document.system.attack.schema.fields; + context.resources = Object.keys(this.document.system.resources).reduce((acc, key) => { + acc[key] = this.document.system.resources[key]; + return acc; + }, {}); + const maxResource = Math.max(context.resources.hitPoints.max, context.resources.stress.max); + context.resources.hitPoints.emptyPips = + context.resources.hitPoints.max < maxResource ? maxResource - context.resources.hitPoints.max : 0; + context.resources.stress.emptyPips = + context.resources.stress.max < maxResource ? maxResource - context.resources.stress.max : 0; + return context; } @@ -155,6 +167,27 @@ export default class AdversarySheet extends DHBaseActorSheet { /* Application Clicks Actions */ /* -------------------------------------------- */ + /** + * Toggles hitpoint resource value. + * @type {ApplicationClickAction} + */ + static async #toggleHitPoints(_, button) { + const hitPointsValue = Number.parseInt(button.dataset.value); + const newValue = + this.document.system.resources.hitPoints.value >= hitPointsValue ? hitPointsValue - 1 : hitPointsValue; + await this.document.update({ 'system.resources.hitPoints.value': newValue }); + } + + /** + * Toggles stress resource value. + * @type {ApplicationClickAction} + */ + static async #toggleStress(_, button) { + const StressValue = Number.parseInt(button.dataset.value); + const newValue = this.document.system.resources.stress.value >= StressValue ? StressValue - 1 : StressValue; + await this.document.update({ 'system.resources.stress.value': newValue }); + } + /** * Performs a reaction roll for an Adversary. * @type {ApplicationClickAction} diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 227a1a39..79fa9893 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -19,6 +19,9 @@ export default class CharacterSheet extends DHBaseActorSheet { actions: { toggleVault: CharacterSheet.#toggleVault, rollAttribute: CharacterSheet.#rollAttribute, + toggleHitPoints: CharacterSheet.#toggleHitPoints, + toggleStress: CharacterSheet.#toggleStress, + toggleArmor: CharacterSheet.#toggleArmor, toggleHope: CharacterSheet.#toggleHope, toggleLoadoutView: CharacterSheet.#toggleLoadoutView, openPack: CharacterSheet.#openPack, @@ -196,6 +199,16 @@ export default class CharacterSheet extends DHBaseActorSheet { return acc; }, {}); + context.resources = Object.keys(this.document.system.resources).reduce((acc, key) => { + acc[key] = this.document.system.resources[key]; + return acc; + }, {}); + const maxResource = Math.max(context.resources.hitPoints.max, context.resources.stress.max); + context.resources.hitPoints.emptyPips = + context.resources.hitPoints.max < maxResource ? maxResource - context.resources.hitPoints.max : 0; + context.resources.stress.emptyPips = + context.resources.stress.max < maxResource ? maxResource - context.resources.stress.max : 0; + context.inventory = { currency: { title: game.i18n.localize('DAGGERHEART.CONFIG.Gold.title'), @@ -746,6 +759,37 @@ export default class CharacterSheet extends DHBaseActorSheet { this.render(); } + /** + * Toggles hitpoint resource value. + * @type {ApplicationClickAction} + */ + static async #toggleHitPoints(_, button) { + const hitPointsValue = Number.parseInt(button.dataset.value); + const newValue = + this.document.system.resources.hitPoints.value >= hitPointsValue ? hitPointsValue - 1 : hitPointsValue; + await this.document.update({ 'system.resources.hitPoints.value': newValue }); + } + + /** + * Toggles stress resource value. + * @type {ApplicationClickAction} + */ + static async #toggleStress(_, button) { + const StressValue = Number.parseInt(button.dataset.value); + const newValue = this.document.system.resources.stress.value >= StressValue ? StressValue - 1 : StressValue; + await this.document.update({ 'system.resources.stress.value': newValue }); + } + + /** + * Toggles ArmorScore resource value. + * @type {ApplicationClickAction} + */ + static async #toggleArmor(_, button, element) { + const ArmorValue = Number.parseInt(button.dataset.value); + const newValue = this.document.system.armor.system.marks.value >= ArmorValue ? ArmorValue - 1 : ArmorValue; + await this.document.system.armor.update({ 'system.marks.value': newValue }); + } + /** * Toggles a hope resource value. * @type {ApplicationClickAction} diff --git a/module/applications/sheets/actors/companion.mjs b/module/applications/sheets/actors/companion.mjs index fd8cddbf..9b85f622 100644 --- a/module/applications/sheets/actors/companion.mjs +++ b/module/applications/sheets/actors/companion.mjs @@ -8,6 +8,7 @@ export default class DhCompanionSheet extends DHBaseActorSheet { classes: ['actor', 'companion'], position: { width: 340 }, actions: { + toggleStress: DhCompanionSheet.#toggleStress, actionRoll: DhCompanionSheet.#actionRoll, levelManagement: DhCompanionSheet.#levelManagement } @@ -50,6 +51,16 @@ export default class DhCompanionSheet extends DHBaseActorSheet { /* Application Clicks Actions */ /* -------------------------------------------- */ + /** + * Toggles stress resource value. + * @type {ApplicationClickAction} + */ + static async #toggleStress(_, button) { + const StressValue = Number.parseInt(button.dataset.value); + const newValue = this.document.system.resources.stress.value >= StressValue ? StressValue - 1 : StressValue; + await this.document.update({ 'system.resources.stress.value': newValue }); + } + /** * */ diff --git a/module/applications/sheets/actors/environment.mjs b/module/applications/sheets/actors/environment.mjs index 30355ccc..e5630ad6 100644 --- a/module/applications/sheets/actors/environment.mjs +++ b/module/applications/sheets/actors/environment.mjs @@ -143,7 +143,6 @@ export default class DhpEnvironment extends DHBaseActorSheet { /* Application Clicks Actions */ /* -------------------------------------------- */ - /** * Toggle the used state of a resource dice. * @type {ApplicationClickAction} @@ -177,5 +176,4 @@ export default class DhpEnvironment extends DHBaseActorSheet { }, {}) }); } - } diff --git a/module/applications/sheets/actors/party.mjs b/module/applications/sheets/actors/party.mjs new file mode 100644 index 00000000..a622dcec --- /dev/null +++ b/module/applications/sheets/actors/party.mjs @@ -0,0 +1,512 @@ +import DHBaseActorSheet from '../api/base-actor.mjs'; +import { getDocFromElement } from '../../../helpers/utils.mjs'; +import { ItemBrowser } from '../../ui/itemBrowser.mjs'; +import FilterMenu from '../../ux/filter-menu.mjs'; +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) { + super(options); + + this.refreshSelections = DaggerheartMenu.defaultRefreshSelections(); + } + + /**@inheritdoc */ + static DEFAULT_OPTIONS = { + classes: ['party'], + position: { + width: 550 + }, + window: { + resizable: true + }, + actions: { + deletePartyMember: Party.#deletePartyMember, + deleteItem: Party.#deleteItem, + toggleHope: Party.#toggleHope, + toggleHitPoints: Party.#toggleHitPoints, + toggleStress: Party.#toggleStress, + toggleArmorSlot: Party.#toggleArmorSlot, + tempBrowser: Party.#tempBrowser, + refeshActions: Party.#refeshActions, + triggerRest: Party.#triggerRest, + tagTeamRoll: Party.#tagTeamRoll, + groupRoll: Party.#groupRoll, + selectRefreshable: DaggerheartMenu.selectRefreshable, + refreshActors: DaggerheartMenu.refreshActors + }, + dragDrop: [{ dragSelector: '.actors-section .inventory-item', dropSelector: null }] + }; + + /**@override */ + static PARTS = { + header: { template: 'systems/daggerheart/templates/sheets/actors/party/header.hbs' }, + tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, + partyMembers: { template: 'systems/daggerheart/templates/sheets/actors/party/party-members.hbs' }, + resources: { + template: 'systems/daggerheart/templates/sheets/actors/party/resources.hbs', + scrollable: [''] + }, + /* NOT YET IMPLEMENTED */ + // projects: { + // template: 'systems/daggerheart/templates/sheets/actors/party/projects.hbs', + // scrollable: [''] + // }, + inventory: { + template: 'systems/daggerheart/templates/sheets/actors/party/inventory.hbs', + scrollable: ['.tab.inventory .items-section'] + }, + notes: { template: 'systems/daggerheart/templates/sheets/actors/party/notes.hbs' } + }; + + /** @inheritdoc */ + static TABS = { + primary: { + tabs: [ + { id: 'partyMembers' }, + { id: 'resources' }, + /* NOT YET IMPLEMENTED */ + // { id: 'projects' }, + { id: 'inventory' }, + { id: 'notes' } + ], + initial: 'partyMembers', + labelPrefix: 'DAGGERHEART.GENERAL.Tabs' + } + }; + + async _onRender(context, options) { + await super._onRender(context, options); + this._createFilterMenus(); + this._createSearchFilter(); + } + + /* -------------------------------------------- */ + /* Prepare Context */ + /* -------------------------------------------- */ + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + + context.inventory = { + currency: { + title: game.i18n.localize('DAGGERHEART.CONFIG.Gold.title'), + coins: game.i18n.localize('DAGGERHEART.CONFIG.Gold.coins'), + handfuls: game.i18n.localize('DAGGERHEART.CONFIG.Gold.handfuls'), + 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) { + context.inventory.currency = homebrewCurrency; + } + + if (context.inventory.length === 0) { + context.inventory = Array(1).fill(Array(5).fill([])); + } + + return context; + } + + async _preparePartContext(partId, context, options) { + context = await super._preparePartContext(partId, context, options); + switch (partId) { + case 'header': + await this._prepareHeaderContext(context, options); + break; + case 'notes': + await this._prepareNotesContext(context, options); + break; + } + return context; + } + + /** + * Prepare render context for the Header part. + * @param {ApplicationRenderContext} context + * @param {ApplicationRenderOptions} options + * @returns {Promise} + * @protected + */ + async _prepareHeaderContext(context, _options) { + const { system } = this.document; + const { TextEditor } = foundry.applications.ux; + + context.description = await TextEditor.implementation.enrichHTML(system.description, { + secrets: this.document.isOwner, + relativeTo: this.document + }); + } + + /** + * Prepare render context for the Biography part. + * @param {ApplicationRenderContext} context + * @param {ApplicationRenderOptions} options + * @returns {Promise} + * @protected + */ + async _prepareNotesContext(context, _options) { + const { system } = this.document; + const { TextEditor } = foundry.applications.ux; + + const paths = { + notes: 'notes' + }; + + for (const [key, path] of Object.entries(paths)) { + const value = foundry.utils.getProperty(system, path); + context[key] = { + field: system.schema.getField(path), + value, + enriched: await TextEditor.implementation.enrichHTML(value, { + secrets: this.document.isOwner, + relativeTo: this.document + }) + }; + } + } + + /** + * Toggles a hope resource value. + * @type {ApplicationClickAction} + */ + static async #toggleHope(_, target) { + const hopeValue = Number.parseInt(target.dataset.value); + const actor = await foundry.utils.fromUuid(target.dataset.actorId); + const newValue = actor.system.resources.hope.value >= hopeValue ? hopeValue - 1 : hopeValue; + await actor.update({ 'system.resources.hope.value': newValue }); + this.render(); + } + + /** + * Toggles a hp resource value. + * @type {ApplicationClickAction} + */ + static async #toggleHitPoints(_, target) { + const hitPointsValue = Number.parseInt(target.dataset.value); + const actor = await foundry.utils.fromUuid(target.dataset.actorId); + const newValue = actor.system.resources.hitPoints.value >= hitPointsValue ? hitPointsValue - 1 : hitPointsValue; + await actor.update({ 'system.resources.hitPoints.value': newValue }); + this.render(); + } + + /** + * Toggles a stress resource value. + * @type {ApplicationClickAction} + */ + static async #toggleStress(_, target) { + const stressValue = Number.parseInt(target.dataset.value); + const actor = await foundry.utils.fromUuid(target.dataset.actorId); + const newValue = actor.system.resources.stress.value >= stressValue ? stressValue - 1 : stressValue; + await actor.update({ 'system.resources.stress.value': newValue }); + this.render(); + } + + /** + * Toggles a armor slot resource value. + * @type {ApplicationClickAction} + */ + static async #toggleArmorSlot(_, target, element) { + const armorItem = await foundry.utils.fromUuid(target.dataset.itemUuid); + const armorValue = Number.parseInt(target.dataset.value); + const newValue = armorItem.system.marks.value >= armorValue ? armorValue - 1 : armorValue; + await armorItem.update({ 'system.marks.value': newValue }); + this.render(); + } + + /** + * Opens Compedium Browser + */ + static async #tempBrowser(_, target) { + new ItemBrowser().render({ force: true }); + } + + static async #refeshActions() { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: 'New Section', + icon: 'fa-solid fa-campground' + }, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/sidebar/daggerheart-menu/main.hbs', + { + refreshables: DaggerheartMenu.defaultRefreshSelections() + } + ), + classes: ['daggerheart', 'dialog', 'dh-style', 'tab', 'sidebar-tab', 'daggerheartMenu-sidebar'] + }); + + if (!confirmed) return; + } + + static async #triggerRest(_, button) { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + 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'] + }); + + if (!confirmed) return; + + this.document.system.partyMembers.forEach(actor => { + game.socket.emit(`system.${CONFIG.DH.id}`, { + action: socketEvent.DowntimeTrigger, + data: { + actorId: actor.uuid, + downtimeType: button.dataset.type + } + }); + }); + } + + static async downtimeMoveQuery({ actorId, downtimeType }) { + const actor = await foundry.utils.fromUuid(actorId); + if (!actor || !actor?.isOwner) reject(); + new game.system.api.applications.dialogs.Downtime(actor, downtimeType === 'shortRest').render({ + force: true + }); + } + + static async #tagTeamRoll() { + new game.system.api.applications.dialogs.TagTeamDialog(this.document.system.partyMembers).render({ + force: true + }); + } + + static async #groupRoll(params) { + new GroupRollDialog(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} + * @protected + */ + static #getItemContextOptions() { + return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true }); + } + /* -------------------------------------------- */ + /* Filter Tracking */ + /* -------------------------------------------- */ + + /** + * The currently active search filter. + * @type {foundry.applications.ux.SearchFilter} + */ + #search = {}; + + /** + * The currently active search filter. + * @type {FilterMenu} + */ + #menu = {}; + + /** + * Tracks which item IDs are currently displayed, organized by filter type and section. + * @type {{ + * inventory: { + * search: Set, + * menu: Set + * }, + * loadout: { + * search: Set, + * menu: Set + * }, + * }} + */ + #filteredItems = { + inventory: { + search: new Set(), + menu: new Set() + }, + loadout: { + search: new Set(), + menu: new Set() + } + }; + + /* -------------------------------------------- */ + /* Search Inputs */ + /* -------------------------------------------- */ + + /** + * 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. + * - One for the loadout, which filters items in the loadout/card grid. + * @private + */ + _createSearchFilter() { + //Filters could be a application option if needed + const filters = [ + { + key: 'inventory', + input: 'input[type="search"].search-inventory', + content: '[data-application-part="inventory"] .items-section', + callback: this._onSearchFilterInventory.bind(this) + } + ]; + + for (const { key, input, content, callback } of filters) { + const filter = new foundry.applications.ux.SearchFilter({ + inputSelector: input, + contentSelector: content, + callback + }); + filter.bind(this.element); + this.#search[key] = filter; + } + } + + /** + * Handle invetory items search and filtering. + * @param {KeyboardEvent} event The keyboard input event. + * @param {string} query The input search string. + * @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) { + 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); + if (matchesSearch) this.#filteredItems.inventory.search.add(item.id); + const { menu } = this.#filteredItems.inventory; + li.hidden = !(menu.has(item.id) && matchesSearch); + } + } + + /* -------------------------------------------- */ + /* Filter Menus */ + /* -------------------------------------------- */ + + _createFilterMenus() { + //Menus could be a application option if needed + const menus = [ + { + key: 'inventory', + container: '[data-application-part="inventory"]', + content: '.items-section', + callback: this._onMenuFilterInventory.bind(this), + target: '.filter-button', + filters: FilterMenu.invetoryFilters + } + ]; + + menus.forEach(m => { + const container = this.element.querySelector(m.container); + this.#menu[m.key] = new FilterMenu(container, m.target, m.filters, m.callback, { + contentSelector: m.content + }); + }); + } + + /** + * Callback when filters change + * @param {PointerEvent} event + * @param {HTMLElement} html + * @param {import('../ux/filter-menu.mjs').FilterItem[]} filters + */ + async _onMenuFilterInventory(_event, html, filters) { + this.#filteredItems.inventory.menu.clear(); + + for (const li of html.querySelectorAll('.inventory-item')) { + 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); + + const { search } = this.#filteredItems.inventory; + li.hidden = !(search.has(item.id) && matchesMenu); + } + } + + /* -------------------------------------------- */ + + async _onDragStart(event) { + const item = event.currentTarget.closest('.inventory-item'); + + 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 _onDrop(event) { + // Prevent event bubbling to avoid duplicate handling + event.preventDefault(); + event.stopPropagation(); + + const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); + const item = await foundry.utils.fromUuid(data.uuid); + + if (item instanceof DhpActor) { + 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')); + } + + await this.document.update({ 'system.partyMembers': [...currentMembers, item.uuid] }); + } else if (item instanceof DHItem) { + this.document.createEmbeddedDocuments('Item', [item.toObject()]); + } else { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.onlyCharactersInPartySheet')); + } + } + + static async #deletePartyMember(event, target) { + const doc = await getDocFromElement(target.closest('.inventory-item')); + + if (!event.shiftKey) { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { + type: game.i18n.localize('TYPES.Actor.adversary'), + name: doc.name + }) + }, + 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 }); + } + + static async #deleteItem(event, target) { + const doc = await getDocFromElement(target.closest('.inventory-item')); + if (!event.shiftKey) { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { + type: game.i18n.localize('TYPES.Actor.party'), + name: doc.name + }) + }, + content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: doc.name }) + }); + + if (!confirmed) return; + } + + this.document.deleteEmbeddedDocuments('Item', [doc.id]); + } +} diff --git a/module/applications/sheets/api/base-actor.mjs b/module/applications/sheets/api/base-actor.mjs index 273a3c67..e1226416 100644 --- a/module/applications/sheets/api/base-actor.mjs +++ b/module/applications/sheets/api/base-actor.mjs @@ -61,6 +61,10 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { async _prepareContext(_options) { const context = await super._prepareContext(_options); context.isNPC = this.document.isNPC; + context.useResourcePips = game.settings.get( + CONFIG.DH.id, + CONFIG.DH.SETTINGS.gameSettings.appearance + ).useResourcePips; context.showAttribution = !game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance) .hideAttribution; diff --git a/module/applications/sidebar/tabs/daggerheartMenu.mjs b/module/applications/sidebar/tabs/daggerheartMenu.mjs index cf7aeae3..0f98f5a0 100644 --- a/module/applications/sidebar/tabs/daggerheartMenu.mjs +++ b/module/applications/sidebar/tabs/daggerheartMenu.mjs @@ -9,10 +9,10 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract constructor(options) { super(options); - this.refreshSelections = DaggerheartMenu.#defaultRefreshSelections(); + this.refreshSelections = DaggerheartMenu.defaultRefreshSelections(); } - static #defaultRefreshSelections() { + static defaultRefreshSelections() { return { session: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.session') }, scene: { selected: false, label: game.i18n.localize('DAGGERHEART.GENERAL.RefreshType.scene') }, @@ -138,7 +138,7 @@ export default class DaggerheartMenu extends HandlebarsApplicationMixin(Abstract types: `[${types}]` }) ); - this.refreshSelections = DaggerheartMenu.#defaultRefreshSelections(); + this.refreshSelections = DaggerheartMenu.defaultRefreshSelections(); const cls = getDocumentClass('ChatMessage'); const msg = { diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index b95e50e1..6b05fe74 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -1,3 +1,6 @@ +import { abilities } from '../../config/actorConfig.mjs'; +import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; + export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLog { constructor(options) { super(options); @@ -35,7 +38,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo // } // }, { - name: 'Reroll Damage', + name: game.i18n.localize('DAGGERHEART.UI.ChatLog.rerollDamage'), icon: '', condition: li => { const message = game.messages.get(li.dataset.messageId); @@ -65,6 +68,18 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo html.querySelectorAll('.reroll-button').forEach(element => element.addEventListener('click', event => this.rerollEvent(event, data.message)) ); + html.querySelectorAll('.group-roll-button').forEach(element => + element.addEventListener('click', event => this.groupRollButton(event, data.message)) + ); + html.querySelectorAll('.group-roll-reroll').forEach(element => + element.addEventListener('click', event => this.groupRollReroll(event, data.message)) + ); + html.querySelectorAll('.group-roll-success').forEach(element => + element.addEventListener('click', event => this.groupRollSuccessEvent(event, data.message)) + ); + html.querySelectorAll('.group-roll-header-expand-section').forEach(element => + element.addEventListener('click', this.groupRollExpandSection) + ); }; setupHooks() { @@ -164,6 +179,169 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo 'system.roll': newRoll, 'rolls': [parsedRoll] }); + + Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll }); + await game.socket.emit(`system.${CONFIG.DH.id}`, { + action: socketEvent.Refresh, + data: { + refreshType: RefreshType.TagTeamRoll + } + }); } } + + async groupRollButton(event, message) { + const path = event.currentTarget.dataset.path; + const { actor: actorData, trait } = foundry.utils.getProperty(message.system, path); + const actor = game.actors.get(actorData._id); + + if (!actor.testUserPermission(game.user, 'OWNER')) { + return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.noActorOwnership')); + } + + const traitLabel = game.i18n.localize(abilities[trait].label); + const config = { + event: event, + title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`, + headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { + ability: traitLabel + }), + roll: { + trait: trait, + advantage: 0, + modifiers: [{ label: traitLabel, value: actor.system.traits[trait].value }] + }, + hasRoll: true, + skips: { + createMessage: true, + resources: true + } + }; + const result = await actor.diceRoll({ + ...config, + headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`, + title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { + ability: traitLabel + }) + }); + + const newMessageData = foundry.utils.deepClone(message.system); + foundry.utils.setProperty(newMessageData, `${path}.result`, result.roll); + const renderData = { system: new game.system.api.models.chatMessages.config.groupRoll(newMessageData) }; + + const updatedContent = await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/ui/chat/groupRoll.hbs', + { ...renderData, user: game.user } + ); + const mess = game.messages.get(message._id); + + await emitAsGM( + GMUpdateEvent.UpdateDocument, + mess.update.bind(mess), + { + ...renderData, + content: updatedContent + }, + mess.uuid + ); + } + + async groupRollReroll(event, message) { + const path = event.currentTarget.dataset.path; + const { actor: actorData, trait } = foundry.utils.getProperty(message.system, path); + const actor = game.actors.get(actorData._id); + + if (!actor.testUserPermission(game.user, 'OWNER')) { + return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.noActorOwnership')); + } + + const traitLabel = game.i18n.localize(abilities[trait].label); + + const config = { + event: event, + title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`, + headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { + ability: traitLabel + }), + roll: { + trait: trait, + advantage: 0, + modifiers: [{ label: traitLabel, value: actor.system.traits[trait].value }] + }, + hasRoll: true, + skips: { + createMessage: true + } + }; + const result = await actor.diceRoll({ + ...config, + headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${actor.name}`, + title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { + ability: traitLabel + }) + }); + + const newMessageData = foundry.utils.deepClone(message.system); + foundry.utils.setProperty(newMessageData, `${path}.result`, { ...result.roll, rerolled: true }); + const renderData = { system: new game.system.api.models.chatMessages.config.groupRoll(newMessageData) }; + + const updatedContent = await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/ui/chat/groupRoll.hbs', + { ...renderData, user: game.user } + ); + const mess = game.messages.get(message._id); + await emitAsGM( + GMUpdateEvent.UpdateDocument, + mess.update.bind(mess), + { + ...renderData, + content: updatedContent + }, + mess.uuid + ); + } + + async groupRollSuccessEvent(event, message) { + if (!game.user.isGM) { + return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.gmOnly')); + } + + const { path, success } = event.currentTarget.dataset; + const { actor: actorData } = foundry.utils.getProperty(message.system, path); + const actor = game.actors.get(actorData._id); + + if (!actor.testUserPermission(game.user, 'OWNER')) { + return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.noActorOwnership')); + } + + const newMessageData = foundry.utils.deepClone(message.system); + foundry.utils.setProperty(newMessageData, `${path}.manualSuccess`, Boolean(success)); + const renderData = { system: new game.system.api.models.chatMessages.config.groupRoll(newMessageData) }; + + const updatedContent = await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/ui/chat/groupRoll.hbs', + { ...renderData, user: game.user } + ); + const mess = game.messages.get(message._id); + await emitAsGM( + GMUpdateEvent.UpdateDocument, + mess.update.bind(mess), + { + ...renderData, + content: updatedContent + }, + mess.uuid + ); + } + + async groupRollExpandSection(event) { + event.target + .closest('.group-roll-header-expand-section') + .querySelectorAll('i') + .forEach(element => { + element.classList.toggle('fa-angle-up'); + element.classList.toggle('fa-angle-down'); + }); + event.target.closest('.group-roll-section').querySelector('.group-roll-content').classList.toggle('closed'); + } } diff --git a/module/applications/ui/fearTracker.mjs b/module/applications/ui/fearTracker.mjs index 2b7c4dac..e9c816db 100644 --- a/module/applications/ui/fearTracker.mjs +++ b/module/applications/ui/fearTracker.mjs @@ -1,4 +1,4 @@ -import { emitAsGM, GMUpdateEvent, socketEvent } from '../../systemRegistration/socket.mjs'; +import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; diff --git a/module/applications/ui/itemBrowser.mjs b/module/applications/ui/itemBrowser.mjs index 4f3053bb..33995aa9 100644 --- a/module/applications/ui/itemBrowser.mjs +++ b/module/applications/ui/itemBrowser.mjs @@ -93,16 +93,17 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { if (lite === true) { this.compendiumBrowserTypeKey = 'compendiumBrowserLite'; } - const userPresetPosition = game.user.getFlag(CONFIG.DH.id, CONFIG.DH.FLAGS[`${this.compendiumBrowserTypeKey}`].position) ; - + const userPresetPosition = game.user.getFlag( + CONFIG.DH.id, + CONFIG.DH.FLAGS[`${this.compendiumBrowserTypeKey}`].position + ); + options.position = userPresetPosition ?? ItemBrowser.DEFAULT_OPTIONS.position; - + if (!userPresetPosition) { const width = noFolder === true || lite === true ? 600 : 850; - if (this.rendered) - this.setPosition({ width }); - else - options.position.width = width; + if (this.rendered) this.setPosition({ width }); + else options.position.width = width; } await super._preRender(context, options); diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index ef0f24cb..6ecc76e6 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -178,7 +178,7 @@ export const defeatedConditions = () => { }, {}); }; -const defeatedConditionChoices = { +export const defeatedConditionChoices = { defeated: { id: 'defeated', name: 'DAGGERHEART.CONFIG.Condition.defeated.name' diff --git a/module/config/settingsConfig.mjs b/module/config/settingsConfig.mjs index 5232cbd9..aea9bc48 100644 --- a/module/config/settingsConfig.mjs +++ b/module/config/settingsConfig.mjs @@ -27,7 +27,8 @@ export const gameSettings = { }, LevelTiers: 'LevelTiers', Countdowns: 'Countdowns', - LastMigrationVersion: 'LastMigrationVersion' + LastMigrationVersion: 'LastMigrationVersion', + TagTeamRoll: 'TagTeamRoll' }; export const actionAutomationChoices = { diff --git a/module/data/_module.mjs b/module/data/_module.mjs index 2188b7bb..2749bfce 100644 --- a/module/data/_module.mjs +++ b/module/data/_module.mjs @@ -1,5 +1,6 @@ export { default as DhCombat } from './combat.mjs'; export { default as DhCombatant } from './combatant.mjs'; +export { default as DhTagTeamRoll } from './tagTeamRoll.mjs'; export * as actions from './action/_module.mjs'; export * as activeEffects from './activeEffect/_module.mjs'; diff --git a/module/data/actor/_module.mjs b/module/data/actor/_module.mjs index c19036eb..99577620 100644 --- a/module/data/actor/_module.mjs +++ b/module/data/actor/_module.mjs @@ -2,12 +2,14 @@ import DhCharacter from './character.mjs'; import DhCompanion from './companion.mjs'; import DhAdversary from './adversary.mjs'; import DhEnvironment from './environment.mjs'; +import DhParty from './party.mjs'; -export { DhCharacter, DhCompanion, DhAdversary, DhEnvironment }; +export { DhCharacter, DhCompanion, DhAdversary, DhEnvironment, DhParty }; export const config = { character: DhCharacter, companion: DhCompanion, adversary: DhAdversary, - environment: DhEnvironment + environment: DhEnvironment, + party: DhParty }; diff --git a/module/data/actor/adversary.mjs b/module/data/actor/adversary.mjs index 00c40baf..0e74e0c8 100644 --- a/module/data/actor/adversary.mjs +++ b/module/data/actor/adversary.mjs @@ -170,4 +170,13 @@ export default class DhpAdversary extends BaseDataActor { } } } + + _getTags() { + const tags = [ + game.i18n.localize(`DAGGERHEART.GENERAL.Tiers.${this.tier}`), + `${game.i18n.localize(`DAGGERHEART.CONFIG.AdversaryType.${this.type}.label`)}`, + `${game.i18n.localize('DAGGERHEART.GENERAL.difficulty')}: ${this.difficulty}` + ]; + return tags; + } } diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index ddcc5bf5..1cf082f7 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -673,4 +673,8 @@ export default class DhCharacter extends BaseDataActor { this.companion.updateLevel(1); } } + + _getTags() { + return [this.class.value?.name, this.class.subclass?.name, this.community?.name, this.ancestry?.name].filter((t) => !!t); + } } diff --git a/module/data/actor/party.mjs b/module/data/actor/party.mjs new file mode 100644 index 00000000..93fb3cde --- /dev/null +++ b/module/data/actor/party.mjs @@ -0,0 +1,48 @@ +import BaseDataActor from './base.mjs'; +import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs'; + +export default class DhParty extends BaseDataActor { + /**@inheritdoc */ + static defineSchema() { + const fields = foundry.data.fields; + return { + ...super.defineSchema(), + partyMembers: new ForeignDocumentUUIDArrayField({ type: 'Actor' }), + notes: new fields.HTMLField(), + gold: new fields.SchemaField({ + coins: new fields.NumberField({ initial: 0, integer: true }), + handfuls: new fields.NumberField({ initial: 1, integer: true }), + bags: new fields.NumberField({ initial: 0, integer: true }), + chests: new fields.NumberField({ initial: 0, integer: true }) + }) + }; + } + + /* -------------------------------------------- */ + + /**@inheritdoc */ + static DEFAULT_ICON = 'systems/daggerheart/assets/icons/documents/actors/dark-squad.svg'; + + /* -------------------------------------------- */ + + prepareBaseData() { + super.prepareBaseData(); + this.partyMembers = this.partyMembers.filter(p => !!p); + + // Register this party to all members + if (game.actors.get(this.parent.id) === this.parent) { + for (const member of this.partyMembers) { + member.parties?.add(this.parent); + } + } + } + + _onDelete(options, userId) { + super._onDelete(options, userId); + + // Clear this party from all members that aren't deleted + for (const member of this.partyMembers) { + member.parties?.delete(this.parent); + } + } +} diff --git a/module/data/chat-message/_modules.mjs b/module/data/chat-message/_modules.mjs index 67046248..ec095aac 100644 --- a/module/data/chat-message/_modules.mjs +++ b/module/data/chat-message/_modules.mjs @@ -1,5 +1,6 @@ import DHAbilityUse from './abilityUse.mjs'; import DHActorRoll from './actorRoll.mjs'; +import DHGroupRoll from './groupRoll.mjs'; import DHSystemMessage from './systemMessage.mjs'; export const config = { @@ -7,5 +8,6 @@ export const config = { adversaryRoll: DHActorRoll, damageRoll: DHActorRoll, dualityRoll: DHActorRoll, + groupRoll: DHGroupRoll, systemMessage: DHSystemMessage }; diff --git a/module/data/chat-message/groupRoll.mjs b/module/data/chat-message/groupRoll.mjs new file mode 100644 index 00000000..a5308323 --- /dev/null +++ b/module/data/chat-message/groupRoll.mjs @@ -0,0 +1,39 @@ +import { abilities } from '../../config/actorConfig.mjs'; + +export default class DHGroupRoll extends foundry.abstract.TypeDataModel { + static defineSchema() { + const fields = foundry.data.fields; + + return { + leader: new fields.EmbeddedDataField(GroupRollMemberField), + members: new fields.ArrayField(new fields.EmbeddedDataField(GroupRollMemberField)) + }; + } + + get totalModifier() { + return this.members.reduce((acc, m) => { + if (m.manualSuccess === null) return acc; + + return acc + (m.manualSuccess ? 1 : -1); + }, 0); + } +} + +class GroupRollMemberField extends foundry.abstract.DataModel { + static defineSchema() { + const fields = foundry.data.fields; + + return { + actor: new fields.ObjectField(), + trait: new fields.StringField({ choices: abilities }), + difficulty: new fields.StringField(), + result: new fields.ObjectField({ nullable: true, initial: null }), + manualSuccess: new fields.BooleanField({ nullable: true, initial: null }) + }; + } + + /* Can be expanded if we handle automation of success/failure */ + get success() { + return manualSuccess; + } +} diff --git a/module/data/item/armor.mjs b/module/data/item/armor.mjs index ca1ca004..e35fae46 100644 --- a/module/data/item/armor.mjs +++ b/module/data/item/armor.mjs @@ -131,6 +131,12 @@ export default class DHArmor extends AttachableItem { _onUpdate(a, b, c) { super._onUpdate(a, b, c); + + if (this.actor?.type === 'character') { + for (const party of this.actor.parties) { + party.render(); + } + } } /** diff --git a/module/data/settings/Appearance.mjs b/module/data/settings/Appearance.mjs index dfdd17e2..47909b2c 100644 --- a/module/data/settings/Appearance.mjs +++ b/module/data/settings/Appearance.mjs @@ -18,6 +18,7 @@ export default class DhAppearance extends foundry.abstract.DataModel { }); return { + useResourcePips: new BooleanField({ initial: false }), displayFear: new StringField({ required: true, choices: CONFIG.DH.GENERAL.fearDisplay, diff --git a/module/data/settings/Automation.mjs b/module/data/settings/Automation.mjs index be1b71ef..fbded2de 100644 --- a/module/data/settings/Automation.mjs +++ b/module/data/settings/Automation.mjs @@ -69,19 +69,19 @@ export default class DhAutomation extends foundry.abstract.DataModel { characterDefault: new fields.StringField({ required: true, choices: CONFIG.DH.GENERAL.defeatedConditionChoices, - initial: 'unconscious', + initial: CONFIG.DH.GENERAL.defeatedConditionChoices.unconscious.id, label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.characterDefault.label' }), adversaryDefault: new fields.StringField({ required: true, choices: CONFIG.DH.GENERAL.defeatedConditionChoices, - initial: 'defeated', + initial: CONFIG.DH.GENERAL.defeatedConditionChoices.dead.id, label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.adversaryDefault.label' }), companionDefault: new fields.StringField({ required: true, choices: CONFIG.DH.GENERAL.defeatedConditionChoices, - initial: 'defeated', + initial: CONFIG.DH.GENERAL.defeatedConditionChoices.defeated.id, label: 'DAGGERHEART.SETTINGS.Automation.FIELDS.defeated.companionDefault.label' }), deadIcon: new fields.FilePathField({ diff --git a/module/data/tagTeamRoll.mjs b/module/data/tagTeamRoll.mjs new file mode 100644 index 00000000..de71a11b --- /dev/null +++ b/module/data/tagTeamRoll.mjs @@ -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 }) + }) + ) + }; + } +} diff --git a/module/dice/damageRoll.mjs b/module/dice/damageRoll.mjs index 534867f8..c10ee6ff 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -1,4 +1,5 @@ import DamageDialog from '../applications/dialogs/damageDialog.mjs'; +import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs'; import DHRoll from './dhRoll.mjs'; export default class DamageRoll extends DHRoll { @@ -338,5 +339,13 @@ export default class DamageRoll extends DHRoll { parts: damageParts } }); + + Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll }); + await game.socket.emit(`system.${CONFIG.DH.id}`, { + action: socketEvent.Refresh, + data: { + refreshType: RefreshType.TagTeamRoll + } + }); } } diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index 3865710a..c9bda197 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -29,6 +29,10 @@ export default class DHRoll extends Roll { config.hooks = [...this.getHooks(), '']; 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) { if (Hooks.call(`${CONFIG.DH.id}.preRoll${hook.capitalize()}`, config, message) === false) return null; } @@ -66,8 +70,13 @@ export default class DHRoll extends Roll { if (Hooks.call(`${CONFIG.DH.id}.postRoll${hook.capitalize()}`, config, message) === false) return null; } - // Create Chat Message - if (!config.source?.message) config.message = await this.toMessage(roll, config); + if (config.skips?.createMessage) { + if (game.modules.get('dice-so-nice')?.active) { + await game.dice3d.showForRoll(roll, game.user, true); + } + } else if (!config.source?.message) { + config.message = await this.toMessage(roll, config); + } } static postEvaluate(roll, config = {}) { @@ -100,6 +109,10 @@ export default class DHRoll extends Roll { if (roll._evaluated) { 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) { await game.dice3d.waitFor3DAnimationByMessageID(message.id); } @@ -228,10 +241,11 @@ export const registerRollDiceHooks = () => { if ( !config.source?.actor || (game.user.isGM ? !hopeFearAutomation.gm : !hopeFearAutomation.players) || - config.actionType === 'reaction' + config.actionType === 'reaction' || + config.tagTeamSelected || + config.skips?.resources ) return; - const actor = await fromUuid(config.source.actor); let updates = []; if (!actor) return; diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index 8fedc368..813c913b 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -256,9 +256,11 @@ export default class DualityRoll extends D20Roll { }); newRoll.extra = newRoll.extra.slice(2); + const tagTeamSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); Hooks.call(`${CONFIG.DH.id}.postRollDuality`, { source: { actor: message.system.source.actor ?? '' }, targets: message.system.targets, + tagTeamSelected: Object.values(tagTeamSettings.members).some(x => x.messageId === message._id), roll: newRoll, rerolledRoll: newRoll.result.duality !== message.system.roll.result.duality ? message.system.roll : undefined diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 6e286fc8..8faf1350 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -5,6 +5,8 @@ import { createScrollText, damageKeyToNumber } from '../helpers/utils.mjs'; import DhCompanionLevelUp from '../applications/levelup/companionLevelup.mjs'; export default class DhpActor extends Actor { + parties = new Set(); + #scrollTextQueue = []; #scrollTextInterval; @@ -74,7 +76,7 @@ export default class DhpActor extends Actor { // Configure prototype token settings const prototypeToken = {}; - if (['character', 'companion'].includes(this.type)) + if (['character', 'companion', 'party'].includes(this.type)) Object.assign(prototypeToken, { sight: { enabled: true }, actorLink: true, @@ -83,6 +85,20 @@ export default class DhpActor extends Actor { this.updateSource({ prototypeToken }); } + _onUpdate(changes, options, userId) { + super._onUpdate(changes, options, userId); + for (const party of this.parties) { + party.render(); + } + } + + _onDelete(options, userId) { + super._onDelete(options, userId); + for (const party of this.parties) { + party.render(); + } + } + async updateLevel(newLevel) { if (!['character', 'companion'].includes(this.type) || newLevel === this.system.levelData.level.changed) return; @@ -808,4 +824,14 @@ export default class DhpActor extends Actor { return await super.importFromJSON(json); } + + /** + * Generate an array of localized tag. + * @returns {string[]} An array of localized tag strings. + */ + _getTags() { + const tags = []; + if (this.system._getTags) tags.push(...this.system._getTags()); + return tags; + } } diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index bb535c6d..ec4c5a49 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -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 { targetHook = null; @@ -16,7 +16,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { const html = await super.renderHTML({ actor: actorData, author: this.author }); if (this.flags.core?.RollTable) { - html.querySelector('.roll-buttons.apply-buttons').remove(); + html.querySelector('.roll-buttons.apply-buttons')?.remove(); } this.enrichChatMessage(html); @@ -155,7 +155,15 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { event.stopPropagation(); const config = foundry.utils.deepClone(this.system); 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) { diff --git a/module/enrichers/LookupEnricher.mjs b/module/enrichers/LookupEnricher.mjs index 7836311e..3566e112 100644 --- a/module/enrichers/LookupEnricher.mjs +++ b/module/enrichers/LookupEnricher.mjs @@ -1,7 +1,7 @@ import { parseInlineParams } from './parser.mjs'; export default function DhLookupEnricher(match, { rollData }) { - const results = parseInlineParams(match[1], { first: 'formula'}); + const results = parseInlineParams(match[1], { first: 'formula' }); const element = document.createElement('span'); element.textContent = Roll.replaceFormulaData(String(results.formula), rollData); return element; diff --git a/module/enrichers/parser.mjs b/module/enrichers/parser.mjs index 9fcc4af1..365caec9 100644 --- a/module/enrichers/parser.mjs +++ b/module/enrichers/parser.mjs @@ -1,5 +1,5 @@ /** - * @param {string} paramString The parameter inside the brackets of something like @Template[] to parse + * @param {string} paramString The parameter inside the brackets of something like @Template[] to parse * @param {Object} options * @param {string} options.first If set, the first parameter is treated as a value with this as its key * @returns {Record | null} diff --git a/module/helpers/handlebarsHelper.mjs b/module/helpers/handlebarsHelper.mjs index 847b04ce..2aa72dfc 100644 --- a/module/helpers/handlebarsHelper.mjs +++ b/module/helpers/handlebarsHelper.mjs @@ -15,7 +15,8 @@ export default class RegisterHandlebarsHelpers { setVar: this.setVar, empty: this.empty, pluralize: this.pluralize, - positive: this.positive + positive: this.positive, + isNullish: this.isNullish }); } static add(a, b) { @@ -94,4 +95,8 @@ export default class RegisterHandlebarsHelpers { static positive(a) { return Math.abs(Number(a)); } + + static isNullish(a) { + return a === null || a === undefined; + } } diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index dbf66ff4..3044cd71 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -418,3 +418,15 @@ export async function createEmbeddedItemsWithEffects(actor, baseData) { export const slugify = name => { return name.toLowerCase().replaceAll(' ', '-').replaceAll('.', ''); }; + +export function shuffleArray(array) { + let currentIndex = array.length; + while (currentIndex != 0) { + let randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex--; + + [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]]; + } + + return array; +} diff --git a/module/systemRegistration/handlebars.mjs b/module/systemRegistration/handlebars.mjs index b26e5fef..2bf820c1 100644 --- a/module/systemRegistration/handlebars.mjs +++ b/module/systemRegistration/handlebars.mjs @@ -13,6 +13,7 @@ export const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/sheets/global/partials/domain-card-item.hbs', 'systems/daggerheart/templates/sheets/global/partials/item-resource.hbs', 'systems/daggerheart/templates/sheets/global/partials/resource-section.hbs', + 'systems/daggerheart/templates/sheets/global/partials/resource-bar.hbs', 'systems/daggerheart/templates/components/card-preview.hbs', 'systems/daggerheart/templates/levelup/parts/selectable-card-preview.hbs', 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs', diff --git a/module/systemRegistration/settings.mjs b/module/systemRegistration/settings.mjs index 565a7740..6954730f 100644 --- a/module/systemRegistration/settings.mjs +++ b/module/systemRegistration/settings.mjs @@ -7,6 +7,7 @@ import { DhHomebrewSettings, DhVariantRuleSettings } from '../applications/settings/_module.mjs'; +import { DhTagTeamRoll } from '../data/_module.mjs'; export const registerDHSettings = () => { registerMenuSettings(); @@ -122,4 +123,10 @@ const registerNonConfigSettings = () => { config: false, type: DhCountdowns }); + + game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, { + scope: 'world', + config: false, + type: DhTagTeamRoll + }); }; diff --git a/module/systemRegistration/socket.mjs b/module/systemRegistration/socket.mjs index 14b4cec1..ac61238f 100644 --- a/module/systemRegistration/socket.mjs +++ b/module/systemRegistration/socket.mjs @@ -1,4 +1,5 @@ import DamageReductionDialog from '../applications/dialogs/damageReductionDialog.mjs'; +import Party from '../applications/sheets/actors/party.mjs'; export function handleSocketEvent({ action = null, data = {} } = {}) { switch (action) { @@ -11,13 +12,17 @@ export function handleSocketEvent({ action = null, data = {} } = {}) { case socketEvent.Refresh: Hooks.call(socketEvent.Refresh, data); break; + case socketEvent.DowntimeTrigger: + Party.downtimeMoveQuery(data); + break; } } export const socketEvent = { GMUpdate: 'DhGMUpdate', Refresh: 'DhRefresh', - DhpFearUpdate: 'DhFearUpdate' + DhpFearUpdate: 'DhFearUpdate', + DowntimeTrigger: 'DowntimeTrigger' }; export const GMUpdateEvent = { @@ -30,7 +35,8 @@ export const GMUpdateEvent = { }; export const RefreshType = { - Countdown: 'DhCoundownRefresh' + Countdown: 'DhCoundownRefresh', + TagTeamRoll: 'DhTagTeamRollRefresh' }; export const registerSocketHooks = () => { diff --git a/styles/less/dialog/dice-roll/roll-selection.less b/styles/less/dialog/dice-roll/roll-selection.less index a0ac42b6..0f082460 100644 --- a/styles/less/dialog/dice-roll/roll-selection.less +++ b/styles/less/dialog/dice-roll/roll-selection.less @@ -11,7 +11,14 @@ .application.daggerheart.dialog.dh-style.views.roll-selection { .dialog-header { display: flex; - justify-content: center; + flex-direction: column; + align-items: center; + gap: 4px; + + .dialog-header-inner { + display: flex; + justify-content: center; + } h1 { 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 { diff --git a/styles/less/dialog/group-roll/group-roll.less b/styles/less/dialog/group-roll/group-roll.less new file mode 100644 index 00000000..f2895d31 --- /dev/null +++ b/styles/less/dialog/group-roll/group-roll.less @@ -0,0 +1,50 @@ +@import '../../utils/colors.less'; + +.application.daggerheart.group-roll { + fieldset.one-column { + min-width: 500px; + margin-bottom: 10px; + } + .actor-item { + display: flex; + align-items: center; + gap: 15px; + width: 100%; + + img { + height: 40px; + width: 40px; + border-radius: 50%; + object-fit: cover; + } + + .actor-info { + display: flex; + flex-direction: column; + gap: 10px; + + .actor-check-info { + display: flex; + gap: 10px; + + .form-fields { + display: flex; + gap: 5px; + align-items: center; + + input { + max-width: 40px; + text-align: center; + } + } + } + } + + .controls { + margin-left: auto; + } + } + .tooltip-container { + width: 100%; + } +} diff --git a/styles/less/dialog/index.less b/styles/less/dialog/index.less index d4104d3c..0f2b053b 100644 --- a/styles/less/dialog/index.less +++ b/styles/less/dialog/index.less @@ -31,4 +31,7 @@ @import './reroll-dialog/sheet.less'; +@import './group-roll/group-roll.less'; +@import './tag-team-dialog/sheet.less'; + @import './image-select/sheet.less'; diff --git a/styles/less/dialog/tag-team-dialog/sheet.less b/styles/less/dialog/tag-team-dialog/sheet.less new file mode 100644 index 00000000..767c66ca --- /dev/null +++ b/styles/less/dialog/tag-team-dialog/sheet.less @@ -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; + } + } +} diff --git a/styles/less/global/dialog.less b/styles/less/global/dialog.less index f164b701..8c532c2b 100644 --- a/styles/less/global/dialog.less +++ b/styles/less/global/dialog.less @@ -67,6 +67,10 @@ } } + .standard-form { + font-family: @font-body; + } + &.two-big-buttons { .window-content { padding-top: 0; diff --git a/styles/less/global/index.less b/styles/less/global/index.less index db61304a..f51140de 100644 --- a/styles/less/global/index.less +++ b/styles/less/global/index.less @@ -19,3 +19,4 @@ @import './filter-menu.less'; @import './tab-attachments.less'; @import './dice.less'; +@import './resource-bar.less'; diff --git a/styles/less/global/inventory-item.less b/styles/less/global/inventory-item.less index 50cdf116..c9ed28d8 100644 --- a/styles/less/global/inventory-item.less +++ b/styles/less/global/inventory-item.less @@ -72,7 +72,7 @@ flex: 0 0 40px; height: 40px; position: relative; - &:has(.roll-img) { + &:has(.roll-img) { cursor: pointer; } @@ -87,6 +87,7 @@ &.actor-img { border-radius: 50%; + object-position: top center; } } @@ -122,6 +123,10 @@ display: flex; gap: 10px; + &.padded { + padding-right: 8px; + } + .item-label { flex: 1; align-self: center; @@ -248,9 +253,9 @@ &.inventory-item-compact { display: grid; - grid-template: - "img name controls" auto - "img labels labels" 1fr + grid-template: + 'img name controls' auto + 'img labels labels' 1fr / 40px 1fr min-content; column-gap: 8px; diff --git a/styles/less/global/resource-bar.less b/styles/less/global/resource-bar.less new file mode 100644 index 00000000..be9bc68b --- /dev/null +++ b/styles/less/global/resource-bar.less @@ -0,0 +1,178 @@ +// Theme sidebar backgrounds +.appTheme({ + .slot-value .slot-bar { + background: @dark-blue; + } +}, { + .slot-value .slot-bar { + background-image: url('../assets/parchments/dh-parchment-light.png'); + } +}); + +.status-bar { + display: flex; + justify-content: center; + position: relative; + width: 120px; + height: 40px; + + .status-label { + position: relative; + top: 40px; + height: 22px; + width: 79px; + clip-path: path('M0 0H79L74 16.5L39 22L4 16.5L0 0Z'); + background: light-dark(@dark-blue, @golden); + + h4 { + font-weight: bold; + text-align: center; + line-height: 18px; + color: light-dark(@beige, @dark-blue); + } + } + .slot-value { + position: absolute; + display: flex; + flex-direction: column; + padding: 0 5px; + font-size: 1.5rem; + align-items: center; + width: 140px; + height: 40px; + justify-content: center; + text-align: center; + z-index: 2; + color: @beige; + + .slot-bar { + display: flex; + flex-wrap: wrap; + gap: 5px; + padding: 5px; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + z-index: 1; + color: light-dark(@dark-blue, @golden); + width: fit-content; + + .slot { + width: 15px; + height: 10px; + border: 1px solid light-dark(@dark-blue, @golden); + background: light-dark(@dark-blue-10, @golden-10); + border-radius: 3px; + transition: all 0.3s ease; + cursor: pointer; + + &.large { + width: 20px; + } + + &.filled { + background: light-dark(@dark-blue, @golden); + } + } + + .empty-slot { + width: 15px; + height: 10px; + } + } + .slot-label { + display: flex; + align-items: center; + color: light-dark(@beige, @dark-blue); + background: light-dark(@dark-blue, @golden); + padding: 0 5px; + width: fit-content; + font-weight: bold; + border-radius: 0px 0px 5px 5px; + font-size: var(--font-size-12); + + .label { + padding-right: 5px; + } + + .value { + padding-left: 6px; + border-left: 1px solid light-dark(@beige, @dark-golden); + } + } + } + + .status-value { + position: absolute; + display: flex; + padding: 0 5px; + font-size: 1.5rem; + align-items: center; + width: 140px; + height: 40px; + justify-content: center; + text-align: center; + z-index: 2; + color: @beige; + + input[type='number'] { + background: transparent; + font-size: 1.5rem; + width: 40px; + height: 30px; + text-align: center; + border: none; + outline: 2px solid transparent; + color: @beige; + + &.bar-input { + padding: 0; + color: @beige; + backdrop-filter: none; + background: transparent; + transition: all 0.3s ease; + + &:hover, + &:focus { + background: @semi-transparent-dark-blue; + backdrop-filter: blur(9.5px); + } + } + } + + .bar-label { + width: 40px; + } + } + .progress-bar { + position: absolute; + appearance: none; + width: 100px; + height: 40px; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + z-index: 1; + background: @dark-blue; + + &::-webkit-progress-bar { + border: none; + background: @dark-blue; + border-radius: 6px; + } + &::-webkit-progress-value { + background: @gradient-hp; + border-radius: 6px; + } + &.stress-color::-webkit-progress-value { + background: @gradient-stress; + border-radius: 6px; + } + &::-moz-progress-bar { + background: @gradient-hp; + border-radius: 6px; + } + &.stress-color::-moz-progress-bar { + background: @gradient-stress; + border-radius: 6px; + } + } +} diff --git a/styles/less/global/sheet.less b/styles/less/global/sheet.less index 6f77a481..1e7bad0a 100755 --- a/styles/less/global/sheet.less +++ b/styles/less/global/sheet.less @@ -14,7 +14,11 @@ body.game:is(.performance-low, .noblur) { .themed.theme-dark .application.daggerheart.sheet.dh-style, .themed.theme-dark.application.daggerheart.sheet.dh-style, &.theme-dark .application.daggerheart { - background: @dark-blue; + &.adversary, + &.character, + &.item { + background: @dark-blue; + } } } diff --git a/styles/less/global/tab-description.less b/styles/less/global/tab-description.less index 04a9d82a..be95ef6d 100644 --- a/styles/less/global/tab-description.less +++ b/styles/less/global/tab-description.less @@ -1,16 +1,16 @@ -@import '../utils/colors.less'; -@import '../utils/fonts.less'; - -.daggerheart.dh-style { - .tab.active.description { - display: flex; - flex-direction: column; - height: -webkit-fill-available !important; - overflow-y: hidden !important; - padding-top: 10px; - - prose-mirror.active + .artist-attribution { - display: none; - } - } -} +@import '../utils/colors.less'; +@import '../utils/fonts.less'; + +.daggerheart.dh-style { + .tab.active.description { + display: flex; + flex-direction: column; + height: -webkit-fill-available !important; + overflow-y: hidden !important; + padding-top: 10px; + + prose-mirror.active + .artist-attribution { + display: none; + } + } +} diff --git a/styles/less/hud/token-hud/token-hud.less b/styles/less/hud/token-hud/token-hud.less index c124d843..9849512b 100644 --- a/styles/less/hud/token-hud/token-hud.less +++ b/styles/less/hud/token-hud/token-hud.less @@ -12,5 +12,13 @@ font-weight: bold; } } + + .clown-car img { + transition: 0.5s; + + &.flipped { + transform: scaleX(-1); + } + } } } diff --git a/styles/less/sheets/actors/adversary/sidebar.less b/styles/less/sheets/actors/adversary/sidebar.less index ab15fa46..f8537525 100644 --- a/styles/less/sheets/actors/adversary/sidebar.less +++ b/styles/less/sheets/actors/adversary/sidebar.less @@ -109,6 +109,14 @@ gap: 16px; margin-bottom: -10px; + &.pip-display { + top: -15px; + + .resources-section { + justify-content: space-around; + } + } + .resources-section { display: flex; justify-content: space-evenly; diff --git a/styles/less/sheets/actors/character/sidebar.less b/styles/less/sheets/actors/character/sidebar.less index 3d244cdd..e66cba82 100644 --- a/styles/less/sheets/actors/character/sidebar.less +++ b/styles/less/sheets/actors/character/sidebar.less @@ -6,9 +6,16 @@ .appTheme({ .character-sidebar-sheet { background-image: url('../assets/parchments/dh-parchment-dark.png'); + .experience-value { background: url(../assets/svg/experience-shield.svg) no-repeat; } + + .info-section .status-section .status-bar.armor-slots { + .slot-value .slot-bar { + background: @dark-blue; + } + } } }, { &.sheet.actor.dh-style.character .character-sidebar-sheet { @@ -21,6 +28,12 @@ .portrait.death-roll .death-roll-btn { filter: brightness(0) drop-shadow(0 0 3px @dark-blue); } + + .info-section .status-section .status-bar.armor-slots { + .slot-value .slot-bar { + background-image: url('../assets/parchments/dh-parchment-light.png'); + } + } } }); @@ -127,6 +140,15 @@ gap: 10px; margin-bottom: -16px; + &.pip-display { + gap: 15px; + + .resources-section { + justify-content: space-around; + margin: 8px 2px 8px 2px; + } + } + .resources-section { display: flex; justify-content: space-evenly; @@ -136,7 +158,7 @@ display: flex; justify-content: center; position: relative; - width: 100px; + width: 120px; height: 40px; .status-label { @@ -154,13 +176,14 @@ color: light-dark(@beige, @dark-blue); } } + .status-value { position: absolute; display: flex; - padding: 0 6px; + padding: 0 5px; font-size: 1.5rem; align-items: center; - width: 100px; + width: 140px; height: 40px; justify-content: center; text-align: center; @@ -237,6 +260,28 @@ gap: 5px; justify-content: center; + &.pip-display { + align-items: end; + + .status-bar.armor-slots { + width: 100px; + height: auto; + + .slot-value { + position: relative; + height: auto; + + .slot-bar { + border-radius: 6px 6px 0 0; + } + } + + .status-value { + padding: 0 5px; + } + } + } + .status-bar.armor-slots { display: flex; justify-content: center; @@ -252,6 +297,7 @@ width: 95px; border-radius: 3px; background: light-dark(@dark-blue, @golden); + clip-path: none; h4 { font-weight: bold; @@ -261,6 +307,66 @@ font-size: var(--font-size-12); } } + .slot-value { + position: absolute; + display: flex; + padding: 0 5px; + font-size: 1.2rem; + align-items: center; + width: 80px; + height: 30px; + justify-content: center; + text-align: center; + z-index: 2; + color: light-dark(@dark-blue, @beige); + flex-direction: column; + + .slot-bar { + display: flex; + flex-wrap: wrap; + gap: 4px; + padding: 5px; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + z-index: 1; + background: @dark-blue; + justify-content: center; + color: light-dark(@dark-blue, @golden); + + .armor-slot { + cursor: pointer; + transition: all 0.3s ease; + font-size: var(--font-size-12); + + .fa-shield-halved { + color: light-dark(@dark-blue-40, @golden-40); + } + } + } + .slot-label { + display: flex; + align-items: center; + color: light-dark(@beige, @dark-blue); + background: light-dark(@dark-blue, @golden); + padding: 0 5px; + width: 120%; + font-weight: bold; + border-radius: 5px; + font-size: var(--font-size-12); + flex-wrap: wrap; + justify-content: center; + + .label { + padding-right: 1px; + border-bottom: 1px solid @dark-golden; + } + + .value { + padding-left: 6px; + border-left: 0; + } + } + } .status-value { position: absolute; display: flex; @@ -292,8 +398,6 @@ color: light-dark(@dark-blue, @beige); backdrop-filter: none; background: transparent; - transition: all 0.3s ease; - &:hover, &:focus { background: @semi-transparent-dark-blue; @@ -306,7 +410,6 @@ width: 30px; } } - .progress-bar { position: absolute; appearance: none; @@ -318,7 +421,6 @@ background: light-dark(transparent, @dark-blue); border-bottom: none; border-radius: 6px 6px 0 0; - &::-webkit-progress-bar { border: none; background: light-dark(transparent, @dark-blue); diff --git a/styles/less/sheets/actors/companion/header.less b/styles/less/sheets/actors/companion/header.less index b85a1819..3616a6b3 100644 --- a/styles/less/sheets/actors/companion/header.less +++ b/styles/less/sheets/actors/companion/header.less @@ -37,11 +37,22 @@ } } + .resource-section { + width: 100%; + display: flex; + justify-content: center; + } + .status-section { display: flex; gap: 5px; justify-content: center; + &.pip-display { + width: 100%; + justify-content: space-around; + } + .status-number { display: flex; flex-direction: column; @@ -84,103 +95,103 @@ } } - .status-bar { - display: flex; - justify-content: center; - position: relative; - width: 100px; - height: 40px; + // .status-bar { + // display: flex; + // justify-content: center; + // position: relative; + // width: 100px; + // height: 40px; - .status-label { - position: relative; - top: 40px; - height: 22px; - width: 79px; - clip-path: path('M0 0H79L74 16.5L39 22L4 16.5L0 0Z'); - background: light-dark(@dark-blue, @golden); + // .status-label { + // position: relative; + // top: 40px; + // height: 22px; + // width: 79px; + // clip-path: path('M0 0H79L74 16.5L39 22L4 16.5L0 0Z'); + // background: light-dark(@dark-blue, @golden); - h4 { - font-weight: bold; - text-align: center; - line-height: 18px; - color: light-dark(@beige, @dark-blue); - } - } - .status-value { - position: absolute; - display: flex; - padding: 0 6px; - font-size: 1.5rem; - align-items: center; - width: 100px; - height: 40px; - justify-content: center; - text-align: center; - z-index: 2; - color: @beige; + // h4 { + // font-weight: bold; + // text-align: center; + // line-height: 18px; + // color: light-dark(@beige, @dark-blue); + // } + // } + // .status-value { + // position: absolute; + // display: flex; + // padding: 0 6px; + // font-size: 1.5rem; + // align-items: center; + // width: 100px; + // height: 40px; + // justify-content: center; + // text-align: center; + // z-index: 2; + // color: @beige; - input[type='number'] { - background: transparent; - font-size: 1.5rem; - width: 40px; - height: 30px; - text-align: center; - border: none; - outline: 2px solid transparent; - color: @beige; + // input[type='number'] { + // background: transparent; + // font-size: 1.5rem; + // width: 40px; + // height: 30px; + // text-align: center; + // border: none; + // outline: 2px solid transparent; + // color: @beige; - &.bar-input { - padding: 0; - color: @beige; - backdrop-filter: none; - background: transparent; - transition: all 0.3s ease; + // &.bar-input { + // padding: 0; + // color: @beige; + // backdrop-filter: none; + // background: transparent; + // transition: all 0.3s ease; - &:hover, - &:focus { - background: @semi-transparent-dark-blue; - backdrop-filter: blur(9.5px); - } - } - } + // &:hover, + // &:focus { + // background: @semi-transparent-dark-blue; + // backdrop-filter: blur(9.5px); + // } + // } + // } - .bar-label { - width: 40px; - } - } - .progress-bar { - position: absolute; - appearance: none; - width: 100px; - height: 40px; - border: 1px solid light-dark(@dark-blue, @golden); - border-radius: 6px; - z-index: 1; - background: @dark-blue; + // .bar-label { + // width: 40px; + // } + // } + // .progress-bar { + // position: absolute; + // appearance: none; + // width: 100px; + // height: 40px; + // border: 1px solid light-dark(@dark-blue, @golden); + // border-radius: 6px; + // z-index: 1; + // background: @dark-blue; - &::-webkit-progress-bar { - border: none; - background: @dark-blue; - border-radius: 6px; - } - &::-webkit-progress-value { - background: @gradient-hp; - border-radius: 6px; - } - &.stress-color::-webkit-progress-value { - background: @gradient-stress; - border-radius: 6px; - } - &::-moz-progress-bar { - background: @gradient-hp; - border-radius: 6px; - } - &.stress-color::-moz-progress-bar { - background: @gradient-stress; - border-radius: 6px; - } - } - } + // &::-webkit-progress-bar { + // border: none; + // background: @dark-blue; + // border-radius: 6px; + // } + // &::-webkit-progress-value { + // background: @gradient-hp; + // border-radius: 6px; + // } + // &.stress-color::-webkit-progress-value { + // background: @gradient-stress; + // border-radius: 6px; + // } + // &::-moz-progress-bar { + // background: @gradient-hp; + // border-radius: 6px; + // } + // &.stress-color::-moz-progress-bar { + // background: @gradient-stress; + // border-radius: 6px; + // } + // } + // } .level-div { white-space: nowrap; diff --git a/styles/less/sheets/actors/party/header.less b/styles/less/sheets/actors/party/header.less new file mode 100644 index 00000000..9a2c7350 --- /dev/null +++ b/styles/less/sheets/actors/party/header.less @@ -0,0 +1,42 @@ +@import '../../../utils/colors.less'; +@import '../../../utils/fonts.less'; + +.party-header-sheet { + display: flex; + flex-direction: column; + justify-content: start; + text-align: center; + + .profile { + height: 235px; + mask-image: linear-gradient(0deg, transparent 0%, black 10%); + cursor: pointer; + } + + .item-container { + .item-name { + padding: 0 20px; + input[type='text'] { + font-size: 32px; + height: 42px; + text-align: center; + transition: all 0.3s ease; + outline: 2px solid transparent; + border: 1px solid transparent; + + &:hover[type='text'], + &:focus[type='text'] { + box-shadow: none; + outline: 2px solid light-dark(@dark-blue, @golden); + } + } + } + + .label { + font-style: normal; + font-weight: 700; + font-size: 16px; + margin: 5px 0; + } + } +} diff --git a/styles/less/sheets/actors/party/inventory.less b/styles/less/sheets/actors/party/inventory.less new file mode 100644 index 00000000..1dfc66de --- /dev/null +++ b/styles/less/sheets/actors/party/inventory.less @@ -0,0 +1,73 @@ +@import '../../../utils/colors.less'; +@import '../../../utils/fonts.less'; + +.application.sheet.daggerheart.actor.dh-style.party { + .tab.inventory { + .search-section { + display: flex; + gap: 10px; + align-items: center; + + .search-bar { + position: relative; + color: light-dark(@dark-blue-50, @beige-50); + width: 100%; + padding-top: 5px; + + input { + border-radius: 50px; + background: light-dark(@dark-blue-10, @golden-10); + border: none; + outline: 2px solid transparent; + transition: all 0.3s ease; + padding: 0 20px; + + &:hover { + outline: 2px solid light-dark(@dark, @golden); + } + + &:placeholder { + color: light-dark(@dark-blue-50, @beige-50); + } + + &::-webkit-search-cancel-button { + -webkit-appearance: none; + display: none; + } + } + + .icon { + align-content: center; + height: 32px; + position: absolute; + right: 20px; + font-size: 16px; + z-index: 1; + color: light-dark(@dark-blue-50, @beige-50); + } + } + } + + .items-section { + display: flex; + flex-direction: column; + gap: 10px; + overflow-y: auto; + mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%); + padding: 20px 0; + + scrollbar-width: thin; + scrollbar-color: light-dark(@dark-blue, @golden) transparent; + } + + .currency-section { + display: flex; + gap: 10px; + padding: 10px 10px 0; + + input { + color: light-dark(@dark, @beige); + } + } + } +} diff --git a/styles/less/sheets/actors/party/party-members.less b/styles/less/sheets/actors/party/party-members.less new file mode 100644 index 00000000..a433ae34 --- /dev/null +++ b/styles/less/sheets/actors/party/party-members.less @@ -0,0 +1,28 @@ +@import '../../../utils/colors.less'; +@import '../../../utils/fonts.less'; + +.application.sheet.daggerheart.actor.dh-style.party { + .tab.partyMembers { + max-height: 400px; + overflow: auto; + + .actors-list { + display: flex; + flex-direction: column; + gap: 10px; + align-items: center; + width: 100%; + } + .actors-dragger { + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + width: 100%; + height: 40px; + border: 1px dashed light-dark(@dark-blue-50, @beige-50); + border-radius: 3px; + color: light-dark(@dark-blue-50, @beige-50); + } + } +} diff --git a/styles/less/sheets/actors/party/resources.less b/styles/less/sheets/actors/party/resources.less new file mode 100644 index 00000000..fc7e0110 --- /dev/null +++ b/styles/less/sheets/actors/party/resources.less @@ -0,0 +1,196 @@ +@import '../../../utils/colors.less'; +@import '../../../utils/fonts.less'; +@import '../../../utils/mixin.less'; + +body.game:is(.performance-low, .noblur) { + .application.sheet.daggerheart.actor.dh-style.party .tab.resources .actors-list .actor-resources { + background: light-dark(@dark-blue, @dark-golden); + + .actor-name { + background: light-dark(@dark-blue, @dark-golden); + } + } +} + +.application.sheet.daggerheart.actor.dh-style.party { + .tab.resources { + overflow: auto; + + .actors-list { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 10px; + align-items: center; + width: 100%; + justify-content: center; + + .actor-resources { + display: flex; + flex-direction: column; + position: relative; + background: light-dark(@dark-blue-40, @dark-golden-40); + border-radius: 6px; + border: 1px solid light-dark(@dark-blue, @golden); + max-width: 230px; + height: -webkit-fill-available; + + .actor-name { + position: absolute; + top: 0px; + background: light-dark(@dark-blue-90, @dark-golden-80); + backdrop-filter: blur(6.5px); + border-radius: 6px 6px 0px 0px; + text-align: center; + width: 100%; + z-index: 1; + font-size: var(--font-size-20); + color: light-dark(@beige, @golden); + font-weight: bold; + padding: 5px 0; + } + + .actor-img { + height: 150px; + object-fit: cover; + object-position: top center; + border-radius: 6px 6px 0px 0px; + mask-image: linear-gradient(180deg, black 88%, transparent 100%); + } + + .resources { + display: flex; + flex-direction: column; + gap: 10px; + align-items: center; + margin: 10px; + + .slot-section { + display: flex; + flex-direction: column; + align-items: center; + + .slot-bar { + display: flex; + flex-wrap: wrap; + gap: 5px; + width: 239px; + + background-color: light-dark(@dark-blue-10, @dark-blue); + color: light-dark(@dark-blue, @golden); + padding: 5px; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + width: fit-content; + + .armor-slot { + cursor: pointer; + transition: all 0.3s ease; + font-size: var(--font-size-12); + + .fa-shield-halved { + color: light-dark(@dark-blue-40, @golden-40); + } + } + + .slot { + width: 20px; + height: 10px; + border: 1px solid light-dark(@dark-blue, @golden); + background: light-dark(@dark-blue-10, @golden-10); + border-radius: 3px; + transition: all 0.3s ease; + cursor: pointer; + + &.filled { + background: light-dark(@dark-blue, @golden); + } + } + } + .slot-label { + display: flex; + align-items: center; + color: light-dark(@beige, @dark-blue); + background: light-dark(@dark-blue, @golden); + padding: 0 5px; + width: fit-content; + font-weight: bold; + border-radius: 0px 0px 5px 5px; + font-size: var(--font-size-12); + + .label { + padding-right: 5px; + } + + .value { + padding-left: 6px; + border-left: 1px solid light-dark(@beige, @dark-golden); + } + } + } + + .hope-section { + position: relative; + display: flex; + gap: 10px; + background-color: light-dark(transparent, @dark-blue); + color: light-dark(@dark-blue, @golden); + padding: 5px 10px; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 3px; + align-items: center; + width: fit-content; + + h4 { + font-size: var(--font-size-12); + font-weight: bold; + text-transform: uppercase; + color: light-dark(@dark-blue, @golden); + } + + .hope-value { + display: flex; + cursor: pointer; + font-size: var(--font-size-12); + } + } + + .threshold-section { + display: flex; + align-self: center; + gap: 10px; + background-color: light-dark(transparent, @dark-blue); + color: light-dark(@dark-blue, @golden); + padding: 5px 10px; + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 3px; + align-items: center; + width: fit-content; + + h4 { + font-size: var(--font-size-12); + font-weight: bold; + text-transform: uppercase; + color: light-dark(@dark-blue, @golden); + + &.threshold-value { + color: light-dark(@dark, @beige); + } + } + } + } + } + } + .actors-dragger { + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + width: 100%; + height: 40px; + border: 1px dashed light-dark(@dark-blue-50, @beige-50); + border-radius: 3px; + color: light-dark(@dark-blue-50, @beige-50); + } + } +} diff --git a/styles/less/sheets/actors/party/sheet.less b/styles/less/sheets/actors/party/sheet.less new file mode 100644 index 00000000..658d9446 --- /dev/null +++ b/styles/less/sheets/actors/party/sheet.less @@ -0,0 +1,45 @@ +@import '../../../utils/colors.less'; +@import '../../../utils/fonts.less'; +@import '../../../utils/mixin.less'; + +.appTheme({ + &.party { + background-image: url('../assets/parchments/dh-parchment-dark.png'); + } +}, { + &.party { + background: url('../assets/parchments/dh-parchment-light.png'); + } +}); + +.application.sheet.daggerheart.actor.dh-style.party { + .tab { + height: -webkit-fill-available; + max-height: 514px; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: light-dark(@dark-blue, @golden) transparent; + + &.active { + overflow: auto; + display: flex; + flex-direction: column; + } + + .actions-section { + display: flex; + align-items: center; + justify-content: center; + padding: 10px; + margin-bottom: 10px; + gap: 20px; + background-color: light-dark(@dark-blue-10, @golden-10); + + button { + span { + font-size: 12px; + } + } + } + } +} diff --git a/styles/less/sheets/index.less b/styles/less/sheets/index.less index 991837c0..1de1b055 100644 --- a/styles/less/sheets/index.less +++ b/styles/less/sheets/index.less @@ -25,6 +25,12 @@ @import './actors/environment/potentialAdversaries.less'; @import './actors/environment/sheet.less'; +@import './actors/party/header.less'; +@import './actors/party/party-members.less'; +@import './actors/party/sheet.less'; +@import './actors/party/inventory.less'; +@import './actors/party/resources.less'; + @import './items/beastform.less'; @import './items/class.less'; @import './items/domain-card.less'; diff --git a/styles/less/ui/chat/group-roll.less b/styles/less/ui/chat/group-roll.less new file mode 100644 index 00000000..02b8e312 --- /dev/null +++ b/styles/less/ui/chat/group-roll.less @@ -0,0 +1,210 @@ +.chat-message .message-content .group-roll { + display: flex; + flex-direction: column; + gap: 8px; + padding-bottom: 8px; + + .group-roll-section { + display: flex; + flex-direction: column; + gap: 4px; + + .group-roll-header { + display: flex; + align-items: center; + font-size: 14px; + margin-bottom: 0; + font-weight: normal; + + &.first { + margin-top: 5px; + } + + .group-roll-header-expand-section { + display: flex; + align-items: center; + gap: 4px; + cursor: pointer; + + label { + cursor: pointer; + } + + i { + color: light-dark(@dark-blue, @golden); + } + } + } + + .group-roll-content { + display: flex; + flex-direction: column; + gap: 16px; + border-radius: 5px; + padding: 5px; + overflow: hidden; + height: auto; + transition: all 0.3s ease; + + &.closed { + height: 0; + padding-top: 0; + padding-bottom: 0; + } + + &.finished { + background: light-dark(@dark-blue-10, @golden-10); + } + + .group-roll-main-roll { + display: flex; + flex-direction: column; + + .divider { + font-size: 14px; + margin-bottom: 0; + font-weight: normal; + } + + .main-roll-content { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + color: light-dark(@dark-blue, @golden); + + .main-value { + font-size: var(--font-size-24); + font-weight: bold; + } + + .main-text { + font-size: var(--font-size-16); + margin-top: 2px; + } + } + } + + .group-roll-member { + display: flex; + justify-content: space-between; + + .group-roll-data { + display: flex; + gap: 4px; + + img { + width: 42px; + height: 42px; + border-radius: 50%; + } + + .group-roll-label-container { + display: flex; + flex-direction: column; + justify-content: space-between; + + .group-roll-label-inner-container { + display: flex; + gap: 8px; + } + + .group-roll-modifier { + padding: 2px 8px; + border: 1px solid light-dark(@green, @green); + border-radius: 6px; + color: light-dark(@green, @green); + background: light-dark(@green-40, @green-40); + + &.failure { + border-color: light-dark(@red, @red); + color: light-dark(@red, @red); + background: light-dark(@red-40, @red-40); + } + } + + .group-roll-trait { + padding: 2px 8px; + border: 1px solid light-dark(white, white); + border-radius: 6px; + color: light-dark(white, white); + background: light-dark(@beige-80, @beige-80); + } + } + } + + .group-roll-rolling { + img { + width: 42px; + height: 42px; + + &:hover { + filter: drop-shadow(0 0 8px light-dark(@dark-blue, @golden)); + } + } + } + + .roll-results { + display: flex; + align-items: center; + border-radius: 5px; + width: fit-content; + gap: 16px; + cursor: pointer; + padding: 5px; + background: light-dark(@dark-blue-10, @golden-10); + color: light-dark(@dark-blue, @golden); + + &.finished { + background-color: initial; + } + + .reroll-result-container { + display: flex; + align-items: center; + gap: 16px; + + .label { + font-style: normal; + font-weight: 400; + font-size: var(--font-size-18); + line-height: 17px; + } + + i { + font-size: 16px; + } + + .success, + .success i { + color: @green; + } + + .failure, + .failure i { + color: @red; + } + } + + .group-roll-reroll { + position: relative; + display: flex; + align-items: center; + justify-content: center; + + .dice-icon { + width: 24px; + } + + .reroll-icon { + position: absolute; + font-size: 14px; + color: black; + filter: drop-shadow(0 0 3px black); + } + } + } + } + } + } +} diff --git a/styles/less/ui/countdown/countdown-edit.less b/styles/less/ui/countdown/countdown-edit.less index 1460c6ef..9051cccb 100644 --- a/styles/less/ui/countdown/countdown-edit.less +++ b/styles/less/ui/countdown/countdown-edit.less @@ -126,7 +126,7 @@ &.tiny { flex: 0; - input { + input { min-width: 2.5rem; } } diff --git a/styles/less/ui/index.less b/styles/less/ui/index.less index 4b1c0c4c..743d16ae 100644 --- a/styles/less/ui/index.less +++ b/styles/less/ui/index.less @@ -4,6 +4,7 @@ @import './chat/damage-summary.less'; @import './chat/downtime.less'; @import './chat/effect-summary.less'; +@import './chat/group-roll.less'; @import './chat/refresh-message.less'; @import './chat/sheet.less'; diff --git a/styles/less/ui/sidebar/tabs.less b/styles/less/ui/sidebar/tabs.less index 91bf0d23..ec4bbe9f 100644 --- a/styles/less/ui/sidebar/tabs.less +++ b/styles/less/ui/sidebar/tabs.less @@ -9,6 +9,7 @@ img { width: 22px; max-width: unset; + filter: @beige-filter; } } } diff --git a/styles/less/utils/colors.less b/styles/less/utils/colors.less index 489bbb29..6fcf6db2 100755 --- a/styles/less/utils/colors.less +++ b/styles/less/utils/colors.less @@ -4,6 +4,7 @@ @golden: #f3c267; @golden-10: #f3c26710; @golden-40: #f3c26740; +@golden-90: #f3c26790; @golden-bg: #f3c2671a; @golden-secondary: #eaaf42; @golden-filter: brightness(0) saturate(100%) invert(89%) sepia(13%) saturate(2008%) hue-rotate(332deg) brightness(99%) @@ -24,6 +25,7 @@ @medium-red-40: #d0474740; @dark-golden: #2b1d03; +@dark-golden-40: #2b1d0340; @dark-golden-80: #2b1d0380; @red: #e54e4e; diff --git a/styles/less/ux/autocomplete/autocomplete.less b/styles/less/ux/autocomplete/autocomplete.less index 808a8972..08854a53 100644 --- a/styles/less/ux/autocomplete/autocomplete.less +++ b/styles/less/ux/autocomplete/autocomplete.less @@ -1,3 +1,6 @@ +@import '../../utils/colors.less'; +@import '../../utils/fonts.less'; + .theme-light .autocomplete { background-image: url('../assets/parchments/dh-parchment-light.png'); color: black; @@ -27,11 +30,15 @@ } li[role='option'] { + display: flex; + align-items: center; + gap: 10px; font-size: var(--font-size-14); - padding-left: 10px; + padding: 0 10px; cursor: pointer; - &:hover { + &:hover, + &.selected { background-color: light-dark(@dark, @beige); color: light-dark(@beige, var(--color-dark-3)); } @@ -39,5 +46,16 @@ > div { white-space: nowrap; } + + img { + height: 40px; + width: 40px; + border-radius: 50%; + margin-bottom: 10px; + + &:first-child { + margin-top: 10px; + } + } } } diff --git a/system.json b/system.json index 2caeca2b..a16b3562 100644 --- a/system.json +++ b/system.json @@ -5,7 +5,7 @@ "version": "1.2.0", "compatibility": { "minimum": "13", - "verified": "13.347", + "verified": "13.350", "maximum": "13" }, "authors": [ @@ -220,6 +220,9 @@ }, "environment": { "htmlFields": ["notes", "description"] + }, + "party": { + "htmlFields": ["notes"] } }, "Item": { @@ -267,6 +270,8 @@ "adversaryRoll": {}, "damageRoll": {}, "abilityUse": {}, + "tagTeam": {}, + "groupRoll": {}, "systemMessage": {} } }, diff --git a/templates/dialogs/dice-roll/header.hbs b/templates/dialogs/dice-roll/header.hbs index f61a86b3..b455462c 100644 --- a/templates/dialogs/dice-roll/header.hbs +++ b/templates/dialogs/dice-roll/header.hbs @@ -1,14 +1,22 @@
-

- {{#if reactionOverride}} - {{localize "DAGGERHEART.CONFIG.ActionType.reaction"}} - {{else}} - {{ifThen rollConfig.headerTitle rollConfig.headerTitle rollConfig.title}} - {{/if}} - {{#if showReaction}} - - {{/if}} -

+
+

+ {{#if reactionOverride}} + {{localize "DAGGERHEART.CONFIG.ActionType.reaction"}} + {{else}} + {{ifThen rollConfig.headerTitle rollConfig.headerTitle rollConfig.title}} + {{/if}} + {{#if showReaction}} + + {{/if}} +

+
+ {{#if (and @root.hasRoll @root.activeTagTeamRoll)}} +
+ + {{localize "Tag Team Roll"}} +
+ {{/if}}
\ No newline at end of file diff --git a/templates/dialogs/group-roll/group-roll.hbs b/templates/dialogs/group-roll/group-roll.hbs new file mode 100644 index 00000000..ab655c1f --- /dev/null +++ b/templates/dialogs/group-roll/group-roll.hbs @@ -0,0 +1,84 @@ +
+
+

{{localize "DAGGERHEART.UI.Chat.groupRoll.title"}}

+
+ +
+ {{localize "DAGGERHEART.UI.Chat.groupRoll.leader"}} + {{#unless leader.actor}} + +
+ {{localize "DAGGERHEART.UI.Chat.groupRoll.selectLeader"}} +
+ {{else}} +
+ {{leader.actor.name}} +
+ {{leader.actor.name}} +
+
+ + +
+ {{!-- Not used yet --}} + {{!--
+ + +
--}} +
+
+
+ + + +
+
+ {{/unless}} +
+ +
+ {{localize "DAGGERHEART.UI.Chat.groupRoll.partyTeam"}} + + + + {{#if (gt this.members.length 0)}} + {{#each members as |member index|}} +
+ {{member.actor.name}} +
+ {{member.actor.name}} +
+
+ + +
+ {{!-- Not used yet --}} + {{!--
+ + +
--}} +
+
+
+ + + +
+
+ {{/each}} + {{/if}} + {{#unless allSelected}} +
+ {{localize "DAGGERHEART.UI.Chat.groupRoll.selectMember"}} +
+ {{/unless}} +
+ +
\ No newline at end of file diff --git a/templates/dialogs/tagTeamDialog.hbs b/templates/dialogs/tagTeamDialog.hbs new file mode 100644 index 00000000..3c96a573 --- /dev/null +++ b/templates/dialogs/tagTeamDialog.hbs @@ -0,0 +1,110 @@ +
+
+
+ {{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/hud/tokenHUD.hbs b/templates/hud/tokenHUD.hbs index 197b94f7..0ea047c5 100644 --- a/templates/hud/tokenHUD.hbs +++ b/templates/hud/tokenHUD.hbs @@ -40,6 +40,7 @@ {{/if}} + {{#if usesEffects}} @@ -54,6 +55,13 @@ {{/each}} {{/if}} + {{/if}} + + {{#if (eq actorType 'party')}} + + {{/if}} + + {{!-- NOT YET IMPLEMENTED --}} + {{!-- --}} + + +
+ {{localize tabs.partyMembers.label}} +
    + {{#each document.system.partyMembers as |actor id|}} + {{> 'daggerheart.inventory-item' + item=actor + type='character' + isActor=true + }} + {{/each}} +
+ {{#unless document.system.partyMembers.length}} +
+ {{localize "DAGGERHEART.GENERAL.dropActorsHere"}} +
+ {{/unless}} +
+ \ No newline at end of file diff --git a/templates/sheets/actors/party/projects.hbs b/templates/sheets/actors/party/projects.hbs new file mode 100644 index 00000000..6338626e --- /dev/null +++ b/templates/sheets/actors/party/projects.hbs @@ -0,0 +1,4 @@ +
+

Soon tm

+
\ No newline at end of file diff --git a/templates/sheets/actors/party/resources.hbs b/templates/sheets/actors/party/resources.hbs new file mode 100644 index 00000000..edb58248 --- /dev/null +++ b/templates/sheets/actors/party/resources.hbs @@ -0,0 +1,99 @@ +
+
+ + +
+ +
+ {{localize tabs.resources.label}} +
    + {{#each document.system.partyMembers as |actor id|}} +
  • +

    {{actor.name}}

    + +
    +
    +
    + {{#times actor.system.resources.hitPoints.max}} + + + {{/times}} +
    +
    + {{localize "DAGGERHEART.GENERAL.HitPoints.short"}} + {{actor.system.resources.hitPoints.value}} / {{actor.system.resources.hitPoints.max}} +
    +
    + +
    +
    + {{#times actor.system.resources.stress.max}} + + + {{/times}} +
    +
    + {{localize "DAGGERHEART.GENERAL.stress"}} + {{actor.system.resources.stress.value}} / {{actor.system.resources.stress.max}} +
    +
    + + {{#if actor.system.armor.system.marks}} +
    + +
    + {{localize "DAGGERHEART.GENERAL.armorSlots"}} + {{actor.system.armor.system.marks.value}} / {{actor.system.armorScore}} +
    +
    + {{/if}} + + +
    +

    {{localize "DAGGERHEART.GENERAL.hope"}}

    + {{#times actor.system.resources.hope.max}} + + {{#if (gte actor.system.resources.hope.value (add this 1))}} + + {{else}} + + {{/if}} + + {{/times}} +
    +
    +

    {{localize "DAGGERHEART.GENERAL.DamageThresholds.minor"}}

    +

    {{actor.system.damageThresholds.major}}

    +

    {{localize "DAGGERHEART.GENERAL.DamageThresholds.major"}}

    +

    {{actor.system.damageThresholds.severe}}

    +

    {{localize "DAGGERHEART.GENERAL.DamageThresholds.severe"}}

    +
    +
    +
  • + {{/each}} +
+
+
\ No newline at end of file diff --git a/templates/sheets/global/partials/inventory-fieldset-items-V2.hbs b/templates/sheets/global/partials/inventory-fieldset-items-V2.hbs index 1ef065d5..e97bfd80 100644 --- a/templates/sheets/global/partials/inventory-fieldset-items-V2.hbs +++ b/templates/sheets/global/partials/inventory-fieldset-items-V2.hbs @@ -10,6 +10,7 @@ Parameters: - isGlassy {boolean} : If true, applies the 'glassy' class to the fieldset. - cardView {boolean} : If true and type is 'domainCard', renders using domain card layout. - isActor {boolean} : Passed through to inventory-item partials. +- actorType {boolean} : The actor type of the parent actor - canCreate {boolean} : If true, show createDoc anchor on legend - inVault {boolean} : If true, the domainCard is created with inVault=true - disabled {boolean}: If true, the ActiveEffect is created with disabled=true; @@ -54,6 +55,7 @@ Parameters: {{> 'daggerheart.inventory-item' item=item type=../type + actorType=../actorType hideControls=../hideControls hideContextMenu=../hideContextMenu isActor=../isActor diff --git a/templates/sheets/global/partials/inventory-item-V2.hbs b/templates/sheets/global/partials/inventory-item-V2.hbs index 96a36e97..561e66c0 100644 --- a/templates/sheets/global/partials/inventory-item-V2.hbs +++ b/templates/sheets/global/partials/inventory-item-V2.hbs @@ -4,6 +4,7 @@ Parameters: - type {string} : The type of items in the list - isActor {boolean} : Passed through to inventory-item partials. +- actorType {boolean} : The actor type of the parent actor - categoryAdversary {string} : Category adversary id. - noExtensible {boolean} : If true, the inventory-item-content would be collapsable/extendible else it always be showed - hideLabels {boolean} : If true, hide label-tags else show label-tags. @@ -17,7 +18,7 @@ Parameters:
  • -
    +
    {{!-- Image --}}
    {{!-- Item Name --}} - {{localize item.name}} {{#unless noExtensible}}{{/unless}} + {{localize item.name}} {{#unless (or noExtensible (not item.system.description))}}{{/unless}} {{!-- Tags Start --}} {{#with item}} @@ -75,18 +76,30 @@ Parameters: {{/if}} + {{#if (eq type 'character')}} + + + + {{/if}} {{else}} - {{#if (eq type 'weapon')}} - - - - {{else if (eq type 'armor')}} - - - - {{else if (eq type 'domainCard')}} + {{#unless (eq actorType 'party')}} + {{#if (eq type 'weapon')}} + + + + {{else if (eq type 'armor')}} + + + + {{/if}} + {{else}} + + + + {{/unless}} + {{#if (eq type 'domainCard')}} @@ -97,7 +110,7 @@ Parameters: {{/if}} - {{#if (hasProperty item "toChat")}} + {{#if (and (hasProperty item "toChat") (not (eq actorType 'party')))}} diff --git a/templates/sheets/global/partials/resource-bar.hbs b/templates/sheets/global/partials/resource-bar.hbs new file mode 100644 index 00000000..c0d13e54 --- /dev/null +++ b/templates/sheets/global/partials/resource-bar.hbs @@ -0,0 +1,35 @@ +
    + {{#if useResourcePips}} +
    +
    + {{#times resource.max}} + + + {{/times}} + + {{#times resource.emptyPips}} + + {{/times}} +
    +
    + {{localize label}} + {{resource.value}} / {{resource.max}} +
    +
    + {{else}} +
    + + / + {{resource.max}} +
    + +
    +

    {{localize label}}

    +
    + {{/if}} +
    \ No newline at end of file diff --git a/templates/ui/chat/groupRoll.hbs b/templates/ui/chat/groupRoll.hbs new file mode 100644 index 00000000..83cc4ce9 --- /dev/null +++ b/templates/ui/chat/groupRoll.hbs @@ -0,0 +1,124 @@ +
    +
    +

    + +

    +
    +
    +
    + +
    +
    {{system.leader.actor.name}}
    +
    + {{#unless (isNullish system.leader.manualSuccess)}} + + {{/unless}} +
    {{localize (concat "DAGGERHEART.CONFIG.Traits." system.leader.trait ".name")}}
    +
    +
    +
    + {{#unless system.leader.result}} +
    + +
    + {{else}} +
    + {{#if (isNullish system.leader.manualSuccess)}} +
    + + +
    + + + + + + {{else}} +
    + {{#if system.leader.manualSuccess}} + + {{else}} + + {{/if}} +
    + {{/if}} +
    + {{/unless}} +
    + {{#if system.leader.result}} +
    +

    + {{localize "DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle" ability=(localize (concat "DAGGERHEART.CONFIG.Traits." system.leader.trait ".name"))}} +

    + + + {{system.leader.result.total}} + {{#unless (isNullish system.totalModifier)}} + {{#if (gte system.totalModifier 0)}}+{{else}}-{{/if}} + {{positive system.totalModifier}} = {{add system.leader.result.total system.totalModifier}} + {{/unless}} + + {{localize "DAGGERHEART.GENERAL.withThing" thing=system.leader.result.result.label}} + +
    + {{/if}} +
    +
    +
    +

    + + + + + +

    +
    + {{#each system.members as |member index|}} +
    +
    + +
    +
    {{member.actor.name}}
    +
    + {{#unless (isNullish member.manualSuccess)}} + + {{/unless}} +
    {{localize (concat "DAGGERHEART.CONFIG.Traits." member.trait ".name")}}
    +
    +
    +
    + {{#unless member.result}} +
    + +
    + {{else}} +
    + {{#if (isNullish member.manualSuccess)}} +
    + {{member.result.total}} + + + +
    + + + + + + {{else}} +
    + {{member.result.total}} + {{#if member.manualSuccess}} + + {{else}} + + {{/if}} +
    + {{/if}} +
    + {{/unless}} +
    + {{/each}} +
    +
    +
    \ No newline at end of file