Загрузка данных
-- CatPlacementClient
-- LocalScript в StarterPlayer > StarterPlayerScripts
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local GuiService = game:GetService("GuiService")
local Workspace = game:GetService("Workspace")
local player = Players.LocalPlayer
local placeEvent = ReplicatedStorage:WaitForChild("PlaceCatEvent")
local ROTATION_STEP = math.rad(15)
local currentTool = nil
local previewModel = nil
local currentFloor = nil
local currentPosition = nil
local rotationY = 0
local canPlace = false
local lastTouchPosition = nil
local renderConnection = nil
local characterConnections = {}
--==================================================
-- МОБИЛЬНЫЙ ИНТЕРФЕЙС
--==================================================
local screenGui = Instance.new("ScreenGui")
screenGui.Name = "CatPlacementGui"
screenGui.ResetOnSpawn = false
screenGui.Enabled = false
screenGui.DisplayOrder = 20
screenGui.Parent = player:WaitForChild("PlayerGui")
local function makeButton(name, text, position, size, color)
local button = Instance.new("TextButton")
button.Name = name
button.Text = text
button.Position = position
button.Size = size
button.BackgroundColor3 = color
button.BackgroundTransparency = 0.1
button.TextColor3 = Color3.new(1, 1, 1)
button.TextScaled = true
button.Font = Enum.Font.GothamBold
button.AutoButtonColor = true
button.BorderSizePixel = 0
button.Parent = screenGui
local corner = Instance.new("UICorner")
corner.CornerRadius = UDim.new(0, 12)
corner.Parent = button
local constraint = Instance.new("UITextSizeConstraint")
constraint.MinTextSize = 12
constraint.MaxTextSize = 24
constraint.Parent = button
return button
end
local rotateLeftButton = makeButton(
"RotateLeft",
"↺ 15°",
UDim2.new(1, -196, 1, -150),
UDim2.fromOffset(88, 48),
Color3.fromRGB(65, 85, 125)
)
local rotateRightButton = makeButton(
"RotateRight",
"↻ 15°",
UDim2.new(1, -100, 1, -150),
UDim2.fromOffset(88, 48),
Color3.fromRGB(65, 85, 125)
)
local placeButton = makeButton(
"Place",
"ПОСТАВИТЬ",
UDim2.new(1, -196, 1, -92),
UDim2.fromOffset(184, 48),
Color3.fromRGB(45, 155, 75)
)
local cancelButton = makeButton(
"Cancel",
"ОТМЕНА",
UDim2.new(1, -196, 1, -36),
UDim2.fromOffset(184, 36),
Color3.fromRGB(165, 65, 65)
)
--==================================================
-- ОЧИСТКА
--==================================================
local function destroyPreview()
if renderConnection then
renderConnection:Disconnect()
renderConnection = nil
end
if previewModel then
previewModel:Destroy()
previewModel = nil
end
currentTool = nil
currentFloor = nil
currentPosition = nil
canPlace = false
rotationY = 0
screenGui.Enabled = false
end
--==================================================
-- СОЗДАНИЕ ПРЕДПРОСМОТРА
--==================================================
local function createPreview(tool)
local model = Instance.new("Model")
model.Name = "CatPlacementPreview"
for _, child in ipairs(tool:GetChildren()) do
if child:IsA("BasePart") then
local part = child:Clone()
for _, descendant in ipairs(part:GetDescendants()) do
if descendant:IsA("Script")
or descendant:IsA("LocalScript")
or descendant:IsA("ProximityPrompt") then
descendant:Destroy()
end
end
part.Anchored = true
part.CanCollide = false
part.CanTouch = false
part.CanQuery = false
part.Transparency = math.max(part.Transparency, 0.45)
part.Parent = model
end
end
local handle = model:FindFirstChild("Handle")
if not handle or not handle:IsA("BasePart") then
model:Destroy()
warn("У кота отсутствует BasePart с именем Handle:", tool.Name)
return nil
end
model.PrimaryPart = handle
model.Parent = Workspace
return model
end
--==================================================
-- ПРОВЕРКА СОБСТВЕННОЙ БАЗЫ
--==================================================
local function isOurFloor(target)
if not target or not target:IsA("BasePart") then
return false
end
if target.Name ~= "Floor" then
return false
end
local baseName = player:GetAttribute("BaseName")
if typeof(baseName) ~= "string" then
return false
end
local base = Workspace:FindFirstChild(baseName)
if not base or not base:IsA("Model") then
return false
end
local floor = base:FindFirstChild("Floor")
return target == floor
and base:GetAttribute("OwnerUserId") == player.UserId
end
local function pointInsideFloor(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
end
--==================================================
-- ПОВОРОТ И ПОЗИЦИЯ
--==================================================
local function rotateCat(direction)
if not previewModel then
return
end
rotationY += ROTATION_STEP * direction
end
local function positionPreview(position, floor)
if not previewModel then
return
end
-- Свободное размещение без сетки.
-- Угол поворота хранится отдельно для каждой установки.
previewModel:PivotTo(
CFrame.new(position) * CFrame.Angles(0, rotationY, 0)
)
-- Ставим нижнюю точку модели на поверхность пола.
local boxCFrame, boxSize = previewModel:GetBoundingBox()
local bottomY = boxCFrame.Position.Y - boxSize.Y / 2
local floorTopY = floor.Position.Y + floor.Size.Y / 2
local correction = floorTopY - bottomY
previewModel:PivotTo(
previewModel:GetPivot()
+ Vector3.new(0, correction, 0)
)
end
--==================================================
-- ПОДТВЕРЖДЕНИЕ И ОТМЕНА
--==================================================
local function confirmPlacement()
if not previewModel
or not currentTool
or not currentFloor
or not currentPosition
or not canPlace then
return
end
placeEvent:FireServer(
currentFloor,
currentPosition,
rotationY
)
end
local function cancelPlacement()
destroyPreview()
end
rotateLeftButton.Activated:Connect(function()
rotateCat(-1)
end)
rotateRightButton.Activated:Connect(function()
rotateCat(1)
end)
placeButton.Activated:Connect(confirmPlacement)
cancelButton.Activated:Connect(cancelPlacement)
--==================================================
-- ПОИСК ПОЛА ПОД УКАЗАТЕЛЕМ
--==================================================
local function isTouchDevice()
return UserInputService.TouchEnabled
and not UserInputService.MouseEnabled
end
local function getScreenPosition()
if isTouchDevice() then
return lastTouchPosition
end
return UserInputService:GetMouseLocation()
end
local function getFloorUnderPointer()
local screenPosition = getScreenPosition()
if not screenPosition then
return nil, nil
end
local camera = Workspace.CurrentCamera
if not camera then
return nil, nil
end
-- ScreenPointToRay использует экранные координаты указателя.
local ray = camera:ScreenPointToRay(
screenPosition.X,
screenPosition.Y
)
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
local exclusions = {}
if player.Character then
table.insert(exclusions, player.Character)
end
if previewModel then
table.insert(exclusions, previewModel)
end
params.FilterDescendantsInstances = exclusions
params.IgnoreWater = true
local result = Workspace:Raycast(
ray.Origin,
ray.Direction * 1000,
params
)
if not result then
return nil, nil
end
if not isOurFloor(result.Instance) then
return nil, nil
end
if not pointInsideFloor(result.Instance, result.Position) then
return nil, nil
end
return result.Instance, result.Position
end
--==================================================
-- КАСАНИЯ И МОБИЛЬНЫЕ КНОПКИ
--==================================================
local function isTouchOverGui(position)
local guiObjects = GuiService:GetGuiObjectsAtPosition(
position.X,
position.Y
)
for _, object in ipairs(guiObjects) do
if object:IsDescendantOf(screenGui) then
return true
end
end
return false
end
UserInputService.InputBegan:Connect(function(input, processed)
if input.UserInputType == Enum.UserInputType.Touch then
local position = Vector2.new(
input.Position.X,
input.Position.Y
)
if not isTouchOverGui(position) then
lastTouchPosition = position
end
return
end
if processed or not previewModel then
return
end
if input.KeyCode == Enum.KeyCode.R then
rotateCat(1)
elseif input.KeyCode == Enum.KeyCode.T then
rotateCat(-1)
elseif input.KeyCode == Enum.KeyCode.Escape then
cancelPlacement()
elseif input.UserInputType == Enum.UserInputType.MouseButton1 then
confirmPlacement()
end
end)
UserInputService.InputChanged:Connect(function(input)
if input.UserInputType == Enum.UserInputType.Touch then
local position = Vector2.new(
input.Position.X,
input.Position.Y
)
if not isTouchOverGui(position) then
lastTouchPosition = position
end
end
end)
--==================================================
-- ПОИСК ЭКИПИРОВАННОГО КОТА
--==================================================
local function findEquippedCat()
local character = player.Character
if not character then
return nil
end
for _, item in ipairs(character:GetChildren()) do
if item:IsA("Tool")
and item:GetAttribute("Income") ~= nil
and item:GetAttribute("Rarity") ~= nil then
return item
end
end
return nil
end
--==================================================
-- ЗАПУСК РЕЖИМА РАЗМЕЩЕНИЯ
--==================================================
local function startPlacement(tool)
if currentTool == tool and previewModel then
return
end
destroyPreview()
local model = createPreview(tool)
if not model then
return
end
currentTool = tool
previewModel = model
rotationY = 0
screenGui.Enabled = true
renderConnection = RunService.RenderStepped:Connect(function()
if not currentTool
or not currentTool.Parent
or not previewModel
or not previewModel.Parent then
destroyPreview()
return
end
local floor, position = getFloorUnderPointer()
currentFloor = floor
currentPosition = position
canPlace = floor ~= nil and position ~= nil
if canPlace then
positionPreview(position, floor)
end
-- Зелёный = допустимая позиция.
-- Красный = вне собственной базы.
local previewColor
if canPlace then
previewColor = Color3.fromRGB(100, 255, 120)
else
previewColor = Color3.fromRGB(255, 90, 90)
end
for _, part in ipairs(previewModel:GetDescendants()) do
if part:IsA("BasePart") then
part.Color = previewColor
end
end
end)
end
local function disconnectCharacterConnections()
for _, connection in ipairs(characterConnections) do
connection:Disconnect()
end
table.clear(characterConnections)
end
local function watchCharacter(character)
destroyPreview()
disconnectCharacterConnections()
table.insert(
characterConnections,
character.ChildAdded:Connect(function(child)
if child:IsA("Tool")
and child:GetAttribute("Income") ~= nil
and child:GetAttribute("Rarity") ~= nil then
task.defer(function()
if child.Parent == character then
startPlacement(child)
end
end)
end
end)
)
table.insert(
characterConnections,
character.ChildRemoved:Connect(function(child)
if child == currentTool then
destroyPreview()
end
end)
)
task.defer(function()
if player.Character ~= character then
return
end
local tool = findEquippedCat()
if tool then
startPlacement(tool)
end
end)
end
player.CharacterAdded:Connect(watchCharacter)
player.CharacterRemoving:Connect(function()
destroyPreview()
disconnectCharacterConnections()
end)
if player.Character then
watchCharacter(player.Character)
end
print("CatPlacementClient запущен")