import Difficulty from "@rbxts/ejt";

export default function getDifficultyDisplayColors(difficulty?: Difficulty): { background: Color3; text: Color3 } {
    const color = difficulty?.color ?? new Color3();

    const background = new Color3(
        math.clamp(color.R, 0.1, 0.9),
        math.clamp(color.G, 0.1, 0.9),
        math.clamp(color.B, 0.1, 0.9),
    );
    const text = color.Lerp(new Color3(1, 1, 1), 0.5) ?? new Color3(1, 1, 1);

    return { background, text };
}


import React, { ReactNode, RefObject, useEffect, useRef, useState } from "@rbxts/react";
import { TweenService } from "@rbxts/services";
import DocumentManager from "client/components/window/DocumentManager";
import WindowCloseButton from "client/components/window/WindowCloseButton";
import WindowTitle from "client/components/window/WindowTitle";
import { playSound } from "shared/asset/GameAssets";

export function useWindowAnimation({
    frameRef,
    contentRef,
    titleRef,
    iconRef,
    textRef,
    closeButtonRef,
    initialPosition,
    visible,
}: {
    frameRef: RefObject<GuiObject>;
    contentRef: RefObject<GuiObject>;
    titleRef?: RefObject<GuiObject>;
    iconRef?: RefObject<ImageLabel>;
    textRef?: RefObject<TextLabel>;
    closeButtonRef?: RefObject<TextButton>;
    initialPosition: UDim2;
    visible: boolean;
}) {
    const [previousVisible, setPreviousVisible] = useState(visible);

    useEffect(() => {
        const action = visible && !previousVisible ? "open" : !visible && previousVisible ? "close" : undefined;
        // Handle animation
        if (action) {
            const frame = frameRef.current!;
            const content = contentRef.current;
            const title = titleRef?.current;
            const icon = iconRef?.current;
            const text = textRef?.current;
            const closeButton = closeButtonRef?.current;

            if (action === "open") {
                frame.Visible = true;
                playSound("MenuOpen.mp3");
            } else {
                playSound("MenuClose.mp3");
            }

            const middle = initialPosition;
            const below = middle.sub(new UDim2(0, 0, 0, 30));
            frame.Position = action === "open" ? below : middle;

            const tweenInfo =
                action === "open"
                    ? new TweenInfo(0.4, Enum.EasingStyle.Back, Enum.EasingDirection.Out)
                    : new TweenInfo(0.1, Enum.EasingStyle.Linear);
            const tween = TweenService.Create(frame, tweenInfo, {
                Position: action === "open" ? middle : below,
            });

            // Animate title with layered effect
            if (title) {
                const titleInitialPos = new UDim2(0, 12, 0, 0);
                const titleOffset = titleInitialPos.add(new UDim2(0, 0, 0, -8));
                title.Position = action === "open" ? titleOffset : titleInitialPos;

                const titleTweenInfo =
                    action === "open"
                        ? new TweenInfo(0.5, Enum.EasingStyle.Back, Enum.EasingDirection.Out)
                        : new TweenInfo(0.075, Enum.EasingStyle.Linear);

                const titleTween = TweenService.Create(title, titleTweenInfo, {
                    Position: action === "open" ? titleInitialPos : titleOffset,
                });
                titleTween.Play();
            }

            // Animate icon with rotation
            if (icon) {
                icon.Rotation = action === "open" ? -90 : 0;

                const iconTweenInfo =
                    action === "open"
                        ? new TweenInfo(0.5, Enum.EasingStyle.Back, Enum.EasingDirection.Out)
                        : new TweenInfo(0.1, Enum.EasingStyle.Linear);

                const iconTween = TweenService.Create(icon, iconTweenInfo, {
                    Rotation: action === "open" ? 0 : -90,
                });
                iconTween.Play();
            }

            // Animate title text with slight rotation
            if (text) {
                text.Rotation = action === "open" ? -5 : 0;

                const textTweenInfo =
                    action === "open"
                        ? new TweenInfo(0.45, Enum.EasingStyle.Back, Enum.EasingDirection.Out)
                        : new TweenInfo(0.1, Enum.EasingStyle.Linear);

                const textTween = TweenService.Create(text, textTweenInfo, {
                    Rotation: action === "open" ? 0 : -5,
                });
                textTween.Play();
            }

            // Animate close button with rotation
            if (closeButton) {
                closeButton.Rotation = action === "open" ? 90 : 0;

                const closeButtonTweenInfo =
                    action === "open"
                        ? new TweenInfo(0.5, Enum.EasingStyle.Back, Enum.EasingDirection.Out)
                        : new TweenInfo(0.1, Enum.EasingStyle.Linear);

                const closeButtonTween = TweenService.Create(closeButton, closeButtonTweenInfo, {
                    Rotation: action === "open" ? 0 : 90,
                });
                closeButtonTween.Play();
            }

            // Animate content with layered effect
            if (content) {
                const contentInitialPos = new UDim2(0.5, 0, 0.075, 0);
                const contentOffset = contentInitialPos.add(new UDim2(0, 0, 0, -10));
                content.Position = action === "open" ? contentOffset : contentInitialPos;

                const contentTweenInfo =
                    action === "open"
                        ? new TweenInfo(0.4, Enum.EasingStyle.Back, Enum.EasingDirection.Out)
                        : new TweenInfo(0.1, Enum.EasingStyle.Linear);

                const contentTween = TweenService.Create(content, contentTweenInfo, {
                    Position: action === "open" ? contentInitialPos : contentOffset,
                });
                contentTween.Play();
            }

            tween.Play();
            tween.Completed.Connect(() => {
                frame.Visible = visible;
            });
        }
        setPreviousVisible(visible);
    }, [visible]);
}

export default function BasicWindow({
    icon,
    id,
    title,
    visible,
    children,
    backgroundColor,
    strokeColor,
}: {
    icon: string;
    id: string;
    title?: string;
    visible: boolean;
    backgroundColor?: ColorSequence;
    strokeColor: ColorSequence;
    children: ReactNode;
    size?: UDim2;
}) {
    if (backgroundColor === undefined) {
        const keypoints = new Array<ColorSequenceKeypoint>();
        for (const keypoint of strokeColor.Keypoints) {
            keypoints.push(new ColorSequenceKeypoint(keypoint.Time, keypoint.Value.Lerp(Color3.fromRGB(0, 0, 0), 0.3)));
        }
        backgroundColor = new ColorSequence(keypoints);
    }
    const frameRef = useRef<Frame>();
    const contentRef = useRef<Frame>();
    const titleRef = useRef<Frame>();
    const iconRef = useRef<ImageLabel>();
    const textRef = useRef<TextLabel>();
    const closeButtonRef = useRef<TextButton>();
    const initialPosition = new UDim2(0.5, 0, 1, -40);

    useWindowAnimation({
        frameRef,
        contentRef,
        titleRef,
        iconRef,
        textRef,
        closeButtonRef,
        initialPosition,
        visible,
    });

    return (
        <frame
            ref={frameRef}
            AnchorPoint={new Vector2(0.5, 1)}
            BackgroundColor3={Color3.fromRGB(255, 255, 255)}
            BackgroundTransparency={0.6}
            BorderColor3={Color3.fromRGB(0, 0, 0)}
            BorderSizePixel={4}
            Position={initialPosition}
            Size={new UDim2(0.45, 200, 0.4, 100)}
            ZIndex={0}
            Visible={false}
        >
            <WindowTitle ref={titleRef} iconRef={iconRef} textRef={textRef} icon={icon} title={title ?? id} />
            <WindowCloseButton ref={closeButtonRef} onClick={() => DocumentManager.setVisible(id, false)} />
            <frame
                ref={contentRef}
                key="MainWindow"
                AnchorPoint={new Vector2(0.5, 0)}
                BackgroundTransparency={1}
                Position={new UDim2(0.5, 0, 0.075, 0)}
                Size={new UDim2(1, -30, 0.925, -15)}
            >
                {children}
            </frame>
            <uistroke ApplyStrokeMode={Enum.ApplyStrokeMode.Border} Color={Color3.fromRGB(255, 255, 255)} Thickness={2}>
                <uigradient Color={strokeColor} Rotation={90} />
            </uistroke>
            <uigradient Color={backgroundColor} Rotation={90} />
        </frame>
    );
}


import React, { StrictMode } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import BasicWindow from "client/components/window/BasicWindow";
import DocumentManager, { useDocument } from "client/components/window/DocumentManager";
import { getAsset } from "shared/asset/AssetMap";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
    },
    () => {
        const { id: id1, visible: visible1, setVisible: setVisible1 } = useDocument({ id: "Test1", priority: 1 });
        const { id: id2, visible: visible2, setVisible: setVisible2 } = useDocument({ id: "Test2", priority: 5 });
        const { id: id3, visible: visible3, setVisible: setVisible3 } = useDocument({ id: "Test3", priority: 0 });

        return (
            <StrictMode>
                {/* Control buttons */}
                <frame
                    AnchorPoint={new Vector2(0.5, 0)}
                    Position={new UDim2(0.5, 0, 0, 10)}
                    Size={new UDim2(0, 300, 0, 60)}
                    BackgroundColor3={Color3.fromRGB(50, 50, 50)}
                >
                    <uilistlayout
                        FillDirection={Enum.FillDirection.Horizontal}
                        HorizontalAlignment={Enum.HorizontalAlignment.Center}
                        VerticalAlignment={Enum.VerticalAlignment.Center}
                        Padding={new UDim(0, 10)}
                    />

                    <textbutton
                        Size={new UDim2(0, 80, 0, 40)}
                        Text="Window 1"
                        Event={{ Activated: () => setVisible1(!visible1) }}
                    />

                    <textbutton
                        Size={new UDim2(0, 80, 0, 40)}
                        Text="Window 2"
                        Event={{ Activated: () => setVisible2(!visible2) }}
                    />

                    <textbutton
                        Size={new UDim2(0, 80, 0, 40)}
                        Text="Window 3"
                        Event={{ Activated: () => setVisible3(!visible3) }}
                    />

                    <textbutton
                        Size={new UDim2(0, 80, 0, 40)}
                        Text="Window 3"
                        Event={{ Activated: () => DocumentManager.toggle("Low Priority Window") }}
                    />
                </frame>

                {/* Test windows with different priorities */}
                <BasicWindow
                    icon={getAsset("assets/Settings.png")}
                    id={id1}
                    title="Normal Priority Window"
                    strokeColor={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(100, 150, 255)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(50, 100, 200)),
                        ])
                    }
                    visible={visible1}
                >
                    <textlabel
                        BackgroundTransparency={1}
                        Size={new UDim2(1, 0, 1, 0)}
                        Text="This is a normal priority window. Press X to close the highest priority visible window."
                        TextWrapped={true}
                        TextScaled={true}
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                    />
                </BasicWindow>

                <BasicWindow
                    icon={getAsset("assets/Quests.png")}
                    id={id2}
                    title="High Priority Window"
                    strokeColor={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(255, 150, 100)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(200, 100, 50)),
                        ])
                    }
                    visible={visible2}
                >
                    <textlabel
                        BackgroundTransparency={1}
                        Size={new UDim2(1, 0, 1, 0)}
                        Text="This is a HIGH priority window. When multiple windows are open, X will close this one first."
                        TextWrapped={true}
                        TextScaled={true}
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                    />
                </BasicWindow>

                <BasicWindow
                    icon={getAsset("assets/Inventory.png")}
                    id={id3}
                    title="Low Priority Window"
                    strokeColor={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(150, 255, 150)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(100, 200, 100)),
                        ])
                    }
                    visible={visible3}
                >
                    <textlabel
                        BackgroundTransparency={1}
                        Size={new UDim2(1, 0, 1, 0)}
                        Text="This is a low priority window. X will close this window last."
                        TextWrapped={true}
                        TextScaled={true}
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                    />
                </BasicWindow>
            </StrictMode>
        );
    },
);


import React, { InstanceProps, useMemo } from "@rbxts/react";
import { RobotoSlabBold } from "shared/asset/GameFonts";

export default function ProgressBar(props: {
    current: number;
    max: number;
    text?: string;
    colorSequence: ColorSequence;
    fontFace?: Font;
    frameProps: InstanceProps<Frame>;
}) {
    const tintedColorSequence = useMemo(() => {
        const keypoints = new Array<ColorSequenceKeypoint>();
        const size = props.colorSequence.Keypoints.size();
        for (let i = 0; i < size; i++) {
            const keypoint = props.colorSequence.Keypoints[i];
            const alpha = 1 - (i + 1) / size;
            keypoints.push(
                new ColorSequenceKeypoint(keypoint.Time, keypoint.Value.Lerp(Color3.fromRGB(255, 255, 255), alpha)),
            );
        }
        return new ColorSequence(keypoints);
    }, [props.colorSequence]);

    return (
        <frame
            key="ProgressBar"
            BackgroundColor3={Color3.fromRGB(39, 39, 39)}
            BorderColor3={Color3.fromRGB(0, 0, 0)}
            BorderSizePixel={5}
            {...props.frameProps}
        >
            <textlabel
                key="BarLabel"
                AnchorPoint={new Vector2(0.5, 0.5)}
                BackgroundTransparency={1}
                FontFace={props.fontFace ?? RobotoSlabBold}
                Position={new UDim2(0.5, 0, 0.5, 0)}
                Size={new UDim2(0.8, 0, 0.8, 0)}
                Text={props.text ?? `${props.current}/${props.max}`}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                ZIndex={2}
            >
                <uistroke Thickness={2} />
            </textlabel>

            <frame
                key="Fill"
                BackgroundColor3={Color3.fromRGB(255, 255, 255)}
                BorderSizePixel={0}
                Size={new UDim2(math.clamp(props.current / props.max, 0, 1), 0, 1, 0)}
                Visible={props.current > 0}
            >
                <uigradient Color={props.colorSequence} Rotation={90} />
                <uistroke Color={Color3.fromRGB(255, 255, 255)} Thickness={3}>
                    <uigradient Color={tintedColorSequence} Rotation={90} />
                </uistroke>
            </frame>

            <uistroke ApplyStrokeMode={Enum.ApplyStrokeMode.Border} Color={Color3.fromRGB(48, 48, 48)} Thickness={3} />
        </frame>
    );
}


import React, { ReactNode, useRef } from "@rbxts/react";
import { useWindowAnimation } from "client/components/window/BasicWindow";
import DocumentManager from "client/components/window/DocumentManager";
import WindowCloseButton from "client/components/window/WindowCloseButton";
import WindowTitle from "client/components/window/WindowTitle";

export default function TechWindow({
    icon,
    id,
    title,
    visible,
    children,
    size = new UDim2(0.9, 0, 0.9, -50),
}: {
    icon: string;
    id: string;
    title?: string;
    visible: boolean;
    children: ReactNode;
    size?: UDim2;
}) {
    const frameRef = useRef<Frame>();
    const contentRef = useRef<Frame>();
    const titleRef = useRef<Frame>();
    const iconRef = useRef<ImageLabel>();
    const textRef = useRef<TextLabel>();
    const closeButtonRef = useRef<TextButton>();
    const initialPosition = new UDim2(0.5, 0, 0.5, 0);

    useWindowAnimation({
        frameRef,
        contentRef,
        titleRef,
        iconRef,
        textRef,
        closeButtonRef,
        initialPosition,
        visible,
    });

    return (
        <frame
            ref={frameRef}
            Active={true}
            AnchorPoint={new Vector2(0.5, 0.5)}
            BackgroundColor3={Color3.fromRGB(13, 13, 13)}
            BorderColor3={Color3.fromRGB(0, 0, 0)}
            BorderSizePixel={4}
            Selectable={true}
            Size={size}
            Position={initialPosition}
            ZIndex={0}
            Visible={false}
        >
            <uistroke ApplyStrokeMode={Enum.ApplyStrokeMode.Border} Color={Color3.fromRGB(255, 255, 255)} />
            <uisizeconstraint MaxSize={new Vector2(800, 600)} />
            <WindowTitle iconRef={iconRef} textRef={textRef} ref={titleRef} icon={icon} title={title ?? id} />
            <WindowCloseButton ref={closeButtonRef} onClick={() => DocumentManager.setVisible(id, false)} />
            <frame
                ref={contentRef}
                key="MainWindow"
                AnchorPoint={new Vector2(0.5, 0)}
                BackgroundTransparency={1}
                Position={new UDim2(0.5, 0, 0.075, 0)}
                Size={new UDim2(1, -30, 0.925, -15)}
            >
                {children}
            </frame>
        </frame>
    );
}


import React, { forwardRef, useRef } from "@rbxts/react";
import { TweenService } from "@rbxts/services";
import { useTooltipProps } from "client/components/tooltip/TooltipManager";
import { getAsset } from "shared/asset/AssetMap";

interface WindowCloseButtonProps {
    onClick: () => void;
    color?: Color3;
}

const WindowCloseButton = forwardRef<TextButton, WindowCloseButtonProps>(
    ({ onClick, color = Color3.fromRGB(255, 76, 76) }, ref) => {
        const closeButtonRef = useRef<TextButton>();

        const handleEnter = () =>
            TweenService.Create(closeButtonRef.current!, new TweenInfo(0.1), {
                BackgroundColor3: color.Lerp(new Color3(1, 1, 1), 0.5),
                Rotation: 5,
            }).Play();

        const handleLeave = () =>
            TweenService.Create(closeButtonRef.current!, new TweenInfo(0.1), {
                BackgroundColor3: color,
                Rotation: 0,
            }).Play();

        const tooltipProps = useTooltipProps({
            data: { message: "Close Window" },
            onEnter: handleEnter,
            onLeave: handleLeave,
        });

        return (
            <textbutton
                key="CloseButton"
                ref={(node) => {
                    closeButtonRef.current = node;
                    if (ref) {
                        if (typeIs(ref, "function")) {
                            ref(node);
                        } else {
                            (ref as React.MutableRefObject<TextButton | undefined>).current = node;
                        }
                    }
                }}
                AnchorPoint={new Vector2(0.5, 0.5)}
                AutoButtonColor={false}
                BackgroundColor3={color}
                BorderColor3={Color3.fromRGB(45, 45, 45)}
                BorderSizePixel={3}
                Event={{
                    ...tooltipProps.events,
                    Activated: onClick,
                }}
                Position={new UDim2(1, -10, 0, 10)}
                Size={new UDim2(0, 30, 0, 30)}
                Text={""}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextSize={14}
                TextStrokeTransparency={0}
                TextWrapped={true}
                ZIndex={104}
            >
                <uiaspectratioconstraint />
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                            new ColorSequenceKeypoint(0.467, Color3.fromRGB(206, 206, 206)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(173, 0, 0)),
                        ])
                    }
                    Rotation={90}
                />
                <imagelabel
                    AnchorPoint={new Vector2(0.5, 0.5)}
                    BackgroundTransparency={1}
                    Image={getAsset("assets/Mul.png")}
                    Position={new UDim2(0.5, 0, 0.5, 0)}
                    Size={new UDim2(1, -10, 1, -10)}
                />
                <uistroke
                    ApplyStrokeMode={Enum.ApplyStrokeMode.Border}
                    Color={Color3.fromRGB(255, 15, 15)}
                    Thickness={2}
                >
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(170, 170, 170)),
                                new ColorSequenceKeypoint(0.5, Color3.fromRGB(171, 171, 171)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(68, 68, 68)),
                            ])
                        }
                        Rotation={90}
                    />
                </uistroke>
            </textbutton>
        );
    },
);

export default WindowCloseButton;


import React, { forwardRef, Ref } from "@rbxts/react";
import { RobotoSlabHeavy } from "shared/asset/GameFonts";

interface WindowTitleProps {
    icon: string;
    title: string;
    font?: Font;
    iconRef?: Ref<ImageLabel>;
    textRef?: Ref<TextLabel>;
}

const WindowTitle = forwardRef<Frame, WindowTitleProps>(
    ({ icon, title, font = RobotoSlabHeavy, iconRef, textRef }, ref) => {
        return (
            <frame
                ref={ref}
                key="Title"
                AnchorPoint={new Vector2(0, 0.5)}
                AutomaticSize={Enum.AutomaticSize.X}
                BackgroundTransparency={1}
                Position={new UDim2(0, 12, 0, 5)}
                Size={new UDim2(0, 0, 0.04, 30)}
            >
                <frame
                    BackgroundTransparency={1}
                    Size={new UDim2(1, 0, 1, 0)}
                    SizeConstraint={Enum.SizeConstraint.RelativeYY}
                >
                    <imagelabel
                        ref={iconRef}
                        AnchorPoint={new Vector2(0.5, 0.5)}
                        BackgroundTransparency={1}
                        Image={icon}
                        Position={new UDim2(0.5, 0, 0.5, 0)}
                        ScaleType={Enum.ScaleType.Fit}
                        Size={new UDim2(1, 0, 1, 0)}
                        ZIndex={2}
                    />
                </frame>
                <frame BackgroundTransparency={1} AutomaticSize={Enum.AutomaticSize.X} Size={new UDim2(0, 0, 1, 0)}>
                    <textlabel
                        ref={textRef}
                        AnchorPoint={new Vector2(0, 0.5)}
                        AutomaticSize={Enum.AutomaticSize.X}
                        BackgroundTransparency={1}
                        FontFace={font}
                        Position={new UDim2(0, 0, 0.5, 0)}
                        Size={new UDim2(0, 0, 0.8, 0)}
                        Text={title}
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextScaled={true}
                        TextSize={14}
                        TextWrapped={true}
                    >
                        <uistroke Thickness={2} />
                    </textlabel>
                </frame>
                <uilistlayout
                    FillDirection={Enum.FillDirection.Horizontal}
                    Padding={new UDim(0, 10)}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                    VerticalAlignment={Enum.VerticalAlignment.Top}
                />
            </frame>
        );
    },
);

export default WindowTitle;


import { OnoeNum } from "@rbxts/serikanum";
import React from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import DarkMatterGui from "client/components/world/DarkMatterGui";
import Packets from "shared/Packets";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            darkMatter: 100,
        },
    },
    (props) => {
        const balance = Packets.balance.get();
        balance.set("Dark Matter", new OnoeNum(props.controls.darkMatter));
        Packets.balance.set(balance);
        // NOTE: You need to switch to Viewport mode to see this story in the world
        return <DarkMatterGui />;
    },
);


import { OnoeNum } from "@rbxts/serikanum";
import React, { Fragment, useEffect, useState } from "@rbxts/react";
import { RobotoSlabBold, RobotoSlabHeavy } from "shared/asset/GameFonts";
import DarkMatter from "shared/currency/mechanics/DarkMatter";
import Packets from "shared/Packets";
import Sandbox from "shared/Sandbox";
import DarkMatterPart from "shared/world/nodes/DarkMatterPart";

function calculateDetails(balance: BaseCurrencyMap) {
    const [boost, darkMatter] = DarkMatter.getBoost(balance);
    return { amount: darkMatter, boost };
}

export default function DarkMatterGui() {
    const [{ boost, amount }, setDetails] = useState<ReturnType<typeof calculateDetails>>(
        calculateDetails(Packets.balance.get()),
    );

    useEffect(() => {
        const connection = Packets.balance.observe((newBalance) => {
            setDetails(calculateDetails(newBalance));
        });
        return () => connection.disconnect();
    }, []);

    const remainingForPower = DarkMatter.BOOSTING_CURRENCIES.Power.requirement.sub(amount ?? 0);
    const powerLocked = remainingForPower.moreThan(0);

    return (
        <surfacegui
            Adornee={DarkMatterPart.waitForInstance()}
            ClipsDescendants={true}
            LightInfluence={1}
            MaxDistance={300}
            ResetOnSpawn={false}
            SizingMode={Enum.SurfaceGuiSizingMode.PixelsPerStud}
            ZIndexBehavior={Enum.ZIndexBehavior.Sibling}
        >
            <uilistlayout
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                Padding={new UDim(0, 15)}
                SortOrder={Enum.SortOrder.LayoutOrder}
                VerticalAlignment={Enum.VerticalAlignment.Center}
            />
            <uipadding PaddingLeft={new UDim(0, 25)} PaddingRight={new UDim(0, 25)} PaddingTop={new UDim(0, 25)} />
            <textlabel
                AutomaticSize={Enum.AutomaticSize.Y}
                BackgroundTransparency={1}
                FontFace={RobotoSlabBold}
                Size={new UDim2(1, 0, 0, 0)}
                Text="You have"
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextSize={100}
                TextWrapped={true}
            >
                <uistroke Thickness={2} />
            </textlabel>
            <textlabel
                AutomaticSize={Enum.AutomaticSize.Y}
                BackgroundTransparency={1}
                FontFace={RobotoSlabBold}
                LayoutOrder={4}
                Size={new UDim2(1, 0, 0, 0)}
                Text="Dark Matter, boosting gain by"
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextSize={80}
                TextWrapped={true}
            >
                <uistroke Thickness={2} />
                <uipadding PaddingBottom={new UDim(0, 20)} />
            </textlabel>
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoSlabHeavy}
                LayoutOrder={2}
                Size={new UDim2(0.5, 0, 0, 100)}
                Text={OnoeNum.toString(amount ?? 0)}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextSize={100}
                TextWrapped={true}
            >
                <uistroke Thickness={2} />
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(255, 170, 255)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(170, 85, 255)),
                        ])
                    }
                    Rotation={90}
                />
                <uiscale Scale={2} />
            </textlabel>
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoSlabHeavy}
                LayoutOrder={4}
                Size={new UDim2(0.5, 0, 0.09, 0)}
                Text={`x${boost?.get("Funds")?.toString() ?? "1"} Funds`}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextSize={70}
                TextWrapped={true}
            >
                <uistroke Thickness={2} />
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(170, 255, 127)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(170, 255, 127)),
                        ])
                    }
                    Rotation={90}
                />
                <uiscale Scale={2} />
            </textlabel>
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoSlabHeavy}
                LayoutOrder={5}
                Size={new UDim2(0.5, 0, 0, powerLocked ? 50 : 70)}
                Text={
                    powerLocked
                        ? `(${remainingForPower.toString()} more to unlock!)`
                        : `x${boost?.get("Power")?.toString() ?? "1"} Power`
                }
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextSize={powerLocked ? 50 : 70}
                TextWrapped={true}
            >
                <uistroke Thickness={2} />
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(255, 170, 0)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(255, 136, 0)),
                        ])
                    }
                    Rotation={90}
                />
                <uiscale Scale={2} />
            </textlabel>
        </surfacegui>
    );
}


import { OnoeNum } from "@rbxts/serikanum";
import React from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import FundsBombsBoardGui from "client/components/world/FundsBombsBoardGui";
import Packets from "shared/Packets";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            fundsBombs: 100,
        },
    },
    (props) => {
        const balance = Packets.balance.get();
        balance.set("Funds Bombs", new OnoeNum(props.controls.fundsBombs));
        Packets.balance.set(balance);
        // NOTE: You need to switch to Viewport mode to see this story in the world
        return <FundsBombsBoardGui />;
    },
);


import { OnoeNum } from "@rbxts/serikanum";
import React, { useEffect } from "@rbxts/react";
import { MarketplaceService } from "@rbxts/services";
import { LOCAL_PLAYER } from "shared/constants";
import { showErrorToast } from "client/components/toast/ToastService";
import { RobotoSlabBold, RobotoSlabHeavy, RobotoSlabMedium } from "shared/asset/GameFonts";
import { getAsset } from "shared/asset/AssetMap";
import { playSound } from "shared/asset/GameAssets";
import { BOMBS_PRODUCTS } from "shared/devproducts/BombsProducts";
import Packets from "shared/Packets";
import FundsBombsBoard from "shared/world/nodes/FundsBombsBoard";

export default function FundsBombsBoardGui() {
    const [amount, setAmount] = React.useState<OnoeNum>(new OnoeNum(Packets.balance.get().get("Funds Bombs") ?? 0));

    useEffect(() => {
        const connection = Packets.balance.observe((newBalance) => {
            setAmount(new OnoeNum(newBalance.get("Funds Bombs") ?? 0));
        });
        return () => connection.disconnect();
    }, []);

    return (
        <surfacegui
            Adornee={FundsBombsBoard.waitForInstance()}
            ClipsDescendants={true}
            LightInfluence={1}
            MaxDistance={150}
            ResetOnSpawn={false}
            SizingMode={Enum.SurfaceGuiSizingMode.PixelsPerStud}
            ZIndexBehavior={Enum.ZIndexBehavior.Sibling}
        >
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoSlabHeavy}
                Size={new UDim2(1, 0, 0.2, 0)}
                Text="Speed it up!"
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextSize={14}
                TextWrapped={true}
            >
                <uistroke Thickness={4}>
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(20, 29, 14)),
                            ])
                        }
                        Rotation={90}
                    />
                </uistroke>
            </textlabel>
            <uipadding
                PaddingBottom={new UDim(0, 25)}
                PaddingLeft={new UDim(0, 25)}
                PaddingRight={new UDim(0, 25)}
                PaddingTop={new UDim(0, 25)}
            >
                <uipadding
                    PaddingBottom={new UDim(0, 15)}
                    PaddingLeft={new UDim(0, 15)}
                    PaddingRight={new UDim(0, 15)}
                    PaddingTop={new UDim(0, 15)}
                />
            </uipadding>
            <imagelabel
                BackgroundTransparency={1}
                Image={getAsset("assets/FundsBomb.png")}
                Position={new UDim2(0, 0, 0.525, 0)}
                ScaleType={Enum.ScaleType.Fit}
                Size={new UDim2(0.4, 0, 0.35, 0)}
            />
            <imagelabel
                BackgroundTransparency={1}
                Image={getAsset("assets/StripedRadialBeams.png")}
                Position={new UDim2(0, 0, 0.5, 0)}
                ScaleType={Enum.ScaleType.Fit}
                Size={new UDim2(0.4, 0, 0.5, 0)}
                ZIndex={-2}
            />
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoSlabMedium}
                Position={new UDim2(0.4, 0, 0.3, 0)}
                Size={new UDim2(0.6, 0, 0.225, 0)}
                Text="Use a Funds Bomb for a x2 Funds boost across ALL servers for 15 minutes!"
                TextColor3={Color3.fromRGB(193, 193, 193)}
                TextSize={40}
                TextWrapped={true}
                TextYAlignment={Enum.TextYAlignment.Top}
            >
                <uistroke Thickness={2}>
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(20, 29, 14)),
                            ])
                        }
                        Rotation={90}
                    />
                </uistroke>
            </textlabel>
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoSlabMedium}
                Position={new UDim2(0, 0, 0.3, 0)}
                Size={new UDim2(0.4, 0, 0.1, 0)}
                Text="You have"
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextSize={50}
                TextWrapped={true}
                TextYAlignment={Enum.TextYAlignment.Top}
            >
                <uistroke Thickness={2}>
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(20, 29, 14)),
                            ])
                        }
                        Rotation={90}
                    />
                </uistroke>
            </textlabel>
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoSlabHeavy}
                Position={new UDim2(0, 0, 0.4, 0)}
                Size={new UDim2(0.4, 0, 0.15, 0)}
                Text={amount.toString()}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextSize={50}
                TextWrapped={true}
                TextYAlignment={Enum.TextYAlignment.Top}
            >
                <uistroke Thickness={2}>
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(20, 29, 14)),
                            ])
                        }
                        Rotation={90}
                    />
                </uistroke>
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(255, 170, 0)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(255, 85, 0)),
                        ])
                    }
                    Rotation={90}
                />
            </textlabel>
            <textbutton
                BackgroundColor3={Color3.fromRGB(255, 255, 255)}
                BorderSizePixel={0}
                Position={new UDim2(0.45, 0, 0.65, 0)}
                Size={new UDim2(0.5, 0, 0.15, 0)}
                Text={""}
                TextColor3={Color3.fromRGB(0, 0, 0)}
                TextSize={14}
                Event={{
                    Activated: () => {
                        playSound("MenuClick.mp3");
                        if (LOCAL_PLAYER === undefined) return;
                        MarketplaceService.PromptProductPurchase(LOCAL_PLAYER, BOMBS_PRODUCTS["Funds Bombs"]);
                    },
                }}
            >
                <uistroke ApplyStrokeMode={Enum.ApplyStrokeMode.Border} Thickness={2} />
                <uicorner CornerRadius={new UDim(0.2, 0)} />
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(255, 170, 0)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(255, 112, 41)),
                        ])
                    }
                    Rotation={90}
                />
                <textlabel
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabHeavy}
                    Size={new UDim2(1, 0, 1, 0)}
                    Text="Use"
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextScaled={true}
                    TextSize={14}
                    TextWrapped={true}
                >
                    <uistroke Thickness={3}>
                        <uigradient
                            Color={
                                new ColorSequence([
                                    new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                                    new ColorSequenceKeypoint(1, Color3.fromRGB(20, 29, 14)),
                                ])
                            }
                            Rotation={90}
                        />
                    </uistroke>
                </textlabel>
                <uipadding
                    PaddingBottom={new UDim(0, 15)}
                    PaddingLeft={new UDim(0, 15)}
                    PaddingRight={new UDim(0, 15)}
                    PaddingTop={new UDim(0, 15)}
                />
            </textbutton>
            <textbutton
                BackgroundColor3={Color3.fromRGB(255, 255, 255)}
                BorderSizePixel={0}
                Position={new UDim2(0.5, 0, 0.825, 0)}
                Size={new UDim2(0.4, 0, 0.1, 0)}
                Text={""}
                TextColor3={Color3.fromRGB(0, 0, 0)}
                TextSize={14}
                Event={{
                    Activated: () => {
                        if (Packets.useBomb.toServer("Funds Bombs") === true) {
                            playSound("ItemPurchase.mp3");
                        } else {
                            playSound("Error.mp3");
                        }
                        showErrorToast("Unable to purchase Funds Bombs.");
                    },
                }}
            >
                <uistroke ApplyStrokeMode={Enum.ApplyStrokeMode.Border} Thickness={2} />
                <uicorner CornerRadius={new UDim(0.2, 0)} />
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(170, 255, 127)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(85, 170, 0)),
                        ])
                    }
                    Rotation={90}
                />
                <textlabel
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabBold}
                    Size={new UDim2(1, 0, 1, 0)}
                    Text="Buy 4 for 200 Robux"
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextScaled={true}
                    TextSize={14}
                    TextWrapped={true}
                >
                    <uistroke Thickness={3}>
                        <uigradient
                            Color={
                                new ColorSequence([
                                    new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                                    new ColorSequenceKeypoint(1, Color3.fromRGB(20, 29, 14)),
                                ])
                            }
                            Rotation={90}
                        />
                    </uistroke>
                </textlabel>
                <uipadding
                    PaddingBottom={new UDim(0, 10)}
                    PaddingLeft={new UDim(0, 25)}
                    PaddingRight={new UDim(0, 25)}
                    PaddingTop={new UDim(0, 10)}
                />
            </textbutton>
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoSlabMedium}
                Position={new UDim2(0.4, 0, 0.525, 0)}
                Size={new UDim2(0.6, 0, 0.075, 0)}
                Text="(this is not p2w i swear)"
                TextColor3={Color3.fromRGB(139, 139, 139)}
                TextSize={25}
                TextWrapped={true}
                TextYAlignment={Enum.TextYAlignment.Top}
            >
                <uistroke Thickness={2}>
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(20, 29, 14)),
                            ])
                        }
                        Rotation={90}
                    />
                </uistroke>
            </textlabel>
        </surfacegui>
    );
}


import React, { Fragment, useEffect, useState } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { Workspace } from "@rbxts/services";
import { CreateReactStory } from "@rbxts/ui-labs";
import ProximityPromptGui from "client/components/world/ProximityPromptGui";
import CustomProximityPrompt from "shared/world/CustomProximityPrompt";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            fundsBombs: 100,
        },
    },
    () => {
        const [prompt, setPrompt] = useState<ProximityPrompt>();

        useEffect(() => {
            const part = new Instance("Part");
            part.Anchored = true;
            part.Position = new Vector3(0, 5, 0);
            part.Size = new Vector3(4, 1, 4);
            part.Parent = Workspace;

            const proximityPrompt = new Instance("ProximityPrompt");
            proximityPrompt.ActionText = "Test Action";
            proximityPrompt.ClickablePrompt = true;
            proximityPrompt.ObjectText = "Test Object";
            proximityPrompt.KeyboardKeyCode = Enum.KeyCode.E;
            proximityPrompt.HoldDuration = 0.5;
            proximityPrompt.Parent = part;
            const cleanup = CustomProximityPrompt.onTrigger(proximityPrompt, () => {
                print("Proximity prompt triggered!");
            });
            setPrompt(proximityPrompt);

            return () => {
                cleanup();
                proximityPrompt.Destroy();
                part.Destroy();
            };
        }, []);

        if (prompt === undefined) return <Fragment />;

        // NOTE: You need to switch to Viewport mode to see this story in the world
        return <ProximityPromptGui prompt={prompt} inputType={Enum.ProximityPromptInputType.Keyboard} />;
    },
);


import { simpleInterval } from "@antivivi/vrldk";
import React, { Fragment, useCallback, useEffect, useRef, useState } from "@rbxts/react";
import {
    CollectionService,
    ProximityPromptService,
    RunService,
    TextService,
    TweenService,
    UserInputService,
} from "@rbxts/services";
import { getAsset } from "shared/asset/AssetMap";
import { RobotoMono, RobotoMonoBold } from "shared/asset/GameFonts";
import { IS_EDIT } from "shared/Context";
import { getPlayerCharacter } from "shared/hamster/getPlayerCharacter";
import CustomProximityPrompt from "shared/world/CustomProximityPrompt";

// Helper functions for button image mappings
function getGamepadButtonImage(keyCode: Enum.KeyCode): string | undefined {
    const map = new Map<Enum.KeyCode, string>([
        [Enum.KeyCode.ButtonX, "rbxasset://textures/ui/Controls/xboxX.png"],
        [Enum.KeyCode.ButtonY, "rbxasset://textures/ui/Controls/xboxY.png"],
        [Enum.KeyCode.ButtonA, "rbxasset://textures/ui/Controls/xboxA.png"],
        [Enum.KeyCode.ButtonB, "rbxasset://textures/ui/Controls/xboxB.png"],
        [Enum.KeyCode.DPadLeft, "rbxasset://textures/ui/Controls/dpadLeft.png"],
        [Enum.KeyCode.DPadRight, "rbxasset://textures/ui/Controls/dpadRight.png"],
        [Enum.KeyCode.DPadUp, "rbxasset://textures/ui/Controls/dpadUp.png"],
        [Enum.KeyCode.DPadDown, "rbxasset://textures/ui/Controls/dpadDown.png"],
        [Enum.KeyCode.ButtonSelect, "rbxasset://textures/ui/Controls/xboxmenu.png"],
        [Enum.KeyCode.ButtonL1, "rbxasset://textures/ui/Controls/xboxLS.png"],
        [Enum.KeyCode.ButtonR1, "rbxasset://textures/ui/Controls/xboxRS.png"],
    ]);
    return map.get(keyCode);
}

function getKeyboardButtonImage(keyCode: Enum.KeyCode): string | undefined {
    const map = new Map<Enum.KeyCode, string>([
        [Enum.KeyCode.Backspace, "rbxasset://textures/ui/Controls/backspace.png"],
        [Enum.KeyCode.Return, "rbxasset://textures/ui/Controls/return.png"],
        [Enum.KeyCode.LeftShift, "rbxasset://textures/ui/Controls/shift.png"],
        [Enum.KeyCode.RightShift, "rbxasset://textures/ui/Controls/shift.png"],
        [Enum.KeyCode.Tab, "rbxasset://textures/ui/Controls/tab.png"],
    ]);
    return map.get(keyCode);
}

function getKeyboardButtonIconMapping(char: string): string | undefined {
    const map: Record<string, string> = {
        ["'"]: "rbxasset://textures/ui/Controls/apostrophe.png",
        [","]: "rbxasset://textures/ui/Controls/comma.png",
        ["`"]: "rbxasset://textures/ui/Controls/graveaccent.png",
        ["."]: "rbxasset://textures/ui/Controls/period.png",
        [" "]: "rbxasset://textures/ui/Controls/spacebar.png",
    };
    return map[char];
}

function getKeyCodeToText(keyCode: Enum.KeyCode): string | undefined {
    const map = new Map<Enum.KeyCode, string>([
        [Enum.KeyCode.LeftControl, "Ctrl"],
        [Enum.KeyCode.RightControl, "Ctrl"],
        [Enum.KeyCode.LeftAlt, "Alt"],
        [Enum.KeyCode.RightAlt, "Alt"],
        [Enum.KeyCode.F1, "F1"],
        [Enum.KeyCode.F2, "F2"],
        [Enum.KeyCode.F3, "F3"],
        [Enum.KeyCode.F4, "F4"],
        [Enum.KeyCode.F5, "F5"],
        [Enum.KeyCode.F6, "F6"],
        [Enum.KeyCode.F7, "F7"],
        [Enum.KeyCode.F8, "F8"],
        [Enum.KeyCode.F9, "F9"],
        [Enum.KeyCode.F10, "F10"],
        [Enum.KeyCode.F11, "F11"],
        [Enum.KeyCode.F12, "F12"],
    ]);
    return map.get(keyCode);
}

const PRESS_ANIMATION_DURATION = 0.12;
const FADE_OUT_DURATION = 0.2;

export default function ProximityPromptGui({
    prompt,
    inputType,
    fadeOut = false,
    fadeIn = false,
}: {
    prompt: ProximityPrompt;
    inputType: Enum.ProximityPromptInputType;
    fadeOut?: boolean;
    fadeIn?: boolean;
}) {
    const [progress, setProgress] = useState(0);
    const [frameSize, setFrameSize] = useState(UDim2.fromScale(1, 1));
    const [frameTransparency, setFrameTransparency] = useState(fadeIn ? 1 : 0.2);
    const [inputScale, setInputScale] = useState(fadeIn ? 0 : 1);
    const [actionTextTransparency, setActionTextTransparency] = useState(fadeIn ? 1 : 0);
    const [objectTextTransparency, setObjectTextTransparency] = useState(fadeIn ? 1 : 0);
    const [promptSize, setPromptSize] = useState(new UDim2(0, 193, 0, 72));
    const [actionTextPosition, setActionTextPosition] = useState(new UDim2(0.5, -24, 0, 9));
    const [objectTextPosition, setObjectTextPosition] = useState(new UDim2(0.5, -24, 0, -10));
    const [actionText, setActionText] = useState(prompt.ActionText);
    const [objectText, setObjectText] = useState(prompt.ObjectText);
    const [isHolding, setIsHolding] = useState(false);
    const [holdStartTime, setHoldStartTime] = useState(0);
    const [isPressAnimating, setIsPressAnimating] = useState(false);

    // Determine button visuals based on input type
    const [buttonImage, setButtonImage] = useState<string | undefined>();
    const [buttonText, setButtonText] = useState<string | undefined>();
    const [buttonIconImage, setButtonIconImage] = useState<string | undefined>();
    const [showButtonImage, setShowButtonImage] = useState(false);
    const [showButtonText, setShowButtonText] = useState(false);
    const [showButtonIcon, setShowButtonIcon] = useState(false);

    // Refs for tween management
    const frameRef = useRef<ImageLabel>();
    const inputScaleRef = useRef<UIScale>();
    const actionTextRef = useRef<TextLabel>();
    const objectTextRef = useRef<TextLabel>();
    const previousPromptRef = useRef<ProximityPrompt | undefined>(prompt);

    // Handle fade in animations
    useEffect(() => {
        if (!fadeIn) return;

        const tweenInfoFast = new TweenInfo(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out);

        // Scale up and fade in
        if (inputScaleRef.current) {
            TweenService.Create(inputScaleRef.current, tweenInfoFast, {
                Scale: 1,
            }).Play();
        }

        if (frameRef.current) {
            TweenService.Create(frameRef.current, tweenInfoFast, {
                ImageTransparency: 0.2,
            }).Play();
        }

        // Tween text transparency
        if (actionTextRef.current) {
            TweenService.Create(actionTextRef.current, tweenInfoFast, {
                TextTransparency: 0,
            }).Play();
        }

        if (objectTextRef.current) {
            TweenService.Create(objectTextRef.current, tweenInfoFast, {
                TextTransparency: 0,
            }).Play();
        }
    }, [fadeIn]);

    // Handle fade out animations
    useEffect(() => {
        if (!fadeOut) return;

        const tweenInfoFast = new TweenInfo(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out);

        // Scale down and fade out
        if (inputScaleRef.current) {
            TweenService.Create(inputScaleRef.current, tweenInfoFast, {
                Scale: 0,
            }).Play();
        }

        if (frameRef.current) {
            TweenService.Create(frameRef.current, tweenInfoFast, {
                ImageTransparency: 1,
            }).Play();
        }

        // Tween text transparency
        if (actionTextRef.current) {
            TweenService.Create(actionTextRef.current, tweenInfoFast, {
                TextTransparency: 1,
            }).Play();
        }

        if (objectTextRef.current) {
            TweenService.Create(objectTextRef.current, tweenInfoFast, {
                TextTransparency: 1,
            }).Play();
        }
    }, [fadeOut]);

    useEffect(() => {
        // Determine button display based on input type
        if (inputType === Enum.ProximityPromptInputType.Gamepad) {
            const gamepadImage = getGamepadButtonImage(prompt.GamepadKeyCode);
            if (gamepadImage) {
                setButtonIconImage(gamepadImage);
                setShowButtonIcon(true);
                setShowButtonText(false);
                setShowButtonImage(false);
            }
        } else if (inputType === Enum.ProximityPromptInputType.Touch) {
            setButtonImage("rbxasset://textures/ui/Controls/TouchTapIcon.png");
            setShowButtonImage(true);
            setShowButtonText(false);
            setShowButtonIcon(false);
        } else {
            // Keyboard input
            setShowButtonImage(true);

            const buttonTextString = UserInputService.GetStringForKeyCode(prompt.KeyboardKeyCode);
            let buttonTextImageAsset = getKeyboardButtonImage(prompt.KeyboardKeyCode);

            if (!buttonTextImageAsset) {
                buttonTextImageAsset = getKeyboardButtonIconMapping(buttonTextString);
            }

            if (buttonTextImageAsset) {
                setButtonIconImage(buttonTextImageAsset);
                setShowButtonIcon(true);
                setShowButtonText(false);
            } else {
                let displayText = buttonTextString;
                const keyCodeMappedText = getKeyCodeToText(prompt.KeyboardKeyCode);
                if (keyCodeMappedText) {
                    displayText = keyCodeMappedText;
                }

                if (displayText && displayText !== "") {
                    setButtonText(displayText);
                    setShowButtonText(true);
                    setShowButtonIcon(false);
                } else {
                    warn(
                        `ProximityPrompt '${prompt.Name}' has an unsupported keycode for rendering UI: ${prompt.KeyboardKeyCode}`,
                    );
                }
            }
        }
    }, [prompt, inputType]);

    useEffect(() => {
        // Update UI sizing based on text
        const updateUIFromPrompt = () => {
            const actionTextSize = TextService.GetTextSize(
                prompt.ActionText,
                19,
                Enum.Font.RobotoMono,
                new Vector2(1000, 1000),
            );
            const objectTextSize = TextService.GetTextSize(
                prompt.ObjectText,
                14,
                Enum.Font.RobotoMono,
                new Vector2(1000, 1000),
            );
            const maxTextWidth = math.max(actionTextSize.X, objectTextSize.X);
            let promptHeight = 72;
            let promptWidth = 72;
            const textPaddingLeft = 72;

            if (
                (prompt.ActionText !== undefined && prompt.ActionText !== "") ||
                (prompt.ObjectText !== undefined && prompt.ObjectText !== "")
            ) {
                promptWidth = maxTextWidth + textPaddingLeft + 24;
            }

            let actionTextYOffset = 0;
            if (prompt.ObjectText !== undefined && prompt.ObjectText !== "") {
                actionTextYOffset = 9;
            }

            setActionTextPosition(new UDim2(0.5, textPaddingLeft - promptWidth / 2, 0, actionTextYOffset));
            setObjectTextPosition(new UDim2(0.5, textPaddingLeft - promptWidth / 2, 0, -10));
            setActionText(prompt.ActionText);
            setObjectText(prompt.ObjectText);
            setPromptSize(UDim2.fromOffset(promptWidth, promptHeight));
        };

        updateUIFromPrompt();
        const changedConnection = (prompt.Changed as RBXScriptSignal<() => void>).Connect(updateUIFromPrompt);

        return () => {
            changedConnection.Disconnect();
        };
    }, [prompt]);

    useEffect(() => {
        if (previousPromptRef.current === prompt) return;

        previousPromptRef.current = prompt;
        setIsPressAnimating(false);
        setIsHolding(false);
    }, [prompt]);

    // Manage hold progress with React state
    useEffect(() => {
        if (!isHolding) return;

        const tweenInfoFast = new TweenInfo(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out);
        const inputFrameScaleFactor = inputType === Enum.ProximityPromptInputType.Touch ? 1.6 : 1.33;

        // Scale up input frame
        if (inputScaleRef.current) {
            TweenService.Create(inputScaleRef.current, tweenInfoFast, {
                Scale: inputFrameScaleFactor,
            }).Play();
        }

        // Shrink frame
        if (frameRef.current) {
            TweenService.Create(frameRef.current, tweenInfoFast, {
                Size: UDim2.fromScale(0.5, 1),
                BackgroundTransparency: 1,
                ImageTransparency: 1,
            }).Play();
        }
        setActionTextTransparency(1);
        setObjectTextTransparency(1);

        const startTime = os.clock();
        const holdDuration = prompt.HoldDuration;

        let connection: RBXScriptConnection | undefined;

        if (holdDuration > 0) {
            // Animate progress over time
            connection = RunService.Heartbeat.Connect(() => {
                const elapsed = os.clock() - startTime;
                const currentProgress = math.min(elapsed / holdDuration, 1);
                setProgress(currentProgress);

                if (currentProgress >= 1) {
                    // Hold completed, trigger the prompt
                    setIsHolding(false);
                    triggerPrompt();
                }
            });
        }

        return () => {
            connection?.Disconnect();

            // Scale down input frame
            if (inputScaleRef.current) {
                TweenService.Create(inputScaleRef.current, tweenInfoFast, {
                    Scale: 1,
                }).Play();
            }

            // Reset progress bar
            setProgress(0);

            // Restore frame
            if (frameRef.current) {
                TweenService.Create(frameRef.current, tweenInfoFast, {
                    Size: UDim2.fromScale(1, 1),
                    ImageTransparency: 0.2,
                }).Play();
            }
            setActionTextTransparency(0);
            setObjectTextTransparency(0);
        };
    }, [isHolding, prompt, inputType]);

    useEffect(() => {
        if (!isPressAnimating) return;

        const pressTweenInfo = new TweenInfo(0.08, Enum.EasingStyle.Quad, Enum.EasingDirection.Out);
        const releaseTweenInfo = new TweenInfo(0.12, Enum.EasingStyle.Quad, Enum.EasingDirection.Out);
        const activeTweens = new Array<Tween>();

        if (inputScaleRef.current) {
            const tween = TweenService.Create(inputScaleRef.current, pressTweenInfo, {
                Scale: 0.92,
            });
            tween.Play();
            activeTweens.push(tween);
        }

        if (frameRef.current) {
            const tween = TweenService.Create(frameRef.current, pressTweenInfo, {
                Size: new UDim2(1, -18, 1, -18),
                ImageTransparency: 0.35,
            });
            tween.Play();
            activeTweens.push(tween);
        }

        setActionTextTransparency(0.15);
        setObjectTextTransparency(0.15);

        const animationThread = task.delay(PRESS_ANIMATION_DURATION, () => {
            setIsPressAnimating(false);
        });

        return () => {
            task.cancel(animationThread);

            for (const tween of activeTweens) {
                tween.Cancel();
            }

            if (inputScaleRef.current) {
                TweenService.Create(inputScaleRef.current, releaseTweenInfo, {
                    Scale: 1,
                }).Play();
            }

            if (frameRef.current) {
                TweenService.Create(frameRef.current, releaseTweenInfo, {
                    Size: new UDim2(1, 0, 1, 0),
                    ImageTransparency: 0.2,
                }).Play();
            }

            setActionTextTransparency(0);
            setObjectTextTransparency(0);
        };
    }, [isPressAnimating, prompt]);

    const startInstantPressAnimation = useCallback(() => {
        if (isPressAnimating) return;

        setHoldStartTime(os.clock());
        setIsPressAnimating(true);
    }, [isPressAnimating]);

    const triggerPrompt = () => {
        CustomProximityPrompt.trigger(prompt);
    };

    // Handle keyboard/gamepad input for triggering prompts
    useEffect(() => {
        const handleInput = (input: InputObject, gameProcessedEvent: boolean) => {
            if (input.KeyCode !== prompt.GamepadKeyCode && input.KeyCode !== prompt.KeyboardKeyCode) return;
            if (IS_EDIT && gameProcessedEvent) return;

            if (input.UserInputState === Enum.UserInputState.Begin) {
                if (prompt.HoldDuration > 0) {
                    setIsHolding(true);
                    setHoldStartTime(os.clock());
                } else {
                    startInstantPressAnimation();
                    triggerPrompt();
                }
                return;
            }

            if (input.UserInputState === Enum.UserInputState.End) {
                if (prompt.HoldDuration > 0) {
                    setIsHolding(false);
                }
            }
        };
        const inputBeganConnection = UserInputService.InputBegan.Connect(handleInput);
        const inputEndedConnection = UserInputService.InputEnded.Connect(handleInput);

        return () => {
            inputBeganConnection.Disconnect();
            inputEndedConnection.Disconnect();
        };
    }, [prompt, inputType, startInstantPressAnimation]);

    // Calculate gradient rotations for circular progress
    const leftGradientRotation = math.clamp(progress * 360, 180, 360);
    const rightGradientRotation = math.clamp(progress * 360, 0, 180);

    // Handle touch/click input
    const handleInputBegan = (rbx: TextButton, input: InputObject) => {
        if (
            (input.UserInputType === Enum.UserInputType.Touch ||
                input.UserInputType === Enum.UserInputType.MouseButton1) &&
            input.UserInputState !== Enum.UserInputState.Change
        ) {
            if (prompt.HoldDuration > 0) {
                // Start holding
                setIsHolding(true);
                setHoldStartTime(os.clock());
            } else {
                // Instant trigger
                startInstantPressAnimation();
                triggerPrompt();
            }
        }
    };

    const handleInputEnded = (rbx: TextButton, input: InputObject) => {
        if (
            input.UserInputType === Enum.UserInputType.Touch ||
            input.UserInputType === Enum.UserInputType.MouseButton1
        ) {
            // Stop holding
            if (prompt.HoldDuration > 0) {
                setIsHolding(false);
            }
        }
    };

    const isInteractive = inputType === Enum.ProximityPromptInputType.Touch || prompt.ClickablePrompt;

    return (
        <billboardgui
            Adornee={prompt.Parent as PVInstance}
            Active={isInteractive}
            AlwaysOnTop={true}
            ResetOnSpawn={false}
            Size={promptSize}
            SizeOffset={
                new Vector2(prompt.UIOffset.X / promptSize.Width.Offset, prompt.UIOffset.Y / promptSize.Height.Offset)
            }
            MaxDistance={prompt.MaxActivationDistance + 50}
        >
            {isInteractive && (
                <textbutton
                    AnchorPoint={new Vector2(0.5, 0.5)}
                    BackgroundTransparency={1}
                    Selectable={false}
                    Position={new UDim2(0.5, 0, 0.5, 0)}
                    Size={new UDim2(1, 0, 1, 0)}
                    TextTransparency={1}
                    Event={{
                        InputBegan: handleInputBegan,
                        InputEnded: handleInputEnded,
                    }}
                />
            )}
            <imagelabel
                ref={frameRef}
                AnchorPoint={new Vector2(0.5, 0.5)}
                BackgroundTransparency={1}
                Image={getAsset("assets/ProximityPromptFrame.png")}
                ImageTransparency={frameTransparency}
                Position={new UDim2(0.5, 0, 0.5, 0)}
                Size={frameSize}
            >
                <frame
                    BackgroundTransparency={1}
                    Size={new UDim2(1, 0, 1, 0)}
                    SizeConstraint={Enum.SizeConstraint.RelativeYY}
                >
                    <frame
                        AnchorPoint={new Vector2(0.5, 0.5)}
                        BackgroundTransparency={1}
                        Position={new UDim2(0.5, 0, 0.5, 0)}
                        Size={new UDim2(1, 0, 1, 0)}
                    >
                        <uiscale ref={inputScaleRef} Scale={inputScale} />
                        {/* Progress Bar */}
                        {prompt.HoldDuration > 0 && (
                            <frame
                                AnchorPoint={new Vector2(0.5, 0.5)}
                                BackgroundTransparency={1}
                                Position={new UDim2(0.5, 0, 0.5, 0)}
                                Size={new UDim2(1, -14, 1, -14)}
                            >
                                {/* Left gradient */}
                                <frame
                                    BackgroundTransparency={1}
                                    ClipsDescendants={true}
                                    Size={new UDim2(0.5, 0, 1, 0)}
                                >
                                    <imagelabel
                                        BackgroundTransparency={1}
                                        Image="rbxasset://textures/ui/Controls/RadialFill.png"
                                        Size={new UDim2(2, 0, 1, 0)}
                                    >
                                        <uigradient
                                            Rotation={leftGradientRotation}
                                            Transparency={
                                                new NumberSequence([
                                                    new NumberSequenceKeypoint(0, 0),
                                                    new NumberSequenceKeypoint(0.4999, 0),
                                                    new NumberSequenceKeypoint(0.5, 1),
                                                    new NumberSequenceKeypoint(1, 1),
                                                ])
                                            }
                                        />
                                    </imagelabel>
                                </frame>
                                {/* Right gradient */}
                                <frame
                                    BackgroundTransparency={1}
                                    ClipsDescendants={true}
                                    Position={new UDim2(0.5, 0, 0, 0)}
                                    Size={new UDim2(0.5, 0, 1, 0)}
                                >
                                    <imagelabel
                                        BackgroundTransparency={1}
                                        Image="rbxasset://textures/ui/Controls/RadialFill.png"
                                        Position={new UDim2(-1, 0, 0, 0)}
                                        Size={new UDim2(2, 0, 1, 0)}
                                    >
                                        <uigradient
                                            Rotation={rightGradientRotation}
                                            Transparency={
                                                new NumberSequence([
                                                    new NumberSequenceKeypoint(0, 0),
                                                    new NumberSequenceKeypoint(0.4999, 0),
                                                    new NumberSequenceKeypoint(0.5, 1),
                                                    new NumberSequenceKeypoint(1, 1),
                                                ])
                                            }
                                        />
                                    </imagelabel>
                                </frame>
                            </frame>
                        )}
                        {/* Button background */}
                        <imagelabel
                            AnchorPoint={new Vector2(0.5, 0.5)}
                            BackgroundTransparency={0.5}
                            BorderColor3={Color3.fromRGB(27, 42, 53)}
                            Image="rbxasset://textures/ui/GuiImagePlaceholder.png"
                            ImageTransparency={1}
                            Position={new UDim2(0.5, 0, 0.5, 0)}
                            Size={new UDim2(1, -24, 1, -24)}
                        >
                            <uicorner CornerRadius={new UDim(0.5, 0)} />
                        </imagelabel>
                        {/* Button Image (keyboard background) */}
                        {showButtonImage && buttonImage !== undefined && (
                            <imagelabel
                                AnchorPoint={new Vector2(0.5, 0.5)}
                                BackgroundTransparency={1}
                                Image={buttonImage}
                                Position={new UDim2(0.5, 0, 0.5, 0)}
                                Size={new UDim2(1, -44, 1, -42)}
                            />
                        )}
                        {showButtonImage && buttonImage === undefined && (
                            <imagelabel
                                AnchorPoint={new Vector2(0.5, 0.5)}
                                BackgroundTransparency={1}
                                Image="rbxasset://textures/ui/Controls/key_single.png"
                                Position={new UDim2(0.5, 0, 0.5, 0)}
                                Size={new UDim2(1, -44, 1, -42)}
                            />
                        )}
                        {/* Button Icon (gamepad/keyboard icons) */}
                        {showButtonIcon && buttonIconImage !== undefined && (
                            <imagelabel
                                AnchorPoint={new Vector2(0.5, 0.5)}
                                BackgroundTransparency={1}
                                Image={buttonIconImage}
                                Position={new UDim2(0.5, 0, 0.5, 0)}
                                Size={new UDim2(1, -48, 1, -48)}
                            />
                        )}
                        {/* Button Text (keyboard key label) */}
                        {showButtonText && buttonText !== undefined && (
                            <textlabel
                                BackgroundTransparency={1}
                                FontFace={RobotoMonoBold}
                                Position={new UDim2(0, 0, 0, -1)}
                                Size={new UDim2(1, 0, 1, 0)}
                                Text={buttonText}
                                TextColor3={Color3.fromRGB(255, 255, 255)}
                                TextSize={buttonText.size() > 2 ? 12 : 14}
                            />
                        )}
                    </frame>
                </frame>
                {/* Object Text */}
                <textlabel
                    ref={objectTextRef}
                    BackgroundTransparency={1}
                    FontFace={RobotoMono}
                    Position={objectTextPosition}
                    Size={new UDim2(1, 0, 1, 0)}
                    Text={objectText}
                    TextColor3={Color3.fromRGB(178, 178, 178)}
                    TextSize={14}
                    TextTransparency={objectTextTransparency}
                    TextXAlignment={Enum.TextXAlignment.Left}
                    AutoLocalize={prompt.AutoLocalize}
                    RootLocalizationTable={prompt.RootLocalizationTable}
                />
                {/* Action Text */}
                <textlabel
                    ref={actionTextRef}
                    BackgroundTransparency={1}
                    FontFace={RobotoMonoBold}
                    Position={actionTextPosition}
                    Size={new UDim2(1, 0, 1, 0)}
                    Text={actionText}
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextSize={19}
                    TextTransparency={actionTextTransparency}
                    TextXAlignment={Enum.TextXAlignment.Left}
                    AutoLocalize={prompt.AutoLocalize}
                    RootLocalizationTable={prompt.RootLocalizationTable}
                />
            </imagelabel>
        </billboardgui>
    );
}

export function ProximityPromptGuiRenderer() {
    const [prompt, setPrompt] = useState<ProximityPrompt | undefined>();
    const [inputType, setInputType] = useState<Enum.ProximityPromptInputType>(Enum.ProximityPromptInputType.Keyboard);
    const [isVisible, setIsVisible] = useState(false);
    const [isFadingOut, setIsFadingOut] = useState(false);
    const [shouldFadeIn, setShouldFadeIn] = useState(false);

    useEffect(() => {
        if (IS_EDIT) {
            const intervalCleanup = simpleInterval(() => {
                const character = getPlayerCharacter();
                if (!character) return;

                // Find the closest prompt in range
                let closestPrompt: ProximityPrompt | undefined;
                let closestDistance = math.huge;

                const position = character.GetPivot().Position;
                for (const prompt of CollectionService.GetTagged("ProximityPrompt")) {
                    if (!prompt.IsA("ProximityPrompt")) continue;
                    if (!prompt.Enabled || prompt.Parent === undefined || !prompt.Parent.IsA("PVInstance")) continue;
                    const distance = position.sub(prompt.Parent.GetPivot().Position).Magnitude;
                    if (distance < closestDistance) {
                        closestPrompt = prompt;
                        closestDistance = distance;
                    }
                }

                if (closestPrompt !== prompt) {
                    if (closestPrompt && closestDistance <= closestPrompt.MaxActivationDistance + 15) {
                        setPrompt(closestPrompt);
                        setIsVisible(true);
                        setIsFadingOut(false);
                        setShouldFadeIn(true);
                    } else {
                        // Start fade out animation
                        setIsFadingOut(true);
                        setShouldFadeIn(false);
                    }
                }
            }, 1);

            const inputTypeChangedConnection = UserInputService.LastInputTypeChanged.Connect((newInputType) => {
                if (newInputType === Enum.UserInputType.Touch || newInputType === Enum.UserInputType.Accelerometer) {
                    setInputType(Enum.ProximityPromptInputType.Touch);
                } else if (
                    newInputType === Enum.UserInputType.Gamepad1 ||
                    newInputType === Enum.UserInputType.Gamepad2 ||
                    newInputType === Enum.UserInputType.Gamepad3 ||
                    newInputType === Enum.UserInputType.Gamepad4
                ) {
                    setInputType(Enum.ProximityPromptInputType.Gamepad);
                } else {
                    setInputType(Enum.ProximityPromptInputType.Keyboard);
                }
            });

            return () => {
                intervalCleanup();
                inputTypeChangedConnection.Disconnect();
            };
        }

        const promptServiceConnection = ProximityPromptService.PromptShown.Connect((prompt, inputType) => {
            setPrompt(prompt);
            setInputType(inputType);
            setIsVisible(true);
            setIsFadingOut(false);
            setShouldFadeIn(true);
        });

        const promptHiddenConnection = ProximityPromptService.PromptHidden.Connect(() => {
            // Start fade out animation instead of immediately hiding
            setIsFadingOut(true);
            setShouldFadeIn(false);
        });

        return () => {
            promptServiceConnection.Disconnect();
            promptHiddenConnection.Disconnect();
        };
    });

    // Handle fade out animation completion
    useEffect(() => {
        if (!isFadingOut) return;

        const fadeOutThread = task.delay(FADE_OUT_DURATION, () => {
            setPrompt(undefined);
            setIsVisible(false);
            setIsFadingOut(false);
        });

        return () => {
            task.cancel(fadeOutThread);
        };
    }, [isFadingOut]);

    return prompt && isVisible ? (
        <ProximityPromptGui prompt={prompt} inputType={inputType} fadeOut={isFadingOut} fadeIn={shouldFadeIn} />
    ) : (
        <Fragment />
    );
}


import React, { Fragment } from "@rbxts/react";
import { ChangelogGui } from "client/components/changelog/Changelog";
import AreaBoardRenderer from "client/components/world/area/AreaBoardRenderer";
import DarkMatterGui from "client/components/world/DarkMatterGui";
import FundsBombsBoardGui from "client/components/world/FundsBombsBoardGui";
import LeaderboardRenderer from "client/components/world/leaderboard/LeaderboardRenderer";
import { ProximityPromptGuiRenderer } from "client/components/world/ProximityPromptGui";
import ResetBoardRenderer from "client/components/world/reset/ResetBoardRenderer";
import Sandbox from "shared/Sandbox";

export default function WorldRenderer() {
    if (Sandbox.getEnabled())
        return (
            <Fragment>
                <ProximityPromptGuiRenderer />
            </Fragment>
        );

    return (
        <Fragment>
            <AreaBoardRenderer />
            <ResetBoardRenderer />
            <ChangelogGui />
            <DarkMatterGui />
            <FundsBombsBoardGui />
            <LeaderboardRenderer />
            <ProximityPromptGuiRenderer />
        </Fragment>
    );
}


import React, { ReactNode, useEffect, useState } from "@rbxts/react";
import ProgressBar from "client/components/window/ProgressBar";
import { RobotoMono, RobotoMonoBold, RobotoSlabHeavy } from "shared/asset/GameFonts";
import Area from "shared/world/Area";

declare global {
    interface AreaBoardGuiProps {
        area: Area;
        colorSequence?: ColorSequence;
        children?: ReactNode;
        dropletCount?: number;
        dropletLimit?: number;
        placedItemsText?: string;
    }
}

export function AreaStatEntry({ title, children }: { title: string; children?: ReactNode }) {
    return (
        <frame BackgroundTransparency={1} Size={new UDim2(0.9, 0, 0.075, 0)}>
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoMonoBold}
                Size={new UDim2(0.35, 0, 0.8, 0)}
                Text={title}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextSize={14}
                TextWrapped={true}
            >
                <uistroke Thickness={4} />
            </textlabel>
            {children}
            <uilistlayout
                FillDirection={Enum.FillDirection.Horizontal}
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                SortOrder={Enum.SortOrder.LayoutOrder}
                VerticalAlignment={Enum.VerticalAlignment.Center}
            />
        </frame>
    );
}

export default function AreaBoardGui({
    area,
    children,
    dropletCount = 0,
    dropletLimit = 100,
    placedItemsText = "0",
}: AreaBoardGuiProps) {
    const boardWorldNode = area.boardWorldNode;
    if (boardWorldNode === undefined) throw `Area ${area.id} does not have a board world node.`;
    const gridWorldNode = area.gridWorldNode;
    if (gridWorldNode === undefined) throw `Area ${area.id} does not have a grid world node.`;

    const [gridSize, setGridSize] = useState<Vector3>(new Vector3(0, 0, 0));

    useEffect(() => {
        const gridPart = gridWorldNode.waitForInstance();
        const onSizeChanged = () => {
            setGridSize(gridPart.Size);
        };
        onSizeChanged();
        const connection = gridPart.GetPropertyChangedSignal("Size").Connect(onSizeChanged);
        return () => connection.Disconnect();
    }, [gridWorldNode]);

    const percentageDropletLimit = dropletLimit === 0 ? 0 : dropletCount / dropletLimit;
    let colorSequence: ColorSequence;
    if (percentageDropletLimit < 0.5) {
        colorSequence = new ColorSequence([
            new ColorSequenceKeypoint(0, Color3.fromRGB(0, 255, 0)),
            new ColorSequenceKeypoint(1, Color3.fromRGB(255, 255, 0)),
        ]);
    } else if (percentageDropletLimit < 0.8) {
        colorSequence = new ColorSequence([
            new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 0)),
            new ColorSequenceKeypoint(1, Color3.fromRGB(255, 165, 0)),
        ]);
    } else {
        colorSequence = new ColorSequence([
            new ColorSequenceKeypoint(0, Color3.fromRGB(255, 0, 0)),
            new ColorSequenceKeypoint(1, Color3.fromRGB(139, 0, 0)),
        ]);
    }
    return (
        <surfacegui
            Adornee={boardWorldNode.waitForInstance()}
            ClipsDescendants={true}
            LightInfluence={1}
            MaxDistance={300}
            ResetOnSpawn={false}
            SizingMode={Enum.SurfaceGuiSizingMode.PixelsPerStud}
            ZIndexBehavior={Enum.ZIndexBehavior.Sibling}
        >
            <uilistlayout
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                Padding={new UDim(0, 15)}
                SortOrder={Enum.SortOrder.LayoutOrder}
            />
            <uipadding PaddingLeft={new UDim(0, 25)} PaddingRight={new UDim(0, 25)} PaddingTop={new UDim(0, 25)} />

            {children}
            <AreaStatEntry title="Droplet Count:">
                <ProgressBar
                    current={dropletCount}
                    max={dropletLimit}
                    colorSequence={colorSequence}
                    fontFace={RobotoMonoBold}
                    frameProps={{ Size: new UDim2(0.35, 0, 0.8, 0) }}
                />
            </AreaStatEntry>
            <AreaStatEntry title="Grid Size:">
                <textlabel
                    AnchorPoint={new Vector2(0.5, 0.5)}
                    BackgroundTransparency={1}
                    FontFace={RobotoMonoBold}
                    Position={new UDim2(0.5, 0, 0.5, 0)}
                    Size={new UDim2(0.35, 0, 0.8, 0)}
                    Text={`${gridSize.X} x ${gridSize.Z}`}
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextScaled={true}
                    TextSize={14}
                    TextWrapped={true}
                >
                    <uistroke Thickness={4} />
                </textlabel>
            </AreaStatEntry>
            <AreaStatEntry title="Placed Items:">
                <textlabel
                    AnchorPoint={new Vector2(0.5, 0.5)}
                    BackgroundTransparency={1}
                    FontFace={RobotoMonoBold}
                    Position={new UDim2(0.5, 0, 0.5, 0)}
                    Size={new UDim2(0.35, 0, 0.8, 0)}
                    Text={placedItemsText}
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextScaled={true}
                    TextSize={14}
                    TextWrapped={true}
                >
                    <uistroke Thickness={4} />
                </textlabel>
            </AreaStatEntry>
        </surfacegui>
    );
}

export function SimpleAreaBoardDescription(props: AreaBoardGuiProps) {
    const dampenedKeypoints = props.colorSequence?.Keypoints.map((keypoint) => {
        return new ColorSequenceKeypoint(keypoint.Time, keypoint.Value.Lerp(Color3.fromRGB(255, 255, 255), 0.8));
    });
    const dampenedColorSequence = new ColorSequence(
        dampenedKeypoints ?? [
            new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
            new ColorSequenceKeypoint(1, Color3.fromRGB(255, 255, 255)),
        ],
    );
    return (
        <textlabel
            BackgroundTransparency={1}
            FontFace={RobotoMono}
            Size={new UDim2(0.8, 0, 0.3, 0)}
            Text={props.area.description}
            TextColor3={Color3.fromRGB(237, 237, 237)}
            TextScaled={true}
            TextWrapped={true}
            TextYAlignment={Enum.TextYAlignment.Top}
        >
            <uigradient Color={dampenedColorSequence} Rotation={90} />
            <uistroke Thickness={2} />
            <uitextsizeconstraint MaxTextSize={60} />
            <uipadding PaddingBottom={new UDim(0, 50)} />
        </textlabel>
    );
}

export function SimpleAreaBoardGui(props: AreaBoardGuiProps) {
    return (
        <AreaBoardGui {...props}>
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoSlabHeavy}
                Size={new UDim2(1, 0, 0.15, 0)}
                Text={props.area.name}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextWrapped={true}
            >
                <uigradient Color={props.colorSequence} Rotation={90} />
                <uistroke Color={Color3.fromRGB(0, 0, 0)} Thickness={3} />
            </textlabel>
            <SimpleAreaBoardDescription {...props} />
        </AreaBoardGui>
    );
}

export function EmphasizedHeaderAreaBoardGui(props: AreaBoardGuiProps) {
    return (
        <AreaBoardGui {...props}>
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoSlabHeavy}
                Size={new UDim2(1, 0, 0.1, 0)}
                Text={props.area.name}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextSize={100}
                TextWrapped={true}
            >
                <uiscale Scale={1.5} />
                <uigradient Color={props.colorSequence} Rotation={90} />
                <uistroke Color={Color3.fromRGB(0, 0, 0)} Thickness={3} />
            </textlabel>
            <SimpleAreaBoardDescription {...props} />
        </AreaBoardGui>
    );
}

export function OminousAreaBoardGui(props: AreaBoardGuiProps) {
    return (
        <AreaBoardGui {...props}>
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoSlabHeavy}
                Size={new UDim2(1, 0, 0.1, 0)}
                Text={props.area.name}
                TextColor3={Color3.fromRGB(0, 0, 0)}
                TextSize={100}
                TextWrapped={true}
            >
                <uiscale Scale={2} />
                <uistroke Color={Color3.fromRGB(255, 255, 255)} Thickness={2}>
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(92, 0, 0)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(43, 0, 0)),
                            ])
                        }
                        Rotation={90}
                    />
                </uistroke>
            </textlabel>
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoMonoBold}
                Size={new UDim2(0.8, 0, 0.3, 0)}
                Text={props.area.description}
                TextColor3={Color3.fromRGB(255, 0, 0)}
                TextScaled={true}
                TextWrapped={true}
                TextYAlignment={Enum.TextYAlignment.Top}
            >
                <uistroke Thickness={2} />
            </textlabel>
        </AreaBoardGui>
    );
}

export function SereneAreaBoardGui(props: AreaBoardGuiProps) {
    return (
        <AreaBoardGui {...props}>
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoSlabHeavy}
                Size={new UDim2(1, 0, 0.125, 0)}
                Text={props.area.name}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextSize={100}
                TextWrapped={true}
            >
                <uiscale Scale={1.75} />
                <uigradient Color={props.colorSequence} Rotation={90} />
                <uistroke Color={Color3.fromRGB(0, 0, 0)} Thickness={3} />
            </textlabel>
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoSlabHeavy}
                Size={new UDim2(0.8, 0, 0.3, 0)}
                Text={props.area.description}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextWrapped={true}
                TextYAlignment={Enum.TextYAlignment.Top}
            >
                <uistroke Thickness={4} />
                <uitextsizeconstraint MaxTextSize={100} />
            </textlabel>
        </AreaBoardGui>
    );
}


import React from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import AreaBoardRenderer from "client/components/world/area/AreaBoardRenderer";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
    },
    () => {
        // NOTE: You need to switch to Viewport mode to see this story in the world
        return <AreaBoardRenderer />;
    },
);


/**
 * @fileoverview Main leaderboard component that displays rankings.
 */

import React from "@rbxts/react";
import LeaderboardHeader, { ColumnHeader } from "client/components/world/leaderboard/LeaderboardHeader";
import LeaderboardSlot from "client/components/world/leaderboard/LeaderboardSlot";

/** A single entry in the leaderboard */
export interface LeaderboardEntry {
    /** The player's position on the leaderboard */
    place: number;
    /** The player's name or empire name */
    name: string;
    /** The value being ranked (currency, time, etc.) */
    amount: number;
}

export type LeaderboardType = Currency | "TimePlayed" | "Donated" | "Level";

/** Default titles for different leaderboard types */
const DEFAULT_TITLES: Record<string, string> = {
    TimePlayed: "Time Played Leaders",
    Level: "Highest Levels",
    Funds: "Richest Empires",
    Power: "Most Powerful",
    Skill: "Most Skilled",
    Donated: "Generous Donors",
};

/** Default value labels for different leaderboard types */
const DEFAULT_VALUE_LABELS: Record<string, string> = {
    TimePlayed: "Hours",
    Level: "Level",
    Funds: "Funds",
    Power: "Power",
    Skill: "Skill",
    Donated: "Donated",
};

/**
 * Complete leaderboard display with header and ranked entries.
 */
export default function Leaderboard({
    leaderboardType,
    entries,
    title,
    valueLabel,
    maxEntries = 100,
}: {
    /** The type/title of the leaderboard */
    leaderboardType: LeaderboardType;
    /** Array of leaderboard entries to display */
    entries: LeaderboardEntry[];
    /** Title of the leaderboard */
    title?: string;
    /** Custom value label (e.g., "Funds", "Hours", "Level") */
    valueLabel?: string;
    /** Maximum entries to display */
    maxEntries?: number;
}) {
    const displayTitle = title || DEFAULT_TITLES[leaderboardType] || `${leaderboardType} Leaderboard`;
    const displayValueLabel = valueLabel || DEFAULT_VALUE_LABELS[leaderboardType] || leaderboardType;

    // Limit entries if maxEntries is specified
    const displayEntries: LeaderboardEntry[] = [];
    for (let i = 0; i < entries.size() && i < maxEntries; i++) {
        displayEntries.push(entries[i]);
    }

    let gradient: ColorSequence;
    switch (leaderboardType) {
        case "Funds":
            gradient = new ColorSequence([
                new ColorSequenceKeypoint(0, Color3.fromRGB(170, 255, 144)),
                new ColorSequenceKeypoint(1, Color3.fromRGB(0, 68, 34)),
            ]);
            break;
        case "Power":
            gradient = new ColorSequence([
                new ColorSequenceKeypoint(0, Color3.fromRGB(255, 155, 33)),
                new ColorSequenceKeypoint(1, Color3.fromRGB(221, 74, 0)),
            ]);
            break;
        case "Skill":
            gradient = new ColorSequence([
                new ColorSequenceKeypoint(0, Color3.fromRGB(85, 170, 127)),
                new ColorSequenceKeypoint(1, Color3.fromRGB(85, 255, 127)),
            ]);
            break;
        case "TimePlayed":
            gradient = new ColorSequence([
                new ColorSequenceKeypoint(0, Color3.fromRGB(152, 202, 255)),
                new ColorSequenceKeypoint(1, Color3.fromRGB(0, 170, 255)),
            ]);
            break;
        case "Level":
            gradient = new ColorSequence([
                new ColorSequenceKeypoint(0, Color3.fromRGB(0, 170, 255)),
                new ColorSequenceKeypoint(1, Color3.fromRGB(0, 170, 255)),
            ]);
            break;
        case "Donated":
            gradient = new ColorSequence([
                new ColorSequenceKeypoint(0, Color3.fromRGB(255, 123, 152)),
                new ColorSequenceKeypoint(1, Color3.fromRGB(255, 85, 127)),
            ]);
            break;
        default:
            gradient = new ColorSequence([
                new ColorSequenceKeypoint(0, Color3.fromRGB(255, 0, 0)),
                new ColorSequenceKeypoint(1, Color3.fromRGB(221, 74, 0)),
            ]);
    }

    return (
        <frame BackgroundColor3={Color3.fromRGB(255, 255, 255)} Size={new UDim2(1, 0, 1, 0)}>
            <LeaderboardHeader title={displayTitle} gradient={gradient} />
            <scrollingframe
                key="Display"
                AutomaticCanvasSize={Enum.AutomaticSize.Y}
                BackgroundTransparency={1}
                BorderSizePixel={0}
                CanvasSize={new UDim2(0, 0, 0, 0)}
                Position={new UDim2(0, 0, 0.12, 0)}
                Selectable={false}
                ScrollBarImageColor3={Color3.fromRGB(79, 79, 79)}
                Size={new UDim2(1, 0, 0.8, 0)}
                ZIndex={-1}
            >
                <uilistlayout Padding={new UDim(0, 5)} SortOrder={Enum.SortOrder.LayoutOrder} />
                <uipadding
                    PaddingBottom={new UDim(0, 15)}
                    PaddingLeft={new UDim(0, 15)}
                    PaddingRight={new UDim(0, 15)}
                    PaddingTop={new UDim(0, 15)}
                />
                <ColumnHeader valueLabel={displayValueLabel} />

                {displayEntries.map((entry: LeaderboardEntry) => (
                    <LeaderboardSlot key={`entry-${entry.place}-${entry.name}`} entry={entry} />
                ))}
            </scrollingframe>
        </frame>
    );
}