Roblox · RPG · урок 13

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

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

Структура

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

Workspace
├─ MapFolder (Folder) — вся карта: Baseplate, SpawnLocation, деревня, тайники
│  ├─ Chest, Coin (Model / Part) — Highlight, CanInteract, Coin, Sound
│  ├─ Note (Part) — Highlight, CanInteract, Text
│  ├─ Market (Model) — лавки; Торговец (Part): CanInteract, CanBuy
│  ├─ ExpStone (Part) — LvlTest
│  └─ Sound (Part) — музыка тайника
├─ SafePart (Part) — куда переносит свиток
├─ CameraPart (Part) — ракурс главного меню
└─ Mobs (Folder) — Wolf: Humanoid, HealthGui → Lvl, Background → Bar (HealthBar), HealthValue; MobAI
ReplicatedStorage
├─ ClassEvent, InteractEvent (RemoteEvent)
├─ BuyFunction (RemoteFunction)
├─ MageEffects (Folder) — FireBall, Explosion, TeleportLocation
└─ Indicator (Part) — Num → Damage
ServerStorage
├─ Classes (Folder) — Swordsman (Dash), Archer (ArcherCamera, Jump, Sneak), Mage (Teleportation)
├─ Weapons (Folder)
│  ├─ Sword (Tool) — Handle, Blade (Level5, Level10), YouAreNotGui, SwordScript (Animation, Swing, Hit)
│  ├─ Bow (Tool) — Handle (Fire, Hit, Level5, Level10), Arrow, Bow, YouAreNotGui, BowLocal, BowServer
│  └─ Staff (Tool) — Handle (Fire, Level5, Level10), SpellCast, FireBall, YouAreNotGui, FireBallLocal, FireBallServer
└─ Shop (Folder) — Swordsman, Archer, Mage → Potion, Scroll, Halo (у каждого Price)
ServerScriptService
└─ PlayerData, ClassServer, InteractServer, UpgradeSystem, MobManager, DamageIndicatorSystem, Buy (Script)
StarterPlayer → StarterPlayerScripts
└─ InteractSystem (LocalScript)
StarterGui — все ScreenGui — ResetOnSpawn выкл.
├─ InteractGui (BillboardGui) — Object, Interact
├─ NoteGui (ScreenGui) — NoteFrame → NoteText, CloseButton
├─ ClassGui (ScreenGui) — Frame → Swordsman, Archer, Mage, ClassLocal
├─ ExpGui (ScreenGui) — ExpFrame → ExpBar, ExpText, LevelText, ExpLocal
├─ MiniMapGui (ScreenGui) — MapFrame (ViewportFrame) → PlayerArrow; MapScript
├─ ShopGui (ScreenGui) — Swordsman, Archer, Mage → Title, Items, Message, CloseButton
└─ MainMenuGui (ScreenGui) — Background, BackFrame → RPG, Logo, PlayButton; MainMenu
Players → игрок (во время игры) — leaderstats → Монеты; Lvl, Exp, RequiredExp; атрибут Class

Шаги

  1. Сверяй сверху вниз: сначала Explorer со «Структурой», потом каждый скрипт целиком.
  2. Имена связывают всё: класс = папка в Classes = папка в Shop = окно в ShopGui = значение CanBuy у торговца = кнопка в ClassGui.
  3. Навыки — LocalScript в папке класса: работают, когда ClassServer копирует папку в персонажа. Оружие выдаётся через StarterGear.
  4. Всё, что даёт деньги, опыт и предметы, решает сервер: InteractServer, MobManager, Buy. Клиент только просит.
  5. Детали инструментов — Anchored выкл.; у меча Handle — невидимая коробка, ею и бьём.
  6. Все ScreenGui — ResetOnSpawn выкл. Workspace → StreamingEnabled выкл.
  7. Анимации, звуки и картинки пустые — вставь свои. Без анимаций всё работает. Для сохранения — API Services.

Скрипты

PlayerData

Где: ServerScriptService · Script
Зачем: Монеты, уровень, опыт, сохранение.

-- игроки
local Players = game:GetService("Players")
-- сохранение
local DSS = game:GetService("DataStoreService")
-- хранилище RPG
local store = DSS:GetDataStore("RPGData")
-- у кого данные загрузились
local loaded = {}

-- создаёт контейнер со значением
local function Value(class, name, value, parent)
    -- контейнер
    local v = Instance.new(class)
    -- имя
    v.Name = name
    -- значение
    v.Value = value
    -- на место
    v.Parent = parent
    -- отдаём
    return v
-- конец функции
end

-- игрок зашёл
local function Setup(player)
    -- таблица по Tab
    local leaderstats = Instance.new("Folder")
    -- имя строго такое
    leaderstats.Name = "leaderstats"
    -- в игрока
    leaderstats.Parent = player
    -- кошелёк
    local coins = Value("IntValue", "Монеты", 0, leaderstats)
    -- уровень
    local lvl = Value("IntValue", "Lvl", 1, player)
    -- опыт
    local exp = Value("IntValue", "Exp", 0, player)
    -- сколько опыта до следующего уровня
    local required = Value("IntValue", "RequiredExp", 100, player)
    -- загружаем сохранение
    local ok, data = pcall(function() return store:GetAsync("rpg_" .. player.UserId) end)
    -- загрузка удалась
    if ok then
        -- сохранение есть
        if type(data) == "table" then
            -- монеты
            coins.Value = data.Coins or 0
            -- уровень
            lvl.Value = data.Lvl or 1
            -- порог уровня
            required.Value = lvl.Value * 100
            -- опыт
            exp.Value = data.Exp or 0
            -- класс — ClassServer выдаст навыки и оружие
            if data.Class then player:SetAttribute("Class", data.Class) end
        -- конец проверки
        end
        -- можно сохранять
        loaded[player] = true
    -- конец проверки
    end
    -- опыт изменился
    exp.Changed:Connect(function()
        -- хватает на уровень (и даже на несколько)
        while exp.Value >= required.Value do
            -- лишний опыт не пропадает
            exp.Value -= required.Value
            -- новый уровень
            lvl.Value += 1
            -- следующий порог выше
            required.Value = lvl.Value * 100
        -- конец цикла
        end
    -- конец функции
    end)
-- конец функции
end

-- сохранить игрока
local function Save(player)
    -- не загрузились — не затираем сохранение нулями
    if not loaded[player] then return end
    -- что сохраняем
    local data = {
        -- монеты
        Coins = player.leaderstats["Монеты"].Value,
        -- уровень
        Lvl = player.Lvl.Value,
        -- опыт
        Exp = player.Exp.Value,
        -- класс
        Class = player:GetAttribute("Class"),
    -- конец таблицы
    }
    -- записываем
    pcall(function() store:SetAsync("rpg_" .. player.UserId, data) end)
-- конец функции
end

-- новые игроки
Players.PlayerAdded:Connect(Setup)
-- кто зашёл раньше скрипта
for _, player in ipairs(Players:GetPlayers()) do task.spawn(Setup, player) end
-- игрок вышел
Players.PlayerRemoving:Connect(function(player)
    -- сохраняем
    Save(player)
    -- забываем
    loaded[player] = nil
-- конец функции
end)
-- сервер закрывается
game:BindToClose(function()
    -- сохраняем всех
    for _, player in ipairs(Players:GetPlayers()) do Save(player) end
-- конец функции
end)

ClassServer

Где: ServerScriptService · Script
Зачем: Класс, навыки, оружие.

-- игроки
local Players = game:GetService("Players")
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- образцы классов и оружия
local SS = game:GetService("ServerStorage")
-- выбор класса
local classEvent = RS:WaitForChild("ClassEvent")
-- папки навыков: Swordsman, Archer, Mage
local classes = SS:WaitForChild("Classes")
-- классовое оружие
local weapons = SS:WaitForChild("Weapons")
-- класс → его оружие
local WEAPON = {Swordsman = "Sword", Archer = "Bow", Mage = "Staff"}

-- папка навыков в персонажа
local function GiveSkills(player)
    -- класс игрока
    local className = player:GetAttribute("Class")
    -- персонаж
    local character = player.Character
    -- нет класса, персонажа или уже выдано
    if not className or not character or character:FindFirstChild(className) then return end
    -- папка класса
    local folder = classes:FindFirstChild(className)
    -- навыки (локальные скрипты) работают в персонаже
    if folder then folder:Clone().Parent = character end
-- конец функции
end

-- классовое оружие
local function GiveWeapon(player)
    -- образец
    local weapon = weapons:FindFirstChild(WEAPON[player:GetAttribute("Class")] or "")
    -- предметы, которые Roblox выдаёт при каждом появлении
    local gear = player:FindFirstChild("StarterGear")
    -- нет оружия или уже выдано
    if not weapon or not gear or gear:FindFirstChild(weapon.Name) then return end
    -- будет у игрока после каждой смерти
    weapon:Clone().Parent = gear
    -- рюкзак — прямо сейчас
    local backpack = player:FindFirstChildOfClass("Backpack")
    -- в рюкзак
    if backpack then weapon:Clone().Parent = backpack end
-- конец функции
end

-- игрок выбрал класс
classEvent.OnServerEvent:Connect(function(player, className)
    -- класс выбирается один раз
    if player:GetAttribute("Class") then return end
    -- нет такого класса
    if typeof(className) ~= "string" or not classes:FindFirstChild(className) then return end
    -- запоминаем — дальше сработает подписка ниже
    player:SetAttribute("Class", className)
-- конец функции
end)

-- игрок зашёл
local function Setup(player)
    -- класс выбран или загружен из сохранения
    player:GetAttributeChangedSignal("Class"):Connect(function()
        -- навыки
        GiveSkills(player)
        -- оружие
        GiveWeapon(player)
    -- конец функции
    end)
    -- каждое появление
    player.CharacterAdded:Connect(function(character)
        -- ждём, пока персонаж соберётся
        character:WaitForChild("Humanoid")
        -- навыки снова (старые удалились вместе с персонажем)
        GiveSkills(player)
    -- конец функции
    end)
    -- класс загрузился раньше, чем мы подписались
    if player:GetAttribute("Class") then
        -- навыки
        GiveSkills(player)
        -- оружие
        GiveWeapon(player)
    -- конец проверки
    end
-- конец функции
end
-- новые игроки
Players.PlayerAdded:Connect(Setup)
-- кто зашёл раньше скрипта
for _, player in ipairs(Players:GetPlayers()) do Setup(player) end

InteractServer

Где: ServerScriptService · Script
Зачем: Монеты и сундуки.

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- клик игрока по монете или сундуку
local interactEvent = RS:WaitForChild("InteractEvent")
-- дальше — не засчитываем (защита от читов)
local MAX_DISTANCE = 32
-- через сколько секунд награда вернётся
local RESPAWN = 60

-- спрятать или показать монету/сундук
local function SetVisible(item, visible)
    -- всё внутри
    local list = item:GetDescendants()
    -- и сам объект
    table.insert(list, item)
    -- каждый
    for _, obj in ipairs(list) do
        -- деталь
        if obj:IsA("BasePart") then
            -- запоминаем обычную прозрачность
            if obj:GetAttribute("BaseTransparency") == nil then obj:SetAttribute("BaseTransparency", obj.Transparency) end
            -- видна или нет
            obj.Transparency = visible and obj:GetAttribute("BaseTransparency") or 1
        -- подсветка
        elseif obj:IsA("Highlight") then
            -- вместе с объектом
            obj.Enabled = visible
        -- конец проверки
        end
    -- конец перебора
    end
-- конец функции
end

-- игрок кликнул
interactEvent.OnServerEvent:Connect(function(player, item)
    -- пришло не то — игнорируем
    if typeof(item) ~= "Instance" or not item:IsDescendantOf(workspace) then return end
    -- метка доступности
    local can = item:FindFirstChild("CanInteract")
    -- сколько монет внутри
    local coin = item:FindFirstChild("Coin")
    -- уже взято или это не награда
    if not can or not can.Value or not coin then return end
    -- где объект
    local position = item:IsA("Model") and item:GetPivot().Position or item.Position
    -- далеко — не засчитываем
    if player:DistanceFromCharacter(position) > MAX_DISTANCE then return end
    -- больше не взять
    can.Value = false
    -- монеты в кошелёк
    player.leaderstats["Монеты"].Value += coin.Value
    -- звук монеты
    local sound = item:FindFirstChild("Sound")
    -- звон
    if sound then sound:Play() end
    -- исчезает
    SetVisible(item, false)
    -- ждём
    task.wait(RESPAWN)
    -- появляется снова
    SetVisible(item, true)
    -- можно взять снова
    can.Value = true
-- конец функции
end)

UpgradeSystem

Где: ServerScriptService · Script
Зачем: Эффекты оружия по уровню.

-- игроки
local Players = game:GetService("Players")
-- оружие → деталь с эффектами
local EFFECT_PART = {Sword = "Blade", Bow = "Handle", Staff = "Handle"}

-- включить эффекты по уровню у всего оружия игрока
local function Apply(player)
    -- уровень
    local lvl = player:FindFirstChild("Lvl")
    -- ещё не создан
    if not lvl then return end
    -- рюкзак, руки и запас на следующую жизнь
    for _, holder in ipairs({player:FindFirstChildOfClass("Backpack"), player.Character, player:FindFirstChild("StarterGear")}) do
        -- есть
        if holder then
            -- каждый предмет
            for _, tool in ipairs(holder:GetChildren()) do
                -- деталь с эффектами
                local part = tool:IsA("Tool") and EFFECT_PART[tool.Name] and tool:FindFirstChild(EFFECT_PART[tool.Name])
                -- это классовое оружие
                if part then
                    -- эффект 5-го уровня (искры)
                    local level5 = part:FindFirstChild("Level5")
                    -- эффект 10-го уровня (огонь)
                    local level10 = part:FindFirstChild("Level10")
                    -- с 5 по 9 уровень
                    if level5 then level5.Enabled = lvl.Value >= 5 and lvl.Value < 10 end
                    -- с 10-го
                    if level10 then level10.Enabled = lvl.Value >= 10 end
                -- конец проверки
                end
            -- конец перебора
            end
        -- конец проверки
        end
    -- конец перебора
    end
-- конец функции
end

-- игрок зашёл
local function Setup(player)
    -- новый уровень — новые эффекты
    player:WaitForChild("Lvl").Changed:Connect(function() Apply(player) end)
    -- в рюкзаке или StarterGear появился предмет
    player.DescendantAdded:Connect(function(obj)
        -- новое оружие сразу с эффектами
        if obj:IsA("Tool") then Apply(player) end
    -- конец функции
    end)
    -- сразу
    Apply(player)
-- конец функции
end
-- новые игроки
Players.PlayerAdded:Connect(Setup)
-- кто зашёл раньше скрипта
for _, player in ipairs(Players:GetPlayers()) do task.spawn(Setup, player) end

MobManager

Где: ServerScriptService · Script
Зачем: Уровни мобов, награда, возрождение.

-- все мобы
local mobs = workspace:WaitForChild("Mobs")
-- через сколько секунд моб вернётся
local RESPAWN = 15

-- следим за одним мобом
local function Setup(mob)
    -- копия целого — для возрождения
    local sample = mob:Clone()
    -- где стоял
    local spot = mob:GetPivot()
    -- Humanoid
    local humanoid = mob:WaitForChild("Humanoid")
    -- уровень моба
    local level = mob:FindFirstChild("Level") or Instance.new("IntValue")
    -- имя — его читают HealthBar и MobAI
    level.Name = "Level"
    -- случайный уровень
    level.Value = math.random(1, 5)
    -- в моба
    level.Parent = mob
    -- здоровье растёт с уровнем
    humanoid.MaxHealth = 50 * level.Value
    -- полное
    humanoid.Health = humanoid.MaxHealth
    -- моб побеждён
    humanoid.Died:Connect(function()
        -- метка от оружия
        local tag = humanoid:FindFirstChild("creator")
        -- кто победил
        local player = tag and tag.Value
        -- игрок ещё в игре
        if player and player.Parent and player:FindFirstChild("Exp") then
            -- опыт
            player.Exp.Value += 40 * level.Value
            -- монеты
            player.leaderstats["Монеты"].Value += 5 * level.Value
        -- конец проверки
        end
        -- ждём
        task.wait(RESPAWN)
        -- убираем тело
        mob:Destroy()
        -- новый моб
        local new = sample:Clone()
        -- на старое место
        new:PivotTo(spot)
        -- в игру
        new.Parent = mobs
        -- следим и за ним
        Setup(new)
    -- конец функции
    end)
-- конец функции
end

-- все мобы
for _, mob in ipairs(mobs:GetChildren()) do Setup(mob) end

DamageIndicatorSystem

Где: ServerScriptService · Script
Зачем: Цифры урона.

-- образец индикатора
local RS = game:GetService("ReplicatedStorage")
-- удаляет индикаторы
local Debris = game:GetService("Debris")
-- кубик с BillboardGui Num → Damage
local template = RS:WaitForChild("Indicator")

-- следим за здоровьем одного персонажа или моба
local function Watch(humanoid)
    -- здоровье до изменения
    local last = humanoid.Health
    -- изменилось
    humanoid.HealthChanged:Connect(function(health)
        -- сколько потерял
        local damage = math.floor(last - health + 0.5)
        -- запоминаем
        last = health
        -- где он
        local root = humanoid.Parent and humanoid.Parent:FindFirstChild("HumanoidRootPart")
        -- это лечение или нет центра
        if damage <= 0 or not root then return end
        -- новый индикатор
        local indicator = template:Clone()
        -- рядом, чуть в стороне
        indicator.CFrame = CFrame.new(root.Position + Vector3.new(math.random(-2, 2), 2 + math.random(0, 2), math.random(-2, 2)))
        -- текст урона
        indicator.Num.Damage.Text = "-" .. damage
        -- показываем
        indicator.Parent = workspace
        -- 0.7 секунды
        Debris:AddItem(indicator, 0.7)
    -- конец функции
    end)
-- конец функции
end

-- все, кто уже есть
for _, obj in ipairs(workspace:GetDescendants()) do
    -- следим
    if obj:IsA("Humanoid") then Watch(obj) end
-- конец перебора
end
-- новые: игроки, возродившиеся мобы
workspace.DescendantAdded:Connect(function(obj)
    -- следим и за ними
    if obj:IsA("Humanoid") then Watch(obj) end
-- конец функции
end)

Buy

Где: ServerScriptService · Script
Зачем: Покупки у торговцев.

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- товары — клиент их не видит и не подменит
local SS = game:GetService("ServerStorage")
-- покупка
local buyFunction = RS:WaitForChild("BuyFunction")
-- папки товаров по классам
local shop = SS:WaitForChild("Shop")

-- игрок нажал «купить»
buyFunction.OnServerInvoke = function(player, className, itemName)
    -- пришёл мусор
    if typeof(className) ~= "string" or typeof(itemName) ~= "string" then return false, "Ошибка" end
    -- чужой торговец
    if player:GetAttribute("Class") ~= className then return false, "Этот торговец не для твоего класса" end
    -- товары класса
    local folder = shop:FindFirstChild(className)
    -- товар
    local item = folder and folder:FindFirstChild(itemName)
    -- цена
    local price = item and item:FindFirstChild("Price")
    -- нет
    if not price then return false, "Нет такого товара" end
    -- кошелёк
    local coins = player.leaderstats["Монеты"]
    -- денег мало
    if coins.Value < price.Value then return false, "Не хватает монет: нужно " .. price.Value end
    -- копия товара
    local copy = item:Clone()
    -- нимб — надеваем
    if copy:IsA("Accessory") then
        -- персонаж
        local humanoid = player.Character and player.Character:FindFirstChildOfClass("Humanoid")
        -- нет персонажа — денег не берём
        if not humanoid then return false, "Персонаж не найден" end
        -- надели
        humanoid:AddAccessory(copy)
    -- инструмент
    else
        -- в рюкзак (а не на землю, как в методичке)
        copy.Parent = player.Backpack
    -- конец проверки
    end
    -- списываем
    coins.Value -= price.Value
    -- ответ
    return true, "Куплено: " .. itemName
-- конец функции
end

InteractSystem

Где: StarterPlayer → StarterPlayerScripts · LocalScript
Зачем: Взаимодействие и магазин.

-- игроки
local Players = game:GetService("Players")
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- каждый кадр
local RunService = game:GetService("RunService")
-- этот игрок
local player = Players.LocalPlayer
-- мышь: mouse.Target — деталь под курсором
local mouse = player:GetMouse()
-- интерфейс игрока
local playerGui = player:WaitForChild("PlayerGui")
-- надпись над объектом (BillboardGui)
local interactGui = playerGui:WaitForChild("InteractGui")
-- окно записки
local noteFrame = playerGui:WaitForChild("NoteGui"):WaitForChild("NoteFrame")
-- просим сервер: «взять монету/сундук»
local interactEvent = RS:WaitForChild("InteractEvent")
-- дальше 30 studs не взаимодействуем
local MAX_DISTANCE = 30
-- окна торговцев
local shopGui = playerGui:WaitForChild("ShopGui")
-- покупка с ответом сервера
local buyFunction = RS:WaitForChild("BuyFunction")

-- объект с CanInteract под курсором (деталь или её модель)
local function Target()
    -- деталь под курсором
    local obj = mouse.Target
    -- поднимаемся к родителям
    while obj and obj ~= workspace do
        -- метка «с этим можно взаимодействовать»
        local can = obj:FindFirstChild("CanInteract")
        -- нашли, и он сейчас доступен
        if can and can.Value then return obj end
        -- выше
        obj = obj.Parent
    -- конец цикла
    end
    -- ничего нет
    return nil
-- конец функции
end

-- где объект
local function PositionOf(item)
    -- у модели — центр
    if item:IsA("Model") then return item:GetPivot().Position end
    -- у детали — её позиция
    return item.Position
-- конец функции
end

-- что написать под именем
local function ActionText(item)
    -- записка
    if item:FindFirstChild("Text") then return "ЛКМ — прочитать" end
    -- торговец
    if item:FindFirstChild("CanBuy") then return "ЛКМ — торговать" end
    -- монета или сундук
    return "ЛКМ — взять"
-- конец функции
end

-- каждый кадр
RunService.RenderStepped:Connect(function()
    -- что под курсором
    local item = Target()
    -- есть и не слишком далеко
    if item and player:DistanceFromCharacter(PositionOf(item)) < MAX_DISTANCE * 2 then
        -- надпись над объектом
        interactGui.Adornee = item:IsA("Model") and (item.PrimaryPart or item:FindFirstChildWhichIsA("BasePart")) or item
        -- имя объекта
        interactGui.Object.Text = item.Name
        -- что можно сделать
        interactGui.Interact.Text = ActionText(item)
        -- показываем
        interactGui.Enabled = true
    -- ничего нет
    else
        -- отвязываем
        interactGui.Adornee = nil
        -- прячем
        interactGui.Enabled = false
    -- конец проверки
    end
-- конец функции
end)

-- окно торговца
local function OpenShop(className)
    -- окно для этого класса
    local frame = shopGui:FindFirstChild(className)
    -- такого окна нет
    if not frame then return end
    -- торговец не для нашего класса
    if player:GetAttribute("Class") ~= className then
        -- его ответ
        noteFrame.NoteText.Text = "Торговец: я работаю только с классом " .. className
        -- показываем
        noteFrame.Visible = true
        -- окно не открываем
        return
    -- конец проверки
    end
    -- без старых сообщений
    frame.Message.Text = ""
    -- открываем
    frame.Visible = true
-- конец функции
end

-- каждое окно торговца — подписываемся один раз
for _, frame in ipairs(shopGui:GetChildren()) do
    -- только окна
    if frame:IsA("Frame") then
        -- «Закрыть»
        frame.CloseButton.MouseButton1Click:Connect(function() frame.Visible = false end)
        -- кнопки товаров
        for _, button in ipairs(frame.Items:GetChildren()) do
            -- только кнопки
            if button:IsA("TextButton") then
                -- нажали
                button.MouseButton1Click:Connect(function()
                    -- класс и товар — серверу, ждём ответ
                    local _, text = buyFunction:InvokeServer(frame.Name, button.Name)
                    -- «Куплено» или «Не хватает монет»
                    frame.Message.Text = text
                -- конец функции
                end)
            -- конец проверки
            end
        -- конец перебора
        end
    -- конец проверки
    end
-- конец перебора
end

-- клик левой кнопкой
mouse.Button1Down:Connect(function()
    -- что под курсором
    local item = Target()
    -- ничего или далеко
    if not item or player:DistanceFromCharacter(PositionOf(item)) > MAX_DISTANCE then return end
    -- это записка?
    local text = item:FindFirstChild("Text")
    -- да
    if text then
        -- текст записки
        noteFrame.NoteText.Text = text.Value
        -- показываем окно
        noteFrame.Visible = true
        -- шелест бумаги
        local sound = item:FindFirstChild("Sound")
        -- играем у себя
        if sound then sound:Play() end
    -- это торговец
    elseif item:FindFirstChild("CanBuy") then
        -- окно его класса
        OpenShop(item.CanBuy.Value)
    -- монета или сундук
    else
        -- деньги начисляет только сервер
        interactEvent:FireServer(item)
    -- конец проверки
    end
-- конец функции
end)
-- «Закрыть» записку — подписка одна
noteFrame:WaitForChild("CloseButton").MouseButton1Click:Connect(function()
    -- прячем
    noteFrame.Visible = false
-- конец функции
end)

MapScript

Где: StarterGui → MiniMapGui · LocalScript
Зачем: Мини-карта.

-- игроки
local Players = game:GetService("Players")
-- каждый кадр
local RunService = game:GetService("RunService")
-- этот игрок
local player = Players.LocalPlayer
-- всё, что видно на мини-карте
local mapFolder = workspace:WaitForChild("MapFolder")
-- ViewportFrame — окно с 3D-копией карты
local mapFrame = script.Parent:WaitForChild("MapFrame")
-- стрелка игрока
local arrow = mapFrame:WaitForChild("PlayerArrow")
-- с какой высоты смотрит камера карты
local HEIGHT = 2500

-- своя камера для окна карты
local camera = Instance.new("Camera")
-- узкий угол — почти без перспективы, как план
camera.FieldOfView = 1
-- лежит в самом окне
camera.Parent = mapFrame
-- окно смотрит через неё
mapFrame.CurrentCamera = camera

-- каждый объект карты
for _, obj in ipairs(mapFolder:GetChildren()) do
    -- копия (некоторые объекты копироваться не умеют)
    local ok, copy = pcall(function() return obj:Clone() end)
    -- получилось
    if ok and copy then
        -- всё внутри копии
        for _, inner in ipairs(copy:GetDescendants()) do
            -- скрипты убираем — иначе локальные заработают
            if inner:IsA("LuaSourceContainer") then inner:Destroy() end
        -- конец перебора
        end
        -- в окно карты
        copy.Parent = mapFrame
    -- конец проверки
    end
-- конец перебора
end

-- каждый кадр
RunService.RenderStepped:Connect(function()
    -- персонаж (после смерти — новый)
    local character = player.Character
    -- его центр
    local root = character and character:FindFirstChild("HumanoidRootPart")
    -- персонажа ещё нет
    if not root then return end
    -- сверху вниз; «верх» карты — север
    camera.CFrame = CFrame.lookAt(root.Position + Vector3.new(0, HEIGHT, 0), root.Position, Vector3.new(0, 0, -1))
    -- стрелка поворачивается вместе с персонажем
    arrow.Rotation = -root.Orientation.Y
-- конец функции
end)

ExpLocal

Где: StarterGui → ExpGui → ExpFrame · LocalScript
Зачем: Полоса опыта.

-- игроки
local Players = game:GetService("Players")
-- этот игрок
local player = Players.LocalPlayer
-- опыт
local exp = player:WaitForChild("Exp")
-- уровень
local lvl = player:WaitForChild("Lvl")
-- порог уровня
local required = player:WaitForChild("RequiredExp")
-- ExpFrame — подложка полосы
local frame = script.Parent
-- полоса опыта
local bar = frame:WaitForChild("ExpBar")
-- «40 / 100»
local expText = frame:WaitForChild("ExpText")
-- «Ур. 2»
local lvlText = frame:WaitForChild("LevelText")

-- обновить полосу и надписи
local function Update()
    -- доля до следующего уровня
    local k = math.clamp(exp.Value / required.Value, 0, 1)
    -- плавно меняем длину
    bar:TweenSize(UDim2.fromScale(k, 1), Enum.EasingDirection.Out, Enum.EasingStyle.Quad, 0.3, true)
    -- числа
    expText.Text = exp.Value .. " / " .. required.Value
    -- уровень
    lvlText.Text = "Ур. " .. lvl.Value
-- конец функции
end
-- опыт изменился
exp.Changed:Connect(Update)
-- новый уровень
lvl.Changed:Connect(Update)
-- новый порог
required.Changed:Connect(Update)
-- сразу при входе
Update()

ClassLocal

Где: StarterGui → ClassGui → Frame · LocalScript
Зачем: Выбор класса.

-- игроки
local Players = game:GetService("Players")
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- этот игрок
local player = Players.LocalPlayer
-- окно выбора класса
local frame = script.Parent
-- сообщить серверу выбор
local classEvent = RS:WaitForChild("ClassEvent")

-- показать окно, только если класса ещё нет
local function Update()
    -- класс выбран (или загружен) — окно не нужно
    frame.Visible = player:GetAttribute("Class") == nil
-- конец функции
end
-- все объекты окна
for _, button in ipairs(frame:GetChildren()) do
    -- кнопки классов
    if button:IsA("TextButton") then
        -- нажали
        button.MouseButton1Click:Connect(function()
            -- прячем окно
            frame.Visible = false
            -- имя кнопки = имя класса
            classEvent:FireServer(button.Name)
        -- конец функции
        end)
    -- конец проверки
    end
-- конец перебора
end
-- класс появился — окно прячется
player:GetAttributeChangedSignal("Class"):Connect(Update)
-- сразу
Update()

MainMenu

Где: StarterGui → MainMenuGui · LocalScript
Зачем: Главное меню.

-- игроки
local Players = game:GetService("Players")
-- каждый кадр
local RunService = game:GetService("RunService")
-- этот игрок
local player = Players.LocalPlayer
-- камера
local camera = workspace.CurrentCamera
-- MainMenuGui
local gui = script.Parent
-- «Играть»
local playButton = gui:WaitForChild("BackFrame"):WaitForChild("PlayButton")
-- откуда смотрит камера меню
local cameraPart = workspace:WaitForChild("CameraPart")
-- управление персонажем
local controls = require(player:WaitForChild("PlayerScripts"):WaitForChild("PlayerModule")):GetControls()

-- в меню персонаж не ходит
controls:Disable()
-- каждый кадр, пока меню
local follow = RunService.RenderStepped:Connect(function()
    -- камерой управляет скрипт
    camera.CameraType = Enum.CameraType.Scriptable
    -- красивый вид на карту
    camera.CFrame = cameraPart.CFrame
-- конец функции
end)

-- «Играть»
playButton.MouseButton1Click:Connect(function()
    -- камера свободна
    follow:Disconnect()
    -- обычная камера за персонажем
    camera.CameraType = Enum.CameraType.Custom
    -- можно ходить
    controls:Enable()
    -- меню прячется (ResetOnSpawn выкл. — после смерти не вернётся)
    gui.Enabled = false
-- конец функции
end)

SwordScript

Где: ServerStorage → Weapons → Sword · Script
Зачем: Меч.

-- игроки
local Players = game:GetService("Players")
-- удаляет метки через время
local Debris = game:GetService("Debris")
-- меч
local tool = script.Parent
-- невидимая коробка вокруг меча — ею и бьём
local handle = tool:WaitForChild("Handle")
-- анимация удара
local anim = script:WaitForChild("Animation")
-- звук взмаха
local swingSound = script:WaitForChild("Swing")
-- звук попадания
local hitSound = script:WaitForChild("Hit")
-- «Ты не мечник»
local notGui = tool:WaitForChild("YouAreNotGui")
-- чей это меч
local CLASS = "Swordsman"
-- длина взмаха
local COOLDOWN = 0.6
-- идёт взмах
local swinging = false
-- кого уже ударили этим взмахом
local hitList = {}

-- клик с мечом в руках
tool.Activated:Connect(function()
    -- хозяин
    local player = Players:GetPlayerFromCharacter(tool.Parent)
    -- его Humanoid
    local humanoid = tool.Parent:FindFirstChildOfClass("Humanoid")
    -- нет хозяина, погиб или взмах идёт
    if not player or not humanoid or humanoid.Health <= 0 or swinging then return end
    -- чужой класс
    if player:GetAttribute("Class") ~= CLASS then
        -- надпись ещё не показана
        if notGui.Parent == tool then
            -- «Ты не …» на экран
            notGui.Parent = player.PlayerGui
            -- 2 секунды
            task.wait(2)
            -- прячем обратно в оружие
            notGui.Parent = tool
        -- конец проверки
        end
        -- не бьём
        return
    -- конец проверки
    end
    -- начали
    swinging = true
    -- новый взмах — список пуст
    table.clear(hitList)
    -- анимация удара
    if anim.AnimationId ~= "" then humanoid:FindFirstChildOfClass("Animator"):LoadAnimation(anim):Play() end
    -- вжух
    swingSound:Play()
    -- ждём
    task.wait(COOLDOWN)
    -- можно снова
    swinging = false
-- конец функции
end)

-- меч коснулся чего-то
handle.Touched:Connect(function(hit)
    -- бьём только во время взмаха
    if not swinging then return end
    -- хозяин
    local player = Players:GetPlayerFromCharacter(tool.Parent)
    -- чья деталь
    local model = hit:FindFirstAncestorOfClass("Model")
    -- её Humanoid
    local target = model and model:FindFirstChildOfClass("Humanoid")
    -- не цель, сам себя или уже ударили
    if not player or not target or model == tool.Parent or target.Health <= 0 or hitList[target] then return end
    -- игроков не бьём — только мобов
    if Players:GetPlayerFromCharacter(model) then return end
    -- этим взмахом больше не бьём
    hitList[target] = true
    -- прошлая метка «кто ударил»
    local old = target:FindFirstChild("creator")
    -- убираем
    if old then old:Destroy() end
    -- новая метка
    local tag = Instance.new("ObjectValue")
    -- стандартное имя метки в Roblox
    tag.Name = "creator"
    -- кто ударил — получит опыт за победу
    tag.Value = player
    -- в Humanoid цели
    tag.Parent = target
    -- через 3 секунды метка исчезнет
    Debris:AddItem(tag, 3)
    -- уровень хозяина
    local lvl = player.Lvl.Value
    -- урон растёт с уровнем, ±20 % случайности
    local damage = math.floor((10 + lvl * 3) * (0.8 + math.random() * 0.4))
    -- бьём
    target:TakeDamage(damage)
    -- звук попадания
    hitSound:Play()
-- конец функции
end)

BowLocal

Где: ServerStorage → Weapons → Bow · LocalScript
Зачем: Лук: поза и клик.

-- игроки
local Players = game:GetService("Players")
-- лук
local tool = script.Parent
-- сигнал «выстрел» на сервер
local bowEvent = tool:WaitForChild("Bow")
-- «Ты не лучник»
local notGui = tool:WaitForChild("YouAreNotGui")
-- поза с луком (Movement, зациклена)
local holdAnim = script:WaitForChild("Animation")
-- этот игрок
local player = Players.LocalPlayer
-- куда целимся
local mouse = player:GetMouse()
-- чей это лук
local CLASS = "Archer"
-- дорожка позы
local holdTrack = nil

-- взяли лук
local function OnEquipped()
    -- Humanoid
    local humanoid = tool.Parent:FindFirstChildOfClass("Humanoid")
    -- анимации
    local animator = humanoid and humanoid:FindFirstChildOfClass("Animator")
    -- поза задана
    if animator and holdAnim.AnimationId ~= "" then
        -- загружаем
        holdTrack = animator:LoadAnimation(holdAnim)
        -- по кругу
        holdTrack.Looped = true
        -- держим лук
        holdTrack:Play()
    -- конец проверки
    end
-- конец функции
end
-- каждый раз, когда берут
tool.Equipped:Connect(OnEquipped)
-- подобрали с земли — уже в руках
if tool.Parent and tool.Parent:FindFirstChildOfClass("Humanoid") then OnEquipped() end
-- убрали лук
tool.Unequipped:Connect(function()
    -- обычная поза
    if holdTrack then holdTrack:Stop() end
    -- дорожки нет
    holdTrack = nil
-- конец функции
end)

-- клик — подписка одна, не внутри Equipped
tool.Activated:Connect(function()
    -- чужой класс
    if player:GetAttribute("Class") ~= CLASS then
        -- надпись ещё не показана
        if notGui.Parent == tool then
            -- «Ты не …» на экран
            notGui.Parent = player.PlayerGui
            -- 2 секунды
            task.wait(2)
            -- прячем обратно в оружие
            notGui.Parent = tool
        -- конец проверки
        end
        -- не стреляем
        return
    -- конец проверки
    end
    -- стреляем туда, где центр экрана
    bowEvent:FireServer(mouse.Hit.Position)
-- конец функции
end)

BowServer

Где: ServerStorage → Weapons → Bow · Script
Зачем: Лук: стрела.

-- игроки
local Players = game:GetService("Players")
-- удаляет стрелы
local Debris = game:GetService("Debris")
-- лук
local tool = script.Parent
-- сам лук
local handle = tool:WaitForChild("Handle")
-- стрела на тетиве
local arrow = tool:WaitForChild("Arrow")
-- выстрел от клиента
local bowEvent = tool:WaitForChild("Bow")
-- звук выстрела
local fireSound = handle:WaitForChild("Fire")
-- звук попадания
local hitSound = handle:WaitForChild("Hit")
-- анимация выстрела (Action)
local anim = script:WaitForChild("Animation")
-- кто может стрелять
local CLASS = "Archer"
-- перезарядка
local COOLDOWN = 1.5
-- скорость стрелы
local SPEED = 200
-- перезаряжаемся
local busy = false

-- клиент просит выстрелить
bowEvent.OnServerEvent:Connect(function(player, target)
    -- кто держит
    local character = tool.Parent
    -- его Humanoid
    local humanoid = character and character:FindFirstChildOfClass("Humanoid")
    -- лук не в его руках или мусор вместо точки
    if busy or Players:GetPlayerFromCharacter(character) ~= player or typeof(target) ~= "Vector3" then return end
    -- погиб или не лучник
    if not humanoid or humanoid.Health <= 0 or player:GetAttribute("Class") ~= CLASS then return end
    -- перезарядка
    busy = true
    -- натягиваем тетиву
    if anim.AnimationId ~= "" then humanoid:FindFirstChildOfClass("Animator"):LoadAnimation(anim):Play() end
    -- момент выстрела
    task.wait(0.3)
    -- звук
    fireSound:Play()
    -- летящая стрела
    local shot = arrow:Clone()
    -- всё внутри копии
    for _, obj in ipairs(shot:GetChildren()) do
        -- сварка с луком — иначе стрела тянет лук за собой
        if obj:IsA("WeldConstraint") or obj:IsA("JointInstance") then obj:Destroy() end
    -- конец перебора
    end
    -- летит
    shot.Anchored = false
    -- не отскакивает
    shot.CanCollide = false
    -- у стрелы есть вес — падает дугой
    shot.Massless = false
    -- у лука, остриём к цели
    shot.CFrame = CFrame.new(handle.Position, target)
    -- в мир
    shot.Parent = workspace
    -- полёт считает сервер
    shot:SetNetworkOwner(nil)
    -- вылетает
    shot.AssemblyLinearVelocity = shot.CFrame.LookVector * SPEED
    -- на тетиве стрелы нет
    arrow.Transparency = 1
    -- стрела ещё не попала
    local done = false
    -- стрела коснулась
    shot.Touched:Connect(function(hit)
        -- уже попала или задела стрелка
        if done or hit:IsDescendantOf(character) then return end
        -- чья деталь
        local model = hit:FindFirstAncestorOfClass("Model")
        -- Humanoid цели
        local target = model and model:FindFirstChildOfClass("Humanoid")
        -- живой моб
        if target and target.Health > 0 and not Players:GetPlayerFromCharacter(model) then
            -- попала
            done = true
            -- прошлая метка «кто ударил»
            local old = target:FindFirstChild("creator")
            -- убираем
            if old then old:Destroy() end
            -- новая метка
            local tag = Instance.new("ObjectValue")
            -- стандартное имя метки в Roblox
            tag.Name = "creator"
            -- кто ударил — получит опыт за победу
            tag.Value = player
            -- в Humanoid цели
            tag.Parent = target
            -- через 3 секунды метка исчезнет
            Debris:AddItem(tag, 3)
            -- урон растёт с уровнем
            target:TakeDamage(15 + player.Lvl.Value * 3)
            -- звук попадания
            hitSound:Play()
            -- стрела исчезает
            shot:Destroy()
        -- врезалась в стену или землю
        elseif hit.CanCollide then
            -- дальше не летит
            done = true
            -- торчит, где попала
            shot.Anchored = true
        -- конец проверки
        end
    -- конец функции
    end)
    -- через 5 секунд стрела исчезнет
    Debris:AddItem(shot, 5)
    -- перезарядка
    task.wait(COOLDOWN)
    -- новая стрела на тетиве
    arrow.Transparency = 0
    -- можно снова
    busy = false
-- конец функции
end)

FireBallLocal

Где: ServerStorage → Weapons → Staff · LocalScript
Зачем: Посох: выпад.

-- игроки
local Players = game:GetService("Players")
-- посох
local tool = script.Parent
-- сигнал «заклинание» на сервер
local fireballEvent = tool:WaitForChild("FireBall")
-- «Ты не маг»
local notGui = tool:WaitForChild("YouAreNotGui")
-- выпад посохом; кадр Release — момент выстрела
local anim = script:WaitForChild("Animation")
-- этот игрок
local player = Players.LocalPlayer
-- куда целимся
local mouse = player:GetMouse()
-- чей посох
local CLASS = "Mage"
-- перезарядка
local COOLDOWN = 1
-- дорожка анимации
local track = nil
-- колдуем
local busy = false

-- взяли посох
local function OnEquipped()
    -- Humanoid
    local humanoid = tool.Parent:FindFirstChildOfClass("Humanoid")
    -- анимации
    local animator = humanoid and humanoid:FindFirstChildOfClass("Animator")
    -- готовим выпад
    if animator and anim.AnimationId ~= "" then track = animator:LoadAnimation(anim) end
-- конец функции
end
-- каждый раз, когда берут
tool.Equipped:Connect(OnEquipped)
-- уже в руках
if tool.Parent and tool.Parent:FindFirstChildOfClass("Humanoid") then OnEquipped() end

-- заклинание
local function Cast()
    -- колдуем
    busy = true
    -- шар ещё не вылетел
    local fired = false
    -- выпустить шар
    local function Fire()
        -- только один раз
        if fired then return end
        -- вылетел
        fired = true
        -- серверу — куда лететь
        fireballEvent:FireServer(mouse.Hit.Position)
    -- конец функции
    end
    -- анимация есть
    if track then
        -- анимация дошла до именованного кадра
        local connection = track.KeyframeReached:Connect(function(name)
            -- кадр выстрела
            if name == "Release" then Fire() end
        -- конец функции
        end)
        -- выпад
        track:Play()
        -- ждём кадр Release
        task.wait(0.6)
        -- подписка только на этот выпад
        connection:Disconnect()
    -- конец проверки
    end
    -- кадра не было или анимации нет — стреляем сейчас
    Fire()
    -- перезарядка
    task.wait(COOLDOWN)
    -- можно снова
    busy = false
-- конец функции
end

-- клик с посохом
tool.Activated:Connect(function()
    -- ещё колдуем
    if busy then return end
    -- чужой класс
    if player:GetAttribute("Class") ~= CLASS then
        -- надпись ещё не показана
        if notGui.Parent == tool then
            -- «Ты не …» на экран
            notGui.Parent = player.PlayerGui
            -- 2 секунды
            task.wait(2)
            -- прячем обратно в оружие
            notGui.Parent = tool
        -- конец проверки
        end
        -- не колдуем
        return
    -- конец проверки
    end
    -- колдуем
    Cast()
-- конец функции
end)

FireBallServer

Где: ServerStorage → Weapons → Staff · Script
Зачем: Посох: огненный шар.

-- игроки
local Players = game:GetService("Players")
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- удаляет эффекты
local Debris = game:GetService("Debris")
-- посох
local tool = script.Parent
-- откуда вылетает шар
local spell = tool:WaitForChild("SpellCast")
-- звук заклинания
local fireSound = tool:WaitForChild("Handle"):WaitForChild("Fire")
-- заклинание от клиента
local fireballEvent = tool:WaitForChild("FireBall")
-- образцы эффектов
local effects = RS:WaitForChild("MageEffects")
-- огненный шар
local fireballSample = effects:WaitForChild("FireBall")
-- взрыв (в методичке по ошибке брался FireBall)
local explosionSample = effects:WaitForChild("Explosion")
-- кто колдует
local CLASS = "Mage"
-- скорость шара
local SPEED = 80
-- перезарядка (проверка на сервере)
local COOLDOWN = 1
-- время прошлого заклинания
local lastCast = 0

-- клиент колдует
fireballEvent.OnServerEvent:Connect(function(player, target)
    -- кто держит посох
    local character = tool.Parent
    -- посох не у него или он не маг
    if Players:GetPlayerFromCharacter(character) ~= player or player:GetAttribute("Class") ~= CLASS then return end
    -- мусор вместо точки или слишком часто
    if typeof(target) ~= "Vector3" or os.clock() - lastCast < COOLDOWN then return end
    -- запоминаем
    lastCast = os.clock()
    -- звук
    fireSound:Play()
    -- новый шар
    local ball = fireballSample:Clone()
    -- летит
    ball.Anchored = false
    -- сквозь своих
    ball.CanCollide = false
    -- у конца посоха, смотрит на цель
    ball.CFrame = CFrame.new(spell.Position, target)
    -- в мир
    ball.Parent = workspace
    -- полёт считает сервер
    ball:SetNetworkOwner(nil)
    -- точка для силы
    local attachment = Instance.new("Attachment", ball)
    -- держит скорость
    local move = Instance.new("LinearVelocity")
    -- за центр шара
    move.Attachment0 = attachment
    -- без предела — летит прямо, без падения
    move.MaxForce = math.huge
    -- к цели
    move.VectorVelocity = ball.CFrame.LookVector * SPEED
    -- в шар
    move.Parent = ball
    -- ещё не взорвался
    local done = false
    -- шар коснулся
    ball.Touched:Connect(function(hit)
        -- уже взорвался или задел мага
        if done or hit:IsDescendantOf(character) then return end
        -- чья деталь
        local model = hit:FindFirstAncestorOfClass("Model")
        -- Humanoid цели
        local target = model and model:FindFirstChildOfClass("Humanoid")
        -- сквозь эффекты и невидимые зоны
        if not target and not hit.CanCollide then return end
        -- взрываемся
        done = true
        -- взрыв
        local boom = explosionSample:Clone()
        -- на месте
        boom.Anchored = true
        -- не мешает
        boom.CanCollide = false
        -- где шар
        boom.Position = ball.Position
        -- показываем
        boom.Parent = workspace
        -- секунду
        Debris:AddItem(boom, 1)
        -- шара больше нет
        ball:Destroy()
        -- попали в моба
        if target and target.Health > 0 and not Players:GetPlayerFromCharacter(model) then
            -- прошлая метка «кто ударил»
            local old = target:FindFirstChild("creator")
            -- убираем
            if old then old:Destroy() end
            -- новая метка
            local tag = Instance.new("ObjectValue")
            -- стандартное имя метки в Roblox
            tag.Name = "creator"
            -- кто ударил — получит опыт за победу
            tag.Value = player
            -- в Humanoid цели
            tag.Parent = target
            -- через 3 секунды метка исчезнет
            Debris:AddItem(tag, 3)
            -- урон растёт с уровнем
            target:TakeDamage(10 + player.Lvl.Value * 3)
            -- обычная скорость цели (запоминаем один раз)
            local base = target:GetAttribute("BaseSpeed") or target.WalkSpeed
            -- храним в самой цели
            target:SetAttribute("BaseSpeed", base)
            -- замедление
            target.WalkSpeed = base * 0.3
            -- через 3 секунды
            task.delay(3, function()
                -- её собственная скорость, а не 16
                if target.Parent then target.WalkSpeed = base end
            -- конец функции
            end)
        -- конец проверки
        end
    -- конец функции
    end)
    -- промах — через 4 секунды исчезнет
    Debris:AddItem(ball, 4)
-- конец функции
end)

Dash

Где: ServerStorage → Classes → Swordsman · LocalScript
Зачем: Кувырок.

-- клавиатура
local UIS = game:GetService("UserInputService")
-- игроки
local Players = game:GetService("Players")
-- персонаж: Dash → Swordsman → персонаж
local character = script.Parent.Parent
-- Humanoid
local humanoid = character:WaitForChild("Humanoid")
-- анимация кувырка
local anim = script:WaitForChild("Animation")
-- навык класса — на E
local KEY = Enum.KeyCode.E
-- перезарядка
local COOLDOWN = 3
-- уровень игрока
local lvl = Players.LocalPlayer:WaitForChild("Lvl")
-- можно использовать
local ready = true

-- нажали клавишу
UIS.InputBegan:Connect(function(input, processed)
    -- не та клавиша, перезарядка или погиб
    if processed or input.KeyCode ~= KEY or not ready or humanoid.Health <= 0 then return end
    -- перезарядка началась
    ready = false
    -- кувырок
    if anim.AnimationId ~= "" then humanoid:FindFirstChildOfClass("Animator"):LoadAnimation(anim):Play() end
    -- скорость до кувырка (вдруг на игроке нимб)
    local old = humanoid.WalkSpeed
    -- чем выше уровень, тем быстрее; не больше 90
    humanoid.WalkSpeed = math.min(40 + lvl.Value * 5, 90)
    -- полсекунды
    task.wait(0.5)
    -- прежняя скорость
    humanoid.WalkSpeed = old
    -- перезарядка
    task.wait(COOLDOWN)
    -- можно снова
    ready = true
-- конец функции
end)

ArcherCamera

Где: ServerStorage → Classes → Archer · LocalScript
Зачем: Вид из-за плеча.

-- мышь
local UIS = game:GetService("UserInputService")
-- каждый кадр
local RunService = game:GetService("RunService")
-- персонаж: скрипт → Archer → персонаж
local character = script.Parent.Parent
-- Humanoid
local humanoid = character:WaitForChild("Humanoid")
-- центр
local root = character:WaitForChild("HumanoidRootPart")
-- камера
local camera = workspace.CurrentCamera
-- сейчас лук в руках?
local aiming = false

-- каждый кадр
RunService.RenderStepped:Connect(function()
    -- что в руках
    local tool = character:FindFirstChildOfClass("Tool")
    -- лук
    if tool and tool.Name == "Bow" then
        -- целимся
        aiming = true
        -- курсор в центре экрана — камера крутится мышью
        UIS.MouseBehavior = Enum.MouseBehavior.LockCenter
        -- камера чуть правее — вид из-за плеча
        humanoid.CameraOffset = Vector3.new(2, 0, 0)
        -- персонаж не разворачивается сам
        humanoid.AutoRotate = false
        -- куда смотрит камера
        local look = camera.CFrame.LookVector
        -- то же направление по горизонтали
        local flat = Vector3.new(look.X, 0, look.Z)
        -- камера не смотрит строго вверх или вниз
        if flat.Magnitude > 0.01 then
            -- персонаж смотрит туда же — не стреляет себе за спину
            root.CFrame = CFrame.new(root.Position, root.Position + flat)
        -- конец проверки
        end
    -- лук убрали
    elseif aiming then
        -- больше не целимся
        aiming = false
        -- обычный курсор
        UIS.MouseBehavior = Enum.MouseBehavior.Default
        -- камера на место
        humanoid.CameraOffset = Vector3.zero
        -- персонаж снова поворачивается сам
        humanoid.AutoRotate = true
    -- конец проверки
    end
-- конец функции
end)

Jump

Где: ServerStorage → Classes → Archer · LocalScript
Зачем: Высокий прыжок.

-- клавиатура
local UIS = game:GetService("UserInputService")
-- игроки
local Players = game:GetService("Players")
-- удаляет толчок
local Debris = game:GetService("Debris")
-- персонаж
local character = script.Parent.Parent
-- Humanoid
local humanoid = character:WaitForChild("Humanoid")
-- центр
local root = character:WaitForChild("HumanoidRootPart")
-- анимация прыжка
local anim = script:WaitForChild("Animation")
-- навык класса — на E
local KEY = Enum.KeyCode.E
-- перезарядка
local COOLDOWN = 5
-- уровень игрока
local lvl = Players.LocalPlayer:WaitForChild("Lvl")
-- можно прыгать
local ready = true

-- нажали клавишу
UIS.InputBegan:Connect(function(input, processed)
    -- не та клавиша, перезарядка или погиб
    if processed or input.KeyCode ~= KEY or not ready or humanoid.Health <= 0 then return end
    -- перезарядка
    ready = false
    -- анимация
    if anim.AnimationId ~= "" then humanoid:FindFirstChildOfClass("Animator"):LoadAnimation(anim):Play() end
    -- точка приложения силы
    local attachment = root:FindFirstChild("RootAttachment") or Instance.new("Attachment", root)
    -- толчок
    local push = Instance.new("LinearVelocity")
    -- за центр
    push.Attachment0 = attachment
    -- только вдоль одной линии
    push.VelocityConstraintMode = Enum.VelocityConstraintMode.Line
    -- вверх — по сторонам можно двигаться
    push.LineDirection = Vector3.new(0, 1, 0)
    -- выше с каждым уровнем, не больше 120
    push.LineVelocity = math.min(50 + lvl.Value * 5, 120)
    -- сильный
    push.MaxForce = 100000
    -- своего персонажа игрок двигает сам
    push.Parent = root
    -- толкает 0.3 секунды
    Debris:AddItem(push, 0.3)
    -- перезарядка
    task.wait(COOLDOWN)
    -- можно снова
    ready = true
-- конец функции
end)

Sneak

Где: ServerStorage → Classes → Archer · LocalScript
Зачем: Красться.

-- клавиатура
local UIS = game:GetService("UserInputService")
-- игроки
local Players = game:GetService("Players")
-- персонаж
local character = script.Parent.Parent
-- Humanoid
local humanoid = character:WaitForChild("Humanoid")
-- поза «крадётся» (Movement)
local anim = script:WaitForChild("Animation")
-- уровень игрока
local lvl = Players.LocalPlayer:WaitForChild("Lvl")
-- держать F — красться
local KEY = Enum.KeyCode.F
-- навык открывается с 3-го уровня
local NEED_LEVEL = 3
-- дорожка позы
local track = nil
-- скорость до того, как начали красться
local old = nil

-- нажали
UIS.InputBegan:Connect(function(input, processed)
    -- не F, рано или уже крадёмся
    if processed or input.KeyCode ~= KEY or lvl.Value < NEED_LEVEL or old then return end
    -- запоминаем скорость
    old = humanoid.WalkSpeed
    -- вдвое медленнее
    humanoid.WalkSpeed = old / 2
    -- поза задана
    if anim.AnimationId ~= "" then
        -- загружаем
        track = humanoid:FindFirstChildOfClass("Animator"):LoadAnimation(anim)
        -- крадёмся
        track:Play()
    -- конец проверки
    end
-- конец функции
end)
-- отпустили
UIS.InputEnded:Connect(function(input)
    -- не F или не крались
    if input.KeyCode ~= KEY or not old then return end
    -- прежняя скорость
    humanoid.WalkSpeed = old
    -- больше не крадёмся
    old = nil
    -- обычная поза
    if track then track:Stop() end
-- конец функции
end)

Teleportation

Где: ServerStorage → Classes → Mage · LocalScript
Зачем: Телепорт.

-- клавиатура
local UIS = game:GetService("UserInputService")
-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- каждый кадр
local RunService = game:GetService("RunService")
-- игроки
local Players = game:GetService("Players")
-- персонаж
local character = script.Parent.Parent
-- центр
local root = character:WaitForChild("HumanoidRootPart")
-- куда указывает курсор
local mouse = Players.LocalPlayer:GetMouse()
-- своя копия маркера — образец не трогаем
local marker = RS:WaitForChild("MageEffects"):WaitForChild("TeleportLocation"):Clone()
-- не падает
marker.Anchored = true
-- сквозь
marker.CanCollide = false
-- курсор его не замечает
marker.CanQuery = false
-- и mouse.Hit тоже
mouse.TargetFilter = marker
-- держать E — выбор места, отпустить — телепорт
local KEY = Enum.KeyCode.E
-- перезарядка
local COOLDOWN = 3
-- уровень игрока
local lvl = Players.LocalPlayer:WaitForChild("Lvl")
-- дальше с каждым уровнем, не больше 100
local function Range() return math.min(35 + lvl.Value * 5, 100) end
-- выбираем место
local aiming = false
-- можно телепортироваться
local ready = true

-- место достаточно близко?
local function InRange()
    -- Magnitude — длина вектора
    return (mouse.Hit.Position - root.Position).Magnitude < Range()
-- конец функции
end

-- нажали
UIS.InputBegan:Connect(function(input, processed)
    -- не E или перезарядка
    if processed or input.KeyCode ~= KEY or not ready or aiming then return end
    -- выбираем место
    aiming = true
    -- маркер виден только этому игроку
    marker.Parent = workspace
-- конец функции
end)
-- каждый кадр
RunService.RenderStepped:Connect(function()
    -- не выбираем
    if not aiming then return end
    -- маркер под курсором
    marker.Position = mouse.Hit.Position
    -- голубой — можно, красный — далеко
    marker.Color = InRange() and Color3.fromRGB(0, 255, 255) or Color3.fromRGB(255, 0, 0)
-- конец функции
end)
-- отпустили
UIS.InputEnded:Connect(function(input)
    -- не E или не выбирали
    if input.KeyCode ~= KEY or not aiming then return end
    -- выбор закончен
    aiming = false
    -- прячем маркер
    marker.Parent = nil
    -- далеко — не телепортируемся
    if not InRange() then return end
    -- перезарядка
    ready = false
    -- переносимся, смотрим в ту же сторону
    character:PivotTo(root.CFrame.Rotation + mouse.Hit.Position + Vector3.new(0, 3, 0))
    -- ждём
    task.wait(COOLDOWN)
    -- можно снова
    ready = true
-- конец функции
end)

HealthBar

Где: Workspace → Mobs → Wolf → HealthGui → Background → Bar · Script
Зачем: Полоса здоровья моба.

-- полоса здоровья
local bar = script.Parent
-- HealthGui: Bar → Background → HealthGui
local gui = bar.Parent.Parent
-- «120 / 150»
local healthText = bar.Parent:WaitForChild("HealthValue")
-- «Ур. 3»
local lvlText = gui:WaitForChild("Lvl")
-- модель моба
local mob = gui.Parent
-- его здоровье
local humanoid = mob:WaitForChild("Humanoid")
-- уровень моба (ставит MobManager)
local level = mob:WaitForChild("Level")

-- обновить полоску
local function Update()
    -- длина = доля здоровья
    bar.Size = UDim2.fromScale(math.clamp(humanoid.Health / humanoid.MaxHealth, 0, 1), 1)
    -- числа
    healthText.Text = math.ceil(humanoid.Health) .. " / " .. humanoid.MaxHealth
    -- уровень
    lvlText.Text = "Ур. " .. level.Value
-- конец функции
end
-- урон
humanoid.HealthChanged:Connect(Update)
-- новый максимум
humanoid:GetPropertyChangedSignal("MaxHealth"):Connect(Update)
-- новый уровень
level.Changed:Connect(Update)
-- сразу
Update()

MobAI

Где: Workspace → Mobs → Wolf · Script
Зачем: Поведение моба.

-- игроки
local Players = game:GetService("Players")
-- моб
local mob = script.Parent
-- Humanoid
local humanoid = mob:WaitForChild("Humanoid")
-- центр
local root = mob:WaitForChild("HumanoidRootPart")
-- уровень — от него урон
local level = mob:WaitForChild("Level")
-- Animator
local animator = humanoid:FindFirstChildOfClass("Animator") or Instance.new("Animator", humanoid)
-- замечает игрока ближе 30 studs
local SIGHT = 30
-- кусает ближе 5 studs
local ATTACK_RANGE = 5
-- секунд между укусами
local COOLDOWN = 1.2

-- ходьба
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 home = root.Position
-- время прошлого укуса
local lastAttack = 0

-- пока жив
while humanoid.Health > 0 do
    -- ближайший игрок
    local best, bestRoot, 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, bestRoot, bestDistance = target, targetRoot, distance end
        -- конец проверки
        end
    -- конец перебора
    end
    -- есть цель
    if best then
        -- бежим к ней
        humanoid:MoveTo(bestRoot.Position)
        -- рядом и укус готов
        if bestDistance <= ATTACK_RANGE and os.clock() - lastAttack >= COOLDOWN then
            -- запоминаем
            lastAttack = os.clock()
            -- сильнее с уровнем моба
            best:TakeDamage(3 + level.Value * 2)
        -- конец проверки
        end
    -- никого
    else
        -- домой
        humanoid:MoveTo(home)
    -- конец проверки
    end
    -- 4 раза в секунду
    task.wait(0.25)
-- конец цикла
end

LvlTest

Где: Workspace → MapFolder → ExpStone · Script
Зачем: Камень опыта.

-- игроки
local Players = game:GetService("Players")
-- камень опыта
local part = script.Parent
-- сколько опыта за касание
local EXP = 200
-- кто недавно получал
local cooldown = {}

-- касание
part.Touched:Connect(function(hit)
    -- игрок?
    local player = Players:GetPlayerFromCharacter(hit.Parent)
    -- не игрок или недавно было
    if not player or cooldown[player] or not player:FindFirstChild("Exp") then return end
    -- пауза
    cooldown[player] = true
    -- опыт
    player.Exp.Value += EXP
    -- секунда
    task.wait(1)
    -- можно снова
    cooldown[player] = nil
-- конец функции
end)

PotionScript

Где: ServerStorage → Shop → класс → Potion · Script
Зачем: Зелье.

-- зелье
local tool = script.Parent
-- анимация глотка
local anim = tool:WaitForChild("Drink")
-- сколько лечит
local HEAL = 50
-- уже выпито
local used = false

-- клик — выпить
tool.Activated:Connect(function()
    -- кто пьёт
    local humanoid = tool.Parent:FindFirstChildOfClass("Humanoid")
    -- уже, нет персонажа или погиб
    if used or not humanoid or humanoid.Health <= 0 then return end
    -- выпито
    used = true
    -- глоток
    if anim.AnimationId ~= "" then humanoid:FindFirstChildOfClass("Animator"):LoadAnimation(anim):Play() end
    -- +50, не выше максимума
    humanoid.Health = math.min(humanoid.Health + HEAL, humanoid.MaxHealth)
    -- зелья больше нет
    tool:Destroy()
-- конец функции
end)

ScrollScript

Где: ServerStorage → Shop → класс → Scroll · Script
Зачем: Свиток.

-- свиток
local tool = script.Parent

-- клик — прочитать свиток
tool.Activated:Connect(function()
    -- кто читает
    local character = tool.Parent
    -- безопасное место
    local safe = workspace:FindFirstChild("SafePart")
    -- места нет или нет персонажа
    if not safe or not character:FindFirstChild("HumanoidRootPart") then return end
    -- переносим персонажа целиком
    character:PivotTo(safe.CFrame + Vector3.new(0, 4, 0))
    -- свиток сгорел
    tool:Destroy()
-- конец функции
end)

HaloScript

Где: ServerStorage → Shop → класс → Halo · Script
Зачем: Нимб.

-- нимб (Accessory) — скрипт запускается, когда нимб надели
local halo = script.Parent
-- прибавка к скорости
local BONUS = 4
-- на ком нимб
local humanoid = halo.Parent and halo.Parent:FindFirstChildOfClass("Humanoid")
-- быстрее — до конца жизни персонажа
if humanoid then humanoid.WalkSpeed += BONUS end

Проверка

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

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