Roblox · Песочница · урок 11

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

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

Структура

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

Workspace — StreamingEnabled выкл.
├─ Terrain (Terrain) — земля и холмы — копается лопатой
├─ Nature (Folder) — Tree, Rock: тег Breakable, атрибуты Tool, Drop, Respawn; MainPart → Health; HitsToBreak
├─ Placed (Folder) — поставленные блоки
├─ Workbench (Model) — Table → ProximityPrompt → OpenCraft
├─ AppleTree (Model) — Apples, PromptPart → ProximityPrompt → TakingFood
├─ House (Model) — Bed → BedPart → ProximityPrompt, BedScript
├─ Zombie (Model) — атрибуты Damage, WalkSpeed, SprintSpeed; EnemyScript
└─ Waypoints (Folder) — точки патруля
ReplicatedStorage
├─ ObjectFolder (Folder) — Wood, Stone, Glass, Fence — тег Breakable, MainPart, HitsToBreak
├─ Items (ModuleScript)
├─ PlacementEvent, CraftOpenEvent, SleepEvent, TimeEvent (RemoteEvent)
└─ CraftFunction (RemoteFunction)
ServerStorage
├─ Tools (Folder) — Pickaxe, Axe, Shovel
└─ Food (Folder) — Apple
ServerScriptService
├─ Inventory (ModuleScript)
└─ InventoryServer, PlacementServer, BreakServer, CraftServer, HungerServer, DayNight (Script)
StarterPack
└─ Pickaxe, Axe (Tool) — Handle, Mining, Wear, ToolScript
StarterPlayer → StarterCharacterScripts
└─ Run, Crouch, Fly (LocalScript)
StarterGui — все ResetOnSpawn выкл.
├─ InventoryGui (ScreenGui) — MainFrame → ItemFrame → Sample; InventoryLocal
├─ CraftGui (ScreenGui) — MainFrame → RecipesFrame, CraftFrame; CraftLocal
├─ HungerGui, StaminaGui (ScreenGui) — Background → Bar
├─ SleepGui (ScreenGui) — SleepFrame → Yes, No; SleepLocal
└─ FaqGui (ScreenGui) — OpenFaq, FaqFrame → ListFrame, InfoFrame; FaqScript
Players → игрок (во время игры) — Inventory (IntValue-счётчики), Hunger

Шаги

  1. Сверяй сверху вниз: сначала Explorer со «Структурой», потом каждый скрипт целиком.
  2. Ломаемое — модели с тегом Breakable, MainPart (PrimaryPart), HitsToBreak и атрибутами Tool и Drop. Блоки в ObjectFolder тоже ломаемые.
  3. Инвентарь — IntValue-счётчики в папке игрока; менять их может только сервер через модуль Inventory.
  4. Рецепты и названия — в одном модуле Items: новый рецепт — одна строчка, кнопка появится сама.
  5. Лопата меняет только Terrain — Baseplate в этой игре не нужен.
  6. Все ScreenGui — ResetOnSpawn выкл.; StarterPlayer → EnableMouseLockOption выкл.; Max Players = 1 (сон перематывает ночь всем).

Скрипты

Inventory

Где: ServerScriptService · ModuleScript
Зачем: Счётчики предметов.

-- таблица модуля
local Inventory = {}

-- папка инвентаря игрока
local function Folder(player)
    -- создаёт InventoryServer
    return player:FindFirstChild("Inventory")
-- конец функции
end

-- сколько у игрока предмета
function Inventory.Count(player, name)
    -- инвентарь
    local folder = Folder(player)
    -- ячейка
    local slot = folder and folder:FindFirstChild(name)
    -- число или 0
    return slot and slot.Value or 0
-- конец функции
end

-- добавить
function Inventory.Add(player, name, amount)
    -- инвентарь
    local folder = Folder(player)
    -- нет инвентаря
    if not folder then return end
    -- ячейка
    local slot = folder:FindFirstChild(name)
    -- такого ещё нет
    if not slot then
        -- новая ячейка-счётчик
        slot = Instance.new("IntValue")
        -- имя = предмет
        slot.Name = name
        -- в инвентарь
        slot.Parent = folder
    -- конец проверки
    end
    -- прибавляем
    slot.Value += amount
-- конец функции
end

-- забрать; false — не хватает
function Inventory.Take(player, name, amount)
    -- не хватает
    if Inventory.Count(player, name) < amount then return false end
    -- ячейка
    local slot = Folder(player)[name]
    -- вычитаем
    slot.Value -= amount
    -- закончился — ячейка пропадает
    if slot.Value <= 0 then slot:Destroy() end
    -- получилось
    return true
-- конец функции
end

-- отдаём модуль
return Inventory

InventoryServer

Где: ServerScriptService · Script
Зачем: Папка инвентаря.

-- игроки
local Players = game:GetService("Players")

-- игрок зашёл
local function Setup(player)
    -- инвентарь
    local folder = Instance.new("Folder")
    -- имя — его ищут скрипты
    folder.Name = "Inventory"
    -- в игрока — клиент видит свои предметы
    folder.Parent = player
-- конец функции
end
-- новые игроки
Players.PlayerAdded:Connect(Setup)
-- кто зашёл раньше скрипта
for _, player in ipairs(Players:GetPlayers()) do Setup(player) end

PlacementServer

Где: ServerScriptService · Script
Зачем: Размещение блоков.

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- теги
local CS = game:GetService("CollectionService")
-- клиент просит поставить
local placementEvent = RS:WaitForChild("PlacementEvent")
-- образцы
local objects = RS:WaitForChild("ObjectFolder")
-- куда ставим постройки
local placed = workspace:WaitForChild("Placed")
-- дальше ставить нельзя
local MAX_DISTANCE = 40
-- инвентарь
local Inventory = require(game:GetService("ServerScriptService"):WaitForChild("Inventory"))

-- клиент прислал блок и место
placementEvent.OnServerEvent:Connect(function(player, name, cf)
    -- пришёл мусор
    if typeof(name) ~= "string" or typeof(cf) ~= "CFrame" then return end
    -- образец
    local sample = objects:FindFirstChild(name)
    -- нет такого или слишком далеко
    if not sample or player:DistanceFromCharacter(cf.Position) > MAX_DISTANCE then return end
    -- блока нет в инвентаре — не ставим
    if not Inventory.Take(player, name, 1) then return end
    -- копия
    local object = sample:Clone()
    -- на место
    object:PivotTo(cf)
    -- в мир — теги копируются, блок можно сломать
    object.Parent = placed
-- конец функции
end)

BreakServer

Где: ServerScriptService · Script
Зачем: Разрушение и добыча.

-- теги: все ломаемые объекты помечены «Breakable»
local CS = game:GetService("CollectionService")
-- игроки
local Players = game:GetService("Players")
-- тег
local TAG = "Breakable"
-- через сколько секунд вырастает дерево или камень
local RESPAWN = 60
-- инвентарь
local Inventory = require(game:GetService("ServerScriptService"):WaitForChild("Inventory"))

-- настраиваем один ломаемый объект
local function Setup(model)
    -- образец в хранилище или уже настроен
    if not model:IsDescendantOf(workspace) or model:GetAttribute("Ready") then return end
    -- MainPart — невидимая коробка вокруг объекта
    local main = model:IsA("Model") and model.PrimaryPart
    -- сколько ударов выдержит
    local hits = model:FindFirstChild("HitsToBreak")
    -- объект собран неправильно
    if not main or not hits then return end
    -- копия — чтобы вырастить снова
    local sample = model:Clone()
    -- настроен
    model:SetAttribute("Ready", true)
    -- полная прочность
    local maxHits = hits.Value
    -- полоска прочности
    local gui = main:FindFirstChild("Health")
    -- пока не били — не видно
    if gui then gui.Enabled = false end
    -- инструмент → номер последнего засчитанного взмаха
    local lastSwing = {}
    -- чего-то коснулись
    main.Touched:Connect(function(hit)
        -- инструмент
        local tool = hit:FindFirstAncestorOfClass("Tool")
        -- идёт ли удар
        local mining = tool and tool:FindFirstChild("Mining")
        -- не удар или не тот инструмент
        if not mining or not mining.Value or tool.Name ~= model:GetAttribute("Tool") then return end
        -- номер взмаха
        local swing = tool:GetAttribute("Swing")
        -- этот взмах уже засчитан
        if lastSwing[tool] == swing then return end
        -- запоминаем
        lastSwing[tool] = swing
        -- удар
        hits.Value -= 1
        -- прочность инструмента
        local wear = tool:FindFirstChild("Wear")
        -- изнашивается
        if wear then wear.Value -= 1 end
        -- полоска есть
        if gui then
            -- показываем
            gui.Enabled = true
            -- сколько осталось
            gui.Green.Size = UDim2.fromScale(math.max(hits.Value, 0) / maxHits, 1)
        -- конец проверки
        end
        -- ещё держится
        if hits.Value > 0 then return end
        -- кто сломал
        local player = Players:GetPlayerFromCharacter(tool.Parent)
        -- добыча — в инвентарь
        if player then Inventory.Add(player, model:GetAttribute("Drop") or model.Name, 1) end
        -- где стоял
        local parent = model.Parent
        -- сломан
        model:Destroy()
        -- природный объект
        if model:GetAttribute("Respawn") then
            -- через минуту вырастет снова
            task.delay(RESPAWN, function() sample:Clone().Parent = parent end)
        -- конец проверки
        end
    -- конец функции
    end)
-- конец функции
end

-- все, что уже есть
for _, model in ipairs(CS:GetTagged(TAG)) do Setup(model) end
-- новые: поставленные блоки, выросшие деревья
CS:GetInstanceAddedSignal(TAG):Connect(Setup)

CraftServer

Где: ServerScriptService · Script
Зачем: Крафт.

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- образцы инструментов
local SS = game:GetService("ServerStorage")
-- рецепты
local Items = require(RS:WaitForChild("Items"))
-- инвентарь
local Inventory = require(game:GetService("ServerScriptService"):WaitForChild("Inventory"))
-- запрос крафта
local craftFunction = RS:WaitForChild("CraftFunction")
-- инструменты, которые можно скрафтить
local tools = SS:WaitForChild("Tools")

-- игрок нажал «Скрафтить»
craftFunction.OnServerInvoke = function(player, item)
    -- рецепт
    local recipe = typeof(item) == "string" and Items.Recipes[item]
    -- нет
    if not recipe then return false, "Нет такого рецепта" end
    -- проверяем каждый ингредиент
    for name, amount in pairs(recipe) do
        -- не хватает
        if Inventory.Count(player, name) < amount then
            -- ответ
            return false, "Не хватает: " .. (Items.Names[name] or name)
        -- конец проверки
        end
    -- конец перебора
    end
    -- забираем ингредиенты
    for name, amount in pairs(recipe) do Inventory.Take(player, name, amount) end
    -- это инструмент?
    local tool = tools:FindFirstChild(item)
    -- да
    if tool then
        -- в рюкзак
        tool:Clone().Parent = player.Backpack
    -- блок
    else
        -- в инвентарь
        Inventory.Add(player, item, 1)
    -- конец проверки
    end
    -- ответ
    return true, "Создано: " .. (Items.Names[item] or item)
-- конец функции
end

HungerServer

Где: ServerScriptService · Script
Зачем: Сытость.

-- игроки
local Players = game:GetService("Players")
-- раз в сколько секунд голод растёт
local FREQUENCY = 4
-- на сколько падает сытость
local DECREASE = 2
-- урон от голода, когда сытость 0
local STARVE_DAMAGE = 5

-- игрок зашёл
local function Setup(player)
    -- сытость
    local hunger = Instance.new("IntValue")
    -- имя — его читают интерфейс и еда
    hunger.Name = "Hunger"
    -- сыт
    hunger.Value = 100
    -- в игрока
    hunger.Parent = player
    -- после возрождения — снова сыт
    player.CharacterAdded:Connect(function() hunger.Value = 100 end)
    -- пока игрок в игре
    while player.Parent do
        -- ждём
        task.wait(FREQUENCY)
        -- его Humanoid
        local humanoid = player.Character and player.Character:FindFirstChildOfClass("Humanoid")
        -- жив
        if humanoid and humanoid.Health > 0 then
            -- ещё не голоден
            if hunger.Value > 0 then
                -- сытость падает
                hunger.Value = math.max(hunger.Value - DECREASE, 0)
            -- голоден
            else
                -- здоровье понемногу уходит
                humanoid:TakeDamage(STARVE_DAMAGE)
            -- конец проверки
            end
        -- конец проверки
        end
    -- конец цикла
    end
-- конец функции
end
-- новые игроки
Players.PlayerAdded:Connect(Setup)
-- кто зашёл раньше скрипта
for _, player in ipairs(Players:GetPlayers()) do task.spawn(Setup, player) end

DayNight

Где: ServerScriptService · Script
Зачем: Сутки.

-- освещение
local Lighting = game:GetService("Lighting")
-- часов за секунду: сутки — 8 минут
local STEP = 0.05

-- бесконечно
while true do
    -- секунда
    task.wait(1)
    -- время идёт, после 24 — снова 0
    Lighting.ClockTime = (Lighting.ClockTime + STEP) % 24
-- конец цикла
end

Items

Где: ReplicatedStorage · ModuleScript
Зачем: Названия и рецепты.

-- таблица модуля
local Items = {}

-- как предметы называются для игрока
Items.Names = {
    -- блоки
    Wood = "Дерево", Stone = "Камень", Glass = "Стекло", Fence = "Забор",
    -- инструменты
    Pickaxe = "Кирка", Axe = "Топор", Shovel = "Лопата",
-- конец таблицы
}

-- рецепты: предмет → что нужно
Items.Recipes = {
    -- стекло из камня
    Glass = {Stone = 2},
    -- забор из дерева
    Fence = {Wood = 2},
    -- лопата
    Shovel = {Wood = 2},
    -- топор
    Axe = {Wood = 2, Stone = 1},
    -- кирка
    Pickaxe = {Wood = 1, Stone = 2},
-- конец таблицы
}

-- отдаём модуль
return Items

InventoryLocal

Где: StarterGui → InventoryGui → MainFrame · LocalScript
Зачем: Инвентарь и размещение.

-- игроки
local Players = game:GetService("Players")
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- клавиатура
local UIS = game:GetService("UserInputService")
-- каждый кадр
local RunService = game:GetService("RunService")
-- этот игрок
local player = Players.LocalPlayer
-- куда указывает курсор
local mouse = player:GetMouse()
-- просим сервер поставить блок
local placementEvent = RS:WaitForChild("PlacementEvent")
-- образцы блоков
local objects = RS:WaitForChild("ObjectFolder")
-- названия предметов
local Items = require(RS:WaitForChild("Items"))
-- предметы игрока
local inventory = player:WaitForChild("Inventory")
-- MainFrame — окно инвентаря
local frame = script.Parent
-- список ячеек
local itemFrame = frame:WaitForChild("ItemFrame")
-- шаблон ячейки
local sample = itemFrame:WaitForChild("Sample")
-- I — открыть или закрыть инвентарь
local TOGGLE_KEY = Enum.KeyCode.I
-- шаг сетки — размер блока
local GRID = 4
-- полупрозрачный блок под курсором
local preview = nil
-- какой блок ставим
local currentName = nil
-- поворот в градусах
local rotation = 0

-- закончить размещение
local function Stop()
    -- убираем призрак
    if preview then preview:Destroy() end
    -- ничего не ставим
    preview = nil
    -- блока нет
    currentName = nil
-- конец функции
end

-- начать ставить блок
local function Start(name)
    -- старый призрак убираем
    Stop()
    -- образец
    local object = objects:FindFirstChild(name)
    -- такого блока нет
    if not object then return end
    -- копия только у этого игрока
    preview = object:Clone()
    -- все детали
    for _, part in ipairs(preview:GetDescendants()) do
        -- деталь
        if part:IsA("BasePart") then
            -- полупрозрачная (MainPart так и остаётся невидимой)
            part.Transparency = math.max(part.Transparency, 0.6)
            -- сквозь
            part.CanCollide = false
            -- курсор её не замечает
            part.CanQuery = false
        -- конец проверки
        end
    -- конец перебора
    end
    -- показываем
    preview.Parent = workspace
    -- mouse.Hit смотрит сквозь призрак
    mouse.TargetFilter = preview
    -- запоминаем
    currentName = name
-- конец функции
end

-- каждый кадр
RunService.RenderStepped:Connect(function()
    -- ничего не ставим
    if not preview or not preview.PrimaryPart then return end
    -- точка под курсором
    local hit = mouse.Hit.Position
    -- по сетке
    local x = math.floor(hit.X / GRID + 0.5) * GRID
    -- по сетке
    local z = math.floor(hit.Z / GRID + 0.5) * GRID
    -- стоит на поверхности, а не утоплен
    local y = hit.Y + preview.PrimaryPart.Size.Y / 2
    -- двигаем призрак
    preview:PivotTo(CFrame.new(x, y, z) * CFrame.Angles(0, math.rad(rotation), 0))
-- конец функции
end)

-- нажали клавишу — подписка одна
UIS.InputBegan:Connect(function(input, processed)
    -- печатаем в чате
    if processed then return end
    -- R
    if input.KeyCode == Enum.KeyCode.R then
        -- поворот на 45°
        rotation = (rotation + 45) % 360
    -- Q
    elseif input.KeyCode == Enum.KeyCode.Q then
        -- отмена
        Stop()
    -- I
    elseif input.KeyCode == TOGGLE_KEY then
        -- инвентарь открыть / закрыть
        frame.Visible = not frame.Visible
    -- клик по миру (клик по кнопке отсеян processed)
    elseif input.UserInputType == Enum.UserInputType.MouseButton1 and preview then
        -- сервер решает, можно ли
        placementEvent:FireServer(currentName, preview:GetPivot())
        -- призрак убираем
        Stop()
    -- конец проверки
    end
-- конец функции
end)

-- перерисовать ячейки инвентаря
local function Refresh()
    -- старые ячейки
    for _, card in ipairs(itemFrame:GetChildren()) do
        -- убираем (шаблон оставляем)
        if card:IsA("ImageButton") and card ~= sample then card:Destroy() end
    -- конец перебора
    end
    -- каждый предмет
    for _, slot in ipairs(inventory:GetChildren()) do
        -- копия шаблона
        local card = sample:Clone()
        -- имя
        card.Name = slot.Name
        -- название по-русски
        card.ItemName.Text = Items.Names[slot.Name] or slot.Name
        -- сколько
        card.Count.Text = "× " .. slot.Value
        -- блок, который можно поставить
        local object = objects:FindFirstChild(slot.Name)
        -- картинка (Texture внутри блока)
        local icon = object and object:FindFirstChild("Texture")
        -- на кнопку
        card.Image = icon and icon.Texture or ""
        -- шаблон был скрыт
        card.Visible = true
        -- в список
        card.Parent = itemFrame
        -- клик — ставить этот блок
        card.MouseButton1Click:Connect(function() Start(slot.Name) end)
    -- конец перебора
    end
-- конец функции
end
-- новый предмет
inventory.ChildAdded:Connect(function(slot)
    -- число изменилось — перерисовать
    slot.Changed:Connect(Refresh)
    -- сразу
    Refresh()
-- конец функции
end)
-- предмет закончился
inventory.ChildRemoved:Connect(Refresh)
-- предметы, что уже есть
for _, slot in ipairs(inventory:GetChildren()) do slot.Changed:Connect(Refresh) end
-- сразу при входе
Refresh()

CraftLocal

Где: StarterGui → CraftGui → MainFrame · LocalScript
Зачем: Меню крафта.

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- рецепты и названия
local Items = require(RS:WaitForChild("Items"))
-- верстак позвал
local openEvent = RS:WaitForChild("CraftOpenEvent")
-- попросить сервер скрафтить
local craftFunction = RS:WaitForChild("CraftFunction")
-- окно крафта
local mainFrame = script.Parent
-- список рецептов
local recipesFrame = mainFrame:WaitForChild("RecipesFrame")
-- шаблон кнопки рецепта
local sample = recipesFrame:WaitForChild("Sample")
-- один рецепт
local craftFrame = mainFrame:WaitForChild("CraftFrame")
-- что сейчас крафтим
local selected = nil

-- открыть рецепт
local function Show(item)
    -- запоминаем
    selected = item
    -- название
    craftFrame.Object.Text = Items.Names[item] or item
    -- строчки ингредиентов
    local parts = {}
    -- каждый ингредиент
    for name, amount in pairs(Items.Recipes[item]) do
        -- «Дерево × 2»
        table.insert(parts, (Items.Names[name] or name) .. " × " .. amount)
    -- конец перебора
    end
    -- список
    craftFrame.Ingredients.Text = "Нужно: " .. table.concat(parts, ", ")
    -- без старых сообщений
    craftFrame.Message.Text = ""
    -- прячем список
    recipesFrame.Visible = false
    -- показываем рецепт
    craftFrame.Visible = true
-- конец функции
end

-- кнопка на каждый рецепт — сама
for item in pairs(Items.Recipes) do
    -- копия шаблона
    local button = sample:Clone()
    -- имя
    button.Name = item
    -- название по-русски
    button.Text = Items.Names[item] or item
    -- шаблон был скрыт
    button.Visible = true
    -- в список
    button.Parent = recipesFrame
    -- клик — рецепт
    button.MouseButton1Click:Connect(function() Show(item) end)
-- конец перебора
end

-- «Скрафтить» — подписка одна
craftFrame.CraftButton.MouseButton1Click:Connect(function()
    -- ничего не выбрано
    if not selected then return end
    -- сервер проверит и скрафтит
    local _, text = craftFunction:InvokeServer(selected)
    -- «Создано» или «Не хватает»
    craftFrame.Message.Text = text
-- конец функции
end)
-- назад к рецептам
craftFrame.BackButton.MouseButton1Click:Connect(function()
    -- прячем рецепт
    craftFrame.Visible = false
    -- показываем список
    recipesFrame.Visible = true
-- конец функции
end)
-- закрыть
mainFrame.CloseButton.MouseButton1Click:Connect(function() mainFrame.Visible = false end)
-- верстак позвал
openEvent.OnClientEvent:Connect(function()
    -- сначала список
    craftFrame.Visible = false
    -- рецептов
    recipesFrame.Visible = true
    -- открываем
    mainFrame.Visible = true
-- конец функции
end)

HungerLocal

Где: StarterGui → HungerGui · LocalScript
Зачем: Полоса сытости.

-- игроки
local Players = game:GetService("Players")
-- сытость
local hunger = Players.LocalPlayer:WaitForChild("Hunger")
-- полоса (растёт снизу вверх)
local bar = script.Parent:WaitForChild("Background"):WaitForChild("Bar")

-- обновить полосу
local function Update()
    -- высота = сытость
    bar.Size = UDim2.fromScale(1, math.clamp(hunger.Value / 100, 0, 1))
-- конец функции
end
-- изменилась
hunger.Changed:Connect(Update)
-- сразу
Update()

SleepLocal

Где: StarterGui → SleepGui → SleepFrame · LocalScript
Зачем: Сон.

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- плавное затемнение
local TS = game:GetService("TweenService")
-- эффекты экрана
local Lighting = game:GetService("Lighting")
-- окно «Лечь спать?»
local frame = script.Parent
-- кровать спросила
local sleepEvent = RS:WaitForChild("SleepEvent")
-- сказать серверу «Да»
local timeEvent = RS:WaitForChild("TimeEvent")
-- сначала скрыто
frame.Visible = false

-- лёг ночью
sleepEvent.OnClientEvent:Connect(function()
    -- спрашиваем
    frame.Visible = true
-- конец функции
end)
-- «Да» — подписка одна
frame:WaitForChild("Yes").MouseButton1Click:Connect(function()
    -- прячем окно
    frame.Visible = false
    -- свой эффект — даже если в Lighting уже есть другой
    local fade = Instance.new("ColorCorrectionEffect")
    -- имя
    fade.Name = "SleepFade"
    -- только у этого игрока
    fade.Parent = Lighting
    -- экран темнеет
    local dark = TS:Create(fade, TweenInfo.new(2), {Brightness = -1})
    -- засыпаем
    dark:Play()
    -- ждём
    dark.Completed:Wait()
    -- сервер ставит утро
    timeEvent:FireServer()
    -- спим
    task.wait(1)
    -- светлеет
    local light = TS:Create(fade, TweenInfo.new(2), {Brightness = 0})
    -- просыпаемся
    light:Play()
    -- ждём
    light.Completed:Wait()
    -- эффект больше не нужен
    fade:Destroy()
-- конец функции
end)
-- «Нет»
frame:WaitForChild("No").MouseButton1Click:Connect(function() frame.Visible = false end)

FaqScript

Где: StarterGui → FaqGui → FaqFrame · LocalScript
Зачем: Подсказки.

-- окно подсказок
local faqFrame = script.Parent
-- кнопка «?»
local openButton = faqFrame.Parent:WaitForChild("OpenFaq")
-- список тем (в методичке по ошибке FaqList)
local listFrame = faqFrame:WaitForChild("ListFrame")
-- текст темы
local infoFrame = faqFrame:WaitForChild("InfoFrame")
-- тема → подсказка; имя кнопки = ключ
local INFO = {
    -- клавиши
    Keys = "Shift — бег, C — присесть, F — полёт, I — инвентарь, R — повернуть блок, Q — отменить размещение.",
    -- кирка
    Pickaxe = "Кирка ломает камни и каменные блоки. Изнашивается: прочность видно по числу ударов.",
    -- топор
    Axe = "Топор рубит деревья, деревянные блоки и заборы. Срубленное дерево вырастает через минуту.",
    -- крафт
    Craft = "Подойди к верстаку и нажми E. Выбери рецепт — если ингредиентов хватает, предмет появится в инвентаре или рюкзаке.",
    -- голод
    Hunger = "Сытость падает со временем. На нуле уходит здоровье. Сорви яблоко с яблони и съешь его.",
    -- сон
    Sleep = "Ночью ляг на кровать: сон перемотает время до утра и восстановит здоровье. Прыжок — встать.",
    -- противник
    Enemy = "Зомби патрулирует лес и обходит стены. Заметив тебя, бежит быстрее. Держись подальше или построй укрытие.",
-- конец таблицы
}

-- «?»
openButton.MouseButton1Click:Connect(function()
    -- открыть / закрыть
    faqFrame.Visible = not faqFrame.Visible
-- конец функции
end)
-- закрыть
faqFrame:WaitForChild("CloseButton").MouseButton1Click:Connect(function() faqFrame.Visible = false end)
-- назад к темам
infoFrame:WaitForChild("BackButton").MouseButton1Click:Connect(function()
    -- прячем текст
    infoFrame.Visible = false
    -- показываем список
    listFrame.Visible = true
-- конец функции
end)
-- кнопки тем
for _, button in ipairs(listFrame:GetChildren()) do
    -- только кнопки
    if button:IsA("TextButton") then
        -- клик по теме
        button.MouseButton1Click:Connect(function()
            -- заголовок
            infoFrame.Object.Text = button.Text
            -- текст
            infoFrame.Info.Text = INFO[button.Name] or "Скоро здесь будет подсказка."
            -- прячем список
            listFrame.Visible = false
            -- показываем текст
            infoFrame.Visible = true
        -- конец функции
        end)
    -- конец проверки
    end
-- конец перебора
end

Run

Где: StarterPlayer → StarterCharacterScripts · LocalScript
Зачем: Бег с выносливостью.

-- клавиатура
local UIS = game:GetService("UserInputService")
-- каждый кадр
local RunService = game:GetService("RunService")
-- игроки
local Players = game:GetService("Players")
-- персонаж
local character = script.Parent
-- Humanoid
local humanoid = character:WaitForChild("Humanoid")
-- полоса выносливости
local gui = Players.LocalPlayer:WaitForChild("PlayerGui"):WaitForChild("StaminaGui")
-- полоса
local bar = gui:WaitForChild("Background"):WaitForChild("Bar")
-- запас
local MAX = 150
-- тратится в секунду
local DRAIN = 30
-- восстанавливается в секунду
local REGEN = 20
-- бег
local RUN_SPEED = 25
-- шаг
local WALK_SPEED = 16
-- сейчас
local stamina = MAX
-- Shift зажат
local shift = false
-- бежим
local running = false

-- нажали
UIS.InputBegan:Connect(function(input, processed)
    -- Shift
    if not processed and input.KeyCode == Enum.KeyCode.LeftShift then shift = true end
-- конец функции
end)
-- отпустили
UIS.InputEnded:Connect(function(input)
    -- Shift
    if input.KeyCode == Enum.KeyCode.LeftShift then shift = false end
-- конец функции
end)

-- каждый кадр
RunService.Heartbeat:Connect(function(dt)
    -- погиб или сидит — не бежим
    if humanoid.Health <= 0 or character:GetAttribute("Crouching") then running = false return end
    -- можно бежать
    local want = shift and humanoid.MoveDirection.Magnitude > 0 and stamina > 0
    -- бежим
    if want then
        -- тратим
        stamina = math.max(stamina - DRAIN * dt, 0)
    -- Shift отпущен
    elseif not shift then
        -- отдыхаем
        stamina = math.min(stamina + REGEN * dt, MAX)
    -- конец проверки
    end
    -- состояние сменилось
    if want ~= running then
        -- запоминаем
        running = want
        -- скорость меняем только в этот момент
        humanoid.WalkSpeed = want and RUN_SPEED or WALK_SPEED
    -- конец проверки
    end
    -- полная — полоску прячем
    gui.Enabled = stamina < MAX
    -- длина полосы
    bar.Size = UDim2.fromScale(stamina / MAX, 1)
-- конец функции
end)

Crouch

Где: StarterPlayer → StarterCharacterScripts · LocalScript
Зачем: Присед.

-- клавиатура
local UIS = game:GetService("UserInputService")
-- персонаж
local character = script.Parent
-- Humanoid
local humanoid = character:WaitForChild("Humanoid")
-- анимации
local animator = humanoid:WaitForChild("Animator")
-- скорость сидя
local CROUCH_SPEED = 6
-- обычная скорость
local WALK_SPEED = 16

-- Animation внутри скрипта
local crouchAnim = script:WaitForChild("CrouchAnim")
-- дорожка анимации
local track = nil
-- своя анимация есть
if crouchAnim.AnimationId ~= "" then
    -- загружаем
    track = animator:LoadAnimation(crouchAnim)
    -- поверх ходьбы и стояния
    track.Priority = Enum.AnimationPriority.Action
    -- по кругу
    track.Looped = true
-- конец проверки
end
-- сидим?
local crouching = false

-- нажали клавишу
UIS.InputBegan:Connect(function(input, processed)
    -- нужна C и не в чате
    if processed or input.KeyCode ~= Enum.KeyCode.C then return end
    -- погибшие не приседают
    if humanoid.Health <= 0 then return end
    -- C переключает: сесть / встать
    crouching = not crouching
    -- сообщаем скрипту бега
    character:SetAttribute("Crouching", crouching)
    -- сели
    if crouching then
        -- медленно
        humanoid.WalkSpeed = CROUCH_SPEED
        -- камера ниже — видно и без анимации
        humanoid.CameraOffset = Vector3.new(0, -1.5, 0)
        -- анимация приседа
        if track then track:Play(0.2) end
    -- встали
    else
        -- обычная скорость
        humanoid.WalkSpeed = WALK_SPEED
        -- камера на место
        humanoid.CameraOffset = Vector3.zero
        -- выключаем анимацию
        if track then track:Stop(0.2) end
    -- конец проверки
    end
-- конец функции
end)

Fly

Где: StarterPlayer → StarterCharacterScripts · LocalScript
Зачем: Полёт.

-- клавиатура
local UIS = game:GetService("UserInputService")
-- каждый кадр
local RunService = game:GetService("RunService")
-- персонаж
local character = script.Parent
-- Humanoid
local humanoid = character:WaitForChild("Humanoid")
-- центр
local root = character:WaitForChild("HumanoidRootPart")
-- камера — летим туда, куда смотрим
local camera = workspace.CurrentCamera
-- F — взлететь / приземлиться
local KEY = Enum.KeyCode.F
-- скорость полёта
local SPEED = 40
-- летим?
local flying = false

-- точка для сил
local attachment = root:FindFirstChild("RootAttachment") or Instance.new("Attachment", root)
-- держит скорость полёта
local move = Instance.new("LinearVelocity")
-- за центр
move.Attachment0 = attachment
-- сильнее гравитации
move.MaxForce = 1000000
-- пока висим
move.VectorVelocity = Vector3.zero
-- выключено до полёта
move.Enabled = false
-- в персонажа
move.Parent = root
-- поворачивает персонажа
local turn = Instance.new("AlignOrientation")
-- к заданному направлению
turn.Mode = Enum.OrientationAlignmentMode.OneAttachment
-- за центр
turn.Attachment0 = attachment
-- жёстко
turn.RigidityEnabled = true
-- выключено до полёта
turn.Enabled = false
-- в персонажа
turn.Parent = root

-- включить или выключить полёт
local function SetFlying(value)
    -- запоминаем
    flying = value
    -- сила
    move.Enabled = value
    -- поворот
    turn.Enabled = value
    -- в полёте ноги не шагают
    humanoid.PlatformStand = value
-- конец функции
end

-- нажали
UIS.InputBegan:Connect(function(input, processed)
    -- F переключает
    if not processed and input.KeyCode == KEY and humanoid.Health > 0 then SetFlying(not flying) end
-- конец функции
end)
-- погиб — падает
humanoid.Died:Connect(function() SetFlying(false) end)

-- каждый кадр
RunService.RenderStepped:Connect(function()
    -- не летим
    if not flying then return end
    -- камера
    local cf = camera.CFrame
    -- направление
    local dir = Vector3.zero
    -- вперёд — куда смотрит камера (и вверх-вниз тоже)
    if UIS:IsKeyDown(Enum.KeyCode.W) then dir += cf.LookVector end
    -- назад
    if UIS:IsKeyDown(Enum.KeyCode.S) then dir -= cf.LookVector end
    -- вправо
    if UIS:IsKeyDown(Enum.KeyCode.D) then dir += cf.RightVector end
    -- влево
    if UIS:IsKeyDown(Enum.KeyCode.A) then dir -= cf.RightVector end
    -- пробел — вверх
    if UIS:IsKeyDown(Enum.KeyCode.Space) then dir += Vector3.new(0, 1, 0) end
    -- одинаковая скорость в любую сторону
    if dir.Magnitude > 0 then dir = dir.Unit end
    -- летим
    move.VectorVelocity = dir * SPEED
    -- куда смотрит камера по горизонтали
    local flat = Vector3.new(cf.LookVector.X, 0, cf.LookVector.Z)
    -- персонаж смотрит туда же
    if flat.Magnitude > 0.01 then turn.CFrame = CFrame.lookAt(Vector3.zero, flat) end
-- конец функции
end)

ToolScript

Где: Pickaxe, Axe (StarterPack и ServerStorage.Tools) · Script
Зачем: Взмах и износ.

-- кирка или топор
local tool = script.Parent
-- взмах
local anim = script:WaitForChild("Animation")
-- звук взмаха
local swingSound = tool:WaitForChild("Handle"):WaitForChild("Swing")
-- BoolValue: true — идёт удар
local mining = tool:WaitForChild("Mining")
-- IntValue: прочность
local wear = tool:WaitForChild("Wear")
-- взмах идёт
local busy = false

-- клик
tool.Activated:Connect(function()
    -- кто держит
    local humanoid = tool.Parent:FindFirstChildOfClass("Humanoid")
    -- взмах идёт или погиб
    if busy or not humanoid or humanoid.Health <= 0 then return end
    -- начали
    busy = true
    -- номер взмаха — один удар за взмах
    tool:SetAttribute("Swing", (tool:GetAttribute("Swing") or 0) + 1)
    -- звук
    swingSound:Play()
    -- взмах
    if anim.AnimationId ~= "" then humanoid:FindFirstChildOfClass("Animator"):LoadAnimation(anim):Play() end
    -- удар
    mining.Value = true
    -- длина взмаха
    task.wait(0.4)
    -- конец удара
    mining.Value = false
    -- можно снова
    busy = false
-- конец функции
end)
-- прочность изменилась
wear.Changed:Connect(function(value)
    -- сломался
    if value <= 0 then tool:Destroy() end
-- конец функции
end)

ShovelLocal

Где: ServerStorage → Tools → Shovel · LocalScript
Зачем: Лопата: клики.

-- игроки
local Players = game:GetService("Players")
-- лопата
local tool = script.Parent
-- просим сервер изменить ландшафт
local terraform = tool:WaitForChild("Terraform")
-- куда указывает курсор
local mouse = Players.LocalPlayer:GetMouse()
-- лопата в руках?
local equipped = false

-- взяли
tool.Equipped:Connect(function() equipped = true end)
-- убрали
tool.Unequipped:Connect(function() equipped = false end)
-- уже в руках
if tool.Parent and tool.Parent:FindFirstChildOfClass("Humanoid") then equipped = true end
-- ЛКМ
tool.Activated:Connect(function()
    -- копать
    terraform:FireServer("Dig", mouse.Hit.Position)
-- конец функции
end)
-- ПКМ
mouse.Button2Down:Connect(function()
    -- насыпать землю
    if equipped then terraform:FireServer("Raise", mouse.Hit.Position) end
-- конец функции
end)

ShovelServer

Где: ServerStorage → Tools → Shovel · Script
Зачем: Лопата: ландшафт.

-- игроки
local Players = game:GetService("Players")
-- лопата
local tool = script.Parent
-- команда от клиента
local terraform = tool:WaitForChild("Terraform")
-- размер ямы или холмика
local RADIUS = 3
-- дальше не дотянуться
local MAX_DISTANCE = 25
-- не чаще
local COOLDOWN = 0.3
-- время прошлого копка
local last = 0

-- клиент копает
terraform.OnServerEvent:Connect(function(player, action, position)
    -- лопата не у него или мусор
    if Players:GetPlayerFromCharacter(tool.Parent) ~= player or typeof(position) ~= "Vector3" then return end
    -- слишком часто или далеко
    if os.clock() - last < COOLDOWN or player:DistanceFromCharacter(position) > MAX_DISTANCE then return end
    -- запоминаем
    last = os.clock()
    -- копать
    if action == "Dig" then
        -- шар воздуха — яма
        workspace.Terrain:FillBall(position, RADIUS, Enum.Material.Air)
    -- насыпать
    elseif action == "Raise" then
        -- шар земли — холмик
        workspace.Terrain:FillBall(position, RADIUS, Enum.Material.Ground)
    -- конец проверки
    end
-- конец функции
end)

OpenCraft

Где: Workspace → Workbench → Table → ProximityPrompt · Script
Зачем: Верстак.

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- ProximityPrompt верстака
local prompt = script.Parent
-- открыть меню крафта
local openEvent = RS:WaitForChild("CraftOpenEvent")

-- игрок нажал E у верстака
prompt.Triggered:Connect(function(player)
    -- меню открывается только у него
    openEvent:FireClient(player)
-- конец функции
end)

FoodScript

Где: ServerStorage → Food → Apple · Script
Зачем: Яблоко.

-- яблоко
local tool = script.Parent
-- сколько сытости даёт
local foodValue = tool:WaitForChild("FoodValue")
-- хруст
local eatSound = tool:WaitForChild("Handle"):WaitForChild("Eat")
-- анимация еды
local anim = script:WaitForChild("Animation")
-- уже съедено
local eaten = false

-- клик — съесть
tool.Activated:Connect(function()
    -- кто ест
    local character = tool.Parent
    -- игрок
    local player = game.Players:GetPlayerFromCharacter(character)
    -- Humanoid
    local humanoid = character:FindFirstChildOfClass("Humanoid")
    -- уже, не игрок или погиб
    if eaten or not player or not humanoid or humanoid.Health <= 0 then return end
    -- съедено
    eaten = true
    -- анимация
    if anim.AnimationId ~= "" then humanoid:FindFirstChildOfClass("Animator"):LoadAnimation(anim):Play() end
    -- хруст
    eatSound:Play()
    -- жуём
    task.wait(1)
    -- сытость
    local hunger = player:FindFirstChild("Hunger")
    -- сытость сервер считает сам, не выше 100
    if hunger then hunger.Value = math.min(hunger.Value + foodValue.Value, 100) end
    -- яблока больше нет
    tool:Destroy()
-- конец функции
end)

TakingFood

Где: Workspace → AppleTree → PromptPart → ProximityPrompt · Script
Зачем: Яблоня.

-- образец яблока
local SS = game:GetService("ServerStorage")
-- ProximityPrompt «Сорвать»
local prompt = script.Parent
-- яблоня: Prompt → PromptPart → AppleTree
local tree = prompt.Parent.Parent
-- яблоко-инструмент
local apple = SS:WaitForChild("Food"):WaitForChild("Apple")
-- через сколько секунд вырастут снова
local REGROW = 10

-- сорвали
prompt.Triggered:Connect(function(player)
    -- яблоки на дереве (декор)
    local apples = tree:FindFirstChild("Apples")
    -- уже сорваны
    if not apples then return end
    -- яблоко в рюкзак
    apple:Clone().Parent = player.Backpack
    -- яблоки пропали с дерева
    apples.Parent = nil
    -- сорвать нечего
    prompt.Enabled = false
    -- растут
    task.wait(REGROW)
    -- снова на дереве
    apples.Parent = tree
    -- можно рвать
    prompt.Enabled = true
-- конец функции
end)

BedScript

Где: Workspace → House → Bed → BedPart · Script
Зачем: Кровать.

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- время суток
local Lighting = game:GetService("Lighting")
-- BedPart — где лежит персонаж
local part = script.Parent
-- «Лечь»
local prompt = part:WaitForChild("ProximityPrompt")
-- спросить игрока «Спать?»
local sleepEvent = RS:WaitForChild("SleepEvent")
-- игрок согласился
local timeEvent = RS:WaitForChild("TimeEvent")
-- кто лежит
local current = nil
-- сварка с кроватью
local weld = nil
-- подписка «прыжок — встать»
local jumpConnection = nil

-- сейчас ночь?
local function IsNight()
    -- с 20 до 6
    return Lighting.ClockTime >= 20 or Lighting.ClockTime < 6
-- конец функции
end

-- встать с кровати
local function StandUp()
    -- никто не лежит
    if not current then return end
    -- отвариваем
    if weld then weld:Destroy() end
    -- отписываемся
    if jumpConnection then jumpConnection:Disconnect() end
    -- ничего нет
    weld, jumpConnection = nil, nil
    -- Humanoid
    local humanoid = current:FindFirstChildOfClass("Humanoid")
    -- снова управляем
    if humanoid then humanoid.PlatformStand = false end
    -- ставим рядом с кроватью
    if current.Parent then current:PivotTo(part.CFrame + Vector3.new(0, 4, 0)) end
    -- кровать свободна
    current = nil
    -- можно лечь снова
    prompt.Enabled = true
-- конец функции
end

-- игрок нажал «Лечь»
prompt.Triggered:Connect(function(player)
    -- персонаж
    local character = player.Character
    -- Humanoid
    local humanoid = character and character:FindFirstChildOfClass("Humanoid")
    -- центр
    local root = character and character:FindFirstChild("HumanoidRootPart")
    -- прошлый игрок вышел из игры — кровать свободна
    if current and not current.Parent then StandUp() end
    -- занято или погиб
    if current or not humanoid or not root or humanoid.Health <= 0 then return end
    -- лежит он
    current = character
    -- второй не ляжет
    prompt.Enabled = false
    -- персонаж «лежит», а не стоит
    humanoid.PlatformStand = true
    -- лёжа на кровати
    root.CFrame = part.CFrame * CFrame.Angles(math.rad(90), 0, 0)
    -- приварим к кровати
    weld = Instance.new("WeldConstraint")
    -- кровать
    weld.Part0 = part
    -- персонаж
    weld.Part1 = root
    -- в кровать
    weld.Parent = part
    -- нажал прыжок
    jumpConnection = humanoid:GetPropertyChangedSignal("Jump"):Connect(function()
        -- встаёт
        if humanoid.Jump then StandUp() end
    -- конец функции
    end)
    -- погиб в кровати — кровать освобождается
    humanoid.Died:Once(function() if current == character then StandUp() end end)
    -- ночью спросим «Спать?»
    if IsNight() then sleepEvent:FireClient(player) end
-- конец функции
end)

-- игрок выбрал «Да»
timeEvent.OnServerEvent:Connect(function(player)
    -- лежит не он или уже не ночь
    if current ~= player.Character or not IsNight() then return end
    -- утро
    Lighting.ClockTime = 7
    -- Humanoid
    local humanoid = current:FindFirstChildOfClass("Humanoid")
    -- выспался — здоровье полное
    if humanoid then humanoid.Health = humanoid.MaxHealth end
-- конец функции
end)

EnemyScript

Где: Workspace → Zombie · Script
Зачем: Противник.

-- поиск пути в обход стен
local PathfindingService = game:GetService("PathfindingService")
-- игроки
local Players = game:GetService("Players")
-- противник
local enemy = script.Parent
-- Humanoid
local humanoid = enemy:WaitForChild("Humanoid")
-- центр
local root = enemy:WaitForChild("HumanoidRootPart")
-- точки патруля
local waypoints = workspace:WaitForChild("Waypoints")
-- замечает игрока ближе 40 studs
local SIGHT = 40
-- бьёт ближе 4 studs
local ATTACK_RANGE = 4
-- секунд между ударами
local COOLDOWN = 1
-- время прошлого удара
local lastAttack = 0
-- Animator
local animator = humanoid:FindFirstChildOfClass("Animator") or Instance.new("Animator", humanoid)
-- ходьба
local walk = Instance.new("Animation")
-- стандартная R15
walk.AnimationId = "rbxassetid://507777826"
-- загружаем
local walkTrack = animator:LoadAnimation(walk)
-- по кругу
walkTrack.Looped = true
-- скорость изменилась
humanoid.Running:Connect(function(speed)
    -- идёт — шагает
    if speed > 0.5 and not walkTrack.IsPlaying then walkTrack:Play() elseif speed <= 0.5 then walkTrack:Stop() end
-- конец функции
end)
-- движение считает сервер
pcall(function() root:SetNetworkOwner(nil) end)

-- настройки пути
local path = PathfindingService:CreatePath({
    -- ширина противника
    AgentRadius = 2,
    -- рост
    AgentHeight = 6,
    -- умеет прыгать
    AgentCanJump = true,
    -- не лазает
    AgentCanClimb = false,
    -- вода «дорогая» — обходит, если можно
    Costs = {Water = 100},
-- конец настроек
})

-- ближайший живой игрок в поле зрения
local function FindTarget()
    -- пока никого
    local best, bestHumanoid, bestDistance = nil, nil, SIGHT
    -- все игроки
    for _, player in ipairs(Players:GetPlayers()) do
        -- персонаж
        local character = player.Character
        -- его Humanoid
        local target = character and character:FindFirstChildOfClass("Humanoid")
        -- его центр
        local targetRoot = character and character:FindFirstChild("HumanoidRootPart")
        -- жив
        if target and targetRoot and target.Health > 0 then
            -- расстояние
            local distance = (targetRoot.Position - root.Position).Magnitude
            -- ближе прошлого
            if distance < bestDistance then best, bestHumanoid, bestDistance = targetRoot, target, distance end
        -- конец проверки
        end
    -- конец перебора
    end
    -- центр, Humanoid и расстояние
    return best, bestHumanoid, bestDistance
-- конец функции
end

-- идти по найденному пути
local function MoveAlong(destination, chasing)
    -- строим путь
    local ok = pcall(function() path:ComputeAsync(root.Position, destination) end)
    -- путь не нашёлся
    if not ok or path.Status ~= Enum.PathStatus.Success then
        -- идём напрямик
        humanoid:MoveTo(destination)
        -- немного
        task.wait(0.5)
        -- потом снова подумаем
        return
    -- конец проверки
    end
    -- точки пути
    for i, waypoint in ipairs(path:GetWaypoints()) do
        -- тут надо прыгнуть
        if waypoint.Action == Enum.PathWaypointAction.Jump then humanoid.Jump = true end
        -- к точке
        humanoid:MoveTo(waypoint.Position)
        -- дошли
        humanoid.MoveToFinished:Wait()
        -- в погоне путь пересчитываем каждые 3 точки — игрок двигается
        if chasing and i >= 3 then return end
        -- в патруле заметил игрока — бросаем патруль
        if not chasing and FindTarget() then return end
    -- конец перебора
    end
-- конец функции
end

-- пока жив
while humanoid.Health > 0 do
    -- есть кто рядом?
    local targetRoot, target, distance = FindTarget()
    -- есть
    if targetRoot then
        -- погоня — быстрее (атрибут SprintSpeed)
        humanoid.WalkSpeed = enemy:GetAttribute("SprintSpeed") or 18
        -- рядом
        if distance <= ATTACK_RANGE then
            -- удар готов
            if os.clock() - lastAttack >= COOLDOWN then
                -- запоминаем
                lastAttack = os.clock()
                -- урон из атрибута Damage
                target:TakeDamage(enemy:GetAttribute("Damage") or 10)
            -- конец проверки
            end
            -- немного
            task.wait(0.2)
        -- далеко
        else
            -- бежим в обход препятствий
            MoveAlong(targetRoot.Position, true)
        -- конец проверки
        end
    -- никого
    else
        -- патруль — медленно
        humanoid.WalkSpeed = enemy:GetAttribute("WalkSpeed") or 8
        -- точки патруля
        local points = waypoints:GetChildren()
        -- есть
        if #points > 0 then
            -- случайная точка
            local point = points[math.random(1, #points)]
            -- идём к ней
            MoveAlong(point:IsA("Model") and point:GetPivot().Position or point.Position, false)
        -- точек нет
        else
            -- стоим
            task.wait(1)
        -- конец проверки
        end
    -- конец проверки
    end
-- конец цикла
end

Проверка

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

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