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



-- CatPlacementServer
-- ServerScriptService > CatPlacementServer
-- Полная версия с проверками, 3D-поворотом и доходом.

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

local MAX_CATS = 16
local MAX_DISTANCE = 35
local RETURN_HOLD_DURATION = 1
local DEBUG = true

local function log(...)
    if 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(name)
    local template = catTools:FindFirstChild(name)
    if template and template:IsA("Tool") then
        local handle = template:FindFirstChild("Handle")
        if handle and handle:IsA("BasePart") then
            return template
        end
    end
    return nil
end

-- Настройка физики настоящего Tool.
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("Нет корректного Handle у", tool:GetFullName())
        return
    end

    tool.RequiresHandle = true

    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

    -- Соединяем дополнительные детали с Handle.
    for _, part in ipairs(tool:GetDescendants()) do
        if part:IsA("BasePart") and part ~= handle then
            local alreadyWelded = false

            for _, joint in ipairs(handle:GetChildren()) do
                if joint:IsA("WeldConstraint")
                    and ((joint.Part0 == handle and joint.Part1 == part)
                    or (joint.Part1 == handle and joint.Part0 == part)) then
                    alreadyWelded = true
                    break
                end
            end

            if not alreadyWelded then
                local weld = Instance.new("WeldConstraint")
                weld.Name = "CatToolAutoWeld"
                weld.Part0 = handle
                weld.Part1 = part
                weld.Parent = handle
            end
        end
    end
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)
    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 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 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 isInsideFloor(floor, position)
    local p = floor.CFrame:PointToObjectSpace(position)
    return math.abs(p.X) <= floor.Size.X / 2
        and math.abs(p.Z) <= floor.Size.Z / 2
        and math.abs(p.Y) <= floor.Size.Y / 2 + 3
end

local function makeRotationCFrame(rotation)
    -- Все три угла передаются в градусах.
    return CFrame.Angles(
        math.rad(rotation.X),
        math.rad(rotation.Y),
        math.rad(rotation.Z)
    )
end

local function positionModel(model, floor, position, rotation)
    local function pivotAt(pos)
        model:PivotTo(CFrame.new(pos) * makeRotationCFrame(rotation))
    end

    pivotAt(position)

    -- Учитываем фактические габариты модели после поворота.
    local boxCFrame, boxSize = model:GetBoundingBox()
    local floorTop = floor.Position.Y + floor.Size.Y / 2
    local bottom = boxCFrame.Position.Y - boxSize.Y / 2
    local correctedY = position.Y + floorTop - bottom

    pivotAt(Vector3.new(position.X, correctedY, position.Z))
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)
    local backpack = player:FindFirstChildOfClass("Backpack")
    if not template or 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, rotation)
    local template = getTemplate(tool.Name)
    if not template then
        return false, "Не найден шаблон кота"
    end

    local income = template:GetAttribute("Income")
    if typeof(income) ~= "number" or income < 0 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()

            for _, sub in ipairs(part:GetDescendants()) do
                if sub:IsA("Script")
                    or sub:IsA("LocalScript")
                    or sub:IsA("ProximityPrompt") then
                    sub:Destroy()
                end
            end

            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, rotation)
        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 = RETURN_HOLD_DURATION
    prompt.MaxActivationDistance = 10
    prompt.RequiresLineOfSight = false
    prompt.Parent = handle

    local returning = false

    prompt.Triggered:Connect(function(triggeringPlayer)
        if returning or triggeringPlayer ~= player or not model.Parent then
            return
        end

        if model:GetAttribute("OwnerUserId") ~= triggeringPlayer.UserId then
            return
        end

        returning = true
        prompt.Enabled = false

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

    return true
end

remote.OnServerEvent:Connect(function(player, floor, position, rotation)
    if typeof(floor) ~= "Instance" or not floor:IsA("BasePart")
        or typeof(position) ~= "Vector3"
        or typeof(rotation) ~= "Vector3" then
        warnLog("Некорректные данные от", player.Name)
        return
    end

    for _, angle in ipairs({rotation.X, rotation.Y, rotation.Z}) do
        if angle ~= angle or math.abs(angle) > 36000 then
            warnLog("Некорректный угол от", player.Name)
            return
        end
    end

    local _, ownFloor = getOwnedBase(player)
    if not ownFloor or floor ~= ownFloor then
        warnLog("Попытка размещения не на своей базе:", player.Name)
        return
    end

    local character = player.Character
    local root = character and character:FindFirstChild("HumanoidRootPart")
    if not root or (root.Position - position).Magnitude > MAX_DISTANCE then
        warnLog("Слишком далеко от места размещения:", player.Name)
        return
    end

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

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

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

    local success, reason = createPlacedCat(player, tool, floor, position, rotation)
    if not success then
        warnLog(reason)
        return
    end

    tool:Destroy()
    log("Размещён кот:", player.Name, "Всего:", countCats(player))
end)

-- Начисление дохода раз в секунду.
task.spawn(function()
    while true do
        task.wait(1)

        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")
                local owner = typeof(ownerId) == "number"
                    and Players:GetPlayerByUserId(ownerId)

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

                if typeof(income) == "number"
                    and income > 0
                    and coins
                    and (coins:IsA("IntValue") or coins:IsA("NumberValue")) then
                    coins.Value += income
                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("PlaceCatEvent:", remote:GetFullName())
log("PlacedCats:", placedFolder:GetFullName())

for _, template in ipairs(catTools:GetChildren()) do
    if template:IsA("Tool") then
        if not getTemplate(template.Name) then
            warnLog("ПРОВЕРЬ Tool и его Handle:", template.Name)
        else
            log("Шаблон OK:", template.Name,
                "Income =", template:GetAttribute("Income"),
                "Rarity =", template:GetAttribute("Rarity"))
        end
    end
end