New source found from dndbeyond.com

This commit is contained in:
2026-07-08 01:00:09 -07:00
parent 9a983a6d7b
commit dcefa0d2f2
2865 changed files with 222467 additions and 49053 deletions
@@ -1,25 +1,10 @@
import Alert from "@mui/material/Alert";
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle";
import Stack from "@mui/material/Stack";
import Tab from "@mui/material/Tab";
import Tabs from "@mui/material/Tabs";
import Typography from "@mui/material/Typography";
import { visuallyHidden } from "@mui/utils";
import { useContext, ReactNode, MouseEvent, PureComponent } from "react";
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 {
DarkBuilderSvg,
FeatureFlagContext,
} from "@dndbeyond/character-components/es";
import {
CharacterCurrencyContract,
CharacterNotes,
CharacterTheme,
Constants,
@@ -33,23 +18,29 @@ import {
SnippetData,
RuleData,
rulesEngineSelectors,
characterSelectors,
BaseCharClass,
ContainerUtils,
InventoryManager,
FormatUtils,
CampaignUtils,
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";
@@ -60,7 +51,6 @@ import { InventoryManagerContext } from "../../../Shared/managers/InventoryManag
import { appEnvSelectors } from "../../../Shared/selectors";
import { PaneIdentifierUtils } from "../../../Shared/utils";
import ContentGroup from "../../components/ContentGroup";
import EquipmentOverview from "../../components/EquipmentOverview";
import Infusions from "../../components/Infusions";
import InventoryFilter from "../../components/InventoryFilter";
import InventoryTableHeader from "../../components/InventoryTableHeader";
@@ -69,6 +59,7 @@ 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;
@@ -77,22 +68,21 @@ interface Props {
inventory: Array<Item>;
partyInventory: Array<Item>;
creatures: Array<Creature>;
currencies: CharacterCurrencyContract | null;
notes: CharacterNotes;
infusionChoices: Array<InfusionChoice>;
weight: number;
weightSpeedType: Constants.WeightSpeedTypeEnum;
ruleData: RuleData;
snippetData: SnippetData;
isReadonly: boolean;
isMobile: boolean;
theme: CharacterTheme;
proficiencyBonus: number;
classes: Array<BaseCharClass>;
classes: Array<CharClass>;
inventoryManager: InventoryManager;
campaignInfo: PartyInfo | null;
coinManager: CoinManager;
paneHistoryStart: PaneInfo["paneHistoryStart"];
itemPlans: Array<ItemPlan>;
maxReplicatedItemsCount: number | null;
}
interface StateFilterData {
filteredInventory: Array<Item>;
@@ -105,7 +95,6 @@ interface State {
filterData: StateFilterData;
filteredEquippedInventory: Array<Item>;
shouldShowPartyInventory: number;
shouldShowDeleteOnlyInfo: boolean;
}
class Equipment extends PureComponent<Props, State> {
@@ -128,7 +117,6 @@ class Equipment extends PureComponent<Props, State> {
},
filteredEquippedInventory: [],
shouldShowPartyInventory: 0, // mui used 0,1,2 for tabs so 0 is off 1 is on...
shouldShowDeleteOnlyInfo: false,
};
}
@@ -157,16 +145,6 @@ class Equipment extends PureComponent<Props, State> {
);
};
handleWeightClick = (): void => {
const { paneHistoryStart } = this.props;
paneHistoryStart(PaneComponentEnum.ENCUMBRANCE);
};
handleCampaignClick = (): void => {
const { paneHistoryStart } = this.props;
paneHistoryStart(PaneComponentEnum.CAMPAIGN);
};
handlePossessionsManage = (): void => {
const { paneHistoryStart, isReadonly } = this.props;
@@ -233,12 +211,26 @@ class Equipment extends PureComponent<Props, State> {
);
};
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<Item>
@@ -380,7 +372,7 @@ class Equipment extends PureComponent<Props, State> {
};
renderAttunement = (): ReactNode => {
const { isMobile, theme } = this.props;
const { isMobile } = this.props;
return (
<ContentGroup header="Attunement">
@@ -389,6 +381,54 @@ class Equipment extends PureComponent<Props, State> {
);
};
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 && (
<span className={styles.replicatedCount}>
({itemPlanWithMappingsCount}/{maxReplicatedItemsCount} Replicated)
</span>
)}
</>
);
let extraNode: ReactNode = null;
if (!isReadonly) {
extraNode = (
<Button
variant="text"
themed
size="x-small"
className="ct-equipment__builder-link-text"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
this.handleItemPlansManagePaneShow();
}}
>
Manage Plans
</Button>
);
}
return (
<ContentGroup header={headerNode} extra={extraNode}>
<MagicItemPlans />
</ContentGroup>
);
};
renderInfusions = (): ReactNode => {
const { infusableChoices } = this.state;
const {
@@ -421,7 +461,7 @@ class Equipment extends PureComponent<Props, State> {
<Link
href={builderUrl}
className="ct-equipment__builder-link"
useRouter
userouter
>
<DarkBuilderSvg />
<span className="ct-equipment__builder-link-text">
@@ -490,7 +530,7 @@ class Equipment extends PureComponent<Props, State> {
);
return (
<div className="ct-equipment__content">
<div className={styles.tabFilter}>
<TabFilter
sharedChildren={<InventoryTableHeader showNotes={showNotes} />}
filters={[
@@ -503,20 +543,16 @@ class Equipment extends PureComponent<Props, State> {
{this.renderCharacterContainers()}
{this.renderAttunement()}
{this.shouldRenderInfusions() && this.renderInfusions()}
{this.renderMagicItemPlans()}
{this.renderOtherPossessions()}
</>
) : null}
<FeatureFlagContext.Consumer>
{({ imsFlag }) => {
return imsFlag &&
shouldShowPartyInventory === 1 &&
inventoryManager.getPartyContainers().length > 0 ? (
<ContentGroup header={headerNode}>
{this.renderPartyContainers()}
</ContentGroup>
) : null;
}}
</FeatureFlagContext.Consumer>
{shouldShowPartyInventory === 1 &&
inventoryManager.getPartyContainers().length > 0 ? (
<ContentGroup header={headerNode}>
{this.renderPartyContainers()}
</ContentGroup>
) : null}
</>
),
},
@@ -551,6 +587,14 @@ class Equipment extends PureComponent<Props, State> {
},
]
: []),
...(this.shouldRenderMagicItemPlans()
? [
{
label: "Magic Item Plans",
content: this.renderMagicItemPlans(),
},
]
: []),
...(!isReadonly
? [
{
@@ -592,126 +636,17 @@ class Equipment extends PureComponent<Props, State> {
shouldShowPartyInventory: newValue,
});
};
handleCloseDeleteOnlyInfo = (event: MouseEvent): void => {
event.stopPropagation();
event.nativeEvent.stopImmediatePropagation();
this.setState({
shouldShowDeleteOnlyInfo: false,
});
};
handleOpenDeleteOnlyInfo = (event: MouseEvent): void => {
event.stopPropagation();
event.nativeEvent.stopImmediatePropagation();
this.setState({
shouldShowDeleteOnlyInfo: true,
});
};
renderDeleteOnlyAlertAction = (): ReactNode => {
const { shouldShowDeleteOnlyInfo } = this.state;
const { campaignInfo } = this.props;
return (
<div>
<ThemeButton
style="outline"
size="medium"
onClick={this.handleOpenDeleteOnlyInfo}
>
More Info
</ThemeButton>
<Dialog
open={shouldShowDeleteOnlyInfo}
onClose={this.handleCloseDeleteOnlyInfo}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete Only</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Party Inventory sharing isnt currently enabled for this campaign.
Your old items are still here, but you wont be able to add new
items until its turned on again.
</DialogContentText>
</DialogContent>
<DialogActions>
{campaignInfo ? (
<Button
variant="outlined"
color="primary"
component="a"
href={CampaignUtils.getLink(campaignInfo) || "/my-campaigns"}
size="small"
>
go to campaign
</Button>
) : null}
<Button
variant="outlined"
color="primary"
onClick={this.handleCloseDeleteOnlyInfo}
autoFocus
size="small"
>
Close
</Button>
</DialogActions>
</Dialog>
</div>
);
};
renderOverviewFiltersAndContent = (): ReactNode => {
const { filterData, shouldShowPartyInventory } = this.state;
const {
currencies,
weight,
weightSpeedType,
inventory,
ruleData,
theme,
partyInventory,
campaignInfo,
inventoryManager,
coinManager,
} = this.props;
const cointainerFlagEnabled: boolean = coinManager.canUseCointainers();
let coin: CharacterCurrencyContract | null = currencies;
if (shouldShowPartyInventory === 1 && campaignInfo) {
// We need all the party coin if flag is on, or just Party Equipment coin if not
if (cointainerFlagEnabled) {
coin = coinManager.getAllPartyCoin();
} else {
coin = CampaignUtils.getCoin(campaignInfo);
}
} else if (cointainerFlagEnabled) {
// We need to change it to all container currencies
coin = coinManager.getAllCharacterCoin();
}
const { inventory, ruleData, theme, partyInventory } = this.props;
return (
<>
<div className="ct-equipment__overview">
<EquipmentOverview
shouldShowPartyInventory={shouldShowPartyInventory === 1}
currencies={coin}
weight={weight}
weightSpeedType={weightSpeedType}
onCurrencyClick={(evt) =>
this.handleCurrencyClick(
evt,
shouldShowPartyInventory === 1
? inventoryManager.getPartyEquipmentContainerDefinitionKey() ??
inventoryManager.getCharacterContainerDefinitionKey()
: inventoryManager.getCharacterContainerDefinitionKey()
)
}
onWeightClick={this.handleWeightClick}
onCampaignClick={this.handleCampaignClick}
enableManage={false}
isDarkMode={theme.isDarkMode}
campaignName={
campaignInfo !== null ? CampaignUtils.getName(campaignInfo) : null
activeEquipmentTab={
shouldShowPartyInventory === 1 ? "party" : "character"
}
/>
</div>
@@ -730,113 +665,39 @@ class Equipment extends PureComponent<Props, State> {
);
};
renderOptinToInventorySharing = (): ReactNode => {
handleChangeTab = (id) =>
this.setState({
shouldShowPartyInventory: id === "party-inventory" ? 1 : 0,
});
render() {
const { campaignInfo } = this.props;
return (
<>
<Stack justifyContent="center">
<Stack marginBottom="10px" marginTop="40px">
<Typography color="primary" align="center" variant="body2">
Want a hand with that loot?
</Typography>
</Stack>
<Stack marginBottom="30px" alignItems={"center"}>
<Typography
align="center"
variant="body1"
sx={{ maxWidth: "320px" }}
>
Party Inventory makes it easy for your group to keep track of all
their shared loot and coin. Add it to your campaign today with a
subscription to D&D Beyond!
</Typography>
</Stack>
<Stack
spacing={2}
direction={{ xs: "column", sm: "row" }}
justifyContent="center"
>
{campaignInfo ? (
<Button
variant="contained"
color="primary"
component="a"
href={CampaignUtils.getLink(campaignInfo) || "/my-campaigns"}
size="large"
sx={{
marginLeft: { xs: 0 },
marginRight: { xs: 0 },
}}
>
go to campaign
</Button>
) : null}
<Button variant="contained" color="primary" size="large">
view subscription plans
</Button>
</Stack>
<Stack
style={{ display: "flex", justifyContent: "center", marginTop: 51 }}
>
<div className="ct-item-detail__full-image">
<img
className="ct-item-detail__full-image-img"
src="https://www.dndbeyond.com/avatars/thumbnails/7/120/315/315/636284708068284913.jpeg"
alt=""
/>
</div>
</Stack>
</Stack>
</>
);
};
render() {
const { shouldShowPartyInventory } = this.state;
const { campaignInfo, inventoryManager } = this.props;
return (
<section className="ct-equipment">
<h2 style={visuallyHidden}>Inventory</h2>
<FeatureFlagContext.Consumer>
{({ imsFlag }) => {
return imsFlag && campaignInfo !== null ? (
<>
<Tabs
value={shouldShowPartyInventory}
indicatorColor="primary"
textColor="primary"
onChange={this.toggleShouldShowPartyInventory}
aria-label="disabled tabs example"
variant="fullWidth"
>
<Tab label="My Inventory" data-testid="my-inventory" />
<Tab label="Party Inventory" data-testid="party-inventory" />
</Tabs>
{/* TODO: make this go away when last itme is removed... */}
{shouldShowPartyInventory &&
inventoryManager.isSharingTurnedDeleteOnly() ? (
<div role="button" onMouseUp={this.handleOpenDeleteOnlyInfo}>
<Alert
severity="info"
action={this.renderDeleteOnlyAlertAction()}
>
Party Inventory is currently turned off for this campaign.
</Alert>
</div>
) : null}
{shouldShowPartyInventory &&
inventoryManager.isSharingTurnedOff()
? this.renderOptinToInventorySharing()
: this.renderOverviewFiltersAndContent()}
</>
) : (
this.renderOverviewFiltersAndContent()
);
}}
</FeatureFlagContext.Consumer>
<section className={clsx(["ct-equipment", styles.section])}>
<h2 className={a11yStyles.screenreaderOnly}>Inventory</h2>
{campaignInfo !== null ? (
<TabList
className={styles.tabList}
variant="fullwidth"
themed={true}
tabs={[
{
label: "My Inventory",
content: this.renderOverviewFiltersAndContent(),
id: "my-inventory",
},
{
label: "Party Inventory",
content: this.renderOverviewFiltersAndContent(),
id: "party-inventory",
},
]}
onChangeTab={this.handleChangeTab}
/>
) : (
this.renderOverviewFiltersAndContent()
)}
</section>
);
}
@@ -848,19 +709,19 @@ function mapStateToProps(state: SheetAppState) {
inventory: rulesEngineSelectors.getInventory(state),
partyInventory: rulesEngineSelectors.getPartyInventory(state),
creatures: rulesEngineSelectors.getCreatures(state),
weight: rulesEngineSelectors.getTotalCarriedWeight(state),
weightSpeedType: rulesEngineSelectors.getCurrentCarriedWeightType(state),
notes: rulesEngineSelectors.getCharacterNotes(state),
infusionChoices: rulesEngineSelectors.getAvailableInfusionChoices(state),
currencies: rulesEngineSelectors.getCurrencies(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: characterSelectors.getClasses(state),
classes: rulesEngineSelectors.getClasses(state),
campaignInfo: serviceDataSelectors.getPartyInfo(state),
itemPlans: rulesEngineSelectors.getAvailableItemPlans(state),
maxReplicatedItemsCount:
rulesEngineSelectors.getMaxReplicatedItemsCount(state),
};
}