Files
dndbeyond_src/ddb_main/tools/js/CharacterSheet/components/Skills/Skills.tsx
T

433 lines
14 KiB
TypeScript

import { orderBy } from "lodash";
import React from "react";
import {
AdvantageIcon,
AdvantageDisadvantageIcon,
DisadvantageIcon,
ProficiencyLevelIcon,
DigitalDiceWrapper,
} from "@dndbeyond/character-components/es";
import {
CharacterTheme,
Constants,
RuleData,
RuleDataUtils,
Skill,
SkillUtils,
ValueLookup,
ValueUtils,
} from "@dndbeyond/character-rules-engine/es";
import { GameLogContext } from "@dndbeyond/game-log-components";
import { RollTypes } from "@dndbeyond/pocket-dimension-dice/constants";
import { RollContext } from "@dndbeyond/pocket-dimension-dice/types";
import { NumberDisplay } from "~/components/NumberDisplay";
import a11yStyles from "~/styles/accessibility.module.css";
interface Props {
skills: Array<Skill>;
customSkills: Array<Skill>;
onCustomSkillClick?: (skill: Skill) => void;
onSkillClick?: (skill: Skill) => void;
onEmptyClick?: () => void;
valueLookup: ValueLookup;
diceEnabled: boolean;
theme: CharacterTheme;
ruleData: RuleData;
rollContext: RollContext;
}
class Skills extends React.PureComponent<Props> {
static defaultProps = {
diceEnabled: false,
};
handleCustomSkillClick = (skill: Skill, evt: React.MouseEvent): void => {
const { onCustomSkillClick } = this.props;
if (onCustomSkillClick) {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
onCustomSkillClick(skill);
}
};
handleEmptyClick = (evt: React.MouseEvent): void => {
const { onEmptyClick } = this.props;
if (onEmptyClick) {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
onEmptyClick();
}
};
handleSkillClick = (skill: Skill, evt: React.MouseEvent): void => {
const { onSkillClick } = this.props;
if (onSkillClick) {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
onSkillClick(skill);
}
};
renderEmptyCustomSkills = (): React.ReactNode => {
return (
<div
className={`ct-skills__additional ct-skills__additional--empty ${
this.props.theme.isDarkMode ? "ct-skills__additional--dark-mode" : ""
}`}
onClick={this.handleEmptyClick}
>
Additional Skills
</div>
);
};
renderCustomSkills = (): React.ReactNode => {
const { customSkills, ruleData, diceEnabled, theme, rollContext } =
this.props;
return customSkills.map((skill) => {
const modifier = SkillUtils.getModifier(skill);
const statName = RuleDataUtils.getAbilityShortName(
SkillUtils.getStat(skill),
ruleData
);
const proficiencyLevel = SkillUtils.getProficiencyLevel(skill);
const name = SkillUtils.getName(skill);
return (
<div
key={skill.id}
className="ct-skills__item"
onClick={this.handleCustomSkillClick.bind(this, skill)}
role="row"
>
<div className="ct-skills__col--proficiency" role="cell">
<ProficiencyLevelIcon
theme={theme}
proficiencyLevel={proficiencyLevel}
/>
</div>
<div
className={`ct-skills__col--stat ${
theme.isDarkMode ? "ct-skills__col--stat--dark-mode" : ""
}`}
role="cell"
>
{statName === null ? "--" : statName}
</div>
<div
className={`ct-skills__col--skill ${
theme.isDarkMode ? "ct-skills__col--skill--dark-mode" : ""
}`}
style={
theme
? {
borderBottomColor: `${theme.themeColor}66`,
}
: undefined
}
role="cell"
>
{name}
</div>
<div
className={"ct-skills__col--modifier"}
style={
theme
? {
borderBottomColor: `${theme.themeColor}66`,
}
: undefined
}
role="cell"
>
{modifier === null ? (
"--"
) : (
<DigitalDiceWrapper
diceNotation={`1d20${
modifier > 0 ? `+${modifier}` : modifier !== 0 ? modifier : ""
}`}
rollType={RollTypes.Check}
action={name ?? "UNKNOWN"}
isDiceEnabled={diceEnabled}
entityId={String(rollContext.entityId)}
entityType={String(rollContext.entityType)}
name={String(rollContext.name)}
>
<NumberDisplay number={modifier} type="signed" />
</DigitalDiceWrapper>
)}
</div>
</div>
);
});
};
render() {
const {
skills,
valueLookup,
customSkills,
diceEnabled,
ruleData,
theme,
rollContext,
} = this.props;
let orderedSkills = orderBy(skills, (skill) => SkillUtils.getName(skill));
return (
<div
className={`ct-skills ${
theme.isDarkMode ? "ct-skills--dark-mode" : ""
}`}
role="table"
>
<div className="ct-skills__header" role="row">
<div className="ct-skills__col--proficiency" role="columnheader">
<span className={a11yStyles.screenreaderOnly}>Proficiency</span>
<abbr
aria-hidden="true"
title="Proficiency"
className={`ct-skills__heading ${
theme.isDarkMode ? "ct-skills__heading--dark-mode" : ""
}`}
>
Prof
</abbr>
</div>
<div className="ct-skills__col--stat" role="columnheader">
<span className={a11yStyles.screenreaderOnly}>Modifier</span>
<abbr
aria-hidden="true"
title="Modifier"
className={`ct-skills__heading ${
theme.isDarkMode ? "ct-skills__heading--dark-mode" : ""
}`}
>
Mod
</abbr>
</div>
<div className="ct-skills__col--skill" role="columnheader">
<span
className={`ct-skills__heading ${
theme.isDarkMode ? "ct-skills__heading--dark-mode" : ""
}`}
>
Skill
</span>
</div>
<div className="ct-skills__col--modifier" role="columnheader">
<span
className={`ct-skills__heading ${
theme.isDarkMode ? "ct-skills__heading--dark-mode" : ""
}`}
>
Bonus
</span>
</div>
</div>
<div
className="ct-skills__list"
role="rowgroup"
data-scrollable-container="true"
>
{orderedSkills.map((skill) => {
let valueTypes: Array<Constants.AdjustmentTypeEnum> = [
Constants.AdjustmentTypeEnum.SKILL_STAT_OVERRIDE,
Constants.AdjustmentTypeEnum.SKILL_PROFICIENCY_LEVEL,
Constants.AdjustmentTypeEnum.SKILL_MISC_BONUS,
Constants.AdjustmentTypeEnum.SKILL_MAGIC_BONUS,
Constants.AdjustmentTypeEnum.SKILL_OVERRIDE,
];
let valueDataLookup = ValueUtils.getDataLookup(
valueLookup,
valueTypes,
ValueUtils.hack__toString(SkillUtils.getId(skill)),
ValueUtils.hack__toString(SkillUtils.getEntityTypeId(skill))
);
let hasModifiedLevel = valueDataLookup.hasOwnProperty(
Constants.AdjustmentTypeEnum.SKILL_PROFICIENCY_LEVEL
);
let hasModifiedValue =
valueDataLookup.hasOwnProperty(
Constants.AdjustmentTypeEnum.SKILL_OVERRIDE
) ||
valueDataLookup.hasOwnProperty(
Constants.AdjustmentTypeEnum.SKILL_MAGIC_BONUS
) ||
valueDataLookup.hasOwnProperty(
Constants.AdjustmentTypeEnum.SKILL_MISC_BONUS
);
let statClassNames: Array<string> = ["ct-skills__col--stat"];
if (
valueDataLookup.hasOwnProperty(
Constants.AdjustmentTypeEnum.SKILL_STAT_OVERRIDE
)
) {
statClassNames.push("ct-skills__col--stat-modified");
}
let modifierClassNames: Array<string> = [
"ct-skills__col--modifier",
];
if (hasModifiedValue) {
modifierClassNames.push("ct-skills__col--modifier-modified");
}
if (theme.isDarkMode) {
statClassNames.push("ct-skills__col--stat--dark-mode");
modifierClassNames.push("ct-skills__col--modifier--dark-mode");
}
let statName = RuleDataUtils.getAbilityShortName(
SkillUtils.getStat(skill),
ruleData
);
let modifier = SkillUtils.getModifier(skill);
let name = SkillUtils.getName(skill);
let usableDiceAdjustmentType =
SkillUtils.getUsableDiceAdjustmentType(skill);
let adjustmentsNode: React.ReactNode;
if (
usableDiceAdjustmentType !==
Constants.UsableDiceAdjustmentTypeEnum.NONE
) {
let iconNode: React.ReactNode;
let iconClassName: string = "ct-skills__adjustment";
switch (usableDiceAdjustmentType) {
case Constants.UsableDiceAdjustmentTypeEnum.ADVANTAGE:
iconNode = (
<AdvantageIcon theme={theme} className={iconClassName} />
);
break;
case Constants.UsableDiceAdjustmentTypeEnum.DISADVANTAGE:
iconNode = (
<DisadvantageIcon theme={theme} className={iconClassName} />
);
break;
case Constants.UsableDiceAdjustmentTypeEnum
.ADVANTAGE_DISADVANTAGE:
iconNode = (
<AdvantageDisadvantageIcon
title={"Both (See Sidebar)"}
theme={theme}
className={iconClassName}
/>
);
break;
default:
// not implemented
}
if (iconNode) {
adjustmentsNode = (
<div
className="ct-skills__col--adjustments"
style={
theme
? {
borderBottomColor: `${theme.themeColor}66`,
}
: undefined
}
>
{iconNode}
</div>
);
}
}
return (
<div
key={SkillUtils.getId(skill)}
className="ct-skills__item"
onClick={this.handleSkillClick.bind(this, skill)}
role="row"
>
<div className="ct-skills__col--proficiency" role="cell">
<ProficiencyLevelIcon
theme={theme}
isModified={hasModifiedLevel}
proficiencyLevel={SkillUtils.getProficiencyLevel(skill)}
/>
</div>
<div className={statClassNames.join(" ")} role="cell">
{statName === null ? "--" : statName}
</div>
<div
className={`ct-skills__col--skill ${
theme.isDarkMode ? "ct-skills__col--skill--dark-mode" : ""
}`}
style={
theme?.isDarkMode
? {
borderColor: `${theme.themeColor}66`,
}
: undefined
}
role="cell"
>
{name}
</div>
{adjustmentsNode}
<div
className={modifierClassNames.join(" ")}
style={
theme?.isDarkMode
? {
borderColor: `${theme.themeColor}66`,
}
: undefined
}
role="cell"
>
{modifier === null ? (
"--"
) : (
<DigitalDiceWrapper
diceNotation={`1d20${
modifier > 0
? `+${modifier}`
: modifier !== 0
? modifier
: ""
}`}
rollType={RollTypes.Check}
action={name ? name : "UNKNOWN"}
isDiceEnabled={diceEnabled}
entityId={String(rollContext.entityId)}
entityType={String(rollContext.entityType)}
name={String(rollContext.name)}
>
<NumberDisplay
number={modifier}
type="signed"
isModified={hasModifiedValue}
/>
</DigitalDiceWrapper>
)}
</div>
</div>
);
})}
{customSkills.length
? this.renderCustomSkills()
: this.renderEmptyCustomSkills()}
</div>
</div>
);
}
}
Skills.contextType = GameLogContext;
export default Skills;