//!native
//!optimize 2
import { findBaseParts, getAllInstanceInfo } from "@antivivi/vrldk";
import { OnoeNum } from "@rbxts/serikanum";
import { Debris } from "@rbxts/services";
import { Server } from "shared/api/APIExpose";
import { IS_EDIT } from "shared/Context";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import Operative from "shared/item/traits/Operative";
import isPlacedItemUnusable from "shared/item/utils/isPlacedItemUnusable";
import { VirtualCollision } from "shared/item/utils/VirtualReplication";
import Packets from "shared/Packets";
import Sandbox from "shared/Sandbox";

declare global {
    interface ItemTraits {
        Furnace: Furnace;
    }
    interface InstanceInfo {
        /**
         * Whether this droplet has already been incinerated by a furnace.
         */
        incinerated?: boolean;

        /**
         * Fired when a droplet is processed by this furnace model.
         * If this is not defined, the default packet {@link Packets.dropletBurnt} will be sent to all clients instead.
         * @param result The resulting value gained from processing the droplet.
         * @param droplet The droplet being processed.
         * @param dropletInfo The instance info of the droplet being processed.
         */
        furnaceProcessed?: (result: CurrencyBundle, droplet: BasePart, dropletInfo: InstanceInfo) => void;
    }

    interface ItemBoost {
        furnaceMul?: number;
    }
}

const ZERO = new OnoeNum(0);

export default class Furnace extends Operative {
    static load(model: Model, furnace: Furnace) {
        const modelInfo = getAllInstanceInfo(model);
        const item = furnace.item;
        const isSandbox = Sandbox.getEnabled();

        for (const lava of findBaseParts(model, "Lava")) {
            const lavaInfo = getAllInstanceInfo(lava);
            lavaInfo.itemId = item.id;

            VirtualCollision.onDropletTouched(model, lava, (dropletModel, dropletInfo) => {
                if (dropletInfo.incinerated === true) return;

                const modelArea = modelInfo.areaId;
                if (modelArea === undefined) {
                    if (!IS_EDIT && !isSandbox) throw `Furnace model ${model.GetFullName()} is missing Area info`;
                } else if (modelArea !== dropletInfo.areaId && dropletInfo.lastTeleport === undefined) {
                    warn(
                        `Droplet ${dropletModel.Name} in area ${dropletInfo.areaId} tried to enter furnace in area ${modelArea}`,
                    );
                    // Sanity check: droplet should be in the same area as the furnace unless it was teleported
                    return;
                }

                if (isPlacedItemUnusable(modelInfo)) return;

                dropletInfo.incinerated = true;
                Debris.AddItem(dropletModel, 6);

                const result = Server.Revenue.calculateDropletValue(dropletModel);
                if (furnace.isCauldron) {
                    result.markAsCauldron();
                }

                // Upgrader boosts, lightning surge, etc.
                result.applySource();

                // Furnace
                if (furnace.isCalculatesFurnace === true) {
                    result.applyOperative(furnace);

                    const boosts = modelInfo.boosts;
                    if (boosts !== undefined) {
                        for (const [_, boost] of boosts) {
                            const mul = boost.furnaceMul;
                            if (mul === undefined) continue;
                            result.applyConstant(mul, "mul");
                        }
                    }

                    // Furnace variance
                    const varianceResult = furnace.varianceResult;
                    if (varianceResult !== undefined) {
                        result.applyConstant(varianceResult, "mul");
                    }
                }

                // Dark Matter, Bombs, etc.
                if (furnace.isCalculatesFinal === true) {
                    result.applyFinal();
                }

                const final = result.coalesce();
                const finalAmountPerCurrency = final.amountPerCurrency;

                for (const [currency, amount] of finalAmountPerCurrency) {
                    if (amount.equals(ZERO)) finalAmountPerCurrency.delete(currency);
                }

                // Grant currencies
                if (furnace.isCalculatesFurnace === true) {
                    Server.Currency.incrementAll(finalAmountPerCurrency);
                }

                const furnaceProcessed = modelInfo.furnaceProcessed;
                if (furnaceProcessed !== undefined) {
                    furnaceProcessed(final, dropletModel, dropletInfo);
                } else {
                    Packets.dropletBurnt.toAllClients(dropletModel.Name, finalAmountPerCurrency);
                }
            });
        }
        item.maintain(model);
    }

    variance?: number;
    varianceResult?: number;

    isCalculatesFurnace = true;
    isCalculatesFinal = true;
    isCauldron = false;

    constructor(item: Item) {
        super(item);
        item.onInit(() => {
            if (this.variance === undefined) return;

            item.repeat(
                undefined,
                () => {
                    const variance = this.variance;
                    if (variance === undefined) return;
                    this.varianceResult = math.random() * variance + 1 - variance * 0.5;
                },
                0.7,
            );
        });
        item.onLoad((model) => Furnace.load(model, this));
    }

    /**
     * Sets the factor of random variance applied to droplet values when processed by this furnace.
     * @param variance The variance factor (0 = no variance, 1 = ±50% variance, 2 = ±100% variance).
     * @returns The furnace trait.
     */
    setVariance(variance: number) {
        if (variance > 2) throw `Furnace variance cannot be greater than 2 (received ${variance})`;
        if (variance < 0) throw `Furnace variance cannot be less than 0 (received ${variance})`;

        this.variance = variance;
        return this;
    }

    /**
     * Whether the furnace will process its boost and grant currencies.
     * @param includesFurnace Whether to include furnace boosts and currency grants.
     * @returns The furnace trait.
     */
    calculatesFurnace(includesFurnace: boolean) {
        this.isCalculatesFurnace = includesFurnace;
        return this;
    }

    /**
     * Whether the furnace will process final boosts such as Dark Matter and Bombs.
     * @param includesFinalBoosts Whether to include final boosts.
     * @returns The furnace trait.
     */
    calculatesFinal(includesFinalBoosts: boolean) {
        this.isCalculatesFinal = includesFinalBoosts;
        return this;
    }

    /**
     * Whether this furnace is a cauldron, disabling upgrader boosts if processed droplets are not sky droplets.
     * @param isCauldron Whether this furnace is a cauldron.
     * @returns The furnace trait.
     */
    setIsCauldron(isCauldron: boolean) {
        this.isCauldron = isCauldron;
        return this;
    }
}


import { OnoeNum } from "@rbxts/serikanum";
import Item from "shared/item/Item";

declare global {
    /**
     * Contains all item traits.
     *
     * @see {@link ItemTrait} for the base class of all item traits.
     */
    interface ItemTraits {}
}

/**
 * Represents a trait for building a specific type of item.
 */
export default abstract class ItemTrait {
    /**
     * Represents a trait for building a specific type of item.
     *
     * @param item The item instance to be built.
     */
    constructor(public readonly item: Item) {}

    /**
     * Returns the item instance of the trait.
     *
     * @returns The item instance.
     */
    exit() {
        return this.item;
    }

    /**
     * Redirects to another trait to build a specific type of item.
     *
     * @param Trait The constructor of the type of item.
     * @returns The redirected item trait.
     */
    trait<T extends ItemTraits[keyof ItemTraits]>(Trait: Constructor<T>): T {
        return this.item.trait(Trait);
    }

    /**
     * Calls the callback function every second by passing the return of the x function in the formula function, and passing the return of that to the callback.
     *
     * @param callback Called every second with the `value` parameter passed as the return of `this.formula`.
     * @param x The value to be used in the formula.
     *
     * @see {@link Item.applyFormula} for the unwrapped version of this function.
     */
    applyFormula(callback: (value: OnoeNum, upgrader: this) => unknown, x: () => OnoeNum) {
        const item = this.item;
        item.formulaCallback = (value) => callback(value, this);
        item.formulaXGet = x;
        return this;
    }

    /**
     * Formats a string with the item trait's properties.
     *
     * @param str The string to format.
     */
    format(str: string) {
        return str;
    }
}


//!native
//!optimize 2

import { OnoeNum } from "@rbxts/serikanum";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import { CURRENCIES } from "shared/currency/CurrencyDetails";
import Item from "shared/item/Item";
import ItemTrait from "shared/item/traits/ItemTrait";

declare global {
    interface ItemTraits {
        Operative: Operative;
    }
}

const ONE = new OnoeNum(1);
const ZERO = new OnoeNum(0);
const ONES = CurrencyBundle.ones();

export interface IOperative {
    /**
     * Addition (x+y) term.
     */
    add?: CurrencyBundle;

    /**
     * Multiplication (x*y) term.
     */
    mul?: CurrencyBundle;

    /**
     * Power (x^y) term.
     */
    pow?: CurrencyBundle;
}

/**
 * Provides mathematical operations to an item.
 */
export default class Operative extends ItemTrait implements IOperative {
    add?: CurrencyBundle;
    mul?: CurrencyBundle;
    pow?: CurrencyBundle;

    /**
     * Constructs an Operative trait.
     *
     * @param item The item to which this trait belongs.
     */
    constructor(item: Item) {
        super(item);
    }

    /**
     * Sets the addition term.
     *
     * @param add Addition term.
     * @returns This operative.
     */
    setAdd(add: CurrencyBundle | undefined) {
        this.add = add;
        return this;
    }

    /**
     * Sets the multiplication term.
     *
     * @param mul Multiplication term.
     * @returns This operative.
     */
    setMul(mul: CurrencyBundle | undefined) {
        this.mul = mul;
        return this;
    }

    /**
     * Sets the power term.
     *
     * @param pow Power term.
     * @returns This operative.
     */
    setPow(pow: CurrencyBundle | undefined) {
        this.pow = pow;
        return this;
    }

    format(str: string): string {
        if (this.add !== undefined) str = str.gsub("%%add%%", this.add.toString(true, "+"))[0];
        if (this.mul !== undefined) str = str.gsub("%%mul%%", this.mul.toString(true, "x"))[0];
        if (this.pow !== undefined) str = str.gsub("%%pow%%", this.pow.toString(true, "^"))[0];
        return str;
    }

    /**
     * Determines if this operative is less than another in one {@link Currency}.
     * Priority is given to the power term, then multiplication, then addition.
     *
     * @param other The other operative.
     * @param currency The currency to compare.
     * @returns True if this operative is less than the other in the specified currency.
     */
    lessThan(other: IOperative, currency: Currency) {
        const otherPow = other.pow?.get(currency) ?? ONE;
        const otherMul = other.mul?.get(currency) ?? ONE;
        const otherAdd = other.add?.get(currency) ?? ZERO;
        const pow = this.pow?.get(currency) ?? ONE;
        const mul = this.mul?.get(currency) ?? ONE;
        const add = this.add?.get(currency) ?? ZERO;
        return pow.lessThan(otherPow) || mul.lessThan(otherMul) || add.lessThan(otherAdd);
    }

    /**
     * Gets all currencies involved in the operations.
     *
     * @returns A set of currencies.
     */
    getCurrencies() {
        const currencies = new Set<Currency>();
        if (this.add !== undefined) this.add.amountPerCurrency.forEach((_, currency) => currencies.add(currency));
        if (this.mul !== undefined) this.mul.amountPerCurrency.forEach((_, currency) => currencies.add(currency));
        if (this.pow !== undefined) this.pow.amountPerCurrency.forEach((_, currency) => currencies.add(currency));
        return currencies;
    }

    /**
     * Apply operations to values.
     *
     * @param totalAdd Addition term to apply to.
     * @param totalMul Multiplication term to apply to.
     * @param totalPow Power term to apply to.
     * @param add Addition term.
     * @param mul Multiplication term.
     * @param pow Power term.
     * @param inverse Whether to apply the inverse of the operations.
     * @param repeats Number of times to repeat the operations. Default is 1, which means no repetition.
     * @param inplace Whether to perform operations in-place.
     * @returns The resulting addition, multiplication, and power terms.
     */
    static applySpreadOperative(
        totalAdd?: CurrencyBundle,
        totalMul?: CurrencyBundle,
        totalPow?: CurrencyBundle,
        add?: CurrencyBundle,
        mul?: CurrencyBundle,
        pow?: CurrencyBundle,
        inverse?: boolean,
        repeats?: number,
        inplace?: boolean,
    ) {
        if (repeats !== undefined) {
            if (add !== undefined) add = add.mulConstant(repeats, inplace);
            if (mul !== undefined) mul = mul.powConstant(repeats, inplace);
            if (pow !== undefined) pow = pow.powConstant(repeats, inplace);
        }

        if (inverse === true) {
            if (add !== undefined) totalAdd = totalAdd?.sub(add, inplace);
            if (mul !== undefined) totalMul = totalMul?.div(mul, inplace);
            if (pow !== undefined) totalPow = totalPow?.mul(ONES.div(pow), inplace);
        } else {
            if (add !== undefined) totalAdd = totalAdd?.add(add, inplace);
            if (mul !== undefined) totalMul = totalMul?.mul(mul, inplace);
            if (pow !== undefined) totalPow = totalPow?.mul(pow, inplace);
        }
        return $tuple(totalAdd, totalMul, totalPow);
    }

    /**
     * Applies an operative to values.
     * @param totalAdd Addition term to apply to.
     * @param totalMul Multiplication term to apply to.
     * @param totalPow Power term to apply to.
     * @param operative The operative to apply.
     * @param inverse Whether to apply the inverse of the operations.
     * @param repeats Number of times to repeat the operations. Default is 1, which means no repetition.
     * @param inplace Whether to perform operations in-place.
     * @returns The resulting addition, multiplication, and power terms.
     */
    static applyOperative(
        totalAdd: CurrencyBundle,
        totalMul: CurrencyBundle,
        totalPow: CurrencyBundle,
        operative: IOperative,
        inverse?: boolean,
        repeats?: number,
        inplace?: boolean,
    ): LuaTuple<[CurrencyBundle, CurrencyBundle, CurrencyBundle]> {
        return Operative.applySpreadOperative(
            totalAdd,
            totalMul,
            totalPow,
            operative.add,
            operative.mul,
            operative.pow,
            inverse,
            repeats,
            inplace,
        ) as LuaTuple<[CurrencyBundle, CurrencyBundle, CurrencyBundle]>;
    }

    /**
     * Applies operations to values.
     *
     * @param add Addition term.
     * @param mul Multiplication term.
     * @param pow Power term.
     * @returns A tuple containing the addition, multiplication, and power terms after applying the operations.
     */
    apply(
        add: CurrencyBundle,
        mul: CurrencyBundle,
        pow: CurrencyBundle,
    ): LuaTuple<[CurrencyBundle, CurrencyBundle, CurrencyBundle]>;
    /**
     * Applies all operations in the operative to a value.
     *
     * @param value The value to which to apply the operations.
     * @returns The value after all operations.
     */
    apply(value: CurrencyBundle): CurrencyBundle;
    apply(add: CurrencyBundle, mul?: CurrencyBundle, pow?: CurrencyBundle, repeats?: number) {
        if (mul !== undefined && pow !== undefined) {
            return Operative.applySpreadOperative(add, mul, pow, this.add, this.mul, this.pow, false, repeats);
        }

        let totalAdd = this.add;
        let totalMul = this.mul;
        let totalPow = this.pow;
        if (repeats !== undefined) {
            if (totalAdd !== undefined) totalAdd = totalAdd.mulConstant(repeats);
            if (totalMul !== undefined) totalMul = totalMul.powConstant(repeats);
            if (totalPow !== undefined) totalPow = totalPow.powConstant(repeats);
        }
        return Operative.coalesce(add, totalAdd, totalMul, totalPow);
    }

    /**
     * Coalesce all values together with built-in validation.
     *
     * @param value The base value.
     * @param totalAdd Total addition term.
     * @param totalMul Total multiplication term.
     * @param totalPow Total power term.
     * @returns The value after all operations.
     */
    static coalesce(
        value: CurrencyBundle,
        totalAdd?: CurrencyBundle,
        totalMul?: CurrencyBundle,
        totalPow?: CurrencyBundle,
    ) {
        const newCurrencies = new Map<Currency, OnoeNum>();
        const base = value.amountPerCurrency;
        const adds = totalAdd?.amountPerCurrency;
        const muls = totalMul?.amountPerCurrency;
        const pows = totalPow?.amountPerCurrency;
        for (const currency of CURRENCIES) {
            let amount = base.get(currency);

            if (adds !== undefined) {
                const add = adds.get(currency);
                if (add !== undefined) amount = amount === undefined ? add : amount.add(add);
            }

            if (amount === undefined) {
                continue;
            }

            if (muls !== undefined) {
                const mul = muls.get(currency);
                if (mul !== undefined) amount = amount.mul(mul);
            }

            if (pows !== undefined) {
                const pow = pows.get(currency);
                if (pow !== undefined) amount = amount.pow(pow);
            }

            if (amount.lessThan(ZERO)) {
                amount = ZERO;
            }

            newCurrencies.set(currency, amount);
        }

        return new CurrencyBundle(newCurrencies);
    }
}


import { formatRichText } from "@antivivi/vrldk";
import { OnoeNum } from "@rbxts/serikanum";
import { Server } from "shared/api/APIExpose";
import Item from "shared/item/Item";
import ItemTrait from "shared/item/traits/ItemTrait";
import { VirtualAttribute } from "shared/item/utils/VirtualReplication";

declare global {
    interface ItemTraits {
        Unique: Unique;
    }

    /**
     * Configuration for a pot (unique stat) that can be generated for a unique item.
     * Defines the scaling range for converting raw 0-100 percentage values to actual values.
     */
    interface PotConfig {
        /**
         * The minimum value for this pot when scaling from 0%.
         */
        min: number;

        /**
         * The maximum value for this pot when scaling from 100%.
         */
        max: number;

        /**
         * Whether the scaled value should be an integer (default: false).
         */
        integer?: boolean;
    }
}

/**
 * Trait for items that can be generated as unique instances with randomized pots.
 *
 * Unique items have randomly generated stats (called "pots") that vary between instances
 * of the same item type. Pot values are stored as raw percentages (0-100) and scaled
 * to the configured min-max ranges when accessed. This ensures that changing pot ranges
 * after publication won't cause inconsistency for existing items.
 *
 * For example, an "Admirer" unique item might boost Admiration from 5% to 100% depending
 * on its pot value, but the raw percentage is always stored as 0-100.
 */
export default class Unique extends ItemTrait {
    /**
     * Configuration for the pots that can be generated for this unique item.
     * Key is the pot name, value is the configuration for that pot.
     */
    private readonly potConfigs = new Map<string, PotConfig>();

    /**
     * Callback to be called when the unique item is loaded.
     * This can be used to apply effects based on the generated pot values.
     */
    private loadCallback: ((model: Model, unique: Unique, scaledPots: Map<string, number>) => void) | undefined;

    private readonly MIN_COLOR = Color3.fromRGB(255, 0, 0);
    private readonly MAX_COLOR = Color3.fromRGB(0, 255, 255);

    static load(model: Model, unique: Unique) {
        const placedItem = Server.Item.getPlacedItem(model.Name);
        if (placedItem === undefined) {
            throw `Unique item model ${model.Name} not found in placed items.`;
        }

        const uuid = placedItem.uniqueItemId;
        if (uuid === undefined) {
            throw `Unique item model ${model.Name} does not have a unique item UUID.`;
        }

        const uniqueInstance = Server.empireData.items.uniqueInstances.get(uuid);
        if (uniqueInstance === undefined) {
            throw `Unique item instance for UUID ${uuid} not found.`;
        }

        const aura = model.FindFirstChild("Aura") as BasePart | undefined;
        if (aura !== undefined) {
            aura.CanCollide = false;
            let maxValue = 0;
            let totalValue = 0;
            for (const [_, potValue] of uniqueInstance.pots) {
                maxValue += 100;
                totalValue += potValue;
            }
            VirtualAttribute.setNumber(model, aura, "Potential", totalValue / maxValue);
        }

        const scaledPots = unique.getScaledPots(uniqueInstance);
        if (unique.loadCallback !== undefined) {
            unique.loadCallback(model, unique, scaledPots);
        }
    }

    static clientLoad(model: Model, unique: Unique) {
        const aura = model.FindFirstChild("Aura") as BasePart | undefined;
        if (aura !== undefined) {
            VirtualAttribute.observeNumber(model, aura, "Potential", (value) => {
                const alpha = math.clamp(value, 0, 1);
                const color = unique.MIN_COLOR.Lerp(unique.MAX_COLOR, alpha);
                aura.Color = color;
            });
        }
    }

    constructor(item: Item) {
        super(item);
        item.onLoad((model) => Unique.load(model, this));
        item.onClientLoad((model) => Unique.clientLoad(model, this));
    }

    onLoad(callback: (model: Model, unique: Unique, scaledPots: Map<string, number>) => void) {
        this.loadCallback = callback;
        return this;
    }

    /**
     * Adds a pot configuration to this unique item.
     *
     * @param name The name of the pot (e.g., "admirationBoost", "dropRate").
     * @param min The minimum value for this pot.
     * @param max The maximum value for this pot.
     * @param integer Whether this value should be an integer (default: false).
     * @returns This trait for method chaining.
     *
     * @example
     * ```ts
     * item.trait(Unique)
     *     .addPot("admirationBoost", 0.05, 1.0)
     *     .addPot("dropRateMultiplier", 1.1, 2.5);
     * ```
     */
    addPot(name: string, min: number, max: number, integer: boolean = false): this {
        this.potConfigs.set(name, {
            min,
            max,
            integer,
        });
        return this;
    }

    /**
     * Gets the pot configurations for this unique item.
     *
     * @returns A read-only map of pot configurations.
     */
    getPotConfigs(): ReadonlyMap<string, PotConfig> {
        return this.potConfigs;
    }

    /**
     * Generates a new unique item instance with randomly generated pot values.
     * Pot values are generated as percentages (0-100) and scaled when accessed.
     *
     * @param allPots Optional parameter to specify a fixed value for all pots (0-100).
     * @returns A unique item instance with generated pots.
     */
    generateInstance(allPots?: number): UniqueItemInstance {
        const pots = new Map<string, number>();

        for (const [potName] of this.potConfigs) {
            // Generate a random percentage from 0 to 100
            const randomPercentage = (allPots ?? math.random()) * 100;
            pots.set(potName, randomPercentage);
        }

        return {
            baseItemId: this.item.id,
            pots,
            created: tick(),
        };
    }

    /**
     * Gets all scaled pot values for a unique item instance.
     *
     * @param instance The unique item instance.
     * @returns A map of pot names to their scaled values.
     */
    getScaledPots(instance: UniqueItemInstance): Map<string, number> {
        const scaledPots = new Map<string, number>();

        for (const [potName, config] of this.potConfigs) {
            let rawValue = instance.pots.get(potName);
            if (rawValue === undefined) {
                warn(`Pot ${potName} not found in unique item instance for ${instance.baseItemId}`);
                instance.pots.set(potName, 0);
                rawValue = 0;
            }
            let scaledValue = config.min + (rawValue / 100) * (config.max - config.min);
            if (config.integer) {
                scaledValue = math.floor(scaledValue);
            }
            scaledPots.set(potName, scaledValue);
        }

        return scaledPots;
    }

    /**
     * Formats a string with pot values from a unique item instance.
     * Uses scaled pot values based on the current pot configurations.
     *
     * @param str The string to format with pot placeholders (e.g., "%admirationBoost%").
     * @param instance The unique item instance to get pot values from.
     * @returns The formatted string.
     */
    formatWithPots(str: string, instance: UniqueItemInstance): string {
        const scaledPots = this.getScaledPots(instance);

        for (const [potName, config] of this.potConfigs) {
            const value = scaledPots.get(potName);
            if (value === undefined) continue;
            const placeholder = `%%${potName}%%`;
            let formatted = new OnoeNum(value).toString();
            const alpha = (value - config.min) / (config.max - config.min);
            formatted = formatRichText(formatted, this.MIN_COLOR.Lerp(this.MAX_COLOR, alpha));
            str = str.gsub(placeholder, formatted)[0];
        }
        return str;
    }
}


import { OnoeNum } from "@rbxts/serikanum";
import { formatRichText, getAllInstanceInfo } from "@antivivi/vrldk";
import { packet } from "@rbxts/fletchette";
import { Server } from "shared/api/APIExpose";
import { CURRENCY_DETAILS } from "shared/currency/CurrencyDetails";
import Item from "shared/item/Item";
import ItemTrait from "shared/item/traits/ItemTrait";
import getPlacedItemsInBounds from "shared/item/utils/getPlacedItemsInBounds";
import isPlacedItemUnusable from "shared/item/utils/isPlacedItemUnusable";
import perItemPacket from "shared/item/utils/perItemPacket";

declare global {
    interface ItemTraits {
        Clicker: Clicker;
    }

    interface ItemBoost {
        clickValueMul?: number;
    }
}

export default class Clicker extends ItemTrait {
    static readonly clickingPacket = perItemPacket(packet<(placementId: string) => void>({ isUnreliable: true }), {
        slowStart: true,
    });

    static load(model: Model, clicker: Clicker) {
        const modelInfo = getAllInstanceInfo(model);

        const clickArea = model.WaitForChild("ClickArea") as BasePart;
        clickArea.CanTouch = true;
        clickArea.CollisionGroup = "ItemHitbox";
        clickArea.Touched.Connect(() => {});

        let target: Model | undefined;
        let event: BindableEvent | undefined;
        let t = 0;
        clicker.item.repeat(model, (dt) => {
            t += dt;
            if (target === undefined || target.Parent === undefined) {
                if (t > 0.05) {
                    t = 0;
                    const found = getPlacedItemsInBounds(clickArea);
                    for (const [model, item] of found)
                        if (item.isA("Clickable")) {
                            target = model;
                            return;
                        }
                }
            } else if (t > 1 / (clicker.clickRate ?? 999)) {
                t = 0;
                if (event === undefined || event.Parent === undefined) {
                    event = target.WaitForChild("Click") as BindableEvent;
                }

                if (isPlacedItemUnusable(modelInfo)) return;

                let rate = 1;
                if (modelInfo.boosts !== undefined) {
                    for (const [, boost] of modelInfo.boosts) {
                        rate *= boost.clickValueMul ?? 1;
                    }
                }
                event.Fire(clicker.clickValue * rate);

                if (clicker.onClick !== undefined) clicker.onClick(model, clicker);
                if (clicker.replicatingClicks === true) this.clickingPacket.toAllClients(model);
            }
        });
    }

    replicatingClicks = false;
    clickRate: number | undefined = undefined;
    clickValue = 1;
    onClick: ((model: Model, item: this) => void) | undefined = undefined;

    constructor(item: Item) {
        super(item);
        item.onLoad((model) => Clicker.load(model, this));
    }

    getCPS() {
        return (this.clickRate ?? 999) * this.clickValue;
    }

    setClickRate(clickRate: number) {
        this.clickRate = clickRate;
        return this;
    }

    setClickValue(clickValue: number) {
        this.clickValue = clickValue;
        return this;
    }

    setOnClick(onClick: (model: Model, item: this) => void) {
        this.onClick = onClick;
        return this;
    }

    replicateClicks() {
        this.replicatingClicks = true;
        return this;
    }

    format(str: string) {
        return str.gsub(
            "%%cps%%",
            formatRichText(`${OnoeNum.toString(this.getCPS())} CPS`, CURRENCY_DETAILS["Purifier Clicks"].color),
        )[0];
    }

    /**
     * Sets up a handler that is called when this clicker has clicked from the server.
     * @param model The model of the clicker.
     * @param callback The callback to call when the clicker is clicked.
     */
    fromServerClicked(model: Model, callback: () => void) {
        Clicker.clickingPacket.fromServer(model, callback);
    }
}


import { getAllInstanceInfo, weldModel } from "@antivivi/vrldk";
import { packet } from "@rbxts/fletchette";
import { RunService, TweenService } from "@rbxts/services";
import { playSound } from "shared/asset/GameAssets";
import { getPlayerCharacter } from "shared/hamster/getPlayerCharacter";
import Item from "shared/item/Item";
import ItemTrait from "shared/item/traits/ItemTrait";
import isPlacedItemUnusable from "shared/item/utils/isPlacedItemUnusable";
import perItemPacket from "shared/item/utils/perItemPacket";
import CustomProximityPrompt from "shared/world/CustomProximityPrompt";

export default class HandCrank extends ItemTrait {
    static readonly crankedPacket = perItemPacket(packet<(placementId: string) => void>());
    callback?: (timeSinceCrank: number, model: Model, modelInfo: InstanceInfo) => void;

    static load(model: Model, handCrank: HandCrank) {
        const modelInfo = getAllInstanceInfo(model);

        let t = 0;
        handCrank.item.repeat(model, () => handCrank.callback?.(os.clock() - t, model, modelInfo), 0.1);

        const crank = model.WaitForChild("Crank") as Model;
        const crankPrimaryPart = crank.PrimaryPart;
        if (crankPrimaryPart === undefined) {
            warn(`HandCrank item ${model.Name} is missing a PrimaryPart on its Crank model.`);
            return;
        }

        const onCrank = (player?: Player) => {
            if (isPlacedItemUnusable(modelInfo) || os.clock() - t < 1) return;

            const character = getPlayerCharacter(player);
            if (character === undefined) return;

            const distance = character.GetPivot().Position.sub(model.GetPivot().Position).Magnitude;
            if (distance > 15) return;

            t = os.clock();
            HandCrank.crankedPacket.toAllClients(model);
        };

        const proximityPrompt = crankPrimaryPart.FindFirstChildOfClass("ProximityPrompt");
        if (proximityPrompt !== undefined) {
            const cleanup = CustomProximityPrompt.onTrigger(proximityPrompt, onCrank);
            model.Destroying.Once(cleanup);
        }
    }

    static clientLoad(model: Model, _handCrank: HandCrank) {
        const crank = model.WaitForChild("Crank") as Model;
        const crankPrimaryPart = weldModel(crank);
        const crankPrimaryPartOriginal = crankPrimaryPart.CFrame;

        HandCrank.crankedPacket.fromServer(model, () => {
            const update = (rotation: number) => {
                return (crankPrimaryPart.CFrame = crankPrimaryPartOriginal.mul(
                    CFrame.Angles(0, 0, math.rad(rotation)),
                ));
            };
            playSound("HandCrank.mp3", crankPrimaryPart);
            let t = 0;
            const connection = RunService.Heartbeat.Connect((dt) => {
                t += dt;
                const rotation = TweenService.GetValue(t, Enum.EasingStyle.Quad, Enum.EasingDirection.Out) * 360;
                update(rotation);
                if (rotation >= 360) {
                    update(0);
                    connection.Disconnect();
                }
            });
        });
    }

    constructor(item: Item) {
        super(item);
        item.onLoad((model) => HandCrank.load(model, this));
        item.onClientLoad((model) => HandCrank.clientLoad(model, this));
    }

    setCallback(callback: (timeSinceCrank: number, model: Model, modelInfo: InstanceInfo) => void) {
        this.callback = callback;
        return this;
    }
}


import { getAllInstanceInfo } from "@antivivi/vrldk";
import { packet } from "@rbxts/fletchette";
import Item from "shared/item/Item";
import ItemTrait from "shared/item/traits/ItemTrait";
import Clickable from "shared/item/traits/action/Clickable";
import isPlacedItemUnusable from "shared/item/utils/isPlacedItemUnusable";
import perItemPacket from "shared/item/utils/perItemPacket";

const clientClickedPacket = perItemPacket(packet<(placementId: string) => void>());

export default class Manumatic extends ItemTrait {
    static load(model: Model, manumatic: Manumatic) {
        const modelInfo = getAllInstanceInfo(model);
        const clickable = manumatic.item.trait(Clickable);

        let last = 0;
        const click = (player: Player | undefined, value: number) => {
            if (isPlacedItemUnusable(modelInfo)) return;

            const onClick = clickable.onClick;
            if (onClick === undefined) return;

            onClick(model, clickable, player, value);
        };
        clientClickedPacket.fromClient(model, (player) => {
            const now = tick();
            if (now - last < clickable.debounce) {
                return;
            }
            click(player, 1);
            last = now;
        });

        const event = new Instance("BindableEvent");
        event.Name = "Click";
        event.Event.Connect((clickValue: number) => click(undefined, clickValue));
        event.Parent = model;
    }

    static clientLoad(model: Model, _manumatic: Manumatic) {
        const clickDetector = new Instance("ClickDetector");
        clickDetector.MouseClick.Connect(() => {
            clientClickedPacket.toServer(model);
        });
        clickDetector.Parent = model.WaitForChild("ClickArea");
    }

    constructor(item: Item) {
        super(item);
        item.onLoad((model) => Manumatic.load(model, this));
        item.onClientLoad((model) => Manumatic.clientLoad(model, this));
    }
}


import { getAllInstanceInfo } from "@antivivi/vrldk";
import { packet } from "@rbxts/fletchette";
import { OnoeNum } from "@rbxts/serikanum";
import { Server } from "shared/api/APIExpose";
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 ItemTrait from "shared/item/traits/ItemTrait";
import isPlacedItemUnusable from "shared/item/utils/isPlacedItemUnusable";
import perItemPacket from "shared/item/utils/perItemPacket";
import Packets from "shared/Packets";
import { AREAS } from "shared/world/Area";

declare global {
    interface ItemTraits {
        Condenser: Condenser;
    }

    interface InstanceInfo {
        /**
         * Whether the droplet has been produced by a condenser.
         * Used to prevent condensers from feeding into each other.
         */
        condensed?: boolean;
    }
}

const storeChangedPacket = perItemPacket(packet<(placementId: string, current: BaseCurrencyMap) => void>());

/**
 * A condenser is an item that 'condenses' multiple droplets into a single droplet, usually of higher value.
 */
export default class Condenser extends ItemTrait {
    droplets = new Array<Droplet>();
    totalValue = new CurrencyBundle();

    quota = 1;

    static getPricePerDroplet(condenser: Condenser) {
        const pricePerDroplet = new Map<Droplet, CurrencyBundle>();
        for (const droplet of condenser.droplets) {
            const value = droplet.value;
            if (value === undefined) continue;
            const price = value.mulConstant(condenser.quota);
            pricePerDroplet.set(droplet, price);
        }
        return pricePerDroplet;
    }

    static load(model: Model, condenser: Condenser) {
        const modelInfo = getAllInstanceInfo(model);

        const item = condenser.item;
        const dropper = item.trait(Dropper);
        const areaId = Server.Item.getPlacedItem(model.Name)?.area;
        const area = areaId !== undefined ? AREAS[areaId as AreaId] : undefined;
        const instantiatorsPerDroplet = new Map<Droplet, () => BasePart>();
        const pricePerDroplet = Condenser.getPricePerDroplet(condenser);
        const maxCosts = new Map<Currency, OnoeNum>();
        for (const [, price] of pricePerDroplet) {
            for (const [currency, amount] of price.amountPerCurrency) {
                maxCosts.set(currency, amount.mul(10));
            }
        }
        for (const droplet of condenser.droplets) {
            const drop = model.WaitForChild("Drop") as BasePart;
            instantiatorsPerDroplet.set(
                droplet,
                Dropper.wrapInstantiator(droplet.getInstantiator(model, drop), dropper, model, drop),
            );
        }

        let current = new CurrencyBundle();
        const upgrades = new Map<string, UpgradeInfo>();

        const check = (force?: boolean) => {
            if (isPlacedItemUnusable(modelInfo)) {
                return;
            }

            let changed = false;
            pricePerDroplet.forEach((price, droplet) => {
                if (area !== undefined && area.dropletCount > area.getDropletLimit()) {
                    return;
                }
                let dontResetUpgrades = false;
                for (const [currency, amount] of pairs(price.amountPerCurrency)) {
                    const currentCost = current.get(currency);
                    if (currentCost === undefined) return;

                    const percentage = currentCost.div(amount);
                    if (percentage.lessThan(1)) {
                        return;
                    }
                    if (percentage.moreThan(1.15)) dontResetUpgrades = true;
                }

                current.sub(price, true);
                changed = true;
                const instantiator = instantiatorsPerDroplet.get(droplet);
                if (instantiator !== undefined) {
                    const droplet = instantiator();
                    const instanceInfo = getAllInstanceInfo(droplet);
                    const replacingUpgrades = instanceInfo.upgrades ?? new Map();
                    for (const [id, upgradeInfo] of upgrades) {
                        const pointer = table.clone(upgradeInfo);
                        pointer.empty = true;
                        replacingUpgrades.set(id, pointer);
                    }
                    if (!dontResetUpgrades) upgrades.clear();
                    instanceInfo.upgrades = replacingUpgrades;
                    instanceInfo.condensed = true;
                }
            });
            if (changed || force === true) {
                storeChangedPacket.toAllClients(model, current.amountPerCurrency);
            }
        };

        modelInfo.furnaceProcessed = (value, droplet) => {
            const instanceInfo = getAllInstanceInfo(droplet);
            if (instanceInfo.dropletId === undefined || instanceInfo.condensed === true) {
                return;
            }

            const currentMap = current.amountPerCurrency;
            const lostValue = new Map<Currency, OnoeNum>();
            for (const [currency, amount] of value.amountPerCurrency) {
                const prev = currentMap.get(currency);
                const newCost = prev === undefined ? amount : prev.add(amount);
                const limit = maxCosts.get(currency);
                if (limit === undefined) {
                    if (amount.moreThan(0)) {
                        lostValue.set(currency, amount);
                    }
                } else {
                    currentMap.set(currency, OnoeNum.min(newCost, limit));
                }
            }

            if (!lostValue.isEmpty()) {
                Server.Currency.incrementAll(lostValue);
                Packets.dropletBurnt.toAllClients(droplet.Name, lostValue);
            } else {
                Packets.dropletBurnt.toAllClients(droplet.Name, new Map());
            }
            const u = instanceInfo.upgrades;
            if (u !== undefined) {
                for (const [id, upgrade] of u) {
                    upgrades.set(id, upgrade);
                }
            }

            check(true);
        };

        item.repeat(model, () => check(), 0.5);
    }

    static clientLoad(model: Model, condenser: Condenser) {
        const surfaceGui = model.WaitForChild("GuiPart").FindFirstChildOfClass("SurfaceGui");
        if (surfaceGui === undefined) {
            return;
        }
        const pricePerDroplet = Condenser.getPricePerDroplet(condenser);
        const update = (current: BaseCurrencyMap) => {
            pricePerDroplet.forEach((price) => {
                for (const [currency, amount] of pairs(price.amountPerCurrency)) {
                    const currentAmount = new OnoeNum(current.get(currency) ?? 0);
                    const progress = currentAmount.div(amount).revert();
                    const bar = surfaceGui.WaitForChild(currency + "Bar", 1);
                    if (bar === undefined) {
                        return;
                    }
                    (bar.WaitForChild("Fill") as Frame).Size = new UDim2(math.min(progress, 1), 0, 1, 0);
                    (bar.WaitForChild("PercentageLabel") as TextLabel).Text = math.floor(progress * 100) + "%";
                }
            });
        };
        update(new Map());
        storeChangedPacket.fromServer(model, update);
    }

    /**
     * Creates a new condenser.
     *
     * @param item The item to create the condenser from.
     */
    constructor(item: Item) {
        super(item);
        const furnace = item.trait(Furnace);
        const dropper = item.trait(Dropper);
        furnace.calculatesFinal(false).calculatesFurnace(false);
        dropper.dropRate = 0;
        item.onLoad((model) => Condenser.load(model, this));
        item.onClientLoad((model) => Condenser.clientLoad(model, this));
    }

    /**
     * Sets the percentage of the original value required to produce one of the droplet.
     *
     * @param percentage The percentage of the original value required to produce one of the droplet.
     * @returns This condenser.
     */
    setQuota(percentage: number) {
        this.quota = percentage;
        return this;
    }

    addDroplets(...droplets: Droplet[]) {
        for (const droplet of droplets) {
            this.droplets.push(droplet);
            this.totalValue = this.totalValue.add(droplet.value);
        }
        return this;
    }

    format(str: string) {
        str = str.gsub("%%val%%", this.totalValue.toString(true))[0];
        return str.gsub("%%quota%%", `${this.quota * 100}%%`)[0];
    }
}


import { getAllInstanceInfo } from "@antivivi/vrldk";
import { packet } from "@rbxts/fletchette";
import { Debris } from "@rbxts/services";
import { Server } from "shared/api/APIExpose";
import { PLACED_ITEMS_FOLDER } from "shared/constants";
import Item from "shared/item/Item";
import Generator from "shared/item/traits/generator/Generator";
import Operative from "shared/item/traits/Operative";
import perItemPacket from "shared/item/utils/perItemPacket";
import isPlacedItemUnusable from "shared/item/utils/isPlacedItemUnusable";

declare global {
    interface ItemTraits {
        Charger: Charger;
    }

    interface ItemBoost {
        /**
         * The charger that is boosting this generator model.
         */
        chargedBy?: Charger;
        /**
         * The InstanceInfo of the charger that is boosting this generator model.
         */
        chargerInfo?: InstanceInfo;
    }

    interface InstanceInfo {
        /** Whether the item model can be charged by a charger. */
        chargeable?: boolean;
    }
}

const generatorChangedPacket =
    perItemPacket(packet<(placementId: string, generatorPlacementId: string, added: boolean) => void>());
const currentGeneratorsPacket = perItemPacket(packet<(placementId: string) => string[]>());

/**
 * A charger is an item that 'charges' generators, giving them a boost in {@link Generator.passiveGain}.
 */
export default class Charger extends Operative {
    static isInRange(charger: Charger, chargerHitbox: BasePart, generatorHitbox: BasePart) {
        const radius = charger.radius + chargerHitbox.Size.X / 2;
        const hPos = generatorHitbox.Position;
        const hitboxPos = chargerHitbox.Position;
        const xDiff = hPos.X - hitboxPos.X;
        const zDiff = hPos.Z - hitboxPos.Z;
        return xDiff * xDiff + zDiff * zDiff <= radius * radius;
    }

    /**
     * Loads the charger onto the given model.
     *
     * @param model The model to load the charger onto.
     * @param charger The charger to load.
     */
    static load(model: Model, charger: Charger) {
        const chargerId = charger.item.id;
        const placementId = model.Name;

        const chargerHitbox = model.PrimaryPart;
        if (chargerHitbox === undefined) return;

        const charging = new Set<Instance>();
        const chargerInfo = getAllInstanceInfo(model);
        const chargerArea = chargerInfo.areaId;
        let isDisabled = false;

        const checkAdd = (generatorModel: Instance) => {
            // Sanity checks

            // is a model
            if (!generatorModel.IsA("Model")) return;

            if (isDisabled) return;

            // can be charged
            const generatorInfo = getAllInstanceInfo(generatorModel);
            if (generatorInfo.chargeable !== true) return;

            // in the same area
            const generatorHitbox = generatorModel.PrimaryPart;
            if (
                generatorHitbox === undefined ||
                generatorInfo.areaId !== chargerArea ||
                !this.isInRange(charger, chargerHitbox, generatorHitbox)
            )
                return;

            // not already being charged by this charger
            const boosts = generatorInfo.boosts;
            if (boosts === undefined || boosts.has(placementId)) return;

            // has valid item IDs
            const generatorItemId = generatorInfo.itemId;
            if (generatorItemId === undefined) return;

            // is whitelisted
            const chargerWhitelist = charger.whitelist;
            if (!chargerWhitelist.isEmpty() && !chargerWhitelist.has(generatorItemId)) return;

            const generatorWhitelist = generatorInfo.generator?.whitelist;
            if (generatorWhitelist !== undefined && !generatorWhitelist.isEmpty() && !generatorWhitelist.has(chargerId))
                return;

            // Check limit of two chargers per generator
            if (charger.ignoreLimit !== true) {
                let existing = 0;
                const staleBoostKeys = new Array<string>();
                for (const [boostKey, boost] of boosts) {
                    if (boost.ignoresLimitations) continue;

                    if (boost.chargedBy !== undefined && Server.Item.getPlacedItem(boostKey) === undefined) {
                        staleBoostKeys.push(boostKey);
                        continue;
                    }

                    ++existing;
                }

                if (!staleBoostKeys.isEmpty()) {
                    for (const staleKey of staleBoostKeys) {
                        Generator.removeBoost(generatorInfo, staleKey);
                    }
                }

                if (existing > 1) {
                    generatorInfo.boostRemoved!.add(() => checkAdd(generatorModel));
                    return;
                }
            }

            charging.add(generatorModel);
            const boost: ItemBoost = {
                chargerInfo,
                ignoresLimitations: charger.ignoreLimit,
                chargedBy: charger,
            };
            if (charger.boost !== undefined) {
                for (const [k, v] of pairs(charger.boost)) {
                    (boost as unknown as { [key: string]: unknown })[k] = v;
                }
            }

            Generator.addBoost(generatorInfo, placementId, boost);
            generatorChangedPacket.toAllClients(model, generatorModel.Name, true);
        };
        const checkRemove = (generatorModel: Instance) => {
            if (!generatorModel.IsA("Model")) return;
            charging.delete(generatorModel);
            const generatorInfo = getAllInstanceInfo(generatorModel);
            Generator.removeBoost(generatorInfo, placementId);
            generatorChangedPacket.toAllClients(model, generatorModel.Name, false);
        };

        const releaseAll = () => {
            for (const generatorModel of [...charging]) {
                checkRemove(generatorModel);
            }
        };

        const rescanAll = () => {
            for (const placed of PLACED_ITEMS_FOLDER.GetChildren()) {
                task.spawn(() => checkAdd(placed));
            }
        };

        isDisabled = isPlacedItemUnusable(chargerInfo);
        if (isDisabled) {
            releaseAll();
        }

        charger.item.repeat(
            model,
            () => {
                const unusable = isPlacedItemUnusable(chargerInfo);
                if (unusable) {
                    if (!isDisabled) {
                        isDisabled = true;
                        releaseAll();
                    }
                    return;
                }

                if (isDisabled) {
                    isDisabled = false;
                    rescanAll();
                }
            },
            0.5,
        );

        currentGeneratorsPacket.fromClient(model, () => {
            return [...charging].map((g) => g.Name);
        });

        for (const m of PLACED_ITEMS_FOLDER.GetChildren()) {
            task.spawn(() => checkAdd(m));
        }
        const connection1 = PLACED_ITEMS_FOLDER.ChildAdded.Connect(checkAdd);
        const connection2 = PLACED_ITEMS_FOLDER.ChildRemoved.Connect(checkRemove);
        model.Destroying.Connect(() => {
            connection1.Disconnect();
            connection2.Disconnect();
            // Explicitly notify clients to remove VFX for all charged generators
            for (const m of charging) {
                // Remove boost and send packet to clients
                charging.delete(m);
                const instanceInfo = getAllInstanceInfo(m);
                Generator.removeBoost(instanceInfo, placementId);
                generatorChangedPacket.toAllClients(model, m.Name, false);
            }
        });
    }

    static clientLoad(model: Model) {
        const connection = model.FindFirstChild("ConnectionVFX");
        if (connection !== undefined) {
            connection.Parent = script;
        }
        const chargerMarker = model.FindFirstChild("Marker");
        const connectionName = "ConnectionVFX" + model.Name;

        const chargerHitbox = model.PrimaryPart;
        if (chargerHitbox === undefined) return;

        const checkAdd = (generatorModel: Instance) => {
            if (!generatorModel.IsA("Model")) return;
            const generatorHitbox = generatorModel.PrimaryPart;
            if (generatorHitbox === undefined) return;

            if (connection !== undefined) {
                if (generatorModel.FindFirstChild(connectionName) === undefined) {
                    const c = connection.Clone();
                    const start = c.WaitForChild("Start") as Attachment;
                    const e = c.WaitForChild("End") as Attachment;
                    start.Parent = chargerMarker ?? chargerHitbox;
                    e.Parent = generatorModel.FindFirstChild("Marker") ?? generatorHitbox;
                    c.Name = connectionName;
                    c.Parent = generatorModel;
                    start.Position = new Vector3();
                    e.Position = new Vector3();
                    c.Destroying.Once(() => {
                        start.Destroy();
                        e.Destroy();
                    });
                }
            }
        };

        const checkRemove = (generatorModel: Instance) => {
            const connectionVFX = generatorModel.FindFirstChild("ConnectionVFX" + connectionName);
            if (connectionVFX !== undefined) {
                connectionVFX.ClearAllChildren();
                Debris.AddItem(connectionVFX, 0.5);
            }
        };

        generatorChangedPacket.fromServer(model, (generatorPlacementId, added) => {
            const generatorModel = PLACED_ITEMS_FOLDER.WaitForChild(generatorPlacementId, 1);
            if (generatorModel === undefined) return;
            if (added) {
                checkAdd(generatorModel);
            } else {
                checkRemove(generatorModel);
            }
        });

        const current = currentGeneratorsPacket.toServer(model);
        if (current !== undefined) {
            for (const generatorPlacementId of current) {
                const generatorModel = PLACED_ITEMS_FOLDER.FindFirstChild(generatorPlacementId);
                if (generatorModel === undefined) continue;
                checkAdd(generatorModel);
            }
        }
    }

    readonly whitelist = new Set<string>();

    boost?: Partial<ItemBoost>;

    /**
     * Whether the charger ignores the limit of two chargers per generator.
     */
    ignoreLimit = false;

    /**
     * The maximum distance from the generator that the charger can charge.
     */
    radius = 0;

    /**
     * Constructs a new charger.
     *
     * @param item The item to which this charger belongs.
     */
    constructor(item: Item) {
        super(item);
        item.onLoad((model) => Charger.load(model, this));
        item.onClientLoad((model) => Charger.clientLoad(model));
    }

    /**
     * Adds an item ID that the charger is allowed to charge.
     * @param itemId The item ID to add to the whitelist.
     * @returns This charger.
     */
    addToWhitelist(itemId: string) {
        this.whitelist.add(itemId);
        return this;
    }

    /**
     * Sets whether the charger ignores the limit of two chargers per generator.
     *
     * @param ignoreLimit Whether the charger ignores the limit of two chargers per generator.
     * @returns This charger.
     */
    ignoresLimit(ignoreLimit: boolean) {
        this.ignoreLimit = ignoreLimit;
        return this;
    }

    /**
     * Sets the maximum distance from the generator that the charger can charge.
     *
     * @param radius The maximum distance from the generator that the charger can charge.
     * @returns This charger.
     */
    setRadius(radius: number) {
        this.radius = radius;
        return this;
    }

    setBoost(boost: Partial<ItemBoost>) {
        this.boost = boost;
        return this;
    }

    format(str: string) {
        str = super.format(str);
        str = str.gsub("%%radius%%", this.radius)[0];
        return str;
    }
}


import { getAllInstanceInfo } from "@antivivi/vrldk";
import { packet } from "@rbxts/fletchette";
import { Workspace } from "@rbxts/services";
import Item from "shared/item/Item";
import ItemTrait from "shared/item/traits/ItemTrait";
import Upgrader from "shared/item/traits/upgrader/Upgrader";
import perItemPacket from "shared/item/utils/perItemPacket";

declare global {
    interface ItemBoost {
        cooldownMul?: number;
    }
}

export default abstract class BaseDropletSlayer extends ItemTrait {
    static readonly activatePacket = perItemPacket(packet<(placementId: string) => void>());

    /**
     * Loads the droplet slayer onto the given model.
     * @param model The model to load the droplet slayer onto.
     * @param slayer The droplet slayer to load.
     * @returns An object containing the repeat reference.
     */
    static baseLoad(model: Model, slayer: BaseDropletSlayer, laserIdFactory?: () => string) {
        const item = slayer.item;
        const upgrader = item.trait(Upgrader);
        const modelInfo = getAllInstanceInfo(model);
        modelInfo.boosts ??= new Map();
        const boosts = modelInfo.boosts;

        const laser = model.WaitForChild("PulsatingLaser") as BasePart;
        const laserInfo = getAllInstanceInfo(laser);
        laserInfo.maintained = false;

        const overlapParams = new OverlapParams();
        overlapParams.CollisionGroup = "DropletInquirer";
        const laserCFrame = laser.CFrame;
        const laserSize = laser.Size;

        const ref = item.repeat(
            model,
            () => {
                laserInfo.maintained = true;
                for (const droplet of Workspace.GetPartBoundsInBox(laserCFrame, laserSize, overlapParams)) {
                    Upgrader.upgrade({
                        model,
                        modelInfo,
                        upgrader,
                        droplet,
                        dropletInfo: getAllInstanceInfo(droplet),
                        laserInfo,
                        laserId: laserIdFactory?.(),
                    });
                }
                laserInfo.maintained = false;
                this.activatePacket.toAllClients(model);

                let newCooldown = slayer.cooldown;
                for (const [, boost] of boosts) {
                    if (boost.cooldownMul !== undefined) {
                        newCooldown *= boost.cooldownMul;
                    }
                }
                return newCooldown;
            },
            slayer.cooldown,
        );

        return { ref };
    }

    cooldown = 4;

    constructor(item: Item) {
        super(item);
    }

    setCooldown(cd: number) {
        this.cooldown = cd;
        return this;
    }
}


import { weldModel } from "@antivivi/vrldk";
import { packet } from "@rbxts/fletchette";
import { TweenService } from "@rbxts/services";
import { Server } from "shared/api/APIExpose";
import Item from "shared/item/Item";
import ItemTrait from "shared/item/traits/ItemTrait";
import Upgrader from "shared/item/traits/upgrader/Upgrader";
import perItemPacket from "shared/item/utils/perItemPacket";
import CustomProximityPrompt from "shared/world/CustomProximityPrompt";

declare global {
    interface PlacedItem {
        direction?: boolean;
    }
}

const tweenInfo = new TweenInfo(0.1, Enum.EasingStyle.Linear);

export default class LaserFan extends ItemTrait {
    static readonly laserFanChanged = perItemPacket(
        packet<(placementId: string, cframe: CFrame) => void>({ isUnreliable: true }),
        { slowStart: true },
    );

    static load(model: Model, laserFan: LaserFan) {
        const motor = model.WaitForChild("Motor") as Model;
        const primaryPart = weldModel(motor);
        const original = primaryPart.CFrame;
        let v = 0;
        const ItemService = Server.Item;
        let direction = ItemService.getPlacedItem(model.Name)?.direction === true;
        laserFan.item.repeat(
            model,
            () => {
                v += (direction ? 1 : -1) * laserFan.speed;
                const newCFrame = original.mul(CFrame.Angles(math.rad(v), 0, 0));
                LaserFan.laserFanChanged.toAllClients(model, newCFrame);
                TweenService.Create(primaryPart, tweenInfo, {
                    CFrame: newCFrame,
                }).Play();
            },
            0.1,
        );

        const proximityPrompt = primaryPart.FindFirstChild("ProximityPrompt") as ProximityPrompt | undefined;
        if (proximityPrompt !== undefined) {
            const cleanup = CustomProximityPrompt.onTrigger(proximityPrompt, () => {
                direction = !direction;
                const pi = ItemService.getPlacedItem(model.Name);
                if (pi === undefined) {
                    return;
                }
                pi.direction = direction;
            });
            model.Destroying.Once(cleanup);
        }
    }

    static clientLoad(model: Model, laserFan: LaserFan) {
        const motor = model.WaitForChild("Motor") as Model;
        const primaryPart = weldModel(motor);
        LaserFan.laserFanChanged.fromServer(model, (cframe) => {
            TweenService.Create(primaryPart, tweenInfo, { CFrame: cframe }).Play();
        });
    }

    speed = 3;

    constructor(item: Item) {
        super(item);
        item.onLoad((model) => LaserFan.load(model, this));
        item.onClientLoad((model) => LaserFan.clientLoad(model, this));
    }

    setSpeed(speed: number) {
        this.speed = speed;
        return this;
    }
}


import { getAllInstanceInfo, loadAnimation } from "@antivivi/vrldk";
import { TweenService } from "@rbxts/services";
import { playSound } from "shared/asset/GameAssets";
import Item from "shared/item/Item";
import Generator from "shared/item/traits/generator/Generator";
import BaseDropletSlayer from "shared/item/traits/other/BaseDropletSlayer";

export default class NoobDropletSlayer extends BaseDropletSlayer {
    static load(model: Model, slayer: NoobDropletSlayer) {
        const modelInfo = getAllInstanceInfo(model);

        let i = 0;
        super.baseLoad(model, slayer, () => {
            let hasRadioNoob = false;
            if (modelInfo.boosts !== undefined) {
                for (const [, boost] of modelInfo.boosts) {
                    if (boost.chargedBy?.item.id === "RadioNoob") {
                        hasRadioNoob = true;
                        break;
                    }
                }
            }
            if (hasRadioNoob) {
                i++;
                i %= 2;
            }
            return tostring(i);
        });
    }

    static clientLoad(model: Model) {
        const noob = model.WaitForChild("Noob") as Model;
        const animationController = noob.WaitForChild("AnimationController") as AnimationController;
        const animTrack = loadAnimation(animationController, 16920778613);

        const slash = model.WaitForChild("Slash") as BasePart;
        slash.Transparency = 1;
        const slashOriginalCFrame = slash.CFrame;

        BaseDropletSlayer.activatePacket.fromServer(model, () => {
            if (slash === undefined) return;
            slash.Transparency = 0.011;
            slash.CFrame = slashOriginalCFrame;
            TweenService.Create(slash, new TweenInfo(0.3), {
                CFrame: slashOriginalCFrame.mul(CFrame.Angles(0, math.rad(180), 0)),
                Transparency: 1,
            }).Play();
            animTrack?.Play();
            playSound("SwordSwing.mp3", noob.PrimaryPart);
        });
    }

    constructor(item: Item) {
        super(item);
        item.trait(Generator).addToWhitelist("RadioNoob");

        item.onLoad((model) => NoobDropletSlayer.load(model, this));
        item.onClientLoad((model) => NoobDropletSlayer.clientLoad(model));
    }
}


import { formatGradient, getAllInstanceInfo } from "@antivivi/vrldk";
import { Workspace } from "@rbxts/services";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import { ASSETS } from "shared/asset/GameAssets";
import Item from "shared/item/Item";
import StatusEffect from "shared/item/traits/status/StatusEffect";

declare global {
    interface ItemTraits {
        Ablaze: Ablaze;
    }

    interface StatusEffectAssets {
        Fire: ParticleEmitter;
    }
}

export default class Ablaze extends StatusEffect {
    static BOOST = CurrencyBundle.ones().mulConstant(2);

    constructor(item: Item) {
        super(item);
        item.onLoad((model) => Ablaze.load(model, this));
    }

    decorate(dropletModel: BasePart) {
        const dropletInfo = getAllInstanceInfo(dropletModel);
        this.item.repeat(
            dropletModel,
            (dt) => {
                if (dropletInfo.incinerated === true) return;
                dropletInfo.health! -= 10 * dt;
            },
            0.5,
        );
        dropletInfo.upgrades!.set("Ablaze", {
            model: Workspace,
            boost: {
                mul: Ablaze.BOOST,
            },
        });
        ASSETS.StatusEffect.Fire.Clone().Parent = dropletModel;

        return {};
    }

    format(str: string): string {
        return str.gsub("%%ablaze%%", formatGradient("Ablaze", new Color3(0.96, 0.41, 0.16), new Color3(1, 0, 0)))[0];
    }
}


import { formatGradient, getAllInstanceInfo } from "@antivivi/vrldk";
import { Workspace } from "@rbxts/services";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Droplet from "shared/item/Droplet";
import Item from "shared/item/Item";
import StatusEffect from "shared/item/traits/status/StatusEffect";

declare global {
    interface ItemTraits {
        Massless: Massless;
        Grounder: Grounder;
    }

    interface StatusEffectAssets {
        Fire: ParticleEmitter;
    }
}

const format = (str: string) => {
    return str.gsub(
        "%%massless%%",
        formatGradient("Massless", new Color3(0.16, 0.96, 0.69), new Color3(0, 1, 0.53)),
    )[0];
};

export default class Massless extends StatusEffect {
    static BOOST = CurrencyBundle.ones().mulConstant(1.2);

    constructor(item: Item) {
        super(item);
        item.onLoad((model) => Massless.load(model, this));
    }

    decorate(dropletModel: BasePart) {
        const dropletInfo = getAllInstanceInfo(dropletModel);
        dropletModel.CustomPhysicalProperties = new PhysicalProperties(0.0001, 2, 0.15);
        dropletInfo.upgrades!.set("Massless", {
            model: Workspace,
            boost: {
                mul: Massless.BOOST,
            },
        });

        return {};
    }

    format(str: string) {
        return format(str);
    }
}

export class Grounder extends StatusEffect {
    constructor(item: Item) {
        super(item);
        item.onLoad((model) => Grounder.load(model, this));
    }

    decorate(dropletModel: BasePart) {
        const dropletInfo = getAllInstanceInfo(dropletModel);
        dropletModel.CustomPhysicalProperties = Droplet.PHYSICAL_PROPERTIES;
        dropletInfo.upgrades!.delete("Massless");
        return {};
    }

    format(str: string) {
        return format(str);
    }
}


import { OnoeNum } from "@rbxts/serikanum";
import { simpleInterval } from "@antivivi/vrldk";
import { packet } from "@rbxts/fletchette";
import { Debris, Players } from "@rbxts/services";
import { Server } from "shared/api/APIExpose";
import { getSound } from "shared/asset/GameAssets";
import { IS_EDIT, IS_SERVER } from "shared/Context";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Formula from "shared/currency/Formula";
import eat from "shared/hamster/eat";
import Item from "shared/item/Item";
import ItemTrait from "shared/item/traits/ItemTrait";
import UpgradeBoard from "shared/item/traits/UpgradeBoard";
import Upgrader from "shared/item/traits/upgrader/Upgrader";
import NamedUpgrade from "shared/namedupgrade/NamedUpgrade";
import Packets from "shared/Packets";

const obbyCompletedPacket = packet<(itemId: string, placementId: string) => void>();

export default class ObbyUpgrader extends ItemTrait {
    static init(obbyUpgrader: ObbyUpgrader) {
        const item = obbyUpgrader.item;
        const upgrader = item.trait(Upgrader);
        item.repeat(
            undefined,
            () => {
                const mul = new CurrencyBundle();
                for (const [upgrade, { currency, formula }] of obbyUpgrader.boosts) {
                    const amount = Server.NamedUpgrade.getUpgradeAmount(upgrade.id);
                    mul.set(currency, formula.evaluate(new OnoeNum(amount)));
                }
                upgrader.setMul(mul);
            },
            1,
        );
    }

    static onClientLoad(model: Model, obbyUpgrader: ObbyUpgrader) {
        const obbyPointsGuiPart = model.WaitForChild("ObbyPointsGuiPart") as BasePart;
        const label = obbyPointsGuiPart.FindFirstChildOfClass("SurfaceGui")?.FindFirstChild("TextLabel") as
            | TextLabel
            | undefined;
        if (label === undefined) throw "ObbyPointsGuiPart does not have a TextLabel in its SurfaceGui";
        const cleanup = simpleInterval(() => {
            label.Text = `OBBY POINTS: ${new OnoeNum(Packets.balance.get().get("Obby Points") ?? 0).toString()}`;
        }, 1);
        model.Destroying.Once(cleanup);

        for (const part of model.GetChildren()) {
            if (!part.IsA("BasePart")) continue;
            switch (part.Name) {
                case "ObbyZone":
                case "ReturnPart":
                    part.Transparency = 1;
                    part.CanTouch = true;
                    part.CanCollide = false;
                    break;
                case "WinPart":
                    part.CanTouch = true;
                    part.CanCollide = false;
                    break;
                default:
                    break;
            }
        }

        const winPart = model.WaitForChild("WinPart") as BasePart;
        winPart.Touched.Connect((hit) => {
            const parent = hit.Parent;
            if (parent === undefined || !parent.IsA("Model")) return;
            if (Players.GetPlayerFromCharacter(parent) !== Players.LocalPlayer) return;
            obbyCompletedPacket.toServer(obbyUpgrader.item.id, model.Name);
        });
    }

    readonly boosts = new Map<
        NamedUpgrade,
        {
            currency: Currency;
            formula: Formula;
        }
    >();

    reward = new OnoeNum(0);

    constructor(item: Item) {
        super(item);
        item.trait(Upgrader);
        item.onInit(() => ObbyUpgrader.init(this));
        item.onClientLoad((model) => ObbyUpgrader.onClientLoad(model, this));
    }

    setBoost(upgrade: NamedUpgrade, currency: Currency, formula: Formula) {
        this.trait(UpgradeBoard).addUpgrade(upgrade);
        this.boosts.set(upgrade, { currency, formula });
        return this;
    }

    setReward(reward: OnoeNum | number) {
        this.reward = new OnoeNum(reward);
        return this;
    }

    static {
        if (IS_SERVER || IS_EDIT) {
            let debounce = false;
            const connection = obbyCompletedPacket.fromClient((player, itemId, placementId) => {
                if (debounce) return;
                const item = Server.Items.itemsPerId.get(itemId);
                if (item === undefined) return;
                const obbyUpgrader = item.trait(ObbyUpgrader);
                if (obbyUpgrader === undefined) return;

                const reward = obbyUpgrader.reward;
                Server.Currency.increment("Obby Points", reward);
                const color = item.difficulty.color ?? new Color3(1, 1, 1);
                const r = color.R * 255;
                const g = color.G * 255;
                const b = color.B * 255;
                Server.ChatHook.sendServerMessage(
                    `${player.Name} has completed the ${item.name} and earned ${reward.toString()} Obby Points!`,
                    `tag:hidden;color:${r},${g},${b}`,
                );
                const sound = getSound("ObbyPointGet.mp3");
                sound.Play();
                sound.Parent = player.Character;
                Debris.AddItem(sound, 5);
                const model = Server.Item.modelPerPlacementId.get(placementId);
                if (model === undefined) return;
                const returnPart = model.WaitForChild("ReturnPart") as BasePart;
                player.Character?.PivotTo(returnPart.CFrame);
                print(item.name, "completed");
                debounce = true;
                task.delay(1, () => {
                    debounce = false;
                });
            });
            eat(connection, "Disconnect");
        }
    }
}


import { getAllInstanceInfo } from "@antivivi/vrldk";
import { Workspace } from "@rbxts/services";
import { Server } from "shared/api/APIExpose";
import { PLACED_ITEMS_FOLDER } from "shared/constants";
import Item from "shared/item/Item";

const OVERLAP_PARAMS = new OverlapParams();
OVERLAP_PARAMS.CollisionGroup = "ItemHitbox";
OVERLAP_PARAMS.FilterType = Enum.RaycastFilterType.Include;
OVERLAP_PARAMS.FilterDescendantsInstances = [PLACED_ITEMS_FOLDER];

export default function getPlacedItemsInBounds(bounds: BasePart) {
    const array = Workspace.GetPartBoundsInBox(bounds.CFrame, bounds.Size, OVERLAP_PARAMS);
    const items = new Map<Model, Item>();
    const itemsPerId = Server.Items.itemsPerId;
    for (const touching of array) {
        const target = touching.Parent as Model;
        const targetInfo = getAllInstanceInfo(target);

        const itemId = targetInfo.itemId;
        if (itemId === undefined) continue;

        const item = itemsPerId.get(itemId);
        if (item === undefined) throw `Placed item model has invalid itemId: ${itemId}`;

        items.set(target, item);
    }
    return items;
}


import { RequestPacket, SignalPacket } from "@rbxts/fletchette";
import { IS_EDIT, IS_SERVER } from "shared/Context";
import eat from "shared/hamster/eat";

type SignalOrRequestPacket<T extends Callback = Callback> =
    ReturnType<T> extends void ? SignalPacket<T> : RequestPacket<Parameters<T>, ReturnType<T>, T>;

function isRequestPacket(packet: SignalOrRequestPacket): packet is RequestPacket<unknown[], unknown, Callback> {
    return packet.className === "RequestPacket";
}

let startTime = os.clock();

/**
 * Creates a packet that can send and receive messages tied to specific item placements.
 * This allows for item-specific communication between the server and clients.
 * @param packet The packet to wrap for per-item communication.
 * @returns The wrapped packet with per-item methods.
 */
export default function perItemPacket<Args extends unknown[], Return>(
    packet: SignalOrRequestPacket<(placementId: string, ...args: Args) => Return | undefined>,
    options: { slowStart?: boolean } = {},
) {
    const isRequest = isRequestPacket(packet);
    const clientHandlers = new Map<string, (...args: Args) => Return | undefined>();
    const serverHandlers = new Map<string, (player: Player, ...args: Args) => Return | undefined>();

    if (IS_SERVER || IS_EDIT) {
        const connection = packet.fromClient((player: Player, placementId: string, ...args: Args) => {
            const handler = serverHandlers.get(placementId);
            if (handler !== undefined) {
                return handler(player, ...args);
            }
        });
        if (connection !== undefined) {
            eat(connection, "Disconnect");
        }
    }

    if (!IS_SERVER || IS_EDIT) {
        const connection = packet.fromServer((placementId: string, ...args: Args) => {
            const handler = clientHandlers.get(placementId);
            if (handler !== undefined) {
                return handler(...args);
            }
        });
        if (connection !== undefined) {
            eat(connection, "Disconnect");
        }
    }

    return {
        toAllClients: (model: Model, ...args: Args) => {
            if (isRequest) {
                throw "Cannot use toAllClients with a RequestPacket. Use toClient instead.";
            }
            if (options.slowStart && os.clock() - startTime < 10) {
                return;
            }

            packet.toAllClients(model.Name, ...args);
        },
        toClient: (model: Model, player: Player, ...args: Args): Return | undefined => {
            if (options.slowStart && os.clock() - startTime < 10) {
                return;
            }

            return packet.toClient(player, model.Name, ...args) as Return | undefined;
        },
        toServer: (model: Model, ...args: Args): Return | undefined => {
            return packet.toServer(model.Name, ...args) as Return | undefined;
        },
        fromServer: (model: Model, handler: (...args: Args) => Return) => {
            clientHandlers.set(model.Name, handler);
            model.Destroying.Once(() => {
                clientHandlers.delete(model.Name);
            });
        },
        fromClient: (model: Model, handler: (player: Player, ...args: Args) => Return) => {
            serverHandlers.set(model.Name, handler);
            model.Destroying.Once(() => {
                serverHandlers.delete(model.Name);
            });
        },
        packet,
    };
}

/**
 * Creates a property-like interface for a per-item packet, allowing setting and observing values tied to specific item placements.
 * @param signal The signal packet used to send updates.
 * @param request The request packet used to fetch the current value.
 * @returns An object with `set` and `observe` methods for managing the property.
 */
export function perItemProperty<T>(
    signal: SignalPacket<(placementId: string, value: T) => void>,
    request: SignalOrRequestPacket<(placementId: string) => T | undefined>,
) {
    const perItemSignal = perItemPacket(signal);
    const values = new Map<string, T>();

    // Hijack the request packet
    if (IS_SERVER || IS_EDIT) {
        request.fromClient((_, placementId) => {
            return values.get(placementId);
        });
    }

    return {
        set: (model: Model, value: T) => {
            const placementId = model.Name;
            values.set(placementId, value);
            perItemSignal.toAllClients(model, value);
            model.Destroying.Once(() => {
                values.delete(placementId);
            });
        },
        observe: (model: Model, handler: (value: T) => void) => {
            perItemSignal.fromServer(model, handler);
            const initial = request.toServer(model.Name);
            if (initial !== undefined) {
                handler(initial);
            }
        },
    };
}


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import Shop from "shared/item/traits/Shop";
import SlamoStore from "shared/items/0/millisecondless/SlamoStore";

export = new Item(script.Name)
    .setName("Class 0 Shop")
    .setDescription("Buy your favorite Millisecondless to Spontaneous items here!")
    .setDifficulty(Difficulty.Millisecondless)
    .addPlaceableArea("BarrenIslands", "SlamoVillage")
    .soldAt(SlamoStore)
    .setPrice(new CurrencyBundle().set("Skill", 0.1), 1, 5)
    .persists()

    .trait(Shop)
    .exit();


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

export = new Item(script.Name)
    .setName("Spatial Dropper")
    .setDescription(
        "A dropper placeable in Slamo Village producing %val% droplets every 2 seconds, saving between skillifications!",
    )
    .setDifficulty(Difficulty.Astronomical)
    .setPrice(new CurrencyBundle().set("Skill", 2), 1)
    .setPrice(new CurrencyBundle().set("Skill", 3), 1)
    .addPlaceableArea("SlamoVillage")
    .soldAt(Class0Shop)

    .trait(Dropper)
    .setDroplet(Droplet.SpatialDroplet)
    .setDropRate(0.5)

    .exit();


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Furnace from "shared/item/traits/Furnace";
import Item from "shared/item/Item";
import Class0Shop from "shared/items/0/Class0Shop";

export = new Item(script.Name)
    .setName("Spatial Furnace")
    .setDescription("A basic furnace placeable in Slamo Village.")
    .setDifficulty(Difficulty.Astronomical)
    .setPrice(new CurrencyBundle().set("Funds", 3e24), 1)
    .addPlaceableArea("SlamoVillage")
    .soldAt(Class0Shop)

    .trait(Furnace)
    .setMul(new CurrencyBundle().set("Funds", 1))

    .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 MagicalCraftingTable from "shared/items/0/millisecondless/MagicalCraftingTable";

export = new Item(script.Name)
    .setName("Automated Stairs Conveyor")
    .setDescription("Moves very small droplets up the stairs to the skyline, all in a 2x2 space.")
    .setDifficulty(Difficulty.Automatic)
    .setPrice(new CurrencyBundle().set("Skill", 700000).set("Dark Matter", 50e30), 1)
    .setCreator("superGirlygamer8o")
    .addPlaceableArea("BarrenIslands", "SlamoVillage")
    .soldAt(MagicalCraftingTable)
    .persists()

    .trait(Conveyor)
    .setSpeed(10)

    .exit();


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import Damager from "shared/item/traits/upgrader/Damager";
import Upgrader from "shared/item/traits/upgrader/Upgrader";
import Class0Shop from "shared/items/0/Class0Shop";

export = new Item(script.Name)
    .setName("Automatic Arbition")
    .setDescription("Does ??? damage to droplets for a %mul% boost. Automatic arbitration was never a good idea.")
    .setDifficulty(Difficulty.Automatic)
    .setPrice(new CurrencyBundle().set("Dark Matter", 2e27).set("Skill", 4400000), 1)
    .setCreator("GIDS214")

    .addPlaceableArea("BarrenIslands")
    .soldAt(Class0Shop)
    .persists()

    .trait(Upgrader)
    .setMul(new CurrencyBundle().set("Skill", 2))

    .trait(Damager)
    .setDamage(50)
    .setVariance(0.5)

    .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 Class0Shop from "shared/items/0/Class0Shop";

export = new Item(script.Name)
    .setName("Automatic Railgun")
    .setDescription("Boom! Launches droplets to the skyline with a %mul% bang.")
    .setDifficulty(Difficulty.Automatic)
    .setPrice(new CurrencyBundle().set("Funds", 250e39).set("Bitcoin", 5e9), 1)
    .addPlaceableArea("BarrenIslands", "SlamoVillage", "SkyPavilion")
    .soldAt(Class0Shop)
    .setCreator("simple13579")

    .trait(Conveyor)
    .setSpeed(20)

    .trait(Upgrader)
    .setSky(true)
    .setMul(new CurrencyBundle().set("Bitcoin", 5))

    .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 Stone from "shared/items/0/millisecondless/Stone";
import EnchantedGrass from "shared/items/negative/tlg/EnchantedGrass";
import Jade from "shared/items/excavation/Jade";
import Wool from "shared/items/negative/a/Wool";
import GoldDiggersHaven from "shared/items/negative/skip/GoldDiggersHaven";

export = new Item(script.Name)
    .setName("Educational Upgrader")
    .setDescription(`Give your droplets an education, allowing them to gain %add%.`)
    .setDifficulty(Difficulty.Joyful)
    .setPrice(new CurrencyBundle().set("Skill", 1), 1)
    .setRequiredItemAmount(Wool, 222)
    .setRequiredItemAmount(GoldDiggersHaven, 2)
    .setRequiredItemAmount(Stone, 600)
    .setRequiredItemAmount(EnchantedGrass, 120)
    .setRequiredItemAmount(Jade, 1)
    .setCreator("emoronq2k")
    .addPlaceableArea("BarrenIslands", "SlamoVillage")
    .persists()

    .trait(Upgrader)
    .setAdd(new CurrencyBundle().set("Skill", 1))

    .trait(Conveyor)
    .setSpeed(1)

    .exit();


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Droplet from "shared/item/Droplet";
import Item from "shared/item/Item";
import Conveyor from "shared/item/traits/conveyor/Conveyor";
import Upgrader from "shared/item/traits/upgrader/Upgrader";
import MagicalCraftingTable from "shared/items/0/millisecondless/MagicalCraftingTable";
import CorruptedGrass from "shared/items/0/happylike/CorruptedGrass";
import GrassConveyor from "shared/items/negative/friendliness/GrassConveyor";

export = new Item("GrassDropletInfuser")
    .setName("Grass Droplet Infuser")
    .setDescription("Infuses grass droplets with %mul%. Only upgrades grass droplets.")
    .setDifficulty(Difficulty.Automatic)
    .setPrice(new CurrencyBundle().set("Funds", 18e36), 1)
    .setRequiredItemAmount(CorruptedGrass, 2)
    .setRequiredItemAmount(GrassConveyor, 3)
    .setCreator("sanjay2133")
    .addPlaceableArea("BarrenIslands")
    .soldAt(MagicalCraftingTable)
    .persists()

    .trait(Upgrader)
    .setRequirement(
        (dropletInfo) =>
            dropletInfo.dropletId === Droplet.GrassDroplet.id ||
            dropletInfo.dropletId === Droplet.NativeGrassDroplet.id ||
            dropletInfo.dropletId === Droplet.MassiveGrassDroplet.id,
    )
    .setMul(new CurrencyBundle().set("Funds", 2000))

    .trait(Conveyor)
    .setSpeed(4)

    .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 Portal from "shared/item/traits/upgrader/Portal";
import Class0Shop from "shared/items/0/Class0Shop";

export = new Item(script.Name)
    .setName("Illusionary Portal")
    .setDescription(
        "Pass droplets through the black void, and it comes out through the radiant end. This is the power of teleportation.",
    )
    .setDifficulty(Difficulty.Automatic)
    .setPrice(new CurrencyBundle().set("Funds", 220e39), 1)
    .setPrice(new CurrencyBundle().set("Funds", 220e36), 2)
    .addPlaceableArea("BarrenIslands", "SlamoVillage", "SkyPavilion")
    .soldAt(Class0Shop)
    .setCreator("simple13579")

    .trait(Portal)
    .trait(Conveyor)
    .setSpeed(5)

    .exit();


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import Bin from "shared/item/traits/Bin";
import Class0Shop from "shared/items/0/Class0Shop";

export = new Item(script.Name)
    .setName("Money Bin")
    .setDescription("A bin that collects money while you are offline.")
    .setDifficulty(Difficulty.Automatic)
    .setPrice(new CurrencyBundle().set("Funds", 22e42), 1)
    .addPlaceableArea("BarrenIslands", "SlamoVillage", "SkyPavilion")
    .soldAt(Class0Shop)
    .setCreator("simple13579")

    .trait(Bin)
    .setMul(new CurrencyBundle().set("Funds", 0.1))

    .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 NormalReactor from "shared/items/0/millisecondless/NormalReactor";
import Class0Shop from "shared/items/0/Class0Shop";

export = new Item(script.Name)
    .setName("Reinforced Reactor")
    .setDescription(
        "This reactor is extremely cheap, providing a %mul% boost to droplets in both of its entrances. However... be completely sure you really want to use this.",
    )
    .setDifficulty(Difficulty.Automatic)
    .setPrice(
        new CurrencyBundle()
            .set("Funds", 100e3)
            .set("Power", 100e3)
            .set("Bitcoin", 100e3)
            .set("Skill", 100e3)
            .set("Purifier Clicks", 10)
            .set("Dark Matter", 10),
        1,
    )
    .setRequiredItemAmount(NormalReactor, 1)
    .addPlaceableArea("BarrenIslands")
    .soldAt(Class0Shop)
    .setCreator("sanjay2133")

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

    .trait(Conveyor)
    .setSpeed(10)

    .exit();


import Difficulty from "@rbxts/ejt";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Conveyor from "shared/item/traits/conveyor/Conveyor";
import Item from "shared/item/Item";
import Upgrader from "shared/item/traits/upgrader/Upgrader";
import Class0Shop from "shared/items/0/Class0Shop";

export = new Item(script.Name)
    .setName("Skill Booster")
    .setDescription(
        "A convoluted piece of machinery that has one purpose: to boost droplets by %mul%. We must continue pushing Skill.",
    )
    .setDifficulty(Difficulty.Automatic)
    .setPrice(new CurrencyBundle().set("Funds", 1.5e42).set("Power", 100e24).set("Skill", 12000000), 1)
    .addPlaceableArea("BarrenIslands", "SlamoVillage")
    .soldAt(Class0Shop)
    .setCreator("CoPKaDT")

    .trait(Upgrader)
    .setMul(new CurrencyBundle().set("Skill", 4))

    .trait(Conveyor)
    .setSpeed(5)

    .exit();


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 MagicalCraftingTable from "shared/items/0/millisecondless/MagicalCraftingTable";

export = new Item(script.Name)
    .setName("Spherical Dropper")
    .setDescription("Produces %val% spheres that start at %health%. You may want to heal them up...")
    .setDifficulty(Difficulty.Automatic)
    .setPrice(new CurrencyBundle().set("Skill", 8000000), 1)
    .setCreator("GIDS214")
    .addPlaceableArea("BarrenIslands", "SlamoVillage")
    .soldAt(MagicalCraftingTable)
    .persists()

    .trait(Dropper)
    .setDroplet(Droplet.SphericalDroplet)
    .setDropRate(0.5)

    .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 Damager from "shared/item/traits/upgrader/Damager";
import Upgrader from "shared/item/traits/upgrader/Upgrader";
import MagicalCraftingTable from "shared/items/0/millisecondless/MagicalCraftingTable";

export = new Item(script.Name)
    .setName("Spherical Healer")
    .setDescription("Heals droplets for %hp_add%.")
    .setDifficulty(Difficulty.Automatic)
    .setPrice(new CurrencyBundle().set("Skill", 8000000), 1)
    .setCreator("GIDS214")
    .addPlaceableArea("BarrenIslands", "SlamoVillage")
    .soldAt(MagicalCraftingTable)
    .persists()

    .trait(Upgrader)

    .trait(Damager)
    .setDamage(-20)

    .trait(Conveyor)
    .setSpeed(5)

    .exit();


import Difficulty from "@rbxts/ejt";
import Furnace from "shared/item/traits/Furnace";
import Item from "shared/item/Item";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Formula from "shared/currency/Formula";
import { Server } from "shared/api/APIExpose";
import RoboticCauldron from "shared/items/0/walkthrough/RoboticCauldron";
import Class0Shop from "shared/items/0/Class0Shop";

const mul = new CurrencyBundle().set("Power", 1);

export = new Item(script.Name)
    .setName("Automatic Joyful Furnace")
    .setDescription(
        `Processes Power at a higher value, boost increasing with Power. Could be better than the ${RoboticCauldron.name} depending on your use case.`,
    )
    .setDifficulty(Difficulty.AutomaticJoyful)
    .setPrice(new CurrencyBundle().set("Purifier Clicks", 1e21), 1)
    .addPlaceableArea("BarrenIslands")
    .soldAt(Class0Shop)

    .setFormula(new Formula().add(1).log(5).mul(32.8).add(5))
    .setFormulaX("power")
    .setFormulaXCap(new CurrencyBundle().set("Power", 10e24))

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

    .exit();


import Difficulty from "@rbxts/ejt";
import { simpleInterval } from "@antivivi/vrldk";
import { ReplicatedStorage } from "@rbxts/services";
import { Server } from "shared/api/APIExpose";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import Upgrader from "shared/item/traits/upgrader/Upgrader";
import Class0Shop from "shared/items/0/Class0Shop";

function updateLaserCommon(
    laser: BasePart,
    primaryPos: Vector3,
    modelName: string,
    getOtherPos: (otherId: string) => Vector3 | undefined,
    setAttribute?: (newConnector?: string) => void,
    newConnector?: string,
) {
    const otherId = ReplicatedStorage.GetAttribute(script.Name) as string | undefined;
    if (!otherId || otherId === modelName) {
        if (setAttribute) setAttribute(newConnector ?? modelName);
        laser.Size = new Vector3(1, 1, 1);
        laser.Position = primaryPos;
        return;
    }

    const posB = getOtherPos(otherId);
    if (!posB) {
        if (setAttribute) setAttribute();
        laser.Size = new Vector3(1, 1, 1);
        laser.Position = primaryPos;
        return;
    }

    const distance = primaryPos.sub(posB).Magnitude;
    if (distance <= 12) {
        laser.Size = new Vector3(4.75, 4.75, distance);
        laser.CFrame = new CFrame(primaryPos, posB).mul(new CFrame(0, 0, -distance / 2));
    } else {
        if (setAttribute) setAttribute();
        laser.Size = new Vector3(1, 1, 1);
        laser.Position = primaryPos;
    }
}

export = new Item(script.Name)
    .setName("Automatic Radiowave Connector")
    .setDescription(
        "Buy 1 get 1 marginally less expensive! Except, you'll need both connectors to earn a boost. Place the two connectors exactly 6 studs apart from each other to create a %mul% upgrader between them.",
    )
    .setDifficulty(Difficulty.AutomaticJoyful)
    .setPrice(new CurrencyBundle().set("Funds", 50e36).set("Skill", 10000), 1)
    .setPrice(new CurrencyBundle().set("Funds", 50e33).set("Skill", 10), 2)
    .addPlaceableArea("BarrenIslands", "SlamoVillage")
    .soldAt(Class0Shop)
    .persists("Skillification")

    .trait(Upgrader)
    .setMul(new CurrencyBundle().set("Skill", 2))
    .exit()

    .onLoad((model, item) => {
        const laser = model.WaitForChild("Laser") as BasePart;
        const primaryPos = model.PrimaryPart!.Position;
        let lastConnector: string | undefined;

        const setAttribute = (newConnector?: string) => {
            if (lastConnector !== newConnector) {
                ReplicatedStorage.SetAttribute(script.Name, newConnector);
                lastConnector = newConnector;
            }
        };

        item.repeat(
            model,
            () =>
                updateLaserCommon(
                    laser,
                    primaryPos,
                    model.Name,
                    (otherId) => {
                        const other = Server.Item.getPlacedItem(otherId);
                        return other ? new Vector3(other.posX, other.posY, other.posZ) : undefined;
                    },
                    setAttribute,
                    model.Name,
                ),
            1,
        );

        // Clear attribute when connector is destroyed/unplaced
        model.Destroying.Connect(() => {
            ReplicatedStorage.SetAttribute(script.Name, undefined);
            lastConnector = undefined;
        });
    })
    .onClientLoad((model) => {
        const laser = model.WaitForChild("Laser") as BasePart;
        const primaryPos = model.PrimaryPart!.Position;

        const getOtherPos = (otherId: string) => {
            const otherModel = model.Parent?.FindFirstChild(otherId) as Model | undefined;
            return otherModel && otherModel.PrimaryPart ? otherModel.PrimaryPart.Position : undefined;
        };

        const updateLaser = () => updateLaserCommon(laser, primaryPos, model.Name, getOtherPos);

        const cleanup = simpleInterval(updateLaser, 1);
        model.Destroying.Once(cleanup);
    });


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 Damager from "shared/item/traits/upgrader/Damager";
import Class0Shop from "shared/items/0/Class0Shop";

export = new Item(script.Name)
    .setName("Diamond Intersection Upgrader")
    .setDescription(
        "I forged a diamond just for you. Let it intersect through your veins for a %mul% boost, dealing %hp_add% to droplets.",
    )
    .setDifficulty(Difficulty.AutomaticJoyful)
    .setPrice(new CurrencyBundle().set("Funds", 5.5e36).set("Skill", 800), 1)
    .setPrice(new CurrencyBundle().set("Funds", 18e36).set("Skill", 2000), 2)
    .addPlaceableArea("BarrenIslands", "SlamoVillage")
    .soldAt(Class0Shop)
    .persists("Skillification")

    .trait(Upgrader)
    .setMul(new CurrencyBundle().set("Skill", 1.8))

    .trait(Damager)
    .setDamage(5)

    .trait(Conveyor)
    .setSpeed(4)

    .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 Damager from "shared/item/traits/upgrader/Damager";
import Upgrader from "shared/item/traits/upgrader/Upgrader";
import MagicalCraftingTable from "shared/items/0/millisecondless/MagicalCraftingTable";
import Quartz from "shared/items/excavation/Quartz";

export = new Item(script.Name)
    .setName("Elevated Coin Killer")
    .setDescription("A hastened death. %add% in exchange for %hp_add%.")
    .setDifficulty(Difficulty.AutomaticJoyful)
    .setPrice(new CurrencyBundle().set("Bitcoin", 16e6))
    .setRequiredItemAmount(Quartz, 1)
    .addPlaceableArea("BarrenIslands")
    .soldAt(MagicalCraftingTable)
    .setCreator("eeeesdfew")
    .persists()

    .trait(Upgrader)
    .setAdd(new CurrencyBundle().set("Bitcoin", 4))

    .trait(Damager)
    .setDamage(50)

    .trait(Conveyor)
    .setSpeed(10)

    .exit();