/**
 * @fileoverview React component for sidebar navigation buttons
 *
 * Modern React implementation of the sidebar buttons system, replacing the
 * monolithic Roblox Studio UI with maintainable React components.
 *
 * Features:
 * - Individual button components for modularity
 * - State management for visibility and notifications
 * - Smooth animations and transitions
 * - Hotkey integration
 * - Notification badges
 * - Glow effects for active states
 */

import React, { useCallback, useEffect, useRef, useState } from "@rbxts/react";
import { TweenService } from "@rbxts/services";
import { HotkeyLabel } from "client/components/hotkeys/HotkeyManager";
import useHotkeyWithTooltip from "client/components/hotkeys/useHotkeyWithTooltip";
import SingleDocumentManager from "client/components/sidebar/SingleDocumentManager";
import { useDocument } from "client/components/window/DocumentManager";
import { getAsset } from "shared/asset/AssetMap";
import { RobotoSlabBold } from "shared/asset/GameFonts";

interface SidebarButtonProps {
    /** Button configuration data */
    data: SidebarButtonData;
    /** Layout order for positioning */
    layoutOrder: number;
    /** Click handler */
    onClick: () => void;
    /** Whether animations are enabled */
    animationsEnabled?: boolean;
}

interface SidebarButtonsProps {
    /** Whether the sidebar is visible */
    visible?: boolean;
    /** Position of the sidebar */
    position?: UDim2;
    /** Whether animations are enabled */
    animationsEnabled?: boolean;
    /** Array of button configurations  */
    buttons?: SidebarButtonData[];
}

interface SidebarButtonData {
    label: HotkeyLabel;
    image: string;
    color: Color3;
    visible: boolean;
    notification?: {
        count: number;
        color: Color3;
    };
}

export const SidebarButtonConfiguration = [
    {
        label: "Quests",
        image: getAsset("assets/Quests.png"),
        color: Color3.fromRGB(255, 94, 94),
        visible: true,
        notification: { count: 0, color: Color3.fromRGB(255, 52, 52) },
    },
    {
        label: "Inventory",
        image: getAsset("assets/Inventory.png"),
        color: Color3.fromRGB(255, 186, 125),
        visible: true,
    },
] as SidebarButtonData[];

/**
 * Individual sidebar button component
 */
export function SidebarButton({ data, layoutOrder, onClick, animationsEnabled = true }: SidebarButtonProps) {
    const [isAnimating, setIsAnimating] = useState(false);
    const frameRef = useRef<Frame>();
    const textButtonRef = useRef<TextButton>();

    const animatePress = useCallback(() => {
        if (!animationsEnabled || isAnimating) return;

        setIsAnimating(true);

        // Find the button element to animate
        let buttonElement: GuiObject | undefined;

        if (frameRef.current) {
            buttonElement = frameRef.current.FindFirstChild("Button") as GuiObject | undefined;
        } else if (textButtonRef.current) {
            buttonElement = textButtonRef.current;
        }

        if (!buttonElement) {
            setIsAnimating(false);
            return;
        }

        buttonElement.Size = new UDim2(0.85, 0, 0.85, 0);
        const releaseTween = TweenService.Create(buttonElement, new TweenInfo(0.2), {
            Size: new UDim2(1, 0, 1, 0),
        });
        releaseTween.Play();

        // When release completes, reset animation state
        releaseTween.Completed.Connect(() => {
            setIsAnimating(false);
        });
    }, [animationsEnabled, isAnimating]);

    const handleClick = useCallback(() => {
        if (animationsEnabled) {
            animatePress();
        }
        onClick();
    }, [onClick, animationsEnabled, animatePress]);

    const { events } = useHotkeyWithTooltip({
        action: () => {
            if (!data.visible) return false;
            handleClick();
            return true;
        },
        label: data.label,
    });

    // Calculate animated size
    const animatedSize = new UDim2(1, 0, 1, 0); // Special handling for different button types
    return (
        <frame
            key={data.label}
            ref={frameRef}
            BackgroundTransparency={1}
            LayoutOrder={layoutOrder}
            Size={new UDim2(1, 0, 1, -20)}
            SizeConstraint={Enum.SizeConstraint.RelativeXX}
        >
            <imagebutton
                key="Button"
                AnchorPoint={new Vector2(0.5, 0.5)}
                AutoButtonColor={false}
                BackgroundTransparency={1}
                Image={data.image}
                LayoutOrder={1}
                Position={new UDim2(0.5, 0, 0.5, 0)}
                Selectable={false}
                Size={animatedSize}
                Event={events}
            >
                <uiaspectratioconstraint
                    AspectType={Enum.AspectType.ScaleWithParentSize}
                    DominantAxis={Enum.DominantAxis.Height}
                />
            </imagebutton>

            {/* Glow effect */}
            <frame
                AnchorPoint={new Vector2(0.5, 0.5)}
                BackgroundColor3={data.color}
                BorderSizePixel={0}
                Position={new UDim2(0, 0, 0.5, 0)}
                Size={new UDim2(0.25, 0, 0.25, 0)}
                SizeConstraint={Enum.SizeConstraint.RelativeYY}
                ZIndex={0}
            >
                <uistroke ApplyStrokeMode={Enum.ApplyStrokeMode.Border} Thickness={2} />
                <uicorner CornerRadius={new UDim(0.5, 0)} />
            </frame>

            {/* Notification badge for Quests */}
            {data.notification && data.notification.count > 0 && (
                <frame
                    AnchorPoint={new Vector2(1, 0.5)}
                    AutomaticSize={Enum.AutomaticSize.XY}
                    BackgroundColor3={Color3.fromRGB(255, 255, 255)}
                    BorderColor3={Color3.fromRGB(0, 0, 0)}
                    BorderSizePixel={2}
                    LayoutOrder={2}
                    Position={new UDim2(1, -3, 0, 3)}
                >
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, data.notification.color),
                                new ColorSequenceKeypoint(
                                    1,
                                    data.notification.color.Lerp(Color3.fromRGB(255, 11, 105), 0.5),
                                ),
                            ])
                        }
                        Rotation={90}
                    />
                    <textlabel
                        AutomaticSize={Enum.AutomaticSize.X}
                        BackgroundTransparency={1}
                        FontFace={RobotoSlabBold}
                        Size={new UDim2(0, 0, 0, 16)}
                        Text={tostring(data.notification.count)}
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextSize={16}
                        TextWrapped={true}
                    />
                    <uipadding PaddingLeft={new UDim(0, 6)} PaddingRight={new UDim(0, 6)} />
                    <uistroke Thickness={2} />
                    <uicorner CornerRadius={new UDim(0.5, 0)} />
                </frame>
            )}
        </frame>
    );
}

/**
 * Main sidebar buttons container component
 */
export default function SidebarButtons({
    position = new UDim2(0, 0, 0.5, 0),
    animationsEnabled = true,
    buttons = SidebarButtonConfiguration,
}: SidebarButtonsProps) {
    const sidebarRef = useRef<Frame>();
    const size = new UDim2(0.025, 40, 0.5, 0);
    const sizeX = new UDim2(size.Width, new UDim(0, 0));
    const closePos = position.sub(sizeX);
    const { visible } = useDocument({ id: "Sidebar", priority: -1 });

    useEffect(() => {
        if (visible) {
            sidebarRef.current?.TweenPosition(position, Enum.EasingDirection.Out, Enum.EasingStyle.Quart, 1, true);
        } else {
            sidebarRef.current?.TweenPosition(closePos, Enum.EasingDirection.In, Enum.EasingStyle.Quart, 1, true);
        }
    }, [visible]);

    return (
        <frame
            key="SidebarButtons"
            ref={sidebarRef}
            AnchorPoint={new Vector2(0, 0.5)}
            BackgroundTransparency={1}
            Position={closePos}
            Size={size}
            ZIndex={-1}
        >
            <uilistlayout
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                Padding={new UDim(0.012, 8)}
                SortOrder={Enum.SortOrder.LayoutOrder}
                VerticalAlignment={Enum.VerticalAlignment.Center}
            />

            {buttons
                .filter((button) => button.visible)
                .map((button, index) => (
                    <SidebarButton
                        key={button.label}
                        data={button}
                        layoutOrder={index + 1}
                        onClick={() => SingleDocumentManager.toggle(button.label)}
                        animationsEnabled={animationsEnabled}
                    />
                ))}
        </frame>
    );
}


import { useEffect } from "@rbxts/react";
import SingleDocumentManager from "client/components/sidebar/SingleDocumentManager";
import { useDocument } from "client/components/window/DocumentManager";

/**
 * A custom hook for managing window visibility state with the SingleDocumentManager.
 * @param windowName The name of the window to manage visibility for
 * @returns An object containing the current visibility state and helper functions
 */
export default function useSingleDocument({
    id,
    defaultVisible = false,
    priority,
    onClose,
    onOpen,
}: {
    /** Unique identifier for the document */
    id: string;
    /** Initial visibility state of the document */
    defaultVisible?: boolean;
    /** Priority for closing documents (higher priority closes first) */
    priority?: number;
    /** Optional callback after the document is closed. */
    onClose?: () => void;
    /** Optional callback after the document is opened. */
    onOpen?: () => void;
}) {
    const { visible, setVisible } = useDocument({ id, defaultVisible, priority });

    useEffect(() => {
        if (visible) {
            SingleDocumentManager.open(id);
            onOpen?.();
        } else {
            SingleDocumentManager.close(id);
            onClose?.();
        }
    }, [visible]);

    return {
        id,
        visible,
        closeDocument: () => setVisible(false),
        openDocument: () => setVisible(true),
        toggleDocument: () => setVisible(!visible),
    };
}


import React, { useCallback, useEffect, useRef, useState } from "@rbxts/react";
import { TweenService } from "@rbxts/services";
import { RobotoMono, RobotoSlabBold, RobotoSlabExtraBold, RobotoSlabMedium } from "shared/asset/GameFonts";
import { getAsset } from "shared/asset/AssetMap";
import { playSound } from "shared/asset/GameAssets";
import Items from "shared/items/Items";

/**
 * About window component with game information and credits
 */
export default function AboutWindow({
    onClose,
}: {
    /** Close callback */
    onClose: () => void;
}) {
    const scrollFrameRef = useRef<ScrollingFrame>();
    const [hasAnimated, setHasAnimated] = useState(false);

    const contributors = new Set<string>();
    for (const item of Items.sortedItems) {
        if (item.creator !== undefined) contributors.add(item.creator);
    }

    // Entrance animation
    useEffect(() => {
        if (!hasAnimated && scrollFrameRef.current) {
            setHasAnimated(true);

            // Start the window below the screen
            scrollFrameRef.current.Position = new UDim2(0, 0, 1.5, 0);

            // Animate sliding up to final position
            const slideUpTween = TweenService.Create(
                scrollFrameRef.current,
                new TweenInfo(0.6, Enum.EasingStyle.Quart, Enum.EasingDirection.Out),
                { Position: new UDim2(0, 0, 0.5, 0) },
            );
            slideUpTween.Play();
        }
    }, [hasAnimated]);

    const handleClose = useCallback(() => {
        playSound("MenuClose.mp3");

        // Animate sliding down before closing
        if (scrollFrameRef.current) {
            const slideDownTween = TweenService.Create(
                scrollFrameRef.current,
                new TweenInfo(0.4, Enum.EasingStyle.Quart, Enum.EasingDirection.In),
                { Position: new UDim2(0, 0, 1.5, 0) },
            );

            slideDownTween.Play();
            slideDownTween.Completed.Connect(() => {
                onClose();
            });
        } else {
            // Fallback if ref is not available
            onClose();
        }
    }, [onClose]);

    return (
        <scrollingframe
            ref={scrollFrameRef}
            AnchorPoint={new Vector2(0, 0.5)}
            AutomaticCanvasSize={Enum.AutomaticSize.Y}
            BackgroundColor3={Color3.fromRGB(25, 25, 35)}
            BackgroundTransparency={0.6}
            BorderSizePixel={0}
            CanvasSize={new UDim2(0, 0, 0, 0)}
            Position={new UDim2(0, 0, 0.5, 0)}
            Size={new UDim2(1, 0, 1, -10)}
            ScrollBarThickness={8}
            ScrollBarImageColor3={Color3.fromRGB(85, 170, 255)}
            ZIndex={10}
        >
            {/* Background styling */}
            <uigradient
                Color={
                    new ColorSequence([
                        new ColorSequenceKeypoint(0, Color3.fromRGB(35, 35, 45)),
                        new ColorSequenceKeypoint(1, Color3.fromRGB(25, 25, 35)),
                    ])
                }
                Rotation={135}
            />

            {/* Content Layout */}
            <uilistlayout
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                ItemLineAlignment={Enum.ItemLineAlignment.Center}
                Padding={new UDim(0, 50)}
                SortOrder={Enum.SortOrder.LayoutOrder}
            />

            {/* Close Button */}
            <imagebutton
                BackgroundTransparency={1}
                Image={getAsset("assets/NPCNotification.png")}
                ScaleType={Enum.ScaleType.Fit}
                Size={new UDim2(1, 0, 0, 35)}
                Event={{ Activated: handleClose }}
            />

            {/* Logo */}
            <imagelabel
                AnchorPoint={new Vector2(0.5, 0)}
                BackgroundTransparency={1}
                Image={getAsset("assets/brand/Wordmark.png")}
                Position={new UDim2(0.15, 0, 0, 0)}
                ScaleType={Enum.ScaleType.Fit}
                Size={new UDim2(1, 0, 0.25, 0)}
            />
            <textlabel
                Active={true}
                AutomaticSize={Enum.AutomaticSize.Y}
                BackgroundTransparency={1}
                FontFace={RobotoSlabMedium}
                Size={new UDim2(0.65, 0, 0, 0)}
                Text="Build your money-making empire. Uncover the world of Obbysia."
                TextColor3={Color3.fromRGB(199, 199, 199)}
                TextSize={20}
            >
                <uistroke Thickness={1.5} />
                <uipadding PaddingTop={new UDim(0, -25)} />
            </textlabel>

            {/* Game Description */}
            <textlabel
                Active={true}
                AutomaticSize={Enum.AutomaticSize.Y}
                BackgroundTransparency={1}
                FontFace={RobotoSlabMedium}
                Size={new UDim2(0.65, 0, 0, 0)}
                Text="Eternal Empire is an incremental-style tycoon building game designed off the EToH Joke Towers Difficulty Chart. Containing various MMO, RPG and Clicker aspects, how will you rise to the top of this capitalistic world?"
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextSize={30}
                TextWrapped={true}
            >
                <uistroke Thickness={2.5} />
            </textlabel>

            {/* Credits Section Header */}
            <textlabel
                Active={true}
                AutomaticSize={Enum.AutomaticSize.Y}
                BackgroundTransparency={1}
                FontFace={RobotoSlabExtraBold}
                Size={new UDim2(0.65, 0, 0, 0)}
                Text="Credits"
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextSize={60}
                TextWrapped={true}
            >
                <uistroke Thickness={2.5} />
            </textlabel>

            {/* Separator */}
            <frame
                BackgroundColor3={Color3.fromRGB(85, 170, 255)}
                BackgroundTransparency={0.3}
                BorderSizePixel={0}
                Size={new UDim2(0.5, 0, 0, 2)}
            >
                <uicorner CornerRadius={new UDim(0, 1)} />
            </frame>

            {/* Creator Section */}
            <frame Active={true} BackgroundTransparency={1} Size={new UDim2(0.7, 0, 0, 75)}>
                <uilistlayout
                    FillDirection={Enum.FillDirection.Horizontal}
                    HorizontalAlignment={Enum.HorizontalAlignment.Center}
                    ItemLineAlignment={Enum.ItemLineAlignment.Center}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                />

                <frame BackgroundTransparency={1} Size={new UDim2(0.5, 0, 1, 0)}>
                    <textlabel
                        Active={true}
                        BackgroundTransparency={1}
                        FontFace={RobotoSlabBold}
                        LayoutOrder={2}
                        Size={new UDim2(1, 0, 0.35, 0)}
                        Text="Creator & Developer"
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextScaled={true}
                        TextSize={50}
                        TextWrapped={true}
                    >
                        <uistroke Thickness={2.5} />
                    </textlabel>
                    <textlabel
                        Active={true}
                        BackgroundTransparency={1}
                        FontFace={RobotoSlabBold}
                        Size={new UDim2(1, 0, 0.645, 0)}
                        Text="migeru_tan"
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextScaled={true}
                        TextSize={50}
                        TextWrapped={true}
                    >
                        <uistroke Thickness={2.5} />
                        <uigradient
                            Color={
                                new ColorSequence([
                                    new ColorSequenceKeypoint(0, Color3.fromRGB(255, 130, 71)),
                                    new ColorSequenceKeypoint(1, Color3.fromRGB(255, 59, 59)),
                                ])
                            }
                            Rotation={90}
                        />
                    </textlabel>
                    <uilistlayout
                        HorizontalAlignment={Enum.HorizontalAlignment.Center}
                        ItemLineAlignment={Enum.ItemLineAlignment.Center}
                        SortOrder={Enum.SortOrder.LayoutOrder}
                    />
                </frame>
            </frame>

            {/* Team Section */}
            <frame Active={true} BackgroundTransparency={1} Size={new UDim2(0.7, 0, 0, 75)}>
                <uilistlayout
                    FillDirection={Enum.FillDirection.Horizontal}
                    HorizontalAlignment={Enum.HorizontalAlignment.Center}
                    ItemLineAlignment={Enum.ItemLineAlignment.Center}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                />

                {/* Music Composer */}
                <frame BackgroundTransparency={1} Size={new UDim2(0.5, 0, 1, 0)}>
                    <textlabel
                        Active={true}
                        BackgroundTransparency={1}
                        FontFace={RobotoSlabBold}
                        LayoutOrder={2}
                        Size={new UDim2(1, 0, 0.35, 0)}
                        Text="Music Composer"
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextScaled={true}
                        TextSize={50}
                        TextWrapped={true}
                    >
                        <uistroke Thickness={2.5} />
                    </textlabel>
                    <textlabel
                        Active={true}
                        BackgroundTransparency={1}
                        FontFace={RobotoSlabBold}
                        Size={new UDim2(1, 0, 0.645, 0)}
                        Text="raika"
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextScaled={true}
                        TextSize={50}
                        TextWrapped={true}
                    >
                        <uistroke Thickness={2.5} />
                        <uigradient
                            Color={
                                new ColorSequence([
                                    new ColorSequenceKeypoint(0, Color3.fromRGB(245, 186, 255)),
                                    new ColorSequenceKeypoint(1, Color3.fromRGB(209, 97, 255)),
                                ])
                            }
                            Rotation={90}
                        />
                    </textlabel>
                    <uilistlayout
                        HorizontalAlignment={Enum.HorizontalAlignment.Center}
                        ItemLineAlignment={Enum.ItemLineAlignment.Center}
                        SortOrder={Enum.SortOrder.LayoutOrder}
                    />
                </frame>
            </frame>

            {/* Testers */}
            <frame Active={true} BackgroundTransparency={1} Size={new UDim2(0.7, 0, 0, 55)}>
                <textlabel
                    Active={true}
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabBold}
                    Size={new UDim2(1, 0, 0.645, 0)}
                    Text="CREATIVITEEE, KillerFish_SD, Bernario, simple13579"
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextScaled={true}
                    TextSize={50}
                    TextWrapped={true}
                >
                    <uistroke Thickness={2.5} />
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(255, 248, 147)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(255, 255, 0)),
                            ])
                        }
                        Rotation={90}
                    />
                </textlabel>
                <textlabel
                    Active={true}
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabBold}
                    LayoutOrder={2}
                    Size={new UDim2(1, 0, 0.35, 0)}
                    Text="Testers"
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextScaled={true}
                    TextSize={50}
                    TextWrapped={true}
                >
                    <uistroke Thickness={2.5} />
                </textlabel>
                <uilistlayout
                    HorizontalAlignment={Enum.HorizontalAlignment.Center}
                    ItemLineAlignment={Enum.ItemLineAlignment.Center}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                />
            </frame>

            {/* Contributors */}
            <frame Active={true} BackgroundTransparency={1} Size={new UDim2(0.7, 0, 0, 55)}>
                <textlabel
                    Active={true}
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabBold}
                    Size={new UDim2(1, 0, 0.645, 0)}
                    Text={[...contributors].sort((a, b) => a < b).join(", ")}
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextScaled={true}
                    TextSize={50}
                    TextWrapped={true}
                >
                    <uistroke Thickness={2.5} />
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(255, 116, 169)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(255, 39, 136)),
                            ])
                        }
                        Rotation={90}
                    />
                </textlabel>
                <textlabel
                    Active={true}
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabBold}
                    Size={new UDim2(1, 0, 0.35, 0)}
                    Text="Contributors"
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextScaled={true}
                    TextSize={50}
                    TextWrapped={true}
                >
                    <uistroke Thickness={2.5} />
                </textlabel>
                <uilistlayout
                    HorizontalAlignment={Enum.HorizontalAlignment.Center}
                    ItemLineAlignment={Enum.ItemLineAlignment.Center}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                />
            </frame>

            {/* Thank You Messages */}
            <textlabel
                Active={true}
                AutomaticSize={Enum.AutomaticSize.Y}
                BackgroundTransparency={1}
                FontFace={RobotoSlabBold}
                Size={new UDim2(0.65, 0, 0, 0)}
                Text="To each and every supporter, my warmest thank you!"
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextSize={40}
                TextWrapped={true}
            >
                <uistroke Thickness={2.5} />
            </textlabel>

            <textlabel
                Active={true}
                AutomaticSize={Enum.AutomaticSize.Y}
                BackgroundTransparency={1}
                FontFace={RobotoMono}
                Size={new UDim2(0.65, 0, 0, 0)}
                Text="That includes you. <3"
                TextColor3={Color3.fromRGB(162, 162, 162)}
                TextSize={30}
                TextWrapped={true}
            >
                <uistroke Thickness={2.5} />
            </textlabel>

            {/* Padding */}
            <uipadding PaddingBottom={new UDim(0, 50)} />
        </scrollingframe>
    );
}


import ComputeNameColor from "@antivivi/rbxnamecolor";
import { convertToHHMMSS } from "@antivivi/vrldk";
import React, { JSX, useCallback, useEffect, useRef, useState } from "@rbxts/react";
import { Players, StarterGui } from "@rbxts/services";
import MenuOption, { BaseMenuOption } from "client/components/start/MenuOption";
import { showErrorToast } from "client/components/toast/ToastService";
import useProperty from "client/hooks/useProperty";
import { playSound } from "shared/asset/GameAssets";
import { RobotoSlab, RobotoSlabBold, RobotoSlabHeavy } from "shared/asset/GameFonts";
import { getNameFromUserId } from "shared/constants";
import { IS_EDIT } from "shared/Context";
import eat from "shared/hamster/eat";
import Packets from "shared/Packets";
import LoadingScreen from "sharedfirst/LoadingScreen";

/**
 * Individual empire option component
 */
function EmpireOption({ empireId, empireInfo }: { empireId: string; empireInfo: EmpireInfo }) {
    const [avatarImage, setAvatarImage] = useState("");

    const color = empireInfo.name
        ? (ComputeNameColor(empireInfo.name) ?? Color3.fromRGB(0, 170, 0))
        : Color3.fromRGB(0, 170, 0);

    // Load avatar asynchronously
    useEffect(() => {
        if (empireInfo.owner) {
            task.spawn(() => {
                try {
                    const [thumbnail] = Players.GetUserThumbnailAsync(
                        empireInfo.owner,
                        Enum.ThumbnailType.HeadShot,
                        Enum.ThumbnailSize.Size150x150,
                    );
                    setAvatarImage(thumbnail);
                } catch (error) {
                    warn("Failed to load avatar:", error);
                }
            });
        }
    }, [empireInfo.owner]);

    const textBorderGradient = new ColorSequence([
        new ColorSequenceKeypoint(0, Color3.fromRGB(122, 122, 122)),
        new ColorSequenceKeypoint(1, Color3.fromRGB(51, 51, 51)),
    ]);

    return (
        <BaseMenuOption
            gradientColors={[color, color.Lerp(Color3.fromRGB(0, 0, 0), 0.5)]}
            size={new UDim2(0.12, 500, 0, 120)}
            onClick={() => {
                playSound("MenuClick.mp3");
                let screenGui: ScreenGui | undefined;
                if (IS_EDIT) {
                    screenGui = new Instance("ScreenGui");
                    screenGui.IgnoreGuiInset = true;
                    screenGui.ResetOnSpawn = false;
                    screenGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling;
                    screenGui.Parent = StarterGui;
                    eat(screenGui, "Destroy");
                }
                LoadingScreen.showLoadingScreen("", false, screenGui);
                const [success, result] = pcall(() => Packets.teleportToEmpire.toServer(empireId));
                if (!success || !result) {
                    LoadingScreen.hideLoadingScreen();
                    showErrorToast(`Failed to join empire: ${result ?? "Unknown error"}`);
                    playSound("Error.mp3");
                }
            }}
            fast={true}
        >
            <frame BackgroundTransparency={1} Size={new UDim2(0.65, 0, 1, 0)}>
                <uilistlayout
                    FillDirection={Enum.FillDirection.Horizontal}
                    Padding={new UDim(0.05, 0)}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                    VerticalAlignment={Enum.VerticalAlignment.Center}
                />
                <imagelabel
                    BackgroundTransparency={1}
                    Image={avatarImage}
                    Size={new UDim2(0.75, 0, 0.75, 0)}
                    SizeConstraint={Enum.SizeConstraint.RelativeYY}
                />
                <frame BackgroundTransparency={1} Size={new UDim2(0.65, 0, 1, -10)}>
                    <uilistlayout
                        HorizontalAlignment={Enum.HorizontalAlignment.Center}
                        SortOrder={Enum.SortOrder.LayoutOrder}
                        VerticalAlignment={Enum.VerticalAlignment.Center}
                    />
                    <textlabel
                        Active={true}
                        AnchorPoint={new Vector2(0.5, 0)}
                        BackgroundTransparency={1}
                        FontFace={RobotoSlabBold}
                        Position={new UDim2(0.5, 0, 0.25, 0)}
                        Size={new UDim2(1, 0, 0.4, 0)}
                        Text={empireInfo.name}
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextScaled={true}
                        TextSize={14}
                        TextWrapped={true}
                        TextXAlignment={Enum.TextXAlignment.Left}
                    >
                        <uistroke Color={color} Thickness={3}>
                            <uigradient Color={textBorderGradient} Rotation={90} />
                        </uistroke>
                    </textlabel>
                    <textlabel
                        Active={true}
                        AnchorPoint={new Vector2(0.5, 0)}
                        BackgroundTransparency={1}
                        FontFace={RobotoSlabBold}
                        Position={new UDim2(0.5, 0, 0, 0)}
                        Size={new UDim2(1, 0, 0.25, 0)}
                        Text={
                            empireInfo.owner ? "Owned by " + getNameFromUserId(empireInfo.owner) : "could not load info"
                        }
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextScaled={true}
                        TextSize={14}
                        TextWrapped={true}
                        TextXAlignment={Enum.TextXAlignment.Left}
                    >
                        <uistroke Color={color} Thickness={2}>
                            <uigradient Color={textBorderGradient} Rotation={90} />
                        </uistroke>
                    </textlabel>
                </frame>
                <frame AnchorPoint={new Vector2(1, 0.5)} BackgroundTransparency={1} Size={new UDim2(0.3, 0, 0.8, -10)}>
                    <uilistlayout
                        HorizontalAlignment={Enum.HorizontalAlignment.Left}
                        Padding={new UDim(0, 0)}
                        SortOrder={Enum.SortOrder.LayoutOrder}
                        VerticalAlignment={Enum.VerticalAlignment.Bottom}
                    />
                    <textlabel
                        Active={true}
                        AnchorPoint={new Vector2(0.5, 0)}
                        BackgroundTransparency={1}
                        FontFace={RobotoSlab}
                        Position={new UDim2(0.5, 0, 0.25, 0)}
                        Size={new UDim2(1, 0, 0.15, 0)}
                        Text={`Created ${os.date("%x", empireInfo.created)}`}
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextScaled={true}
                        TextSize={14}
                        TextXAlignment={Enum.TextXAlignment.Left}
                        TextWrapped={true}
                    >
                        <uistroke Color={color} Thickness={2}>
                            <uigradient Color={textBorderGradient} Rotation={90} />
                        </uistroke>
                    </textlabel>
                    <textlabel
                        Active={true}
                        AnchorPoint={new Vector2(0.5, 0)}
                        BackgroundTransparency={1}
                        FontFace={RobotoSlab}
                        Position={new UDim2(0.5, 0, 0.25, 0)}
                        Size={new UDim2(1, 0, 0.15, 0)}
                        Text={`Played for ${convertToHHMMSS(empireInfo.playtime)}`}
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextScaled={true}
                        TextXAlignment={Enum.TextXAlignment.Left}
                        TextWrapped={true}
                    >
                        <uistroke Color={color} Thickness={2}>
                            <uigradient Color={textBorderGradient} Rotation={90} />
                        </uistroke>
                    </textlabel>
                </frame>
            </frame>
            <textlabel
                Active={true}
                AnchorPoint={new Vector2(0.5, 1)}
                BackgroundTransparency={1}
                FontFace={RobotoSlab}
                Position={new UDim2(0.5, 0, 0.95, 0)}
                Size={new UDim2(1, 0, 0.01, 15)}
                Text={empireId}
                TextColor3={Color3.fromRGB(156, 156, 156)}
                TextScaled={true}
                TextSize={14}
                TextTransparency={0.9}
                TextWrapped={true}
            />
        </BaseMenuOption>
    );
}

/**
 * Empires window component for empire selection and creation
 */
export default function EmpiresWindow({ exitStart, onClose }: { exitStart: () => void; onClose: () => void }) {
    const labelRef = useRef<TextLabel>();
    const availableEmpires = useProperty(Packets.availableEmpires);
    const [isCreatingEmpire, setIsCreatingEmpire] = useState(false);

    const handleCreateEmpire = useCallback(() => {
        if (isCreatingEmpire) return;

        setIsCreatingEmpire(true);
        const success = Packets.createNewEmpire.toServer();
        if (success) {
            playSound("MenuClick.mp3");
        } else {
            playSound("Error.mp3");
            showErrorToast("Failed to create a new empire.");
        }

        // Reset state after delay
        task.delay(3, () => setIsCreatingEmpire(false));
    }, [isCreatingEmpire]);

    return (
        <frame BackgroundTransparency={1} Position={new UDim2(0, 0, 0, 0)} Size={new UDim2(1, 0, 1, 0)}>
            <uilistlayout
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                SortOrder={Enum.SortOrder.LayoutOrder}
                VerticalAlignment={Enum.VerticalAlignment.Center}
                VerticalFlex={Enum.UIFlexAlignment.Fill}
            />

            <frame BackgroundTransparency={1} Size={new UDim2(1, 0, 0.05, 0)}>
                <uiflexitem FlexMode={Enum.UIFlexMode.Shrink} />
            </frame>

            <MenuOption
                label="Back"
                gradientColors={[Color3.fromRGB(255, 224, 84), Color3.fromRGB(171, 105, 5)]}
                onClick={() => {
                    playSound("EmphasisMenuSelect.mp3", undefined, (sound) => (sound.Volume = 0.4));
                    onClose();
                }}
                height={50}
                animationDelay={0}
                fast={true}
            />

            <frame BackgroundTransparency={1} Size={new UDim2(1, 0, 0.05, 0)} />

            {/* Create New Empire Option */}
            <MenuOption
                label={isCreatingEmpire ? "Creating empire..." : "New Empire"}
                gradientColors={[Color3.fromRGB(255, 158, 250), Color3.fromRGB(235, 48, 209)]}
                onClick={() => {
                    playSound("EmphasisMenuSelect.mp3", undefined, (sound) => (sound.Volume = 0.4));
                    handleCreateEmpire();
                }}
                height={70}
                animationDelay={0}
                fast={true}
            />

            {/* Empire Options Scrolling Frame */}
            <scrollingframe
                Active={true}
                BackgroundColor3={Color3.fromRGB(15, 15, 25)}
                BackgroundTransparency={1}
                BorderSizePixel={0}
                Size={new UDim2(1, 0, 0.65, 0)}
                ScrollBarThickness={6}
                ScrollBarImageColor3={Color3.fromRGB(85, 170, 255)}
                CanvasSize={new UDim2(0, 0, 0, 0)}
                AutomaticCanvasSize={Enum.AutomaticSize.Y}
            >
                <uilistlayout HorizontalAlignment={Enum.HorizontalAlignment.Left} SortOrder={Enum.SortOrder.Name} />

                {/* Empire Options */}
                {(() => {
                    const empireElements = new Array<JSX.Element>();
                    for (const [empireId, empireInfo] of availableEmpires) {
                        empireElements.push(
                            <EmpireOption key={empireId} empireId={empireId} empireInfo={empireInfo} />,
                        );
                    }
                    return empireElements;
                })()}
            </scrollingframe>

            <frame BackgroundTransparency={1} Size={new UDim2(1, 0, 0, 0)}>
                <uiflexitem FlexMode={Enum.UIFlexMode.Fill} />
            </frame>

            {/* Public Empire Window */}
            <BaseMenuOption
                gradientColors={[Color3.fromRGB(84, 255, 159), Color3.fromRGB(5, 171, 105)]}
                onClick={() => {
                    playSound("EmphasisMenuSelect.mp3", undefined, (sound) => (sound.Volume = 0.4));
                    exitStart();
                }}
                size={new UDim2(0.1, 400, 0, 60)}
                fast={true}
            >
                {/* Label */}
                <textlabel
                    ref={labelRef}
                    Active={true}
                    AutomaticSize={Enum.AutomaticSize.X}
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabHeavy}
                    Position={new UDim2(0.05, 0, 0, 5)}
                    Size={new UDim2(0, 0, 0.7, -10)}
                    Text={"Join Public Empire"}
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextScaled={true}
                    TextSize={60}
                    TextWrapped={true}
                    TextXAlignment={Enum.TextXAlignment.Left}
                    ZIndex={2}
                >
                    <uistroke Thickness={3} />
                </textlabel>
                <textlabel
                    Active={true}
                    AutomaticSize={Enum.AutomaticSize.X}
                    BackgroundTransparency={1}
                    FontFace={RobotoSlab}
                    Position={new UDim2(0.05, 0, 0.7, -10)}
                    Size={new UDim2(0, 0, 0.3, 0)}
                    Text={"Your data will not be saved with this option!"}
                    TextColor3={Color3.fromRGB(196, 196, 196)}
                    TextScaled={true}
                    TextSize={60}
                    TextWrapped={true}
                    TextXAlignment={Enum.TextXAlignment.Left}
                >
                    <uistroke Thickness={2} />
                </textlabel>
            </BaseMenuOption>
        </frame>
    );
}


import React, { ReactNode, RefObject, useCallback, useEffect, useRef, useState } from "@rbxts/react";
import { TweenService } from "@rbxts/services";
import { RobotoSlabHeavy } from "shared/asset/GameFonts";
import { getAsset } from "shared/asset/AssetMap";
import { playSound } from "shared/asset/GameAssets";

export function BaseMenuOption({
    children,
    gradientColors,
    size,
    onClick,
    animationDelay = 0,
    fast = false,
    labelRef,
}: {
    children: ReactNode;
    gradientColors: [Color3, Color3];
    size: UDim2;
    onClick?: () => void;
    animationDelay?: number;
    fast?: boolean;
    labelRef?: RefObject<TextLabel>;
}) {
    const [isHovered, setIsHovered] = useState(false);
    const buttonRef = useRef<ImageButton>();
    const shadowRef = useRef<ImageLabel>();

    // Entrance animation effect
    useEffect(() => {
        const button = buttonRef.current;
        const label = labelRef?.current;
        const shadow = shadowRef.current;
        if (button && shadow) {
            // Set initial off-screen positions
            const originalButtonPos = button.Position;
            const originalShadowPos = shadow.Position;

            button.Position = new UDim2(0, -600, originalButtonPos.Y.Scale, originalButtonPos.Y.Offset);
            shadow.Position = new UDim2(0, -594, originalShadowPos.Y.Scale, originalShadowPos.Y.Offset);
            if (label) {
                label.TextTransparency = 1;
                if (label.FindFirstChild("UIStroke")) {
                    (label.FindFirstChild("UIStroke") as UIStroke).Transparency = 1;
                }
            }

            // Use reduced delays if fast mode is enabled
            const baseDelay = fast ? 0 : 2;
            const animDelay = fast ? animationDelay * 0.1 : animationDelay;

            // Start entrance animation after delay
            task.delay(baseDelay + animDelay, () => {
                if (button && shadow) {
                    const bounce = new TweenInfo(fast ? 0.75 : 1, Enum.EasingStyle.Back, Enum.EasingDirection.Out);

                    TweenService.Create(button, bounce, { Position: originalButtonPos }).Play();
                    TweenService.Create(shadow, bounce, { Position: originalShadowPos }).Play();
                }
                if (label) {
                    TweenService.Create(label, new TweenInfo(fast ? 1 : 1.4), {
                        TextTransparency: 0,
                    }).Play();

                    const stroke = label.FindFirstChild("UIStroke") as UIStroke;
                    if (stroke) {
                        TweenService.Create(stroke, new TweenInfo(fast ? 1 : 1.4), { Transparency: 0 }).Play();
                    }
                }
            });
        }
    }, [animationDelay, fast]);

    const handleMouseEnter = useCallback(() => {
        playSound("EmphasisButtonHover.mp3", undefined, (sound) => (sound.Volume = 0.25));
        setIsHovered(true);
    }, []);

    const handleMouseLeave = useCallback(() => {
        setIsHovered(false);
    }, []);

    return (
        <frame Active={true} BackgroundTransparency={1} Size={size}>
            <uiflexitem FlexMode={Enum.UIFlexMode.None} ItemLineAlignment={Enum.ItemLineAlignment.Start} />
            {/* Shadow */}
            <imagelabel
                ref={shadowRef}
                Active={true}
                AnchorPoint={new Vector2(0, 1)}
                BackgroundTransparency={1}
                Image={getAsset("assets/start/MenuOptionBackground.png")}
                ImageColor3={Color3.fromRGB(0, 0, 0)}
                ImageTransparency={0.4}
                Position={new UDim2(0, -100, 1, -7)}
                Size={new UDim2(1, 0, 0.6, -10)}
                ZIndex={-5}
            />

            {/* Main Button */}
            <imagebutton
                ref={buttonRef}
                AnchorPoint={new Vector2(0, 1)}
                AutoButtonColor={false}
                BackgroundTransparency={1}
                Image={getAsset("assets/start/MenuOptionBackground.png")}
                LayoutOrder={1}
                Position={new UDim2(0, -106, 1, -10)}
                Size={new UDim2(1, 0, 0.6, -10)}
                ZIndex={0}
                Event={{
                    Activated: onClick,
                    MouseEnter: handleMouseEnter,
                    MouseLeave: handleMouseLeave,
                }}
            >
                {/* Button Gradient */}
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, gradientColors[0]),
                            new ColorSequenceKeypoint(1, gradientColors[1]),
                        ])
                    }
                    Rotation={90}
                />

                {/* Overlay Pattern */}
                <imagelabel
                    BackgroundTransparency={1}
                    Image={getAsset("assets/start/MenuOptionOverlay.png")}
                    ImageColor3={gradientColors[0]}
                    ImageTransparency={0.9}
                    Interactable={false}
                    Size={new UDim2(1, 0, 1, 0)}
                    TileSize={new UDim2(0, 50, 0, 50)}
                />

                {/* Hover Effect */}
                {isHovered && (
                    <imagelabel
                        BackgroundTransparency={1}
                        BorderSizePixel={0}
                        Image={getAsset("assets/start/MenuOptionBackground.png")}
                        ImageTransparency={0.8}
                        Size={new UDim2(1, 0, 1, 0)}
                        ZIndex={2}
                    />
                )}
            </imagebutton>

            {children}
        </frame>
    );
}

/**
 * Reusable menu option component with modern styling and hover effects
 */
export default function MenuOption({
    label,
    gradientColors,
    onClick,
    height,
    animationDelay = 0,
    fast = false,
}: {
    /** Display text for the option */
    label: string;
    /** Gradient colors for the button [start, end] */
    gradientColors: [Color3, Color3];
    /** Click handler */
    onClick: () => void;
    /** Height of the option */
    height: number;
    /** Delay before starting entrance animation (in seconds) */
    animationDelay?: number;
    /** Whether to use fast animations (skip delays) */
    fast?: boolean;
}) {
    const labelRef = useRef<TextLabel>();
    return (
        <BaseMenuOption
            labelRef={labelRef}
            gradientColors={gradientColors}
            size={new UDim2(0.1, 400, 0, height)}
            onClick={onClick}
            animationDelay={animationDelay}
            fast={fast}
        >
            {/* Label */}
            <textlabel
                ref={labelRef}
                Active={true}
                AutomaticSize={Enum.AutomaticSize.X}
                BackgroundTransparency={1}
                FontFace={RobotoSlabHeavy}
                Position={new UDim2(0.05, 0, 0, 5)}
                Size={new UDim2(0, 0, 0.8, -10)}
                Text={label}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextSize={60}
                TextWrapped={true}
                TextXAlignment={Enum.TextXAlignment.Left}
            >
                <uistroke key={"UIStroke"} Thickness={3}>
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(25, 25, 35)),
                                new ColorSequenceKeypoint(1, gradientColors[0]),
                            ])
                        }
                        Rotation={90}
                    />
                </uistroke>
            </textlabel>
        </BaseMenuOption>
    );
}


import React, { Fragment, useEffect } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { ReplicatedStorage } from "@rbxts/services";
import { CreateReactStory } from "@rbxts/ui-labs";
import App from "client/components/App";
import performNewBeginningsWakeUp from "client/components/start/performNewBeginningsWakeUp";
import StoryMocking from "client/components/StoryMocking";
import Packets from "shared/Packets";
/**
 * Story for previewing the intro sequence where the player wakes up
 */
export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
    },
    () => {
        Packets.getLogs.toServer = () => [];
        ReplicatedStorage.SetAttribute("NewBeginningsWakingUp", undefined);
        StoryMocking.mockCharacter(false);

        useEffect(() => {
            return () => ReplicatedStorage.SetAttribute("NewBeginningsWakingUp", undefined);
        }, []);

        return (
            <Fragment>
                <textbutton
                    AnchorPoint={new Vector2(0.5, 0)}
                    Text={"Play Intro Sequence"}
                    Size={new UDim2(0, 300, 0, 50)}
                    Position={new UDim2(0.5, 0, 0, 0)}
                    Event={{
                        MouseButton1Click: () => {
                            ReplicatedStorage.SetAttribute("NewBeginningsWakingUp", true);
                            performNewBeginningsWakeUp();
                        },
                    }}
                />

                <App />
            </Fragment>
        );
    },
);


import React, { Fragment, useEffect } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { Workspace } from "@rbxts/services";
import { CreateReactStory } from "@rbxts/ui-labs";
import SettingsWindow from "client/components/settings/SettingsWindow";
import TitleScreen from "client/components/start/TitleScreen";
import useVisibility from "client/hooks/useVisibility";
import MusicManager from "client/MusicManager";
import Packets from "shared/Packets";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            visible: true,
            fastTransitions: false,
        },
    },
    (props) => {
        useVisibility("Title", props.controls.visible);

        Packets.availableEmpires.set(
            new Map([
                [
                    "Empire1",
                    {
                        name: "The First Empire",
                        owner: 1,
                        created: 500,
                        items: 55,
                        playtime: 2599,
                    },
                ],
                [
                    "Empire2",
                    {
                        name: "The Second Empire",
                        owner: 2,
                        created: 1500,
                        items: 150,
                        playtime: 3600,
                    },
                ],
            ]),
        );

        useEffect(() => {
            Workspace.SetAttribute("Title", true);
            const settings = table.clone(Packets.settings.get());
            settings.Music = true;
            Packets.settings.set(settings);
            return MusicManager.init();
        }, []);

        return (
            <Fragment>
                <TitleScreen fastTransitions={props.controls.fastTransitions} />
                <SettingsWindow />
            </Fragment>
        );
    },
);


import React, { Fragment, useCallback, useEffect, useRef, useState } from "@rbxts/react";
import { TweenService } from "@rbxts/services";
import SingleDocumentManager from "client/components/sidebar/SingleDocumentManager";
import AboutWindow from "client/components/start/AboutWindow";
import EmpiresWindow from "client/components/start/EmpiresWindow";
import MenuOption from "client/components/start/MenuOption";
import performNewBeginningsWakeUp from "client/components/start/performNewBeginningsWakeUp";
import { showErrorToast } from "client/components/toast/ToastService";
import DocumentManager, { useDocument } from "client/components/window/DocumentManager";
import { LOCAL_PLAYER } from "shared/constants";
import { getAsset } from "shared/asset/AssetMap";
import { playSound } from "shared/asset/GameAssets";
import { RobotoMonoBold } from "shared/asset/GameFonts";
import { CAMERA } from "shared/constants";
import Packets from "shared/Packets";
import MinerHavenNameplate from "shared/world/nodes/MinerHavenNameplate";
import StartCamera from "shared/world/nodes/StartCamera";

/**
 * Main start window component that handles the title screen, empire selection, and about page
 */
export default function TitleScreen({ fastTransitions = false }: { fastTransitions?: boolean }) {
    const logoRef = useRef<ImageLabel>();
    const { visible, setVisible } = useDocument({ id: "Title" });
    const [currentView, setCurrentView] = useState<"main" | "empires" | "about" | "none">("main");
    const [hasEnteredScreen, setHasEnteredScreen] = useState(false);
    const firstRenderTime = useRef<number>(fastTransitions ? 0 : os.clock());

    // Helper function to determine if we should use fast animations
    const shouldUseFastAnimations = useCallback(() => {
        const timeSinceFirstRender = os.clock() - firstRenderTime.current;
        return timeSinceFirstRender > 1;
    }, []);

    const logoTweenInInfo = new TweenInfo(1, Enum.EasingStyle.Back, Enum.EasingDirection.Out);

    // Animation entrance effect when component becomes visible
    useEffect(() => {
        if (visible && !hasEnteredScreen) {
            setHasEnteredScreen(true);

            // Set initial positions for entrance animation
            if (logoRef.current) {
                logoRef.current.Position = new UDim2(1.25, -50, 0.5, 0);
                logoRef.current.Rotation = 40;
            }

            task.delay(shouldUseFastAnimations() ? 0.2 : 0.8, () => {
                if (logoRef.current) {
                    TweenService.Create(logoRef.current, logoTweenInInfo, {
                        Position: new UDim2(1, 0, 0.5, 0),
                        Rotation: 0,
                    }).Play();
                }
            });
        }
    }, [visible, hasEnteredScreen]);

    // Camera transition effect - cycles through StartCamera scenes every 10 seconds
    useEffect(() => {
        if (!visible) return;

        CAMERA.CameraType = Enum.CameraType.Scriptable;
        CAMERA.CFrame = StartCamera.waitForInstance().CFrame;

        return () => {
            CAMERA.CameraType = Enum.CameraType.Custom;
        };
    }, [visible]);

    // Handle smooth transitions between views
    const transitionToView = useCallback(
        (view: "main" | "empires" | "about" | "none") => {
            setCurrentView(view);

            const logo = logoRef.current;
            if (!logo) return;

            if (view === "none" || view === "about") {
                const tweenInfo = new TweenInfo(1, Enum.EasingStyle.Circular, Enum.EasingDirection.Out);
                TweenService.Create(logo, tweenInfo, {
                    Position: new UDim2(1.25, -50, 0.5, 0),
                    Rotation: 40,
                }).Play();
            } else if (view === "main" || view === "empires") {
                TweenService.Create(logo, logoTweenInInfo, {
                    Position: new UDim2(1, 0, 0.5, 0),
                    Rotation: 0,
                }).Play();
            }
        },
        [currentView],
    );

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

        const connection = DocumentManager.visibilityChanged.connect((id, isVisible) => {
            if (id !== "Settings") return;
            if (isVisible) {
                transitionToView("none");
            } else {
                transitionToView("main");
            }
        });

        return () => connection.Disconnect();
    }, [visible, transitionToView]);

    // Cleanup camera when component becomes invisible
    useEffect(() => {
        if (!visible) {
            // Reset camera to custom mode when leaving start window
            CAMERA.CameraType = Enum.CameraType.Custom;
        }
    }, [visible]);

    if (!visible) {
        return <Fragment />;
    }

    const empireName = Packets.empireName.get();

    return (
        <Fragment>
            <surfacegui
                Adornee={MinerHavenNameplate.waitForInstance(20)}
                ClipsDescendants={true}
                Face={Enum.NormalId.Top}
                LightInfluence={1}
                MaxDistance={1000}
                ResetOnSpawn={false}
                SizingMode={Enum.SurfaceGuiSizingMode.PixelsPerStud}
                ZIndexBehavior={Enum.ZIndexBehavior.Sibling}
            >
                <textlabel
                    AnchorPoint={new Vector2(0.5, 0.5)}
                    BackgroundTransparency={1}
                    FontFace={RobotoMonoBold}
                    Position={new UDim2(0.5, 0, 0.5, 0)}
                    Size={new UDim2(1, 0, 1, 0)}
                    Text={empireName === "no name" ? LOCAL_PLAYER?.Name : empireName}
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextSize={100}
                    TextWrapped={true}
                >
                    <uiscale Scale={3} />
                    <uistroke Thickness={2} />
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                                new ColorSequenceKeypoint(0.45, Color3.fromRGB(170, 255, 255)),
                                new ColorSequenceKeypoint(0.55, Color3.fromRGB(110, 161, 255)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(255, 255, 255)),
                            ])
                        }
                        Rotation={90}
                    />
                </textlabel>
            </surfacegui>

            {/* Logo */}
            <imagelabel
                ref={logoRef}
                AnchorPoint={new Vector2(1, 0.5)}
                BackgroundTransparency={1}
                ScaleType={Enum.ScaleType.Fit}
                Size={new UDim2(1, 0, 0.2, 50)}
                Image={getAsset("assets/brand/Wordmark.png")}
            >
                <uiaspectratioconstraint AspectRatio={1.5} />
            </imagelabel>

            {/* Main Menu View */}
            {currentView === "main" && (
                <frame BackgroundTransparency={1} Position={new UDim2(0, 0, 0.4, 0)} Size={new UDim2(1, 0, 0.55, 0)}>
                    <uilistlayout
                        HorizontalAlignment={Enum.HorizontalAlignment.Center}
                        SortOrder={Enum.SortOrder.LayoutOrder}
                        VerticalAlignment={Enum.VerticalAlignment.Center}
                        VerticalFlex={Enum.UIFlexAlignment.Fill}
                    />

                    <MenuOption
                        label="Play"
                        gradientColors={[Color3.fromRGB(85, 255, 127), Color3.fromRGB(5, 170, 60)]}
                        onClick={() => {
                            playSound("EmphasisMenuSelect.mp3", undefined, (sound) => (sound.Volume = 0.4));
                            transitionToView("empires");
                        }}
                        height={70}
                        animationDelay={0}
                        fast={shouldUseFastAnimations()}
                    />

                    {/** Spacer */}
                    <frame Size={new UDim2(1, 0, 0, 35)} BackgroundTransparency={1}>
                        <uiflexitem FlexMode={Enum.UIFlexMode.Shrink} />
                    </frame>

                    <MenuOption
                        label="Settings"
                        gradientColors={[Color3.fromRGB(172, 172, 172), Color3.fromRGB(102, 102, 102)]}
                        onClick={() => {
                            playSound("EmphasisMenuSelect.mp3", undefined, (sound) => (sound.Volume = 0.35));
                            SingleDocumentManager.open("Settings");
                        }}
                        height={60}
                        animationDelay={0.2}
                        fast={shouldUseFastAnimations()}
                    />

                    <MenuOption
                        label="About"
                        gradientColors={[Color3.fromRGB(34, 189, 255), Color3.fromRGB(8, 127, 255)]}
                        onClick={() => {
                            playSound("EmphasisMenuSelect.mp3", undefined, (sound) => (sound.Volume = 0.35));
                            transitionToView("about");
                        }}
                        height={60}
                        animationDelay={0.4}
                        fast={shouldUseFastAnimations()}
                    />
                </frame>
            )}

            {/* Empires Selection View */}
            {currentView === "empires" && (
                <EmpiresWindow
                    onClose={() => transitionToView("main")}
                    exitStart={() => {
                        const success = Packets.loadCharacter.toServer();
                        if (!success) {
                            showErrorToast("Failed to load character");
                            return;
                        }
                        CAMERA.CameraType = Enum.CameraType.Custom;
                        setVisible(false);
                        performNewBeginningsWakeUp();
                    }}
                />
            )}

            {/* About View */}
            {currentView === "about" && <AboutWindow onClose={() => transitionToView("main")} />}
        </Fragment>
    );
}


import { loadAnimation } from "@antivivi/vrldk";
import { ReplicatedStorage, TweenService, Workspace } from "@rbxts/services";
import { questState } from "client/components/quest/QuestState";
import { PLAYER_GUI } from "shared/constants";
import { setVisibilityMain } from "client/hooks/useVisibility";
import MusicManager from "client/MusicManager";
import type NewBeginnings from "server/quests/NewBeginnings";
import Packets from "shared/Packets";
import { playSound } from "shared/asset/GameAssets";
import { WAYPOINTS } from "shared/constants";
import { IS_EDIT } from "shared/Context";
import eat from "shared/hamster/eat";
import { getPlayerCharacter } from "shared/hamster/getPlayerCharacter";

let isIntroSequenceDone = false;
let isCurrentlyInIntroSequence = false;

function exit() {
    setVisibilityMain(true);
    MusicManager.loopMusic();
    return false;
}

/**
 * Plays the cutscene sequence that happens in the {@link NewBeginnings} quest.
 */
export default function performNewBeginningsWakeUp() {
    Workspace.SetAttribute("Title", false);
    const stagePerQuest = Packets.stagePerQuest.get();
    const newBeginningsStage = stagePerQuest?.get("NewBeginnings");
    if (newBeginningsStage !== undefined && newBeginningsStage !== 0) return exit();
    if (ReplicatedStorage.GetAttribute("NewBeginningsWakingUp") !== true) return exit();
    if (isIntroSequenceDone === true) return exit();

    const connection = ReplicatedStorage.GetAttributeChangedSignal("NewBeginningsWakingUp").Connect(() => {
        if (ReplicatedStorage.GetAttribute("NewBeginningsWakingUp") === false) {
            connection.Disconnect();
            exit();
        }
    });

    print("performing intro sequence");
    const screenGui = new Instance("ScreenGui");
    screenGui.IgnoreGuiInset = true;
    screenGui.ResetOnSpawn = false;
    screenGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling;
    screenGui.Name = "NewBeginningsWakingUp";
    screenGui.DisplayOrder = 30;
    screenGui.Parent = PLAYER_GUI;
    if (IS_EDIT) {
        eat(screenGui, "Destroy");
    }

    const blackWindow = new Instance("Frame");
    blackWindow.Size = new UDim2(1, 0, 1, 0);
    blackWindow.BackgroundColor3 = Color3.fromRGB(0, 0, 0);
    blackWindow.BackgroundTransparency = 0;
    blackWindow.Visible = true;
    blackWindow.Parent = screenGui;

    // Waking up
    let character = getPlayerCharacter();
    if (!character) {
        while (!character) {
            character = getPlayerCharacter();
            task.wait();
        }
    }
    const humanoid = character.WaitForChild("Humanoid") as Humanoid;

    let camera = Workspace.CurrentCamera;
    if (!camera) {
        while (!camera) {
            camera = Workspace.CurrentCamera;
            task.wait();
        }
    }

    isIntroSequenceDone = true;
    isCurrentlyInIntroSequence = true;
    MusicManager.loopMusic();
    const head = humanoid.Parent?.WaitForChild("Head") as BasePart;
    const transparencyParts = [head];
    for (const transparencyPart of head.GetDescendants()) {
        if (transparencyPart.IsA("BasePart")) {
            transparencyPart.LocalTransparencyModifier = 1;
            transparencyParts.push(transparencyPart);
        }
    }
    head.LocalTransparencyModifier = 1;
    const sleepingAnimation = loadAnimation(humanoid, 17789845379);
    sleepingAnimation?.Play();
    camera.CameraType = Enum.CameraType.Scriptable;
    camera.CFrame = WAYPOINTS.NewBeginningsCamera0.CFrame;

    const fabricRustle = () => playSound("FabricRustle.mp3");
    task.delay(2, () => {
        fabricRustle();
        TweenService.Create(camera, new TweenInfo(0.5), { CFrame: WAYPOINTS.NewBeginningsCamera1.CFrame }).Play();
        TweenService.Create(blackWindow, new TweenInfo(2), { BackgroundTransparency: 1 }).Play();
    });
    task.delay(2.96, () => {
        fabricRustle();
        TweenService.Create(camera, new TweenInfo(0.5), { CFrame: WAYPOINTS.NewBeginningsCamera2.CFrame }).Play();
    });
    task.delay(3.7, () => {
        fabricRustle();
        TweenService.Create(camera, new TweenInfo(0.5), { CFrame: WAYPOINTS.NewBeginningsCamera3.CFrame }).Play();
    });
    task.delay(5, () => {
        playSound("JumpSwish.mp3");
        sleepingAnimation?.Stop();
        camera.CFrame = WAYPOINTS.NewBeginningsCamera4.CFrame;
        camera.CameraType = Enum.CameraType.Custom;
        humanoid.SetStateEnabled(Enum.HumanoidStateType.Jumping, true);
        for (const transparencyPart of transparencyParts) {
            transparencyPart.LocalTransparencyModifier = 0;
        }
    });
    task.delay(7.5, () => {
        questState.setTrackedQuest("NewBeginnings");
        isCurrentlyInIntroSequence = false;
    });
    return true;
}


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

interface SectionHeaderProps {
    title: string;
    layoutOrder?: number;
    icon: string;
}

/**
 * Enhanced section header component for organizing statistics into categories
 */
export default function SectionHeader({ title, layoutOrder, icon }: SectionHeaderProps) {
    return (
        <frame
            BackgroundColor3={Color3.fromRGB(25, 25, 30)}
            BackgroundTransparency={0.2}
            BorderSizePixel={0}
            LayoutOrder={layoutOrder}
            Size={new UDim2(1, 0, 0, 45)}
        >
            <canvasgroup BackgroundTransparency={1} BorderSizePixel={0} Size={new UDim2(1, 0, 1, 0)} ZIndex={-1}>
                <uicorner CornerRadius={new UDim(0, 8)} />

                <frame BorderSizePixel={0} Size={new UDim2(1, 0, 1, 0)} ZIndex={-2}>
                    <uicorner CornerRadius={new UDim(0, 8)} />
                    {/* Gradient overlay for depth */}
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(50, 50, 60)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(25, 25, 30)),
                            ])
                        }
                        Rotation={90}
                        Transparency={
                            new NumberSequence([new NumberSequenceKeypoint(0, 0.7), new NumberSequenceKeypoint(1, 0.3)])
                        }
                    />
                </frame>

                <uistroke Color={Color3.fromRGB(80, 80, 90)} Thickness={1} Transparency={0.4} />

                {/* Left accent line */}
                <frame
                    key="AccentBar"
                    BackgroundColor3={Color3.fromRGB(253, 84, 137)}
                    BorderSizePixel={0}
                    Size={new UDim2(0, 4, 1, 0)}
                    Position={new UDim2(0, 0, 0, 0)}
                />
            </canvasgroup>

            {/* Left padding to account for accent line */}
            <frame key="LeftSpace" BackgroundTransparency={1} Size={new UDim2(0, 15, 1, 0)} LayoutOrder={0} />

            {/* Icon (if provided) */}
            <imagelabel
                key="Icon"
                AnchorPoint={new Vector2(0, 0.5)}
                BackgroundTransparency={1}
                Image={icon}
                Position={new UDim2(0, 10, 0.5, 0)}
                ScaleType={Enum.ScaleType.Fit}
                Size={new UDim2(0, 50, 1, -10)}
                SizeConstraint={Enum.SizeConstraint.RelativeYY}
                LayoutOrder={1}
            />

            <textlabel
                key="HeaderText"
                AnchorPoint={new Vector2(0, 0.5)}
                BackgroundTransparency={1}
                FontFace={RobotoSlabBold}
                Position={new UDim2(0, 70, 0.5, 0)}
                Size={new UDim2(0, 200, 0.7, 0)}
                Text={title}
                TextColor3={Color3.fromRGB(220, 220, 230)}
                TextScaled={true}
                TextSize={18}
                TextWrapped={true}
                TextXAlignment={Enum.TextXAlignment.Left}
                LayoutOrder={2}
            >
                <uistroke Color={Color3.fromRGB(0, 0, 0)} Thickness={1.5} Transparency={0.3} />
            </textlabel>
        </frame>
    );
}


import React, { memo, useRef } from "@rbxts/react";
import useHover from "client/hooks/useHover";
import { RobotoSlabBold, RobotoSlabMedium } from "shared/asset/GameFonts";

interface StatItemProps {
    label: string;
    value: string;
    image?: string; // Optional image prop for currency icons
    layoutOrder?: number;
    accent?: boolean; // For highlighting important stats
}

/**
 * Enhanced reusable component for displaying individual stat items with label and value
 */
function StatItem({ label, value, image, layoutOrder, accent = false }: StatItemProps) {
    const frameRef = useRef<Frame>();
    const { hovering, events } = useHover({});

    const backgroundColor = accent
        ? Color3.fromRGB(45, 85, 125) // Blue accent for important stats
        : Color3.fromRGB(35, 35, 40); // Dark gray for regular stats

    const borderColor = accent
        ? Color3.fromRGB(70, 130, 180) // Steel blue border
        : Color3.fromRGB(60, 60, 65); // Subtle gray border

    return (
        <frame
            ref={frameRef}
            BackgroundTransparency={1}
            LayoutOrder={layoutOrder}
            Size={new UDim2(1, 0, 0, 30)}
            Event={{ ...events }}
        >
            <canvasgroup BackgroundTransparency={1} BorderSizePixel={0} Size={new UDim2(1, 0, 1, 0)} ZIndex={-1}>
                <uicorner CornerRadius={new UDim(0, 8)} />

                <frame
                    BackgroundColor3={hovering ? backgroundColor.Lerp(new Color3(255, 255, 255), 0.2) : backgroundColor}
                    BackgroundTransparency={accent ? 0.3 : 0.4}
                    BorderSizePixel={0}
                    Size={new UDim2(1, 0, 1, 0)}
                    ZIndex={-2}
                >
                    <uicorner CornerRadius={new UDim(0, 8)} />
                    {/* Gradient overlay for depth */}
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(180, 180, 180)),
                            ])
                        }
                        Rotation={90}
                        Transparency={
                            new NumberSequence([
                                new NumberSequenceKeypoint(0, 0.9),
                                new NumberSequenceKeypoint(1, 0.95),
                            ])
                        }
                    />
                </frame>

                <uistroke Color={borderColor} Thickness={1.5} Transparency={0.3} />

                {/* Left accent bar for important stats */}
                <frame
                    key="AccentBar"
                    BackgroundColor3={Color3.fromRGB(100, 180, 255)}
                    BorderSizePixel={0}
                    Size={new UDim2(0, 4, 1, 0)}
                    Position={new UDim2(0, 0, 0, 0)}
                    Visible={accent}
                />
            </canvasgroup>

            {/* Add padding from left edge */}
            <frame
                key="LeftPadding"
                BackgroundTransparency={1}
                Size={new UDim2(0, accent ? 10 : 5, 1, 0)}
                LayoutOrder={0}
            />

            <imagelabel
                key="CurrencyIcon"
                AnchorPoint={new Vector2(0, 0.5)}
                BackgroundTransparency={1}
                Image={image ?? ""}
                Position={new UDim2(0, 20, 0.5, 0)}
                ScaleType={Enum.ScaleType.Fit}
                Size={new UDim2(0, 20, 0.8, 0)}
                SizeConstraint={Enum.SizeConstraint.RelativeYY}
                Visible={image !== undefined}
            />

            <textlabel
                key="StatLabel"
                AnchorPoint={new Vector2(0, 0.5)}
                BackgroundTransparency={1}
                FontFace={RobotoSlabBold}
                Size={new UDim2(0.55, 0, 0.8, 0)}
                Text={label}
                TextColor3={Color3.fromRGB(240, 240, 245)}
                TextScaled={true}
                TextSize={16}
                TextWrapped={true}
                TextXAlignment={Enum.TextXAlignment.Left}
                Position={new UDim2(0, 50, 0.5, 0)}
            >
                <uistroke Color={Color3.fromRGB(0, 0, 0)} Thickness={1.5} Transparency={0.5} />
            </textlabel>

            <textlabel
                key="AmountLabel"
                AnchorPoint={new Vector2(1, 0.5)}
                BackgroundTransparency={1}
                FontFace={RobotoSlabMedium}
                Size={new UDim2(0.35, 0, 0.8, 0)}
                Text={value}
                TextColor3={accent ? Color3.fromRGB(150, 220, 255) : Color3.fromRGB(200, 200, 205)}
                TextScaled={true}
                TextSize={16}
                TextWrapped={true}
                TextXAlignment={Enum.TextXAlignment.Right}
                Position={new UDim2(1, -20, 0.5, 0)}
            >
                <uistroke Color={Color3.fromRGB(0, 0, 0)} Thickness={1.5} Transparency={0.5} />
            </textlabel>

            {/* Right padding */}
            <frame key="RightPadding" BackgroundTransparency={1} Size={new UDim2(0, 5, 1, 0)} LayoutOrder={3} />
        </frame>
    );
}

export default memo(StatItem);


import { OnoeNum } from "@rbxts/serikanum";
import React from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import StatsWindow, { PingManager } from "client/components/stats/StatsWindow";
import StoryMocking from "client/components/StoryMocking";
import { useSingleDocumentVisibility } from "client/hooks/useVisibility";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import { CURRENCY_DETAILS } from "shared/currency/CurrencyDetails";
import Packets from "shared/Packets";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            visible: true,
        },
    },
    (props) => {
        StoryMocking.mockData();

        useSingleDocumentVisibility("Stats", props.controls.visible);

        for (let i = 0; i < 10; i++) {
            PingManager.logPing(math.random(50, 300) / 1000); // Simulate random ping
        }
        const balance = new CurrencyBundle();
        for (const [currency] of pairs(CURRENCY_DETAILS)) {
            balance.set(currency, new OnoeNum(math.random(1, 5000)));
        }
        Packets.mostBalance.set(balance.amountPerCurrency);

        return <StatsWindow />;
    },
);


import React, { useEffect } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import StoryMocking from "client/components/StoryMocking";
import SimulationCommandInterface from "client/components/story/SimulationCommandInterface";
import manuallyIgniteFlamework from "shared/hamster/manuallyIgniteFlamework";
import ItemViewport from "shared/item/ItemViewport";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {},
    },
    () => {
        StoryMocking.mockCharacter();
        useEffect(() => {
            const flameworkContext = manuallyIgniteFlamework();
            return () => {
                flameworkContext.cleanup();
                ItemViewport.cleanup();
            };
        }, []);

        return <SimulationCommandInterface />;
    },
);


import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from "@rbxts/react";
import { TweenService, Workspace } from "@rbxts/services";
import {
    DEFAULT_TOAST_DURATION,
    MAX_TOAST_QUEUE,
    subscribeToasts,
    type ToastRequest,
    type ToastVariant,
} from "client/components/toast/ToastService";
import { RobotoMono, RobotoSlabBold } from "shared/asset/GameFonts";

const VARIANT_STYLES: Record<ToastVariant, { background: Color3; border: Color3; text: Color3 }> = {
    info: {
        background: Color3.fromRGB(35, 41, 52),
        border: Color3.fromRGB(83, 122, 255),
        text: Color3.fromRGB(223, 231, 255),
    },
    success: {
        background: Color3.fromRGB(30, 52, 41),
        border: Color3.fromRGB(74, 222, 128),
        text: Color3.fromRGB(220, 255, 231),
    },
    warning: {
        background: Color3.fromRGB(64, 52, 28),
        border: Color3.fromRGB(250, 204, 21),
        text: Color3.fromRGB(255, 241, 213),
    },
    error: {
        background: Color3.fromRGB(61, 28, 32),
        border: Color3.fromRGB(248, 113, 113),
        text: Color3.fromRGB(254, 226, 226),
    },
};

const DEFAULT_MAX_WIDTH = 420;
const ENTER_TWEEN_INFO = new TweenInfo(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.Out);
const EXIT_TWEEN_INFO = new TweenInfo(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.In);
const VISIBLE_BACKGROUND_TRANSPARENCY = 0.15;
const VISIBLE_STROKE_TRANSPARENCY = 0.4;
const HINT_TEXT_TRANSPARENCY = 0.35;

type ToastLifecycle = "entering" | "visible" | "exiting";

type ManagedToast = ToastRequest & {
    status: ToastLifecycle;
    count: number;
    baseMessage: string;
    revision: number;
};

export default function ToastManager() {
    const [toasts, setToasts] = useState<Array<ManagedToast>>([]);
    const [toastWidth, setToastWidth] = useState(DEFAULT_MAX_WIDTH);

    useEffect(() => {
        let viewportConnection: RBXScriptConnection | undefined;

        const computeWidth = () => {
            const camera = Workspace.CurrentCamera;
            if (!camera) {
                setToastWidth((prev) => (prev === DEFAULT_MAX_WIDTH ? prev : DEFAULT_MAX_WIDTH));
                return;
            }

            const viewport = camera.ViewportSize;
            const clamped = math.clamp(math.floor(viewport.X * 0.55), 260, DEFAULT_MAX_WIDTH);
            setToastWidth((prev) => (prev === clamped ? prev : clamped));
        };

        const bindViewport = () => {
            viewportConnection?.Disconnect();
            const camera = Workspace.CurrentCamera;
            if (camera) {
                viewportConnection = camera.GetPropertyChangedSignal("ViewportSize").Connect(computeWidth);
            }
            computeWidth();
        };

        const cameraConnection = Workspace.GetPropertyChangedSignal("CurrentCamera").Connect(bindViewport);
        bindViewport();

        return () => {
            cameraConnection.Disconnect();
            viewportConnection?.Disconnect();
        };
    }, []);

    const dismissToast = useCallback((id: string, expectedRevision?: number) => {
        setToasts((prev) =>
            prev.map((toast) => {
                if (toast.id !== id) return toast;
                if (toast.status === "exiting") return toast;
                if (expectedRevision !== undefined && toast.revision !== expectedRevision) return toast;
                return { ...toast, status: "exiting" };
            }),
        );
    }, []);

    const handleEnterComplete = useCallback((id: string) => {
        setToasts((prev) =>
            prev.map((toast) => {
                if (toast.id !== id || toast.status !== "entering") return toast;
                return { ...toast, status: "visible" };
            }),
        );
    }, []);

    const handleExitComplete = useCallback((id: string) => {
        setToasts((prev) => prev.filter((toast) => toast.id !== id));
    }, []);

    useEffect(() => {
        let mounted = true;
        const connection = subscribeToasts((toast) => {
            if (!mounted) return;

            setToasts((prev) => {
                const updated = [...prev];

                let existingIndex: number | undefined;
                for (let index = updated.size() - 1; index >= 0; index--) {
                    const candidate = updated[index];
                    if (candidate.variant === toast.variant && candidate.message === toast.message) {
                        existingIndex = index;
                        break;
                    }
                }

                if (existingIndex !== undefined) {
                    const existingToast = updated[existingIndex];
                    const nextRevision = existingToast.revision + 1;
                    const mergedDuration = toast.duration ?? existingToast.duration ?? DEFAULT_TOAST_DURATION;
                    const mergedToast: ManagedToast = {
                        ...existingToast,
                        ...toast,
                        id: existingToast.id,
                        message: existingToast.baseMessage,
                        status: "entering",
                        count: existingToast.count + 1,
                        baseMessage: existingToast.baseMessage,
                        revision: nextRevision,
                        createdAt: os.clock(),
                        duration: mergedDuration,
                    };
                    updated[existingIndex] = mergedToast;
                    return updated;
                }

                const managedToast: ManagedToast = {
                    ...toast,
                    status: "entering",
                    count: 1,
                    baseMessage: toast.message,
                    revision: 0,
                    duration: toast.duration ?? DEFAULT_TOAST_DURATION,
                };
                updated.push(managedToast);

                while (updated.size() > MAX_TOAST_QUEUE) {
                    const index = updated.findIndex((candidate) => candidate.status !== "exiting");
                    if (index === -1) break;
                    if (updated[index].id === managedToast.id) break;
                    updated[index] = { ...updated[index], status: "exiting" };
                    break;
                }

                return updated;
            });
        });

        return () => {
            mounted = false;
            connection.disconnect();
        };
    }, [dismissToast]);

    const toastElements = useMemo(() => {
        return toasts.map((toast) => {
            const style = VARIANT_STYLES[toast.variant];

            return (
                <MemoizedToastItem
                    key={toast.id}
                    toast={toast}
                    style={style}
                    width={toastWidth}
                    onDismiss={dismissToast}
                    onEnterComplete={handleEnterComplete}
                    onExitComplete={handleExitComplete}
                />
            );
        });
    }, [toasts, toastWidth, dismissToast, handleEnterComplete, handleExitComplete]);

    return (
        <frame BackgroundTransparency={1} Size={new UDim2(1, 0, 1, 0)}>
            <frame
                AnchorPoint={new Vector2(0.5, 0)}
                AutomaticSize={Enum.AutomaticSize.XY}
                BackgroundTransparency={1}
                LayoutOrder={1}
                Position={new UDim2(0.5, 0, 0.05, 0)}
            >
                <uilistlayout
                    FillDirection={Enum.FillDirection.Vertical}
                    HorizontalAlignment={Enum.HorizontalAlignment.Center}
                    Padding={new UDim(0, 8)}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                    VerticalAlignment={Enum.VerticalAlignment.Top}
                />
                {toastElements}
            </frame>
        </frame>
    );
}

interface ToastItemProps {
    toast: ManagedToast;
    style: { background: Color3; border: Color3; text: Color3 };
    width: number;
    onDismiss: (id: string, expectedRevision?: number) => void;
    onEnterComplete: (id: string) => void;
    onExitComplete: (id: string) => void;
}

function ToastItem({ toast, style, width, onDismiss, onEnterComplete, onExitComplete }: ToastItemProps) {
    const buttonRef = useRef<TextButton>();
    const primaryTextRef = useRef<TextLabel>();
    const hintTextRef = useRef<TextLabel>();
    const strokeRef = useRef<UIStroke>();
    const scaleRef = useRef<UIScale>();
    const lastEnterRevision = useRef(-1);
    const lastExitRevision = useRef(-1);

    const horizontalPadding = math.floor(math.clamp((20 * width) / DEFAULT_MAX_WIDTH, 12, 20));
    const verticalPadding = math.floor(math.clamp((12 * width) / DEFAULT_MAX_WIDTH, 8, 12));
    const primaryTextSize = math.floor(math.clamp((22 * width) / DEFAULT_MAX_WIDTH, 16, 22));
    const hintTextSize = math.floor(math.clamp((14 * width) / DEFAULT_MAX_WIDTH, 11, 14));
    const listPadding = math.floor(math.clamp((6 * width) / DEFAULT_MAX_WIDTH, 4, 6));
    const baseMessage = toast.baseMessage ?? toast.message;
    const displayMessage = toast.count > 1 ? `${baseMessage} (x${toast.count})` : baseMessage;
    const shouldScheduleDismiss = toast.status !== "exiting";

    useEffect(() => {
        if (!shouldScheduleDismiss) {
            return undefined;
        }

        const revision = toast.revision;
        const duration = toast.duration ?? DEFAULT_TOAST_DURATION;
        const thread = task.delay(duration, () => {
            onDismiss(toast.id, revision);
        });

        return () => {
            task.cancel(thread);
        };
    }, [toast.id, toast.revision, toast.duration, shouldScheduleDismiss, onDismiss]);

    useEffect(() => {
        const button = buttonRef.current;
        const primary = primaryTextRef.current;
        const hint = hintTextRef.current;
        const stroke = strokeRef.current;
        const scale = scaleRef.current;
        if (!button || !primary || !hint || !stroke || !scale) return;

        if (button.AutomaticSize === Enum.AutomaticSize.Y) {
            button.Size = new UDim2(0, width, 0, 0);
        } else {
            button.Size = new UDim2(0, width, 0, button.Size.Y.Offset);
        }

        if (toast.status === "entering" && lastEnterRevision.current !== toast.revision) {
            lastEnterRevision.current = toast.revision;
            lastExitRevision.current = math.max(lastExitRevision.current, toast.revision - 1);

            button.BackgroundTransparency = 0.6;
            button.Size = new UDim2(0, width, 0, 0);
            primary.TextTransparency = 1;
            hint.TextTransparency = 1;
            stroke.Transparency = 1;
            scale.Scale = 0.92;

            const buttonTween = TweenService.Create(button, ENTER_TWEEN_INFO, {
                BackgroundTransparency: VISIBLE_BACKGROUND_TRANSPARENCY,
            });
            const primaryTween = TweenService.Create(primary, ENTER_TWEEN_INFO, {
                TextTransparency: 0,
            });
            const hintTween = TweenService.Create(hint, ENTER_TWEEN_INFO, {
                TextTransparency: HINT_TEXT_TRANSPARENCY,
            });
            const strokeTween = TweenService.Create(stroke, ENTER_TWEEN_INFO, {
                Transparency: VISIBLE_STROKE_TRANSPARENCY,
            });
            const scaleTween = TweenService.Create(scale, ENTER_TWEEN_INFO, {
                Scale: 1,
            });

            buttonTween.Play();
            primaryTween.Play();
            hintTween.Play();
            strokeTween.Play();
            scaleTween.Play();

            scaleTween.Completed.Once(() => {
                onEnterComplete(toast.id);
            });
        } else if (toast.status === "exiting" && lastExitRevision.current !== toast.revision) {
            lastExitRevision.current = toast.revision;

            const currentHeight = math.max(button.AbsoluteSize.Y, 4);
            button.ClipsDescendants = true;
            button.AutomaticSize = Enum.AutomaticSize.None;
            button.Size = new UDim2(0, width, 0, currentHeight);

            const buttonTween = TweenService.Create(button, EXIT_TWEEN_INFO, {
                BackgroundTransparency: 1,
                Size: new UDim2(0, width, 0, 0),
            });
            const primaryTween = TweenService.Create(primary, EXIT_TWEEN_INFO, {
                TextTransparency: 1,
            });
            const hintTween = TweenService.Create(hint, EXIT_TWEEN_INFO, {
                TextTransparency: 1,
            });
            const strokeTween = TweenService.Create(stroke, EXIT_TWEEN_INFO, {
                Transparency: 1,
            });
            const scaleTween = TweenService.Create(scale, EXIT_TWEEN_INFO, {
                Scale: 0.85,
            });

            buttonTween.Completed.Once(() => {
                onExitComplete(toast.id);
            });

            buttonTween.Play();
            primaryTween.Play();
            hintTween.Play();
            strokeTween.Play();
            scaleTween.Play();
        }
    }, [toast.status, toast.id, toast.revision, width, onEnterComplete, onExitComplete]);

    const handleDismiss = useCallback(() => onDismiss(toast.id), [onDismiss, toast.id]);

    return (
        <textbutton
            AutoButtonColor={false}
            BackgroundColor3={style.background}
            BackgroundTransparency={VISIBLE_BACKGROUND_TRANSPARENCY}
            LayoutOrder={toast.createdAt}
            Size={new UDim2(0, width, 0, 0)}
            AutomaticSize={Enum.AutomaticSize.Y}
            Text=""
            ZIndex={5}
            ref={buttonRef}
            Event={{
                Activated: handleDismiss,
                MouseButton1Click: handleDismiss,
            }}
        >
            <uicorner CornerRadius={new UDim(0, 8)} />
            <uistroke
                ApplyStrokeMode={Enum.ApplyStrokeMode.Border}
                Color={style.border}
                Thickness={2}
                Transparency={VISIBLE_STROKE_TRANSPARENCY}
                ref={strokeRef}
            />
            <uiscale ref={scaleRef} />
            <uipadding
                PaddingBottom={new UDim(0, verticalPadding)}
                PaddingLeft={new UDim(0, horizontalPadding)}
                PaddingRight={new UDim(0, horizontalPadding - 4)}
                PaddingTop={new UDim(0, verticalPadding)}
            />
            <uisizeconstraint MaxSize={new Vector2(width, 200)} MinSize={new Vector2(width, 0)} />
            <uilistlayout
                FillDirection={Enum.FillDirection.Vertical}
                HorizontalAlignment={Enum.HorizontalAlignment.Left}
                Padding={new UDim(0, listPadding)}
                SortOrder={Enum.SortOrder.LayoutOrder}
                VerticalAlignment={Enum.VerticalAlignment.Top}
            />
            <textlabel
                AutomaticSize={Enum.AutomaticSize.Y}
                BackgroundTransparency={1}
                FontFace={RobotoSlabBold}
                LayoutOrder={1}
                Size={new UDim2(1, 0, 0, 0)}
                Text={displayMessage}
                TextColor3={style.text}
                TextScaled={false}
                TextSize={primaryTextSize}
                TextWrapped={true}
                TextXAlignment={Enum.TextXAlignment.Left}
                TextYAlignment={Enum.TextYAlignment.Top}
                ref={primaryTextRef}
            />
            <textlabel
                AutomaticSize={Enum.AutomaticSize.Y}
                BackgroundTransparency={1}
                FontFace={RobotoMono}
                LayoutOrder={2}
                Size={new UDim2(1, 0, 0, 0)}
                Text="Click to dismiss"
                TextColor3={style.text.Lerp(new Color3(1, 1, 1), 0.2)}
                TextSize={hintTextSize}
                TextTransparency={HINT_TEXT_TRANSPARENCY}
                TextWrapped={false}
                TextXAlignment={Enum.TextXAlignment.Left}
                TextYAlignment={Enum.TextYAlignment.Top}
                ref={hintTextRef}
            />
        </textbutton>
    );
}

const MemoizedToastItem = memo(ToastItem);


import Signal from "@antivivi/lemon-signal";
import { HttpService } from "@rbxts/services";

type ToastVariant = "info" | "success" | "warning" | "error";

interface ToastOptions {
    message: string;
    duration?: number;
    variant?: ToastVariant;
    icon?: string;
    id?: string;
}

interface ToastRequest extends ToastOptions {
    id: string;
    createdAt: number;
    variant: ToastVariant;
    duration: number;
}

const toastSignal = new Signal<(toast: ToastRequest) => void>();

export const DEFAULT_TOAST_DURATION = 3.5;
export const MAX_TOAST_QUEUE = 3;

export function subscribeToasts(listener: (toast: ToastRequest) => void) {
    return toastSignal.connect(listener);
}

export function showToast(options: ToastOptions): string {
    const { message, duration, variant, icon, id } = options;
    if (message === undefined || message === "") {
        error("Toast message cannot be empty");
    }

    const toast: ToastRequest = {
        id: id ?? HttpService.GenerateGUID(false),
        message,
        duration: duration ?? DEFAULT_TOAST_DURATION,
        variant: variant ?? "info",
        icon,
        createdAt: tick(),
    };

    toastSignal.fire(toast);
    return toast.id;
}

export function showErrorToast(message: string, duration?: number) {
    return showToast({ message, duration, variant: "error" });
}

export function showSuccessToast(message: string, duration?: number) {
    return showToast({ message, duration, variant: "success" });
}

export function showWarningToast(message: string, duration?: number) {
    return showToast({ message, duration, variant: "warning" });
}

export type { ToastOptions, ToastRequest, ToastVariant };


import React, { StrictMode } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import SidebarButtons from "client/components/sidebar/SidebarButtons";
import TooltipWindow from "client/components/tooltip/TooltipWindow";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
    },
    () => {
        return (
            <StrictMode>
                <TooltipWindow />
                <SidebarButtons />
            </StrictMode>
        );
    },
);


/**
 * @fileoverview React tooltip provider and context for managing tooltips across the application.
 *
 * Provides a centralized tooltip system using React Context and hooks, replacing the
 * Flamework controller pattern with modern React patterns. Supports both item tooltips
 * and simple message tooltips with smooth animations and positioning.
 */

import { useCallback } from "@rbxts/react";
import { TooltipManager } from "client/components/tooltip/TooltipWindow";
import useHover from "client/hooks/useHover";
import Item from "shared/item/Item";

declare global {
    interface UseTooltipProps {
        data: TooltipData | (() => TooltipData);
        onMoved?: () => void;
        onEnter?: () => void;
        onLeave?: () => void;
    }

    interface TooltipData {
        /** Plain text message for simple tooltips */
        message?: string;
        /** Item to display for item tooltips */
        item?: Item;
        /** Unique item instance */
        uniqueInstance?: UniqueItemInstance;
        /** Position where tooltip should appear */
        position?: Vector2;
    }
}

/**
 * Hook that provides tooltip event handlers for components
 *
 * @param tooltipData Static tooltip data or function that returns tooltip data
 * @returns Hover data object
 */
export function useTooltipProps({ data, onEnter, onLeave }: UseTooltipProps): UseHoverReturn {
    const handleMouseEnter = useCallback(() => {
        onEnter?.();
        const tooltipData = typeIs(data, "function") ? data() : data;
        TooltipManager.showTooltip(tooltipData);
    }, [onEnter]);

    const handleMouseLeave = useCallback(() => {
        onLeave?.();
        TooltipManager.hideTooltip();
    }, [onLeave]);

    return useHover({ onEnter: handleMouseEnter, onLeave: handleMouseLeave });
}

/**
 * Convenience hook for item tooltips
 *
 * @param item The item to display in the tooltip
 * @param uniqueInstance Unique item identifier
 * @returns Hover data object
 */

export function useItemTooltip(item: Item, uniqueInstance?: UniqueItemInstance): UseHoverReturn {
    return useTooltipProps({ data: { item, uniqueInstance } });
} /**
 * Convenience hook for message tooltips
 *
 * @param message The message to display in the tooltip
 * @returns Hover data object
 */

export function useMessageTooltip(message: string): UseHoverReturn {
    return useTooltipProps({ data: { message } });
}