import clsx from "clsx"; import { useContext, ReactNode, PureComponent } from "react"; import React from "react"; import { connect } from "react-redux"; import { DarkBuilderSvg } from "@dndbeyond/character-components/es"; import { CharacterNotes, CharacterTheme, Constants, ContainerManager, Creature, CreatureUtils, InfusionChoice, InfusionChoiceUtils, Item, ItemUtils, SnippetData, RuleData, rulesEngineSelectors, ContainerUtils, InventoryManager, FormatUtils, serviceDataSelectors, PartyInfo, CoinManager, ClassUtils, CharClass, ItemPlan, ItemPlanUtils, } from "@dndbeyond/character-rules-engine/es"; import { Button } from "~/components/Button"; import { ItemName } from "~/components/ItemName"; import { Link } from "~/components/Link"; import { NumberDisplay } from "~/components/NumberDisplay"; import { TabFilter } from "~/components/TabFilter"; import { TabList } from "~/components/TabList"; import { useSidebar } from "~/contexts/Sidebar"; import { PaneInfo } from "~/contexts/Sidebar/Sidebar"; import a11yStyles from "~/styles/accessibility.module.css"; import { EquipmentOverview } from "~/subApps/sheet/components/Equipment/EquipmentOverview"; import { MagicItemPlans } from "~/subApps/sheet/components/MagicItemPlans"; import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types"; import CurrencyButton from "../../../CharacterSheet/components/CurrencyButton"; import { ItemSlotManager } from "../../../Shared/components/ItemSlotManager"; import { ThemeButton } from "../../../Shared/components/common/Button"; import { CoinManagerContext } from "../../../Shared/managers/CoinManagerContext"; import { InventoryManagerContext } from "../../../Shared/managers/InventoryManagerContext"; import { appEnvSelectors } from "../../../Shared/selectors"; import { PaneIdentifierUtils } from "../../../Shared/utils"; import ContentGroup from "../../components/ContentGroup"; import Infusions from "../../components/Infusions"; import InventoryFilter from "../../components/InventoryFilter"; import InventoryTableHeader from "../../components/InventoryTableHeader"; import OtherPossessions from "../../components/OtherPossessions"; import { sheetAppSelectors } from "../../selectors"; import { SheetAppState } from "../../typings"; import Attunement from "../Attunement"; import Inventory from "../Inventory"; import styles from "./styles.module.css"; interface Props { showNotes: boolean; builderUrl: string; inventory: Array; partyInventory: Array; creatures: Array; notes: CharacterNotes; infusionChoices: Array; ruleData: RuleData; snippetData: SnippetData; isReadonly: boolean; isMobile: boolean; theme: CharacterTheme; proficiencyBonus: number; classes: Array; inventoryManager: InventoryManager; campaignInfo: PartyInfo | null; coinManager: CoinManager; paneHistoryStart: PaneInfo["paneHistoryStart"]; itemPlans: Array; maxReplicatedItemsCount: number | null; } interface StateFilterData { filteredInventory: Array; filteredPartyInventory: Array; showAdvancedFilters: boolean; isFiltering: boolean; } interface State { infusableChoices: Array; filterData: StateFilterData; filteredEquippedInventory: Array; shouldShowPartyInventory: number; } class Equipment extends PureComponent { static defaultProps = { showNotes: true, }; constructor(props: Props) { super(props); this.state = { infusableChoices: props.infusionChoices.filter((infusionChoice) => InfusionChoiceUtils.canInfuse(infusionChoice) ), filterData: { filteredInventory: [], filteredPartyInventory: [], showAdvancedFilters: false, isFiltering: false, }, filteredEquippedInventory: [], shouldShowPartyInventory: 0, // mui used 0,1,2 for tabs so 0 is off 1 is on... }; } componentDidUpdate(prevProps: Props, prevState: State) { if (prevProps.infusionChoices !== this.props.infusionChoices) { this.setState({ infusableChoices: this.props.infusionChoices.filter((infusionChoice) => InfusionChoiceUtils.canInfuse(infusionChoice) ), }); } } handleCurrencyClick = ( evt: React.MouseEvent | React.KeyboardEvent, containerDefinitionKey: string ): void => { const { paneHistoryStart } = this.props; evt.stopPropagation(); evt.nativeEvent.stopImmediatePropagation(); paneHistoryStart( PaneComponentEnum.CURRENCY, PaneIdentifierUtils.generateCurrencyContext(containerDefinitionKey) ); }; handlePossessionsManage = (): void => { const { paneHistoryStart, isReadonly } = this.props; if (!isReadonly) { paneHistoryStart( PaneComponentEnum.NOTE_MANAGE, PaneIdentifierUtils.generateNote( Constants.NoteKeyEnum.PERSONAL_POSSESSIONS ) ); } }; handleManageClick = (): void => { const { paneHistoryStart } = this.props; paneHistoryStart(PaneComponentEnum.EQUIPMENT_MANAGE); }; handleFilterUpdate = (filterData: StateFilterData): void => { this.setState({ filterData, filteredEquippedInventory: [ ...filterData.filteredInventory.filter(ItemUtils.isEquipped), ...filterData.filteredPartyInventory.filter(ItemUtils.isEquipped), ], }); }; handleInfusionChoiceShow = (infusionChoice: InfusionChoice): void => { const { paneHistoryStart } = this.props; const choiceKey = InfusionChoiceUtils.getKey(infusionChoice); if (choiceKey !== null) { paneHistoryStart( PaneComponentEnum.INFUSION_CHOICE, PaneIdentifierUtils.generateInfusionChoice(choiceKey) ); } }; handleItemShow = (item: Item): void => { const { paneHistoryStart } = this.props; paneHistoryStart( PaneComponentEnum.ITEM_DETAIL, PaneIdentifierUtils.generateItem(ItemUtils.getMappingId(item)) ); }; handleContainerShow = (container: ContainerManager): void => { const { paneHistoryStart } = this.props; paneHistoryStart( PaneComponentEnum.CONTAINER, PaneIdentifierUtils.generateContainer(container.getDefinitionKey()) ); }; handleCreatureShow = (creature: Creature): void => { const { paneHistoryStart } = this.props; paneHistoryStart( PaneComponentEnum.CREATURE, PaneIdentifierUtils.generateCreature(CreatureUtils.getMappingId(creature)) ); }; handleItemPlansManagePaneShow = (): void => { const { paneHistoryStart } = this.props; paneHistoryStart(PaneComponentEnum.ITEM_PLANS_MANAGE); }; shouldRenderInfusions = (): boolean => { const { infusionChoices } = this.props; return infusionChoices.length > 0; }; shouldRenderMagicItemPlans = (): boolean => { const { classes } = this.props; return classes.some((charClass) => ClassUtils.getEnablesReplicateMagicItem(charClass) ); }; renderContainer = ( container: ContainerManager, inventory: Array ): ReactNode => { const { shouldShowPartyInventory } = this.state; const { isReadonly, showNotes, theme, coinManager } = this.props; const containerMappingId = container.getMappingId(); const containerName = container.getName(); const weightInfo = container.getWeightInfo(); const containerInventory = container.getInventoryItems({ filteredInventory: inventory, }).items; const containerItem = container.getContainerItem(); const quantityNode = ( {`(${containerInventory.length})`} ); const nameNode: ReactNode = containerItem ? ( <> {quantityNode} ) : ( <> {containerName} {quantityNode} ); const headerNode: ReactNode = (
{ evt.nativeEvent.stopImmediatePropagation(); this.handleContainerShow(container); }} role="button" data-testid={`${containerItem?.getName() || containerName}-section` .toLowerCase() .replace(/\s/g, "-")} >
{containerItem && ( { if (uses === 0) { containerItem.handleUnequip(); } if (uses === 1) { containerItem.handleEquip(); } }} theme={theme} useTooltip={false} /> )}
{nameNode}
{weightInfo.capacity > 0 && ( ({Math.ceil(weightInfo.total)}/ {FormatUtils.renderWeight(weightInfo.capacity)}) )}
{coinManager.canUseCointainers() ? (
this.handleCurrencyClick( evt, ContainerUtils.getDefinitionKey(container.container) ) } />
) : ( containerItem && container.isEquipped() && !containerItem.isEquippedToCurrentCharacter() && (
Equipped by {containerItem.getEquippedCharacterName()}
) )}
); return ( ); }; renderCharacterContainers = (): ReactNode => { const { filterData } = this.state; const { inventoryManager } = this.props; return inventoryManager .getCharacterContainers() .map((container) => this.renderContainer(container, filterData.filteredInventory) ); }; renderPartyContainers = (): ReactNode => { const { filterData } = this.state; const { inventoryManager } = this.props; return inventoryManager .getPartyContainers() .map((container) => this.renderContainer(container, filterData.filteredPartyInventory) ); }; renderAttunement = (): ReactNode => { const { isMobile } = this.props; return ( ); }; renderMagicItemPlans = (): ReactNode => { const { isReadonly, itemPlans, maxReplicatedItemsCount } = this.props; if (!this.shouldRenderMagicItemPlans()) { return null; } const itemPlanWithMappingsCount = itemPlans.filter((plan) => ItemPlanUtils.getItem(plan) ).length; const headerNode: ReactNode = ( <> Magic Item Plans{" "} {maxReplicatedItemsCount !== null && ( ({itemPlanWithMappingsCount}/{maxReplicatedItemsCount} Replicated) )} ); let extraNode: ReactNode = null; if (!isReadonly) { extraNode = ( ); } return ( ); }; renderInfusions = (): ReactNode => { const { infusableChoices } = this.state; const { builderUrl, infusionChoices, isReadonly, snippetData, inventory, partyInventory, creatures, ruleData, proficiencyBonus, theme, } = this.props; const headerNode: ReactNode = ( <> Infusions{" "} {infusableChoices.length !== infusionChoices.length && ( <> ({infusableChoices.length}/{infusionChoices.length} Known Infusions) )} ); let extraNode: ReactNode = null; if (!isReadonly) { extraNode = ( Manage Infusions ); } return ( ); }; renderOtherPossessions = (): ReactNode => { const { notes, isReadonly } = this.props; if (isReadonly) { return null; } return ( ); }; renderContent = (): ReactNode => { const { isReadonly, inventoryManager, inventory, partyInventory, showNotes, } = this.props; const { shouldShowPartyInventory } = this.state; const headerNode: ReactNode = (
{ evt.nativeEvent.stopImmediatePropagation(); this.handleManageClick(); }} > Party Inventory
); return (
} filters={[ { label: "All", content: ( <> {shouldShowPartyInventory === 0 ? ( <> {this.renderCharacterContainers()} {this.renderAttunement()} {this.shouldRenderInfusions() && this.renderInfusions()} {this.renderMagicItemPlans()} {this.renderOtherPossessions()} ) : null} {shouldShowPartyInventory === 1 && inventoryManager.getPartyContainers().length > 0 ? ( {this.renderPartyContainers()} ) : null} ), }, ...(shouldShowPartyInventory === 0 ? inventoryManager .getCharacterContainers() .map((characterContainer) => ({ label: characterContainer.getName(), content: this.renderContainer( characterContainer, inventory ), })) : []), ...(shouldShowPartyInventory === 1 ? inventoryManager.getPartyContainers().map((partyContainer) => ({ label: partyContainer.getName(), content: this.renderContainer(partyContainer, partyInventory), })) : []), ...(shouldShowPartyInventory === 0 ? [ { label: "Attunement", content: this.renderAttunement(), }, ...(this.shouldRenderInfusions() ? [ { label: "Infusions", content: this.renderInfusions(), }, ] : []), ...(this.shouldRenderMagicItemPlans() ? [ { label: "Magic Item Plans", content: this.renderMagicItemPlans(), }, ] : []), ...(!isReadonly ? [ { label: "Other Possessions", content: this.renderOtherPossessions(), }, ] : []), ] : []), ]} showAllTab={false} />
); }; renderManageButton = (): ReactNode => { const { isReadonly } = this.props; if (isReadonly) { return null; } return ( Manage Inventory ); }; toggleShouldShowPartyInventory = (event, newValue): void => { this.setState({ shouldShowPartyInventory: newValue, }); }; renderOverviewFiltersAndContent = (): ReactNode => { const { filterData, shouldShowPartyInventory } = this.state; const { inventory, ruleData, theme, partyInventory } = this.props; return ( <>
{!filterData.showAdvancedFilters && this.renderContent()} ); }; handleChangeTab = (id) => this.setState({ shouldShowPartyInventory: id === "party-inventory" ? 1 : 0, }); render() { const { campaignInfo } = this.props; return (

Inventory

{campaignInfo !== null ? ( ) : ( this.renderOverviewFiltersAndContent() )}
); } } function mapStateToProps(state: SheetAppState) { return { builderUrl: sheetAppSelectors.getBuilderUrl(state), inventory: rulesEngineSelectors.getInventory(state), partyInventory: rulesEngineSelectors.getPartyInventory(state), creatures: rulesEngineSelectors.getCreatures(state), notes: rulesEngineSelectors.getCharacterNotes(state), infusionChoices: rulesEngineSelectors.getAvailableInfusionChoices(state), ruleData: rulesEngineSelectors.getRuleData(state), snippetData: rulesEngineSelectors.getSnippetData(state), theme: rulesEngineSelectors.getCharacterTheme(state), isReadonly: appEnvSelectors.getIsReadonly(state), isMobile: appEnvSelectors.getIsMobile(state), proficiencyBonus: rulesEngineSelectors.getProficiencyBonus(state), classes: rulesEngineSelectors.getClasses(state), campaignInfo: serviceDataSelectors.getPartyInfo(state), itemPlans: rulesEngineSelectors.getAvailableItemPlans(state), maxReplicatedItemsCount: rulesEngineSelectors.getMaxReplicatedItemsCount(state), }; } const EquipmentContainer = (props) => { const { inventoryManager } = useContext(InventoryManagerContext); const { coinManager } = useContext(CoinManagerContext); const { pane: { paneHistoryStart }, } = useSidebar(); return ( ); }; export default connect(mapStateToProps)(EquipmentContainer);