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