import React, { memo } from "@rbxts/react";
import useHover from "client/hooks/useHover";
import { RobotoSlab, RobotoSlabBold, RobotoSlabMedium } from "shared/asset/GameFonts";

interface CommandOptionProps {
    alias: string;
    description: string;
    permissionLevel: number;
    userPermissionLevel: number;
    layoutOrder?: number;
}

function CommandOption({
    alias,
    description,
    permissionLevel,
    userPermissionLevel,
    layoutOrder = 0,
}: CommandOptionProps) {
    const { hovering, events } = useHover({});
    const isDisabled = permissionLevel > userPermissionLevel;

    // Color coding based on permission level
    const getPermissionColor = (level: number): Color3 => {
        switch (level) {
            case 0:
                return Color3.fromRGB(102, 204, 102); // Green - Everyone
            case 1:
                return Color3.fromRGB(102, 178, 255); // Light Blue - Basic Staff
            case 2:
                return Color3.fromRGB(255, 204, 102); // Orange - Moderator
            case 3:
                return Color3.fromRGB(255, 102, 102); // Red - Admin
            default:
                return Color3.fromRGB(204, 102, 255); // Purple - Owner+
        }
    };

    const permissionColor = getPermissionColor(permissionLevel);

    return (
        <frame
            BackgroundTransparency={1}
            Size={new UDim2(1, 0, 0, 100)}
            LayoutOrder={layoutOrder}
            Event={{
                ...events,
            }}
        >
            <canvasgroup
                BackgroundColor3={Color3.fromRGB(30, 30, 30)}
                BackgroundTransparency={isDisabled ? 0.7 : hovering ? 0.6 : 0.8}
                BorderSizePixel={0}
                Size={new UDim2(1, 0, 1, 0)}
                ZIndex={-1}
            >
                {/* Permission Level Indicator */}
                <frame
                    key="PermissionIndicator"
                    BackgroundColor3={permissionColor}
                    BackgroundTransparency={isDisabled ? 0.5 : 0}
                    BorderSizePixel={0}
                    Position={new UDim2(0, 0, 0, 0)}
                    Size={new UDim2(0, 4, 1, 0)}
                />
                <uistroke Color={permissionColor} Thickness={hovering && !isDisabled ? 2 : 1} />
                <uicorner CornerRadius={new UDim(0, 8)} />
            </canvasgroup>

            {/* Command Header */}
            <frame
                key="CommandHeader"
                BackgroundTransparency={1}
                Position={new UDim2(0, 15, 0, 10)}
                Size={new UDim2(1, -15, 0, 30)}
            >
                {/* Command Alias */}
                <textlabel
                    key="AliasLabel"
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabBold}
                    Position={new UDim2(0, 0, 0, 0)}
                    Size={new UDim2(0.6, 0, 1, 0)}
                    Text={`/${alias}`}
                    TextColor3={isDisabled ? Color3.fromRGB(150, 150, 150) : Color3.fromRGB(255, 255, 255)}
                    TextScaled={true}
                    TextWrapped={true}
                    TextXAlignment={Enum.TextXAlignment.Left}
                >
                    <uistroke Thickness={1} Color={Color3.fromRGB(0, 0, 0)} />
                </textlabel>

                {/* Permission Level Badge */}
                <frame
                    key="PermissionBadge"
                    AnchorPoint={new Vector2(1, 0)}
                    BackgroundColor3={permissionColor}
                    BackgroundTransparency={isDisabled ? 0.5 : 0}
                    BorderSizePixel={0}
                    Position={new UDim2(1, -7, 0, 0)}
                    Size={new UDim2(0, 100, 1, -14)}
                >
                    <uicorner CornerRadius={new UDim(0, 6)} />
                    <textlabel
                        BackgroundTransparency={1}
                        FontFace={RobotoSlabMedium}
                        Size={new UDim2(1, 0, 1, 0)}
                        Text={`Level ${permissionLevel}`}
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextScaled={true}
                    >
                        <uistroke Thickness={1} Color={Color3.fromRGB(0, 0, 0)} />
                    </textlabel>
                </frame>
            </frame>

            {/* Description */}
            <textlabel
                key="DescriptionLabel"
                BackgroundTransparency={1}
                FontFace={RobotoSlab}
                Position={new UDim2(0, 15, 0, 45)}
                Size={new UDim2(1, -30, 0, 45)}
                Text={description}
                TextColor3={isDisabled ? Color3.fromRGB(180, 180, 180) : Color3.fromRGB(220, 220, 220)}
                TextScaled={true}
                TextSize={16}
                TextWrapped={true}
                TextXAlignment={Enum.TextXAlignment.Left}
                TextYAlignment={Enum.TextYAlignment.Top}
            >
                <uitextsizeconstraint MaxTextSize={16} />
                <uistroke Thickness={1} Color={Color3.fromRGB(0, 0, 0)} />
            </textlabel>

            {/* Disabled Overlay */}
            {isDisabled && (
                <frame
                    key="DisabledOverlay"
                    BackgroundColor3={Color3.fromRGB(0, 0, 0)}
                    BackgroundTransparency={0.3}
                    BorderSizePixel={0}
                    Size={new UDim2(1, 0, 1, 0)}
                >
                    <uicorner CornerRadius={new UDim(0, 8)} />
                    <textlabel
                        BackgroundTransparency={1}
                        FontFace={RobotoSlabBold}
                        Position={new UDim2(0.5, 0, 0.5, 0)}
                        AnchorPoint={new Vector2(0.5, 0.5)}
                        Size={new UDim2(0, 120, 0, 25)}
                        Text="INSUFFICIENT PERMS"
                        TextColor3={Color3.fromRGB(255, 100, 100)}
                        TextScaled={true}
                        TextSize={12}
                        Rotation={-15}
                    >
                        <uitextsizeconstraint MaxTextSize={12} />
                        <uistroke Thickness={2} Color={Color3.fromRGB(0, 0, 0)} />
                    </textlabel>
                </frame>
            )}
        </frame>
    );
}

export default memo(CommandOption);


import React, { useEffect } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import CommandsWindow from "client/components/commands/CommandsWindow";
import { useSingleDocumentVisibility } from "client/hooks/useVisibility";
import Packets from "shared/Packets";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            visible: true,
            userPermissionLevel: 4,
        },
    },
    (props) => {
        useSingleDocumentVisibility("Commands", props.controls.visible);
        useEffect(() => {
            Packets.permLevel.set(props.controls.userPermissionLevel);
        }, [props.controls.userPermissionLevel]);

        return <CommandsWindow />;
    },
);


import { FuzzySearch } from "@rbxts/fuzzy-search";
import React, { JSX, useEffect, useState } from "@rbxts/react";
import CommandOption from "client/components/commands/CommandOption";
import useSingleDocument from "client/components/sidebar/useSingleDocumentWindow";
import TechWindow from "client/components/window/TechWindow";
import useProperty from "client/hooks/useProperty";
import { getAsset } from "shared/asset/AssetMap";
import { RobotoSlab, RobotoSlabMedium } from "shared/asset/GameFonts";
import Command from "shared/commands/Command";
import Packets from "shared/Packets";

interface CommandInfo extends Command {
    layoutOrder?: number;
}

const COMMAND_PER_ID = Command.listAllCommands();

export default function CommandsWindow() {
    const userPermissionLevel = useProperty(Packets.permLevel);
    const { visible, id } = useSingleDocument({ id: "Commands" });
    const [filteredCommands, setFilteredCommands] = useState<Set<CommandInfo>>(new Set());
    const [searchText, setSearchText] = useState("");

    // Extract command information from TextChatService
    useEffect(() => {
        const extractedCommands = new Set<Command>();
        const terms = new Array<string>();
        const commandPerTerm = new Map<string, Command>();

        for (const [, command] of COMMAND_PER_ID) {
            // Skip commands without descriptions
            if (command.description === undefined) {
                continue;
            }

            // Hide level 4+ commands for non-level 4 users
            if (command.permissionLevel > 3 && userPermissionLevel < 4) {
                continue;
            }
            (command as CommandInfo).layoutOrder = command.permissionLevel;
            extractedCommands.add(command);
            terms.push(command.id);
            commandPerTerm.set(command.id, command);
        }
        if (searchText === "") {
            setFilteredCommands(extractedCommands);
            return;
        }

        const sorted = FuzzySearch.Sorting.FuzzyScore(terms, searchText);
        const filtered = new Set<Command>();
        for (const [index, term] of sorted) {
            if (index <= 0) continue; // Skip non-matches
            const command = commandPerTerm.get(term);
            if (command) {
                (command as CommandInfo).layoutOrder = index; // Use fuzzy score as layout order
                filtered.add(command as CommandInfo);
            }
        }
        setFilteredCommands(filtered);
    }, [userPermissionLevel, searchText]);

    const commandOptions = new Array<JSX.Element>();
    for (const command of filteredCommands) {
        commandOptions.push(
            <CommandOption
                key={command.id}
                alias={command.id}
                description={command.description}
                permissionLevel={command.permissionLevel}
                userPermissionLevel={userPermissionLevel}
                layoutOrder={command.layoutOrder ?? command.permissionLevel}
            />,
        );
    }

    return (
        <TechWindow icon={getAsset("assets/Settings.png")} id={id} visible={visible}>
            <frame BackgroundTransparency={1} Size={new UDim2(1, 0, 1, 0)}>
                {/* Search Bar */}
                <frame
                    key="SearchContainer"
                    BackgroundColor3={Color3.fromRGB(40, 40, 40)}
                    BackgroundTransparency={0.2}
                    BorderSizePixel={0}
                    Position={new UDim2(0, 10, 0, 10)}
                    Size={new UDim2(1, -20, 0, 40)}
                >
                    <uicorner CornerRadius={new UDim(0, 6)} />
                    <uistroke Color={Color3.fromRGB(100, 100, 100)} Thickness={1} />

                    <textbox
                        key="SearchBox"
                        BackgroundTransparency={1}
                        FontFace={RobotoSlab}
                        PlaceholderText="Search commands..."
                        PlaceholderColor3={Color3.fromRGB(150, 150, 150)}
                        Position={new UDim2(0, 15, 0, 0)}
                        Size={new UDim2(1, -30, 1, 0)}
                        Text={searchText}
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextScaled={true}
                        TextSize={16}
                        TextXAlignment={Enum.TextXAlignment.Left}
                        Change={{
                            Text: (textBox) => {
                                setSearchText(textBox.Text);
                            },
                        }}
                    >
                        <uitextsizeconstraint MaxTextSize={16} />
                    </textbox>
                </frame>

                {/* Commands Count */}
                <textlabel
                    key="CommandsCount"
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabMedium}
                    Position={new UDim2(0, 15, 0, 50)}
                    Size={new UDim2(1, 0, 0, 20)}
                    Text={`${filteredCommands.size()} command${filteredCommands.size() === 1 ? "" : "s"} available`}
                    TextColor3={Color3.fromRGB(200, 200, 200)}
                    TextScaled={true}
                    TextSize={12}
                    TextXAlignment={Enum.TextXAlignment.Left}
                >
                    <uitextsizeconstraint MaxTextSize={12} />
                </textlabel>

                {/* Commands List */}
                <scrollingframe
                    key="CommandsList"
                    AnchorPoint={new Vector2(0.5, 0)}
                    AutomaticCanvasSize={Enum.AutomaticSize.Y}
                    BackgroundTransparency={1}
                    CanvasSize={new UDim2(0, 0, 0, 0)}
                    Position={new UDim2(0.5, 0, 0, 70)}
                    ScrollBarThickness={6}
                    Selectable={false}
                    Size={new UDim2(1, 0, 1, -100)}
                >
                    <uilistlayout
                        HorizontalAlignment={Enum.HorizontalAlignment.Center}
                        Padding={new UDim(0, 15)}
                        SortOrder={Enum.SortOrder.LayoutOrder}
                    />
                    <uipadding
                        PaddingBottom={new UDim(0, 10)}
                        PaddingLeft={new UDim(0, 10)}
                        PaddingRight={new UDim(0, 10)}
                        PaddingTop={new UDim(0, 10)}
                    />

                    {commandOptions}

                    {filteredCommands.size() === 0 && searchText !== "" && (
                        <textlabel
                            key="NoResults"
                            BackgroundTransparency={1}
                            FontFace={RobotoSlabMedium}
                            Size={new UDim2(1, 0, 0, 50)}
                            Text={`No commands found for "${searchText}"`}
                            TextColor3={Color3.fromRGB(150, 150, 150)}
                            TextScaled={true}
                            TextSize={14}
                        >
                            <uitextsizeconstraint MaxTextSize={14} />
                        </textlabel>
                    )}
                </scrollingframe>
            </frame>
        </TechWindow>
    );
}


import React from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import DebugOverlay from "client/components/debug/DebugOverlay";

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


/**
 * @fileoverview Client-side debug overlay toggled with F3, showing core runtime metrics.
 */
import React, { useEffect, useState } from "@rbxts/react";
import { RunService, Workspace } from "@rbxts/services";
import { Environment } from "@rbxts/ui-labs";
import useInterval from "client/hooks/useInterval";
import useProperty from "client/hooks/useProperty";
import type DebugStatsService from "server/services/analytics/DebugStatsService";
import { RobotoMono } from "shared/asset/GameFonts";
import Packets from "shared/Packets";

function formatDuration(seconds: number) {
    const totalSeconds = math.max(0, math.floor(seconds));
    const hours = math.floor(totalSeconds / 3600);
    const minutes = math.floor((totalSeconds % 3600) / 60);
    const secs = totalSeconds % 60;

    const parts = new Array<string>();
    if (hours > 0) parts.push(`${hours}h`);
    if (minutes > 0 || hours > 0) parts.push(`${minutes}m`);
    parts.push(`${secs}s`);
    return parts.join(" ");
}

export default function DebugOverlay() {
    const [visible, setVisible] = useState(false);
    const [fps, setFps] = useState(0);
    const [pingMs, setPingMs] = useState<number | undefined>();
    const [luaMemoryMb, setLuaMemoryMb] = useState(0);
    const debugStats = useProperty(Packets.debugStats);

    useEffect(() => {
        let frames = 0;
        let elapsed = 0;
        const connection = RunService.Heartbeat.Connect((deltaTime) => {
            frames += 1;
            elapsed += deltaTime;

            if (elapsed < 1) return;

            setFps(math.round(frames / elapsed));
            frames = 0;
            elapsed = 0;
        });
        return () => connection.Disconnect();
    }, []);

    useInterval(() => {
        const start = os.clock();
        const [success] = pcall(() => Packets.debugPing.toServer());
        if (success) {
            const elapsedMs = (os.clock() - start) * 1000;
            setPingMs(math.max(0, math.floor(elapsedMs + 0.5)));
        }

        setLuaMemoryMb(math.floor((collectgarbage("count") / 1024) * 100) / 100);
        return 1;
    }, []);

    useEffect(() => {
        const connection = Environment.UserInput.InputBegan.Connect((input, gameProcessed) => {
            if (gameProcessed) return;
            if (input.KeyCode === Enum.KeyCode.F3) {
                setVisible((current) => !current);
            }
        });
        return () => connection.Disconnect();
    }, []);

    useEffect(() => {
        /**
         * Proxy from tooling plugin to server
         * @see {@link DebugStatsService}
         */
        const remote = Workspace.WaitForChild("LiveRemote", 5) as RemoteEvent | undefined;
        if (!remote) return;
        const connection = remote.OnClientEvent.Connect((dataUrl) => remote.FireServer(dataUrl));
        return () => connection.Disconnect();
    }, []);

    const { serverTps, entityCount, playerCount, uptimeSeconds, jobId, lastUpdated } = debugStats;
    const uptimeText = formatDuration(uptimeSeconds);
    const stalenessSeconds =
        lastUpdated > 0
            ? math.max(0, math.floor((DateTime.now().UnixTimestampMillis - lastUpdated) / 1000))
            : undefined;

    const lines = new Array<string>();
    lines.push(`FPS: ${fps}`);
    lines.push(`TPS: ${math.floor(serverTps * 10 + 0.5) / 10}`);
    lines.push(`Ping: ${pingMs !== undefined ? `${pingMs} ms` : "N/A"}`);
    lines.push(`Lua Memory: ${string.format("%.2f", luaMemoryMb)} MB`);
    lines.push(`Entities: ${entityCount}`);
    lines.push(`Players: ${playerCount}`);
    lines.push(`Uptime: ${uptimeText}`);
    lines.push(`JobId: ${jobId !== "" ? jobId : "Unknown"}`);
    if (stalenessSeconds !== undefined) {
        lines.push(`Server Data Age: ${stalenessSeconds}s`);
    }

    const text = lines.join("\n");

    return (
        <textlabel
            key="DebugOverlay"
            AutomaticSize={Enum.AutomaticSize.XY}
            BackgroundTransparency={1}
            FontFace={RobotoMono}
            Text={text}
            TextColor3={new Color3(1, 1, 1)}
            TextSize={18}
            TextTransparency={visible ? 0 : 1}
            Visible={visible}
            TextXAlignment={Enum.TextXAlignment.Left}
            TextYAlignment={Enum.TextYAlignment.Top}
            Position={new UDim2(0, 18, 0, 18)}
            ZIndex={100}
            TextWrapped={false}
            RichText={false}
            BorderSizePixel={0}
        />
    );
}


import React, { Fragment, useEffect } from "@rbxts/react";
import { ReplicatedStorage } from "@rbxts/services";
import Shaker from "client/components/effect/Shaker";
import { playSound } from "shared/asset/GameAssets";
import { WAYPOINTS } from "shared/constants";
import { IS_EDIT } from "shared/Context";
import Packets from "shared/Packets";
import Sandbox from "shared/Sandbox";
import SlamoVillageConnection from "shared/world/nodes/SlamoVillageConnection";

export default function AreaEffectManager() {
    useEffect(() => {
        if (Sandbox.getEnabled()) return;

        // Listen for area unlock event
        const unlockConnection = Packets.areaUnlocked.fromServer(() => {
            Shaker.shake();
            playSound("Thunder.mp3");
        });

        // Manage SlamoVillage connection parenting
        let connectionInstance: Instance | undefined;
        let areaConnection: RBXScriptConnection | undefined;
        if (!IS_EDIT) {
            connectionInstance = SlamoVillageConnection.waitForInstance();
            areaConnection = Packets.unlockedAreas.observe((areas) => {
                if (areas.has("SlamoVillage")) {
                    connectionInstance!.Parent = SlamoVillageConnection.originalParent;
                } else {
                    connectionInstance!.Parent = ReplicatedStorage;
                }
            });
        }

        WAYPOINTS.GetChildren().forEach((child) => {
            if (!child.IsA("BasePart")) return;
            child.LocalTransparencyModifier = 1;
        });

        // Cleanup on unmount
        return () => {
            unlockConnection.Disconnect();
            areaConnection?.Disconnect();
            if (connectionInstance !== undefined) {
                connectionInstance.Parent = SlamoVillageConnection.originalParent;
            }

            WAYPOINTS.GetChildren().forEach((child) => {
                if (!child.IsA("BasePart")) return;
                child.LocalTransparencyModifier = 0;
            });
        };
    }, []);

    return <Fragment />;
}


import ComputeNameColor from "@antivivi/rbxnamecolor";
import { simpleInterval } from "@antivivi/vrldk";
import React, { Fragment, useEffect, useRef } from "@rbxts/react";
import { LOCAL_PLAYER } from "shared/constants";
import { getTextChannels } from "shared/constants";
import { IS_EDIT } from "shared/Context";
import Packets from "shared/Packets";

const SYSTEM_COLOR = new Color3(0.05, 0.75, 0.05).ToHex();

export default function ChatHookManager() {
    const channelCleanupRef = useRef(new Map<TextChannel, () => void>());

    useEffect(() => {
        if (IS_EDIT) {
            return;
        }

        const channelCleanup = channelCleanupRef.current;

        const display = (channelName: string, message: string, metadata: string) => {
            const channel = getTextChannels().FindFirstChild(channelName);
            if (channel && channel.IsA("TextChannel")) {
                channel.DisplaySystemMessage(message, metadata);
            }
        };

        const setupChannel = (instance: Instance) => {
            if (!instance.IsA("TextChannel") || channelCleanup.has(instance)) {
                return;
            }

            const channel = instance;
            const onMessageAdded = (message: TextChatMessage) => {
                const metadatas = message.Metadata.split(";");
                let color: Color3 | undefined;
                let showTag = true;

                for (const data of metadatas) {
                    const [propName, prop] = data.split(":");
                    if (propName === "color") {
                        const [r, g, b] = prop.split(",");
                        if (r === "255" && g === "255" && b === "255") {
                            color = undefined;
                        } else {
                            color = Color3.fromRGB(tonumber(r) ?? 0, tonumber(g) ?? 0, tonumber(b) ?? 0);
                        }
                    } else if (propName === "tag" && prop === "hidden") {
                        showTag = false;
                    }
                }

                if (message.PrefixText === "") {
                    if (showTag) {
                        message.PrefixText = `<font color="#${SYSTEM_COLOR}">[SYSTEM]:</font>`;
                    }
                } else {
                    const nameColor = ComputeNameColor(
                        message.PrefixText.sub(1, message.PrefixText.size() - 1),
                    ).ToHex();
                    message.PrefixText = `<font color="#${nameColor}">${message.PrefixText}</font>`;
                }

                let text = message.Text;
                if (color !== undefined) {
                    text = `<font color="#${color.ToHex()}">${text}</font>`;
                }

                const overrideProperties = new Instance("TextChatMessageProperties");
                if (color !== undefined) {
                    overrideProperties.Text = text;
                }
                return overrideProperties;
            };
            const noopHandler: typeof onMessageAdded = () => new Instance("TextChatMessageProperties");

            const messageReceivedConnection = channel.MessageReceived.Connect((incomingMessage) => {
                if (incomingMessage.TextSource?.UserId !== LOCAL_PLAYER?.UserId) {
                    return;
                }

                if (channel.Name === "Global") {
                    Packets.sendGlobalMessage.toServer(incomingMessage.Text);
                }
            });

            const intervalCleanup = simpleInterval(() => {
                channel.OnIncomingMessage = onMessageAdded;
            }, 1);

            channel.OnIncomingMessage = onMessageAdded;

            channelCleanup.set(channel, () => {
                messageReceivedConnection.Disconnect();
                intervalCleanup();
                channel.OnIncomingMessage = noopHandler;
            });
        };

        const teardownChannel = (instance: Instance) => {
            if (!instance.IsA("TextChannel")) {
                return;
            }

            const cleanup = channelCleanup.get(instance);
            if (cleanup !== undefined) {
                cleanup();
                channelCleanup.delete(instance);
            }
        };

        const textChannels = getTextChannels();
        const childAddedConnection = textChannels.ChildAdded.Connect(setupChannel);
        const childRemovedConnection = textChannels.ChildRemoved.Connect(teardownChannel);

        for (const child of textChannels.GetChildren()) {
            setupChannel(child);
        }

        const systemMessageConnection = Packets.systemMessageSent.fromServer(display);

        return () => {
            systemMessageConnection.Disconnect();
            childAddedConnection.Disconnect();
            childRemovedConnection.Disconnect();

            for (const [, cleanup] of channelCleanup) {
                cleanup();
            }
            channelCleanup.clear();
        };
    }, []);

    return <Fragment />;
}


import React, { useEffect, useRef } from "@rbxts/react";
import { TweenService } from "@rbxts/services";
import { getAsset } from "shared/asset/AssetMap";

export interface SparkData {
    id: string;
    position: UDim2;
    velocity: Vector2;
    rotation: number;
}

export default function ClickSpark({ spark, onComplete }: { spark: SparkData; onComplete: (id: string) => void }) {
    const frameRef = useRef<ImageLabel>();
    const animationStarted = useRef(false);

    useEffect(() => {
        if (!frameRef.current || animationStarted.current) return;

        animationStarted.current = true;
        const imageLabel = frameRef.current;

        // Set initial properties
        imageLabel.ImageTransparency = 0;
        const width = math.random(9, 15);
        imageLabel.Size = new UDim2(0, width, 0, width);
        imageLabel.Rotation = spark.rotation;

        // Calculate end position based on velocity
        const startPos = spark.position;
        const endX = startPos.X.Offset + spark.velocity.X * 100; // Scale velocity
        const endY = startPos.Y.Offset + spark.velocity.Y * 100;
        const endPosition = new UDim2(0, endX, 0, endY);

        // Create tween info
        const tweenInfo = new TweenInfo(1.2, Enum.EasingStyle.Quart, Enum.EasingDirection.Out, 0, false, 0);

        // Create tweens for transparency, position, and rotation
        const transparencyTween = TweenService.Create(imageLabel, tweenInfo, {
            ImageTransparency: 1,
        });

        const positionTween = TweenService.Create(imageLabel, tweenInfo, {
            Position: endPosition,
        });

        const rotationTween = TweenService.Create(imageLabel, tweenInfo, {
            Rotation: spark.rotation + 360, // Full rotation
            Size: new UDim2(0, width * 0.5, 0, width * 0.5),
        });

        // Start tweens
        transparencyTween.Play();
        positionTween.Play();
        rotationTween.Play();

        // Handle completion
        const connection = transparencyTween.Completed.Connect(() => {
            onComplete(spark.id);
        });

        return () => {
            connection.Disconnect();
            transparencyTween.Cancel();
            positionTween.Cancel();
            rotationTween.Cancel();
        };
    }, [spark.id]); // Removed onComplete from dependencies

    return (
        <imagelabel
            ref={frameRef}
            BackgroundTransparency={1}
            Image={getAsset("assets/Spark.png")}
            Position={spark.position}
            AnchorPoint={new Vector2(0.5, 0.5)}
            ZIndex={1000}
        />
    );
}


import React, { StrictMode } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import ClickSparkManager from "client/components/effect/ClickSparkManager";

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


import React, { Fragment, useEffect } from "@rbxts/react";
import { Debris, TweenService } from "@rbxts/services";
import Droplet from "shared/item/Droplet";
import Packets from "shared/Packets";

export default function DropletSurgeManager() {
    useEffect(() => {
        const connection = Packets.dropletSurged.fromServer((dropletModelId) => {
            const droplet = Droplet.MODEL_PER_SPAWN_ID.get(dropletModelId);
            if (droplet === undefined) return;

            // Visual effect for surged droplet
            const light = new Instance("PointLight");
            light.Color = Color3.fromRGB(255, 255, 255);
            light.Brightness = 10;
            light.Range = 3;
            light.Parent = droplet;

            // Fade back to original color over time
            TweenService.Create(light, new TweenInfo(2), { Brightness: 0 }).Play();
            Debris.AddItem(light, 2);
        });

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

    return <Fragment />;
}


import React, { Fragment } from "@rbxts/react";
import AreaEffectManager from "client/components/effect/AreaEffectManager";
import ChatHookManager from "client/components/effect/ChatHookManager";
import ClickSparkManager from "client/components/effect/ClickSparkManager";
import DropletSurgeManager from "client/components/effect/DropletSurgeManager";
import EyeContactManager from "client/components/effect/EyeContactManager";
import HamsterManager from "client/components/effect/HamsterManager";
import ItemEffectManager from "client/components/effect/ItemEffectManager";
import LightingAtmosphereManager from "client/components/effect/LightingAtmosphereManager";
import WalkspeedManager from "client/components/effect/WalkspeedManager";
import EtohKitClientManager from "client/components/effect/etoh/EtohKitClientManager";

export default function EffectManager() {
    return (
        <Fragment>
            <AreaEffectManager />
            <ClickSparkManager />
            <DropletSurgeManager />
            <HamsterManager />
            <ItemEffectManager />
            <LightingAtmosphereManager />
            <ChatHookManager />
            <EtohKitClientManager />
            <EyeContactManager />
            <WalkspeedManager />
        </Fragment>
    );
}


import { simpleInterval } from "@antivivi/vrldk";
import React, { Fragment, useEffect, useRef } from "@rbxts/react";
import { CollectionService, Workspace } from "@rbxts/services";
import Sandbox from "shared/Sandbox";
import WorldNode from "shared/world/nodes/WorldNode";

interface TrackingDetails {
    model: Model;
    head: BasePart;
    neck: Motor6D;
    originalNeckC0: CFrame;
}

const TRACKING_DISTANCE = 20;
const UPDATE_INTERVAL = 0.05;
const LERP_ALPHA = 0.3;

export default function EyeContactManager() {
    const trackingRef = useRef(new Map<Model, TrackingDetails>());

    useEffect(() => {
        if (Sandbox.getEnabled()) {
            return;
        }

        const tracking = trackingRef.current;

        const getClosestTarget = (model: Model) => {
            let closest: Model | undefined;
            let closestDistance = TRACKING_DISTANCE;
            const modelPrimaryPart = model.PrimaryPart;
            if (modelPrimaryPart === undefined) {
                return undefined;
            }

            for (const candidate of CollectionService.GetTagged("NPC")) {
                if (!candidate.IsA("Model") || !candidate.FindFirstChild("Humanoid")) {
                    continue;
                }
                if (candidate === model) {
                    continue;
                }

                const primaryPart = candidate.PrimaryPart;
                if (primaryPart === undefined) {
                    continue;
                }

                const distance = modelPrimaryPart.Position.sub(primaryPart.Position).Magnitude;
                if (distance < closestDistance) {
                    closestDistance = distance;
                    closest = candidate;
                }
            }

            return closest;
        };

        const lookAt = (model: Model, target: BasePart | undefined) => {
            const details = tracking.get(model);
            if (details === undefined) {
                return;
            }

            const { neck, head, originalNeckC0 } = details;
            if (target === undefined) {
                neck.C0 = neck.C0.Lerp(originalNeckC0, LERP_ALPHA);
                return;
            }

            const headCFrame = head.CFrame;
            const delta = headCFrame.Position.sub(target.Position);
            const distance = delta.Magnitude;
            if (distance === 0) {
                neck.C0 = neck.C0.Lerp(originalNeckC0, LERP_ALPHA);
                return;
            }

            const verticalOffset = delta.Y;
            const crossTerm = headCFrame.Position.sub(target.Position).Unit.Cross(headCFrame.LookVector).Y;
            const destination = originalNeckC0.mul(
                CFrame.Angles(math.atan(verticalOffset / distance) * 0.5, 0, crossTerm * 0.8),
            );
            neck.C0 = neck.C0.Lerp(destination, LERP_ALPHA);
        };

        const resetModel = (details: TrackingDetails) => {
            details.neck.C0 = details.originalNeckC0;
        };

        const npcWorldNode = new WorldNode<Model>(
            "NPC",
            (npcModel) => {
                if (!npcModel.IsA("Model") || tracking.has(npcModel)) {
                    return;
                }

                const torso = npcModel.FindFirstChild("Torso") as BasePart | undefined;
                if (torso === undefined) {
                    return;
                }

                const neck = torso.FindFirstChild("Neck") as Motor6D | undefined;
                if (neck === undefined) {
                    return;
                }

                const head = npcModel.FindFirstChild("Head") as BasePart | undefined;
                if (head === undefined) {
                    return;
                }

                tracking.set(npcModel, {
                    model: npcModel,
                    head,
                    neck,
                    originalNeckC0: neck.C0,
                });
            },
            (npcModel) => {
                if (!npcModel.IsA("Model")) {
                    return;
                }

                const details = tracking.get(npcModel);
                if (details !== undefined) {
                    resetModel(details);
                    tracking.delete(npcModel);
                }
            },
        );

        const intervalCleanup = simpleInterval(() => {
            for (const [model] of tracking) {
                const closest = getClosestTarget(model);
                if (closest !== undefined) {
                    const head = closest.FindFirstChild("Head") as BasePart | undefined;
                    lookAt(model, head);
                } else {
                    lookAt(model, undefined);
                }
            }
        }, UPDATE_INTERVAL);

        return () => {
            intervalCleanup();
            npcWorldNode.cleanup();
            for (const [, details] of tracking) {
                resetModel(details);
            }
            tracking.clear();
        };
    }, []);

    return <Fragment />;
}


import React, { Fragment, useEffect } from "@rbxts/react";
import Items from "shared/items/Items";
import Packets from "shared/Packets";

export default function HamsterManager() {
    useEffect(() => {
        const connection = Packets.modifyGame.fromServer((param) => {
            if (param === "markplaceableeverywhere") {
                Items.itemsPerId.forEach((item) => item.placeableEverywhere());
            }
        });

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

    return <Fragment />;
}


import { rainbowEffect } from "@antivivi/vrldk";
import React, { Fragment, useEffect } from "@rbxts/react";
import { TweenService } from "@rbxts/services";
import { PLACED_ITEMS_FOLDER } from "shared/constants";
import WorldNode from "shared/world/nodes/WorldNode";

export default function DropletSurgeManager() {
    useEffect(() => {
        const isReady = (instance: Instance): instance is BasePart => {
            if (!instance.IsA("BasePart")) return false;
            if (!instance.IsDescendantOf(PLACED_ITEMS_FOLDER)) return false;
            return true;
        };

        const rainbowWorldNode = new WorldNode("Rainbow", (instance) => {
            if (!isReady(instance)) return;
            rainbowEffect(instance as BasePart, 2);
        });

        const spinnerWorldNode = new WorldNode("Spinner", (instance) => {
            if (!isReady(instance)) return;

            const createRandomTween = () => {
                if (instance === undefined || instance.Parent === undefined) return;

                const tween = TweenService.Create(instance, new TweenInfo(2, Enum.EasingStyle.Linear), {
                    Orientation: new Vector3(math.random(0, 360), math.random(0, 360), math.random(0, 360)),
                });
                tween.Completed.Once(createRandomTween);
                tween.Play();
            };
            createRandomTween();
        });

        const tweenInfo = new TweenInfo(2, Enum.EasingStyle.Linear);
        function rotationLoop(instance: BasePart, delta: number) {
            const tween = TweenService.Create(instance, tweenInfo, {
                CFrame: instance.CFrame.mul(CFrame.Angles(0, delta, 0)),
            });
            tween.Completed.Once(() => rotationLoop(instance, delta));
            tween.Play();
            return tween;
        }

        const clockwiseWorldNode = new WorldNode("Clockwise", (instance) => {
            if (!isReady(instance)) return;
            const tween = rotationLoop(instance, math.pi);
            return () => tween.Destroy();
        });

        const anticlockwiseWorldNode = new WorldNode("Anticlockwise", (instance) => {
            if (!isReady(instance)) return;
            const tween = rotationLoop(instance, -math.pi);
            return () => tween.Destroy();
        });

        return () => {
            rainbowWorldNode.cleanup();
            spinnerWorldNode.cleanup();
            clockwiseWorldNode.cleanup();
            anticlockwiseWorldNode.cleanup();
        };
    }, []);

    return <Fragment />;
}


/**
 * @fileoverview React component for managing unified lighting and atmosphere effects.
 *
 * Handles:
 * - Area-based lighting configurations (base layer)
 * - Weather-based atmospheric modifications (multiplicative/additive layer)
 * - Dynamic light cycling based on time of day and quality settings
 * - Rain effects and visual weather states
 *
 * This component replaces both LightingController and AtmosphereController with a unified
 * React-based approach that properly layers lighting effects without conflicts.
 *
 * @since 1.0.0
 */
import { variableInterval } from "@antivivi/vrldk";
import React, { Fragment, useEffect, useRef } from "@rbxts/react";
import { Lighting } from "@rbxts/services";
import RainParallel from "client/parallel/rain/RainParallel";
import UserGameSettings from "shared/api/UserGameSettings";
import { IS_EDIT } from "shared/Context";
import Packets from "shared/Packets";
import { WeatherState, WeatherType } from "shared/weather/WeatherTypes";
import { AREAS } from "shared/world/Area";
import WorldNode from "shared/world/nodes/WorldNode";

interface LightingConfig {
    Ambient?: Color3;
    OutdoorAmbient?: Color3;
    EnvironmentDiffuseScale?: number;
    EnvironmentSpecularScale?: number;
    FogEnd?: number;
    FogStart?: number;
    FogColor?: Color3;
    Brightness?: number;
}

interface WeatherModifiers {
    brightnessMultiplier: number;
    outdoorAmbientModifier?: Color3;
    fogEndOverride?: number;
    lightBrightnessMultiplier: number;
}

/**
 * Component that manages all lighting and atmosphere effects in a unified, layered approach.
 *
 * Lighting layers (applied in order):
 * 1. Default lighting (captured at mount)
 * 2. Area-specific lighting configuration (base layer)
 * 3. Weather modifications (multiplicative/additive effects)
 */
export default function LightingAtmosphereManager() {
    // Store default lighting properties captured at mount
    const defaultLighting = useRef<LightingConfig>({});

    // Current area and weather state
    const currentAreaConfig = useRef<LightingConfig | undefined>(undefined);
    const currentWeather = useRef<WeatherState>({
        type: WeatherType.Clear,
        intensity: 0,
        duration: 300,
        timeRemaining: 300,
    });

    // Dynamic lights that cycle with time of day
    const cyclingLights = useRef(new Map<Light, number>());

    // Cleanup functions
    const cleanupFns = useRef<Array<() => void>>([]);

    useEffect(() => {
        if (IS_EDIT) return;

        // Capture default lighting on mount
        defaultLighting.current = {
            Ambient: Lighting.Ambient,
            OutdoorAmbient: Lighting.OutdoorAmbient,
            EnvironmentDiffuseScale: Lighting.EnvironmentDiffuseScale,
            EnvironmentSpecularScale: Lighting.EnvironmentSpecularScale,
            FogEnd: Lighting.FogEnd,
            FogStart: Lighting.FogStart,
            FogColor: Lighting.FogColor,
            Brightness: Lighting.Brightness,
        };

        // --- Helper: Find light in container ---
        function findLight(container: Instance): Light | undefined {
            const light = container.FindFirstChildOfClass("Light");
            if (light !== undefined) return light;
            return container.FindFirstChild("LightSource")?.FindFirstChildWhichIsA("Light");
        }

        // --- Helper: Get weather modifiers ---
        function getWeatherModifiers(weather: WeatherState): WeatherModifiers {
            switch (weather.type) {
                case WeatherType.Clear:
                    return {
                        brightnessMultiplier: 1.0,
                        lightBrightnessMultiplier: 1.0,
                    };
                case WeatherType.Cloudy:
                    return {
                        brightnessMultiplier: 0.75,
                        outdoorAmbientModifier: Color3.fromRGB(100, 100, 120),
                        fogEndOverride: 50000,
                        lightBrightnessMultiplier: 0.8,
                    };
                case WeatherType.Rainy:
                    return {
                        brightnessMultiplier: 0.5,
                        outdoorAmbientModifier: Color3.fromRGB(80, 80, 100),
                        fogEndOverride: 20000,
                        lightBrightnessMultiplier: 0.6,
                    };
                case WeatherType.Thunderstorm:
                    return {
                        brightnessMultiplier: 0.4,
                        outdoorAmbientModifier: Color3.fromRGB(60, 60, 80),
                        fogEndOverride: 15000,
                        lightBrightnessMultiplier: 0.6,
                    };
                default:
                    return {
                        brightnessMultiplier: 1.0,
                        lightBrightnessMultiplier: 1.0,
                    };
            }
        }

        // --- Helper: Apply layered lighting ---
        function applyLighting() {
            // Layer 1: Start with area config or defaults
            const baseConfig = currentAreaConfig.current ?? defaultLighting.current;

            // Layer 2: Apply weather modifiers
            const weatherMods = getWeatherModifiers(currentWeather.current);

            // Apply base config
            for (const [key, value] of pairs(baseConfig)) {
                (Lighting as unknown as { [key: string]: unknown })[key] = value;
            }

            // Apply weather modifications (multiplicative/additive)
            if (baseConfig.Brightness !== undefined) {
                Lighting.Brightness = baseConfig.Brightness * weatherMods.brightnessMultiplier;
            }

            if (weatherMods.outdoorAmbientModifier !== undefined) {
                Lighting.OutdoorAmbient = weatherMods.outdoorAmbientModifier;
            }

            if (weatherMods.fogEndOverride !== undefined) {
                Lighting.FogEnd = weatherMods.fogEndOverride;
            }

            // Update rain effect
            RainParallel.setWeatherState(currentWeather.current);
        }

        // --- Subscribe to area changes ---
        const areaConnection = Packets.currentArea.observe((areaId) => {
            currentAreaConfig.current = areaId !== undefined ? AREAS[areaId]?.lightingConfiguration : undefined;
            applyLighting();
        });
        cleanupFns.current.push(() => areaConnection.disconnect());

        // Initial area apply
        const initialAreaId = Packets.currentArea.get();
        currentAreaConfig.current =
            initialAreaId !== undefined ? AREAS[initialAreaId]?.lightingConfiguration : undefined;
        applyLighting();

        // --- Subscribe to weather changes ---
        const weatherConnection = Packets.weatherChanged.fromServer((weatherState: object) => {
            currentWeather.current = weatherState as WeatherState;
            applyLighting();
        });
        cleanupFns.current.push(() => weatherConnection.Disconnect());

        // Request initial weather state
        task.spawn(() => {
            const weatherState = Packets.getWeatherState.toServer() as WeatherState;
            currentWeather.current = weatherState;
            applyLighting();
        });

        // --- Set up cycling lights (WorldNode) ---
        const lightNode = new WorldNode(
            "CyclingLight",
            (container) => {
                const light = findLight(container);
                if (light !== undefined) cyclingLights.current.set(light, light.Brightness);
            },
            (container) => {
                const light = findLight(container);
                if (light !== undefined) cyclingLights.current.delete(light);
            },
        );
        cleanupFns.current.push(() => {
            lightNode.cleanup();
        });

        // --- Variable interval for updating dynamic lights ---
        let oldQualityLevel = UserGameSettings!.SavedQualityLevel.Value;
        const updateLights = () => {
            const qualityLevel = UserGameSettings!.SavedQualityLevel.Value;
            const currentWeatherMods = getWeatherModifiers(currentWeather.current);

            for (const [light, base] of cyclingLights.current) {
                // Update shadows based on quality
                if (oldQualityLevel !== qualityLevel) {
                    light.Shadows = qualityLevel === 10;
                }

                // Calculate base brightness from time of day
                let brightness = qualityLevel === 1 ? 0 : (math.abs(Lighting.ClockTime - 12) / 8 - 0.25) * base * 2;

                // Apply weather dimming (multiplicative)
                brightness *= currentWeatherMods.lightBrightnessMultiplier;

                light.Brightness = brightness;
            }
            oldQualityLevel = qualityLevel;
        };

        const options = { interval: 1 };
        const cleanup = variableInterval(() => {
            updateLights();
            const qualityLevel = UserGameSettings!.SavedQualityLevel.Value;
            options.interval = qualityLevel >= 5 ? 1 / 60 : 1;
        }, options);
        cleanupFns.current.push(cleanup);

        // --- Cleanup on unmount ---
        return () => {
            // Run all cleanup functions
            cleanupFns.current.forEach((fn) => fn());

            // Restore default lighting
            for (const [key, value] of pairs(defaultLighting.current)) {
                (Lighting as unknown as { [key: string]: unknown })[key] = value;
            }

            // Clear cycling lights
            cyclingLights.current.clear();
        };
    }, []);

    return <Fragment />;
}


import React, { Fragment, useEffect, useRef } from "@rbxts/react";
import { observeCharacter } from "shared/constants";
import { showToast } from "client/components/toast/ToastService";
import Sandbox from "shared/Sandbox";

const OBSTACLE_ZONE_NAME = "ObbyZone";
const DEFAULT_OBBY_WALK_SPEED = 16;
const DEBOUNCE_DURATION = 0.1;

export default function WalkspeedManager() {
    const lastCustomWalkspeed = useRef(DEFAULT_OBBY_WALK_SPEED);
    const isDebounced = useRef(false);

    useEffect(() => {
        if (Sandbox.getEnabled()) {
            return;
        }

        let cleanupCharacterConnections: (() => void) | undefined;

        const releaseDebounce = () => {
            task.delay(DEBOUNCE_DURATION, () => {
                isDebounced.current = false;
            });
        };

        const disconnectCharacterObserver = observeCharacter((character) => {
            cleanupCharacterConnections?.();

            const humanoid = character.FindFirstChildOfClass("Humanoid");
            const rootPart = character.FindFirstChild("HumanoidRootPart") as BasePart | undefined;

            if (!humanoid || !rootPart) {
                return;
            }

            lastCustomWalkspeed.current = humanoid.WalkSpeed;

            const handleZoneEntered = (part: BasePart) => {
                if (part.Name !== OBSTACLE_ZONE_NAME || isDebounced.current) {
                    return;
                }

                isDebounced.current = true;
                lastCustomWalkspeed.current = humanoid.WalkSpeed;
                const newWalkspeed = DEFAULT_OBBY_WALK_SPEED;
                humanoid.WalkSpeed = newWalkspeed;
                showToast({
                    message: `You have entered an Obby. (Walkspeed = ${newWalkspeed})`,
                    variant: "info",
                });
                releaseDebounce();
            };

            const handleZoneExited = (part: BasePart) => {
                if (part.Name !== OBSTACLE_ZONE_NAME || isDebounced.current) {
                    return;
                }

                isDebounced.current = true;
                const restoredWalkspeed = lastCustomWalkspeed.current;
                humanoid.WalkSpeed = restoredWalkspeed;
                showToast({
                    message: `You have left the Obby. (Walkspeed = ${restoredWalkspeed})`,
                    variant: "info",
                });
                releaseDebounce();
            };

            const touchedConnection = rootPart.Touched.Connect(handleZoneEntered);
            const touchEndedConnection = rootPart.TouchEnded.Connect(handleZoneExited);

            cleanupCharacterConnections = () => {
                touchedConnection.Disconnect();
                touchEndedConnection.Disconnect();
            };
        });

        return () => {
            cleanupCharacterConnections?.();
            disconnectCharacterObserver();
        };
    }, []);

    return <Fragment />;
}


import { Players, TweenService } from "@rbxts/services";
import { playSound } from "shared/asset/GameAssets";

/**
 * Upstream EToH teleporter script adapted for client-side use.
 * @param model The teleporter model
 * @returns A function to initialize the teleporter
 */
export default function createEtohTeleporter(model: Model) {
    const easingStyle = Enum.EasingStyle.Quad; // FEEL FREE TO CHANGE THIS
    const easingDirection = Enum.EasingDirection.Out; // FEEL FREE TO CHANGE THIS

    function tween(part: BasePart, twTime: number, inf: Partial<TweenInfo & { CFrame?: CFrame }>) {
        const tweenInfo = new TweenInfo(twTime, easingStyle, easingDirection, 0, false, 0);
        const tween = TweenService.Create(part, tweenInfo, inf);
        tween.Play();
        tween.Completed.Connect(() => {
            tween.Destroy();
        });
    }

    function teleport(part: BasePart, newCF: CFrame) {
        if (part.Anchored) return;

        const transitionTime = model.FindFirstChild("TransitionTime") as NumberValue | undefined;
        if (transitionTime && transitionTime.Value > 0) {
            part.Anchored = true;
            tween(part, transitionTime.Value, { CFrame: newCF });
            task.wait(transitionTime.Value);
            part.Anchored = false;
        } else {
            part.CFrame = newCF;
        }

        const keepVelocity = model.FindFirstChild("KeepVelocity") as BoolValue | undefined;
        if (keepVelocity && !keepVelocity.Value) {
            part.AssemblyLinearVelocity = Vector3.zero;
            part.AssemblyAngularVelocity = Vector3.zero;
        }
    }

    return () => {
        const destinations: Array<BasePart> = [];

        for (const tp of model.GetDescendants()) {
            if (tp.Name === "Destination" && tp.IsA("BasePart")) {
                destinations.push(tp);
            } else if (tp.Name === "Teleporter" && tp.IsA("BasePart")) {
                if (tp.FindFirstChild("ButtonActivated")) {
                    const a = new Instance("BoolValue");
                    a.Name = "Activated";
                    a.Parent = tp;
                    if (tp.FindFirstChild("Invert")) {
                        a.Value = true;
                    }
                }
                tp.LocalTransparencyModifier = 1;
                tp.Touched.Connect((t) => {
                    const activated = tp.FindFirstChild("Activated") as BoolValue | undefined;
                    if (activated && !activated.Value) return;

                    const dests = destinations.filter((d) => {
                        const act = d.FindFirstChild("Activated") as BoolValue | undefined;
                        return !(act && !act.Value);
                    });
                    if (dests.size() <= 0) return;
                    const d = dests[math.random(0, dests.size() - 1)];

                    const teleportPushboxes = (model.FindFirstChild("TeleportPushboxes") as BoolValue | undefined)
                        ?.Value;
                    if (teleportPushboxes && (t.Name === "Pushbox" || t.FindFirstChild("IsBox"))) {
                        playSound("Teleport.mp3");
                        teleport(t, d.GetPivot()!.add(new Vector3(0, 5, 0)));
                    }

                    const teleportPlayers = (model.FindFirstChild("TeleportPlayers") as BoolValue | undefined)?.Value;
                    const player = Players.GetPlayerFromCharacter(t.Parent);
                    if (teleportPlayers && player === Players.LocalPlayer) {
                        playSound("Teleport.mp3", undefined, (sound) => (sound.Volume = 0.25));
                        const primaryPart = (t.Parent as Model).PrimaryPart;
                        if (primaryPart) {
                            teleport(primaryPart, d.GetPivot()!.add(new Vector3(0, 5, 0)));
                        }
                    }
                });
            }
        }
    };
}


/**
 * @fileoverview Hooks for integrating hotkeys with tooltips and UI components.
 *
 * Provides hooks that combine hotkey functionality with tooltip display,
 * matching the patterns from the original HotkeysController.
 */

import { useCallback } from "@rbxts/react";
import { HotkeyLabel, useHotkey } from "client/components/hotkeys/HotkeyManager";
import { useTooltipProps } from "client/components/tooltip/TooltipManager";

/**
 * Hook that combines hotkey binding with tooltip integration
 * Similar to the original HotkeysController.setHotkey method
 */
export default function useHotkeyWithTooltip({
    action,
    label,
    endAction,
    onEnter,
    onLeave,
}: {
    /**
     * The action to execute when the hotkey is pressed
     * @param usedHotkey Whether the action was triggered by a hotkey (true) or a click (false)
     */
    action: (usedHotkey: boolean) => boolean;
    /** The label for the hotkey */
    label: HotkeyLabel;
    /** Optional action to execute when the hotkey is released */
    endAction?: () => boolean;
    /** Optional callback when the tooltip is shown */
    onEnter?: () => void;
    /** Optional callback when the tooltip is hidden */
    onLeave?: () => void;
}) {
    // Bind the hotkey
    useHotkey({
        action,
        label: label,
        endAction: endAction,
    });

    // Get tooltip props
    const tooltipProps = useTooltipProps({ data: { message: label }, onEnter, onLeave });

    // Return both the action handler and tooltip props
    const handleClick = useCallback(() => {
        return action(false);
    }, [action]);

    return {
        hovering: tooltipProps.hovering,
        onEnter: tooltipProps.onEnter,
        onLeave: tooltipProps.onLeave,
        onClick: handleClick,
        events: {
            ...tooltipProps.events,
            Activated: handleClick,
        },
    };
}


import { getAllInstanceInfo } from "@antivivi/vrldk";
import React, { Fragment, useEffect, useRef, useState } from "@rbxts/react";
import { TweenService } from "@rbxts/services";
import Packets from "shared/Packets";
import { getAsset } from "shared/asset/AssetMap";
import { PLACED_ITEMS_FOLDER } from "shared/constants";

function BrokenItemIndicator({ model }: { model: Model }) {
    const primaryPart = model.PrimaryPart;
    const imageLabelRef = useRef<ImageLabel>();

    useEffect(() => {
        let active = true;
        const loop = () => {
            if (!active) return;
            if (imageLabelRef.current === undefined) return;
            const tweenInfo = new TweenInfo(1, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut);
            TweenService.Create(imageLabelRef.current, tweenInfo, {
                ImageColor3: Color3.fromRGB(255, 156, 156),
            }).Play();
            task.wait(1);

            if (imageLabelRef.current === undefined) return;
            TweenService.Create(imageLabelRef.current, tweenInfo, {
                ImageColor3: Color3.fromRGB(255, 255, 255),
            }).Play();
            task.wait(1);

            loop();
        };
        task.delay(1, loop);

        return () => {
            active = false;
        };
    }, []);

    if (primaryPart === undefined) return <Fragment />;

    return (
        <Fragment>
            <highlight
                Adornee={model}
                FillColor={Color3.fromRGB(255, 0, 0)}
                FillTransparency={0.9}
                OutlineTransparency={1}
            />
            <billboardgui
                Adornee={primaryPart}
                Active={true}
                AlwaysOnTop={true}
                Enabled={true}
                LightInfluence={0}
                MaxDistance={80}
                Size={new UDim2(2, 0, 2, 0)}
                ZIndexBehavior={Enum.ZIndexBehavior.Sibling}
            >
                <imagelabel
                    ref={imageLabelRef}
                    BackgroundTransparency={1}
                    Image={getAsset("assets/Broken.png")}
                    Size={new UDim2(1, 0, 1, 0)}
                />
            </billboardgui>
        </Fragment>
    );
}

export default function BrokenItemIndicatorRenderer() {
    const [models, setModels] = useState<Set<Model>>(new Set());

    useEffect(() => {
        const brokenModels = new Set<Model>();

        const connection = Packets.brokenPlacedItems.observe((brokenPlacedItems) => {
            for (const model of brokenModels) {
                if (!brokenPlacedItems.has(model.Name)) {
                    const modelInfo = getAllInstanceInfo(model);
                    modelInfo.broken = false;
                    brokenModels.delete(model);
                }
            }

            const newModels = new Set<Model>();
            for (const placementId of brokenPlacedItems) {
                const model = PLACED_ITEMS_FOLDER.WaitForChild(placementId, 1);
                if (model === undefined) continue;

                newModels.add(model as Model);
                brokenModels.add(model as Model);
                const modelInfo = getAllInstanceInfo(model);
                modelInfo.broken = true;
            }
            setModels(newModels);
        });

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

    return (
        <Fragment>
            {[...models].map((model) => (
                <BrokenItemIndicator model={model} />
            ))}
        </Fragment>
    );
}


import React, { Fragment, memo, useEffect, useMemo, useState } from "@rbxts/react";
import { Debris, TweenService } from "@rbxts/services";
import { getAsset } from "shared/asset/AssetMap";
import { emitEffect, playSound } from "shared/asset/GameAssets";
import { RobotoSlabExtraBold, RobotoSlabHeavy } from "shared/asset/GameFonts";
import { getPlayerCharacter } from "shared/hamster/getPlayerCharacter";
import Items from "shared/items/Items";
import HARVESTABLES from "shared/world/harvestable/Harvestable";

function HarvestableGui({ enabled, model }: { enabled: boolean; model: PVInstance }) {
    const harvestable = HARVESTABLES[model.Name as HarvestableId] as HarvestableData | undefined;
    if (harvestable === undefined) return <Fragment />;
    const key = useMemo(() => tostring(model.GetPivot()), [model]);

    const [health, setHealth] = useState(harvestable.health);
    const maxHealth = harvestable.health;

    const colorSequence = useMemo(() => {
        const healthPercent = health / maxHealth;

        // Create a dynamic gradient based on health percentage
        if (healthPercent > 0.5) {
            return new ColorSequence([
                new ColorSequenceKeypoint(0, Color3.fromRGB(46, 204, 113)),
                new ColorSequenceKeypoint(1, Color3.fromRGB(46, 204, 84)),
            ]);
        } else if (healthPercent > 0.25) {
            return new ColorSequence([
                new ColorSequenceKeypoint(0, Color3.fromRGB(241, 196, 15)),
                new ColorSequenceKeypoint(1, Color3.fromRGB(230, 126, 34)),
            ]);
        } else {
            return new ColorSequence([
                new ColorSequenceKeypoint(0, Color3.fromRGB(230, 126, 34)),
                new ColorSequenceKeypoint(1, Color3.fromRGB(231, 76, 60)),
            ]);
        }
    }, [health, maxHealth]);

    const item = Items.getItem(model.Name);

    useEffect(() => {
        if (!model.IsA("PVInstance")) return;
        const harvestable = HARVESTABLES[model.Name as HarvestableId];
        if (harvestable === undefined) return;
        let isNew = true;
        let prevHealth = 0;
        const updateHealth = () => {
            const currentHealth = (model.GetAttribute("Health") as number) ?? harvestable.health;
            setHealth(currentHealth);
            const drop = currentHealth - prevHealth;
            prevHealth = currentHealth;

            if (isNew === true) {
                isNew = false;
                return;
            }

            const currentTool = getPlayerCharacter()?.FindFirstChildOfClass("Tool");
            if (currentTool === undefined) return;

            const highlight = new Instance("Highlight");
            TweenService.Create(highlight, new TweenInfo(0.5), { FillTransparency: 1, OutlineTransparency: 1 }).Play();
            highlight.Adornee = model;

            const blade = currentTool.FindFirstChild("Blade") as BasePart | undefined;
            const effect = emitEffect("ToolUse", blade ?? model);
            effect.Color = new ColorSequence(blade?.Color ?? new Color3(255, 0, 0));

            if (drop < 0) {
                const item = Items.getItem(currentTool.Name);
                if (item === undefined) return;

                const gear = item.findTrait("Gear");
                if (gear === undefined) return;

                const multi = -drop / gear.damage!;
                effect.Brightness = multi;

                const baseColor = Color3.fromRGB(217, 0, 0);
                const critColor = Color3.fromRGB(158, 0, 217);
                const colorBlend = math.clamp((multi - 1) / 3, 0, 1);
                const damageColor = baseColor.Lerp(critColor, colorBlend);

                const damageGui = new Instance("BillboardGui");
                const horizontalJitter = math.random(-25, 25) / 75;
                const startingOffset = new Vector3(horizontalJitter, 0, horizontalJitter);
                damageGui.Size = new UDim2(5, 0, 1, 0);
                damageGui.StudsOffsetWorldSpace = startingOffset;
                damageGui.AlwaysOnTop = true;
                damageGui.Active = true;
                damageGui.MaxDistance = 60;
                damageGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling;
                damageGui.Name = "HarvestableDamageLabel";

                const valueLabel = new Instance("TextLabel");
                valueLabel.AnchorPoint = new Vector2(0.5, 0.5);
                valueLabel.Position = new UDim2(0.5, 0, 0.5, 0);
                valueLabel.BackgroundTransparency = 1;
                valueLabel.FontFace = RobotoSlabExtraBold;
                valueLabel.Text = `-${math.floor(-drop * 10) / 10}`;
                valueLabel.TextScaled = true;
                valueLabel.TextSize = 14;
                valueLabel.TextWrapped = true;
                valueLabel.TextColor3 = damageColor;
                valueLabel.TextTransparency = 0;
                valueLabel.Size = new UDim2(1, 0, 0.125 * (math.max(multi, 1) / 2) + 0.7, 0);

                const stroke = new Instance("UIStroke");
                stroke.Thickness = 4;
                stroke.Color = Color3.fromRGB(0, 0, 0);
                stroke.Transparency = 0;
                stroke.Parent = valueLabel;

                valueLabel.Parent = damageGui;
                damageGui.Adornee = model;
                damageGui.Parent = model;

                const tweenInfo = new TweenInfo(1, Enum.EasingStyle.Cubic, Enum.EasingDirection.In);
                TweenService.Create(damageGui, tweenInfo, {
                    StudsOffsetWorldSpace: new Vector3(0, -1.75, 0),
                }).Play();
                TweenService.Create(valueLabel, tweenInfo, {
                    TextTransparency: 1,
                    Rotation: 15 * (math.random() - 0.5),
                }).Play();
                TweenService.Create(stroke, tweenInfo, {
                    Transparency: 1,
                }).Play();

                Debris.AddItem(damageGui, 4);

                if (multi > 1.5) {
                    playSound("Critical.mp3");
                }
            }

            playSound("Harvest.mp3");

            highlight.Parent = model;
            Debris.AddItem(highlight, 2);
        };
        updateHealth();
        const connection = model.GetAttributeChangedSignal("Health").Connect(updateHealth);

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

    const accent = useMemo(() => {
        if (model.IsA("BasePart")) {
            return model.Color;
        }

        const colors = new Array<Color3>();

        for (const part of model.GetChildren()) {
            if (part.IsA("BasePart")) {
                colors.push(part.Color);
            }
        }
        if (colors.isEmpty()) return Color3.fromRGB(255, 255, 255);
        let r = 0;
        let g = 0;
        let b = 0;
        for (const color of colors) {
            r += color.R;
            g += color.G;
            b += color.B;
        }
        const size = colors.size();
        return new Color3(r / size, g / size, b / size);
    }, [model]);

    const changeTweenInfo = new TweenInfo(0.2, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out);

    const healthText = `${math.floor(health * 10) / 10}/${maxHealth}`;

    return (
        <billboardgui
            key={key}
            Adornee={model}
            Active={true}
            AlwaysOnTop={true}
            Enabled={enabled}
            MaxDistance={25}
            Size={new UDim2(6, 0, 2, 0)}
            StudsOffset={new Vector3(0, 2.5, 0)}
            ZIndexBehavior={Enum.ZIndexBehavior.Sibling}
        >
            {/* Main window */}
            <imagelabel
                AnchorPoint={new Vector2(0.5, 0)}
                BackgroundColor3={Color3.fromRGB(255, 255, 255)}
                BackgroundTransparency={0}
                BorderSizePixel={0}
                Image={getAsset("assets/HarvestableFrame.png")}
                Position={new UDim2(0.5, 0, 0.3, 0)}
                Size={new UDim2(1.1, 0, 0.6, 0)}
                ZIndex={-5}
            >
                <uigradient
                    Color={
                        new ColorSequence(
                            accent.Lerp(Color3.fromRGB(0, 0, 0), 0.2),
                            accent.Lerp(Color3.fromRGB(0, 0, 0), 0.4),
                        )
                    }
                    Rotation={90}
                />
                <uipadding
                    PaddingTop={new UDim(0.05, 0)}
                    PaddingBottom={new UDim(0.05, 0)}
                    PaddingLeft={new UDim(0.035, 0)}
                    PaddingRight={new UDim(0.035, 0)}
                />
            </imagelabel>

            {/* Title text container */}
            <textlabel
                AnchorPoint={new Vector2(0.5, 0)}
                BackgroundTransparency={1}
                FontFace={RobotoSlabHeavy}
                Text={item?.name ?? harvestable?.name ?? model.Name}
                TextColor3={accent.Lerp(Color3.fromRGB(255, 255, 255), 0.75)}
                TextScaled={true}
                TextSize={14}
                TextWrapped={true}
                TextXAlignment={Enum.TextXAlignment.Center}
                ZIndex={2}
                Position={new UDim2(0.5, 0, 0, 0)}
                Size={new UDim2(2, 0, 0.5, 0)}
            >
                <uistroke StrokeSizingMode={Enum.StrokeSizingMode.ScaledSize} Thickness={0.07} Color={accent}>
                    <uigradient Color={new ColorSequence(Color3.fromRGB(64, 64, 64))} />
                </uistroke>
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(201, 201, 201)),
                        ])
                    }
                    Rotation={90}
                />
            </textlabel>

            {/* Health bar container  */}
            <frame
                AnchorPoint={new Vector2(0.5, 0)}
                BackgroundColor3={Color3.fromRGB(255, 255, 255)}
                BorderSizePixel={0}
                Position={new UDim2(0.5, 0, 0.55, 0)}
                Size={new UDim2(1, 0, 0.4, 0)}
            >
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(38, 38, 38)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(23, 23, 23)),
                        ])
                    }
                    Rotation={90}
                />
                <uistroke
                    StrokeSizingMode={Enum.StrokeSizingMode.ScaledSize}
                    Thickness={0.07}
                    Color={Color3.fromRGB(28, 28, 28)}
                />
                {/* Health text main */}
                <textlabel
                    AnchorPoint={new Vector2(0.5, 0.5)}
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabExtraBold}
                    Position={new UDim2(0.5, 0, 0.5, 0)}
                    Size={new UDim2(0.9, 0, 0.95, 0)}
                    Text={healthText}
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextScaled={true}
                    ZIndex={2}
                >
                    <uistroke
                        StrokeSizingMode={Enum.StrokeSizingMode.ScaledSize}
                        Thickness={0.07}
                        Color={Color3.fromRGB(82, 82, 82)}
                    >
                        <uigradient Color={colorSequence} />
                    </uistroke>
                </textlabel>

                <frame
                    ref={(rbx) => {
                        if (rbx === undefined) return;
                        TweenService.Create(rbx, changeTweenInfo, {
                            Size: new UDim2(math.clamp(health / maxHealth, 0, 1), 0, 1, 0),
                        }).Play();
                    }}
                    BackgroundColor3={Color3.fromRGB(255, 255, 255)}
                    BorderSizePixel={0}
                    ZIndex={0}
                >
                    <uigradient Color={colorSequence} Rotation={90} />
                </frame>
            </frame>
        </billboardgui>
    );
}

export default memo(HarvestableGui);


import React, { Fragment, useEffect, useMemo, useState } from "@rbxts/react";
import HarvestableGui from "client/components/item/HarvestableGui";
import { observeCharacter } from "shared/constants";
import { AREAS } from "shared/world/Area";
import HARVESTABLES from "shared/world/harvestable/Harvestable";

export default function HarvestableGuiRenderer({ overrideModels }: { overrideModels?: PVInstance[] }) {
    const [enabled, setEnabled] = useState(false);

    const harvestableModels =
        overrideModels ??
        useMemo(() => {
            const models = new Array<PVInstance>();
            for (const [, area] of pairs(AREAS)) {
                const folder = area.worldNode.getInstance()?.FindFirstChild("Harvestable");
                if (folder === undefined) continue;
                for (const child of folder.GetChildren()) {
                    if (child.IsA("PVInstance") && HARVESTABLES[child.Name as HarvestableId] !== undefined) {
                        models.push(child);
                    }
                }
            }
            return models;
        }, []);

    useEffect(() => {
        let childAddedConnection: RBXScriptConnection | undefined;
        let childRemovedConnection: RBXScriptConnection | undefined;

        const cleanup = observeCharacter((character: Model) => {
            childAddedConnection?.Disconnect();
            childRemovedConnection?.Disconnect();
            childAddedConnection = character.ChildAdded.Connect((child) => {
                if (child.IsA("Tool")) {
                    setEnabled(true);
                }
            });
            childRemovedConnection = character.ChildRemoved.Connect((child) => {
                if (child.IsA("Tool")) {
                    setEnabled(false);
                }
            });
        });

        return () => {
            childAddedConnection?.Disconnect();
            childRemovedConnection?.Disconnect();
            cleanup();
        };
    }, []);

    return (
        <Fragment>
            {harvestableModels?.map((model) => (
                <HarvestableGui enabled={enabled} model={model as PVInstance} />
            ))}
        </Fragment>
    );
}


import React from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import PortableBeaconWindow from "client/components/item/PortableBeaconWindow";
import useVisibility from "client/hooks/useVisibility";
import Packets from "shared/Packets";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            visible: true,
        },
    },
    (props) => {
        useVisibility("PortableBeacon", props.controls.visible);

        Packets.tpToArea.fromClient(() => true);

        return <PortableBeaconWindow />;
    },
);


import React, { JSX, useEffect, useRef, useState } from "@rbxts/react";
import { TweenService } from "@rbxts/services";
import useSingleDocument from "client/components/sidebar/useSingleDocumentWindow";
import { showErrorToast } from "client/components/toast/ToastService";
import TechWindow from "client/components/window/TechWindow";
import { COLOR_SEQUENCE_PER_AREA } from "client/components/world/area/AreaBoardRenderer";
import { getAsset } from "shared/asset/AssetMap";
import { playSound } from "shared/asset/GameAssets";
import { RobotoMono, RobotoMonoBold } from "shared/asset/GameFonts";
import { observeCharacter } from "shared/constants";
import { getPlayerCharacter } from "shared/hamster/getPlayerCharacter";
import PortableBeacon from "shared/items/tools/PortableBeacon";
import Packets from "shared/Packets";

interface AreaInfo {
    name: string;
    description: string;
    image: AssetPath;
    layoutOrder: number;
}

// Define the main areas that should appear in the teleportation window
const TELEPORTABLE_AREAS: {
    [key in AreaId]?: AreaInfo;
} = {
    BarrenIslands: {
        name: "Barren Islands",
        description: "An abandoned region where your empire begins",
        image: "assets/area/BarrenIslandsLandscape.png",
        layoutOrder: 1,
    },
    SlamoVillage: {
        name: "Slamo Village",
        description: "A humble settlement where time seems to slow",
        image: "assets/area/SlamoVillageLandscape.png",
        layoutOrder: 2,
    },
    SkyPavilion: {
        name: "Sky Pavilion",
        description: "A floating sanctuary of modern architecture",
        image: "assets/area/SkyPavilionLandscape.png",
        layoutOrder: 3,
    },
};

export default function PortableBeaconWindow() {
    const { id, visible, openDocument, closeDocument } = useSingleDocument({ id: "PortableBeacon" });
    const [unlockedAreas, setUnlockedAreas] = useState<Set<AreaId>>(new Set(["BarrenIslands"]));

    useEffect(() => {
        const unlockedAreasConnection = Packets.unlockedAreas.observe((areas) => {
            const newAreas = new Set<AreaId>(["BarrenIslands"]);
            for (const areaId of areas) {
                newAreas.add(areaId);
            }
            setUnlockedAreas(newAreas);
        });

        let childAddedConnection: RBXScriptConnection | undefined;
        let childRemovedConnection: RBXScriptConnection | undefined;
        const onCharacterAdded = (character: Model) => {
            childAddedConnection?.Disconnect();
            childRemovedConnection?.Disconnect();
            childAddedConnection = character.ChildAdded.Connect((child) => {
                if (child.Name === PortableBeacon.id) {
                    openDocument();
                }
            });
            childRemovedConnection = character.ChildRemoved.Connect((child) => {
                if (child.Name === PortableBeacon.id) {
                    closeDocument();
                }
            });
        };
        const cleanup = observeCharacter(onCharacterAdded);

        return () => {
            unlockedAreasConnection.Disconnect();
            childAddedConnection?.Disconnect();
            childRemovedConnection?.Disconnect();
            cleanup();
        };
    }, []);

    useEffect(() => {
        if (!visible) {
            getPlayerCharacter()?.FindFirstChildOfClass("Humanoid")?.UnequipTools();
        }
    }, [visible]);

    const areaElements = new Array<JSX.Element>();

    for (const [areaId, areaInfo] of pairs(TELEPORTABLE_AREAS)) {
        const isUnlocked = unlockedAreas.has(areaId);

        areaElements.push(<PortableBeaconOption areaId={areaId} areaInfo={areaInfo} isUnlocked={isUnlocked} />);
    }

    return (
        <TechWindow icon={getAsset("assets/PortableBeacon.png")} id={id} title="Portable Beacon" visible={visible}>
            <scrollingframe
                AutomaticCanvasSize={Enum.AutomaticSize.Y}
                BackgroundTransparency={1}
                CanvasSize={new UDim2(1, 0, 0, 0)}
                Size={new UDim2(1, 0, 1, 0)}
                ScrollBarThickness={6}
                ScrollingDirection={Enum.ScrollingDirection.Y}
            >
                <uilistlayout
                    Padding={new UDim(0, 10)}
                    HorizontalAlignment={Enum.HorizontalAlignment.Center}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                />
                <uipadding
                    PaddingTop={new UDim(0, 10)}
                    PaddingBottom={new UDim(0, 10)}
                    PaddingLeft={new UDim(0, 10)}
                    PaddingRight={new UDim(0, 10)}
                />
                {areaElements}
            </scrollingframe>
        </TechWindow>
    );
}
function PortableBeaconOption({
    areaId,
    areaInfo,
    isUnlocked,
}: {
    areaId: AreaId;
    areaInfo: AreaInfo;
    isUnlocked: boolean;
}) {
    const ref = useRef<ImageButton>();
    const [isHovered, setHovered] = useState<boolean>(false);

    useEffect(() => {
        const button = ref.current;
        if (!button) return;
        if (isHovered) {
            TweenService.Create(button, new TweenInfo(0.2), {
                ImageTransparency: isUnlocked ? 0 : 0.75,
            }).Play();
        } else {
            TweenService.Create(button, new TweenInfo(0.2), {
                ImageTransparency: isUnlocked ? 0.2 : 0.8,
            }).Play();
        }
    }, [isHovered]);

    return (
        <imagebutton
            ref={ref}
            AutoButtonColor={false}
            BackgroundColor3={Color3.fromRGB(0, 0, 0)}
            BorderColor3={isHovered ? Color3.fromRGB(255, 237, 138) : Color3.fromRGB(61, 61, 61)}
            BorderSizePixel={3}
            Image={getAsset(areaInfo.image)}
            ImageTransparency={isUnlocked ? 0.2 : 0.8}
            Position={new UDim2(0, 10, 0, 10)}
            Size={new UDim2(1, -20, 0, 120)}
            ScaleType={Enum.ScaleType.Crop}
            LayoutOrder={areaInfo.layoutOrder}
            Active={isUnlocked}
            Event={{
                Activated: () => {
                    const success = Packets.tpToArea.toServer(areaId);
                    if (success) {
                        playSound("Teleport.mp3");
                        getPlayerCharacter()?.FindFirstChildOfClass("Humanoid")?.UnequipTools();
                    } else {
                        playSound("Error.mp3");
                        showErrorToast("Teleport failed. Try again in a moment.");
                    }
                },
                MouseEnter: () => setHovered(true),
                MouseLeave: () => setHovered(false),
            }}
        >
            <uipadding
                PaddingTop={new UDim(0, 10)}
                PaddingBottom={new UDim(0, 10)}
                PaddingLeft={new UDim(0, 20)}
                PaddingRight={new UDim(0, 20)}
            />
            <uistroke
                Color={new Color3(255, 255, 255)}
                Thickness={isHovered ? 2 : 1}
                Transparency={isUnlocked ? 0 : 0.7}
            >
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(156, 156, 156)),
                        ])
                    }
                    Rotation={90}
                />
            </uistroke>
            {!isUnlocked && (
                <imagelabel
                    BackgroundTransparency={1}
                    Image={getAsset("assets/Lock.png")}
                    Position={new UDim2(0.5, -12, 0.5, -12)}
                    Size={new UDim2(0, 24, 0, 24)}
                />
            )}

            {/* Area Info */}
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoMonoBold}
                Size={new UDim2(1, 0, 0.3, 0)}
                Text={areaInfo.name}
                TextColor3={new Color3(255, 255, 255)}
                TextScaled={true}
                TextXAlignment={Enum.TextXAlignment.Left}
            >
                <uistroke Color={new Color3(0, 0, 0)} Thickness={2} />
                <uigradient Color={COLOR_SEQUENCE_PER_AREA[areaId]} Rotation={90} />
            </textlabel>

            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoMono}
                Position={new UDim2(0, 0, 0, 28)}
                Size={new UDim2(1, 0, 0.7, 0)}
                Text={isUnlocked ? areaInfo.description : "Area locked"}
                TextColor3={isUnlocked ? new Color3(255, 255, 255) : new Color3(140, 140, 140)}
                TextScaled={true}
                TextXAlignment={Enum.TextXAlignment.Left}
                TextYAlignment={Enum.TextYAlignment.Top}
                TextWrapped={true}
            >
                <uitextsizeconstraint MaxTextSize={24} />
                <uistroke Color={new Color3(0, 0, 0)} Thickness={2} />
            </textlabel>
        </imagebutton>
    );
}


import React, { useEffect } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import RepairWindow, { RepairManager } from "client/components/item/RepairWindow";
import useVisibility from "client/hooks/useVisibility";
import TheFirstConveyor from "shared/items/negative/tfd/TheFirstConveyor";
import Packets from "shared/Packets";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            visible: true,
        },
    },
    (props) => {
        useVisibility("Repair", props.controls.visible);

        useEffect(() => {
            Packets.placedItems.set(
                new Map([
                    [
                        "TestPlacementId",
                        {
                            item: TheFirstConveyor.id,
                            posX: 0,
                            posY: 0,
                            posZ: 0,
                            rotX: 0,
                            rotY: 0,
                            rotZ: 0,
                        },
                    ],
                ]),
            );
            RepairManager.item = TheFirstConveyor;
            RepairManager.model = undefined;
            RepairManager.placementId = "TestPlacementId";
            RepairManager.modelInfo = undefined;
            RepairManager.updated.fire();

            Packets.repairItem.fromClient(() => {
                Packets.itemRepairCompleted.toAllClients("TestPlacementId", "Perfect");
                return true;
            });
        }, []);

        return <RepairWindow />;
    },
);