diff --git a/assets/icons/arrow-dunk.png b/assets/icons/arrow-dunk.png new file mode 100644 index 00000000..1958713e Binary files /dev/null and b/assets/icons/arrow-dunk.png differ 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