Загрузка данных
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)