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


1
-- CatPlacementClient
-- StarterPlayer > StarterPlayerScripts > CatPlacementClient
-- Свободное размещение и вращение по трём осям.

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")

local player = Players.LocalPlayer
local remote = ReplicatedStorage:WaitForChild("PlaceCatEvent")

local STEP = 15
local MAX_DISTANCE = 35
local DEBUG = true

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

local placing = false
local equippedTool
local preview
local rotation = Vector3.zero
local targetFloor
local targetPosition
local canPlace = false
local lastTouchPosition
local renderConnection
local characterConnections = {}

local gui
local statusLabel

local function getCharacter()
    return player.Character
end

local function getRoot()
    local character = getCharacter()
    return character and character:FindFirstChild("HumanoidRootPart")
end

local function getOwnedFloor()
    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 floor and floor:IsA("BasePart") then
        return floor
    end

    return nil
end

local function updateStatus(message)
    if statusLabel then
        statusLabel.Text = message
    end
end

local function rotate(axis, amount)
    if not placing then
        return
    end

    if axis == "X" then
        rotation = Vector3.new(rotation.X + amount, rotation.Y, rotation.Z)
    elseif axis == "Y" then
        rotation = Vector3.new(rotation.X, rotation.Y + amount, rotation.Z)
    elseif axis == "Z" then
        rotation = Vector3.new(rotation.X, rotation.Y, rotation.Z + amount)
    end

    log("Rotation:", rotation.X, rotation.Y, rotation.Z)
end

local function rotationCFrame()
    return CFrame.Angles(
        math.rad(rotation.X),
        math.rad(rotation.Y),
        math.rad(rotation.Z)
    )
end

local function setPreviewColor(valid)
    if not preview then
        return
    end

    local color = valid
        and Color3.fromRGB(70, 255, 100)
        or Color3.fromRGB(255, 65, 65)

    for _, obj in ipairs(preview:GetDescendants()) do
        if obj:IsA("BasePart") then
            obj.Color = color
            obj.Transparency = 0.55
            obj.Anchored = true
            obj.CanCollide = false
            obj.CanTouch = false
            obj.CanQuery = false
        end
    end
end

local function createPreview(tool)
    if preview then
        preview:Destroy()
        preview = nil
    end

    if not tool:FindFirstChild("Handle") then
        warn("[CatPlacementClient] У Tool отсутствует Handle:", tool.Name)
        return false
    end

    local clone = tool:Clone()
    clone.Name = "CatPreview"

    for _, obj in ipairs(clone:GetDescendants()) do
        if obj:IsA("Script")
            or obj:IsA("LocalScript")
            or obj:IsA("ModuleScript")
            or obj:IsA("ProximityPrompt")
            or obj:IsA("ClickDetector")
            or obj:IsA("RemoteEvent")
            or obj:IsA("RemoteFunction") then
            obj:Destroy()
        end
    end

    local model = Instance.new("Model")
    model.Name = "CatPreview"

    for _, child in ipairs(clone:GetChildren()) do
        child.Parent = model
    end
    clone:Destroy()

    local handle = model:FindFirstChild("Handle", true)
    if not handle or not handle:IsA("BasePart") then
        model:Destroy()
        warn("[CatPlacementClient] Не найден Handle в предпросмотре")
        return false
    end

    model.PrimaryPart = handle

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

    model.Parent = Workspace
    preview = model
    setPreviewColor(false)

    return true
end

local function stopPlacement(unequip)
    placing = false
    canPlace = false
    targetFloor = nil
    targetPosition = nil
    lastTouchPosition = nil

    if renderConnection then
        renderConnection:Disconnect()
        renderConnection = nil
    end

    if preview then
        preview:Destroy()
        preview = nil
    end

    if gui then
        gui.Enabled = false
    end

    local oldTool = equippedTool
    equippedTool = nil

    if unequip then
        local character = getCharacter()
        local humanoid = character and character:FindFirstChildOfClass("Humanoid")
        if humanoid then
            humanoid:UnequipTools()
        end
    end

    log("Режим размещения завершён", oldTool and oldTool.Name or "")
end

local function sendPlacement()
    if not placing or not canPlace or not targetFloor or not targetPosition then
        updateStatus("Нельзя поставить кота здесь")
        return
    end

    remote:FireServer(targetFloor, targetPosition, rotation)
    updateStatus("Запрос отправлен серверу…")
end

local function createGui()
    local playerGui = player:WaitForChild("PlayerGui")

    local oldGui = playerGui:FindFirstChild("CatPlacementGui")
    if oldGui then
        oldGui:Destroy()
    end

    gui = Instance.new("ScreenGui")
    gui.Name = "CatPlacementGui"
    gui.ResetOnSpawn = false
    gui.Enabled = false
    gui.DisplayOrder = 20
    gui.Parent = playerGui

    statusLabel = Instance.new("TextLabel")
    statusLabel.Name = "Status"
    statusLabel.AnchorPoint = Vector2.new(0.5, 0)
    statusLabel.Position = UDim2.new(0.5, 0, 0.04, 0)
    statusLabel.Size = UDim2.new(0.8, 0, 0, 36)
    statusLabel.BackgroundColor3 = Color3.fromRGB(25, 25, 25)
    statusLabel.BackgroundTransparency = 0.2
    statusLabel.TextColor3 = Color3.new(1, 1, 1)
    statusLabel.Font = Enum.Font.GothamBold
    statusLabel.TextScaled = true
    statusLabel.Text = "Выбери место для кота"
    statusLabel.Parent = gui

    local function makeButton(name, text, position, size, callback)
        local button = Instance.new("TextButton")
        button.Name = name
        button.AnchorPoint = Vector2.new(0.5, 0.5)
        button.Position = position
        button.Size = size
        button.BackgroundColor3 = Color3.fromRGB(40, 40, 40)
        button.BackgroundTransparency = 0.12
        button.TextColor3 = Color3.new(1, 1, 1)
        button.Font = Enum.Font.GothamBold
        button.TextScaled = true
        button.Text = text
        button.Parent = gui
        button.Activated:Connect(callback)
        return button
    end

    if UserInputService.TouchEnabled then
        -- Кнопки расположены справа внизу экрана.
        local function axisButton(name, text, x, y, axis, amount)
            makeButton(
                name,
                text,
                UDim2.new(1, x, 1, y),
                UDim2.fromOffset(68, 40),
                function()
                    rotate(axis, amount)
                end
            )
        end

        axisButton("XMinus", "X− 15°", -222, -202, "X", -STEP)
        axisButton("XPlus",  "X+ 15°", -148, -202, "X", STEP)

        axisButton("YMinus", "Y− 15°", -222, -157, "Y", -STEP)
        axisButton("YPlus",  "Y+ 15°", -148, -157, "Y", STEP)

        axisButton("ZMinus", "Z− 15°", -222, -112, "Z", -STEP)
        axisButton("ZPlus",  "Z+ 15°", -148, -112, "Z", STEP)

        makeButton(
            "Place",
            "ПОСТАВИТЬ",
            UDim2.new(1, -185, 1, -58),
            UDim2.fromOffset(142, 42),
            sendPlacement
        )

        makeButton(
            "Cancel",
            "ОТМЕНА",
            UDim2.new(1, -65, 1, -58),
            UDim2.fromOffset(88, 42),
            function()
                stopPlacement(true)
            end
        )
    else
        local hint = Instance.new("TextLabel")
        hint.Name = "PCControls"
        hint.AnchorPoint = Vector2.new(0.5, 1)
        hint.Position = UDim2.new(0.5, 0, 1, -16)
        hint.Size = UDim2.new(0.96, 0, 0, 42)
        hint.BackgroundColor3 = Color3.fromRGB(25, 25, 25)
        hint.BackgroundTransparency = 0.25
        hint.TextColor3 = Color3.new(1, 1, 1)
        hint.Font = Enum.Font.GothamBold
        hint.TextScaled = true
        hint.Text = "ЛКМ — поставить | R/T: Y | F/G: X | V/B: Z | Esc — отмена"
        hint.Parent = gui
    end
end

createGui()

local function beginPlacement(tool)
    if placing then
        stopPlacement(false)
    end

    if not tool or not tool:IsA("Tool") then
        return
    end

    if not getOwnedFloor() then
        warn("[CatPlacementClient] База не найдена. Проверь BaseName и OwnerUserId.")
        return
    end

    if not createPreview(tool) then
        return
    end

    equippedTool = tool
    rotation = Vector3.zero
    placing = true
    gui.Enabled = true

    updateStatus("Наведи на пол базы. Зелёный цвет — можно поставить.")

    renderConnection = RunService.RenderStepped:Connect(function()
        if not placing or not preview or not equippedTool then
            return
        end

        local character = getCharacter()
        local root = getRoot()
        local camera = Workspace.CurrentCamera

        if not character or not root or not camera
            or equippedTool.Parent ~= character then
            canPlace = false
            setPreviewColor(false)
            updateStatus("Экипируй кота, чтобы продолжить")
            return
        end

        local screenPosition

        if UserInputService.TouchEnabled then
            screenPosition = lastTouchPosition
        else
            screenPosition = UserInputService:GetMouseLocation()
        end

        if not screenPosition then
            canPlace = false
            setPreviewColor(false)
            return
        end

        local ray = camera:ScreenPointToRay(
            screenPosition.X,
            screenPosition.Y
        )

        local params = RaycastParams.new()
        params.FilterType = Enum.RaycastFilterType.Exclude
        params.FilterDescendantsInstances = {character, preview}
        params.IgnoreWater = true

        local result = Workspace:Raycast(ray.Origin, ray.Direction * 500, params)
        local ownFloor = getOwnedFloor()

        canPlace = false
        targetFloor = nil
        targetPosition = nil

        if result and ownFloor and result.Instance == ownFloor then
            local distance = (root.Position - result.Position).Magnitude
            local localPos = ownFloor.CFrame:PointToObjectSpace(result.Position)

            local inside = math.abs(localPos.X) <= ownFloor.Size.X / 2
                and math.abs(localPos.Z) <= ownFloor.Size.Z / 2

            if distance <= MAX_DISTANCE and inside then
                canPlace = true
                targetFloor = ownFloor
                targetPosition = result.Position

                -- Сначала поворачиваем вокруг точки размещения.
                preview:PivotTo(
                    CFrame.new(result.Position) * rotationCFrame()
                )

                -- Поднимаем кота так, чтобы его нижняя граница
                -- касалась верхней поверхности пола.
                local boxCFrame, boxSize = preview:GetBoundingBox()
                local floorTop = ownFloor.Position.Y + ownFloor.Size.Y / 2
                local bottom = boxCFrame.Position.Y - boxSize.Y / 2
                local correctedY = result.Position.Y + floorTop - bottom

                preview:PivotTo(
                    CFrame.new(
                        result.Position.X,
                        correctedY,
                        result.Position.Z
                    ) * rotationCFrame()
                )
            end
        end

        setPreviewColor(canPlace)

        if canPlace then
            updateStatus(string.format(
                "Можно поставить | X: %d° Y: %d° Z: %d°",
                rotation.X, rotation.Y, rotation.Z
            ))
        elseif not ownFloor then
            updateStatus("База не найдена — проверь BaseManager")
        else
            updateStatus("Наведи на пол своей базы в пределах досягаемости")
        end
    end)
end

local function watchCharacter(character)
    for _, connection in ipairs(characterConnections) do
        connection:Disconnect()
    end
    table.clear(characterConnections)

    local function watchTool(child)
        if not child:IsA("Tool") then
            return
        end

        table.insert(characterConnections, child.Equipped:Connect(function()
            task.defer(function()
                if child.Parent == character then
                    beginPlacement(child)
                end
            end)
        end))

        table.insert(characterConnections, child.Unequipped:Connect(function()
            if equippedTool == child then
                stopPlacement(false)
            end
        end))
    end

    for _, child in ipairs(character:GetChildren()) do
        watchTool(child)
    end

    table.insert(characterConnections, character.ChildAdded:Connect(watchTool))

    table.insert(characterConnections, character.ChildRemoved:Connect(function(child)
        if child == equippedTool then
            stopPlacement(false)
        end
    end))
end

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

player.CharacterAdded:Connect(watchCharacter)

UserInputService.InputBegan:Connect(function(input, processed)
    if processed or not placing then
        return
    end

    if input.KeyCode == Enum.KeyCode.R then
        rotate("Y", STEP)
    elseif input.KeyCode == Enum.KeyCode.T then
        rotate("Y", -STEP)
    elseif input.KeyCode == Enum.KeyCode.F then
        rotate("X", STEP)
    elseif input.KeyCode == Enum.KeyCode.G then
        rotate("X", -STEP)
    elseif input.KeyCode == Enum.KeyCode.V then
        rotate("Z", STEP)
    elseif input.KeyCode == Enum.KeyCode.B then
        rotate("Z", -STEP)
    elseif input.KeyCode == Enum.KeyCode.Escape then
        stopPlacement(true)
    elseif input.UserInputType == Enum.UserInputType.MouseButton1
        and not UserInputService.TouchEnabled then
        sendPlacement()
    elseif input.UserInputType == Enum.UserInputType.Touch then
        lastTouchPosition = input.Position
    end
end)

UserInputService.InputChanged:Connect(function(input)
    if input.UserInputType == Enum.UserInputType.Touch then
        lastTouchPosition = input.Position
    end
end)

player:GetAttributeChangedSignal("BaseName"):Connect(function()
    if placing and not getOwnedFloor() then
        stopPlacement(true)
        warn("[CatPlacementClient] Собственная база стала недоступна.")
    end
end)

log("Клиент запущен. Сенсорное управление:", UserInputService.TouchEnabled)