/**
 * @fileoverview Reusable build button component for the build system
 *
 * A styled button component that matches the build window design with smooth animations,
 * hover effects, and scaling transitions.
 */

import React, { memo, useCallback, useRef, useState } from "@rbxts/react";
import { TweenService } from "@rbxts/services";
import BuildManager from "client/components/build/BuildManager";
import { RobotoSlabMedium } from "shared/asset/GameFonts";

interface BuildButtonProps {
    /** Button anchor point */
    anchorPoint?: Vector2;
    /** Button text label */
    text: string;
    /** Optional icon asset ID */
    icon?: string;
    /** Icon color (defaults to white with blue gradient) */
    iconColor?: Color3;
    /** Button layout order */
    layoutOrder?: number;
    /** Whether the button is currently visible */
    visible?: boolean;
    /** Click event handler */
    onClick?: () => void;
    /** Button position */
    position?: UDim2;
    /** Button size (defaults to standard build button size) */
    size?: UDim2;
    /** Custom background color */
    backgroundColor?: Color3;
    /** Whether the button is in a disabled state */
    disabled?: boolean;
}

const DEFAULT_BACKGROUND_COLOR = Color3.fromRGB(102, 102, 102);
const HOVER_COLOR_LERP = 0.1;
const SCALE_TWEEN_INFO = new TweenInfo(0.3, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out);

/**
 * Styled button component for the build system with animations and hover effects
 */
function BuildButton({
    anchorPoint,
    text,
    icon,
    iconColor = Color3.fromRGB(255, 255, 255),
    layoutOrder,
    visible = true,
    onClick,
    position,
    size = new UDim2(0.5, 0, 1, 0),
    backgroundColor = DEFAULT_BACKGROUND_COLOR,
    disabled = false,
}: BuildButtonProps) {
    anchorPoint ??= icon ? new Vector2(0, 1) : new Vector2(1, 1);
    position ??= icon ? new UDim2(0, 0, 1, 0) : new UDim2(1, 0, 1, 0);
    const buttonRef = useRef<TextButton>();
    const scaleRef = useRef<UIScale>();
    const [isHovered, setIsHovered] = useState(false);
    const [isPressed, setIsPressed] = useState(false);
    const animationsEnabled = BuildManager.animationsEnabled;

    const handleClick = useCallback(() => {
        if (disabled || !onClick) return;

        if (animationsEnabled) {
            const scale = scaleRef.current;
            if (scale) {
                // Quick press animation
                scale.Scale = 0.9;
                TweenService.Create(scale, SCALE_TWEEN_INFO, { Scale: 1 }).Play();
            }
        }

        onClick();
    }, [disabled, onClick, animationsEnabled]);

    const handleMouseEnter = useCallback(() => {
        if (disabled) return;
        setIsHovered(true);
    }, [disabled]);

    const handleMouseLeave = useCallback(() => {
        setIsHovered(false);
        setIsPressed(false);
    }, []);

    const handleMouseDown = useCallback(() => {
        if (disabled) return;
        setIsPressed(true);
    }, [disabled]);

    const handleMouseUp = useCallback(() => {
        setIsPressed(false);
    }, []);

    // Calculate button color based on state
    const currentBackgroundColor = disabled
        ? backgroundColor.Lerp(Color3.fromRGB(0, 0, 0), 0.5)
        : isPressed
          ? backgroundColor.Lerp(Color3.fromRGB(0, 0, 0), 0.2)
          : isHovered
            ? backgroundColor.Lerp(Color3.fromRGB(255, 255, 255), HOVER_COLOR_LERP)
            : backgroundColor;

    return (
        <textbutton
            ref={buttonRef}
            AnchorPoint={anchorPoint}
            BackgroundColor3={currentBackgroundColor}
            BorderColor3={Color3.fromRGB(27, 42, 53)}
            ClipsDescendants={true}
            LayoutOrder={layoutOrder}
            Position={position}
            Selectable={false}
            Size={size}
            Text=""
            Visible={visible}
            AutoButtonColor={false}
            Event={{
                Activated: handleClick,
                MouseEnter: handleMouseEnter,
                MouseLeave: handleMouseLeave,
                MouseButton1Down: handleMouseDown,
                MouseButton1Up: handleMouseUp,
            }}
        >
            <uicorner CornerRadius={new UDim(0, 4)} />
            <uistroke
                ApplyStrokeMode={Enum.ApplyStrokeMode.Border}
                Color={disabled ? Color3.fromRGB(40, 40, 40) : Color3.fromRGB(77, 77, 77)}
            />
            <uigradient
                Color={
                    new ColorSequence([
                        new ColorSequenceKeypoint(0, Color3.fromRGB(35, 35, 35)),
                        new ColorSequenceKeypoint(1, Color3.fromRGB(89, 89, 89)),
                    ])
                }
                Rotation={270}
            />

            {icon ? (
                <imagelabel
                    AnchorPoint={new Vector2(0.5, 0.5)}
                    BackgroundTransparency={1}
                    Image={icon}
                    ImageColor3={disabled ? iconColor.Lerp(Color3.fromRGB(0, 0, 0), 0.5) : iconColor}
                    Position={new UDim2(0.5, 0, 0.5, 0)}
                    ScaleType={Enum.ScaleType.Fit}
                    Size={new UDim2(0.7, 0, 0.7, 0)}
                    SizeConstraint={Enum.SizeConstraint.RelativeYY}
                >
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                                new ColorSequenceKeypoint(1, Color3.fromRGB(175, 199, 255)),
                            ])
                        }
                        Rotation={90}
                    />
                </imagelabel>
            ) : undefined}

            <textlabel
                AutomaticSize={Enum.AutomaticSize.X}
                BackgroundTransparency={1}
                FontFace={RobotoSlabMedium}
                Size={icon ? new UDim2(0, 0, 0.55, 0) : new UDim2(1, 0, 1, 0)}
                Position={icon ? new UDim2(0, 0, 0, 0) : new UDim2(0, 0, 0, 0)}
                Text={text}
                TextColor3={disabled ? Color3.fromRGB(120, 120, 120) : Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextSize={14}
                TextWrapped={true}
            >
                <uistroke Thickness={2} />
                <uipadding PaddingLeft={new UDim(0, 10)} />
            </textlabel>

            <uilistlayout
                FillDirection={Enum.FillDirection.Horizontal}
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                SortOrder={Enum.SortOrder.LayoutOrder}
                VerticalAlignment={Enum.VerticalAlignment.Center}
            />

            {/* Padding */}
            {icon ? undefined : (
                <uipadding
                    PaddingBottom={new UDim(0, 5)}
                    PaddingLeft={new UDim(0, 5)}
                    PaddingRight={new UDim(0, 5)}
                    PaddingTop={new UDim(0, 5)}
                />
            )}

            {/* Scale for animations */}
            <uiscale ref={scaleRef} Scale={1} />
        </textbutton>
    );
}

export default memo(BuildButton);


import React from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import BuildManager from "client/components/build/BuildManager";
import BuildWindow from "client/components/build/BuildWindow";
import StoryMocking from "client/components/StoryMocking";
import useVisibility from "client/hooks/useVisibility";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            visible: true,
            hasSelection: true,
            isRestricted: false,
            animationsEnabled: true,
        },
    },
    (props) => {
        StoryMocking.mockData();
        useVisibility("Build", props.controls.visible);
        BuildManager.animationsEnabled = props.controls.animationsEnabled;

        return (
            <BuildWindow
                getRestricted={() => props.controls.isRestricted}
                hasSelection={() => props.controls.hasSelection}
            />
        );
    },
);


/**
 * @fileoverview Main build window React component
 *
 * Displays build mode controls including deselect, rotate, delete, and place buttons.
 * Manages visibility, animations, and user interactions for the build system.
 */

import React, { useEffect, useRef } from "@rbxts/react";
import { Environment } from "@rbxts/ui-labs";
import BuildButton from "client/components/build/BuildButton";
import BuildManager from "client/components/build/BuildManager";
import loadBuildProcessing from "client/components/build/loadBuildProcessing";
import useHotkeyWithTooltip from "client/components/hotkeys/useHotkeyWithTooltip";
import { useDocument } from "client/components/window/DocumentManager";
import { getAsset } from "shared/asset/AssetMap";

/**
 * Main build window component that displays build controls
 */
export default function BuildWindow({
    hasSelection = BuildManager.hasSelection,
    getRestricted = BuildManager.getRestricted,
}: {
    hasSelection?: () => boolean;
    getRestricted?: () => boolean;
}) {
    const ref = useRef<Frame>();
    const { visible } = useDocument({ id: "Build", priority: -1 });
    const openPosition = new UDim2(0.5, 0, 0.95, -5);
    const closedPosition = new UDim2(0.5, 0, 1.2, 0);

    useEffect(() => {
        if (visible) {
            ref.current?.TweenPosition(openPosition, Enum.EasingDirection.Out, Enum.EasingStyle.Quad, 0.2, true);
        } else {
            ref.current?.TweenPosition(closedPosition, Enum.EasingDirection.Out, Enum.EasingStyle.Quad, 0.2, true);
        }
    }, [visible]);

    useEffect(() => {
        return BuildManager.init();
    }, []);

    useEffect(() => {
        const { destroy } = loadBuildProcessing();
        return destroy;
    }, []);

    const { events: deselectEvents } = useHotkeyWithTooltip({
        label: "Deselect Build",
        action: () => {
            if (!hasSelection() || getRestricted()) return false;
            BuildManager.revertSelected();
            BuildManager.deselectAll();
            return true;
        },
    });

    const { events: rotateEvents } = useHotkeyWithTooltip({
        label: "Rotate Build",
        action: () => {
            if (!hasSelection() || getRestricted()) return false;
            BuildManager.rotateSelection();
            return true;
        },
    });

    const { events: deleteEvents } = useHotkeyWithTooltip({
        label: "Delete Build",
        action: () => {
            if (!hasSelection() || getRestricted()) return false;
            BuildManager.deleteSelection();
            return true;
        },
    });

    return (
        <frame
            ref={ref}
            Active={true}
            AnchorPoint={new Vector2(0.5, 1)}
            BackgroundTransparency={1}
            Position={closedPosition}
            Size={new UDim2(0.3, 0, 0, 75)}
        >
            {/* Deselect button */}
            <BuildButton
                anchorPoint={new Vector2(0.5, 0)}
                text="Deselect"
                position={new UDim2(0.5, 0, 0.05, 0)}
                size={new UDim2(0.7, 0, 0.4, -2)}
                onClick={deselectEvents.Activated}
            />

            {/* Options container */}
            <frame
                AnchorPoint={new Vector2(0, 1)}
                BackgroundTransparency={1}
                Position={new UDim2(0, 0, 1, 0)}
                Size={new UDim2(1, 0, 0.5, -2)}
            >
                {/* Rotate button */}
                <BuildButton
                    text="Rotate"
                    icon={getAsset("assets/building/Rotate.png")}
                    layoutOrder={4}
                    onClick={rotateEvents.Activated}
                />

                {/* Place button (only visible on touch devices) */}
                <BuildButton
                    text="Place"
                    icon={getAsset("assets/building/Place.png")}
                    iconColor={Color3.fromRGB(170, 255, 127)}
                    layoutOrder={5}
                    visible={Environment.UserInput.TouchEnabled}
                    onClick={() => {
                        BuildManager.placeSelection();
                    }}
                />

                {/* Delete button */}
                <BuildButton
                    text="Delete"
                    icon={getAsset("assets/building/Delete.png")}
                    iconColor={Color3.fromRGB(255, 70, 70)}
                    layoutOrder={6}
                    onClick={deleteEvents.Activated}
                />

                {/* Layout for option buttons */}
                <uilistlayout
                    FillDirection={Enum.FillDirection.Horizontal}
                    HorizontalAlignment={Enum.HorizontalAlignment.Center}
                    Padding={new UDim(0, 5)}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                    VerticalAlignment={Enum.VerticalAlignment.Center}
                />
            </frame>
        </frame>
    );
}


//!native
//!optimize 2
import { CollectionService, RunService, Workspace } from "@rbxts/services";
import { Environment } from "@rbxts/ui-labs";
import BuildManager from "client/components/build/BuildManager";
import { cancelCollision, requestCollision } from "client/parallel/build/PlacementCollisionWorker";
import { ASSETS } from "shared/asset/GameAssets";
import { CAMERA, PLACED_ITEMS_FOLDER } from "shared/constants";
import Conveyor from "shared/item/traits/conveyor/Conveyor";
import Items from "shared/items/Items";
import ItemPlacement from "shared/placement/ItemPlacement";
import Sandbox from "shared/Sandbox";
import Area from "shared/world/Area";

declare global {
    interface Assets {
        ChargerRing: Beam;
    }
}

const COLLISION_COLOR = BuildManager.COLLISION_COLOR;
const NONCOLLISION_COLOR = BuildManager.NONCOLLISION_COLOR;

/**
 * Retrieves an instance of a given class and name from a parent, or creates it if missing.
 * @param parent The parent instance to search within.
 * @param className The class name of the instance to retrieve.
 * @param name The name of the instance to retrieve.
 * @returns The found or newly created instance.
 */
function retrieve<T extends ConstructorParameters<typeof Instance>[0]>(parent: Instance, className: T, name: string) {
    let instance = parent.FindFirstChild(name);
    if (instance === undefined) {
        instance = new Instance(className);
        instance.Name = name;
        instance.Parent = parent;
    }
    return instance as Instances[T];
}

const KEY = "BuildProcessing";

export default function loadBuildProcessing() {
    const sandboxEnabled = Sandbox.getEnabled();
    const updatePerModel = new Map<Model, () => void>();
    const cleanupPerModel = new Map<Model, () => void>();
    let area: Area | undefined;

    // Continuously load models for placed items in the workspace
    RunService.BindToRenderStep(KEY, 1, () => {
        for (const child of PLACED_ITEMS_FOLDER.GetChildren()) {
            onModelAdded(child);
        }
    });

    /**
     * Handles logic when a model is added to the workspace for placement or display.
     * Sets up visuals, collision, and trait-specific effects.
     * @param model The model instance being added.
     */
    function onModelAdded(model: Instance) {
        if (!model.IsA("Model") || updatePerModel.has(model)) return;
        const itemId = model.GetAttribute("ItemId") as string;
        const item = Items.getItem(itemId);
        if (item === undefined) throw "Model " + model.Name + " does not have item defined";

        const isPlacing = model.GetAttribute("Selected") === true;
        const hitbox = model.PrimaryPart;
        if (hitbox === undefined) return;
        const conveyor = item.findTrait("Conveyor");
        const arrows = new Set<Beam>();
        const connections = new Set<RBXScriptConnection>();
        let pendingCollisionJob: number | undefined;
        let collisionRequestVersion = 0;

        if (conveyor !== undefined) {
            const loadedArrows = Conveyor.loadConveyorArrow(model, conveyor);
            for (const [, { beam }] of loadedArrows) {
                beam.Enabled = isPlacing;
                arrows.add(beam);
            }
        }

        /**
         * Loads and configures a child instance of the model for placement visuals.
         * @param instance The instance to process.
         */
        function loadInstance(instance: Instance) {
            // Remove certain GUIs and beams during placement
            if (
                instance.IsA("BillboardGui") ||
                (instance.IsA("Beam") && conveyor === undefined) ||
                instance.IsA("SurfaceGui")
            ) {
                if (isPlacing === true) instance.Destroy();
            }
            // Make parts semi-transparent and non-collidable during placement
            else if (instance.IsA("BasePart") && isPlacing === true) {
                instance.CanCollide = false;
                instance.Transparency = 1 - (1 - instance.Transparency) / 2;
                instance.CastShadow = false;
            }
            // For nested models, ensure all parts are non-collidable during placement
            else if (instance.IsA("Model") && isPlacing) {
                const children = instance.GetChildren();
                for (const child of children) {
                    if (child.IsA("BasePart")) {
                        child.CanCollide = false;
                    }
                }
            }
        }

        // Process all children for placement visuals
        model.GetChildren().forEach(loadInstance);

        if (isPlacing === true) {
            // Snap model to mouse if in a valid area
            if (area !== undefined && item.placeableAreas.has(area)) {
                const mouseLocation = Environment.UserInput.GetMouseLocation();
                const viewRay = CAMERA.ViewportPointToRay(mouseLocation.X, mouseLocation.Y);
                const ray = Workspace.Raycast(viewRay.Origin, viewRay.Direction.mul(1000));
                if (ray !== undefined) {
                    model.PivotTo(new CFrame(ray.Position));
                }
            }
            // Show charger ring VFX if item has Charger trait
            const charger = item.findTrait("Charger");
            if (charger !== undefined) {
                task.spawn(() => {
                    // Wait for connection VFX to exist (handles streaming delay)
                    const connectionVFX = model.WaitForChild("ConnectionVFX", 1);

                    // Use item difficulty color or fallback
                    const color =
                        (connectionVFX?.FindFirstChild("w") as Beam | undefined)?.Color ??
                        new ColorSequence(item.difficulty.color ?? new Color3());
                    const ring0 = ASSETS.ChargerRing.Clone();
                    const ring1 = ASSETS.ChargerRing.Clone();
                    const diameter = (charger.radius ?? 0) * 2 + hitbox.Size.X;
                    const l0 = (11 / 18) * diameter;
                    const l1 = (-12 / 18) * diameter;
                    const attachment0 = new Instance("Attachment");
                    const attachment1 = new Instance("Attachment");
                    const yOffset = -(hitbox.Size.Y / 2);
                    attachment0.Position = new Vector3(0, yOffset, diameter / 2 - 0.25);
                    attachment1.Position = new Vector3(0, yOffset, -(diameter / 2 - 0.25));
                    attachment0.Parent = hitbox;
                    attachment1.Parent = hitbox;
                    ring0.Color = color;
                    ring1.Color = color;
                    ring0.CurveSize0 = l0;
                    ring0.CurveSize1 = l1;
                    ring1.CurveSize0 = l1;
                    ring1.CurveSize1 = l0;
                    ring0.Attachment0 = attachment0;
                    ring0.Attachment1 = attachment1;
                    ring1.Attachment0 = attachment0;
                    ring1.Attachment1 = attachment1;
                    ring0.Parent = hitbox;
                    ring1.Parent = hitbox;
                    connectionVFX?.Destroy();
                });
            }

            const indicator = hitbox.WaitForChild("Indicator") as BasePart;
            collisionRequestVersion = 0;

            const applyPlacementState = (canPlace: boolean) => {
                const selectionBox = indicator.FindFirstChildOfClass("SelectionBox");
                if (selectionBox === undefined) return;

                if (canPlace) {
                    selectionBox.Color3 = NONCOLLISION_COLOR;
                    selectionBox.SurfaceColor3 = NONCOLLISION_COLOR;
                } else {
                    selectionBox.Color3 = COLLISION_COLOR;
                    selectionBox.SurfaceColor3 = COLLISION_COLOR;
                }
            };

            const evaluatePlacement = (colliding: boolean) => {
                let canPlace = !colliding;
                if (canPlace && !sandboxEnabled && !ItemPlacement.isInPlaceableArea(model, item)) {
                    canPlace = false;
                }
                applyPlacementState(canPlace);
            };

            const queueCollisionCheck = () => {
                const currentVersion = ++collisionRequestVersion;

                if (pendingCollisionJob !== undefined) {
                    cancelCollision(pendingCollisionJob);
                    pendingCollisionJob = undefined;
                }

                pendingCollisionJob = requestCollision(model, (colliding) => {
                    if (currentVersion !== collisionRequestVersion) return;
                    pendingCollisionJob = undefined;
                    evaluatePlacement(colliding);
                });
            };

            const collisionConnection = hitbox.GetPropertyChangedSignal("CFrame").Connect(() => {
                queueCollisionCheck();
            });
            connections.add(collisionConnection);
            queueCollisionCheck();
        }

        let previousTransparency = 1;

        /**
         * Updates hitbox and selection visuals based on model state and workspace attributes.
         */
        const update = () => {
            // Only allow hitbox queries if not placing and in build mode
            hitbox.CanQuery = (Workspace.GetAttribute("BuildMode") as boolean) && !isPlacing;

            // Hide hitbox indicator if currently placing
            if (isPlacing) return;

            // Calculate transparency based on dragging, hovering, and build mode
            let transparency = 1;
            if (model.GetAttribute("Dragging") === true) transparency *= 0.6;

            if (model.GetAttribute("Hovering") === true) transparency *= 0.5;

            if (Workspace.GetAttribute("BuildMode") === true) transparency *= 0.7;

            if (transparency === previousTransparency) return;
            previousTransparency = transparency;

            // Show or hide selection box based on transparency
            if (transparency < 1) {
                const selectionBox = retrieve(hitbox, "SelectionBox", "HitboxIndicator");
                selectionBox.Adornee = hitbox;
                selectionBox.Transparency = transparency;
                selectionBox.SurfaceTransparency = transparency * 1.3 + 0.25;
                selectionBox.Color3 = NONCOLLISION_COLOR;
                selectionBox.SurfaceColor3 = NONCOLLISION_COLOR;
            } else {
                hitbox.FindFirstChild("HitboxIndicator")?.Destroy();
            }

            // Show/hide conveyor arrows based on transparency
            const arrowsVisible = transparency < 0.7;
            for (const arrow of arrows) {
                arrow.Enabled = arrowsVisible;
            }
        };

        // Listen for relevant attribute changes to update visuals
        connections.add(model.GetAttributeChangedSignal("Dragging").Connect(update));
        connections.add(model.GetAttributeChangedSignal("Hovering").Connect(update));
        connections.add(
            model.Destroying.Once(() => {
                updatePerModel.delete(model);
                cleanupPerModel.delete(model);
            }),
        );
        updatePerModel.set(model, update);
        cleanupPerModel.set(model, () => {
            for (const conn of connections) {
                conn.Disconnect();
            }
            for (const arrow of arrows) {
                arrow.Destroy();
            }
            if (pendingCollisionJob !== undefined) {
                cancelCollision(pendingCollisionJob);
                pendingCollisionJob = undefined;
            }
        });
        update();
    }

    type Interactive = Instance & { Enabled: boolean };

    // Disable interactives during build mode
    const buildConnection = Workspace.GetAttributeChangedSignal("BuildMode").Connect(() => {
        for (const [, update] of updatePerModel) {
            update();
        }
        for (const interactive of CollectionService.GetTagged("Interactive")) {
            (interactive as Interactive).Enabled = !Workspace.GetAttribute("BuildMode") as boolean;
        }
    });
    // Ensure new interactives are disabled if build mode is active
    const interactiveConnection = CollectionService.GetInstanceAddedSignal("Interactive").Connect((interactive) => {
        (interactive as Interactive).Enabled = !Workspace.GetAttribute("BuildMode") as boolean;
    });

    return {
        destroy: () => {
            RunService.UnbindFromRenderStep(KEY);
            buildConnection.Disconnect();
            interactiveConnection.Disconnect();
            updatePerModel.clear();
            for (const [, cleanup] of cleanupPerModel) {
                cleanup();
            }
        },
    };
}


/**
 * @fileoverview UI Labs story for ChallengeCompletionNotification component.
 * Provides interactive controls for testing epic challenge completion animations.
 */

import React, { Fragment, useEffect } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { Workspace } from "@rbxts/services";
import { CreateReactStory, Number } from "@rbxts/ui-labs";
import { CHALLENGE_PER_ID } from "shared/Challenge";
import { LOCAL_PLAYER } from "shared/constants";
import ChallengesBoard from "shared/items/bonuses/ChallengesBoard";
import Packets from "shared/Packets";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            empireLevel: Number(1, 0, 100, 1),
            currentChallengeLevel: Number(0, 0, 10, 1),
        },
    },
    (props) => {
        Packets.level.set(props.controls.empireLevel);

        const levelPerChallenge = new Map<string, number>();
        for (const [challengeId] of CHALLENGE_PER_ID) {
            levelPerChallenge.set(challengeId, props.controls.currentChallengeLevel);
        }
        Packets.currentLevelPerChallenge.set(levelPerChallenge);

        Packets.startChallenge.fromClient((_, challengeId) => {
            return (
                CHALLENGE_PER_ID.get(challengeId)!.requiredEmpireLevel(props.controls.currentChallengeLevel + 1) <=
                props.controls.empireLevel
            );
        });

        useEffect(() => {
            const model = ChallengesBoard.createModel({
                item: ChallengesBoard.id,
                posX: 0,
                posY: 50,
                posZ: -5,
                rotX: 0,
                rotY: 0,
                rotZ: 0,
            });
            if (model) {
                model.Parent = Workspace;
                ChallengesBoard.clientLoad(model, LOCAL_PLAYER!);
            }
            return () => {
                model?.Destroy();
            };
        }, []);

        return <Fragment />;
    },
);


/**
 * @fileoverview Challenge completion notification manager.
 * Listens for challenge completion packets and displays epic completion animations.
 */

import React, { Fragment, useEffect, useState } from "@rbxts/react";
import ChallengeCompletionNotification, {
    ChallengeCompletionData,
} from "client/components/challenge/ChallengeCompletionNotification";
import Packets from "shared/Packets";

interface CompletionQueueItem {
    id: string;
    challengeName: string;
    rewardText: string;
    challengeColors: {
        primary: Color3;
        secondary: Color3;
    };
    timestamp: number;
}

/**
 * Manager component that handles challenge completion notifications
 */
export default function ChallengeCompletionManager() {
    const [currentNotification, setCurrentNotification] = useState<ChallengeCompletionData | undefined>(undefined);
    const [completionQueue, setCompletionQueue] = useState<CompletionQueueItem[]>([]);

    // Listen for challenge completion packets
    useEffect(() => {
        const connection = Packets.challengeCompleted.fromServer((challengeName, rewardText) => {
            const newCompletion: CompletionQueueItem = {
                id: `completion_${tick()}_${math.random()}`,
                challengeName,
                rewardText,
                challengeColors: {
                    // Default epic colors - can be enhanced to get actual challenge colors
                    primary: Color3.fromRGB(255, 215, 0),
                    secondary: Color3.fromRGB(255, 140, 0),
                },
                timestamp: tick(),
            };

            setCompletionQueue((prev) => [...prev, newCompletion]);
        });

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

    // Process queue when no notification is currently showing
    useEffect(() => {
        if (!currentNotification && completionQueue.size() > 0) {
            const nextCompletion = completionQueue[0];
            setCurrentNotification({
                challengeName: nextCompletion.challengeName,
                rewardText: nextCompletion.rewardText,
                challengeColors: nextCompletion.challengeColors,
                visible: true,
            });

            // Remove from queue
            setCompletionQueue((prev) => prev.filter((item) => item.id !== nextCompletion.id));
        }
    }, [currentNotification, completionQueue]);

    const handleNotificationComplete = () => {
        setCurrentNotification(undefined);
    };

    if (!currentNotification) {
        return <Fragment />;
    }

    return <ChallengeCompletionNotification data={currentNotification} onComplete={handleNotificationComplete} />;
}


/**
 * @fileoverview UI Labs story for ChallengeCompletionNotification component.
 * Provides interactive controls for testing epic challenge completion animations.
 */

import React, { StrictMode, useState } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import ChallengeCompletionNotification, {
    ChallengeCompletionData,
} from "client/components/challenge/ChallengeCompletionNotification";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            challengeName: "Melting Economy I",
            rewardText: "Boost: x$1 > x$2",
            primaryColorR: 255,
            primaryColorG: 215,
            primaryColorB: 0,
            secondaryColorR: 255,
            secondaryColorG: 140,
            secondaryColorB: 0,
        },
    },
    (props) => {
        const [notificationData, setNotificationData] = useState<ChallengeCompletionData>({
            challengeName: props.controls.challengeName,
            rewardText: props.controls.rewardText,
            challengeColors: {
                primary: Color3.fromRGB(
                    props.controls.primaryColorR,
                    props.controls.primaryColorG,
                    props.controls.primaryColorB,
                ),
                secondary: Color3.fromRGB(
                    props.controls.secondaryColorR,
                    props.controls.secondaryColorG,
                    props.controls.secondaryColorB,
                ),
            },
            visible: false,
        });

        // Trigger challenge completion manually with button
        const triggerChallengeCompletion = () => {
            setNotificationData({
                challengeName: props.controls.challengeName,
                rewardText: props.controls.rewardText,
                challengeColors: {
                    primary: Color3.fromRGB(
                        props.controls.primaryColorR,
                        props.controls.primaryColorG,
                        props.controls.primaryColorB,
                    ),
                    secondary: Color3.fromRGB(
                        props.controls.secondaryColorR,
                        props.controls.secondaryColorG,
                        props.controls.secondaryColorB,
                    ),
                },
                visible: true,
            });
        };

        const handleComplete = () => {
            setNotificationData((prev) => ({ ...prev, visible: false }));
            print("Challenge completion notification finished!");
        };

        return (
            <StrictMode>
                <frame BackgroundTransparency={1} Size={new UDim2(1, 0, 1, 0)}>
                    {/* Background for context */}
                    <frame
                        BackgroundColor3={Color3.fromRGB(30, 30, 40)}
                        BorderSizePixel={0}
                        Size={new UDim2(1, 0, 1, 0)}
                    >
                        <uigradient
                            Color={
                                new ColorSequence([
                                    new ColorSequenceKeypoint(0, Color3.fromRGB(50, 50, 60)),
                                    new ColorSequenceKeypoint(1, Color3.fromRGB(20, 20, 30)),
                                ])
                            }
                            Rotation={45}
                        />
                    </frame>

                    <ChallengeCompletionNotification data={notificationData} onComplete={handleComplete} />

                    {/* Trigger Button */}
                    <textbutton
                        BackgroundColor3={Color3.fromRGB(255, 140, 0)}
                        BorderSizePixel={0}
                        Position={new UDim2(0, 50, 0, 50)}
                        Size={new UDim2(0, 250, 0, 60)}
                        Text="🏆 TRIGGER CHALLENGE COMPLETE!"
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextScaled={true}
                        ZIndex={5}
                        Event={{
                            Activated: triggerChallengeCompletion,
                        }}
                    >
                        <uicorner CornerRadius={new UDim(0, 8)} />
                        <uistroke Color={Color3.fromRGB(255, 215, 0)} Thickness={2} />
                        <uigradient
                            Color={
                                new ColorSequence([
                                    new ColorSequenceKeypoint(0, Color3.fromRGB(255, 180, 50)),
                                    new ColorSequenceKeypoint(1, Color3.fromRGB(255, 120, 0)),
                                ])
                            }
                            Rotation={45}
                        />
                    </textbutton>

                    {/* Control Info Display */}
                    <frame
                        BackgroundColor3={Color3.fromRGB(40, 40, 50)}
                        BorderSizePixel={0}
                        Position={new UDim2(0, 50, 0, 130)}
                        Size={new UDim2(0, 300, 0, 150)}
                        ZIndex={5}
                    >
                        <uicorner CornerRadius={new UDim(0, 8)} />
                        <textlabel
                            BackgroundTransparency={1}
                            Position={new UDim2(0, 10, 0, 5)}
                            Size={new UDim2(1, -20, 0, 25)}
                            Text={`Challenge: ${props.controls.challengeName}`}
                            TextColor3={Color3.fromRGB(255, 255, 255)}
                            TextSize={14}
                            TextXAlignment={Enum.TextXAlignment.Left}
                        />
                        <textlabel
                            BackgroundTransparency={1}
                            Position={new UDim2(0, 10, 0, 30)}
                            Size={new UDim2(1, -20, 0, 25)}
                            Text={`Reward: ${props.controls.rewardText}`}
                            TextColor3={Color3.fromRGB(255, 255, 255)}
                            TextSize={14}
                            TextXAlignment={Enum.TextXAlignment.Left}
                        />
                        <textlabel
                            BackgroundTransparency={1}
                            Position={new UDim2(0, 10, 0, 55)}
                            Size={new UDim2(1, -20, 0, 25)}
                            Text={`Primary: RGB(${props.controls.primaryColorR}, ${props.controls.primaryColorG}, ${props.controls.primaryColorB})`}
                            TextColor3={Color3.fromRGB(255, 255, 255)}
                            TextSize={14}
                            TextXAlignment={Enum.TextXAlignment.Left}
                        />
                        <textlabel
                            BackgroundTransparency={1}
                            Position={new UDim2(0, 10, 0, 80)}
                            Size={new UDim2(1, -20, 0, 25)}
                            Text={`Secondary: RGB(${props.controls.secondaryColorR}, ${props.controls.secondaryColorG}, ${props.controls.secondaryColorB})`}
                            TextColor3={Color3.fromRGB(255, 255, 255)}
                            TextSize={14}
                            TextXAlignment={Enum.TextXAlignment.Left}
                        />
                        <textlabel
                            BackgroundTransparency={1}
                            Position={new UDim2(0, 10, 0, 110)}
                            Size={new UDim2(1, -20, 0, 35)}
                            Text="Click the button to trigger the epic challenge completion animation!"
                            TextColor3={Color3.fromRGB(200, 200, 200)}
                            TextSize={12}
                            TextWrapped={true}
                            TextXAlignment={Enum.TextXAlignment.Left}
                        />
                    </frame>

                    {/* Instructions */}
                    <textlabel
                        AnchorPoint={new Vector2(1, 0)}
                        BackgroundColor3={Color3.fromRGB(40, 40, 40)}
                        BorderSizePixel={0}
                        Position={new UDim2(1, -10, 0, 10)}
                        Size={new UDim2(0, 350, 0, 180)}
                        Text={`Challenge Completion Demo:

• Challenge: ${props.controls.challengeName}
• Reward: ${props.controls.rewardText}
• Primary Color: RGB(${props.controls.primaryColorR}, ${props.controls.primaryColorG}, ${props.controls.primaryColorB})
• Secondary Color: RGB(${props.controls.secondaryColorR}, ${props.controls.secondaryColorG}, ${props.controls.secondaryColorB})

Click the "TRIGGER CHALLENGE COMPLETE!" button to start the epic animation sequence!

Adjust the controls to customize the challenge name, reward text, and colors, then trigger the animation to see your changes.`}
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextScaled={true}
                        TextWrapped={true}
                        TextXAlignment={Enum.TextXAlignment.Left}
                        TextYAlignment={Enum.TextYAlignment.Top}
                        ZIndex={10}
                    >
                        <uicorner CornerRadius={new UDim(0, 8)} />
                        <uipadding
                            PaddingBottom={new UDim(0, 8)}
                            PaddingLeft={new UDim(0, 8)}
                            PaddingRight={new UDim(0, 8)}
                            PaddingTop={new UDim(0, 8)}
                        />
                    </textlabel>

                    {/* Sample challenge items for visual context */}
                    <frame
                        AnchorPoint={new Vector2(0, 1)}
                        BackgroundColor3={Color3.fromRGB(60, 60, 70)}
                        BorderSizePixel={0}
                        Position={new UDim2(0, 10, 1, -10)}
                        Size={new UDim2(0, 200, 0, 100)}
                    >
                        <uicorner CornerRadius={new UDim(0, 8)} />
                        <textlabel
                            BackgroundTransparency={1}
                            FontSize={Enum.FontSize.Size18}
                            Size={new UDim2(1, 0, 1, 0)}
                            Text="🏆 Challenge System Demo
Epic completion animations with particles, sound effects, and gradient colors!"
                            TextColor3={Color3.fromRGB(255, 255, 255)}
                            TextWrapped={true}
                            TextYAlignment={Enum.TextYAlignment.Center}
                        >
                            <uipadding
                                PaddingBottom={new UDim(0, 8)}
                                PaddingLeft={new UDim(0, 8)}
                                PaddingRight={new UDim(0, 8)}
                                PaddingTop={new UDim(0, 8)}
                            />
                        </textlabel>
                    </frame>
                </frame>
            </StrictMode>
        );
    },
);


/**
 * @fileoverview Challenge completion notification component with epic animations and reward display.
 *
 * Features:
 * - Dramatic sliding entrance animation from top
 * - Glowing text effects with challenge colors
 * - Reward display with animated emphasis
 * - Victory particle effects radiating outward
 * - Sound feedback and screen effects
 * - Triumphant exit animation
 */

import React, { Fragment, useEffect, useRef, useState } from "@rbxts/react";
import { RunService, TweenService } from "@rbxts/services";
import { RobotoSlabBold, RobotoSlabHeavy } from "shared/asset/GameFonts";
import { getAsset } from "shared/asset/AssetMap";
import { playSound } from "shared/asset/GameAssets";

export interface ChallengeCompletionData {
    challengeName: string;
    rewardText: string;
    challengeColors: {
        primary: Color3;
        secondary: Color3;
    };
    visible: boolean;
}

interface ChallengeCompletionNotificationProps {
    data: ChallengeCompletionData;
    onComplete: () => void;
}

export default function ChallengeCompletionNotification({ data, onComplete }: ChallengeCompletionNotificationProps) {
    const mainFrameRef = useRef<Frame>();
    const challengeNameRef = useRef<TextLabel>();
    const titleTextRef = useRef<TextLabel>();
    const shineTextRef = useRef<TextLabel>();
    const glowRef = useRef<Frame>();
    const rewardContainerRef = useRef<Frame>();
    const [victoryParticles, setVictoryParticles] = useState<
        Array<{ id: string; angle: number; delay: number; speed: number }>
    >([]);
    const animationStarted = useRef(false);

    // Create victory particle effects
    const createVictoryParticles = () => {
        const particles: Array<{ id: string; angle: number; delay: number; speed: number }> = [];
        for (let i = 0; i < 12; i++) {
            particles.push({
                id: `victory_particle_${i}_${tick()}`,
                angle: (i / 12) * math.pi * 2,
                delay: math.random() * 0.4,
                speed: 0.8 + math.random() * 0.6,
            });
        }
        setVictoryParticles(particles);
    };

    // Animated shine effect for the title text with challenge colors
    const startShineAnimation = () => {
        if (!shineTextRef.current) return;

        const shineText = shineTextRef.current;
        const shineGradient = shineText.FindFirstChild("UIGradient") as UIGradient;

        if (!shineGradient) return;

        let shineConnection: RBXScriptConnection | undefined;
        let time = 0;

        shineConnection = RunService.Heartbeat.Connect((deltaTime) => {
            time += deltaTime * 2;

            const rotation = (time * 150) % 360;
            shineGradient.Rotation = rotation;

            const pulse = (math.sin(time * 3) + 1) / 2;
            const transparency = 0.5 + pulse * 0.4;
            shineText.TextTransparency = transparency;

            if (time > 3) {
                shineConnection?.Disconnect();
                shineText.TextTransparency = 0.9;
            }
        });
    };

    // Main animation sequence
    useEffect(() => {
        if (!data.visible || !mainFrameRef.current || animationStarted.current) return;

        animationStarted.current = true;
        const mainFrame = mainFrameRef.current;
        const challengeName = challengeNameRef.current;
        const rewardContainer = rewardContainerRef.current;
        const glow = glowRef.current;

        // Play challenge completion sound
        playSound("QuestComplete.mp3");

        // Initial setup - start off-screen from top
        mainFrame.Position = new UDim2(0.5, 0, -0.5, 0);
        mainFrame.Size = new UDim2(0, 0, 0, 0);

        // Initialize text elements
        if (titleTextRef.current) {
            const titleFrame = titleTextRef.current.Parent as Frame;
            const textLabels = titleFrame.GetChildren().filter((child) => child.IsA("TextLabel")) as TextLabel[];

            textLabels.forEach((label) => {
                label.TextTransparency = 1;
                label.TextStrokeTransparency = 1;
            });
        }

        if (challengeName) {
            challengeName.TextTransparency = 1;
            challengeName.TextStrokeTransparency = 1;
        }

        if (rewardContainer) {
            rewardContainer.BackgroundTransparency = 1;
            const rewardElements = rewardContainer
                .GetDescendants()
                .filter((child) => child.IsA("TextLabel") || child.IsA("ImageLabel"));
            rewardElements.forEach((element) => {
                if (element.IsA("TextLabel")) {
                    element.TextTransparency = 1;
                } else if (element.IsA("ImageLabel")) {
                    element.ImageTransparency = 1;
                }
            });
        }

        if (glow) {
            glow.BackgroundTransparency = 1;
        }

        // Create victory particles
        createVictoryParticles();

        // Phase 1: Epic entrance from top with bounce (0.8s)
        const entranceTween = TweenService.Create(
            mainFrame,
            new TweenInfo(0.8, Enum.EasingStyle.Bounce, Enum.EasingDirection.Out),
            {
                Position: new UDim2(0.5, 0, 0.2, 20), // More spacing from top + offset
                Size: new UDim2(0.35, 0, 0.22, 0), // Slightly smaller for better fit
            },
        );

        // Phase 2: Text and glow dramatic reveal (0.5s, starts 0.4s after entrance)
        const contentReveal = () => {
            if (titleTextRef.current) {
                const titleFrame = titleTextRef.current.Parent as Frame;
                const textLabels = titleFrame.GetChildren().filter((child) => child.IsA("TextLabel")) as TextLabel[];

                textLabels.forEach((label) => {
                    TweenService.Create(label, new TweenInfo(0.5, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {
                        TextTransparency: label === shineTextRef.current ? 0.7 : 0,
                        TextStrokeTransparency: 0,
                    }).Play();
                });
            }

            if (challengeName) {
                TweenService.Create(
                    challengeName,
                    new TweenInfo(0.5, Enum.EasingStyle.Back, Enum.EasingDirection.Out),
                    {
                        TextTransparency: 0,
                        TextStrokeTransparency: 0,
                    },
                ).Play();
            }

            if (glow) {
                TweenService.Create(glow, new TweenInfo(0.5, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {
                    BackgroundTransparency: 0.3,
                }).Play();
            }

            // Start epic shine animation
            task.delay(0.5, startShineAnimation);
        };

        // Phase 3: Reward display with emphasis (0.6s, starts 1s after entrance)
        const showRewards = () => {
            if (rewardContainer) {
                TweenService.Create(
                    rewardContainer,
                    new TweenInfo(0.4, Enum.EasingStyle.Back, Enum.EasingDirection.Out),
                    {
                        BackgroundTransparency: 0.1,
                    },
                ).Play();

                const rewardElements = rewardContainer
                    .GetDescendants()
                    .filter((child) => child.IsA("TextLabel") || child.IsA("ImageLabel"));
                rewardElements.forEach((element, index) => {
                    task.delay(index * 0.15, () => {
                        if (element.IsA("TextLabel")) {
                            TweenService.Create(
                                element,
                                new TweenInfo(0.4, Enum.EasingStyle.Elastic, Enum.EasingDirection.Out),
                                {
                                    TextTransparency: 0,
                                },
                            ).Play();
                        } else if (element.IsA("ImageLabel")) {
                            TweenService.Create(
                                element,
                                new TweenInfo(0.4, Enum.EasingStyle.Elastic, Enum.EasingDirection.Out),
                                {
                                    ImageTransparency: 0,
                                },
                            ).Play();
                        }
                    });
                });
            }
        };

        // Phase 4: Pulsing challenge-colored glow effect (continuous for 2.5s)
        const startGlowPulse = () => {
            if (!glow) return;

            let pulseConnection: RBXScriptConnection | undefined;
            let time = 0;

            pulseConnection = RunService.Heartbeat.Connect((deltaTime) => {
                time += deltaTime;
                if (time > 2.5) {
                    pulseConnection?.Disconnect();
                    return;
                }

                const pulse = (math.sin(time * 4) + 1) / 2;
                const transparency = 0.1 + pulse * 0.4;

                // Alternate between challenge colors
                const colorLerp = (math.sin(time * 2) + 1) / 2;
                const color = data.challengeColors.primary.Lerp(data.challengeColors.secondary, colorLerp);

                glow.BackgroundTransparency = transparency;
                glow.BackgroundColor3 = color;
            });
        };

        // Phase 5: Triumphant exit animation (0.6s, starts after 5s total)
        const exitAnimation = () => {
            const exitTween = TweenService.Create(
                mainFrame,
                new TweenInfo(0.6, Enum.EasingStyle.Back, Enum.EasingDirection.In),
                {
                    Position: new UDim2(0.5, 0, -0.5, 0),
                    Size: new UDim2(0.2, 0, 0.1, 0),
                },
            );

            // Fade out all elements with stagger
            if (titleTextRef.current) {
                const titleFrame = titleTextRef.current.Parent as Frame;
                const textLabels = titleFrame.GetChildren().filter((child) => child.IsA("TextLabel")) as TextLabel[];

                textLabels.forEach((label, index) => {
                    task.delay(index * 0.1, () => {
                        TweenService.Create(label, new TweenInfo(0.6), {
                            TextTransparency: 1,
                            TextStrokeTransparency: 1,
                        }).Play();
                    });
                });
            }

            if (challengeName) {
                TweenService.Create(challengeName, new TweenInfo(0.6), {
                    TextTransparency: 1,
                    TextStrokeTransparency: 1,
                }).Play();
            }

            if (rewardContainer) {
                TweenService.Create(rewardContainer, new TweenInfo(0.6), {
                    BackgroundTransparency: 1,
                }).Play();

                const rewardElements = rewardContainer
                    .GetDescendants()
                    .filter((child) => child.IsA("TextLabel") || child.IsA("ImageLabel"));
                rewardElements.forEach((element, index) => {
                    task.delay(index * 0.05, () => {
                        if (element.IsA("TextLabel")) {
                            TweenService.Create(element, new TweenInfo(0.6), {
                                TextTransparency: 1,
                            }).Play();
                        } else if (element.IsA("ImageLabel")) {
                            TweenService.Create(element, new TweenInfo(0.6), {
                                ImageTransparency: 1,
                            }).Play();
                        }
                    });
                });
            }

            if (glow) {
                TweenService.Create(glow, new TweenInfo(0.6), {
                    BackgroundTransparency: 1,
                }).Play();
            }

            exitTween.Play();
            exitTween.Completed.Connect(() => {
                setVictoryParticles([]);
                onComplete();
            });
        };

        // Execute animation sequence
        entranceTween.Play();
        task.delay(0.4, contentReveal);
        task.delay(1, showRewards);
        task.delay(1.2, startGlowPulse);
        task.delay(5, exitAnimation);

        return () => {
            animationStarted.current = false;
        };
    }, [data.visible, data.challengeName, data.challengeColors, data.rewardText, onComplete]);

    if (!data.visible) {
        return <Fragment />;
    }

    return (
        <frame ref={mainFrameRef} AnchorPoint={new Vector2(0.5, 0.5)} BackgroundTransparency={1} ZIndex={10}>
            {/* Size constraints - more conservative for smaller screens */}
            <uisizeconstraint MaxSize={new Vector2(500, 300)} MinSize={new Vector2(280, 120)} />

            {/* Epic background glow effect */}
            <frame
                ref={glowRef}
                AnchorPoint={new Vector2(0.5, 0.5)}
                BackgroundColor3={data.challengeColors.primary}
                BackgroundTransparency={0.3}
                BorderSizePixel={0}
                Position={new UDim2(0.5, 0, 0.5, 0)}
                Size={new UDim2(1, 30, 1, 30)}
                ZIndex={-1}
            >
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, data.challengeColors.primary),
                            new ColorSequenceKeypoint(0.5, Color3.fromRGB(255, 255, 255)),
                            new ColorSequenceKeypoint(1, data.challengeColors.secondary),
                        ])
                    }
                    Rotation={45}
                />
                <uistroke Color={data.challengeColors.secondary} Thickness={2} />
            </frame>

            {/* Main content background */}
            <frame
                BackgroundColor3={Color3.fromRGB(25, 25, 35)}
                BorderColor3={data.challengeColors.primary}
                BorderSizePixel={3}
                Size={new UDim2(1, 0, 1, 0)}
            >
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(45, 45, 55)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(15, 15, 25)),
                        ])
                    }
                    Rotation={135}
                />
                <uistroke Color={data.challengeColors.secondary} Thickness={2} />
                <uipadding
                    PaddingBottom={new UDim(0, 15)}
                    PaddingLeft={new UDim(0, 15)}
                    PaddingRight={new UDim(0, 15)}
                    PaddingTop={new UDim(0, 15)}
                />

                {/* "CHALLENGE CONQUERED!" title with epic 3D effects */}
                <frame BackgroundTransparency={1} Size={new UDim2(1, 0, 0.4, 0)} ZIndex={2}>
                    {/* 3D Shadow layer */}
                    <textlabel
                        BackgroundTransparency={1}
                        FontFace={RobotoSlabHeavy}
                        Position={new UDim2(0, 3, 0, 3)}
                        Size={new UDim2(1, 0, 1, 0)}
                        Text="CHALLENGE CONQUERED!"
                        TextColor3={Color3.fromRGB(10, 10, 10)}
                        TextScaled={true}
                        TextStrokeColor3={Color3.fromRGB(0, 0, 0)}
                        TextStrokeTransparency={0.2}
                        TextTransparency={0.3}
                        ZIndex={1}
                    >
                        <uistroke Thickness={2} />
                    </textlabel>

                    {/* Main text layer */}
                    <textlabel
                        ref={titleTextRef}
                        BackgroundTransparency={1}
                        FontFace={RobotoSlabHeavy}
                        Position={new UDim2(0, 0, 0, 0)}
                        Size={new UDim2(1, 0, 1, 0)}
                        Text="CHALLENGE CONQUERED!"
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextScaled={true}
                        TextStrokeColor3={Color3.fromRGB(0, 0, 0)}
                        TextStrokeTransparency={0}
                        ZIndex={2}
                    >
                        <uistroke Thickness={3} />
                        <uigradient
                            Color={
                                new ColorSequence([
                                    new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                                    new ColorSequenceKeypoint(0.5, data.challengeColors.primary),
                                    new ColorSequenceKeypoint(1, data.challengeColors.secondary),
                                ])
                            }
                            Rotation={-45}
                        />
                    </textlabel>

                    {/* Epic shine overlay */}
                    <textlabel
                        ref={shineTextRef}
                        BackgroundTransparency={1}
                        FontFace={RobotoSlabHeavy}
                        Position={new UDim2(0, 0, 0, 0)}
                        Size={new UDim2(1, 0, 1, 0)}
                        Text="CHALLENGE CONQUERED!"
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextScaled={true}
                        TextTransparency={0.7}
                        ZIndex={3}
                    >
                        <uigradient
                            Color={
                                new ColorSequence([
                                    new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                                    new ColorSequenceKeypoint(1, Color3.fromRGB(255, 255, 255)),
                                ])
                            }
                            Transparency={
                                new NumberSequence([
                                    new NumberSequenceKeypoint(0, 1),
                                    new NumberSequenceKeypoint(0.3, 1),
                                    new NumberSequenceKeypoint(0.5, 0.1),
                                    new NumberSequenceKeypoint(0.7, 1),
                                    new NumberSequenceKeypoint(1, 1),
                                ])
                            }
                            Rotation={60}
                        />
                    </textlabel>
                </frame>

                {/* Challenge name */}
                <textlabel
                    ref={challengeNameRef}
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabBold}
                    Position={new UDim2(0, 0, 0.4, 0)}
                    Size={new UDim2(1, 0, 0.25, 0)}
                    Text={data.challengeName}
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextScaled={true}
                    TextStrokeColor3={Color3.fromRGB(0, 0, 0)}
                    TextStrokeTransparency={0}
                    ZIndex={2}
                >
                    <uistroke Thickness={2} />
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, data.challengeColors.primary),
                                new ColorSequenceKeypoint(1, data.challengeColors.secondary),
                            ])
                        }
                        Rotation={90}
                    />
                </textlabel>

                {/* Reward display */}
                <frame
                    ref={rewardContainerRef}
                    AnchorPoint={new Vector2(0.5, 0)}
                    BackgroundColor3={Color3.fromRGB(5, 5, 15)}
                    BackgroundTransparency={0.1}
                    BorderSizePixel={0}
                    LayoutOrder={3}
                    Position={new UDim2(0.5, 0, 0.7, 0)}
                    Size={new UDim2(0.95, 0, 0.25, 0)}
                >
                    <uistroke Color={data.challengeColors.primary} Thickness={2} />
                    <uigradient
                        Color={
                            new ColorSequence([
                                new ColorSequenceKeypoint(0, data.challengeColors.primary),
                                new ColorSequenceKeypoint(1, data.challengeColors.secondary),
                            ])
                        }
                        Rotation={180}
                        Transparency={
                            new NumberSequence([new NumberSequenceKeypoint(0, 0.8), new NumberSequenceKeypoint(1, 0.9)])
                        }
                    />

                    <textlabel
                        AnchorPoint={new Vector2(0, 0.5)}
                        BackgroundTransparency={1}
                        FontFace={RobotoSlabBold}
                        Text="Rewards"
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextScaled={true}
                        TextStrokeColor3={Color3.fromRGB(0, 0, 0)}
                        TextStrokeTransparency={0}
                        TextXAlignment={Enum.TextXAlignment.Left}
                        Position={new UDim2(0, 8, 0, 0)}
                        Size={new UDim2(0.2, 0, 1, 0)}
                    >
                        <uistroke Thickness={2} />
                    </textlabel>

                    <textlabel
                        AnchorPoint={new Vector2(0.5, 0.5)}
                        BackgroundTransparency={1}
                        FontFace={RobotoSlabBold}
                        Position={new UDim2(0.5, 0, 0.5, 0)}
                        Size={new UDim2(0.75, 0, 0.8, 0)}
                        Text={data.rewardText}
                        TextColor3={Color3.fromRGB(255, 215, 0)}
                        TextScaled={true}
                        TextStrokeColor3={Color3.fromRGB(0, 0, 0)}
                        TextStrokeTransparency={0}
                        TextWrapped={true}
                        ZIndex={2}
                    >
                        <uistroke Thickness={2} />
                        <uigradient
                            Color={
                                new ColorSequence([
                                    new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 100)),
                                    new ColorSequenceKeypoint(1, Color3.fromRGB(255, 215, 0)),
                                ])
                            }
                            Rotation={45}
                        />
                    </textlabel>
                </frame>
            </frame>

            {/* Victory particle effects */}
            {victoryParticles.map((particle) => (
                <VictoryParticle
                    key={particle.id}
                    angle={particle.angle}
                    delay={particle.delay}
                    speed={particle.speed}
                    color={data.challengeColors.primary}
                />
            ))}
        </frame>
    );
}

// Individual victory particle component
function VictoryParticle({
    angle,
    delay,
    speed,
    color,
}: {
    angle: number;
    delay: number;
    speed: number;
    color: Color3;
}) {
    const particleRef = useRef<ImageLabel>();

    useEffect(() => {
        if (!particleRef.current) return;

        const particle = particleRef.current;

        // Calculate initial position
        const centerX = 0.5;
        const centerY = 0.5;
        const distance = 80;
        const initialX = centerX + (math.cos(angle) * distance) / 600;
        const initialY = centerY + (math.sin(angle) * distance) / 400;

        particle.Position = new UDim2(initialX, 0, initialY, 0);
        particle.ImageTransparency = 1;
        particle.Size = new UDim2(0, 16, 0, 16);
        particle.ImageColor3 = color;

        // Delayed animation start
        task.delay(delay, () => {
            if (!particle.Parent) return;

            particle.ImageTransparency = 0;

            // Animate epic outward burst
            const burstTween = TweenService.Create(
                particle,
                new TweenInfo(2 * speed, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
                {
                    Position: new UDim2(
                        initialX + (math.cos(angle) * 150 * speed) / 600,
                        0,
                        initialY + (math.sin(angle) * 150 * speed) / 400,
                        0,
                    ),
                    ImageTransparency: 1,
                    Size: new UDim2(0, 8, 0, 8),
                    Rotation: 360,
                },
            );

            // Add sparkle effect
            const sparkleTween = TweenService.Create(
                particle,
                new TweenInfo(0.5, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut, 4),
                {
                    Size: new UDim2(0, 20, 0, 20),
                },
            );

            burstTween.Play();
            sparkleTween.Play();
        });

        return () => {
            if (particle.Parent) {
                TweenService.Create(particle, new TweenInfo(0.1), { ImageTransparency: 1 }).Play();
            }
        };
    }, [angle, delay, speed, color]);

    return (
        <imagelabel
            ref={particleRef}
            AnchorPoint={new Vector2(0.5, 0.5)}
            BackgroundTransparency={1}
            Image={getAsset("assets/Spark.png")}
            Size={new UDim2(0, 16, 0, 16)}
            ZIndex={8}
        />
    );
}


import React, { Fragment } from "@rbxts/react";
import useProperty from "client/hooks/useProperty";
import { RobotoSlabBold } from "shared/asset/GameFonts";
import { CHALLENGE_PER_ID } from "shared/Challenge";
import Packets from "shared/Packets";

/**
 * Displays current challenge information as a HUD overlay
 */
export default function ChallengeHudWindow() {
    const currentChallengeId = useProperty(Packets.currentChallenge);
    const currentLevel = useProperty(Packets.currentLevelPerChallenge).get(currentChallengeId) ?? 0;
    const currentChallenge = CHALLENGE_PER_ID.get(currentChallengeId);

    if (!currentChallenge) {
        return <Fragment />;
    }

    return (
        <frame
            AnchorPoint={new Vector2(0.5, 0)}
            AutomaticSize={Enum.AutomaticSize.Y}
            BackgroundTransparency={1}
            Position={new UDim2(0.5, 0, 0.078, 35)}
            Size={new UDim2(1, -300, 0, 0)}
        >
            <uilistlayout
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                SortOrder={Enum.SortOrder.LayoutOrder}
            />

            <textlabel
                AutomaticSize={Enum.AutomaticSize.Y}
                BackgroundTransparency={1}
                FontFace={RobotoSlabBold}
                Size={new UDim2(0.9, 0, 0, 0)}
                Text="You are currently in:"
                TextColor3={Color3.fromRGB(255, 103, 103)}
                TextSize={24}
                TextWrapped={true}
            >
                <uistroke Thickness={2} />
            </textlabel>

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

            {/* Challenge Description/Requirement */}
            <textlabel
                AutomaticSize={Enum.AutomaticSize.Y}
                BackgroundTransparency={1}
                FontFace={RobotoSlabBold}
                LayoutOrder={3}
                Size={new UDim2(0.9, 0, 0, 0)}
                Text={currentChallenge.description(currentLevel)}
                TextColor3={Color3.fromRGB(255, 103, 103)}
                TextSize={20}
                TextWrapped={true}
                TextYAlignment={Enum.TextYAlignment.Top}
            >
                <uistroke Thickness={2} />
                <uitextsizeconstraint MaxTextSize={24} />
            </textlabel>
        </frame>
    );
}


import React from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import { ChangelogGui } from "client/components/changelog/Changelog";

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


/**
 * @fileoverview Chest loot notification manager.
 * Listens for chest loot packets and displays smooth loot collection animations.
 */

import React, { Fragment, useEffect, useState } from "@rbxts/react";
import ChestLootNotification from "client/components/chest/ChestLootNotification";
import Packets from "shared/Packets";
import WorldNode from "shared/world/nodes/WorldNode";

/**
 * Manager component that handles chest loot notifications
 */
export default function ChestLootManager() {
    const [currentNotification, setCurrentNotification] = useState<
        { loot: Array<LootInfo>; visible: boolean } | undefined
    >(undefined);

    // Listen for loot packets and directly show notifications
    useEffect(() => {
        const connection = Packets.showLoot.fromServer((lootArray) => {
            setCurrentNotification({
                loot: lootArray,
                visible: true,
            });
        });

        const chestMarkerWorldNode = new WorldNode<BasePart>(
            "ChestMarker",
            (instance) => (instance.LocalTransparencyModifier = 1),
            (instance) => (instance.LocalTransparencyModifier = 0),
        );

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

    const handleNotificationComplete = () => {
        setCurrentNotification(undefined);
    };

    if (!currentNotification) {
        return <Fragment />;
    }

    return (
        <ChestLootNotification
            loot={currentNotification.loot}
            visible={currentNotification.visible}
            onComplete={handleNotificationComplete}
        />
    );
}


/**
 * @fileoverview UI Labs story for ChestLootNotification component.
 * Provides interactive controls for testing chest loot animations with item previews.
 */

import React, { StrictMode, useState } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import TooltipWindow from "client/components/tooltip/TooltipWindow";
import ChestLootNotification from "client/components/chest/ChestLootNotification";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            includeXP: true,
            xpAmount: 5,
            includeGrass: true,
            grassAmount: 3,
            includeStone: true,
            stoneAmount: 2,
            includeGem: false,
            gemAmount: 1,
        },
    },
    (props) => {
        const [lootData, setLootData] = useState<{ loot: Array<LootInfo>; visible: boolean }>({
            loot: [],
            visible: false,
        });

        const triggerLoot = () => {
            const loot: Array<LootInfo> = [];

            if (props.controls.includeXP) {
                loot.push({ id: "xp", amount: props.controls.xpAmount });
            }
            if (props.controls.includeGrass) {
                loot.push({ id: "Grass", amount: props.controls.grassAmount });
            }
            if (props.controls.includeStone) {
                loot.push({ id: "ExcavationStone", amount: props.controls.stoneAmount });
            }
            if (props.controls.includeGem) {
                loot.push({ id: "WhiteGem", amount: props.controls.gemAmount });
            }

            setLootData({
                loot: loot,
                visible: true,
            });
        };

        const handleComplete = () => {
            setLootData((prev) => ({ ...prev, visible: false }));
            print("Chest loot notification finished!");
        };

        return (
            <StrictMode>
                <TooltipWindow />
                <frame BackgroundTransparency={1} Size={new UDim2(1, 0, 1, 0)}>
                    {/* Background for context */}
                    <frame
                        BackgroundColor3={Color3.fromRGB(40, 50, 40)}
                        BorderSizePixel={0}
                        Size={new UDim2(1, 0, 1, 0)}
                    >
                        <uigradient
                            Color={
                                new ColorSequence([
                                    new ColorSequenceKeypoint(0, Color3.fromRGB(60, 70, 60)),
                                    new ColorSequenceKeypoint(1, Color3.fromRGB(30, 40, 30)),
                                ])
                            }
                            Rotation={45}
                        />
                    </frame>

                    <ChestLootNotification
                        loot={lootData.loot}
                        visible={lootData.visible}
                        onComplete={handleComplete}
                    />

                    {/* Trigger Button */}
                    <textbutton
                        BackgroundColor3={Color3.fromRGB(100, 150, 200)}
                        BorderSizePixel={0}
                        Position={new UDim2(0, 50, 0, 50)}
                        Size={new UDim2(0, 200, 0, 50)}
                        Text="📦 TRIGGER CHEST LOOT!"
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextScaled={true}
                        ZIndex={5}
                        Event={{
                            Activated: triggerLoot,
                        }}
                    >
                        <uicorner CornerRadius={new UDim(0, 8)} />
                        <uistroke Color={Color3.fromRGB(150, 200, 255)} Thickness={2} />
                    </textbutton>

                    {/* Control Info Display */}
                    <frame
                        BackgroundColor3={Color3.fromRGB(40, 40, 50)}
                        BorderSizePixel={0}
                        Position={new UDim2(0, 50, 0, 120)}
                        Size={new UDim2(0, 300, 0, 200)}
                        ZIndex={5}
                    >
                        <uicorner CornerRadius={new UDim(0, 8)} />
                        <textlabel
                            BackgroundTransparency={1}
                            Position={new UDim2(0, 10, 0, 5)}
                            Size={new UDim2(1, -20, 0, 25)}
                            Text={`XP: ${props.controls.includeXP ? `+${props.controls.xpAmount}` : "None"}`}
                            TextColor3={Color3.fromRGB(255, 255, 255)}
                            TextSize={14}
                            TextXAlignment={Enum.TextXAlignment.Left}
                        />
                        <textlabel
                            BackgroundTransparency={1}
                            Position={new UDim2(0, 10, 0, 30)}
                            Size={new UDim2(1, -20, 0, 25)}
                            Text={`Grass: ${props.controls.includeGrass ? `x${props.controls.grassAmount}` : "None"}`}
                            TextColor3={Color3.fromRGB(255, 255, 255)}
                            TextSize={14}
                            TextXAlignment={Enum.TextXAlignment.Left}
                        />
                        <textlabel
                            BackgroundTransparency={1}
                            Position={new UDim2(0, 10, 0, 55)}
                            Size={new UDim2(1, -20, 0, 25)}
                            Text={`Stone: ${props.controls.includeStone ? `x${props.controls.stoneAmount}` : "None"}`}
                            TextColor3={Color3.fromRGB(255, 255, 255)}
                            TextSize={14}
                            TextXAlignment={Enum.TextXAlignment.Left}
                        />
                        <textlabel
                            BackgroundTransparency={1}
                            Position={new UDim2(0, 10, 0, 80)}
                            Size={new UDim2(1, -20, 0, 25)}
                            Text={`Gem: ${props.controls.includeGem ? `x${props.controls.gemAmount}` : "None"}`}
                            TextColor3={Color3.fromRGB(255, 255, 255)}
                            TextSize={14}
                            TextXAlignment={Enum.TextXAlignment.Left}
                        />
                        <textlabel
                            BackgroundTransparency={1}
                            Position={new UDim2(0, 10, 0, 110)}
                            Size={new UDim2(1, -20, 0, 80)}
                            Text="Toggle the checkboxes to customize loot, then click the button to trigger the chest loot animation with 3D item previews!"
                            TextColor3={Color3.fromRGB(200, 200, 200)}
                            TextSize={12}
                            TextWrapped={true}
                            TextXAlignment={Enum.TextXAlignment.Left}
                            TextYAlignment={Enum.TextYAlignment.Top}
                        />
                    </frame>

                    {/* Instructions */}
                    <textlabel
                        AnchorPoint={new Vector2(1, 0)}
                        BackgroundColor3={Color3.fromRGB(40, 40, 40)}
                        BorderSizePixel={0}
                        Position={new UDim2(1, -10, 0, 10)}
                        Size={new UDim2(0, 300, 0, 120)}
                        Text={`Chest Loot Demo:

A more subdued notification for frequent chest openings. Features 3D viewport previews of items, organized loot display, and smooth animations.

Adjust the controls to test different loot combinations.`}
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextScaled={true}
                        TextWrapped={true}
                        TextXAlignment={Enum.TextXAlignment.Left}
                        TextYAlignment={Enum.TextYAlignment.Top}
                        ZIndex={10}
                    >
                        <uicorner CornerRadius={new UDim(0, 8)} />
                        <uipadding
                            PaddingBottom={new UDim(0, 8)}
                            PaddingLeft={new UDim(0, 8)}
                            PaddingRight={new UDim(0, 8)}
                            PaddingTop={new UDim(0, 8)}
                        />
                    </textlabel>
                </frame>
            </StrictMode>
        );
    },
);


import React, { Fragment, useEffect, useRef, useState } from "@rbxts/react";
import { TweenService } from "@rbxts/services";
import { useItemViewport } from "shared/item/ItemViewport";
import { TooltipManager } from "client/components/tooltip/TooltipWindow";
import { getAsset } from "shared/asset/AssetMap";
import { playSound } from "shared/asset/GameAssets";
import { RobotoSlab, RobotoSlabBold } from "shared/asset/GameFonts";
import Items from "shared/items/Items";

export default function ChestLootNotification({
    loot,
    visible,
    onComplete,
}: {
    loot: Array<LootInfo>;
    visible: boolean;
    onComplete: () => void;
}) {
    const mainFrameRef = useRef<Frame>();
    const animationStarted = useRef(false);
    const exitTweenRef = useRef<Tween>();
    const entranceTweenRef = useRef<Tween>();
    const exitTimeoutRef = useRef<thread>();

    // Main animation sequence
    useEffect(() => {
        if (!visible || !mainFrameRef.current) return;
        // If animation is already running, clean up and restart
        if (animationStarted.current) {
            // Cancel existing tweens and timeouts
            if (exitTweenRef.current) {
                exitTweenRef.current.Cancel();
                exitTweenRef.current = undefined;
            }
            if (entranceTweenRef.current) {
                entranceTweenRef.current.Cancel();
                entranceTweenRef.current = undefined;
            }
            if (exitTimeoutRef.current) {
                task.cancel(exitTimeoutRef.current);
                exitTimeoutRef.current = undefined;
            }
        }

        animationStarted.current = true;
        const mainFrame = mainFrameRef.current;

        // Play loot collection sound
        playSound("UnlockItem.mp3");

        // Initial setup - start off-screen from bottom with no size
        mainFrame.Position = new UDim2(0.5, 0, 1.2, 0);
        mainFrame.Size = new UDim2(0, 0, 0, 0);

        // Phase 1: Smooth slide-in from bottom with scale animation (0.4s)
        const entranceTween = TweenService.Create(
            mainFrame,
            new TweenInfo(0.4, Enum.EasingStyle.Back, Enum.EasingDirection.Out),
            {
                Position: new UDim2(0.5, 0, 0.85, 0), // Bottom area of screen
                Size: new UDim2(0, 0, 0.04, 60), // Scale up to normal size
            },
        );
        entranceTweenRef.current = entranceTween;

        // Phase 2: Hold for 2.5 seconds to let player see the loot

        // Phase 3: Fade out and slide down with scale down (0.3s, starts after 3s total)
        const exitAnimation = () => {
            if (!mainFrame.Parent) return; // Guard against disposed frame

            const exitTween = TweenService.Create(
                mainFrame,
                new TweenInfo(0.3, Enum.EasingStyle.Quart, Enum.EasingDirection.In),
                {
                    Position: new UDim2(0.5, 0, 1.2, 0),
                    Size: new UDim2(0, 0, 0.02, 40), // Scale down while exiting
                },
            );
            exitTweenRef.current = exitTween;

            exitTween.Play();
            exitTween.Completed.Connect(() => {
                if (exitTweenRef.current === exitTween) {
                    // Only clean up if this is still the current tween
                    animationStarted.current = false;
                    exitTweenRef.current = undefined;
                    onComplete();
                }
            });
        };

        // Execute animation sequence
        entranceTween.Play();
        exitTimeoutRef.current = task.delay(3, exitAnimation);

        return () => {
            // Cleanup function to cancel any ongoing animations
            animationStarted.current = false;
            if (exitTweenRef.current) {
                exitTweenRef.current.Cancel();
                exitTweenRef.current = undefined;
            }
            if (entranceTweenRef.current) {
                entranceTweenRef.current.Cancel();
                entranceTweenRef.current = undefined;
            }
            if (exitTimeoutRef.current) {
                task.cancel(exitTimeoutRef.current);
                exitTimeoutRef.current = undefined;
            }
        };
    }, [visible, onComplete]);

    if (!visible) {
        return <Fragment />;
    }

    // Organize loot by type
    const xpReward = loot.find((item) => item.id === "xp");
    const itemRewards = loot.filter((item) => item.id !== "xp");

    return (
        <frame
            ref={mainFrameRef}
            AutomaticSize={Enum.AutomaticSize.X}
            AnchorPoint={new Vector2(0.5, 0.5)}
            BackgroundTransparency={1}
            Size={new UDim2(0, 0, 0.02, 40)}
            ZIndex={15}
        >
            {/* Main container */}
            <frame BackgroundColor3={Color3.fromRGB(35, 35, 45)} BorderSizePixel={0} Size={new UDim2(1, 0, 1, 0)}>
                <uicorner CornerRadius={new UDim(0, 12)} />
                <uistroke Color={Color3.fromRGB(160, 130, 98)} Thickness={2} />
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(50, 50, 60)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(25, 25, 35)),
                        ])
                    }
                    Rotation={90}
                />
                <uipadding
                    PaddingBottom={new UDim(0, 10)}
                    PaddingLeft={new UDim(0, 10)}
                    PaddingRight={new UDim(0, 10)}
                    PaddingTop={new UDim(0, 10)}
                />
                <uilistlayout
                    FillDirection={Enum.FillDirection.Horizontal}
                    HorizontalAlignment={Enum.HorizontalAlignment.Center}
                    Padding={new UDim(0, 8)}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                    VerticalAlignment={Enum.VerticalAlignment.Center}
                />
                {/* XP Reward */}
                {xpReward && <XPRewardSlot amount={xpReward.amount} />}

                {/* Item Rewards */}
                {itemRewards.map((lootItem, index) => (
                    <LootItemSlot itemId={lootItem.id} amount={lootItem.amount} layoutOrder={index + 2} />
                ))}
            </frame>
        </frame>
    );
}

// XP Reward slot component with sparks
function XPRewardSlot({ amount }: { amount: number }) {
    const slotRef = useRef<Frame>();
    const [sparkParticles, setSparkParticles] = useState<Array<{ id: string; angle: number; delay: number }>>([]);

    // Create spark particles for this slot
    const createSlotSparks = () => {
        const particles: Array<{ id: string; angle: number; delay: number }> = [];
        for (let i = 0; i < 6; i++) {
            particles.push({
                id: `xp_spark_${i}_${tick()}`,
                angle: (i / 3) * math.pi * 2,
                delay: math.random() * 0.2,
            });
        }
        setSparkParticles(particles);
    };

    useEffect(() => {
        // Create sparks when the slot mounts
        task.delay(0.1, createSlotSparks);

        // Clean up sparks after animation
        task.delay(2, () => setSparkParticles([]));
    }, []);

    return (
        <frame
            ref={slotRef}
            BackgroundColor3={Color3.fromRGB(255, 255, 255)}
            BorderSizePixel={0}
            LayoutOrder={1}
            Size={new UDim2(0, 60, 1, 0)}
        >
            <uicorner CornerRadius={new UDim(0, 8)} />
            <uigradient
                Color={
                    new ColorSequence([
                        new ColorSequenceKeypoint(0, Color3.fromRGB(255, 182, 193)),
                        new ColorSequenceKeypoint(1, Color3.fromRGB(255, 105, 180)),
                    ])
                }
                Rotation={45}
            />
            <textlabel
                AnchorPoint={new Vector2(0.5, 0.3)}
                BackgroundTransparency={1}
                FontFace={RobotoSlabBold}
                Position={new UDim2(0.5, 0, 0.3, 0)}
                Size={new UDim2(0.9, 0, 0.4, 0)}
                Text="XP"
                TextColor3={Color3.fromRGB(100, 20, 60)}
                TextScaled={true}
            />
            <textlabel
                AnchorPoint={new Vector2(0.5, 0.8)}
                BackgroundTransparency={1}
                FontFace={RobotoSlab}
                Position={new UDim2(0.5, 0, 0.8, 0)}
                Size={new UDim2(1, 0, 0.3, 0)}
                Text={`+${amount}`}
                TextColor3={Color3.fromRGB(80, 15, 50)}
                TextScaled={true}
            />

            {/* Spark particles for XP slot */}
            {sparkParticles.map((particle) => (
                <SlotSparkParticle
                    key={particle.id}
                    angle={particle.angle}
                    delay={particle.delay}
                    color={Color3.fromRGB(255, 255, 255)}
                />
            ))}
        </frame>
    );
}

// Individual loot item slot component
function LootItemSlot({ itemId, amount, layoutOrder }: { itemId: string; amount: number; layoutOrder: number }) {
    const viewportRef = useRef<ViewportFrame>();
    const item = Items.getItem(itemId);
    const [sparkParticles, setSparkParticles] = useState<Array<{ id: string; angle: number; delay: number }>>([]);

    useItemViewport(viewportRef, itemId);

    // Create spark particles for this slot
    const createSlotSparks = () => {
        const particles: Array<{ id: string; angle: number; delay: number }> = [];
        for (let i = 0; i < 6; i++) {
            particles.push({
                id: `item_spark_${i}_${tick()}`,
                angle: (i / 3) * math.pi * 2,
                delay: math.random() * 0.2,
            });
        }
        setSparkParticles(particles);
    };

    useEffect(() => {
        // Create sparks when the slot mounts
        task.delay(0.1, createSlotSparks);

        // Clean up sparks after animation
        task.delay(2, () => setSparkParticles([]));
    }, []);

    if (!item) {
        return <Fragment />;
    }

    const backgroundColor = item.difficulty.color ?? Color3.fromRGB(52, 155, 255);

    return (
        <frame
            BackgroundColor3={backgroundColor}
            BorderSizePixel={0}
            LayoutOrder={layoutOrder}
            Size={new UDim2(0, 60, 1, 0)}
            Event={{
                MouseMoved: () => {
                    if (!item) return;
                    TooltipManager.showTooltip({ item });
                },
                MouseLeave: () => {
                    TooltipManager.hideTooltip();
                },
            }}
        >
            <uicorner CornerRadius={new UDim(0, 8)} />
            <uistroke Color={Color3.fromRGB(255, 255, 255)} Thickness={1} />
            <uigradient
                Color={
                    new ColorSequence([
                        new ColorSequenceKeypoint(0, Color3.fromRGB(80, 80, 90)),
                        new ColorSequenceKeypoint(1, Color3.fromRGB(60, 60, 70)),
                    ])
                }
                Rotation={90}
            />

            {/* Item viewport or image */}
            {item.image !== undefined ? (
                <imagelabel
                    AnchorPoint={new Vector2(0.5, 0.5)}
                    BackgroundTransparency={1}
                    Image={item.image}
                    Position={new UDim2(0.5, 0, 0.5, 0)}
                    Size={new UDim2(1, 0, 0, 0)}
                    ScaleType={Enum.ScaleType.Fit}
                />
            ) : (
                <viewportframe
                    ref={viewportRef}
                    AnchorPoint={new Vector2(0.5, 0.5)}
                    BackgroundTransparency={1}
                    Position={new UDim2(0.5, 0, 0.5, 0)}
                    Size={new UDim2(1, 0, 1, 0)}
                />
            )}

            {/* Amount label */}
            <textlabel
                AnchorPoint={new Vector2(0.5, 0)}
                BackgroundTransparency={1}
                FontFace={RobotoSlab}
                Position={new UDim2(0.5, 0, 0.55, 0)}
                Size={new UDim2(1, 0, 0.35, 0)}
                Text={amount > 1 ? `x${amount}` : ""}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextStrokeColor3={Color3.fromRGB(0, 0, 0)}
                TextStrokeTransparency={0}
            >
                <uistroke Thickness={1} />
            </textlabel>

            {/* Item difficulty border */}
            <uistroke ApplyStrokeMode={Enum.ApplyStrokeMode.Border} Color={backgroundColor} Thickness={2} />

            {/* Spark particles for item slot */}
            {sparkParticles.map((particle) => (
                <SlotSparkParticle
                    key={particle.id}
                    angle={particle.angle}
                    delay={particle.delay}
                    color={backgroundColor}
                />
            ))}
        </frame>
    );
}

// Individual slot spark particle component
function SlotSparkParticle({ angle, delay, color }: { angle: number; delay: number; color: Color3 }) {
    const particleRef = useRef<ImageLabel>();

    useEffect(() => {
        if (!particleRef.current) return;

        const particle = particleRef.current;

        // Calculate initial position (center of slot)
        const centerX = 0.5;
        const centerY = 0.5;
        const distance = 20;
        const initialX = centerX + (math.cos(angle) * distance) / 200;
        const initialY = centerY + (math.sin(angle) * distance) / 100;

        particle.Position = new UDim2(initialX, 0, initialY, 0);
        particle.ImageTransparency = 1;
        particle.Size = new UDim2(0, 6, 0, 6);

        // Delayed animation start
        task.delay(delay, () => {
            if (!particle.Parent) return;

            particle.ImageTransparency = 0;

            // Animate outward burst
            const burstTween = TweenService.Create(
                particle,
                new TweenInfo(0.8, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
                {
                    Position: new UDim2(
                        initialX + (math.cos(angle) * 40) / 200,
                        0,
                        initialY + (math.sin(angle) * 40) / 100,
                        0,
                    ),
                    ImageTransparency: 1,
                    Size: new UDim2(0, 3, 0, 3),
                    Rotation: 60,
                },
            );

            burstTween.Play();
        });

        return () => {
            if (particle.Parent) {
                TweenService.Create(particle, new TweenInfo(0.1), { ImageTransparency: 1 }).Play();
            }
        };
    }, [angle, delay]);

    return (
        <imagelabel
            ref={particleRef}
            AnchorPoint={new Vector2(0.5, 0.5)}
            BackgroundTransparency={1}
            Image={getAsset("assets/Spark.png")}
            ImageColor3={color}
            Size={new UDim2(0, 9, 0, 9)}
            ZIndex={16}
        />
    );
}