import React, { memo, useEffect, useRef } from "@rbxts/react";
import { TweenService } from "@rbxts/services";
import { getPositionDetails } from "client/components/quest/TrackedQuestWindow";
import { getLengthColor, getLengthName, getRewardLabel } from "client/components/quest/useQuestData";
import { getAsset } from "shared/asset/AssetMap";
import { RobotoSlabBold, RobotoSlabExtraBold, RobotoSlabHeavy, RobotoSlabMedium } from "shared/asset/GameFonts";

interface QuestOptionProps {
    questId: string;
    quest: QuestInfo;
    currentStage: number;
    level: number;
    isTracked: boolean;
    isExpanded: boolean;
    onToggleExpanded: () => void;
    onTrack: () => void;
}

function QuestOption({
    questId,
    quest,
    currentStage,
    level,
    isTracked,
    isExpanded,
    onToggleExpanded,
    onTrack,
}: QuestOptionProps) {
    const color = Color3.fromRGB(quest.colorR, quest.colorG, quest.colorB);
    const belowRequirement = level < quest.level;
    const isCompleted = currentStage < 0;
    const isAvailable = !isCompleted && !belowRequirement;

    // Create ref for the arrow image
    const arrowRef = useRef<ImageLabel>();

    // Set initial rotation without animation
    useEffect(() => {
        const arrow = arrowRef.current;
        if (!arrow) return;

        // Set initial rotation based on expanded state
        arrow.Rotation = isExpanded ? 0 : 180;
    }, []);

    // Animate arrow rotation when expanded state changes
    useEffect(() => {
        const arrow = arrowRef.current;
        if (!arrow) return;

        const targetRotation = isExpanded ? 0 : 180;

        const tweenInfo = new TweenInfo(
            0.3, // Duration in seconds
            Enum.EasingStyle.Quart,
            Enum.EasingDirection.Out,
        );

        const tween = TweenService.Create(arrow, tweenInfo, {
            Rotation: targetRotation,
        });

        tween.Play();

        return () => {
            tween.Cancel();
        };
    }, [isExpanded]); // Calculate layout order for quest sorting
    const getLayoutOrder = () => {
        const baseOrder = quest.level * 10 + (quest.order + 1);
        if (isCompleted) return baseOrder + 1000000000;
        if (belowRequirement) return baseOrder + 100000000;
        return baseOrder;
    };

    const transparency = isCompleted ? 0.5 : 0;
    const trackButtonColor = isTracked ? Color3.fromRGB(85, 255, 127) : Color3.fromRGB(255, 52, 52);

    return (
        <frame
            key={questId}
            AutomaticSize={Enum.AutomaticSize.Y}
            BackgroundColor3={color}
            BorderSizePixel={0}
            LayoutOrder={getLayoutOrder()}
            Size={new UDim2(1, 0, 0, 0)}
        >
            <uilistlayout
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                Padding={new UDim(0, 10)}
                SortOrder={Enum.SortOrder.LayoutOrder}
            />

            {/* Quest Header (Dropdown Button) */}
            <textbutton
                key="Dropdown"
                BackgroundTransparency={1}
                LayoutOrder={-1}
                Size={new UDim2(1, 0, 0, 30)}
                Text=""
                Event={{
                    Activated: onToggleExpanded,
                }}
            >
                {/* Dropdown Arrow */}
                <imagelabel
                    ref={arrowRef}
                    AnchorPoint={new Vector2(1, 0.5)}
                    BackgroundTransparency={1}
                    Image={getAsset("assets/Dropdown.png")}
                    Position={new UDim2(0.95, 0, 0.5, 0)}
                    ScaleType={Enum.ScaleType.Fit}
                    Size={new UDim2(0.8, 0, 0.8, 0)}
                    SizeConstraint={Enum.SizeConstraint.RelativeYY}
                />

                {/* Level Label */}
                <textlabel
                    key="LevelLabel"
                    AnchorPoint={new Vector2(0, 0.5)}
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabExtraBold}
                    Position={new UDim2(0, 0, 0.5, 0)}
                    Size={new UDim2(0.2, 0, 0.9, 0)}
                    Text={`Lv. ${quest.level}`}
                    TextColor3={belowRequirement ? Color3.fromRGB(255, 52, 52) : Color3.fromRGB(255, 255, 255)}
                    TextScaled={true}
                    TextTransparency={transparency}
                >
                    <uistroke Thickness={2} Transparency={transparency} />
                </textlabel>

                {/* Quest Name */}
                <textlabel
                    key="NameLabel"
                    AnchorPoint={new Vector2(0.5, 0.5)}
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabHeavy}
                    Position={new UDim2(0.5, 0, 0.5, 0)}
                    Size={new UDim2(0.5, 0, 1, 0)}
                    Text={quest.name || "no name"}
                    TextColor3={color}
                    TextScaled={true}
                    TextTransparency={transparency}
                >
                    <uistroke Thickness={2} Transparency={transparency} />
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                                new ColorSequenceKeypoint(0.694, Color3.fromRGB(253, 253, 253)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(176, 176, 176)),
                            ])
                        }
                        Rotation={90}
                    />
                </textlabel>
            </textbutton>

            {/* Quest Content (Expandable) */}
            {isExpanded && (
                <frame
                    key="Content"
                    AutomaticSize={Enum.AutomaticSize.Y}
                    BackgroundColor3={Color3.fromRGB(0, 0, 0)}
                    BackgroundTransparency={0.95}
                    BorderSizePixel={0}
                    Size={new UDim2(1, 0, 0, 0)}
                >
                    <uilistlayout
                        HorizontalAlignment={Enum.HorizontalAlignment.Center}
                        Padding={new UDim(0, 7)}
                        SortOrder={Enum.SortOrder.LayoutOrder}
                    />

                    {/* Current Stage Description */}
                    <textlabel
                        key="CurrentStepLabel"
                        AutomaticSize={Enum.AutomaticSize.Y}
                        BackgroundTransparency={1}
                        FontFace={RobotoSlabMedium}
                        LayoutOrder={1}
                        Size={new UDim2(0.9, 0, 0, 0)}
                        Text={getPositionDetails(questId, quest, currentStage).description}
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextSize={20}
                        TextWrapped={true}
                        TextXAlignment={Enum.TextXAlignment.Left}
                    >
                        <uistroke Thickness={2} />
                    </textlabel>

                    {/* Length Label */}
                    <textlabel
                        key="LengthLabel"
                        AutomaticSize={Enum.AutomaticSize.Y}
                        BackgroundTransparency={1}
                        FontFace={RobotoSlabExtraBold}
                        LayoutOrder={2}
                        Size={new UDim2(0.9, 0, 0, 0)}
                        Text={`Length: ${getLengthName(quest.length)}`}
                        TextColor3={getLengthColor(quest.length)}
                        TextSize={20}
                        TextWrapped={true}
                        TextXAlignment={Enum.TextXAlignment.Left}
                    >
                        <uistroke Thickness={2} />
                        <uigradient
                            Color={
                                new ColorSequence([
                                    new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                                    new ColorSequenceKeypoint(0.547, Color3.fromRGB(243, 243, 243)),
                                    new ColorSequenceKeypoint(1, Color3.fromRGB(206, 206, 206)),
                                ])
                            }
                            Rotation={90}
                        />
                        <uipadding PaddingTop={new UDim(0, 10)} />
                    </textlabel>

                    {/* Reward Label */}
                    <textlabel
                        key="RewardLabel"
                        AutomaticSize={Enum.AutomaticSize.Y}
                        BackgroundTransparency={1}
                        FontFace={RobotoSlabExtraBold}
                        LayoutOrder={3}
                        Size={new UDim2(0.9, 0, 0, 0)}
                        Text={`Reward: ${getRewardLabel(quest.reward)}`}
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextSize={20}
                        TextWrapped={true}
                        TextXAlignment={Enum.TextXAlignment.Left}
                    >
                        <uistroke Thickness={2} />
                        <uigradient
                            Color={
                                new ColorSequence([
                                    new ColorSequenceKeypoint(0, Color3.fromRGB(170, 255, 255)),
                                    new ColorSequenceKeypoint(1, Color3.fromRGB(0, 255, 255)),
                                ])
                            }
                            Rotation={90}
                        />
                    </textlabel>

                    {/* Track Button */}
                    {isAvailable && (
                        <textbutton
                            key="Track"
                            AutomaticSize={Enum.AutomaticSize.Y}
                            BackgroundColor3={trackButtonColor}
                            BorderSizePixel={0}
                            LayoutOrder={15}
                            Size={new UDim2(0.4, 0, 0, 0)}
                            Text=""
                            Event={{
                                Activated: onTrack,
                            }}
                        >
                            <uigradient
                                Color={
                                    new ColorSequence([
                                        new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                                        new ColorSequenceKeypoint(1, Color3.fromRGB(156, 156, 156)),
                                    ])
                                }
                                Rotation={90}
                            />
                            <uistroke
                                ApplyStrokeMode={Enum.ApplyStrokeMode.Border}
                                Color={trackButtonColor}
                                Thickness={2}
                            />

                            <textlabel
                                key="Label"
                                AutomaticSize={Enum.AutomaticSize.Y}
                                BackgroundTransparency={1}
                                FontFace={RobotoSlabBold}
                                Position={new UDim2(0.5, 0, 0.5, 0)}
                                AnchorPoint={new Vector2(0.5, 0.5)}
                                Size={new UDim2(0.9, 0, 0, 0)}
                                Text={isTracked ? "Untrack" : "Track"}
                                TextColor3={Color3.fromRGB(255, 255, 255)}
                                TextSize={20}
                                TextWrapped={true}
                            >
                                <uistroke Thickness={2} />
                            </textlabel>

                            <uilistlayout
                                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                                Padding={new UDim(0, 10)}
                                SortOrder={Enum.SortOrder.LayoutOrder}
                            />
                            <uipadding
                                PaddingBottom={new UDim(0, 5)}
                                PaddingLeft={new UDim(0, 5)}
                                PaddingRight={new UDim(0, 5)}
                                PaddingTop={new UDim(0, 5)}
                            />
                        </textbutton>
                    )}

                    <uipadding PaddingBottom={new UDim(0, 10)} PaddingTop={new UDim(0, 10)} />
                </frame>
            )}

            {/* Quest Option Styling */}
            <uigradient
                Color={
                    new ColorSequence([
                        new ColorSequenceKeypoint(0, Color3.fromRGB(150, 150, 150)),
                        new ColorSequenceKeypoint(1, Color3.fromRGB(85, 85, 85)),
                    ])
                }
                Rotation={90}
            />
            <uistroke ApplyStrokeMode={Enum.ApplyStrokeMode.Border} Color={color} Thickness={4}>
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                            new ColorSequenceKeypoint(0.375, Color3.fromRGB(255, 255, 255)),
                            new ColorSequenceKeypoint(0.51, Color3.fromRGB(118, 118, 118)),
                            new ColorSequenceKeypoint(0.583, Color3.fromRGB(255, 255, 255)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(255, 255, 255)),
                        ])
                    }
                    Rotation={85}
                />
            </uistroke>
            <uipadding PaddingBottom={new UDim(0, 10)} PaddingTop={new UDim(0, 10)} />
        </frame>
    );
}

export default memo(QuestOption);


import React, { StrictMode } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import QuestWindow from "client/components/quest/QuestWindow";
import TrackedQuestWindow from "client/components/quest/TrackedQuestWindow";
import StoryMocking from "client/components/StoryMocking";
import TooltipWindow from "client/components/tooltip/TooltipWindow";
import { useSingleDocumentVisibility } from "client/hooks/useVisibility";

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

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

        return (
            <StrictMode>
                <TrackedQuestWindow />
                <QuestWindow />
                <TooltipWindow />
            </StrictMode>
        );
    },
);


import React, { useCallback, useMemo, useState } from "@rbxts/react";
import QuestOption from "client/components/quest/QuestOption";
import { useQuestData } from "client/components/quest/useQuestData";
import useSingleDocument from "client/components/sidebar/useSingleDocumentWindow";
import BasicWindow from "client/components/window/BasicWindow";
import ProgressBar from "client/components/window/ProgressBar";
import { getAsset } from "shared/asset/AssetMap";
import { playSound } from "shared/asset/GameAssets";
import { RobotoSlabHeavy } from "shared/asset/GameFonts";
import { getMaxXp } from "shared/constants";

export default function QuestWindow() {
    const { id, visible } = useSingleDocument({ id: "Quests" });
    const { questInfo, stagePerQuest, level, xp, trackedQuest, onTrackQuest } = useQuestData();
    const maxXp = getMaxXp(level);

    const [expandedQuests, setExpandedQuests] = useState(new Set<string>());

    const handleToggleQuestContent = useCallback((questId: string) => {
        setExpandedQuests((prev) => {
            const newSet = table.clone(prev);
            if (newSet.has(questId)) {
                playSound("CheckOff.mp3");
                newSet.delete(questId);
            } else {
                playSound("CheckOn.mp3");
                newSet.add(questId);
            }
            return newSet;
        });
    }, []);

    const handleTrackQuest = useCallback(
        (questId: string) => {
            if (trackedQuest === questId) {
                onTrackQuest(undefined); // Untrack
            } else {
                onTrackQuest(questId); // Track
            }
        },
        [trackedQuest, onTrackQuest],
    );

    const sortedQuests = useMemo(() => {
        const quests = new Array<[string, QuestInfo]>();
        for (const [questId, quest] of questInfo) {
            quests.push([questId, quest]);
        }
        return quests
            .filter(([id]) => {
                // Filter out very high level quests (likely admin/debug quests)
                const quest = questInfo.get(id);
                return quest && quest.level < 999;
            })
            .sort(([aId, aQuest], [bId, bQuest]) => {
                const aStage = stagePerQuest.get(aId) ?? 0;
                const bStage = stagePerQuest.get(bId) ?? 0;
                const aBelowReq = level < aQuest.level;
                const bBelowReq = level < bQuest.level;
                const aCompleted = aStage < 0;
                const bCompleted = bStage < 0;

                // Completed quests go to bottom
                if (aCompleted !== bCompleted) {
                    return aCompleted;
                }

                // Below requirement quests go to middle
                if (aBelowReq !== bBelowReq) {
                    return aBelowReq;
                }

                // Otherwise sort by level then order
                const levelDiff = aQuest.level - bQuest.level;
                if (levelDiff !== 0) return levelDiff < 0;

                return aQuest.order < bQuest.order;
            });
    }, [questInfo, stagePerQuest, level]);

    return (
        <BasicWindow
            id={id}
            icon={getAsset("assets/Quests.png")}
            backgroundColor={
                new ColorSequence([
                    new ColorSequenceKeypoint(0, Color3.fromRGB(89, 33, 46)),
                    new ColorSequenceKeypoint(1, Color3.fromRGB(56, 10, 10)),
                ])
            }
            strokeColor={
                new ColorSequence([
                    new ColorSequenceKeypoint(0, Color3.fromRGB(255, 94, 94)),
                    new ColorSequenceKeypoint(1, Color3.fromRGB(255, 18, 18)),
                ])
            }
            visible={visible}
        >
            <uilistlayout
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                Padding={new UDim(0, 10)}
                SortOrder={Enum.SortOrder.LayoutOrder}
            />

            {/* Level and XP Progress */}
            <frame key="Level" BackgroundTransparency={1} LayoutOrder={-1} Size={new UDim2(1, 0, 0, 32)}>
                {/* Current Level Display */}
                <frame
                    key="Current"
                    Active={true}
                    BackgroundColor3={Color3.fromRGB(255, 223, 62)}
                    BorderColor3={Color3.fromRGB(0, 0, 0)}
                    BorderSizePixel={5}
                    Selectable={true}
                    Size={new UDim2(0.3, 0, 1, 0)}
                >
                    <uistroke
                        ApplyStrokeMode={Enum.ApplyStrokeMode.Border}
                        Color={Color3.fromRGB(255, 255, 255)}
                        Thickness={3}
                    >
                        <uigradient
                            Color={
                                new ColorSequence([
                                    new ColorSequenceKeypoint(0, Color3.fromRGB(255, 223, 62)),
                                    new ColorSequenceKeypoint(1, Color3.fromRGB(255, 137, 3)),
                                ])
                            }
                            Rotation={90}
                        />
                    </uistroke>
                    <textlabel
                        key="LevelLabel"
                        AnchorPoint={new Vector2(0.5, 0.5)}
                        BackgroundTransparency={1}
                        FontFace={RobotoSlabHeavy}
                        Position={new UDim2(0.5, 0, 0.5, 0)}
                        Size={new UDim2(0.8, 0, 0.8250000000000001, 0)}
                        Text={`Lv. ${level}`}
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextScaled={true}
                        TextSize={14}
                        TextWrapped={true}
                    >
                        <uistroke Thickness={2} />
                    </textlabel>
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(255, 223, 41)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(255, 131, 7)),
                            ])
                        }
                        Rotation={90}
                    />
                </frame>

                {/* XP Progress Bar */}
                <ProgressBar
                    current={xp}
                    max={maxXp}
                    text={`${xp}/${maxXp} XP to Lv. ${level + 1}`}
                    colorSequence={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(255, 170, 255)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(189, 58, 255)),
                        ])
                    }
                    frameProps={{ Size: new UDim2(0.5, 0, 1, 0) }}
                />

                <uilistlayout
                    HorizontalAlignment={Enum.HorizontalAlignment.Center}
                    VerticalAlignment={Enum.VerticalAlignment.Center}
                    FillDirection={Enum.FillDirection.Horizontal}
                    Padding={new UDim(0, 15)}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                />
            </frame>

            {/* Quest List */}
            <scrollingframe
                key="QuestList"
                AutomaticCanvasSize={Enum.AutomaticSize.Y}
                BackgroundTransparency={1}
                BorderSizePixel={0}
                CanvasSize={new UDim2(0, 0, 0, 0)}
                ScrollBarThickness={8}
                Size={new UDim2(1, 0, 1, -50)}
            >
                <uilistlayout
                    HorizontalAlignment={Enum.HorizontalAlignment.Center}
                    Padding={new UDim(0, 15)}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                />

                <uipadding
                    PaddingBottom={new UDim(0, 5)}
                    PaddingLeft={new UDim(0, 15)}
                    PaddingRight={new UDim(0, 15)}
                    PaddingTop={new UDim(0, 8)}
                />

                {/* Render Quest Options */}
                {sortedQuests.map(([questId, quest]) => {
                    const currentStage = stagePerQuest.get(questId) ?? 0;
                    const isTracked = trackedQuest === questId;
                    const isExpanded = expandedQuests.has(questId);

                    return (
                        <QuestOption
                            key={questId}
                            questId={questId}
                            quest={quest}
                            currentStage={currentStage}
                            level={level}
                            isTracked={isTracked}
                            isExpanded={isExpanded}
                            onToggleExpanded={() => handleToggleQuestContent(questId)}
                            onTrack={() => handleTrackQuest(questId)}
                        />
                    );
                })}
            </scrollingframe>
        </BasicWindow>
    );
}


import React from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import { questState } from "client/components/quest/QuestState";
import TrackedQuestWindow from "client/components/quest/TrackedQuestWindow";
import StoryMocking from "client/components/StoryMocking";
import useVisibility from "client/hooks/useVisibility";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            visible: true,
        },
    },
    (props) => {
        StoryMocking.mockData();
        questState.setTrackedQuest("Quest1");
        useVisibility("TrackedQuest", props.controls.visible);
        return <TrackedQuestWindow />;
    },
);


import { combineHumanReadable } from "@antivivi/vrldk";
import React, { useEffect, useState, useMemo, useCallback } from "@rbxts/react";
import Packets from "shared/Packets";
import Items from "shared/items/Items";
import { questState } from "client/components/quest/QuestState";

export interface QuestHookData {
    questInfo: Map<string, QuestInfo>;
    stagePerQuest: Map<string, number>;
    level: number;
    xp: number;
}

export interface QuestActions {
    onTrackQuest: (questId: string | undefined) => void;
}

/**
 * Hook to manage quest data and interactions
 */
export function useQuestData(): QuestHookData & QuestActions & { trackedQuest: string | undefined } {
    const [questInfo, setQuestInfo] = useState(Packets.questInfo.get() || new Map<string, QuestInfo>());
    const [stagePerQuest, setStagePerQuest] = useState(Packets.stagePerQuest.get() || new Map<string, number>());
    const [level, setLevel] = useState(Packets.level.get());
    const [xp, setXp] = useState(Packets.xp.get() || 0);
    const [trackedQuest, setTrackedQuest] = useState<string | undefined>(questState.getTrackedQuest());

    // Set up packet observers
    useEffect(() => {
        const connections = [
            Packets.questInfo.observe((value) => setQuestInfo(value)),
            Packets.stagePerQuest.observe((value) => setStagePerQuest(value)),
            Packets.level.observe((value) => setLevel(value)),
            Packets.xp.observe((value) => setXp(value)),
            // Listen to shared quest state changes
            questState.trackedQuestChanged.connect((questId) => setTrackedQuest(questId)),
        ];

        return () => {
            connections.forEach((conn) => conn.disconnect());
        };
    }, []);

    const onTrackQuest = useCallback((questId: string | undefined) => {
        // Update shared quest state
        questState.setTrackedQuest(questId);
    }, []);

    return {
        questInfo,
        stagePerQuest,
        level,
        xp,
        trackedQuest,
        onTrackQuest,
    };
}

export function getLengthName(length: number) {
    switch (length) {
        case 0:
            return "Tiny";
        case 1:
            return "Short";
        case 2:
            return "Medium";
        case 3:
            return "Long";
        case 4:
            return "Journey";
        default:
            return "???";
    }
}

export function getLengthColor(length: number) {
    switch (length) {
        case 0:
            return Color3.fromRGB(143, 255, 115);
        case 1:
            return Color3.fromRGB(0, 255, 56);
        case 2:
            return Color3.fromRGB(255, 250, 0);
        case 3:
            return Color3.fromRGB(255, 123, 28);
        case 4:
            return Color3.fromRGB(255, 20, 20);
        default:
            return Color3.fromRGB(255, 255, 255);
    }
}

export function getRewardLabel(reward: Reward) {
    const items = new Array<string>();
    if (reward.xp !== undefined) {
        items.push(`${reward.xp} XP`);
    }
    if (reward.items !== undefined) {
        for (const [itemId, amount] of reward.items) {
            const item = Items.getItem(itemId);
            const itemName = item?.name ?? itemId;
            items.push(`${amount} ${itemName}`);
        }
    }
    return combineHumanReadable(...items);
}


import React, { useCallback, useEffect, useRef, useState } from "@rbxts/react";
import { TweenService } from "@rbxts/services";
import { RobotoSlab, RobotoSlabBold } from "shared/asset/GameFonts";

interface EmpireNameInputProps {
    prefix: string;
    initialValue: string;
    onTextChanged: (text: string) => void;
    maxLength?: number;
    minLength?: number;
}

/**
 * Empire name input component with prefix display and validation
 */
export default function EmpireNameInput({
    prefix,
    initialValue,
    onTextChanged,
    maxLength = 16,
    minLength = 5,
}: EmpireNameInputProps) {
    const [text, setText] = useState(initialValue);
    const [isFocused, setIsFocused] = useState(false);
    const textBoxRef = useRef<TextBox>();

    const isValid = text.size() >= minLength && text.size() <= maxLength;
    const characterCount = text.size();

    const handleTextChanged = useCallback(
        (newText: string) => {
            // Sanitize input - only allow alphanumeric, underscore, and space
            const [sanitized] = newText.gsub("[^%w_ ]", "");
            const truncated = sanitized.sub(1, maxLength);
            setText(truncated);
            onTextChanged(truncated);
        },
        [maxLength, onTextChanged],
    );

    const handleFocused = useCallback(() => {
        setIsFocused(true);
        const textBox = textBoxRef.current;
        if (textBox) {
            const tween = TweenService.Create(textBox, new TweenInfo(0.2), {
                BackgroundTransparency: 0.5,
            });
            tween.Play();
        }
    }, []);

    const handleFocusLost = useCallback(() => {
        setIsFocused(false);
        const textBox = textBoxRef.current;
        if (textBox) {
            const tween = TweenService.Create(textBox, new TweenInfo(0.2), {
                BackgroundTransparency: 0.7,
            });
            tween.Play();
        }
    }, []);

    // Update text when initialValue changes
    useEffect(() => {
        setText(initialValue);
    }, [initialValue]);

    // Listen for text changes
    useEffect(() => {
        const textBox = textBoxRef.current;
        if (!textBox) return;

        const connection = textBox.GetPropertyChangedSignal("Text").Connect(() => {
            const newText = textBox.Text;
            handleTextChanged(newText);
        });

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

    const borderColor = isFocused
        ? isValid
            ? Color3.fromRGB(100, 180, 255) // Blue when focused and valid
            : Color3.fromRGB(255, 120, 120) // Red when focused and invalid
        : Color3.fromRGB(100, 100, 110); // Gray when not focused

    const textColor = isValid ? Color3.fromRGB(255, 255, 255) : Color3.fromRGB(255, 180, 180);
    const backgroundColor = isFocused ? Color3.fromRGB(35, 35, 45) : Color3.fromRGB(25, 25, 35);

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

            {/* Prefix label */}
            <textlabel
                AutomaticSize={Enum.AutomaticSize.X}
                BackgroundTransparency={1}
                FontFace={RobotoSlabBold}
                Size={new UDim2(0, 0, 0.5, 0)}
                Text={prefix}
                TextColor3={Color3.fromRGB(220, 220, 230)}
                TextScaled={true}
                TextSize={22}
                TextWrapped={true}
                TextXAlignment={Enum.TextXAlignment.Right}
                LayoutOrder={1}
            >
                <uistroke Color={Color3.fromRGB(0, 0, 0)} Thickness={1.5} />
            </textlabel>

            {/* Text input container */}
            <frame
                BackgroundColor3={backgroundColor}
                BackgroundTransparency={1}
                BorderSizePixel={0}
                Size={new UDim2(0.3, 0, 0.5, 0)}
                LayoutOrder={2}
            >
                <uicorner CornerRadius={new UDim(0, 10)} />
                <uistroke Color={borderColor} Thickness={2} ApplyStrokeMode={Enum.ApplyStrokeMode.Border} />
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, backgroundColor),
                            new ColorSequenceKeypoint(1, backgroundColor.Lerp(Color3.fromRGB(0, 0, 0), 0.2)),
                        ])
                    }
                    Rotation={90}
                />

                {/* Text input box */}
                <textbox
                    ref={textBoxRef}
                    BackgroundTransparency={1}
                    BorderSizePixel={0}
                    ClearTextOnFocus={false}
                    Size={new UDim2(1, 0, 1, 0)}
                    Text={text}
                    TextColor3={textColor}
                    TextSize={20}
                    FontFace={RobotoSlab}
                    TextWrapped={true}
                    PlaceholderText="Enter empire name..."
                    PlaceholderColor3={Color3.fromRGB(120, 120, 130)}
                    Event={{
                        Focused: handleFocused,
                        FocusLost: handleFocusLost,
                    }}
                >
                    <uicorner CornerRadius={new UDim(0, 10)} />
                    <uipadding
                        PaddingLeft={new UDim(0, 12)}
                        PaddingRight={new UDim(0, 12)}
                        PaddingTop={new UDim(0, 6)}
                        PaddingBottom={new UDim(0, 6)}
                    />
                </textbox>
            </frame>

            {/* Character counter */}
            <frame
                BackgroundColor3={Color3.fromRGB(20, 20, 25)}
                BackgroundTransparency={0.2}
                BorderSizePixel={0}
                Size={new UDim2(0.1, 0, 0.5, 0)}
                LayoutOrder={3}
            >
                <uicorner CornerRadius={new UDim(0, 8)} />
                <uistroke
                    Color={isValid ? Color3.fromRGB(100, 180, 255) : Color3.fromRGB(255, 120, 120)}
                    Thickness={1}
                />
                <uipadding
                    PaddingTop={new UDim(0, 4)}
                    PaddingBottom={new UDim(0, 4)}
                    PaddingLeft={new UDim(0, 6)}
                    PaddingRight={new UDim(0, 6)}
                />

                <textlabel
                    AnchorPoint={new Vector2(0.5, 0.5)}
                    BackgroundTransparency={1}
                    FontFace={RobotoSlab}
                    Position={new UDim2(0.5, 0, 0.5, 0)}
                    Size={new UDim2(1, -6, 1, -6)}
                    Text={`${characterCount}/${maxLength}`}
                    TextColor3={isValid ? Color3.fromRGB(180, 220, 255) : Color3.fromRGB(255, 150, 150)}
                    TextScaled={true}
                    TextSize={14}
                    TextWrapped={true}
                >
                    <uistroke Color={Color3.fromRGB(0, 0, 0)} Thickness={1} />
                </textlabel>
            </frame>
        </frame>
    );
}


import React, { useCallback, useRef, useState } from "@rbxts/react";
import { TweenService } from "@rbxts/services";
import { RobotoSlabBold } from "shared/asset/GameFonts";

interface PurchaseButtonProps {
    text: string;
    price: string;
    currency: "robux" | "funds";
    onClick: () => void;
    disabled?: boolean;
}

/**
 * Purchase button component for Robux or Funds payments with modern styling
 */
export default function PurchaseButton({ text, price, currency, onClick, disabled = false }: PurchaseButtonProps) {
    const buttonRef = useRef<TextButton>();
    const [isHovered, setIsHovered] = useState(false);
    const [isPressed, setIsPressed] = useState(false);

    const handleMouseEnter = useCallback(() => {
        if (disabled) return;
        setIsHovered(true);
        const button = buttonRef.current;
        if (button) {
            const tween = TweenService.Create(button, new TweenInfo(0.2, Enum.EasingStyle.Quad), {
                Size: new UDim2(0.42, 4, 1, 4),
            });
            tween.Play();
        }
    }, [disabled]);

    const handleMouseLeave = useCallback(() => {
        if (disabled) return;
        setIsHovered(false);
        setIsPressed(false);
        const button = buttonRef.current;
        if (button) {
            const tween = TweenService.Create(button, new TweenInfo(0.2, Enum.EasingStyle.Quad), {
                Size: new UDim2(0.42, 0, 1, 0),
            });
            tween.Play();
        }
    }, [disabled]);

    const handleMouseButton1Down = useCallback(() => {
        if (disabled) return;
        setIsPressed(true);
    }, [disabled]);

    const handleMouseButton1Up = useCallback(() => {
        if (disabled) return;
        setIsPressed(false);
    }, [disabled]);

    const handleActivated = useCallback(() => {
        if (disabled) return;
        onClick();
    }, [disabled, onClick]);

    // Color scheme based on currency type
    const isRobux = currency === "robux";
    const gradientColors = isRobux
        ? [Color3.fromRGB(40, 180, 40), Color3.fromRGB(20, 140, 20)] // Enhanced green for Robux
        : [Color3.fromRGB(100, 150, 255), Color3.fromRGB(60, 110, 200)]; // Blue for Funds

    const strokeColor = isRobux ? Color3.fromRGB(80, 220, 80) : Color3.fromRGB(120, 170, 255);
    const shadowColor = isRobux ? Color3.fromRGB(15, 80, 15) : Color3.fromRGB(30, 60, 120);

    return (
        <textbutton
            ref={buttonRef}
            BackgroundColor3={Color3.fromRGB(255, 255, 255)}
            BackgroundTransparency={disabled ? 0.6 : 0}
            BorderSizePixel={0}
            Size={new UDim2(0.42, 0, 1, 0)}
            Text=""
            TextColor3={Color3.fromRGB(0, 0, 0)}
            TextSize={14}
            AutoButtonColor={false}
            Event={{
                MouseEnter: handleMouseEnter,
                MouseLeave: handleMouseLeave,
                MouseButton1Down: handleMouseButton1Down,
                MouseButton1Up: handleMouseButton1Up,
                Activated: handleActivated,
            }}
        >
            <uicorner CornerRadius={new UDim(0, 12)} />
            <uistroke
                Color={strokeColor}
                Thickness={2}
                ApplyStrokeMode={Enum.ApplyStrokeMode.Border}
                Transparency={disabled ? 0.6 : 0}
            />

            {/* Shadow effect */}
            <frame
                BackgroundColor3={shadowColor}
                BackgroundTransparency={0.3}
                BorderSizePixel={0}
                Position={new UDim2(0, 2, 0, 2)}
                Size={new UDim2(1, 0, 1, 0)}
                ZIndex={-1}
            >
                <uicorner CornerRadius={new UDim(0, 12)} />
            </frame>

            {/* Gradient background */}
            <uigradient
                Color={
                    new ColorSequence([
                        new ColorSequenceKeypoint(0, gradientColors[0]),
                        new ColorSequenceKeypoint(1, gradientColors[1]),
                    ])
                }
                Rotation={90}
                Transparency={disabled ? new NumberSequence(0.5) : new NumberSequence(0)}
            />

            {/* Shine effect for hover */}
            {isHovered && !disabled && (
                <frame
                    BackgroundColor3={Color3.fromRGB(255, 255, 255)}
                    BackgroundTransparency={0.7}
                    BorderSizePixel={0}
                    Size={new UDim2(1, 0, 0.3, 0)}
                >
                    <uicorner CornerRadius={new UDim(0, 12)} />
                    <uigradient
                        Color={new ColorSequence(Color3.fromRGB(255, 255, 255))}
                        Rotation={90}
                        Transparency={
                            new NumberSequence([new NumberSequenceKeypoint(0, 0.3), new NumberSequenceKeypoint(1, 1)])
                        }
                    />
                </frame>
            )}

            {/* Button text */}
            <textlabel
                AnchorPoint={new Vector2(0.5, 0.5)}
                BackgroundTransparency={1}
                FontFace={RobotoSlabBold}
                Position={new UDim2(0.5, 0, 0.5, 0)}
                Size={new UDim2(0.85, 0, 0.8, 0)}
                Text={price}
                TextColor3={disabled ? Color3.fromRGB(160, 160, 160) : Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextSize={16}
                TextWrapped={true}
            >
                <uistroke Color={Color3.fromRGB(0, 0, 0)} Thickness={2} Transparency={disabled ? 0.5 : 0} />
            </textlabel>

            {/* Pressed effect overlay */}
            {isPressed && !disabled && (
                <frame
                    BackgroundColor3={Color3.fromRGB(0, 0, 0)}
                    BackgroundTransparency={0.4}
                    BorderSizePixel={0}
                    Size={new UDim2(1, 0, 1, 0)}
                >
                    <uicorner CornerRadius={new UDim(0, 12)} />
                </frame>
            )}
        </textbutton>
    );
}


import { OnoeNum } from "@rbxts/serikanum";
import React from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import RenameWindow from "client/components/rename/RenameWindow";
import StoryMocking from "client/components/StoryMocking";
import { useSingleDocumentVisibility } from "client/hooks/useVisibility";
import Packets from "shared/Packets";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            visible: true,
            name: "Empire Test2",
            renameExponent: 36,
        },
    },
    (props) => {
        StoryMocking.mockData();

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

        Packets.empireName.set(props.controls.name);
        Packets.renameCost.set(OnoeNum.fromSerika(1, props.controls.renameExponent));

        return <RenameWindow />;
    },
);


/**
 * @fileoverview Modern React TSX implementation of the rename window using TechWindow
 *
 * Provides interface for players to rename their empire with:
 * - Clean, modern UI design with gradients and hover effects
 * - Two payment options: Robux and Funds
 * - Real-time input validation and character counting
 * - Smooth animations and professional styling
 * - Integration with existing packet system
 *
 * Replaces the old Roblox Studio UI with maintainable React components.
 */

import { OnoeNum } from "@rbxts/serikanum";
import React, { useCallback, useState } from "@rbxts/react";
import EmpireNameInput from "client/components/rename/EmpireNameInput";
import PurchaseButton from "client/components/rename/PurchaseButton";
import useSingleDocument from "client/components/sidebar/useSingleDocumentWindow";
import TechWindow from "client/components/window/TechWindow";
import { showErrorToast } from "client/components/toast/ToastService";
import { RobotoSlab, RobotoSlabBold } from "shared/asset/GameFonts";
import useProperty from "client/hooks/useProperty";
import { getAsset } from "shared/asset/AssetMap";
import { playSound } from "shared/asset/GameAssets";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Packets from "shared/Packets";

/**
 * Main rename window component for empire renaming functionality
 * Features modern design with validation and smooth user interactions
 */
export default function RenameWindow() {
    const { id, visible, closeDocument } = useSingleDocument({ id: "Rename", priority: 3 });

    // Observe data from packets
    const renameCost = useProperty(Packets.renameCost);
    const empireName = useProperty(Packets.empireName);

    // Local state for form
    const [inputText, setInputText] = useState("");
    const [isProcessing, setIsProcessing] = useState(false);

    // Parse empire name to get prefix and suffix
    const [prefix, suffix] = empireName!.split("'s ");
    const fullPrefix = prefix + "'s ";

    const handleTextChanged = useCallback((text: string) => {
        setInputText(text);
    }, []);

    const handleRobuxPurchase = useCallback(() => {
        if (isProcessing || inputText.size() < 5 || inputText.size() > 16) return;

        setIsProcessing(true);
        const result = Packets.promptRename.toServer(inputText, "robux");

        // Reset processing state after a short delay
        task.wait(1);
        setIsProcessing(false);

        if (result) {
            closeDocument();
        }
    }, [inputText, isProcessing, closeDocument]);

    const handleFundsPurchase = useCallback(() => {
        if (isProcessing || inputText.size() < 5 || inputText.size() > 16) return;

        setIsProcessing(true);
        const result = Packets.promptRename.toServer(inputText, "funds");

        if (result === true) {
            playSound("ItemPurchase.mp3");
            closeDocument();
        } else {
            playSound("Error.mp3");
            showErrorToast("Renaming failed. Try a different name.");
        }

        setIsProcessing(false);
    }, [inputText, isProcessing, closeDocument]);

    const isInputValid = inputText.size() >= 5 && inputText.size() <= 16;
    const fundsPrice = CurrencyBundle.getFormatted("Funds", renameCost || new OnoeNum(0));

    return (
        <TechWindow icon={getAsset("assets/Settings.png")} id={id} title="Rename Empire" visible={visible}>
            <frame BackgroundTransparency={1} Size={new UDim2(1, 0, 1, 0)} Visible={!isProcessing}>
                <uilistlayout
                    HorizontalAlignment={Enum.HorizontalAlignment.Center}
                    Padding={new UDim(0, 15)}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                    VerticalAlignment={Enum.VerticalAlignment.Center}
                />

                {/* Header Card */}
                <frame
                    key="HeaderCard"
                    BackgroundColor3={Color3.fromRGB(25, 25, 30)}
                    BorderSizePixel={0}
                    LayoutOrder={1}
                    Size={new UDim2(0.95, 0, 0.2, 0)}
                >
                    <uicorner CornerRadius={new UDim(0, 12)} />
                    <uistroke Color={Color3.fromRGB(70, 70, 80)} Thickness={1} />
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(30, 30, 35)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(20, 20, 25)),
                            ])
                        }
                        Rotation={90}
                    />

                    <frame BackgroundTransparency={1} Size={new UDim2(1, 0, 1, 0)}>
                        <uilistlayout
                            HorizontalAlignment={Enum.HorizontalAlignment.Center}
                            VerticalAlignment={Enum.VerticalAlignment.Center}
                            SortOrder={Enum.SortOrder.LayoutOrder}
                        />
                        <uipadding PaddingBottom={new UDim(0, 10)} PaddingTop={new UDim(0, 10)} />

                        {/* Main title */}
                        <textlabel
                            BackgroundTransparency={1}
                            FontFace={RobotoSlabBold}
                            LayoutOrder={1}
                            Size={new UDim2(1, 0, 0.6, 0)}
                            Text="Rename your empire!"
                            TextColor3={Color3.fromRGB(255, 255, 255)}
                            TextScaled={true}
                            TextWrapped={true}
                        >
                            <uistroke Color={Color3.fromRGB(0, 0, 0)} Thickness={2} />
                        </textlabel>

                        {/* Subtitle */}
                        <textlabel
                            BackgroundTransparency={1}
                            FontFace={RobotoSlab}
                            LayoutOrder={2}
                            Size={new UDim2(1, 0, 0.4, 0)}
                            Text="(5-16 characters)"
                            TextColor3={Color3.fromRGB(200, 200, 210)}
                            TextScaled={true}
                            TextWrapped={true}
                        >
                            <uistroke Color={Color3.fromRGB(0, 0, 0)} Thickness={1.5} />
                        </textlabel>
                    </frame>
                </frame>

                {/* Input Card */}
                <frame
                    key="InputCard"
                    BackgroundColor3={Color3.fromRGB(25, 25, 30)}
                    BorderSizePixel={0}
                    LayoutOrder={2}
                    Size={new UDim2(0.95, 0, 0.25, 0)}
                >
                    <uicorner CornerRadius={new UDim(0, 12)} />
                    <uistroke Color={Color3.fromRGB(70, 70, 80)} Thickness={1} />
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(30, 30, 35)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(20, 20, 25)),
                            ])
                        }
                        Rotation={90}
                    />

                    <frame BackgroundTransparency={1} Size={new UDim2(1, 0, 1, 0)}>
                        <uilistlayout
                            HorizontalAlignment={Enum.HorizontalAlignment.Center}
                            VerticalAlignment={Enum.VerticalAlignment.Center}
                            SortOrder={Enum.SortOrder.LayoutOrder}
                        />
                        <uipadding PaddingBottom={new UDim(0, 10)} PaddingTop={new UDim(0, 10)} />

                        {/* Empire name input */}
                        <EmpireNameInput
                            key="NameInput"
                            prefix={fullPrefix}
                            initialValue={suffix || ""}
                            onTextChanged={handleTextChanged}
                            maxLength={16}
                            minLength={5}
                        />
                    </frame>
                </frame>

                {/* Purchase Card */}
                <frame
                    key="PurchaseCard"
                    BackgroundColor3={Color3.fromRGB(25, 25, 30)}
                    BorderSizePixel={0}
                    LayoutOrder={3}
                    Size={new UDim2(0.95, 0, 0.25, 0)}
                >
                    <uicorner CornerRadius={new UDim(0, 12)} />
                    <uistroke Color={Color3.fromRGB(70, 70, 80)} Thickness={1} />
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(30, 30, 35)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(20, 20, 25)),
                            ])
                        }
                        Rotation={90}
                    />

                    <frame BackgroundTransparency={1} Size={new UDim2(1, 0, 1, 0)}>
                        <uilistlayout
                            HorizontalAlignment={Enum.HorizontalAlignment.Center}
                            VerticalAlignment={Enum.VerticalAlignment.Center}
                            SortOrder={Enum.SortOrder.LayoutOrder}
                            Padding={new UDim(0, 15)}
                        />

                        <uipadding
                            PaddingBottom={new UDim(0, 20)}
                            PaddingLeft={new UDim(0, 20)}
                            PaddingRight={new UDim(0, 20)}
                            PaddingTop={new UDim(0, 20)}
                        />

                        {/* Purchase section label */}
                        <textlabel
                            BackgroundTransparency={1}
                            FontFace={RobotoSlabBold}
                            LayoutOrder={1}
                            Size={new UDim2(1, 0, 0, 30)}
                            Text="Purchase for:"
                            TextColor3={Color3.fromRGB(255, 255, 255)}
                            TextSize={24}
                            TextWrapped={true}
                            TextYAlignment={Enum.TextYAlignment.Center}
                        >
                            <uistroke Color={Color3.fromRGB(0, 0, 0)} Thickness={1.5} />
                        </textlabel>

                        {/* Purchase options */}
                        <frame
                            key="PurchaseOptions"
                            BackgroundTransparency={1}
                            LayoutOrder={2}
                            Size={new UDim2(0.9, 0, 0, 40)}
                        >
                            <uilistlayout
                                FillDirection={Enum.FillDirection.Horizontal}
                                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                                Padding={new UDim(0, 25)}
                                SortOrder={Enum.SortOrder.LayoutOrder}
                                VerticalAlignment={Enum.VerticalAlignment.Center}
                            />

                            <PurchaseButton
                                key="RobuxButton"
                                text="Robux"
                                price="25 Robux"
                                currency="robux"
                                onClick={handleRobuxPurchase}
                                disabled={!isInputValid || isProcessing}
                            />

                            {/* "or" separator */}
                            <textlabel
                                key="OrLabel"
                                BackgroundTransparency={1}
                                FontFace={RobotoSlabBold}
                                Size={new UDim2(0, 40, 0.8, 0)}
                                Text="or"
                                TextColor3={Color3.fromRGB(200, 200, 210)}
                                TextSize={20}
                                TextWrapped={true}
                                TextYAlignment={Enum.TextYAlignment.Center}
                            >
                                <uistroke Color={Color3.fromRGB(0, 0, 0)} Thickness={1.5} />
                            </textlabel>

                            <PurchaseButton
                                key="FundsButton"
                                text="Funds"
                                price={fundsPrice}
                                currency="funds"
                                onClick={handleFundsPurchase}
                                disabled={!isInputValid || isProcessing}
                            />
                        </frame>
                    </frame>
                </frame>

                {/* Warning Card */}
                <frame
                    key="WarningCard"
                    BackgroundColor3={Color3.fromRGB(40, 25, 25)}
                    BorderSizePixel={0}
                    LayoutOrder={4}
                    Size={new UDim2(0.95, 0, 0.15, 0)}
                >
                    <uicorner CornerRadius={new UDim(0, 12)} />
                    <uistroke Color={Color3.fromRGB(100, 60, 60)} Thickness={1} />
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(45, 30, 30)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(35, 20, 20)),
                            ])
                        }
                        Rotation={90}
                    />

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

                        <uipadding
                            PaddingBottom={new UDim(0, 5)}
                            PaddingLeft={new UDim(0, 20)}
                            PaddingRight={new UDim(0, 20)}
                            PaddingTop={new UDim(0, 5)}
                        />

                        {/* Warning icon and text */}
                        <frame BackgroundTransparency={1} LayoutOrder={1} Size={new UDim2(1, 0, 1, 0)}>
                            <uilistlayout
                                FillDirection={Enum.FillDirection.Horizontal}
                                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                                VerticalAlignment={Enum.VerticalAlignment.Center}
                                Padding={new UDim(0, 10)}
                                SortOrder={Enum.SortOrder.LayoutOrder}
                            />

                            {/* Warning icon */}
                            <textlabel
                                BackgroundTransparency={1}
                                FontFace={RobotoSlabBold}
                                LayoutOrder={1}
                                Size={new UDim2(0, 30, 0, 30)}
                                Text="⚠"
                                TextColor3={Color3.fromRGB(255, 200, 100)}
                                TextSize={24}
                                TextScaled={true}
                            />

                            {/* Warning text */}
                            <textlabel
                                BackgroundTransparency={1}
                                FontFace={RobotoSlab}
                                LayoutOrder={2}
                                Size={new UDim2(1, -40, 1, 0)}
                                Text={`Purchasing with Funds will increase the next Funds price by 1000x.
If you are on the leaderboards, you may need to rejoin to view the rename.`}
                                TextColor3={Color3.fromRGB(220, 180, 180)}
                                TextSize={16}
                                TextWrapped={true}
                                TextXAlignment={Enum.TextXAlignment.Left}
                                TextYAlignment={Enum.TextYAlignment.Center}
                            >
                                <uistroke Color={Color3.fromRGB(0, 0, 0)} Thickness={1} />
                            </textlabel>
                        </frame>
                    </frame>
                </frame>
            </frame>
            {/* Processing indicator */}
            {isProcessing && (
                <frame
                    key="ProcessingOverlay"
                    BackgroundTransparency={1}
                    BorderSizePixel={0}
                    Size={new UDim2(1, 0, 1, 0)}
                    ZIndex={10}
                >
                    <textlabel
                        AnchorPoint={new Vector2(0.5, 0.5)}
                        BackgroundTransparency={1}
                        FontFace={RobotoSlabBold}
                        Position={new UDim2(0.5, 0, 0.5, 0)}
                        Size={new UDim2(0.6, 0, 0.2, 0)}
                        Text="Processing..."
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextScaled={true}
                        TextSize={24}
                    >
                        <uistroke Color={Color3.fromRGB(0, 0, 0)} Thickness={2} />
                    </textlabel>
                </frame>
            )}
        </TechWindow>
    );
}


import React, { Fragment } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { OnoeNum } from "@rbxts/serikanum";
import { CreateReactStory } from "@rbxts/ui-labs";
import BalanceWindow from "client/components/balance/BalanceWindow";
import CurrencyGainManager from "client/components/balance/CurrencyGainManager";
import playSkillificationSequence from "client/components/reset/playSkillificationSequence";
import ResetRenderer from "client/components/reset/ResetRenderer";
import useVisibility from "client/hooks/useVisibility";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
    },
    () => {
        useVisibility("Balance", true);

        return (
            <Fragment>
                <textbutton
                    AnchorPoint={new Vector2(0.5, 0)}
                    Text={"Play Skillification Sequence"}
                    Size={new UDim2(0, 300, 0, 50)}
                    Position={new UDim2(0.5, 0, 0, 0)}
                    Event={{
                        MouseButton1Click: () => {
                            playSkillificationSequence(new OnoeNum(50));
                        },
                    }}
                />
                <ResetRenderer />
                <BalanceWindow />
                <CurrencyGainManager />
            </Fragment>
        );
    },
);


import React, { Fragment, useEffect } from "@rbxts/react";
import playSkillificationSequence from "client/components/reset/playSkillificationSequence";
import Packets from "shared/Packets";

export default function ResetRenderer() {
    useEffect(() => {
        const connection = Packets.reset.fromServer((layer, amount) => {
            if (layer === "Skillification") {
                playSkillificationSequence(amount);
            }
        });

        return () => connection.Disconnect();
    });
    return <Fragment />;
}


import { BaseOnoeNum, OnoeNum } from "@rbxts/serikanum";
import { Debris, TextService, TweenService, Workspace } from "@rbxts/services";
import Shaker from "client/components/effect/Shaker";
import { PLAYER_GUI } from "shared/constants";
import MusicManager from "client/MusicManager";
import { UISignals } from "shared/api/APIExpose";
import { ASSETS, playSound } from "shared/asset/GameAssets";
import { AREAS } from "shared/world/Area";

declare global {
    interface Assets {
        Resets: Folder;
    }
}

function createDramaticIntro(): ScreenGui {
    // Create the black screen overlay
    const screenGui = new Instance("ScreenGui");
    screenGui.Name = "ResetDramaticIntro";
    screenGui.DisplayOrder = 1000; // High display order to ensure it's on top
    screenGui.IgnoreGuiInset = true;
    screenGui.ResetOnSpawn = false;
    screenGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling;
    screenGui.Parent = PLAYER_GUI;

    // Black background frame
    const blackFrame = new Instance("Frame");
    blackFrame.Name = "BlackOverlay";
    blackFrame.Size = new UDim2(1, 0, 1, 0);
    blackFrame.Position = new UDim2(0, 0, 0, 0);
    blackFrame.BackgroundColor3 = new Color3(0, 0, 0);
    blackFrame.BackgroundTransparency = 0;
    blackFrame.BorderSizePixel = 0;
    blackFrame.ZIndex = 1;
    blackFrame.Parent = screenGui;

    // Dramatic text label - start with empty text
    const textLabel = new Instance("TextLabel");
    textLabel.Name = "DramaticText";
    textLabel.AnchorPoint = new Vector2(0.5, 0.5);
    textLabel.Position = new UDim2(0.5, 0, 0.5, 0);
    textLabel.BackgroundTransparency = 1;
    textLabel.Font = Enum.Font.Merriweather;
    textLabel.Text = ""; // Start with empty text for character-by-character animation
    textLabel.TextColor3 = new Color3(1, 1, 1);
    textLabel.TextSize = 30;
    textLabel.TextWrapped = true;
    textLabel.TextXAlignment = Enum.TextXAlignment.Left;
    textLabel.TextYAlignment = Enum.TextYAlignment.Center;
    textLabel.ZIndex = 2;
    textLabel.TextTransparency = 0; // Make visible since we'll control character appearance
    textLabel.Parent = screenGui;

    const textStroke = new Instance("UIStroke");
    textStroke.Color = new Color3(0, 0, 0);
    textStroke.Thickness = 3;
    textStroke.Transparency = 0;
    textStroke.Parent = textLabel;

    return screenGui;
}

async function animateTextCharacterByCharacter(textLabel: TextLabel, fullText: string): Promise<void> {
    const charDelay = 0.06;
    const fadeInDuration = 0.3; // Duration for each character to fade in
    const characterLabels: TextLabel[] = [];

    // Set up the container size
    const textSize = TextService.GetTextSize(fullText, 30, textLabel.Font, new Vector2(1000, 1000));
    textLabel.Size = new UDim2(0, textSize.X, 0, 30);
    textLabel.Text = ""; // Keep main label empty

    // Create individual character labels
    for (let i = 0; i < fullText.size(); i++) {
        const char = fullText.sub(i + 1, i + 1);
        const charLabel = new Instance("TextLabel");
        charLabel.Name = `Char${i}`;
        charLabel.BackgroundTransparency = 1;
        charLabel.Font = textLabel.Font;
        charLabel.Text = char;
        charLabel.TextSize = 30;
        charLabel.TextColor3 = textLabel.TextColor3;
        charLabel.TextSize = textLabel.TextSize;
        charLabel.TextXAlignment = Enum.TextXAlignment.Left;
        charLabel.TextYAlignment = Enum.TextYAlignment.Center;
        charLabel.TextTransparency = 1; // Start invisible
        charLabel.ZIndex = textLabel.ZIndex;

        const stroke = new Instance("UIStroke");
        stroke.Color = new Color3(0, 0, 0);
        stroke.Thickness = 3;
        stroke.Transparency = 1;
        stroke.Parent = charLabel;

        // Calculate position for this character
        const precedingText = fullText.sub(1, i);
        const precedingSize = TextService.GetTextSize(precedingText, 30, textLabel.Font, new Vector2(1000, 1000));
        const charSize = TextService.GetTextSize(char, 30, textLabel.Font, new Vector2(1000, 1000));

        charLabel.Size = new UDim2(0, charSize.X, 1, 0);
        charLabel.Position = new UDim2(0, precedingSize.X, 0, 0);
        charLabel.Parent = textLabel;

        characterLabels.push(charLabel);
    }

    // Animate characters one by one
    for (let currentIndex = 0; currentIndex < fullText.size(); currentIndex++) {
        const charLabel = characterLabels[currentIndex];
        const currentChar = fullText.sub(currentIndex + 1, currentIndex + 1);

        // Fade in the character
        TweenService.Create(charLabel, new TweenInfo(fadeInDuration, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {
            TextTransparency: 0,
        }).Play();

        const stroke = charLabel.FindFirstChildOfClass("UIStroke");
        if (stroke) {
            TweenService.Create(
                stroke,
                new TweenInfo(fadeInDuration, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
                { Transparency: 0 },
            ).Play();
        }

        if (currentIndex < fullText.size() - 1) {
            // Add extra delay for punctuation marks
            let nextDelay = charDelay;

            // Check for ellipses (three dots in a row)
            const isEllipsis =
                currentChar === "." &&
                currentIndex >= 2 &&
                fullText.sub(currentIndex - 1, currentIndex - 1) === "." &&
                fullText.sub(currentIndex, currentIndex) === ".";

            if (isEllipsis) {
                // For the third dot in "...", add 1.5 seconds total delay
                nextDelay = 1.5;
            } else if (currentChar === "." || currentChar === "!" || currentChar === "?") {
                nextDelay = 1;
            } else if (currentChar === "," || currentChar === ";") {
                nextDelay = 0.5;
            }

            await new Promise<void>((resolve) => {
                task.delay(nextDelay, () => resolve());
            });
        } else {
            // Wait for the last character to finish fading in
            await new Promise<void>((resolve) => {
                task.delay(fadeInDuration, () => resolve());
            });
        }
    }
}

async function fadeOutTextElements(textLabel: TextLabel, textStroke?: UIStroke): Promise<void> {
    // Fade out all individual character labels
    const characterLabels = textLabel.GetChildren();
    for (const charLabel of characterLabels) {
        if (charLabel.IsA("TextLabel")) {
            TweenService.Create(charLabel, new TweenInfo(1, Enum.EasingStyle.Quad, Enum.EasingDirection.In), {
                TextTransparency: 1,
            }).Play();
            const stroke = charLabel.FindFirstChildOfClass("UIStroke");
            if (stroke) {
                TweenService.Create(stroke, new TweenInfo(1, Enum.EasingStyle.Quad, Enum.EasingDirection.In), {
                    Transparency: 1,
                }).Play();
            }
        }
    }

    // Also fade out the stroke
    if (textStroke) {
        const strokeFadeOut = TweenService.Create(
            textStroke,
            new TweenInfo(1, Enum.EasingStyle.Quad, Enum.EasingDirection.In),
            { Transparency: 1 },
        );
        strokeFadeOut.Play();
    }

    // Wait for fade out to complete
    await new Promise<void>((resolve) => {
        task.delay(1, () => resolve());
    });
}

async function fadeOutBlackScreenAndStartCameraSequence(
    dramaticIntro: ScreenGui,
    currentCamera: Camera,
): Promise<void> {
    const blackFrame = dramaticIntro.FindFirstChild("BlackOverlay") as Frame;
    const blackFadeOut = TweenService.Create(
        blackFrame,
        new TweenInfo(1, Enum.EasingStyle.Quad, Enum.EasingDirection.In),
        { BackgroundTransparency: 1 },
    );
    blackFadeOut.Play();

    // Start the original camera sequence
    currentCamera.CameraType = Enum.CameraType.Scriptable;
    const instance = AREAS.BarrenIslands.worldNode.waitForInstance().WaitForChild("ResetCamera");
    const toCFrame = (instance as BasePart).CFrame;
    TweenService.Create(currentCamera, new TweenInfo(1, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {
        CFrame: toCFrame,
    }).Play();

    // Clean up the dramatic intro GUI after 1 second
    await new Promise<void>((resolve) => {
        task.delay(1, () => {
            dramaticIntro.Destroy();
            resolve();
        });
    });
}

async function playThunderSequence(currentCamera: Camera): Promise<void> {
    playSound("Thunder.mp3");
    const lightning = ASSETS.Resets.WaitForChild("SkillificationLightning").Clone() as BasePart;
    lightning.Parent = Workspace;
    TweenService.Create(lightning, new TweenInfo(1), { Transparency: 1 }).Play();

    await new Promise<void>((resolve) => {
        task.delay(0.35, () => {
            const effects = lightning.GetChildren();
            for (const effect of effects) {
                if (effect.IsA("ParticleEmitter")) effect.Enabled = false;
            }
            currentCamera.CameraType = Enum.CameraType.Custom;
            Shaker.shake();

            // Fade music back in
            if (MusicManager.playing !== undefined) {
                MusicManager.fadeIn(MusicManager.playing);
            }
            resolve();
        });
    });

    Debris.AddItem(lightning, 2);
}

export default async function playSkillificationSequence(amount: BaseOnoeNum) {
    const currentCamera = Workspace.CurrentCamera;
    if (currentCamera === undefined) return;
    if (currentCamera.CameraType === Enum.CameraType.Scriptable) return;

    // Create the dramatic intro overlay
    const dramaticIntro = createDramaticIntro();
    const textLabel = dramaticIntro.FindFirstChild("DramaticText") as TextLabel;
    const textStroke = textLabel.FindFirstChild("UIStroke") as UIStroke;

    // Start the dramatic sequence
    playSound("MagicCast.mp3");
    playSound("resets/Skillification.mp3");
    if (MusicManager.playing !== undefined) {
        MusicManager.fadeOut(MusicManager.playing);
    }

    // Wait initial delay
    task.wait(0.5);

    // Phase 1: Animate text character by character
    await animateTextCharacterByCharacter(textLabel, "You sacrificed everything for power... Now, embrace it.");

    // Phase 2: Hold the text for dramatic effect (2 seconds)
    task.wait(2);

    // Phase 3: Fade out the text (1 second)
    await fadeOutTextElements(textLabel, textStroke);

    // Phase 4: Fade out the black screen and start original sequence (1 second)
    await fadeOutBlackScreenAndStartCameraSequence(dramaticIntro, currentCamera);

    // Thunder and lightning sequence (1 second delay + sequence)
    await new Promise<void>((resolve) => {
        task.delay(1, () => resolve());
    });

    await playThunderSequence(currentCamera);

    UISignals.showCurrencyGain.fire(undefined, new Map([["Skill", new OnoeNum(amount)]]));
}


import React, { useEffect } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import CopyWindow from "client/components/settings/CopyWindow";
import StoryMocking from "client/components/StoryMocking";
import { useSingleDocumentVisibility } from "client/hooks/useVisibility";
import Packets from "shared/Packets";

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

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

        useEffect(() => {
            Packets.codeReceived.toAllClients(props.controls.text);
        }, [props.controls.text]);

        return <CopyWindow />;
    },
);


import React, { useEffect, useState } from "@rbxts/react";
import useSingleDocument from "client/components/sidebar/useSingleDocumentWindow";
import TechWindow from "client/components/window/TechWindow";
import { RobotoMono } from "shared/asset/GameFonts";
import Packets from "shared/Packets";

export default function CopyWindow() {
    const { id, visible, openDocument } = useSingleDocument({ id: "Copy", priority: 10 });
    const [text, setText] = useState("Copy this text");

    useEffect(() => {
        const connection = Packets.codeReceived.fromServer((code: string) => {
            setText(code);
            openDocument();
        });

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

    return (
        <TechWindow icon="" id={id} title="Text Content" visible={visible} size={new UDim2(0.5, 0, 0.5, -50)}>
            <textbox
                BackgroundColor3={Color3.fromRGB(20, 20, 20)}
                BackgroundTransparency={0.3}
                ClearTextOnFocus={false}
                FontFace={RobotoMono}
                Size={new UDim2(1, 0, 1, 0)}
                Text={text}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextSize={16}
                TextWrapped={true}
            >
                <uistroke
                    ApplyStrokeMode={Enum.ApplyStrokeMode.Border}
                    Color={Color3.fromRGB(54, 54, 54)}
                    Thickness={1}
                />
            </textbox>
        </TechWindow>
    );
}


import React, { useEffect } from "@rbxts/react";
import { Environment } from "@rbxts/ui-labs";
import { RobotoMonoBold } from "shared/asset/GameFonts";
import { playSound } from "shared/asset/GameAssets";

interface HotkeyOptionProps {
    title: string;
    keyText: string;
    layoutOrder?: number;
    isSelected?: boolean;
    onSelect?: () => void;
    onHotkeyChange?: (newKeyCode: Enum.KeyCode) => void;
    onDeselect?: () => void;
}

export default function HotkeyOption({
    title,
    keyText,
    layoutOrder = 0,
    isSelected = false,
    onSelect,
    onHotkeyChange,
    onDeselect,
}: HotkeyOptionProps) {
    const toggleColor = isSelected ? Color3.fromRGB(255, 138, 138) : Color3.fromRGB(85, 255, 127);
    const displayText = isSelected ? ".." : keyText;

    const handleSelect = () => {
        if (!isSelected) {
            playSound("CheckOff.mp3");
        } else {
            playSound("CheckOn.mp3");
        }
        onSelect?.();
    };

    // Listen for key presses when this hotkey option is selected
    useEffect(() => {
        if (!isSelected || !onHotkeyChange) return;

        const connection = Environment.UserInput.InputBegan.Connect((input, gameProcessed) => {
            if (gameProcessed) return;

            // Only accept actual key presses, not mouse clicks or unknown keys
            if (input.KeyCode !== Enum.KeyCode.Unknown && input.UserInputType === Enum.UserInputType.Keyboard) {
                onHotkeyChange(input.KeyCode);
                playSound("CheckOn.mp3");
            }
        });

        return () => connection.Disconnect();
    }, [isSelected, onHotkeyChange, onDeselect]);

    // Handle deselection when pressing Escape key
    useEffect(() => {
        if (!isSelected) return;

        const connection = Environment.UserInput.InputBegan.Connect((input, gameProcessed) => {
            if (gameProcessed) return;

            // Deselect on Escape key press
            if (input.KeyCode === Enum.KeyCode.Escape) {
                onDeselect?.();
            }
        });

        return () => connection.Disconnect();
    }, [isSelected, onDeselect]);

    return (
        <frame BackgroundTransparency={1} LayoutOrder={layoutOrder} Size={new UDim2(1, 0, 0, 40)}>
            <textbutton
                key="Bind"
                AnchorPoint={new Vector2(1, 0.5)}
                BackgroundColor3={toggleColor}
                BorderColor3={Color3.fromRGB(255, 255, 255)}
                BorderSizePixel={3}
                FontFace={RobotoMonoBold}
                Position={new UDim2(0.975, 0, 0.5, 0)}
                Size={new UDim2(0.75, 0, 0.75, 0)}
                SizeConstraint={Enum.SizeConstraint.RelativeYY}
                Text=""
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextSize={14}
                TextStrokeColor3={Color3.fromRGB(153, 102, 0)}
                TextStrokeTransparency={0}
                TextWrapped={true}
                ZIndex={25}
                Event={{
                    Activated: handleSelect,
                    MouseEnter: () => {
                        playSound("EmphasisButtonHover.mp3", undefined, (sound) => {
                            sound.Volume = 0.1;
                            sound.PlaybackSpeed = 2;
                        });
                    },
                }}
            >
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                            new ColorSequenceKeypoint(0.456, Color3.fromRGB(255, 255, 255)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(131, 131, 131)),
                        ])
                    }
                    Rotation={90}
                />
                <uistroke ApplyStrokeMode={Enum.ApplyStrokeMode.Border} Color={toggleColor} Thickness={2}>
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(109, 109, 109)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(109, 109, 109)),
                            ])
                        }
                    />
                </uistroke>
                <uistroke Color={toggleColor}>
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(109, 109, 109)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(109, 109, 109)),
                            ])
                        }
                    />
                </uistroke>
                <textlabel
                    key="KeybindLabel"
                    AnchorPoint={new Vector2(0.5, 0.5)}
                    BackgroundTransparency={1}
                    FontFace={RobotoMonoBold}
                    Position={new UDim2(0.5, 0, 0.5, 0)}
                    Size={new UDim2(0.8, 0, 0.8, 0)}
                    Text={displayText}
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextScaled={true}
                    TextSize={14}
                    TextStrokeTransparency={0}
                    ZIndex={26}
                />
            </textbutton>
            <textlabel
                key="TitleLabel"
                AnchorPoint={new Vector2(0, 0.5)}
                AutomaticSize={Enum.AutomaticSize.XY}
                BackgroundTransparency={1}
                FontFace={RobotoMonoBold}
                LayoutOrder={-1}
                Position={new UDim2(0.025, 0, 0.5, 0)}
                Text={title}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextSize={30}
                TextStrokeTransparency={0}
                ZIndex={25}
            />
        </frame>
    );
}


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

interface SettingSectionProps {
    title: string;
    layoutOrder?: number;
}

export default function SettingSection({ title, layoutOrder = 0 }: SettingSectionProps) {
    return (
        <frame
            AutomaticSize={Enum.AutomaticSize.Y}
            BackgroundTransparency={1}
            LayoutOrder={layoutOrder}
            Size={new UDim2(1, 0, 0, 0)}
        >
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoMonoBold}
                Size={new UDim2(0, 200, 0, 35)}
                Text={title}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextSize={14}
                TextWrapped={true}
                TextXAlignment={Enum.TextXAlignment.Left}
            >
                <uistroke Thickness={2}>
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(30, 30, 30)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(30, 30, 30)),
                            ])
                        }
                        Rotation={90}
                    />
                </uistroke>
            </textlabel>
            <frame
                key="Line"
                BackgroundColor3={Color3.fromRGB(255, 255, 255)}
                BorderSizePixel={0}
                LayoutOrder={99}
                Position={new UDim2(0, 0, 1, 0)}
                Size={new UDim2(1, 0, 0, 3)}
            >
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(208, 208, 208)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(208, 208, 208)),
                        ])
                    }
                    Transparency={
                        new NumberSequence([
                            new NumberSequenceKeypoint(0, 0, 0),
                            new NumberSequenceKeypoint(0.5, 0, 0),
                            new NumberSequenceKeypoint(1, 1, 0),
                        ])
                    }
                />
            </frame>
            <uilistlayout SortOrder={Enum.SortOrder.LayoutOrder} />
            <uipadding PaddingBottom={new UDim(0, 10)} PaddingTop={new UDim(0, 10)} />
        </frame>
    );
}


import React, { useEffect, useRef, useState } from "@rbxts/react";
import { TweenService } from "@rbxts/services";
import { playSound } from "shared/asset/GameAssets";
import Packets from "shared/Packets";
import { RobotoMonoBold } from "shared/asset/GameFonts";
import useProperty from "client/hooks/useProperty";

interface SettingToggleProps {
    setting: keyof Settings;
    title?: string;
    subtitle?: string;
    layoutOrder?: number;
}

export default function SettingToggle({ setting, title, subtitle, layoutOrder = 0 }: SettingToggleProps) {
    title ??= setting;

    const [hovering, setHovering] = useState(false);
    const [justClicked, setJustClicked] = useState(false);
    const settings = useProperty(Packets.settings);
    const enabled = settings[setting] === true;

    const color = enabled ? Color3.fromRGB(170, 255, 127) : Color3.fromRGB(255, 52, 52);
    const hoverColor = color.Lerp(Color3.fromRGB(255, 255, 255), 0.5);
    const buttonRef = useRef<TextButton>();

    useEffect(() => {
        TweenService.Create(buttonRef.current!, new TweenInfo(0.2), {
            BackgroundColor3: hovering && !justClicked ? hoverColor : color,
        }).Play();
    }, [hovering, enabled, justClicked]);

    // Reset justClicked state after a short delay
    useEffect(() => {
        if (justClicked) {
            task.delay(0.3, () => {
                setJustClicked(false);
            });
        }
    }, [justClicked]);

    const handleToggle = () => {
        const newEnabled = !enabled;

        setJustClicked(true);

        // Play appropriate sound based on the new state
        if (newEnabled) {
            playSound("CheckOn.mp3");
        } else {
            playSound("CheckOff.mp3");
        }

        Packets.setSetting.toServer(setting, newEnabled);
    };

    return (
        <frame BackgroundTransparency={1} LayoutOrder={layoutOrder} Size={new UDim2(1, 0, 0, 40)}>
            <textbutton
                key="Toggle"
                ref={buttonRef}
                AnchorPoint={new Vector2(1, 0.5)}
                AutoButtonColor={false}
                BorderColor3={Color3.fromRGB(255, 255, 255)}
                BorderSizePixel={3}
                FontFace={RobotoMonoBold}
                Position={new UDim2(0.975, 0, 0.5, 0)}
                Size={new UDim2(0.75, 0, 0.75, 0)}
                SizeConstraint={Enum.SizeConstraint.RelativeYY}
                Text=""
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextSize={14}
                TextStrokeColor3={Color3.fromRGB(153, 102, 0)}
                TextStrokeTransparency={0}
                TextWrapped={true}
                ZIndex={25}
                Event={{
                    Activated: handleToggle,
                    MouseEnter: () => {
                        setHovering(true);
                        playSound("EmphasisButtonHover.mp3", undefined, (sound) => {
                            sound.Volume = 0.1;
                            sound.PlaybackSpeed = enabled ? 2 : 1.75;
                        });
                    },
                    MouseLeave: () => setHovering(false),
                }}
            >
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                            new ColorSequenceKeypoint(0.456, Color3.fromRGB(255, 255, 255)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(131, 131, 131)),
                        ])
                    }
                    Rotation={90}
                />
                <uistroke ApplyStrokeMode={Enum.ApplyStrokeMode.Border} Color={color} Thickness={2}>
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(109, 109, 109)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(109, 109, 109)),
                            ])
                        }
                    />
                </uistroke>
                <uistroke Color={color}>
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(109, 109, 109)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(109, 109, 109)),
                            ])
                        }
                    />
                </uistroke>
            </textbutton>
            <textlabel
                key="Title"
                AnchorPoint={new Vector2(0, 0.5)}
                AutomaticSize={Enum.AutomaticSize.XY}
                BackgroundTransparency={1}
                FontFace={RobotoMonoBold}
                LayoutOrder={-1}
                Position={new UDim2(0.025, 0, 0.5, 0)}
                RichText={subtitle !== undefined}
                Text={subtitle ? `${title}\n<font color="#bebebe" size="14">${subtitle}</font>` : title}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextSize={subtitle ? 25 : 30}
                TextStrokeTransparency={0}
                TextXAlignment={Enum.TextXAlignment.Left}
                ZIndex={25}
            />
        </frame>
    );
}


import React, { Fragment } from "@rbxts/react";
import SettingsWindow, { SettingsButton } from "client/components/settings/SettingsWindow";

export default function SettingsManager() {
    return (
        <Fragment>
            <SettingsWindow />
            <SettingsButton />
        </Fragment>
    );
}


import React from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import SettingsWindow from "client/components/settings/SettingsWindow";
import StoryMocking from "client/components/StoryMocking";
import { useSingleDocumentVisibility } from "client/hooks/useVisibility";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            visible: true,
        },
    },
    (props) => {
        StoryMocking.mockData();
        useSingleDocumentVisibility("Settings", props.controls.visible);
        return <SettingsWindow />;
    },
);


import React, { JSX, useCallback, useEffect, useState } from "@rbxts/react";
import SerikaNum from "@rbxts/serikanum";
import HotkeyManager, { HOTKEY_BINDINGS } from "client/components/hotkeys/HotkeyManager";
import useHotkeyWithTooltip from "client/components/hotkeys/useHotkeyWithTooltip";
import IconButton from "client/components/IconButton";
import HotkeyOption from "client/components/settings/HotkeyOption";
import SettingSection from "client/components/settings/SettingSection";
import SettingToggle from "client/components/settings/SettingToggle";
import SingleDocumentManager from "client/components/sidebar/SingleDocumentManager";
import useSingleDocument from "client/components/sidebar/useSingleDocumentWindow";
import TechWindow from "client/components/window/TechWindow";
import { getAsset } from "shared/asset/AssetMap";
import Packets from "shared/Packets";

export function SettingsButton() {
    const tooltipProps = useHotkeyWithTooltip({
        action: () => {
            SingleDocumentManager.toggle("Settings");
            return true;
        },
        label: "Settings",
    });

    return (
        <IconButton
            image={getAsset("assets/Settings.png")}
            buttonProps={{
                AnchorPoint: new Vector2(0, 1),
                Size: new UDim2(0, 35, 0.5, 0),
                Position: new UDim2(0, 4, 1, -4),
            }}
            {...tooltipProps}
        />
    );
}

export default function SettingsWindow() {
    const { id, visible } = useSingleDocument({ id: "Settings", priority: 10 });
    const [selectedHotkey, setSelectedHotkey] = useState<string | undefined>();
    const [settings, setSettings] = useState(Packets.settings.get());

    useEffect(() => {
        const connection = Packets.settings.observe((newSettings) => {
            SerikaNum.changeDefaultAbbreviation(newSettings.ScientificNotation === true ? "scientific" : "suffix");
            setSettings(newSettings);
        });
        return () => connection.disconnect();
    }, []);

    // Cleanup hotkey setting state when component unmounts or window closes
    useEffect(() => {
        if (!visible && selectedHotkey !== undefined) {
            setSelectedHotkey(undefined);
            HotkeyManager.setIsSettingHotkey(false);
        }
    }, [visible]);

    const onHotkeySelect = useCallback(
        (hotkeyName: string) => {
            const newSelected = selectedHotkey === hotkeyName ? undefined : hotkeyName;
            setSelectedHotkey(newSelected);
            HotkeyManager.setIsSettingHotkey(newSelected !== undefined);
        },
        [selectedHotkey],
    );

    const onHotkeyDeselect = useCallback(() => {
        setSelectedHotkey(undefined);
        HotkeyManager.setIsSettingHotkey(false);
    }, []);

    const handleHotkeyChange = useCallback(
        (label: string, keyCode: Enum.KeyCode) => {
            Packets.setHotkey.toServer(label, keyCode.Value);
            setSelectedHotkey(undefined);
            task.delay(0.5, () => HotkeyManager.setIsSettingHotkey(false)); // Slight delay to avoid immediate re-trigger
        },
        [setSelectedHotkey],
    );

    const hotkeyOptions = new Array<JSX.Element>();
    for (const binding of HOTKEY_BINDINGS) {
        const label = binding.label;
        const value = settings.hotkeys[label];
        const keyText = value !== undefined ? Enum.KeyCode.FromValue(value)?.Name : binding.keyCode.Name;
        hotkeyOptions.push(
            <HotkeyOption
                key={label}
                title={label}
                keyText={keyText ?? "?"}
                isSelected={selectedHotkey === label}
                onSelect={() => onHotkeySelect?.(label)}
                onHotkeyChange={(newKeyCode) => handleHotkeyChange(label, newKeyCode)}
                onDeselect={() => onHotkeyDeselect?.()}
            />,
        );
    }

    return (
        <TechWindow icon={getAsset("assets/Settings.png")} id={id} visible={visible}>
            <scrollingframe
                key="InteractionOptions"
                AnchorPoint={new Vector2(0.5, 0.5)}
                AutomaticCanvasSize={Enum.AutomaticSize.Y}
                BackgroundTransparency={1}
                CanvasSize={new UDim2(0, 0, 0, 0)}
                Position={new UDim2(0.5, 0, 0.5, 0)}
                ScrollBarThickness={6}
                Selectable={false}
                Size={new UDim2(0.8, 0, 1, 0)}
            >
                <uilistlayout Padding={new UDim(0, 9)} SortOrder={Enum.SortOrder.LayoutOrder} />
                <uipadding PaddingRight={new UDim(0, 5)} />

                {/* General Section */}
                <SettingSection title="General" />

                <SettingToggle setting="Music" />

                <SettingToggle setting="SoundEffects" title="Sound Effects" />

                <SettingToggle setting="ScientificNotation" title="Scientific Notation" />

                <SettingToggle
                    setting="FormatCurrencies"
                    title="Format Currencies"
                    subtitle="This is forcefully disabled on smaller screens."
                />

                {/* Performance Section */}
                <SettingSection title="Performance" />

                <SettingToggle setting="ResetAnimation" title="Reset Layer Animations" />

                <SettingToggle setting="BuildAnimation" title="Build Animations" />

                <SettingToggle setting="CurrencyGainAnimation" title="Currency Gain Animations" />

                <SettingToggle
                    setting="Particles"
                    title="Item Particles"
                    subtitle="Items may need to be placed again to apply changes."
                />

                <SettingToggle
                    setting="ItemShadows"
                    title="Item Shadows"
                    subtitle="Items may need to be placed again to apply changes."
                />

                <SettingToggle setting="VisualRain" title="Visual Rain" subtitle="Toggle rain particle effects." />

                {/* Layout Section */}
                <SettingSection title="Layout" />

                <SettingToggle title="Hide Maxed Items" setting="HideMaxedItems" />

                <SettingToggle title="Focus Camera On Shops" setting="FocusShopCamera" />

                {/* Controls Section */}
                <SettingSection title="Controls" />

                {/* Hotkeys */}
                {hotkeyOptions}
            </scrollingframe>
        </TechWindow>
    );
}


import React from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import SidebarButtons from "client/components/sidebar/SidebarButtons";
import useVisibility from "client/hooks/useVisibility";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            visible: true,
        },
    },
    (props) => {
        useVisibility("Sidebar", props.controls.visible);
        return <SidebarButtons />;
    },
);