Загрузка данных
-- CatPlacementClient
-- Разместить в StarterPlayer/StarterPlayerScripts как LocalScript
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 CONFIG = {
ROTATION_STEP = 15,
MAX_DISTANCE = 35,
DEBUG = true,
}
local function log(...)
if CONFIG.DEBUG then
print("[CatPlacementClient]", ...)
end
end
local placing = false
local equippedTool = nil
local preview = nil
local rotationY = 0
local targetFloor = nil
local targetPosition = nil
local canPlace = false
local lastTouchPosition = nil
local renderConnection = nil
local characterConnections = {}
local gui
local statusLabel
local function getCharacter()
return player.Character
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 getRoot()
local character = getCharacter()
return character and character:FindFirstChild("HumanoidRootPart")
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
local handle = tool:FindFirstChild("Handle")
if not handle or not handle:IsA("BasePart") 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"
-- Переносим содержимое клона в отдельную Model.
for _, child in ipairs(clone:GetChildren()) do
child.Parent = model
end
clone:Destroy()
local previewHandle = model:FindFirstChild("Handle", true)
if not previewHandle or not previewHandle:IsA("BasePart") then
model:Destroy()
warn("[CatPlacementClient] Не удалось создать предпросмотр:", tool.Name)
return false
end
model.PrimaryPart = previewHandle
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
if unequip then
local character = getCharacter()
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
if humanoid then
humanoid:UnequipTools()
end
end
equippedTool = nil
end
local function rotate(amount)
if not placing then
return
end
rotationY = (rotationY + amount) % 360
log("Угол поворота:", rotationY)
end
local function updateStatus(text)
if statusLabel then
statusLabel.Text = text
end
end
local function createGui()
local playerGui = player:WaitForChild("PlayerGui")
local existing = playerGui:FindFirstChild("CatPlacementGui")
if existing then
existing:Destroy()
end
gui = Instance.new("ScreenGui")
gui.Name = "CatPlacementGui"
gui.ResetOnSpawn = false
gui.Enabled = false
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.06, 0)
statusLabel.Size = UDim2.new(0.8, 0, 0, 38)
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
local touch = UserInputService.TouchEnabled
if touch then
makeButton(
"RotateLeft", "↺ 15°",
UDim2.new(1, -175, 1, -170),
UDim2.fromOffset(76, 46),
function() rotate(-CONFIG.ROTATION_STEP) end
)
makeButton(
"RotateRight", "↻ 15°",
UDim2.new(1, -85, 1, -170),
UDim2.fromOffset(76, 46),
function() rotate(CONFIG.ROTATION_STEP) end
)
makeButton(
"Place", "ПОСТАВИТЬ",
UDim2.new(1, -130, 1, -105),
UDim2.fromOffset(165, 48),
function()
if placing and canPlace and targetFloor and targetPosition then
remote:FireServer(targetFloor, targetPosition, rotationY)
updateStatus("Отправлен запрос на размещение…")
else
updateStatus("Нельзя поставить кота здесь")
end
end
)
makeButton(
"Cancel", "ОТМЕНА",
UDim2.new(1, -130, 1, -48),
UDim2.fromOffset(165, 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, -20)
hint.Size = UDim2.new(0.9, 0, 0, 34)
hint.BackgroundTransparency = 1
hint.TextColor3 = Color3.new(1, 1, 1)
hint.TextStrokeTransparency = 0.3
hint.Font = Enum.Font.GothamBold
hint.TextScaled = true
hint.Text = "ЛКМ — поставить R/T — поворот 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
local floor = getOwnedFloor()
if not floor then
warn("[CatPlacementClient] Не найдена собственная база или Floor. Проверь BaseName и OwnerUserId.")
return
end
if not createPreview(tool) then
return
end
equippedTool = tool
rotationY = 0
placing = true
gui.Enabled = true
updateStatus("Зелёный — можно поставить, красный — нельзя")
if renderConnection then
renderConnection:Disconnect()
end
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 not equippedTool.Parent
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
if distance <= CONFIG.MAX_DISTANCE then
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 inside then
targetFloor = ownFloor
targetPosition = result.Position
canPlace = true
preview:PivotTo(
CFrame.new(result.Position)
* CFrame.Angles(0, math.rad(rotationY), 0)
)
-- Поднимаем предпросмотр так, чтобы его низ совпал с верхом пола.
local boxCFrame, boxSize = preview:GetBoundingBox()
local floorTop = ownFloor.Position.Y + ownFloor.Size.Y / 2
local modelBottom = boxCFrame.Position.Y - boxSize.Y / 2
local shiftY = floorTop - modelBottom
preview:PivotTo(
CFrame.new(result.Position.X, result.Position.Y + shiftY, result.Position.Z)
* CFrame.Angles(0, math.rad(rotationY), 0)
)
end
end
end
setPreviewColor(canPlace)
if canPlace then
updateStatus("Можно поставить кота")
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
local connection = child.Equipped:Connect(function()
task.defer(function()
if child.Parent == character then
beginPlacement(child)
end
end)
end)
table.insert(characterConnections, connection)
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(CONFIG.ROTATION_STEP)
elseif input.KeyCode == Enum.KeyCode.T then
rotate(-CONFIG.ROTATION_STEP)
elseif input.KeyCode == Enum.KeyCode.Escape then
stopPlacement(true)
elseif input.UserInputType == Enum.UserInputType.MouseButton1
and not UserInputService.TouchEnabled then
if canPlace and targetFloor and targetPosition then
remote:FireServer(targetFloor, targetPosition, rotationY)
updateStatus("Отправлен запрос на размещение…")
else
updateStatus("Нельзя поставить кота здесь")
end
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)
remote.OnClientEvent:Connect(function(action, message)
-- На случай будущих серверных ответов; размещение подтверждает сервер.
if action == "Result" and typeof(message) == "string" then
updateStatus(message)
end
end)
player:GetAttributeChangedSignal("BaseName"):Connect(function()
if placing and not getOwnedFloor() then
stopPlacement(true)
warn("[CatPlacementClient] Назначенная база стала недоступна.")
end
end)
log("Клиент запущен. TouchEnabled =", UserInputService.TouchEnabled)