2025-05-28 15:36:51 -07:00

290 lines
7.8 KiB
TypeScript

import React, { useCallback, useMemo } from "react";
import {
HelperUtils,
HtmlSelectOption,
RollResultContract,
RollValueContract,
} from "@dndbeyond/character-rules-engine/es";
import { Dice, RollRequest } from "@dndbeyond/dice";
import { DataLoadingStatusEnum } from "../componentConstants";
import { Button, RemoveButton } from "../legacy";
import { TypeScriptUtils, DiceComponentUtils } from "../utils";
import { DiceRoll, DiceRollProps } from "./DiceRoll";
export interface DiceRollGroupProps {
className: string;
componentKey: string;
diceRolls: Array<RollResultContract>;
exclusiveOptions?: boolean;
confirmButtonText?: string;
onConfirm?: (
groupKey: string,
rollResults: Array<RollResultContract>
) => void;
onUpdateGroup?: (
groupKey: string,
rollResults: Array<RollResultContract>
) => void;
onRollError?: (errorMessage: string) => void;
groupKey: string;
nextGroupKey: string | null;
showRemoveButton?: boolean;
onRemoveGroup?: (groupKey: string, nextGroupKey: string | null) => void;
onResetGroup?: (
groupKey: string,
rollResults: Array<RollResultContract>
) => void;
onUpdateDiceRoll: (
rollKey: string,
properties: Omit<Partial<RollResultContract>, "rollKey">,
nextRollKey: string | null
) => void;
onSetRollStatus: (rollKey: string, status: DataLoadingStatusEnum) => void;
rollStatusLookup: Record<string, DataLoadingStatusEnum>;
options?: DiceRollProps["options"];
diceRollRequest:
| DiceRollProps["diceRollRequest"]
| Array<DiceRollProps["diceRollRequest"]>;
}
const DiceRollGroup: React.FunctionComponent<DiceRollGroupProps> = ({
className = "",
componentKey,
diceRolls,
options,
confirmButtonText = "Confirm",
showRemoveButton = false,
exclusiveOptions = false,
diceRollRequest,
groupKey,
nextGroupKey,
onUpdateGroup,
onRemoveGroup,
onRollError,
onConfirm,
onResetGroup,
onSetRollStatus,
onUpdateDiceRoll,
rollStatusLookup,
}) => {
const classNames = useMemo<Array<string>>(
() => ["ddbc-dice-roll-group", className],
[className]
);
const areRollsEmpty = useMemo<boolean>(
() => !diceRolls.some((roll) => roll.rollTotal !== null),
[diceRolls]
);
const areScoresUnassigned = useMemo<boolean>(
() =>
!diceRolls.some(
(roll) => roll.assignedValue !== null && roll.assignedValue !== ""
),
[diceRolls]
);
const assignedValues = useMemo<Array<string>>(() => {
return diceRolls
.map((roll) => roll.assignedValue)
.filter(TypeScriptUtils.isNotNullOrUndefined);
}, [diceRolls]);
const generatedOptions = useMemo<Array<Array<HtmlSelectOption>>>(() => {
if (!options) {
return [];
}
return diceRolls.map((roll): Array<HtmlSelectOption> => {
if (!exclusiveOptions) {
return options;
}
return options.filter(
(option) =>
roll.assignedValue === option.value ||
!assignedValues.includes(option.value.toString())
);
});
}, [options, diceRolls, exclusiveOptions, assignedValues]);
const generatedRollRequests = useMemo<Array<RollRequest>>(() => {
let requests: Array<RollRequest> = [];
for (let i = 0; i < diceRolls.length; i++) {
if (!Array.isArray(diceRollRequest)) {
requests.push(diceRollRequest);
} else {
if (diceRollRequest[i]) {
requests.push(diceRollRequest[i]);
} else {
requests.push(diceRollRequest[0]);
}
}
}
return requests;
}, [diceRollRequest, diceRolls]);
const handleRoll = useCallback(
(
rollKey: string,
diceRollRequest: RollRequest,
nextRollKey: string | null
): void => {
onSetRollStatus(rollKey, DataLoadingStatusEnum.LOADING);
Dice.roll(diceRollRequest)
.then((result) => {
//RollResult | undefined - RollResult is not exported from Dice;
// only expect to have one single rollRequest.
const rollResult = result.rolls[0].result ?? null;
if (!rollResult) {
throw new Error("error in rollResult");
}
const rollValues =
DiceComponentUtils.generateRollValueContracts(result);
if (!rollValues) {
throw new Error("error in rollResult values");
}
//update status
onSetRollStatus(rollKey, DataLoadingStatusEnum.LOADED);
//update diceRoll
onUpdateDiceRoll(
rollKey,
{ rollValues, rollTotal: rollResult.total },
nextRollKey
);
})
.catch((error: Error) => {
//this could handle different errors types from dice package or thrown errors from result
onSetRollStatus(rollKey, DataLoadingStatusEnum.NOT_LOADED);
if (onRollError) {
onRollError(error.message);
}
console.log("roll error", error);
});
},
[]
);
const handleChange = useCallback(
(
rollKey: string,
newValue: string | null,
prevValue: string | null,
nextRollKey: string | null,
rollTotal: number | null,
rollValues: Array<RollValueContract>
): void => {
if (newValue === prevValue) {
return;
}
onUpdateDiceRoll(
rollKey,
{ assignedValue: newValue, rollTotal, rollValues },
nextRollKey
);
},
[]
);
const handleConfirm = useCallback((): void => {
if (onConfirm) {
return onConfirm(groupKey, diceRolls);
}
}, [onConfirm, groupKey, diceRolls]);
const handleResetGroup = useCallback((): void => {
if (onResetGroup) {
onResetGroup(groupKey, diceRolls);
}
diceRolls.forEach((roll) => {
onSetRollStatus(roll.rollKey, DataLoadingStatusEnum.NOT_INITIALIZED);
});
}, [onResetGroup, groupKey]);
const handleRemoveGroup = useCallback((): void => {
if (onRemoveGroup) {
onRemoveGroup(groupKey, nextGroupKey);
}
}, [onRemoveGroup, groupKey, nextGroupKey]);
return (
<div className={classNames.join(" ")}>
<div className="ddbc-dice-roll-group__actions">
{showRemoveButton && (
<RemoveButton
size="small"
onClick={handleRemoveGroup}
enableConfirm={true}
>
Delete group
</RemoveButton>
)}
</div>
<div className="ddbc-dice-roll-group__rolls">
{diceRolls.map((diceRoll, idx) => {
const diceRollRequest = generatedRollRequests[idx];
if (!diceRollRequest) {
return null;
}
const rollStatus = HelperUtils.lookupDataOrFallback(
rollStatusLookup,
diceRoll.rollKey,
DataLoadingStatusEnum.NOT_INITIALIZED
);
return (
<DiceRoll
key={diceRoll.rollKey}
rollKey={diceRoll.rollKey}
nextRollKey={diceRoll.nextRollKey}
rollTotal={diceRoll.rollTotal}
rollValues={diceRoll.rollValues}
assignedValue={diceRoll.assignedValue}
options={generatedOptions[idx]}
onChange={handleChange}
onRoll={handleRoll}
diceRollRequest={diceRollRequest}
rollStatus={rollStatus}
/>
);
})}
</div>
<div className="ddbc-dice-roll-group__actions">
<RemoveButton
size="medium"
style="filled"
onClick={handleResetGroup}
disabled={areRollsEmpty}
enableConfirm={true}
>
Reset Group
</RemoveButton>
{onConfirm && (
<Button
size="medium"
onClick={handleConfirm}
disabled={areScoresUnassigned}
>
{confirmButtonText}
</Button>
)}
</div>
</div>
);
};
export default React.memo(DiceRollGroup);