Загрузка данных
-- CatPlacementServer
-- Разместить в ServerScriptService как обычный Script
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Workspace = game:GetService("Workspace")
local CONFIG = {
MAX_CATS = 16,
MAX_DISTANCE = 35,
ROTATION_STEP = 15,
RETURN_HOLD_DURATION = 1,
INCOME_INTERVAL = 1,
DEBUG = true,
}
local function log(...)
if CONFIG.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(toolName)
local template = catTools:FindFirstChild(toolName)
if template and template:IsA("Tool")
and template:FindFirstChild("Handle")
and template.Handle:IsA("BasePart") then
return template
end
return nil
end
-- Настройка настоящего Tool. Handle никогда не должен быть Anchored,
-- пока Tool находится в Backpack или Character.
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("У Tool нет корректного Handle:", tool:GetFullName())
return
end
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
-- Дополнительные части Tool соединяются с Handle.
-- Не создаём повторный WeldConstraint, если части уже соединены.
for _, obj in ipairs(tool:GetDescendants()) do
if obj:IsA("BasePart") and obj ~= handle then
local hasConnection = false
for _, joint in ipairs(obj:GetDescendants()) do
if joint:IsA("WeldConstraint")
and joint.Part0 and joint.Part1 then
hasConnection = true
break
end
end
if not hasConnection then
local weld = Instance.new("WeldConstraint")
weld.Name = "CatToolAutoWeld"
weld.Part0 = handle
weld.Part1 = obj
weld.Parent = handle
end
end
end
tool.RequiresHandle = true
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)
task.defer(function()
for _, child in ipairs(character:GetChildren()) do
if child:IsA("Tool") then
prepareTool(child)
end
end
end)
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 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 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 isInsideFloor(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
and math.abs(localPosition.Y) <= floor.Size.Y / 2 + 3
end
local function positionModel(model, floor, position, yaw)
-- Сначала размещаем модель, затем корректируем высоту по её габаритам.
model:PivotTo(
CFrame.new(position) * CFrame.Angles(0, math.rad(yaw), 0)
)
local boxCFrame, boxSize = model:GetBoundingBox()
local floorTop = floor.Position.Y + floor.Size.Y / 2
local modelBottom = boxCFrame.Position.Y - boxSize.Y / 2
local correctedY = position.Y + (floorTop - modelBottom)
model:PivotTo(
CFrame.new(position.X, correctedY, position.Z)
* CFrame.Angles(0, math.rad(yaw), 0)
)
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)
if not template then
warnLog("Не удалось вернуть кота: шаблон отсутствует", templateName)
return false
end
local backpack = player:FindFirstChildOfClass("Backpack")
if 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, yaw)
local template = getTemplate(tool.Name)
if not template then
return false, "Шаблон кота не найден"
end
local income = template:GetAttribute("Income")
if typeof(income) ~= "number" 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()
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, yaw)
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 = CONFIG.RETURN_HOLD_DURATION
prompt.MaxActivationDistance = 10
prompt.RequiresLineOfSight = false
prompt.Parent = handle
local returning = false
prompt.Triggered:Connect(function(triggeringPlayer)
if returning then
return
end
if triggeringPlayer ~= player then
return
end
if model:GetAttribute("OwnerUserId") ~= triggeringPlayer.UserId then
return
end
if not model.Parent then
return
end
returning = true
prompt.Enabled = false
if giveCatBack(triggeringPlayer, model:GetAttribute("TemplateName")) then
model:Destroy()
log("Кот возвращён:", triggeringPlayer.Name, template.Name)
else
returning = false
if prompt.Parent then
prompt.Enabled = true
end
end
end)
return true
end
remote.OnServerEvent:Connect(function(player, floor, position, yaw)
-- Проверка входных данных: клиент не является доверенным источником.
if typeof(floor) ~= "Instance" or not floor:IsA("BasePart") then
warnLog("Отклонён запрос: некорректный Floor от", player.Name)
return
end
if typeof(position) ~= "Vector3" then
warnLog("Отклонён запрос: некорректная позиция от", player.Name)
return
end
if typeof(yaw) ~= "number" or yaw ~= yaw
or math.abs(yaw) > 100000 then
warnLog("Отклонён запрос: некорректный угол от", player.Name)
return
end
local base, ownFloor = getOwnedBase(player)
if not base or floor ~= ownFloor then
warnLog("Отклонено: игрок пытается ставить кота не на свою базу", player.Name)
return
end
local character = player.Character
local root = character and character:FindFirstChild("HumanoidRootPart")
if not root then
return
end
if (root.Position - position).Magnitude > CONFIG.MAX_DISTANCE then
warnLog("Отклонено: позиция слишком далеко от игрока", player.Name)
return
end
if not isInsideFloor(floor, position) then
warnLog("Отклонено: позиция вне пола базы", player.Name)
return
end
if countCats(player) >= CONFIG.MAX_CATS then
warnLog("Лимит котов достигнут:", player.Name)
return
end
local tool = getEquippedCat(player)
if not tool then
warnLog("Отклонено: у игрока нет экипированного Tool-кота", player.Name)
return
end
local success, reason = createPlacedCat(player, tool, floor, position, yaw)
if not success then
warnLog("Не удалось разместить кота:", reason)
return
end
tool:Destroy()
log("Размещён кот:", player.Name, "Количество:", countCats(player))
end)
-- Доход начисляется только владельцу кота.
task.spawn(function()
while true do
task.wait(CONFIG.INCOME_INTERVAL)
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")
if typeof(ownerId) == "number"
and typeof(income) == "number"
and income > 0 then
local owner = Players:GetPlayerByUserId(ownerId)
local stats = owner and owner:FindFirstChild("leaderstats")
local coins = stats and stats:FindFirstChild("Coins")
if coins and (coins:IsA("IntValue") or coins:IsA("NumberValue")) then
coins.Value += income
end
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("CatTools:", catTools:GetFullName())
log("PlaceCatEvent:", remote:GetFullName())
log("PlacedCats:", placedFolder:GetFullName())
for _, template in ipairs(catTools:GetChildren()) do
if template:IsA("Tool") then
if not template:FindFirstChild("Handle") then
warnLog("ПРОВЕРКА НЕ ПРОЙДЕНА: нет Handle у", template.Name)
elseif typeof(template:GetAttribute("Income")) ~= "number" then
warnLog("ПРОВЕРКА: у", template.Name, "не задан числовой Income")
else
log("Шаблон проверен:", template.Name,
"Income =", template:GetAttribute("Income"),
"Rarity =", template:GetAttribute("Rarity"))
end
end
end