import React, { Fragment, JSX } from "@rbxts/react";
import PrinterGui from "client/components/item/printer/PrinterGui";
import useTaggedItemModels from "client/components/world/useTaggedItemModels";

export default function PrinterRenderer() {
    const models = useTaggedItemModels("Printer");

    const printerGuis = new Array<JSX.Element>();
    for (const [model, item] of models) {
        const printer = item.findTrait("Printer");
        if (!printer) return;

        const adornee = model.FindFirstChild("GuiPart");
        if (!adornee) return;

        const areaId = printer.area;
        if (areaId === undefined) return;

        printerGuis.push(<PrinterGui adornee={adornee as Part} areaId={areaId} />);
    }

    return <Fragment>{printerGuis}</Fragment>;
}


import React from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import SetupOption from "client/components/item/printer/SetupOption";

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


import React, { createRef } from "@rbxts/react";
import { TweenService } from "@rbxts/services";
import ActionButton from "client/components/item/printer/ActionButton";
import ToggleSwitch from "client/components/item/printer/ToggleSwitch";
import { RobotoSlabBold } from "shared/asset/GameFonts";
import { getAsset } from "shared/asset/AssetMap";
import { playSound } from "shared/asset/GameAssets";
import Packets from "shared/Packets";

export default function SetupOption({
    placementId = "Printer1",
    setupName = "Setup 1",
    cost = "Cost: $1Qd, 100 W, 140 Purifier Clicks",
}: {
    placementId?: string;
    setupName?: string;
    cost?: string;
}) {
    const textBoxRef = createRef<TextBox>();
    const onSave = () => {
        playSound("MagicSprinkle.mp3", undefined, (sound) => {
            sound.PlaybackSpeed = 1.15;
        });
        const text = textBoxRef.current?.Text;
        if (!text || text.size() === 0) return;
        Packets.saveSetup.toServer(placementId, text);
    };

    const onLoad = () => {
        playSound("MagicSprinkle.mp3", undefined, (sound) => {
            sound.PlaybackSpeed = 0.95;
            const reverb = new Instance("ReverbSoundEffect");
            reverb.Parent = sound;
        });
        Packets.loadSetup.toServer(placementId, setupName);
    };

    return (
        <frame
            BackgroundColor3={Color3.fromRGB(0, 0, 0)}
            BackgroundTransparency={0.8}
            BorderSizePixel={0}
            Size={new UDim2(1, 0, 0, 125)}
        >
            <uilistlayout
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                SortOrder={Enum.SortOrder.LayoutOrder}
                VerticalAlignment={Enum.VerticalAlignment.Center}
            />
            <frame BackgroundTransparency={1} Size={new UDim2(1, -60, 0.33, 0)}>
                <imagebutton
                    BackgroundTransparency={1}
                    Image={getAsset("assets/PrinterNameEdit.png")}
                    ImageTransparency={0.1}
                    LayoutOrder={2}
                    Size={new UDim2(0.6, 0, 0.6, 0)}
                    Event={{
                        Activated: () => {
                            textBoxRef.current?.CaptureFocus();
                        },
                        MouseEnter: (rbx) => {
                            const tween = TweenService.Create(
                                rbx,
                                new TweenInfo(0.2, Enum.EasingStyle.Bounce, Enum.EasingDirection.Out),
                                { Size: new UDim2(0.65, 0, 0.65, 0) },
                            );
                            tween.Play();
                        },
                        MouseLeave: (rbx) => {
                            const tween = TweenService.Create(
                                rbx,
                                new TweenInfo(0.15, Enum.EasingStyle.Back, Enum.EasingDirection.Out),
                                { Size: new UDim2(0.6, 0, 0.6, 0) },
                            );
                            tween.Play();
                        },
                        MouseButton1Down: (rbx) => {
                            const tween = TweenService.Create(
                                rbx,
                                new TweenInfo(0.1, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut),
                                { Size: new UDim2(0.55, 0, 0.55, 0) },
                            );
                            tween.Play();
                        },
                        MouseButton1Up: (rbx) => {
                            const tween = TweenService.Create(
                                rbx,
                                new TweenInfo(0.2, Enum.EasingStyle.Bounce, Enum.EasingDirection.Out),
                                { Size: new UDim2(0.65, 0, 0.65, 0) },
                            );
                            tween.Play();
                        },
                    }}
                >
                    <uiaspectratioconstraint />
                </imagebutton>
                <uilistlayout
                    FillDirection={Enum.FillDirection.Horizontal}
                    HorizontalFlex={Enum.UIFlexAlignment.Fill}
                    Padding={new UDim(0, 15)}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                    VerticalAlignment={Enum.VerticalAlignment.Center}
                />
                <textbox
                    ref={textBoxRef}
                    Active={false}
                    AutomaticSize={Enum.AutomaticSize.X}
                    BackgroundTransparency={1}
                    ClearTextOnFocus={false}
                    FontFace={RobotoSlabBold}
                    LayoutOrder={-5}
                    Selectable={false}
                    Size={new UDim2(0, 0, 0.8, 0)}
                    Text={setupName}
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextEditable={false}
                    TextScaled={true}
                    TextSize={14}
                    TextWrapped={true}
                    Event={{
                        FocusLost: (rbx, enterPressed) => {
                            if (!enterPressed) return;
                            Packets.renameSetup.toServer(setupName, rbx.Text);
                        },
                    }}
                >
                    <uistroke Thickness={2} />
                    <frame
                        AnchorPoint={new Vector2(0, 1)}
                        BackgroundColor3={Color3.fromRGB(255, 255, 255)}
                        BackgroundTransparency={0.8}
                        BorderSizePixel={0}
                        Position={new UDim2(0, 0, 1, 0)}
                        Size={new UDim2(1, 6, 0, 2)}
                    />
                </textbox>
                <textlabel
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabBold}
                    LayoutOrder={5}
                    Size={new UDim2(0, 0, 0.8, 0)}
                    Text={cost}
                    TextColor3={Color3.fromRGB(170, 255, 127)}
                    TextScaled={true}
                    TextSize={14}
                    TextWrapped={true}
                    TextXAlignment={Enum.TextXAlignment.Right}
                >
                    <uistroke Thickness={2} />
                    <uitextsizeconstraint MaxTextSize={22} />
                </textlabel>
            </frame>
            <frame BackgroundTransparency={1} Size={new UDim2(1, 0, 0.66, 0)}>
                <ActionButton text="Save" backgroundColor={Color3.fromRGB(170, 255, 127)} onClick={onSave} />
                <ActionButton
                    text="Load"
                    backgroundColor={Color3.fromRGB(255, 170, 0)}
                    layoutOrder={1}
                    onClick={onLoad}
                />
                <uilistlayout
                    FillDirection={Enum.FillDirection.Horizontal}
                    HorizontalAlignment={Enum.HorizontalAlignment.Center}
                    Padding={new UDim(0.025, 0)}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                    VerticalAlignment={Enum.VerticalAlignment.Center}
                />
                <ToggleSwitch label="Alert when affordable" setupName={setupName} />
            </frame>
        </frame>
    );
}


import React, { createRef } from "@rbxts/react";
import { TweenService } from "@rbxts/services";
import ActionButton from "client/components/item/printer/ActionButton";
import { RobotoSlabBold } from "shared/asset/GameFonts";
import { getAsset } from "shared/asset/AssetMap";
import { playSound } from "shared/asset/GameAssets";
import Packets from "shared/Packets";

export default function TemplateSetup({
    placementId = "Printer1",
    defaultSetupName = "Setup 1",
}: {
    placementId?: string;
    defaultSetupName?: string;
}) {
    const textBoxRef = createRef<TextBox>();

    const onSave = () => {
        playSound("MagicSprinkle.mp3", undefined, (sound) => {
            sound.PlaybackSpeed = 1.15;
        });
        const text = textBoxRef.current?.Text;
        if (!text || text.size() === 0) return;
        Packets.saveSetup.toServer(placementId, text);
    };

    return (
        <frame
            BackgroundColor3={Color3.fromRGB(0, 0, 0)}
            BackgroundTransparency={0.8}
            BorderSizePixel={0}
            Size={new UDim2(1, 0, 0, 125)}
            LayoutOrder={95211925} // High layout order to appear at the bottom
        >
            <uilistlayout
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                SortOrder={Enum.SortOrder.LayoutOrder}
                VerticalAlignment={Enum.VerticalAlignment.Center}
            />
            {/* Header section with name input */}
            <frame BackgroundTransparency={1} Size={new UDim2(1, -60, 0.33, 0)}>
                <imagebutton
                    BackgroundTransparency={1}
                    Image={getAsset("assets/PrinterNameEdit.png")}
                    ImageTransparency={0.1}
                    LayoutOrder={2}
                    Size={new UDim2(0.6, 0, 0.6, 0)}
                    Visible={false} // Hidden for template as name is always editable
                >
                    <uiaspectratioconstraint />
                </imagebutton>
                <uilistlayout
                    FillDirection={Enum.FillDirection.Horizontal}
                    HorizontalFlex={Enum.UIFlexAlignment.Fill}
                    Padding={new UDim(0, 15)}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                    VerticalAlignment={Enum.VerticalAlignment.Center}
                />
                <textbox
                    ref={textBoxRef}
                    Active={false}
                    AutomaticSize={Enum.AutomaticSize.X}
                    BackgroundTransparency={1}
                    ClearTextOnFocus={false}
                    FontFace={RobotoSlabBold}
                    LayoutOrder={-5}
                    Selectable={false}
                    Size={new UDim2(0, 0, 0.8, 0)}
                    Text={defaultSetupName}
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextEditable={true} // Always editable for template
                    TextScaled={true}
                    TextSize={14}
                    TextWrapped={true}
                    Event={{
                        FocusLost: (rbx, enterPressed) => {
                            if (!enterPressed) return;
                            // For template, save when enter is pressed
                            onSave();
                        },
                    }}
                >
                    <uistroke Thickness={2} />
                    {/* Always show underline for template */}
                    <frame
                        AnchorPoint={new Vector2(0, 1)}
                        BackgroundColor3={Color3.fromRGB(255, 255, 255)}
                        BackgroundTransparency={0.8}
                        BorderSizePixel={0}
                        Position={new UDim2(0, 0, 1, 0)}
                        Size={new UDim2(1, 6, 0, 2)}
                    />
                </textbox>
                {/* No cost label for template */}
            </frame>
            {/* Body section with only save button */}
            <frame BackgroundTransparency={1} Size={new UDim2(1, 0, 0.66, 0)}>
                <ActionButton text="Save" backgroundColor={Color3.fromRGB(170, 255, 127)} onClick={onSave} />
                <uilistlayout
                    FillDirection={Enum.FillDirection.Horizontal}
                    HorizontalAlignment={Enum.HorizontalAlignment.Center}
                    Padding={new UDim(0.025, 0)}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                    VerticalAlignment={Enum.VerticalAlignment.Center}
                />
                {/* No Load button or Toggle switch for template */}
            </frame>
        </frame>
    );
}


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

export default function ToggleSwitch({
    setupName,
    label,
    initialEnabled = false,
    onToggle,
}: {
    setupName: string;
    label: string;
    initialEnabled?: boolean;
    onToggle?: (newState: boolean) => void;
}) {
    const [isEnabled, setIsEnabled] = useState(initialEnabled);
    const toggleRef = useRef<Frame>();
    const buttonRef = useRef<TextButton>();

    const handleToggle = () => {
        const newState = Packets.autoloadSetup.toServer(setupName);
        if (newState) {
            playSound("CheckOn.mp3");
        } else {
            playSound("CheckOff.mp3");
        }
        setIsEnabled(newState);

        // Animate the toggle
        if (toggleRef.current && buttonRef.current) {
            const targetPosition = newState ? new UDim2(1, -4, 0.5, 0) : new UDim2(0, 4, 0.5, 0);
            const targetAnchor = newState ? new Vector2(1, 0.5) : new Vector2(0, 0.5);
            const targetColor = newState ? Color3.fromRGB(127, 255, 127) : Color3.fromRGB(255, 79, 79);

            const tweenInfo = new TweenInfo(0.4, Enum.EasingStyle.Quart, Enum.EasingDirection.Out);

            // Tween position and color
            TweenService.Create(toggleRef.current, tweenInfo, {
                Position: targetPosition,
                AnchorPoint: targetAnchor,
                BackgroundColor3: targetColor,
            }).Play();
        }

        if (onToggle) {
            onToggle(newState);
        }
    };

    const toggleColor = isEnabled ? Color3.fromRGB(127, 255, 127) : Color3.fromRGB(255, 79, 79);
    const togglePosition = isEnabled ? new UDim2(1, -4, 0.5, 0) : new UDim2(0, 4, 0.5, 0);
    const toggleAnchor = isEnabled ? new Vector2(1, 0.5) : new Vector2(0, 0.5);

    return (
        <frame BackgroundTransparency={1} LayoutOrder={-5} Size={new UDim2(0.4, 0, 1, 0)}>
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoSlab}
                Size={new UDim2(1, 0, 0.35, 0)}
                Text={label}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextSize={14}
                TextWrapped={true}
            >
                <uistroke Thickness={2} />
            </textlabel>
            <uilistlayout
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                SortOrder={Enum.SortOrder.LayoutOrder}
                VerticalAlignment={Enum.VerticalAlignment.Center}
            />
            <textbutton
                ref={buttonRef}
                BackgroundColor3={toggleColor.Lerp(Color3.fromRGB(0, 0, 0), 0.8)}
                BorderSizePixel={0}
                Font={Enum.Font.SourceSans}
                LayoutOrder={5}
                Size={new UDim2(0.4, 0, 0.25, 0)}
                Text={""}
                TextColor3={Color3.fromRGB(0, 0, 0)}
                TextSize={14}
                Event={{
                    Activated: handleToggle,
                }}
            >
                <uistroke ApplyStrokeMode={Enum.ApplyStrokeMode.Border} Thickness={2} />
                <frame
                    ref={toggleRef}
                    AnchorPoint={toggleAnchor}
                    BackgroundColor3={toggleColor}
                    BorderSizePixel={0}
                    Position={togglePosition}
                    Size={new UDim2(0.3, 0, 0.8, -2)}
                >
                    <uistroke ApplyStrokeMode={Enum.ApplyStrokeMode.Border} Thickness={2} />
                </frame>
            </textbutton>
        </frame>
    );
}


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

export default function ItemWindow({
    icon,
    id,
    visible,
    children,
    size = new UDim2(0.45, 200, 0.4, 100),
    backgroundColor,
}: {
    icon: string;
    id: string;
    title?: string;
    visible: boolean;
    backgroundColor: Color3;
    children: ReactNode;
    size?: UDim2;
}) {
    const wrapperRef = useRef<ImageLabel>();
    const contentRef = useRef<Frame>();
    const titleRef = useRef<Frame>();
    const iconRef = useRef<ImageLabel>();
    const textRef = useRef<TextLabel>();
    const closeButtonRef = useRef<TextButton>();
    const initialPosition = new UDim2(0.5, 0, 1, -40);

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

    return (
        <imagelabel
            ref={wrapperRef}
            Active={true}
            AnchorPoint={new Vector2(0.5, 1)}
            BackgroundColor3={backgroundColor}
            BackgroundTransparency={0.2}
            BorderColor3={Color3.fromRGB(0, 0, 0)}
            BorderSizePixel={3}
            Image={getAsset("assets/Grid.png")}
            ImageColor3={Color3.fromRGB(126, 126, 126)}
            ImageTransparency={0.6}
            ScaleType={Enum.ScaleType.Tile}
            Selectable={true}
            Size={size}
            Position={initialPosition}
            TileSize={new UDim2(0, 100, 0, 100)}
            Visible={false}
            ZIndex={0}
        >
            <uistroke
                ApplyStrokeMode={Enum.ApplyStrokeMode.Border}
                Color={backgroundColor}
                Thickness={2}
                Transparency={0.2}
            >
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(236, 236, 236)),
                            new ColorSequenceKeypoint(0.299, Color3.fromRGB(255, 255, 255)),
                            new ColorSequenceKeypoint(0.51, Color3.fromRGB(118, 118, 118)),
                            new ColorSequenceKeypoint(0.822, Color3.fromRGB(255, 255, 255)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(220, 220, 220)),
                        ])
                    }
                    Rotation={35}
                />
            </uistroke>
            <uigradient
                Color={
                    new ColorSequence([
                        new ColorSequenceKeypoint(0, Color3.fromRGB(39, 39, 39)),
                        new ColorSequenceKeypoint(1, Color3.fromRGB(58, 58, 58)),
                    ])
                }
                Rotation={270}
            />

            <uistroke ApplyStrokeMode={Enum.ApplyStrokeMode.Border} Color={Color3.fromRGB(255, 255, 255)} />
            <uisizeconstraint MaxSize={new Vector2(800, 600)} />
            <WindowTitle iconRef={iconRef} textRef={textRef} ref={titleRef} icon={icon} title={id} />
            <WindowCloseButton ref={closeButtonRef} onClick={() => DocumentManager.setVisible(id, false)} />
            <frame
                ref={contentRef}
                key="MainWindow"
                AnchorPoint={new Vector2(0.5, 0)}
                BackgroundTransparency={1}
                Position={new UDim2(0.5, 0, 0.075, 0)}
                Size={new UDim2(1, -30, 0.925, -15)}
            >
                {children}
            </frame>
        </imagelabel>
    );
}


import { OnoeNum } from "@rbxts/serikanum";
import React, { useMemo, useRef } from "@rbxts/react";
import { TextService } from "@rbxts/services";
import displayBalanceCurrency from "client/components/balance/displayBalanceCurrency";
import { useItemViewport } from "shared/item/ItemViewport";
import { RobotoSlabHeavy } from "shared/asset/GameFonts";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import { CURRENCY_DETAILS } from "shared/currency/CurrencyDetails";
import Item from "shared/item/Item";
import Items from "shared/items/Items";

type RowItemInfo = {
    text: string;
    currency?: Currency;
    item?: Item;
    width: number;
    height: number;
    position?: UDim2;
    size?: UDim2;
};

const OPTION_BETWEEN_PADDING = 4;
const OPTION_X_PADDING = 10;
const OPTION_Y_PADDING = 1;

const GET_TEXT_BOUNDS_PARAMS = new Instance("GetTextBoundsParams");
GET_TEXT_BOUNDS_PARAMS.Font = RobotoSlabHeavy;

function PriceOption({
    currency,
    item,
    text,
    affordable,
    position,
    width,
    height = 25,
}: {
    /** Currency amount and type */
    currency?: Currency;
    /** Item amount and type */
    item?: Item;
    /** Text to display */
    text: string;
    /** Whether the price is affordable */
    affordable: boolean;
    /** Position of the price option */
    position?: UDim2;
    /** Width of the price option */
    width: number;
    /** Height of the price option */
    height?: number;
}) {
    const viewportRef = useRef<ViewportFrame>();
    const textColor = affordable ? Color3.fromRGB(255, 255, 255) : Color3.fromRGB(255, 80, 80);
    if (item) {
        useItemViewport(viewportRef, item.id);
    }

    return (
        <frame BackgroundTransparency={1} Position={position} Size={new UDim2(0, width, 0, height)}>
            <uilistlayout
                FillDirection={Enum.FillDirection.Horizontal}
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                Padding={new UDim(0, OPTION_BETWEEN_PADDING)}
                SortOrder={Enum.SortOrder.LayoutOrder}
                VerticalAlignment={Enum.VerticalAlignment.Center}
            />
            <uipadding
                PaddingBottom={new UDim(0, OPTION_Y_PADDING)}
                PaddingTop={new UDim(0, OPTION_Y_PADDING)}
                PaddingLeft={new UDim(0, OPTION_X_PADDING)}
                PaddingRight={new UDim(0, OPTION_X_PADDING)}
            />

            {currency && (
                <imagelabel
                    BackgroundTransparency={1}
                    Image={CURRENCY_DETAILS[currency].image}
                    ScaleType={Enum.ScaleType.Fit}
                    Size={new UDim2(1, 0, 1, 0)}
                    SizeConstraint={Enum.SizeConstraint.RelativeYY}
                />
            )}

            {item && (
                <viewportframe
                    ref={viewportRef}
                    BackgroundTransparency={1}
                    Size={new UDim2(1, 0, 1, 0)}
                    SizeConstraint={Enum.SizeConstraint.RelativeYY}
                />
            )}

            <textlabel
                AutomaticSize={Enum.AutomaticSize.X}
                BackgroundTransparency={1}
                FontFace={RobotoSlabHeavy}
                Size={new UDim2(0, 0, 1, 0)}
                Text={text}
                TextColor3={textColor}
                TextScaled={true}
                TextXAlignment={Enum.TextXAlignment.Left}
            >
                <uistroke Thickness={2} />
            </textlabel>
        </frame>
    );
}

export function WrappingPriceOptions({
    priceOptionHeight = 25,
    padding = 5,
    price,
    requiredItems,
    affordablePerCurrency,
    affordablePerItemId,
}: {
    priceOptionHeight?: number;
    padding?: number;
    price: CurrencyBundle;
    requiredItems: Map<string, number>;
    affordablePerCurrency: Map<Currency, boolean>;
    affordablePerItemId: Map<string, boolean>;
}) {
    const ref = useRef<Frame>();
    const containerXPixels = ref.current?.AbsoluteSize.X;

    // Create a unique key based on price and required items to force re-render when they change
    const priceKey = useMemo(() => {
        const currencyKey = price.amountPerCurrency.isEmpty()
            ? ""
            : [...price.amountPerCurrency].map(([c, a]) => `${c}:${a.toString()}`).join(",");
        const itemKey = requiredItems.size() === 0 ? "" : [...requiredItems].map(([i, a]) => `${i}:${a}`).join(",");
        return `${currencyKey}|${itemKey}`;
    }, [price, requiredItems]);

    if (containerXPixels === undefined)
        return <frame key={priceKey} ref={ref} BackgroundTransparency={1} Size={new UDim2(1, 0, 0, 0)} />;

    const getText = (currency: Currency | undefined, item: Item | undefined, amount: OnoeNum | number) => {
        return currency ? displayBalanceCurrency(currency, amount as OnoeNum) : `${amount} ${item?.name}`;
    };

    const sortedInfos = new Array<{ text: string; currency?: Currency; item?: Item }>();
    for (const [currency] of CurrencyBundle.SORTED_DETAILS) {
        const amount = price.amountPerCurrency.get(currency);
        if (amount === undefined) continue;
        sortedInfos.push({ text: getText(currency, undefined, amount), currency });
    }
    for (const requiredItem of Items.sortedItems) {
        const amount = requiredItems.get(requiredItem.id);
        if (amount === undefined) continue;
        sortedInfos.push({ text: getText(undefined, requiredItem, amount), item: requiredItem });
    }

    // Account for the button's left and right padding
    const availableWidth = containerXPixels - padding * 2;

    // Early return if no price options are available yet
    if (sortedInfos.size() === 0) return;

    // First pass: organize options into rows
    const rows: Array<Array<RowItemInfo>> = [];
    let currentRow: Array<RowItemInfo> = [];
    let currentRowWidth = 0;

    GET_TEXT_BOUNDS_PARAMS.Size = priceOptionHeight;
    GET_TEXT_BOUNDS_PARAMS.Width = 1000; // Arbitrary large width to avoid wrapping

    for (const info of sortedInfos) {
        GET_TEXT_BOUNDS_PARAMS.Text = info.text;
        const textSize = TextService.GetTextBoundsAsync(GET_TEXT_BOUNDS_PARAMS);
        const sizeX = priceOptionHeight + OPTION_BETWEEN_PADDING + textSize.X + OPTION_X_PADDING * 2;
        const sizeY = textSize.Y + OPTION_Y_PADDING * 2;

        // Skip instances that haven't been sized yet
        if (sizeX === 0 || sizeY === 0) continue;

        // Check if this option fits in the current row
        const neededWidth = currentRowWidth + sizeX + (currentRow.size() > 0 ? padding : 0);
        if (neededWidth > availableWidth && currentRow.size()) {
            // Start a new row
            rows.push(currentRow);
            currentRow = [{ ...info, width: sizeX, height: sizeY }];
            currentRowWidth = sizeX;
        } else {
            // Add to current row
            currentRow.push({ ...info, width: sizeX, height: sizeY });
            currentRowWidth = neededWidth;
        }
    }
    if (currentRow.size() > 0) {
        rows.push(currentRow);
    }

    // Second pass: position options with distributed widths
    let currentY = 0;
    const infos = new Array<RowItemInfo>();
    for (const row of rows) {
        if (row.size() === 0) continue;

        // Calculate total original width used by this row
        const totalOriginalWidth = row.reduce((sum, item) => sum + item.width, 0);
        const totalPadding = (row.size() - 1) * padding;
        const usedWidth = totalOriginalWidth + totalPadding;
        const excessWidth = availableWidth - usedWidth + padding * 2;
        const widthPerOption = excessWidth / row.size();

        // Position and resize options in this row
        let currentX = 0;
        for (const info of row) {
            const newWidth = info.width + widthPerOption;
            info.position = new UDim2(0, currentX, 0, currentY);
            info.size = new UDim2(0, newWidth, 0, info.height);
            currentX += newWidth + padding;
            infos.push(info);
        }

        currentY += row[0].height + padding;
    }
    const totalPriceOptions = infos.size();

    return (
        <frame
            key={priceKey}
            ref={ref}
            BackgroundTransparency={1}
            Size={new UDim2(1, 0, 0, rows.size() * priceOptionHeight)}
        >
            {infos.map((info) => {
                const affordable = info.currency
                    ? (affordablePerCurrency.get(info.currency) ?? false)
                    : info.item
                      ? (affordablePerItemId.get(info.item.id) ?? false)
                      : false;

                return (
                    <frame
                        BackgroundColor3={new Color3()}
                        BackgroundTransparency={totalPriceOptions > 1 ? 0.8 : 1}
                        Position={info.position}
                        Size={info.size}
                    >
                        <uicorner CornerRadius={new UDim(0, 5)} />
                        <uilistlayout HorizontalAlignment={Enum.HorizontalAlignment.Center} />
                        <PriceOption
                            affordable={affordable}
                            currency={info.currency}
                            item={info.item}
                            text={info.text}
                            width={info.width}
                        />
                    </frame>
                );
            })}
        </frame>
    );
}


import React, { StrictMode, useEffect } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { Choose, CreateReactStory } from "@rbxts/ui-labs";
import PurchaseWindow, { PurchaseManager } from "client/components/item/shop/PurchaseWindow";
import StoryMocking from "client/components/StoryMocking";
import TooltipWindow from "client/components/tooltip/TooltipWindow";
import { useSingleDocumentVisibility } from "client/hooks/useVisibility";
import Items from "shared/items/Items";
import TheFirstDropper from "shared/items/negative/tfd/TheFirstDropper";
import Packets from "shared/Packets";

const options = new Array<string>();
for (const item of Items.sortedItems) options.push(item.id);

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

        if (props.controls.broke) {
            Packets.balance.set(new Map());
            Packets.inventory.set(new Map());
        }

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

        const item = Items.getItem(props.controls.item) ?? TheFirstDropper;
        useEffect(() => {
            PurchaseManager.select(item);
        }, [item]);

        return (
            <StrictMode>
                <TooltipWindow />
                <PurchaseWindow />
            </StrictMode>
        );
    },
);


import Signal from "@antivivi/lemon-signal";
import { OnoeNum } from "@rbxts/serikanum";
import React, { Fragment, useEffect, useMemo, useRef, useState } from "@rbxts/react";
import { Debris, TweenService } from "@rbxts/services";
import StringBuilder from "@rbxts/stringbuilder";
import { Environment } from "@rbxts/ui-labs";
import { useHotkey } from "client/components/hotkeys/HotkeyManager";
import {
    createInventorySlot,
    updateInventorySlot,
    type InventorySlotHandle,
} from "client/components/item/inventory/InventorySlot";
import ItemViewport from "shared/item/ItemViewport";
import ItemWindow from "client/components/item/shop/ItemWindow";
import { WrappingPriceOptions } from "client/components/item/shop/PriceOption";
import useSingleDocument from "client/components/sidebar/useSingleDocumentWindow";
import { showErrorToast } from "client/components/toast/ToastService";
import getDifficultyDisplayColors from "client/components/tooltip/getDifficultyDisplayColors";
import { METADATA_PER_ITEM, TooltipManager } from "client/components/tooltip/TooltipWindow";
import useInterval from "client/hooks/useInterval";
import { getAsset } from "shared/asset/AssetMap";
import { playSound } from "shared/asset/GameAssets";
import { RobotoMono, RobotoSlab, RobotoSlabHeavy, RobotoSlabMedium } from "shared/asset/GameFonts";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import { CURRENCY_DETAILS } from "shared/currency/CurrencyDetails";
import Item from "shared/item/Item";
import Items from "shared/items/Items";
import TheFirstDropper from "shared/items/negative/tfd/TheFirstDropper";
import Packets from "shared/Packets";

export class PurchaseManager {
    static readonly itemSelected = new Signal<Item>();

    static select(item: Item) {
        this.itemSelected.fire(item);
    }
}

/**
 * Purchase window component for buying shop items
 */
export default function PurchaseWindow() {
    const windowWrapperRef = useRef<Frame>();
    const paddingRef = useRef<UIPadding>();
    const itemSlotRef = useRef<TextButton>();
    const itemSlotContainerRef = useRef<Frame>();
    const itemSlotHandleRef = useRef<InventorySlotHandle>();
    const { id, visible, openDocument } = useSingleDocument({ id: "Purchase" });
    const [{ bought, price }, setBoughtData] = useState({ bought: 0, price: new CurrencyBundle() });
    const [unaffordableLabel, setUnaffordableLabel] = useState("UNAFFORDABLE");
    const [affordablePerCurrency, setAffordablePerCurrency] = useState(new Map<Currency, boolean>());
    const [affordablePerItemId, setAffordablePerItemId] = useState(new Map<string, boolean>());
    const [item, setItem] = useState<Item>(TheFirstDropper);
    const metadata = METADATA_PER_ITEM.get(item);

    useEffect(() => {
        const connection = PurchaseManager.itemSelected.connect((newItem) => {
            setItem(newItem);
            openDocument();
        });

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

    // Hidden cycle for unaffordable label changes
    useInterval(() => {
        if (visible) return 5;

        const options = ["UNAFFORDABLE", "YOU ARE BROKE", "GET MORE STUFF", "WORK HARDER", "NOPE", "YOU WISH"];
        setUnaffordableLabel(options[math.floor(math.random(0, options.size() - 1))]);
        return 5;
    }, [visible]);

    useEffect(() => {
        const boughtConnection = Packets.bought.observe((boughtPerItem) => {
            const bought = boughtPerItem.get(item.id) ?? 0;
            setBoughtData({
                bought,
                price: item.getPrice(bought + 1) ?? new CurrencyBundle(),
            });
        });
        return () => boughtConnection.disconnect();
    }, [item]);

    useEffect(() => {
        const balanceConnection = Packets.balance.observe((balance) => {
            if (!price) return;

            const affordablePerCurrency = new Map<Currency, boolean>();
            for (const [currency, amount] of price.amountPerCurrency) {
                const inBalance = balance.get(currency) ?? new OnoeNum(0);
                affordablePerCurrency.set(currency, amount.lessEquals(inBalance));
            }
            setAffordablePerCurrency(affordablePerCurrency);
        });

        const updateItemAffordability = () => {
            if (!price) return;

            const inventory = Packets.inventory.get() ?? new Map<string, number>();
            const researching = Packets.researching.get() ?? new Map<string, number>();
            const affordablePerItemId = new Map<string, boolean>();
            for (const [requiredItemId, amount] of item.requiredItems) {
                const available = math.max(
                    (inventory.get(requiredItemId) ?? 0) - (researching.get(requiredItemId) ?? 0),
                    0,
                );
                affordablePerItemId.set(requiredItemId, available >= amount);
            }
            setAffordablePerItemId(affordablePerItemId);
        };

        const inventoryConnection = Packets.inventory.observe(updateItemAffordability);
        const researchingConnection = Packets.researching.observe(updateItemAffordability);
        updateItemAffordability();

        return () => {
            balanceConnection.disconnect();
            inventoryConnection.disconnect();
            researchingConnection.disconnect();
        };
    }, [item, price]);

    useEffect(() => {
        return () => {
            const handle = itemSlotHandleRef.current;
            if (handle) {
                handle.destroy();
                itemSlotHandleRef.current = undefined;
            }
            itemSlotRef.current = undefined;
        };
    }, []);

    useEffect(() => {
        const parent = itemSlotContainerRef.current;
        if (!parent) return;

        let handle = itemSlotHandleRef.current;
        if (handle && handle.item !== item) {
            if (itemSlotRef.current === handle.button) {
                itemSlotRef.current = undefined;
            }
            handle.destroy();
            handle = undefined;
            itemSlotHandleRef.current = undefined;
        }

        if (!handle) {
            handle = createInventorySlot(item, {
                parent,
                size: new UDim2(0, 0, 1, 0),
                layoutOrder: -1,
                visible: true,
                tooltip: false,
                onActivated: () => {},
            });
            itemSlotHandleRef.current = handle;
        }

        itemSlotRef.current = handle.button;

        updateInventorySlot(handle, {
            size: new UDim2(0, 0, 1, 0),
            layoutOrder: -1,
            visible: true,
            amount: bought,
        });
    }, [item, bought]);

    const itemSlot = itemSlotRef.current;
    const purchase = () => {
        if (!visible) return false;

        if (Packets.buyItem.toServer(item.id)) {
            playSound("ItemPurchase.mp3");
            if (!itemSlot) return true;

            // Item flying into item slot animation
            const mousePosition = Environment.UserInput.GetMouseLocation();
            let frame: GuiObject;
            if (item.image) {
                const imageLabel = new Instance("ImageLabel");
                imageLabel.AnchorPoint = new Vector2(0.5, 0.5);
                imageLabel.BackgroundTransparency = 1;
                imageLabel.Image = item.image;
                imageLabel.Size = new UDim2(0, 60, 0, 60);
                frame = imageLabel;
            } else {
                const viewportFrame = new Instance("ViewportFrame");
                viewportFrame.AnchorPoint = new Vector2(0.5, 0.5);
                viewportFrame.BackgroundTransparency = 1;
                viewportFrame.Size = new UDim2(0, 60, 0, 60);
                ItemViewport.loadItemIntoViewport(viewportFrame, item.id);
                frame = viewportFrame;
            }
            frame.Position = new UDim2(0, mousePosition.X, 0, mousePosition.Y);

            const destination = itemSlot.AbsolutePosition.add(itemSlot.AbsoluteSize.div(2));
            TweenService.Create(frame, new TweenInfo(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.In), {
                Position: new UDim2(0, destination.X, 0, destination.Y),
                Size: new UDim2(0, 0, 0, 0),
            }).Play();
            frame.Parent = windowWrapperRef.current;
            Debris.AddItem(frame, 0.5);

            // Padding animation
            const padding = paddingRef.current;
            if (!padding) return true;
            padding.PaddingLeft = new UDim(0, containerPadding * 1.5);
            padding.PaddingRight = new UDim(0, containerPadding * 1.5);
            padding.PaddingTop = new UDim(0, containerPadding * 2);
            padding.PaddingBottom = new UDim(0, containerPadding * 2);
            TweenService.Create(padding, new TweenInfo(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {
                PaddingLeft: new UDim(0, containerPadding),
                PaddingRight: new UDim(0, containerPadding),
                PaddingTop: new UDim(0, containerPadding),
                PaddingBottom: new UDim(0, containerPadding),
            }).Play();
        } else {
            playSound("Error.mp3");
            showErrorToast("Purchase failed. Check your resources.");
        }
        return true;
    };
    useHotkey({
        action: purchase,
        label: "Purchase",
    });

    const description = metadata?.formatItemDescription(undefined, false, Color3.fromRGB(255, 255, 255), 20);

    const { background: backgroundColor, text: textColor } = useMemo(() => {
        return getDifficultyDisplayColors(item.difficulty);
    }, [item]);

    const containerPadding = 5;
    const requiredItems = item.requiredItems;

    const totalAffordable = useMemo(() => {
        let totalAffordable = true;
        for (const [, affordable] of affordablePerCurrency) {
            if (!affordable) {
                totalAffordable = false;
                break;
            }
        }
        for (const [, affordable] of affordablePerItemId) {
            if (!affordable) {
                totalAffordable = false;
                break;
            }
        }
        return totalAffordable;
    }, [affordablePerCurrency, affordablePerItemId]);

    const window = (
        <ItemWindow icon={getAsset("assets/Purchase.png")} id={id} backgroundColor={backgroundColor} visible={visible}>
            {/* Main layout */}
            <uilistlayout
                FillDirection={Enum.FillDirection.Vertical}
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                Padding={new UDim(0, 10)}
                SortOrder={Enum.SortOrder.LayoutOrder}
            />

            {/* Item slot display */}
            <frame
                ref={itemSlotContainerRef}
                BackgroundTransparency={1}
                LayoutOrder={1}
                Size={new UDim2(0.9, 0, 0.075, 30)}
            >
                <uilistlayout
                    FillDirection={Enum.FillDirection.Horizontal}
                    HorizontalAlignment={Enum.HorizontalAlignment.Left}
                    Padding={new UDim(0, 15)}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                    VerticalAlignment={Enum.VerticalAlignment.Center}
                />
                {/* Item slot is managed imperatively */}

                <frame AutomaticSize={Enum.AutomaticSize.X} BackgroundTransparency={1} Size={new UDim2(0, 0, 1, 0)}>
                    <textlabel
                        AutomaticSize={Enum.AutomaticSize.X}
                        BackgroundTransparency={1}
                        FontFace={RobotoSlabHeavy}
                        Position={new UDim2(0, 110, 0, 15)}
                        Size={new UDim2(0, 0, 0.57, 0)}
                        Text={item.name}
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextScaled={true}
                        TextSize={14}
                        TextWrapped={true}
                    >
                        <uistroke Thickness={2} />
                    </textlabel>
                    <frame
                        AnchorPoint={new Vector2(0, 1)}
                        AutomaticSize={Enum.AutomaticSize.X}
                        BackgroundTransparency={1}
                        LayoutOrder={1}
                        Position={new UDim2(0, 110, 1, -15)}
                        Size={new UDim2(0, 0, 0.43, 0)}
                    >
                        <uilistlayout
                            FillDirection={Enum.FillDirection.Horizontal}
                            Padding={new UDim(0, 10)}
                            SortOrder={Enum.SortOrder.LayoutOrder}
                            VerticalAlignment={Enum.VerticalAlignment.Center}
                        />
                        <imagelabel
                            AnchorPoint={new Vector2(1, 0.5)}
                            BackgroundColor3={item.difficulty.color ?? Color3.fromRGB(255, 255, 255)}
                            Image={item.difficulty.image}
                            LayoutOrder={-1}
                            Position={new UDim2(1, -4, 0.5, 0)}
                            Size={new UDim2(0, 0, 1, 0)}
                        >
                            <uiaspectratioconstraint
                                AspectType={Enum.AspectType.ScaleWithParentSize}
                                DominantAxis={Enum.DominantAxis.Height}
                            />
                            <uistroke ApplyStrokeMode={Enum.ApplyStrokeMode.Border} Thickness={2} />
                        </imagelabel>
                        <textlabel
                            AutomaticSize={Enum.AutomaticSize.X}
                            BackgroundTransparency={1}
                            FontFace={RobotoSlabMedium}
                            Position={new UDim2(0, 110, 0, 40)}
                            Size={new UDim2(0, 0, 1, 0)}
                            Text={item.difficulty?.name ?? ""}
                            TextColor3={textColor}
                            TextScaled={true}
                            TextSize={14}
                            TextWrapped={true}
                        >
                            <uistroke Thickness={2} />
                            <uigradient
                                Color={
                                    new ColorSequence([
                                        new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                                        new ColorSequenceKeypoint(0.5, Color3.fromRGB(225, 225, 225)),
                                        new ColorSequenceKeypoint(1, Color3.fromRGB(148, 148, 148)),
                                    ])
                                }
                                Rotation={90}
                            />
                        </textlabel>
                    </frame>
                    <uilistlayout
                        SortOrder={Enum.SortOrder.LayoutOrder}
                        VerticalAlignment={Enum.VerticalAlignment.Center}
                    />
                </frame>
            </frame>

            {/* Description frame */}
            <scrollingframe
                Active={true}
                AutomaticCanvasSize={Enum.AutomaticSize.Y}
                BackgroundTransparency={1}
                CanvasSize={new UDim2(1, 0, 0.925, -45)}
                LayoutOrder={2}
                ScrollBarThickness={6}
                ScrollingDirection={Enum.ScrollingDirection.Y}
                Selectable={false}
                Size={new UDim2(1, 0, 0.925, -40)}
            >
                <uilistlayout
                    FillDirection={Enum.FillDirection.Vertical}
                    VerticalFlex={Enum.UIFlexAlignment.Fill}
                    HorizontalAlignment={Enum.HorizontalAlignment.Center}
                    Padding={new UDim(0, 10)}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                />

                {/* Description label */}
                <textlabel
                    AutomaticSize={Enum.AutomaticSize.Y}
                    BackgroundTransparency={1}
                    FontFace={RobotoSlab}
                    RichText={true}
                    Size={new UDim2(1, 0, 0, 0)}
                    Text={description}
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextScaled={false}
                    TextSize={18}
                    TextStrokeColor3={Color3.fromRGB(0, 0, 0)}
                    TextStrokeTransparency={0}
                    TextWrapped={true}
                    TextXAlignment={Enum.TextXAlignment.Left}
                    TextYAlignment={Enum.TextYAlignment.Top}
                >
                    <uiflexitem FlexMode={Enum.UIFlexMode.None} />
                </textlabel>

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

                {/* Creator label */}
                {item.creator ? (
                    <textlabel
                        AutomaticSize={Enum.AutomaticSize.Y}
                        BackgroundTransparency={1}
                        FontFace={RobotoMono}
                        Size={new UDim2(1, 0, 0, 0)}
                        Text={`Creator: ${item.creator}`}
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextScaled={false}
                        TextSize={14}
                        TextTransparency={0.8}
                        TextWrapped={true}
                        TextXAlignment={Enum.TextXAlignment.Left}
                    >
                        <uistroke Color={Color3.fromRGB(0, 0, 0)} Thickness={2} Transparency={0.8} />
                        <uiflexitem FlexMode={Enum.UIFlexMode.None} />
                    </textlabel>
                ) : undefined}

                <frame AutomaticSize={Enum.AutomaticSize.Y} BackgroundTransparency={1} Size={new UDim2(1, 0, 0, 0)}>
                    {/* Purchase container */}
                    {!price.amountPerCurrency.isEmpty() ? (
                        <Fragment>
                            <textbutton
                                AutomaticSize={Enum.AutomaticSize.Y}
                                BackgroundColor3={Color3.fromRGB(85, 255, 127)}
                                BackgroundTransparency={totalAffordable ? 0 : 0.5}
                                BorderColor3={Color3.fromRGB(0, 28, 5)}
                                BorderSizePixel={3}
                                FontFace={RobotoSlabHeavy}
                                Selectable={false}
                                Text={totalAffordable ? "PURCHASE" : unaffordableLabel}
                                TextColor3={new Color3(0, 0, 0)}
                                TextSize={24}
                                TextTransparency={0.9}
                                TextXAlignment={Enum.TextXAlignment.Right}
                                TextYAlignment={Enum.TextYAlignment.Bottom}
                                Size={new UDim2(0.8, 0, 0, 0)}
                                Event={{
                                    MouseMoved: () => {
                                        if (totalAffordable) return;
                                        const balance = Packets.balance.get();
                                        const inventory = Packets.inventory.get();
                                        const researching = Packets.researching.get();
                                        const builder = new StringBuilder("Missing:");
                                        for (const [currency] of CurrencyBundle.SORTED_DETAILS) {
                                            if (affordablePerCurrency.get(currency) ?? true) continue;
                                            builder
                                                .append("<font size='16' color='#")
                                                .append(
                                                    CURRENCY_DETAILS[currency].color
                                                        .Lerp(new Color3(1, 1, 1), 0.7)
                                                        .ToHex(),
                                                )
                                                .append("'>\n- ")
                                                .append(new OnoeNum(balance.get(currency) ?? 0).toString())
                                                .append("/")
                                                .append(price.get(currency)!.toString())
                                                .append(" ")
                                                .append(currency)
                                                .append("</font>");
                                        }
                                        for (const requiredItem of Items.sortedItems) {
                                            if (affordablePerItemId.get(requiredItem.id) ?? true) continue;
                                            const color =
                                                requiredItem.difficulty.color ?? Color3.fromRGB(255, 255, 255);
                                            const available = math.max(
                                                (inventory?.get(requiredItem.id) ?? 0) -
                                                    (researching?.get(requiredItem.id) ?? 0),
                                                0,
                                            );
                                            builder
                                                .append("<font size='16' color='#")
                                                .append(color.Lerp(new Color3(1, 1, 1), 0.7).ToHex())
                                                .append("'>\n- ")
                                                .append(available)
                                                .append("/")
                                                .append(requiredItems.get(requiredItem.id))
                                                .append(" ")
                                                .append(requiredItem.name)
                                                .append("</font>");
                                        }
                                        TooltipManager.showTooltip({ message: builder.toString() });
                                    },
                                    MouseLeave: () => {
                                        TooltipManager.hideTooltip();
                                    },
                                    Activated: purchase,
                                }}
                            >
                                {
                                    <WrappingPriceOptions
                                        price={price}
                                        requiredItems={requiredItems}
                                        affordablePerCurrency={affordablePerCurrency}
                                        affordablePerItemId={affordablePerItemId}
                                    />
                                }
                                <uipadding
                                    ref={paddingRef}
                                    PaddingBottom={new UDim(0, containerPadding)}
                                    PaddingTop={new UDim(0, containerPadding)}
                                    PaddingLeft={new UDim(0, containerPadding)}
                                    PaddingRight={new UDim(0, containerPadding)}
                                />
                                <uistroke
                                    ApplyStrokeMode={Enum.ApplyStrokeMode.Border}
                                    Color={Color3.fromRGB(156, 255, 156)}
                                    Thickness={1}
                                />

                                <uigradient
                                    Color={
                                        new ColorSequence([
                                            new ColorSequenceKeypoint(0, Color3.fromRGB(170, 170, 255)),
                                            new ColorSequenceKeypoint(1, Color3.fromRGB(255, 255, 255)),
                                        ])
                                    }
                                    Rotation={270}
                                />
                            </textbutton>
                        </Fragment>
                    ) : (
                        <Fragment>
                            {/* Not purchasable disclaimer */}
                            <frame
                                AutomaticSize={Enum.AutomaticSize.Y}
                                BackgroundColor3={Color3.fromRGB(255, 80, 80)}
                                BackgroundTransparency={0.2}
                                BorderColor3={Color3.fromRGB(180, 0, 0)}
                                BorderSizePixel={2}
                                Size={new UDim2(0.8, 0, 0, 0)}
                            >
                                <uipadding
                                    PaddingBottom={new UDim(0, 10)}
                                    PaddingTop={new UDim(0, 10)}
                                    PaddingLeft={new UDim(0, 15)}
                                    PaddingRight={new UDim(0, 15)}
                                />
                                <uistroke
                                    ApplyStrokeMode={Enum.ApplyStrokeMode.Border}
                                    Color={Color3.fromRGB(255, 120, 120)}
                                    Thickness={1}
                                />

                                <textlabel
                                    AutomaticSize={Enum.AutomaticSize.Y}
                                    BackgroundTransparency={1}
                                    FontFace={RobotoSlabHeavy}
                                    Size={new UDim2(1, 0, 0, 0)}
                                    Text="NOT PURCHASABLE"
                                    TextColor3={Color3.fromRGB(255, 255, 255)}
                                    TextScaled={false}
                                    TextSize={20}
                                    TextWrapped={true}
                                    TextXAlignment={Enum.TextXAlignment.Center}
                                    TextYAlignment={Enum.TextYAlignment.Center}
                                >
                                    <uistroke Color={Color3.fromRGB(150, 0, 0)} Thickness={2} />
                                </textlabel>

                                <textlabel
                                    AutomaticSize={Enum.AutomaticSize.Y}
                                    BackgroundTransparency={1}
                                    FontFace={RobotoSlab}
                                    Position={new UDim2(0, 0, 0, 20)}
                                    Size={new UDim2(1, 0, 0, 0)}
                                    Text="This item cannot be purchased anymore."
                                    TextColor3={Color3.fromRGB(255, 220, 220)}
                                    TextScaled={false}
                                    TextSize={16}
                                    TextWrapped={true}
                                    TextXAlignment={Enum.TextXAlignment.Center}
                                    TextYAlignment={Enum.TextYAlignment.Top}
                                >
                                    <uistroke Color={Color3.fromRGB(100, 0, 0)} Thickness={1} />
                                </textlabel>
                            </frame>
                        </Fragment>
                    )}
                    <uilistlayout
                        HorizontalAlignment={Enum.HorizontalAlignment.Center}
                        VerticalAlignment={Enum.VerticalAlignment.Bottom}
                    />
                    <uipadding PaddingBottom={new UDim(0, 2)} />
                    <uiflexitem FlexMode={Enum.UIFlexMode.None} />
                </frame>
            </scrollingframe>
        </ItemWindow>
    );

    return (
        <frame ref={windowWrapperRef} BackgroundTransparency={1} Size={new UDim2(1, 0, 1, 0)}>
            {window}
        </frame>
    );
}


import React, { StrictMode, useEffect } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { Workspace } from "@rbxts/services";
import { CreateReactStory, EnumList } from "@rbxts/ui-labs";
import { ChooseOptionType } from "@rbxts/ui-labs/src/ControlTypings/Advanced";
import PurchaseWindow from "client/components/item/shop/PurchaseWindow";
import ShopGui from "client/components/item/shop/ShopGui";
import ShopManager from "client/components/item/shop/ShopManager";
import StoryMocking from "client/components/StoryMocking";
import TooltipWindow from "client/components/tooltip/TooltipWindow";
import Items from "shared/items/Items";
import ClassLowerNegativeShop from "shared/items/negative/ClassLowerNegativeShop";
import Packets from "shared/Packets";

const shops: Record<string, ChooseOptionType> = {};
for (const item of Items.sortedItems) {
    if (item.findTrait("Shop") !== undefined) {
        shops[item.name] = item.id;
    }
}

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            hideMaxedItems: false,
            shop: EnumList(shops, ClassLowerNegativeShop.name),
        },
    },
    (props) => {
        StoryMocking.mockData();

        const item = Items.getItem(props.controls.shop as string) ?? ClassLowerNegativeShop;
        const shop = item.findTrait("Shop")!;

        const [adornee, setAdornee] = React.useState<Part | undefined>(undefined);
        useEffect(() => {
            const part = new Instance("Part");
            part.Color = Color3.fromRGB(27, 42, 53);
            part.Size = new Vector3(20, 11, 0.5);
            part.Anchored = true;
            part.Position = new Vector3(0, 10, 0);
            part.Parent = Workspace;
            setAdornee(part);

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

        useEffect(() => {
            const settings = Packets.settings.get();
            settings.HideMaxedItems = props.controls.hideMaxedItems;
            settings.FocusShopCamera = true;
            Packets.settings.set(settings);
        }, [props.controls.hideMaxedItems]);

        ShopManager.refreshShop(adornee, shop);

        return (
            <StrictMode>
                <TooltipWindow />
                <ShopGui />
                <PurchaseWindow />
            </StrictMode>
        );
    },
);


import { getAllInstanceInfo } from "@antivivi/vrldk";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "@rbxts/react";
import { CollectionService } from "@rbxts/services";
import useHotkeyWithTooltip from "client/components/hotkeys/useHotkeyWithTooltip";
import InventoryFilter, {
    filterItems,
    useBasicInventoryFilter,
} from "client/components/item/inventory/InventoryFilter";
import { PurchaseManager } from "client/components/item/shop/PurchaseWindow";
import ShopManager, { type ShopCandidate } from "client/components/item/shop/ShopManager";
import { createShopSlot, updateShopSlot, type ShopSlotHandle } from "client/components/item/shop/ShopSlot";
import { showErrorToast } from "client/components/toast/ToastService";
import useProperty from "client/hooks/useProperty";
import { playSound } from "shared/asset/GameAssets";
import { RobotoSlabHeavy } from "shared/asset/GameFonts";
import Item from "shared/item/Item";
import Shop from "shared/item/traits/Shop";
import Items from "shared/items/Items";
import Packets from "shared/Packets";

/**
 * Main shop window component with integrated filtering
 */
export default function ShopGui() {
    const [{ shop, adornee }, setShopInfo] = useState<{ shop?: Shop; adornee?: Part }>({});
    const { searchQuery, props: filterProps } = useBasicInventoryFilter();
    const [hideMaxedItems, setHideMaxedItems] = useState(Packets.settings.get().HideMaxedItems);
    const [focusShopCamera, setFocusShopCamera] = useState(Packets.settings.get().FocusShopCamera === true);
    const ownedPerItem = useProperty(Packets.bought) ?? new Map<string, number>();
    const empireLevel = useProperty(Packets.level);
    const shopItems = shop?.items ?? new Set<Item>();
    const scrollingFrameRef = useRef<ScrollingFrame>();
    const itemSlotsRef = useRef(new Map<string, ShopSlotHandle>());
    const handleItemClick = useCallback((item: Item) => {
        playSound("MenuClick.mp3");
        PurchaseManager.select(item);
    }, []);

    useEffect(() => {
        const settingsConnection = Packets.settings.observe((settings) => {
            setHideMaxedItems(settings.HideMaxedItems);
            setFocusShopCamera(settings.FocusShopCamera === true);
        });
        const openedConnection = ShopManager.opened.connect((shop, adornee) => {
            setShopInfo((prev) => {
                if (shop === undefined) {
                    return { shop: prev.shop, adornee: undefined };
                }
                return { shop, adornee };
            });
        });

        const candidates = new Map<BasePart, ShopCandidate>();
        const addCandidate = (hitbox: BasePart) => {
            const model = hitbox.Parent;
            if (model === undefined) return;
            const modelInfo = getAllInstanceInfo(model);

            const itemId = modelInfo.itemId;
            if (itemId === undefined) return;
            const item = Items.getItem(itemId);
            if (item === undefined) return;
            const shop = item.findTrait("Shop");
            if (shop === undefined) return;
            const shopGuiPart = model.FindFirstChild("ShopGuiPart") as Part;
            if (shopGuiPart === undefined) return;
            candidates.set(hitbox, { guiPart: shopGuiPart, shop, placementId: model.Name });
            ShopManager.hideShopGuiPart(shopGuiPart);
        };
        for (const hitbox of CollectionService.GetTagged("Shop")) {
            addCandidate(hitbox as BasePart);
        }
        CollectionService.GetInstanceAddedSignal("Shop").Connect((hitbox) => {
            addCandidate(hitbox as BasePart);
        });
        CollectionService.GetInstanceRemovedSignal("Shop").Connect((hitbox) => {
            candidates.delete(hitbox as BasePart);
        });

        let active = true;
        const loop = () => {
            if (active === false) return;
            ShopManager.checkForShop(candidates);
            task.delay(0.1, loop);
        };
        loop();
        return () => {
            active = false;
            settingsConnection.disconnect();
            openedConnection.disconnect();
            ShopManager.refreshShop();
        };
    }, []);

    useEffect(() => {
        ShopManager.setCameraFocusEnabled(focusShopCamera);
    }, [focusShopCamera]);

    useEffect(() => {
        return () => {
            const slots = itemSlotsRef.current;
            for (const [, slot] of slots) {
                slot.destroy();
            }
            slots.clear();
        };
    }, []);

    useEffect(() => {
        const frame = scrollingFrameRef.current;
        if (!frame) return;

        const slots = itemSlotsRef.current;
        for (const item of Items.sortedItems) {
            if (slots.has(item.id)) continue;

            const slot = createShopSlot(item, {
                parent: frame,
                layoutOrder: item.layoutOrder,
                visible: false,
                onActivated: handleItemClick,
            });
            slots.set(item.id, slot);
        }
    }, [handleItemClick]);

    const dataPerItem = useMemo(() => {
        return filterItems(shopItems, searchQuery, filterProps.traitFilters);
    }, [shopItems, searchQuery, filterProps.traitFilters]);

    useEffect(() => {
        const frame = scrollingFrameRef.current;
        if (!frame) return;

        const slots = itemSlotsRef.current;
        for (const item of Items.sortedItems) {
            const slot = slots.get(item.id);
            if (slot === undefined) continue;

            const slotData = dataPerItem.get(item.id);
            const layoutOrder = slotData?.layoutOrder ?? item.layoutOrder;
            const baseVisible = shopItems.has(item) && slotData?.visible === true;
            const ownedAmount = ownedPerItem.get(item.id) ?? 0;

            updateShopSlot(slot, {
                layoutOrder,
                baseVisible,
                hideMaxedItems,
                ownedAmount,
                onActivated: handleItemClick,
                empireLevel: empireLevel ?? 0,
            });
        }
    }, [dataPerItem, shopItems, hideMaxedItems, ownedPerItem, handleItemClick, empireLevel]);

    const { events } = useHotkeyWithTooltip({
        action: () => {
            if (!shop) return false;
            const serialized = new Set<string>();
            for (const item of shop.items) {
                serialized.add(item.id);
            }
            if (Packets.buyAllItems.toServer(serialized)) {
                playSound("ItemPurchase.mp3");
            } else {
                playSound("Error.mp3");
                showErrorToast("Unable to purchase all items.");
            }
            return true;
        },
        label: "Purchase All",
    });

    return (
        <surfacegui
            key={"Shop"}
            Adornee={adornee}
            AlwaysOnTop={true}
            ClipsDescendants={true}
            Enabled={adornee !== undefined && shop !== undefined}
            Face={Enum.NormalId.Front}
            LightInfluence={0}
            PixelsPerStud={50}
            ResetOnSpawn={false}
            SizingMode={Enum.SurfaceGuiSizingMode.PixelsPerStud}
            ZIndexBehavior={Enum.ZIndexBehavior.Sibling}
        >
            {/* Main container */}
            <uipadding
                PaddingBottom={new UDim(0, 5)}
                PaddingLeft={new UDim(0, 5)}
                PaddingRight={new UDim(0, 5)}
                PaddingTop={new UDim(0, 5)}
            />
            <uilistlayout
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                Padding={new UDim(0, 10)}
                SortOrder={Enum.SortOrder.LayoutOrder}
                VerticalAlignment={Enum.VerticalAlignment.Center}
            />

            {/* Filter options */}
            <InventoryFilter color={shop?.item.difficulty.color} {...filterProps} />

            {/* Item list scrolling frame */}
            <scrollingframe
                ref={scrollingFrameRef}
                Active={true}
                AnchorPoint={new Vector2(0.5, 1)}
                AutomaticCanvasSize={Enum.AutomaticSize.Y}
                BackgroundTransparency={1}
                CanvasSize={new UDim2(0, 0, 0, 0)}
                LayoutOrder={1}
                Position={new UDim2(0.5, 0, 1, 0)}
                ScrollBarThickness={6}
                Selectable={false}
                Size={new UDim2(1, -5, 0.9, 0)}
            >
                {/* Grid layout for items */}
                <uigridlayout
                    CellPadding={new UDim2(0, 12, 0, 12)}
                    CellSize={new UDim2(0.167, -12, 0, 0)}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                >
                    <uiaspectratioconstraint AspectRatio={0.8} AspectType={Enum.AspectType.ScaleWithParentSize} />
                </uigridlayout>

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

                {/* Border stroke */}
                <uistroke Color={shop?.item.difficulty.color} Thickness={3} />

                {/* Buy All button */}
                <frame BackgroundTransparency={1} LayoutOrder={99999} Size={new UDim2(0, 100, 0, 100)}>
                    <textbutton
                        AnchorPoint={new Vector2(0.5, 0.5)}
                        BackgroundColor3={Color3.fromRGB(85, 85, 255)}
                        BorderColor3={Color3.fromRGB(27, 42, 53)}
                        LayoutOrder={9999}
                        Position={new UDim2(0.5, 0, 0.5, 0)}
                        Selectable={false}
                        Size={new UDim2(0.7, 0, 0.6, 0)}
                        Text=""
                        Event={{
                            ...events,
                        }}
                    >
                        {/* Button stroke */}
                        <uistroke
                            ApplyStrokeMode={Enum.ApplyStrokeMode.Border}
                            Color={Color3.fromRGB(54, 44, 194)}
                            Thickness={3}
                        >
                            <uigradient
                                Color={
                                    new ColorSequence([
                                        new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                                        new ColorSequenceKeypoint(0.597, Color3.fromRGB(156, 156, 156)),
                                        new ColorSequenceKeypoint(1, Color3.fromRGB(255, 255, 255)),
                                    ])
                                }
                                Rotation={60}
                            />
                        </uistroke>

                        {/* Button gradient */}
                        <uigradient
                            Color={
                                new ColorSequence([
                                    new ColorSequenceKeypoint(0, Color3.fromRGB(170, 170, 255)),
                                    new ColorSequenceKeypoint(1, Color3.fromRGB(255, 255, 255)),
                                ])
                            }
                            Rotation={270}
                        />

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

                        {/* Button layout */}
                        <uilistlayout
                            HorizontalAlignment={Enum.HorizontalAlignment.Center}
                            Padding={new UDim(0, 5)}
                            SortOrder={Enum.SortOrder.LayoutOrder}
                            VerticalAlignment={Enum.VerticalAlignment.Center}
                        />

                        {/* Button text */}
                        <textlabel
                            BackgroundTransparency={1}
                            FontFace={RobotoSlabHeavy}
                            LayoutOrder={-5}
                            Size={new UDim2(1, 0, 1, 0)}
                            Text="Buy All Items"
                            TextColor3={Color3.fromRGB(255, 255, 255)}
                            TextSize={30}
                            TextWrapped={true}
                        >
                            <uistroke Color={Color3.fromRGB(5, 16, 0)} Thickness={2} />
                        </textlabel>
                    </textbutton>
                </frame>
            </scrollingframe>
        </surfacegui>
    );
}


import { OnoeNum } from "@rbxts/serikanum";
import React, { Fragment, useEffect, useRef, useState } from "@rbxts/react";
import { TweenService } from "@rbxts/services";
import displayBalanceCurrency from "client/components/balance/displayBalanceCurrency";
import { useItemViewport } from "shared/item/ItemViewport";
import { getAsset } from "shared/asset/AssetMap";
import { RobotoSlabHeavy } from "shared/asset/GameFonts";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import { CURRENCY_DETAILS } from "shared/currency/CurrencyDetails";
import Item from "shared/item/Item";
import Items from "shared/items/Items";

function ShopPriceOption({
    currency,
    item,
    amount,
    active,
}: {
    currency?: Currency;
    item?: Item;
    amount?: OnoeNum | number;
    active?: boolean;
}) {
    // Always call hooks unconditionally
    const viewportRef = useRef<ViewportFrame>();
    const textLabelRef = useRef<TextLabel>();
    const currentTweenRef = useRef<Tween>();

    // Only pass itemId if item is defined, otherwise pass empty string
    useItemViewport(viewportRef, item?.id ?? "");

    const image = currency !== undefined ? CURRENCY_DETAILS[currency].image : item?.image;
    let targetColor = Color3.fromRGB(255, 255, 255);
    if (currency !== undefined) {
        targetColor = CURRENCY_DETAILS[currency].color;
    } else if (item !== undefined) {
        targetColor = item.difficulty?.color ?? Color3.fromRGB(255, 255, 255);
    }
    targetColor = new Color3(
        math.clamp(targetColor.R, 0.1, 0.9),
        math.clamp(targetColor.G, 0.1, 0.9),
        math.clamp(targetColor.B, 0.1, 0.9),
    );

    // Tween color when currency or item changes
    useEffect(() => {
        if (!textLabelRef.current || !active) return;

        // Cancel any existing tween
        if (currentTweenRef.current) {
            currentTweenRef.current.Cancel();
        }

        const tweenInfo = new TweenInfo(0.3, Enum.EasingStyle.Quart, Enum.EasingDirection.Out);
        const tween = TweenService.Create(textLabelRef.current, tweenInfo, {
            TextColor3: targetColor,
        });

        currentTweenRef.current = tween;
        tween.Play();

        return () => {
            if (currentTweenRef.current) {
                currentTweenRef.current.Cancel();
                currentTweenRef.current = undefined;
            }
        };
    }, [currency, item, active]);

    return (
        <frame AutomaticSize={Enum.AutomaticSize.X} BackgroundTransparency={1} Size={new UDim2(1, 0, 0, 28)}>
            <uicorner CornerRadius={new UDim(0, 4)} />
            <uilistlayout
                Padding={new UDim(0, 5)}
                FillDirection={Enum.FillDirection.Horizontal}
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
            />
            {image !== undefined ? (
                <imagelabel
                    BackgroundTransparency={1}
                    Image={image}
                    Size={new UDim2(1, 0, 1, 0)}
                    SizeConstraint={Enum.SizeConstraint.RelativeYY}
                />
            ) : (
                item !== undefined && (
                    <viewportframe
                        ref={viewportRef}
                        BackgroundTransparency={1}
                        Size={new UDim2(1, 0, 1, 0)}
                        SizeConstraint={Enum.SizeConstraint.RelativeYY}
                    />
                )
            )}
            {(currency !== undefined || item !== undefined) && amount !== undefined ? (
                <textlabel
                    ref={textLabelRef}
                    AutomaticSize={Enum.AutomaticSize.X}
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabHeavy}
                    Size={new UDim2(0, 0, 1, 0)}
                    Text={currency !== undefined ? displayBalanceCurrency(currency, amount) : tostring(amount)}
                    TextScaled={true}
                >
                    <uistroke Color={Color3.fromRGB(0, 0, 0)} Thickness={2} />
                </textlabel>
            ) : (
                <textlabel
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabHeavy}
                    Size={new UDim2(1, 0, 1, 0)}
                    Text={"MAXED"}
                    TextColor3={Color3.fromRGB(199, 199, 199)}
                    TextScaled={true}
                >
                    <uistroke Color={Color3.fromRGB(0, 0, 0)} Thickness={2} />
                </textlabel>
            )}
        </frame>
    );
}

export function ShopItemSlotStyling({ gridTransparency = 0.85 }: { gridTransparency?: number }) {
    return (
        <Fragment>
            <uigradient
                Color={
                    new ColorSequence([
                        new ColorSequenceKeypoint(0, Color3.fromRGB(72, 72, 72)),
                        new ColorSequenceKeypoint(1, Color3.fromRGB(76, 76, 76)),
                    ])
                }
                Rotation={90}
            />
            <imagelabel
                BackgroundTransparency={1}
                Image={getAsset("assets/Vignette.png")}
                ImageTransparency={0.2}
                Size={new UDim2(1, 0, 1, 0)}
                ZIndex={-4}
            />
            <imagelabel
                AnchorPoint={new Vector2(0.5, 0.5)}
                BackgroundTransparency={1}
                Image={getAsset("assets/Grid.png")}
                ImageColor3={Color3.fromRGB(126, 126, 126)}
                ImageTransparency={gridTransparency}
                Position={new UDim2(0.5, 0, 0.5, 0)}
                ScaleType={Enum.ScaleType.Tile}
                Size={new UDim2(1, 0, 1, 0)}
                TileSize={new UDim2(0, 100, 0, 100)}
                ZIndex={-5}
            />
        </Fragment>
    );
}

/**
 * Individual shop item slot component
 */
export default function ShopItemSlot({
    item,
    hideMaxedItems,
    ownedAmount,
    onClick,
    layoutOrder = 0,
    visible,
}: {
    /** The item to display in the slot */
    item: Item;
    /** Whether to hide items that are already maxed out */
    hideMaxedItems: boolean;
    /** Amount of the item the player currently owns */
    ownedAmount: number;
    /** Callback when the slot is clicked */
    onClick: () => void;
    /** Layout order for sorting */
    layoutOrder?: number;
    /** Whether the slot is visible */
    visible: boolean;
}) {
    const viewportRef = useRef<ViewportFrame>();
    const difficulty = item.difficulty;
    const color = difficulty?.color ?? Color3.fromRGB(52, 155, 255);
    useItemViewport(viewportRef, item.id);

    const price = item.getPrice(ownedAmount + 1);
    const requiredItems = item.requiredItems;

    const [viewing, setViewing] = useState<Currency | Item>();
    useEffect(() => {
        if (price === undefined || !visible) {
            setViewing(undefined);
            return;
        }

        const options = new Array<Currency | Item>();
        for (const [currency] of CurrencyBundle.SORTED_DETAILS) {
            const amount = price.get(currency);
            if (amount === undefined) continue;
            options.push(currency);
        }
        for (const item of Items.sortedItems) {
            if (!requiredItems.has(item.id)) continue;
            options.push(item);
        }
        let active = true;
        let i = 0;
        const max = options.size();
        const scrollToNext = () => {
            if (!active) return;
            if (++i >= max) i = 0;
            setViewing(options[i]);
            task.delay(2, scrollToNext);
        };
        if (max > 1) {
            scrollToNext();
        } else {
            setViewing(options[0]);
        }
        return () => {
            active = false;
        };
    }, [price, requiredItems, visible]);

    const isCurrency = typeIs(viewing, "string");
    const currency = isCurrency ? (viewing as Currency) : undefined;
    const reqItem = !isCurrency ? (viewing as Item) : undefined;
    const amount = currency ? price?.get(currency) : reqItem !== undefined ? requiredItems.get(reqItem.id) : 0;
    const shouldHide = hideMaxedItems && price === undefined;

    return (
        <frame BackgroundTransparency={1} LayoutOrder={layoutOrder} Visible={shouldHide ? false : visible}>
            <textbutton
                BackgroundColor3={color}
                BorderColor3={Color3.fromRGB(0, 0, 0)}
                BorderSizePixel={5}
                Size={new UDim2(1, 0, 1, -40)}
                Selectable={false}
                Text=""
                Event={{
                    Activated: onClick,
                }}
            >
                {/* Viewport frame for 3D item display */}
                {item.image !== undefined ? (
                    <imagelabel
                        AnchorPoint={new Vector2(0.5, 0.5)}
                        BackgroundTransparency={1}
                        Image={item.image}
                        Position={new UDim2(0.5, 0, 0.5, 0)}
                        Size={new UDim2(0.7, 0, 0.7, 0)}
                        SizeConstraint={Enum.SizeConstraint.RelativeYY}
                    />
                ) : (
                    <viewportframe
                        ref={viewportRef}
                        AnchorPoint={new Vector2(0.5, 0.5)}
                        BackgroundTransparency={1}
                        Position={new UDim2(0.5, 0, 0.5, 0)}
                        Size={new UDim2(1, 0, 1, 0)}
                    />
                )}
                <uipadding
                    PaddingBottom={new UDim(0, 4)}
                    PaddingTop={new UDim(0, 4)}
                    PaddingLeft={new UDim(0, 4)}
                    PaddingRight={new UDim(0, 4)}
                />

                <uistroke ApplyStrokeMode={Enum.ApplyStrokeMode.Border} Color={color} Thickness={2}>
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                                new ColorSequenceKeypoint(0.299, Color3.fromRGB(255, 255, 255)),
                                new ColorSequenceKeypoint(0.51, Color3.fromRGB(118, 118, 118)),
                                new ColorSequenceKeypoint(0.822, Color3.fromRGB(255, 255, 255)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(255, 255, 255)),
                            ])
                        }
                        Rotation={35}
                    />
                </uistroke>
                <ShopItemSlotStyling />
            </textbutton>
            <frame
                AnchorPoint={new Vector2(0, 1)}
                BackgroundColor3={color.Lerp(Color3.fromRGB(0, 0, 0), 0.7)}
                BorderColor3={new Color3(0, 0, 0)}
                BorderSizePixel={5}
                Position={new UDim2(0, 0, 1, 0)}
                Size={new UDim2(1, 0, 0, 30)}
            >
                <ShopPriceOption active={visible} currency={currency} item={reqItem} amount={amount} />
                <ShopItemSlotStyling gridTransparency={0.9} />
            </frame>
        </frame>
    );
}


import Signal from "@antivivi/lemon-signal";
import { Debris, TweenService, Workspace } from "@rbxts/services";
import { getSound } from "shared/asset/GameAssets";
import { IS_EDIT } from "shared/Context";
import { getPlayerCharacter } from "shared/hamster/getPlayerCharacter";
import Shop from "shared/item/traits/Shop";
import Packets from "shared/Packets";

export interface ShopCandidate {
    guiPart: Part;
    shop: Shop;
    placementId?: string;
}

interface CameraState {
    cframe: CFrame;
    cameraType: Enum.CameraType;
    cameraSubject?: Humanoid | BasePart;
    focus: CFrame;
}

let shopGuiPart: Part | undefined;
let cameraState: CameraState | undefined;
let cameraTween: Tween | undefined;
let focusCameraEnabled = false;
let cameraFocusSuppressed = false;
const CAMERA_DEBOUNCE_SECONDS = 0.4;
let focusRequestToken = 0;
let lastFocusRequest = 0;

const opened = new Signal<(shop?: Shop, adornee?: Part) => void>();
let brokenPlacementIds: ReadonlySet<string> = Packets.brokenPlacedItems.get() ?? new Set<string>();

Packets.brokenPlacedItems.observe((value) => {
    brokenPlacementIds = value ?? new Set<string>();

    if (shopGuiPart === undefined) return;

    const currentPlacementId = shopGuiPart.Parent?.Name;
    if (currentPlacementId !== undefined && brokenPlacementIds.has(currentPlacementId)) {
        refreshShop();
    }
});

function cancelCameraTween() {
    if (cameraTween) {
        cameraTween.Cancel();
        cameraTween = undefined;
    }
}

function performCameraFocus(guiPart: Part) {
    if (IS_EDIT || focusCameraEnabled === false || cameraFocusSuppressed) return;

    const camera = Workspace.CurrentCamera;
    if (camera === undefined) return;

    if (cameraState === undefined) {
        cameraState = {
            cframe: camera.CFrame,
            cameraType: camera.CameraType,
            cameraSubject: camera.CameraSubject,
            focus: camera.Focus,
        };
    }

    const forward = guiPart.CFrame.LookVector;
    const from = guiPart.Position.add(forward.mul(10));

    camera.CameraType = Enum.CameraType.Scriptable;
    camera.CameraSubject = undefined;

    const targetCFrame = CFrame.lookAt(from, guiPart.Position);
    const tweenInfo = new TweenInfo(0.35, Enum.EasingStyle.Quad, Enum.EasingDirection.Out);

    cancelCameraTween();
    const tween = TweenService.Create(camera, tweenInfo, { CFrame: targetCFrame });
    cameraTween = tween;
    tween.Completed.Once(() => {
        if (cameraTween === tween) {
            cameraTween = undefined;
        }
    });
    tween.Play();

    lastFocusRequest = tick();
}

function requestCameraFocus(guiPart: Part) {
    if (IS_EDIT || focusCameraEnabled === false || cameraFocusSuppressed) return;

    const now = tick();
    const elapsed = now - lastFocusRequest;
    if (elapsed < CAMERA_DEBOUNCE_SECONDS) {
        const remaining = CAMERA_DEBOUNCE_SECONDS - elapsed;
        const delayDuration = math.max(remaining, 0);
        const tokenAtSchedule = ++focusRequestToken;
        task.delay(delayDuration, () => {
            if (focusRequestToken !== tokenAtSchedule) return;
            if (focusCameraEnabled === false || cameraFocusSuppressed) return;
            if (shopGuiPart !== guiPart) return;
            performCameraFocus(guiPart);
        });
        return;
    }

    focusRequestToken++;
    performCameraFocus(guiPart);
}

function restoreCamera() {
    focusRequestToken++;
    const state = cameraState;
    const camera = Workspace.CurrentCamera;
    if (state === undefined || camera === undefined) {
        cameraState = undefined;
        cancelCameraTween();
        return;
    }

    cancelCameraTween();

    const tweenInfo = new TweenInfo(0.35, Enum.EasingStyle.Quad, Enum.EasingDirection.Out);
    const tween = TweenService.Create(camera, tweenInfo, { CFrame: state.cframe });
    camera.CameraType = Enum.CameraType.Scriptable;
    camera.CameraSubject = undefined;
    camera.Focus = state.focus;
    cameraTween = tween;
    tween.Completed.Once(() => {
        if (cameraTween === tween) {
            cameraTween = undefined;
        }
        camera.CameraType = state.cameraType;
        camera.CameraSubject = state.cameraSubject;
        if (state.cameraType !== Enum.CameraType.Scriptable && state.cameraSubject === undefined) {
            camera.Focus = state.focus;
        }
    });

    cameraState = undefined;
    tween.Play();

    lastFocusRequest = tick();
}

function hideShopGuiPart(guiPart: Part) {
    TweenService.Create(guiPart, new TweenInfo(0.3), { LocalTransparencyModifier: 1 }).Play();
    guiPart.CanQuery = false;

    const sound = getSound("ShopClose.mp3");
    sound.Play();
    sound.Parent = guiPart;
    Debris.AddItem(sound, 5);
}

function playOpenEffects(guiPart: Part) {
    const sound = getSound("ShopOpen.mp3");
    sound.Play();
    sound.Parent = guiPart;
    Debris.AddItem(sound, 5);

    TweenService.Create(guiPart, new TweenInfo(0.3), { LocalTransparencyModifier: 0 }).Play();
    guiPart.CanQuery = true;
}

function setCameraFocusEnabled(enabled: boolean) {
    if (focusCameraEnabled === enabled) return;
    focusCameraEnabled = enabled;

    if (!enabled) {
        restoreCamera();
        cameraFocusSuppressed = false;
        return;
    }

    if (shopGuiPart !== undefined && cameraFocusSuppressed === false) {
        requestCameraFocus(shopGuiPart);
    }
}

function releaseCameraFocus(): boolean {
    if (focusCameraEnabled === false) return false;
    if (cameraFocusSuppressed === true) return false;
    if (shopGuiPart === undefined) return false;
    if (cameraState === undefined) return false;

    cameraFocusSuppressed = true;
    restoreCamera();
    return true;
}

function refreshShop(guiPart?: Part, shop?: Shop) {
    if (shopGuiPart === guiPart) return;

    const previousShopGuiPart = shopGuiPart;
    shopGuiPart = guiPart;
    focusRequestToken++;

    if (previousShopGuiPart !== undefined && previousShopGuiPart !== guiPart) {
        hideShopGuiPart(previousShopGuiPart);
    }

    if (guiPart === undefined || shop === undefined) {
        restoreCamera();
        cameraFocusSuppressed = false;
        opened.fire();
        return;
    }

    playOpenEffects(guiPart);
    if (cameraFocusSuppressed === false) {
        requestCameraFocus(guiPart);
    }
    opened.fire(shop, guiPart);
}

function isCandidateBroken(candidate: ShopCandidate): boolean {
    const { placementId } = candidate;
    return placementId !== undefined && brokenPlacementIds.has(placementId);
}

function checkForShop(candidates: Map<BasePart, ShopCandidate>) {
    const primaryPart = getPlayerCharacter()?.PrimaryPart;
    if (primaryPart === undefined) return;

    let shopFound = false;
    for (const [hitbox, candidate] of candidates) {
        if (isCandidateBroken(candidate)) continue;

        const { guiPart, shop } = candidate;
        const localPosition = hitbox.CFrame.PointToObjectSpace(primaryPart.Position);
        if (math.abs(localPosition.X) > hitbox.Size.X / 2 || math.abs(localPosition.Z) > hitbox.Size.Z / 2) continue;

        refreshShop(guiPart, shop);
        shopGuiPart = guiPart;
        shopFound = true;
        break;
    }

    if (shopFound === false) {
        refreshShop();
    }
}

const ShopManager = {
    opened,
    setCameraFocusEnabled,
    releaseCameraFocus,
    refreshShop,
    checkForShop,
    hideShopGuiPart,
};

export default ShopManager;