import { simpleInterval } from "@antivivi/vrldk";
import React, { Fragment, JSX, useEffect, useState } from "@rbxts/react";
import { CollectionService } from "@rbxts/services";
import CreateListingForm from "client/components/marketplace/CreateListingForm";
import ListingCard from "client/components/marketplace/ListingCard";
import SearchFilters, { MarketplaceSortOption } from "client/components/marketplace/SearchFilters";
import useSingleDocument from "client/components/sidebar/useSingleDocumentWindow";
import TechWindow from "client/components/window/TechWindow";
import useProperty from "client/hooks/useProperty";
import { getAsset } from "shared/asset/AssetMap";
import { playSound } from "shared/asset/GameAssets";
import { RobotoMono, RobotoMonoBold } from "shared/asset/GameFonts";
import { getPlayerCharacter } from "shared/hamster/getPlayerCharacter";
import "shared/marketplace/MarketplaceListing";
import Packets from "shared/Packets";

type TabType = "Browse" | "MyListings" | "CreateListing";

// Main Marketplace Window Component
export default function MarketplaceWindow() {
    const { id, visible, openDocument, closeDocument } = useSingleDocument({ id: "Marketplace", priority: 1 });
    const [activeTab, setActiveTab] = useState<TabType>("Browse");
    const [listings, setListings] = useState<MarketplaceListing[]>([]);
    const myListings = useProperty(Packets.empireActiveListings);
    const [sort, setSort] = useState<MarketplaceSortOption>();

    // Track whether player was previously in range to control window open/close logic
    useEffect(() => {
        let wasInRange = false;
        const cleanup = simpleInterval(() => {
            const models = CollectionService.GetTagged("MarketplaceTerminal");
            let marketplacePosition: Vector3 | undefined;
            for (const model of models) {
                if (!model.IsA("Model")) continue;
                marketplacePosition = model.GetPivot().Position;
                break;
            }
            if (marketplacePosition === undefined) return;

            const characterPosition = getPlayerCharacter()?.GetPivot();
            if (characterPosition === undefined) return;

            const distance = marketplacePosition.sub(characterPosition.Position).Magnitude;
            const inRange = distance <= 15;

            if (inRange && !wasInRange) {
                openDocument();
            }
            if (!inRange && wasInRange) {
                closeDocument();
            }
            wasInRange = inRange;
        }, 0.5);

        return () => {
            cleanup();
        };
    }, []);

    // Load data when window becomes visible
    useEffect(() => {
        if (visible) {
            // Get current state
            const currentListings = Packets.searchListings.toServer("", 1);
            if (currentListings) setListings(currentListings);
        }
    }, [visible]);

    const handleBuyItem = (uuid: string) => {
        Packets.buyItem.toServer(uuid);
    };

    const handleCancelListing = (uuid: string) => {
        const success = Packets.cancelListing.toServer(uuid);
        if (success) {
            playSound("Success.mp3");
        } else {
            playSound("Error.mp3");
        }
    };

    const handleCreateListing = (uuid: string, price: number) => {
        return Packets.createListing.toServer(uuid, price);
    };

    const tabLabels: Record<TabType, string> = {
        Browse: "Browse",
        MyListings: "My Listings",
        CreateListing: "Create Listing",
    };

    return (
        <TechWindow title="Marketplace" icon={getAsset("assets/Purchase.png")} id={id} visible={visible}>
            <uilistlayout
                FillDirection={Enum.FillDirection.Vertical}
                HorizontalAlignment={Enum.HorizontalAlignment.Left}
                SortOrder={Enum.SortOrder.LayoutOrder}
                Padding={new UDim(0, 18)}
            />

            {/* Tab Bar */}
            <frame BackgroundTransparency={1} LayoutOrder={0} Size={new UDim2(1, 0, 0, 30)}>
                <uilistlayout
                    FillDirection={Enum.FillDirection.Horizontal}
                    HorizontalAlignment={Enum.HorizontalAlignment.Left}
                    Padding={new UDim(0.02, 0)}
                />

                {(["Browse", "MyListings", "CreateListing"] as TabType[]).map((tab) => {
                    const isActive = tab === activeTab;
                    return (
                        <textbutton
                            BackgroundColor3={isActive ? Color3.fromRGB(35, 98, 189) : Color3.fromRGB(24, 32, 48)}
                            BackgroundTransparency={isActive ? 0 : 0.2}
                            BorderColor3={new Color3(1, 1, 1)}
                            BorderSizePixel={2}
                            Size={new UDim2(0.32, 0, 1, 0)}
                            Text={tabLabels[tab]}
                            TextColor3={isActive ? Color3.fromRGB(226, 238, 255) : Color3.fromRGB(155, 178, 216)}
                            TextScaled={true}
                            FontFace={isActive ? RobotoMonoBold : RobotoMono}
                            Event={{
                                Activated: () => {
                                    playSound("MenuClick.mp3");
                                    setActiveTab(tab);
                                },
                                MouseEnter: () => {
                                    playSound("EmphasisButtonHover.mp3", undefined, (sound) => {
                                        sound.Volume = 0.2;
                                        sound.PlaybackSpeed = 2;
                                    });
                                },
                            }}
                        >
                            <uipadding
                                PaddingTop={new UDim(0, 4)}
                                PaddingBottom={new UDim(0, 4)}
                                PaddingLeft={new UDim(0, 8)}
                                PaddingRight={new UDim(0, 8)}
                            />
                            <uistroke
                                ApplyStrokeMode={Enum.ApplyStrokeMode.Border}
                                Thickness={1}
                                Color={Color3.fromRGB(26, 26, 28)}
                            />
                            {isActive ? (
                                <uigradient
                                    Color={
                                        new ColorSequence([
                                            new ColorSequenceKeypoint(0, Color3.fromRGB(111, 182, 255)),
                                            new ColorSequenceKeypoint(1, Color3.fromRGB(52, 142, 255)),
                                        ])
                                    }
                                />
                            ) : undefined}
                        </textbutton>
                    );
                })}
            </frame>

            {/* Content Area */}
            <frame BackgroundTransparency={1} Size={new UDim2(1, 0, 1, -48)} ZIndex={2}>
                {/* Browse Tab */}
                {activeTab === "Browse" ? (
                    <frame BackgroundTransparency={1} Size={new UDim2(1, 0, 1, 0)}>
                        <uilistlayout
                            FillDirection={Enum.FillDirection.Vertical}
                            HorizontalAlignment={Enum.HorizontalAlignment.Center}
                            Padding={new UDim(0, 20)}
                        />

                        <SearchFilters
                            onSearch={(query) => setListings(Packets.searchListings.toServer(query, 1))}
                            onSort={(sort) => setSort(sort)}
                        />

                        <scrollingframe
                            BackgroundTransparency={1}
                            AutomaticCanvasSize={Enum.AutomaticSize.Y}
                            CanvasSize={new UDim2(0, 0, 0, 0)}
                            ScrollBarThickness={6}
                            ScrollBarImageColor3={Color3.fromRGB(74, 140, 255)}
                            Size={new UDim2(1, 0, 1, -120)}
                        >
                            <uipadding
                                PaddingTop={new UDim(0, 6)}
                                PaddingBottom={new UDim(0, 6)}
                                PaddingLeft={new UDim(0, 2)}
                                PaddingRight={new UDim(0, 2)}
                            />
                            <uilistlayout
                                FillDirection={Enum.FillDirection.Vertical}
                                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                                Padding={new UDim(0, 14)}
                            />

                            {(() => {
                                return listings.map((listing) => (
                                    <ListingCard listing={listing} onBuy={handleBuyItem} />
                                ));
                            })()}

                            {listings.size() === 0 && (
                                <textlabel
                                    BackgroundTransparency={1}
                                    FontFace={RobotoMono}
                                    Size={new UDim2(1, 0, 0, 20)}
                                    Text="No listings found."
                                    TextColor3={Color3.fromRGB(118, 138, 170)}
                                    TextScaled={true}
                                >
                                    <uistroke Thickness={1} Color={Color3.fromRGB(47, 64, 96)} Transparency={0.3} />
                                </textlabel>
                            )}
                        </scrollingframe>
                    </frame>
                ) : (
                    <Fragment />
                )}

                {/* My Listings Tab */}
                {activeTab === "MyListings" ? (
                    <scrollingframe
                        BackgroundTransparency={1}
                        AutomaticCanvasSize={Enum.AutomaticSize.Y}
                        CanvasSize={new UDim2(0, 0, 0, 0)}
                        ScrollBarThickness={6}
                        ScrollBarImageColor3={Color3.fromRGB(74, 140, 255)}
                        Size={new UDim2(1, 0, 1, 0)}
                    >
                        <uipadding
                            PaddingTop={new UDim(0, 6)}
                            PaddingBottom={new UDim(0, 6)}
                            PaddingLeft={new UDim(0, 2)}
                            PaddingRight={new UDim(0, 2)}
                        />
                        <uilistlayout
                            FillDirection={Enum.FillDirection.Vertical}
                            HorizontalAlignment={Enum.HorizontalAlignment.Center}
                            Padding={new UDim(0, 14)}
                        />

                        {(() => {
                            const elements = new Array<JSX.Element>();
                            for (const [, listing] of myListings) {
                                elements.push(
                                    <ListingCard listing={listing} onCancel={handleCancelListing} isOwner={true} />,
                                );
                            }
                            return elements;
                        })()}

                        {myListings.size() === 0 && (
                            <textlabel
                                BackgroundTransparency={1}
                                FontFace={RobotoMono}
                                Size={new UDim2(1, 0, 0, 20)}
                                Text="You have no active listings."
                                TextColor3={Color3.fromRGB(118, 138, 170)}
                                TextScaled={true}
                            >
                                <uistroke Thickness={1} Color={Color3.fromRGB(47, 64, 96)} Transparency={0.3} />
                            </textlabel>
                        )}
                    </scrollingframe>
                ) : (
                    <Fragment />
                )}

                {/* Create Listing Tab */}
                {activeTab === "CreateListing" ? <CreateListingForm onSubmit={handleCreateListing} /> : <Fragment />}
            </frame>
        </TechWindow>
    );
}


import React, { useEffect, useState } from "@rbxts/react";
import { playSound } from "shared/asset/GameAssets";
import { RobotoMono, RobotoMonoBold } from "shared/asset/GameFonts";

export type MarketplaceSortOption = "price_asc" | "price_desc" | "created_asc" | "created_desc";

export default function SearchFilters({
    onSearch,
    onSort,
}: {
    onSearch: (query: string) => void;
    onSort: (sortBy: MarketplaceSortOption) => void;
}) {
    const [searchQuery, setSearchQuery] = useState("");
    const [sortBy, setSortBy] = useState<MarketplaceSortOption>("created_desc");

    useEffect(() => {
        onSort(sortBy as MarketplaceSortOption);
    }, [sortBy]);

    const sortLabel = (() => {
        switch (sortBy) {
            case "created_desc":
                return "Newest First";
            case "created_asc":
                return "Oldest First";
            case "price_asc":
                return "Price Low to High";
            default:
                return "Price High to Low";
        }
    })();

    return (
        <frame
            BackgroundColor3={Color3.fromRGB(18, 24, 36)}
            BorderColor3={Color3.fromRGB(255, 255, 255)}
            BorderSizePixel={2}
            Size={new UDim2(1, 0, 0, 0)}
            AutomaticSize={Enum.AutomaticSize.Y}
        >
            <uistroke Thickness={1} Color={Color3.fromRGB(58, 86, 142)} />
            <uigradient
                Rotation={0}
                Color={
                    new ColorSequence([
                        new ColorSequenceKeypoint(0, Color3.fromRGB(20, 26, 39)),
                        new ColorSequenceKeypoint(1, Color3.fromRGB(12, 16, 24)),
                    ])
                }
            />
            <frame BackgroundTransparency={1} Size={new UDim2(1, 0, 1, 0)} ZIndex={1}>
                <uipadding
                    PaddingTop={new UDim(0, 6)}
                    PaddingBottom={new UDim(0, 6)}
                    PaddingLeft={new UDim(0, 6)}
                    PaddingRight={new UDim(0, 6)}
                />
                <uilistlayout
                    FillDirection={Enum.FillDirection.Horizontal}
                    HorizontalAlignment={Enum.HorizontalAlignment.Left}
                    VerticalAlignment={Enum.VerticalAlignment.Top}
                    Padding={new UDim(0, 6)}
                />

                {/* Search Box */}
                <frame
                    BackgroundTransparency={1}
                    LayoutOrder={1}
                    Size={new UDim2(0.75, -12, 0, 0)}
                    AutomaticSize={Enum.AutomaticSize.Y}
                >
                    <uipadding
                        PaddingTop={new UDim(0, 6)}
                        PaddingBottom={new UDim(0, 6)}
                        PaddingLeft={new UDim(0, 6)}
                        PaddingRight={new UDim(0, 6)}
                    />
                    <uilistlayout
                        FillDirection={Enum.FillDirection.Vertical}
                        HorizontalAlignment={Enum.HorizontalAlignment.Left}
                        VerticalAlignment={Enum.VerticalAlignment.Top}
                        Padding={new UDim(0, 8)}
                    />

                    <textlabel
                        BackgroundTransparency={1}
                        FontFace={RobotoMonoBold}
                        Size={new UDim2(1, 0, 0, 20)}
                        Text="Search Items"
                        TextColor3={Color3.fromRGB(204, 222, 255)}
                        TextScaled={true}
                        TextXAlignment={Enum.TextXAlignment.Left}
                    />

                    <textbox
                        BackgroundColor3={Color3.fromRGB(24, 32, 48)}
                        BorderColor3={Color3.fromRGB(100, 100, 100)}
                        BorderSizePixel={1}
                        Size={new UDim2(1, 0, 0, 26)}
                        Text={""}
                        PlaceholderText="Search by item name or ID"
                        PlaceholderColor3={Color3.fromRGB(120, 140, 175)}
                        TextColor3={Color3.fromRGB(226, 238, 255)}
                        TextScaled={true}
                        TextXAlignment={Enum.TextXAlignment.Left}
                        FontFace={RobotoMono}
                        ClearTextOnFocus={false}
                        Change={{
                            Text: (textBox) => {
                                if (textBox.Text.size() > searchQuery.size()) {
                                    switch (math.random(1, 4)) {
                                        case 1:
                                            playSound("KeyPress1.mp3");
                                            break;
                                        case 2:
                                            playSound("KeyPress2.mp3");
                                            break;
                                        case 3:
                                            playSound("KeyPress3.mp3");
                                            break;
                                        case 4:
                                            playSound("KeyPress4.mp3");
                                            break;
                                    }
                                } else {
                                    playSound("KeyDelete.mp3");
                                }
                                setSearchQuery(textBox.Text);
                            },
                        }}
                        Event={{
                            FocusLost: (textBox, enterPressed) => {
                                setSearchQuery(textBox.Text);
                                if (enterPressed) onSearch(searchQuery);
                            },
                        }}
                    >
                        <uipadding
                            PaddingTop={new UDim(0, 4)}
                            PaddingBottom={new UDim(0, 4)}
                            PaddingLeft={new UDim(0, 8)}
                            PaddingRight={new UDim(0, 8)}
                        />
                    </textbox>
                </frame>

                {/* Sort Dropdown */}
                <frame
                    BackgroundTransparency={1}
                    LayoutOrder={2}
                    Size={new UDim2(0.25, -6, 0, 0)}
                    AutomaticSize={Enum.AutomaticSize.Y}
                >
                    <uipadding
                        PaddingTop={new UDim(0, 6)}
                        PaddingBottom={new UDim(0, 6)}
                        PaddingLeft={new UDim(0, 6)}
                        PaddingRight={new UDim(0, 6)}
                    />
                    <uilistlayout
                        FillDirection={Enum.FillDirection.Vertical}
                        HorizontalAlignment={Enum.HorizontalAlignment.Left}
                        VerticalAlignment={Enum.VerticalAlignment.Top}
                        Padding={new UDim(0, 8)}
                    />

                    <textlabel
                        BackgroundTransparency={1}
                        FontFace={RobotoMonoBold}
                        Size={new UDim2(1, 0, 0, 20)}
                        Text="Sort By"
                        TextColor3={Color3.fromRGB(204, 222, 255)}
                        TextScaled={true}
                        TextXAlignment={Enum.TextXAlignment.Left}
                    />

                    <textbutton
                        BackgroundColor3={Color3.fromRGB(24, 32, 48)}
                        BorderColor3={Color3.fromRGB(100, 100, 100)}
                        BorderSizePixel={1}
                        Size={new UDim2(1, 0, 0, 26)}
                        Text={sortLabel}
                        TextColor3={Color3.fromRGB(226, 238, 255)}
                        TextScaled={true}
                        FontFace={RobotoMono}
                        Event={{
                            Activated: () => {
                                const options: MarketplaceSortOption[] = [
                                    "created_desc",
                                    "created_asc",
                                    "price_asc",
                                    "price_desc",
                                ];
                                const currentIndex = options.indexOf(sortBy);
                                const nextIndex = (currentIndex + 1) % options.size();
                                setSortBy(options[nextIndex]);
                                playSound("CheckOn.mp3");
                            },
                        }}
                    >
                        <uipadding
                            PaddingTop={new UDim(0, 4)}
                            PaddingBottom={new UDim(0, 4)}
                            PaddingLeft={new UDim(0, 8)}
                            PaddingRight={new UDim(0, 8)}
                        />
                    </textbutton>
                </frame>
            </frame>
        </frame>
    );
}


/**
 * @fileoverview React component for displaying NPC headshots in dialogue windows.
 *
 * Handles:
 * - Viewport frame setup for 3D model display
 * - Camera positioning and focusing on the NPC's head
 * - Model cloning and anchoring for viewport display
 */

import React, { useEffect, useRef } from "@rbxts/react";
import { Workspace } from "@rbxts/services";

interface DialogueHeadshotProps {
    /** The NPC model to display in the viewport */
    model?: Model;
}

export default function DialogueHeadshot({ model }: DialogueHeadshotProps) {
    const viewportRef = useRef<ViewportFrame>();

    useEffect(() => {
        if (!model || !viewportRef.current || model === Workspace) return;

        const viewport = viewportRef.current;
        viewport.ClearAllChildren();

        const camera = new Instance("Camera");
        camera.Parent = viewport;
        viewport.CurrentCamera = camera;

        const clone = model.Clone();

        // Anchor all parts in the model
        for (const part of clone.GetChildren()) {
            if (part.IsA("BasePart")) {
                part.Anchored = true;
            }
        }

        clone.PivotTo(new CFrame(0, 0, 0));

        // Set camera to focus on the head of the model
        const head = (clone.FindFirstChild("Head") as BasePart) ?? clone.PrimaryPart;
        if (!head) {
            warn("NPC model does not have a Head or PrimaryPart.");
            return;
        }

        const headCFrame = head.CFrame;
        const distance = 1 + head.Size.Y;
        camera.CFrame = headCFrame.mul(CFrame.fromEulerAnglesXYZ(0, math.pi, 0)).mul(new CFrame(0, 0, distance));

        // Set the camera's field of view
        camera.FieldOfView = 70;
        clone.Parent = viewport;
    }, [model]);

    return (
        <viewportframe
            ref={viewportRef}
            key="ViewportFrame"
            AnchorPoint={new Vector2(1, 0.5)}
            BackgroundTransparency={1}
            Position={new UDim2(1, 0, 0, 0)}
            Size={new UDim2(0, 45, 0, 45)}
        />
    );
}


import React, { StrictMode, useEffect } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { Workspace } from "@rbxts/services";
import { CreateReactStory } from "@rbxts/ui-labs";
import DialogueWindow from "client/components/npc/DialogueWindow";
import StoryMocking from "client/components/StoryMocking";
import TooltipWindow from "client/components/tooltip/TooltipWindow";
import Packets from "shared/Packets";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            npcName: "Merchant Bob",
            dialogueText:
                "Welcome to my shop! I have many fine wares for sale. Would you like to take a look at my inventory?",
        },
    },
    (props) => {
        StoryMocking.mockData();

        Packets.nextDialogue.fromClient(() => {
            return true;
        });

        useEffect(() => {
            const model = new Instance("Model");
            model.Name = props.controls.npcName;

            const head = new Instance("Part");
            head.Name = "Head";
            head.Shape = Enum.PartType.Ball;
            head.Size = new Vector3(2, 1, 1);
            head.BrickColor = new BrickColor("Light orange");
            head.Parent = model;

            const humanoid = new Instance("Humanoid");
            humanoid.Parent = model;

            model.PrimaryPart = head;
            model.Parent = Workspace;

            task.delay(0.5, () => {
                Packets.npcMessage.toAllClients(props.controls.dialogueText, 1, 1, true, model);
            });

            return () => {
                model.Destroy();
            };
        }, [props.controls.dialogueText, props.controls.npcName]);

        // Test the component directly
        return (
            <StrictMode>
                <DialogueWindow />
                <TooltipWindow />
            </StrictMode>
        );
    },
);


/**
 * @fileoverview React component for displaying NPC dialogue windows.
 *
 * Handles:
 * - Displaying dialogue text with typing animation
 * - NPC name display with dynamic colors
 * - Headshot viewport integration
 * - Click-to-continue functionality
 * - Smooth entrance/exit animations
 */

import ComputeNameColor from "@antivivi/rbxnamecolor";
import { observeTag } from "@antivivi/vrldk";
import React, { useEffect, useRef, useState } from "@rbxts/react";
import { createRoot } from "@rbxts/react-roblox";
import { TextChatService } from "@rbxts/services";
import DialogueHeadshot from "client/components/npc/DialogueHeadshot";
import NPCNotification from "client/components/npc/NPCNotification";
import { useDocument } from "client/components/window/DocumentManager";
import { RobotoSlab, RobotoSlabBold } from "shared/asset/GameFonts";
import { useTextAnimation } from "client/hooks/useTextAnimation";
import { getAsset } from "shared/asset/AssetMap";
import { ASSETS } from "shared/asset/GameAssets";
import { getDisplayName, getTextChannels } from "shared/constants";
import { IS_EDIT } from "shared/Context";
import Packets from "shared/Packets";

interface DialogueData {
    /** The NPC's name to display */
    name?: string;
    /** The dialogue text to show */
    text: string;
    /** The NPC model for headshot display */
    model?: Model;
    /** Custom text sound for this NPC */
    textSound?: Sound;
}

/**
 * Computes the background and stroke colors for the dialogue window based on NPC name
 * @param name The NPC's name
 * @returns Object containing backgroundColor and strokeColor
 */
function computeDialogueColors(name?: string): { backgroundColor: Color3; strokeColor: Color3 } {
    const baseColor =
        name === undefined ? Color3.fromRGB(165, 165, 165) : ComputeNameColor(name).Lerp(new Color3(), 0.3);

    return {
        backgroundColor: baseColor,
        strokeColor: baseColor,
    };
}

/**
 * Gets the appropriate text sound for an NPC model
 * @param model The NPC model
 * @returns The sound to use for text typing, if available
 */
function getTextSound(model?: Model): Sound | undefined {
    if (!model) return undefined;
    return ASSETS.NPCTextSounds.FindFirstChild(model.Name) as Sound | undefined;
}

let currentText = "";

export default function DialogueWindow() {
    const { visible, setVisible } = useDocument({ id: "Dialogue", priority: -1 });
    const [dialogueData, setDialogueData] = useState<DialogueData>();
    const frameRef = useRef<TextButton>();
    const textLabelRef = useRef<TextLabel>();
    const hintLabelRef = useRef<TextLabel>();

    const { backgroundColor, strokeColor } = computeDialogueColors(dialogueData?.name);
    const textSound = getTextSound(dialogueData?.model);

    // Handle window show/hide animations
    const openPosition = new UDim2(0.5, 0, 0.975, -30);
    const closedPosition = new UDim2(0.5, 0, 1.5, 0);

    useEffect(() => {
        const frame = frameRef.current;
        if (!frame) return;
        if (visible) {
            frame.TweenPosition(openPosition, Enum.EasingDirection.Out, Enum.EasingStyle.Quad, 0.3, true);
        } else {
            frame.TweenPosition(closedPosition, Enum.EasingDirection.In, Enum.EasingStyle.Linear, 0.2, true);
        }
    }, [visible]);

    useEffect(() => {
        const channel = IS_EDIT ? undefined : (getTextChannels().WaitForChild("RBXGeneral") as TextChannel);
        const npcTagColor = Color3.fromRGB(201, 255, 13).ToHex();
        const emptyColor = Color3.fromRGB(0, 181, 28).ToHex();
        const connection = Packets.npcMessage.fromServer((message, pos, endPos, prompt, model) => {
            currentText = message;
            print(`Received NPC message: ${message} (pos ${pos}/${endPos})`);
            const humanoid = model?.FindFirstChildOfClass("Humanoid");
            let name = undefined as string | undefined;

            if (model !== undefined && humanoid !== undefined) {
                name = getDisplayName(humanoid);
                channel?.DisplaySystemMessage(
                    `<font color="#${npcTagColor}">[${pos}/${endPos}]</font> <font color="#${ComputeNameColor(name).ToHex()}">${name}:</font> ${message}`,
                    "tag:hidden",
                );
                TextChatService.DisplayBubble(model.WaitForChild("Head") as BasePart, message);
            } else {
                channel?.DisplaySystemMessage(`<font color="#${emptyColor}">${message}</font>`, "tag:hidden");
            }

            // Show dialogue window if prompted
            if (prompt === true) {
                setDialogueData({
                    name,
                    text: message,
                    model: model as Model,
                    textSound: getTextSound(model as Model),
                });
                setVisible(true);
            }
        });

        const cleanupPerPrompt = new Map<ProximityPrompt, () => void>();
        const cleanup = observeTag(
            "NPCPrompt",
            (prompt) => {
                if (!prompt.IsA("ProximityPrompt") || cleanupPerPrompt.has(prompt)) return;
                const gui = new Instance("BillboardGui");
                gui.Active = true;
                gui.ClipsDescendants = true;
                gui.Size = new UDim2(2, 0, 2, 0);
                gui.StudsOffset = new Vector3(0, 4, 0);
                gui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling;

                const root = createRoot(gui);
                cleanupPerPrompt.set(prompt, () => {
                    root.unmount();
                    gui.Destroy();
                });
                root.render(<NPCNotification prompt={prompt} />);

                gui.Parent = prompt.Parent;
            },
            (prompt) => {
                if (!prompt.IsA("ProximityPrompt")) return;
                const cleanup = cleanupPerPrompt.get(prompt);
                if (cleanup !== undefined) {
                    cleanup();
                    cleanupPerPrompt.delete(prompt);
                }
            },
        );

        return () => {
            connection.Disconnect();
            cleanup();
            cleanupPerPrompt.forEach((fn) => fn());
            cleanupPerPrompt.clear();
        };
    }, []);

    // Handle text typing animation
    const { displayText, isComplete, skipToEnd } = useTextAnimation({
        text: dialogueData?.text ?? "",
        textLabelRef,
        hintLabelRef,
        visible,
        textSound,
    });

    return (
        <textbutton
            ref={frameRef}
            AnchorPoint={new Vector2(0.5, 1)}
            AutoButtonColor={false}
            BackgroundColor3={backgroundColor}
            BorderSizePixel={0}
            Position={closedPosition}
            Size={new UDim2(0.25, 250, 0.15, 100)}
            Text=""
            TextColor3={Color3.fromRGB(0, 0, 0)}
            TextSize={14}
            Event={{
                Activated: () => {
                    if (!isComplete) {
                        // Skip to end of text if still typing
                        skipToEnd();
                        return;
                    }
                    let message = currentText;
                    if (Packets.nextDialogue.toServer()) {
                        if (currentText !== message) {
                            return; // We have a new message, don't close yet
                        }
                        setVisible(false);
                    }
                },
            }}
        >
            <folder>
                <imagelabel
                    AnchorPoint={new Vector2(1, 0)}
                    BackgroundTransparency={1}
                    Image={getAsset("assets/GlassReflection.png")}
                    ImageColor3={Color3.fromRGB(0, 0, 0)}
                    ImageTransparency={0.9}
                    Position={new UDim2(1, 0, 0, 0)}
                    Rotation={270}
                    ScaleType={Enum.ScaleType.Fit}
                    Size={new UDim2(1, 0, 1, 0)}
                    SizeConstraint={Enum.SizeConstraint.RelativeYY}
                    ZIndex={-2}
                />
                <frame
                    AnchorPoint={new Vector2(0.5, 0.5)}
                    BackgroundColor3={Color3.fromRGB(0, 0, 0)}
                    BackgroundTransparency={0.8}
                    BorderSizePixel={0}
                    Position={new UDim2(0.5, 0, 0.5, 0)}
                    Size={new UDim2(1, 14, 1, -6)}
                    ZIndex={-2}
                >
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(0, 0, 0)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(0, 0, 0)),
                            ])
                        }
                    />
                </frame>
                <imagelabel
                    AnchorPoint={new Vector2(0.5, 0)}
                    BackgroundTransparency={1}
                    Image={getAsset("assets/GridCheckers.png")}
                    ImageColor3={Color3.fromRGB(0, 0, 0)}
                    ImageTransparency={0.95}
                    Position={new UDim2(0.5, 0, 0, 0)}
                    ScaleType={Enum.ScaleType.Tile}
                    Size={new UDim2(1, 20, 1, 0)}
                    TileSize={new UDim2(0, 25, 0, 25)}
                    ZIndex={-4}
                />
            </folder>
            <uigradient
                Color={
                    new ColorSequence([
                        new ColorSequenceKeypoint(0, Color3.fromRGB(189, 189, 189)),
                        new ColorSequenceKeypoint(1, Color3.fromRGB(255, 255, 255)),
                    ])
                }
                Rotation={272}
            />
            <uipadding PaddingLeft={new UDim(0, 10)} PaddingRight={new UDim(0, 10)} />
            <uistroke ApplyStrokeMode={Enum.ApplyStrokeMode.Border} Color={strokeColor} Thickness={2}>
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(97, 97, 97)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(63, 63, 63)),
                        ])
                    }
                    Rotation={90}
                />
            </uistroke>

            {/* NPC Name Label */}
            <textlabel
                key="NameLabel"
                Active={true}
                AnchorPoint={new Vector2(0, 0.5)}
                BackgroundTransparency={1}
                FontFace={RobotoSlabBold}
                Position={new UDim2(0, 10, 0, 0)}
                Size={new UDim2(0.5, 0, 0, 40)}
                Text={dialogueData?.name ?? ""}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextSize={34}
                TextWrapped={true}
                TextXAlignment={Enum.TextXAlignment.Left}
            >
                <uistroke Color={Color3.fromRGB(50, 50, 50)} Thickness={2}>
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(97, 97, 97)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(63, 63, 63)),
                            ])
                        }
                        Rotation={90}
                    />
                </uistroke>
            </textlabel>

            {/* Dialogue Text Label */}
            <textlabel
                ref={textLabelRef}
                key="TextLabel"
                Active={true}
                AnchorPoint={new Vector2(0.5, 0)}
                BackgroundTransparency={1}
                FontFace={RobotoSlab}
                Position={new UDim2(0.5, 0, 0, 25)}
                Size={new UDim2(1, -30, 1, -75)}
                Text={displayText}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextSize={25}
                TextWrapped={true}
                TextXAlignment={Enum.TextXAlignment.Left}
                TextYAlignment={Enum.TextYAlignment.Top}
            >
                <uistroke Color={Color3.fromRGB(50, 50, 50)} Thickness={2}>
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(97, 97, 97)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(63, 63, 63)),
                            ])
                        }
                        Rotation={90}
                    />
                </uistroke>
                <uitextsizeconstraint MaxTextSize={25} />
            </textlabel>

            {/* Click to continue hint */}
            <textlabel
                ref={hintLabelRef}
                key="HintLabel"
                Active={true}
                AnchorPoint={new Vector2(0.5, 1)}
                BackgroundTransparency={1}
                FontFace={RobotoSlab}
                Position={new UDim2(0.5, 0, 1, -20)}
                Size={new UDim2(1, -30, 0, 20)}
                Text="Click to continue"
                TextColor3={Color3.fromRGB(198, 198, 198)}
                TextScaled={true}
                TextSize={25}
                TextWrapped={true}
                TextXAlignment={Enum.TextXAlignment.Right}
                TextYAlignment={Enum.TextYAlignment.Top}
                Visible={isComplete}
            >
                <uistroke Color={Color3.fromRGB(50, 50, 50)} Thickness={2}>
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(97, 97, 97)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(63, 63, 63)),
                            ])
                        }
                        Rotation={90}
                    />
                </uistroke>
            </textlabel>

            {/* NPC Headshot Viewport */}
            <DialogueHeadshot model={dialogueData?.model} />
        </textbutton>
    );
}


import React, { StrictMode, useEffect, useState } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import NPCNotification from "client/components/npc/NPCNotification";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            visible: true,
        },
    },
    (props) => {
        const [proximityPrompt] = useState(new Instance("ProximityPrompt"));

        useEffect(() => {
            proximityPrompt.Enabled = props.controls.visible;
        }, [props.controls.visible]);

        return (
            <StrictMode>
                <NPCNotification prompt={proximityPrompt} />
            </StrictMode>
        );
    },
);


import React, { useEffect, useRef } from "@rbxts/react";
import { TweenService } from "@rbxts/services";
import { getAsset } from "shared/asset/AssetMap";

export default function NPCNotification({ prompt }: { prompt: ProximityPrompt }) {
    const imageRef = useRef<ImageLabel>();

    useEffect(() => {
        const update = () => {
            if (!imageRef.current) return;

            if (prompt.Enabled) {
                TweenService.Create(imageRef.current, new TweenInfo(0.3), { ImageTransparency: 0 }).Play();
            } else {
                TweenService.Create(imageRef.current, new TweenInfo(0.15), { ImageTransparency: 1 }).Play();
            }
        };
        const conn = prompt.GetPropertyChangedSignal("Enabled").Connect(update);
        update();
        return () => conn.Disconnect();
    }, []);

    return (
        <imagelabel
            ref={imageRef}
            BackgroundTransparency={1}
            Image={getAsset("assets/NPCNotification.png")}
            Size={new UDim2(1, 0, 1, 0)}
        />
    );
}


import React, { useEffect, useRef, useState } from "@rbxts/react";
import { Players, TweenService, Workspace } from "@rbxts/services";
import PlayerListPanel from "client/components/playerlist/PlayerListPanel";
import { PlayerRosterEntry } from "client/components/playerlist/PlayerListRow";
import { useHotkey } from "client/components/hotkeys/HotkeyManager";
import { useDocument } from "client/components/window/DocumentManager";
import { LOCAL_PLAYER } from "shared/constants";

const THUMBNAIL_CACHE = new Map<number, string>();
const DEFAULT_PANEL_SIZE = new UDim2(0, 410, 0, 420);
const OFFSCREEN_PADDING = 56;
const WIDTH_RATIO = 0.5;
const HEIGHT_RATIO = 0.6;
const MIN_PANEL_WIDTH = 300;
const MAX_PANEL_WIDTH = 410;
const MIN_PANEL_HEIGHT = 300;
const MAX_PANEL_HEIGHT = 420;

interface PanelLayout {
    size: UDim2;
    slideOffset: number;
}

function getLeaderstatString(player: Player, name: string) {
    const leaderstats = player.FindFirstChild("leaderstats") as Folder | undefined;
    if (!leaderstats) return undefined;
    const valueObject = leaderstats.FindFirstChild(name);
    if (valueObject && valueObject.IsA("StringValue")) {
        return valueObject.Value;
    }
    return undefined;
}

function getLeaderstatNumber(player: Player, name: string) {
    const leaderstats = player.FindFirstChild("leaderstats") as Folder | undefined;
    if (!leaderstats) return undefined;
    const valueObject = leaderstats.FindFirstChild(name);
    if (valueObject && (valueObject.IsA("IntValue") || valueObject.IsA("NumberValue"))) {
        return valueObject.Value;
    }
    return undefined;
}

function getThumbnail(player: Player) {
    const cached = THUMBNAIL_CACHE.get(player.UserId);
    if (cached !== undefined) {
        return cached;
    }

    const [success, content, ready] = pcall(() =>
        Players.GetUserThumbnailAsync(player.UserId, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size100x100),
    );

    if (success && ready === true) {
        const thumbnail = content as string;
        THUMBNAIL_CACHE.set(player.UserId, thumbnail);
        return thumbnail;
    }

    return undefined;
}

function buildRoster(): PlayerRosterEntry[] {
    const roster = new Array<PlayerRosterEntry>();
    const currentPlayers = Players.GetPlayers();

    for (let index = 0; index < currentPlayers.size(); index++) {
        const player = currentPlayers[index];
        roster.push({
            userId: player.UserId,
            displayName: player.DisplayName,
            username: player.Name,
            area: getLeaderstatString(player, "Area") ?? "Unknown",
            donated: getLeaderstatNumber(player, "Donated") ?? 0,
            accountAgeDays: player.AccountAge,
            isLocal: player === LOCAL_PLAYER,
            isDeveloper: player.GetAttribute("Developer") === true,
            thumbnail: getThumbnail(player),
            joinOrder: index,
        });
    }

    roster.sort((a, b) => {
        if (a.isLocal !== b.isLocal) {
            return a.isLocal;
        }
        if (a.isDeveloper !== b.isDeveloper) {
            return a.isDeveloper;
        }
        if (a.donated !== b.donated) {
            return a.donated > b.donated;
        }
        if (a.displayName !== b.displayName) {
            return a.displayName.lower() < b.displayName.lower();
        }
        return a.joinOrder < b.joinOrder;
    });

    return roster;
}

function computePanelLayout(): PanelLayout {
    const camera = Workspace.CurrentCamera;
    if (!camera) {
        return {
            size: DEFAULT_PANEL_SIZE,
            slideOffset: DEFAULT_PANEL_SIZE.X.Offset + OFFSCREEN_PADDING,
        };
    }

    const viewport = camera.ViewportSize;
    const width = math.clamp(math.floor(viewport.X * WIDTH_RATIO), MIN_PANEL_WIDTH, MAX_PANEL_WIDTH);
    const height = math.clamp(math.floor(viewport.Y * HEIGHT_RATIO), MIN_PANEL_HEIGHT, MAX_PANEL_HEIGHT);
    const size = new UDim2(0, width, 0, height);

    return {
        size,
        slideOffset: width + OFFSCREEN_PADDING,
    };
}

export default function PlayerListContainer() {
    const { visible, setVisible } = useDocument({ id: "PlayerList", defaultVisible: false, priority: -1 });
    const [players, setPlayers] = useState<PlayerRosterEntry[]>(() => buildRoster());
    const [rendered, setRendered] = useState(visible);
    const [progress, setProgress] = React.useBinding(visible ? 1 : 0);
    const progressRef = useRef(visible ? 1 : 0);
    const [layout, setLayout] = useState<PanelLayout>(() => computePanelLayout());

    useHotkey(
        {
            label: "Player List",
            action: () => {
                setVisible(true);
                return true;
            },
            endAction: () => {
                setVisible(false);
                return true;
            },
        },
        [setVisible],
    );

    useEffect(() => {
        let alive = true;

        const update = () => setPlayers(buildRoster());
        update();

        const addedConnection = Players.PlayerAdded.Connect(update);
        const removingConnection = Players.PlayerRemoving.Connect(update);

        task.spawn(() => {
            while (alive) {
                task.wait(1.25);
                update();
            }
        });

        return () => {
            alive = false;
            addedConnection.Disconnect();
            removingConnection.Disconnect();
        };
    }, []);

    useEffect(() => {
        if (visible) {
            setPlayers(buildRoster());
            setRendered(true);
        }

        const numberValue = new Instance("NumberValue");
        numberValue.Value = progressRef.current;

        const tween = TweenService.Create(
            numberValue,
            new TweenInfo(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
            { Value: visible ? 1 : 0 },
        );

        const changed = numberValue.Changed.Connect(() => {
            const value = numberValue.Value;
            progressRef.current = value;
            setProgress(value);
        });

        const completed = tween.Completed.Connect((status) => {
            if (!visible && status === Enum.PlaybackState.Completed) {
                setRendered(false);
            }
        });

        tween.Play();

        return () => {
            tween.Cancel();
            changed.Disconnect();
            completed.Disconnect();
            numberValue.Destroy();
        };
    }, [visible]);

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

        const updateLayout = () => {
            const computed = computePanelLayout();
            setLayout((prev) => {
                if (
                    prev.size.X.Offset === computed.size.X.Offset &&
                    prev.size.Y.Offset === computed.size.Y.Offset &&
                    prev.slideOffset === computed.slideOffset
                ) {
                    return prev;
                }
                return computed;
            });
        };

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

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

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

    return (
        <PlayerListPanel
            players={players}
            visible={rendered}
            progress={progress}
            size={layout.size}
            slideOffset={layout.slideOffset}
        />
    );
}


import React from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import PlayerListPanel from "client/components/playerlist/PlayerListPanel";
import { PlayerRosterEntry } from "client/components/playerlist/PlayerListRow";
import { TweenService, Workspace } from "@rbxts/services";

const DEFAULT_PANEL_SIZE = new UDim2(0, 410, 0, 420);
const OFFSCREEN_PADDING = 56;
const WIDTH_RATIO = 0.5;
const HEIGHT_RATIO = 0.6;
const MIN_PANEL_WIDTH = 300;
const MAX_PANEL_WIDTH = 410;
const MIN_PANEL_HEIGHT = 300;
const MAX_PANEL_HEIGHT = 420;

interface PanelLayout {
    size: UDim2;
    slideOffset: number;
}

function computePanelLayout(): PanelLayout {
    const camera = Workspace.CurrentCamera;
    if (!camera) {
        return {
            size: DEFAULT_PANEL_SIZE,
            slideOffset: DEFAULT_PANEL_SIZE.X.Offset + OFFSCREEN_PADDING,
        };
    }

    const viewport = camera.ViewportSize;
    const width = math.clamp(math.floor(viewport.X * WIDTH_RATIO), MIN_PANEL_WIDTH, MAX_PANEL_WIDTH);
    const height = math.clamp(math.floor(viewport.Y * HEIGHT_RATIO), MIN_PANEL_HEIGHT, MAX_PANEL_HEIGHT);
    const size = new UDim2(0, width, 0, height);

    return {
        size,
        slideOffset: width + OFFSCREEN_PADDING,
    };
}

const MOCK_PLAYERS: readonly PlayerRosterEntry[] = [
    {
        userId: 101,
        displayName: "Aurora Synthesis",
        username: "AuroraSynth",
        area: "Slamo Village",
        donated: 1250,
        accountAgeDays: 1890,
        isLocal: true,
        isDeveloper: false,
        thumbnail: "rbxassetid://12187368625",
        joinOrder: 0,
    },
    {
        userId: 202,
        displayName: "Cipher Branch",
        username: "CipherBranch",
        area: "Eden",
        donated: 325,
        accountAgeDays: 860,
        isLocal: false,
        isDeveloper: true,
        thumbnail: "rbxassetid://12187368625",
        joinOrder: 1,
    },
    {
        userId: 303,
        displayName: "Nova Prospect",
        username: "NovaProspect",
        area: "Abandoned Rig",
        donated: 0,
        accountAgeDays: 540,
        isLocal: false,
        isDeveloper: false,
        thumbnail: "rbxassetid://12187368625",
        joinOrder: 2,
    },
    {
        userId: 404,
        displayName: "Flux Metric",
        username: "FluxMetric",
        area: "Barren Islands",
        donated: 58,
        accountAgeDays: 126,
        isLocal: false,
        isDeveloper: false,
        thumbnail: "rbxassetid://12187368625",
        joinOrder: 3,
    },
] as const;

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            visible: true,
            highlightLocal: true,
            showDevelopers: true,
            playerCount: 4,
        },
    },
    (props) => {
        const { visible, highlightLocal, showDevelopers } = props.controls;
        const playerCount = math.clamp(math.floor(props.controls.playerCount), 0, MOCK_PLAYERS.size());

        const roster = new Array<PlayerRosterEntry>();
        for (let index = 0; index < playerCount; index++) {
            const template = MOCK_PLAYERS[index];
            roster.push({
                ...template,
                isLocal: template.isLocal && highlightLocal,
                isDeveloper: template.isDeveloper && showDevelopers,
            });
        }

        const [rendered, setRendered] = React.useState(visible);
        const [progress, setProgress] = React.useBinding(visible ? 1 : 0);
        const progressRef = React.useRef(visible ? 1 : 0);
        const [layout, setLayout] = React.useState<PanelLayout>(() => computePanelLayout());

        React.useEffect(() => {
            if (visible) {
                setRendered(true);
            }

            const driver = new Instance("NumberValue");
            driver.Value = progressRef.current;

            const tween = TweenService.Create(
                driver,
                new TweenInfo(0.3, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
                { Value: visible ? 1 : 0 },
            );

            const changed = driver.Changed.Connect(() => {
                const value = driver.Value;
                progressRef.current = value;
                setProgress(value);
            });

            const completed = tween.Completed.Connect((status) => {
                if (!visible && status === Enum.PlaybackState.Completed) {
                    setRendered(false);
                }
            });

            tween.Play();

            return () => {
                tween.Cancel();
                changed.Disconnect();
                completed.Disconnect();
                driver.Destroy();
            };
        }, [visible]);

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

            const updateLayout = () => {
                const computed = computePanelLayout();
                setLayout((prev) => {
                    if (
                        prev.size.X.Offset === computed.size.X.Offset &&
                        prev.size.Y.Offset === computed.size.Y.Offset &&
                        prev.slideOffset === computed.slideOffset
                    ) {
                        return prev;
                    }
                    return computed;
                });
            };

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

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

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

        return (
            <PlayerListPanel
                players={roster}
                visible={rendered}
                progress={progress}
                size={layout.size}
                slideOffset={layout.slideOffset}
            />
        );
    },
);


import type { Binding } from "@rbxts/react";
import React from "@rbxts/react";
import PlayerListRow, { PlayerRosterEntry } from "client/components/playerlist/PlayerListRow";
import { RobotoMono, RobotoSlabBold, RobotoSlabSemiBold } from "shared/asset/GameFonts";

interface PlayerListPanelProps {
    players: PlayerRosterEntry[];
    visible: boolean;
    progress: Binding<number>;
    size: UDim2;
    slideOffset: number;
}

const PANEL_MARGIN = 12;

export default function PlayerListPanel({ players, visible, progress, size, slideOffset }: PlayerListPanelProps) {
    const playerCount = players.size();
    const positionBinding = progress.map(
        (alpha) => new UDim2(1, -PANEL_MARGIN + (1 - alpha) * slideOffset, 0, PANEL_MARGIN),
    );
    return (
        <frame
            key="PlayerListPanel"
            AnchorPoint={new Vector2(1, 0)}
            BackgroundColor3={Color3.fromRGB(10, 14, 22)}
            BackgroundTransparency={progress.map((alpha) => 0.08 + (1 - alpha) * 0.35)}
            BorderSizePixel={0}
            Position={positionBinding}
            Size={size}
            Visible={visible}
            ZIndex={20}
        >
            <uicorner CornerRadius={new UDim(0, 14)} />
            <uistroke
                Color={Color3.fromRGB(30, 44, 64)}
                Transparency={progress.map((alpha) => 0.3 + (1 - alpha) * 0.5)}
                Thickness={1.5}
            />
            <uigradient
                Color={
                    new ColorSequence([
                        new ColorSequenceKeypoint(0, Color3.fromRGB(16, 19, 28)),
                        new ColorSequenceKeypoint(1, Color3.fromRGB(12, 15, 24)),
                    ])
                }
                Rotation={-12}
            />
            <uipadding
                PaddingBottom={new UDim(0, 16)}
                PaddingLeft={new UDim(0, 16)}
                PaddingRight={new UDim(0, 16)}
                PaddingTop={new UDim(0, 16)}
            />
            <uilistlayout
                FillDirection={Enum.FillDirection.Vertical}
                Padding={new UDim(0, 12)}
                SortOrder={Enum.SortOrder.LayoutOrder}
            />

            <frame
                key="Header"
                AutomaticSize={Enum.AutomaticSize.Y}
                BackgroundTransparency={1}
                LayoutOrder={0}
                Size={new UDim2(1, 0, 0, 0)}
            >
                <uilistlayout
                    FillDirection={Enum.FillDirection.Vertical}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                    VerticalAlignment={Enum.VerticalAlignment.Top}
                />
                <textlabel
                    key="Title"
                    AutomaticSize={Enum.AutomaticSize.Y}
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabBold}
                    LayoutOrder={0}
                    Size={new UDim2(1, 0, 0, 0)}
                    Text="EMPIRE ROSTER"
                    TextColor3={Color3.fromRGB(218, 228, 255)}
                    TextScaled={false}
                    TextSize={28}
                    TextWrapped={false}
                    TextXAlignment={Enum.TextXAlignment.Left}
                />
                <frame
                    key="HeaderMeta"
                    AutomaticSize={Enum.AutomaticSize.Y}
                    BackgroundTransparency={1}
                    LayoutOrder={1}
                    Size={new UDim2(1, 0, 0, 0)}
                >
                    <uilistlayout
                        FillDirection={Enum.FillDirection.Horizontal}
                        SortOrder={Enum.SortOrder.LayoutOrder}
                        VerticalAlignment={Enum.VerticalAlignment.Center}
                    />
                    <textlabel
                        key="Count"
                        AutomaticSize={Enum.AutomaticSize.XY}
                        BackgroundTransparency={1}
                        FontFace={RobotoMono}
                        LayoutOrder={0}
                        Text={`${playerCount} player${playerCount === 1 ? "" : "s"} online`}
                        TextColor3={Color3.fromRGB(167, 190, 234)}
                        TextSize={16}
                        TextTransparency={0.05}
                        TextWrapped={false}
                        TextXAlignment={Enum.TextXAlignment.Left}
                    />
                </frame>
            </frame>
            <frame key="ListFrame" BackgroundTransparency={1} LayoutOrder={1} Size={new UDim2(1, 0, 1, -50)}>
                <scrollingframe
                    key="Roster"
                    AutomaticCanvasSize={Enum.AutomaticSize.Y}
                    BackgroundTransparency={1}
                    BorderSizePixel={0}
                    CanvasSize={new UDim2(0, 0, 0, 0)}
                    ScrollBarImageColor3={Color3.fromRGB(60, 80, 122)}
                    ScrollBarThickness={6}
                    Size={new UDim2(1, 0, 1, 0)}
                >
                    <uipadding
                        PaddingLeft={new UDim(0, 5)}
                        PaddingRight={new UDim(0, 10)}
                        PaddingTop={new UDim(0, 5)}
                        PaddingBottom={new UDim(0, 5)}
                    />
                    <uilistlayout
                        FillDirection={Enum.FillDirection.Vertical}
                        Padding={new UDim(0, 10)}
                        SortOrder={Enum.SortOrder.LayoutOrder}
                    />

                    {playerCount === 0 ? (
                        <textlabel
                            key="Empty"
                            AutomaticSize={Enum.AutomaticSize.Y}
                            BackgroundTransparency={1}
                            FontFace={RobotoSlabSemiBold}
                            Size={new UDim2(1, 0, 0, 0)}
                            Text="No empires enlisted."
                            TextColor3={Color3.fromRGB(178, 196, 233)}
                            TextSize={18}
                            TextTransparency={0.1}
                            TextWrapped={false}
                            TextXAlignment={Enum.TextXAlignment.Center}
                        />
                    ) : undefined}

                    {players.map((entry) => (
                        <PlayerListRow key={`row-${entry.userId}`} entry={entry} />
                    ))}
                </scrollingframe>
            </frame>
        </frame>
    );
}


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

export interface PlayerRosterEntry {
    userId: number;
    displayName: string;
    username: string;
    area: string;
    donated: number;
    accountAgeDays: number;
    isLocal: boolean;
    isDeveloper: boolean;
    thumbnail?: string;
    joinOrder: number;
}

interface PlayerListRowProps {
    entry: PlayerRosterEntry;
}

function getAccentColor(entry: PlayerRosterEntry) {
    if (entry.isLocal) {
        return Color3.fromRGB(0, 176, 255);
    }
    if (entry.isDeveloper) {
        return Color3.fromRGB(217, 131, 255);
    }
    return Color3.fromRGB(146, 186, 255);
}

function getBaseColor(entry: PlayerRosterEntry) {
    if (entry.isLocal) {
        return Color3.fromRGB(20, 28, 40);
    }
    if (entry.isDeveloper) {
        return Color3.fromRGB(33, 24, 39);
    }
    return Color3.fromRGB(18, 21, 27);
}

function formatDonated(amount: number) {
    if (amount >= 1000) {
        return `${math.floor(amount / 10) / 100}K R$ :D`;
    }
    return `${math.floor(amount * 100) / 100} R$`;
}

export default function PlayerListRow({ entry }: PlayerListRowProps) {
    const accent = getAccentColor(entry);
    const base = getBaseColor(entry);

    return (
        <frame
            BackgroundColor3={base}
            BorderSizePixel={0}
            LayoutOrder={entry.isLocal ? -1 : entry.joinOrder}
            Size={new UDim2(1, 0, 0, 72)}
        >
            <uicorner CornerRadius={new UDim(0, 10)} />
            <uistroke Color={accent} Transparency={entry.isLocal ? 0.35 : 0.65} Thickness={1.5} />
            <uigradient
                Color={
                    new ColorSequence([
                        new ColorSequenceKeypoint(0, base),
                        new ColorSequenceKeypoint(1, base.Lerp(accent, 0.18)),
                    ])
                }
                Rotation={0}
            />
            <uipadding
                PaddingBottom={new UDim(0, 10)}
                PaddingLeft={new UDim(0, 12)}
                PaddingRight={new UDim(0, 12)}
                PaddingTop={new UDim(0, 10)}
            />
            <uilistlayout
                FillDirection={Enum.FillDirection.Horizontal}
                HorizontalAlignment={Enum.HorizontalAlignment.Left}
                Padding={new UDim(0, 0)}
                SortOrder={Enum.SortOrder.LayoutOrder}
                VerticalAlignment={Enum.VerticalAlignment.Center}
            />

            <frame
                AutomaticSize={Enum.AutomaticSize.XY}
                BackgroundTransparency={1}
                LayoutOrder={0}
                Size={new UDim2(0.65, 0, 1, 0)}
            >
                <uilistlayout
                    FillDirection={Enum.FillDirection.Horizontal}
                    HorizontalAlignment={Enum.HorizontalAlignment.Left}
                    Padding={new UDim(0, 10)}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                    VerticalAlignment={Enum.VerticalAlignment.Center}
                />

                <imagelabel
                    BackgroundColor3={accent}
                    BackgroundTransparency={entry.thumbnail === undefined ? 0 : 1}
                    BorderSizePixel={0}
                    Image={entry.thumbnail ?? ""}
                    LayoutOrder={0}
                    Size={new UDim2(0, 48, 0, 48)}
                >
                    <uicorner CornerRadius={new UDim(0.5, 0)} />
                    <uistroke Color={accent.Lerp(new Color3(0, 0, 0), 0.4)} Transparency={0.4} Thickness={1.5} />
                </imagelabel>

                <frame AutomaticSize={Enum.AutomaticSize.XY} BackgroundTransparency={1} LayoutOrder={1}>
                    <uilistlayout
                        HorizontalAlignment={Enum.HorizontalAlignment.Left}
                        SortOrder={Enum.SortOrder.LayoutOrder}
                        VerticalAlignment={Enum.VerticalAlignment.Center}
                    />

                    <textlabel
                        AutomaticSize={Enum.AutomaticSize.XY}
                        BackgroundTransparency={1}
                        FontFace={RobotoSlabBold}
                        LayoutOrder={0}
                        Text={entry.displayName}
                        TextColor3={Color3.fromRGB(230, 238, 255)}
                        TextSize={18}
                        TextWrapped={true}
                    />
                    <frame AutomaticSize={Enum.AutomaticSize.XY} BackgroundTransparency={1} LayoutOrder={1}>
                        <uilistlayout
                            FillDirection={Enum.FillDirection.Horizontal}
                            HorizontalAlignment={Enum.HorizontalAlignment.Left}
                            Padding={new UDim(0, 6)}
                            SortOrder={Enum.SortOrder.LayoutOrder}
                            VerticalAlignment={Enum.VerticalAlignment.Center}
                        />
                        <textlabel
                            AutomaticSize={Enum.AutomaticSize.XY}
                            BackgroundTransparency={1}
                            FontFace={RobotoMono}
                            LayoutOrder={0}
                            Text={`@${entry.username}`}
                            TextColor3={Color3.fromRGB(175, 190, 222)}
                            TextSize={14}
                            TextTransparency={0.1}
                            TextWrapped={false}
                        />
                        {entry.isDeveloper && (
                            <textlabel
                                AutomaticSize={Enum.AutomaticSize.XY}
                                BackgroundColor3={accent}
                                FontFace={RobotoMono}
                                LayoutOrder={1}
                                Text="DEV"
                                TextColor3={Color3.fromRGB(14, 18, 23)}
                                TextSize={12}
                            >
                                <uicorner CornerRadius={new UDim(0.5, 0)} />
                                <uipadding
                                    PaddingBottom={new UDim(0, 2)}
                                    PaddingLeft={new UDim(0, 6)}
                                    PaddingRight={new UDim(0, 6)}
                                    PaddingTop={new UDim(0, 2)}
                                />
                            </textlabel>
                        )}
                        {entry.isLocal && (
                            <textlabel
                                AutomaticSize={Enum.AutomaticSize.XY}
                                BackgroundColor3={accent}
                                FontFace={RobotoMono}
                                LayoutOrder={2}
                                Text="YOU"
                                TextColor3={Color3.fromRGB(13, 17, 22)}
                                TextSize={12}
                            >
                                <uicorner CornerRadius={new UDim(0.5, 0)} />
                                <uipadding
                                    PaddingBottom={new UDim(0, 2)}
                                    PaddingLeft={new UDim(0, 6)}
                                    PaddingRight={new UDim(0, 6)}
                                    PaddingTop={new UDim(0, 2)}
                                />
                            </textlabel>
                        )}
                    </frame>
                </frame>
            </frame>

            <frame
                AutomaticSize={Enum.AutomaticSize.Y}
                BackgroundTransparency={1}
                LayoutOrder={1}
                Size={new UDim2(0.35, 0, 1, 0)}
            >
                <uilistlayout
                    FillDirection={Enum.FillDirection.Vertical}
                    HorizontalAlignment={Enum.HorizontalAlignment.Right}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                    VerticalAlignment={Enum.VerticalAlignment.Center}
                />

                <textlabel
                    AutomaticSize={Enum.AutomaticSize.Y}
                    BackgroundTransparency={1}
                    FontFace={RobotoMono}
                    LayoutOrder={0}
                    Text={entry.area}
                    TextColor3={Color3.fromRGB(197, 213, 243)}
                    TextSize={14}
                    TextXAlignment={Enum.TextXAlignment.Right}
                    TextWrapped={false}
                    Size={new UDim2(1, 0, 0, 0)}
                />

                <textlabel
                    AutomaticSize={Enum.AutomaticSize.XY}
                    BackgroundTransparency={1}
                    FontFace={RobotoMono}
                    LayoutOrder={0}
                    Text={entry.donated > 0 ? `Donated ${formatDonated(entry.donated)}` : "-"}
                    TextColor3={Color3.fromRGB(181, 240, 214)}
                    TextSize={14}
                    TextXAlignment={Enum.TextXAlignment.Right}
                    TextWrapped={false}
                    Size={new UDim2(1, 0, 0, 0)}
                />
            </frame>
        </frame>
    );
}


import React, { StrictMode } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import PositionManager from "client/components/position/PositionManager";
import TooltipWindow from "client/components/tooltip/TooltipWindow";
import useVisibility from "client/hooks/useVisibility";

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

        return (
            <StrictMode>
                <PositionManager {...props.controls} />
                <TooltipWindow />
            </StrictMode>
        );
    },
);


import React, { StrictMode } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import PositionWindow from "client/components/position/PositionWindow";
import TooltipWindow from "client/components/tooltip/TooltipWindow";
import useVisibility from "client/hooks/useVisibility";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            visible: true,
        },
    },
    (props) => {
        useVisibility("Position", props.controls.visible);
        return (
            <StrictMode>
                <PositionWindow characterPosition={new Vector3(100, 50, 200)} />
                <TooltipWindow />
            </StrictMode>
        );
    },
);


import React, { useEffect, useRef } from "@rbxts/react";
import { useMessageTooltip } from "client/components/tooltip/TooltipManager";
import { useDocument } from "client/components/window/DocumentManager";
import { getAsset } from "shared/asset/AssetMap";
import { RobotoSlabHeavy } from "shared/asset/GameFonts";

/**
 * PositionWindow displays the player's current coordinates in a styled frame.
 *
 * Features a compass icon and coordinates display with gradient styling.
 * The component is designed to be positioned in the top-right corner of the screen.
 */
export default function PositionWindow({
    characterPosition = new Vector3(),
    anchorPoint = new Vector2(1, 0),
    windowPosition = new UDim2(1, -20, 0, 10),
    size = new UDim2(0, 0, 0, 16),
}: {
    /** The position coordinates to display */
    characterPosition?: Vector3;
    /** Custom styling for the window */
    anchorPoint?: Vector2;
    /** Custom position for the window */
    windowPosition?: UDim2;
    /** Custom size for the window */
    size?: UDim2;
}) {
    const ref = useRef<Frame>();
    const closedPosition = windowPosition.add(new UDim2(0, 0, 0, -50));
    const { visible } = useDocument({ id: "Position", priority: -1 });

    useEffect(() => {
        if (visible) {
            ref.current?.TweenPosition(windowPosition, Enum.EasingDirection.Out, Enum.EasingStyle.Quad, 1.5, true);
        } else {
            ref.current?.TweenPosition(closedPosition, Enum.EasingDirection.In, Enum.EasingStyle.Quad, 1.5, true);
        }
    }, [visible]);

    const positionText = `${math.round(characterPosition.X)}, ${math.round(characterPosition.Y)}, ${math.round(characterPosition.Z)}`;
    const { events } = useMessageTooltip(
        `Position of your character.\n<font color="rgb(200, 200, 200)" size="16">Use these coordinates to search your way through the world.</font>`,
    );

    return (
        <frame
            ref={ref}
            key="PositionWindow"
            AnchorPoint={anchorPoint}
            BackgroundTransparency={1}
            Position={closedPosition}
            Size={size}
        >
            {/* Compass Icon */}
            <imagelabel
                AnchorPoint={new Vector2(0.5, 0.5)}
                BackgroundTransparency={1}
                Image={getAsset("assets/Compass.png")}
                Position={new UDim2(1, 0, 0.5, 0)}
                Size={new UDim2(1.75, 0, 1.75, 0)}
                SizeConstraint={Enum.SizeConstraint.RelativeYY}
                ZIndex={2}
            />

            {/* Overlay */}
            <imagelabel
                key="Frame"
                AnchorPoint={new Vector2(1, 0.5)}
                AutomaticSize={Enum.AutomaticSize.X}
                BackgroundColor3={Color3.fromRGB(255, 255, 255)}
                BorderColor3={Color3.fromRGB(0, 0, 0)}
                BorderSizePixel={2}
                Image={getAsset("assets/Grid.png")}
                ImageTransparency={0.6}
                Position={new UDim2(0, 0, 0.5, 0)}
                ScaleType={Enum.ScaleType.Tile}
                Size={new UDim2(0, 0, 0, 16)}
                TileSize={new UDim2(0, 32, 0, 32)}
                Event={{ ...events }}
            >
                {/* Position Text Label */}
                <textlabel
                    key="PositionLabel"
                    AnchorPoint={new Vector2(1, 0.5)}
                    AutomaticSize={Enum.AutomaticSize.X}
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabHeavy}
                    Size={new UDim2(0, 0, 1, 0)}
                    Text={positionText}
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextSize={20}
                    TextXAlignment={Enum.TextXAlignment.Right}
                >
                    <uistroke Thickness={2} />
                </textlabel>

                {/* Background Gradient */}
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(85, 254, 171)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(0, 164, 170)),
                        ])
                    }
                    Rotation={90}
                />

                {/* Border Stroke with Gradient */}
                <uistroke
                    ApplyStrokeMode={Enum.ApplyStrokeMode.Border}
                    Color={Color3.fromRGB(255, 255, 255)}
                    Transparency={0.2}
                >
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                                new ColorSequenceKeypoint(0.587, Color3.fromRGB(173, 173, 173)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(255, 255, 255)),
                            ])
                        }
                        Rotation={80}
                    />
                </uistroke>

                {/* Layout and Constraints */}
                <uilistlayout
                    HorizontalAlignment={Enum.HorizontalAlignment.Center}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                    VerticalAlignment={Enum.VerticalAlignment.Center}
                />
                <uipadding PaddingLeft={new UDim(0, 10)} PaddingRight={new UDim(0, 20)} />
                <uisizeconstraint MinSize={new Vector2(70, 0)} />
            </imagelabel>
        </frame>
    );
}


import React, { useEffect, useState } from "@rbxts/react";
import { RunService, Workspace } from "@rbxts/services";

/**
 * Hook that tracks the player's character position in real-time.
 *
 * @param character The player's character model to track
 * @returns The current position as Vector3, or undefined if no character/position available
 */
export function usePlayerPosition(character?: Model): Vector3 | undefined {
    const [position, setPosition] = useState<Vector3 | undefined>(undefined);

    useEffect(() => {
        const rootPart = (character?.FindFirstChild("Humanoid") as Humanoid | undefined)?.RootPart;

        // Set up real-time position updates
        const connection = RunService.Heartbeat.Connect(() => {
            setPosition(rootPart?.Position ?? Workspace.CurrentCamera?.CFrame.Position);
        });

        // Clean up connection when character dies or component unmounts
        const deathConnection = rootPart?.Destroying.Connect(() => {
            connection.Disconnect();
            setPosition(undefined);
        });

        // Cleanup function
        return () => {
            connection.Disconnect();
            deathConnection?.Disconnect();
            setPosition(undefined);
        };
    }, [character]);

    return position;
}

/**
 * Hook that provides the current player's character and position.
 *
 * @param player The player to track (defaults to LocalPlayer)
 * @returns Object containing character and current position
 */
export function useCharacterPosition(player?: Player): { character?: Model; position?: Vector3 } {
    const [character, setCharacter] = useState<Model | undefined>();

    // Get position using the character
    const position = usePlayerPosition(character);

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

        // Set initial character if it exists
        if (player.Character) {
            setCharacter(player.Character);
        }

        // Listen for character spawns
        const characterAddedConnection = player.CharacterAdded.Connect((newCharacter) => {
            setCharacter(newCharacter);
        });

        // Listen for character removal
        const characterRemovingConnection = player.CharacterRemoving.Connect(() => {
            setCharacter(undefined);
        });

        // Cleanup
        return () => {
            characterAddedConnection.Disconnect();
            characterRemovingConnection.Disconnect();
        };
    }, [player]);

    return { character, position };
}


import React, { Fragment, useCallback, useEffect, useState } from "@rbxts/react";
import { RobotoSlabBold } from "shared/asset/GameFonts";
import Packets from "shared/Packets";

const COLORS = [
    Color3.fromRGB(239, 68, 68), // Red
    Color3.fromRGB(59, 130, 246), // Blue
    Color3.fromRGB(34, 197, 94), // Green
    Color3.fromRGB(250, 204, 21), // Yellow
];

export default function PillarPuzzle() {
    const [visible, setVisible] = useState(false);
    const [sequence, setSequence] = useState<number[]>([]);
    const [playerSequence, setPlayerSequence] = useState<number[]>([]);
    const [showingSequence, setShowingSequence] = useState(false);
    const [message, setMessage] = useState("Watch the sequence!");

    useEffect(() => {
        const visibleConnection = Packets.pillarPuzzleVisible.observe((value) => {
            setVisible(value);
            if (value) {
                setPlayerSequence([]);
                setShowingSequence(false);
                setMessage("Watch the sequence!");
            }
        });

        const sequenceConnection = Packets.pillarPuzzleSequence.observe((seq) => {
            setSequence(seq);
            if (seq.size() > 0) {
                setShowingSequence(true);
                setMessage("Watch the sequence!");
            }
        });

        return () => {
            visibleConnection.disconnect();
            sequenceConnection.disconnect();
        };
    }, []);

    const handleButtonClick = useCallback(
        (index: number) => {
            if (showingSequence) return;

            const newSequence = [...playerSequence, index];
            setPlayerSequence(newSequence);

            // Check if the player's sequence matches so far
            if (sequence[newSequence.size() - 1] !== index) {
                setMessage("Wrong! Try again.");
                task.delay(1, () => {
                    setPlayerSequence([]);
                    setMessage("Watch the sequence again!");
                    setShowingSequence(true);
                });
            } else if (newSequence.size() === sequence.size()) {
                setMessage("Correct! Submitting...");
                Packets.submitPuzzleAnswer.toServer(newSequence);
                task.delay(1, () => {
                    setVisible(false);
                });
            } else {
                setMessage(`Keep going! (${newSequence.size()}/${sequence.size()})`);
            }
        },
        [showingSequence, playerSequence, sequence],
    );

    useEffect(() => {
        if (showingSequence && sequence.size() > 0) {
            let index = 0;
            const showNext = () => {
                if (index < sequence.size()) {
                    const buttonIndex = sequence[index];
                    // Flash the button (visual feedback would be nice but keeping it simple)
                    index++;
                    task.delay(0.6, showNext);
                } else {
                    setShowingSequence(false);
                    setMessage("Now repeat the sequence!");
                }
            };
            task.delay(0.5, showNext);
        }
    }, [showingSequence, sequence]);

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

    return (
        <frame
            AnchorPoint={new Vector2(0.5, 0.5)}
            BackgroundColor3={Color3.fromRGB(25, 27, 31)}
            BorderSizePixel={0}
            Position={new UDim2(0.5, 0, 0.5, 0)}
            Size={new UDim2(0, 400, 0, 450)}
            ZIndex={100}
        >
            <uicorner CornerRadius={new UDim(0, 12)} />
            <uistroke Color={Color3.fromRGB(255, 255, 255)} Thickness={3} Transparency={0.5} />

            {/* Title */}
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoSlabBold}
                Position={new UDim2(0, 0, 0, 20)}
                Size={new UDim2(1, 0, 0, 40)}
                Text="Pillar Memory Puzzle"
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextWrapped={true}
                ZIndex={101}
            >
                <uipadding PaddingLeft={new UDim(0, 20)} PaddingRight={new UDim(0, 20)} />
            </textlabel>

            {/* Message */}
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoSlabBold}
                Position={new UDim2(0, 0, 0, 70)}
                Size={new UDim2(1, 0, 0, 30)}
                Text={message}
                TextColor3={Color3.fromRGB(200, 200, 200)}
                TextScaled={true}
                TextWrapped={true}
                ZIndex={101}
            >
                <uipadding PaddingLeft={new UDim(0, 20)} PaddingRight={new UDim(0, 20)} />
            </textlabel>

            {/* Buttons */}
            <frame
                AnchorPoint={new Vector2(0.5, 0.5)}
                BackgroundTransparency={1}
                Position={new UDim2(0.5, 0, 0.55, 0)}
                Size={new UDim2(0, 300, 0, 300)}
                ZIndex={101}
            >
                <uigridlayout
                    CellPadding={new UDim2(0, 10, 0, 10)}
                    CellSize={new UDim2(0, 140, 0, 140)}
                    HorizontalAlignment={Enum.HorizontalAlignment.Center}
                    VerticalAlignment={Enum.VerticalAlignment.Center}
                />
                {COLORS.map((color, index) => (
                    <textbutton
                        key={index}
                        BackgroundColor3={color}
                        BorderSizePixel={0}
                        Text=""
                        ZIndex={102}
                        Event={{
                            Activated: () => handleButtonClick(index),
                        }}
                    >
                        <uicorner CornerRadius={new UDim(0, 8)} />
                        <uistroke Color={Color3.fromRGB(255, 255, 255)} Thickness={2} Transparency={0.3} />
                    </textbutton>
                ))}
            </frame>
        </frame>
    );
}


/**
 * @fileoverview Quest completion manager component that handles quest completion events.
 *
 * Features:
 * - Listens to quest completion packets from the server
 * - Manages quest completion notification state
 * - Triggers quest completion animations
 * - Handles multiple quest completions in sequence
 */

import React, { Fragment, useEffect, useState } from "@rbxts/react";
import QuestCompletionNotification, { QuestCompletionData } from "client/components/quest/QuestCompletionNotification";
import Packets from "shared/Packets";

interface QuestCompletionQueue {
    id: string;
    data: QuestCompletionData;
}

export default function QuestCompletionManager() {
    const [questQueue, setQuestQueue] = useState<QuestCompletionQueue[]>([]);
    const [currentQuest, setCurrentQuest] = useState<QuestCompletionQueue | undefined>();

    // Listen for quest completion events
    useEffect(() => {
        const connections: RBXScriptConnection[] = [];

        // Listen for quest completion packets
        connections.push(
            Packets.questCompleted.fromServer((questId) => {
                const questInfo = Packets.questInfo.get()?.get(questId);
                if (!questInfo) {
                    warn(`Quest info not found for completed quest: ${questId}`);
                    return;
                }

                if (questInfo.level === 0) return;

                // Create quest completion data
                const completionData: QuestCompletionData = {
                    questName: questInfo.name,
                    questColor: Color3.fromRGB(questInfo.colorR, questInfo.colorG, questInfo.colorB),
                    reward: questInfo.reward,
                    visible: true,
                };

                // Add to queue
                const queueItem: QuestCompletionQueue = {
                    id: `quest_${questId}_${tick()}`,
                    data: completionData,
                };

                setQuestQueue((prevQueue) => [...prevQueue, queueItem]);
            }),
        );

        return () => {
            connections.forEach((connection) => connection.Disconnect());
        };
    }, []);

    // Process quest queue
    useEffect(() => {
        if (!currentQuest && questQueue.size() > 0) {
            const nextQuest = questQueue[0];
            setCurrentQuest(nextQuest);
            setQuestQueue((prevQueue) => {
                const newQueue = [...prevQueue];
                newQueue.shift();
                return newQueue;
            });
        }
    }, [currentQuest, questQueue]);

    // Handle quest completion finish
    const handleQuestComplete = () => {
        setCurrentQuest(undefined);
    };

    return (
        <Fragment>
            {currentQuest && <QuestCompletionNotification data={currentQuest.data} onComplete={handleQuestComplete} />}
        </Fragment>
    );
}