import Difficulty from "@rbxts/ejt";
import { OnoeNum } from "@rbxts/serikanum";
import { Server } from "shared/api/APIExpose";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Formula from "shared/currency/Formula";
import Item from "shared/item/Item";
import Generator from "shared/item/traits/generator/Generator";

const amt = new OnoeNum(100e12);
const base = new CurrencyBundle().set("Skill", amt);

export = new Item(script.Name)
    .setName("True Skill Generator")
    .setDescription(
        "A legendary generator that harnesses the essence of victory to produce boundless skill points, growing stronger with every triumph.",
    )
    .setDifficulty(Difficulty.Remorseless)
    .setPrice(new CurrencyBundle().set("Wins", 2e12), 1)
    .addPlaceableArea("BarrenIslands")
    .setCreator("CoPKaDT")

    .setFormula(new Formula().sqrt())
    .setFormulaX("wins")

    .trait(Generator)
    .setPassiveGain(base)
    .applyFormula(
        (v, item) => {
            return item.setPassiveGain(base.set("Skill", amt.mul(v)));
        },
        () => Server.Currency.get("Wins"),
    )

    .exit();


import Difficulty from "@rbxts/ejt";
import Droplet from "shared/item/Droplet";
import Item from "shared/item/Item";
import Dropper from "shared/item/traits/dropper/Dropper";

export = new Item(script.Name)
    .setName("Purple Syringe")
    .setDescription(
        "What is this...? It seems to ooze a strange substance that contains %val%. Try placing it down to find out.",
    )
    .setDifficulty(Difficulty.Unreal)
    .addPlaceableArea("MinerHaven")
    .setLevelReq(0)

    .trait(Dropper)
    .setDroplet(Droplet.UnrealDroplet)
    .setDropRate(1)

    .exit();


//!native
//!optimize 2
import Difficulty from "@rbxts/ejt";
import { Server } from "shared/api/APIExpose";
import { getAsset } from "shared/asset/AssetMap";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import type Charm from "shared/item/traits/Charm";

/**
 * Utility class to manage all items.
 */
abstract class Items {
    /**
     * Map of item ID to Item object.
     * If encountering circular dependency issues, use {@link ITEM_PER_ID} instead.
     */
    static readonly itemsPerId = (function () {
        const folder = script.Parent;
        if (folder === undefined) throw "No folder specified";

        const itemsPerId = new Map<string, Item>();
        for (const moduleScript of folder.GetDescendants()) {
            if (moduleScript.IsA("ModuleScript") && moduleScript !== script) {
                const i = require(moduleScript);
                if (i !== undefined) {
                    const item = i as Item;
                    itemsPerId.set(item.id, item);
                }
            }
        }
        return itemsPerId;
    })();

    /** Map of item name to Item object. */
    static readonly itemsPerName = (function () {
        const itemsPerName = new Map<string, Item>();
        for (const [_, item] of Items.itemsPerId) {
            itemsPerName.set(item.name, item);
        }
        return itemsPerName;
    })();

    /** Set of all charm traits */
    static readonly charms = (function () {
        const charms = new Set<Charm>();
        for (const [_, item] of Items.itemsPerId) {
            const charm = item.findTrait("Charm");
            if (charm !== undefined) {
                charms.add(charm);
            }
        }
        return charms;
    })();

    /** Set of all unique items */
    static readonly uniqueItems = (function () {
        const uniqueItems = new Set<Item>();
        for (const [_, item] of Items.itemsPerId) {
            if (item.isA("Unique")) {
                uniqueItems.add(item);
            }
        }
        return uniqueItems;
    })();

    /**
     * Ordered list of all items by:
     * 1. Difficulty rating
     * 2. Requirements (items that require others come after)
     * 3. Price (total value across all currencies)
     * 4. Layout order
     * 5. Name
     */
    static readonly sortedItems = (function () {
        const describePrice = (item: Item) => {
            const price = item.getPrice(1);
            if (price === undefined) {
                return "<none>";
            }

            const parts = new Array<string>();
            for (const [currency, amount] of price.amountPerCurrency) {
                parts.push(`${currency}=${amount.toString()}`);
            }
            if (parts.size() === 0) {
                return "<empty bundle>";
            }
            return parts.join(", ");
        };

        const describeRequirements = (item: Item) => {
            if (item.requiredItems.size() === 0) {
                return "<none>";
            }
            const parts = new Array<string>();
            for (const [reqId, count] of item.requiredItems) {
                parts.push(`${reqId}x${count}`);
            }
            return parts.join(", ");
        };

        const priceCurrencyOrder = new Array<Currency>();
        for (const [currency] of CurrencyBundle.SORTED_DETAILS) {
            priceCurrencyOrder.push(currency);
        }

        const comparePrices = (aPrice?: CurrencyBundle, bPrice?: CurrencyBundle) => {
            if (aPrice === undefined && bPrice === undefined) {
                return undefined;
            }
            if (aPrice === undefined) {
                return true;
            }
            if (bPrice === undefined) {
                return false;
            }

            for (const currency of priceCurrencyOrder) {
                const aAmount = aPrice.amountPerCurrency.get(currency);
                const bAmount = bPrice.amountPerCurrency.get(currency);

                if (aAmount !== undefined && bAmount !== undefined) {
                    if (aAmount.moreThan(bAmount)) {
                        return false;
                    }
                    if (bAmount.moreThan(aAmount)) {
                        return true;
                    }
                } else if (aAmount !== undefined) {
                    return false;
                } else if (bAmount !== undefined) {
                    return true;
                }
            }

            const aCount = aPrice.amountPerCurrency.size();
            const bCount = bPrice.amountPerCurrency.size();
            if (aCount !== bCount) {
                return aCount < bCount;
            }

            return undefined;
        };

        const itemsArray = new Array<Item>();
        for (const [, item] of Items.itemsPerId) {
            itemsArray.push(item);
        }

        // Comparison function for secondary sorting criteria
        const compareItems = (a: Item, b: Item): boolean => {
            if (a.difficulty.layoutRating !== undefined && b.difficulty.layoutRating !== undefined) {
                if (a.difficulty.layoutRating !== b.difficulty.layoutRating) {
                    return a.difficulty.layoutRating < b.difficulty.layoutRating;
                }
            }

            const aPrice = a.getPrice(1);
            const bPrice = b.getPrice(1);
            const priceResult = comparePrices(aPrice, bPrice);
            if (priceResult !== undefined) {
                return priceResult;
            }

            if (a.layoutOrder !== b.layoutOrder) {
                return a.layoutOrder < b.layoutOrder;
            }

            if (a.name !== b.name) {
                return a.name < b.name;
            }
            return a.id < b.id;
        };

        const stableSort = (array: Array<Item>, cmp: (a: Item, b: Item) => boolean) => {
            for (let i = 1; i < array.size(); i++) {
                const current = array[i];
                let j = i - 1;

                while (j >= 0 && cmp(current, array[j])) {
                    array[j + 1] = array[j];
                    j--;
                }

                array[j + 1] = current;
            }
        };

        // Initial sort by difficulty, price, layoutOrder, name, id
        stableSort(itemsArray, compareItems);

        // Build position map for current order
        const positionMap = new Map<string, number>();
        for (let i = 0; i < itemsArray.size(); i++) {
            positionMap.set(itemsArray[i].id, i);
        }

        // Check if item A depends on item B (directly or transitively)
        const dependsOn = (itemA: Item, itemB: Item, visited = new Set<string>()): boolean => {
            if (visited.has(itemA.id)) return false; // Prevent infinite loops
            visited.add(itemA.id);

            for (const [requiredId] of itemA.requiredItems) {
                if (requiredId === itemB.id) return true;
                const requiredItem = Items.itemsPerId.get(requiredId);
                if (requiredItem !== undefined && dependsOn(requiredItem, itemB, visited)) {
                    return true;
                }
            }
            return false;
        };

        // Iteratively fix dependency violations and sort violations
        let changed = true;
        let iterations = 0;
        const maxIterations = itemsArray.size() * itemsArray.size(); // Prevent infinite loops

        while (changed && iterations < maxIterations) {
            changed = false;
            iterations++;

            // Fix dependency violations
            for (let i = 0; i < itemsArray.size(); i++) {
                const item = itemsArray[i];

                // Find the latest position among all requirements
                let maxRequiredPos = -1;
                for (const [requiredId] of item.requiredItems) {
                    const requiredPos = positionMap.get(requiredId);
                    if (requiredPos !== undefined && requiredPos > maxRequiredPos) {
                        maxRequiredPos = requiredPos;
                    }
                }

                // If this item is before any of its requirements, move it after
                if (maxRequiredPos >= i) {
                    const movedItem = itemsArray.remove(i)!;
                    const insertPos = math.min(maxRequiredPos + 1, itemsArray.size());
                    itemsArray.insert(insertPos, movedItem);

                    // Rebuild position map
                    for (let j = 0; j < itemsArray.size(); j++) {
                        positionMap.set(itemsArray[j].id, j);
                    }

                    changed = true;
                    break;
                }
            }

            if (changed) continue;

            // Fix sort violations (bubble better items forward if dependencies allow)
            for (let i = 1; i < itemsArray.size(); i++) {
                const currentItem = itemsArray[i];
                const prevItem = itemsArray[i - 1];

                // If current item should come before previous item
                if (compareItems(currentItem, prevItem)) {
                    // Only block swaps when the current item depends on the previous item
                    if (!dependsOn(currentItem, prevItem)) {
                        // Safe to swap
                        itemsArray[i] = prevItem;
                        itemsArray[i - 1] = currentItem;

                        // Update position map
                        positionMap.set(prevItem.id, i);
                        positionMap.set(currentItem.id, i - 1);

                        changed = true;
                        break;
                    }
                }
            }
        }

        // Assign final layoutOrder
        for (let i = 0; i < itemsArray.size(); i++) {
            const item = itemsArray[i];
            item.layoutOrder = i;
        }

        return itemsArray;
    })();

    /** Total number of items. */
    static readonly length = Items.itemsPerId.size();

    /**
     * Retrieve the Item object at the specified ID.
     *
     * @param itemId The ID of the item.
     * @returns Item object or undefined if not found.
     */
    static getItem(itemId: string): Item | undefined {
        return this.itemsPerId.get(itemId);
    }

    /**
     * Set the Item object at the specified ID.
     *
     * @param itemId The ID of the item.
     * @param item The Item object.
     */
    static setItem(itemId: string, item: Item) {
        this.itemsPerId.set(itemId, item);
    }

    static {
        Difficulty.Excavation.setImage(Difficulty.Construct.image!);
        Difficulty.Miscellaneous.setImage(getAsset("assets/MiscellaneousDifficulty.png"));
        Difficulty.ReversedPeripherality.setVisualRating("-10^10");
        Server.Items = this;
    }
}

export = Items;


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";

export = new Item(script.Name)
    .setName("100K Visits Trophy")
    .setDescription("To commemorate 100K visits!")
    .setDifficulty(Difficulty.Bonuses)
    .setPrice(new CurrencyBundle().set("Funds", 100000), 1)
    .placeableEverywhere();


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import ClassLowerNegativeShop from "shared/items/negative/ClassLowerNegativeShop";

export = new Item(script.Name)
    .setName("1M Visits Trophy")
    .setDescription("To commemorate 1M visits!")
    .setDifficulty(Difficulty.Bonuses)
    .setPrice(new CurrencyBundle().set("Funds", 1000000), 1)
    .soldAt(ClassLowerNegativeShop)
    .placeableEverywhere();


import Difficulty from "@rbxts/ejt";
import React, { Fragment, JSX, useEffect, useState } from "@rbxts/react";
import { createRoot } from "@rbxts/react-roblox";
import { toNumeral } from "@rbxts/roman-numerals";
import { TweenService } from "@rbxts/services";
import useProperty from "client/hooks/useProperty";
import { Server } from "shared/api/APIExpose";
import { getAsset } from "shared/asset/AssetMap";
import { playSound } from "shared/asset/GameAssets";
import { RobotoSlabBold, RobotoSlabExtraBold, RobotoSlabHeavy, RobotoSlabMedium } from "shared/asset/GameFonts";
import { Challenge, CHALLENGE_PER_ID } from "shared/Challenge";
import Item from "shared/item/Item";
import Packets from "shared/Packets";
import Sandbox from "shared/Sandbox";

/**
 * Individual challenge option component
 */
function ChallengeOption({ challenge, currentLevel }: { challenge: Challenge; currentLevel: number }) {
    const [isExpanded, setIsExpanded] = useState(false);
    const [isHovered, setIsHovered] = useState(false);
    const [isAnimating, setIsAnimating] = useState(false);
    const [confirmationState, setConfirmationState] = useState<{
        showConfirmation: boolean;
        lastClickTime: number;
    }>({ showConfirmation: false, lastClickTime: 0 });

    const requiredLevel = challenge.requiredEmpireLevel(currentLevel + 1);
    const empireLevel = useProperty(Packets.level);
    const meetsRequirement = empireLevel >= requiredLevel;

    const handleStartClick = () => {
        const currentTime = tick();

        if (confirmationState.showConfirmation && currentTime - confirmationState.lastClickTime < 3) {
            // Send challenge start packet to server
            const success = Packets.startChallenge.toServer(challenge.id);
            setConfirmationState({ showConfirmation: false, lastClickTime: currentTime });
            if (success) {
                playSound("MagicCast.mp3");
            } else {
                playSound("Error.mp3");
            }
        } else {
            // Show confirmation
            setConfirmationState({ showConfirmation: true, lastClickTime: currentTime });
        }
    };

    const handleToggleExpand = () => {
        playSound(isExpanded ? "CheckOff.mp3" : "CheckOn.mp3");
        if (!isExpanded) {
            setIsAnimating(true);
        }
        setIsExpanded(!isExpanded);
    };

    return (
        <frame
            ref={(instance) => {
                if (!instance) return;
                if (isExpanded && isAnimating) {
                    const targetSize = new UDim2(1, 0, 0, 150);
                    TweenService.Create(instance, new TweenInfo(0.4, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {
                        Size: targetSize,
                    }).Play();
                    task.delay(0.2, () => setIsAnimating(false));
                } else if (!isExpanded) {
                    TweenService.Create(instance, new TweenInfo(0.3, Enum.EasingStyle.Quart, Enum.EasingDirection.In), {
                        Size: new UDim2(1, 0, 0, 50),
                    }).Play();
                }
            }}
            BackgroundTransparency={1}
            Size={isExpanded && !isAnimating ? new UDim2(1, 0, 0, 150) : new UDim2(1, 0, 0, 50)}
            AutomaticSize={isExpanded ? Enum.AutomaticSize.None : Enum.AutomaticSize.Y}
        >
            {/* Background decorative images */}
            <imagelabel
                ref={(instance) => {
                    if (!instance) return;
                    const targetTransparency = isHovered ? 0.4 : 0.6;
                    TweenService.Create(instance, new TweenInfo(0.3), { ImageTransparency: targetTransparency }).Play();
                }}
                AnchorPoint={new Vector2(0, 1)}
                BackgroundTransparency={1}
                Image={getAsset("assets/challenge/ChallengeOptionBackground.png")}
                ImageColor3={Color3.fromRGB(0, 0, 0)}
                ImageTransparency={0.6}
                Position={new UDim2(0, 0, 1, -4)}
                Size={new UDim2(0.96, 0, 1, 0)}
                ZIndex={-2}
            >
                <imagelabel
                    BackgroundTransparency={1}
                    Image={getAsset("assets/challenge/ChallengeOptionOverlay.png")}
                    ImageTransparency={0.95}
                    Size={new UDim2(1, 0, 1, 0)}
                    ZIndex={-2}
                />
            </imagelabel>

            {/* Dropdown Toggle Button */}
            <textbutton
                BackgroundTransparency={1}
                BorderSizePixel={0}
                Position={new UDim2(0, 0, 0, 0)}
                Size={new UDim2(1, 0, 0, 40)}
                Text=""
                Event={{
                    Activated: handleToggleExpand,
                    MouseEnter: () => setIsHovered(true),
                    MouseLeave: () => setIsHovered(false),
                }}
            >
                <uicorner CornerRadius={new UDim(0.1, 0)} />
                <uistroke Thickness={2}>
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, challenge.colors.primary),
                                new ColorSequenceKeypoint(1, challenge.colors.secondary),
                            ])
                        }
                    />
                </uistroke>

                {/* Challenge Title */}
                <textlabel
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabBold}
                    Position={new UDim2(0.05, 0, 0, 0)}
                    Size={new UDim2(0.75, 0, 1, 0)}
                    Text={`${challenge.name} ${toNumeral(currentLevel + 1)}`}
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextScaled={true}
                    TextWrapped={true}
                    TextXAlignment={Enum.TextXAlignment.Left}
                >
                    <uistroke Thickness={2} />
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, challenge.colors.primary),
                                new ColorSequenceKeypoint(1, challenge.colors.secondary),
                            ])
                        }
                    />
                    <uipadding
                        PaddingBottom={new UDim(0, 5)}
                        PaddingLeft={new UDim(0, 5)}
                        PaddingRight={new UDim(0, 5)}
                        PaddingTop={new UDim(0, 5)}
                    />
                </textlabel>

                {/* Required Empire Level */}
                <textlabel
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabBold}
                    Position={new UDim2(0.65, 0, 0, 0)}
                    Size={new UDim2(0.2, 0, 1, 0)}
                    Text={`Lv. ${requiredLevel}`}
                    TextColor3={meetsRequirement ? Color3.fromRGB(255, 255, 255) : Color3.fromRGB(255, 80, 80)}
                    TextScaled={true}
                    TextWrapped={true}
                    TextXAlignment={Enum.TextXAlignment.Right}
                >
                    <uistroke Thickness={2} />
                    <uipadding
                        PaddingBottom={new UDim(0, 5)}
                        PaddingLeft={new UDim(0, 5)}
                        PaddingRight={new UDim(0, 5)}
                        PaddingTop={new UDim(0, 5)}
                    />
                </textlabel>

                {/* Dropdown Arrow */}
                <imagelabel
                    ref={(instance) => {
                        if (!instance) return;
                        TweenService.Create(instance, new TweenInfo(0.2), { Rotation: isExpanded ? 180 : 0 }).Play();
                    }}
                    BackgroundTransparency={1}
                    Position={new UDim2(0.85, 0, 0.1, 0)}
                    Size={new UDim2(0.1, 0, 0.8, 0)}
                    Image={getAsset("assets/Dropdown.png")}
                    ScaleType={Enum.ScaleType.Fit}
                />
            </textbutton>

            {/* Expanded Content */}
            {isExpanded && (
                <frame
                    ref={(instance) => {
                        if (!instance) return;
                        if (isAnimating) {
                            instance.GetDescendants().forEach((descendant) => {
                                if (descendant.IsA("TextLabel")) {
                                    TweenService.Create(
                                        descendant,
                                        new TweenInfo(0.3, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
                                        { TextTransparency: 0, TextStrokeTransparency: 0 },
                                    ).Play();
                                    descendant.GetChildren().forEach((child) => {
                                        if (child.IsA("UIStroke")) {
                                            TweenService.Create(
                                                child,
                                                new TweenInfo(0.3, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
                                                { Transparency: 0 },
                                            ).Play();
                                        }
                                    });
                                } else if (descendant.IsA("TextButton")) {
                                    TweenService.Create(
                                        descendant,
                                        new TweenInfo(0.3, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
                                        { BackgroundTransparency: 0, TextTransparency: 0 },
                                    ).Play();
                                    descendant.GetChildren().forEach((child) => {
                                        if (child.IsA("UIStroke")) {
                                            TweenService.Create(
                                                child,
                                                new TweenInfo(0.3, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
                                                { Transparency: 0 },
                                            ).Play();
                                        }
                                        if (child.IsA("UIGradient")) {
                                            task.delay(0, () => {
                                                child.Transparency = new NumberSequence(0);
                                            });
                                        }
                                    });
                                }
                            });
                        }
                    }}
                    BackgroundTransparency={1}
                    Position={new UDim2(0, 0, 0, 45)}
                    Size={new UDim2(1, 0, 1, -45)}
                >
                    {/* Description Section */}
                    <frame
                        AnchorPoint={new Vector2(0.5, 0)}
                        BackgroundTransparency={1}
                        Position={new UDim2(0.5, 0, 0, 0)}
                        Size={new UDim2(0.85, 0, 0.6, 0)}
                    >
                        <uilistlayout
                            SortOrder={Enum.SortOrder.LayoutOrder}
                            VerticalFlex={Enum.UIFlexAlignment.SpaceBetween}
                        />

                        {/* Description */}
                        <textlabel
                            AutomaticSize={Enum.AutomaticSize.Y}
                            BackgroundTransparency={1}
                            FontFace={RobotoSlabMedium}
                            Size={new UDim2(1, 0, 0, 0)}
                            Text={challenge.description(currentLevel + 1)}
                            TextColor3={Color3.fromRGB(209, 209, 209)}
                            TextScaled={true}
                            TextStrokeTransparency={isAnimating ? 1 : 0}
                            TextTransparency={isAnimating ? 1 : 0}
                            TextWrapped={true}
                            TextXAlignment={Enum.TextXAlignment.Left}
                            TextYAlignment={Enum.TextYAlignment.Top}
                        >
                            <uitextsizeconstraint MaxTextSize={22} />
                            <uiflexitem />
                        </textlabel>

                        {/* Requirement */}
                        <textlabel
                            AutomaticSize={Enum.AutomaticSize.Y}
                            BackgroundTransparency={1}
                            FontFace={RobotoSlabMedium}
                            Size={new UDim2(1, 0, 0, 0)}
                            Text={`Task: ${challenge.getTaskLabel()}`}
                            TextColor3={Color3.fromRGB(126, 255, 145)}
                            TextScaled={true}
                            TextStrokeTransparency={isAnimating ? 1 : 0}
                            TextTransparency={isAnimating ? 1 : 0}
                            TextWrapped={true}
                            TextXAlignment={Enum.TextXAlignment.Left}
                            TextYAlignment={Enum.TextYAlignment.Top}
                        >
                            <uitextsizeconstraint MaxTextSize={18} />
                            <uiflexitem />
                        </textlabel>

                        {/* Reward Label */}
                        <textlabel
                            AutomaticSize={Enum.AutomaticSize.Y}
                            BackgroundTransparency={1}
                            FontFace={RobotoSlabMedium}
                            Size={new UDim2(1, 0, 0, 0)}
                            Text={`Reward: ${challenge.getRewardLabel(currentLevel)}`}
                            TextColor3={Color3.fromRGB(255, 215, 100)}
                            TextScaled={true}
                            TextStrokeTransparency={isAnimating ? 1 : 0}
                            TextTransparency={isAnimating ? 1 : 0}
                            TextWrapped={true}
                            TextXAlignment={Enum.TextXAlignment.Left}
                            TextYAlignment={Enum.TextYAlignment.Top}
                        >
                            <uitextsizeconstraint MaxTextSize={18} />
                            <uiflexitem />
                        </textlabel>
                    </frame>

                    {/* Start Button */}
                    <textbutton
                        BackgroundColor3={Color3.fromRGB(255, 255, 255)}
                        BackgroundTransparency={isAnimating ? 1 : 0}
                        BorderSizePixel={0}
                        Position={new UDim2(0.55, 0, 0.5, 0)}
                        Size={new UDim2(0.35, 0, 0.35, 0)}
                        Text=""
                        Event={{
                            Activated: handleStartClick,
                        }}
                    >
                        <uistroke
                            ApplyStrokeMode={Enum.ApplyStrokeMode.Border}
                            Thickness={2}
                            Transparency={isAnimating ? 1 : 0}
                        />
                        <uicorner CornerRadius={new UDim(0.2, 0)} />
                        <uigradient
                            Color={
                                new ColorSequence([
                                    new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 127)),
                                    new ColorSequenceKeypoint(1, Color3.fromRGB(255, 157, 0)),
                                ])
                            }
                            Rotation={90}
                            Transparency={isAnimating ? new NumberSequence(1) : new NumberSequence(0)}
                        />
                        <textlabel
                            BackgroundTransparency={1}
                            FontFace={RobotoSlabHeavy}
                            Size={new UDim2(1, 0, 1, 0)}
                            Text={confirmationState.showConfirmation ? "Are you sure?" : "Enter"}
                            TextColor3={Color3.fromRGB(255, 255, 255)}
                            TextScaled={true}
                            TextTransparency={isAnimating ? 1 : 0}
                            TextWrapped={true}
                        >
                            <uistroke Thickness={3}>
                                <uigradient
                                    Color={
                                        new ColorSequence([
                                            new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                                            new ColorSequenceKeypoint(1, Color3.fromRGB(20, 29, 14)),
                                        ])
                                    }
                                    Rotation={90}
                                />
                            </uistroke>
                        </textlabel>
                        <uipadding
                            PaddingBottom={new UDim(0, 7)}
                            PaddingLeft={new UDim(0, 7)}
                            PaddingRight={new UDim(0, 7)}
                            PaddingTop={new UDim(0, 7)}
                        />
                    </textbutton>
                </frame>
            )}
        </frame>
    );
}

/**
 * Current challenge display component
 */
function CurrentChallenge({ challengeId, currentLevel = 0 }: { challengeId?: string; currentLevel?: number }) {
    const challenge = challengeId ? CHALLENGE_PER_ID.get(challengeId) : undefined;
    if (challenge === undefined || challengeId === undefined) {
        return <Fragment />;
    }

    const [debounce, setDebounce] = useState(0);

    const handleQuitClick = () => {
        const currentTime = os.clock();
        if (currentTime - debounce < 1) return;
        setDebounce(currentTime);

        playSound("MagicCast.mp3");
        Packets.quitChallenge.toServer();
    };

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

            {/* "You are currently in:" label */}
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoSlabBold}
                Size={new UDim2(0.9, 0, 0.15, 0)}
                Text="You are currently in:"
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextWrapped={true}
            >
                <uistroke Thickness={3} />
            </textlabel>

            {/* Challenge Title */}
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoSlabBold}
                LayoutOrder={2}
                Size={new UDim2(1, 0, 0.2, 0)}
                Text={challenge.name}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextWrapped={true}
            >
                <uistroke Thickness={4} />
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, challenge.colors.primary),
                            new ColorSequenceKeypoint(1, challenge.colors.secondary),
                        ])
                    }
                />
            </textlabel>

            {/* Spacer */}
            <frame BackgroundTransparency={1} LayoutOrder={3} Size={new UDim2(0, 0, 0.1, 0)} />

            {/* Requirement Label */}
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoSlabBold}
                LayoutOrder={2}
                Size={new UDim2(0.9, 0, 0.1, 0)}
                Text={challenge.description(currentLevel)}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextWrapped={true}
            >
                <uistroke Thickness={3} />
            </textlabel>

            {/* Quit Button */}
            <textbutton
                BackgroundColor3={Color3.fromRGB(255, 255, 255)}
                BorderSizePixel={0}
                LayoutOrder={4}
                Size={new UDim2(0.3, 0, 0.15, 0)}
                Text=""
                Event={{
                    Activated: handleQuitClick,
                }}
            >
                <uistroke ApplyStrokeMode={Enum.ApplyStrokeMode.Border} Thickness={2} />
                <uicorner CornerRadius={new UDim(0.2, 0)} />
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(255, 96, 75)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(180, 32, 32)),
                        ])
                    }
                    Rotation={90}
                />
                <textlabel
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabHeavy}
                    Size={new UDim2(1, 0, 1, 0)}
                    Text="Quit"
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextScaled={true}
                    TextWrapped={true}
                >
                    <uistroke Thickness={3}>
                        <uigradient
                            Color={
                                new ColorSequence([
                                    new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                                    new ColorSequenceKeypoint(1, Color3.fromRGB(20, 29, 14)),
                                ])
                            }
                            Rotation={90}
                        />
                    </uistroke>
                </textlabel>
                <uipadding
                    PaddingBottom={new UDim(0, 7)}
                    PaddingLeft={new UDim(0, 7)}
                    PaddingRight={new UDim(0, 7)}
                    PaddingTop={new UDim(0, 7)}
                />
            </textbutton>
        </frame>
    );
}

/**
 * Main Challenge GUI component
 */
function ChallengeGui({ adornee }: { adornee: BasePart }) {
    const currentChallengeId = useProperty(Packets.currentChallenge);
    const currentLevelPerChallenge = useProperty(Packets.currentLevelPerChallenge);
    const isInChallenge = currentChallengeId !== undefined && currentChallengeId !== "";
    const challengeOptions = new Array<JSX.Element>();
    for (const challenge of Challenge.CHALLENGES) {
        const currentLevel = currentLevelPerChallenge.get(challenge.id) ?? 0;
        challengeOptions.push(<ChallengeOption challenge={challenge} currentLevel={currentLevel} />);
    }

    useEffect(() => {
        const connection = Packets.challengeCompleted.fromServer((challenge, rewardLabel) => {
            // Play completion sound
            playSound("MagicCast.mp3");

            // Could show a completion notification here
            print(`Challenge completed: ${challenge} - Rewards: ${rewardLabel}`);
        });

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

    return (
        <surfacegui
            Adornee={adornee}
            ClipsDescendants={true}
            MaxDistance={30}
            ResetOnSpawn={false}
            SizingMode={Enum.SurfaceGuiSizingMode.PixelsPerStud}
            ZIndexBehavior={Enum.ZIndexBehavior.Sibling}
        >
            <uilistlayout
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                SortOrder={Enum.SortOrder.LayoutOrder}
                VerticalAlignment={Enum.VerticalAlignment.Center}
                Padding={new UDim(0.04, 0)}
            />

            {/* Title */}
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoSlabExtraBold}
                Size={new UDim2(0.9, 0, 0.125, 0)}
                Text="Challenges"
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextWrapped={true}
                TextXAlignment={Enum.TextXAlignment.Left}
            >
                <uistroke Thickness={3} />
            </textlabel>

            {/* Challenge Options (when not in challenge) */}
            {!isInChallenge && (
                <scrollingframe
                    Active={true}
                    AutomaticCanvasSize={Enum.AutomaticSize.Y}
                    BackgroundTransparency={1}
                    CanvasSize={new UDim2(0, 0, 0, 0)}
                    LayoutOrder={1}
                    Selectable={false}
                    Size={new UDim2(1, 0, 0.75, 0)}
                    Visible={true}
                >
                    <uilistlayout
                        HorizontalAlignment={Enum.HorizontalAlignment.Center}
                        SortOrder={Enum.SortOrder.LayoutOrder}
                        Padding={new UDim(0, 5)}
                    />

                    {challengeOptions}
                </scrollingframe>
            )}

            <CurrentChallenge
                challengeId={currentChallengeId}
                currentLevel={currentLevelPerChallenge.get(currentChallengeId) ?? 0}
            />
        </surfacegui>
    );
}

export = new Item(script.Name)
    .setName("Challenges Board")
    .setDescription("A board that offers various challenges to complete.")
    .setDifficulty(Difficulty.Bonuses)
    .placeableEverywhere()
    .onLoad((model) => {
        if (Sandbox.getEnabled()) {
            const questMetadata = Server.empireData.questMetadata;

            questMetadata.set("ChallengesUnlocked", true);
            Server.Challenge.refreshChallenges();

            model.Destroying.Connect(() => {
                questMetadata.set("ChallengesUnlocked", false);
            });
        }
    })
    .onClientLoad((model) => {
        const board = model.WaitForChild("ChallengesBoard") as BasePart;

        const root = createRoot(board);
        root.render(<ChallengeGui adornee={board} />);

        model.Destroying.Connect(() => {
            root.unmount();
        });
    });


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import Shop from "shared/item/traits/Shop";

export = new Item(script.Name)
    .setName("Crafting Shop")
    .setDescription("A shop that sells crafting tables.")
    .setDifficulty(Difficulty.Bonuses)

    .trait(Shop)
    .exit();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import Shop from "shared/item/traits/Shop";

export = new Item(script.Name)
    .setName("Excavation Shop")
    .setDescription("A shop that sells minerals.")
    .setDifficulty(Difficulty.Bonuses)

    .trait(Shop)
    .exit();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import Shop from "shared/item/traits/Shop";

export = new Item(script.Name)
    .setName("Grass Shop")
    .setDescription("A shop that sells grass.")
    .setDifficulty(Difficulty.Bonuses)

    .trait(Shop)
    .exit();


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";

export = new Item(script.Name)
    .setName("My Key.")
    .setDescription("Just for me.")
    .setDifficulty(Difficulty.Bonuses)
    .setPrice(new CurrencyBundle().set("Power", 1), 1);


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";

export = new Item(script.Name)
    .setName("Sadness In My Heart")
    .setDescription("I'm sorry...")
    .setDifficulty(Difficulty.Bonuses)
    .addPlaceableArea("BarrenIslands", "SlamoVillage");


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import SuspiciousStud from "shared/items/bonuses/SuspiciousStud";

export = new Item(script.Name)
    .setName("Stud")
    .setDescription("Stud")
    .setDifficulty(Difficulty.Bonuses)
    .setPrice(new CurrencyBundle().set("Funds", 1), 1)
    .soldAt(SuspiciousStud);


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import Shop from "shared/item/traits/Shop";

export = new Item(script.Name)
    .setName("Suspicious Stud")
    .setDifficulty(Difficulty.Bonuses)

    .trait(Shop)
    .exit();


import Difficulty from "@rbxts/ejt";
import { BadgeService, Players } from "@rbxts/services";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";

export = new Item(script.Name)
    .setName("30/30 rated tower 😱")
    .setDescription("you will not last 5 SECONDS in this tower")
    .setDifficulty(Difficulty.Bonuses)
    .setPrice(new CurrencyBundle().set("Funds", 10), 1)
    .addPlaceableArea("BarrenIslands")

    .onLoad((model) => {
        const touchPart = model.WaitForChild("TouchPart") as BasePart;
        touchPart.CanTouch = true;
        touchPart.Touched.Connect((other) => {
            const player = Players.GetPlayerFromCharacter(other.Parent);
            if (!player) return;
            try {
                BadgeService.AwardBadge(player.UserId, 1077478080869672); // TODO: change badge
            } catch {
                print("Failed to award badge");
            }
        });
    });


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import Shop from "shared/item/traits/Shop";

export = new Item(script.Name)
    .setName("Grass Shop")
    .setDescription("A shop that sells wool.")
    .setDifficulty(Difficulty.Bonuses)

    .trait(Shop)
    .exit();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";
import Volt from "shared/items/excavation/Volt";

export = new Item(script.Name)
    .setName("Aquamarine")
    .setDescription(
        "A serene cyan gem distilled from ocean currents. Its lattice resonates with water-aspected upgrades.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setRequiredItemAmount(Volt, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";
import Gyge from "shared/items/excavation/Gyge";

export = new Item(script.Name)
    .setName("Auly Plate")
    .setDescription(
        "A pale gold laminate forged from layered microfibers. Lightweight, yet reinforces any chassis it touches.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setRequiredItemAmount(Gyge, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";
import Uranium from "shared/items/excavation/Uranium";

export = new Item(script.Name)
    .setName("Bismuth")
    .setDescription(
        "A divine, iridescent metal incomprehensible to most. Used in the construction of the Iridiscent Tesseract and other empyrean artifacts.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setRequiredItemAmount(Uranium, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";
import Bismuth from "shared/items/excavation/Bismuth";

export = new Item(script.Name)
    .setName("Boracite")
    .setDescription(
        "A rare turquoise crystal with ferroelectric properties, perfect for synchronizing multi-phase droplet networks.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setRequiredItemAmount(Bismuth, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";
import Lollipop from "shared/items/excavation/Lollipop";

export = new Item(script.Name)
    .setName("C0RR8PT10N")
    .setDescription(
        "A glitching neon crystal that flickers between realities. Engineers swear it distorts probability itself.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setRequiredItemAmount(Lollipop, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";
import Singularity from "shared/items/excavation/Singularity";

export = new Item(script.Name)
    .setName("Capsuled Singularity")
    .setDescription(
        "A Singularity safely locked within an alloy capsule. Emits a faint haze that bends reality yet remains stable.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setRequiredItemAmount(Singularity, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import WhiteGem from "shared/items/excavation/WhiteGem";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";

export = new Item(script.Name)
    .setName("Crystal")
    .setDescription(
        "A precious metal made from the crystallization of liquid Amethyst in the low pressures of Sky Pavilion. Highly reactive with White Gems.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setPrice(new CurrencyBundle().set("Power", 1))
    .setRequiredItemAmount(WhiteGem, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";
import Sapphire from "shared/items/excavation/Sapphire";

export = new Item(script.Name)
    .setName("Diamond")
    .setDescription(
        "Harder than any alloy known to mortals; its faultless facets make it perfect for high-pressure condensers and reactors.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setRequiredItemAmount(Sapphire, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";
import Ruby from "shared/items/excavation/Ruby";

export = new Item(script.Name)
    .setName("Emerald")
    .setDescription(
        "A verdant gem saturated with natural mana. Commonly embedded in conduits to stabilize volatile energy flows.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setRequiredItemAmount(Ruby, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";

export = new Item(script.Name)
    .setName("Stone")
    .setDescription("A basic crafting resource, found littered everywhere around the world.")
    .setDifficulty(Difficulty.Excavation)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import Iron from "shared/items/excavation/Iron";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";

export = new Item(script.Name)
    .setName("Gold")
    .setDescription(
        "A precious metal mostly used to flaunt wealth. Has many uses in science, but very expensive to harvest and produce.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setPrice(new CurrencyBundle().set("Skill", 1))
    .setRequiredItemAmount(Iron, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";
import StargazedMetal from "shared/items/excavation/StargazedMetal";

export = new Item(script.Name)
    .setName("Gyge")
    .setDescription(
        "A deep magenta mineral that absorbs ambient sound. Often etched into silencers for volatile droplet rigs.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setRequiredItemAmount(StargazedMetal, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";
import Starlight from "shared/items/excavation/Starlight";

export = new Item(script.Name)
    .setName("Ion")
    .setDescription(
        "Hyper-charged mineral fragments buzzing with unstable particles. Handle with insulated gloves—or a robot arm.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setRequiredItemAmount(Starlight, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import Crystal from "shared/items/excavation/Crystal";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";

export = new Item(script.Name)
    .setName("Iron")
    .setDescription(
        "Commonly found in abandoned caves, but nowadays harvested using advanced mining technology to prevent chance of falls.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setPrice(new CurrencyBundle().set("Bitcoin", 1))
    .setRequiredItemAmount(Crystal, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";
import Quartz from "shared/items/excavation/Quartz";

export = new Item(script.Name)
    .setName("Jade")
    .setDescription(
        "A beautiful green stone that is highly valued in many cultures. It is often used in jewelry and ornaments, symbolizing purity and serenity.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setRequiredItemAmount(Quartz, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";
import Boracite from "shared/items/excavation/Boracite";

export = new Item(script.Name)
    .setName("Nissonite")
    .setDescription(
        "A midnight blue shard, said to form where lightning strikes ancient seabeds. It hums softly beneath ultraviolet light.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setRequiredItemAmount(Boracite, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";
import Jade from "shared/items/excavation/Jade";

export = new Item(script.Name)
    .setName("Obsidian")
    .setDescription(
        "Volcanic glass forged in ancient eruptions. Razor sharp, yet prized for its uncanny ability to store arcane energy.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setRequiredItemAmount(Jade, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";
import Nissonite from "shared/items/excavation/Nissonite";

export = new Item(script.Name)
    .setName("Orpiment")
    .setDescription(
        "A vivid scarlet mineral that leaves a metallic sheen wherever it touches. Handle with care—its fumes are legendary.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setRequiredItemAmount(Nissonite, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import Gold from "shared/items/excavation/Gold";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";

export = new Item(script.Name)
    .setName("Quartz")
    .setDescription(
        "No one knows why such a common mineral in the ancient days is so hard to come by nowadays. It's as if someone harvested all of it and kept it hidden beneath everyone's eyes.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setPrice(new CurrencyBundle().set("Wins", 1))
    .setRequiredItemAmount(Gold, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";
import Obsidian from "shared/items/excavation/Obsidian";

export = new Item(script.Name)
    .setName("Ruby")
    .setDescription(
        "A radiant crimson gemstone whose crystalline lattice resonates perfectly with high-heat furnaces and laser arrays.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setRequiredItemAmount(Obsidian, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";
import Emerald from "shared/items/excavation/Emerald";

export = new Item(script.Name)
    .setName("Sapphire")
    .setDescription(
        "A deep blue crystal that refracts light into steady pulses. Essential in optics for high-precision droplet scanners.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setRequiredItemAmount(Emerald, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";
import AulyPlate from "shared/items/excavation/AulyPlate";

export = new Item(script.Name)
    .setName("Shell Piece")
    .setDescription(
        "Fragments of an ancient exoshell, coated in pearlescent dust. Holds remarkable pressure tolerance.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setRequiredItemAmount(AulyPlate, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";
import ShellPiece from "shared/items/excavation/ShellPiece";

export = new Item(script.Name)
    .setName("Singularity")
    .setDescription(
        "A gravitational anomaly condensed into a portable shard. Warps nearby matter ever so slightly inward.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setRequiredItemAmount(ShellPiece, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";
import C0RR8PT10N from "shared/items/excavation/C0RR8PT10N";

export = new Item(script.Name)
    .setName("Stargazed Metal")
    .setDescription(
        "Alloyed under meteor showers, this violet ingot bends light around its surface in mesmerizing waves.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setRequiredItemAmount(C0RR8PT10N, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";
import Diamond from "shared/items/excavation/Diamond";

export = new Item(script.Name)
    .setName("Starlight")
    .setDescription(
        "Solidified beams harvested in the upper atmosphere. Emits a gentle glow that amplifies photonic machinery.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setRequiredItemAmount(Diamond, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";
import Orpiment from "shared/items/excavation/Orpiment";

export = new Item(script.Name)
    .setName("Tetra")
    .setDescription(
        "A teal crystal growing naturally as perfect tetrahedrons. Engineers use it to align multi-axis droplet routes.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setRequiredItemAmount(Orpiment, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";
import Ion from "shared/items/excavation/Ion";

export = new Item(script.Name)
    .setName("Uranium")
    .setDescription(
        "A dense, radioactive ore that hums with latent power. Integral to reactor cores and questionable science projects.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setRequiredItemAmount(Ion, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";
import Tetra from "shared/items/excavation/Tetra";

export = new Item(script.Name)
    .setName("Volt")
    .setDescription(
        "Crackling yellow ore packed with static charge. A single fragment can jump-start dormant machinery.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setRequiredItemAmount(Tetra, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import ExcavationStone from "shared/items/excavation/ExcavationStone";
import ExcavationShop from "shared/items/bonuses/ExcavationShop";

export = new Item(script.Name)
    .setName("White Gem")
    .setDescription(
        "A commonly found metal used in the production of many electric appliances. Has a cheap cost of production, outweighing its unusually weak composition.",
    )
    .setDifficulty(Difficulty.Excavation)
    .setPrice(new CurrencyBundle().set("Funds", 1))
    .setRequiredItemAmount(ExcavationStone, 256)
    .placeableEverywhere()
    .soldAt(ExcavationShop)
    .persists();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import Shop from "shared/item/traits/Shop";

export = new Item(script.Name)
    .setName("Class Negative Shop")
    .setDescription("Purchase all of your items here! Well, at least all items below Instant Win difficulty.")
    .setDifficulty(Difficulty.TheFirstDifficulty)
    .addPlaceableArea("BarrenIslands")
    .persists()

    .trait(Shop)
    .exit();


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import Upgrader from "shared/item/traits/upgrader/Upgrader";
import ClassLowerNegativeShop from "shared/items/negative/ClassLowerNegativeShop";

export = new Item(script.Name)
    .setName("Advanced Refiner")
    .setDescription("Boosts droplets passing through this upgrader by %mul%.")
    .setDifficulty(Difficulty.A)
    .setPrice(new CurrencyBundle().set("Funds", 4.5e12), 1)
    .setPrice(new CurrencyBundle().set("Funds", 8.1e12), 2)
    .setPrice(new CurrencyBundle().set("Funds", 16.3e12), 3)
    .addPlaceableArea("BarrenIslands")
    .soldAt(ClassLowerNegativeShop)

    .trait(Upgrader)
    .setMul(new CurrencyBundle().set("Funds", 1.75).set("Power", 1.75))

    .exit();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";

export = new Item(script.Name)
    .setName("Apple")
    .setDescription("The Slamos' favorite past-time snack. Grows extraordinarily well in Slamo Village.")
    .setDifficulty(Difficulty.A)
    .persists();


import { getAllInstanceInfo } from "@antivivi/vrldk";
import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Droplet from "shared/item/Droplet";
import Item from "shared/item/Item";
import Dropper from "shared/item/traits/dropper/Dropper";
import Furnace from "shared/item/traits/Furnace";
import ClassLowerNegativeShop from "shared/items/negative/ClassLowerNegativeShop";
import Packets from "shared/Packets";

export = new Item(script.Name)
    .setName("Recycling Dropper")
    .setDescription(
        "Produces a %val% droplet every time a droplet is processed through the attached furnace. No need for conjoined droppers anymore.",
    )
    .setDifficulty(Difficulty.A)
    .setPrice(new CurrencyBundle().set("Funds", 6.5e12), 1)
    .addPlaceableArea("BarrenIslands")
    .soldAt(ClassLowerNegativeShop)

    .trait(Dropper)
    .setDroplet(Droplet.CommunismDroplet)

    .trait(Furnace)
    .calculatesFurnace(false)
    .exit()

    .onLoad((model) => {
        const modelInfo = getAllInstanceInfo(model);

        modelInfo.furnaceProcessed = (_result, droplet) => {
            const dropInfo = getAllInstanceInfo(model.WaitForChild("Drop"));

            dropInfo.instantiator?.();
            Packets.dropletBurnt.toAllClients(droplet.Name, new Map());
        };
    });


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Droplet from "shared/item/Droplet";
import Item from "shared/item/Item";
import Dropper from "shared/item/traits/dropper/Dropper";
import ClassLowerNegativeShop from "shared/items/negative/ClassLowerNegativeShop";

export = new Item(script.Name)
    .setName("Dual Dropper")
    .setDescription(
        "One day, a scientist realised that using the power of A, they could communize anything - and that included droppers. Thus, this amalgamation was born: one side producing $3.6K droplets, the other producing 4 W droplets, both per second.",
    )
    .setDifficulty(Difficulty.A)
    .setPrice(new CurrencyBundle().set("Funds", 2.78e12), 1)
    .setPrice(new CurrencyBundle().set("Funds", 4.1e12), 2)
    .addPlaceableArea("BarrenIslands")
    .soldAt(ClassLowerNegativeShop)

    .trait(Dropper)
    .setDroplet(Droplet.CommunismFundsDroplet)
    .setDroplet(Droplet.CommunismPowerDroplet, "PowerDrop")
    .setDropRate(1)

    .exit();


import Difficulty from "@rbxts/ejt";
import { packet } from "@rbxts/fletchette";
import { Server } from "shared/api/APIExpose";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import eat from "shared/hamster/eat";
import Item from "shared/item/Item";
import Conveyor from "shared/item/traits/conveyor/Conveyor";
import Upgrader from "shared/item/traits/upgrader/Upgrader";
import Crystal from "shared/items/excavation/Crystal";
import WhiteGem from "shared/items/excavation/WhiteGem";
import CraftingTable from "shared/items/negative/tfd/CraftingTable";

const modePacket = packet<number>();

export = new Item(script.Name)
    .setName("Electroshocked Coil")
    .setDescription(
        "Has increasing effects as Power milestones are reached: +3 W by default, +50 W at 1M W and +1000 W at 1T W.",
    )
    .setDifficulty(Difficulty.A)
    .setPrice(new CurrencyBundle().set("Funds", 1e12))
    .setRequiredItemAmount(WhiteGem, 35)
    .setRequiredItemAmount(Crystal, 1)
    .addPlaceableArea("BarrenIslands")
    .soldAt(CraftingTable)
    .setCreator("Trabitic")
    .persists()

    .trait(Upgrader)
    .trait(Conveyor)
    .setSpeed(3)
    .exit()

    .onInit((item) => {
        const upgrader = item.trait(Upgrader);
        const first = new CurrencyBundle().set("Power", 3);
        const second = new CurrencyBundle().set("Power", 50);
        const third = new CurrencyBundle().set("Power", 1000);
        let lastMode = -1;
        let mode = 0;

        const connection = Server.Currency.balanceChanged.connect((balance) => {
            const power = balance.get("Power");
            if (power === undefined || power.lessThan(1000000)) {
                upgrader.setAdd(first);
                mode = 1;
            } else if (power.lessThan(1e12)) {
                upgrader.setAdd(second);
                mode = 2;
            } else {
                upgrader.setAdd(third);
                mode = 3;
            }
            if (mode !== lastMode) {
                lastMode = mode;
                modePacket.set(mode);
            }
        });
        eat(connection, "Disconnect");
    })
    .onClientLoad((model) => {
        const rings = [model.WaitForChild(1), model.WaitForChild(2), model.WaitForChild(3)] as BasePart[];
        modePacket.observe((mode) => {
            for (let i = 0; i < 3; i++) {
                const ring = rings[i];
                ring.Transparency = mode > i ? 0 : 0.7;
            }
        });
    });


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Droplet from "shared/item/Droplet";
import Item from "shared/item/Item";
import Dropper from "shared/item/traits/dropper/Dropper";
import ExcavationStone from "shared/items/excavation/ExcavationStone";
import Gold from "shared/items/excavation/Gold";
import Grass from "shared/items/negative/tfd/Grass";
import WhiteGem from "shared/items/excavation/WhiteGem";
import CraftingTable from "shared/items/negative/tfd/CraftingTable";

export = new Item(script.Name)
    .setName("Energy Powered Dropper")
    .setDescription("Produces %val% droplets per second but drains a considerable %drain%.")
    .setDifficulty(Difficulty.A)
    .setPrice(new CurrencyBundle().set("Funds", 3e12), 1)
    .setRequiredItemAmount(ExcavationStone, 15)
    .setRequiredItemAmount(WhiteGem, 5)
    .setRequiredItemAmount(Gold, 1)
    .setRequiredItemAmount(Grass, 25)
    .addPlaceableArea("BarrenIslands")
    .soldAt(CraftingTable)
    .persists()

    .trait(Dropper)
    .setDroplet(Droplet.EnergyPoweredDroplet)
    .setDropRate(1)
    .exit()

    .setDrain(new CurrencyBundle().set("Power", 20));


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import HandCrank from "shared/item/traits/action/HandCrank";
import Boostable from "shared/item/traits/boost/Boostable";
import Generator from "shared/item/traits/generator/Generator";
import ClassLowerNegativeShop from "shared/items/negative/ClassLowerNegativeShop";

const key = "HandCrank";

export = new Item(script.Name)
    .setName("Hand Crank Generator")
    .setDescription(
        "Did you enjoy the Hand Crank Dropper? If so, you'll love the all-new Hand Crank Generator! Produces %gain%, tripling its stats when cranked.",
    )
    .setDifficulty(Difficulty.A)
    .setPrice(new CurrencyBundle().set("Power", 104000), 1)
    .setPrice(new CurrencyBundle().set("Power", 648100), 2)
    .addPlaceableArea("BarrenIslands")
    .soldAt(ClassLowerNegativeShop)

    .trait(Generator)
    .setBounceEffectEnabled(false) // conflicts with hand crank
    .setPassiveGain(new CurrencyBundle().set("Power", 26).set("Funds", 1e9))

    .trait(HandCrank)
    .setCallback((t, _, modelInfo) => {
        if (t < 5) {
            if (Boostable.hasBoost(modelInfo, key)) return;
            Boostable.addBoost(modelInfo, key, {
                ignoresLimitations: true,
                generatorCompound: { mul: new CurrencyBundle().set("Funds", 3).set("Power", 3) },
            });
        } else {
            Boostable.removeBoost(modelInfo, key);
        }
    })

    .exit();


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import InstantiationDelimiter from "shared/item/traits/InstantiationDelimiter";
import ClassLowerNegativeShop from "shared/items/negative/ClassLowerNegativeShop";
import InstantiationDelimiterI from "shared/items/negative/negativity/InstantiationDelimiterI";

export = new Item(script.Name)
    .setName("Instantiation Delimiter II")
    .setDescription("Increases droplet limit by 40, at the cost of %drain%.")
    .setDifficulty(Difficulty.A)
    .setPrice(new CurrencyBundle().set("Power", 30000), 1)
    .setRequiredItemAmount(InstantiationDelimiterI, 1)
    .addPlaceableArea("BarrenIslands")
    .soldAt(ClassLowerNegativeShop)

    .setDrain(new CurrencyBundle().set("Power", 15))

    .trait(InstantiationDelimiter)
    .setDropletIncrease(40)

    .exit();


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import Conveyor from "shared/item/traits/conveyor/Conveyor";
import OmniUpgrader from "shared/item/traits/upgrader/OmniUpgrader";
import Crystal from "shared/items/excavation/Crystal";
import ExcavationStone from "shared/items/excavation/ExcavationStone";
import Iron from "shared/items/excavation/Iron";
import WhiteGem from "shared/items/excavation/WhiteGem";
import Wool from "shared/items/negative/a/Wool";
import CraftingTable from "shared/items/negative/tfd/CraftingTable";

export = new Item(script.Name)
    .setName("Polarized Refiner")
    .setDescription(
        "The result of combining two upgraders together, with a harmful side effect. The green laser gives a x2 Funds boost while the yellow laser gives a x2 Power boost. The middle laser, however, negates all boosts and makes the droplet worthless.",
    )
    .setDifficulty(Difficulty.A)
    .setPrice(new CurrencyBundle().set("Funds", 2e12), 1)
    .setRequiredItemAmount(Wool, 1)
    .setRequiredItemAmount(ExcavationStone, 40)
    .setRequiredItemAmount(WhiteGem, 20)
    .setRequiredItemAmount(Crystal, 15)
    .setRequiredItemAmount(Iron, 2)
    .addPlaceableArea("BarrenIslands")
    .soldAt(CraftingTable)
    .setCreator("simple13579")
    .persists()

    .trait(OmniUpgrader)
    .setMuls(
        new Map([
            ["FundsLaser", new CurrencyBundle().set("Funds", 2)],
            ["PowerLaser", new CurrencyBundle().set("Power", 2)],
            ["NoneLaser", new CurrencyBundle().set("Funds", 0).set("Power", 0)],
        ]),
    )

    .trait(Conveyor)
    .setSpeed(5)

    .exit();


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import WoolShop from "shared/items/bonuses/WoolShop";

export = new Item(script.Name)
    .setName("Wool")
    .setDescription("Wool for the masses.")
    .setDifficulty(Difficulty.TrueEase)
    .setPrice(new CurrencyBundle().set("Funds", 24e12).set("Power", 100))
    .placeableEverywhere()
    .soldAt(WoolShop)
    .persists();


import { getAllInstanceInfo } from "@antivivi/vrldk";
import Difficulty from "@rbxts/ejt";
import { packet } from "@rbxts/fletchette";
import { ReplicatedStorage } from "@rbxts/services";
import { playSound } from "shared/asset/GameAssets";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import Furnace from "shared/item/traits/Furnace";
import perItemPacket, { perItemProperty } from "shared/item/utils/perItemPacket";
import ClassLowerNegativeShop from "shared/items/negative/ClassLowerNegativeShop";

declare global {
    interface InstanceInfo {
        colorStrictColor?: number;
        colorStrictTime?: number;
    }
}

const SERVER_COLOR_INFO: Record<number, () => { add?: CurrencyBundle; mul: CurrencyBundle }> = {
    1: () => ({ mul: new CurrencyBundle().set("Funds", 4500).set("Power", 6) }),
    2: () => ({ mul: new CurrencyBundle().set("Funds", 2500).set("Power", 6) }),
    3: () => ({ mul: new CurrencyBundle().set("Funds", 2500).set("Power", 10) }),
    4: () => ({ mul: new CurrencyBundle().set("Funds", 4500).set("Power", 10) }),
    5: () => ({
        add: new CurrencyBundle().set("Purifier Clicks", 1),
        mul: new CurrencyBundle().set("Funds", 3500).set("Power", 8).set("Purifier Clicks", 2),
    }),
};

const CLIENT_COLOR_INFO: Record<number, { name: string; boostLabel: string }> = {
    1: { name: "Green", boostLabel: "x4500 Funds, x6 Power" },
    2: { name: "Blue", boostLabel: "x2500 Funds, x6 Power" },
    3: { name: "Orange", boostLabel: "x2500 Funds, x10 Power" },
    4: { name: "Red", boostLabel: "x4500 Funds, x10 Power" },
    5: { name: "Violet", boostLabel: "x3500 Funds, x8 Power, +1 then x2 Purifier Clicks" },
};

interface ColorState {
    selectedColor: number;
    strictColor?: number;
    strictColorTime: number;
}

const colorStateProperty = perItemProperty(
    packet<(placementId: string, state: ColorState) => void>(),
    packet<(placementId: string) => ColorState>(),
);
const selectColorPacket = perItemPacket(packet<(placementId: string, selectedColor: number) => void>());

export = new Item(script.Name)
    .setName("Color Strict Furnace")
    .setDescription(
        "Only processes droplets if the color it's on matches with the enabled switch. Different colors apply different boosts.",
    )
    .setDifficulty(Difficulty.Exist)
    .setPrice(new CurrencyBundle().set("Funds", 2.56e15), 1)
    .addPlaceableArea("BarrenIslands")
    .soldAt(ClassLowerNegativeShop)

    .onInit((item) => {
        const replicatedStorageInfo = getAllInstanceInfo(ReplicatedStorage);

        const randomColor = () => {
            replicatedStorageInfo.colorStrictColor = math.random(1, 5);
            replicatedStorageInfo.colorStrictTime = tick();
        };
        randomColor();
        item.repeat(undefined, randomColor, 300);
    })
    .onLoad((model, item) => {
        const replicatedStorageInfo = getAllInstanceInfo(ReplicatedStorage);
        const furnace = item.trait(Furnace);
        let selectedColor = 0;
        let lastStrictColor = replicatedStorageInfo.colorStrictColor;
        let lastStrictTime = replicatedStorageInfo.colorStrictTime ?? 0;

        const applyFurnace = (strictColor?: number) => {
            if (strictColor === undefined) {
                furnace.setAdd(undefined).setMul(undefined);
                return;
            }

            const buildInfo = SERVER_COLOR_INFO[strictColor];
            if (buildInfo !== undefined && strictColor === selectedColor) {
                const info = buildInfo();
                furnace.setAdd(info.add).setMul(info.mul);
            } else {
                furnace.setAdd(undefined).setMul(undefined);
            }
        };

        const broadcastState = (strictColor?: number, strictTime?: number) => {
            colorStateProperty.set(model, {
                selectedColor,
                strictColor,
                strictColorTime: strictTime ?? lastStrictTime,
            });
        };

        const refreshState = () => {
            const replicatedStorageInfo = getAllInstanceInfo(ReplicatedStorage);

            const strictColor = replicatedStorageInfo.colorStrictColor;
            const strictTime = replicatedStorageInfo.colorStrictTime ?? 0;
            lastStrictColor = strictColor;
            lastStrictTime = strictTime;
            applyFurnace(strictColor);
            broadcastState(strictColor, strictTime);
        };

        selectColorPacket.fromClient(model, (_player, newColor) => {
            const normalized = math.clamp(math.floor(newColor), 0, 5);
            if (normalized === selectedColor) return;
            selectedColor = normalized;
            refreshState();
        });

        item.repeat(
            model,
            () => {
                const strictColor = replicatedStorageInfo.colorStrictColor;
                const strictTime = replicatedStorageInfo.colorStrictTime ?? 0;
                let shouldBroadcast = false;

                if (strictColor !== lastStrictColor) {
                    lastStrictColor = strictColor;
                    applyFurnace(strictColor);
                    shouldBroadcast = true;
                }

                if (strictTime !== lastStrictTime) {
                    lastStrictTime = strictTime;
                    shouldBroadcast = true;
                }

                if (shouldBroadcast) {
                    broadcastState(strictColor, strictTime);
                }
            },
            0.1,
        );

        refreshState();
    })
    .onClientLoad((model, item) => {
        const bar = model.WaitForChild("GuiPart").WaitForChild("SurfaceGui").WaitForChild("Bar") as Frame;
        const fill = bar.WaitForChild("Fill") as Frame;
        const colorLabel = bar.WaitForChild("ColorLabel") as TextLabel;
        const boostLabel = model
            .WaitForChild("BoostGuiPart")
            .WaitForChild("SurfaceGui")
            .WaitForChild("BoostLabel") as TextLabel;
        const hitbox = model.WaitForChild("Hitbox");
        const alertSound = hitbox.WaitForChild("AlertSound") as Sound;
        const border = model.WaitForChild("Border") as UnionOperation;

        const colorSwitches = new Map<number, BasePart>();
        for (const colorSwitch of model.GetChildren()) {
            if (colorSwitch.IsA("BasePart") && colorSwitch.Material === Enum.Material.Neon) {
                const id = tonumber(colorSwitch.Name);
                if (id === undefined) continue;
                let clickDetector = colorSwitch.FindFirstChildOfClass("ClickDetector");
                if (clickDetector === undefined) {
                    clickDetector = new Instance("ClickDetector");
                    clickDetector.Parent = colorSwitch;
                }
                colorSwitches.set(id, colorSwitch);
                clickDetector.MouseClick.Connect(() => {
                    playSound("SwitchFlick.mp3", colorSwitch);
                    selectColorPacket.toServer(model, id);
                });
            }
        }

        let currentSelected = 0;
        let currentStrictColor: number | undefined;
        let currentStrictTime = 0;

        const updateUI = () => {
            const strictColor = currentStrictColor ?? 0;
            const info = CLIENT_COLOR_INFO[strictColor];
            const strictSwitch = colorSwitches.get(strictColor);
            const selectedSwitch = currentSelected === 0 ? undefined : colorSwitches.get(currentSelected);

            if (info !== undefined) {
                colorLabel.Text = info.name + "!";
                if (strictSwitch !== undefined) {
                    fill.BackgroundColor3 = strictSwitch.Color;
                    colorLabel.TextColor3 = strictSwitch.Color;
                } else {
                    fill.BackgroundColor3 = new Color3(1, 1, 1);
                    colorLabel.TextColor3 = new Color3(1, 1, 1);
                }
                if (strictColor !== 0 && strictColor === currentSelected) {
                    boostLabel.Text = info.boostLabel;
                    if (alertSound.IsPlaying) alertSound.Stop();
                } else if (strictColor === 0 && currentSelected === 0) {
                    boostLabel.Text = info.boostLabel;
                    if (alertSound.IsPlaying) alertSound.Stop();
                } else {
                    boostLabel.Text = "WRONG COLOR";
                    if (!alertSound.IsPlaying) alertSound.Play();
                }
            } else {
                colorLabel.Text = "NO COLOR";
                fill.BackgroundColor3 = new Color3(1, 1, 1);
                colorLabel.TextColor3 = new Color3(1, 1, 1);
                boostLabel.Text = "";
                if (alertSound.IsPlaying) alertSound.Stop();
            }

            border.Color = selectedSwitch ? selectedSwitch.Color : new Color3(1, 1, 1);
        };

        colorStateProperty.observe(model, (state) => {
            currentSelected = state.selectedColor;
            currentStrictColor = state.strictColor;
            currentStrictTime = state.strictColorTime;
            updateUI();
        });

        item.repeat(
            model,
            () => {
                const progress = math.clamp((tick() - currentStrictTime) / 300, 0, 1);
                fill.Size = new UDim2(progress, 0, 1, 0);
            },
            0.1,
        );
    })

    .trait(Furnace)
    .exit();


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import NoobDropletSlayer from "shared/item/traits/other/NoobDropletSlayer";
import Upgrader from "shared/item/traits/upgrader/Upgrader";
import ClassLowerNegativeShop from "shared/items/negative/ClassLowerNegativeShop";
import DropletSlayerMkI from "shared/items/negative/negativity/DropletSlayerMkI";

export = new Item(script.Name)
    .setName("Droplet Slayer Mk. II")
    .setDescription(
        "This is getting ridiculous. Literally hire a noob to slay droplets for you, multiplying their value by %mul% every 4 seconds.",
    )
    .setDifficulty(Difficulty.Exist)
    .setPrice(new CurrencyBundle().set("Funds", 12.1e15), 1)
    .setRequiredItemAmount(DropletSlayerMkI, 1)
    .addPlaceableArea("BarrenIslands")
    .soldAt(ClassLowerNegativeShop)

    .trait(Upgrader)
    .setMul(new CurrencyBundle().set("Funds", 4).set("Power", 2))

    .trait(NoobDropletSlayer)
    .setCooldown(4)
    .exit();


import Difficulty from "@rbxts/ejt";
import { Server } from "shared/api/APIExpose";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Formula from "shared/currency/Formula";
import Item from "shared/item/Item";
import Conveyor from "shared/item/traits/conveyor/Conveyor";
import FormulaBundled from "shared/item/traits/FormulaBundled";
import Upgrader from "shared/item/traits/upgrader/Upgrader";
import ClassLowerNegativeShop from "shared/items/negative/ClassLowerNegativeShop";

export = new Item(script.Name)
    .setName("Effervescent Droplet Spray")
    .setDescription(
        `Rinses droplets to make them sparkling clean! Just put droplets above the mystical veil for an enchanting experience.

The more Power you have, the better the boost this spray applies to Funds and Power.`,
    )
    .setDifficulty(Difficulty.Exist)
    .setPrice(new CurrencyBundle().set("Funds", 504e12), 1)
    .addPlaceableArea("BarrenIslands")
    .soldAt(ClassLowerNegativeShop)

    .setFormula(new Formula().div(20).add(1).log(4).mul(0.3).add(1))
    .setFormulaX("power")
    .setFormulaXCap(new CurrencyBundle().set("Power", 10e12))

    .trait(FormulaBundled)
    .setRatio("Power", 2)
    .setRatio("Funds", 1)
    .setX(() => Server.Currency.get("Power"))
    .apply(Upgrader)

    .trait(Conveyor)
    .setSpeed(1)

    .exit();


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import Crystal from "shared/items/excavation/Crystal";
import Iron from "shared/items/excavation/Iron";
import WhiteGem from "shared/items/excavation/WhiteGem";
import Wool from "shared/items/negative/a/Wool";
import BillonIngot from "shared/items/negative/friendliness/BillonIngot";
import CrystalIngot from "shared/items/negative/unimpossible/CrystalIngot";

export = new Item(script.Name)
    .setName("Existenite")
    .setDescription("The essence of existence itself.")
    .setDifficulty(Difficulty.Exist)
    .setPrice(new CurrencyBundle().set("Power", 1))
    .setRequiredItemAmount(CrystalIngot, 1)
    .setRequiredItemAmount(BillonIngot, 1)
    .setRequiredItemAmount(Wool, 1)
    .setRequiredItemAmount(Iron, 1)
    .setRequiredItemAmount(Crystal, 1)
    .setRequiredItemAmount(WhiteGem, 1)
    .placeableEverywhere()
    .persists();


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import Conveyor from "shared/item/traits/conveyor/Conveyor";
import Upgrader from "shared/item/traits/upgrader/Upgrader";
import Crystal from "shared/items/excavation/Crystal";
import Iron from "shared/items/excavation/Iron";
import WhiteGem from "shared/items/excavation/WhiteGem";
import CraftingTable from "shared/items/negative/tfd/CraftingTable";

export = new Item(script.Name)
    .setName("Fictional Upgrader")
    .setDescription(
        "The epitome of trying to exist. Seems to boost droplet values by %mul%, but its gap can only fit 1x1 droplets.",
    )
    .setDifficulty(Difficulty.Exist)
    .setPrice(new CurrencyBundle().set("Funds", 1e15), 1)
    .setRequiredItemAmount(WhiteGem, 20)
    .setRequiredItemAmount(Crystal, 10)
    .setRequiredItemAmount(Iron, 5)
    .addPlaceableArea("BarrenIslands")
    .soldAt(CraftingTable)
    .setCreator("CoPKaDT")
    .persists()

    .trait(Upgrader)
    .setMul(new CurrencyBundle().set("Funds", 1.5).set("Power", 1.25).set("Purifier Clicks", 2))

    .trait(Conveyor)
    .setSpeed(3)

    .exit();


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import Conveyor from "shared/item/traits/conveyor/Conveyor";
import Upgrader from "shared/item/traits/upgrader/Upgrader";
import ExcavationStone from "shared/items/excavation/ExcavationStone";
import WhiteGem from "shared/items/excavation/WhiteGem";
import Wool from "shared/items/negative/a/Wool";
import CraftingTable from "shared/items/negative/tfd/CraftingTable";

export = new Item(script.Name)
    .setName("Industrial Overpass")
    .setDescription("A convoluted cross-over. Both lasers add %add% in droplet value.")
    .setDifficulty(Difficulty.Exist)
    .setPrice(new CurrencyBundle().set("Power", 1000000))
    .setRequiredItemAmount(Wool, 20)
    .setRequiredItemAmount(ExcavationStone, 30)
    .setRequiredItemAmount(WhiteGem, 15)
    .addPlaceableArea("BarrenIslands")
    .soldAt(CraftingTable)
    .setCreator("simple13579")
    .persists()

    .trait(Upgrader)
    .setAdd(new CurrencyBundle().set("Power", 5))

    .trait(Conveyor)
    .setSpeed(3)

    .exit();


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import Iron from "shared/items/excavation/Iron";

export = new Item(script.Name)
    .setName("Iron Ingot")
    .setDescription("A refined bar of iron.")
    .setDifficulty(Difficulty.Exist)
    .setPrice(new CurrencyBundle().set("Power", 1_048_576))
    .setRequiredItemAmount(Iron, 6)
    .placeableEverywhere()
    .persists();


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import Upgrader from "shared/item/traits/upgrader/Upgrader";
import AdvancedRefiner from "shared/items/negative/a/AdvancedRefiner";
import ClassLowerNegativeShop from "shared/items/negative/ClassLowerNegativeShop";

export = new Item(script.Name)
    .setName("Purifier's Refiner")
    .setDescription(
        "Upgrade one of your Advanced Refiners, keeping its original boosts while adding %add% value to droplets.",
    )
    .setDifficulty(Difficulty.Exist)
    .setPrice(new CurrencyBundle().set("Funds", 4e15), 1)
    .setRequiredItemAmount(AdvancedRefiner, 1)
    .addPlaceableArea("BarrenIslands")
    .soldAt(ClassLowerNegativeShop)

    .trait(Upgrader)
    .setAdd(new CurrencyBundle().set("Purifier Clicks", 1))
    .setMul(new CurrencyBundle().set("Funds", 1.75).set("Power", 1.75))

    .exit();


import Difficulty from "@rbxts/ejt";
import Droplet from "shared/item/Droplet";
import Dropper from "shared/item/traits/dropper/Dropper";
import Item from "shared/item/Item";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import ClassLowerNegativeShop from "shared/items/negative/ClassLowerNegativeShop";

export = new Item(script.Name)
    .setName("Rapid Dropper")
    .setDescription("Pew pew pew... or something. Produces %val% droplets every 0.5 seconds.")
    .setDifficulty(Difficulty.Exist)
    .setPrice(new CurrencyBundle().set("Funds", 920e12).set("Power", 740000), 1)
    .setPrice(new CurrencyBundle().set("Funds", 2.92e15).set("Power", 1520000), 2)
    .addPlaceableArea("BarrenIslands")
    .soldAt(ClassLowerNegativeShop)

    .trait(Dropper)
    .setDroplet(Droplet.RapidDroplet)
    .setDropRate(2)

    .exit();


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import Upgrader from "shared/item/traits/upgrader/Upgrader";
import EnergisedRefiner from "shared/items/negative/friendliness/EnergisedRefiner";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Formula from "shared/currency/Formula";
import ClassLowerNegativeShop from "shared/items/negative/ClassLowerNegativeShop";
import { Server } from "shared/api/APIExpose";

const mul = new CurrencyBundle().set("Funds", 0);

export = new Item(script.Name)
    .setName("Shocking Refiner")
    .setDescription(
        "Gives droplets a bigger shock than an Energised Refiner. Funds boost increases with Power, maxing out at %cap%. Uses %drain%.",
    )
    .setDifficulty(Difficulty.Exist)
    .setPrice(new CurrencyBundle().set("Funds", 16.25e15).set("Power", 1760000), 1)
    .setPrice(new CurrencyBundle().set("Funds", 24.6e15).set("Power", 3240000), 2)
    .setRequiredItemAmount(EnergisedRefiner, 1)
    .addPlaceableArea("BarrenIslands")
    .soldAt(ClassLowerNegativeShop)

    .setFormula(new Formula().mul(2).add(1).log(3).add(1))
    .setFormulaX("power")
    .setFormulaXCap(new CurrencyBundle().set("Power", 4e12))

    .setDrain(new CurrencyBundle().set("Power", 2000))

    .trait(Upgrader)
    .applyFormula(
        (v, item) => item.setMul(mul.set("Funds", v)),
        () => Server.Currency.get("Power"),
    )

    .exit();


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import Stone from "shared/items/0/millisecondless/Stone";
import Iron from "shared/items/excavation/Iron";
import WhiteGem from "shared/items/excavation/WhiteGem";

export = new Item(script.Name)
    .setName("Steel Ingot")
    .setDescription("A dense, gleaming bar forged from iron, stone, and rare gems.")
    .setDifficulty(Difficulty.Exist)
    .setPrice(new CurrencyBundle().set("Power", 1_048_576))
    .setRequiredItemAmount(Iron, 6)
    .setRequiredItemAmount(WhiteGem, 1)
    .setRequiredItemAmount(Stone, 1)
    .placeableEverywhere()
    .persists();


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Charger from "shared/item/traits/generator/Charger";
import Item from "shared/item/Item";
import ClassLowerNegativeShop from "shared/items/negative/ClassLowerNegativeShop";

export = new Item(script.Name)
    .setName("Advanced Charger")
    .setDescription("Boosts Power gain of generators within %radius% studs radius of this charger by %mul%.")
    .setDifficulty(Difficulty.FelixTheDA)
    .setPrice(new CurrencyBundle().set("Funds", 108e12).set("Power", 102000), 1)
    .setPrice(new CurrencyBundle().set("Funds", 226e12).set("Power", 320200), 2)
    .addPlaceableArea("BarrenIslands")
    .soldAt(ClassLowerNegativeShop)

    .trait(Charger)
    .setRadius(11)
    .setMul(new CurrencyBundle().set("Power", 4))

    .exit();


import Difficulty from "@rbxts/ejt";
import { BaseOnoeNum, OnoeNum } from "@rbxts/serikanum";
import { packet } from "@rbxts/fletchette";
import PartCacheModule from "@rbxts/partcache";
import { PartCache } from "@rbxts/partcache/out/class";
import { TweenService, Workspace } from "@rbxts/services";
import { Server, UISignals } from "shared/api/APIExpose";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import Clickable from "shared/item/traits/action/Clickable";
import Manumatic from "shared/item/traits/action/Manumatic";
import Operative from "shared/item/traits/Operative";
import Upgrader from "shared/item/traits/upgrader/Upgrader";
import NamedUpgrades from "shared/namedupgrade/NamedUpgrades";
import Packets from "shared/Packets";
import ClassLowerNegativeShop from "shared/items/negative/ClassLowerNegativeShop";

const clickedPacket = packet<(gain: BaseOnoeNum, total: BaseOnoeNum) => void>();
const getFundsBoost = (amount: OnoeNum) => OnoeNum.log(amount.div(50).add(1), 8)?.pow(2).div(2).add(1);
const getClickMultiplier = (amount: OnoeNum) => OnoeNum.log10(amount.sub(90).div(10))?.pow(2).div(6).add(1);

export = new Item(script.Name)
    .setName("Awesome Manumatic Purifier")
    .setDescription(
        "A majestic tower standing through the innumerable trials. Applies a Funds boost to raised droplets passing through it. Click the structure to increase the boost!",
    )
    .setDifficulty(Difficulty.FelixTheDA)
    .setPrice(new CurrencyBundle().set("Funds", OnoeNum.fromSerika(158, 12)), 1)
    .addPlaceableArea("BarrenIslands")
    .soldAt(ClassLowerNegativeShop)

    .trait(Manumatic)
    .trait(Upgrader)
    .exit()

    .onLoad((model, item) => {
        const upgrader = item.trait(Upgrader);
        const clickable = item.trait(Clickable);

        const CurrencyService = Server.Currency;
        const RevenueService = Server.Revenue;
        const PURIFIER_UPGRADES = NamedUpgrades.getUpgrades("Purifier");

        const update = () => {
            const amount = CurrencyService.balance.get("Purifier Clicks") ?? new OnoeNum(0);
            const fundsBoost = getFundsBoost(amount);
            upgrader.setMul(new CurrencyBundle().set("Funds", fundsBoost));
        };

        // Fortunately this is a singleton
        clickable.setOnClick((_model, _item, player, value) => {
            if (player !== undefined) {
                const data = Server.dataPerPlayer.get(player.UserId);
                if (data === undefined) return;
                const newRawClicks = ++data.rawPurifierClicks;
                Packets.rawPurifierClicks.setFor(player, newRawClicks);
            }
            const purifierClicks = CurrencyService.balance.get("Purifier Clicks");
            const cpc = purifierClicks ? getClickMultiplier(purifierClicks) : undefined;
            const baseValue = new CurrencyBundle().set("Purifier Clicks", cpc ? cpc.mul(value) : value);

            const final = RevenueService.calculateSingleRevenue(baseValue, undefined, PURIFIER_UPGRADES);

            CurrencyService.incrementAll(final.amountPerCurrency);
            clickedPacket.toAllClients(
                final.get("Purifier Clicks") ?? new OnoeNum(0),
                CurrencyService.balance.get("Purifier Clicks") ?? new OnoeNum(0),
            );
            update();
        });
        item.repeat(model, () => update(), 0.5);
        update();
    })
    .onClientLoad((model) => {
        (model.WaitForChild("Laser") as BasePart).Transparency = 0.7;
        const billboardGui = model.WaitForChild("GuiPart").WaitForChild("BillboardGui") as BillboardGui;
        const amountLabel = billboardGui.WaitForChild("AmountLabel") as TextLabel;
        const fundsLabel = billboardGui.WaitForChild("FundsLabel") as TextLabel;
        const cpcLabel = billboardGui.WaitForChild("CPCLabel") as TextLabel;

        const update = (total: BaseOnoeNum) => {
            const amount = new OnoeNum(total);
            amountLabel.Text = "Clicked: " + tostring(OnoeNum.round(amount));
            const fundsBoost = getFundsBoost(amount);
            fundsLabel.Text = `x${fundsBoost?.toString()} Funds`;

            const purifierBoostReq = new OnoeNum(100);
            if (amount.lessThan(purifierBoostReq)) {
                cpcLabel.Text = `(Click ${purifierBoostReq.sub(amount).round()} more times to unlock this boost!)`;
            } else {
                cpcLabel.Text = `${getClickMultiplier(amount)?.toString()}x Purifier Clicks/click`;
            }
        };
        update(Packets.balance.get().get("Purifier Clicks") ?? new OnoeNum(0));

        const tweenInfo = new TweenInfo(0.2);
        let shadow: BasePart | undefined;
        let partCache: PartCache;
        let shadowSize: Vector3;
        let newSize: Vector3;

        const connection = clickedPacket.fromServer((gain, total) => {
            update(total);
            UISignals.showCurrencyGain.fire(model.PrimaryPart!.Position, new Map([["Purifier Clicks", gain]]));
            model.PrimaryPart!.FindFirstChildOfClass("Sound")?.Play();

            if (shadow === undefined) {
                shadow = model.FindFirstChild("Shadow") as BasePart | undefined;
                if (shadow === undefined) {
                    return;
                }
                partCache = new PartCacheModule(shadow, 25);
                shadowSize = shadow.Size;
                newSize = shadowSize.add(new Vector3(1, 1, 1));
            }

            const clone = partCache.GetPart();
            clone.Position = shadow.Position;
            clone.Size = shadowSize;
            clone.Transparency = 0.5;
            TweenService.Create(clone, tweenInfo, { Transparency: 1, Size: newSize }).Play();
            clone.Parent = Workspace;
            task.wait(1);
            partCache.ReturnPart(clone);
        });
        model.Destroying.Once(() => {
            connection.Disconnect();
            task.delay(2, () => {
                // wait for any tweens to finish
                partCache?.Dispose();
            });
        });
    });