/**
 * @fileoverview Header component for leaderboards showing column titles.
 */

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

interface LeaderboardHeaderProps {
    title: string;
    gradient: ColorSequence;
}

/**
 * Header section for leaderboards showing the title and column labels.
 */
export default function LeaderboardHeader({ title, gradient }: LeaderboardHeaderProps) {
    return (
        <textlabel
            key="Header"
            BackgroundTransparency={1}
            FontFace={RobotoSlabHeavy}
            Position={new UDim2(0, 0, 0.02, 0)}
            Size={new UDim2(1, 0, 0, 50)}
            Text={title}
            TextColor3={Color3.fromRGB(255, 255, 255)}
            TextScaled={true}
            TextSize={36}
            TextWrapped={true}
        >
            <uigradient Color={gradient} Rotation={89} />
        </textlabel>
    );
}

export function ColumnHeader({ valueLabel }: { valueLabel: string }) {
    return (
        <frame key="ColumnHeaders" BackgroundTransparency={1} LayoutOrder={-1} Size={new UDim2(1, 0, 0, 40)}>
            <textlabel
                key="PlaceHeader"
                BackgroundTransparency={1}
                FontFace={RobotoMonoBold}
                Size={new UDim2(0.125, 0, 1, 0)}
                Text="#"
                TextColor3={Color3.fromRGB(0, 0, 0)}
                TextScaled={true}
                TextSize={14}
                TextStrokeColor3={Color3.fromRGB(255, 255, 255)}
                TextStrokeTransparency={0.9}
                TextWrapped={true}
            />
            <textlabel
                key="NameHeader"
                BackgroundTransparency={1}
                FontFace={RobotoMonoBold}
                Position={new UDim2(0.15, 0, 0, 0)}
                Size={new UDim2(0.6, 0, 1, 0)}
                Text="Name"
                TextColor3={Color3.fromRGB(0, 0, 0)}
                TextScaled={true}
                TextSize={14}
                TextStrokeColor3={Color3.fromRGB(255, 255, 255)}
                TextStrokeTransparency={0.9}
                TextWrapped={true}
            />
            <textlabel
                key="ValueHeader"
                BackgroundTransparency={1}
                FontFace={RobotoMonoBold}
                Position={new UDim2(0.85, 0, 0, 0)}
                Size={new UDim2(0.2, 0, 1, 0)}
                Text={valueLabel}
                TextColor3={Color3.fromRGB(0, 0, 0)}
                TextScaled={true}
                TextSize={14}
                TextStrokeColor3={Color3.fromRGB(255, 255, 255)}
                TextStrokeTransparency={0.9}
                TextWrapped={true}
            />
            <uicorner CornerRadius={new UDim(0, 9)} />
            <uilistlayout
                FillDirection={Enum.FillDirection.Horizontal}
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                Padding={new UDim(0, 5)}
                SortOrder={Enum.SortOrder.LayoutOrder}
                VerticalAlignment={Enum.VerticalAlignment.Center}
            />
            <uipadding PaddingLeft={new UDim(0, 6)} PaddingRight={new UDim(0, 6)} />
        </frame>
    );
}


import React, { Fragment, JSX, useEffect, useState } from "@rbxts/react";
import Shaker from "client/components/effect/Shaker";
import Leaderboard, { LeaderboardType } from "client/components/world/leaderboard/Leaderboard";
import useProperty from "client/hooks/useProperty";
import { playSound } from "shared/asset/GameAssets";
import { RobotoSlabExtraBold, RobotoSlabHeavy } from "shared/asset/GameFonts";
import { DONATION_PRODUCTS } from "shared/devproducts/DonationProducts";
import Packets from "shared/Packets";
import WorldNode, { SingleWorldNode } from "shared/world/nodes/WorldNode";

const DonationGuiPart = new SingleWorldNode<Part>("DonationGuiPart");

export default function LeaderboardRenderer() {
    const [guiParts, setGuiParts] = useState<Map<Instance, BasePart>>(new Map());
    const leaderboardData = useProperty(Packets.leaderboardData);

    useEffect(() => {
        let guiParts = new Map<Instance, BasePart>();

        const leaderboardNode = new WorldNode<Model>(
            "Leaderboard",
            (instance) => {
                const guiPart = instance.WaitForChild("GuiPart") as BasePart;
                guiParts.set(instance, guiPart);
                setGuiParts(table.clone(guiParts));
            },
            (instance) => {
                guiParts.delete(instance);
            },
        );

        const connection = Packets.donationGiven.fromServer(() => {
            playSound("PowerUp.mp3");
            Shaker.shake();
        });

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

    const elements = new Array<JSX.Element>();
    for (const [instance, guiPart] of guiParts) {
        const leaderboardEntries = leaderboardData.get(instance.Name as LeaderboardType) ?? [];
        elements.push(
            <surfacegui
                Adornee={guiPart}
                LightInfluence={0}
                MaxDistance={50}
                ResetOnSpawn={false}
                SizingMode={Enum.SurfaceGuiSizingMode.PixelsPerStud}
                ZIndexBehavior="Sibling"
            >
                <Leaderboard leaderboardType={instance.Name as LeaderboardType} entries={leaderboardEntries} />
            </surfacegui>,
        );
    }

    return (
        <Fragment>
            <surfacegui
                Adornee={DonationGuiPart.waitForInstance()}
                ClipsDescendants={true}
                LightInfluence={1}
                MaxDistance={50}
                ResetOnSpawn={false}
                SizingMode={Enum.SurfaceGuiSizingMode.PixelsPerStud}
                ZIndexBehavior={Enum.ZIndexBehavior.Sibling}
            >
                <textlabel
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabHeavy}
                    Position={new UDim2(0, 0, 0.02, 0)}
                    Size={new UDim2(1, 0, 0.15, 0)}
                    Text="Thanks for supporting  <3"
                    TextColor3={Color3.fromRGB(255, 85, 127)}
                    TextScaled={true}
                    TextSize={36}
                    TextWrapped={true}
                />
                <scrollingframe
                    Active={true}
                    AutomaticCanvasSize={Enum.AutomaticSize.Y}
                    BackgroundColor3={Color3.fromRGB(209, 209, 209)}
                    BorderSizePixel={0}
                    CanvasSize={new UDim2(0, 0, 0, 0)}
                    Position={new UDim2(0.1, 0, 0.22, 0)}
                    Selectable={false}
                    Size={new UDim2(0.8, 0, 0.7, 0)}
                >
                    <uilistlayout Padding={new UDim(0, 5)} SortOrder={Enum.SortOrder.LayoutOrder}>
                        <uilistlayout
                            FillDirection={Enum.FillDirection.Horizontal}
                            HorizontalAlignment={Enum.HorizontalAlignment.Center}
                            Padding={new UDim(0, 5)}
                            SortOrder={Enum.SortOrder.LayoutOrder}
                            VerticalAlignment={Enum.VerticalAlignment.Center}
                        />
                    </uilistlayout>
                    <uipadding
                        PaddingBottom={new UDim(0, 15)}
                        PaddingLeft={new UDim(0, 15)}
                        PaddingRight={new UDim(0, 15)}
                        PaddingTop={new UDim(0, 15)}
                    />
                    {DONATION_PRODUCTS.map((dp) => (
                        <DonationOption key={`donation-${dp.id}`} amount={dp.amount} />
                    ))}
                </scrollingframe>
            </surfacegui>
            {elements}
        </Fragment>
    );
}

function DonationOption({ amount }: { amount: number }) {
    return (
        <textbutton
            BackgroundColor3={Color3.fromRGB(255, 255, 255)}
            BorderSizePixel={0}
            LayoutOrder={5}
            Selectable={false}
            Size={new UDim2(1, 0, 0, 40)}
            Text={""}
            Event={{
                Activated: () => {
                    let donationProduct = 0;
                    for (const dp of DONATION_PRODUCTS) {
                        if (dp.amount === amount) {
                            donationProduct = dp.id;
                        }
                    }
                    if (donationProduct === 0) {
                        warn(`No donation product found for amount: ${amount}`);
                        return;
                    }

                    playSound("MenuClick.mp3");
                    Packets.promptDonation.toServer(donationProduct);
                },
            }}
        >
            <uicorner CornerRadius={new UDim(0, 9)} />
            <uilistlayout
                FillDirection={Enum.FillDirection.Horizontal}
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                Padding={new UDim(0, 5)}
                SortOrder={Enum.SortOrder.LayoutOrder}
                VerticalAlignment={Enum.VerticalAlignment.Center}
            />
            <uipadding PaddingLeft={new UDim(0, 6)} PaddingRight={new UDim(0, 6)} />
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoSlabExtraBold}
                Position={new UDim2(0.15, 0, 0, 0)}
                Size={new UDim2(0.65, 0, 0.9, 0)}
                Text={`Donate ${amount} Robux`}
                TextColor3={Color3.fromRGB(0, 0, 0)}
                TextScaled={true}
                TextSize={14}
                TextStrokeColor3={Color3.fromRGB(255, 255, 255)}
                TextStrokeTransparency={0.9}
                TextWrapped={true}
            />
        </textbutton>
    );
}


/**
 * @fileoverview Individual leaderboard slot component displaying a single entry.
 */

import React, { memo, useEffect } from "@rbxts/react";
import { OnoeNum } from "@rbxts/serikanum";
import { RunService } from "@rbxts/services";
import { LeaderboardEntry } from "client/components/world/leaderboard/Leaderboard";
import { RobotoMono } from "shared/asset/GameFonts";

const DEFAULT_BACKGROUND = Color3.fromRGB(255, 255, 255);
const DEFAULT_TEXT = Color3.fromRGB(0, 0, 0);
const DEFAULT_STROKE = Color3.fromRGB(255, 255, 255);

const TOP_PLACE_COLORS: Record<number, Color3> = {
    1: Color3.fromRGB(255, 215, 0),
    2: Color3.fromRGB(192, 192, 192),
    3: Color3.fromRGB(205, 127, 50),
};

const TOP_PLACE_GRADIENTS: Record<number, ColorSequence> = {
    1: new ColorSequence([
        new ColorSequenceKeypoint(0, Color3.fromRGB(255, 215, 0)), // Gold
        new ColorSequenceKeypoint(0.3, Color3.fromRGB(255, 255, 0)), // Bright Yellow
        new ColorSequenceKeypoint(0.6, Color3.fromRGB(255, 140, 0)), // Orange
        new ColorSequenceKeypoint(1, Color3.fromRGB(255, 215, 0)), // Gold
    ]),
    2: new ColorSequence([
        new ColorSequenceKeypoint(0, Color3.fromRGB(192, 192, 192)), // Silver
        new ColorSequenceKeypoint(0.3, Color3.fromRGB(220, 220, 220)), // Bright Silver
        new ColorSequenceKeypoint(0.6, Color3.fromRGB(169, 169, 169)), // Dark Silver
        new ColorSequenceKeypoint(1, Color3.fromRGB(192, 192, 192)), // Silver
    ]),
    3: new ColorSequence([
        new ColorSequenceKeypoint(0, Color3.fromRGB(205, 127, 50)), // Bronze
        new ColorSequenceKeypoint(0.3, Color3.fromRGB(255, 165, 79)), // Bright Bronze
        new ColorSequenceKeypoint(0.6, Color3.fromRGB(184, 115, 51)), // Dark Bronze
        new ColorSequenceKeypoint(1, Color3.fromRGB(205, 127, 50)), // Bronze
    ]),
};

/** Props for individual leaderboard slots */
export interface LeaderboardSlotProps {
    /** The leaderboard entry data */
    entry: LeaderboardEntry;
    /** Whether this slot is highlighted */
    isHighlighted?: boolean;
}

/**
 * A single leaderboard entry displaying place, name, and amount.
 */
function LeaderboardSlot({ entry, isHighlighted = false }: LeaderboardSlotProps) {
    // Animation state for top 3 places gradients (binding to avoid full re-renders every frame)
    const [gradientOffset, setGradientOffset] = React.useBinding(0);
    const gradientOffsetVector = gradientOffset.map((value) => new Vector2(value, 0));

    // Animate gradient movement for top 3 places
    useEffect(() => {
        if (entry.place > 3) {
            setGradientOffset(0);
            return;
        }

        let offset = -1;
        let cooldown = 0;
        setGradientOffset(offset);

        const speed = entry.place === 1 ? 0.004 : entry.place === 2 ? 0.003 : 0.002;
        const connection = RunService.Heartbeat.Connect((dt) => {
            cooldown += dt;
            if (cooldown < 1.5) {
                return;
            }

            offset += speed;
            if (offset >= 1) {
                offset = -1;
                cooldown = 0;
            }

            setGradientOffset(offset);
        });

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

    // Special styling for top 3 places
    let backgroundColor: Color3;
    let textColor: Color3;
    let strokeColor: Color3;
    let strokeTransparency: number;

    if (entry.place > 3) {
        // Default styling for other places
        backgroundColor = isHighlighted ? Color3.fromRGB(255, 255, 170) : DEFAULT_BACKGROUND;
        textColor = DEFAULT_TEXT;
        strokeColor = DEFAULT_STROKE;
        strokeTransparency = 0.9;
    } else {
        backgroundColor = TOP_PLACE_COLORS[entry.place] || DEFAULT_BACKGROUND;
        textColor = backgroundColor.Lerp(new Color3(0, 0, 0), 0.7);
        strokeColor = backgroundColor.Lerp(Color3.fromRGB(255, 255, 255), 0.4);
        strokeTransparency = 0.3;
    }

    // Special height for
    const slotHeight = entry.place === 1 ? 70 : 45;

    return (
        <frame
            key="LeaderboardSlot"
            BackgroundColor3={backgroundColor}
            BorderSizePixel={0}
            Size={new UDim2(1, 0, 0, slotHeight)}
            LayoutOrder={entry.place}
        >
            {/* Add a subtle glow effect for top 3 places */}
            {entry.place <= 3 && <uistroke Color={TOP_PLACE_COLORS[entry.place]} Thickness={2} Transparency={0.3} />}

            {/* Animated gradient for 1st place */}
            {entry.place === 1 && <uigradient Color={TOP_PLACE_GRADIENTS[1]} Offset={gradientOffsetVector} />}

            {/* Animated gradient for 2nd place */}
            {entry.place === 2 && <uigradient Color={TOP_PLACE_GRADIENTS[2]} Offset={gradientOffsetVector} />}

            {/* Animated gradient for 3rd place */}
            {entry.place === 3 && <uigradient Color={TOP_PLACE_GRADIENTS[3]} Offset={gradientOffsetVector} />}
            <textlabel
                key="PlaceLabel"
                BackgroundTransparency={1}
                FontFace={RobotoMono}
                Size={new UDim2(0.125, 0, 1, 0)}
                Text={tostring(entry.place)}
                TextColor3={textColor}
                TextScaled={true}
                TextSize={14}
                TextStrokeColor3={strokeColor}
                TextStrokeTransparency={strokeTransparency}
                TextWrapped={true}
            />
            <textlabel
                key="ServerLabel"
                BackgroundTransparency={1}
                FontFace={RobotoMono}
                Position={new UDim2(0.15, 0, 0, 0)}
                Size={new UDim2(0.65, 0, 1, 0)}
                Text={entry.name}
                TextColor3={textColor}
                TextScaled={true}
                TextSize={14}
                TextStrokeColor3={strokeColor}
                TextStrokeTransparency={strokeTransparency}
                TextWrapped={true}
            />
            <textlabel
                key="AmountLabel"
                BackgroundTransparency={1}
                FontFace={RobotoMono}
                Position={new UDim2(0.85, 0, 0, 0)}
                Size={new UDim2(0.15, 0, 1, 0)}
                Text={OnoeNum.fromSingle(entry.amount).toString()}
                TextColor3={textColor}
                TextScaled={true}
                TextSize={14}
                TextStrokeColor3={strokeColor}
                TextStrokeTransparency={strokeTransparency}
                TextWrapped={true}
            />
            <uicorner CornerRadius={new UDim(0, 9)} />
            <uilistlayout
                FillDirection={Enum.FillDirection.Horizontal}
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                Padding={new UDim(0, 5)}
                SortOrder={Enum.SortOrder.LayoutOrder}
                VerticalAlignment={Enum.VerticalAlignment.Center}
            />
            <uipadding
                PaddingLeft={new UDim(0, 6)}
                PaddingRight={new UDim(0, 6)}
                PaddingTop={new UDim(0, 3)}
                PaddingBottom={new UDim(0, 3)}
            />
        </frame>
    );
}

export default memo(LeaderboardSlot);


import Difficulty from "@rbxts/ejt";
import React, { ReactNode } from "@rbxts/react";
import { RobotoMonoBold, RobotoSlabHeavy } from "shared/asset/GameFonts";

export default function BasicResetBoardFrontGui({
    adornee,
    difficulty,
    children,
}: {
    adornee: BasePart;
    difficulty: Difficulty;
    children: ReactNode;
}) {
    return (
        <surfacegui
            Adornee={adornee}
            ClipsDescendants={true}
            Face={Enum.NormalId.Front}
            MaxDistance={200}
            ResetOnSpawn={false}
            SizingMode={Enum.SurfaceGuiSizingMode.PixelsPerStud}
            ZIndexBehavior={Enum.ZIndexBehavior.Sibling}
        >
            <imagelabel
                BackgroundTransparency={1}
                Image={difficulty.image}
                ImageTransparency={0.9}
                ScaleType={Enum.ScaleType.Crop}
                Size={new UDim2(1, 0, 1, 0)}
            >
                <uipadding
                    PaddingBottom={new UDim(0, 5)}
                    PaddingLeft={new UDim(0, 5)}
                    PaddingRight={new UDim(0, 5)}
                    PaddingTop={new UDim(0, 5)}
                >
                    <uipadding
                        PaddingBottom={new UDim(0, 5)}
                        PaddingLeft={new UDim(0, 5)}
                        PaddingRight={new UDim(0, 5)}
                        PaddingTop={new UDim(0, 5)}
                    />
                </uipadding>
                <frame BackgroundTransparency={1} Size={new UDim2(1, 0, 1, 0)}>
                    <uistroke Color={Color3.fromRGB(255, 255, 255)} Thickness={3} />
                    {children}
                </frame>
                <uilistlayout
                    HorizontalAlignment={Enum.HorizontalAlignment.Center}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                    VerticalAlignment={Enum.VerticalAlignment.Center}
                />
            </imagelabel>
        </surfacegui>
    );
}

export function BasicResetBoardBackGui({
    adornee,
    resetLayer,
    nameOfX,
}: {
    adornee: BasePart;
    resetLayer: ResetLayer;
    nameOfX: string;
}) {
    return (
        <surfacegui
            Adornee={adornee}
            ClipsDescendants={true}
            Face={Enum.NormalId.Back}
            MaxDistance={200}
            ResetOnSpawn={false}
            SizingMode={Enum.SurfaceGuiSizingMode.PixelsPerStud}
            ZIndexBehavior={Enum.ZIndexBehavior.Sibling}
        >
            <textlabel
                AnchorPoint={new Vector2(0.5, 0.5)}
                BackgroundTransparency={1}
                FontFace={RobotoMonoBold}
                LayoutOrder={4}
                RichText={true}
                Position={new UDim2(0.5, 0, 0.5, 0)}
                Size={new UDim2(0.6, 0, 0.1, 0)}
                Text={resetLayer.formula.tostring(nameOfX, 50)}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextSize={60}
                TextStrokeTransparency={0}
                TextWrapped={true}
            >
                <uistroke Thickness={3} />
            </textlabel>
        </surfacegui>
    );
}

export function BasicZoneGui({
    adornee,
    text,
    size = new UDim2(16, 0, 2, 0),
    colorSequence,
}: {
    adornee: BasePart;
    text?: string;
    size?: UDim2;
    colorSequence: ColorSequence;
}) {
    return (
        <billboardgui
            Adornee={adornee}
            Active={true}
            ClipsDescendants={true}
            MaxDistance={500}
            Size={size}
            ZIndexBehavior={Enum.ZIndexBehavior.Sibling}
        >
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoSlabHeavy}
                Size={new UDim2(1, 0, 1, 0)}
                Text={text}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextSize={14}
                TextWrapped={true}
            >
                <uistroke Thickness={2} />
                <uigradient Color={colorSequence} Rotation={90} />
            </textlabel>
        </billboardgui>
    );
}

export function BasicResetBoardTitle({ text, colorSequence }: { text: string; colorSequence: ColorSequence }) {
    return (
        <textlabel
            BackgroundTransparency={1}
            FontFace={RobotoSlabHeavy}
            Size={new UDim2(0.5, 0, 0.125, 0)}
            Text={text}
            TextColor3={Color3.fromRGB(255, 255, 255)}
            TextSize={100}
            TextWrapped={true}
        >
            <uiscale Scale={2} />
            <uistroke Thickness={2} />
            <uigradient Color={colorSequence} Rotation={90} />
        </textlabel>
    );
}


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

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


import React, { Fragment, useEffect, useState } from "@rbxts/react";
import { SkillificationGui } from "client/components/world/reset/SkillificationGui";
import WinificationGui from "client/components/world/reset/WinificationGui";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import { RESET_LAYERS } from "shared/currency/mechanics/ResetLayer";
import Packets from "shared/Packets";

export default function ResetBoardRenderer() {
    const [amountLabelPerLayer, setAmountLabelPerLayer] = useState<Map<ResetLayerId, string>>(new Map());
    const [countdownPerLayer, setCountdownPerLayer] = useState<Map<ResetLayerId, number>>(new Map());

    useEffect(() => {
        const gainConnection = Packets.gainPerResetLayer.observe((gainPerResetLayer) => {
            const amountLabelPerLayer = new Map<ResetLayerId, string>();
            for (const [layer, amount] of gainPerResetLayer) {
                amountLabelPerLayer.set(layer, CurrencyBundle.getFormatted(RESET_LAYERS[layer].gives, amount));
            }
            setAmountLabelPerLayer(amountLabelPerLayer);
        });
        const countdownConnection = Packets.resetCountdown.fromServer((layer, countdown) => {
            setCountdownPerLayer((prev) => {
                prev.set(layer, countdown);
                return table.clone(prev);
            });
        });
        return () => {
            gainConnection.disconnect();
            countdownConnection.Disconnect();
        };
    }, []);

    const skillDelta = amountLabelPerLayer.get("Skillification");
    const winsDelta = amountLabelPerLayer.get("Winification");

    return (
        <Fragment>
            <SkillificationGui
                amountText={skillDelta ?? "???"}
                noticeText={
                    skillDelta
                        ? `Stand on the altar for ${countdownPerLayer.get("Skillification") ?? 0} seconds to reset`
                        : `You need ${CurrencyBundle.getFormatted(RESET_LAYERS.Skillification.scalesWith, RESET_LAYERS.Skillification.minimum)} to reset`
                }
            />
            <WinificationGui
                amountText={winsDelta ?? "???"}
                noticeText={
                    winsDelta
                        ? `Stand on the altar for ${countdownPerLayer.get("Winification") ?? 0} seconds to reset`
                        : `You need ${CurrencyBundle.getFormatted(RESET_LAYERS.Winification.scalesWith, RESET_LAYERS.Winification.minimum)} to reset`
                }
            />
        </Fragment>
    );
}


import Difficulty from "@rbxts/ejt";
import React, { Fragment } from "@rbxts/react";
import BasicResetBoardFrontGui, {
    BasicResetBoardBackGui,
    BasicResetBoardTitle,
    BasicZoneGui,
} from "client/components/world/reset/ResetBoard";
import { RobotoSlabBold, RobotoSlabHeavy } from "shared/asset/GameFonts";
import { RESET_LAYERS } from "shared/currency/mechanics/ResetLayer";
import SkillificationBoard from "shared/world/nodes/SkillificationBoard";
import SkillificationZone from "shared/world/nodes/SkillificationZone";

export function SkillificationGui({ amountText, noticeText }: { amountText: string; noticeText: string }) {
    const adornee = SkillificationBoard.waitForInstance();

    const colorSequence = new ColorSequence([
        new ColorSequenceKeypoint(0, Color3.fromRGB(170, 255, 127)),
        new ColorSequenceKeypoint(1, Color3.fromRGB(105, 255, 122)),
    ]);

    const front = (
        <BasicResetBoardFrontGui adornee={adornee} difficulty={Difficulty.TheFirstDifficulty}>
            <uilistlayout
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                SortOrder={Enum.SortOrder.LayoutOrder}
            />
            <BasicResetBoardTitle text="Skillification" colorSequence={colorSequence} />
            <frame BackgroundTransparency={1} Size={new UDim2(0.9, 0, 0.5, 0)}>
                <textlabel
                    AutomaticSize={Enum.AutomaticSize.Y}
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabBold}
                    Size={new UDim2(1, 0, 0, 0)}
                    Text="The world begs for more as you continue expanding your empire, continuously breaking your own limits."
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextSize={75}
                    TextStrokeTransparency={0}
                    TextWrapped={true}
                    TextYAlignment={Enum.TextYAlignment.Top}
                >
                    <uistroke Thickness={3} />
                </textlabel>
                <textlabel
                    AutomaticSize={Enum.AutomaticSize.Y}
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabBold}
                    Size={new UDim2(1, 0, 0, 0)}
                    Text="Your items, balance, everything that led you this far — sacrifice it all."
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextSize={75}
                    TextStrokeTransparency={0}
                    TextWrapped={true}
                    TextYAlignment={Enum.TextYAlignment.Top}
                >
                    <uistroke Thickness={3} />
                </textlabel>
                <textlabel
                    AutomaticSize={Enum.AutomaticSize.Y}
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabBold}
                    Size={new UDim2(1, 0, 0, 0)}
                    Text="In return, gain Skill, a new currency that can be spent to unlock items and boosts to massively speed up progression."
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextSize={75}
                    TextStrokeTransparency={0}
                    TextWrapped={true}
                    TextYAlignment={Enum.TextYAlignment.Top}
                >
                    <uistroke Thickness={3} />
                </textlabel>
                <uilistlayout
                    HorizontalAlignment={Enum.HorizontalAlignment.Center}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                    VerticalFlex={Enum.UIFlexAlignment.SpaceBetween}
                />
                <textlabel
                    AutomaticSize={Enum.AutomaticSize.Y}
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabBold}
                    Size={new UDim2(1, 0, 0, 0)}
                    Text="Note that this reset layer does not reset any progression past Barren Islands and quests. "
                    TextColor3={Color3.fromRGB(186, 186, 186)}
                    TextSize={50}
                    TextStrokeTransparency={0}
                    TextWrapped={true}
                    TextYAlignment={Enum.TextYAlignment.Top}
                >
                    <uistroke Thickness={3} />
                </textlabel>
            </frame>
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoSlabHeavy}
                Size={new UDim2(0.45, 0, 0.1, 0)}
                Text={amountText}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextSize={80}
                TextStrokeTransparency={0}
                TextWrapped={true}
                TextXAlignment={Enum.TextXAlignment.Right}
            >
                <uistroke Thickness={2} />
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(170, 255, 127)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(105, 255, 122)),
                        ])
                    }
                    Rotation={90}
                />
                <textlabel
                    AnchorPoint={new Vector2(0, 0.5)}
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabBold}
                    LayoutOrder={5}
                    Position={new UDim2(0, 0, 0.5, 0)}
                    Size={new UDim2(0.45, 0, 0.9, 0)}
                    Text="Reset now for:"
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextScaled={true}
                    TextSize={25}
                    TextStrokeTransparency={0}
                    TextWrapped={true}
                    TextXAlignment={Enum.TextXAlignment.Left}
                >
                    <uistroke Thickness={2} />
                </textlabel>
                <uiscale Scale={2} />
            </textlabel>
        </BasicResetBoardFrontGui>
    );

    return (
        <Fragment>
            {front}
            <BasicResetBoardBackGui adornee={adornee} resetLayer={RESET_LAYERS.Skillification} nameOfX="Skill" />
            <BasicZoneGui
                adornee={SkillificationZone.waitForInstance()}
                text={noticeText}
                colorSequence={colorSequence}
            />
        </Fragment>
    );
}


import Difficulty from "@rbxts/ejt";
import React, { Fragment } from "@rbxts/react";
import BasicResetBoardFrontGui, {
    BasicResetBoardBackGui,
    BasicResetBoardTitle,
    BasicZoneGui,
} from "client/components/world/reset/ResetBoard";
import { RobotoSlabBold, RobotoSlabHeavy } from "shared/asset/GameFonts";
import { RESET_LAYERS } from "shared/currency/mechanics/ResetLayer";
import WinificationDetails from "shared/world/nodes/WinificationDetails";
import WinificationReward from "shared/world/nodes/WinificationReward";
import WinificationZone from "shared/world/nodes/WinificationZone";

const colorSequence = new ColorSequence([
    new ColorSequenceKeypoint(0, Color3.fromRGB(0, 85, 255)),
    new ColorSequenceKeypoint(1, Color3.fromRGB(255, 85, 255)),
]);

export function WinificationDetailsGui() {
    const adornee = WinificationDetails.waitForInstance();

    return (
        <Fragment>
            <BasicResetBoardFrontGui adornee={adornee} difficulty={Difficulty.Millisecondless}>
                <uilistlayout
                    HorizontalAlignment={Enum.HorizontalAlignment.Center}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                />
                <BasicResetBoardTitle text="Winification" colorSequence={colorSequence} />
                <frame
                    AutomaticSize={Enum.AutomaticSize.Y}
                    BackgroundTransparency={1}
                    LayoutOrder={4}
                    Size={new UDim2(1, 0, 0.7, 0)}
                >
                    <textlabel
                        AutomaticSize={Enum.AutomaticSize.Y}
                        BackgroundTransparency={1}
                        FontFace={RobotoSlabBold}
                        LayoutOrder={4}
                        Size={new UDim2(0.9, 0, 0, 0)}
                        Text="Yet, you seek more. True victory demands sacrifice beyond reason."
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextSize={75}
                        TextStrokeTransparency={0}
                        TextWrapped={true}
                        TextYAlignment={Enum.TextYAlignment.Top}
                    >
                        <uistroke Thickness={3} />
                    </textlabel>
                    <textlabel
                        AutomaticSize={Enum.AutomaticSize.Y}
                        BackgroundTransparency={1}
                        FontFace={RobotoSlabBold}
                        LayoutOrder={5}
                        Size={new UDim2(0.9, 0, 0, 0)}
                        Text="Everything you’ve built — your Skill, your boosts, your advantages — must be surrendered."
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextSize={75}
                        TextStrokeTransparency={0}
                        TextWrapped={true}
                        TextYAlignment={Enum.TextYAlignment.Top}
                    >
                        <uistroke Thickness={3} />
                    </textlabel>
                    <textlabel
                        AutomaticSize={Enum.AutomaticSize.Y}
                        BackgroundTransparency={1}
                        FontFace={RobotoSlabBold}
                        LayoutOrder={6}
                        Size={new UDim2(0.9, 0, 0, 0)}
                        Text=" But in return, you are reborn stronger than ever, earning Wins, a supreme currency used to unlock insanely powerful items that transcend all previous limits."
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextSize={75}
                        TextStrokeTransparency={0}
                        TextWrapped={true}
                        TextYAlignment={Enum.TextYAlignment.Top}
                    >
                        <uistroke Thickness={3} />
                    </textlabel>
                    <uilistlayout
                        HorizontalAlignment={Enum.HorizontalAlignment.Center}
                        SortOrder={Enum.SortOrder.LayoutOrder}
                        VerticalAlignment={Enum.VerticalAlignment.Center}
                        VerticalFlex={Enum.UIFlexAlignment.SpaceBetween}
                    />
                </frame>
            </BasicResetBoardFrontGui>
        </Fragment>
    );
}

export function WinificationRewardGui({ text }: { text?: string }) {
    const adornee = WinificationReward.waitForInstance();

    return (
        <Fragment>
            <BasicResetBoardFrontGui adornee={adornee} difficulty={Difficulty.Millisecondless}>
                <textlabel
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabHeavy}
                    LayoutOrder={99}
                    Size={new UDim2(0.25, 0, 0.5, 0)}
                    Text={text ?? "???"}
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextSize={80}
                    TextStrokeTransparency={0}
                    TextWrapped={true}
                    TextXAlignment={Enum.TextXAlignment.Right}
                >
                    <uistroke Thickness={2} />
                    <uigradient Color={colorSequence} Rotation={90} />
                    <uiscale Scale={2} />
                </textlabel>
                <uilistlayout
                    FillDirection={Enum.FillDirection.Horizontal}
                    HorizontalAlignment={Enum.HorizontalAlignment.Center}
                    Padding={new UDim(0, 50)}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                    VerticalAlignment={Enum.VerticalAlignment.Center}
                />
                <uistroke Color={Color3.fromRGB(255, 255, 255)} Thickness={3} />
                <textlabel
                    AnchorPoint={new Vector2(0, 0.5)}
                    AutomaticSize={Enum.AutomaticSize.X}
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabBold}
                    LayoutOrder={5}
                    Position={new UDim2(0, 0, 0.5, 0)}
                    Size={new UDim2(0, 0, 0.9, 0)}
                    Text="Reward:"
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextSize={100}
                    TextStrokeTransparency={0}
                    TextWrapped={true}
                    TextXAlignment={Enum.TextXAlignment.Left}
                >
                    <uistroke Thickness={4} />
                </textlabel>
            </BasicResetBoardFrontGui>
            <BasicResetBoardBackGui adornee={adornee} resetLayer={RESET_LAYERS.Winification} nameOfX="skill" />
        </Fragment>
    );
}

export default function WinificationGui({ amountText, noticeText }: { amountText: string; noticeText: string }) {
    return (
        <Fragment>
            <WinificationDetailsGui />
            <WinificationRewardGui text={amountText} />
            <BasicZoneGui
                adornee={WinificationZone.waitForInstance()}
                text={noticeText}
                size={new UDim2(64, 0, 8, 0)}
                colorSequence={colorSequence}
            />
        </Fragment>
    );
}


/**
 * @fileoverview React hook for making elements draggable.
 *
 * Provides drag functionality with position tracking, constraints, and optional callbacks.
 * Works with any GuiObject that supports InputBegan, InputChanged, and InputEnded events.
 */

import { useCallback, useRef, useState, useEffect } from "@rbxts/react";
import { UserInputService, Workspace } from "@rbxts/services";

export interface DragConstraints {
    /** Minimum X position (in offset pixels) */
    minX?: number;
    /** Maximum X position (in offset pixels) */
    maxX?: number;
    /** Minimum Y position (in offset pixels) */
    minY?: number;
    /** Maximum Y position (in offset pixels) */
    maxY?: number;
}

export interface UseDraggableOptions {
    /** Initial position of the draggable element */
    initialPosition?: UDim2;
    /** Constraints for dragging boundaries */
    constraints?: DragConstraints;
    /** Whether dragging is disabled */
    disabled?: boolean;
    /** Whether to automatically constrain to screen bounds (default: true) */
    constrainToScreen?: boolean;
    /** Size of the draggable element (required for screen constraints) */
    elementSize?: UDim2;
    /** Callback when drag starts */
    onDragStart?: (position: UDim2) => void;
    /** Callback during drag (called frequently) */
    onDrag?: (position: UDim2) => void;
    /** Callback when drag ends */
    onDragEnd?: (position: UDim2) => void;
}

export interface DragState {
    /** Whether the element is currently being dragged */
    isDragging: boolean;
    /** Current position of the element */
    position: UDim2;
}

/**
 * Hook that provides dragging functionality for GUI elements
 *
 * @param options Configuration options for dragging behavior
 * @returns Object containing drag state, position, and event handlers
 *
 * @example
 * ```tsx
 * function DraggableWindow() {
 *   const windowSize = new UDim2(0, 300, 0, 200);
 *   const { isDragging, position, dragProps } = useDraggable({
 *     initialPosition: new UDim2(0.5, -150, 0.5, -100),
 *     elementSize: windowSize,
 *     constrainToScreen: true, // Window won't go outside screen bounds
 *     onDragStart: () => print("Started dragging"),
 *     onDragEnd: (pos) => print("Ended at", pos)
 *   });
 *
 *   return (
 *     <frame
 *       Size={windowSize}
 *       Position={position}
 *       Event={{ ...dragProps }}
 *     />
 *   );
 * }
 * ```
 */
export default function useDraggable(options: UseDraggableOptions = {}) {
    const {
        initialPosition = new UDim2(0.5, 0, 0.5, 0),
        constraints,
        disabled = false,
        constrainToScreen = true,
        elementSize,
        onDragStart,
        onDrag,
        onDragEnd,
    } = options;

    const [isDragging, setIsDragging] = useState(false);
    const [position, setPosition] = useState(initialPosition);
    const [screenConstraints, setScreenConstraints] = useState<DragConstraints | undefined>();

    const dragStartRef = useRef<Vector2>();
    const initialPositionRef = useRef<Vector2>();
    const connectionRef = useRef<RBXScriptConnection>();

    // Calculate screen constraints
    useEffect(() => {
        if (!constrainToScreen || !elementSize) {
            setScreenConstraints(undefined);
            return;
        }

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

        const viewportSize = camera.ViewportSize;

        // Calculate element size in pixels
        const elementWidth = elementSize.X.Scale * viewportSize.X + elementSize.X.Offset;
        const elementHeight = elementSize.Y.Scale * viewportSize.Y + elementSize.Y.Offset;

        setScreenConstraints({
            minX: 0,
            maxX: viewportSize.X - elementWidth,
            minY: 0,
            maxY: viewportSize.Y - elementHeight,
        });
    }, [constrainToScreen, elementSize]);

    const applyConstraints = useCallback(
        (newPosition: UDim2): UDim2 => {
            // Combine manual constraints with screen constraints
            const combinedConstraints: DragConstraints = {
                ...screenConstraints,
                ...constraints, // Manual constraints override screen constraints
            };

            // Check if we have any constraints to apply
            const hasConstraints =
                combinedConstraints.minX !== undefined ||
                combinedConstraints.maxX !== undefined ||
                combinedConstraints.minY !== undefined ||
                combinedConstraints.maxY !== undefined;

            if (!hasConstraints) return newPosition;

            const offsetX = newPosition.X.Offset;
            const offsetY = newPosition.Y.Offset;

            let constrainedX = offsetX;
            let constrainedY = offsetY;

            if (combinedConstraints.minX !== undefined) constrainedX = math.max(constrainedX, combinedConstraints.minX);
            if (combinedConstraints.maxX !== undefined) constrainedX = math.min(constrainedX, combinedConstraints.maxX);
            if (combinedConstraints.minY !== undefined) constrainedY = math.max(constrainedY, combinedConstraints.minY);
            if (combinedConstraints.maxY !== undefined) constrainedY = math.min(constrainedY, combinedConstraints.maxY);

            return new UDim2(newPosition.X.Scale, constrainedX, newPosition.Y.Scale, constrainedY);
        },
        [constraints, screenConstraints],
    );

    const onInputBegan = useCallback(
        (rbx: GuiObject, input: InputObject) => {
            if (disabled) return;

            if (
                input.UserInputType === Enum.UserInputType.MouseButton1 ||
                input.UserInputType === Enum.UserInputType.Touch
            ) {
                setIsDragging(true);
                dragStartRef.current = new Vector2(input.Position.X, input.Position.Y);
                initialPositionRef.current = new Vector2(position.X.Offset, position.Y.Offset);

                onDragStart?.(position);

                // Connect to user input service for global mouse/touch movement
                connectionRef.current = UserInputService.InputChanged.Connect((changedInput) => {
                    if (
                        changedInput.UserInputType === Enum.UserInputType.MouseMovement ||
                        changedInput.UserInputType === Enum.UserInputType.Touch
                    ) {
                        if (dragStartRef.current && initialPositionRef.current) {
                            const delta = new Vector2(
                                changedInput.Position.X - dragStartRef.current.X,
                                changedInput.Position.Y - dragStartRef.current.Y,
                            );

                            const newPosition = new UDim2(
                                position.X.Scale,
                                initialPositionRef.current.X + delta.X,
                                position.Y.Scale,
                                initialPositionRef.current.Y + delta.Y,
                            );

                            const constrainedPosition = applyConstraints(newPosition);
                            setPosition(constrainedPosition);
                            onDrag?.(constrainedPosition);
                        }
                    }
                });

                // Connect to input ended to stop dragging
                const endConnection = UserInputService.InputEnded.Connect((endedInput) => {
                    if (
                        endedInput.UserInputType === Enum.UserInputType.MouseButton1 ||
                        endedInput.UserInputType === Enum.UserInputType.Touch
                    ) {
                        setIsDragging(false);
                        dragStartRef.current = undefined;
                        initialPositionRef.current = undefined;

                        connectionRef.current?.Disconnect();
                        endConnection.Disconnect();

                        onDragEnd?.(position);
                    }
                });
            }
        },
        [disabled, position, applyConstraints, onDragStart, onDrag, onDragEnd],
    );

    // Cleanup connection on unmount
    const cleanup = useCallback(() => {
        connectionRef.current?.Disconnect();
    }, []);

    return {
        isDragging,
        position,
        dragProps: {
            InputBegan: onInputBegan,
        },
        setPosition,
        cleanup,
    };
}


import { JSX, useEffect, useMemo } from "@rbxts/react";
import { createRoot } from "@rbxts/react-roblox";
import { IS_EDIT } from "shared/Context";

export default function useExternalRender({ element, parent }: { element: JSX.Element; parent: Instance }) {
    const root = useMemo(() => {
        return createRoot(parent);
    }, [parent]);
    useEffect(() => {
        root.render(element);
        if (IS_EDIT) {
            print("Rendered to", parent);
        }
        return () => root.unmount();
    }, [root]);
}


/**
 * @fileoverview React hook for managing hover states.
 *
 * Provides a simple `useHover` hook that returns hover state and event handlers,
 * eliminating the need to manually write MouseEnter/MouseLeave events every time.
 */

import { useCallback, useState } from "@rbxts/react";

declare global {
    interface UseHoverProps {
        onMoved?: () => void;
        onEnter?: () => void;
        onLeave?: () => void;
        initialState?: boolean;
    }

    type UseHoverReturn = ReturnType<typeof useHover>;
}

/**
 * Hook that provides hover state with custom callbacks
 *
 * @param onMove Callback when mouse moves
 * @param onEnter Callback when mouse enters
 * @param onLeave Callback when mouse leaves
 * @param initialState Initial hover state (defaults to false)
 * @returns Object containing hover state and event handlers
 *
 * @example
 * ```tsx
 * function MyButton() {
 *   const { hovering, events } = useHoverWithCallbacks(
 *     () => playSound("hover.mp3"),
 *     () => playSound("unhover.mp3")
 *   );
 *
 *   return (
 *     <textbutton
 *       Text="Button"
 *       Event={{ ...events }}
 *     />
 *   );
 * }
 * ```
 */
export default function useHover(props: UseHoverProps) {
    const [hovering, setHovering] = useState(props.initialState);

    const onMoved = useCallback(() => {
        setHovering(true);
        props.onMoved?.();
    }, [props.onMoved]);

    const onEnter = useCallback(() => {
        setHovering(true);
        props.onEnter?.();
    }, [props.onEnter]);

    const onLeave = useCallback(() => {
        setHovering(false);
        props.onLeave?.();
    }, [props.onLeave]);

    return {
        hovering,
        onEnter,
        onLeave,
        events: {
            MouseMoved: onMoved,
            MouseEnter: onEnter,
            MouseLeave: onLeave,
        },
    };
}


import { DependencyList, useEffect } from "@rbxts/react";

/**
 * A hook that runs a callback at specified intervals.
 * @param callback A function that returns the time in seconds until the next call.
 * @param deps An array of dependencies that will trigger the effect when changed.
 */
export default function useInterval(callback: () => number, deps: DependencyList) {
    useEffect(() => {
        let active = true;
        const loop = () => {
            if (!active) return;
            const waitTime = callback();
            task.delay(waitTime, loop);
        };
        loop();
        return () => {
            active = false;
        };
    }, deps);
}


import AbstractPropertyPacket from "@rbxts/fletchette/out/AbstractPropertyPacket";
import { useEffect, useState } from "@rbxts/react";
import { IS_EDIT } from "shared/Context";

/**
 * A custom hook for observing a property packet and getting its current value.
 *
 * @param propertyPacket The property packet to observe.
 * @returns The current value of the property.
 */
function useProperty<T>(propertyPacket: AbstractPropertyPacket<T>) {
    // Allow property to be T | undefined for correct typing
    const [property, setProperty] = useState<T | undefined>(propertyPacket.get());

    useEffect(() => {
        // Set the state every time the server sends a property update
        const connection = propertyPacket.observe((newProperty) => {
            if (IS_EDIT && typeIs(newProperty, "table")) {
                newProperty = table.clone(newProperty);
            }
            setProperty(newProperty);
        });
        return () => connection.disconnect();
    }, [propertyPacket]);

    return property!;
}

export = useProperty;


/**
 * @fileoverview React hook for handling text animation in dialogue windows.
 *
 * Provides typing effect with character-by-character reveal, sound playback,
 * and timing variations for punctuation marks. Based on the original DialogueController logic.
 */

import { RefObject, useEffect, useState } from "@rbxts/react";
import { Debris, ReplicatedStorage, RunService } from "@rbxts/services";
import { getSound } from "shared/asset/GameAssets";
import Packets from "shared/Packets";

declare global {
    interface Assets {
        NPCTextSounds: Folder;
    }

    interface UseTextAnimationProps {
        /** The full text to animate */
        text: string;
        /** Reference to the text label that displays the animated text */
        textLabelRef: RefObject<TextLabel>;
        /** Reference to the hint label that shows when animation is complete */
        hintLabelRef: RefObject<TextLabel>;
        /** Whether the dialogue window is visible */
        visible: boolean;
        /** Sound to play for text typing (optional) */
        textSound?: Sound;
    }
}

/**
 * Hook that provides text typing animation for dialogue windows
 *
 * @param props Configuration object for text animation
 * @returns Object containing current display text and completion status
 */
export function useTextAnimation({ text, textLabelRef, hintLabelRef, visible, textSound }: UseTextAnimationProps) {
    const [displayText, setDisplayText] = useState("");
    const [charIndex, setCharIndex] = useState(0);
    const [isComplete, setIsComplete] = useState(false);

    const defaultTextSound = (function () {
        const defaultText = getSound("DefaultText.mp3").Clone();
        defaultText.Volume = 0.25;
        return defaultText;
    })();

    useEffect(() => {
        if (!visible || !text) {
            setDisplayText("");
            setCharIndex(0);
            setIsComplete(false);
            return;
        }

        // Reset animation when text changes
        setDisplayText("");
        setCharIndex(0);
        setIsComplete(false);
    }, [text, visible]);

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

        let connection: RBXScriptConnection | undefined;
        let time = 0;
        let minDeltaTime = 0.03;

        const updateAnimation = (deltaTime: number) => {
            time += deltaTime;
            if (time < minDeltaTime) return;
            time = 0;

            if (charIndex < text.size()) {
                const newIndex = charIndex + 1;
                const newDisplayText = text.sub(1, newIndex);
                const currentChar = text.sub(newIndex, newIndex);
                const isSpace = currentChar === " ";

                setDisplayText(newDisplayText);
                setCharIndex(newIndex);

                // Update hint label visibility
                if (hintLabelRef.current) {
                    hintLabelRef.current.Visible = false;
                }

                // Play sound for non-space characters
                if (!isSpace) {
                    const sound = (textSound ?? defaultTextSound).Clone();
                    sound.Parent = ReplicatedStorage;
                    if (Packets.settings.get()?.SoundEffects) {
                        sound.Play();
                    }
                    Debris.AddItem(sound);
                }

                // Adjust timing based on character type
                const lastChar = text.sub(newIndex - 1, newIndex - 1);
                const isPunctuation = lastChar === "." || lastChar === "?" || lastChar === "!";
                minDeltaTime = isSpace ? (isPunctuation ? 0.3 : lastChar === "," ? 0.15 : 0.03) : 0.03;
            } else {
                // Animation complete
                setIsComplete(true);
                if (hintLabelRef.current) {
                    hintLabelRef.current.Visible = true;
                }
            }
        };

        connection = RunService.Heartbeat.Connect(updateAnimation);

        return () => {
            if (connection) {
                connection.Disconnect();
            }
        };
    }, [charIndex, text, visible, textSound, hintLabelRef]);

    return {
        displayText,
        isComplete,
        skipToEnd: () => {
            if (charIndex < text.size()) {
                setDisplayText(text);
                setCharIndex(text.size());
                setIsComplete(true);
                if (hintLabelRef.current) {
                    hintLabelRef.current.Visible = true;
                }
            }
        },
    };
}


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

/**
 * A custom hook that sets the visibility of a window whenever the `id` or `visible` parameters change.
 * @internal Should only be used in storybooks.
 * @param id The unique identifier for the window
 * @param visible A boolean indicating whether the window should be visible
 */
export default function useVisibility(id: string, visible: boolean) {
    useEffect(() => {
        DocumentManager.setVisible(id, visible);
    }, [id, visible]);
}

/**
 * A custom hook that opens or closes a window in the SingleDocumentManager whenever the `id` or `visible` parameters change.
 * @internal Should only be used in storybooks.
 * @param id The unique identifier for the window
 * @param visible A boolean indicating whether the window should be visible
 */
export function useSingleDocumentVisibility(id: string, visible: boolean) {
    useEffect(() => {
        if (visible) {
            SingleDocumentManager.open(id);
        } else {
            SingleDocumentManager.close(id);
        }
    }, [id, visible]);
}

/**
 * Sets the visibility of main UI documents.
 * @param visible Whether the main UI documents should be visible
 * @internal Should only be used in storybooks.
 */
export function setVisibilityMain(visible: boolean) {
    DocumentManager.setVisible("Backpack", visible);
    DocumentManager.setVisible("Balance", visible);
    DocumentManager.setVisible("Sidebar", visible);
    DocumentManager.setVisible("Position", visible);
    DocumentManager.setVisible("TrackedQuest", visible);
}


//!optimize 2
import { PathfindingService, Workspace } from "@rbxts/services";
import { ASSETS } from "shared/asset/GameAssets";

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

function unstuck(character: Model) {
    const humanoid = character.WaitForChild("Humanoid") as Humanoid;
    humanoid.Move(new Vector3(math.random(-1, 1), 0, math.random(-1, 1)));
    humanoid.Jump = true;
    task.wait(0.5);
}

function getPathingFactory(character: Model) {
    const humanoid = character.WaitForChild("Humanoid") as Humanoid;
    const rootPart = humanoid.RootPart ?? (character.WaitForChild("HumanoidRootPart") as BasePart);

    let pathCount = 0;

    const pathToLocation = (location: Vector3) => {
        const path = PathfindingService.CreatePath();
        path.ComputeAsync(rootPart.Position, location);
        pathCount += 1;

        if (path.Status === Enum.PathStatus.Success) {
            const currentPathCount = pathCount;
            for (const waypoint of path.GetWaypoints()) {
                if (currentPathCount !== pathCount) {
                    return; // Path was recalculated, exit early
                }
                if (waypoint.Action === Enum.PathWaypointAction.Jump) {
                    humanoid.Jump = true;
                }
                humanoid.MoveTo(waypoint.Position);
                task.delay(0.5, () => {
                    if (humanoid.WalkToPoint.Y > rootPart.Position.Y) {
                        humanoid.Jump = true;
                    }
                });
                const moveSucccess = humanoid.MoveToFinished.Wait();
                if (!moveSucccess) {
                    break;
                }
            }
        } else if (path.Status === Enum.PathStatus.NoPath || path.Status === Enum.PathStatus.ClosestNoPath) {
            return;
        } else {
            unstuck(character);
        }
    };

    return pathToLocation;
}

function loadWanderer(character: Model) {
    for (const child of character.GetChildren()) {
        if (child.IsA("BasePart")) {
            child.CollisionGroup = "NPC";
        }
    }

    const pathToLocation = getPathingFactory(character);
    const humanoid = character.WaitForChild("Humanoid") as Humanoid;
    const rootPart = humanoid.RootPart ?? (character.WaitForChild("HumanoidRootPart") as BasePart);

    const wander = () => {
        let randX = math.random(-60, 60);
        randX += math.sign(randX) * 10; // Ensure it's not too close to zero
        let randZ = math.random(-60, 60);
        randZ += math.sign(randZ) * 10;
        const goal = rootPart.Position.add(new Vector3(randX, 0, randZ));
        pathToLocation(goal);
    };

    task.spawn(() => {
        while (task.wait()) {
            wander();
            task.wait(math.random(30, 60)); // Make this massive to avoid performance issues
        }
    });
}

// disabled until performance is improved
const enabled = false;
if (enabled) {
    for (const startPos of Workspace.WaitForChild("Wanderers").GetChildren()) {
        if (!startPos.IsA("BasePart")) continue;

        startPos.Transparency = 1;

        const name = startPos.Name;
        const character = ASSETS.Wanderers.WaitForChild(name).Clone() as Model;
        const animateScript = character.FindFirstChild("Animate") as Script | undefined;

        character.PivotTo(startPos.CFrame);
        character.Parent = Workspace;
        loadWanderer(character);
        if (animateScript !== undefined) {
            animateScript.Enabled = true;
        }

        startPos.Destroy();
    }
}


import React, { Fragment } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { Workspace } from "@rbxts/services";
import { CreateReactStory } from "@rbxts/ui-labs";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            name: "MODELNAMEHERE",
        },
    },
    (props) => {
        const model = Workspace.FindFirstChild(props.controls.name);
        if (!model || !model.IsA("Model")) {
            return <Fragment />;
        }
        const modelClone = model.Clone();
        for (const child of modelClone.GetDescendants()) {
            if (child.IsA("BasePart")) {
                const snappedSize = new Vector3(
                    math.round(child.Size.X * 8) / 8,
                    math.round(child.Size.Y * 8) / 8,
                    math.round(child.Size.Z * 8) / 8,
                );
                // Snap minimum corner to 1/8th grid, then recenter
                const minCorner = child.Position.sub(child.Size.div(2));
                const snappedMinCorner = new Vector3(
                    math.round(minCorner.X * 8) / 8,
                    math.round(minCorner.Y * 8) / 8,
                    math.round(minCorner.Z * 8) / 8,
                );
                const snappedPosition = snappedMinCorner.add(snappedSize.div(2));
                child.Size = snappedSize;
                child.Position = snappedPosition;
            }
        }
        modelClone.Parent = Workspace;

        return <Fragment />;
    },
);


import React, { Fragment } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { Workspace } from "@rbxts/services";
import { CreateReactStory } from "@rbxts/ui-labs";

function cleanModel(model: Model) {
    let cleaned = false;
    for (const child of model.GetChildren()) {
        if (child.Name === "Conveyor") {
            for (const attachment of child.GetChildren()) {
                if (
                    attachment.Name === "ClientObject" ||
                    attachment.Name === "Speed" ||
                    attachment.Name === "ClientObjectScript"
                ) {
                    attachment.Destroy();
                    cleaned = true;
                    continue;
                }
                if (attachment.Name !== "Attachment") continue;
                const beam = attachment.FindFirstChildOfClass("Beam");
                if (beam) {
                    cleaned = true;
                    attachment.Name = "Attachment0";
                    beam.Destroy();
                } else {
                    cleaned = true;
                    attachment.Name = "Attachment1";
                }
            }
        }
    }
    if (cleaned) {
        print(model);
    }
}

/**
 * This story cleans models in the workspace according to conveyor/attachment rules.
 */
export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
    },
    () => {
        for (const v of Workspace.GetChildren()) {
            if (!v.IsA("Model")) continue;

            if (!v.PrimaryPart) {
                const hitbox = v.FindFirstChild("Hitbox");
                if (hitbox && hitbox.IsA("BasePart")) {
                    v.PrimaryPart = hitbox;
                } else {
                    continue;
                }
            }

            if (v.PrimaryPart && v.PrimaryPart.Name === "Hitbox") {
                cleanModel(v);
            }
        }

        return <Fragment />;
    },
);


import React, { Fragment } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { DataStoreService, Players } from "@rbxts/services";
import { CreateReactStory } from "@rbxts/ui-labs";
import AvailableEmpire from "shared/data/AvailableEmpire";
import { EmpireProfileManager } from "shared/data/profile/ProfileManager";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            username: "migeru_tan",
            userId: 0,
            empireId: "EMPIREID_HERE",
            empireIndex: 0,
            copyTo: "COPIED_DATA",
        },
    },
    (props) => {
        let { username, userId, empireId, empireIndex, copyTo } = props.controls;
        if (userId === 0) {
            userId = Players.GetUserIdFromNameAsync(username);
            print(userId);
        }
        if (empireId === "EMPIREID_HERE") {
            empireId = AvailableEmpire.viewPlayerData(userId).ownedEmpires[empireIndex];
            print(empireId);
        }
        const profile = EmpireProfileManager.load(empireId, true);

        const key = EmpireProfileManager.getKey(copyTo);
        DataStoreService.GetDataStore(EmpireProfileManager.storeName).SetAsync(key, profile?.Data);
        print(`Copied data to key ${key}`);
        print(profile?.Data);

        return <Fragment />;
    },
);


import React, { Fragment } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { Workspace } from "@rbxts/services";
import { CreateReactStory } from "@rbxts/ui-labs";

/**
 * This story prints all poly parts in workspace descendants that are collidable and not named Ghost/Decoration.
 */
export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
    },
    () => {
        for (const v of Workspace.GetDescendants()) {
            const isPoly =
                v.IsA("WedgePart") ||
                v.IsA("CornerWedgePart") ||
                (v.IsA("Part") &&
                    (v.Shape === Enum.PartType.Wedge ||
                        v.Shape === Enum.PartType.CornerWedge ||
                        v.Shape === Enum.PartType.Cylinder));
            if (isPoly && v.Name !== "Ghost" && v.Name !== "Decoration" && v.CanCollide) {
                print(v);
            }
        }
        return <Fragment />;
    },
);


import Difficulty from "@rbxts/ejt";
import React, { Fragment } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import Item from "shared/item/Item";
import Items from "shared/items/Items";

/**
 * This story prints all poly parts in workspace descendants that are collidable and not named Ghost/Decoration.
 */
export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
    },
    () => {
        const inaccessibleItems = new Set<Item>();
        for (const item of Items.sortedItems) {
            if (item.difficulty === Difficulty.Bonuses) continue;
            inaccessibleItems.add(item);
        }
        for (const item of Items.sortedItems) {
            const shop = item.findTrait("Shop");
            if (shop) {
                for (const entry of shop.items) {
                    inaccessibleItems.delete(entry);
                }
            }
        }
        print("Inaccessible items:");
        for (const item of inaccessibleItems) {
            print(`- ${item.name}`);
        }

        return <Fragment />;
    },
);


import React, { Fragment } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import Items from "shared/items/Items";

/**
 * This story prints all poly parts in workspace descendants that are collidable and not named Ghost/Decoration.
 */
export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
    },
    () => {
        const fullInventory = new Map<string, number>();
        for (const item of Items.sortedItems) {
            fullInventory.set(item.id, item.pricePerIteration.size());

            for (const [requiredItem, amount] of item.requiredItems) {
                const currentAmount = fullInventory.get(requiredItem) ?? 0;
                fullInventory.set(requiredItem, currentAmount - amount);
            }
        }
        for (const [itemId, amount] of fullInventory) {
            if (amount <= 0) continue;
            print(`${itemId}: ${amount}`);
        }

        return <Fragment />;
    },
);


import { OnoeNum } from "@rbxts/serikanum";
import React from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { DataStoreService } from "@rbxts/services";
import { CreateReactStory } from "@rbxts/ui-labs";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            store: "Funds",
        },
    },
    (props) => {
        const parsed = DataStoreService.GetOrderedDataStore(props.controls.store).GetSortedAsync(false, 100);
        const page = parsed.GetCurrentPage();
        const content = new Array<{ position: number; player: string; score: string }>();
        for (let i = 0; i < page.size(); i++) {
            const entry = page[i];
            if (entry === undefined) continue;
            const value = OnoeNum.fromSingle(entry.value as number).toString();
            content.push({ position: i + 1, player: entry.key, score: value });
            print(`${i + 1}. ${entry.key} - ${value}`); // For debugging purposes
        }

        return (
            <scrollingframe
                AutomaticCanvasSize={Enum.AutomaticSize.Y}
                BackgroundColor3={new Color3(1, 1, 1)}
                Size={UDim2.fromScale(1, 1)}
            >
                <uilistlayout Padding={new UDim(0, 5)} SortOrder={Enum.SortOrder.LayoutOrder} />
                {content.map((entry) => (
                    <textlabel
                        Size={new UDim2(1, 0, 0, 50)}
                        BackgroundTransparency={1}
                        Text={`${entry.position}. ${entry.player} - ${entry.score}`}
                    />
                ))}
            </scrollingframe>
        );
    },
);


import React, { Fragment } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { Workspace } from "@rbxts/services";
import { CreateReactStory } from "@rbxts/ui-labs";

/**
 * This story checks for unanchored parts and non-0/1 transparency parts in workspace descendants.
 */
export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
    },
    () => {
        // Check for unanchored parts
        for (const v of Workspace.GetDescendants()) {
            if (v.IsA("BasePart") && !v.Anchored) {
                print(v);
            }
        }

        // Check for non-0/1 transparency parts
        for (const v of Workspace.GetDescendants()) {
            if (
                v.IsA("BasePart") &&
                v.Transparency !== 0 &&
                v.Transparency !== 1 &&
                v.Name !== "Antighost" &&
                v.Name !== "Laser"
            ) {
                print(v);
            }
        }

        // Snap models to baseplate (commented out, see Lua for reference)
        // const ITEM_MODELS = (() => {
        //     const itemModels: Record<string, Model> = {};
        //     const folder = Workspace.ItemModels;
        //     const findModels = (instance: Instance): Model[] => {
        //         let models: Model[] = [];
        //         for (const child of instance.GetChildren()) {
        //             if (child.IsA("Folder")) {
        //                 models = models.concat(findModels(child));
        //             } else if (child.IsA("Model")) {
        //                 models.push(child as Model);
        //             }
        //         }
        //         return models;
        //     };
        //     const served = findModels(folder);
        //     for (const model of served) {
        //         itemModels[model.Name] = model;
        //     }
        //     return itemModels;
        // })();
        // const BuildBounds = require(game.ReplicatedStorage.TS.utils.BuildBounds);
        // const bb = BuildBounds.new(Workspace.Baseplate);
        // for (const [name, v] of pairs(ITEM_MODELS)) {
        //     const pp = v.PrimaryPart;
        //     if (!pp) {
        //         warn(`no pp for ${name}`);
        //         continue;
        //     }
        //     (v as Model).PivotTo(bb.snap(pp.Size, pp.Position, 0));
        // }

        return <Fragment />;
    },
);


import React, { Fragment } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import Item from "shared/item/Item";
import Items from "shared/items/Items";

/**
 * This story prints out all items that are not in any shop.
 */
export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
    },
    () => {
        const itemsInShop = new Set<Item>();
        for (const item of Items.sortedItems) {
            const shop = item.findTrait("Shop");
            if (!shop) continue;

            for (const shopItem of shop.items) {
                itemsInShop.add(shopItem);
            }
        }
        for (const item of Items.sortedItems) {
            if (item.findTrait("Shop")) continue;
            if (itemsInShop.has(item)) continue;
            print(item.id);
        }

        return <Fragment />;
    },
);


import { Storybook } from "@rbxts/ui-labs";

const storybook: Storybook = {
    name: "Quality Assurance",
    storyRoots: [script.Parent!],
};

export = storybook;


import "client/react-config";

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

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
    },
    () => {
        StoryMocking.mockPhysics();
        StoryMocking.mockCharacter();
        ItemViewport.disable();

        useEffect(() => {
            const flameworkContext = manuallyIgniteFlamework();
            return () => {
                flameworkContext.cleanup();
                ItemViewport.cleanup();
            };
        }, []);

        return (
            <Fragment>
                <App />
                <SimulationCommandInterface />
            </Fragment>
        );
    },
);


import { Storybook } from "@rbxts/ui-labs";

const storybook: Storybook = {
    name: "Simulations",
    storyRoots: [script.Parent!],
};

export = storybook;


import "client/react-config";

import React from "@rbxts/react";
import { createRoot } from "@rbxts/react-roblox";
import { ReplicatedStorage } from "@rbxts/services";
import App from "client/components/App";
import eat from "shared/hamster/eat";

const container = new Instance("Folder");
container.Name = "AppContainer";
container.Parent = ReplicatedStorage;
const root = createRoot(container);
root.render(<App />);
eat(() => {
    root.unmount();
    container.Destroy();
});


import Signal, { Connection } from "@antivivi/lemon-signal";
import { ProximityPromptService } from "@rbxts/services";
import { Dialogue } from "server/interactive/npc/NPC";
import eat from "shared/hamster/eat";
import { Identifiable, ModuleRegistry } from "shared/hamster/ModuleRegistry";
import { SingleWorldNode } from "shared/world/nodes/WorldNode";

class InteractableObject extends Identifiable {
    static readonly REGISTRY = new ModuleRegistry<InteractableObject>(script.Parent!, new Set([script]));

    worldNode?: SingleWorldNode;
    readonly interacted = new Signal<(player: Player) => void>();
    dialogue?: Dialogue;
    dialogueInteractConnection?: Connection;

    dialogueUponInteract(dialogue: Dialogue) {
        this.dialogue = dialogue;
        if (this.dialogueInteractConnection === undefined)
            this.dialogueInteractConnection = this.interacted.connect(() => {
                if (this.dialogue !== undefined) this.dialogue.talk();
            });
        return this;
    }

    onInteract(callback: (player: Player, object: this) => void) {
        eat(this.interacted.connect((player) => callback(player, this)));
        return this;
    }

    getWorldNode() {
        this.worldNode ??= new SingleWorldNode(this.id);
        return this.worldNode;
    }

    override init() {
        return () => {
            this.dialogueInteractConnection?.disconnect();
            table.clear(this);
        };
    }

    static {
        const promptTriggeredConnection = ProximityPromptService.PromptTriggered.Connect((prompt, player) => {
            if (Dialogue.isInteractionEnabled === false || prompt.Parent === undefined) return;
            const interactableObject = InteractableObject.REGISTRY.OBJECTS.get(prompt.Parent.Name);
            if (interactableObject === undefined) return;
            Dialogue.proximityPrompts.add(prompt);
            interactableObject.interacted.fire(player);
        });
        eat(promptTriggeredConnection, "Disconnect");
    }
}

export = InteractableObject;


import { Players, RunService } from "@rbxts/services";

const IS_EDIT = RunService.IsStudio() && (!RunService.IsRunning() || (RunService.IsServer() && RunService.IsClient()));
if (!IS_EDIT) {
    // Only import modules if not in edit mode to avoid duplicate runtime errors in simulation stories.

    const [s1, sandbox] = import("shared/Sandbox").await();
    if (!s1) {
        throw "Failed to load sandbox";
    }
    const Sandbox = sandbox.default;
    if (!Sandbox.synthesise()) {
        Players.CharacterAutoLoads = false;
    }

    const [s2, context] = import("shared/Context").await();
    if (!s2) {
        throw "Failed to load context";
    }
    context.igniteFlameworkServer();
}


import { RunService } from "@rbxts/services";
import { Dialogue } from "server/interactive/npc/NPC";
import Prest from "server/interactive/npc/Prest";
import Tria from "server/interactive/npc/Tria";
import Quest, { Stage } from "server/quests/Quest";
import { Server } from "shared/api/APIExpose";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import SkillPod from "shared/items/0/millisecondless/SkillPod";
import GrassConveyor from "shared/items/negative/friendliness/GrassConveyor";

const prestAnnoyance = new Dialogue(
    Prest,
    "If you don't want to purchase anything, then scram. I have other customers waiting for me.",
);

export = new Quest(script.Name)
    .setName("Friendly Negotiations")
    .setLength(1)
    .setLevel(2)
    .addStage(
        new Stage()
            .setDescription(`Talk to Tria again at %coords%.`)
            .setNPC(Tria, true)
            .setDialogue(
                new Dialogue(
                    Tria,
                    `O-Oh! Hi again... Uh, I... I see you've been... doing well around here? I guess... people know your name now.`,
                )
                    .monologue(
                        "Um... so... I have a... something to ask you... You know that guy... selling crates and logs outside my place?",
                    )
                    .monologue(
                        "Normally, I wouldn't mind... I mean, it's not like it's huge deal... but... he's charging... um... kinda ridiculous prices!",
                    )
                    .monologue(
                        "It... it kinda makes me look like I'm one of his... associates too... which is... ugh... not great.",
                    )
                    .monologue(
                        "Could you maybe... find a way to... make him change his prices? Uh... however you want... I trust you... I think.",
                    ).root,
            )
            .onReached((stage) => {
                const replacement = new Dialogue(
                    Prest,
                    "Buy your crates and logs here! Wait, nevermind. We're out of stock.",
                );
                replacement.add();
                const connection = stage.dialogue!.finished.connect(() => {
                    stage.complete();
                });
                return () => {
                    connection.disconnect();
                    replacement.remove();
                };
            }),
    )
    .addStage(
        new Stage()
            .setDescription(`Negotiate with Prest at %coords%.`)
            .setNPC(Prest, true)
            .setDialogue(
                new Dialogue(
                    Prest,
                    `Buy your crates and logs here! Seriously, only for ${new CurrencyBundle().set("Funds", 1e18).toString()}! These are the best quality you'll find anywhere.`,
                )
                    .monologue(
                        "Hey, customer! What are you waiting for? Give me your money and get some crates and logs! This is a once-in-a-lifetime offer, so don't miss out!",
                    )
                    .monologue(
                        "What, you want the prices lowered? No can do. They're already the cheapest they can be! You won't find a better deal, I promise you.",
                    )
                    .monologue(`...Unless, you have a ${GrassConveyor.name}. Hah, I know you don't.`).root,
            )
            .onReached((stage) => {
                const replacement = new Dialogue(
                    Tria,
                    "O-oh! Uh... y-you're... you're not supposed to... um... be talking to me right now, I... I mean... sorry! I... I didn't mean to... ugh, never mind!",
                );
                replacement.add();

                const replacement2 = new Dialogue(
                    Tria,
                    "I... I just... uh... wanted to say something, but... maybe I shouldn't...",
                ).monologue("P-please... just... come back later, okay? I... I'll explain then...").root;

                const replacementConn = replacement.finished.connect(() => {
                    replacement.remove();
                    replacement2.add();
                });
                const stageDialogueConn = stage.dialogue!.finished.connect(() => {
                    stage.complete();
                });
                return () => {
                    replacementConn.disconnect();
                    stageDialogueConn.disconnect();
                    replacement.remove();
                    replacement2.remove();
                };
            }),
    )
    .addStage(
        new Stage()
            .setDescription(`Get a ${GrassConveyor.name}.`)
            .setDialogue(
                new Dialogue(Tria, `H-He said he wants a ${GrassConveyor.name}? Uh... wow.`)
                    .monologue(
                        "What a... um... jerk, right? Unfortunately... I don't think he'll... bend any other way.",
                    )
                    .monologue(
                        "You... you'll have to do this yourself... sorry. It costs a bit, but... I believe in you, maybe...",
                    ).root,
            )
            .onReached((stage) => {
                prestAnnoyance.add();
                let t = 0;
                const ItemService = Server.Item;
                const connection = RunService.Heartbeat.Connect((dt) => {
                    t += dt;
                    if (t < 0.5) return;
                    t = 0;
                    if (ItemService.getBoughtAmount(GrassConveyor) > 0) {
                        stage.complete();
                    }
                });
                return () => {
                    connection.Disconnect();
                    prestAnnoyance.remove();
                };
            }),
    )
    .addStage(
        new Stage()
            .setDescription(`Return to Prest with the ${GrassConveyor.name}.`)
            .setDialogue(new Dialogue(Prest, "Can you stop bothering me already?").root)
            .setNPC(Prest, true)
            .onReached((stage) => {
                const replacement = new Dialogue(
                    Tria,
                    "O-okay... uh... I... I hope this is... fine? I mean... I don't know if it's fine... ugh...",
                )
                    .monologue(
                        "W-wait... did I... say the wrong thing? I... I just... uh... hope he doesn't get mad...",
                    )
                    .monologue("P-please... don't mess this up... I... I believe in you, maybe...").root;
                replacement.add();

                const continuation = new Dialogue(Prest, "Wait... Is that actually it?")
                    .monologue("...")
                    .monologue("......")
                    .monologue(
                        `Actually, I also said I wanted a ${SkillPod.name}. I'll be taking that ${GrassConveyor.name} though.`,
                    )
                    .monologue(
                        `I know you don't have a ${SkillPod.name}, but I'm gonna be nice and hold onto your ${GrassConveyor.name} for you.`,
                    )
                    .monologue("See you again never!").root;
                const stageDialogueConn = stage.dialogue!.finished.connect(() => {
                    continuation.talk();
                });
                const continuationConn = continuation.finished.connect(() => {
                    if (Server.Quest.takeQuestItem(GrassConveyor, 1) === true) {
                        stage.complete();
                    }
                });
                return () => {
                    stageDialogueConn.disconnect();
                    continuationConn.disconnect();
                    replacement.remove();
                };
            }),
    )
    .addStage(
        new Stage()
            .setDescription("Negotiating with him any further would be futile. Get assistance from Tria.")
            .setNPC(Tria, true)
            .setDialogue(
                new Dialogue(
                    Tria,
                    "That guy... ugh... I can't really blame him. He just... wants to get out of here, I guess... like the rest of us.",
                )
                    .monologue("But... the way he's doing it... it's kinda wrong, haha... I think.")
                    .monologue(`Here... uh... take this ${SkillPod.name}.`)
                    .monologue("It's... a bit much, maybe, but... it should... probably work?").root,
            )
            .onReached((stage) => {
                prestAnnoyance.add();
                const connection = stage.dialogue!.finished.connect(() => {
                    Server.Quest.giveQuestItem(SkillPod, 1);
                    stage.complete();
                });
                return () => {
                    connection.disconnect();
                    prestAnnoyance.remove();
                };
            }),
    )
    .addStage(
        new Stage()
            .setDescription(`Talk to Prest again.`)
            .setNPC(Prest, true)
            .setDialogue(
                new Dialogue(Prest, "Buy your crates and logs here! ...No, really. Please just leave.")
                    .monologue(`Wait. Is that... A ${SkillPod.name?.upper()}!?!?`)
                    .monologue("And for ME!?!? Thank you, thank you! Take my entire stock!")
                    .monologue("You don't want it? If that's so, I'm just gonna give away everything on me!")
                    .monologue(
                        `This ${SkillPod.name} is gonna change my life... Thank you, from the bottom of my heart!`,
                    ).root,
            )
            .onReached((stage) => {
                const replacement = new Dialogue(
                    Tria,
                    `Uh... okay... use that ${SkillPod.name} to... help with your... negotiations... I guess.`,
                ).monologue("I... I hope it works... fingers crossed...").root;
                replacement.add();
                const connection = stage.dialogue!.finished.connect(() => {
                    Server.Quest.takeQuestItem(SkillPod, 1);
                    stage.complete();
                });
                return () => {
                    connection.disconnect();
                    replacement.remove();
                };
            }),
    )
    .setCompletionDialogue(new Dialogue(Prest, "Selling my last crates and logs for free! I'm gonna be a success now!"))
    .setReward({
        xp: 170,
    });


import { Players, Workspace } from "@rbxts/services";
import { Soliloquy } from "server/interactive/npc/NPC";
import Quest, { Stage } from "server/quests/Quest";
import { Server } from "shared/api/APIExpose";
import { getMaxXp } from "shared/constants";
import AllConsumingTub from "shared/items/2/anguish/AllConsumingTub";
import TimeAccelerator from "shared/items/2/horrific/TimeAccelerator";
import PurpleSyringe from "shared/items/2/unreal/PurpleSyringe";
import Packets from "shared/Packets";

export = new Quest(script.Name)
    .setName("Miner Haven")
    .setLength(1)
    .setLevel(999)
    .setOrder(1)
    .addStage(
        new Stage().setDescription(`...`).onReached((stage) => {
            Packets.serverMusicEnabled.set(false);
            Server.empireData.items.inventory.set(PurpleSyringe.id, 1);
            Server.empireData.items.inventory.set(AllConsumingTub.id, 1);
            Server.Item.requestChanges();

            const dialogue = new Soliloquy("...my mine.").monologue(
                "I haven't checked my inventory in a while. I wonder if I have any new items...",
            ).root;

            const onChildAdded = (child: Instance) => {
                const player = Players.GetPlayerFromCharacter(child);
                if (!player) return;

                task.delay(3.5, () => {
                    dialogue.talk(false, [player]);
                    print("Prompting player to check inventory");
                });
            };

            const childAddedConnection = Workspace.ChildAdded.Connect(onChildAdded);
            for (const child of Workspace.GetChildren()) {
                onChildAdded(child);
            }

            const finishedConnection = dialogue.finished.connect(() => {
                stage.complete();
            });

            return () => {
                childAddedConnection.Disconnect();
                finishedConnection.Disconnect();
            };
        }),
    )
    .addStage(
        new Stage().setDescription("Check your inventory. Earn Time.").onReached((stage) => {
            Packets.serverMusicEnabled.set(true);
            Packets.trackQuest.toAllClients(script.Name);

            let completed = false;
            const connection = Server.Currency.balanceChanged.connect((balance) => {
                if (completed) return;

                if (balance.get("Time")?.moreThan(0)) {
                    stage.complete();
                    completed = true;
                }
            });

            return () => {
                connection.Disconnect();
            };
        }),
    )
    .addStage(
        new Stage().setDescription(`...`).onReached((stage) => {
            const dialogue = new Soliloquy("Time... It's a precious resource.")
                .monologue("I should probably start using it to buy some upgrades for my mine...")
                .monologue("After all, I can't let my competitors get ahead of me.").root;

            dialogue.talk();

            const finishedConnection = dialogue.finished.connect(() => {
                stage.complete();
            });

            return () => {
                finishedConnection.Disconnect();
            };
        }),
    )
    .addStage(
        new Stage().setDescription(`Buy some upgrades from the shop to earn more Time.`).onReached((stage) => {
            let completed = false;
            const connection = Server.Currency.balanceChanged.connect((balance) => {
                if (completed) return;

                if (balance.get("Time")?.moreThan(10000)) {
                    stage.complete();
                    completed = true;
                }
            });

            return () => {
                connection.Disconnect();
            };
        }),
    )
    .addStage(
        new Stage().setDescription(`...`).onReached((stage) => {
            const dialogue = new Soliloquy("Upgrades... They make such a difference.")
                .monologue("With these, I can optimize my mine and increase my Time production.")
                .monologue("The future looks bright for my mining endeavors.").root;

            dialogue.talk();

            const finishedConnection = dialogue.finished.connect(() => {
                stage.complete();
                Server.Item.unplaceItemsInArea(undefined, "MinerHaven");
                Server.Data.empireData.items.inventory.set(PurpleSyringe.id, 0);
                Server.Data.empireData.items.inventory.set(AllConsumingTub.id, 0);
                Server.Data.empireData.items.inventory.set(TimeAccelerator.id, 0);
                Server.Item.setBoughtAmount(PurpleSyringe, 0);
                Server.Item.setBoughtAmount(AllConsumingTub, 0);
                Server.Item.setBoughtAmount(TimeAccelerator, 0);
                Server.Item.requestChanges();
            });

            return () => {
                finishedConnection.Disconnect();
            };
        }),
    )
    .setReward({
        xp: getMaxXp(0),
    });