/**
 * @fileoverview Manages chest spawning, loot pools, and chest interactions.
 *
 * This service provides:
 * - Chest model spawning and initialization in all areas
 * - Loot pool management and random loot selection
 * - Handling chest opening, cooldowns, and rewards
 * - Synchronizing chest state and rewards with clients
 *
 * @since 1.0.0
 */

import { convertToMMSS, simpleInterval, weldModel } from "@antivivi/vrldk";
import { OnInit, OnStart, Service } from "@flamework/core";
import { CollectionService, TweenService, Workspace } from "@rbxts/services";
import DataService from "server/services/data/DataService";
import LevelService from "server/services/data/LevelService";
import ItemService from "server/services/item/ItemService";
import { ASSETS, playSound } from "shared/asset/GameAssets";
import eat from "shared/hamster/eat";
import Item from "shared/item/Item";
import Crystal from "shared/items/excavation/Crystal";
import ExcavationStone from "shared/items/excavation/ExcavationStone";
import Gold from "shared/items/excavation/Gold";
import CorruptedGrass from "shared/items/0/happylike/CorruptedGrass";
import EnchantedGrass from "shared/items/negative/tlg/EnchantedGrass";
import Grass from "shared/items/negative/tfd/Grass";
import StaleWood from "shared/items/negative/tfd/StaleWood";
import Iron from "shared/items/excavation/Iron";
import Quartz from "shared/items/excavation/Quartz";
import WhiteGem from "shared/items/excavation/WhiteGem";
import Items from "shared/items/Items";
import Packets from "shared/Packets";
import { AREAS } from "shared/world/Area";
import CustomProximityPrompt from "shared/world/CustomProximityPrompt";

declare global {
    type ChestModel = Model & {
        Lid: Model;
        Base: Model;
        Hitbox: BasePart & {
            CooldownGui: CooldownGui;
        };
    };

    type CooldownGui = BillboardGui & {
        CooldownLabel: TextLabel;
    };

    interface Assets {
        Chest: ChestModel;
    }
}

/**
 * Represents a possible loot drop from a chest.
 * Can be an item, a harvestable, or XP.
 */
interface Loot {
    /**
     * The item that was dropped.
     */
    itemId?: string;

    /**
     * The harvestable ID that was dropped.
     */
    harvestable?: HarvestableId;

    /**
     * The amount that was dropped.
     */
    xp?: number;
}

/**
 * LootPool manages weighted random selection of loot for chests.
 * Supports adding items, harvestables, and XP with weights.
 */
class LootPool {
    /** Static random instance for consistent pulls. */
    static readonly RANDOM = new Random(tick());

    /** Map of loot objects to their weights. */
    pool = new Map<Loot, number>();

    /**
     * Adds an item to the loot pool with a given weight.
     *
     * @param item The item to add.
     * @param weight The weight for random selection.
     */
    addItem(item: Item, weight: number) {
        this.pool.set({ itemId: item.id }, weight);
        return this;
    }

    /**
     * Adds a harvestable to the loot pool with a given weight.
     *
     * @param harvestable The harvestable ID.
     * @param weight The weight for random selection.
     */
    addHarvestable(harvestable: HarvestableId, weight: number) {
        this.pool.set({ harvestable: harvestable }, weight);
        return this;
    }

    /**
     * Adds XP to the loot pool with a given weight.
     *
     * @param xp The XP amount.
     * @param weight The weight for random selection.
     */
    addXP(xp: number, weight: number) {
        this.pool.set({ xp: xp }, weight);
        return this;
    }

    /**
     * Calculates the total weight of all loot in the pool.
     */
    getTotalWeight() {
        let totalWeight = 0;
        for (const [_piece, weight] of this.pool) {
            totalWeight += weight;
        }
        return totalWeight;
    }

    /**
     * Pulls a number of loot items from the pool using weighted random selection.
     *
     * @param amount The number of loot items to pull (default 5).
     * @returns An array of selected loot objects.
     */
    pull(amount = 5) {
        // Selects a loot item based on weights.
        const totalWeight = this.getTotalWeight();
        const get = () => {
            // Generate a random number between 1 and totalWeight.
            const chance = LootPool.RANDOM.NextInteger(1, totalWeight);
            let counter = 0;
            for (const [piece, weight] of this.pool) {
                counter += weight;
                // If the random number falls within this weight, select this loot.
                if (chance < counter) {
                    return piece;
                }
            }
        };
        // Pull the requested number of loot items.
        const loot = new Array<Loot>();
        for (let i = 0; i < amount; i++) {
            const got = get();
            if (got !== undefined) loot.push(got);
        }
        return loot;
    }
}

/**
 * Service that manages chest spawning, loot, and interactions.
 */
@Service()
export default class ChestService implements OnInit, OnStart {
    /** Loot pools per chest level. */
    poolPerLevel = new Map<string, LootPool>();

    /** Chest cooldown in seconds. */
    cooldown = 900;

    readonly chests = new Map<
        Vector3,
        {
            markLastOpen: (lastOpen: number) => void;
            model: ChestModel;
            cleanup: () => void;
        }
    >();

    /** Tween info for chest lid animation. */
    openTweenInfo = new TweenInfo(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.Out);

    constructor(
        private dataService: DataService,
        private levelService: LevelService,
        private itemService: ItemService,
    ) {}

    /**
     * Rounds a Vector3 to integer values.
     * @param vector3 The vector to round.
     */
    round(vector3: Vector3) {
        return new Vector3(math.round(vector3.X), math.round(vector3.Y), math.round(vector3.Z));
    }

    /**
     * Marks the last open time for a chest and updates its state.
     *
     * @param chestLocation The chest's location.
     * @param lastOpen The last open timestamp.
     */
    markLastOpen(chestLocation: Vector3, lastOpen: number) {
        const chest = this.chests.get(chestLocation);
        if (chest === undefined) return false;
        chest.markLastOpen(lastOpen);
        return true;
    }

    /**
     * Rewards loot to the player, updating items and XP.
     *
     * @param loots The loot objects to reward.
     */
    rewardLoot(...loots: Loot[]) {
        let totalXp = 0;
        const items = new Map<string, number>();
        const addItem = (item: Item) => {
            const itemId = item.id;
            this.itemService.giveItem(item, 1);
            items.set(itemId, (items.get(itemId) ?? 0) + 1);
        };
        for (const loot of loots) {
            if (loot.xp !== undefined) totalXp += loot.xp;
            if (loot.itemId !== undefined) addItem(Items.getItem(loot.itemId)!);
            if (loot.harvestable !== undefined) addItem(Items.getItem(loot.harvestable)!);
        }
        const current = this.levelService.getXp();
        const serialized = new Array<LootInfo>();

        if (totalXp > 0) {
            this.levelService.setXp(current + totalXp);
            serialized.push({ id: "xp", amount: totalXp });
        }
        for (const [id, amount] of items) {
            serialized.push({ id, amount });
        }

        Packets.showLoot.toAllClients(serialized);
    }

    /**
     * Opens a chest by ID and rewards loot.
     * @param chestId The chest ID.
     * @param amount The amount of loot to pull.
     */
    openChest(chestId: string, amount: number) {
        const pool = this.poolPerLevel.get(chestId);
        if (pool === undefined) throw `No loot pool found for chest ${chestId}`;
        this.rewardLoot(...pool.pull(amount));
    }

    /**
     * Initializes loot pools for each chest level.
     */
    onInit() {
        this.poolPerLevel.set(
            "1",
            new LootPool()
                .addXP(1, 2000)
                .addXP(3, 1000)
                .addXP(5, 500)
                .addItem(Grass, 1000)
                .addItem(StaleWood, 1000)
                .addItem(ExcavationStone, 1000)
                .addItem(WhiteGem, 200)
                .addItem(EnchantedGrass, 100)
                .addHarvestable("MagicalWood", 50)
                .addItem(Crystal, 50)
                .addItem(Iron, 10)
                .addItem(Gold, 1),
        );
        this.poolPerLevel.set(
            "2",
            new LootPool()
                .addXP(1, 500)
                .addXP(3, 1500)
                .addXP(5, 1000)
                .addXP(9, 500)
                .addItem(Grass, 600)
                .addItem(StaleWood, 600)
                .addItem(ExcavationStone, 600)
                .addItem(WhiteGem, 400)
                .addItem(EnchantedGrass, 200)
                .addHarvestable("MagicalWood", 150)
                .addItem(Crystal, 150)
                .addItem(Iron, 70)
                .addItem(Gold, 30)
                .addItem(Quartz, 1),
        );
        this.poolPerLevel.set(
            "3",
            new LootPool()
                .addXP(4, 500)
                .addXP(6, 1500)
                .addXP(9, 1000)
                .addXP(15, 500)
                .addItem(WhiteGem, 400)
                .addItem(EnchantedGrass, 200)
                .addItem(CorruptedGrass, 2)
                .addItem(Crystal, 200)
                .addItem(Iron, 70)
                .addItem(Gold, 30)
                .addItem(Quartz, 2),
        );
    }

    /**
     * Spawns chests in all areas, sets up chest logic, and restores previous state.
     */
    onStart() {
        let i = 0;
        for (const chestMarker of CollectionService.GetTagged("ChestMarker")) {
            // Sanity checks
            if (!chestMarker.IsA("BasePart")) continue;

            const areaFolder = chestMarker.Parent?.Parent;
            if (areaFolder === undefined) continue;

            const area = AREAS[areaFolder.Name as AreaId];
            if (area === undefined) {
                warn(`Chest marker ${chestMarker.GetFullName()} is not in a valid area`);
                continue;
            }

            const chestLocation = this.round(chestMarker.Position);

            const chestModel = ASSETS.Chest.Clone();
            chestModel.PivotTo(chestMarker.CFrame);
            chestModel.Name = `Chest_${i++}`;
            chestModel.Parent = Workspace;

            const prompt = new Instance("ProximityPrompt");
            prompt.AddTag("ProximityPrompt");
            prompt.Style = Enum.ProximityPromptStyle.Custom;
            prompt.ActionText = "Open";
            prompt.ObjectText = "Chest";
            prompt.RequiresLineOfSight = false;
            prompt.MaxActivationDistance = 6;
            prompt.Parent = chestModel.PrimaryPart;

            const bp = weldModel(chestModel.Lid);
            const originalLidPivot = bp.CFrame;
            let lastOpen = 0;
            let isOpened = false;
            const markLastOpen = (lo: number) => {
                lastOpen = lo;
                isOpened = tick() - lastOpen < this.cooldown;
                TweenService.Create(bp, this.openTweenInfo, {
                    CFrame: isOpened ? originalLidPivot.mul(CFrame.Angles(-1, 0, 0)) : originalLidPivot,
                }).Play();
                prompt.Enabled = !isOpened;
                chestModel.Hitbox.CooldownGui.Enabled = isOpened;
            };
            const intervalCleanup = simpleInterval(() => {
                const elapsed = tick() - lastOpen;
                if (elapsed > this.cooldown && isOpened === true) {
                    isOpened = false;
                    markLastOpen(lastOpen);
                }
                if (isOpened) {
                    chestModel.Hitbox.CooldownGui.CooldownLabel.Text = convertToMMSS(
                        math.floor(this.cooldown - elapsed),
                    );
                }
            }, 1);

            const triggerCleanup = CustomProximityPrompt.onTrigger(prompt, () => {
                if (!this.dataService.empireData.unlockedAreas.has(area.id)) return;

                playSound("ChestOpen.mp3", chestModel.PrimaryPart);
                const t = tick();
                const amount = lastOpen === 0 ? math.random(10, 14) : math.random(3, 7);
                this.dataService.empireData.openedChests.set(
                    `${chestLocation.X}_${chestLocation.Y}_${chestLocation.Z}`,
                    t,
                );
                markLastOpen(t);
                task.delay(0.25, () => {
                    this.openChest(chestMarker.Name, amount);
                });
            });

            this.chests.set(chestLocation, {
                markLastOpen,
                model: chestModel,
                cleanup: () => {
                    chestModel.Destroy();
                    intervalCleanup();
                    triggerCleanup();
                },
            });
        }

        eat(() => {
            for (const [, chest] of this.chests) {
                chest.cleanup();
            }
            this.chests.clear();
        });

        const lastOpenPerLocation = this.dataService.empireData.openedChests;
        for (const [location, lastOpen] of lastOpenPerLocation) {
            const [xString, yString, zString] = location.split("_");
            if (!this.markLastOpen(new Vector3(tonumber(xString), tonumber(yString), tonumber(zString)), lastOpen)) {
                lastOpenPerLocation.delete(location);
            }
        }
    }
}


/**
 * @fileoverview Configures and manages Roblox physics collision groups.
 *
 * This service:
 * - Registers all required collision groups for the game world
 * - Sets up collision rules between groups for gameplay logic
 * - Ensures collision groups are initialized before any physics interactions
 *
 * @since 1.0.0
 */

import { OnInit, OnStart, Service } from "@flamework/core";
import { PhysicsService } from "@rbxts/services";
import { OnPlayerAdded } from "server/services/ModdingService";
import { getPlayerCharacter } from "shared/hamster/getPlayerCharacter";

/**
 * Service that sets up and manages collision groups and their interactions.
 */
@Service()
export default class CollisionGroupService implements OnInit, OnStart, OnPlayerAdded {
    onCharacterAdded(character: Model | undefined) {
        if (character === undefined) return;
        const rootPart = character.WaitForChild("HumanoidRootPart") as BasePart;
        for (const part of character.GetChildren()) {
            if (part.IsA("BasePart")) {
                part.CollisionGroup = part === rootPart ? "PlayerHitbox" : "Player";
            }
        }
    }

    onPlayerAdded(player: Player) {
        player.CharacterAdded.Connect((character) => this.onCharacterAdded(character));
        this.onCharacterAdded(player.Character);
    }

    /**
     * Initializes collision groups and configures their interactions.
     * Called automatically on service initialization.
     */
    onInit() {
        // Physics Collision Group Setup
        // Initialize all collision groups before any physics interactions occur

        PhysicsService.RegisterCollisionGroup("Decoration");
        PhysicsService.RegisterCollisionGroup("ItemHitbox");
        PhysicsService.RegisterCollisionGroup("Item");
        PhysicsService.RegisterCollisionGroup("BuildGrid");
        PhysicsService.RegisterCollisionGroup("QueryableGhost");
        PhysicsService.RegisterCollisionGroup("Antighost");
        PhysicsService.RegisterCollisionGroup("Droplet");
        PhysicsService.RegisterCollisionGroup("DropletInquirer");
        PhysicsService.RegisterCollisionGroup("Player");
        PhysicsService.RegisterCollisionGroup("PlayerHitbox");
        PhysicsService.RegisterCollisionGroup("PlayerInquirer");
        PhysicsService.RegisterCollisionGroup("NPC");
        PhysicsService.RegisterCollisionGroup("HarvestableHitbox");
        PhysicsService.RegisterCollisionGroup("HarvestableInquirer");

        // Configure collision interactions for droplets
        PhysicsService.CollisionGroupSetCollidable("Droplet", "Default", false);
        PhysicsService.CollisionGroupSetCollidable("Droplet", "Droplet", false);
        PhysicsService.CollisionGroupSetCollidable("Droplet", "QueryableGhost", false);
        PhysicsService.CollisionGroupSetCollidable("Droplet", "Decoration", false);
        PhysicsService.CollisionGroupSetCollidable("Droplet", "Item", true);
        PhysicsService.CollisionGroupSetCollidable("Droplet", "BuildGrid", true);
        PhysicsService.CollisionGroupSetCollidable("Droplet", "DropletInquirer", true);

        // Configure item hitbox interactions
        PhysicsService.CollisionGroupSetCollidable("ItemHitbox", "Droplet", false);
        PhysicsService.CollisionGroupSetCollidable("ItemHitbox", "Item", false);
        PhysicsService.CollisionGroupSetCollidable("ItemHitbox", "BuildGrid", false);

        // Configure special ghost groups for selective collision
        for (const group of PhysicsService.GetRegisteredCollisionGroups()) {
            PhysicsService.CollisionGroupSetCollidable("QueryableGhost", group.name, group.name === "QueryableGhost");
            PhysicsService.CollisionGroupSetCollidable("Antighost", group.name, group.name === "Droplet");
            PhysicsService.CollisionGroupSetCollidable(
                "NPC",
                group.name,
                group.name === "Default" || group.name === "BuildGrid",
            );
            PhysicsService.CollisionGroupSetCollidable("PlayerInquirer", group.name, group.name === "PlayerHitbox");
            PhysicsService.CollisionGroupSetCollidable(
                "HarvestableHitbox",
                group.name,
                group.name === "HarvestableInquirer" || group.name === "Player",
            );
        }
    }

    onStart() {
        const char = getPlayerCharacter();
        if (char) this.onCharacterAdded(char);
    }
}


/**
 * @fileoverview Manages grabbable items scattered throughout the world.
 *
 * This service handles:
 * - Finding and initializing ProximityPrompts tagged as "Grabbable"
 * - Visual representation of grabbable items (replacing placeholder parts with actual item models)
 * - Tracking item collection status using event completion system
 * - Giving collected items to players and showing reward notifications
 * - Cleaning up grabbed items to prevent duplicate collection
 *
 * Grabbable items are typically placed in the world as collectibles that players can
 * interact with once. They use the event system to ensure each item can only be
 * collected once per server session.
 *
 * @since 1.0.0
 */

import { OnInit, Service } from "@flamework/core";
import { CollectionService, Workspace } from "@rbxts/services";
import EventService from "server/services/data/EventService";
import ItemService from "server/services/item/ItemService";
import { IS_EDIT } from "shared/Context";
import eat from "shared/hamster/eat";
import Items from "shared/items/Items";
import Packets from "shared/Packets";
import CustomProximityPrompt from "shared/world/CustomProximityPrompt";

/**
 * Service that manages grabbable items throughout the world.
 *
 * Handles the initialization, visual setup, and collection mechanics for
 * items that players can find and collect in the game world. Each grabbable
 * item can only be collected once and is tracked via the event system.
 */
@Service()
export default class GrabbableService implements OnInit {
    constructor(
        private readonly eventService: EventService,
        private readonly itemService: ItemService,
    ) {}

    onInit() {
        for (const proximityPrompt of CollectionService.GetTagged("Grabbable")) {
            // Validate that the tagged object is actually a ProximityPrompt
            if (!proximityPrompt.IsA("ProximityPrompt")) continue;

            const parent = proximityPrompt.Parent;
            if (parent === undefined) continue;

            // Ensure the prompt has a valid BasePart parent
            if (!parent.IsA("BasePart")) {
                warn("ProximityPrompt parent is not a BasePart");
                continue;
            }

            // Get the item data using the parent's name as the item ID
            const itemId = parent.Name;
            const item = Items.getItem(itemId);
            if (item === undefined) {
                warn(`Item with ID ${itemId} not found`);
                continue;
            }

            // Replace cyan placeholder parts (0, 255, 255) with actual item models
            let model: Model | undefined;
            if (parent.Color === Color3.fromRGB(0, 255, 255) && parent.Transparency === 0.5) {
                model = item.MODEL?.Clone();
                if (model) {
                    model.PivotTo(parent.CFrame);
                    model.Parent = Workspace;
                    eat(model, "Destroy");
                }
                // Hide the placeholder part
                if (!IS_EDIT) parent.Transparency = 1;
            }

            // Check if this item has already been collected
            const eventId = itemId + "_grabbed";
            if (this.eventService.isEventCompleted(eventId)) {
                if (!IS_EDIT) {
                    proximityPrompt.Parent?.Destroy();
                }
                model?.Destroy();
                continue;
            }

            // Set up collection event handler
            const cleanup = CustomProximityPrompt.onTrigger(proximityPrompt, () => {
                // Double-check that the item hasn't been collected since initialization
                if (this.eventService.isEventCompleted(eventId)) {
                    return;
                }

                // Mark the item as collected and give it to the player
                this.eventService.setEventCompleted(eventId, true);
                this.itemService.giveItem(item, 1);
                Packets.showLoot.toAllClients([{ id: itemId, amount: 1 }]);

                // Remove the grabbable object from the world
                if (!IS_EDIT) {
                    proximityPrompt.Parent?.Destroy();
                }
                model?.Destroy();
            });
            eat(cleanup);
        }
    }
}


import { toNumeral } from "@rbxts/roman-numerals";
import { OnoeNum } from "@rbxts/serikanum";
import { Debris, TweenService, Workspace } from "@rbxts/services";
import StringBuilder from "@rbxts/stringbuilder";
import { Server } from "shared/api/APIExpose";
import { ASSETS, playSound } from "shared/asset/GameAssets";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Item from "shared/item/Item";
import EfficientKillingUpgrader from "shared/items/0/win/EfficientKillingUpgrader";
import Admiration from "shared/items/negative/instantwin/Admiration";
import Codependence from "shared/items/negative/instantwin/Codependence";
import KillbrickUpgrader from "shared/items/negative/restful/KillbrickUpgrader";
import { GainUpgrade } from "shared/namedupgrade/NamedUpgrade";
import NamedUpgrades from "shared/namedupgrade/NamedUpgrades";
import Packets from "shared/Packets";
import { AREAS } from "shared/world/Area";

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

export class Challenge {
    static readonly CHALLENGES = new Array<Challenge>();

    /** The name of the challenge. */
    name: string;

    /**
     * Gets the description for the challenge at a given level.
     * @param level The challenge description at the given level.
     * @returns The description.
     */
    description = (level: number) => "No description provided.";

    /**
     * The colors used for the challenge UI.
     */
    colors = { primary: new Color3(), secondary: new Color3() };

    /** The maximum level for the challenge. */
    cap = 1;

    /**
     * Minimum empire level required to start the challenge at a given level.
     * @param level The challenge level to start.
     * @return The required empire level.
     */
    requiredEmpireLevel = (level: number) => 0;

    challengeEffectInterval = -1;
    challengeEffect?: (dt: number, level: number, forceEnd: (message: string) => void) => void;
    challengeUpgrade?: GainUpgrade;
    rewardUpgrade?: GainUpgrade;
    itemRewards?: Map<number, { item: Item; count: number }>;
    resets?: ResetLayerId;
    goal: Item[] | string = "No goal provided.";
    itemRestrictionFilter?: (item: Item) => boolean;

    constructor(public readonly id: string) {
        this.name = this.id;
        Challenge.CHALLENGES.push(this);
    }

    setName(name: string) {
        this.name = name;
        return this;
    }

    setDescription(description: (level: number) => string) {
        this.description = description;
        return this;
    }

    setColors(primary: Color3, secondary: Color3) {
        this.colors = { primary, secondary };
        return this;
    }

    setCap(cap: number) {
        this.cap = cap;
        return this;
    }

    setRequiredEmpireLevel(empireLevel: (level: number) => number) {
        this.requiredEmpireLevel = empireLevel;
        return this;
    }

    setChallengeEffectInterval(interval: number) {
        this.challengeEffectInterval = interval;
        return this;
    }

    setChallengeEffect(effect: (dt: number, level: number, forceEnd: (message: string) => void) => void) {
        this.challengeEffect = effect;
        return this;
    }

    setChallengeUpgrade(upgrade: GainUpgrade) {
        const upgradeId = this.id + "_ch";
        NamedUpgrades.register(upgradeId, upgrade);
        this.challengeUpgrade = upgrade;
        return this;
    }

    setRewardUpgrade(upgrade: GainUpgrade) {
        const upgradeId = this.id + "_rw";
        NamedUpgrades.register(upgradeId, upgrade);
        this.rewardUpgrade = upgrade;
        return this;
    }

    setItemRewards(rewards: Map<number, { item: Item; count: number }>) {
        this.itemRewards = rewards;
        return this;
    }

    setResets(layerId: ResetLayerId) {
        this.resets = layerId;
        return this;
    }

    setGoal(goal: Item[] | string) {
        this.goal = goal;
        return this;
    }

    setItemRestrictionFilter(filter: (item: Item) => boolean) {
        this.itemRestrictionFilter = filter;
        return this;
    }

    /**
     * Returns the formatted title label for a challenge.
     * @param level The challenge level.
     */
    getTitleLabel(level: number) {
        return `${this.name} ${toNumeral(level)}`;
    }

    /**
     * Returns the task label for a challenge.
     * @param challenge The challenge details.
     */
    getTaskLabel() {
        if (typeIs(this.goal, "table")) {
            const builder = new StringBuilder("Get ");
            this.goal.forEach((item, i) => builder.append(i === 0 ? item.name : "/" + item.name));
            return builder.toString();
        }
        return "No task";
    }

    /**
     * Returns the reward label for a challenge at the given level.
     * @param currentLevel The current challenge level.
     */
    getRewardLabel(currentLevel: number) {
        const itemReward = this.itemRewards?.get(currentLevel + 1);
        if (itemReward !== undefined) {
            return `${itemReward.count}x ${itemReward.item.name}`;
        }
        const upgrade = this.rewardUpgrade;
        if (upgrade !== undefined) {
            return upgrade.toString(currentLevel) + " -> " + upgrade.toString(currentLevel + 1);
        }
        return "No reward";
    }

    static readonly MeltingEconomy = (() => {
        const getNerf = (level: number) => {
            if (level > 9) {
                return 0.01;
            } else {
                return 1 - level * 0.1;
            }
        };

        const itemRewards = new Map<number, { item: Item; count: number }>();
        itemRewards.set(1, { item: KillbrickUpgrader, count: 1 });
        itemRewards.set(2, { item: KillbrickUpgrader, count: 1 });
        itemRewards.set(3, { item: KillbrickUpgrader, count: 1 });
        itemRewards.set(4, { item: KillbrickUpgrader, count: 1 });
        itemRewards.set(5, { item: EfficientKillingUpgrader, count: 1 });

        return new Challenge("MeltingEconomy")
            .setName("Melting Economy")
            .setDescription((level) => `Funds gain is heavily nerfed by ^${getNerf(level)}.`)
            .setColors(Color3.fromRGB(170, 255, 151), Color3.fromRGB(0, 170, 255))
            .setCap(5)
            .setRequiredEmpireLevel((level) => level + 5)
            .setChallengeUpgrade(new GainUpgrade().setPow((x) => new CurrencyBundle().set("Funds", getNerf(x))))
            .setItemRewards(itemRewards)
            .setResets("Skillification" as ResetLayerId)
            .setGoal([Admiration, Codependence]);
    })();

    static readonly CataclysmicWorld = (() => {
        const cwMeteorTween = new TweenInfo(0.5, Enum.EasingStyle.Linear);
        const cataclysicWorldCd = (level: number) => 22 - level * 7;
        const spawnMeteor = () => {
            const meteor = ASSETS.VFX.WaitForChild("Meteor").Clone() as BasePart;
            const grid = AREAS.BarrenIslands.gridWorldNode!.waitForInstance();
            const rand = grid.Size.X * 0.25;
            const target = grid.Position.add(
                new Vector3((math.random() - 0.5) * rand, 0, (math.random() - 0.5) * rand),
            );
            meteor.CFrame = CFrame.lookAt(
                target.add(new Vector3(math.random(-50, 50), 300, math.random(-50, 50))),
                target,
            );
            TweenService.Create(meteor, cwMeteorTween, { Position: target }).Play();
            meteor.Parent = Workspace;
            task.delay(0.5, () => {
                meteor.Transparency = 1;
                meteor.ClearAllChildren();
                playSound("Explosion.mp3", meteor);
                meteor.CanCollide = false;
                meteor.CanTouch = false;
                meteor.CanQuery = false;
                const reverb = () => {
                    const part = new Instance("Part");
                    part.Anchored = true;
                    part.CanCollide = false;
                    part.Shape = Enum.PartType.Ball;
                    part.Position = meteor.Position;
                    part.Color = new Color3(1, 0.48, 0);
                    part.Transparency = 0.6;
                    part.CanCollide = false;
                    part.CanTouch = false;
                    part.CanQuery = false;
                    TweenService.Create(part, new TweenInfo(0.2), {
                        Size: new Vector3(100, 100, 100),
                        Transparency: 1,
                    }).Play();
                    part.Parent = meteor;
                };
                reverb();
                task.delay(0.1, reverb);
                task.delay(0.2, reverb);
                const explosion = new Instance("Explosion");
                explosion.ExplosionType = Enum.ExplosionType.NoCraters;
                explosion.BlastRadius = 150;
                explosion.BlastPressure = 500000;
                explosion.DestroyJointRadiusPercent = 0;
                explosion.Position = meteor.Position;
                explosion.Parent = meteor;
                Packets.shakeCamera.toAllClients("Bump");
            });
            Debris.AddItem(meteor, 5);
        };

        return new Challenge("CataclysmicWorld")
            .setName("Cataclysmic World")
            .setDescription(
                (level) =>
                    `Meteors strike your setup every ${cataclysicWorldCd(level)} seconds. Every 4 minutes, the cooldown halves.`,
            )
            .setColors(Color3.fromRGB(234, 7, 255), Color3.fromRGB(255, 85, 127))
            .setCap(3)
            .setRequiredEmpireLevel((level) => level + 6)
            .setChallengeEffect((dt: number, level: number, forceEnd: (message: string) => void) => {
                const challengeStart = Server.empireData.currentChallengeStartTime;
                const questMetadata = Server.empireData.questMetadata;
                let meteorCooldown = questMetadata.get("CataclysmicWorldCooldown") as number | undefined;
                if (meteorCooldown === undefined || meteorCooldown <= 0) {
                    const elapsed = math.floor((Server.empireData.playtime - challengeStart) / 240);
                    meteorCooldown = cataclysicWorldCd(level) / math.pow(2, elapsed);
                    if (meteorCooldown < 0.5) {
                        forceEnd(
                            "Cataclysmic World was forcefully stopped because it was impossible to continue. You could not save the world.",
                        );
                        return;
                    }
                    spawnMeteor();
                } else {
                    meteorCooldown -= dt;
                }
                questMetadata.set("CataclysmicWorldCooldown", meteorCooldown);
            })
            .setChallengeEffectInterval(1)
            .setResets("Skillification" as ResetLayerId)
            .setRewardUpgrade(
                new GainUpgrade().setMul((x) => new CurrencyBundle().set("Skill", new OnoeNum(1.5).pow(x))),
            )
            .setGoal([Admiration, Codependence]);
    })();

    static readonly PinnedProgress = (() => {
        return new Challenge("PinnedProgress")
            .setName("Pinned Progress")
            .setDescription(() => `You cannot place Conveyors down.`)
            .setColors(Color3.fromRGB(29, 67, 80), Color3.fromRGB(164, 57, 49))
            .setCap(3)
            .setRequiredEmpireLevel((level) => level * 2 + 6)
            .setItemRestrictionFilter((item: Item) => {
                return !item.isA("Conveyor");
            })
            .setRewardUpgrade(new GainUpgrade().setMul((x) => new CurrencyBundle().set("Power", new OnoeNum(2).pow(x))))
            .setResets("Skillification" as ResetLayerId)
            .setGoal([Admiration, Codependence]);
    })();
}

export const [CHALLENGE_PER_ID, CHALLENGE_UPGRADES, REWARD_UPGRADES] = (function () {
    const challengePerId = new Map<string, Challenge>();
    const challengeUpgrades = new Map<string, string>();
    const rewardUpgrades = new Map<string, string>();
    for (const challenge of Challenge.CHALLENGES) {
        challengePerId.set(challenge.id, challenge);
        if (challenge.challengeUpgrade !== undefined) {
            challengeUpgrades.set(challenge.id, challenge.challengeUpgrade.id);
        }
        if (challenge.rewardUpgrade !== undefined) {
            rewardUpgrades.set(challenge.id, challenge.rewardUpgrade.id);
        }
    }
    return [challengePerId, challengeUpgrades, rewardUpgrades];
})();


import { Flamework } from "@flamework/core";
import { RunService, Workspace } from "@rbxts/services";
import { eater } from "shared/hamster/eat";
import StartScreenValue from "shared/world/nodes/StartScreenValue";

/**
 * Whether the current context is the server.
 */
export const IS_SERVER = RunService.IsServer();

/**
 * Whether the game is running in an "edit" environment, where the experience is not running
 * and scripts are being run directly on the development server.
 */
export const IS_EDIT = eater.isEdit;

/**
 * Whether the current context is Roblox Studio.
 */
export const IS_STUDIO = RunService.IsStudio();

/**
 * Whether the game is in Single Server mode.
 */
export const IS_SINGLE_SERVER = game.PlaceId === 17479698702;

export const IS_PUBLIC_SERVER = (() => {
    if (IS_STUDIO) {
        return StartScreenValue.getInstance()?.Value ?? false;
    }

    let key = "IsPublicServer";
    if (!IS_SERVER) return Workspace.GetAttribute(key) === true;

    let value = game.PrivateServerId === "";
    Workspace.SetAttribute(key, value);
    return value;
})();

export function preloadFlameworkServer() {
    Flamework.addPaths("src/server/services");
}

export function igniteFlameworkServer() {
    preloadFlameworkServer();
    Flamework.ignite();
}


import { ReplicatedStorage, Workspace } from "@rbxts/services";
import BuildBounds from "shared/placement/BuildBounds";

/**
 * Provides utilities for managing the sandbox environment in the game,
 * including toggling sandbox mode, creating baseplate bounds, and synthesizing workspace objects.
 */
export default class Sandbox {
    private static readonly sandboxValue = (() => {
        const value = Workspace.FindFirstChild("SANDBOX") as BoolValue | undefined;
        if (value?.Value === true) {
            Workspace.SetAttribute("Sandbox", true);
        }
        return value;
    })();

    /**
     * The `Workspace.Baseplate` part if sandbox mode is enabled.
     */
    static readonly baseplate = (() => {
        if (!this.getEnabled()) return undefined;
        return Workspace.FindFirstChild("Baseplate") as Part;
    })();

    /**
     * BuildBounds for {@link baseplate} if sandbox mode is enabled.
     */
    static readonly baseplateBounds = (() => (this.baseplate ? new BuildBounds(this.baseplate) : undefined))();

    /**
     * Checks if sandbox mode is enabled.
     * @returns True if sandbox mode is enabled, false otherwise.
     */
    static getEnabled() {
        return Workspace.GetAttribute("Sandbox") === true;
    }

    /**
     * Synthesizes and sets up sandbox workspace objects if sandbox mode is enabled.
     * @returns True if synthesis occurred, false otherwise.
     */
    static synthesise() {
        if (!this.getEnabled()) {
            return false;
        }
        const workspaceModels = Workspace.FindFirstChild("ItemModels");
        if (workspaceModels !== undefined) {
            workspaceModels.Parent = ReplicatedStorage;
        }
        return true;
    }

    static getId() {
        return this.sandboxValue?.GetAttribute("Id") as string | undefined;
    }
}


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

const UserGameSettings = RunService.IsClient() ? UserSettings().GetService("UserGameSettings") : undefined;

export default UserGameSettings;


import { Debris, ReplicatedStorage, SoundService } from "@rbxts/services";
import { Environment } from "@rbxts/ui-labs";
import { assets } from "shared/asset/AssetMap";
import { IS_EDIT } from "shared/Context";
import eat from "shared/hamster/eat";

declare global {
    /**
     * The structure of the Assets folder in StarterGui.
     *
     * Modify this interface to match the actual structure of the Assets folder.
     */
    interface Assets extends Folder {
        Effects: Folder;
    }

    type AssetPath = keyof typeof assets;

    // Only asset paths that start with "assets/sounds/"
    type SoundAssetPath = Extract<AssetPath, `assets/sounds/${string}`>;

    // Extract the path after "assets/sounds/" while preserving subdirectories
    type SoundPath<T extends SoundAssetPath> = T extends `assets/sounds/${infer Rest}` ? Rest : never;
    type SoundAssetSubPath = SoundPath<SoundAssetPath>;

    type Filename<T extends string> = T extends `${string}/${infer Rest}` ? Filename<Rest> : T;
}

/**
 * Contains game assets.
 */
export const ASSETS = ReplicatedStorage.WaitForChild("Assets") as Assets;

/**
 * The sound group for sound effects.
 *
 * This group is used to categorize and manage sound effects in the game.
 */
export const SOUND_EFFECTS_GROUP = (() => {
    const key = "SoundEffects";
    let group = SoundService.FindFirstChild(key) as SoundGroup | undefined;
    if (group === undefined) {
        group = new Instance("SoundGroup");
        group.Name = key;
        group.Parent = SoundService;
    }
    if (IS_EDIT) {
        eat(group);
    }
    return group;
})();

/**
 * Returns a sound from the Assets folder.
 *
 * @param soundPath The path to the sound relative to the sounds folder (preserving subdirectories).
 * @returns The sound instance from the Assets folder.
 */
export function getSound(soundPath: SoundAssetSubPath) {
    const fullPath = ("assets/sounds/" + soundPath) as AssetPath;
    const cached = ReplicatedStorage.FindFirstChild(fullPath) as Sound | undefined;
    if (cached !== undefined) {
        return cached;
    }
    const sound = new Instance("Sound");
    sound.Name = fullPath;
    sound.SoundId = assets[fullPath];
    sound.SoundGroup = SOUND_EFFECTS_GROUP;
    sound.Parent = ReplicatedStorage;
    eat(sound, "Destroy");
    return sound;
}

/**
 * Plays a sound at a specific part's position.
 *
 * @param soundPath The path to the sound relative to the sounds folder (preserving subdirectories).
 * @param part The part where the sound should be played. If not provided, it plays in ReplicatedStorage.
 * @param modifier An optional function to modify the sound instance before playing it.
 */
export function playSound(soundPath: SoundAssetSubPath, part?: Instance, modifier?: (sound: Sound) => void) {
    let sound = getSound(soundPath);
    if (IS_EDIT) {
        sound.Parent = Environment.PluginWidget; // Sounds can only play in PluginWidget when not in-game
    } else {
        sound = sound.Clone();
        sound.Parent = part ?? ReplicatedStorage;
    }

    task.delay(1, () => {
        Debris.AddItem(sound, (sound.TimeLength - sound.TimePosition) / sound.PlaybackSpeed);
    });

    if (modifier) {
        modifier(sound);
    }

    sound.Play();
}

/**
 * Returns a particle emitter from the Assets folder.
 *
 * @param effectName The name of the effect to retrieve, without the "Effect" suffix.
 * @returns The particle emitter instance from the Assets folder.
 */
export function getEffect(effectName: string) {
    return ASSETS.Effects.WaitForChild(effectName + "Effect") as ParticleEmitter;
}

/**
 * Emits a particle effect from the Assets folder.
 *
 * @param effectName The name of the effect to emit, without the "Effect" suffix.
 * @param parent The parent instance to attach the effect to.
 * @param amount The number of particles to emit (default is 1).
 * @returns The emitted effect instance.
 */
export function emitEffect(effectName: string, parent: Instance | undefined, amount = 1) {
    const effect = getEffect(effectName).Clone();
    effect.Enabled = false;
    effect.Parent = parent;
    effect.Emit(amount);
    Debris.AddItem(effect, 4);
    return effect;
}


import { TeleportService } from "@rbxts/services";
import Command from "shared/commands/Command";
import { IS_SINGLE_SERVER } from "shared/Context";

export = new Command(script.Name)
    .addAlias("j")
    .setDescription("<accesscode> : Joins an empire given an access code.")
    .setExecute((sender, accessCode) => {
        if (sender === undefined) {
            warn("join command can only be run by a player.");
            return;
        }
        if (IS_SINGLE_SERVER) {
            return;
        }
        const [ac, id] = accessCode.split("|");
        TeleportService.TeleportToPrivateServer(game.PlaceId, ac, [sender], undefined, id);
    })
    .setPermissionLevel(0);


import { Debris, Players } from "@rbxts/services";
import { Server } from "shared/api/APIExpose";
import Command from "shared/commands/Command";

export = new Command(script.Name)
    .addAlias("vr")
    .setDescription("<player> : Vote to restrict a player.")
    .setExecute((sender, p) => {
        if (sender === undefined) {
            warn("voterestrict command can only be run by a player.");
            return;
        }

        const targets = Server.Command.findPlayers(sender, p);
        if (targets.size() < 1) {
            Server.ChatHook.sendPrivateMessage(sender, `Could not find matching players ${p}`, "color:255,43,43");
            return;
        }
        const data = Server.empireData;
        const playerCount = Players.GetPlayers()
            .filter((player) => Server.Permissions.getPermissionLevel(player.UserId) > -1)
            .size();
        for (const target of targets) {
            const userId = target.UserId;
            if (Server.Permissions.getPermissionLevel(userId) >= 1) {
                Server.ChatHook.sendPrivateMessage(
                    sender,
                    "You can't vote to restrict a trusted player",
                    "color:255,43,43",
                );
                continue;
            }
            if (sender.FindFirstChild(userId) !== undefined) {
                Server.ChatHook.sendPrivateMessage(
                    sender,
                    "You have already voted to restrict this player",
                    "color:255,43,43",
                );
                continue;
            }
            const votes = ((target.GetAttribute("Votes") as number) ?? 0) + 1;
            target.SetAttribute("Votes", votes);
            if (votes === 0) {
                Server.ChatHook.sendServerMessage(
                    `A vote has started to restrict player ${target.Name}. Type /vr ${target.Name} to vote to restrict them too.`,
                    "color:138,255,138",
                );
            }
            const requirement = math.round((playerCount * 2) / 3);
            Server.ChatHook.sendServerMessage(`${votes}/${requirement} votes needed.`, "color:138,255,138");
            const voteToken = new Instance("NumberValue");
            voteToken.Value = tick();
            voteToken.Name = tostring(userId);
            voteToken.Parent = sender;
            Debris.AddItem(voteToken, 60);
            task.delay(60, () => {
                if (target === undefined) {
                    return;
                }
                target.SetAttribute("Votes", (target.GetAttribute("Votes") as number) - 1);
                if ((target.GetAttribute("RestrictionTime") as number) ?? 0 < tick()) {
                    Server.ChatHook.sendPrivateMessage(
                        sender,
                        `Your vote to restrict ${target.Name} has worn off.`,
                        "color:138,255,138",
                    );
                }
            });
            Server.ChatHook.sendPrivateMessage(
                sender,
                `Voted to restrict ${target.Name}. Your vote will wear off after 60 seconds.`,
                "color:138,255,138",
            );
            if (votes >= requirement) {
                Server.ChatHook.sendServerMessage(
                    `${target.Name} has been restricted for 20 minutes.`,
                    "color:138,255,138",
                );
                data.restricted.set(userId, tick() + 1200);
                task.delay(1201, () => {
                    const t = data.restricted.get(userId);
                    if (t === undefined || tick() - t > 0) {
                        data.restricted.delete(userId);
                        Server.Permissions.updatePermissionLevel(userId);
                    }
                });
                Server.Permissions.updatePermissionLevel(userId);
            }
        }
    })
    .setPermissionLevel(0);


import { Workspace } from "@rbxts/services";
import { Server } from "shared/api/APIExpose";
import Command from "shared/commands/Command";
import { getPlayerCharacter } from "shared/hamster/getPlayerCharacter";

export = new Command(script.Name)
    .addAlias("ws")
    .setDescription(
        "<amount> : Sets your walk speed to the specified amount. If higher than the limit, it will be capped to the maximum allowed speed.",
    )
    .setExecute((sender, amount) => {
        let walkspeed = tonumber(amount);
        if (walkspeed !== undefined && walkspeed < 0) {
            Server.ChatHook.sendPrivateMessage(sender, "Walk speed cannot be negative.", "color:255,43,43");
            return;
        }
        const maxWalkSpeed = (Workspace.GetAttribute("WalkSpeed") as number) ?? 16;
        walkspeed ??= 16;
        if (walkspeed > maxWalkSpeed) {
            Server.ChatHook.sendPrivateMessage(sender, `Walk speed capped at ${maxWalkSpeed}.`, "color:255,43,43");
        }
        walkspeed = math.min(walkspeed, maxWalkSpeed);
        const humanoid = getPlayerCharacter(sender)?.FindFirstChildOfClass("Humanoid");
        if (humanoid === undefined) {
            return;
        }
        humanoid.WalkSpeed = walkspeed;
        Server.ChatHook.sendPrivateMessage(sender, `Walk speed set to ${walkspeed}.`, "color:138,255,138");
    })
    .setPermissionLevel(0);


import { Players } from "@rbxts/services";
import { playSound } from "shared/asset/GameAssets";
import { Server } from "shared/api/APIExpose";
import Command from "shared/commands/Command";

export = new Command(script.Name)
    .addAlias("b")
    .setDescription("<player> <useId: boolean> : Bans a player from the server.")
    .setExecute((sender, p, useId) => {
        const targets = Server.Command.findPlayers(sender, p);
        if (targets.size() < 1) {
            const userId = useId === "true" ? tonumber(p) : Players.GetUserIdFromNameAsync(p);
            if (userId !== undefined) {
                if (Server.Permissions.isLowerLevel(sender, userId)) {
                    Server.ChatHook.sendPrivateMessage(
                        sender,
                        "You can't ban someone with an equal/higher permission level.",
                        "color:255,43,43",
                    );
                    return;
                }
                const success = Server.Permissions.add("banned", userId);
                if (success) {
                    Server.ChatHook.sendPrivateMessage(
                        sender,
                        `Banned ${Server.Command.fp(p, userId)}`,
                        "color:138,255,138",
                    );
                } else {
                    Server.ChatHook.sendPrivateMessage(
                        sender,
                        `${Server.Command.fp(p, userId)} is already banned`,
                        "color:255,43,43",
                    );
                }
                return;
            }
            Server.ChatHook.sendPrivateMessage(sender, `Could not find matching players ${p}`, "color:255,43,43");
            return;
        }
        for (const target of targets) {
            if (target === sender) {
                Server.ChatHook.sendPrivateMessage(sender, "You can't ban yourself.", "color:255,43,43");
                continue;
            }
            if (Server.Permissions.isLowerLevel(sender, target.UserId)) {
                Server.ChatHook.sendPrivateMessage(
                    sender,
                    "You can't ban someone with an equal/higher permission level.",
                    "color:255,43,43",
                );
                continue;
            }
            Server.ChatHook.sendPrivateMessage(sender, `Banned player ${target.Name}`, "color:138,255,138");
            const h = target.Character?.FindFirstChildOfClass("Humanoid");
            if (h !== undefined && h.RootPart !== undefined) {
                const smoke = new Instance("Smoke");
                smoke.Size = 5;
                smoke.TimeScale = 20;
                smoke.Parent = h.RootPart;
                const attachment =
                    (h.RootPart.FindFirstChild("LVAttachment") as Attachment) ?? new Instance("Attachment", h.RootPart);
                attachment.Name = "LVAttachment";
                const vector = new Vector3(0, 300, 0);
                const linearVelocity = new Instance("LinearVelocity");
                linearVelocity.MaxForce = vector.Magnitude * 20000;
                linearVelocity.VectorVelocity = vector;
                linearVelocity.Attachment0 = attachment;
                linearVelocity.Parent = attachment;
                playSound("Rocket.mp3", h.RootPart);
            }
            Server.Permissions.add("banned", target.UserId);
            task.delay(1, () => {
                if (target !== undefined) {
                    target.Kick(`You were banned by ${sender?.Name ?? "an administrator"}.`);
                }
            });
        }
    })
    .setPermissionLevel(2);


import { CollectionService } from "@rbxts/services";
import { Server } from "shared/api/APIExpose";
import Command from "shared/commands/Command";

export = new Command(script.Name)
    .addAlias("cd")
    .setDescription("Delete ALL droplets in ALL areas.")
    .setExecute((_o) => {
        for (const droplet of CollectionService.GetTagged("Droplet")) if (droplet.IsA("BasePart")) droplet.Destroy();
        Server.ChatHook.sendServerMessage("Deleted all droplets");
    })
    .setPermissionLevel(2);


import { ReplicatedStorage } from "@rbxts/services";
import { Server } from "shared/api/APIExpose";
import Command from "shared/commands/Command";

export = new Command(script.Name)
    .addAlias("csset")
    .setDescription("<id> : Set the color for color strict items.")
    .setExecute((_o, colorId) => {
        ReplicatedStorage.SetAttribute("ColorStrictColor", tonumber(colorId) ?? 0);
        Server.ChatHook.sendServerMessage(`Color strict color set to ${tonumber(colorId) ?? 0}.`);
    })
    .setPermissionLevel(4);


import { Workspace } from "@rbxts/services";
import { Server } from "shared/api/APIExpose";
import Command from "shared/commands/Command";

export = new Command(script.Name)
    .addAlias("getpartcount")
    .setDescription("Get the part count of the current world.")
    .setExecute(() => {
        let i = 0;
        for (const part of Workspace.GetDescendants()) if (part.IsA("BasePart")) i++;
        Server.ChatHook.sendServerMessage("Part count: " + i);
    })
    .setPermissionLevel(4);


import { OnoeNum } from "@rbxts/serikanum";
import { Server } from "shared/api/APIExpose";
import Command from "shared/commands/Command";
import { CURRENCY_DETAILS } from "shared/currency/CurrencyDetails";

export = new Command(script.Name)
    .addAlias("ecoset")
    .setDescription(
        "<currency> <first> <second> : Set balance for a currency. You can type _ as a replacement for spaces.",
    )
    .setExecute((_o, currency, first, second) => {
        const amount =
            second === undefined
                ? new OnoeNum(tonumber(first) ?? 0)
                : OnoeNum.fromSerika(tonumber(first) ?? 0, tonumber(second) ?? 0);

        if (currency === "all") {
            for (const [c, _] of pairs(CURRENCY_DETAILS)) {
                Server.Currency.set(c as Currency, amount);
                Server.empireData.mostCurrencies.set(c as Currency, amount);
            }
            return;
        }

        currency = currency.gsub("_", " ")[0];
        if (CURRENCY_DETAILS[currency as Currency] === undefined) return;
        Server.Currency.set(currency as Currency, amount);
        Server.empireData.mostCurrencies.set(currency as Currency, amount);
    })
    .setPermissionLevel(4);


import { Players } from "@rbxts/services";
import { Server } from "shared/api/APIExpose";
import Command from "shared/commands/Command";

export = new Command(script.Name)
    .addAlias("woosh")
    .setDescription("<player> : Weeeeee")
    .setExecute((o, p) => {
        const targets = Server.Command.findPlayers(o, p);
        if (targets.size() < 1) {
            Server.ChatHook.sendPrivateMessage(o, `Could not find matching players ${p}`, "color:255,43,43");
            return;
        }
        const rng = new Random();
        for (const target of targets) {
            const humanoid = target.Character?.FindFirstChildOfClass("Humanoid");
            if (humanoid === undefined) continue;
            const rootPart = humanoid.RootPart!;
            rootPart.PivotTo(rootPart.GetPivot().add(new Vector3(0, 1, 0)));
            rootPart.AssemblyLinearVelocity = rng.NextUnitVector().mul(5000);
            rootPart.AssemblyAngularVelocity = rng.NextUnitVector().mul(5000);
        }
        Server.ChatHook.sendPrivateMessage(o, `Flung players`, "color:138,255,138");
    })
    .setPermissionLevel(4);


import { Workspace } from "@rbxts/services";
import { Server } from "shared/api/APIExpose";
import Command from "shared/commands/Command";

export = new Command(script.Name)
    .addAlias("wipedata")
    .setDescription("Reset all data like no progress was ever made.")
    .setExecute(() => {
        const attempts = ((Workspace.GetAttribute("ResetAttempts") as number | undefined) ?? 0) + 1;
        Workspace.SetAttribute("ResetAttempts", attempts);
        if (attempts === 1)
            Server.ChatHook.sendServerMessage(
                "Are you sure you want to reset your data? Type /resetdata again to confirm.",
            );
        else if (attempts === 2)
            Server.ChatHook.sendServerMessage(
                "Yeah, but are you REALLY sure? Like, REALLY REALLY sure? You can't recover this data once it's gone.",
            );
        else if (attempts === 3)
            Server.ChatHook.sendServerMessage(
                "I'm saying that you gain nothing in return for doing this. Literally nothing.",
            );
        else if (attempts === 4) Server.ChatHook.sendServerMessage("...");
        else if (attempts === 5) Server.ChatHook.sendServerMessage(".....");
        else if (attempts === 6)
            Server.ChatHook.sendServerMessage("If you say so. Type /resetdata 3 more times to confirm.");
        else if (attempts === 7) Server.ChatHook.sendServerMessage("Type /resetdata 2 more times to confirm.");
        else if (attempts === 8) Server.ChatHook.sendServerMessage("Type /resetdata 1 more time to confirm.");
        else if (attempts === 9) {
            Server.ChatHook.sendServerMessage("You have confirmed the data reset.");
            task.delay(2, () => Server.ChatHook.sendServerMessage("This world will cease to exist in 5 seconds."));
            task.delay(4, () => Server.ChatHook.sendServerMessage("Goodbye"));
            task.delay(7, () => {
                const players = Server.Command.findPlayers(undefined as unknown as Player, "all");
                for (const player of players) player.Kick("This world has collapsed.");
            });
        }
    })
    .setPermissionLevel(4);


import { AssetService } from "@rbxts/services";
import { Server } from "shared/api/APIExpose";
import Command from "shared/commands/Command";
import getPlayerBackpack from "shared/hamster/getPlayerBackpack";
import { getPlayerCharacter } from "shared/hamster/getPlayerCharacter";

export = new Command(script.Name)
    .addAlias("rbxt")
    .setDescription(
        "<player> <id> - Gives the specified player a Roblox tool by asset ID. Default ID is the classic Roblox sword.",
    )
    .setExecute((sender, target, providedId) => {
        const assetId = tonumber(providedId) ?? 47433;

        const asset = AssetService.LoadAssetAsync(assetId);

        if (!asset) {
            warn(`Robloxtool command: could not load asset with ID ${assetId}`);
            return;
        }

        const players = Server.Command.findPlayers(sender, target);
        for (const player of players) {
            for (const child of asset.GetChildren()) {
                child.Clone().Parent = getPlayerCharacter(player);
            }
        }
    })
    .setPermissionLevel(4);


import { Lighting } from "@rbxts/services";
import { Server } from "shared/api/APIExpose";
import Command from "shared/commands/Command";

export = new Command(script.Name)
    .addAlias("time")
    .setDescription("<hours> : Set the hours after midnight")
    .setExecute((_o, hours) => {
        Lighting.ClockTime = tonumber(hours) ?? 0;
    })
    .setPermissionLevel(4);


import { ServerStorage, Workspace } from "@rbxts/services";
import Command from "shared/commands/Command";
import { AREAS } from "shared/world/Area";

export = new Command(script.Name)
    .addAlias("apocalypse")
    .setDescription("Brains")
    .setExecute((_o) => {
        const asset = ServerStorage.WaitForChild("Fun").WaitForChild("Zombie") as Model;
        for (let i = 0; i < 15; i++) {
            for (const [_, area] of pairs(AREAS)) {
                const spawnLocation = area.spawnLocationWorldNode?.getInstance();
                if (spawnLocation === undefined) continue;
                const zombie = asset.Clone();
                const humanoid = zombie.FindFirstChildOfClass("Humanoid");
                if (humanoid !== undefined) humanoid.WalkSpeed = math.random(14, 26);
                zombie.PivotTo(spawnLocation.CFrame.add(new Vector3(math.random(-45, 45), 0, math.random(-45, 45))));
                zombie.Parent = Workspace;
            }
        }
    })
    .setPermissionLevel(4);


import { simpleInterval } from "@antivivi/vrldk";
import { Players, StarterGui, TextChatService, Workspace } from "@rbxts/services";
import type ClientItemReplication from "client/components/item/ClientItemReplication";
import { Workspace_Waypoints } from "services";
import { IS_EDIT, IS_SERVER } from "shared/Context";
import eat from "shared/hamster/eat";
import { getPlayerCharacter } from "shared/hamster/getPlayerCharacter";

/**
 * Returns the display name of a humanoid, falling back to the parent name if the display name is empty.
 * @param humanoid The humanoid to get the display name from.
 * @returns The display name of the humanoid, or the parent name if the display name is empty.
 */
export function getDisplayName(humanoid: Humanoid) {
    return humanoid.DisplayName === "" ? (humanoid.Parent?.Name ?? "???") : humanoid.DisplayName;
}

/**
 * Calculates the XP needed to reach the next level based on the current level.
 * @see https://www.desmos.com/calculator/dukqdmjbas for the formula graph
 * @param currentLevel The player's current level.
 * @returns The maximum XP required to reach the next level.
 */
export function getMaxXp(currentLevel: number) {
    return math.floor((math.pow(1.1, currentLevel + 25) * 80 - 853) / 10 + 0.5) * 10;
}

/**
 * Caches and returns the name associated with a userId.
 * @param userId The userId to get the name for.
 * @returns The name associated with the userId, or "Server" if the userId is undefined.
 */
export function getNameFromUserId(userId: number | undefined) {
    if (userId === undefined) return "Server";
    const name = NAMES_PER_USER_ID.get(userId);
    if (name !== undefined) {
        return name;
    }
    const [success, value] = pcall(() => Players.GetNameFromUserIdAsync(userId));
    if (!success) {
        return "no name";
    }
    NAMES_PER_USER_ID.set(userId, value);
    return value;
}

/** The map storing names associated with userIds */
export const NAMES_PER_USER_ID = new Map<number, string>();

/** The camera instance */
export const CAMERA = Workspace.WaitForChild("Camera") as Camera;

/**
 * The folder where all placed items are stored.
 *
 * Note that the contents of this folder does **not** replicate to clients automatically.
 * See how placed items are manually replicated in {@link ClientItemReplication.useManualItemReplication}.
 */
export const PLACED_ITEMS_FOLDER = (() => {
    const key = "PlacedItems";
    let folder = CAMERA.FindFirstChild(key) as Folder | undefined;
    if (folder === undefined) {
        folder = new Instance("Folder");
        folder.Name = key;
        folder.Parent = CAMERA;
    }
    eat(folder, "Destroy");
    return folder;
})();

/**
 * Returns the folder automatically created containing all the text channels.
 * This is a function to prevent yielding when running tests.
 */
export const getTextChannels = () => TextChatService.WaitForChild("TextChannels", 5) as Folder;

/** The folder containing all waypoints. */
export const WAYPOINTS = Workspace.WaitForChild("Waypoints") as Workspace_Waypoints;

/**
 * Reference to the local player.
 */
export const LOCAL_PLAYER = Players.LocalPlayer as Player | undefined;

/**
 * Observes changes to the local player's character and invokes the provided callback when it changes.
 * Also invokes the callback immediately if the character is already loaded.
 * @param callback Function to call with the new character model when it changes.
 * @returns A function to disconnect the observer.
 */
export function observeCharacter(callback: (character: Model) => void) {
    if (IS_EDIT) {
        let dummy: Model | undefined;
        const cleanup = simpleInterval(() => {
            const newDummy = getPlayerCharacter();
            if (newDummy !== dummy) {
                dummy = newDummy;
                if (dummy) callback(dummy);
            }
        }, 1);
        return cleanup;
    }
    if (LOCAL_PLAYER) {
        const connection = LOCAL_PLAYER.CharacterAdded.Connect(callback);
        if (LOCAL_PLAYER.Character) {
            callback(LOCAL_PLAYER.Character);
        }
        return () => connection.Disconnect();
    } else {
        return () => {};
    }
}
/**
 * Reference to the local player's PlayerGui.
 * *This constant should not be used on the server side.*
 */
export const PLAYER_GUI = (() => {
    if (IS_SERVER && !IS_EDIT) {
        return undefined as never;
    }
    return (IS_EDIT ? StarterGui : LOCAL_PLAYER!.WaitForChild("PlayerGui")) as StarterGui;
})();


//!native
//!optimize 2

import { buildRichText } from "@antivivi/vrldk";
import { BaseOnoeNum, OnoeNum } from "@rbxts/serikanum";
import StringBuilder from "@rbxts/stringbuilder";
import { CURRENCIES, CURRENCY_CATEGORIES, CURRENCY_DETAILS } from "shared/currency/CurrencyDetails";
import CurrencyMap from "shared/currency/CurrencyMap";

declare global {
    type PropertyKey = string | number | symbol;
    type _Combine<T, K extends PropertyKey = T extends unknown ? keyof T : never> = T extends unknown
        ? T & Partial<Record<Exclude<K, keyof T>, never>>
        : never;
    type Combine<T> = { [K in keyof _Combine<T>]: _Combine<T>[K] };

    type Balance = { [currency in Currency]: number };
}

const ONE = new OnoeNum(1);
const ZERO = new OnoeNum(0);

/**
 * Utility class for combining multiple currencies into a single instance.
 *
 * Usually helpful in keeping track and applying formulas on. An example would be a simple droplet upgrader below:
 *
 * @example
 * ```ts
 * let dropletValue = new CurrencyBundle().set("Funds", 50); // Initial droplet value
 * dropletValue = dropletValue.mul(2); // Multiply all currencies by 2
 * dropletValue = dropletValue.mul(new CurrencyBundle().set("Funds", 2)); // Only multiply Funds by 2
 * return dropletValue; // Pass the value to be used in other modules, e.g. adding to balance
 * ```
 */
class CurrencyBundle {
    /**
     * Sorted array of currency details based on their layout order.
     * Used for string formatting.
     */
    static readonly SORTED_DETAILS = (() => {
        const array = new Array<[Currency, CurrencyDetails]>();
        for (const [currency, details] of pairs(CURRENCY_DETAILS)) array.push([currency, details]);
        return array.sort((a, b) => a[1].layoutOrder < b[1].layoutOrder);
    })();

    /**
     * Returns the page name for the specified index.
     *
     * @param page Index number for page
     * @returns Page name in string
     */
    static getCategory(page: number) {
        for (const [c, p] of pairs(CURRENCY_CATEGORIES)) {
            if (page === p) {
                return c;
            }
        }
        return undefined;
    }

    /**
     * Formats the specified amount using the specified currency's format string.
     * If no format is available for the currency, it will instead default to the format ```"<value> <currency>"```.
     * ```ts
     * print(CurrencyBundle.getFormatted("Funds", new OnoeNum(5120))); // Output: $5.12K
     * ```
     *
     * @param currency Currency to use to format
     * @param amount Amount to format
     * @param excludeName Whether to exclude ```<currency>``` in the default format. If left blank, does not exclude
     * @returns Formatted string version of the amount
     */
    static getFormatted(currency: Currency, amount?: BaseOnoeNum | number, excludeName?: boolean) {
        const format = CURRENCY_DETAILS[currency].format;
        const c = amount === undefined ? "" : OnoeNum.toString(amount);
        if (format !== undefined) return format.format(c);
        return excludeName === true ? c : `${c} ${currency}`;
    }

    /**
     * Concatenates all formatted amounts in the instance to a human-readable string that can be used in displaying the prices of items, droplet boosts, etc.
     *
     * Skips currencies that belong to the Internal category.
     *
     * @param amountPerCurrency A map of currency to amount
     * @param isColored Whether to color the output based on currency details
     * @param prefix A string to add before each amount
     * @param suffix A string to add after each amount
     * @returns A formatted string of all amounts
     */
    static currenciesToString(
        amountPerCurrency: BaseCurrencyMap,
        isColored?: boolean,
        prefix?: string,
        suffix?: string,
    ) {
        const builder = new StringBuilder();
        let i = 1;
        const size = amountPerCurrency.size();
        const last = size - 1;
        for (const [name, details] of CurrencyBundle.SORTED_DETAILS) {
            if (details.page === CURRENCY_CATEGORIES.Internal) continue;

            const amount = amountPerCurrency.get(name);
            if (amount !== undefined) {
                let text = CurrencyBundle.getFormatted(name, amount);
                if (prefix !== undefined) text = prefix + text;
                if (suffix !== undefined) text += suffix;

                if (isColored === true && details !== undefined) {
                    buildRichText(builder, text, details.color, undefined, "Bold");
                } else {
                    builder.append(text);
                }

                if (i < size) {
                    builder.append(i === last ? " and " : ", ");
                }
                i++;
            }
        }
        return builder.toString();
    }

    /**
     * Creates a {@link CurrencyBundle} instance with amounts of one for each currency.
     */
    static ones() {
        const ONES = new CurrencyBundle();
        for (const currency of CURRENCIES) {
            ONES.set(currency, 1);
        }
        return ONES;
    }

    /**
     * A {@link CurrencyBundle} instance with amounts of one for each currency.
     * @deprecated Use {@link CurrencyBundle.ones} instead.
     */
    static readonly ONES = CurrencyBundle.ones();

    /**
     * The amounts of each currency in the instance.
     */
    amountPerCurrency: CurrencyMap;

    /**
     * Creates a new CurrencyBundle instance with the specified amounts.
     *
     * @param amountPerCurrency A map of currency to amount. If undefined, the instance will be empty.
     * @param inplace Whether to not clone {@link amountPerCurrency} when creating the instance, therefore allowing direct modification.
     */
    constructor(amountPerCurrency?: CurrencyMap, inplace?: boolean) {
        if (amountPerCurrency !== undefined) {
            if (inplace === true) {
                this.amountPerCurrency = amountPerCurrency;
            } else {
                const cloned = new Map<Currency, OnoeNum>();
                new Map();
                for (const [currency, amount] of amountPerCurrency) {
                    cloned.set(currency, new OnoeNum(amount));
                }
                this.amountPerCurrency = cloned;
            }
        } else {
            this.amountPerCurrency = new Map();
        }
    }

    /**
     * Retrieves the first currency and its amount in the instance.
     * Use this if absolutely certain that there is only one currency in the instance, since {@link Map} does not guarantee order.
     *
     * @returns A tuple containing the currency and its amount, or undefined if none exists.
     */
    getFirst(): LuaTuple<[Currency | undefined, OnoeNum | undefined]> {
        for (const [currency, amount] of this.amountPerCurrency) {
            return $tuple(currency, amount);
        }
        return $tuple(undefined, undefined);
    }

    /**
     * Retrieves the amount of the specified currency in the instance.
     *
     * @param currency The currency to retrieve the amount of
     * @returns The amount of the specified currency. If the currency does not exist, returns undefined.
     */
    get(currency: Currency) {
        return this.amountPerCurrency.get(currency);
    }

    /**
     * Sets the amount for the specified currency in the instance.
     *
     * @param currency The currency to set the amount for
     * @param amount The amount to set, can be an OnoeNum or a number
     * @returns The CurrencyBundle instance for chaining
     */
    set(currency: Currency, amount?: OnoeNum | number) {
        if (amount === undefined) {
            this.amountPerCurrency.delete(currency);
            return this;
        }
        this.amountPerCurrency.set(currency, typeIs(amount, "number") ? new OnoeNum(amount) : (amount as OnoeNum));
        return this;
    }

    /**
     * Checks if all currency amounts in the instance are equal to the specified currency amounts.
     *
     * @param value The currency amounts to check against.
     * @returns True if all amounts are equal, false otherwise.
     */
    equals(value: CurrencyBundle) {
        let equal = true;
        const otherSize = value.amountPerCurrency.size();
        let size = 0;
        for (const [currency, amount] of this.amountPerCurrency) {
            const other = value.get(currency);
            if (other === undefined || !amount.equals(other)) {
                equal = false;
                break;
            }
            size++;
        }
        return equal && size === otherSize;
    }

    /**
     * Concatenates all formatted amounts in the instance to a human-readable string that can be used
     * in displaying the prices of items, droplet boosts, etc.
     * Note that the order in which currencies are shown is determined based on their layout order.
     * ```
     * print(new CurrencyBundle().set("Funds", 100).set("Power", 5).toString()) // Output: $100, 5 W
     * ```
     *
     * @param currency A currency to format into a string. Omit this to format all currencies in the instance.
     * @param amount The amount of the specified currency. Omit this to use the amount in the instance instead.
     * @returns A formatted huamn-readable string of the CurrencyBundle instance
     */
    toString(isColored?: boolean, prefix?: string, suffix?: string) {
        return CurrencyBundle.currenciesToString(this.amountPerCurrency, isColored, prefix, suffix);
    }

    /**
     * Addition operator for adding two CurrencyBundle instances together.
     *
     * @param value The currencies to add.
     * @param inplace Whether to add the amounts to the instance itself.
     * @returns CurrencyBundle instance with the added amounts.
     */
    add(value: CurrencyBundle, inplace?: boolean) {
        const result = CurrencyMap.add(this.amountPerCurrency, value.amountPerCurrency, inplace);
        return inplace === true ? this : new CurrencyBundle(result, true);
    }

    /**
     * Subtraction operator for subtracting two CurrencyBundle instances.
     *
     * @param value The currencies to subtract.
     * @param inplace Whether to subtract the amounts from the instance itself.
     * @returns A new CurrencyBundle instance with the subtracted amounts.
     */
    sub(value: CurrencyBundle, inplace?: boolean) {
        const result = CurrencyMap.sub(this.amountPerCurrency, value.amountPerCurrency, inplace);
        return inplace === true ? this : new CurrencyBundle(result, true);
    }

    /**
     * Multiplies the instance by the specified value.
     *
     * @param value The value to multiply by.
     * @param inplace Whether to multiply the amounts in the instance itself.
     * @returns A new CurrencyBundle instance with the multiplied amounts.
     */
    mul(value: CurrencyBundle, inplace?: boolean) {
        const result = CurrencyMap.mul(this.amountPerCurrency, value.amountPerCurrency, inplace);
        return inplace === true ? this : new CurrencyBundle(result, true);
    }

    /**
     * Multiplies the instance by a constant value.
     * @example
     * ```ts
     * let bundle = new CurrencyBundle().set("Funds", 100).set("Power", 5);
     * bundle = bundle.mulConstant(2); // Now bundle has 200 Funds and 10 Power
     * ```
     * @param value The constant value to multiply by.
     * @param inplace Whether to multiply the amounts in the instance itself.
     * @returns A new CurrencyBundle instance with the multiplied amounts.
     */
    mulConstant(value: number | BaseOnoeNum, inplace?: boolean) {
        const result = CurrencyMap.mulConstant(this.amountPerCurrency, new OnoeNum(value), inplace);
        return inplace === true ? this : new CurrencyBundle(result, true);
    }

    /**
     * Divides the instance by the specified value.
     *
     * @param value The value to divide by.
     * @param inplace Whether to divide the amounts in the instance itself.
     * @returns A new CurrencyBundle instance with the divided amounts.
     */
    div(value: CurrencyBundle, inplace?: boolean) {
        const result = CurrencyMap.div(this.amountPerCurrency, value.amountPerCurrency, inplace);
        return inplace === true ? this : new CurrencyBundle(result, true);
    }

    /**
     * Divides the instance by a constant value.
     * @example
     * ```ts
     * let bundle = new CurrencyBundle().set("Funds", 100).set("Power", 5);
     * bundle = bundle.divConstant(2); // Now bundle has 50 Funds and 2.5 Power
     * ```
     * @param value The constant value to divide by.
     * @param inplace Whether to divide the amounts in the instance itself.
     * @returns A new CurrencyBundle instance with the divided amounts.
     */
    divConstant(value: number | BaseOnoeNum, inplace?: boolean) {
        const result = CurrencyMap.divConstant(this.amountPerCurrency, new OnoeNum(value), inplace);
        return inplace === true ? this : new CurrencyBundle(result, true);
    }

    /**
     * Raises the instance to the power of the specified value.
     * @param value The value to raise to.
     * @param inplace Whether to raise the amounts in the instance itself.
     * @returns A new CurrencyBundle instance with the powered amounts.
     */
    pow(value: CurrencyBundle, inplace?: boolean) {
        const result = CurrencyMap.pow(this.amountPerCurrency, value.amountPerCurrency, inplace);
        return inplace === true ? this : new CurrencyBundle(result, true);
    }

    /**
     * Raises the instance to the power of a constant value.
     * @example
     * ```ts
     * let bundle = new CurrencyBundle().set("Funds", 100).set("Power", 5);
     * bundle = bundle.powConstant(2); // Now bundle has 10000 Funds and 25 Power
     * ```
     * @param value The constant value to raise to.
     * @param inplace Whether to raise the amounts in the instance itself.
     * @returns A new CurrencyBundle instance with the powered amounts.
     */
    powConstant(value: number | BaseOnoeNum, inplace?: boolean) {
        const result = CurrencyMap.powConstant(this.amountPerCurrency, new OnoeNum(value), inplace);
        return inplace === true ? this : new CurrencyBundle(result, true);
    }

    /**
     * Checks if the instance can afford the specified required currency amounts.
     *
     * @param required The required currency amounts to check against.
     * @param result The result to store the remaining currency amounts after subtraction. If undefined, a new CurrencyBundle instance will be created.
     * @returns True if the instance can afford the required amounts, false otherwise.
     */
    canAfford(required: BaseCurrencyMap, result?: CurrencyMap) {
        return CurrencyMap.canAfford(this.amountPerCurrency, required, result);
    }

    /**
     * Checks if the instance has all currency amounts not equal to zero.
     *
     * @returns True if all currency amounts are not zero, false otherwise.
     */
    hasAll() {
        for (const currency of CURRENCIES) {
            const amount = this.get(currency);
            if (amount === undefined || amount.equals(ZERO)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Creates a clone of the instance.
     *
     * @returns A new CurrencyBundle instance with the same amounts.
     */
    clone() {
        return new CurrencyBundle(this.amountPerCurrency);
    }
}

export = CurrencyBundle;


//!native
//!optimize 2
import { OnoeNum } from "@rbxts/serikanum";

type Number = number | OnoeNum;
type Operation = {
    type: number;
    amount?: OnoeNum;
    base?: number;
};

const ZERO = new OnoeNum(0);
const HALF = new OnoeNum(1 / 2);
const ONETHIRD = new OnoeNum(1 / 3);
const E = math.exp(1);

// Operation types as bit flags
const OPERATION_ADD = 0x0001;
const OPERATION_SUB = 0x0002;
const OPERATION_MUL = 0x0004;
const OPERATION_DIV = 0x0008;
const OPERATION_POW = 0x0010;
const OPERATION_SQRT = 0x0020;
const OPERATION_CBRT = 0x0040;
const OPERATION_LOG = 0x0080;
const OPERATION_LN = 0x0100;

/**
 * Represents a chainable mathematical formula composed of operations.
 * Supports addition, subtraction, multiplication, division, powers, roots, and logarithms.
 */
class Formula {
    operations = new Array<Operation>();

    constructor() {}

    /**
     * Adds a value to the formula.
     * @param number The value to add.
     * @returns This Formula instance.
     */
    add(number: Number) {
        this.operations.push({
            type: OPERATION_ADD,
            amount: new OnoeNum(number),
        });
        return this;
    }

    /**
     * Subtracts a value from the formula.
     * @param number The value to subtract.
     * @returns This Formula instance.
     */
    sub(number: Number) {
        this.operations.push({
            type: OPERATION_SUB,
            amount: new OnoeNum(number),
        });
        return this;
    }

    /**
     * Multiplies the formula by a value.
     * @param number The value to multiply by.
     * @returns This Formula instance.
     */
    mul(number: Number) {
        this.operations.push({
            type: OPERATION_MUL,
            amount: new OnoeNum(number),
        });
        return this;
    }

    /**
     * Divides the formula by a value.
     * @param number The value to divide by.
     * @returns This Formula instance.
     */
    div(number: Number) {
        this.operations.push({
            type: OPERATION_DIV,
            amount: new OnoeNum(number),
        });
        return this;
    }

    /**
     * Raises the formula to the power of a value.
     * @param number The exponent.
     * @returns This Formula instance.
     */
    pow(number: Number) {
        this.operations.push({
            type: OPERATION_POW,
            amount: new OnoeNum(number),
        });
        return this;
    }

    /**
     * Applies a square root to the formula.
     * @returns This Formula instance.
     */
    sqrt() {
        this.operations.push({ type: OPERATION_SQRT });
        return this;
    }

    /**
     * Applies a logarithm with the specified base to the formula.
     * @param number The base of the logarithm.
     * @returns This Formula instance.
     */
    log(number: number) {
        this.operations.push({
            type: OPERATION_LOG,
            base: number,
        });
        return this;
    }

    /**
     * Applies a natural logarithm (ln) to the formula.
     * @returns This Formula instance.
     */
    ln() {
        this.operations.push({
            type: OPERATION_LN,
        });
        return this;
    }

    /**
     * Applies the formula's operations to a given OnoeNum value.
     * @param number The input value.
     * @returns The result after applying all operations.
     */
    evaluate(number: OnoeNum) {
        for (const operation of this.operations) {
            switch (operation.type) {
                case OPERATION_ADD:
                    number = number.add(operation.amount!);
                    break;
                case OPERATION_SUB:
                    number = number.sub(operation.amount!);
                    break;
                case OPERATION_MUL:
                    number = number.mul(operation.amount!);
                    break;
                case OPERATION_DIV:
                    number = number.div(operation.amount!);
                    break;
                case OPERATION_POW:
                    number = number.pow(operation.amount!);
                    break;
                case OPERATION_SQRT:
                    number = number.pow(HALF);
                    break;
                case OPERATION_CBRT:
                    number = number.pow(ONETHIRD);
                    break;
                case OPERATION_LOG:
                    if (number.lessEquals(ZERO)) return ZERO; // Prefer 0 over NaN
                    number = OnoeNum.log(number, operation.base!) ?? ZERO;
                    break;
                case OPERATION_LN:
                    if (number.lessEquals(ZERO)) return ZERO;
                    number = OnoeNum.log(number, E) ?? ZERO;
                    break;
            }
        }
        return number;
    }

    /**
     * Returns a string representation of the formula, using the provided variable name.
     * @param nameOfX The variable name to use (e.g., "x").
     * @param subFontSize The font size for logarithm bases in the string representation.
     * @returns The formula as a string.
     */
    tostring(nameOfX: string, subFontSize = 16) {
        let lastOperator: number | undefined;
        for (const operation of this.operations) {
            switch (operation.type) {
                case OPERATION_ADD:
                    nameOfX += " + " + operation.amount;
                    break;
                case OPERATION_SUB:
                    nameOfX += " - " + operation.amount;
                    break;
                case OPERATION_MUL:
                    nameOfX += " * " + operation.amount;
                    break;
                case OPERATION_DIV:
                    nameOfX += " / " + operation.amount;
                    break;
                case OPERATION_POW:
                    if (
                        lastOperator === OPERATION_ADD ||
                        lastOperator === OPERATION_SUB ||
                        lastOperator === OPERATION_MUL ||
                        lastOperator === OPERATION_DIV
                    ) {
                        nameOfX = "(" + nameOfX + ") ^ " + operation.amount;
                    } else {
                        nameOfX += " ^ " + operation.amount;
                    }
                    break;
                case OPERATION_SQRT:
                    nameOfX = "√(" + nameOfX + ")";
                    break;
                case OPERATION_LOG:
                    nameOfX = `log<font size='${subFontSize}'>${operation.base}</font>(${nameOfX})`;
                    break;
                case OPERATION_LN:
                    nameOfX = "ln(" + nameOfX + ")";
                    break;
            }
            lastOperator = operation.type;
        }
        return nameOfX;
    }
}

export = Formula;


import { OnoeNum } from "@rbxts/serikanum";

type QueueLog = {
    amount: OnoeNum;
    time: number;
};

const ZERO = new OnoeNum(0);

/**
 * Queue class to store the last 20 recorded values and their time of recording.
 * It is used to calculate the average gain over the last 20 values.
 */
class Queue {
    /**
     * The last recorded value and its time of recording.
     */
    previousLog: QueueLog | undefined = undefined;

    /**
     * The last 20 recorded values and their time of recording.
     */
    calculations = new Array<QueueLog>();

    /**
     * Record a new value with the current time, calculating the delta from the previous value.
     * If this is the first value, it will be stored without a delta.
     *
     * @param amount The value to record.
     */
    addDelta(amount: OnoeNum) {
        const log = {
            amount: amount,
            time: tick(),
        };
        if (this.previousLog === undefined) {
            this.previousLog = log;
            return;
        }
        const newAmount = amount.sub(this.previousLog.amount);
        if (newAmount.equals(ZERO)) return;

        this.calculations.push({
            amount: newAmount,
            time: log.time - this.previousLog.time,
        });
        if (this.calculations.size() > 20) {
            this.calculations.remove(0);
        }
        this.previousLog = log;
    }

    /**
     * Get the average gain per second over the last 20 values.
     *
     * @returns The average gain as an OnoeNum.
     */
    meanDelta() {
        let average = new OnoeNum(0);
        if (this.previousLog === undefined || tick() - this.previousLog.time > 10) return average;
        let i = 0;
        for (const calculation of this.calculations) {
            ++i;
            average = average.add(calculation.amount.div(calculation.time));
        }
        if (i > 1) {
            average = average.div(i);
        }
        return average;
    }

    /**
     * Record a new value.
     *
     * @param amount The value to record.
     */
    add(amount: OnoeNum) {
        const log = {
            amount: amount,
            time: tick(),
        };
        this.calculations.push(log);
        if (this.calculations.size() > 20) {
            this.calculations.remove(0);
        }
    }

    /**
     * Get the mean of the recorded values.
     *
     * @returns The mean as an OnoeNum.
     */
    mean() {
        let mean = new OnoeNum(0);
        let size = 0;
        for (const calculation of this.calculations) {
            mean = mean.add(calculation.amount);
            size++;
        }
        if (size === 0) return new OnoeNum(0);
        return mean.div(size);
    }
}

export = Queue;


import { OnoeNum } from "@rbxts/serikanum";
import Formula from "shared/currency/Formula";
import SkillificationZone from "shared/world/nodes/SkillificationZone";
import WinificationZone from "shared/world/nodes/WinificationZone";

declare global {
    type ResetLayerId = keyof typeof RESET_LAYERS;
    type ResetLayer = (typeof RESET_LAYERS)[ResetLayerId];
}

export const RESET_LAYERS = {
    Skillification: {
        order: 1,
        area: "BarrenIslands" as AreaId,
        formula: new Formula().add(1).div(1e12).log(16).pow(1.8).add(1),
        minimum: new OnoeNum(1e12),
        scalesWith: "Power" as Currency,
        gives: "Skill" as Currency,
        resettingCurrencies: ["Funds", "Power", "Purifier Clicks"] as Currency[],
        resettingUpgrades: ["MoreFunds", "MorePower"],
        touchPart: SkillificationZone,
        badgeId: 2011792716581358, // TODO: change badge
    },
    Winification: {
        order: 2,
        area: "SlamoVillage" as AreaId,
        formula: new Formula().add(1).div(1e12).log(18).pow(1.7).add(1),
        minimum: new OnoeNum(1e12),
        scalesWith: "Skill" as Currency,
        gives: "Wins" as Currency,
        resettingCurrencies: ["Skill", "Bitcoin", "Dark Matter"] as Currency[],
        resettingUpgrades: [
            "CryptographicFunds",
            "CryptographicPower",
            "SkilledMining",
            "ArtOfPurification",
            "DarkerMatter",
            "EfficientLearning",
        ],
        touchPart: WinificationZone,
        badgeId: 838386835884859, // TODO: change badge
    },
};


import Difficulty from "@rbxts/ejt";
import Item from "shared/item/Item";
import countItemEverywhere from "shared/item/utils/countItemEverywhere";
import Items from "shared/items/Items";

const maxItemAmounts = new Map<string, number>();

/**
 * Fix duped items and bad bought amounts.
 * @param items Data to fix.
 * @returns Fixed data.
 */
export default function fixDuplicatedItemsData(items: ItemsData) {
    Items.itemsPerId.forEach((item: Item) => {
        if (item.defaultPrice !== undefined)
            // buy limit is uncapped, dont check
            return;

        const itemId = item.id;
        const [invCount, placedCount] = countItemEverywhere(items.inventory, items.worldPlaced, itemId);
        const totalCount = invCount + placedCount;

        let max = maxItemAmounts.get(itemId);
        if (max === undefined) {
            max = -1;
            for (const [amount, _] of item.pricePerIteration) if (amount > max) max = amount;

            maxItemAmounts.set(itemId, max);
        }

        if (max === -1 || totalCount <= max) return;

        // this is the point where there are clearly more items than allowed. remove the excess
        const diff = totalCount - max;
        const fromInvCount = math.min(diff, invCount);
        warn("Removing", fromInvCount, itemId);
        items.inventory.set(itemId, invCount - fromInvCount);

        const remaining = diff - fromInvCount;
        if (remaining > 0) {
            // if there isnt enough in inventory to remove, remove from placed items
            print("Removing", remaining, itemId, "from placed items");
            let removed = 0;
            for (const [placementId, placedItem] of items.worldPlaced) {
                if (placedItem.item === itemId) {
                    items.worldPlaced.delete(placementId);
                    if (++removed >= remaining) {
                        return;
                    }
                }
            }
        }
    });

    // fix bad bought
    const addAmount = (list: Map<string, number>, itemId: string, amount: number) => {
        list.set(itemId, (list.get(itemId) ?? 0) + amount);
    };

    const baseAmounts = new Map<string, number>();
    for (const [_, placedItem] of items.worldPlaced) addAmount(baseAmounts, placedItem.item, 1);
    for (const [itemId, amount] of items.inventory) addAmount(baseAmounts, itemId, amount);

    const nestCheck = (base: Map<string, number>, item: Item, amount?: number) => {
        if (amount === undefined) return;
        for (const [subItemId, requiredAmount] of item.requiredItems) {
            const totalAmount = requiredAmount * amount;
            addAmount(base, subItemId, totalAmount);
            const subItem = Items.getItem(subItemId);
            if (subItem !== undefined) {
                nestCheck(base, subItem, totalAmount);
            }
        }
    };
    const addedAmounts = new Map<string, number>();
    for (const [itemId, item] of Items.itemsPerId) {
        nestCheck(addedAmounts, item, baseAmounts.get(itemId));
    }
    for (const [itemId, item] of Items.itemsPerId) {
        if (
            item.isA("Gear") ||
            item.pricePerIteration.size() === 0 ||
            item.difficulty === Difficulty.Excavation ||
            item.isA("Unique")
        )
            continue;

        const amount = (addedAmounts.get(itemId) ?? 0) + (baseAmounts.get(itemId) ?? 0);
        if (amount < 0) continue;

        const cached = items.bought.get(itemId) ?? 0;
        if (cached !== amount) {
            warn(itemId, "has", cached, "bought, found", amount);
            items.bought.set(itemId, amount);
        }
    }
}