391 lines
11 KiB
TypeScript
391 lines
11 KiB
TypeScript
import React, { useCallback, useContext, useMemo, useState } from "react";
|
|
|
|
import {
|
|
CampaignUtils,
|
|
HelperUtils,
|
|
HtmlSelectOption,
|
|
RollResultContract,
|
|
RollValueContract,
|
|
} from "@dndbeyond/character-rules-engine/es";
|
|
import { Platform } from "@dndbeyond/event-pipeline-lib/shared";
|
|
import {
|
|
GameLogContext,
|
|
isMessage,
|
|
useMessageBroker,
|
|
} from "@dndbeyond/game-log-components";
|
|
import { DiceVisibilities } from "@dndbeyond/pocket-dimension-dice/constants";
|
|
import { Context as DiceWorkerContext } from "@dndbeyond/pocket-dimension-dice/context";
|
|
import { instrumentDiceRoll } from "@dndbeyond/pocket-dimension-dice/helpers/bi/instrumentDiceRoll";
|
|
import { rollDice } from "@dndbeyond/pocket-dimension-dice/helpers/rollDice";
|
|
import { useUserDiceDataStore } from "@dndbeyond/pocket-dimension-dice/hooks/useUserDiceDataStore";
|
|
import { RollDicePayload } from "@dndbeyond/pocket-dimension-dice/types";
|
|
|
|
import { useAuth } from "~/contexts/Authentication";
|
|
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
|
|
|
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: RollDicePayload["data"] | Array<RollDicePayload["data"]>;
|
|
}
|
|
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 { env, hasPddErrored, isPddReady } = useContext(DiceWorkerContext);
|
|
const [{ userId }] = useContext(GameLogContext);
|
|
const { campaign } = useCharacterEngine();
|
|
const gameId = campaign ? String(campaign.id) : undefined;
|
|
const dmId = campaign
|
|
? String(CampaignUtils.getDmUserId(campaign))
|
|
: undefined;
|
|
const [groupRolls, setGroupRolls] = useState<
|
|
Record<
|
|
string,
|
|
{
|
|
rollKey: string;
|
|
properties: Partial<RollResultContract>;
|
|
nextRollKey: string | null;
|
|
}
|
|
>
|
|
>({});
|
|
const user = useAuth();
|
|
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<RollDicePayload["data"]>>(() => {
|
|
let requests: Array<RollDicePayload["data"]> = [];
|
|
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: RollDicePayload["data"],
|
|
nextRollKey: string | null
|
|
): void => {
|
|
const diceNotation = `${diceRollRequest.rolls[0].diceNotation.set
|
|
.map((set) => `${set?.count}${set?.dieType}`)
|
|
.join("")}`;
|
|
const { data } = rollDice(
|
|
{
|
|
action: diceRollRequest.action,
|
|
diceNotation,
|
|
entityId: String(diceRollRequest.context?.entityId),
|
|
entityType: String(diceRollRequest.context?.entityType),
|
|
gameId,
|
|
rollType: diceRollRequest.rolls[0].rollType,
|
|
setId: String(
|
|
useUserDiceDataStore.getState().getUserDiceData(Number(userId))
|
|
?.setId
|
|
),
|
|
userId,
|
|
avatarUrl: diceRollRequest.context?.avatarUrl,
|
|
name: diceRollRequest.context?.name,
|
|
operand: diceRollRequest.rolls[0].diceNotation.set[0]?.operand,
|
|
dmId,
|
|
},
|
|
hasPddErrored,
|
|
isPddReady,
|
|
(eventPayload) =>
|
|
instrumentDiceRoll(
|
|
user,
|
|
eventPayload,
|
|
Platform.Web,
|
|
"characterBuilderRollGroup",
|
|
campaign?.id,
|
|
undefined,
|
|
env
|
|
)
|
|
);
|
|
|
|
const rollValues = DiceComponentUtils.generateRollValueContracts(data);
|
|
|
|
if (!rollValues) {
|
|
throw new Error("error in rollResult values");
|
|
}
|
|
|
|
const newGroupRoll = {
|
|
rollKey,
|
|
properties: {
|
|
rollValues,
|
|
rollTotal: data.rolls[0].result?.total,
|
|
},
|
|
nextRollKey,
|
|
};
|
|
|
|
setGroupRolls((prevState) => ({
|
|
...prevState,
|
|
[String(data?.rollId)]: newGroupRoll,
|
|
}));
|
|
|
|
if (
|
|
data.rollId &&
|
|
useUserDiceDataStore.getState().getUserDiceData(Number(userId))
|
|
?.settings.visibility === DiceVisibilities.Disabled
|
|
) {
|
|
//update status
|
|
onSetRollStatus(newGroupRoll.rollKey, DataLoadingStatusEnum.LOADED);
|
|
|
|
//update diceRoll
|
|
onUpdateDiceRoll(
|
|
newGroupRoll.rollKey,
|
|
newGroupRoll.properties,
|
|
newGroupRoll.nextRollKey
|
|
);
|
|
} else {
|
|
onSetRollStatus(rollKey, DataLoadingStatusEnum.LOADING);
|
|
}
|
|
},
|
|
[
|
|
campaign?.id,
|
|
dmId,
|
|
env,
|
|
gameId,
|
|
hasPddErrored,
|
|
isPddReady,
|
|
onSetRollStatus,
|
|
onUpdateDiceRoll,
|
|
user,
|
|
userId,
|
|
]
|
|
);
|
|
|
|
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]);
|
|
|
|
useMessageBroker(
|
|
useCallback(
|
|
(message) => {
|
|
if (
|
|
isMessage(message) &&
|
|
message.eventType === "dice/roll/fulfilled" &&
|
|
groupRolls[message.data.rollId]
|
|
) {
|
|
//update status
|
|
onSetRollStatus(
|
|
groupRolls[message.data.rollId].rollKey,
|
|
DataLoadingStatusEnum.LOADED
|
|
);
|
|
|
|
//update diceRoll
|
|
onUpdateDiceRoll(
|
|
groupRolls[message.data.rollId].rollKey,
|
|
groupRolls[message.data.rollId].properties,
|
|
groupRolls[message.data.rollId].nextRollKey
|
|
);
|
|
}
|
|
},
|
|
[groupRolls, onSetRollStatus, onUpdateDiceRoll]
|
|
)
|
|
);
|
|
|
|
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);
|