diff --git a/src/lang/en.json b/src/lang/en.json index 15ff69ae..38633f8d 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -155,7 +155,10 @@ "Consume": "Cost", "Uses": "Uses", "Use": "Use action", - "Strike": "Strike" + "Strike": "Strike", + "BaseSectionName": "{type} Actions", + "MiscSectionName": "Miscellaneous Actions", + "NewItem": "Add Action" }, "Equipment": { "Quantity": "Quantity", @@ -269,22 +272,26 @@ "Weapon": { "label": "Weapon", "label_plural": "Weapons", - "desc_placeholder": "Edit this to describe what this weapon is & how it looks." + "desc_placeholder": "Edit this to describe what this weapon is & how it looks.", + "New": "New Weapon" }, "Armor": { "label": "Armor", "label_plural": "Armor", - "desc_placeholder": "Edit this to describe what this armor is & how it looks." + "desc_placeholder": "Edit this to describe what this armor is & how it looks.", + "New": "New Armor" }, "Equipment": { "label": "Equipment", "label_plural": "Equipment", - "desc_placeholder": "Edit this to describe what this piece of equipment is & how it looks." + "desc_placeholder": "Edit this to describe what this piece of equipment is & how it looks.", + "New": "New Equipment" }, "Loot": { "label": "Loot", "label_plural": "Loot", - "desc_placeholder": "Edit this to describe the item." + "desc_placeholder": "Edit this to describe the item.", + "New": "New Loot" }, "Ancestry": { "label": "Ancestry", @@ -309,17 +316,20 @@ "Talent": { "label": "Talent", "label_plural": "Talents", - "desc_placeholder": "Edit this to describe what this Talent does." + "desc_placeholder": "Edit this to describe what this Talent does.", + "New": "New Talent" }, "Action": { "label": "Action", "label_plural": "Actions", - "desc_placeholder": "Edit this to describe what this action is and does." + "desc_placeholder": "Edit this to describe what this action is and does.", + "New": "New Action" }, "Trait": { "label": "Trait", "label_plural": "Traits", - "desc_placeholder": "Edit this to describe this trait." + "desc_placeholder": "Edit this to describe this trait.", + "New": "New Trait" }, "Injury": { "label": "Injury", @@ -854,6 +864,7 @@ "Clear": "Clear", "Name": "Name", "Level": "Level", + "Add": "Add", "Button": { "Roll": "Roll", "Continue": "Continue", diff --git a/src/style/sheets/actor/module.scss b/src/style/sheets/actor/module.scss index 80e0e15e..0896500e 100644 --- a/src/style/sheets/actor/module.scss +++ b/src/style/sheets/actor/module.scss @@ -740,8 +740,6 @@ } .controls { - // width: 3rem; - color: rgba(231, 209, 177, 0.3); height: 100%; > * { @@ -758,6 +756,10 @@ } } + &:not(.header) .controls { + color: rgba(231, 209, 177, 0.3); + } + &:not(.expanded) { .controls > a[data-action='toggle-action-details'] diff --git a/src/system/applications/actor/components/actions-list.ts b/src/system/applications/actor/components/actions-list.ts index de4e97b5..fd6bdd95 100644 --- a/src/system/applications/actor/components/actions-list.ts +++ b/src/system/applications/actor/components/actions-list.ts @@ -1,5 +1,11 @@ -import { ActionType, ItemType } from '@system/types/cosmere'; -import { CosmereItem, TalentItem } from '@system/documents/item'; +import { + ActionType, + ItemType, + ActivationType, + ActionCostType, +} from '@system/types/cosmere'; +import { CosmereItem } from '@system/documents/item'; +import { CosmereActor } from '@system/documents'; import { ConstructorOf } from '@system/types/utils'; import { AppContextMenu } from '@system/applications/utils/context-menu'; @@ -20,6 +26,39 @@ interface AdditionalItemData { descriptionHTML?: string; } +export interface ListSection { + /** + * The id of the section + */ + id: string; + + /** + * Nicely formatted label for the section + */ + label: string; + + /** + * Whether this section counts as default. + * Default sections are always shown in edit mode, even if they are empty. + */ + default: boolean; + + /** + * Filter function to determine if an item should be included in this section + */ + filter: (item: CosmereItem) => boolean; + + /** + * Factory function to create a new item of this type + */ + new?: (parent: CosmereActor) => Promise; +} + +export interface ListSectionData extends ListSection { + items: CosmereItem[]; + itemData: Record; +} + export interface ActorActionsListComponentRenderContext extends BaseActorSheetRenderContext { actionsSearch?: { @@ -28,6 +67,92 @@ export interface ActorActionsListComponentRenderContext }; } +// Constants +const STATIC_SECTIONS = { + Weapons: { + id: 'weapons', + label: 'COSMERE.Item.Type.Weapon.label_plural', + default: false, + filter: (item: CosmereItem) => item.isWeapon(), + new: (parent: CosmereActor) => + CosmereItem.create( + { + type: ItemType.Weapon, + name: game.i18n!.localize('COSMERE.Item.Type.Weapon.New'), + system: { + activation: { + type: ActivationType.SkillTest, + cost: { + type: ActionCostType.Action, + value: 1, + }, + }, + equipped: true, + }, + }, + { parent }, + ) as Promise, + }, + Equipment: { + id: 'equipment', + label: 'COSMERE.Item.Type.Equipment.label_plural', + default: false, + filter: (item: CosmereItem) => item.isEquipment(), + new: (parent: CosmereActor) => + CosmereItem.create( + { + type: ItemType.Equipment, + name: game.i18n!.localize( + 'COSMERE.Item.Type.Equipment.New', + ), + system: { + activation: { + type: ActivationType.Utility, + cost: { + type: ActionCostType.Action, + value: 1, + }, + }, + }, + }, + { parent }, + ) as Promise, + }, + BasicActions: { + id: 'basic-actions', + label: 'COSMERE.Item.Action.Type.Basic.label_plural', + default: true, + filter: (item: CosmereItem) => + item.isAction() && item.system.type === ActionType.Basic, + new: (parent: CosmereActor) => + CosmereItem.create( + { + type: ItemType.Action, + name: game.i18n!.localize('COSMERE.Item.Type.Action.New'), + system: { + type: ActionType.Basic, + activation: { + type: ActivationType.Utility, + cost: { + type: ActionCostType.Action, + value: 1, + }, + }, + }, + }, + { parent }, + ) as Promise, + }, + + // Section for any items that don't fit into the other categories + MiscActions: { + id: 'misc-actions', + label: 'COSMERE.Actor.Sheet.Actions.MiscSectionName', + default: false, + filter: () => false, // Filter function is not used for this section + }, +}; + export class ActorActionsListComponent extends HandlebarsApplicationComponent< ConstructorOf > { @@ -42,9 +167,12 @@ export class ActorActionsListComponent extends HandlebarsApplicationComponent< static readonly ACTIONS = { 'toggle-action-details': this.onToggleActionDetails, 'use-item': this.onUseItem, + 'new-item': this.onNewItem, }; /* eslint-enable @typescript-eslint/unbound-method */ + protected sections: ListSection[] = []; + /** * Map of id to state */ @@ -81,17 +209,33 @@ export class ActorActionsListComponent extends HandlebarsApplicationComponent< void this.application.actor.useItem(item); } + private static async onNewItem( + this: ActorActionsListComponent, + event: Event, + ) { + // Get section element + const sectionElement = $(event.target!).closest('[data-section-id]'); + + // Get section id + const sectionId = sectionElement.data('section-id') as string; + + // Get section + const section = this.sections.find((s) => s.id === sectionId); + if (!section) return; + + // Create a new item + const item = await section.new?.(this.application.actor); + + // Render the item sheet + void item?.sheet?.render(true); + } + /* --- Context --- */ public async _prepareContext( params: unknown, context: ActorActionsListComponentRenderContext, ) { - // Get action types - const actionTypes = Object.keys( - CONFIG.COSMERE.action.types, - ) as ActionType[]; - // Get all activatable items (actions & items with an associated action) const activatableItems = this.application.actor.items .filter((item) => item.hasActivation()) @@ -102,17 +246,6 @@ export class ActorActionsListComponent extends HandlebarsApplicationComponent< item.system.alwaysEquipped, ); - // Get all items that are not actions or talents (but are activatable, e.g. weapons) - const nonActionItems = activatableItems.filter( - (item) => !item.isAction() && !item.isTalent(), - ); - - // Get talent items - const talentItems = activatableItems.filter((item) => item.isTalent()); - - // Get action items - const actionItems = activatableItems.filter((item) => item.isAction()); - // Ensure all items have an expand state record activatableItems.forEach((item) => { if (!(item.id in this.itemState)) { @@ -125,206 +258,160 @@ export class ActorActionsListComponent extends HandlebarsApplicationComponent< const searchText = context.actionsSearch?.text ?? ''; const sortDir = context.actionsSearch?.sort ?? SortDirection.Descending; + // Prepare sections + this.sections = this.prepareSections(); + + // Prepare sections data + const sectionsData = await this.prepareSectionsData( + this.sections, + activatableItems, + searchText, + sortDir, + ); + return { ...context, - sections: [ - ...(await this.categorizeItemsByType( - nonActionItems, - searchText, - sortDir, - )), - ...(await this.prepareTalentsData( - talentItems, - searchText, - sortDir, - )), - ...( - await Promise.all( - actionTypes.map(async (type) => { - const items = actionItems - .filter((i) => i.system.type === type) - .filter((i) => - i.name.toLowerCase().includes(searchText), - ) - .sort( - (a, b) => - a.name.compare(b.name) * - (sortDir === SortDirection.Descending - ? 1 - : -1), - ); - - return { - id: type, - label: CONFIG.COSMERE.action.types[type] - .labelPlural, - items, - itemData: await this.prepareItemData(items), - }; - }), - ) - ).filter((section) => section.items.length > 0), - ], + sections: sectionsData.filter( + (section) => + section.items.length > 0 || + (this.application.mode === 'edit' && section.default), + ), itemState: this.itemState, }; } - protected async categorizeItemsByType( - items: CosmereItem[], - filterText: string, - sort: SortDirection, - ) { - // Get item types - const types = Object.keys(CONFIG.COSMERE.items.types) as ItemType[]; - - // Categorize items by types - const categories = types.reduce( - (result, type) => { - // Get all physical items of type - result[type] = items - .filter((item) => item.type === type) - .filter((i) => i.name.toLowerCase().includes(filterText)) - .sort( - (a, b) => - a.name.compare(b.name) * - (sort === SortDirection.Descending ? 1 : -1), - ); + protected prepareSections() { + // Get paths + const paths = this.application.actor.paths; - return result; - }, - {} as Record, - ); + // Get ancestry + const ancestry = this.application.actor.ancestry; - // Set up sections - return await Promise.all( - (Object.keys(categories) as ItemType[]) - .filter((type) => categories[type].length > 0) - .map(async (type) => ({ - id: type, - label: CONFIG.COSMERE.items.types[type].labelPlural, - items: categories[type], - itemData: await this.prepareItemData(categories[type]), - })), - ); - } - - protected async prepareTalentsData( - items: TalentItem[], - filterText: string, - sort: SortDirection, - ) { - // Get all path items - const pathItems = this.application.actor.items.filter((item) => - item.isPath(), - ); + return [ + STATIC_SECTIONS.Weapons, - // Get paths - const paths = pathItems - .map((item) => ({ - label: item.name, - id: item.system.id, - modality: undefined as string | undefined, - })) - .sort((a, b) => a.label.compare(b.label)); - - // Get ancestry item - const ancestryItem = this.application.actor.items.find((item) => - item.isAncestry(), - ); + ...paths.map((path) => ({ + id: path.system.id, + label: game.i18n!.format( + 'COSMERE.Actor.Sheet.Actions.BaseSectionName', + { + type: path.name, + }, + ), + default: true, + filter: (item: CosmereItem) => + item.isTalent() && item.system.path === path.system.id, + new: (parent: CosmereActor) => + CosmereItem.create( + { + type: ItemType.Talent, + name: game.i18n!.localize( + 'COSMERE.Item.Type.Talent.New', + ), + system: { + path: path.system.id, + activation: { + type: ActivationType.Utility, + cost: { + type: ActionCostType.Action, + value: 1, + }, + }, + }, + }, + { parent }, + ) as Promise, + })), - // Map talents to paths - const talentsByPath = items.reduce( - (result, talent) => { - if (!talent.system.path) return result; + ...(ancestry + ? [ + { + id: ancestry.system.id, + label: game.i18n!.format( + 'COSMERE.Actor.Sheet.Actions.BaseSectionName', + { + type: ancestry.name, + }, + ), + default: false, + filter: (item: CosmereItem) => + item.isTalent() && + item.system.ancestry === ancestry.system.id, + new: (parent: CosmereActor) => + CosmereItem.create( + { + type: ItemType.Talent, + name: game.i18n!.localize( + 'COSMERE.Item.Type.Talent.New', + ), + system: { + ancestry: ancestry.system.id, + activation: { + type: ActivationType.Utility, + cost: { + type: ActionCostType.Action, + value: 1, + }, + }, + }, + }, + { parent }, + ) as Promise, + }, + ] + : []), - const path = paths.find((p) => p.id === talent.system.path); - if (!path) return result; + STATIC_SECTIONS.Equipment, + STATIC_SECTIONS.BasicActions, + STATIC_SECTIONS.MiscActions, + ]; + } - if (!result[path.id]) result[path.id] = []; - result[path.id].push(talent); + protected async prepareSectionsData( + sections: ListSection[], + items: CosmereItem[], + searchText: string, + sort: SortDirection, + ): Promise { + // Filter items into sections, putting all items that don't fit into a section into a "Misc" section + const itemsBySectionId = items.reduce( + (result, item) => { + const section = sections.find((s) => s.filter(item)); + if (!section) { + result['misc-actions'] ??= []; + result['misc-actions'].push(item); + } else { + if (!result[section.id]) result[section.id] = []; + result[section.id].push(item); + } return result; }, - {} as Record, + {} as Record, ); - // Get ancestry talents - const ancestryTalents = ( - ancestryItem - ? items.filter( - (item) => item.system.ancestry === ancestryItem.system.id, - ) - : [] - ) - .filter((item) => item.name.toLowerCase().includes(filterText)) - .sort( - (a, b) => - a.name.compare(b.name) * - (sort === SortDirection.Descending ? 1 : -1), - ); - - // Get remaining talents - const remainingTalents = items - .filter( - (item) => - (!item.system.path || !talentsByPath[item.system.path]) && - (!item.system.ancestry || - item.system.ancestry !== ancestryItem?.id), - ) - .filter((item) => item.name.toLowerCase().includes(filterText)) - .sort( - (a, b) => - a.name.compare(b.name) * - (sort === SortDirection.Descending ? 1 : -1), - ); - - return await Promise.all([ - ...(ancestryItem && ancestryTalents.length > 0 - ? [ - { - label: ancestryItem.name, - id: ancestryItem.system.id, - items: ancestryTalents, - itemData: await this.prepareItemData(ancestryTalents), - }, - ] - : []), + // Prepare sections + return await Promise.all( + sections.map(async (section) => { + // Get items for section, filter by search text, and sort + const sectionItems = (itemsBySectionId[section.id] ?? []) + .filter((i) => i.name.toLowerCase().includes(searchText)) + .sort( + (a, b) => + a.name.compare(b.name) * + (sort === SortDirection.Descending ? 1 : -1), + ); - ...paths - .filter((path) => talentsByPath[path.id]?.length > 0) - .map(async (path) => { - // Get talents - const talents = talentsByPath[path.id] - .filter((item) => - item.name.toLowerCase().includes(filterText), - ) - .sort( - (a, b) => - a.name.compare(b.name) * - (sort === SortDirection.Descending ? 1 : -1), - ); - - return { - ...path, - items: talents, - itemData: await this.prepareItemData(talents), - }; - }), - - ...(remainingTalents.length > 0 - ? [ - { - label: 'COSMERE.Item.Type.Talent.label_plural', - id: 'talents', - items: remainingTalents, - itemData: - await this.prepareItemData(remainingTalents), - }, - ] - : []), - ]); + return { + ...section, + canAddNewItems: !!section.new, + items: sectionItems, + itemData: await this.prepareItemData(sectionItems), + }; + }), + ); } protected async prepareItemData(items: CosmereItem[]) { diff --git a/src/system/applications/actor/components/adversary/actions-list.ts b/src/system/applications/actor/components/adversary/actions-list.ts index 76253dd4..aa9361d0 100644 --- a/src/system/applications/actor/components/adversary/actions-list.ts +++ b/src/system/applications/actor/components/adversary/actions-list.ts @@ -1,13 +1,21 @@ -import { ItemType } from '@system/types/cosmere'; -import { CosmereItem } from '@system/documents'; +import { + ItemType, + ActionType, + ActivationType, + ActionCostType, +} from '@system/types/cosmere'; +import { CosmereItem, CosmereActor } from '@system/documents'; // Components import { ActorActionsListComponent, ActorActionsListComponentRenderContext, + ListSection, } from '../actions-list'; import { SortDirection } from '../search-bar'; +// Constants + export class AdversaryActionsListComponent extends ActorActionsListComponent { /* --- Context --- */ @@ -25,17 +33,6 @@ export class AdversaryActionsListComponent extends ActorActionsListComponent { item.system.alwaysEquipped, ); - // Get all traits items - const traitItems = activatableItems.filter((item) => item.isTrait()); - - // Get all weapon items - const weaponItems = activatableItems.filter((item) => item.isWeapon()); - - // Get all action items (all non-trait, non-weapon items) - const actionItems = activatableItems.filter( - (item) => !item.isTrait() && !item.isWeapon(), - ); - // Ensure all items have an expand state record activatableItems.forEach((item) => { if (!(item.id in this.itemState)) { @@ -45,6 +42,13 @@ export class AdversaryActionsListComponent extends ActorActionsListComponent { } }); + // Prepare sections + this.sections = [ + this.prepareSection(ItemType.Trait), + this.prepareSection(ItemType.Weapon), + this.prepareSection(ItemType.Action), + ]; + const searchText = context.actionsSearch?.text ?? ''; const sortDir = context.actionsSearch?.sort ?? SortDirection.Descending; @@ -52,25 +56,29 @@ export class AdversaryActionsListComponent extends ActorActionsListComponent { ...context, sections: [ - await this.prepareSection( - ItemType.Trait, - traitItems, + await this.prepareSectionData( + this.sections[0], + activatableItems, searchText, sortDir, ), - await this.prepareSection( - ItemType.Weapon, - weaponItems, + await this.prepareSectionData( + this.sections[1], + activatableItems, searchText, sortDir, ), - await this.prepareSection( - ItemType.Action, - actionItems, + await this.prepareSectionData( + this.sections[2], + activatableItems, searchText, sortDir, ), - ].filter((section) => section.items.length > 0), + ].filter( + (section) => + section.items.length > 0 || + (this.application.mode === 'edit' && section.default), + ), itemState: this.itemState, }; @@ -78,23 +86,61 @@ export class AdversaryActionsListComponent extends ActorActionsListComponent { /* --- Helpers --- */ - private async prepareSection( - type: ItemType, + private prepareSection(type: ItemType): ListSection { + return { + id: type, + label: CONFIG.COSMERE.items.types[type].labelPlural, + default: true, + filter: (item: CosmereItem) => item.type === type, + new: (parent: CosmereActor) => + CosmereItem.create( + { + type, + name: game.i18n!.localize( + `COSMERE.Item.Type.${type.capitalize()}.New`, + ), + system: { + activation: { + type: ActivationType.Utility, + cost: { + type: ActionCostType.Action, + value: 1, + }, + }, + + ...(type === ItemType.Weapon + ? { + equipped: true, + } + : {}), + }, + }, + { parent }, + ) as Promise, + }; + } + + private async prepareSectionData( + section: ListSection, items: CosmereItem[], searchText: string, - sortDir: SortDirection, + sort: SortDirection, ) { + // Get items for section, filter by search text, and sort + const sectionItems = items + .filter(section.filter) + .filter((i) => i.name.toLowerCase().includes(searchText)) + .sort( + (a, b) => + a.name.compare(b.name) * + (sort === SortDirection.Descending ? 1 : -1), + ); + return { - id: type, - label: CONFIG.COSMERE.items.types[type].labelPlural, - items: items - .filter((i) => i.name.toLowerCase().includes(searchText)) - .sort( - (a, b) => - a.name.compare(b.name) * - (sortDir === SortDirection.Descending ? 1 : -1), - ), - itemData: await this.prepareItemData(items), + ...section, + canAddNewItems: !!section.new, + items: sectionItems, + itemData: await this.prepareItemData(sectionItems), }; } } diff --git a/src/system/applications/actor/components/equipment-list.ts b/src/system/applications/actor/components/equipment-list.ts index 4f1c0ef8..8a59f6b5 100644 --- a/src/system/applications/actor/components/equipment-list.ts +++ b/src/system/applications/actor/components/equipment-list.ts @@ -1,5 +1,6 @@ import { EquipHand, ItemType } from '@system/types/cosmere'; import { CosmereItem } from '@system/documents/item'; +import { CosmereActor } from '@system/documents/actor'; import { ConstructorOf } from '@system/types/utils'; import { AppContextMenu } from '@system/applications/utils/context-menu'; @@ -20,6 +21,39 @@ interface AdditionalItemData { descriptionHTML?: string; } +export interface ListSection { + /** + * The id of the section + */ + id: string; + + /** + * Nicely formatted label for the section + */ + label: string; + + /** + * Whether this section counts as default. + * Default sections are always shown in edit mode, even if they are empty. + */ + default: boolean; + + /** + * Filter function to determine if an item should be included in this section + */ + filter: (item: CosmereItem) => boolean; + + /** + * Factory function to create a new item of this type + */ + new?: (parent: CosmereActor) => Promise; +} + +export interface ListSectionData extends ListSection { + items: CosmereItem[]; + itemData: Record; +} + interface RenderContext extends BaseActorSheetRenderContext { equipmentSearch: { text: string; @@ -41,6 +75,7 @@ export class ActorEquipmentListComponent extends HandlebarsApplicationComponent< static readonly ACTIONS = { 'toggle-action-details': this.onToggleActionDetails, 'use-item': this.onUseItem, + 'new-item': this.onNewItem, 'toggle-equip': this.onToggleEquip, 'cycle-equip': this.onCycleEquip, 'decrease-quantity': this.onDecreaseQuantity, @@ -48,6 +83,8 @@ export class ActorEquipmentListComponent extends HandlebarsApplicationComponent< }; /* eslint-enable @typescript-eslint/unbound-method */ + protected sections: ListSection[] = []; + /** * Map of id to state */ @@ -84,6 +121,28 @@ export class ActorEquipmentListComponent extends HandlebarsApplicationComponent< void this.application.actor.useItem(item); } + private static async onNewItem( + this: ActorEquipmentListComponent, + event: Event, + ) { + // Get section element + const sectionElement = $(event.target!).closest('[data-section-id]'); + + // Get section id + const sectionId = sectionElement.data('section-id') as string; + + // Get section + const section = this.sections.find((s) => s.id === sectionId); + if (!section) return; + + // Create new item + const item = await section.new?.(this.application.actor); + if (!item) return; + + // Render the item sheet + void item.sheet?.render(true); + } + public static onToggleEquip( this: ActorEquipmentListComponent, event: Event, @@ -188,56 +247,73 @@ export class ActorEquipmentListComponent extends HandlebarsApplicationComponent< } }); + // Prepare sections + this.sections = [ + this.prepareSection(ItemType.Weapon), + this.prepareSection(ItemType.Armor), + this.prepareSection(ItemType.Equipment), + this.prepareSection(ItemType.Loot), + ]; + return { ...context, - sections: await this.categorizeItemsByType( - physicalItems, - context.equipmentSearch.text, - context.equipmentSearch.sort, + sections: await Promise.all( + this.sections.map((section) => + this.prepareSectionData( + section, + physicalItems, + context.equipmentSearch.text, + context.equipmentSearch.sort, + ), + ), ), itemState: this.itemState, }; } - private async categorizeItemsByType( + protected prepareSection(type: ItemType): ListSection { + return { + id: type, + label: CONFIG.COSMERE.items.types[type].labelPlural, + default: true, + filter: (item) => item.type === type, + new: (parent: CosmereActor) => + CosmereItem.create( + { + type, + name: game.i18n!.localize( + `COSMERE.Item.Type.${type.capitalize()}.New`, + ), + }, + { parent }, + ) as Promise, + }; + } + + protected async prepareSectionData( + section: ListSection, items: CosmereItem[], filterText: string, sort: SortDirection, ) { - // Get item types - const types = Object.keys(CONFIG.COSMERE.items.types) as ItemType[]; - - // Categorize items by types - const categories = types.reduce( - (result, type) => { - // Get all physical items of type - result[type] = items - .filter((item) => item.type === type) - .filter((i) => i.name.toLowerCase().includes(filterText)) - .sort( - (a, b) => - a.name.compare(b.name) * - (sort === SortDirection.Descending ? 1 : -1), - ); - - return result; - }, - {} as Record, - ); + // Get items for section, filter by search text, and sort + const sectionItems = items + .filter(section.filter) + .filter((i) => i.name.toLowerCase().includes(filterText)) + .sort( + (a, b) => + a.name.compare(b.name) * + (sort === SortDirection.Descending ? 1 : -1), + ); - // Set up sections - return await Promise.all( - (Object.keys(categories) as ItemType[]) - .filter((type) => categories[type].length > 0) - .map(async (type) => ({ - id: type, - label: CONFIG.COSMERE.items.types[type].labelPlural, - items: categories[type], - itemData: await this.prepareItemData(categories[type]), - })), - ); + return { + ...section, + canAddNewItems: !!section.new, + items: sectionItems, + itemData: await this.prepareItemData(sectionItems), + }; } private async prepareItemData(items: CosmereItem[]) { diff --git a/src/system/applications/component-system/system.ts b/src/system/applications/component-system/system.ts index 5ee9cce2..4f6972e4 100644 --- a/src/system/applications/component-system/system.ts +++ b/src/system/applications/component-system/system.ts @@ -310,8 +310,8 @@ export async function renderComponent( // Render const content = await renderTemplate(ComponentClass.TEMPLATE, { - ...context, ...instance, + ...context, __application: instance.application, __componentRef: componentRef, partId: instance.partId, diff --git a/src/system/documents/item.ts b/src/system/documents/item.ts index da7c8d13..5ea4b66f 100644 --- a/src/system/documents/item.ts +++ b/src/system/documents/item.ts @@ -136,6 +136,10 @@ export class CosmereItem< return this.type === ItemType.Trait; } + public isEquipment(): this is CosmereItem { + return this.type === ItemType.Equipment; + } + /* --- Mixin type guards --- */ /** diff --git a/src/templates/actors/components/actions-list.hbs b/src/templates/actors/components/actions-list.hbs index 6a37214f..384ac586 100644 --- a/src/templates/actors/components/actions-list.hbs +++ b/src/templates/actors/components/actions-list.hbs @@ -14,7 +14,15 @@
{{#if @root.editable}} -
+ {{#if section.canAddNewItems}} + + + + {{else}} +
+ {{/if}} {{/if}}
diff --git a/src/templates/actors/components/equipment-list.hbs b/src/templates/actors/components/equipment-list.hbs index 490b8f44..3f7d492c 100644 --- a/src/templates/actors/components/equipment-list.hbs +++ b/src/templates/actors/components/equipment-list.hbs @@ -12,7 +12,15 @@
{{#if @root.editable}} -
+ {{#if section.canAddNewItems}} + + + + {{else}} +
+ {{/if}} {{/if}}