Загрузка данных



-- CatPlacementServer
-- Разместить в ServerScriptService как обычный Script

local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Workspace = game:GetService("Workspace")

local CONFIG = {
    MAX_CATS = 16,
    MAX_DISTANCE = 35,
    ROTATION_STEP = 15,
    RETURN_HOLD_DURATION = 1,
    INCOME_INTERVAL = 1,
    DEBUG = true,
}

local function log(...)
    if CONFIG.DEBUG then
        print("[CatPlacement]", ...)
    end
end

local function warnLog(...)
    warn("[CatPlacement]", ...)
end

local catTools = ServerStorage:FindFirstChild("CatTools")
assert(catTools, "Не найдена папка ServerStorage/CatTools")

local remote = ReplicatedStorage:FindFirstChild("PlaceCatEvent")
if remote and not remote:IsA("RemoteEvent") then
    error("PlaceCatEvent существует, но это не RemoteEvent")
end
if not remote then
    remote = Instance.new("RemoteEvent")
    remote.Name = "PlaceCatEvent"
    remote.Parent = ReplicatedStorage
end

local placedFolder = Workspace:FindFirstChild("PlacedCats")
if not placedFolder then
    placedFolder = Instance.new("Folder")
    placedFolder.Name = "PlacedCats"
    placedFolder.Parent = Workspace
end

local function getTemplate(toolName)
    local template = catTools:FindFirstChild(toolName)
    if template and template:IsA("Tool")
        and template:FindFirstChild("Handle")
        and template.Handle:IsA("BasePart") then
        return template
    end
    return nil
end

-- Настройка настоящего Tool. Handle никогда не должен быть Anchored,
-- пока Tool находится в Backpack или Character.
local function prepareTool(tool)
    if not tool:IsA("Tool") then
        return
    end

    local handle = tool:FindFirstChild("Handle")
    if not handle or not handle:IsA("BasePart") then
        warnLog("У Tool нет корректного Handle:", tool:GetFullName())
        return
    end

    for _, obj in ipairs(tool:GetDescendants()) do
        if obj:IsA("BasePart") then
            obj.Anchored = false
            obj.CanCollide = false
            obj.CanTouch = false
            obj.Massless = true
        end
    end

    -- Дополнительные части Tool соединяются с Handle.
    -- Не создаём повторный WeldConstraint, если части уже соединены.
    for _, obj in ipairs(tool:GetDescendants()) do
        if obj:IsA("BasePart") and obj ~= handle then
            local hasConnection = false
            for _, joint in ipairs(obj:GetDescendants()) do
                if joint:IsA("WeldConstraint")
                    and joint.Part0 and joint.Part1 then
                    hasConnection = true
                    break
                end
            end

            if not hasConnection then
                local weld = Instance.new("WeldConstraint")
                weld.Name = "CatToolAutoWeld"
                weld.Part0 = handle
                weld.Part1 = obj
                weld.Parent = handle
            end
        end
    end

    tool.RequiresHandle = true
end

local function watchContainer(container)
    for _, child in ipairs(container:GetChildren()) do
        if child:IsA("Tool") then
            prepareTool(child)
        end
    end

    container.ChildAdded:Connect(function(child)
        if child:IsA("Tool") then
            task.defer(prepareTool, child)
        end
    end)
end

local function setupPlayer(player)
    local backpack = player:WaitForChild("Backpack")
    watchContainer(backpack)

    player.CharacterAdded:Connect(function(character)
        watchContainer(character)
        task.defer(function()
            for _, child in ipairs(character:GetChildren()) do
                if child:IsA("Tool") then
                    prepareTool(child)
                end
            end
        end)
    end)

    if player.Character then
        watchContainer(player.Character)
    end
end

for _, player in ipairs(Players:GetPlayers()) do
    task.spawn(setupPlayer, player)
end
Players.PlayerAdded:Connect(setupPlayer)

local function getOwnedBase(player)
    local baseName = player:GetAttribute("BaseName")
    if typeof(baseName) ~= "string" then
        return nil
    end

    local base = Workspace:FindFirstChild(baseName)
    if not base or not base:IsA("Model") then
        return nil
    end

    if base:GetAttribute("OwnerUserId") ~= player.UserId then
        return nil
    end

    local floor = base:FindFirstChild("Floor")
    if not floor or not floor:IsA("BasePart") then
        return nil
    end

    return base, floor
end

local function getEquippedCat(player)
    local character = player.Character
    if not character then
        return nil
    end

    for _, child in ipairs(character:GetChildren()) do
        if child:IsA("Tool") and getTemplate(child.Name) then
            prepareTool(child)
            return child
        end
    end

    return nil
end

local function countCats(player)
    local count = 0
    for _, model in ipairs(placedFolder:GetChildren()) do
        if model:IsA("Model")
            and model:GetAttribute("OwnerUserId") == player.UserId then
            count += 1
        end
    end
    return count
end

local function isInsideFloor(floor, position)
    local localPosition = floor.CFrame:PointToObjectSpace(position)
    return math.abs(localPosition.X) <= floor.Size.X / 2
        and math.abs(localPosition.Z) <= floor.Size.Z / 2
        and math.abs(localPosition.Y) <= floor.Size.Y / 2 + 3
end

local function positionModel(model, floor, position, yaw)
    -- Сначала размещаем модель, затем корректируем высоту по её габаритам.
    model:PivotTo(
        CFrame.new(position) * CFrame.Angles(0, math.rad(yaw), 0)
    )

    local boxCFrame, boxSize = model:GetBoundingBox()
    local floorTop = floor.Position.Y + floor.Size.Y / 2
    local modelBottom = boxCFrame.Position.Y - boxSize.Y / 2
    local correctedY = position.Y + (floorTop - modelBottom)

    model:PivotTo(
        CFrame.new(position.X, correctedY, position.Z)
            * CFrame.Angles(0, math.rad(yaw), 0)
    )
end

local function createIncomeLabel(model, income)
    local handle = model.PrimaryPart
    if not handle then
        return
    end

    local gui = Instance.new("BillboardGui")
    gui.Name = "IncomeLabel"
    gui.Adornee = handle
    gui.Size = UDim2.fromOffset(150, 38)
    gui.StudsOffset = Vector3.new(0, 2.5, 0)
    gui.AlwaysOnTop = true
    gui.MaxDistance = 100
    gui.Parent = handle

    local label = Instance.new("TextLabel")
    label.Name = "IncomeText"
    label.Size = UDim2.fromScale(1, 1)
    label.BackgroundTransparency = 1
    label.Text = "+" .. tostring(income) .. " / сек"
    label.TextColor3 = Color3.fromRGB(100, 255, 120)
    label.TextStrokeTransparency = 0.25
    label.Font = Enum.Font.GothamBold
    label.TextScaled = true
    label.Parent = gui
end

local function giveCatBack(player, templateName)
    local template = getTemplate(templateName)
    if not template then
        warnLog("Не удалось вернуть кота: шаблон отсутствует", templateName)
        return false
    end

    local backpack = player:FindFirstChildOfClass("Backpack")
    if not backpack then
        return false
    end

    local tool = template:Clone()
    prepareTool(tool)
    tool.Parent = backpack
    return true
end

local function createPlacedCat(player, tool, floor, position, yaw)
    local template = getTemplate(tool.Name)
    if not template then
        return false, "Шаблон кота не найден"
    end

    local income = template:GetAttribute("Income")
    if typeof(income) ~= "number" then
        income = 1
        warnLog("Нет числового атрибута Income у", template.Name)
    end

    local model = Instance.new("Model")
    model.Name = template.Name
    model:SetAttribute("OwnerUserId", player.UserId)
    model:SetAttribute("TemplateName", template.Name)
    model:SetAttribute("Income", income)

    for _, obj in ipairs(template:GetDescendants()) do
        if obj:IsA("BasePart") then
            local part = obj:Clone()
            part.Anchored = true
            part.CanCollide = false
            part.CanTouch = false
            part.CanQuery = false
            part.Massless = true
            part.Parent = model
        end
    end

    local handle = model:FindFirstChild("Handle")
    if not handle or not handle:IsA("BasePart") then
        model:Destroy()
        return false, "В модели кота отсутствует Handle"
    end

    model.PrimaryPart = handle
    model.Parent = placedFolder

    local ok, err = pcall(function()
        positionModel(model, floor, position, yaw)
        createIncomeLabel(model, income)
    end)

    if not ok then
        model:Destroy()
        warnLog("Ошибка создания модели:", err)
        return false, "Ошибка позиционирования кота"
    end

    local prompt = Instance.new("ProximityPrompt")
    prompt.Name = "ReturnCatPrompt"
    prompt.ActionText = "Забрать кота"
    prompt.ObjectText = template.Name
    prompt.KeyboardKeyCode = Enum.KeyCode.E
    prompt.HoldDuration = CONFIG.RETURN_HOLD_DURATION
    prompt.MaxActivationDistance = 10
    prompt.RequiresLineOfSight = false
    prompt.Parent = handle

    local returning = false
    prompt.Triggered:Connect(function(triggeringPlayer)
        if returning then
            return
        end
        if triggeringPlayer ~= player then
            return
        end
        if model:GetAttribute("OwnerUserId") ~= triggeringPlayer.UserId then
            return
        end
        if not model.Parent then
            return
        end

        returning = true
        prompt.Enabled = false

        if giveCatBack(triggeringPlayer, model:GetAttribute("TemplateName")) then
            model:Destroy()
            log("Кот возвращён:", triggeringPlayer.Name, template.Name)
        else
            returning = false
            if prompt.Parent then
                prompt.Enabled = true
            end
        end
    end)

    return true
end

remote.OnServerEvent:Connect(function(player, floor, position, yaw)
    -- Проверка входных данных: клиент не является доверенным источником.
    if typeof(floor) ~= "Instance" or not floor:IsA("BasePart") then
        warnLog("Отклонён запрос: некорректный Floor от", player.Name)
        return
    end
    if typeof(position) ~= "Vector3" then
        warnLog("Отклонён запрос: некорректная позиция от", player.Name)
        return
    end
    if typeof(yaw) ~= "number" or yaw ~= yaw
        or math.abs(yaw) > 100000 then
        warnLog("Отклонён запрос: некорректный угол от", player.Name)
        return
    end

    local base, ownFloor = getOwnedBase(player)
    if not base or floor ~= ownFloor then
        warnLog("Отклонено: игрок пытается ставить кота не на свою базу", player.Name)
        return
    end

    local character = player.Character
    local root = character and character:FindFirstChild("HumanoidRootPart")
    if not root then
        return
    end

    if (root.Position - position).Magnitude > CONFIG.MAX_DISTANCE then
        warnLog("Отклонено: позиция слишком далеко от игрока", player.Name)
        return
    end

    if not isInsideFloor(floor, position) then
        warnLog("Отклонено: позиция вне пола базы", player.Name)
        return
    end

    if countCats(player) >= CONFIG.MAX_CATS then
        warnLog("Лимит котов достигнут:", player.Name)
        return
    end

    local tool = getEquippedCat(player)
    if not tool then
        warnLog("Отклонено: у игрока нет экипированного Tool-кота", player.Name)
        return
    end

    local success, reason = createPlacedCat(player, tool, floor, position, yaw)
    if not success then
        warnLog("Не удалось разместить кота:", reason)
        return
    end

    tool:Destroy()
    log("Размещён кот:", player.Name, "Количество:", countCats(player))
end)

-- Доход начисляется только владельцу кота.
task.spawn(function()
    while true do
        task.wait(CONFIG.INCOME_INTERVAL)

        for _, model in ipairs(placedFolder:GetChildren()) do
            if model:IsA("Model") and model.PrimaryPart then
                local ownerId = model:GetAttribute("OwnerUserId")
                local income = model:GetAttribute("Income")

                if typeof(ownerId) == "number"
                    and typeof(income) == "number"
                    and income > 0 then

                    local owner = Players:GetPlayerByUserId(ownerId)
                    local stats = owner and owner:FindFirstChild("leaderstats")
                    local coins = stats and stats:FindFirstChild("Coins")

                    if coins and (coins:IsA("IntValue") or coins:IsA("NumberValue")) then
                        coins.Value += income
                    end
                end
            end
        end
    end
end)

Players.PlayerRemoving:Connect(function(player)
    for _, model in ipairs(placedFolder:GetChildren()) do
        if model:IsA("Model")
            and model:GetAttribute("OwnerUserId") == player.UserId then
            model:Destroy()
        end
    end
end)

-- Самопроверка при старте
log("Сервер запущен")
log("CatTools:", catTools:GetFullName())
log("PlaceCatEvent:", remote:GetFullName())
log("PlacedCats:", placedFolder:GetFullName())

for _, template in ipairs(catTools:GetChildren()) do
    if template:IsA("Tool") then
        if not template:FindFirstChild("Handle") then
            warnLog("ПРОВЕРКА НЕ ПРОЙДЕНА: нет Handle у", template.Name)
        elseif typeof(template:GetAttribute("Income")) ~= "number" then
            warnLog("ПРОВЕРКА: у", template.Name, "не задан числовой Income")
        else
            log("Шаблон проверен:", template.Name,
                "Income =", template:GetAttribute("Income"),
                "Rarity =", template:GetAttribute("Rarity"))
        end
    end
end