diff --git a/lang/en.json b/lang/en.json index 82193133..6756c3fa 100755 --- a/lang/en.json +++ b/lang/en.json @@ -251,6 +251,7 @@ "InvalidOldCharacterImportTitle": "Old Character Import", "InvalidOldCharacterImportText": "Character data exported prior to system version 1.1 will not generate a complete character. Do you wish to continue?", "cancelBeastform": "Cancel Beastform", + "sidebarFavoritesHint": "Drag items, features and domain cards from the sheet to here", "resetCharacterConfirmationTitle": "Reset Character", "resetCharacterConfirmationContent": "You are reseting all character data except name and portrait. Are you sure?" }, @@ -2240,6 +2241,10 @@ "plural": "Experiences" }, "failure": "Failure", + "favorite": { + "single": "Favorite", + "plural": "Favorites" + }, "fate": "Fate", "fateRoll": "Fate Roll", "fear": "Fear", diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 4ecaeb06..2ec176ae 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -55,6 +55,10 @@ export default class CharacterSheet extends DHBaseActorSheet { { dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]', dropSelector: null + }, + { + dragSelector: null, + dropSelector: '.character-sidebar-sheet' } ], contextMenus: [ @@ -260,6 +264,9 @@ export default class CharacterSheet extends DHBaseActorSheet { */ async _prepareSidebarContext(context, _options) { context.isDeath = this.document.system.deathMoveViable; + context.sidebarFavoritesEmpty = this.document.system.sidebarFavorites.length === 0; + context.showfavorites = !context.sidebarFavoritesEmpty || this.document.system.usedUnarmed; + context.sidebarFavorites = this.document.system.sidebarFavorites.sort((a, b) => a.name.localeCompare(b.name)); } /** @@ -309,7 +316,9 @@ export default class CharacterSheet extends DHBaseActorSheet { icon: 'fa-solid fa-arrow-up', condition: target => { const doc = getDocFromElementSync(target); - return doc && doc.system.inVault; + const inCharacterSidebar = + this.document.type === 'character' && target.closest('.items-sidebar-list'); + return doc && doc.system.inVault && !inCharacterSidebar; }, callback: async target => { const doc = await getDocFromElement(target); @@ -323,7 +332,9 @@ export default class CharacterSheet extends DHBaseActorSheet { icon: 'fa-solid fa-bolt-lightning', condition: target => { const doc = getDocFromElementSync(target); - return doc && doc.system.inVault; + const inCharacterSidebar = + this.document.type === 'character' && target.closest('.items-sidebar-list'); + return doc && doc.system.inVault && !inCharacterSidebar; }, callback: async (target, event) => { const doc = await getDocFromElement(target); @@ -362,7 +373,9 @@ export default class CharacterSheet extends DHBaseActorSheet { icon: 'fa-solid fa-arrow-down', condition: target => { const doc = getDocFromElementSync(target); - return doc && !doc.system.inVault; + const inCharacterSidebar = + this.document.type === 'character' && target.closest('.items-sidebar-list'); + return doc && !doc.system.inVault && !inCharacterSidebar; }, callback: async target => (await getDocFromElement(target)).update({ 'system.inVault': true }) } @@ -741,8 +754,6 @@ export default class CharacterSheet extends DHBaseActorSheet { await config.resourceUpdates.updateResources(); } - //TODO: redo toggleEquipItem method - /** * Toggles the equipped state of an item (armor or weapon). * @type {ApplicationClickAction} @@ -750,32 +761,14 @@ export default class CharacterSheet extends DHBaseActorSheet { static async #toggleEquipItem(_event, button) { const item = await getDocFromElement(button); if (!item) return; - if (item.system.equipped) { - await item.update({ 'system.equipped': false }); - return; - } - switch (item.type) { - case 'armor': - const currentArmor = this.document.system.armor; - if (currentArmor) { - await currentArmor.update({ 'system.equipped': false }); - } - - await item.update({ 'system.equipped': true }); - break; - case 'weapon': - if (this.document.effects.find(x => !x.disabled && x.type === 'beastform')) { - return ui.notifications.warn( - game.i18n.localize('DAGGERHEART.UI.Notifications.beastformEquipWeapon') - ); - } - - await this.document.system.constructor.unequipBeforeEquip.bind(this.document.system)(item); - - await item.update({ 'system.equipped': true }); - break; - } + const changedData = await this.document.toggleEquipItem(item); + const removedData = changedData.filter(x => !x.add); + this.document.update({ + 'system.sidebarFavorites': [ + ...this.document.system.sidebarFavorites.filter(x => removedData.every(r => r.item.id !== x.id)) + ] + }); } /** @@ -835,12 +828,13 @@ export default class CharacterSheet extends DHBaseActorSheet { */ static async #toggleVault(_event, button) { const doc = await getDocFromElement(button); - const { available } = this.document.system.loadoutSlot; - if (doc.system.inVault && !available && !doc.system.loadoutIgnore) { - return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.loadoutMaxReached')); - } - - await doc?.update({ 'system.inVault': !doc.system.inVault }); + const changedData = await this.document.toggleDomainCardVault(doc); + const removedData = changedData.filter(x => !x.add); + this.document.update({ + 'system.sidebarFavorites': [ + ...this.document.system.sidebarFavorites.filter(x => removedData.every(r => r.item.id !== x.id)) + ] + }); } /** @@ -962,6 +956,11 @@ export default class CharacterSheet extends DHBaseActorSheet { } async _onDropItem(event, item) { + const sidebarDrop = event.target.closest('.character-sidebar-sheet'); + if (sidebarDrop) { + return this._onSidebarDrop(event, item); + } + const setupCriticalItemTypes = ['class', 'subclass', 'ancestry', 'community']; if (this.document.system.needsCharacterSetup && setupCriticalItemTypes.includes(item.type)) { const confirmed = await foundry.applications.api.DialogV2.confirm({ @@ -1012,4 +1011,18 @@ export default class CharacterSheet extends DHBaseActorSheet { itemData = itemData instanceof Array ? itemData : [itemData]; return this.document.createEmbeddedDocuments('Item', itemData); } + + async _onSidebarDrop(event, item) { + if (!item.testUserPermission(game.user, 'OWNER')) return; + + if (!(item instanceof game.system.api.documents.DHItem)) return; + if (!this.document.items.get(item.id)) return; + + const allowedItemTypes = ['domainCard', 'feature', 'weapon', 'armor', 'loot', 'consumable']; + if (!allowedItemTypes.includes(item.type)) return; + + if (this.document.system.sidebarFavorites.some(x => x.id === item.id)) return; + + this.document.setFavoriteItem(item, true); + } } diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index 449880fb..3e1898fe 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -532,6 +532,40 @@ export default function DHApplicationMixin(Base) { callback: async target => (await getDocFromElement(target)).toChat(this.document.uuid) }); + options.push({ + name: 'Unfavorite', + icon: 'fa-regular fa-star', + condition: target => { + const doc = getDocFromElementSync(target); + const isFavorited = this.document.system.sidebarFavorites.some(x => x.id === doc.id); + return this.document.type === 'character' && isFavorited; + }, + callback: async (target, _event) => { + const doc = await getDocFromElement(target); + this.document.update({ + 'system.sidebarFavorites': this.document.system.sidebarFavorites.filter(x => x.id !== doc.id) + }); + } + }); + + options.push({ + name: 'Favorite', + icon: 'fa-solid fa-star', + condition: target => { + const doc = getDocFromElementSync(target); + const isFavorited = this.document.system.sidebarFavorites.some(x => x.id === doc.id); + return ( + !(doc instanceof game.system.api.documents.DhActiveEffect) && + this.document.type === 'character' && + !isFavorited + ); + }, + callback: async (target, _event) => { + const doc = await getDocFromElement(target); + this.document.setFavoriteItem(doc, true); + } + }); + if (deletable) options.push({ name: 'CONTROLS.CommonDelete', diff --git a/module/config/actorConfig.mjs b/module/config/actorConfig.mjs index ac55117a..2235f873 100644 --- a/module/config/actorConfig.mjs +++ b/module/config/actorConfig.mjs @@ -506,8 +506,8 @@ export const subclassFeatureLabels = { * @property {number[]} damage */ -/** - * @type {Record} +/** + * @type {Record} * Scaling data used to change an adversary's tier. Each rank is applied incrementally. */ export const adversaryScalingData = { @@ -518,7 +518,7 @@ export const adversaryScalingData = { severeThreshold: 10, hp: 1, stress: 2, - attack: 2, + attack: 2 }, 3: { difficulty: 2, @@ -526,7 +526,7 @@ export const adversaryScalingData = { severeThreshold: 15, hp: 1, stress: 0, - attack: 2, + attack: 2 }, 4: { difficulty: 2, @@ -534,7 +534,7 @@ export const adversaryScalingData = { severeThreshold: 25, hp: 1, stress: 0, - attack: 2, + attack: 2 } }, horde: { @@ -544,7 +544,7 @@ export const adversaryScalingData = { severeThreshold: 8, hp: 2, stress: 0, - attack: 0, + attack: 0 }, 3: { difficulty: 2, @@ -552,7 +552,7 @@ export const adversaryScalingData = { severeThreshold: 12, hp: 0, stress: 1, - attack: 1, + attack: 1 }, 4: { difficulty: 2, @@ -560,7 +560,7 @@ export const adversaryScalingData = { severeThreshold: 15, hp: 2, stress: 0, - attack: 0, + attack: 0 } }, leader: { @@ -570,7 +570,7 @@ export const adversaryScalingData = { severeThreshold: 10, hp: 0, stress: 0, - attack: 1, + attack: 1 }, 3: { difficulty: 2, @@ -578,7 +578,7 @@ export const adversaryScalingData = { severeThreshold: 15, hp: 1, stress: 0, - attack: 2, + attack: 2 }, 4: { difficulty: 2, @@ -586,7 +586,7 @@ export const adversaryScalingData = { severeThreshold: 25, hp: 1, stress: 1, - attack: 3, + attack: 3 } }, minion: { @@ -596,7 +596,7 @@ export const adversaryScalingData = { severeThreshold: 0, hp: 0, stress: 0, - attack: 1, + attack: 1 }, 3: { difficulty: 2, @@ -604,7 +604,7 @@ export const adversaryScalingData = { severeThreshold: 0, hp: 0, stress: 1, - attack: 1, + attack: 1 }, 4: { difficulty: 2, @@ -612,7 +612,7 @@ export const adversaryScalingData = { severeThreshold: 0, hp: 0, stress: 0, - attack: 1, + attack: 1 } }, ranged: { @@ -622,7 +622,7 @@ export const adversaryScalingData = { severeThreshold: 6, hp: 1, stress: 0, - attack: 1, + attack: 1 }, 3: { difficulty: 2, @@ -630,7 +630,7 @@ export const adversaryScalingData = { severeThreshold: 14, hp: 1, stress: 1, - attack: 2, + attack: 2 }, 4: { difficulty: 2, @@ -638,7 +638,7 @@ export const adversaryScalingData = { severeThreshold: 10, hp: 1, stress: 1, - attack: 1, + attack: 1 } }, skulk: { @@ -648,7 +648,7 @@ export const adversaryScalingData = { severeThreshold: 8, hp: 1, stress: 1, - attack: 1, + attack: 1 }, 3: { difficulty: 2, @@ -656,7 +656,7 @@ export const adversaryScalingData = { severeThreshold: 12, hp: 1, stress: 1, - attack: 1, + attack: 1 }, 4: { difficulty: 2, @@ -664,7 +664,7 @@ export const adversaryScalingData = { severeThreshold: 10, hp: 1, stress: 1, - attack: 1, + attack: 1 } }, solo: { @@ -674,7 +674,7 @@ export const adversaryScalingData = { severeThreshold: 10, hp: 0, stress: 1, - attack: 2, + attack: 2 }, 3: { difficulty: 2, @@ -682,7 +682,7 @@ export const adversaryScalingData = { severeThreshold: 15, hp: 2, stress: 1, - attack: 2, + attack: 2 }, 4: { difficulty: 2, @@ -690,7 +690,7 @@ export const adversaryScalingData = { severeThreshold: 25, hp: 0, stress: 1, - attack: 3, + attack: 3 } }, standard: { @@ -700,7 +700,7 @@ export const adversaryScalingData = { severeThreshold: 8, hp: 0, stress: 0, - attack: 1, + attack: 1 }, 3: { difficulty: 2, @@ -708,7 +708,7 @@ export const adversaryScalingData = { severeThreshold: 15, hp: 1, stress: 1, - attack: 1, + attack: 1 }, 4: { difficulty: 2, @@ -716,7 +716,7 @@ export const adversaryScalingData = { severeThreshold: 15, hp: 0, stress: 1, - attack: 1, + attack: 1 } }, support: { @@ -726,7 +726,7 @@ export const adversaryScalingData = { severeThreshold: 8, hp: 1, stress: 1, - attack: 1, + attack: 1 }, 3: { difficulty: 2, @@ -734,7 +734,7 @@ export const adversaryScalingData = { severeThreshold: 12, hp: 0, stress: 0, - attack: 1, + attack: 1 }, 4: { difficulty: 2, @@ -742,27 +742,27 @@ export const adversaryScalingData = { severeThreshold: 10, hp: 1, stress: 1, - attack: 1, + attack: 1 } } }; -/** +/** * Scaling data used for an adversary's damage. * Tier 4 is missing certain adversary types and therefore skews upwards. * We manually set tier 4 data to hopefully lead to better results */ export const adversaryExpectedDamage = { - basic: { - 1: { mean: 7.321428571428571, deviation: 1.962519002770912 }, - 2: { mean: 12.444444444444445, deviation: 2.0631069425529676 }, - 3: { mean: 15.722222222222221, deviation: 2.486565208464823 }, - 4: { mean: 26, deviation: 5.2 } - }, - minion: { - 1: { mean: 2.142857142857143, deviation: 1.0690449676496976 }, - 2: { mean: 5, deviation: 0.816496580927726 }, - 3: { mean: 6.5, deviation: 2.1213203435596424 }, - 4: { mean: 11, deviation: 1 } - } + basic: { + 1: { mean: 7.321428571428571, deviation: 1.962519002770912 }, + 2: { mean: 12.444444444444445, deviation: 2.0631069425529676 }, + 3: { mean: 15.722222222222221, deviation: 2.486565208464823 }, + 4: { mean: 26, deviation: 5.2 } + }, + minion: { + 1: { mean: 2.142857142857143, deviation: 1.0690449676496976 }, + 2: { mean: 5, deviation: 0.816496580927726 }, + 3: { mean: 6.5, deviation: 2.1213203435596424 }, + 4: { mean: 11, deviation: 1 } + } }; diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index 10fba63c..465247dc 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -6,6 +6,7 @@ import DhCreature from './creature.mjs'; import { attributeField, resourceField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs'; import { ActionField } from '../fields/actionField.mjs'; import DHCharacterSettings from '../../applications/sheets-configs/character-settings.mjs'; +import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs'; export default class DhCharacter extends DhCreature { /**@override */ @@ -317,7 +318,8 @@ export default class DhCharacter extends DhCreature { hint: 'DAGGERHEART.ACTORS.Character.roll.guaranteedCritical.hint' }) }) - }) + }), + sidebarFavorites: new ForeignDocumentUUIDArrayField({ type: 'Item' }) }; } @@ -586,28 +588,6 @@ export default class DhCharacter extends DhCreature { return diceTypes[attackDiceIndex]; } - static async unequipBeforeEquip(itemToEquip) { - const primary = this.primaryWeapon, - secondary = this.secondaryWeapon; - if (itemToEquip.system.secondary) { - if (primary && primary.burden === CONFIG.DH.GENERAL.burden.twoHanded.value) { - await primary.update({ 'system.equipped': false }); - } - - if (secondary) { - await secondary.update({ 'system.equipped': false }); - } - } else { - if (secondary && itemToEquip.system.burden === CONFIG.DH.GENERAL.burden.twoHanded.value) { - await secondary.update({ 'system.equipped': false }); - } - - if (primary) { - await primary.update({ 'system.equipped': false }); - } - } - } - prepareBaseData() { this.evasion += this.class.value?.system?.evasion ?? 0; diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index e8bea0bf..9d2de96b 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -992,4 +992,111 @@ export default class DhpActor extends Actor { return allTokens; } + + async toggleDomainCardVault(card, options = { render: true }) { + const { render } = options; + const { available } = this.system.loadoutSlot; + + if (card.system.inVault && !available && !card.system.loadoutIgnore) { + return ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.loadoutMaxReached')); + } + + const toVault = options.toVault ?? !card.system.inVault; + await card?.update({ 'system.inVault': toVault }, { render }); + return [{ item: card, add: !toVault }]; + } + + async unequipBeforeEquip(itemToEquip, options = { render: true }) { + const { render } = options; + + const primary = this.system.primaryWeapon, + secondary = this.system.secondaryWeapon; + let unequippedItems = []; + if (itemToEquip.system.secondary) { + if (primary && primary.system.burden === CONFIG.DH.GENERAL.burden.twoHanded.value) { + unequippedItems.push(primary); + } + + if (secondary) { + unequippedItems.push(secondary); + } + } else { + if (secondary && itemToEquip.system.burden === CONFIG.DH.GENERAL.burden.twoHanded.value) { + unequippedItems.push(secondary); + } + + if (primary) { + unequippedItems.push(primary); + } + } + + for (const item of unequippedItems) await item?.update({ 'system.equipped': false }, { render }); + + return unequippedItems; + } + + async toggleEquipItem(item, options = { render: true }) { + const { render } = options; + const changedItems = []; + const updateAndAddChangedItem = async (item, equip) => { + changedItems.push({ item, add: equip }); + await item.update({ 'system.equipped': equip }, { render }); + }; + + if (item.system.equipped && [undefined, false].includes(options.equip)) { + await updateAndAddChangedItem(item, false); + return changedItems; + } + + switch (item.type) { + case 'armor': + const currentArmor = this.system.armor; + if (currentArmor) { + await updateAndAddChangedItem(currentArmor, false); + } + + await updateAndAddChangedItem(item, true); + break; + case 'weapon': + if (this.effects.find(x => !x.disabled && x.type === 'beastform')) { + return ui.notifications.warn( + game.i18n.localize('DAGGERHEART.UI.Notifications.beastformEquipWeapon') + ); + } + + const unequippedItems = await this.unequipBeforeEquip(item, { render: false }); + changedItems.push(...unequippedItems.map(x => ({ item: x, add: false }))); + await updateAndAddChangedItem(item, true); + break; + } + + return changedItems; + } + + /* This is very convoluted, and there is almost certainly a better way to do it. I couldn't get it working any better way atm though. */ + async setFavoriteItem(item, setFavorited) { + const favoritesToRemove = []; + const favoritesToAdd = []; + if (['weapon', 'armor'].includes(item.type)) { + const changedData = await this.toggleEquipItem(item, { render: false, equip: setFavorited }); + for (const data of changedData) { + if (data.add) favoritesToAdd.push(data.item); + else favoritesToRemove.push(data.item); + } + } else if (item.type === 'domainCard') { + const changedData = await this.toggleDomainCardVault(item, { render: false, toVault: !setFavorited }); + for (const data of changedData) { + if (data.add) favoritesToAdd.push(data.item); + else favoritesToRemove.push(data.item); + } + } else if (setFavorited) favoritesToAdd.push(item); + else favoritesToRemove.push(item); + + this.update({ + 'system.sidebarFavorites': [ + ...this.system.sidebarFavorites.filter(x => favoritesToRemove.every(r => r.id !== x.id)), + ...favoritesToAdd + ] + }); + } } diff --git a/module/documents/item.mjs b/module/documents/item.mjs index 67f7d253..b658e0df 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -229,5 +229,11 @@ export default class DHItem extends foundry.documents.Item { async _preDelete() { this.deleteTriggers(); + + if (this.parent?.type === 'character') { + const filteredFavorites = this.parent.system.sidebarFavorites.filter(x => x.id !== this.id); + if (this.parent.system.sidebarFavorites.length !== filteredFavorites.length) + this.parent.update({ 'system.sidebarFavorites': filteredFavorites }); + } } } diff --git a/styles/less/sheets/actors/character/sidebar.less b/styles/less/sheets/actors/character/sidebar.less index 04baf2b9..0f710055 100644 --- a/styles/less/sheets/actors/character/sidebar.less +++ b/styles/less/sheets/actors/character/sidebar.less @@ -552,7 +552,6 @@ .shortcut-items-section { overflow-y: hidden; padding-top: 10px; - padding-bottom: 20px; mask-image: linear-gradient(0deg, transparent 0%, black 5%); scrollbar-gutter: stable; scrollbar-width: thin; @@ -561,6 +560,26 @@ overflow-y: auto; scrollbar-color: light-dark(@dark-blue, @golden) transparent; } + + .empty-favorites { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + box-sizing: border-box; + border: 1px dashed light-dark(@dark-blue-50, @beige-50); + border-radius: 3px; + color: light-dark(@dark-blue-50, @beige-50); + text-align: center; + margin-bottom: 10px; + margin-left: 4px; + padding: 4px 0; + + span { + width: 250px; + } + } } .equipment-section, diff --git a/styles/less/ui/chat/deathmoves.less b/styles/less/ui/chat/deathmoves.less index 175b8753..4cfe1b13 100644 --- a/styles/less/ui/chat/deathmoves.less +++ b/styles/less/ui/chat/deathmoves.less @@ -143,10 +143,10 @@ } .risk-it-all-button { - width: -webkit-fill-available; - margin: 0 8px; - font-weight: 600; - height: 40px; + width: -webkit-fill-available; + margin: 0 8px; + font-weight: 600; + height: 40px; } } } diff --git a/templates/sheets/actors/character/sidebar.hbs b/templates/sheets/actors/character/sidebar.hbs index b2757b55..36244c35 100644 --- a/templates/sheets/actors/character/sidebar.hbs +++ b/templates/sheets/actors/character/sidebar.hbs @@ -99,63 +99,51 @@
-

{{localize "DAGGERHEART.GENERAL.equipment"}}

+

{{localize "DAGGERHEART.GENERAL.favorite.plural"}}

    - {{#if document.system.usedUnarmed}} - {{> 'daggerheart.inventory-item-compact' - item=document.system.usedUnarmed - type='attack' - }} - {{/if}} - {{#each document.items as |item|}} - {{#if item.system.equipped}} - {{> 'daggerheart.inventory-item-compact' - item=item - type=item.type - }} + {{#if this.document.system.usedUnarmed}} + {{> 'daggerheart.inventory-item-compact' + item=this.document.system.usedUnarmed + type="attack" + }} {{/if}} + {{#each sidebarFavorites as |item|}} + {{> 'daggerheart.inventory-item-compact' + item=item + type=item.type + }} {{/each}}
+ {{#if sidebarFavoritesEmpty}} +
+ {{localize "DAGGERHEART.ACTORS.Character.sidebarFavoritesHint"}} +
+ {{/if}}
-
-
- -

{{localize "DAGGERHEART.GENERAL.loadout"}}

- -
-
    - {{#each document.system.domainCards.loadout as |card|}} - {{> 'daggerheart.inventory-item-compact' - item=card - type='domainCard' - }} - - {{/each}} -
+
+ +
+
+ +

{{localize "DAGGERHEART.GENERAL.experience.single"}}

+
-
-
- -

{{localize "DAGGERHEART.GENERAL.experience.single"}}

- -
-
- {{#each document.system.experiences as |experience id|}} -
- - {{numberFormat experience.value sign=true}} - - {{experience.name}} -
- - - -
+
+ {{#each document.system.experiences as |experience id|}} +
+ + {{numberFormat experience.value sign=true}} + + {{experience.name}} + - {{/each}} -
+
+ {{/each}}
\ No newline at end of file diff --git a/templates/sheets/global/partials/inventory-item-compact.hbs b/templates/sheets/global/partials/inventory-item-compact.hbs index bbdf9116..6f7205fa 100644 --- a/templates/sheets/global/partials/inventory-item-compact.hbs +++ b/templates/sheets/global/partials/inventory-item-compact.hbs @@ -58,11 +58,6 @@ data-tooltip="DAGGERHEART.UI.Tooltip.{{ifThen item.system.equipped 'unequip' 'equip' }}"> - {{else if (eq type 'domainCard')}} - - - {{else if (eq type 'effect')}} diff --git a/tools/analyze-damage.mjs b/tools/analyze-damage.mjs index 6d5da3de..7b3fb9e5 100644 --- a/tools/analyze-damage.mjs +++ b/tools/analyze-damage.mjs @@ -5,38 +5,41 @@ * Maybe if future book monsters can be part of what we release, we can analyze those too. */ -import fs from "fs/promises"; -import path from "path"; +import fs from 'fs/promises'; +import path from 'path'; const allData = []; // Read adversary pack data for average damage for attacks -const adversariesDirectory = path.join("src/packs/adversaries"); +const adversariesDirectory = path.join('src/packs/adversaries'); for (const basefile of await fs.readdir(adversariesDirectory)) { - if (!basefile.endsWith(".json")) continue; + if (!basefile.endsWith('.json')) continue; const filepath = path.join(adversariesDirectory, basefile); - const data = JSON.parse(await fs.readFile(filepath, "utf8")); - if (data?.type !== "adversary" || data.system.type === "social") continue; + const data = JSON.parse(await fs.readFile(filepath, 'utf8')); + if (data?.type !== 'adversary' || data.system.type === 'social') continue; allData.push({ name: data.name, tier: data.system.tier, adversaryType: data.system.type, - damage: parseDamage(data.system.attack.damage), + damage: parseDamage(data.system.attack.damage) }); } const adversaryTypes = new Set(allData.map(a => a.adversaryType)); for (const type of [...adversaryTypes].toSorted()) { - const perTier = Object.groupBy(allData.filter(a => a.adversaryType === type), a => a.tier); - console.log(`${type} per Tier: ${[1, 2, 3, 4].map(t => perTier[t]?.length ?? 0).join(" ")}`) + const perTier = Object.groupBy( + allData.filter(a => a.adversaryType === type), + a => a.tier + ); + console.log(`${type} per Tier: ${[1, 2, 3, 4].map(t => perTier[t]?.length ?? 0).join(' ')}`); } const result = { - basic: compileData(allData.filter(d => d.adversaryType !== "minion")), - solos_and_bruisers: compileData(allData.filter(d => ["solo", "bruiser"].includes(d.adversaryType))), - leader_and_ranged: compileData(allData.filter(d => ["leader", "ranged"].includes(d.adversaryType))), - minion: compileData(allData.filter(d => d.adversaryType === "minion")), + basic: compileData(allData.filter(d => d.adversaryType !== 'minion')), + solos_and_bruisers: compileData(allData.filter(d => ['solo', 'bruiser'].includes(d.adversaryType))), + leader_and_ranged: compileData(allData.filter(d => ['leader', 'ranged'].includes(d.adversaryType))), + minion: compileData(allData.filter(d => d.adversaryType === 'minion')) }; console.log(result); @@ -52,7 +55,7 @@ function compileData(entries) { if (tier === 4) console.log(allDamage); results[tier] = { mean, - deviation: getStandardDeviation(allDamage, { mean }), + deviation: getStandardDeviation(allDamage, { mean }) }; } @@ -64,7 +67,7 @@ function removeOutliers(data) { const startIdx = Math.floor(data.length * 0.25); const endIdx = Math.ceil(data.length * 0.75); const iqrBound = (data[endIdx] - data[startIdx]) * 1.25; - return data.filter((d) => d >= data[startIdx] - iqrBound && d <= data[endIdx] + iqrBound); + return data.filter(d => d >= data[startIdx] - iqrBound && d <= data[endIdx] + iqrBound); } function getMedian(numbers) { @@ -84,7 +87,7 @@ function getMedianAverageDeviation(numbers, { median }) { } function getStandardDeviation(numbers, { mean }) { - const deviations = numbers.map((r) => r - mean); + const deviations = numbers.map(r => r - mean); return Math.sqrt(deviations.reduce((r, d) => r + d * d, 0) / (numbers.length - 1)); } @@ -95,8 +98,8 @@ function parseDamage(damage) { p.value.custom.enabled ? p.value.custom.formula : [p.value.flatMultiplier ? `${p.value.flatMultiplier}${p.value.dice}` : 0, p.value.bonus ?? 0] - .filter(p => !!p) - .join('+') + .filter(p => !!p) + .join('+') ) .join('+'); return getExpectedDamage(formula); @@ -107,19 +110,23 @@ function parseDamage(damage) { * All subtracted terms become negative terms. */ function getExpectedDamage(formula) { - const terms = formula.replace("+", " + ").replace("-", " - ").split(" ").map(t => t.trim()); + const terms = formula + .replace('+', ' + ') + .replace('-', ' - ') + .split(' ') + .map(t => t.trim()); let multiplier = 1; return terms.reduce((total, term) => { - if (term === "-") { + if (term === '-') { multiplier = -1; return total; - } else if (term === "+") { + } else if (term === '+') { return total; } const currentMultiplier = multiplier; multiplier = 1; - + const number = Number(term); if (!Number.isNaN(number)) { return total + currentMultiplier * number;