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; exclusiveOptions?: boolean; confirmButtonText?: string; onConfirm?: ( groupKey: string, rollResults: Array ) => void; onUpdateGroup?: ( groupKey: string, rollResults: Array ) => void; onRollError?: (errorMessage: string) => void; groupKey: string; nextGroupKey: string | null; showRemoveButton?: boolean; onRemoveGroup?: (groupKey: string, nextGroupKey: string | null) => void; onResetGroup?: ( groupKey: string, rollResults: Array ) => void; onUpdateDiceRoll: ( rollKey: string, properties: Omit, "rollKey">, nextRollKey: string | null ) => void; onSetRollStatus: (rollKey: string, status: DataLoadingStatusEnum) => void; rollStatusLookup: Record; options?: DiceRollProps["options"]; diceRollRequest: RollDicePayload["data"] | Array; } const DiceRollGroup: React.FunctionComponent = ({ 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; nextRollKey: string | null; } > >({}); const user = useAuth(); const classNames = useMemo>( () => ["ddbc-dice-roll-group", className], [className] ); const areRollsEmpty = useMemo( () => !diceRolls.some((roll) => roll.rollTotal !== null), [diceRolls] ); const areScoresUnassigned = useMemo( () => !diceRolls.some( (roll) => roll.assignedValue !== null && roll.assignedValue !== "" ), [diceRolls] ); const assignedValues = useMemo>(() => { return diceRolls .map((roll) => roll.assignedValue) .filter(TypeScriptUtils.isNotNullOrUndefined); }, [diceRolls]); const generatedOptions = useMemo>>(() => { if (!options) { return []; } return diceRolls.map((roll): Array => { if (!exclusiveOptions) { return options; } return options.filter( (option) => roll.assignedValue === option.value || !assignedValues.includes(option.value.toString()) ); }); }, [options, diceRolls, exclusiveOptions, assignedValues]); const generatedRollRequests = useMemo>(() => { let requests: Array = []; 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 ): 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 (
{showRemoveButton && ( Delete group )}
{diceRolls.map((diceRoll, idx) => { const diceRollRequest = generatedRollRequests[idx]; if (!diceRollRequest) { return null; } const rollStatus = HelperUtils.lookupDataOrFallback( rollStatusLookup, diceRoll.rollKey, DataLoadingStatusEnum.NOT_INITIALIZED ); return ( ); })}
Reset Group {onConfirm && ( )}
); }; export default React.memo(DiceRollGroup);