import clsx from "clsx"; import { FC, HTMLAttributes, ReactNode } from "react"; import { Creature, CreatureUtils, ConditionUtils, DamageAdjustmentContract, FormatUtils, RuleData, CreatureAbilityInfo, } from "@dndbeyond/character-rules-engine/es"; import { HtmlContent } from "~/components/HtmlContent"; import { AbilityStatMentalList, AbilityStatPhysicalList, DDB_MEDIA_URL, StatBlockTypeEnum, } from "../../../../constants"; import styles from "./styles.module.css"; interface Props extends HTMLAttributes { variant: "default" | "basic"; creature: Creature; ruleData: RuleData; } export const CreatureBlock: FC = ({ variant = "default", creature, ruleData, className = "", }) => { const statBlockType = CreatureUtils.getStatBlockType(creature); const isVersion2024 = statBlockType === StatBlockTypeEnum.CORE_RULES_2024; const getDamageAdjustment = ( type: "vulnerability" | "resistance" | "immunity" ): string | null => { let damageAdjustments: Array = []; switch (type) { case "vulnerability": damageAdjustments = CreatureUtils.getDamageVulnerabilities(creature); break; case "resistance": damageAdjustments = CreatureUtils.getDamageResistances(creature); break; case "immunity": damageAdjustments = CreatureUtils.getDamageImmunities(creature); break; default: break; } if (!damageAdjustments.length) { return null; } else { return damageAdjustments .map((damageAdjustment) => damageAdjustment.name) .join(", "); } }; const renderSeparator = (): ReactNode => { if (isVersion2024) { return null; } //TODO not hard-code the src url here? return ( ); }; const renderStatTable = ( stats: Array, type: "physical" | "mental" ): ReactNode => { return ( {stats.map((stat) => { return ( ); })}
Mod Save
{stat.statKey} {stat.score} {FormatUtils.renderSignedNumber(stat.modifier)} {FormatUtils.renderSignedNumber(stat.saveModifier)}
); }; const renderStats = (): ReactNode => { const stats = CreatureUtils.getAbilities(creature); if (isVersion2024) { const mentalStats: Array = []; const physicalStats: Array = []; stats.forEach((ability) => { if (AbilityStatPhysicalList.includes(ability.id)) { physicalStats.push(ability); } if (AbilityStatMentalList.includes(ability.id)) { mentalStats.push(ability); } }); return (
{renderStatTable(physicalStats, "physical")} {renderStatTable(mentalStats, "mental")}
); } return (
{stats.map((stat) => { const statKey: string = stat.statKey ? stat.statKey : ""; const modifier: number = stat.modifier ? stat.modifier : 0; const score: number | null = stat.score; return (

{statKey}

{score}

({FormatUtils.renderSignedNumber(modifier)})

); })}
); }; const renderActions = (): ReactNode => { let groupActionNode: ReactNode; const groupActionSnippet = CreatureUtils.getGroupActionSnippet(creature); if (groupActionSnippet) { groupActionNode = ; } let actionsNode: ReactNode; const actionsDescription = CreatureUtils.getActionsDescription(creature); if (actionsDescription) { actionsNode = ; } const contentNode: ReactNode = ( <> {groupActionNode} {actionsNode} ); return renderDescription("Actions", contentNode); }; const renderLegendaryActions = (): ReactNode => { if (!CreatureUtils.canUseLegendaryActions(creature)) { return null; } return renderDescription( "Legendary Actions", CreatureUtils.getLegendaryActionsDescription(creature) ); }; const renderSpecialDescription = (): ReactNode => { const groupInfo = CreatureUtils.getGroupInfo(creature); let groupNode: ReactNode; if (groupInfo?.specialQualityText) { let groupTitle: ReactNode; if (groupInfo.specialQualityTitle) { groupTitle = ( <> {groupInfo.specialQualityTitle} .{" "} ); } groupNode = (

{groupTitle} {groupInfo.specialQualityText}

); } let monsterNode: ReactNode; const monsterDescription = CreatureUtils.getSpecialTraitsDescription(creature); if (monsterDescription) { monsterNode = ; } let contentNode: ReactNode = null; if (monsterNode || groupNode) { contentNode = ( <> {monsterNode} {groupNode} ); } return renderDescription("Traits", contentNode); }; const renderDescription = ( label: string | null, description: ReactNode ): ReactNode => { if (!description) { return null; } let descriptionNode: ReactNode; if (typeof description === "string") { descriptionNode = ( ); } else { descriptionNode =
{description}
; } return ( <> {label &&

{label}

} {descriptionNode} ); }; const renderAttribute = (label: string, content: string | null) => { if (!content) { return null; } return (

{label}

{content}

); }; const renderImmunities = (): ReactNode => { const damageImmunities = getDamageAdjustment("immunity"); const conditionImmunities = CreatureUtils.getConditionImmunities(creature) .map((condition) => ConditionUtils.getName(condition)) .join(", "); if (!isVersion2024) { if (!damageImmunities && !conditionImmunities) { return null; } const immunityChunks: Array = []; if (damageImmunities) { immunityChunks.push(damageImmunities); } if (conditionImmunities) { immunityChunks.push(conditionImmunities); } return renderAttribute("Immunities", immunityChunks.join("; ")); } return ( <> {renderAttribute("Damage Immunities", damageImmunities)} {renderAttribute("Condition Immunities", conditionImmunities)} ); }; return (

{CreatureUtils.getName(creature)}

{CreatureUtils.renderMetaText(creature, ruleData, variant === "basic")}

{renderSeparator()} {renderAttribute( isVersion2024 ? "AC" : "Armor Class", CreatureUtils.renderArmorClass(creature) )} {renderAttribute( "Initiative", CreatureUtils.renderInitiativeInfo(creature) )} {renderAttribute( isVersion2024 ? "HP" : "Hit Points", CreatureUtils.renderHitPointInfo(creature) )} {renderAttribute( "Speed", CreatureUtils.renderSpeedInfo(creature, ruleData) )} {variant === "default" && ( <> {renderSeparator()} {renderStats()} {renderSeparator()} )} {variant === "default" && ( <> {!isVersion2024 && renderAttribute( "Saving Throws", CreatureUtils.renderSavingThrows(creature) )} {renderAttribute( "Skills", CreatureUtils.renderSkills(creature, ruleData) )} {renderAttribute( isVersion2024 ? "Vulnerabilities" : "Damage Vulnerabilities", getDamageAdjustment("vulnerability") )} {renderAttribute( isVersion2024 ? "Resistances" : "Damage Resistances", getDamageAdjustment("resistance") )} {renderImmunities()} {renderAttribute("Gear", CreatureUtils.getGear(creature))} {renderAttribute( "Senses", CreatureUtils.renderSensesInfo(creature, ruleData) )} {renderAttribute( "Languages", CreatureUtils.renderLanguages(creature, ruleData) )} )} {renderAttribute( isVersion2024 ? "CR" : "Challenge", CreatureUtils.renderChallengeRatingInfo(creature) )} {variant === "default" && ( <> {!isVersion2024 && renderAttribute( "Proficiency Bonus", CreatureUtils.renderProficiencyBonus(creature) )} {renderSeparator()}
{renderSpecialDescription()} {renderActions()} {renderDescription( "Bonus Actions", CreatureUtils.getBonusActionsDescription(creature) )} {renderDescription( "Reactions", CreatureUtils.getReactionsDescription(creature) )} {renderLegendaryActions()}
)}
); };