diff --git a/daggerheart.mjs b/daggerheart.mjs index 7c72e421..2eb5109f 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -43,6 +43,7 @@ CONFIG.Item.dataModels = models.items.config; CONFIG.ActiveEffect.documentClass = documents.DhActiveEffect; CONFIG.ActiveEffect.dataModels = models.activeEffects.config; +CONFIG.ActiveEffect.changeTypes = { ...CONFIG.ActiveEffect.changeTypes, ...models.activeEffects.changeEffects }; CONFIG.Combat.documentClass = documents.DhpCombat; CONFIG.Combat.dataModels = { base: models.DhCombat }; @@ -211,6 +212,7 @@ Hooks.once('init', () => { SYSTEM.id, applications.sheetConfigs.ActiveEffectConfig, { + types: ['base', 'beastform', 'horde'], makeDefault: true, label: sheetLabel('DOCUMENT.ActiveEffect') } @@ -268,7 +270,6 @@ Hooks.on('setup', () => { ...damageThresholds, 'proficiency', 'evasion', - 'armorScore', 'scars', 'levelData.level.current' ] @@ -402,6 +403,17 @@ Hooks.on('chatMessage', (_, message) => { } }); +Hooks.on(CONFIG.DH.HOOKS.hooksConfig.tagTeamStart, async data => { + if (data.openForAllPlayers && data.partyId) { + const party = game.actors.get(data.partyId); + if (!party) return; + + const dialog = new game.system.api.applications.dialogs.TagTeamDialog(party); + dialog.tabGroups.application = 'tagTeamRoll'; + await dialog.render({ force: true }); + } +}); + const updateActorsRangeDependentEffects = async token => { const rangeMeasurement = game.settings.get( CONFIG.DH.id, diff --git a/lang/en.json b/lang/en.json index 114176b1..74bfa77a 100755 --- a/lang/en.json +++ b/lang/en.json @@ -452,7 +452,7 @@ "text": "Are you sure you want to delete {name}?" }, "DamageReduction": { - "armorMarks": "Armor Marks", + "maxUseableArmor": "Useable Armor Slots", "armorWithStress": "Spend 1 stress to use an extra mark", "thresholdImmunities": "Threshold Immunities", "stress": "Stress", @@ -678,16 +678,35 @@ }, "TagTeamSelect": { "title": "Tag Team Roll", + "FIELDS": { + "initiator": { + "memberId": { "label": "Initiating Character" }, + "cost": { "label": "Initiation Cost" } + } + }, "leaderTitle": "Initiating Character", "membersTitle": "Participants", "partyTeam": "Party Team", "hopeCost": "Hope Cost", "initiatingCharacter": "Initiating Character", + "selectParticipants": "Select the two participants", + "startTagTeamRoll": "Start Tag Team Roll", + "openDialogForAll": "Open Dialog For All", + "rollType": "Roll Type", + "makeYourRoll": "Make your roll", + "cancelTagTeamRoll": "Cancel Tag Team Roll", + "finishTagTeamRoll": "Finish Tag Team Roll", "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" + "createTagTeam": "Create Tag Team Roll", + "chatMessageRollTitle": "Roll", + "cancelConfirmTitle": "Cancel Tag Team Roll", + "cancelConfirmText": "Are you sure you want to cancel the Tag Team Roll? This will close it for all other players too.", + "hints": { + "completeRolls": "Set up and complete the rolls for the characters", + "selectRoll": "Select which roll value to be used for the Tag Team" + } }, "TokenConfig": { "actorSizeUsed": "Actor size is set, determining the dimensions" @@ -776,6 +795,11 @@ "bruiser": "for each Bruiser adversary.", "solo": "for each Solo adversary." }, + "ArmorInteraction": { + "none": { "label": "Ignores Armor" }, + "active": { "label": "Active w/ Armor" }, + "inactive": { "label": "Inactive w/ Armor" } + }, "ArmorFeature": { "burning": { "name": "Burning", @@ -1231,6 +1255,11 @@ "selectType": "Select Action Type", "selectAction": "Action Selection" }, + "TagTeamRollTypes": { + "trait": "Trait", + "ability": "Ability", + "damageAbility": "Damage Ability" + }, "TargetTypes": { "any": "Any", "friendly": "Friendly", @@ -1859,6 +1888,17 @@ "name": "Healing Roll" } }, + "ChangeTypes": { + "armor": { + "newArmorEffect": "Armor Effect", + "FIELDS": { + "interaction": { + "label": "Armor Interaction", + "hint": "Does the character wearing armor suppress this effect?" + } + } + } + }, "Duration": { "passive": "Passive", "temporary": "Temporary" @@ -1883,6 +1923,10 @@ } }, "GENERAL": { + "Ability": { + "single": "Ability", + "plural": "Abilities" + }, "Action": { "single": "Action", "plural": "Actions" @@ -2264,6 +2308,7 @@ "duality": "Duality", "dualityDice": "Duality Dice", "dualityRoll": "Duality Roll", + "effect": "Effect", "enabled": "Enabled", "evasion": "Evasion", "equipment": "Equipment", @@ -2336,6 +2381,10 @@ "rerolled": "Rerolled", "rerollThing": "Reroll {thing}", "resource": "Resource", + "result": { + "single": "Result", + "plural": "Results" + }, "roll": "Roll", "rollAll": "Roll All", "rollDamage": "Roll Damage", @@ -3068,6 +3117,9 @@ "knowTheTide": "Know The Tide gained a token", "lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}" }, + "Progress": { + "migrationLabel": "Performing system migration. Please wait and do not close Foundry." + }, "Sidebar": { "actorDirectory": { "tier": "Tier {tier} {type}", diff --git a/module/applications/dialogs/d20RollDialog.mjs b/module/applications/dialogs/d20RollDialog.mjs index 8e79ba58..0fdb1896 100644 --- a/module/applications/dialogs/d20RollDialog.mjs +++ b/module/applications/dialogs/d20RollDialog.mjs @@ -35,7 +35,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio updateIsAdvantage: this.updateIsAdvantage, selectExperience: this.selectExperience, toggleReaction: this.toggleReaction, - toggleTagTeamRoll: this.toggleTagTeamRoll, toggleSelectedEffect: this.toggleSelectedEffect, submitRoll: this.submitRoll }, @@ -133,12 +132,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio context.reactionOverride = this.reactionOverride; } - const tagTeamSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); - if (this.actor && tagTeamSetting.members[this.actor.id] && !this.config.skips?.createMessage) { - context.activeTagTeamRoll = true; - context.tagTeamSelected = this.config.tagTeamSelected; - } - return context; } @@ -215,11 +208,6 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio } } - static toggleTagTeamRoll() { - this.config.tagTeamSelected = !this.config.tagTeamSelected; - this.render(); - } - static toggleSelectedEffect(_event, button) { this.selectedEffects[button.dataset.key].selected = !this.selectedEffects[button.dataset.key].selected; this.render(); diff --git a/module/applications/dialogs/damageReductionDialog.mjs b/module/applications/dialogs/damageReductionDialog.mjs index cd0a5cf7..930ca1a1 100644 --- a/module/applications/dialogs/damageReductionDialog.mjs +++ b/module/applications/dialogs/damageReductionDialog.mjs @@ -1,4 +1,4 @@ -import { damageKeyToNumber, getDamageLabel } from '../../helpers/utils.mjs'; +import { damageKeyToNumber, getArmorSources, getDamageLabel } from '../../helpers/utils.mjs'; const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; @@ -10,6 +10,7 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap this.reject = reject; this.actor = actor; this.damage = damage; + this.damageType = damageType; this.rulesDefault = game.settings.get( CONFIG.DH.id, @@ -20,14 +21,20 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap this.rulesDefault ); - const canApplyArmor = damageType.every(t => actor.system.armorApplicableDamageTypes[t] === true); - const availableArmor = actor.system.armorScore - actor.system.armor.system.marks.value; - const maxArmorMarks = canApplyArmor ? availableArmor : 0; + const orderedArmorSources = getArmorSources(actor).filter(s => !s.disabled); + const armor = orderedArmorSources.reduce((acc, { document }) => { + const { current, max } = document.type === 'armor' ? document.system.armor : document.system.armorData; + acc.push({ + effect: document, + marks: [...Array(max).keys()].reduce((acc, _, index) => { + const spent = index < current; + acc[foundry.utils.randomID()] = { selected: false, disabled: spent, spent }; + return acc; + }, {}) + }); - const armor = [...Array(maxArmorMarks).keys()].reduce((acc, _) => { - acc[foundry.utils.randomID()] = { selected: false }; return acc; - }, {}); + }, []); const stress = [...Array(actor.system.rules.damageReduction.maxArmorMarked.stressExtra ?? 0).keys()].reduce( (acc, _) => { acc[foundry.utils.randomID()] = { selected: false }; @@ -121,13 +128,11 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap context.thresholdImmunities = Object.keys(this.thresholdImmunities).length > 0 ? this.thresholdImmunities : null; - const { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage } = + const { selectedStressMarks, stressReductions, currentMarks, currentDamage, maxArmorUsed, availableArmor } = this.getDamageInfo(); - context.armorScore = this.actor.system.armorScore; + context.armorScore = this.actor.system.armorScore.max; context.armorMarks = currentMarks; - context.basicMarksUsed = - selectedArmorMarks.length === this.actor.system.rules.damageReduction.maxArmorMarked.value; const stressReductionStress = this.availableStressReductions ? stressReductions.reduce((acc, red) => acc + red.cost, 0) @@ -141,16 +146,30 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap } : null; - const maxArmor = this.actor.system.rules.damageReduction.maxArmorMarked.value; - context.marks = { - armor: Object.keys(this.marks.armor).reduce((acc, key, index) => { - const mark = this.marks.armor[key]; - if (!this.rulesOn || index + 1 <= maxArmor) acc[key] = mark; + context.maxArmorUsed = maxArmorUsed; + context.availableArmor = availableArmor; + context.basicMarksUsed = availableArmor === 0 || selectedStressMarks.length; - return acc; - }, {}), + const armorSources = []; + for (const source of this.marks.armor) { + const parent = source.effect.origin + ? await foundry.utils.fromUuid(source.effect.origin) + : source.effect.parent; + + const useEffectName = parent.type === 'armor' || parent instanceof Actor; + const label = useEffectName ? source.effect.name : parent.name; + armorSources.push({ + label: label, + uuid: source.effect.uuid, + marks: source.marks + }); + } + context.marks = { + armor: armorSources, stress: this.marks.stress }; + + context.usesStressArmor = Object.keys(context.marks.stress).length; context.availableStressReductions = this.availableStressReductions; context.damage = getDamageLabel(this.damage); @@ -167,27 +186,31 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap } getDamageInfo = () => { - const selectedArmorMarks = Object.values(this.marks.armor).filter(x => x.selected); + const selectedArmorMarks = this.marks.armor.flatMap(x => Object.values(x.marks).filter(x => x.selected)); const selectedStressMarks = Object.values(this.marks.stress).filter(x => x.selected); const stressReductions = this.availableStressReductions ? Object.values(this.availableStressReductions).filter(red => red.selected) : []; - const currentMarks = - this.actor.system.armor.system.marks.value + selectedArmorMarks.length + selectedStressMarks.length; + const currentMarks = this.actor.system.armorScore.value + selectedArmorMarks.length; + + const maxArmorUsed = this.actor.system.rules.damageReduction.maxArmorMarked.value + selectedStressMarks.length; + const availableArmor = + maxArmorUsed - + this.marks.armor.reduce((acc, source) => { + acc += Object.values(source.marks).filter(x => x.selected).length; + return acc; + }, 0); const armorMarkReduction = selectedArmorMarks.length * this.actor.system.rules.damageReduction.increasePerArmorMark; - let currentDamage = Math.max( - this.damage - armorMarkReduction - selectedStressMarks.length - stressReductions.length, - 0 - ); + let currentDamage = Math.max(this.damage - armorMarkReduction - stressReductions.length, 0); if (this.reduceSeverity) { currentDamage = Math.max(currentDamage - this.reduceSeverity, 0); } if (this.thresholdImmunities[currentDamage]) currentDamage = 0; - return { selectedArmorMarks, selectedStressMarks, stressReductions, currentMarks, currentDamage }; + return { selectedStressMarks, stressReductions, currentMarks, currentDamage, maxArmorUsed, availableArmor }; }; static toggleRules() { @@ -195,13 +218,10 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap const maxArmor = this.actor.system.rules.damageReduction.maxArmorMarked.value; this.marks = { - armor: Object.keys(this.marks.armor).reduce((acc, key, index) => { - const mark = this.marks.armor[key]; + armor: this.marks.armor.map((mark, index) => { const keepSelectValue = !this.rulesOn || index + 1 <= maxArmor; - acc[key] = { ...mark, selected: keepSelectValue ? mark.selected : false }; - - return acc; - }, {}), + return { ...mark, selected: keepSelectValue ? mark.selected : false }; + }), stress: this.marks.stress }; @@ -209,8 +229,8 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap } static setMarks(_, target) { - const currentMark = this.marks[target.dataset.type][target.dataset.key]; - const { selectedStressMarks, stressReductions, currentMarks, currentDamage } = this.getDamageInfo(); + const currentMark = foundry.utils.getProperty(this.marks, target.dataset.path); + const { selectedStressMarks, stressReductions, currentDamage, availableArmor } = this.getDamageInfo(); if (!currentMark.selected && currentDamage === 0) { ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.damageAlreadyNone')); @@ -218,12 +238,18 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap } if (this.rulesOn) { - if (!currentMark.selected && currentMarks === this.actor.system.armorScore) { + if (target.dataset.type === 'armor' && !currentMark.selected && !availableArmor) { ui.notifications.info(game.i18n.localize('DAGGERHEART.UI.Notifications.noAvailableArmorMarks')); return; } } + const stressUsed = selectedStressMarks.length; + if (target.dataset.type === 'armor' && stressUsed) { + const updateResult = this.updateStressArmor(target.dataset.id, !currentMark.selected); + if (updateResult === false) return; + } + if (currentMark.selected) { const currentDamageLabel = getDamageLabel(currentDamage); for (let reduction of stressReductions) { @@ -232,8 +258,16 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap } } - if (target.dataset.type === 'armor' && selectedStressMarks.length > 0) { - selectedStressMarks.forEach(mark => (mark.selected = false)); + if (target.dataset.type === 'stress' && currentMark.armorMarkId) { + for (const source of this.marks.armor) { + const match = Object.keys(source.marks).find(key => key === currentMark.armorMarkId); + if (match) { + source.marks[match].selected = false; + break; + } + } + + currentMark.armorMarkId = null; } } @@ -241,6 +275,25 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap this.render(); } + updateStressArmor(armorMarkId, select) { + let stressMarkKey = null; + if (select) { + stressMarkKey = Object.keys(this.marks.stress).find( + key => this.marks.stress[key].selected && !this.marks.stress[key].armorMarkId + ); + } else { + stressMarkKey = Object.keys(this.marks.stress).find( + key => this.marks.stress[key].armorMarkId === armorMarkId + ); + if (!stressMarkKey) + stressMarkKey = Object.keys(this.marks.stress).find(key => this.marks.stress[key].selected); + } + + if (!stressMarkKey) return false; + + this.marks.stress[stressMarkKey].armorMarkId = select ? armorMarkId : null; + } + static useStressReduction(_, target) { const damageValue = Number(target.dataset.reduction); const stressReduction = this.availableStressReductions[damageValue]; @@ -279,11 +332,18 @@ export default class DamageReductionDialog extends HandlebarsApplicationMixin(Ap } static async takeDamage() { - const { selectedArmorMarks, selectedStressMarks, stressReductions, currentDamage } = this.getDamageInfo(); - const armorSpent = selectedArmorMarks.length + selectedStressMarks.length; - const stressSpent = selectedStressMarks.length + stressReductions.reduce((acc, red) => acc + red.cost, 0); + const { selectedStressMarks, stressReductions, currentDamage } = this.getDamageInfo(); + const armorChanges = this.marks.armor.reduce((acc, source) => { + const amount = Object.values(source.marks).filter(x => x.selected).length; + if (amount) acc.push({ uuid: source.effect.uuid, amount }); - this.resolve({ modifiedDamage: currentDamage, armorSpent, stressSpent }); + return acc; + }, []); + const stressSpent = + selectedStressMarks.filter(x => x.armorMarkId).length + + stressReductions.reduce((acc, red) => acc + red.cost, 0); + + this.resolve({ modifiedDamage: currentDamage, armorChanges, stressSpent }); await this.close(true); } diff --git a/module/applications/dialogs/downtime.mjs b/module/applications/dialogs/downtime.mjs index 52cced3e..3475dee7 100644 --- a/module/applications/dialogs/downtime.mjs +++ b/module/applications/dialogs/downtime.mjs @@ -203,7 +203,7 @@ export default class DhpDowntime extends HandlebarsApplicationMixin(ApplicationV const msg = { user: game.user.id, system: { - moves: moves, + moves: moves.map(move => ({ ...move, actions: Array.from(move.actions) })), actor: this.actor.uuid }, speaker: cls.getSpeaker(), diff --git a/module/applications/dialogs/rerollDamageDialog.mjs b/module/applications/dialogs/rerollDamageDialog.mjs index e1b75eb7..b821bd24 100644 --- a/module/applications/dialogs/rerollDamageDialog.mjs +++ b/module/applications/dialogs/rerollDamageDialog.mjs @@ -1,5 +1,3 @@ -import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; - const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; export default class RerollDamageDialog extends HandlebarsApplicationMixin(ApplicationV2) { @@ -123,16 +121,8 @@ export default class RerollDamageDialog extends HandlebarsApplicationMixin(Appli return acc; }, {}) }; + 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 index c28b773c..ddaabcb4 100644 --- a/module/applications/dialogs/tagTeamDialog.mjs +++ b/module/applications/dialogs/tagTeamDialog.mjs @@ -1,5 +1,7 @@ +import { MemberData } from '../../data/tagTeamData.mjs'; import { getCritDamageBonus } from '../../helpers/utils.mjs'; -import { GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; +import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; +import Party from '../sheets/actors/party.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -7,15 +9,23 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio constructor(party) { super(); - this.data = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); this.party = party; + this.partyMembers = party.system.partyMembers + .filter(x => Party.DICE_ROLL_ACTOR_TYPES.includes(x.type)) + .map(member => ({ + ...member.toObject(), + uuid: member.uuid, + id: member.id, + selected: false + })); + this.intiator = null; + this.openForAllPlayers = true; - this.setupHooks = Hooks.on(socketEvent.Refresh, ({ refreshType }) => { - if (refreshType === RefreshType.TagTeamRoll) { - this.data = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); - this.render(); - } - }); + this.tabGroups.application = Object.keys(party.system.tagTeam.members).length + ? 'tagTeamRoll' + : 'initialization'; + + Hooks.on(socketEvent.Refresh, this.tagTeamRefresh.bind()); } get title() { @@ -24,324 +34,639 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio static DEFAULT_OPTIONS = { tag: 'form', + id: 'TagTeamDialog', classes: ['daggerheart', 'views', 'dh-style', 'dialog', 'tag-team-dialog'], position: { width: 550, height: 'auto' }, actions: { - removeMember: TagTeamDialog.#removeMember, - unlinkMessage: TagTeamDialog.#unlinkMessage, - selectMessage: TagTeamDialog.#selectMessage, - createTagTeam: TagTeamDialog.#createTagTeam + toggleSelectMember: TagTeamDialog.#toggleSelectMember, + startTagTeamRoll: TagTeamDialog.#startTagTeamRoll, + makeRoll: TagTeamDialog.#makeRoll, + removeRoll: TagTeamDialog.#removeRoll, + rerollDice: TagTeamDialog.#rerollDice, + makeDamageRoll: TagTeamDialog.#makeDamageRoll, + removeDamageRoll: TagTeamDialog.#removeDamageRoll, + rerollDamageDice: TagTeamDialog.#rerollDamageDice, + selectRoll: TagTeamDialog.#selectRoll, + cancelRoll: TagTeamDialog.#onCancelRoll, + finishRoll: TagTeamDialog.#finishRoll }, form: { handler: this.updateData, submitOnChange: true, closeOnSubmit: false } }; static PARTS = { - application: { - id: 'tag-team-dialog', - template: 'systems/daggerheart/templates/dialogs/tagTeamDialog.hbs' + initialization: { + id: 'initialization', + template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/initialization.hbs' + }, + tagTeamRoll: { + id: 'tagTeamRoll', + template: 'systems/daggerheart/templates/dialogs/tagTeamDialog/tagTeamRoll.hbs' } }; + /** @inheritdoc */ + static TABS = { + application: { + tabs: [{ id: 'initialization' }, { id: 'tagTeamRoll' }] + } + }; + + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + + for (const element of htmlElement.querySelectorAll('.roll-type-select')) + element.addEventListener('change', this.updateRollType.bind(this)); + } + async _prepareContext(_options) { const context = await super._prepareContext(_options); - context.hopeCost = this.hopeCost; - context.data = this.data; - - context.memberOptions = this.party.filter(c => !this.data.members[c.id]); - context.selectedCharacterOptions = this.party.filter(c => this.data.members[c.id]); - - context.members = Object.keys(this.data.members).map(id => { - const roll = this.data.members[id].messageId ? game.messages.get(this.data.members[id].messageId) : null; - - context.usesDamage = - context.usesDamage === undefined - ? roll?.system.hasDamage - : context.usesDamage && roll?.system.hasDamage; - return { - character: this.party.find(x => x.id === id), - selected: this.data.members[id].selected, - roll: roll, - damageValues: roll - ? Object.keys(roll.system.damage).map(key => ({ - key: key, - name: game.i18n.localize(CONFIG.DH.GENERAL.healingTypes[key].label), - total: roll.system.damage[key].total - })) - : null - }; - }); - - const initiatorChar = this.party.find(x => x.id === this.data.initiator.id); - context.initiator = { - character: initiatorChar, - cost: this.data.initiator.cost - }; - - const selectedMember = Object.values(context.members).find(x => x.selected && x.roll); - const selectedIsCritical = selectedMember?.roll?.system?.isCritical; - context.selectedData = { - result: selectedMember - ? `${selectedMember.roll.system.roll.total} ${selectedMember.roll.system.roll.result.label}` - : null, - damageValues: null - }; - - for (const member of Object.values(context.members)) { - if (!member.roll) continue; - if (context.usesDamage) { - if (!context.selectedData.damageValues) context.selectedData.damageValues = {}; - for (let damage of member.damageValues) { - const damageTotal = member.roll.system.isCritical - ? damage.total - : selectedIsCritical - ? damage.total + (await getCritDamageBonus(member.roll.system.damage[damage.key].formula)) - : damage.total; - if (context.selectedData.damageValues[damage.key]) { - context.selectedData.damageValues[damage.key].total += damageTotal; - } else { - context.selectedData.damageValues[damage.key] = { - ...foundry.utils.deepClone(damage), - total: damageTotal - }; - } - } - } - } - - context.showResult = Object.values(context.members).reduce((enabled, member) => { - if (!member.roll) return enabled; - if (context.usesDamage) { - enabled = enabled === null ? member.damageValues.length > 0 : enabled && member.damageValues.length > 0; - } else { - enabled = enabled === null ? Boolean(member.roll) : enabled && Boolean(member.roll); - } - - return enabled; - }, null); - - context.createDisabled = - !context.selectedData.result || - !this.data.initiator.id || - Object.keys(this.data.members).length === 0 || - Object.values(context.members).some(x => - context.usesDamage ? !x.damageValues || x.damageValues.length === 0 : !x.roll - ); + context.isEditable = this.getIsEditable(); return context; } - async updateSource(update) { - await this.data.updateSource(update); + async _preparePartContext(partId, context, options) { + const partContext = await super._preparePartContext(partId, context, options); + switch (partId) { + case 'initialization': + partContext.tagTeamFields = this.party.system.schema.fields.tagTeam.fields; + partContext.memberSelection = this.partyMembers; + const selectedMembers = partContext.memberSelection.filter(x => x.selected); - 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}`, { + partContext.allSelected = selectedMembers.length === 2; + partContext.canStartTagTeam = partContext.allSelected && this.initiator; + partContext.initiator = this.initiator; + partContext.initiatorOptions = selectedMembers.map(x => ({ value: x.id, label: x.name })); + partContext.initiatorDisabled = !selectedMembers.length; + partContext.openForAllPlayers = this.openForAllPlayers; + + break; + case 'tagTeamRoll': + partContext.fields = this.party.system.schema.fields.tagTeam.fields; + partContext.data = this.party.system.tagTeam; + partContext.rollTypes = CONFIG.DH.GENERAL.tagTeamRollTypes; + partContext.traitOptions = CONFIG.DH.ACTOR.abilities; + + const selectedRoll = Object.values(this.party.system.tagTeam.members).find(member => member.selected); + const critSelected = !selectedRoll + ? undefined + : (selectedRoll?.rollData?.options?.roll?.isCritical ?? false); + + partContext.members = {}; + for (const actorId in this.party.system.tagTeam.members) { + const data = this.party.system.tagTeam.members[actorId]; + const actor = game.actors.get(actorId); + + const rollOptions = []; + const damageRollOptions = []; + for (const item of actor.items) { + if (item.system.metadata.hasActions) { + const actions = [ + ...item.system.actions, + ...(item.system.attack ? [item.system.attack] : []) + ]; + for (const action of actions) { + if (action.hasRoll) { + const actionItem = { + value: action.uuid, + label: action.name, + group: item.name, + baseAction: action.baseAction + }; + + if (action.hasDamage) damageRollOptions.push(actionItem); + else rollOptions.push(actionItem); + } + } + } + } + + const damage = data.rollData?.options?.damage; + partContext.hasDamage |= Boolean(damage); + const critHitPointsDamage = await this.getCriticalDamage(damage); + + partContext.members[actorId] = { + ...data, + isEditable: actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER), + key: actorId, + readyToRoll: Boolean(data.rollChoice), + hasRolled: Boolean(data.rollData), + rollOptions, + damageRollOptions, + damage: damage, + critDamage: critHitPointsDamage, + useCritDamage: + critSelected || (critSelected === undefined && data.rollData?.options?.roll?.isCritical) + }; + } + + partContext.hintText = await this.getInfoTexts(this.party.system.tagTeam.members); + partContext.joinedRoll = await this.getJoinedRoll({ + overrideIsCritical: critSelected, + displayVersion: true + }); + + break; + } + + return partContext; + } + + static async updateData(_event, _, formData) { + const { initiator, openForAllPlayers, ...partyData } = foundry.utils.expandObject(formData.object); + this.initiator = initiator; + this.openForAllPlayers = openForAllPlayers !== undefined ? openForAllPlayers : this.openForAllPlayers; + + this.updatePartyData(partyData); + } + + async updatePartyData(update, options = { render: true }) { + const gmUpdate = async update => { + await this.party.update(update); + this.render(); + game.socket.emit(`system.${CONFIG.DH.id}`, { action: socketEvent.Refresh, - data: { - refreshType: RefreshType.TagTeamRoll - } + data: { refreshType: RefreshType.TagTeamRoll, action: 'refresh' } }); - } 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 } - } - }); - } + }; + + await emitAsGM( + GMUpdateEvent.UpdateDocument, + gmUpdate, + update, + this.party.uuid, + options.render ? { refreshType: RefreshType.TagTeamRoll, action: 'refresh' } : undefined + ); } - 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}`]: _del }; - 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 - } + getIsEditable() { + return this.party.system.partyMembers.some(actor => { + const selected = Boolean(this.party.system.tagTeam.members[actor.id]); + return selected && actor.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER); }); } - 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); + tagTeamRefresh = ({ refreshType, action }) => { + if (refreshType !== RefreshType.TagTeamRoll) return; - if (this.data.initiator.cost) { - const initiator = this.party.find(x => x.id === this.data.initiator.id); - if (initiator.system.resources.hope.value < this.data.initiator.cost) { + switch (action) { + case 'startTagTeamRoll': + this.tabGroups.application = 'tagTeamRoll'; + break; + case 'refresh': + this.render(); + break; + case 'close': + this.close(); + break; + } + }; + + async close(options = {}) { + /* Opt out of Foundry's standard behavior of closing all application windows marked as UI when Escape is pressed */ + if (options.closeKey) return; + + Hooks.off(socketEvent.Refresh, this.tagTeamRefresh); + return super.close(options); + } + + checkInitiatorHopeError(initiator) { + if (initiator.cost && initiator.memberId) { + const actor = game.actors.get(initiator.memberId); + if (actor.system.resources.hope.value < 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)); + //#region Initialization + static #toggleSelectMember(_, button) { + const member = this.partyMembers.find(x => x.id === button.dataset.id); + if (member.selected && this.initiator?.memberId === member.id) this.initiator = null; - const systemData = foundry.utils.deepClone(mainRoll).system.toObject(); - const criticalRoll = systemData.roll.isCritical; - for (let roll of secondaryRolls) { - if (roll.system.hasDamage) { - for (let key in roll.system.damage) { - var damage = roll.system.damage[key]; - const damageTotal = - !roll.system.isCritical && criticalRoll - ? (await getCritDamageBonus(damage.formula)) + damage.total - : damage.total; - const updatedDamageParts = damage.parts; - if (systemData.damage[key]) { - if (!roll.system.isCritical && criticalRoll) { - for (let part of updatedDamageParts) { - const criticalDamage = await getCritDamageBonus(part.formula); - if (criticalDamage) { - damage.formula = `${damage.formula} + ${criticalDamage}`; - part.formula = `${part.formula} + ${criticalDamage}`; - part.modifierTotal = part.modifierTotal + criticalDamage; - part.total += criticalDamage; - part.roll = new Roll(part.formula); - } - } - } + member.selected = !member.selected; + this.render(); + } - systemData.damage[key].formula = `${systemData.damage[key].formula} + ${damage.formula}`; - systemData.damage[key].total += damageTotal; - systemData.damage[key].parts = [...systemData.damage[key].parts, ...updatedDamageParts]; - } else { - systemData.damage[key] = { ...damage, total: damageTotal, parts: updatedDamageParts }; - } + static async #startTagTeamRoll() { + const error = this.checkInitiatorHopeError(this.initiator); + if (error) return error; + + await this.party.update({ + 'system.tagTeam': _replace( + new game.system.api.data.TagTeamData({ + ...this.party.system.tagTeam.toObject(), + initiator: this.initiator, + members: this.partyMembers.reduce((acc, member) => { + if (member.selected) + acc[member.id] = { + name: member.name, + img: member.img, + rollType: CONFIG.DH.GENERAL.tagTeamRollTypes.trait.id + }; + return acc; + }, {}) + }) + ) + }); + + const hookData = { openForAllPlayers: this.openForAllPlayers, partyId: this.party.id }; + Hooks.callAll(CONFIG.DH.HOOKS.hooksConfig.tagTeamStart, hookData); + game.socket.emit(`system.${CONFIG.DH.id}`, { + action: socketEvent.TagTeamStart, + data: hookData + }); + + this.render(); + } + //#endregion + //#region Tag Team Roll + + async getInfoTexts(members) { + let rollsAreFinished = true; + let rollIsSelected = false; + for (const member of Object.values(members)) { + const rollFinished = Boolean(member.rollData); + const damageFinished = + member.rollData?.options?.hasDamage !== undefined ? member.rollData.options.damage : true; + + rollsAreFinished = rollsAreFinished && rollFinished && damageFinished; + rollIsSelected = rollIsSelected || member.selected; + } + + let hint = null; + if (!rollsAreFinished) hint = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.hints.completeRolls'); + else if (!rollIsSelected) hint = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.hints.selectRoll'); + + return hint; + } + + async updateRollType(event) { + this.updatePartyData({ + [`system.tagTeam.members.${event.target.dataset.member}`]: { + rollType: event.target.value, + rollChoice: null + } + }); + } + + static async #removeRoll(_, button) { + this.updatePartyData({ + [`system.tagTeam.members.${button.dataset.member}`]: { + rollData: null, + rollChoice: null, + selected: false + } + }); + } + + static async #makeRoll(event, button) { + const { member } = button.dataset; + + let result = null; + switch (this.party.system.tagTeam.members[member].rollType) { + case CONFIG.DH.GENERAL.tagTeamRollTypes.trait.id: + result = await this.makeTraitRoll(member); + break; + case CONFIG.DH.GENERAL.tagTeamRollTypes.ability.id: + case CONFIG.DH.GENERAL.tagTeamRollTypes.damageAbility.id: + result = await this.makeAbilityRoll(event, member); + break; + } + + if (!result) return; + + if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice }); + + const rollData = result.messageRoll.toJSON(); + delete rollData.options.messageRoll; + this.updatePartyData({ + [`system.tagTeam.members.${member}.rollData`]: rollData + }); + } + + async makeTraitRoll(memberKey) { + const actor = game.actors.find(x => x.id === memberKey); + if (!actor) return; + + const memberData = this.party.system.tagTeam.members[memberKey]; + return await actor.rollTrait(memberData.rollChoice, { + skips: { + createMessage: true, + resources: true, + triggers: true + } + }); + } + + async makeAbilityRoll(event, memberKey) { + const actor = game.actors.find(x => x.id === memberKey); + if (!actor) return; + + const memberData = this.party.system.tagTeam.members[memberKey]; + const action = await foundry.utils.fromUuid(memberData.rollChoice); + + return await action.use(event, { + skips: { + createMessage: true, + resources: true, + triggers: true + } + }); + } + + static async #rerollDice(_, button) { + const { member, diceType } = button.dataset; + const memberData = this.party.system.tagTeam.members[member]; + + const dieIndex = diceType === 'hope' ? 0 : diceType === 'fear' ? 2 : 4; + + const { parsedRoll, newRoll } = await game.system.api.dice.DualityRoll.reroll( + memberData.rollData, + dieIndex, + diceType + ); + const rollData = parsedRoll.toJSON(); + this.updatePartyData({ + [`system.tagTeam.members.${member}.rollData`]: { + ...rollData, + options: { + ...rollData.options, + roll: newRoll } } + }); + } + + static async #makeDamageRoll(event, button) { + const { memberKey } = button.dataset; + const actor = game.actors.find(x => x.id === memberKey); + if (!actor) return; + + const memberData = this.party.system.tagTeam.members[memberKey]; + const action = await foundry.utils.fromUuid(memberData.rollChoice); + const config = { + ...memberData.rollData.options, + dialog: { + configure: !event.shiftKey + }, + skips: { + createMessage: true, + resources: true, + triggers: true + } + }; + + await action.workflow.get('damage').execute(config, null, true); + if (!config.damage) return; + + const current = this.party.system.tagTeam.members[memberKey].rollData; + await this.updatePartyData({ + [`system.tagTeam.members.${memberKey}.rollData`]: { + ...current, + options: { + ...current.options, + damage: config.damage + } + } + }); + } + + static async #removeDamageRoll(_, button) { + const { memberKey } = button.dataset; + const current = this.party.system.tagTeam.members[memberKey].rollData; + this.updatePartyData({ + [`system.tagTeam.members.${memberKey}.rollData`]: { + ...current, + options: { + ...current.options, + damage: null + } + } + }); + } + + static async #rerollDamageDice(_, button) { + const { memberKey, damageKey, part, dice } = button.dataset; + const memberData = this.party.system.tagTeam.members[memberKey]; + const partData = memberData.rollData.options.damage[damageKey].parts[part]; + const activeDiceResultKey = Object.keys(partData.dice[dice].results).find( + index => partData.dice[dice].results[index].active + ); + const { parsedRoll, rerolledDice } = await game.system.api.dice.DamageRoll.reroll( + partData, + dice, + activeDiceResultKey + ); + + const rollData = this.party.system.tagTeam.members[memberKey].rollData; + rollData.options.damage[damageKey].parts = rollData.options.damage[damageKey].parts.map((damagePart, index) => { + if (index !== Number.parseInt(part)) return damagePart; + + return { + ...damagePart, + total: parsedRoll.total, + dice: rerolledDice + }; + }); + rollData.options.damage[damageKey].total = rollData.options.damage[damageKey].parts.reduce((acc, part) => { + acc += part.total; + return acc; + }, 0); + + this.updatePartyData({ + [`system.tagTeam.members.${memberKey}.rollData`]: rollData + }); + } + + async getCriticalDamage(damage) { + const newDamage = foundry.utils.deepClone(damage); + for (let key in newDamage) { + var damage = newDamage[key]; + damage.formula = ''; + damage.total = 0; + + for (let part of damage.parts) { + const criticalDamage = await getCritDamageBonus(part.formula); + if (criticalDamage) { + part.modifierTotal += criticalDamage; + part.total += criticalDamage; + part.formula = `${part.dice.map(x => x.formula).join(' + ')} + ${part.modifierTotal}`; + part.roll = new Roll(part.formula); + } + + damage.formula = [damage.formula, part.formula].filter(x => x).join(' + '); + damage.total += part.total; + } } - systemData.title = game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.chatMessageRollTitle'); + return newDamage; + } + + async getNonCriticalDamage(config) { + const newDamage = foundry.utils.deepClone(config.damage); + for (let key in newDamage) { + var damage = newDamage[key]; + damage.formula = ''; + damage.total = 0; + + for (let part of damage.parts) { + const critDamageBonus = await getCritDamageBonus(part.formula); + part.modifierTotal -= critDamageBonus; + part.total -= critDamageBonus; + part.formula = `${part.dice.map(x => x.formula).join(' + ')} + ${part.modifierTotal}`; + part.roll = new Roll(part.formula); + + damage.formula = [damage.formula, part.formula].filter(x => x).join(' + '); + damage.total += part.total; + } + } + + return newDamage; + } + + static async #selectRoll(_, button) { + const { memberKey } = button.dataset; + this.updatePartyData({ + [`system.tagTeam.members`]: Object.entries(this.party.system.tagTeam.members).reduce( + (acc, [key, member]) => { + acc[key] = { selected: key === memberKey ? !member.selected : false }; + return acc; + }, + {} + ) + }); + } + + async getJoinedRoll({ overrideIsCritical, displayVersion } = {}) { + const memberValues = Object.values(this.party.system.tagTeam.members); + const selectedRoll = memberValues.find(x => x.selected); + let baseMainRoll = selectedRoll ?? memberValues[0]; + let baseSecondaryRoll = selectedRoll + ? memberValues.find(x => !x.selected) + : memberValues.length > 1 + ? memberValues[1] + : null; + + if (!baseMainRoll?.rollData || !baseSecondaryRoll) return null; + + const mainRoll = new MemberData(baseMainRoll.toObject()); + const secondaryRollData = new MemberData(baseSecondaryRoll.toObject()).rollData; + const systemData = mainRoll.rollData.options; + const isCritical = overrideIsCritical ?? systemData.roll.isCritical; + if (isCritical) systemData.damage = await this.getCriticalDamage(systemData.damage); + + if (secondaryRollData?.options.hasDamage) { + const secondaryDamage = (displayVersion ? overrideIsCritical : isCritical) + ? await this.getCriticalDamage(secondaryRollData.options.damage) + : secondaryRollData.options.damage; + if (systemData.damage) { + for (const key in secondaryDamage) { + const damage = secondaryDamage[key]; + systemData.damage[key].formula = [systemData.damage[key].formula, damage.formula] + .filter(x => x) + .join(' + '); + systemData.damage[key].total += damage.total; + systemData.damage[key].parts.push(...damage.parts); + } + } else { + systemData.damage = secondaryDamage; + } + } + + return mainRoll; + } + + static async #onCancelRoll(_event, _button, options = { confirm: true }) { + this.cancelRoll(options); + } + + async cancelRoll(options = { confirm: true }) { + if (options.confirm) { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.cancelConfirmTitle') + }, + content: game.i18n.localize('DAGGERHEART.APPLICATIONS.TagTeamSelect.cancelConfirmText') + }); + + if (!confirmed) return; + } + + await this.updatePartyData( + { + 'system.tagTeam': { + initiator: null, + members: _replace({}) + } + }, + { render: false } + ); + + this.close(); + game.socket.emit(`system.${CONFIG.DH.id}`, { + action: socketEvent.Refresh, + data: { refreshType: RefreshType.TagTeamRoll, action: 'close' } + }); + } + + static async #finishRoll() { + const error = this.checkInitiatorHopeError(this.party.system.tagTeam.initiator); + if (error) return error; + + const mainRoll = (await this.getJoinedRoll()).rollData; + + const mainActor = this.party.system.partyMembers.find(x => x.uuid === mainRoll.options.source.actor); + mainRoll.options.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, + speaker: cls.getSpeaker({ actor: mainActor }), + system: mainRoll.options, + rolls: [mainRoll], sound: null, flags: { core: { RollTable: true } } }; await cls.create(msgData); + /* Handle resource updates from the finished TagTeamRoll */ + const tagTeamData = this.party.system.tagTeam; const fearUpdate = { key: 'fear', value: null, total: null, enabled: true }; - for (let memberId of Object.keys(this.data.members)) { + for (let memberId in tagTeamData.members) { const resourceUpdates = []; - const rollGivesHope = systemData.roll.isCritical || systemData.roll.result.duality === 1; - if (memberId === this.data.initiator.id) { - const value = this.data.initiator.cost + const rollGivesHope = mainRoll.options.roll.isCritical || mainRoll.options.roll.result.duality === 1; + if (memberId === tagTeamData.initiator.memberId) { + const value = tagTeamData.initiator.cost ? rollGivesHope - ? 1 - this.data.initiator.cost - : -this.data.initiator.cost + ? 1 - tagTeamData.initiator.cost + : -tagTeamData.initiator.cost : 1; resourceUpdates.push({ key: 'hope', value: value, total: -value, enabled: true }); } else if (rollGivesHope) { resourceUpdates.push({ key: 'hope', value: 1, total: -1, enabled: true }); } - if (systemData.roll.isCritical) resourceUpdates.push({ key: 'stress', value: -1, total: 1, enabled: true }); - if (systemData.roll.result.duality === -1) { + if (mainRoll.options.roll.isCritical) + resourceUpdates.push({ key: 'stress', value: -1, total: 1, enabled: true }); + if (mainRoll.options.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); + game.actors.get(memberId).modifyResource(resourceUpdates); } if (fearUpdate.value) { - this.party.find(x => x.id === mainRollId).modifyResource([fearUpdate]); + mainActor.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 } - } - }); - } + /* Fin */ + this.cancelRoll({ confirm: false }); } - static async assignRoll(char, message) { - const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); - const character = settings.members[char.id]; - if (!character) return; - - await settings.updateSource({ [`members.${char.id}.messageId`]: message.id }); - - if (game.user.isGM) { - await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, settings); - Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll }); - await game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.Refresh, - data: { - refreshType: RefreshType.TagTeamRoll - } - }); - } else { - await game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.GMUpdate, - data: { - action: GMUpdateEvent.UpdateSetting, - uuid: CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, - update: settings, - refresh: { refreshType: RefreshType.TagTeamRoll } - } - }); - } - } - - async close(options = {}) { - Hooks.off(socketEvent.Refresh, this.setupHooks); - await super.close(options); - } + //#endregion } diff --git a/module/applications/sheets-configs/_module.mjs b/module/applications/sheets-configs/_module.mjs index d3fb3c39..4b83a042 100644 --- a/module/applications/sheets-configs/_module.mjs +++ b/module/applications/sheets-configs/_module.mjs @@ -3,7 +3,6 @@ export { default as ActionSettingsConfig } from './action-settings-config.mjs'; export { default as CharacterSettings } from './character-settings.mjs'; export { default as AdversarySettings } from './adversary-settings.mjs'; export { default as CompanionSettings } from './companion-settings.mjs'; -export { default as SettingActiveEffectConfig } from './setting-active-effect-config.mjs'; export { default as SettingFeatureConfig } from './setting-feature-config.mjs'; export { default as EnvironmentSettings } from './environment-settings.mjs'; export { default as ActiveEffectConfig } from './activeEffectConfig.mjs'; diff --git a/module/applications/sheets-configs/action-config.mjs b/module/applications/sheets-configs/action-config.mjs index 0dbc377a..e75e16ab 100644 --- a/module/applications/sheets-configs/action-config.mjs +++ b/module/applications/sheets-configs/action-config.mjs @@ -24,9 +24,12 @@ export default class DHActionConfig extends DHActionBaseConfig { const effectData = this._addEffectData.bind(this)(); const data = this.action.toObject(); - const [created] = await this.action.item.createEmbeddedDocuments('ActiveEffect', [effectData], { + const created = await game.system.api.documents.DhActiveEffect.createDialog(effectData, { + parent: this.action.item, render: false }); + if (!created) return; + data.effects.push({ _id: created._id }); this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) }); this.action.item.effects.get(created._id).sheet.render(true); diff --git a/module/applications/sheets-configs/action-settings-config.mjs b/module/applications/sheets-configs/action-settings-config.mjs index 91b85802..9cb866bc 100644 --- a/module/applications/sheets-configs/action-settings-config.mjs +++ b/module/applications/sheets-configs/action-settings-config.mjs @@ -55,7 +55,7 @@ export default class DHActionSettingsConfig extends DHActionBaseConfig { static async editEffect(event) { const id = event.target.closest('[data-effect-id]')?.dataset?.effectId; - const updatedEffect = await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure( + const updatedEffect = await game.system.api.applications.sheetConfigs.ActiveEffectConfig.configureSetting( this.getEffectDetails(id) ); if (!updatedEffect) return; diff --git a/module/applications/sheets-configs/activeEffectConfig.mjs b/module/applications/sheets-configs/activeEffectConfig.mjs index 2bd7d5b9..9f970e1d 100644 --- a/module/applications/sheets-configs/activeEffectConfig.mjs +++ b/module/applications/sheets-configs/activeEffectConfig.mjs @@ -150,6 +150,14 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac minLength: 0 }); }); + + htmlElement + .querySelector('.armor-change-checkbox') + ?.addEventListener('change', this.armorChangeToggle.bind(this)); + + htmlElement + .querySelector('.armor-damage-thresholds-checkbox') + ?.addEventListener('change', this.armorDamageThresholdToggle.bind(this)); } async _prepareContext(options) { @@ -186,21 +194,66 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac })); break; case 'changes': - const fields = this.document.system.schema.fields.changes.element.fields; - partContext.changes = await Promise.all( - foundry.utils - .deepClone(context.source.changes) - .map((c, i) => this._prepareChangeContext(c, i, fields)) - ); + const singleTypes = ['armor']; + const typedChanges = context.source.changes.reduce((acc, change, index) => { + if (singleTypes.includes(change.type)) { + acc[change.type] = { ...change, index }; + } + return acc; + }, {}); + partContext.changes = partContext.changes.filter(c => !!c); + partContext.typedChanges = typedChanges; break; } return partContext; } - _prepareChangeContext(change, index, fields) { + armorChangeToggle(event) { + if (event.target.checked) { + this.addArmorChange(); + } else { + this.removeTypedChange(event.target.dataset.index); + } + } + + /* Could be generalised if needed later */ + addArmorChange() { + const submitData = this._processFormData(null, this.form, new FormDataExtended(this.form)); + const changes = Object.values(submitData.system?.changes ?? {}); + changes.push(game.system.api.data.activeEffects.changeTypes.armor.getInitialValue()); + return this.submit({ updateData: { system: { changes } } }); + } + + removeTypedChange(indexString) { + const submitData = this._processFormData(null, this.form, new FormDataExtended(this.form)); + const changes = Object.values(submitData.system.changes); + const index = Number(indexString); + changes.splice(index, 1); + return this.submit({ updateData: { system: { changes } } }); + } + + armorDamageThresholdToggle(event) { + const submitData = this._processFormData(null, this.form, new FormDataExtended(this.form)); + const changes = Object.values(submitData.system?.changes ?? {}); + const index = Number(event.target.dataset.index); + if (event.target.checked) { + changes[index].value.damageThresholds = { major: 0, severe: 0 }; + } else { + changes[index].value.damageThresholds = null; + } + + return this.submit({ updateData: { system: { changes } } }); + } + + /** @inheritdoc */ + _renderChange(context) { + const { change, index, defaultPriority } = context; + if (!(change.type in CONFIG.DH.GENERAL.baseActiveEffectModes)) return null; + + const changeTypesSchema = this.document.system.schema.fields.changes.element.types; + const fields = context.fields ?? (changeTypesSchema[change.type] ?? changeTypesSchema.add).fields; if (typeof change.value !== 'string') change.value = JSON.stringify(change.value); - const defaultPriority = game.system.api.documents.DhActiveEffect.CHANGE_TYPES[change.type]?.defaultPriority; Object.assign( change, ['key', 'type', 'value', 'priority'].reduce((paths, fieldName) => { @@ -220,7 +273,11 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac change, index, defaultPriority, - fields + fields, + types: Object.keys(CONFIG.DH.GENERAL.baseActiveEffectModes).reduce((r, key) => { + r[key] = CONFIG.DH.GENERAL.baseActiveEffectModes[key].label; + return r; + }, {}) } ) ); @@ -247,4 +304,34 @@ export default class DhActiveEffectConfig extends foundry.applications.sheets.Ac return submitData; } + + /** @inheritDoc */ + _processSubmitData(event, form, submitData, options) { + if (this.options.isSetting) { + // Settings should update source instead + this.document.updateSource(submitData); + this.render(); + } else { + return super._processSubmitData(event, form, submitData, options); + } + } + + /** Creates an active effect config for a setting */ + static async configureSetting(effect, options = {}) { + const document = new CONFIG.ActiveEffect.documentClass({ ...foundry.utils.duplicate(effect), _id: effect.id }); + return new Promise(resolve => { + const app = new this({ document, ...options, isSetting: true }); + app.addEventListener( + 'close', + () => { + const newEffect = app.document.toObject(true); + newEffect.id = newEffect._id; + delete newEffect._id; + resolve(newEffect); + }, + { once: true } + ); + app.render({ force: true }); + }); + } } diff --git a/module/applications/sheets-configs/setting-active-effect-config.mjs b/module/applications/sheets-configs/setting-active-effect-config.mjs deleted file mode 100644 index 12ac90d1..00000000 --- a/module/applications/sheets-configs/setting-active-effect-config.mjs +++ /dev/null @@ -1,223 +0,0 @@ -import autocomplete from 'autocompleter'; - -const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; - -export default class SettingActiveEffectConfig extends HandlebarsApplicationMixin(ApplicationV2) { - constructor(effect) { - super({}); - - this.effect = foundry.utils.deepClone(effect); - this.changeChoices = game.system.api.applications.sheetConfigs.ActiveEffectConfig.getChangeChoices(); - } - - static DEFAULT_OPTIONS = { - classes: ['daggerheart', 'sheet', 'dh-style', 'active-effect-config', 'standard-form'], - tag: 'form', - position: { - width: 560 - }, - form: { - submitOnChange: false, - closeOnSubmit: false, - handler: SettingActiveEffectConfig.#onSubmit - }, - actions: { - editImage: SettingActiveEffectConfig.#editImage, - addChange: SettingActiveEffectConfig.#addChange, - deleteChange: SettingActiveEffectConfig.#deleteChange - } - }; - - static PARTS = { - header: { template: 'systems/daggerheart/templates/sheets/activeEffect/header.hbs' }, - tabs: { template: 'templates/generic/tab-navigation.hbs' }, - details: { template: 'systems/daggerheart/templates/sheets/activeEffect/details.hbs', scrollable: [''] }, - settings: { template: 'systems/daggerheart/templates/sheets/activeEffect/settings.hbs' }, - changes: { - template: 'systems/daggerheart/templates/sheets/activeEffect/changes.hbs', - scrollable: ['ol[data-changes]'] - }, - footer: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-form-footer.hbs' } - }; - - static TABS = { - sheet: { - tabs: [ - { id: 'details', icon: 'fa-solid fa-book' }, - { id: 'settings', icon: 'fa-solid fa-bars', label: 'DAGGERHEART.GENERAL.Tabs.settings' }, - { id: 'changes', icon: 'fa-solid fa-gears' } - ], - initial: 'details', - labelPrefix: 'EFFECT.TABS' - } - }; - - /**@inheritdoc */ - async _onFirstRender(context, options) { - await super._onFirstRender(context, options); - } - - async _prepareContext(_options) { - const context = await super._prepareContext(_options); - context.source = this.effect; - context.fields = game.system.api.documents.DhActiveEffect.schema.fields; - context.systemFields = game.system.api.data.activeEffects.BaseEffect._schema.fields; - - return context; - } - - _attachPartListeners(partId, htmlElement, options) { - super._attachPartListeners(partId, htmlElement, options); - const changeChoices = this.changeChoices; - - htmlElement.querySelectorAll('.effect-change-input').forEach(element => { - autocomplete({ - input: element, - fetch: function (text, update) { - if (!text) { - update(changeChoices); - } else { - text = text.toLowerCase(); - var suggestions = changeChoices.filter(n => n.label.toLowerCase().includes(text)); - update(suggestions); - } - }, - render: function (item, search) { - const label = game.i18n.localize(item.label); - const matchIndex = label.toLowerCase().indexOf(search); - - const beforeText = label.slice(0, matchIndex); - const matchText = label.slice(matchIndex, matchIndex + search.length); - const after = label.slice(matchIndex + search.length, label.length); - - const element = document.createElement('li'); - element.innerHTML = - `${beforeText}${matchText ? `${matchText}` : ''}${after}`.replaceAll( - ' ', - ' ' - ); - if (item.hint) { - element.dataset.tooltip = game.i18n.localize(item.hint); - } - - return element; - }, - renderGroup: function (label) { - const itemElement = document.createElement('div'); - itemElement.textContent = game.i18n.localize(label); - return itemElement; - }, - onSelect: function (item) { - element.value = `system.${item.value}`; - }, - click: e => e.fetch(), - customize: function (_input, _inputRect, container) { - container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ; - }, - minLength: 0 - }); - }); - } - - async _preparePartContext(partId, context) { - if (partId in context.tabs) context.tab = context.tabs[partId]; - switch (partId) { - case 'details': - context.statuses = CONFIG.statusEffects.map(s => ({ value: s.id, label: game.i18n.localize(s.name) })); - context.isActorEffect = false; - context.isItemEffect = true; - const useGeneric = game.settings.get( - CONFIG.DH.id, - CONFIG.DH.SETTINGS.gameSettings.appearance - ).showGenericStatusEffects; - if (!useGeneric) { - context.statuses = [ - ...context.statuses, - Object.values(CONFIG.DH.GENERAL.conditions).map(status => ({ - value: status.id, - label: game.i18n.localize(status.name) - })) - ]; - } - break; - case 'changes': - context.modes = Object.entries(CONST.ACTIVE_EFFECT_MODES).reduce((modes, [key, value]) => { - modes[value] = game.i18n.localize(`EFFECT.MODE_${key}`); - return modes; - }, {}); - - context.priorities = ActiveEffectConfig.DEFAULT_PRIORITIES; - break; - } - - return context; - } - - static async #onSubmit(_event, _form, formData) { - this.data = foundry.utils.expandObject(formData.object); - this.close(); - } - - /** - * Edit a Document image. - * @this {DocumentSheetV2} - * @type {ApplicationClickAction} - */ - static async #editImage(_event, target) { - if (target.nodeName !== 'IMG') { - throw new Error('The editImage action is available only for IMG elements.'); - } - - const attr = target.dataset.edit; - const current = foundry.utils.getProperty(this.effect, attr); - const fp = new FilePicker.implementation({ - current, - type: 'image', - callback: path => (target.src = path), - position: { - top: this.position.top + 40, - left: this.position.left + 10 - } - }); - - await fp.browse(); - } - - /** - * Add a new change to the effect's changes array. - * @this {ActiveEffectConfig} - * @type {ApplicationClickAction} - */ - static async #addChange() { - const { changes, ...rest } = foundry.utils.expandObject(new FormDataExtended(this.form).object); - const updatedChanges = Object.values(changes ?? {}); - updatedChanges.push({}); - - this.effect = { ...rest, changes: updatedChanges }; - this.render(); - } - - /** - * Delete a change from the effect's changes array. - * @this {ActiveEffectConfig} - * @type {ApplicationClickAction} - */ - static async #deleteChange(event) { - const submitData = foundry.utils.expandObject(new FormDataExtended(this.form).object); - const updatedChanges = Object.values(submitData.changes); - const row = event.target.closest('li'); - const index = Number(row.dataset.index) || 0; - updatedChanges.splice(index, 1); - - this.effect = { ...submitData, changes: updatedChanges }; - this.render(); - } - - static async configure(effect, options = {}) { - return new Promise(resolve => { - const app = new this(effect, options); - app.addEventListener('close', () => resolve(app.data), { once: true }); - app.render({ force: true }); - }); - } -} diff --git a/module/applications/sheets-configs/setting-feature-config.mjs b/module/applications/sheets-configs/setting-feature-config.mjs index fb790f7f..f90bb52f 100644 --- a/module/applications/sheets-configs/setting-feature-config.mjs +++ b/module/applications/sheets-configs/setting-feature-config.mjs @@ -147,7 +147,7 @@ export default class SettingFeatureConfig extends HandlebarsApplicationMixin(App const effectIndex = this.move.effects.findIndex(x => x.id === id); const effect = this.move.effects[effectIndex]; const updatedEffect = - await game.system.api.applications.sheetConfigs.SettingActiveEffectConfig.configure(effect); + await game.system.api.applications.sheetConfigs.ActiveEffectConfig.configureSetting(effect); if (!updatedEffect) return; await this.updateMove({ diff --git a/module/applications/sheets-configs/token-config-mixin.mjs b/module/applications/sheets-configs/token-config-mixin.mjs index 5242d797..bed7f628 100644 --- a/module/applications/sheets-configs/token-config-mixin.mjs +++ b/module/applications/sheets-configs/token-config-mixin.mjs @@ -67,9 +67,9 @@ export default function DHTokenConfigMixin(Base) { changes.height = tokenSize; } - const deletions = { actorId: _del, actorLink: _del }; - const mergeOptions = { inplace: false, performDeletions: true }; - this._preview.updateSource(foundry.utils.mergeObject(changes, deletions, mergeOptions)); + // const deletions = { actorId: _del }; + // const mergeOptions = { inplace: false, performDeletions: true, actorLink: false }; + this._preview.updateSource(changes); if (this._preview?.object?.destroyed === false) { this._preview.object.initializeSources(); diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 14dd1ae7..f2686fdd 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -1,10 +1,9 @@ import DHBaseActorSheet from '../api/base-actor.mjs'; import DhDeathMove from '../../dialogs/deathMove.mjs'; -import { abilities } from '../../../config/actorConfig.mjs'; import { CharacterLevelup, LevelupViewMode } from '../../levelup/_module.mjs'; import DhCharacterCreation from '../../characterCreation/characterCreation.mjs'; import FilterMenu from '../../ux/filter-menu.mjs'; -import { getDocFromElement, getDocFromElementSync } from '../../../helpers/utils.mjs'; +import { getArmorSources, getDocFromElement, getDocFromElementSync } from '../../../helpers/utils.mjs'; /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ @@ -35,7 +34,8 @@ export default class CharacterSheet extends DHBaseActorSheet { cancelBeastform: CharacterSheet.#cancelBeastform, toggleResourceManagement: CharacterSheet.#toggleResourceManagement, useDowntime: this.useDowntime, - viewParty: CharacterSheet.#viewParty + viewParty: CharacterSheet.#viewParty, + toggleArmorMangement: CharacterSheet.#toggleArmorManagement }, window: { resizable: true, @@ -639,12 +639,12 @@ export default class CharacterSheet extends DHBaseActorSheet { } async updateArmorMarks(event) { - const armor = this.document.system.armor; - if (!armor) return; + const inputValue = Number(event.currentTarget.value); + const { value, max } = this.document.system.armorScore; + const changeValue = Math.min(inputValue - value, max - value); - const maxMarks = this.document.system.armorScore; - const value = Math.min(Math.max(Number(event.currentTarget.value), 0), maxMarks); - await armor.update({ 'system.marks.value': value }); + event.currentTarget.value = inputValue < 0 ? 0 : value + changeValue; + this.document.system.updateArmorValue({ value: changeValue }); } /* -------------------------------------------- */ @@ -720,35 +720,16 @@ export default class CharacterSheet extends DHBaseActorSheet { * Rolls an attribute check based on the clicked button's dataset attribute. * @type {ApplicationClickAction} */ - static async #rollAttribute(event, button) { - const abilityLabel = game.i18n.localize(abilities[button.dataset.attribute].label); - const config = { - event: event, - title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.actor.name}`, - headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { - ability: abilityLabel - }), - effects: await game.system.api.data.actions.actionsTypes.base.getEffects(this.document), - roll: { - trait: button.dataset.attribute, - type: 'trait' - }, - hasRoll: true, - actionType: 'action', - headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.actor.name}`, - title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { - ability: abilityLabel - }) - }; - const result = await this.document.diceRoll(config); + static async #rollAttribute(_event, button) { + const result = await this.document.rollTrait(button.dataset.attribute); if (!result) return; /* This could be avoided by baking config.costs into config.resourceUpdates. Didn't feel like messing with it at the time */ const costResources = result.costs?.filter(x => x.enabled).map(cost => ({ ...cost, value: -cost.value, total: -cost.total })) || {}; - config.resourceUpdates.addResources(costResources); - await config.resourceUpdates.updateResources(); + result.resourceUpdates.addResources(costResources); + await result.resourceUpdates.updateResources(); } //TODO: redo toggleEquipItem method @@ -823,10 +804,13 @@ export default class CharacterSheet extends DHBaseActorSheet { * 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 }); + static async #toggleArmor(_, button, _element) { + const { value, max } = this.document.system.armorScore; + const inputValue = Number.parseInt(button.dataset.value); + const newValue = value >= inputValue ? inputValue - 1 : inputValue; + const changeValue = Math.min(newValue - value, max - value); + + this.document.system.updateArmorValue({ value: changeValue }); } /** @@ -952,6 +936,99 @@ export default class CharacterSheet extends DHBaseActorSheet { }); } + static async #toggleArmorManagement(_event, target) { + const existingTooltip = document.body.querySelector('.locked-tooltip .armor-management-container'); + if (existingTooltip) { + game.tooltip.dismissLockedTooltips(); + return; + } + + const armorSources = getArmorSources(this.document) + .filter(s => !s.disabled) + .toReversed() + .map(({ name, document, data }) => ({ + ...data, + uuid: document.uuid, + name + })); + if (!armorSources.length) return; + + const useResourcePips = game.settings.get( + CONFIG.DH.id, + CONFIG.DH.SETTINGS.gameSettings.appearance + ).useResourcePips; + const html = document.createElement('div'); + html.innerHTML = await foundry.applications.handlebars.renderTemplate( + `systems/daggerheart/templates/ui/tooltip/armorManagement.hbs`, + { + sources: armorSources, + useResourcePips + } + ); + + game.tooltip.dismissLockedTooltips(); + game.tooltip.activate(target, { + html, + locked: true, + cssClass: 'bordered-tooltip', + direction: 'DOWN' + }); + + html.querySelectorAll('.armor-slot').forEach(element => { + element.addEventListener('click', CharacterSheet.armorSourcePipUpdate); + }); + } + + static async armorSourceInput(event) { + const effect = await foundry.utils.fromUuid(event.target.dataset.uuid); + const value = Math.max(Math.min(Number.parseInt(event.target.value), effect.system.armorData.max), 0); + event.target.value = value; + const progressBar = event.target.closest('.status-bar.armor-slots').querySelector('progress'); + progressBar.value = value; + } + + /** Update specific armor source */ + static async armorSourcePipUpdate(event) { + const target = event.target.closest('.armor-slot'); + const { uuid, value } = target.dataset; + const document = await foundry.utils.fromUuid(uuid); + + let inputValue = Number.parseInt(value); + let decreasing = false; + let newCurrent = 0; + + if (document.type === 'armor') { + decreasing = document.system.armor.current >= inputValue; + newCurrent = decreasing ? inputValue - 1 : inputValue; + await document.update({ 'system.armor.current': newCurrent }); + } else if (document.system.armorData) { + const { current } = document.system.armorData; + decreasing = current >= inputValue; + newCurrent = decreasing ? inputValue - 1 : inputValue; + + const newChanges = document.system.changes.map(change => ({ + ...change, + value: change.type === 'armor' ? { ...change.value, current: newCurrent } : change.value + })); + + await document.update({ 'system.changes': newChanges }); + } else { + return; + } + + const container = target.closest('.slot-bar'); + for (const armorSlot of container.querySelectorAll('.armor-slot i')) { + const index = Number.parseInt(armorSlot.dataset.index); + if (decreasing && index >= newCurrent) { + armorSlot.classList.remove('fa-shield'); + armorSlot.classList.add('fa-shield-halved'); + } else if (!decreasing && index < newCurrent) { + armorSlot.classList.add('fa-shield'); + armorSlot.classList.remove('fa-shield-halved'); + } + } + } + static async #toggleResourceManagement(event, button) { event.stopPropagation(); const existingTooltip = document.body.querySelector('.locked-tooltip .resource-management-container'); @@ -985,7 +1062,6 @@ export default class CharacterSheet extends DHBaseActorSheet { ); const target = button.closest('.resource-section'); - game.tooltip.dismissLockedTooltips(); game.tooltip.activate(target, { html, diff --git a/module/applications/sheets/actors/party.mjs b/module/applications/sheets/actors/party.mjs index 1b1722db..c5e77112 100644 --- a/module/applications/sheets/actors/party.mjs +++ b/module/applications/sheets/actors/party.mjs @@ -35,9 +35,7 @@ export default class Party extends DHBaseActorSheet { refeshActions: Party.#refeshActions, triggerRest: Party.#triggerRest, tagTeamRoll: Party.#tagTeamRoll, - groupRoll: Party.#groupRoll, - selectRefreshable: DaggerheartMenu.selectRefreshable, - refreshActors: DaggerheartMenu.refreshActors + groupRoll: Party.#groupRoll }, dragDrop: [{ dragSelector: '[data-item-id]', dropSelector: null }] }; @@ -120,6 +118,7 @@ export default class Party extends DHBaseActorSheet { secrets: this.document.isOwner, relativeTo: this.document }); + context.tagTeamActive = Boolean(this.document.system.tagTeam.initiator); } /** @@ -190,11 +189,14 @@ export default class Party extends DHBaseActorSheet { * 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 }); + static async #toggleArmorSlot(_, target) { + const actor = game.actors.get(target.dataset.actorId); + const { value, max } = actor.system.armorScore; + const inputValue = Number.parseInt(target.dataset.value); + const newValue = value >= inputValue ? inputValue - 1 : inputValue; + const changeValue = Math.min(newValue - value, max - value); + + await actor.system.updateArmorValue({ value: changeValue }); this.render(); } @@ -255,11 +257,7 @@ export default class Party extends DHBaseActorSheet { } static async #tagTeamRoll() { - new game.system.api.applications.dialogs.TagTeamDialog( - this.document.system.partyMembers.filter(x => Party.DICE_ROLL_ACTOR_TYPES.includes(x.type)) - ).render({ - force: true - }); + new game.system.api.applications.dialogs.TagTeamDialog(this.document).render({ force: true }); } static async #groupRoll(_params) { diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index 89941399..83313454 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -749,11 +749,13 @@ export default function DHApplicationMixin(Base) { const cls = type === 'action' ? game.system.api.models.actions.actionsTypes.base : getDocumentClass(documentClass); + const data = { name: cls.defaultName({ type, parent }), type, system: systemData }; + if (inVault) data['system.inVault'] = true; if (disabled) data.disabled = true; if (type === 'domainCard' && parent?.system.domains?.length) { diff --git a/module/applications/sheets/api/base-actor.mjs b/module/applications/sheets/api/base-actor.mjs index 6f994faf..4b0fd7d9 100644 --- a/module/applications/sheets/api/base-actor.mjs +++ b/module/applications/sheets/api/base-actor.mjs @@ -160,7 +160,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { inactives: [] }; - for (const effect of this.actor.allApplicableEffects()) { + for (const effect of this.actor.allApplicableEffects({ noTransferArmor: true })) { const list = effect.active ? context.effects.actives : context.effects.inactives; list.push(effect); } diff --git a/module/applications/sheets/items/armor.mjs b/module/applications/sheets/items/armor.mjs index 2550b415..54685fee 100644 --- a/module/applications/sheets/items/armor.mjs +++ b/module/applications/sheets/items/armor.mjs @@ -47,6 +47,15 @@ export default class ArmorSheet extends ItemAttachmentSheet(DHBaseItemSheet) { return context; } + async updateArmorEffect(event) { + const value = Number.parseInt(event.target.value); + const armorEffect = this.document.system.armorEffect; + if (Number.isNaN(value) || !armorEffect) return; + + await armorEffect.system.armorChange.updateArmorMax(value); + this.render(); + } + /** * Callback function used by `tagifyElement`. * @param {Array} selectedOptions - The currently selected tag objects. diff --git a/module/applications/ui/_module.mjs b/module/applications/ui/_module.mjs index 8c5c020e..80d3ebe4 100644 --- a/module/applications/ui/_module.mjs +++ b/module/applications/ui/_module.mjs @@ -7,3 +7,4 @@ export { default as DhFearTracker } from './fearTracker.mjs'; export { default as DhHotbar } from './hotbar.mjs'; export { default as DhSceneNavigation } from './sceneNavigation.mjs'; export { ItemBrowser } from './itemBrowser.mjs'; +export { default as DhProgress } from './progress.mjs'; diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index 2b489f58..e29498e6 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -190,7 +190,24 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo const target = event.target.closest('[data-die-index]'); if (target.dataset.type === 'damage') { - game.system.api.dice.DamageRoll.reroll(target, message); + const { damageType, part, dice, result } = target.dataset; + const damagePart = message.system.damage[damageType].parts[part]; + const { parsedRoll, rerolledDice } = await game.system.api.dice.DamageRoll.reroll(damagePart, dice, result); + const damageParts = message.system.damage[damageType].parts.map((damagePart, index) => { + if (index !== Number(part)) return damagePart; + return { + ...damagePart, + total: parsedRoll.total, + dice: rerolledDice + }; + }); + const updateMessage = game.messages.get(message._id); + await updateMessage.update({ + [`system.damage.${damageType}`]: { + total: parsedRoll.total, + parts: damageParts + } + }); } else { let originalRoll_parsed = message.rolls.map(roll => JSON.parse(roll))[0]; const rollClass = @@ -204,20 +221,16 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo if (!game.modules.get('dice-so-nice')?.active) foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice }); - const { newRoll, parsedRoll } = await rollClass.reroll(originalRoll_parsed, target, message); + const { newRoll, parsedRoll } = await rollClass.reroll( + originalRoll_parsed, + target.dataset.dieIndex, + target.dataset.type + ); await game.messages.get(message._id).update({ '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 - } - }); } } diff --git a/module/applications/ui/itemBrowser.mjs b/module/applications/ui/itemBrowser.mjs index 0c3a77fa..2d2e8cdc 100644 --- a/module/applications/ui/itemBrowser.mjs +++ b/module/applications/ui/itemBrowser.mjs @@ -253,8 +253,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { for (const item of this.items) { if (['weapon', 'armor'].includes(item.type)) { item.system.enrichedTags = await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/sheets/global/partials/item-tags.hbs', - item.system + 'systems/daggerheart/templates/ui/itemBrowser/item-tags.hbs', + { item: item.system } ); } item.system.enrichedDescription = diff --git a/module/applications/ui/progress.mjs b/module/applications/ui/progress.mjs new file mode 100644 index 00000000..2fb1b445 --- /dev/null +++ b/module/applications/ui/progress.mjs @@ -0,0 +1,27 @@ +export default class DhProgress { + #notification; + + constructor({ max, label = '' }) { + this.max = max; + this.label = label; + this.#notification = ui.notifications.info(this.label, { progress: true }); + } + + updateMax(newMax) { + this.max = newMax; + } + + advance({ by = 1, label = this.label } = {}) { + if (this.value === this.max) return; + this.value = (this.value ?? 0) + Math.abs(by); + this.#notification.update({ message: label, pct: this.value / this.max }); + } + + close({ label = '' } = {}) { + this.#notification.update({ message: label, pct: 1 }); + } + + static createMigrationProgress(max = 0) { + return new DhProgress({ max, label: game.i18n.localize('DAGGERHEART.UI.Progress.migrationLabel') }); + } +} diff --git a/module/canvas/placeables/regionLayer.mjs b/module/canvas/placeables/regionLayer.mjs index 75c192a0..81fd96db 100644 --- a/module/canvas/placeables/regionLayer.mjs +++ b/module/canvas/placeables/regionLayer.mjs @@ -44,7 +44,10 @@ export default class DhRegionLayer extends foundry.canvas.layers.RegionLayer { if (game.activeTool === 'inFront') return { type: 'cone', x: 0, y: 0, radius: 0, angle: 180, hole }; const shape = super._createDragShapeData(event); - const token = shape?.type === 'emanation' && shape.base?.type === 'token' ? this.#findTokenInBounds(event.interactionData.origin) : null; + const token = + shape?.type === 'emanation' && shape.base?.type === 'token' + ? this.#findTokenInBounds(event.interactionData.origin) + : null; if (token) { shape.base.width = token.width; shape.base.height = token.height; diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 31e78f0b..c15cae56 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -959,7 +959,22 @@ export const sceneRangeMeasurementSetting = { } }; -export const activeEffectModes = { +export const tagTeamRollTypes = { + trait: { + id: 'trait', + label: 'DAGGERHEART.CONFIG.TagTeamRollTypes.trait' + }, + ability: { + id: 'ability', + label: 'DAGGERHEART.CONFIG.TagTeamRollTypes.ability' + }, + damageAbility: { + id: 'damageAbility', + label: 'DAGGERHEART.CONFIG.TagTeamRollTypes.damageAbility' + } +}; + +export const baseActiveEffectModes = { custom: { id: 'custom', priority: 0, @@ -997,6 +1012,21 @@ export const activeEffectModes = { } }; +export const activeEffectModes = { + armor: { + id: 'armor', + priority: 20, + label: 'TYPES.ActiveEffect.armor' + }, + ...baseActiveEffectModes +}; + +export const activeEffectArmorInteraction = { + none: { id: 'none', label: 'DAGGERHEART.CONFIG.ArmorInteraction.none.label' }, + active: { id: 'active', label: 'DAGGERHEART.CONFIG.ArmorInteraction.active.label' }, + inactive: { id: 'inactive', label: 'DAGGERHEART.CONFIG.ArmorInteraction.inactive.label' } +}; + export const activeEffectDurations = { temporary: { id: 'temporary', diff --git a/module/config/hooksConfig.mjs b/module/config/hooksConfig.mjs index 61ba594c..8d04be6d 100644 --- a/module/config/hooksConfig.mjs +++ b/module/config/hooksConfig.mjs @@ -1,4 +1,5 @@ export const hooksConfig = { effectDisplayToggle: 'DHEffectDisplayToggle', - lockedTooltipDismissed: 'DHLockedTooltipDismissed' + lockedTooltipDismissed: 'DHLockedTooltipDismissed', + tagTeamStart: 'DHTagTeamRollStart' }; diff --git a/module/config/itemConfig.mjs b/module/config/itemConfig.mjs index b424c707..a3e785c3 100644 --- a/module/config/itemConfig.mjs +++ b/module/config/itemConfig.mjs @@ -489,15 +489,18 @@ export const weaponFeatures = { description: 'DAGGERHEART.CONFIG.WeaponFeature.barrier.effects.barrier.description', img: 'icons/skills/melee/shield-block-bash-blue.webp', changes: [ - { - key: 'system.armorScore', - mode: 2, - value: 'ITEM.@system.tier + 1' - }, { key: 'system.evasion', mode: 2, value: '-1' + }, + { + key: 'Armor', + type: 'armor', + typeData: { + type: 'armor', + max: 'ITEM.@system.tier + 1' + } } ] } @@ -789,11 +792,6 @@ export const weaponFeatures = { description: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.description', img: 'icons/skills/melee/sword-shield-stylized-white.webp', changes: [ - { - key: 'system.armorScore', - mode: 2, - value: '1' - }, { key: 'system.bonuses.damage.primaryWeapon.bonus', mode: 2, @@ -808,6 +806,22 @@ export const weaponFeatures = { type: 'withinRange' } } + }, + { + name: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.name', + description: 'DAGGERHEART.CONFIG.WeaponFeature.doubleDuty.effects.doubleDuty.description', + img: 'icons/skills/melee/sword-shield-stylized-white.webp', + changes: [ + { + key: 'Armor', + type: 'armor', + value: 0, + typeData: { + type: 'armor', + max: 1 + } + } + ] } ] }, @@ -1191,9 +1205,13 @@ export const weaponFeatures = { img: 'icons/skills/melee/shield-block-gray-orange.webp', changes: [ { - key: 'system.armorScore', - mode: 2, - value: 'ITEM.@system.tier' + key: 'Armor', + type: 'armor', + value: 0, + typeData: { + type: 'armor', + max: 'ITEM.@system.tier' + } } ] } diff --git a/module/config/settingsConfig.mjs b/module/config/settingsConfig.mjs index 0b28f0ab..52a316cf 100644 --- a/module/config/settingsConfig.mjs +++ b/module/config/settingsConfig.mjs @@ -34,7 +34,6 @@ export const gameSettings = { LevelTiers: 'LevelTiers', Countdowns: 'Countdowns', LastMigrationVersion: 'LastMigrationVersion', - TagTeamRoll: 'TagTeamRoll', SpotlightRequestQueue: 'SpotlightRequestQueue', CompendiumBrowserSettings: 'CompendiumBrowserSettings' }; diff --git a/module/data/_module.mjs b/module/data/_module.mjs index 52fa689e..43ff7807 100644 --- a/module/data/_module.mjs +++ b/module/data/_module.mjs @@ -1,9 +1,9 @@ export { default as DhCombat } from './combat.mjs'; export { default as DhCombatant } from './combatant.mjs'; -export { default as DhTagTeamRoll } from './tagTeamRoll.mjs'; export { default as DhRollTable } from './rollTable.mjs'; export { default as RegisteredTriggers } from './registeredTriggers.mjs'; export { default as CompendiumBrowserSettings } from './compendiumBrowserSettings.mjs'; +export { default as TagTeamData } from './tagTeamData.mjs'; export * as countdowns from './countdowns.mjs'; export * as actions from './action/_module.mjs'; diff --git a/module/data/action/attackAction.mjs b/module/data/action/attackAction.mjs index 3671613d..a2d47309 100644 --- a/module/data/action/attackAction.mjs +++ b/module/data/action/attackAction.mjs @@ -50,9 +50,8 @@ export default class DHAttackAction extends DHDamageAction { async use(event, options) { const result = await super.use(event, options); - if (!result.message) return; - if (result.message.system.action.roll?.type === 'attack') { + if (result.message?.system.action.roll?.type === 'attack') { const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns; await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.characterAttack.id); } diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index 75fc3981..01139b30 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -207,10 +207,10 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel * @param {Event} event Event from the button used to trigger the Action * @returns {object} */ - async use(event) { + async use(event, configOptions = {}) { if (!this.actor) throw new Error("An Action can't be used outside of an Actor context."); - let config = this.prepareConfig(event); + let config = this.prepareConfig(event, configOptions); if (!config) return; config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(this.actor, this.item); @@ -231,7 +231,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel if (Hooks.call(`${CONFIG.DH.id}.postUseAction`, this, config) === false) return; - if (this.chatDisplay && !config.actionChatMessageHandled) await this.toChat(); + if (this.chatDisplay && !config.skips.createMessage && !config.actionChatMessageHandled) await this.toChat(); return config; } @@ -241,7 +241,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel * @param {Event} event Event from the button used to trigger the Action * @returns {object} */ - prepareBaseConfig(event) { + prepareBaseConfig(event, configOptions = {}) { const isActor = this.item instanceof CONFIG.Actor.documentClass; const actionTitle = game.i18n.localize(this.name); const itemTitle = isActor || this.item.name === actionTitle ? '' : `${this.item.name} - `; @@ -268,7 +268,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel data: this.getRollData(), evaluate: this.hasRoll, resourceUpdates: new ResourceUpdateMap(this.actor), - targetUuid: this.targetUuid + targetUuid: this.targetUuid, + ...configOptions }; DHBaseAction.applyKeybindings(config); @@ -280,8 +281,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel * @param {Event} event Event from the button used to trigger the Action * @returns {object} */ - prepareConfig(event) { - const config = this.prepareBaseConfig(event); + prepareConfig(event, configOptions = {}) { + const config = this.prepareBaseConfig(event, configOptions); for (const clsField of Object.values(this.schema.fields)) { if (clsField?.prepareConfig) if (clsField.prepareConfig.call(this, config) === false) return false; } @@ -297,17 +298,19 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel static async getEffects(actor, effectParent) { if (!actor) return []; - return Array.from(await actor.allApplicableEffects()).filter(effect => { - /* Effects on weapons only ever apply for the weapon itself */ - if (effect.parent.type === 'weapon') { - /* Unless they're secondary - then they apply only to other primary weapons */ - if (effect.parent.system.secondary) { - if (effectParent?.type !== 'weapon' || effectParent?.system.secondary) return false; - } else if (effectParent?.id !== effect.parent.id) return false; - } + return Array.from(await actor.allApplicableEffects({ noTransferArmor: true, noSelfArmor: true })).filter( + effect => { + /* Effects on weapons only ever apply for the weapon itself */ + if (effect.parent.type === 'weapon') { + /* Unless they're secondary - then they apply only to other primary weapons */ + if (effect.parent.system.secondary) { + if (effectParent?.type !== 'weapon' || effectParent?.system.secondary) return false; + } else if (effectParent?.id !== effect.parent.id) return false; + } - return !effect.isSuppressed; - }); + return !effect.isSuppressed; + } + ); } /** @@ -354,11 +357,11 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel } get hasDamage() { - return !foundry.utils.isEmpty(this.damage?.parts) && this.type !== 'healing'; + return Boolean(Object.keys(this.damage?.parts ?? {}).length) && this.type !== 'healing'; } get hasHealing() { - return !foundry.utils.isEmpty(this.damage?.parts) && this.type === 'healing'; + return Boolean(Object.keys(this.damage?.parts ?? {}).length) && this.type === 'healing'; } get hasSave() { diff --git a/module/data/activeEffect/_module.mjs b/module/data/activeEffect/_module.mjs index 1a50088a..3c933a9c 100644 --- a/module/data/activeEffect/_module.mjs +++ b/module/data/activeEffect/_module.mjs @@ -1,6 +1,7 @@ import BaseEffect from './baseEffect.mjs'; import BeastformEffect from './beastformEffect.mjs'; import HordeEffect from './hordeEffect.mjs'; +export { changeTypes, changeEffects } from './changeTypes/_module.mjs'; export { BaseEffect, BeastformEffect, HordeEffect }; diff --git a/module/data/activeEffect/baseEffect.mjs b/module/data/activeEffect/baseEffect.mjs index c0ed1b36..5b3d00ed 100644 --- a/module/data/activeEffect/baseEffect.mjs +++ b/module/data/activeEffect/baseEffect.mjs @@ -12,26 +12,41 @@ * "Anything that uses another data model value as its value": +1 - Effects that increase traits have to be calculated first at Base priority. (EX: Raise evasion by half your agility) */ +import { getScrollTextData } from '../../helpers/utils.mjs'; +import { changeTypes } from './_module.mjs'; + export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel { static defineSchema() { const fields = foundry.data.fields; + const baseChanges = Object.keys(CONFIG.DH.GENERAL.baseActiveEffectModes).reduce((r, type) => { + r[type] = new fields.SchemaField({ + key: new fields.StringField({ required: true }), + type: new fields.StringField({ + required: true, + choices: [type], + initial: type, + validate: BaseEffect.#validateType + }), + value: new fields.AnyField({ + required: true, + nullable: true, + serializable: true, + initial: '' + }), + phase: new fields.StringField({ required: true, blank: false, initial: 'initial' }), + priority: new fields.NumberField() + }); + return r; + }, {}); + return { ...super.defineSchema(), changes: new fields.ArrayField( - new fields.SchemaField({ - key: new fields.StringField({ required: true }), - type: new fields.StringField({ - required: true, - blank: false, - choices: CONFIG.DH.GENERAL.activeEffectModes, - initial: CONFIG.DH.GENERAL.activeEffectModes.add.id, - validate: BaseEffect.#validateType - }), - value: new fields.AnyField({ required: true, nullable: true, serializable: true, initial: '' }), - phase: new fields.StringField({ required: true, blank: false, initial: 'initial' }), - priority: new fields.NumberField() - }) + new fields.TypedSchemaField( + { ...changeTypes, ...baseChanges }, + { initial: baseChanges.add.getInitialValue() } + ) ), duration: new fields.SchemaField({ type: new fields.StringField({ @@ -97,6 +112,23 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel { return true; } + get isSuppressed() { + for (const change of this.changes) { + if (change.isSuppressed) return true; + } + } + + get armorChange() { + return this.changes.find(x => x.type === CONFIG.DH.GENERAL.activeEffectModes.armor.id); + } + + get armorData() { + const armorChange = this.armorChange; + if (!armorChange) return null; + + return armorChange.getArmorData(); + } + static getDefaultObject() { return { name: 'New Effect', @@ -116,4 +148,31 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel { } }; } + + async _preUpdate(changed, options, userId) { + const allowed = await super._preUpdate(changed, options, userId); + if (allowed === false) return false; + + const autoSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation); + if ( + autoSettings.resourceScrollTexts && + this.parent.actor?.type === 'character' && + this.parent.actor.system.resources.armor + ) { + const newArmorTotal = (changed.system?.changes ?? []).reduce((acc, change) => { + if (change.type === 'armor') acc += change.value.current; + return acc; + }, this.parent.actor.system.armor?.system?.armor?.current ?? 0); + + const armorData = getScrollTextData(this.parent.actor, { value: newArmorTotal }, 'armor'); + options.scrollingTextData = [armorData]; + } + } + + _onUpdate(changed, options, userId) { + super._onUpdate(changed, options, userId); + + if (this.parent.actor && options.scrollingTextData) + this.parent.actor.queueScrollText(options.scrollingTextData); + } } diff --git a/module/data/activeEffect/changeTypes/_module.mjs b/module/data/activeEffect/changeTypes/_module.mjs new file mode 100644 index 00000000..cf872304 --- /dev/null +++ b/module/data/activeEffect/changeTypes/_module.mjs @@ -0,0 +1,9 @@ +import Armor from './armor.mjs'; + +export const changeEffects = { + armor: Armor.changeEffect +}; + +export const changeTypes = { + armor: Armor +}; diff --git a/module/data/activeEffect/changeTypes/armor.mjs b/module/data/activeEffect/changeTypes/armor.mjs new file mode 100644 index 00000000..f400d41b --- /dev/null +++ b/module/data/activeEffect/changeTypes/armor.mjs @@ -0,0 +1,206 @@ +import { itemAbleRollParse } from '../../../helpers/utils.mjs'; + +const fields = foundry.data.fields; + +export default class ArmorChange extends foundry.abstract.DataModel { + static defineSchema() { + return { + type: new fields.StringField({ required: true, choices: ['armor'], initial: 'armor' }), + priority: new fields.NumberField(), + phase: new fields.StringField({ required: true, blank: false, initial: 'initial' }), + value: new fields.SchemaField({ + current: new fields.NumberField({ integer: true, min: 0, initial: 0 }), + max: new fields.StringField({ + required: true, + nullable: false, + initial: '1', + label: 'DAGGERHEART.GENERAL.max' + }), + damageThresholds: new fields.SchemaField( + { + major: new fields.StringField({ + initial: '0', + label: 'DAGGERHEART.GENERAL.DamageThresholds.majorThreshold' + }), + severe: new fields.StringField({ + initial: '0', + label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold' + }) + }, + { nullable: true, initial: null } + ), + interaction: new fields.StringField({ + required: true, + choices: CONFIG.DH.GENERAL.activeEffectArmorInteraction, + initial: CONFIG.DH.GENERAL.activeEffectArmorInteraction.none.id, + label: 'DAGGERHEART.EFFECTS.ChangeTypes.armor.FIELDS.interaction.label', + hint: 'DAGGERHEART.EFFECTS.ChangeTypes.armor.FIELDS.interaction.hint' + }) + }) + }; + } + + static changeEffect = { + label: 'Armor', + defaultPriority: 20, + handler: (actor, change, _options, _field, replacementData) => { + const parsedMax = itemAbleRollParse(change.value.max, actor, change.effect.parent); + game.system.api.documents.DhActiveEffect.applyChange( + actor, + { + ...change, + key: 'system.armorScore.value', + type: CONFIG.DH.GENERAL.activeEffectModes.add.id, + value: change.value.current + }, + replacementData + ); + game.system.api.documents.DhActiveEffect.applyChange( + actor, + { + ...change, + key: 'system.armorScore.max', + type: CONFIG.DH.GENERAL.activeEffectModes.add.id, + value: parsedMax + }, + replacementData + ); + + if (change.value.damageThresholds) { + const getThresholdValue = value => { + const parsed = itemAbleRollParse(value, actor, change.effect.parent); + const roll = new Roll(parsed).evaluateSync(); + return roll ? (roll.isDeterministic ? roll.total : null) : null; + }; + const major = getThresholdValue(change.value.damageThresholds.major); + const severe = getThresholdValue(change.value.damageThresholds.severe); + + if (major) { + game.system.api.documents.DhActiveEffect.applyChange( + actor, + { + ...change, + key: 'system.damageThresholds.major', + type: CONFIG.DH.GENERAL.activeEffectModes.override.id, + priority: 50, + value: major + }, + replacementData + ); + } + + if (severe) { + game.system.api.documents.DhActiveEffect.applyChange( + actor, + { + ...change, + key: 'system.damageThresholds.severe', + type: CONFIG.DH.GENERAL.activeEffectModes.override.id, + priority: 50, + value: severe + }, + replacementData + ); + } + } + + return {}; + }, + render: null + }; + + get isSuppressed() { + switch (this.value.interaction) { + case CONFIG.DH.GENERAL.activeEffectArmorInteraction.active.id: + return !this.parent.parent?.actor.system.armor; + case CONFIG.DH.GENERAL.activeEffectArmorInteraction.inactive.id: + return Boolean(this.parent.parent?.actor.system.armor); + default: + return false; + } + } + + static getInitialValue() { + return { + type: CONFIG.DH.GENERAL.activeEffectModes.armor.id, + value: { + current: 0, + max: 0 + }, + phase: 'initial', + priority: 20 + }; + } + + static getDefaultArmorEffect() { + return { + name: game.i18n.localize('DAGGERHEART.EFFECTS.ChangeTypes.armor.newArmorEffect'), + img: 'icons/equipment/chest/breastplate-helmet-metal.webp', + system: { + changes: [ArmorChange.getInitialValue()] + } + }; + } + + /* Helpers */ + + getArmorData() { + const actor = this.parent.parent?.actor?.type === 'character' ? this.parent.parent.actor : null; + const maxParse = actor ? itemAbleRollParse(this.value.max, actor, this.parent.parent.parent) : null; + const maxRoll = maxParse ? new Roll(maxParse).evaluateSync() : null; + const maxEvaluated = maxRoll ? (maxRoll.isDeterministic ? maxRoll.total : null) : null; + + return { + current: this.value.current, + max: maxEvaluated ?? this.value.max + }; + } + + async updateArmorMax(newMax) { + const newChanges = [ + ...this.parent.changes.map(change => ({ + ...change, + value: + change.type === 'armor' + ? { + ...change.value, + current: Math.min(change.value.current, newMax), + max: newMax + } + : change.value + })) + ]; + await this.parent.parent.update({ 'system.changes': newChanges }); + } + + static orderEffectsForAutoChange(armorEffects, increasing) { + const getEffectWeight = effect => { + switch (effect.parent.type) { + case 'class': + case 'subclass': + case 'ancestry': + case 'community': + case 'feature': + case 'domainCard': + return 2; + case 'armor': + return 3; + case 'loot': + case 'consumable': + return 4; + case 'weapon': + return 5; + case 'character': + return 6; + default: + return 1; + } + }; + + return armorEffects + .filter(x => !x.disabled && !x.isSuppressed) + .sort((a, b) => + increasing ? getEffectWeight(b) - getEffectWeight(a) : getEffectWeight(a) - getEffectWeight(b) + ); + } +} diff --git a/module/data/actor/base.mjs b/module/data/actor/base.mjs index 9f571edb..89ba5db2 100644 --- a/module/data/actor/base.mjs +++ b/module/data/actor/base.mjs @@ -189,19 +189,6 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel { return true; } - async _preDelete() { - /* Clear all partyMembers from tagTeam setting.*/ - /* Revisit this when tagTeam is improved for many parties */ - if (this.parent.parties.size > 0) { - const tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); - await tagTeam.updateSource({ - initiator: this.parent.id === tagTeam.initiator ? null : tagTeam.initiator, - members: Object.keys(tagTeam.members).find(x => x === this.parent.id) ? { [this.parent.id]: _del } : {} - }); - await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, tagTeam); - } - } - async _preUpdate(changes, options, userId) { const allowed = await super._preUpdate(changes, options, userId); if (allowed === false) return; diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 7af4da9c..55c1c7d8 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -6,6 +6,7 @@ import DhCreature from './creature.mjs'; import { attributeField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs'; import { ActionField } from '../fields/actionField.mjs'; import DHCharacterSettings from '../../applications/sheets-configs/character-settings.mjs'; +import { getArmorSources } from '../../helpers/utils.mjs'; export default class DhCharacter extends DhCreature { /**@override */ @@ -41,17 +42,16 @@ export default class DhCharacter extends DhCreature { label: 'DAGGERHEART.GENERAL.proficiency' }), evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }), - armorScore: new fields.NumberField({ integer: true, initial: 0, label: 'DAGGERHEART.GENERAL.armorScore' }), damageThresholds: new fields.SchemaField({ - severe: new fields.NumberField({ - integer: true, - initial: 0, - label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold' - }), major: new fields.NumberField({ integer: true, initial: 0, label: 'DAGGERHEART.GENERAL.DamageThresholds.majorThreshold' + }), + severe: new fields.NumberField({ + integer: true, + initial: 0, + label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold' }) }), experiences: new fields.TypedObjectField( @@ -465,6 +465,101 @@ export default class DhCharacter extends DhCreature { } } + async updateArmorValue({ value: armorChange = 0, clear = false }) { + if (armorChange === 0 && !clear) return; + + const increasing = armorChange >= 0; + let remainingChange = Math.abs(armorChange); + const orderedSources = getArmorSources(this.parent).filter(s => !s.disabled); + + const handleArmorData = (embeddedUpdates, doc, armorData) => { + let usedArmorChange = 0; + if (clear) { + usedArmorChange -= armorData.current; + } else { + if (increasing) { + const remainingArmor = armorData.max - armorData.current; + usedArmorChange = Math.min(remainingChange, remainingArmor); + remainingChange -= usedArmorChange; + } else { + const changeChange = Math.min(armorData.current, remainingChange); + usedArmorChange -= changeChange; + remainingChange -= changeChange; + } + } + + if (!usedArmorChange) return usedArmorChange; + else { + if (!embeddedUpdates[doc.id]) embeddedUpdates[doc.id] = { doc: doc, updates: [] }; + + return usedArmorChange; + } + }; + + const armorUpdates = []; + const effectUpdates = []; + for (const { document: armorSource } of orderedSources) { + const usedArmorChange = handleArmorData( + armorSource.type === 'armor' ? armorUpdates : effectUpdates, + armorSource.parent, + armorSource.type === 'armor' ? armorSource.system.armor : armorSource.system.armorData + ); + if (!usedArmorChange) continue; + + if (armorSource.type === 'armor') { + armorUpdates[armorSource.parent.id].updates.push({ + '_id': armorSource.id, + 'system.armor.current': armorSource.system.armor.current + usedArmorChange + }); + } else { + effectUpdates[armorSource.parent.id].updates.push({ + '_id': armorSource.id, + 'system.changes': armorSource.system.changes.map(change => ({ + ...change, + value: + change.type === 'armor' + ? { + ...change.value, + current: armorSource.system.armorChange.value.current + usedArmorChange + } + : change.value + })) + }); + } + + if (remainingChange === 0 && !clear) break; + } + + const armorUpdateValues = Object.values(armorUpdates); + for (const [index, { doc, updates }] of armorUpdateValues.entries()) + await doc.updateEmbeddedDocuments('Item', updates, { render: index === armorUpdateValues.length - 1 }); + + const effectUpdateValues = Object.values(effectUpdates); + for (const [index, { doc, updates }] of effectUpdateValues.entries()) + await doc.updateEmbeddedDocuments('ActiveEffect', updates, { + render: index === effectUpdateValues.length - 1 + }); + } + + async updateArmorEffectValue({ uuid, value }) { + const source = await foundry.utils.fromUuid(uuid); + if (source.type === 'armor') { + await source.update({ + 'system.armor.current': source.system.armor.current + value + }); + } else { + const effectValue = source.system.armorChange.value; + await source.update({ + 'system.changes': [ + { + ...source.system.armorChange, + value: { ...effectValue, current: effectValue.current + value } + } + ] + }); + } + } + get sheetLists() { const ancestryFeatures = [], communityFeatures = [], @@ -588,6 +683,10 @@ export default class DhCharacter extends DhCreature { prepareBaseData() { super.prepareBaseData(); + this.armorScore = { + max: this.armor?.system.armor.max ?? 0, + value: this.armor?.system.armor.current ?? 0 + }; this.evasion += this.class.value?.system?.evasion ?? 0; const currentLevel = this.levelData.level.current; @@ -637,14 +736,12 @@ export default class DhCharacter extends DhCreature { } } - const armor = this.armor; - this.armorScore = armor ? armor.system.baseScore : 0; this.damageThresholds = { - major: armor - ? armor.system.baseThresholds.major + this.levelData.level.current + major: this.armor + ? this.armor.system.baseThresholds.major + this.levelData.level.current : this.levelData.level.current, - severe: armor - ? armor.system.baseThresholds.severe + this.levelData.level.current + severe: this.armor + ? this.armor.system.baseThresholds.severe + this.levelData.level.current : this.levelData.level.current * 2 }; @@ -679,9 +776,8 @@ export default class DhCharacter extends DhCreature { this.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait; this.resources.armor = { + ...this.armorScore, label: 'DAGGERHEART.GENERAL.armor', - value: this.armor?.system?.marks?.value ?? 0, - max: this.armorScore, isReversed: true }; @@ -757,7 +853,6 @@ export default class DhCharacter extends DhCreature { static migrateData(source) { if (typeof source.scars === 'object') source.scars = 0; - if (source.resources?.hope?.max) source.scars = Math.max(6 - source.resources.hope.max, 0); return super.migrateData(source); } diff --git a/module/data/actor/environment.mjs b/module/data/actor/environment.mjs index 0aaf8eb0..e06f038c 100644 --- a/module/data/actor/environment.mjs +++ b/module/data/actor/environment.mjs @@ -75,10 +75,6 @@ export default class DhEnvironment extends BaseDataActor { ); scene.update({ 'flags.daggerheart.sceneEnvironments': newSceneEnvironments }).then(() => { Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Scene }); - game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.Refresh, - data: { refreshType: RefreshType.TagTeamRoll } - }); }); } } diff --git a/module/data/actor/party.mjs b/module/data/actor/party.mjs index 3eddf235..2c797803 100644 --- a/module/data/actor/party.mjs +++ b/module/data/actor/party.mjs @@ -1,5 +1,6 @@ import BaseDataActor from './base.mjs'; import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs'; +import TagTeamData from '../tagTeamData.mjs'; export default class DhParty extends BaseDataActor { /**@inheritdoc */ @@ -14,7 +15,8 @@ export default class DhParty extends BaseDataActor { handfuls: new fields.NumberField({ initial: 1, integer: true }), bags: new fields.NumberField({ initial: 0, integer: true }), chests: new fields.NumberField({ initial: 0, integer: true }) - }) + }), + tagTeam: new fields.EmbeddedDataField(TagTeamData) }; } @@ -40,23 +42,6 @@ export default class DhParty extends BaseDataActor { } } - async _preDelete() { - /* Clear all partyMembers from tagTeam setting.*/ - /* Revisit this when tagTeam is improved for many parties */ - const tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); - await tagTeam.updateSource({ - initiator: this.partyMembers.some(x => x.id === tagTeam.initiator) ? null : tagTeam.initiator, - members: Object.keys(tagTeam.members).reduce((acc, key) => { - if (this.partyMembers.find(x => x.id === key)) { - acc[key] = _del; - } - - return acc; - }, {}) - }); - await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, tagTeam); - } - _onDelete(options, userId) { super._onDelete(options, userId); diff --git a/module/data/fields/action/damageField.mjs b/module/data/fields/action/damageField.mjs index e79a91a2..b7ef852e 100644 --- a/module/data/fields/action/damageField.mjs +++ b/module/data/fields/action/damageField.mjs @@ -50,9 +50,9 @@ export default class DamageField extends fields.SchemaField { formulas = DamageField.formatFormulas.call(this, formulas, config); const damageConfig = { + dialog: {}, ...config, roll: formulas, - dialog: {}, data: this.getRollData() }; delete damageConfig.evaluate; diff --git a/module/data/fields/action/effectsField.mjs b/module/data/fields/action/effectsField.mjs index df0c0b09..9a4ffc31 100644 --- a/module/data/fields/action/effectsField.mjs +++ b/module/data/fields/action/effectsField.mjs @@ -27,7 +27,7 @@ export default class EffectsField extends fields.ArrayField { static async execute(config, targets = null, force = false) { if (!config.hasEffect) return; let message = config.message ?? ui.chat.collection.get(config.parent?._id); - if (!message) { + if (!message && !config.skips.createMessage) { const roll = new CONFIG.Dice.daggerheart.DHRoll(''); roll._evaluated = true; message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config); diff --git a/module/data/fields/action/saveField.mjs b/module/data/fields/action/saveField.mjs index c9030036..0629353e 100644 --- a/module/data/fields/action/saveField.mjs +++ b/module/data/fields/action/saveField.mjs @@ -38,7 +38,7 @@ export default class SaveField extends fields.SchemaField { if (!config.hasSave) return; let message = config.message ?? ui.chat.collection.get(config.parent?._id); - if (!message) { + if (!message && !config.skips.createMessage) { const roll = new CONFIG.Dice.daggerheart.DHRoll(''); roll._evaluated = true; message = config.message = await CONFIG.Dice.daggerheart.DHRoll.toMessage(roll, config); diff --git a/module/data/fields/actorField.mjs b/module/data/fields/actorField.mjs index 69ba6bf1..25e04317 100644 --- a/module/data/fields/actorField.mjs +++ b/module/data/fields/actorField.mjs @@ -82,6 +82,24 @@ class ResourcesField extends fields.TypedObjectField { } return data; } + + /** + * Foundry bar attributes are unable to handle finding the schema field nor the label normally. + * This returns the element if its a valid resource key and overwrites the element's label for that retrieval. + */ + _getField(path) { + if (path.length === 0) return this; + const first = path.shift(); + if (first === this.element.name) return this.element_getField(path); + + const resources = CONFIG.DH.RESOURCE[this.actorType].all; + if (first in resources) { + this.element.label = resources[first].label; + return this.element._getField(path); + } + + return undefined; + } } export { attributeField, ResourcesField, stressDamageReductionRule, bonusField }; diff --git a/module/data/fields/iterableTypedObjectField.mjs b/module/data/fields/iterableTypedObjectField.mjs index d50d7dbf..d360b641 100644 --- a/module/data/fields/iterableTypedObjectField.mjs +++ b/module/data/fields/iterableTypedObjectField.mjs @@ -16,12 +16,12 @@ export default class IterableTypedObjectField extends foundry.data.fields.TypedO } } -/** - * The prototype of an iterable object. +/** + * The prototype of an iterable object. * This allows the functionality of a class but also allows foundry.utils.getType() to return "Object" instead of "Unknown". */ const IterableObjectPrototype = { - [Symbol.iterator]: function*() { + [Symbol.iterator]: function* () { for (const value of Object.values(this)) { yield value; } @@ -29,4 +29,4 @@ const IterableObjectPrototype = { map: function (func) { return Array.from(this, func); } -}; \ No newline at end of file +}; diff --git a/module/data/item/armor.mjs b/module/data/item/armor.mjs index 264d7f93..b0e4847f 100644 --- a/module/data/item/armor.mjs +++ b/module/data/item/armor.mjs @@ -19,7 +19,14 @@ export default class DHArmor extends AttachableItem { ...super.defineSchema(), tier: new fields.NumberField({ required: true, integer: true, initial: 1, min: 1 }), equipped: new fields.BooleanField({ initial: false }), - baseScore: new fields.NumberField({ integer: true, initial: 0 }), + armor: new fields.SchemaField({ + current: new fields.NumberField({ integer: true, min: 0, initial: 0 }), + max: new fields.NumberField({ required: true, integer: true, initial: 0 }) + }), + baseThresholds: new fields.SchemaField({ + major: new fields.NumberField({ integer: true, initial: 0 }), + severe: new fields.NumberField({ integer: true, initial: 0 }) + }), armorFeatures: new fields.ArrayField( new fields.SchemaField({ value: new fields.StringField({ @@ -28,14 +35,7 @@ export default class DHArmor extends AttachableItem { effectIds: new fields.ArrayField(new fields.StringField({ required: true })), actionIds: new fields.ArrayField(new fields.StringField({ required: true })) }) - ), - marks: new fields.SchemaField({ - value: new fields.NumberField({ initial: 0, integer: true }) - }), - baseThresholds: new fields.SchemaField({ - major: new fields.NumberField({ integer: true, initial: 0 }), - severe: new fields.NumberField({ integer: true, initial: 0 }) - }) + ) }; } @@ -151,13 +151,20 @@ export default class DHArmor extends AttachableItem { } } + /** @inheritDoc */ + static migrateDocumentData(source) { + if (!source.system.armor) { + source.system.armor = { current: source.system.marks?.value ?? 0, max: source.system.baseScore ?? 0 }; + } + } + /** * Generates a list of localized tags based on this item's type-specific properties. * @returns {string[]} An array of localized tag strings. */ _getTags() { const tags = [ - `${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.baseScore}`, + `${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.armor.max}`, `${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseThresholds.base')}: ${this.baseThresholds.major} / ${this.baseThresholds.severe}` ]; @@ -169,9 +176,7 @@ export default class DHArmor extends AttachableItem { * @returns {(string | { value: string, icons: string[] })[]} An array of localized strings and damage label objects. */ _getLabels() { - const labels = []; - if (this.baseScore) - labels.push(`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.baseScore}`); + const labels = [`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.armor.max}`]; return labels; } diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index 5a16927a..21a11149 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -222,9 +222,14 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { const autoSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation); const armorChanged = - changed.system?.marks?.value !== undefined && changed.system.marks.value !== this.marks.value; + changed.system?.armor?.current !== undefined && changed.system.armor.current !== this.armor.current; if (armorChanged && autoSettings.resourceScrollTexts && this.parent.parent?.type === 'character') { - const armorData = getScrollTextData(this.parent.parent, changed.system.marks, 'armor'); + const armorChangeValue = changed.system.armor.current - this.armor.current; + const armorData = getScrollTextData( + this.parent.parent, + { value: armorChangeValue + this.parent.parent.system.armorScore.value }, + 'armor' + ); options.scrollingTextData = [armorData]; } diff --git a/module/data/tagTeamData.mjs b/module/data/tagTeamData.mjs new file mode 100644 index 00000000..25158606 --- /dev/null +++ b/module/data/tagTeamData.mjs @@ -0,0 +1,47 @@ +export default class TagTeamData extends foundry.abstract.DataModel { + static defineSchema() { + const fields = foundry.data.fields; + + return { + initiator: new fields.SchemaField( + { + memberId: new fields.StringField({ + required: true, + label: 'DAGGERHEART.APPLICATIONS.TagTeamSelect.FIELDS.initiator.memberId.label' + }), + cost: new fields.NumberField({ + integer: true, + initial: 3, + label: 'DAGGERHEART.APPLICATIONS.TagTeamSelect.FIELDS.initiator.cost.label' + }) + }, + { nullable: true, initial: null } + ), + members: new fields.TypedObjectField(new fields.EmbeddedDataField(MemberData)) + }; + } +} + +export class MemberData extends foundry.abstract.DataModel { + static defineSchema() { + const fields = foundry.data.fields; + + return { + name: new fields.StringField({ required: true }), + img: new fields.StringField({ required: true }), + rollType: new fields.StringField({ + required: true, + choices: CONFIG.DH.GENERAL.tagTeamRollTypes, + initial: CONFIG.DH.GENERAL.tagTeamRollTypes.trait.id, + label: 'Roll Type' + }), + rollChoice: new fields.StringField({ nullable: true, initial: null }), + rollData: new fields.JSONField({ nullable: true, initial: null }), + selected: new fields.BooleanField({ initial: false }) + }; + } + + get roll() { + return this.rollData ? CONFIG.Dice.daggerheart.DualityRoll.fromData(this.rollData) : null; + } +} diff --git a/module/data/tagTeamRoll.mjs b/module/data/tagTeamRoll.mjs deleted file mode 100644 index de71a11b..00000000 --- a/module/data/tagTeamRoll.mjs +++ /dev/null @@ -1,20 +0,0 @@ -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 b322aae7..8cd3caac 100644 --- a/module/dice/damageRoll.mjs +++ b/module/dice/damageRoll.mjs @@ -1,6 +1,5 @@ import DamageDialog from '../applications/dialogs/damageDialog.mjs'; import { parseRallyDice } from '../helpers/utils.mjs'; -import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs'; import DHRoll from './dhRoll.mjs'; export default class DamageRoll extends DHRoll { @@ -281,10 +280,7 @@ export default class DamageRoll extends DHRoll { return mods; } - static async reroll(target, message) { - const { damageType, part, dice, result } = target.dataset; - const rollPart = message.system.damage[damageType].parts[part]; - + static async reroll(rollPart, dice, result) { let diceIndex = 0; let parsedRoll = game.system.api.dice.DamageRoll.fromData({ ...rollPart.roll, @@ -353,29 +349,6 @@ export default class DamageRoll extends DHRoll { }; }); - const updateMessage = game.messages.get(message._id); - const damageParts = updateMessage.system.damage[damageType].parts.map((damagePart, index) => { - if (index !== Number(part)) return damagePart; - return { - ...rollPart, - total: parsedRoll.total, - dice: rerolledDice - }; - }); - await updateMessage.update({ - [`system.damage.${damageType}`]: { - ...updateMessage, - total: parsedRoll.total, - parts: damageParts - } - }); - - Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll }); - await game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.Refresh, - data: { - refreshType: RefreshType.TagTeamRoll - } - }); + return { parsedRoll, rerolledDice }; } } diff --git a/module/dice/dhRoll.mjs b/module/dice/dhRoll.mjs index ec2ea0c7..b74adaf3 100644 --- a/module/dice/dhRoll.mjs +++ b/module/dice/dhRoll.mjs @@ -21,6 +21,9 @@ export default class DHRoll extends Roll { static async build(config = {}, message = {}) { const roll = await this.buildConfigure(config, message); if (!roll) return; + + if (config.skips?.createMessage) config.messageRoll = roll; + await this.buildEvaluate(roll, config, (message = {})); await this.buildPost(roll, config, (message = {})); return config; @@ -30,12 +33,6 @@ export default class DHRoll extends Roll { config.hooks = [...this.getHooks(), '']; config.dialog ??= {}; - const actorIdSplit = config.source?.actor?.split('.'); - if (actorIdSplit) { - const tagTeamSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); - config.tagTeamSelected = Boolean(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; } @@ -146,6 +143,7 @@ export default class DHRoll extends Roll { return foundry.applications.handlebars.renderTemplate(template, { ...chatData, parent: chatData.parent, + targetMode: chatData.targetMode, metagamingSettings }); } diff --git a/module/dice/dualityRoll.mjs b/module/dice/dualityRoll.mjs index eb15fa5e..03035f68 100644 --- a/module/dice/dualityRoll.mjs +++ b/module/dice/dualityRoll.mjs @@ -374,9 +374,9 @@ export default class DualityRoll extends D20Roll { } } - static async reroll(rollString, target, message) { - let parsedRoll = game.system.api.dice.DualityRoll.fromData({ ...rollString, evaluated: false }); - const term = parsedRoll.terms[target.dataset.dieIndex]; + static async reroll(rollBase, dieIndex, diceType) { + let parsedRoll = game.system.api.dice.DualityRoll.fromData({ ...rollBase, evaluated: false }); + const term = parsedRoll.terms[dieIndex]; await term.reroll(`/r1=${term.total}`); const result = await parsedRoll.evaluate(); @@ -393,35 +393,35 @@ export default class DualityRoll extends D20Roll { options: { appearance: {} } }; - const diceSoNicePresets = await getDiceSoNicePresets(result, `d${term._faces}`, `d${term._faces}`); - const type = target.dataset.type; - if (diceSoNicePresets[type]) { - diceSoNiceRoll.dice[0].options = diceSoNicePresets[type]; + const diceSoNicePresets = await getDiceSoNicePresets(`d${term._faces}`, `d${term._faces}`); + if (diceSoNicePresets[diceType]) { + diceSoNiceRoll.dice[0].options = diceSoNicePresets[diceType]; } await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true); + } else { + foundry.audio.AudioHelper.play({ src: CONFIG.sounds.dice }); } const newRoll = game.system.api.dice.DualityRoll.postEvaluate(parsedRoll, { - targets: message.system.targets, + targets: parsedRoll.options.targets ?? [], roll: { - advantage: message.system.roll.advantage?.type, - difficulty: message.system.roll.difficulty ? Number(message.system.roll.difficulty) : null + advantage: parsedRoll.options.roll.advantage?.type, + difficulty: parsedRoll.options.roll.difficulty ? Number(parsedRoll.options.roll.difficulty) : null } }); const extraIndex = newRoll.advantage ? 3 : 2; newRoll.extra = newRoll.extra.slice(extraIndex); - const tagTeamSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); - - const actor = message.system.source.actor ? await foundry.utils.fromUuid(message.system.source.actor) : null; + const actor = parsedRoll.options.source.actor + ? await foundry.utils.fromUuid(parsedRoll.options.source.actor) + : null; const config = { - source: { actor: message.system.source.actor ?? '' }, - targets: message.system.targets, - tagTeamSelected: Object.values(tagTeamSettings.members).some(x => x.messageId === message._id), + source: { actor: parsedRoll.options.source.actor ?? '' }, + targets: parsedRoll.targets, roll: newRoll, - rerolledRoll: message.system.roll, + rerolledRoll: parsedRoll.roll, resourceUpdates: new ResourceUpdateMap(actor) }; diff --git a/module/documents/activeEffect.mjs b/module/documents/activeEffect.mjs index 2869ab98..ec31ec71 100644 --- a/module/documents/activeEffect.mjs +++ b/module/documents/activeEffect.mjs @@ -8,6 +8,8 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { /**@override */ get isSuppressed() { + if (this.system.isSuppressed === true) return true; + // If this is a copied effect from an attachment, never suppress it // (These effects have attachmentSource metadata) if (this.flags?.daggerheart?.attachmentSource) { @@ -15,7 +17,7 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { } // Then apply the standard suppression rules - if (['weapon', 'armor'].includes(this.parent?.type)) { + if (['weapon', 'armor'].includes(this.parent?.type) && this.transfer) { return !this.parent.system.equipped; } @@ -76,7 +78,7 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { throw new Error('The array of sub-types to restrict to must not be empty.'); } - const creatableEffects = ['base']; + const creatableEffects = types || ['base']; const documentTypes = this.TYPES.filter(type => creatableEffects.includes(type)).map(type => { const labelKey = `TYPES.ActiveEffect.${type}`; const label = game.i18n.has(labelKey) ? game.i18n.localize(labelKey) : type; @@ -175,9 +177,9 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { super.applyChangeField(model, change, field); } - _applyLegacy(actor, change, changes) { + static _applyChangeUnguided(actor, change, changes, options) { change.value = DhActiveEffect.getChangeValue(actor, change, change.effect); - super._applyLegacy(actor, change, changes); + super._applyChangeUnguided(actor, change, changes, options); } static getChangeValue(model, change, effect) { diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index f0538bff..023beaa0 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -4,6 +4,7 @@ import DHFeature from '../data/item/feature.mjs'; import { createScrollText, damageKeyToNumber, getDamageKey } from '../helpers/utils.mjs'; import DhCompanionLevelUp from '../applications/levelup/companionLevelup.mjs'; import { ResourceUpdateMap } from '../data/action/baseAction.mjs'; +import { abilities } from '../config/actorConfig.mjs'; export default class DhpActor extends Actor { parties = new Set(); @@ -509,6 +510,30 @@ export default class DhpActor extends Actor { return await rollClass.build(config); } + async rollTrait(trait, options = {}) { + const abilityLabel = game.i18n.localize(abilities[trait].label); + const config = { + event: event, + title: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.name}`, + headerTitle: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { + ability: abilityLabel + }), + effects: await game.system.api.data.actions.actionsTypes.base.getEffects(this), + roll: { + trait: trait, + type: 'trait' + }, + hasRoll: true, + actionType: 'action', + headerTitle: `${game.i18n.localize('DAGGERHEART.GENERAL.dualityRoll')}: ${this.name}`, + title: game.i18n.format('DAGGERHEART.UI.Chat.dualityRoll.abilityCheckTitle', { + ability: abilityLabel + }), + ...options + }; + return await this.diceRoll(config); + } + get rollClass() { return CONFIG.Dice.daggerheart[['character', 'companion'].includes(this.type) ? 'DualityRoll' : 'D20Roll']; } @@ -573,8 +598,7 @@ export default class DhpActor extends Actor { const availableStress = this.system.resources.stress.max - this.system.resources.stress.value; const canUseArmor = - this.system.armor && - this.system.armor.system.marks.value < this.system.armorScore && + this.system.armorScore.value < this.system.armorScore.max && type.every(t => this.system.armorApplicableDamageTypes[t] === true); const canUseStress = Object.keys(stressDamageReduction).reduce((acc, x) => { const rule = stressDamageReduction[x]; @@ -614,12 +638,7 @@ export default class DhpActor extends Actor { const hpDamage = updates.find(u => u.key === CONFIG.DH.GENERAL.healingTypes.hitPoints.id); if (hpDamage?.value) { hpDamage.value = this.convertDamageToThreshold(hpDamage.value); - if ( - this.type === 'character' && - !isDirect && - this.system.armor && - this.#canReduceDamage(hpDamage.value, hpDamage.damageTypes) - ) { + if (this.type === 'character' && !isDirect && this.#canReduceDamage(hpDamage.value, hpDamage.damageTypes)) { const armorSlotResult = await this.owner.query( 'armorSlot', { @@ -632,12 +651,10 @@ export default class DhpActor extends Actor { } ); if (armorSlotResult) { - const { modifiedDamage, armorSpent, stressSpent } = armorSlotResult; + const { modifiedDamage, armorChanges, stressSpent } = armorSlotResult; updates.find(u => u.key === 'hitPoints').value = modifiedDamage; - if (armorSpent) { - const armorUpdate = updates.find(u => u.key === 'armor'); - if (armorUpdate) armorUpdate.value += armorSpent; - else updates.push({ value: armorSpent, key: 'armor' }); + for (const armorChange of armorChanges) { + updates.push({ value: armorChange.amount, key: 'armor', uuid: armorChange.uuid }); } if (stressSpent) { const stressUpdate = updates.find(u => u.key === 'stress'); @@ -784,12 +801,8 @@ export default class DhpActor extends Actor { ); break; case 'armor': - if (this.system.armor?.system?.marks) { - updates.armor.resources['system.marks.value'] = Math.max( - Math.min(valueFunc(this.system.armor.system.marks, r), this.system.armorScore), - 0 - ); - } + if (!r.uuid) this.system.updateArmorValue(r); + else this.system.updateArmorEffectValue(r); break; default: if (this.system.resources?.[r.key]) { @@ -1005,4 +1018,20 @@ export default class DhpActor extends Actor { return allTokens; } + + /**@inheritdoc */ + *allApplicableEffects({ noSelfArmor, noTransferArmor } = {}) { + for (const effect of this.effects) { + if (!noSelfArmor || effect.type !== 'armor') yield effect; + } + for (const item of this.items) { + for (const effect of item.effects) { + if (effect.transfer && (!noTransferArmor || effect.type !== 'armor')) yield effect; + } + } + } + + applyActiveEffects(phase) { + super.applyActiveEffects(phase); + } } diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index 53921329..8b094678 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -177,14 +177,6 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { config.effects = await game.system.api.data.actions.actionsTypes.base.getEffects(actor, item); 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/documents/item.mjs b/module/documents/item.mjs index 67f7d253..56048a81 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -230,4 +230,14 @@ export default class DHItem extends foundry.documents.Item { async _preDelete() { this.deleteTriggers(); } + + /** @inheritDoc */ + static migrateData(source) { + const documentClass = game.system.api.data.items[`DH${source.type?.capitalize()}`]; + if (documentClass?.migrateDocumentData) { + documentClass.migrateDocumentData(source); + } + + return super.migrateData(source); + } } diff --git a/module/documents/tooltipManager.mjs b/module/documents/tooltipManager.mjs index 4793f1f7..6cfecb00 100644 --- a/module/documents/tooltipManager.mjs +++ b/module/documents/tooltipManager.mjs @@ -224,7 +224,7 @@ export default class DhTooltipManager extends foundry.helpers.interaction.Toolti if (locked || element.dataset.hasOwnProperty('locked')) this.lockTooltip(); } - _setAnchor(direction, options) { + _setAnchor(direction, options = {}) { const directions = this.constructor.TOOLTIP_DIRECTIONS; const pad = this.constructor.TOOLTIP_MARGIN_PX; const pos = this.element.getBoundingClientRect(); diff --git a/module/helpers/handlebarsHelper.mjs b/module/helpers/handlebarsHelper.mjs index 1c47f8dc..7f30d970 100644 --- a/module/helpers/handlebarsHelper.mjs +++ b/module/helpers/handlebarsHelper.mjs @@ -49,7 +49,8 @@ export default class RegisterHandlebarsHelpers { } static damageSymbols(damageParts) { - const symbols = [...new Set(damageParts.map(x => x.type))].map(p => CONFIG.DH.GENERAL.damageTypes[p].icon); + const allTypes = [...new Set([...damageParts].flatMap(x => Array.from(x.type)))]; + const symbols = allTypes.map(p => CONFIG.DH.GENERAL.damageTypes[p].icon); return new Handlebars.SafeString(Array.from(symbols).map(symbol => ``)); } diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index bb05fc39..9038bb08 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -528,7 +528,8 @@ export function expireActiveEffects(actor, allowedTypes = null) { export async function getCritDamageBonus(formula) { const critRoll = new Roll(formula); - return critRoll.dice.reduce((acc, dice) => acc + dice.faces * dice.number, 0); + await critRoll.evaluate(); + return critRoll.dice.reduce((acc, dice) => acc + dice.faces * dice.results.filter(r => r.active).length, 0); } export function htmlToText(html) { @@ -742,3 +743,67 @@ export function getUnusedDamageTypes(parts) { return acc; }, []); } + +/** Returns resolved armor sources ordered by application order */ +export function getArmorSources(actor) { + const rawArmorSources = Array.from(actor.allApplicableEffects()).filter(x => x.system.armorData); + if (actor.system.armor) rawArmorSources.push(actor.system.armor); + + const data = rawArmorSources.map(doc => { + // Get the origin item. Since the actor is already loaded, it should already be cached + // Consider the relative function versions if this causes an issue + const isItem = doc instanceof Item; + const origin = isItem ? doc : doc.origin ? foundry.utils.fromUuidSync(doc.origin) : doc.parent; + return { + origin, + name: origin.name, + document: doc, + data: doc.system.armor ?? doc.system.armorData, + disabled: !!doc.disabled || !!doc.isSuppressed + }; + }); + + return sortBy(data, ({ origin }) => { + switch (origin?.type) { + case 'class': + case 'subclass': + case 'ancestry': + case 'community': + case 'feature': + case 'domainCard': + return 2; + case 'loot': + case 'consumable': + return 3; + case 'character': + return 4; + case 'weapon': + return 5; + case 'armor': + return 6; + default: + return 1; + } + }); +} + +/** + * Returns an array sorted by a function that returns a thing to compare, or an array to compare in order + * Similar to lodash's sortBy function. + */ +export function sortBy(arr, fn) { + const directCompare = (a, b) => (a < b ? -1 : a > b ? 1 : 0); + const cmp = (a, b) => { + const resultA = fn(a); + const resultB = fn(b); + if (Array.isArray(resultA) && Array.isArray(resultB)) { + for (let idx = 0; idx < Math.min(resultA.length, resultB.length); idx++) { + const result = directCompare(resultA[idx], resultB[idx]); + if (result !== 0) return result; + } + return 0; + } + return directCompare(resultA, resultB); + }; + return arr.sort(cmp); +} diff --git a/module/systemRegistration/handlebars.mjs b/module/systemRegistration/handlebars.mjs index f51e1035..36df8b54 100644 --- a/module/systemRegistration/handlebars.mjs +++ b/module/systemRegistration/handlebars.mjs @@ -39,6 +39,7 @@ export const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/ui/tooltip/parts/tooltipChips.hbs', 'systems/daggerheart/templates/ui/tooltip/parts/tooltipTags.hbs', 'systems/daggerheart/templates/dialogs/downtime/activities.hbs', + 'systems/daggerheart/templates/dialogs/tagTeamDialog/parts/tagTeamDamageParts.hbs', 'systems/daggerheart/templates/dialogs/dice-roll/costSelection.hbs', 'systems/daggerheart/templates/ui/chat/parts/roll-part.hbs', 'systems/daggerheart/templates/ui/chat/parts/description-part.hbs', @@ -47,6 +48,7 @@ export const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/ui/chat/parts/button-part.hbs', 'systems/daggerheart/templates/ui/itemBrowser/itemContainer.hbs', 'systems/daggerheart/templates/scene/dh-config.hbs', - 'systems/daggerheart/templates/settings/appearance-settings/diceSoNiceTab.hbs' + 'systems/daggerheart/templates/settings/appearance-settings/diceSoNiceTab.hbs', + 'systems/daggerheart/templates/sheets/activeEffect/typeChanges/armorChange.hbs' ]); }; diff --git a/module/systemRegistration/migrations.mjs b/module/systemRegistration/migrations.mjs index 4216c38f..458ee6ef 100644 --- a/module/systemRegistration/migrations.mjs +++ b/module/systemRegistration/migrations.mjs @@ -193,7 +193,7 @@ export async function runMigrations() { } if (foundry.utils.isNewerVersion('1.2.7', lastMigrationVersion)) { - const tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll); + const tagTeam = game.settings.get(CONFIG.DH.id, 'TagTeamRoll'); const initatorMissing = tagTeam.initiator && !game.actors.some(actor => actor.id === tagTeam.initiator); const missingMembers = Object.keys(tagTeam.members).reduce((acc, id) => { if (!game.actors.some(actor => actor.id === id)) { @@ -206,7 +206,7 @@ export async function runMigrations() { initiator: initatorMissing ? null : tagTeam.initiator, members: missingMembers }); - await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, tagTeam); + await game.settings.set(CONFIG.DH.id, 'TagTeamRoll', tagTeam); lastMigrationVersion = '1.2.7'; } @@ -246,6 +246,101 @@ export async function runMigrations() { lastMigrationVersion = '1.6.0'; } + + if (foundry.utils.isNewerVersion('2.0.0', lastMigrationVersion)) { + const progress = game.system.api.applications.ui.DhProgress.createMigrationProgress(0); + const progressBuffer = 50; + + //#region Data Setup + const lockedPacks = []; + const itemPacks = game.packs.filter(x => x.metadata.type === 'Item'); + const actorPacks = game.packs.filter(x => x.metadata.type === 'Actor'); + + const getIndexes = async (packs, type) => { + const indexes = []; + for (const pack of packs) { + const indexValues = pack.index.values().reduce((acc, index) => { + if (!type || index.type === type) acc.push(index.uuid); + return acc; + }, []); + + if (indexValues.length && pack.locked) { + lockedPacks.push(pack.collection); + await pack.configure({ locked: false }); + } + + indexes.push(...indexValues); + } + + return indexes; + }; + + const itemEntries = await getIndexes(itemPacks); + const characterEntries = await getIndexes(actorPacks, 'character'); + + const worldItems = game.items; + const worldCharacters = game.actors.filter(x => x.type === 'character'); + + /* The async fetches are the mainstay of time. Leaving 1 progress for the sync logic */ + const newMax = itemEntries.length + characterEntries.length + progressBuffer; + progress.updateMax(newMax); + + const compendiumItems = []; + for (const entry of itemEntries) { + const item = await foundry.utils.fromUuid(entry); + compendiumItems.push(item); + progress.advance(); + } + + const compendiumCharacters = []; + for (const entry of characterEntries) { + const character = await foundry.utils.fromUuid(entry); + compendiumCharacters.push(character); + progress.advance(); + } + //#endregion + + /* Migrate existing effects modifying armor, creating new Armor Effects instead */ + const migrateEffects = async entity => { + for (const effect of entity.effects) { + if (effect.system.changes.every(x => x.key !== 'system.armorScore')) continue; + + effect.update({ + 'system.changes': effect.system.changes.map(change => ({ + ...change, + type: change.key === 'system.armorScore' ? 'armor' : change.type, + value: change.key === 'system.armorScore' ? { current: 0, max: change.value } : change.value + })) + }); + } + }; + + /* Migrate existing armors effects */ + const migrateItems = async items => { + for (const item of items) { + await migrateEffects(item); + } + }; + + await migrateItems([...compendiumItems, ...worldItems]); + progress.advance({ by: progressBuffer / 2 }); + + for (const actor of [...compendiumCharacters, ...worldCharacters]) { + await migrateEffects(actor); + await migrateItems(actor.items); + } + + progress.advance({ by: progressBuffer / 2 }); + + for (let packId of lockedPacks) { + const pack = game.packs.get(packId); + await pack.configure({ locked: true }); + } + + progress.close(); + + lastMigrationVersion = '2.0.0'; + } //#endregion await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LastMigrationVersion, lastMigrationVersion); diff --git a/module/systemRegistration/settings.mjs b/module/systemRegistration/settings.mjs index e7ec37f5..658d2bd1 100644 --- a/module/systemRegistration/settings.mjs +++ b/module/systemRegistration/settings.mjs @@ -15,7 +15,7 @@ import { DhMetagamingSettings, DhVariantRuleSettings } from '../applications/settings/_module.mjs'; -import { CompendiumBrowserSettings, DhTagTeamRoll } from '../data/_module.mjs'; +import { CompendiumBrowserSettings } from '../data/_module.mjs'; export const registerDHSettings = () => { registerMenuSettings(); @@ -157,12 +157,6 @@ const registerNonConfigSettings = () => { type: DhCountdowns }); - game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll, { - scope: 'world', - config: false, - type: DhTagTeamRoll - }); - game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.CompendiumBrowserSettings, { scope: 'world', config: false, diff --git a/module/systemRegistration/socket.mjs b/module/systemRegistration/socket.mjs index 173ef02b..fb152959 100644 --- a/module/systemRegistration/socket.mjs +++ b/module/systemRegistration/socket.mjs @@ -15,6 +15,9 @@ export function handleSocketEvent({ action = null, data = {} } = {}) { case socketEvent.DowntimeTrigger: Party.downtimeMoveQuery(data); break; + case socketEvent.TagTeamStart: + Hooks.callAll(CONFIG.DH.HOOKS.hooksConfig.tagTeamStart, data); + break; } } @@ -22,7 +25,8 @@ export const socketEvent = { GMUpdate: 'DhGMUpdate', Refresh: 'DhRefresh', DhpFearUpdate: 'DhFearUpdate', - DowntimeTrigger: 'DowntimeTrigger' + DowntimeTrigger: 'DowntimeTrigger', + TagTeamStart: 'DhTagTeamStart' }; export const GMUpdateEvent = { diff --git a/src/packs/classes/feature_Strange_Patterns_6YsfFjmCGuFYVhT4.json b/src/packs/classes/feature_Strange_Patterns_6YsfFjmCGuFYVhT4.json index ce1f499f..6939ff7f 100644 --- a/src/packs/classes/feature_Strange_Patterns_6YsfFjmCGuFYVhT4.json +++ b/src/packs/classes/feature_Strange_Patterns_6YsfFjmCGuFYVhT4.json @@ -85,7 +85,7 @@ { "trigger": "dualityRoll", "triggeringActorType": "self", - "command": "/* Ignore if it's a TagTeam roll */\nconst tagTeam = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);\nif (tagTeam.members[actor.id]) return;\n\n/* Check if there's a Strange Pattern match */\nconst dice = [roll.dFear.total, roll.dHope.total];\nconst resource = this.parent.resource?.diceStates ? Object.values(this.parent.resource.diceStates).map(x => x.value)[0] : null;\nconst nrMatches = dice.filter(x => x === resource).length;\n\nif (!nrMatches) return;\n\n/* Create a dialog to choose Hope or Stress - or to cancel*/\nconst content = `\n
${game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsContentTitle', { nr: nrMatches })}
\n
${game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsContentSubTitle', { nr: nrMatches })}
\n
${game.i18n.localize('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsActionExplanation')}
\n
\n \n \n
\n
`;\n\nconst result = await foundry.applications.api.DialogV2.input({\n classes: ['dh-style', 'two-big-buttons'],\n window: { title: this.item.name },\n content: content,\n render: (_, dialog) => {\n const hopeButton = dialog.element.querySelector('#hopeButton');\n const stressButton = dialog.element.querySelector('#stressButton');\ndialog.element.querySelector('button[type=\"submit\"]').disabled = true;\n \n const updateFunc = (event, selector, adding, clamp) => {\n const button = event.target.closest(`#${selector}Button`);\n const parent = event.target.closest('.flexrow');\n const hope = Number.parseInt(parent.querySelector('#hopeButton label').innerHTML);\n const stress = Number.parseInt(parent.querySelector('#stressButton label').innerHTML);\n const currentTotal = (Number.isNumeric(hope) ? hope : 0) + (Number.isNumeric(stress) ? stress : 0);\n if (adding && currentTotal === nrMatches) return;\n \n const current = Number.parseInt(button.querySelector('label').innerHTML);\n if (!adding && current === 0) return;\n \n const value = Number.isNumeric(current) ? adding ? current+1 : current-1 : 1;\n if (!dialog.data) dialog.data = {};\n dialog.data[selector] = clamp(value);\n button.querySelector('label').innerHTML = dialog.data[selector];\n\n event.target.closest('.dialog-form').querySelector('button[type=\"submit\"]').disabled = !adding || currentTotal < (nrMatches-1);\n \n };\n hopeButton.addEventListener('click', event => updateFunc(event, 'hope', true, x => Math.min(x, nrMatches)));\n hopeButton.addEventListener('contextmenu', event => updateFunc(event, 'hope', false, x => Math.max(x, 0)));\n stressButton.addEventListener('click', event => updateFunc(event, 'stress', true, x => Math.min(x, nrMatches)));\n stressButton.addEventListener('contextmenu', event => updateFunc(event, 'stress', false, x => Math.max(x, 0)));\n },\n ok: { callback: (_event, _result, dialog) => {\n const hope = dialog.data.hope ?? 0;\n const stress = dialog.data.stress ?? 0;\n if (!hope && !stress) return;\n\n /* Return resource update according to choices */\n const hopeUpdate = hope ? { key: 'hope', value: hope, total: -hope, enabled: true } : null;\n const stressUpdate = stress ? { key: 'stress', value: -stress, total: stress, enabled: true } : null;\n return { updates: [hopeUpdate, stressUpdate].filter(x => x) };\n }}\n});\n\nreturn result;" + "command": "/* Check if there's a Strange Pattern match */\nconst dice = [roll.dFear.total, roll.dHope.total];\nconst resource = this.parent.resource?.diceStates ? Object.values(this.parent.resource.diceStates).map(x => x.value)[0] : null;\nconst nrMatches = dice.filter(x => x === resource).length;\n\nif (!nrMatches) return;\n\n/* Create a dialog to choose Hope or Stress - or to cancel*/\nconst content = `\n
${game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsContentTitle', { nr: nrMatches })}
\n
${game.i18n.format('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsContentSubTitle', { nr: nrMatches })}
\n
${game.i18n.localize('DAGGERHEART.CONFIG.Triggers.triggerTexts.strangePatternsActionExplanation')}
\n
\n \n \n
\n
`;\n\nconst result = await foundry.applications.api.DialogV2.input({\n classes: ['dh-style', 'two-big-buttons'],\n window: { title: this.item.name },\n content: content,\n render: (_, dialog) => {\n const hopeButton = dialog.element.querySelector('#hopeButton');\n const stressButton = dialog.element.querySelector('#stressButton');\ndialog.element.querySelector('button[type=\"submit\"]').disabled = true;\n \n const updateFunc = (event, selector, adding, clamp) => {\n const button = event.target.closest(`#${selector}Button`);\n const parent = event.target.closest('.flexrow');\n const hope = Number.parseInt(parent.querySelector('#hopeButton label').innerHTML);\n const stress = Number.parseInt(parent.querySelector('#stressButton label').innerHTML);\n const currentTotal = (Number.isNumeric(hope) ? hope : 0) + (Number.isNumeric(stress) ? stress : 0);\n if (adding && currentTotal === nrMatches) return;\n \n const current = Number.parseInt(button.querySelector('label').innerHTML);\n if (!adding && current === 0) return;\n \n const value = Number.isNumeric(current) ? adding ? current+1 : current-1 : 1;\n if (!dialog.data) dialog.data = {};\n dialog.data[selector] = clamp(value);\n button.querySelector('label').innerHTML = dialog.data[selector];\n\n event.target.closest('.dialog-form').querySelector('button[type=\"submit\"]').disabled = !adding || currentTotal < (nrMatches-1);\n \n };\n hopeButton.addEventListener('click', event => updateFunc(event, 'hope', true, x => Math.min(x, nrMatches)));\n hopeButton.addEventListener('contextmenu', event => updateFunc(event, 'hope', false, x => Math.max(x, 0)));\n stressButton.addEventListener('click', event => updateFunc(event, 'stress', true, x => Math.min(x, nrMatches)));\n stressButton.addEventListener('contextmenu', event => updateFunc(event, 'stress', false, x => Math.max(x, 0)));\n },\n ok: { callback: (_event, _result, dialog) => {\n const hope = dialog.data.hope ?? 0;\n const stress = dialog.data.stress ?? 0;\n if (!hope && !stress) return;\n\n /* Return resource update according to choices */\n const hopeUpdate = hope ? { key: 'hope', value: hope, total: -hope, enabled: true } : null;\n const stressUpdate = stress ? { key: 'stress', value: -stress, total: stress, enabled: true } : null;\n return { updates: [hopeUpdate, stressUpdate].filter(x => x) };\n }}\n});\n\nreturn result;" } ] } diff --git a/src/packs/domains/domainCard_Armorer_cy8GjBPGc9w9RaGO.json b/src/packs/domains/domainCard_Armorer_cy8GjBPGc9w9RaGO.json index 059fb24c..cad6012e 100644 --- a/src/packs/domains/domainCard_Armorer_cy8GjBPGc9w9RaGO.json +++ b/src/packs/domains/domainCard_Armorer_cy8GjBPGc9w9RaGO.json @@ -92,34 +92,28 @@ "name": "Armorer", "type": "base", "system": { - "rangeDependence": { - "enabled": false, - "type": "withinRange", - "target": "hostile", - "range": "melee" - } + "changes": [ + { + "type": "armor", + "phase": "initial", + "priority": 20, + "value": { + "max": "1", + "interaction": "active" + } + } + ] }, - "_id": "cED730OjuMW5haJR", + "_id": "tJw2JIPcT9hEMRXg", "img": "icons/tools/hand/hammer-and-nail.webp", - "changes": [ - { - "key": "system.armorScore", - "mode": 2, - "value": "1", - "priority": null - } - ], "disabled": false, "duration": { - "startTime": null, - "combat": null, - "seconds": null, - "rounds": null, - "turns": null, - "startRound": null, - "startTurn": null + "value": null, + "units": "seconds", + "expiry": null, + "expired": false }, - "description": "

While you’re wearing armor, gain a +1 bonus to your Armor Score.

", + "description": "

While you’re wearing armor, gain a +1 bonus to your Armor Score.

", "origin": null, "tint": "#ffffff", "transfer": true, @@ -129,7 +123,10 @@ "_stats": { "compendiumSource": null }, - "_key": "!items.effects!cy8GjBPGc9w9RaGO.cED730OjuMW5haJR" + "start": null, + "showIcon": 1, + "folder": null, + "_key": "!items.effects!cy8GjBPGc9w9RaGO.tJw2JIPcT9hEMRXg" } ], "ownership": { diff --git a/src/packs/domains/domainCard_Bare_Bones_l5D9kq901JDESaXw.json b/src/packs/domains/domainCard_Bare_Bones_l5D9kq901JDESaXw.json index 3b1ea76a..098f5f4c 100644 --- a/src/packs/domains/domainCard_Bare_Bones_l5D9kq901JDESaXw.json +++ b/src/packs/domains/domainCard_Bare_Bones_l5D9kq901JDESaXw.json @@ -19,7 +19,52 @@ } }, "flags": {}, - "effects": [], + "effects": [ + { + "name": "Bare Bones", + "type": "base", + "system": { + "changes": [ + { + "type": "armor", + "phase": "initial", + "priority": 20, + "value": { + "max": "3 + @system.traits.strength.value", + "interaction": "inactive", + "damageThresholds": { + "major": "9 + (@tier - 1) * 5 + max(0, (@tier -2) * 2 )", + "severe": "19 + (@tier - 1) * 5 + max(0, (@tier -2) * 2 )" + } + } + } + ] + }, + "_id": "FCsgz7Tdsw6QUzBs", + "img": "icons/magic/control/buff-strength-muscle-damage-orange.webp", + "disabled": false, + "start": null, + "duration": { + "value": null, + "units": "seconds", + "expiry": null, + "expired": false + }, + "description": "

You have a base Armor Score of 3 + your Strength.

", + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "showIcon": 1, + "folder": null, + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null + }, + "_key": "!items.effects!l5D9kq901JDESaXw.FCsgz7Tdsw6QUzBs" + } + ], "ownership": { "default": 0, "MQSznptE5yLT7kj8": 3 diff --git a/src/packs/domains/domainCard_Book_of_Ava_YtZzYBtR0yLPPA93.json b/src/packs/domains/domainCard_Book_of_Ava_YtZzYBtR0yLPPA93.json index 7977e56a..fa247c89 100644 --- a/src/packs/domains/domainCard_Book_of_Ava_YtZzYBtR0yLPPA93.json +++ b/src/packs/domains/domainCard_Book_of_Ava_YtZzYBtR0yLPPA93.json @@ -105,7 +105,7 @@ }, "effects": [ { - "_id": "LdcT1nrkd5ORCU4n", + "_id": "ptYT10JZ2WJHvFMd", "onSave": false } ], @@ -252,7 +252,7 @@ "img": "icons/magic/defensive/shield-barrier-glowing-triangle-blue.webp", "origin": "Compendium.daggerheart.domains.Item.YtZzYBtR0yLPPA93", "transfer": false, - "_id": "LdcT1nrkd5ORCU4n", + "_id": "ptYT10JZ2WJHvFMd", "type": "base", "system": { "rangeDependence": { @@ -263,10 +263,12 @@ }, "changes": [ { - "key": "system.armorScore", - "value": 1, - "priority": null, - "type": "add" + "type": "armor", + "phase": "initial", + "priority": 20, + "value": { + "max": "1" + } } ], "duration": { @@ -298,7 +300,7 @@ }, "showIcon": 1, "folder": null, - "_key": "!items.effects!YtZzYBtR0yLPPA93.LdcT1nrkd5ORCU4n" + "_key": "!items.effects!YtZzYBtR0yLPPA93.ptYT10JZ2WJHvFMd" } ], "ownership": { diff --git a/src/packs/domains/domainCard_Valor_Touched_k1AtYd3lSchIymBr.json b/src/packs/domains/domainCard_Valor_Touched_k1AtYd3lSchIymBr.json index 84ef1025..61c7ace8 100644 --- a/src/packs/domains/domainCard_Valor_Touched_k1AtYd3lSchIymBr.json +++ b/src/packs/domains/domainCard_Valor_Touched_k1AtYd3lSchIymBr.json @@ -93,32 +93,25 @@ "name": "Valor-Touched", "type": "base", "system": { - "rangeDependence": { - "enabled": false, - "type": "withinRange", - "target": "hostile", - "range": "melee" - } + "changes": [ + { + "type": "armor", + "phase": "initial", + "priority": 20, + "value": { + "max": "1" + } + } + ] }, - "_id": "H9lgIqqp1imSNOv9", + "_id": "Ma8Zp005QYKPWIEN", "img": "icons/magic/control/control-influence-rally-purple.webp", - "changes": [ - { - "key": "system.armorScore", - "mode": 2, - "value": "1", - "priority": null - } - ], "disabled": false, "duration": { - "startTime": null, - "combat": null, - "seconds": null, - "rounds": null, - "turns": null, - "startRound": null, - "startTurn": null + "value": null, + "units": "seconds", + "expiry": null, + "expired": false }, "description": "", "origin": null, @@ -130,7 +123,10 @@ "_stats": { "compendiumSource": null }, - "_key": "!items.effects!k1AtYd3lSchIymBr.H9lgIqqp1imSNOv9" + "start": null, + "showIcon": 1, + "folder": null, + "_key": "!items.effects!k1AtYd3lSchIymBr.Ma8Zp005QYKPWIEN" } ], "ownership": { diff --git a/src/packs/environments/folders_Tier_1_GQ0VnOLrKBIHR6Us.json b/src/packs/environments/folders_Tier_1_GQ0VnOLrKBIHR6Us.json index 7e6595b9..38f35169 100644 --- a/src/packs/environments/folders_Tier_1_GQ0VnOLrKBIHR6Us.json +++ b/src/packs/environments/folders_Tier_1_GQ0VnOLrKBIHR6Us.json @@ -1,5 +1,5 @@ { - "type": "Item", + "type": "Actor", "folder": null, "name": "Tier 1", "color": null, diff --git a/src/packs/environments/folders_Tier_2_XMeecO3IRvu5ck6F.json b/src/packs/environments/folders_Tier_2_XMeecO3IRvu5ck6F.json index 9a77a641..2af9df23 100644 --- a/src/packs/environments/folders_Tier_2_XMeecO3IRvu5ck6F.json +++ b/src/packs/environments/folders_Tier_2_XMeecO3IRvu5ck6F.json @@ -1,5 +1,5 @@ { - "type": "Item", + "type": "Actor", "folder": null, "name": "Tier 2", "color": null, diff --git a/src/packs/environments/folders_Tier_3_MfrIkJK12PAEfbPL.json b/src/packs/environments/folders_Tier_3_MfrIkJK12PAEfbPL.json index 1ae0a609..400a2ca9 100644 --- a/src/packs/environments/folders_Tier_3_MfrIkJK12PAEfbPL.json +++ b/src/packs/environments/folders_Tier_3_MfrIkJK12PAEfbPL.json @@ -1,5 +1,5 @@ { - "type": "Item", + "type": "Actor", "folder": null, "name": "Tier 3", "color": null, diff --git a/src/packs/environments/folders_Tier_4_IKumu5HTLqONLYqb.json b/src/packs/environments/folders_Tier_4_IKumu5HTLqONLYqb.json index ed5b9f0a..3a171ddb 100644 --- a/src/packs/environments/folders_Tier_4_IKumu5HTLqONLYqb.json +++ b/src/packs/environments/folders_Tier_4_IKumu5HTLqONLYqb.json @@ -1,5 +1,5 @@ { - "type": "Item", + "type": "Actor", "folder": null, "name": "Tier 4", "color": null, diff --git a/src/packs/items/armors/armor_Advanced_Chainmail_Armor_LzLOJ9EVaHWAjoq9.json b/src/packs/items/armors/armor_Advanced_Chainmail_Armor_LzLOJ9EVaHWAjoq9.json index 174f20c8..4566396a 100644 --- a/src/packs/items/armors/armor_Advanced_Chainmail_Armor_LzLOJ9EVaHWAjoq9.json +++ b/src/packs/items/armors/armor_Advanced_Chainmail_Armor_LzLOJ9EVaHWAjoq9.json @@ -5,6 +5,10 @@ "_id": "LzLOJ9EVaHWAjoq9", "img": "icons/equipment/chest/breastplate-banded-steel-gold.webp", "system": { + "armor": { + "current": 0, + "max": 6 + }, "description": "", "actions": {}, "attached": [], diff --git a/src/packs/items/armors/armor_Advanced_Full_Plate_Armor_crIbCb9NZ4K0VpoU.json b/src/packs/items/armors/armor_Advanced_Full_Plate_Armor_crIbCb9NZ4K0VpoU.json index dbc9d29f..52adc7aa 100644 --- a/src/packs/items/armors/armor_Advanced_Full_Plate_Armor_crIbCb9NZ4K0VpoU.json +++ b/src/packs/items/armors/armor_Advanced_Full_Plate_Armor_crIbCb9NZ4K0VpoU.json @@ -5,6 +5,10 @@ "_id": "crIbCb9NZ4K0VpoU", "img": "icons/equipment/chest/breastplate-layered-steel-grey.webp", "system": { + "armor": { + "current": 0, + "max": 6 + }, "description": "", "actions": {}, "attached": [], diff --git a/src/packs/items/armors/armor_Advanced_Gambeson_Armor_epkAmlZVk7HOfUUT.json b/src/packs/items/armors/armor_Advanced_Gambeson_Armor_epkAmlZVk7HOfUUT.json index c9ffc8a3..36edec39 100644 --- a/src/packs/items/armors/armor_Advanced_Gambeson_Armor_epkAmlZVk7HOfUUT.json +++ b/src/packs/items/armors/armor_Advanced_Gambeson_Armor_epkAmlZVk7HOfUUT.json @@ -5,6 +5,10 @@ "_id": "epkAmlZVk7HOfUUT", "img": "icons/equipment/chest/breastplate-purple.webp", "system": { + "armor": { + "current": 0, + "max": 5 + }, "description": "", "actions": {}, "attached": [], diff --git a/src/packs/items/armors/armor_Advanced_Leather_Armor_itSOp2GCyem0f7oM.json b/src/packs/items/armors/armor_Advanced_Leather_Armor_itSOp2GCyem0f7oM.json index 4e1927e3..3e5dbd3b 100644 --- a/src/packs/items/armors/armor_Advanced_Leather_Armor_itSOp2GCyem0f7oM.json +++ b/src/packs/items/armors/armor_Advanced_Leather_Armor_itSOp2GCyem0f7oM.json @@ -5,6 +5,10 @@ "_id": "itSOp2GCyem0f7oM", "img": "icons/equipment/chest/breastplate-layered-leather-blue.webp", "system": { + "armor": { + "current": 0, + "max": 5 + }, "description": "", "actions": {}, "attached": [], diff --git a/src/packs/items/armors/armor_Bare_Bones_ITAjcigTcUw5pMCN.json b/src/packs/items/armors/armor_Bare_Bones_ITAjcigTcUw5pMCN.json deleted file mode 100644 index 5158b100..00000000 --- a/src/packs/items/armors/armor_Bare_Bones_ITAjcigTcUw5pMCN.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "folder": "tI3bfr6Sgi16Z7zm", - "name": "Bare Bones", - "type": "armor", - "_id": "ITAjcigTcUw5pMCN", - "img": "icons/magic/control/buff-strength-muscle-damage.webp", - "system": { - "description": "

When you choose not to equip armor, you have a base Armor Score of 3 + your Strength and use the following as your base damage thresholds:

", - "actions": {}, - "attached": [], - "tier": 1, - "equipped": false, - "baseScore": 3, - "armorFeatures": [], - "marks": { - "value": 0 - }, - "baseThresholds": { - "major": 9, - "severe": 19 - } - }, - "effects": [ - { - "name": "Bare Bones", - "type": "base", - "system": { - "rangeDependence": { - "enabled": false, - "type": "withinRange", - "target": "hostile", - "range": "melee" - } - }, - "_id": "8ze88zUwdkQSKKJq", - "img": "icons/magic/control/buff-strength-muscle-damage.webp", - "changes": [ - { - "key": "system.armorScore", - "mode": 2, - "value": "@system.traits.strength.value", - "priority": 21 - } - ], - "disabled": false, - "duration": { - "startTime": null, - "combat": null, - "seconds": null, - "rounds": null, - "turns": null, - "startRound": null, - "startTurn": null - }, - "description": "

When you choose not to equip armor, you have a base Armor Score of 3 + your Strength and use the following as your base damage thresholds:

", - "origin": null, - "tint": "#ffffff", - "transfer": true, - "statuses": [], - "sort": 0, - "flags": {}, - "_stats": { - "compendiumSource": null - }, - "_key": "!items.effects!ITAjcigTcUw5pMCN.8ze88zUwdkQSKKJq" - } - ], - "sort": 0, - "ownership": { - "default": 0, - "MQSznptE5yLT7kj8": 3 - }, - "flags": {}, - "_key": "!items!ITAjcigTcUw5pMCN" -} diff --git a/src/packs/items/armors/armor_Bellamoi_Fine_Armor_WuoVwZA53XRAIt6d.json b/src/packs/items/armors/armor_Bellamoi_Fine_Armor_WuoVwZA53XRAIt6d.json index ce4e35fd..c470de87 100644 --- a/src/packs/items/armors/armor_Bellamoi_Fine_Armor_WuoVwZA53XRAIt6d.json +++ b/src/packs/items/armors/armor_Bellamoi_Fine_Armor_WuoVwZA53XRAIt6d.json @@ -5,6 +5,10 @@ "_id": "WuoVwZA53XRAIt6d", "img": "icons/equipment/chest/breastplate-layered-gold.webp", "system": { + "armor": { + "current": 0, + "max": 5 + }, "description": "", "actions": {}, "attached": [], diff --git a/src/packs/items/armors/armor_Bladefare_Armor_mNN6pvcsS10ChrWF.json b/src/packs/items/armors/armor_Bladefare_Armor_mNN6pvcsS10ChrWF.json index 8b276d5f..4ee73939 100644 --- a/src/packs/items/armors/armor_Bladefare_Armor_mNN6pvcsS10ChrWF.json +++ b/src/packs/items/armors/armor_Bladefare_Armor_mNN6pvcsS10ChrWF.json @@ -5,6 +5,10 @@ "_id": "mNN6pvcsS10ChrWF", "img": "icons/equipment/chest/breastplate-collared-steel-grey.webp", "system": { + "armor": { + "current": 0, + "max": 6 + }, "description": "", "actions": {}, "attached": [], diff --git a/src/packs/items/armors/armor_Chainmail_Armor_haULhuEg37zUUvhb.json b/src/packs/items/armors/armor_Chainmail_Armor_haULhuEg37zUUvhb.json index f7526e96..4f0719a7 100644 --- a/src/packs/items/armors/armor_Chainmail_Armor_haULhuEg37zUUvhb.json +++ b/src/packs/items/armors/armor_Chainmail_Armor_haULhuEg37zUUvhb.json @@ -5,6 +5,10 @@ "_id": "haULhuEg37zUUvhb", "img": "icons/equipment/chest/breastplate-scale-grey.webp", "system": { + "armor": { + "current": 0, + "max": 4 + }, "description": "", "actions": {}, "attached": [], diff --git a/src/packs/items/armors/armor_Channeling_Armor_vMJxEWz1srfwMsoj.json b/src/packs/items/armors/armor_Channeling_Armor_vMJxEWz1srfwMsoj.json index a4bd0fea..e805d5d1 100644 --- a/src/packs/items/armors/armor_Channeling_Armor_vMJxEWz1srfwMsoj.json +++ b/src/packs/items/armors/armor_Channeling_Armor_vMJxEWz1srfwMsoj.json @@ -5,6 +5,10 @@ "_id": "vMJxEWz1srfwMsoj", "img": "icons/equipment/chest/robe-collared-blue.webp", "system": { + "armor": { + "current": 0, + "max": 5 + }, "description": "", "actions": {}, "attached": [], diff --git a/src/packs/items/armors/armor_Dragonscale_Armor_mdQ69eFHyAQUDmE7.json b/src/packs/items/armors/armor_Dragonscale_Armor_mdQ69eFHyAQUDmE7.json index 5b39e41d..4b270234 100644 --- a/src/packs/items/armors/armor_Dragonscale_Armor_mdQ69eFHyAQUDmE7.json +++ b/src/packs/items/armors/armor_Dragonscale_Armor_mdQ69eFHyAQUDmE7.json @@ -5,6 +5,10 @@ "_id": "mdQ69eFHyAQUDmE7", "img": "icons/equipment/chest/breastplate-rivited-red.webp", "system": { + "armor": { + "current": 0, + "max": 5 + }, "description": "", "actions": { "J1MCpcfXByKaSSgx": { diff --git a/src/packs/items/armors/armor_Dunamis_Silkchain_hAY6UgdGT7dj22Pr.json b/src/packs/items/armors/armor_Dunamis_Silkchain_hAY6UgdGT7dj22Pr.json index 588d9cf9..6d223ada 100644 --- a/src/packs/items/armors/armor_Dunamis_Silkchain_hAY6UgdGT7dj22Pr.json +++ b/src/packs/items/armors/armor_Dunamis_Silkchain_hAY6UgdGT7dj22Pr.json @@ -5,6 +5,10 @@ "_id": "hAY6UgdGT7dj22Pr", "img": "icons/equipment/chest/robe-layered-red.webp", "system": { + "armor": { + "current": 0, + "max": 7 + }, "description": "", "actions": { "8PD5JQuS05IA6HJT": { diff --git a/src/packs/items/armors/armor_Elundrian_Chain_Armor_Q6LxmtFetDDkoZVZ.json b/src/packs/items/armors/armor_Elundrian_Chain_Armor_Q6LxmtFetDDkoZVZ.json index d63ce4df..1cf74e2e 100644 --- a/src/packs/items/armors/armor_Elundrian_Chain_Armor_Q6LxmtFetDDkoZVZ.json +++ b/src/packs/items/armors/armor_Elundrian_Chain_Armor_Q6LxmtFetDDkoZVZ.json @@ -5,6 +5,10 @@ "_id": "Q6LxmtFetDDkoZVZ", "img": "icons/equipment/chest/breastplate-sculpted-green.webp", "system": { + "armor": { + "current": 0, + "max": 4 + }, "description": "", "actions": {}, "attached": [], diff --git a/src/packs/items/armors/armor_Emberwoven_Armor_bcQUh4QG3qFX0Vx6.json b/src/packs/items/armors/armor_Emberwoven_Armor_bcQUh4QG3qFX0Vx6.json index 15b07474..616bbadf 100644 --- a/src/packs/items/armors/armor_Emberwoven_Armor_bcQUh4QG3qFX0Vx6.json +++ b/src/packs/items/armors/armor_Emberwoven_Armor_bcQUh4QG3qFX0Vx6.json @@ -5,6 +5,10 @@ "_id": "bcQUh4QG3qFX0Vx6", "img": "icons/equipment/chest/breastplate-layered-gilded-orange.webp", "system": { + "armor": { + "current": 0, + "max": 6 + }, "description": "", "actions": { "L8mHf4A8SylyxsMH": { diff --git a/src/packs/items/armors/armor_Full_Fortified_Armor_7emTSt6nhZuTlvt5.json b/src/packs/items/armors/armor_Full_Fortified_Armor_7emTSt6nhZuTlvt5.json index 8eb964cc..9f2d7ece 100644 --- a/src/packs/items/armors/armor_Full_Fortified_Armor_7emTSt6nhZuTlvt5.json +++ b/src/packs/items/armors/armor_Full_Fortified_Armor_7emTSt6nhZuTlvt5.json @@ -5,6 +5,10 @@ "_id": "7emTSt6nhZuTlvt5", "img": "icons/equipment/chest/breastplate-layered-steel.webp", "system": { + "armor": { + "current": 0, + "max": 4 + }, "description": "", "actions": {}, "attached": [], diff --git a/src/packs/items/armors/armor_Full_Plate_Armor_UdUJNa31WxFW2noa.json b/src/packs/items/armors/armor_Full_Plate_Armor_UdUJNa31WxFW2noa.json index 1ea120ed..7701d063 100644 --- a/src/packs/items/armors/armor_Full_Plate_Armor_UdUJNa31WxFW2noa.json +++ b/src/packs/items/armors/armor_Full_Plate_Armor_UdUJNa31WxFW2noa.json @@ -5,6 +5,10 @@ "_id": "UdUJNa31WxFW2noa", "img": "icons/equipment/chest/breastplate-collared-steel.webp", "system": { + "armor": { + "current": 0, + "max": 4 + }, "description": "", "actions": {}, "attached": [], diff --git a/src/packs/items/armors/armor_Gambeson_Armor_yJFp1bfpecDcStVK.json b/src/packs/items/armors/armor_Gambeson_Armor_yJFp1bfpecDcStVK.json index 1c775402..0ede5b60 100644 --- a/src/packs/items/armors/armor_Gambeson_Armor_yJFp1bfpecDcStVK.json +++ b/src/packs/items/armors/armor_Gambeson_Armor_yJFp1bfpecDcStVK.json @@ -5,6 +5,10 @@ "_id": "yJFp1bfpecDcStVK", "img": "icons/equipment/chest/vest-leather-tattered-white.webp", "system": { + "armor": { + "current": 0, + "max": 3 + }, "description": "", "actions": {}, "attached": [], diff --git a/src/packs/items/armors/armor_Harrowbone_Armor_dvyQeUVRLc9y6rnt.json b/src/packs/items/armors/armor_Harrowbone_Armor_dvyQeUVRLc9y6rnt.json index 82b4f791..85ad1d6a 100644 --- a/src/packs/items/armors/armor_Harrowbone_Armor_dvyQeUVRLc9y6rnt.json +++ b/src/packs/items/armors/armor_Harrowbone_Armor_dvyQeUVRLc9y6rnt.json @@ -5,6 +5,10 @@ "_id": "dvyQeUVRLc9y6rnt", "img": "icons/equipment/chest/breastplate-gorget-steel.webp", "system": { + "armor": { + "current": 0, + "max": 4 + }, "description": "", "actions": { "IzM88FIxQ35P5VB2": { diff --git a/src/packs/items/armors/armor_Improved_Chainmail_Armor_K5WkjS0NGqHYmhU3.json b/src/packs/items/armors/armor_Improved_Chainmail_Armor_K5WkjS0NGqHYmhU3.json index 96e320d1..ef93ecdd 100644 --- a/src/packs/items/armors/armor_Improved_Chainmail_Armor_K5WkjS0NGqHYmhU3.json +++ b/src/packs/items/armors/armor_Improved_Chainmail_Armor_K5WkjS0NGqHYmhU3.json @@ -5,6 +5,10 @@ "_id": "K5WkjS0NGqHYmhU3", "img": "icons/equipment/chest/breastplate-metal-scaled-grey.webp", "system": { + "armor": { + "current": 0, + "max": 5 + }, "description": "", "actions": {}, "attached": [], diff --git a/src/packs/items/armors/armor_Improved_Full_Plate_Armor_9f7RozpPTqrzJS1m.json b/src/packs/items/armors/armor_Improved_Full_Plate_Armor_9f7RozpPTqrzJS1m.json index ee63a774..1723c53a 100644 --- a/src/packs/items/armors/armor_Improved_Full_Plate_Armor_9f7RozpPTqrzJS1m.json +++ b/src/packs/items/armors/armor_Improved_Full_Plate_Armor_9f7RozpPTqrzJS1m.json @@ -5,6 +5,10 @@ "_id": "9f7RozpPTqrzJS1m", "img": "icons/equipment/chest/breastplate-cuirass-steel-grey.webp", "system": { + "armor": { + "current": 0, + "max": 5 + }, "description": "", "actions": {}, "attached": [], diff --git a/src/packs/items/armors/armor_Improved_Gambeson_Armor_jphnMZjnS2FkOH3s.json b/src/packs/items/armors/armor_Improved_Gambeson_Armor_jphnMZjnS2FkOH3s.json index 6f4ea1c3..a2ff6554 100644 --- a/src/packs/items/armors/armor_Improved_Gambeson_Armor_jphnMZjnS2FkOH3s.json +++ b/src/packs/items/armors/armor_Improved_Gambeson_Armor_jphnMZjnS2FkOH3s.json @@ -5,6 +5,10 @@ "_id": "jphnMZjnS2FkOH3s", "img": "icons/equipment/chest/breastplate-quilted-brown.webp", "system": { + "armor": { + "current": 0, + "max": 4 + }, "description": "", "actions": {}, "attached": [], diff --git a/src/packs/items/armors/armor_Improved_Leather_Armor_t91M61pSCMKStTNt.json b/src/packs/items/armors/armor_Improved_Leather_Armor_t91M61pSCMKStTNt.json index a4f38cc6..efa3643d 100644 --- a/src/packs/items/armors/armor_Improved_Leather_Armor_t91M61pSCMKStTNt.json +++ b/src/packs/items/armors/armor_Improved_Leather_Armor_t91M61pSCMKStTNt.json @@ -5,6 +5,10 @@ "_id": "t91M61pSCMKStTNt", "img": "icons/equipment/chest/breastplate-banded-simple-leather-brown.webp", "system": { + "armor": { + "current": 0, + "max": 4 + }, "description": "", "actions": {}, "attached": [], diff --git a/src/packs/items/armors/armor_Irontree_Breastplate_Armor_tzZntboNtHL5C6VM.json b/src/packs/items/armors/armor_Irontree_Breastplate_Armor_tzZntboNtHL5C6VM.json index a9e9eaca..a664ad9c 100644 --- a/src/packs/items/armors/armor_Irontree_Breastplate_Armor_tzZntboNtHL5C6VM.json +++ b/src/packs/items/armors/armor_Irontree_Breastplate_Armor_tzZntboNtHL5C6VM.json @@ -5,6 +5,10 @@ "_id": "tzZntboNtHL5C6VM", "img": "icons/equipment/chest/breastplate-layered-leather-brown-silver.webp", "system": { + "armor": { + "current": 0, + "max": 4 + }, "description": "", "actions": {}, "attached": [], diff --git a/src/packs/items/armors/armor_Leather_Armor_nibfdNtp2PtxvbVz.json b/src/packs/items/armors/armor_Leather_Armor_nibfdNtp2PtxvbVz.json index 37a13f2b..93d8f0cc 100644 --- a/src/packs/items/armors/armor_Leather_Armor_nibfdNtp2PtxvbVz.json +++ b/src/packs/items/armors/armor_Leather_Armor_nibfdNtp2PtxvbVz.json @@ -5,6 +5,10 @@ "_id": "nibfdNtp2PtxvbVz", "img": "icons/equipment/chest/breastplate-layered-leather-brown.webp", "system": { + "armor": { + "current": 0, + "max": 3 + }, "description": "", "actions": {}, "attached": [], diff --git a/src/packs/items/armors/armor_Legendary_Chainmail_Armor_EsIN5OLKe9ZYFNXZ.json b/src/packs/items/armors/armor_Legendary_Chainmail_Armor_EsIN5OLKe9ZYFNXZ.json index 4bee5e4f..6c93cbe4 100644 --- a/src/packs/items/armors/armor_Legendary_Chainmail_Armor_EsIN5OLKe9ZYFNXZ.json +++ b/src/packs/items/armors/armor_Legendary_Chainmail_Armor_EsIN5OLKe9ZYFNXZ.json @@ -5,6 +5,10 @@ "_id": "EsIN5OLKe9ZYFNXZ", "img": "icons/equipment/chest/breastplate-banded-blue.webp", "system": { + "armor": { + "current": 0, + "max": 7 + }, "description": "", "actions": {}, "attached": [], diff --git a/src/packs/items/armors/armor_Legendary_Full_Plate_Armor_SXWjUR2aUR6bYvdl.json b/src/packs/items/armors/armor_Legendary_Full_Plate_Armor_SXWjUR2aUR6bYvdl.json index baf544c2..f66e4c38 100644 --- a/src/packs/items/armors/armor_Legendary_Full_Plate_Armor_SXWjUR2aUR6bYvdl.json +++ b/src/packs/items/armors/armor_Legendary_Full_Plate_Armor_SXWjUR2aUR6bYvdl.json @@ -5,6 +5,10 @@ "_id": "SXWjUR2aUR6bYvdl", "img": "icons/equipment/chest/breastplate-layered-steel-blue-gold.webp", "system": { + "armor": { + "current": 0, + "max": 7 + }, "description": "", "actions": {}, "attached": [], diff --git a/src/packs/items/armors/armor_Legendary_Gambeson_Armor_c6tMXz4rPf9ioQrf.json b/src/packs/items/armors/armor_Legendary_Gambeson_Armor_c6tMXz4rPf9ioQrf.json index 338c85e8..4cf1c856 100644 --- a/src/packs/items/armors/armor_Legendary_Gambeson_Armor_c6tMXz4rPf9ioQrf.json +++ b/src/packs/items/armors/armor_Legendary_Gambeson_Armor_c6tMXz4rPf9ioQrf.json @@ -5,6 +5,10 @@ "_id": "c6tMXz4rPf9ioQrf", "img": "icons/equipment/chest/breastplate-layered-leather-blue-gold.webp", "system": { + "armor": { + "current": 0, + "max": 6 + }, "description": "", "actions": {}, "attached": [], diff --git a/src/packs/items/armors/armor_Legendary_Leather_Armor_Tptgl5WOj76TyFn7.json b/src/packs/items/armors/armor_Legendary_Leather_Armor_Tptgl5WOj76TyFn7.json index 42334dc4..3ddc5ed7 100644 --- a/src/packs/items/armors/armor_Legendary_Leather_Armor_Tptgl5WOj76TyFn7.json +++ b/src/packs/items/armors/armor_Legendary_Leather_Armor_Tptgl5WOj76TyFn7.json @@ -5,6 +5,10 @@ "_id": "Tptgl5WOj76TyFn7", "img": "icons/equipment/chest/breastplate-layered-gilded-black.webp", "system": { + "armor": { + "current": 0, + "max": 6 + }, "description": "", "actions": {}, "attached": [], diff --git a/src/packs/items/armors/armor_Monett_s_Cloak_AQzU2RsqS5V5bd1v.json b/src/packs/items/armors/armor_Monett_s_Cloak_AQzU2RsqS5V5bd1v.json index 9a8e1f22..6bb479a4 100644 --- a/src/packs/items/armors/armor_Monett_s_Cloak_AQzU2RsqS5V5bd1v.json +++ b/src/packs/items/armors/armor_Monett_s_Cloak_AQzU2RsqS5V5bd1v.json @@ -5,6 +5,10 @@ "_id": "AQzU2RsqS5V5bd1v", "img": "icons/equipment/chest/coat-collared-red.webp", "system": { + "armor": { + "current": 0, + "max": 6 + }, "description": "", "actions": {}, "attached": [], diff --git a/src/packs/items/armors/armor_Rosewild_Armor_tN8kAeBvNKM3EBFo.json b/src/packs/items/armors/armor_Rosewild_Armor_tN8kAeBvNKM3EBFo.json index 0f0f6430..e3cde9fb 100644 --- a/src/packs/items/armors/armor_Rosewild_Armor_tN8kAeBvNKM3EBFo.json +++ b/src/packs/items/armors/armor_Rosewild_Armor_tN8kAeBvNKM3EBFo.json @@ -5,6 +5,10 @@ "_id": "tN8kAeBvNKM3EBFo", "img": "icons/equipment/chest/breastplate-banded-leather-purple.webp", "system": { + "armor": { + "current": 0, + "max": 5 + }, "description": "", "actions": { "QRTnCYxJfuJHdnyV": { diff --git a/src/packs/items/armors/armor_Runes_of_Fortification_P4qAEDJUoNLgVRsA.json b/src/packs/items/armors/armor_Runes_of_Fortification_P4qAEDJUoNLgVRsA.json index 240a4f3e..2ccc80da 100644 --- a/src/packs/items/armors/armor_Runes_of_Fortification_P4qAEDJUoNLgVRsA.json +++ b/src/packs/items/armors/armor_Runes_of_Fortification_P4qAEDJUoNLgVRsA.json @@ -5,6 +5,10 @@ "_id": "P4qAEDJUoNLgVRsA", "img": "icons/magic/symbols/rune-sigil-red-orange.webp", "system": { + "armor": { + "current": 0, + "max": 6 + }, "description": "", "actions": { "37KLF2bim9nRdPTU": { diff --git a/src/packs/items/armors/armor_Runetan_Floating_Armor_tHlBUDQC24YMZqd6.json b/src/packs/items/armors/armor_Runetan_Floating_Armor_tHlBUDQC24YMZqd6.json index 8d4af425..593bc8e0 100644 --- a/src/packs/items/armors/armor_Runetan_Floating_Armor_tHlBUDQC24YMZqd6.json +++ b/src/packs/items/armors/armor_Runetan_Floating_Armor_tHlBUDQC24YMZqd6.json @@ -5,6 +5,10 @@ "_id": "tHlBUDQC24YMZqd6", "img": "icons/equipment/chest/breastplate-layered-leather-black.webp", "system": { + "armor": { + "current": 0, + "max": 4 + }, "description": "", "actions": { "Nn33zCIcWe6LQMDH": { diff --git a/src/packs/items/armors/armor_Savior_Chainmail_8X16lJQ3xltTwynm.json b/src/packs/items/armors/armor_Savior_Chainmail_8X16lJQ3xltTwynm.json index 714e8592..6826254a 100644 --- a/src/packs/items/armors/armor_Savior_Chainmail_8X16lJQ3xltTwynm.json +++ b/src/packs/items/armors/armor_Savior_Chainmail_8X16lJQ3xltTwynm.json @@ -5,6 +5,10 @@ "_id": "8X16lJQ3xltTwynm", "img": "icons/equipment/chest/breastplate-layered-leather-green.webp", "system": { + "armor": { + "current": 0, + "max": 8 + }, "description": "", "actions": {}, "attached": [], diff --git a/src/packs/items/armors/armor_Spiked_Plate_Armor_QjwsIhXKqnlvRBMv.json b/src/packs/items/armors/armor_Spiked_Plate_Armor_QjwsIhXKqnlvRBMv.json index cb5fc720..ac9115a2 100644 --- a/src/packs/items/armors/armor_Spiked_Plate_Armor_QjwsIhXKqnlvRBMv.json +++ b/src/packs/items/armors/armor_Spiked_Plate_Armor_QjwsIhXKqnlvRBMv.json @@ -5,6 +5,10 @@ "_id": "QjwsIhXKqnlvRBMv", "img": "icons/equipment/chest/breastplate-banded-steel-studded.webp", "system": { + "armor": { + "current": 0, + "max": 5 + }, "description": "", "actions": {}, "attached": [], diff --git a/src/packs/items/armors/armor_Tyris_Soft_Armor_PSW3BxCGmtLeWOxM.json b/src/packs/items/armors/armor_Tyris_Soft_Armor_PSW3BxCGmtLeWOxM.json index da640830..c6766018 100644 --- a/src/packs/items/armors/armor_Tyris_Soft_Armor_PSW3BxCGmtLeWOxM.json +++ b/src/packs/items/armors/armor_Tyris_Soft_Armor_PSW3BxCGmtLeWOxM.json @@ -5,6 +5,10 @@ "_id": "PSW3BxCGmtLeWOxM", "img": "icons/equipment/chest/robe-layered-purple.webp", "system": { + "armor": { + "current": 0, + "max": 5 + }, "description": "", "actions": { "Ch6IhuPewBeseGez": { diff --git a/src/packs/items/armors/armor_Veritas_Opal_Armor_OvzgUTYy2RCN85vV.json b/src/packs/items/armors/armor_Veritas_Opal_Armor_OvzgUTYy2RCN85vV.json index 08a1b573..403b79e8 100644 --- a/src/packs/items/armors/armor_Veritas_Opal_Armor_OvzgUTYy2RCN85vV.json +++ b/src/packs/items/armors/armor_Veritas_Opal_Armor_OvzgUTYy2RCN85vV.json @@ -5,6 +5,10 @@ "_id": "OvzgUTYy2RCN85vV", "img": "icons/equipment/chest/breastplate-collared-steel-green.webp", "system": { + "armor": { + "current": 0, + "max": 6 + }, "description": "", "actions": { "sY3W5JYspN5Du5ag": { diff --git a/src/packs/items/armors/folders_Special_tI3bfr6Sgi16Z7zm.json b/src/packs/items/armors/folders_Special_tI3bfr6Sgi16Z7zm.json deleted file mode 100644 index 65c4eca8..00000000 --- a/src/packs/items/armors/folders_Special_tI3bfr6Sgi16Z7zm.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "type": "Item", - "folder": null, - "name": "Special", - "color": null, - "sorting": "a", - "_id": "tI3bfr6Sgi16Z7zm", - "description": "", - "sort": 0, - "flags": {}, - "_key": "!folders!tI3bfr6Sgi16Z7zm" -} diff --git a/src/packs/items/weapons/weapon_Advanced_Round_Shield_hiEOGF2reabGLUoi.json b/src/packs/items/weapons/weapon_Advanced_Round_Shield_hiEOGF2reabGLUoi.json index da5d5bf8..54800642 100644 --- a/src/packs/items/weapons/weapon_Advanced_Round_Shield_hiEOGF2reabGLUoi.json +++ b/src/packs/items/weapons/weapon_Advanced_Round_Shield_hiEOGF2reabGLUoi.json @@ -113,26 +113,26 @@ "name": "Protective", "description": "

Add the item's Tier to your Armor Score

", "img": "icons/skills/melee/shield-block-gray-orange.webp", - "changes": [ - { - "key": "system.armorScore", - "mode": 2, - "value": "ITEM.@system.tier", - "priority": null - } - ], - "_id": "i5HfkF5aKQuUCTEG", + "_id": "7285CRGdZfHCEtT2", "type": "base", - "system": {}, + "system": { + "changes": [ + { + "type": "armor", + "phase": "initial", + "priority": 20, + "value": { + "max": "ITEM.@system.tier" + } + } + ] + }, "disabled": false, "duration": { - "startTime": null, - "combat": null, - "seconds": null, - "rounds": null, - "turns": null, - "startRound": null, - "startTurn": null + "value": null, + "units": "seconds", + "expiry": null, + "expired": false }, "origin": null, "tint": "#ffffff", @@ -143,7 +143,10 @@ "_stats": { "compendiumSource": null }, - "_key": "!items.effects!hiEOGF2reabGLUoi.i5HfkF5aKQuUCTEG" + "start": null, + "showIcon": 1, + "folder": null, + "_key": "!items.effects!hiEOGF2reabGLUoi.7285CRGdZfHCEtT2" } ], "sort": 0, diff --git a/src/packs/items/weapons/weapon_Advanced_Tower_Shield_OfOzQbs4hg6QbfTG.json b/src/packs/items/weapons/weapon_Advanced_Tower_Shield_OfOzQbs4hg6QbfTG.json index 9fe47fc0..a88749a8 100644 --- a/src/packs/items/weapons/weapon_Advanced_Tower_Shield_OfOzQbs4hg6QbfTG.json +++ b/src/packs/items/weapons/weapon_Advanced_Tower_Shield_OfOzQbs4hg6QbfTG.json @@ -113,25 +113,25 @@ "name": "Barrier", "description": "Gain Weapon Tier + 1 to Armor Score; -1 to Evasion", "img": "icons/skills/melee/shield-block-bash-blue.webp", - "changes": [ - { - "key": "system.armorScore", - "mode": 2, - "value": "ITEM.@system.tier + 1" - }, - { - "key": "system.evasion", - "mode": 2, - "value": "-1" - } - ], "_id": "87gedjJZGdFY81Mt", "type": "base", - "system": {}, + "system": { + "changes": [ + { + "key": "system.evasion", + "type": "add", + "value": -1, + "phase": "initial", + "priority": 0 + } + ] + }, "disabled": false, "duration": { - "startTime": null, - "combat": null + "value": null, + "units": "seconds", + "expiry": null, + "expired": false }, "origin": null, "tint": "#ffffff", @@ -142,7 +142,49 @@ "_stats": { "compendiumSource": null }, + "start": null, + "showIcon": 1, + "folder": null, "_key": "!items.effects!OfOzQbs4hg6QbfTG.87gedjJZGdFY81Mt" + }, + { + "name": "Barrier", + "description": "Gain Weapon Tier + 1 to Armor Score; -1 to Evasion", + "img": "icons/skills/melee/shield-block-bash-blue.webp", + "_id": "J0f7zqqOr61ADpdy", + "type": "base", + "system": { + "changes": [ + { + "type": "armor", + "phase": "initial", + "priority": 20, + "value": { + "max": "ITEM.@system.tier + 1" + } + } + ] + }, + "disabled": false, + "duration": { + "value": null, + "units": "seconds", + "expiry": null, + "expired": false + }, + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null + }, + "start": null, + "showIcon": 1, + "folder": null, + "_key": "!items.effects!OfOzQbs4hg6QbfTG.J0f7zqqOr61ADpdy" } ], "sort": 0, diff --git a/src/packs/items/weapons/weapon_Improved_Round_Shield_DlinEBGZfIlvreO3.json b/src/packs/items/weapons/weapon_Improved_Round_Shield_DlinEBGZfIlvreO3.json index 091355c2..65868950 100644 --- a/src/packs/items/weapons/weapon_Improved_Round_Shield_DlinEBGZfIlvreO3.json +++ b/src/packs/items/weapons/weapon_Improved_Round_Shield_DlinEBGZfIlvreO3.json @@ -113,26 +113,26 @@ "name": "Protective", "description": "

Add the item's Tier to your Armor Score

", "img": "icons/skills/melee/shield-block-gray-orange.webp", - "changes": [ - { - "key": "system.armorScore", - "mode": 2, - "value": "ITEM.@system.tier", - "priority": null - } - ], - "_id": "cXWSV50apzaNQkdA", + "_id": "pZCrWd7zLTarvEQK", "type": "base", - "system": {}, + "system": { + "changes": [ + { + "type": "armor", + "phase": "initial", + "priority": 20, + "value": { + "max": "ITEM.@system.tier" + } + } + ] + }, "disabled": false, "duration": { - "startTime": null, - "combat": null, - "seconds": null, - "rounds": null, - "turns": null, - "startRound": null, - "startTurn": null + "value": null, + "units": "seconds", + "expiry": null, + "expired": false }, "origin": null, "tint": "#ffffff", @@ -143,7 +143,10 @@ "_stats": { "compendiumSource": null }, - "_key": "!items.effects!DlinEBGZfIlvreO3.cXWSV50apzaNQkdA" + "start": null, + "showIcon": 1, + "folder": null, + "_key": "!items.effects!DlinEBGZfIlvreO3.pZCrWd7zLTarvEQK" } ], "sort": 0, diff --git a/src/packs/items/weapons/weapon_Improved_Tower_Shield_bxt3NsbMqTSdI5ab.json b/src/packs/items/weapons/weapon_Improved_Tower_Shield_bxt3NsbMqTSdI5ab.json index 6f55fe3d..64555dfa 100644 --- a/src/packs/items/weapons/weapon_Improved_Tower_Shield_bxt3NsbMqTSdI5ab.json +++ b/src/packs/items/weapons/weapon_Improved_Tower_Shield_bxt3NsbMqTSdI5ab.json @@ -113,25 +113,25 @@ "name": "Barrier", "description": "Gain Weapon Tier + 1 to Armor Score; -1 to Evasion", "img": "icons/skills/melee/shield-block-bash-blue.webp", - "changes": [ - { - "key": "system.armorScore", - "mode": 2, - "value": "ITEM.@system.tier + 1" - }, - { - "key": "system.evasion", - "mode": 2, - "value": "-1" - } - ], "_id": "tkNEA1PO3jEFhKCa", "type": "base", - "system": {}, + "system": { + "changes": [ + { + "key": "system.evasion", + "type": "add", + "value": -1, + "phase": "initial", + "priority": 0 + } + ] + }, "disabled": false, "duration": { - "startTime": null, - "combat": null + "value": null, + "units": "seconds", + "expiry": null, + "expired": false }, "origin": null, "tint": "#ffffff", @@ -142,7 +142,49 @@ "_stats": { "compendiumSource": null }, + "start": null, + "showIcon": 1, + "folder": null, "_key": "!items.effects!bxt3NsbMqTSdI5ab.tkNEA1PO3jEFhKCa" + }, + { + "name": "Barrier", + "description": "Gain Weapon Tier + 1 to Armor Score; -1 to Evasion", + "img": "icons/skills/melee/shield-block-bash-blue.webp", + "_id": "XugJeHJdnC6IymSa", + "type": "base", + "system": { + "changes": [ + { + "type": "armor", + "phase": "initial", + "priority": 20, + "value": { + "max": "ITEM.@system.tier + 1" + } + } + ] + }, + "disabled": false, + "duration": { + "value": null, + "units": "seconds", + "expiry": null, + "expired": false + }, + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null + }, + "start": null, + "showIcon": 1, + "folder": null, + "_key": "!items.effects!bxt3NsbMqTSdI5ab.XugJeHJdnC6IymSa" } ], "sort": 0, diff --git a/src/packs/items/weapons/weapon_Knuckle_Blades_U8gfyvxoHm024inM.json b/src/packs/items/weapons/weapon_Knuckle_Blades_U8gfyvxoHm024inM.json index 56ff6d62..ffe92071 100644 --- a/src/packs/items/weapons/weapon_Knuckle_Blades_U8gfyvxoHm024inM.json +++ b/src/packs/items/weapons/weapon_Knuckle_Blades_U8gfyvxoHm024inM.json @@ -33,7 +33,7 @@ "tier": 2, "equipped": false, "secondary": false, - "burden": "oneHanded", + "burden": "twoHanded", "weaponFeatures": [ { "value": "brutal", diff --git a/src/packs/items/weapons/weapon_Labrys_Axe_ijWppQzSOqVCb3rE.json b/src/packs/items/weapons/weapon_Labrys_Axe_ijWppQzSOqVCb3rE.json index 0dd7d23a..11994d3e 100644 --- a/src/packs/items/weapons/weapon_Labrys_Axe_ijWppQzSOqVCb3rE.json +++ b/src/packs/items/weapons/weapon_Labrys_Axe_ijWppQzSOqVCb3rE.json @@ -113,20 +113,26 @@ "name": "Protective", "description": "Add your character's Tier to your Armor Score", "img": "icons/skills/melee/shield-block-gray-orange.webp", - "changes": [ - { - "key": "system.armorScore", - "mode": 2, - "value": "ITEM.@system.tier" - } - ], - "_id": "qTxADRsQnKiYfOiQ", + "_id": "vnR4Zhnb0rOqwrFw", "type": "base", - "system": {}, + "system": { + "changes": [ + { + "type": "armor", + "phase": "initial", + "priority": 20, + "value": { + "max": "ITEM.@system.tier" + } + } + ] + }, "disabled": false, "duration": { - "startTime": null, - "combat": null + "value": null, + "units": "seconds", + "expiry": null, + "expired": false }, "origin": null, "tint": "#ffffff", @@ -137,7 +143,10 @@ "_stats": { "compendiumSource": null }, - "_key": "!items.effects!ijWppQzSOqVCb3rE.qTxADRsQnKiYfOiQ" + "start": null, + "showIcon": 1, + "folder": null, + "_key": "!items.effects!ijWppQzSOqVCb3rE.vnR4Zhnb0rOqwrFw" } ], "sort": 0, diff --git a/src/packs/items/weapons/weapon_Legendary_Round_Shield_A28WL9E2lJ3iLZHW.json b/src/packs/items/weapons/weapon_Legendary_Round_Shield_A28WL9E2lJ3iLZHW.json index 5eedb6c2..85134d21 100644 --- a/src/packs/items/weapons/weapon_Legendary_Round_Shield_A28WL9E2lJ3iLZHW.json +++ b/src/packs/items/weapons/weapon_Legendary_Round_Shield_A28WL9E2lJ3iLZHW.json @@ -113,26 +113,26 @@ "name": "Protective", "description": "

Add the item's Tier to your Armor Score

", "img": "icons/skills/melee/shield-block-gray-orange.webp", - "changes": [ - { - "key": "system.armorScore", - "mode": 2, - "value": "ITEM.@system.tier", - "priority": null - } - ], - "_id": "Z2p00q5h6x6seXys", + "_id": "EixxJrRHyc6kj3Wg", "type": "base", - "system": {}, + "system": { + "changes": [ + { + "type": "armor", + "phase": "initial", + "priority": 20, + "value": { + "max": "ITEM.@system.tier" + } + } + ] + }, "disabled": false, "duration": { - "startTime": null, - "combat": null, - "seconds": null, - "rounds": null, - "turns": null, - "startRound": null, - "startTurn": null + "value": null, + "units": "seconds", + "expiry": null, + "expired": false }, "origin": null, "tint": "#ffffff", @@ -143,7 +143,10 @@ "_stats": { "compendiumSource": null }, - "_key": "!items.effects!A28WL9E2lJ3iLZHW.Z2p00q5h6x6seXys" + "start": null, + "showIcon": 1, + "folder": null, + "_key": "!items.effects!A28WL9E2lJ3iLZHW.EixxJrRHyc6kj3Wg" } ], "sort": 0, diff --git a/src/packs/items/weapons/weapon_Legendary_Tower_Shield_MaJIROht7A9LxIZx.json b/src/packs/items/weapons/weapon_Legendary_Tower_Shield_MaJIROht7A9LxIZx.json index 49ca454a..772e9ca9 100644 --- a/src/packs/items/weapons/weapon_Legendary_Tower_Shield_MaJIROht7A9LxIZx.json +++ b/src/packs/items/weapons/weapon_Legendary_Tower_Shield_MaJIROht7A9LxIZx.json @@ -113,25 +113,25 @@ "name": "Barrier", "description": "Gain Weapon Tier + 1 to Armor Score; -1 to Evasion", "img": "icons/skills/melee/shield-block-bash-blue.webp", - "changes": [ - { - "key": "system.armorScore", - "mode": 2, - "value": "ITEM.@system.tier + 1" - }, - { - "key": "system.evasion", - "mode": 2, - "value": "-1" - } - ], "_id": "lBJMzxdGO2nJdTQS", "type": "base", - "system": {}, + "system": { + "changes": [ + { + "key": "system.evasion", + "type": "add", + "value": -1, + "phase": "initial", + "priority": 0 + } + ] + }, "disabled": false, "duration": { - "startTime": null, - "combat": null + "value": null, + "units": "seconds", + "expiry": null, + "expired": false }, "origin": null, "tint": "#ffffff", @@ -142,7 +142,49 @@ "_stats": { "compendiumSource": null }, + "start": null, + "showIcon": 1, + "folder": null, "_key": "!items.effects!MaJIROht7A9LxIZx.lBJMzxdGO2nJdTQS" + }, + { + "name": "Barrier", + "description": "Gain Weapon Tier + 1 to Armor Score; -1 to Evasion", + "img": "icons/skills/melee/shield-block-bash-blue.webp", + "_id": "1fgUIaXl6VQrhP7j", + "type": "base", + "system": { + "changes": [ + { + "type": "armor", + "phase": "initial", + "priority": 20, + "value": { + "max": "ITEM.@system.tier + 1" + } + } + ] + }, + "disabled": false, + "duration": { + "value": null, + "units": "seconds", + "expiry": null, + "expired": false + }, + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null + }, + "start": null, + "showIcon": 1, + "folder": null, + "_key": "!items.effects!MaJIROht7A9LxIZx.1fgUIaXl6VQrhP7j" } ], "sort": 0, diff --git a/src/packs/items/weapons/weapon_Round_Shield_mxwWKDujgsRcZWPT.json b/src/packs/items/weapons/weapon_Round_Shield_mxwWKDujgsRcZWPT.json index ccba4b0e..55e92f01 100644 --- a/src/packs/items/weapons/weapon_Round_Shield_mxwWKDujgsRcZWPT.json +++ b/src/packs/items/weapons/weapon_Round_Shield_mxwWKDujgsRcZWPT.json @@ -113,26 +113,26 @@ "name": "Protective", "description": "

Add the item's Tier to your Armor Score

", "img": "icons/skills/melee/shield-block-gray-orange.webp", - "changes": [ - { - "key": "system.armorScore", - "mode": 2, - "value": "ITEM.@system.tier", - "priority": null - } - ], - "_id": "M70a81e0Mg66jHRL", + "_id": "eV4lFIpQMiKERj4U", "type": "base", - "system": {}, + "system": { + "changes": [ + { + "type": "armor", + "phase": "initial", + "priority": 20, + "value": { + "max": "ITEM.@system.tier" + } + } + ] + }, "disabled": false, "duration": { - "startTime": null, - "combat": null, - "seconds": null, - "rounds": null, - "turns": null, - "startRound": null, - "startTurn": null + "value": null, + "units": "seconds", + "expiry": null, + "expired": false }, "origin": null, "tint": "#ffffff", @@ -143,7 +143,10 @@ "_stats": { "compendiumSource": null }, - "_key": "!items.effects!mxwWKDujgsRcZWPT.M70a81e0Mg66jHRL" + "start": null, + "showIcon": 1, + "folder": null, + "_key": "!items.effects!mxwWKDujgsRcZWPT.eV4lFIpQMiKERj4U" } ], "sort": 0, diff --git a/src/packs/items/weapons/weapon_Spiked_Shield_vzyzFwLUniWZV1rt.json b/src/packs/items/weapons/weapon_Spiked_Shield_vzyzFwLUniWZV1rt.json index 2dd08026..39c18b08 100644 --- a/src/packs/items/weapons/weapon_Spiked_Shield_vzyzFwLUniWZV1rt.json +++ b/src/packs/items/weapons/weapon_Spiked_Shield_vzyzFwLUniWZV1rt.json @@ -113,32 +113,31 @@ "name": "Double Duty", "description": "+1 to Armor Score; +1 to primary weapon damage within Melee range", "img": "icons/skills/melee/sword-shield-stylized-white.webp", - "changes": [ - { - "key": "system.armorScore", - "mode": 2, - "value": "1" - }, - { - "key": "system.bonuses.damage.primaryWeapon.bonus", - "mode": 2, - "value": "1" - } - ], "system": { "rangeDependence": { "enabled": true, "range": "melee", "target": "hostile", "type": "withinRange" - } + }, + "changes": [ + { + "key": "system.bonuses.damage.primaryWeapon.bonus", + "type": "add", + "value": 1, + "phase": "initial", + "priority": 0 + } + ] }, "_id": "d3TJtlpoHBCztbom", "type": "base", "disabled": false, "duration": { - "startTime": null, - "combat": null + "value": null, + "units": "seconds", + "expiry": null, + "expired": false }, "origin": null, "tint": "#ffffff", @@ -149,7 +148,49 @@ "_stats": { "compendiumSource": null }, + "start": null, + "showIcon": 1, + "folder": null, "_key": "!items.effects!vzyzFwLUniWZV1rt.d3TJtlpoHBCztbom" + }, + { + "name": "Double Duty", + "description": "+1 to Armor Score; +1 to primary weapon damage within Melee range", + "img": "icons/skills/melee/sword-shield-stylized-white.webp", + "system": { + "changes": [ + { + "type": "armor", + "phase": "initial", + "priority": 20, + "value": { + "max": "1" + } + } + ] + }, + "_id": "mvUY9LGfwICak7cE", + "type": "base", + "disabled": false, + "duration": { + "value": null, + "units": "seconds", + "expiry": null, + "expired": false + }, + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null + }, + "start": null, + "showIcon": 1, + "folder": null, + "_key": "!items.effects!vzyzFwLUniWZV1rt.mvUY9LGfwICak7cE" } ], "sort": 0, diff --git a/src/packs/items/weapons/weapon_Tower_Shield_C9aWpK1shVMWP4m5.json b/src/packs/items/weapons/weapon_Tower_Shield_C9aWpK1shVMWP4m5.json index 47043c54..e584b63c 100644 --- a/src/packs/items/weapons/weapon_Tower_Shield_C9aWpK1shVMWP4m5.json +++ b/src/packs/items/weapons/weapon_Tower_Shield_C9aWpK1shVMWP4m5.json @@ -113,25 +113,25 @@ "name": "Barrier", "description": "Gain Weapon Tier + 1 to Armor Score; -1 to Evasion", "img": "icons/skills/melee/shield-block-bash-blue.webp", - "changes": [ - { - "key": "system.armorScore", - "mode": 2, - "value": "ITEM.@system.tier + 1" - }, - { - "key": "system.evasion", - "mode": 2, - "value": "-1" - } - ], "_id": "8r0TcKWXboFV0eqS", "type": "base", - "system": {}, + "system": { + "changes": [ + { + "key": "system.evasion", + "type": "add", + "value": -1, + "phase": "initial", + "priority": 0 + } + ] + }, "disabled": false, "duration": { - "startTime": null, - "combat": null + "value": null, + "units": "seconds", + "expiry": null, + "expired": false }, "origin": null, "tint": "#ffffff", @@ -142,7 +142,49 @@ "_stats": { "compendiumSource": null }, + "start": null, + "showIcon": 1, + "folder": null, "_key": "!items.effects!C9aWpK1shVMWP4m5.8r0TcKWXboFV0eqS" + }, + { + "name": "Barrier", + "description": "Gain Weapon Tier + 1 to Armor Score; -1 to Evasion", + "img": "icons/skills/melee/shield-block-bash-blue.webp", + "_id": "tLRc4UAnGuIq7er3", + "type": "base", + "system": { + "changes": [ + { + "type": "armor", + "phase": "initial", + "priority": 20, + "value": { + "max": "ITEM.@system.tier + 1" + } + } + ] + }, + "disabled": false, + "duration": { + "value": null, + "units": "seconds", + "expiry": null, + "expired": false + }, + "origin": null, + "tint": "#ffffff", + "transfer": true, + "statuses": [], + "sort": 0, + "flags": {}, + "_stats": { + "compendiumSource": null + }, + "start": null, + "showIcon": 1, + "folder": null, + "_key": "!items.effects!C9aWpK1shVMWP4m5.tLRc4UAnGuIq7er3" } ], "sort": 0, diff --git a/styles/less/dialog/damage-reduction/damage-reduction-container.less b/styles/less/dialog/damage-reduction/damage-reduction-container.less index 2f343fb3..e8242bdd 100644 --- a/styles/less/dialog/damage-reduction/damage-reduction-container.less +++ b/styles/less/dialog/damage-reduction/damage-reduction-container.less @@ -35,7 +35,10 @@ display: flex; flex-direction: column; align-items: center; - width: 100%; + + &.full-width { + width: 100%; + } } .padded { @@ -45,6 +48,7 @@ .armor-title { margin: 0; white-space: nowrap; + width: 100%; } .resources-container { @@ -62,12 +66,17 @@ .mark-selection { display: flex; - align-items: center; + flex-direction: column; width: 100%; margin: 0; + h4 { + margin: 0; + } + .mark-selection-inner { display: flex; + justify-content: center; gap: 8px; .mark-container { @@ -91,6 +100,19 @@ opacity: 0.2; } + &.spent { + ::after { + position: absolute; + content: '/'; + color: red; + font-weight: 700; + font-size: 1.8em; + left: -1px; + top: -7px; + rotate: 13deg; + } + } + .fa-shield { position: relative; right: 0.5px; diff --git a/styles/less/dialog/dice-roll/roll-selection.less b/styles/less/dialog/dice-roll/roll-selection.less index 7fdae77a..a1a01e6b 100644 --- a/styles/less/dialog/dice-roll/roll-selection.less +++ b/styles/less/dialog/dice-roll/roll-selection.less @@ -69,29 +69,6 @@ background: light-dark(@dark-blue-40, @golden-40); } } - - .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/index.less b/styles/less/dialog/index.less index 0c70df9f..73738eaa 100644 --- a/styles/less/dialog/index.less +++ b/styles/less/dialog/index.less @@ -32,6 +32,8 @@ @import './reroll-dialog/sheet.less'; @import './group-roll/group-roll.less'; + +@import './tag-team-dialog/initialization.less'; @import './tag-team-dialog/sheet.less'; @import './image-select/sheet.less'; diff --git a/styles/less/dialog/tag-team-dialog/initialization.less b/styles/less/dialog/tag-team-dialog/initialization.less new file mode 100644 index 00000000..30676f82 --- /dev/null +++ b/styles/less/dialog/tag-team-dialog/initialization.less @@ -0,0 +1,59 @@ +.daggerheart.dialog.dh-style.views.tag-team-dialog { + .initialization-container { + h2 { + text-align: center; + } + + .members-container { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + gap: 8px; + + .member-container { + position: relative; + display: flex; + justify-content: center; + + &.inactive { + opacity: 0.4; + } + + .member-name { + position: absolute; + } + } + } + + .initiator-container { + margin-top: 8px; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + + &.inactive { + opacity: 0.4; + } + } + + footer { + margin-top: 8px; + display: flex; + gap: 8px; + + button { + flex: 1; + } + + .finish-tools { + flex: none; + display: flex; + align-items: center; + gap: 4px; + + &.inactive { + opacity: 0.4; + } + } + } + } +} diff --git a/styles/less/dialog/tag-team-dialog/sheet.less b/styles/less/dialog/tag-team-dialog/sheet.less index 767c66ca..e72b4956 100644 --- a/styles/less/dialog/tag-team-dialog/sheet.less +++ b/styles/less/dialog/tag-team-dialog/sheet.less @@ -1,178 +1,251 @@ .daggerheart.dialog.dh-style.views.tag-team-dialog { - .tag-team-container { + .tag-team-roll-container { display: flex; flex-direction: column; gap: 16px; - .tag-team-data-container { - display: flex; - align-items: center; - gap: 8px; - - .form-group { - flex: 0; - - label { - white-space: nowrap; - } - - &.flex-group { - flex: 1; - } - } + &.inactive { + opacity: 0.4; + pointer-events: none; } - .title-row { + .team-container { display: flex; - align-items: center; - gap: 8px; + gap: 16px; - h2 { - text-align: start; - } - - select { - flex: 1; - } - } - - .participants-container { - margin-top: 8px; - display: flex; - flex-direction: column; - gap: 4px; - - .participant-outer-container { - padding: 8px; + .member-container { display: flex; flex-direction: column; - gap: 4px; - cursor: pointer; - border-radius: 6px; + justify-content: space-between; + gap: 8px; + flex: 1; - &.selected, - &:hover { - background-color: light-dark(@golden-40, @golden-40); + &.inactive { + opacity: 0.4; + pointer-events: none; } - .participant-container { + .data-container { + display: flex; + flex-direction: column; + gap: 8px; + width: 100%; + } + + .member-info { display: flex; align-items: center; justify-content: space-between; - gap: 8px; + width: 100%; - .participant-inner-container { + img { + height: 64px; + border-radius: 6px; + border: 1px solid light-dark(@dark-blue, @golden); + } + + .member-name { flex: 1; - 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; - } - } - } + text-align: center; + font-size: var(--font-size-18); } } - .participant-empty-roll-container { - border: 1px dashed white; - padding: 8px 2px; - text-align: center; - font-style: italic; + .roll-setup { + width: 100%; } - .participant-roll-outer-container { + .roll-container { display: flex; flex-direction: column; - gap: 2px; - color: light-dark(@dark-blue, @golden); + } - h4 { - text-align: center; + .roll-title { + font-size: var(--font-size-20); + font-weight: bold; + color: light-dark(@dark-blue, @golden); + text-align: center; + display: flex; + align-items: center; + gap: 8px; + + &::before, + &::after { color: light-dark(@dark-blue, @golden); + content: ''; + flex: 1; + height: 2px; } - .participant-roll-container { + &::before { + background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, light-dark(@dark-blue, @golden) 100%); + } + + &::after { + background: linear-gradient(90deg, light-dark(@dark-blue, @golden) 0%, rgba(0, 0, 0, 0) 100%); + } + } + + .roll-tools { + display: flex; + gap: 4px; + align-items: center; + + img { + height: 16px; + } + + a { + display: flex; + font-size: 16px; + + &:hover { + text-shadow: none; + filter: drop-shadow(0 0 8px var(--golden)); + } + } + } + + .roll-data { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + + &.hope { + --text-color: @golden; + --bg-color: @golden-40; + } + + &.fear { + --text-color: @chat-blue; + --bg-color: @chat-blue-40; + } + + &.critical { + --text-color: @chat-purple; + --bg-color: @chat-purple-40; + } + + .duality-label { + color: var(--text-color); + font-size: var(--font-size-20); + font-weight: bold; + text-align: center; + + .unused-damage { + text-decoration: line-through; + } + } + + .roll-dice-container { display: flex; align-items: center; justify-content: center; - white-space: nowrap; - - .participant-roll-text-container { - padding: 0 8px; - white-space: nowrap; - display: flex; - } - } - - .damage-values-container { - display: flex; - justify-content: space-around; + flex-wrap: wrap; gap: 8px; - .damage-container { - border: 1px solid light-dark(white, white); - border-radius: 6px; - padding: 0 4px; + .roll-dice { + position: relative; display: flex; - gap: 4px; + align-items: center; + justify-content: center; + + .dice-label { + position: absolute; + color: white; + font-size: 1rem; + paint-order: stroke fill; + -webkit-text-stroke: 2px black; + } + + img { + height: 32px; + } + } + + .roll-operator { + font-size: var(--font-size-24); + } + + .roll-value { + font-size: 18px; + } + } + + .roll-total { + background: var(--bg-color); + color: var(--text-color); + border-radius: 4px; + padding: 3px; + } + } + + .select-roll-button { + margin-top: 8px; + + i { + color: light-dark(@dark-blue, @golden); + font-size: 48px; + + &.inactive { + opacity: 0.4; } } } } } - h2 { - text-align: center; - } - - .result-container { - display: grid; - grid-template-columns: 1fr 1fr; + .results-container { + display: flex; + flex-direction: column; align-items: center; - gap: 8px; + gap: 4px; + border: 2px solid light-dark(@dark-blue, @golden); + border-radius: 6px; + padding: 8px 10px; - .result-damages-container { + .result-container-label { + font-size: var(--font-size-24); + font-weight: bold; + } + + .results-inner-container { display: flex; - flex-wrap: wrap; - gap: 4px; + justify-content: space-between; + gap: 8px; + width: 100%; - .result-damage-container { - border: 1px solid light-dark(white, white); - border-radius: 6px; - padding: 0 4px; + .result-section-label { + font-size: var(--font-size-20); + } + + .result-container { + width: 100%; + text-align: center; + + .result-info { + display: flex; + gap: 4px; + align-items: center; + justify-content: center; + } } } } - .roll-leader-container { + .finish-container { + gap: 16px; display: grid; - grid-template-columns: 1fr 1fr; - gap: 8px; + grid-template-columns: 1fr 1fr 1fr; + + .finish-button { + grid-column: span 2; + } + } + + .hint { + text-align: center; } } } diff --git a/styles/less/global/sheet.less b/styles/less/global/sheet.less index 1e7bad0a..6f77a481 100755 --- a/styles/less/global/sheet.less +++ b/styles/less/global/sheet.less @@ -14,11 +14,7 @@ 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 { - &.adversary, - &.character, - &.item { - background: @dark-blue; - } + background: @dark-blue; } } diff --git a/styles/less/sheets/activeEffects/activeEffects.less b/styles/less/sheets/activeEffects/activeEffects.less index ba3ff43f..3f6526cb 100644 --- a/styles/less/sheets/activeEffects/activeEffects.less +++ b/styles/less/sheets/activeEffects/activeEffects.less @@ -31,5 +31,70 @@ text-align: center; } } + + .armor-change-container { + padding-top: 0; + padding-bottom: 4px; + row-gap: 0; + + legend { + display: flex; + align-items: center; + padding-left: 3px; + } + + header { + padding: 0; + left: -0.25rem; // TODO: Find why this header is offset 0.25rem to the right so this can be removed. + } + + header, + ol { + grid-template-columns: 5rem 7rem 12rem 4rem; + } + + .damage-thresholds-container { + width: 100%; + display: flex; + flex-direction: column; + gap: 4px; + + .damage-threshold-title { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + + &::before, + &::after { + content: ''; + flex: 1; + height: 2px; + } + + &::before { + background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, light-dark(@dark-blue, @golden) 100%); + } + + &::after { + background: linear-gradient(90deg, light-dark(@dark-blue, @golden) 0%, rgba(0, 0, 0, 0) 100%); + } + + span { + font-size: var(--font-size-18); + } + } + + .form-group { + flex-direction: column; + gap: 0; + + label { + color: inherit; + line-height: 16px; + } + } + } + } } } diff --git a/styles/less/sheets/actors/character/sidebar.less b/styles/less/sheets/actors/character/sidebar.less index 04baf2b9..bb0a43cd 100644 --- a/styles/less/sheets/actors/character/sidebar.less +++ b/styles/less/sheets/actors/character/sidebar.less @@ -276,6 +276,23 @@ } } + .slot-label { + .slot-value-container { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + + i { + position: absolute; + right: 0; + font-size: 12px; + color: light-dark(@beige, @dark-blue); + } + } + } + .status-value { padding: 0 5px; } @@ -298,6 +315,12 @@ border-radius: 3px; background: light-dark(@dark-blue, @golden); clip-path: none; + cursor: pointer; + display: flex; + align-items: center; + gap: 4px; + border: 1px solid transparent; + transition: all 0.3s ease; h4 { font-weight: bold; @@ -306,6 +329,20 @@ color: light-dark(@beige, @dark-blue); font-size: var(--font-size-12); } + + i { + font-size: 12px; + color: light-dark(@beige, @dark-blue); + } + + &:hover { + background: light-dark(@light-black, @dark-blue); + border: 1px solid light-dark(@dark-blue, @golden); + + h4, i { + color: light-dark(@dark-blue, @golden); + } + } } .slot-value { position: absolute; @@ -343,7 +380,7 @@ } } } - .slot-label { + .slot-label { display: flex; align-items: center; color: light-dark(@beige, @dark-blue); @@ -355,6 +392,17 @@ font-size: var(--font-size-12); flex-wrap: wrap; justify-content: center; + border: 1px solid transparent; + transition: all 0.3s ease; + + &:hover { + background: light-dark(@light-black, @dark-blue); + border: 1px solid light-dark(@dark-blue, @golden); + + .label, .value, i { + color: light-dark(@dark-blue, @golden); + } + } .label { padding-right: 1px; diff --git a/styles/less/sheets/actors/party/sheet.less b/styles/less/sheets/actors/party/sheet.less index 2d1344e8..6b51de53 100644 --- a/styles/less/sheets/actors/party/sheet.less +++ b/styles/less/sheets/actors/party/sheet.less @@ -7,8 +7,12 @@ background-image: url('../assets/parchments/dh-parchment-dark.png'); } }, { - &.party { + &.sheet.actor.dh-style.party { background: url('../assets/parchments/dh-parchment-light.png'); + + .tab .actions-section .active-action { + animation: glow-dark 0.75s infinite alternate; + } } }); @@ -40,6 +44,10 @@ font-size: 12px; } } + + .active-action { + animation: glow 0.75s infinite alternate; + } } } } diff --git a/styles/less/ux/index.less b/styles/less/ux/index.less index c6c40f78..6fad3ab1 100644 --- a/styles/less/ux/index.less +++ b/styles/less/ux/index.less @@ -1,4 +1,5 @@ @import './tooltip/tooltip.less'; +@import './tooltip/armorManagement.less'; @import './tooltip/battlepoints.less'; @import './tooltip/bordered-tooltip.less'; @import './tooltip/domain-cards.less'; diff --git a/styles/less/ux/tooltip/armorManagement.less b/styles/less/ux/tooltip/armorManagement.less index bc716fa0..e1ac6bb9 100644 --- a/styles/less/ux/tooltip/armorManagement.less +++ b/styles/less/ux/tooltip/armorManagement.less @@ -1,7 +1,19 @@ +@import '../../utils/fonts.less'; +@import '../../utils/colors.less'; + .bordered-tooltip.locked-tooltip .daggerheart.armor-management-container { display: flex; flex-direction: column; - gap: 16px; + gap: 10px; + padding-bottom: 10px; + + h3 { + font-family: @font-subtitle; + margin: 0; + border: none; + font-weight: normal; + font-size: var(--font-size-20); + } .armor-source-container { display: flex; @@ -10,16 +22,16 @@ gap: 4px; .armor-source-label { - font-size: var(--font-size-24); - font-weight: bold; + font-family: @font-body; + margin: 0; } .status-bar { display: flex; justify-content: center; position: relative; - width: 80px; - height: 20px; + width: 100%; + height: 30px; .status-value { position: absolute; @@ -27,8 +39,8 @@ padding: 0 5px; font-size: 1rem; align-items: center; - width: 80px; - height: 20px; + width: 100%; + height: 30px; justify-content: center; text-align: center; z-index: 2; @@ -36,13 +48,13 @@ input[type='number'] { background: transparent; - font-size: 1rem; + font-size: 1.2rem; width: 30px; - height: 15px; text-align: center; border: none; outline: 2px solid transparent; color: @beige; + font-family: @font-body; &.bar-input { padding: 0; @@ -50,6 +62,7 @@ backdrop-filter: none; background: transparent; transition: all 0.3s ease; + height: 25px; &:hover, &:focus { @@ -60,14 +73,16 @@ } .bar-label { + font-family: @font-body; width: 40px; + font-size: 1.2rem; } } .progress-bar { position: absolute; appearance: none; - width: 80px; - height: 20px; + width: 100%; + height: 30px; border: 1px solid light-dark(@dark-blue, @golden); border-radius: 6px; z-index: 1; @@ -97,4 +112,30 @@ } } } + + .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; + align-items: center; + justify-content: center; + color: light-dark(@dark-blue, @golden); + min-height: 30px; + width: 100%; + + .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); + } + } + } } diff --git a/system.json b/system.json index 41e46edb..9a78bf50 100644 --- a/system.json +++ b/system.json @@ -5,7 +5,7 @@ "version": "2.0.0", "compatibility": { "minimum": "14.355", - "verified": "14.356", + "verified": "14.357", "maximum": "14" }, "authors": [ diff --git a/templates/dialogs/damageReduction.hbs b/templates/dialogs/damageReduction.hbs index 57d7ee61..50fe3422 100644 --- a/templates/dialogs/damageReduction.hbs +++ b/templates/dialogs/damageReduction.hbs @@ -7,53 +7,57 @@
-

{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.armorMarks"}}

-
{{armorMarks}}/{{armorScore}}
+

{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.maxUseableArmor"}}

+
{{availableArmor}}
- {{#if this.stress}} -
-

{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.stress"}}

-
{{this.stress.value}}/{{this.stress.max}}
-
- {{/if}}
-
-

-
- {{#each marks.armor}} -
- -
- {{/each}} +
+ {{#each marks.armor as |source|}} +
+

{{source.label}}

+
+ {{#each source.marks}} + + + + {{/each}} +
+
+ {{/each}} + {{#if usesStressArmor}} +
+

{{localize "Stress"}}

+
{{#each marks.stress}}
{{/each}} +
-

-
{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.usedMarks"}}
+ {{/if}}
{{#if availableStressReductions}}
-

{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.stressReduction"}}

+

{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.stressReduction"}}

{{/if}} {{#each availableStressReductions}}
-

+

{{#if this.any}} {{localize "DAGGERHEART.GENERAL.any"}} @@ -74,7 +78,7 @@ {{#if reduceSeverity}}
-

{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.reduceSeverity" nr=reduceSeverity}}

+

{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.reduceSeverity" nr=reduceSeverity}}

{{/if}} @@ -82,7 +86,7 @@ {{#if thresholdImmunities}}
-

{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.thresholdImmunities"}}

+

{{localize "DAGGERHEART.APPLICATIONS.DamageReduction.thresholdImmunities"}}

{{/if}} diff --git a/templates/dialogs/dice-roll/header.hbs b/templates/dialogs/dice-roll/header.hbs index 0cb58a01..486fcc8f 100644 --- a/templates/dialogs/dice-roll/header.hbs +++ b/templates/dialogs/dice-roll/header.hbs @@ -8,10 +8,4 @@
{{/if}}

- {{#if (and @root.hasRoll @root.activeTagTeamRoll)}} -
- - {{localize "DAGGERHEART.APPLICATIONS.TagTeamSelect.title"}} -
- {{/if}} \ No newline at end of file diff --git a/templates/dialogs/tagTeamDialog.hbs b/templates/dialogs/tagTeamDialog.hbs deleted file mode 100644 index 3c96a573..00000000 --- a/templates/dialogs/tagTeamDialog.hbs +++ /dev/null @@ -1,110 +0,0 @@ -
-
-
- {{localize "DAGGERHEART.APPLICATIONS.TagTeamSelect.partyTeam"}} - -
- - -
- -
- -
- {{#each members as |member|}} -
-
-
- -
-
{{member.character.name}}
-
- {{#if member.character.system.class.value}} -
{{member.character.system.class.value.name}}
- {{/if}} - {{#if member.system.multiclass.value}} -
{{member.character.system.multiclass.value.name}}
- {{/if}} -
-
-
- -
- {{#if member.roll}} -
-
-

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

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

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

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

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

-

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

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

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

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

{{localize "DAGGERHEART.APPLICATIONS.TagTeamSelect.selectParticipants"}}

+
+ {{#each memberSelection as |member|}} + + {{member.name}} + + + {{/each}} +
+ +
+
+ +
+ +
+
+ {{formGroup tagTeamFields.initiator.fields.cost name="initiator.cost" value=initiator.cost disabled=initiatorDisabled localize=true }} +
+ +
+ +
+ {{localize "DAGGERHEART.APPLICATIONS.TagTeamSelect.openDialogForAll"}} + +
+
+
\ No newline at end of file diff --git a/templates/dialogs/tagTeamDialog/parts/tagTeamDamageParts.hbs b/templates/dialogs/tagTeamDialog/parts/tagTeamDamageParts.hbs new file mode 100644 index 00000000..058777a5 --- /dev/null +++ b/templates/dialogs/tagTeamDialog/parts/tagTeamDamageParts.hbs @@ -0,0 +1,25 @@ +{{#each damage as |damage key|}} +
+
+ {{localize (concat "DAGGERHEART.CONFIG.HealingType." key ".name")}}: + {{damage.total}} +
+ {{#each damage.parts as |part|}} +
+ {{#each part.dice as |dice index|}} + + {{dice.total}} + + + {{#unless @last}} + + + {{/unless}} + {{/each}} + {{#if part.modifierTotal}} + {{#if (gte part.modifierTotal 0)}}+{{else}}-{{/if}} + {{positive part.modifierTotal}} + {{/if}} +
+ {{/each}} +
+{{/each}} \ No newline at end of file diff --git a/templates/dialogs/tagTeamDialog/tagTeamRoll.hbs b/templates/dialogs/tagTeamDialog/tagTeamRoll.hbs new file mode 100644 index 00000000..30369d52 --- /dev/null +++ b/templates/dialogs/tagTeamDialog/tagTeamRoll.hbs @@ -0,0 +1,173 @@ +
+
+
+ {{#each members as |member key|}} +
+
+
+ + {{member.name}} +
+
+
+
+ + +
+
+ + {{#if (eq member.rollType 'trait')}} +
+
+ + +
+
+ {{else if (eq member.rollType 'damageAbility')}} +
+
+ + +
+
+ {{else}} +
+
+ + +
+
+ {{/if}} +
+ + {{#if member.readyToRoll}} +
+ + {{localize "DAGGERHEART.GENERAL.roll"}} +
+ + + + + {{#if member.hasRolled}} + + + + {{/if}} +
+
+ + {{#if member.rollData}} + {{#with member.rollData.options.roll}} +
+
{{this.total}} {{localize "DAGGERHEART.GENERAL.withThing" thing=this.result.label}}
+
+ + {{this.hope.value}} + + + + + + {{this.fear.value}} + + + {{#if this.advantage.type}} + {{#if (eq this.advantage.type 1)}}+{{else}}-{{/if}} + + {{this.advantage.value}} + + + {{/if}} + {{#if (gte this.modifierTotal 0)}}+{{else}}-{{/if}} + {{positive this.modifierTotal}} +
+
+ {{/with}} + {{else}} + {{localize "DAGGERHEART.APPLICATIONS.TagTeamSelect.makeYourRoll"}} + {{/if}} +
+ {{/if}} + + {{#if member.rollData.options.hasDamage}} +
+ + {{localize "DAGGERHEART.GENERAL.damage"}} +
+ + + + + {{#if damage}} + + + + {{/if}} +
+
+ {{#if damage}} + {{#if useCritDamage}} + {{> "systems/daggerheart/templates/dialogs/tagTeamDialog/parts/tagTeamDamageParts.hbs" damage=critDamage isCritical=true }} + {{else}} + {{> "systems/daggerheart/templates/dialogs/tagTeamDialog/parts/tagTeamDamageParts.hbs" damage=damage }} + {{/if}} + {{else}} + {{localize "DAGGERHEART.APPLICATIONS.TagTeamSelect.makeYourRoll"}} + {{/if}} +
+ {{/if}} +
+ + {{#if member.hasRolled}} + + + + {{/if}} +
+ {{/each}} +
+ +
+ {{localize "DAGGERHEART.GENERAL.result.plural"}} +
+ {{#if hintText}} +
{{localize hintText}}
+ {{else}} + {{#if joinedRoll.rollData}} +
+ +
+
{{joinedRoll.rollData.options.roll.total}}
+
{{localize "DAGGERHEART.GENERAL.withThing" thing=joinedRoll.rollData.options.roll.result.label}}
+
+
+ {{/if}} + {{#if hasDamage}} +
+ + {{#each joinedRoll.rollData.options.damage as |damage key|}} +
+
{{localize (concat "DAGGERHEART.CONFIG.HealingType." key ".name")}}
+
{{damage.total}}
+
+ {{/each}} +
+ {{/if}} + {{/if}} +
+
+ +
+ + +
+
+
\ No newline at end of file diff --git a/templates/sheets/activeEffect/change.hbs b/templates/sheets/activeEffect/change.hbs index 30f643b8..d7d396e4 100644 --- a/templates/sheets/activeEffect/change.hbs +++ b/templates/sheets/activeEffect/change.hbs @@ -3,7 +3,9 @@
- {{formInput fields.type name=change.typePath value=change.type localize=true}} +
{{formInput fields.value name=change.valuePath value=change.value elementType="input"}} diff --git a/templates/sheets/activeEffect/changes.hbs b/templates/sheets/activeEffect/changes.hbs index 026ffd90..47a48382 100644 --- a/templates/sheets/activeEffect/changes.hbs +++ b/templates/sheets/activeEffect/changes.hbs @@ -13,4 +13,11 @@ {{{change}}} {{/each}} + +
+ {{localize "DAGGERHEART.GENERAL.armor"}} + {{#if typedChanges.armor}} + {{> "systems/daggerheart/templates/sheets/activeEffect/typeChanges/armorChange.hbs" typedChanges.armor fields=@root.systemFields.changes.element.types.armor.fields}} + {{/if}} +
diff --git a/templates/sheets/activeEffect/typeChanges/armorChange.hbs b/templates/sheets/activeEffect/typeChanges/armorChange.hbs new file mode 100644 index 00000000..662a1294 --- /dev/null +++ b/templates/sheets/activeEffect/typeChanges/armorChange.hbs @@ -0,0 +1,28 @@ +
+
{{localize "EFFECT.FIELDS.changes.element.value.label"}}
+
{{localize "DAGGERHEART.GENERAL.max"}}
+
{{localize "DAGGERHEART.EFFECTS.ChangeTypes.armor.FIELDS.interaction.label"}}
+
{{localize "EFFECT.FIELDS.changes.element.priority.label"}}
+
+
    +
  1. + + + {{formInput fields.value.fields.current name=(concat "system.changes." index ".value.current") value=value.current data-dtype="Number"}} + {{formInput fields.value.fields.max name=(concat "system.changes." index ".value.max") value=value.max data-dtype="Number"}} + {{formInput fields.value.fields.interaction name=(concat "system.changes." index ".value.interaction") value=value.interaction localize=true}} + {{formInput fields.priority name=(concat "system.changes." index ".priority") value=priority}} +
  2. +
+
+
+ {{localize "DAGGERHEART.GENERAL.DamageThresholds.title"}} + +
+ {{#if value.damageThresholds}} +
+ {{formGroup fields.value.fields.damageThresholds.fields.major name=(concat "system.changes." index ".value.damageThresholds.major") value=value.damageThresholds.major localize=true }} + {{formGroup fields.value.fields.damageThresholds.fields.severe name=(concat "system.changes." index ".value.damageThresholds.severe") value=value.damageThresholds.severe localize=true }} +
+ {{/if}} +
\ No newline at end of file diff --git a/templates/sheets/actors/character/effects.hbs b/templates/sheets/actors/character/effects.hbs index 30fb31f4..3355d575 100644 --- a/templates/sheets/actors/character/effects.hbs +++ b/templates/sheets/actors/character/effects.hbs @@ -18,6 +18,7 @@ collection=effects.inactives canCreate=true hideResources=true + disabled=true }}
\ No newline at end of file diff --git a/templates/sheets/actors/character/sidebar.hbs b/templates/sheets/actors/character/sidebar.hbs index b2757b55..d3be4983 100644 --- a/templates/sheets/actors/character/sidebar.hbs +++ b/templates/sheets/actors/character/sidebar.hbs @@ -30,14 +30,14 @@ - {{#if document.system.armor.system.marks}} + {{#if document.system.armorScore.max}}
{{#if useResourcePips}} {{else}}
- + / - {{document.system.armorScore}} + {{document.system.armorScore.max}}
- + + {{/if}}
{{else}} diff --git a/templates/sheets/actors/party/party-members.hbs b/templates/sheets/actors/party/party-members.hbs index b5903cfc..b3dd53e6 100644 --- a/templates/sheets/actors/party/party-members.hbs +++ b/templates/sheets/actors/party/party-members.hbs @@ -5,7 +5,7 @@ >
- diff --git a/templates/sheets/actors/party/resources.hbs b/templates/sheets/actors/party/resources.hbs index 74f94806..bfbfb578 100644 --- a/templates/sheets/actors/party/resources.hbs +++ b/templates/sheets/actors/party/resources.hbs @@ -52,13 +52,12 @@
- {{#if actor.system.armor.system.marks}} + {{#if actor.system.armorScore.max}}
{{localize "DAGGERHEART.GENERAL.armorSlots"}} - {{actor.system.armor.system.marks.value}} / {{actor.system.armorScore}} + {{actor.system.armorScore.value}} / {{actor.system.armorScore.max}}
{{/if}} diff --git a/templates/sheets/global/partials/inventory-item-V2.hbs b/templates/sheets/global/partials/inventory-item-V2.hbs index 86d2e2d3..a758a28f 100644 --- a/templates/sheets/global/partials/inventory-item-V2.hbs +++ b/templates/sheets/global/partials/inventory-item-V2.hbs @@ -40,23 +40,20 @@ Parameters: {{!-- Name & Tags --}}
+ {{!-- Item Name --}} + {{localize item.name}} {{#unless (or noExtensible (not item.system.description))}}{{/unless}} - {{!-- Item Name --}} - {{localize item.name}} {{#unless (or noExtensible (not item.system.description))}}{{/unless}} - - {{!-- Tags Start --}} + {{!-- Tags Start --}} {{#if (not ../hideTags)}} - {{#> "systems/daggerheart/templates/sheets/global/partials/item-tags.hbs" item }} - {{#if (eq ../type 'feature')}} - {{#if (or (eq @root.document.type 'adversary') (eq @root.document.type 'environment'))}} - {{#if system.featureForm}} + {{#> "systems/daggerheart/templates/sheets/global/partials/item-tags.hbs" item}} + {{#if (eq ../type 'feature')}} + {{#if (and system.featureForm (or (eq @root.document.type "adversary") (eq @root.document.type "environment")))}}
{{localize (concat "DAGGERHEART.CONFIG.FeatureForm." system.featureForm)}}
{{/if}} {{/if}} - {{/if}} - {{/ "systems/daggerheart/templates/sheets/global/partials/item-tags.hbs"}} + {{/ "systems/daggerheart/templates/sheets/global/partials/item-tags.hbs"}} {{/if}} {{!--Tags End --}} diff --git a/templates/sheets/global/partials/item-tags.hbs b/templates/sheets/global/partials/item-tags.hbs index b30fcbf2..2edc1eac 100644 --- a/templates/sheets/global/partials/item-tags.hbs +++ b/templates/sheets/global/partials/item-tags.hbs @@ -4,5 +4,5 @@ {{tag}}
{{/each}} - {{#if @partial-block}}{{> @partial-block}}{{/if}} + {{> @partial-block}} \ No newline at end of file diff --git a/templates/sheets/items/armor/header.hbs b/templates/sheets/items/armor/header.hbs index d4306131..d8044332 100644 --- a/templates/sheets/items/armor/header.hbs +++ b/templates/sheets/items/armor/header.hbs @@ -9,7 +9,7 @@

{{localize "DAGGERHEART.ITEMS.Armor.baseScore"}}: - {{source.system.baseScore}} + {{source.system.armor.max}} - {{localize "DAGGERHEART.ITEMS.Armor.baseThresholds.base"}}: {{source.system.baseThresholds.major}} diff --git a/templates/sheets/items/armor/settings.hbs b/templates/sheets/items/armor/settings.hbs index e7bde6fe..51bf1746 100644 --- a/templates/sheets/items/armor/settings.hbs +++ b/templates/sheets/items/armor/settings.hbs @@ -6,9 +6,9 @@
{{localize tabs.settings.label}} {{localize "DAGGERHEART.GENERAL.Tiers.singular"}} - {{formField systemFields.tier value=source.system.tier}} + {{ formField systemFields.tier value=source.system.tier }} {{localize "DAGGERHEART.ITEMS.Armor.baseScore"}} - {{formField systemFields.baseScore value=source.system.baseScore}} + {{ formField systemFields.armor.fields.max value=source.system.armor.max }} {{localize "TYPES.Item.feature"}} diff --git a/templates/ui/itemBrowser/item-tags.hbs b/templates/ui/itemBrowser/item-tags.hbs new file mode 100644 index 00000000..ba2d39de --- /dev/null +++ b/templates/ui/itemBrowser/item-tags.hbs @@ -0,0 +1,2 @@ +{{#> "systems/daggerheart/templates/sheets/global/partials/item-tags.hbs" item }} +{{/"systems/daggerheart/templates/sheets/global/partials/item-tags.hbs"}} diff --git a/templates/ui/tooltip/armorManagement.hbs b/templates/ui/tooltip/armorManagement.hbs index aa8c9878..963959c5 100644 --- a/templates/ui/tooltip/armorManagement.hbs +++ b/templates/ui/tooltip/armorManagement.hbs @@ -1,18 +1,18 @@