Roblox · Pet Simulator · урок 7

Общий: вся игра целиком

Итог курса на одной странице: полная структура Pet Simulator и финальные версии всех скриптов — без «добавь строку» и «замени блок». Удобно сверить свою игру или собрать её заново.

Структура

Синим — что добавляем на этом уроке.

Workspace
├─ PlayerPets (Folder) — папка каждого игрока с его питомцами
├─ Drops (Folder)
│  └─ FirstArea, SecondArea (Folder)
│     └─ FirstDrop, SecondDrop (Model) — Main, HealthGui (Background → Bar, HealthText), Health, MaxHealth, Coins, (Gems), ClickDetector
├─ Eggs (Folder)
│  └─ Start Egg, Gem Egg (Part) — ProximityPrompt
└─ Doors (Folder)
   └─ SecondArea (Part) — Price, PriceGui → PriceText, Prompt → ProximityPrompt
ReplicatedStorage
├─ Pets (Folder)
│  └─ Bunny, Cat, Dog, Dragon (Model) — Primary + детали (Anchored), Attack, Damage
└─ Events (Folder)
   ├─ HatchEggEvent, BuyAreaEvent (RemoteEvent)
   └─ EquipPetFunction, BuyAreaFunction (RemoteFunction)
ServerScriptService
├─ MainScript (Script)
│  ├─ PlayerInfo (Folder)
│  │  ├─ Values (Folder) — Target (ObjectValue)
│  │  ├─ leaderstats (Folder) — Coins, Gems
│  │  ├─ Pets (Folder)
│  │  └─ Areas (Folder)
│  ├─ Pet (ModuleScript)
│  │  └─ Drop (ModuleScript)
│  └─ SaveData (ModuleScript)
└─ EggsServer (Script)
   └─ EggInfo (ModuleScript)
StarterPlayer → StarterPlayerScripts
└─ PetFollow, EggsLocal, MainLocal (LocalScript)
StarterGui — у всех ResetOnSpawn выкл.
├─ EggSystemGui (ScreenGui) — EggViewport → EggName, EggRarity
├─ PetInventoryGui (ScreenGui) — OpenCloseButton (+ OpenCloseScript), PetFrame → Title, ScrollingFrame → Sample → Button, PetInventoryLocal
└─ BuyAreaGui (ScreenGui) — BuyFrame → Question, YesButton, NoButton

Шаги

  1. Сверяй сверху вниз: сначала Explorer со «Структурой», потом каждый скрипт целиком.
  2. Питомцы: все детали Anchored, CanCollide и CanQuery выкл., PrimaryPart = Primary, внутри Attack и Damage.
  3. Цели: Main (PrimaryPart), HealthGui → Background → Bar и HealthText, Health = MaxHealth, Coins, ClickDetector.
  4. Имена связывают всё: имя яйца = ключ в EggInfo, имя питомца в EggInfo = модель в Pets, имя стены = название области.
  5. Все ScreenGui — ResetOnSpawn выкл.: их скрипты держат ссылки на интерфейс.
  6. Сохранение работает в Studio только после публикации и включения Game Settings → Security → API Services.

Скрипты

MainScript

Где: ServerScriptService · Script
Зачем: Папки игрока, атака, инвентарь, сохранение, области.

-- все игроки
local Players = game:GetService("Players")
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- модуль питомцев
local pet = require(script.Pet)
-- модуль целей
local drop = require(script.Pet.Drop)
-- сохранение
local saveData = require(script.SaveData)
-- события
local events = RS:WaitForChild("Events")
-- надеть / снять питомца
local equipFunction = events:WaitForChild("EquipPetFunction")
-- показать окно покупки
local buyAreaEvent = events:WaitForChild("BuyAreaEvent")
-- купить область
local buyAreaFunction = events:WaitForChild("BuyAreaFunction")
-- образцы питомцев
local pets = RS:WaitForChild("Pets")
-- питомцы игроков на карте
local playerPets = workspace:WaitForChild("PlayerPets")
-- цели
local drops = workspace:WaitForChild("Drops")
-- стены областей
local doors = workspace:WaitForChild("Doors")
-- сколько питомцев можно взять с собой
local MAX_EQUIPPED = 3

-- игрок зашёл
Players.PlayerAdded:Connect(function(player)
    -- папка для его питомцев
    local folder = Instance.new("Folder")
    -- имя = имя игрока
    folder.Name = player.Name
    -- в PlayerPets
    folder.Parent = playerPets
    -- папки из PlayerInfo
    for _, info in ipairs(script.PlayerInfo:GetChildren()) do
        -- копия — в игрока
        info:Clone().Parent = player
    -- конец перебора
    end
    -- загружаем монеты, питомцев, области
    saveData.Load(player)
-- конец обработчика
end)

-- игрок вышел
Players.PlayerRemoving:Connect(function(player)
    -- сохраняем
    saveData.Save(player)
    -- его папка
    local folder = playerPets:FindFirstChild(player.Name)
    -- убираем питомцев
    if folder then folder:Destroy() end
-- конец обработчика
end)

-- сервер выключается — сохраняем всех и ждём
game:BindToClose(saveData.SaveAll)

-- подготовить цель
local function SetupDrop(dropObject)
    -- полоска здоровья
    drop.UpdateGui(dropObject)
    -- клик по цели — подключаем ОДИН раз
    dropObject.ClickDetector.MouseClick:Connect(function(player)
        -- питомцы атакуют
        pet.Attack(player, dropObject)
    -- конец обработчика
    end)
-- конец функции
end
-- цели, что уже стоят
for _, obj in ipairs(drops:GetDescendants()) do
    -- это цель
    if obj:IsA("Model") and obj:FindFirstChild("ClickDetector") then SetupDrop(obj) end
-- конец перебора
end
-- цель появилась снова
drops.DescendantAdded:Connect(function(obj)
    -- подключаем и её
    if obj:IsA("Model") and obj:FindFirstChild("ClickDetector") then SetupDrop(obj) end
-- конец обработчика
end)
-- отдельный поток
task.spawn(function()
    -- каждую секунду
    while task.wait(1) do
        -- питомцы наносят урон
        pet.Tick()
    -- конец цикла
    end
-- конец потока
end)

-- игрок нажал на иконку питомца
equipFunction.OnServerInvoke = function(player, petName, equip)
    -- прислали не текст
    if typeof(petName) ~= "string" then return false end
    -- его питомцы на карте
    local folder = playerPets:FindFirstChild(player.Name)
    -- образец
    local template = pets:FindFirstChild(petName)
    -- нет папки или такого питомца
    if not folder or not template then return false end
    -- снять
    if not equip then
        -- этот питомец на карте
        local active = folder:FindFirstChild(petName)
        -- убираем
        if active then active:Destroy() end
        -- готово
        return true
    -- конец проверки
    end
    -- сколько таких есть и сколько уже надето
    local owned, active = 0, 0
    -- купленные питомцы
    for _, value in ipairs(player.Pets:GetChildren()) do
        -- считаем таких
        if value.Name == petName then owned += 1 end
    -- конец перебора
    end
    -- надетые
    for _, petModel in ipairs(folder:GetChildren()) do
        -- считаем таких
        if petModel.Name == petName then active += 1 end
    -- конец перебора
    end
    -- не свой или уже максимум
    if active >= owned or #folder:GetChildren() >= MAX_EQUIPPED then return false end
    -- питомец
    local clone = template:Clone()
    -- появляется у хозяина
    if player.Character then clone:PivotTo(player.Character:GetPivot()) end
    -- сразу к текущей цели
    clone.Attack.Value = player.Values.Target.Value
    -- на карту
    clone.Parent = folder
    -- надет
    return true
-- конец функции
end

-- игрок нажал «Да»
buyAreaFunction.OnServerInvoke = function(player, door)
    -- это не стена области
    if typeof(door) ~= "Instance" or door.Parent ~= doors then return false end
    -- уже куплено
    if player.Areas:FindFirstChild(door.Name) then return false end
    -- кошелёк
    local coins = player.leaderstats.Coins
    -- не хватает
    if coins.Value < door.Price.Value then return false end
    -- списываем
    coins.Value -= door.Price.Value
    -- отметка о покупке
    local areaValue = Instance.new("StringValue")
    -- имя области
    areaValue.Name = door.Name
    -- в папку Areas — клиент уберёт стену сам
    areaValue.Parent = player.Areas
    -- куплено
    return true
-- конец функции
end

-- каждая стена
for _, door in ipairs(doors:GetChildren()) do
    -- цена на стене
    door.PriceGui.PriceText.Text = door.Price.Value .. " Coins"
    -- игрок нажал E у стены
    door.Prompt.ProximityPrompt.Triggered:Connect(function(player)
        -- ещё не купил
        if not player.Areas:FindFirstChild(door.Name) then
            -- показываем ему окно покупки
            buyAreaEvent:FireClient(player, door)
        -- конец проверки
        end
    -- конец обработчика
    end)
-- конец перебора
end

Pet

Где: ServerScriptService → MainScript · ModuleScript
Зачем: Цели питомцев и урон раз в секунду.

-- все игроки
local Players = game:GetService("Players")
-- модуль целей
local drop = require(script.Drop)
-- папки с питомцами игроков
local playerPets = workspace:WaitForChild("PlayerPets")
-- таблица модуля
local pet = {}

-- питомцы игрока, которые сейчас с ним
local function PetsOf(player)
    -- его папка
    local folder = playerPets:FindFirstChild(player.Name)
    -- список или пустая таблица
    return folder and folder:GetChildren() or {}
-- конец функции
end

-- назначает цель всем питомцам игрока (nil — отозвать)
function pet.SetTarget(player, dropObject)
    -- цель игрока
    player.Values.Target.Value = dropObject
    -- каждый питомец
    for _, playerPet in ipairs(PetsOf(player)) do
        -- бежит к цели или к хозяину
        playerPet.Attack.Value = dropObject
    -- конец перебора
    end
-- конец функции
end

-- игрок кликнул по цели
function pet.Attack(player, dropObject)
    -- питомцев нет — атаковать некому
    if #PetsOf(player) == 0 then return end
    -- клик по той же цели
    if player.Values.Target.Value == dropObject then
        -- отзываем питомцев
        pet.SetTarget(player, nil)
    -- новая цель
    else
        -- все питомцы — к ней
        pet.SetTarget(player, dropObject)
    -- конец проверки
    end
-- конец функции
end

-- раз в секунду: питомцы бьют свои цели
function pet.Tick()
    -- каждый игрок
    for _, player in ipairs(Players:GetPlayers()) do
        -- его служебная папка
        local values = player:FindFirstChild("Values")
        -- его цель
        local target = values and values.Target.Value
        -- цель уже разрушена кем-то другим
        if target and not target.Parent then
            -- питомцы возвращаются
            pet.SetTarget(player, nil)
        -- цель на месте
        elseif target then
            -- общий урон
            local damage = 0
            -- каждый питомец
            for _, playerPet in ipairs(PetsOf(player)) do
                -- его урон
                local petDamage = playerPet:FindFirstChild("Damage")
                -- без Damage — 10
                damage += petDamage and petDamage.Value or 10
            -- конец перебора
            end
            -- бьём; цель разрушена
            if damage > 0 and drop.Damage(player, target, damage) then
                -- все игроки
                for _, other in ipairs(Players:GetPlayers()) do
                    -- их цели
                    local otherValues = other:FindFirstChild("Values")
                    -- тоже били эту цель
                    if otherValues and otherValues.Target.Value == target then
                        -- их питомцы возвращаются
                        pet.SetTarget(other, nil)
                    -- конец проверки
                    end
                -- конец перебора
                end
            -- конец проверки
            end
        -- конец проверки
        end
    -- конец перебора
    end
-- конец функции
end

-- отдаём таблицу модуля
return pet

Drop

Где: ServerScriptService → MainScript → Pet · ModuleScript
Зачем: Здоровье целей, награда, возвращение.

-- таблица модуля
local drop = {}
-- через сколько секунд цель появляется снова
local RESPAWN = 5

-- обновляет полоску здоровья над целью
function drop.UpdateGui(dropObject)
    -- полоска
    local gui = dropObject:FindFirstChild("HealthGui")
    -- полоски нет
    if not gui then return end
    -- здоровье сейчас
    local health = dropObject.Health.Value
    -- максимум
    local maxHealth = dropObject.MaxHealth.Value
    -- текст «70 / 100»
    gui.Background.HealthText.Text = math.ceil(health) .. " / " .. maxHealth
    -- зелёная полоса укорачивается
    gui.Background.Bar.Size = UDim2.fromScale(health / maxHealth, 1)
-- конец функции
end

-- урон цели; true — если цель разрушена
function drop.Damage(player, dropObject, damage)
    -- здоровье цели
    local health = dropObject.Health
    -- уже разрушена
    if health.Value <= 0 then return false end
    -- вычитаем урон, не ниже нуля
    health.Value = math.max(health.Value - damage, 0)
    -- обновляем полоску
    drop.UpdateGui(dropObject)
    -- ещё цела
    if health.Value > 0 then return false end
    -- награда монетами
    player.leaderstats.Coins.Value += dropObject.Coins.Value
    -- даёт ли цель кристаллы
    local gems = dropObject:FindFirstChild("Gems")
    -- награда кристаллами
    if gems then player.leaderstats.Gems.Value += gems.Value end
    -- папка области
    local parent = dropObject.Parent
    -- копия для возвращения
    local respawn = dropObject:Clone()
    -- у копии полное здоровье
    respawn.Health.Value = respawn.MaxHealth.Value
    -- и полная полоска
    drop.UpdateGui(respawn)
    -- цель исчезает
    dropObject:Destroy()
    -- через несколько секунд
    task.delay(RESPAWN, function()
        -- цель снова на месте
        respawn.Parent = parent
    -- конец отложенной функции
    end)
    -- разрушена
    return true
-- конец функции
end

-- отдаём таблицу модуля
return drop

SaveData

Где: ServerScriptService → MainScript · ModuleScript
Зачем: Загрузка и сохранение.

-- хранилище данных Roblox
local DSS = game:GetService("DataStoreService")
-- все игроки
local Players = game:GetService("Players")
-- наше хранилище
local dataStore = DSS:GetDataStore("PetSimulatorData")
-- какие папки игрока сохраняем (Values — нет)
local SAVE = {"leaderstats", "Pets", "Areas"}
-- таблица модуля
local saveData = {}
-- у кого данные загрузились
local loaded = {}

-- загрузить данные игрока
function saveData.Load(player)
    -- запрос может не пройти
    local ok, data = pcall(function()
        -- данные по номеру игрока
        return dataStore:GetAsync(player.UserId .. "Key")
    -- конец попытки
    end)
    -- не загрузилось
    if not ok then
        -- без загрузки не сохраняем — иначе затрём
        warn("Данные не загрузились (" .. tostring(data) .. ") — сохранение для " .. player.Name .. " выключено")
        -- играем без сохранения
        return
    -- конец проверки
    end
    -- можно сохранять
    loaded[player] = true
    -- новый игрок — данных ещё нет
    if not data then return end
    -- каждая сохраняемая папка
    for _, folderName in ipairs(SAVE) do
        -- папка в игроке
        local folder = player:FindFirstChild(folderName)
        -- что сохранено
        local saved = data[folderName]
        -- есть и то и другое
        if folder and saved then
            -- каждое значение
            for _, item in ipairs(saved) do
                -- такое значение уже есть (Coins, Gems)
                local existing = folder:FindFirstChild(item[1])
                -- есть
                if existing then
                    -- просто меняем число
                    existing.Value = item[2]
                -- нет (питомцы, области)
                else
                    -- создаём того же класса
                    local value = Instance.new(item[3])
                    -- имя
                    value.Name = item[1]
                    -- значение
                    value.Value = item[2]
                    -- в папку
                    value.Parent = folder
                -- конец проверки
                end
            -- конец перебора
            end
        -- конец проверки
        end
    -- конец перебора
    end
-- конец функции
end

-- сохранить данные игрока
function saveData.Save(player)
    -- не загружали — не трогаем
    if not loaded[player] then return end
    -- второй раз не сохраняем
    loaded[player] = nil
    -- что запишем
    local data = {}
    -- каждая папка
    for _, folderName in ipairs(SAVE) do
        -- в игроке
        local folder = player:FindFirstChild(folderName)
        -- есть
        if folder then
            -- список значений
            data[folderName] = {}
            -- каждое значение
            for _, value in ipairs(folder:GetChildren()) do
                -- имя, значение, класс
                table.insert(data[folderName], {value.Name, value.Value, value.ClassName})
            -- конец перебора
            end
        -- конец проверки
        end
    -- конец перебора
    end
    -- запись может не пройти
    local ok, err = pcall(function()
        -- записываем
        dataStore:SetAsync(player.UserId .. "Key", data)
    -- конец попытки
    end)
    -- результат в Output
    if ok then print("Сохранено: " .. player.Name) else warn(err) end
-- конец функции
end

-- сохранить всех и дождаться (выключение сервера)
function saveData.SaveAll()
    -- сколько сохранений ещё идёт
    local left = 0
    -- каждый игрок
    for _, player in ipairs(Players:GetPlayers()) do
        -- ещё одно
        left += 1
        -- параллельно
        task.spawn(function()
            -- сохраняем
            saveData.Save(player)
            -- готово
            left -= 1
        -- конец потока
        end)
    -- конец перебора
    end
    -- ждём все сохранения
    while left > 0 do task.wait() end
-- конец функции
end

-- отдаём таблицу модуля
return saveData

EggsServer

Где: ServerScriptService · Script
Зачем: Покупка яиц.

-- данные яиц
local eggData = require(script.EggInfo)
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- яйца на карте
local eggsFolder = workspace:WaitForChild("Eggs")
-- показать анимацию игроку
local hatchEggEvent = RS:WaitForChild("Events"):WaitForChild("HatchEggEvent")
-- кто сейчас открывает яйцо
local hatching = {}

-- случайный питомец с учётом шансов
local function SelectRandomPet(petTable)
    -- сумма шансов
    local total = 0
    -- складываем — не обязательно 100
    for _, info in ipairs(petTable) do total += info.Chance end
    -- случайное число
    local roll = math.random(1, total)
    -- накопленный шанс
    local sum = 0
    -- по питомцам
    for _, info in ipairs(petTable) do
        -- прибавляем
        sum += info.Chance
        -- число попало на этого питомца
        if roll <= sum then return info end
    -- конец перебора
    end
-- конец функции
end

-- каждое яйцо
for _, egg in ipairs(eggsFolder:GetChildren()) do
    -- его данные
    local info = eggData[egg.Name]
    -- надпись «Купить»
    local prompt = egg:FindFirstChildWhichIsA("ProximityPrompt", true)
    -- чего-то нет
    if not info or not prompt then
        -- подсказка в Output
        warn("Яйцо " .. egg.Name .. ": нет данных в EggInfo или ProximityPrompt")
    -- всё есть
    else
        -- название
        prompt.ObjectText = egg.Name
        -- цена на надписи
        prompt.ActionText = info.EggPrice .. " " .. info.EggMoney
        -- игрок нажал E
        prompt.Triggered:Connect(function(player)
            -- уже открывает — ждём
            if hatching[player] then return end
            -- нужная валюта
            local money = player.leaderstats:FindFirstChild(info.EggMoney)
            -- не хватает
            if not money or money.Value < info.EggPrice then return end
            -- открывает
            hatching[player] = true
            -- списываем
            money.Value -= info.EggPrice
            -- кто выпал
            local selected = SelectRandomPet(info.EggPets)
            -- запись о питомце
            local petValue = Instance.new("StringValue")
            -- имя
            petValue.Name = selected.PetName
            -- в папку Pets игрока
            petValue.Parent = player.Pets
            -- анимация у игрока
            hatchEggEvent:FireClient(player, egg.Name, selected.PetName, selected.Rarity)
            -- пока идёт анимация
            task.wait(5)
            -- можно открыть следующее
            hatching[player] = nil
        -- конец обработчика
        end)
    -- конец проверки
    end
-- конец перебора
end

EggInfo

Где: ServerScriptService → EggsServer · ModuleScript
Зачем: Цены яиц и шансы питомцев.

-- все яйца
local eggData = {
    -- имя — как у яйца в папке Eggs
    ["Start Egg"] = {
        -- цена
        EggPrice = 10,
        -- валюта: Coins или Gems
        EggMoney = "Coins",
        -- кто может выпасть
        EggPets = {
            -- имя — как у модели в Pets; Chance — вес
            {PetName = "Bunny", Chance = 50, Rarity = "Common"},
            -- ещё один
            {PetName = "Cat", Chance = 50, Rarity = "Common"},
        -- конец списка
        },
    -- конец яйца
    },
    -- яйцо за кристаллы
    ["Gem Egg"] = {
        -- цена
        EggPrice = 5,
        -- валюта
        EggMoney = "Gems",
        -- кто может выпасть
        EggPets = {
            -- чаще
            {PetName = "Dog", Chance = 70, Rarity = "Uncommon"},
            -- реже
            {PetName = "Dragon", Chance = 30, Rarity = "Rare"},
        -- конец списка
        },
    -- конец яйца
    },
-- конец таблицы
}

-- отдаём таблицу
return eggData

PetFollow

Где: StarterPlayer → StarterPlayerScripts · LocalScript
Зачем: Движение питомцев.

-- события каждого кадра
local RunService = game:GetService("RunService")
-- все игроки
local Players = game:GetService("Players")
-- папки с питомцами игроков
local playerPets = workspace:WaitForChild("PlayerPets")
-- полный круг в радианах (360°)
local CIRCLE = math.pi * 2

-- точка на окружности вокруг персонажа
local function GetPosition(angle, radius)
    -- x и z этой точки
    return math.cos(angle) * radius, math.sin(angle) * radius
-- конец функции
end

-- двигает питомцев одного игрока
local function PositionPets(character, folder)
    -- центр персонажа
    local root = character:FindFirstChild("HumanoidRootPart")
    -- его Humanoid
    local humanoid = character:FindFirstChildOfClass("Humanoid")
    -- персонаж ещё не загрузился
    if not root or not humanoid then return end
    -- питомцы игрока
    local list = folder:GetChildren()
    -- сколько их
    local count = #list
    -- чем больше питомцев, тем шире круг
    local radius = 4 + count
    -- размер персонажа
    local _, characterSize = character:GetBoundingBox()
    -- каждый питомец
    for i, pet in ipairs(list) do
        -- это модель с главной деталью
        if pet:IsA("Model") and pet.PrimaryPart then
            -- своё место на круге
            local angle = i * (CIRCLE / count)
            -- координаты места
            local x, z = GetPosition(angle, radius)
            -- размер питомца
            local _, petSize = pet:GetBoundingBox()
            -- питомец стоит на земле, а не висит
            local offsetY = -characterSize.Y / 2 + petSize.Y / 2
            -- подскоки: от 0 до 1
            local bounce = math.sin(15 * time() + i) * 0.5 + 0.5
            -- покачивание вбок
            local tilt = math.cos(7 * time() + i) / 4
            -- куда питомец хочет попасть
            local goal
            -- кого атакует
            local attack = pet:FindFirstChild("Attack")
            -- цель или nil
            local target = attack and attack.Value
            -- цель есть и ещё не разрушена
            if target and target.Parent and target.PrimaryPart then
                -- центр цели
                local center = target.PrimaryPart.Position
                -- место вокруг цели
                local position = center + Vector3.new(x, 0, z)
                -- смотрит на цель и прыгает
                goal = CFrame.lookAt(position, center) * CFrame.new(0, 1 + bounce, 0) * CFrame.Angles(0, 0, tilt)
            -- хозяин идёт
            elseif humanoid.MoveDirection.Magnitude > 0 then
                -- бежит рядом вприпрыжку
                goal = root.CFrame * CFrame.new(x, offsetY + bounce, z) * CFrame.Angles(0, 0, tilt)
            -- хозяин стоит
            else
                -- спокойно стоит рядом
                goal = root.CFrame * CFrame.new(x, offsetY, z)
            -- конец проверки
            end
            -- каждый кадр проходит 10% пути — плавно
            pet:PivotTo(pet:GetPivot():Lerp(goal, 0.1))
        -- конец проверки
        end
    -- конец перебора
    end
-- конец функции
end

-- каждый кадр
RunService.RenderStepped:Connect(function()
    -- папка каждого игрока
    for _, folder in ipairs(playerPets:GetChildren()) do
        -- игрок с таким именем
        local player = Players:FindFirstChild(folder.Name)
        -- он в игре и с персонажем
        if player and player.Character then
            -- двигаем его питомцев
            PositionPets(player.Character, folder)
        -- конец проверки
        end
    -- конец перебора
    end
-- конец обработчика
end)

EggsLocal

Где: StarterPlayer → StarterPlayerScripts · LocalScript
Зачем: Анимация яйца.

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- анимации
local TS = game:GetService("TweenService")
-- яйца
local eggsFolder = workspace:WaitForChild("Eggs")
-- образцы питомцев
local pets = RS:WaitForChild("Pets")
-- сигнал от сервера
local hatchEggEvent = RS:WaitForChild("Events"):WaitForChild("HatchEggEvent")
-- этот игрок
local player = game.Players.LocalPlayer
-- его интерфейс
local playerGui = player:WaitForChild("PlayerGui")
-- окно яйца
local eggSystemGui = playerGui:WaitForChild("EggSystemGui")
-- 3D-окошко
local eggViewport = eggSystemGui:WaitForChild("EggViewport")
-- имя питомца
local eggName = eggViewport:WaitForChild("EggName")
-- редкость
local eggRarity = eggViewport:WaitForChild("EggRarity")
-- размер окна
local FULL = UDim2.fromScale(0.3, 0.5)
-- окно схлопнуто
local HIDDEN = UDim2.fromScale(0, 0)

-- убрать из окошка яйцо, питомца и камеру
local function Clear()
    -- всё внутри
    for _, obj in ipairs(eggViewport:GetChildren()) do
        -- камеры, детали, модели (надписи остаются)
        if obj:IsA("Camera") or obj:IsA("PVInstance") then obj:Destroy() end
    -- конец перебора
    end
-- конец функции
end

-- поставить объект в окошко
local function Show(object)
    -- старое убираем
    Clear()
    -- новый объект
    object.Parent = eggViewport
    -- его размер
    local size = object:IsA("Model") and object:GetExtentsSize() or object.Size
    -- камера окошка
    local camera = Instance.new("Camera")
    -- отходим тем дальше, чем больше объект
    camera.CFrame = CFrame.new(0, 0, size.Magnitude * 1.1)
    -- в окошко
    camera.Parent = eggViewport
    -- окошко смотрит через неё
    eggViewport.CurrentCamera = camera
-- конец функции
end

-- плавно меняет размер окна
local function Resize(size)
    -- за полсекунды
    local tween = TS:Create(eggViewport, TweenInfo.new(0.5), {Size = size})
    -- запускаем
    tween:Play()
    -- ждём
    tween.Completed:Wait()
-- конец функции
end

-- наклон окна — яйцо качается
local function Rotate(rotation)
    -- за 0,3 с
    local tween = TS:Create(eggViewport, TweenInfo.new(0.3, Enum.EasingStyle.Sine), {Rotation = rotation})
    -- запускаем
    tween:Play()
    -- ждём
    tween.Completed:Wait()
-- конец функции
end

-- вся анимация открытия
local function HatchEgg(eggNameText, petName, petRarity)
    -- интерфейсы, которые прячем
    local hidden = {}
    -- весь интерфейс игрока
    for _, ui in ipairs(playerGui:GetChildren()) do
        -- включённый и не окно яйца
        if ui:IsA("ScreenGui") and ui ~= eggSystemGui and ui.Enabled then
            -- прячем
            ui.Enabled = false
            -- запоминаем, чтобы вернуть только их
            table.insert(hidden, ui)
        -- конец проверки
        end
    -- конец перебора
    end
    -- надписи пока скрыты
    eggName.Visible = false
    -- тоже
    eggRarity.Visible = false
    -- окно схлопнуто
    eggViewport.Size = HIDDEN
    -- включаем окно яйца
    eggSystemGui.Enabled = true
    -- копия яйца
    local egg = eggsFolder:FindFirstChild(eggNameText):Clone()
    -- в центр окошка
    egg:PivotTo(CFrame.new())
    -- показываем
    Show(egg)
    -- окно раскрывается
    Resize(FULL)
    -- два раза
    for _ = 1, 2 do
        -- качнулось вправо
        Rotate(20)
        -- влево
        Rotate(-20)
    -- конец цикла
    end
    -- ровно
    Rotate(0)
    -- окно схлопнулось — яйцо «лопнуло»
    Resize(HIDDEN)
    -- образец питомца
    local template = pets:FindFirstChild(petName)
    -- есть
    if template then
        -- копия
        local petModel = template:Clone()
        -- мордой к камере
        petModel:PivotTo(CFrame.Angles(0, math.rad(180), 0))
        -- показываем
        Show(petModel)
    -- конец проверки
    end
    -- имя
    eggName.Text = petName
    -- редкость
    eggRarity.Text = petRarity
    -- показываем
    eggName.Visible = true
    -- показываем
    eggRarity.Visible = true
    -- окно раскрывается
    Resize(FULL)
    -- любуемся
    task.wait(3)
    -- схлопываем
    Resize(HIDDEN)
    -- чистим
    Clear()
    -- выключаем окно
    eggSystemGui.Enabled = false
    -- возвращаем спрятанные интерфейсы
    for _, ui in ipairs(hidden) do ui.Enabled = true end
-- конец функции
end

-- сервер прислал питомца — анимация
hatchEggEvent.OnClientEvent:Connect(HatchEgg)

MainLocal

Где: StarterPlayer → StarterPlayerScripts · LocalScript
Зачем: Покупка областей.

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- события
local events = RS:WaitForChild("Events")
-- сервер: покажи окно
local buyAreaEvent = events:WaitForChild("BuyAreaEvent")
-- купить
local buyAreaFunction = events:WaitForChild("BuyAreaFunction")
-- этот игрок
local player = game.Players.LocalPlayer
-- его купленные области
local areas = player:WaitForChild("Areas")
-- стены
local doors = workspace:WaitForChild("Doors")
-- окно покупки
local buyFrame = player:WaitForChild("PlayerGui"):WaitForChild("BuyAreaGui"):WaitForChild("BuyFrame")
-- вопрос
local question = buyFrame:WaitForChild("Question")
-- «Да»
local yesButton = buyFrame:WaitForChild("YesButton")
-- «Нет»
local noButton = buyFrame:WaitForChild("NoButton")
-- какую стену покупаем
local currentDoor = nil

-- убрать стену — только у этого игрока
local function RemoveDoor(name)
    -- стена с именем области
    local door = doors:FindFirstChild(name)
    -- убираем
    if door then door:Destroy() end
-- конец функции
end
-- уже купленные (после загрузки)
for _, area in ipairs(areas:GetChildren()) do RemoveDoor(area.Name) end
-- купил или данные загрузились позже
areas.ChildAdded:Connect(function(area) RemoveDoor(area.Name) end)

-- сервер просит показать окно
buyAreaEvent.OnClientEvent:Connect(function(door)
    -- запоминаем стену
    currentDoor = door
    -- вопрос с ценой
    question.Text = "Открыть " .. door.Name .. " за " .. door.Price.Value .. " Coins?"
    -- показываем окно
    buyFrame.Visible = true
-- конец обработчика
end)

-- «Да» — обработчик один на всю игру
yesButton.MouseButton1Click:Connect(function()
    -- окно не для покупки
    if not currentDoor then return end
    -- стена
    local door = currentDoor
    -- сбрасываем
    currentDoor = nil
    -- сервер продал
    if buyAreaFunction:InvokeServer(door) then
        -- закрываем — стену уберёт ChildAdded
        buyFrame.Visible = false
    -- не продал
    else
        -- сообщаем
        question.Text = "Не хватает монет"
        -- полторы секунды
        task.wait(1.5)
        -- закрываем
        buyFrame.Visible = false
    -- конец проверки
    end
-- конец обработчика
end)
-- «Нет»
noButton.MouseButton1Click:Connect(function()
    -- ничего не покупаем
    currentDoor = nil
    -- закрываем
    buyFrame.Visible = false
-- конец обработчика
end)

OpenCloseScript

Где: StarterGui → PetInventoryGui → OpenCloseButton · LocalScript
Зачем: Кнопка инвентаря.

-- кнопка «Питомцы»
local button = script.Parent
-- окно инвентаря
local frame = button.Parent:WaitForChild("PetFrame")

-- нажали
button.MouseButton1Click:Connect(function()
    -- открыто — закрываем, закрыто — открываем
    frame.Visible = not frame.Visible
-- конец обработчика
end)

PetInventoryLocal

Где: StarterGui → PetInventoryGui → PetFrame · LocalScript
Зачем: Иконки и надевание питомцев.

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- образцы питомцев
local pets = RS:WaitForChild("Pets")
-- надеть / снять
local equipFunction = RS:WaitForChild("Events"):WaitForChild("EquipPetFunction")
-- этот игрок
local player = game.Players.LocalPlayer
-- его купленные питомцы
local ownedPets = player:WaitForChild("Pets")
-- окно инвентаря
local frame = script.Parent
-- шаблон иконки
local sample = frame:WaitForChild("ScrollingFrame"):WaitForChild("Sample")
-- цвет: не надет
local OFF = Color3.fromRGB(80, 200, 255)
-- цвет: надет
local ON = Color3.fromRGB(90, 230, 120)

-- иконка одного питомца
local function AddIcon(petValue)
    -- его образец
    local template = pets:FindFirstChild(petValue.Name)
    -- нет такого
    if not template then return end
    -- копия шаблона
    local icon = sample:Clone()
    -- имя
    icon.Name = petValue.Name
    -- показываем
    icon.Visible = true
    -- не надет
    icon.BackgroundColor3 = OFF
    -- в список
    icon.Parent = sample.Parent
    -- питомец для картинки
    local petModel = template:Clone()
    -- мордой к камере
    petModel:PivotTo(CFrame.Angles(0, math.rad(180), 0))
    -- в окошко иконки
    petModel.Parent = icon
    -- камера иконки
    local camera = Instance.new("Camera")
    -- отходим под размер питомца
    camera.CFrame = CFrame.new(0, 0, petModel:GetExtentsSize().Magnitude * 1.2)
    -- в иконку
    camera.Parent = icon
    -- иконка смотрит через неё
    icon.CurrentCamera = camera
    -- надет ли этот питомец
    local equipped = false
    -- нажали на иконку
    icon.Button.MouseButton1Click:Connect(function()
        -- сервер согласился
        if equipFunction:InvokeServer(petValue.Name, not equipped) then
            -- меняем состояние
            equipped = not equipped
            -- и цвет
            icon.BackgroundColor3 = equipped and ON or OFF
        -- конец проверки
        end
    -- конец обработчика
    end)
    -- питомца не стало
    petValue.AncestryChanged:Connect(function()
        -- убираем иконку
        if not petValue.Parent then icon:Destroy() end
    -- конец обработчика
    end)
-- конец функции
end

-- питомцы, что уже есть
for _, petValue in ipairs(ownedPets:GetChildren()) do AddIcon(petValue) end
-- новый питомец
ownedPets.ChildAdded:Connect(function(petValue)
    -- иконка — после анимации яйца
    task.wait(4)
    -- добавляем
    AddIcon(petValue)
-- конец обработчика
end)

Проверка

Запусти Play (или Run) и убедись:

Собрать всю игру одной командой