Roblox · Королевская битва · урок 12

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

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

Структура

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

Workspace — StreamingEnabled выкл.
├─ Terrain (Terrain) — остров
├─ SafeZone (Part) — цилиндр зоны
├─ SpawnRange, PlaneStart, PlaneEnd (Part) — невидимые
├─ Houses (Folder) — дома
├─ LootSpawns (Folder) — плитки на полах
├─ Loot (Folder) — лут на карте
├─ Helicopter (Model) — Body, VehicleSeat, MainBlades, BackBlades, HelicopterServer
├─ Zip (Model) — Start (Beam, ProximityPrompt → ZipScript), End
└─ Lobby (Model) — SpawnLocation, Gate (GateScript), ReadyZone, LeaveRoomPart, SafeZone
ReplicatedStorage — атрибуты Phase, Alive, Winner, ZoneText, TimeLeft
└─ PlaneEvent, ParachuteEvent, LeaveEvent, TeleportingEvent, DeathEvent, FlashEvent (RemoteEvent)
ServerStorage
├─ Loot (Folder) — Pistol, Rifle, Shotgun, Medkit, Flashbang, SmokeGrenade
├─ Plane (Model) — PrimaryPart — Body
├─ Parachute (Model) — PrimaryPart — MainPart
├─ FallTrail (Trail)
├─ HealthDrop (Part) — Sound
└─ RoundStart (BindableEvent)
ServerScriptService
├─ ZoneModule, WeaponEffects (ModuleScript)
└─ RoundServer, LootSpawn, ParachuteServer, BackpackLimit, DeathServer, DropServer, SafeZoneScript (Script)
StarterPlayer → StarterCharacterScripts
└─ HelicopterLocal, PlaneLocal, ParachuteLocal, ScrollSwitch (LocalScript)
StarterPlayer → StarterPlayerScripts
└─ FlashEffect (LocalScript)
StarterGui — все ResetOnSpawn выкл.
├─ RoundGui (ScreenGui) — TimerFrame, WarnText, AliveText, CenterText; RoundLocal
├─ MainGui (ScreenGui) — Teleporting, LeaveButton
├─ DeathGui (ScreenGui) — DeathFrame; DeathLocal
└─ StartGui (ScreenGui) — StartFrame; StartLocal
Players — CharacterAutoLoads выкл.

Шаги

  1. Сверяй сверху вниз: сначала Explorer со «Структурой», потом каждый скрипт целиком.
  2. Цикл игры: лобби (GateScript) → RoundStart → самолёт и бой (RoundServer + ZoneModule) → победитель в лобби, погибшие возвращаются через экран смерти (DeathServer).
  3. Общее состояние раунда — атрибуты ReplicatedStorage: Phase (Lobby, Plane, Fight), Alive, Winner, ZoneText, TimeLeft. Интерфейс только читает их.
  4. Оружие — из раздела «Оружие» (нужен модуль WeaponEffects в ServerScriptService). Новый предмет лута — просто положи Tool в ServerStorage → Loot.
  5. Победы сохраняются в DataStore: Game Settings → Security → Enable Studio Access to API Services.
  6. Проверка с несколькими игроками: Test → Local Server, 2–3 игрока.

Скрипты

ZoneModule

Где: ServerScriptService · ModuleScript
Зачем: Зона и волны.

-- плавное сужение
local TweenService = game:GetService("TweenService")
-- сюда пишем текст таймера для игроков
local RS = game:GetService("ReplicatedStorage")

-- таблица модуля — её получают через require
local Zone = {}
-- цилиндр безопасной зоны
Zone.Part = workspace:WaitForChild("SafeZone")
-- центр зоны
Zone.Center = Zone.Part.Position
-- у лежащего цилиндра диаметр — Size.Y и Size.Z
Zone.StartRadius = Zone.Part.Size.Y / 2
-- урон в секунду за пределами зоны
Zone.Damage = 1
-- волны: ждём wait секунд, сужаемся до radius за shrink секунд, урон damage
Zone.WAVES = {
    -- 1 волна
    {wait = 60, radius = 140, shrink = 20, damage = 2},
    -- 2 волна
    {wait = 45, radius = 80, shrink = 20, damage = 4},
    -- 3 волна
    {wait = 40, radius = 40, shrink = 15, damage = 7},
    -- 4 волна
    {wait = 30, radius = 12, shrink = 15, damage = 12},
    -- финал — пятачок, все лицом к лицу
    {wait = 20, radius = 2, shrink = 10, damage = 20},
-- конец таблицы
}

-- радиус сейчас
function Zone.Radius()
    -- половина диаметра
    return Zone.Part.Size.Y / 2
-- конец функции
end

-- точка внутри зоны?
function Zone.IsInside(position)
    -- расстояние по земле — высота не важна
    local flat = Vector3.new(position.X - Zone.Center.X, 0, position.Z - Zone.Center.Z)
    -- ближе радиуса — внутри
    return flat.Magnitude <= Zone.Radius()
-- конец функции
end

-- зона снова во всю карту
function Zone.Reset()
    -- размер
    local size = Zone.Part.Size
    -- исходный диаметр
    Zone.Part.Size = Vector3.new(size.X, Zone.StartRadius * 2, Zone.StartRadius * 2)
    -- урон снова слабый
    Zone.Damage = 1
    -- таймер прячется
    RS:SetAttribute("ZoneText", "")
-- конец функции
end

-- текст и время для таймера у всех игроков
local function Show(text, seconds)
    -- что происходит
    RS:SetAttribute("ZoneText", text)
    -- сколько секунд
    RS:SetAttribute("TimeLeft", seconds)
-- конец функции
end

-- все волны по очереди; isRunning() вернёт false — стоп
function Zone.Run(isRunning)
    -- с начала
    Zone.Reset()
    -- каждая волна
    for _, wave in ipairs(Zone.WAVES) do
        -- ожидание
        for t = wave.wait, 1, -1 do
            -- раунд кончился
            if not isRunning() then return end
            -- таймер
            Show("Зона сузится через", t)
            -- секунда
            task.wait(1)
        -- конец ожидания
        end
        -- за зоной бьёт сильнее
        Zone.Damage = wave.damage
        -- размер
        local size = Zone.Part.Size
        -- сужение
        local tween = TweenService:Create(Zone.Part, TweenInfo.new(wave.shrink, Enum.EasingStyle.Linear), {Size = Vector3.new(size.X, wave.radius * 2, wave.radius * 2)})
        -- поехали
        tween:Play()
        -- пока сужается
        for t = wave.shrink, 1, -1 do
            -- раунд кончился
            if not isRunning() then tween:Cancel() return end
            -- таймер
            Show("Зона сужается", t)
            -- секунда
            task.wait(1)
        -- конец сужения
        end
    -- конец волн
    end
    -- дальше сужаться некуда
    Show("Финальная зона", 0)
-- конец функции
end

-- урон тем, кто за зоной (вызывать раз в секунду)
function Zone.Hurt(players)
    -- каждый игрок
    for _, player in ipairs(players) do
        -- персонаж
        local character = player.Character
        -- Humanoid
        local humanoid = character and character:FindFirstChildOfClass("Humanoid")
        -- центр
        local root = character and character:FindFirstChild("HumanoidRootPart")
        -- жив и за зоной
        local outside = humanoid ~= nil and root ~= nil and humanoid.Health > 0 and not Zone.IsInside(root.Position)
        -- интерфейс покажет предупреждение
        player:SetAttribute("OutsideZone", outside)
        -- урон
        if outside then humanoid:TakeDamage(Zone.Damage) end
    -- конец перебора
    end
-- конец функции
end

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

RoundServer

Где: ServerScriptService · Script
Зачем: Самолёт, бой, победа.

-- игроки
local Players = game:GetService("Players")
-- атрибуты раунда и события
local RS = game:GetService("ReplicatedStorage")
-- образец самолёта
local SS = game:GetService("ServerStorage")
-- каждый кадр
local RunService = game:GetService("RunService")
-- сохранение побед
local DSS = game:GetService("DataStoreService")
-- модуль зоны
local Zone = require(script.Parent:WaitForChild("ZoneModule"))
-- хранилище
local store = DSS:GetDataStore("BattleRoyale")
-- лобби отправило игроков
local roundStart = SS:WaitForChild("RoundStart")
-- самолёт (PrimaryPart — Body)
local planeSample = SS:WaitForChild("Plane")
-- начало маршрута
local planeStart = workspace:WaitForChild("PlaneStart")
-- конец маршрута
local planeEnd = workspace:WaitForChild("PlaneEnd")
-- над этой областью можно прыгать
local spawnRange = workspace:WaitForChild("SpawnRange")
-- куда вернуть победителя
local lobbySpawn = workspace:WaitForChild("Lobby"):WaitForChild("SpawnLocation")
-- прыжок из самолёта
local planeEvent = RS:WaitForChild("PlaneEvent")
-- секунд полёта от края до края
local FLIGHT_TIME = 30
-- игрок → его персонаж: кто ещё жив в раунде
local alive = {}
-- игрок → true: кто ещё в самолёте
local onBoard = {}
-- самолёт сейчас
local plane = nil
-- Lobby, Plane или Fight
RS:SetAttribute("Phase", "Lobby")
-- имени победителя пока нет
RS:SetAttribute("Winner", "")

-- игрок зашёл: таблица лидеров
local function Setup(player)
    -- папка
    local stats = Instance.new("Folder")
    -- так Roblox показывает её в таблице справа
    stats.Name = "leaderstats"
    -- в игрока
    stats.Parent = player
    -- победы
    local wins = Instance.new("IntValue")
    -- имя
    wins.Name = "Wins"
    -- в таблицу
    wins.Parent = stats
    -- загружаем (pcall — сервер может не ответить)
    local ok, value = pcall(function() return store:GetAsync(player.UserId .. "Wins") end)
    -- были победы — ставим
    if ok and value then wins.Value = value end
-- конец функции
end
-- сохранить победы
local function Save(player)
    -- таблица
    local stats = player:FindFirstChild("leaderstats")
    -- сохраняем
    if stats then pcall(function() store:SetAsync(player.UserId .. "Wins", stats.Wins.Value) end) end
-- конец функции
end
-- новые игроки
Players.PlayerAdded:Connect(Setup)
-- кто зашёл раньше скрипта
for _, player in ipairs(Players:GetPlayers()) do Setup(player) end
-- вышел
Players.PlayerRemoving:Connect(function(player)
    -- сохраняем
    Save(player)
    -- больше не в раунде
    alive[player] = nil
    -- и не в самолёте
    onBoard[player] = nil
-- конец функции
end)
-- сервер выключается — сохраняем всех
game:BindToClose(function() for _, player in ipairs(Players:GetPlayers()) do Save(player) end end)

-- самолёт над островом?
local function OverRange(position)
    -- координаты относительно SpawnRange
    local p = spawnRange.CFrame:PointToObjectSpace(position)
    -- внутри по ширине и длине
    return math.abs(p.X) <= spawnRange.Size.X / 2 and math.abs(p.Z) <= spawnRange.Size.Z / 2
-- конец функции
end
-- выпустить игрока из самолёта
local function Drop(player)
    -- уже прыгнул
    if not onBoard[player] or not plane then return end
    -- больше не в самолёте
    onBoard[player] = nil
    -- интерфейс: подсказка пропадает
    player:SetAttribute("OnPlane", false)
    -- камера снова за персонажем
    planeEvent:FireClient(player, false)
    -- центр персонажа
    local root = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
    -- персонажа нет
    if not root then return end
    -- под самолётом
    root.CFrame = plane:GetPivot() * CFrame.new(0, -15, 0)
    -- падает
    root.Anchored = false
-- конец функции
end
-- игрок нажал пробел
planeEvent.OnServerEvent:Connect(function(player)
    -- только над островом
    if plane and OverRange(plane:GetPivot().Position) then Drop(player) end
-- конец функции
end)

-- посадить игроков в самолёт
local function Board(list)
    -- новый раунд
    table.clear(alive)
    -- самолёт пуст
    table.clear(onBoard)
    -- каждый из лобби
    for _, player in ipairs(list) do
        -- персонаж
        local character = player.Character
        -- Humanoid
        local humanoid = character and character:FindFirstChildOfClass("Humanoid")
        -- центр
        local root = character and character:FindFirstChild("HumanoidRootPart")
        -- живой и в игре
        if player.Parent and humanoid and root and humanoid.Health > 0 then
            -- в раунде
            alive[player] = character
            -- в самолёте
            onBoard[player] = true
            -- летит вместе с самолётом, а не падает
            root.Anchored = true
            -- интерфейс: «Пробел — прыгнуть»
            player:SetAttribute("OnPlane", true)
            -- камера смотрит на самолёт
            planeEvent:FireClient(player, true)
        -- конец проверки
        end
    -- конец перебора
    end
-- конец функции
end

-- полёт самолёта от PlaneStart к PlaneEnd
local function Fly()
    -- в начале смотрит на конец
    local startCF = CFrame.lookAt(planeStart.Position, planeEnd.Position)
    -- в конце — тот же поворот
    local endCF = startCF.Rotation + planeEnd.Position
    -- новый самолёт
    plane = planeSample:Clone()
    -- в начало
    plane:PivotTo(startCF)
    -- в мир
    plane.Parent = workspace
    -- сколько летим
    local t = 0
    -- пока не долетели
    while t < FLIGHT_TIME do
        -- прошёл кадр
        t += RunService.Heartbeat:Wait()
        -- точка на маршруте
        local cf = startCF:Lerp(endCF, math.min(t / FLIGHT_TIME, 1))
        -- самолёт летит
        plane:PivotTo(cf)
        -- кто внутри
        for player in pairs(onBoard) do
            -- центр
            local root = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
            -- внутри самолёта
            if root then root.CFrame = cf end
        -- конец перебора
        end
    -- конец полёта
    end
    -- кто не прыгнул — сбрасываем в конце маршрута
    for player in pairs(onBoard) do Drop(player) end
    -- самолёт улетел
    plane:Destroy()
    -- самолёта нет
    plane = nil
-- конец функции
end

-- убрать погибших и вышедших; вернуть число живых
local function CheckAlive()
    -- счётчик и последний живой
    local count, last = 0, nil
    -- каждый в раунде
    for player, character in pairs(alive) do
        -- Humanoid
        local humanoid = character:FindFirstChildOfClass("Humanoid")
        -- вышел, возродился или погиб
        if not player.Parent or player.Character ~= character or not humanoid or humanoid.Health <= 0 then
            -- выбыл
            alive[player] = nil
        -- жив
        else
            -- считаем
            count += 1
            -- запоминаем
            last = player
        -- конец проверки
        end
    -- конец перебора
    end
    -- интерфейс: «Живых: N»
    RS:SetAttribute("Alive", count)
    -- число и последний
    return count, last
-- конец функции
end

-- весь раунд
local function RunRound(list)
    -- полёт
    RS:SetAttribute("Phase", "Plane")
    -- в самолёт
    Board(list)
    -- один игрок — раунд до его смерти (для проверки)
    local solo = CheckAlive() <= 1
    -- летим 30 секунд
    Fly()
    -- бой
    RS:SetAttribute("Phase", "Fight")
    -- раунд идёт
    local running = true
    -- волны зоны — отдельно
    task.spawn(Zone.Run, function() return running end)
    -- победитель
    local winner = nil
    -- раз в секунду
    while true do
        -- секунда
        task.wait(1)
        -- кто жив
        local count, last = CheckAlive()
        -- список живых
        local players = {}
        -- собираем
        for player in pairs(alive) do table.insert(players, player) end
        -- урон тем, кто за зоной
        Zone.Hurt(players)
        -- остался один (или никого)
        if count == 0 or (count == 1 and not solo) then
            -- он победил
            winner = last
            -- раунд окончен
            break
        -- конец проверки
        end
    -- конец цикла
    end
    -- зона останавливается
    running = false
    -- есть победитель
    if winner then
        -- +1 победа
        winner.leaderstats.Wins.Value += 1
        -- все видят имя
        RS:SetAttribute("Winner", winner.DisplayName)
    -- конец проверки
    end
    -- празднуем
    task.wait(6)
    -- надпись пропадает
    RS:SetAttribute("Winner", "")
    -- предупреждения гаснут
    for _, player in ipairs(Players:GetPlayers()) do player:SetAttribute("OutsideZone", false) end
    -- победитель — в лобби
    if winner and winner.Character then winner.Character:PivotTo(lobbySpawn.CFrame + Vector3.new(0, 4, 0)) end
    -- раунда больше нет
    table.clear(alive)
    -- зона снова большая
    Zone.Reset()
    -- лобби снова набирает игроков
    RS:SetAttribute("Phase", "Lobby")
-- конец функции
end

-- лобби отправило игроков
roundStart.Event:Connect(function(list)
    -- раунд не идёт — начинаем
    if RS:GetAttribute("Phase") == "Lobby" and #list > 0 then RunRound(list) end
-- конец функции
end)

LootSpawn

Где: ServerScriptService · Script
Зачем: Случайный лут.

-- образцы лута
local SS = game:GetService("ServerStorage")
-- оружие, гранаты, аптечки (Tool)
local loot = SS:WaitForChild("Loot")
-- невидимые плитки на полах домов
local spawns = workspace:WaitForChild("LootSpawns")
-- сюда кладём появившийся лут
local folder = workspace:WaitForChild("Loot")
-- сколько предметов лежит одновременно
local MAX_ACTIVE = 10
-- через сколько секунд после подбора появится новый
local RESPAWN_DELAY = 10
-- сколько лежит сейчас
local active = 0

-- случайная точка на случайной плитке
local function RandomPoint()
    -- все плитки
    local tiles = spawns:GetChildren()
    -- случайная
    local tile = tiles[math.random(1, #tiles)]
    -- по ширине плитки
    local x = (math.random() - 0.5) * tile.Size.X
    -- по длине
    local z = (math.random() - 0.5) * tile.Size.Z
    -- чуть выше плитки — предмет упадёт на пол
    return (tile.CFrame * CFrame.new(x, tile.Size.Y / 2 + 1.5, z)).Position
-- конец функции
end

-- положить один предмет
local function SpawnItem()
    -- образцы
    local items = loot:GetChildren()
    -- нечего, некуда или хватит
    if #items == 0 or #spawns:GetChildren() == 0 or active >= MAX_ACTIVE then return end
    -- случайный предмет
    local item = items[math.random(1, #items)]:Clone()
    -- за неё двигаем — приваренные детали едут следом
    local handle = item:FindFirstChild("Handle")
    -- предмет без Handle
    if not handle then return end
    -- место и случайный поворот
    handle.CFrame = CFrame.new(RandomPoint()) * CFrame.Angles(0, math.rad(math.random(0, 359)), 0)
    -- в мир
    item.Parent = folder
    -- лежит на один больше
    active += 1
    -- подписка на подбор
    local connection
    -- предмет куда-то переложили
    connection = item:GetPropertyChangedSignal("Parent"):Connect(function()
        -- всё ещё лежит
        if item.Parent == folder then return end
        -- больше не следим
        connection:Disconnect()
        -- подобрали
        active -= 1
        -- через 10 секунд — новый где-то ещё
        task.delay(RESPAWN_DELAY, SpawnItem)
    -- конец функции
    end)
-- конец функции
end

-- сразу раскладываем лут по карте
for i = 1, MAX_ACTIVE do SpawnItem() end

ParachuteServer

Где: ServerScriptService · Script
Зачем: Парашют и следы.

-- события
local RS = game:GetService("ReplicatedStorage")
-- образцы
local SS = game:GetService("ServerStorage")
-- просьбы клиента
local event = RS:WaitForChild("ParachuteEvent")
-- модель парашюта (PrimaryPart — MainPart)
local parachuteSample = SS:WaitForChild("Parachute")
-- след в воздухе
local trailSample = SS:WaitForChild("FallTrail")
-- скорость спуска на парашюте
local FALL_SPEED = 20
-- где рисуем следы (R15)
local TRAIL_PARTS = {"LeftHand", "RightHand", "LeftFoot", "RightFoot"}

-- удалить всё с этим именем
local function Remove(character, name)
    -- все части персонажа
    for _, item in ipairs(character:GetDescendants()) do
        -- нашли — удаляем
        if item.Name == name then item:Destroy() end
    -- конец перебора
    end
-- конец функции
end

-- клиент попросил
event.OnServerEvent:Connect(function(player, action)
    -- персонаж
    local character = player.Character
    -- Humanoid
    local humanoid = character and character:FindFirstChildOfClass("Humanoid")
    -- центр
    local root = character and character:FindFirstChild("HumanoidRootPart")
    -- погиб
    if not humanoid or not root or humanoid.Health <= 0 then return end
    -- долгое падение — следы
    if action == "Fall" then
        -- старые убираем
        Remove(character, "FallTrail")
        -- руки и ноги
        for _, partName in ipairs(TRAIL_PARTS) do
            -- часть тела
            local part = character:FindFirstChild(partName)
            -- есть (у R6 таких нет)
            if part then
                -- начало следа
                local a0 = Instance.new("Attachment")
                -- имя — чтобы потом удалить
                a0.Name = "FallTrail"
                -- с одной стороны
                a0.Position = Vector3.new(0.4, 0, 0)
                -- в часть тела
                a0.Parent = part
                -- конец следа
                local a1 = a0:Clone()
                -- с другой стороны
                a1.Position = Vector3.new(-0.4, 0, 0)
                -- в часть тела
                a1.Parent = part
                -- след
                local trail = trailSample:Clone()
                -- имя
                trail.Name = "FallTrail"
                -- между двумя точками
                trail.Attachment0 = a0
                -- получается лента
                trail.Attachment1 = a1
                -- в часть тела
                trail.Parent = part
            -- конец проверки
            end
        -- конец перебора
        end
    -- раскрыть парашют
    elseif action == "Open" then
        -- уже раскрыт
        if character:FindFirstChild("Parachute") then return end
        -- туловище R15 или R6
        local torso = character:FindFirstChild("UpperTorso") or character:FindFirstChild("Torso")
        -- нет туловища
        if not torso then return end
        -- парашют
        local chute = parachuteSample:Clone()
        -- имя
        chute.Name = "Parachute"
        -- над головой
        chute:PivotTo(torso.CFrame * CFrame.new(0, 6, 0))
        -- приварим к туловищу
        local weld = Instance.new("WeldConstraint")
        -- туловище
        weld.Part0 = torso
        -- MainPart парашюта
        weld.Part1 = chute.PrimaryPart
        -- в парашют
        weld.Parent = chute.PrimaryPart
        -- в персонажа
        chute.Parent = character
        -- точка для силы
        local attachment = Instance.new("Attachment")
        -- имя
        attachment.Name = "ParachuteForce"
        -- в центр
        attachment.Parent = root
        -- держит скорость падения
        local slow = Instance.new("LinearVelocity")
        -- имя
        slow.Name = "ParachuteForce"
        -- за центр
        slow.Attachment0 = attachment
        -- только по одной оси…
        slow.VelocityConstraintMode = Enum.VelocityConstraintMode.Line
        -- …вертикальной: по сторонам игрок управляет сам
        slow.LineDirection = Vector3.new(0, 1, 0)
        -- вниз 20 studs/с
        slow.LineVelocity = -FALL_SPEED
        -- достаточно, чтобы затормозить
        slow.MaxForce = 100000
        -- в центр
        slow.Parent = root
    -- приземлился
    elseif action == "Landed" then
        -- парашют
        Remove(character, "Parachute")
        -- торможение
        Remove(character, "ParachuteForce")
        -- следы
        Remove(character, "FallTrail")
    -- конец проверки
    end
-- конец функции
end)

BackpackLimit

Где: ServerScriptService · Script
Зачем: Лимит рюкзака.

-- игроки
local Players = game:GetService("Players")
-- сколько предметов можно нести
local LIMIT = 4

-- все предметы: рюкзак и руки
local function Tools(player, character)
    -- список
    local list = {}
    -- рюкзак
    for _, tool in ipairs(player.Backpack:GetChildren()) do
        -- только инструменты
        if tool:IsA("Tool") then table.insert(list, tool) end
    -- конец перебора
    end
    -- руки
    for _, tool in ipairs(character:GetChildren()) do
        -- в руках
        if tool:IsA("Tool") then table.insert(list, tool) end
    -- конец перебора
    end
    -- список
    return list
-- конец функции
end
-- подобрал предмет — не много ли?
local function Check(player, character, newTool)
    -- влезает
    if not newTool:IsA("Tool") or #Tools(player, character) <= LIMIT then return end
    -- центр
    local root = character:FindFirstChild("HumanoidRootPart")
    -- персонажа нет
    if not root then return end
    -- что в руках
    local held = character:FindFirstChildOfClass("Tool")
    -- меняем: выбрасываем то, что в руках, иначе новое
    local drop = (held and held ~= newTool) and held or newTool
    -- сразу во время подбора менять родителя нельзя — чуть позже
    task.defer(function()
        -- за что двигать
        local handle = drop:FindFirstChild("Handle")
        -- на землю
        drop.Parent = workspace:FindFirstChild("Loot") or workspace
        -- в 6 studs позади
        if handle then handle.CFrame = root.CFrame * CFrame.new(0, 0, 6) end
    -- конец функции
    end)
-- конец функции
end

-- игрок зашёл
local function Setup(player)
    -- каждый новый персонаж (рюкзак тоже новый)
    player.CharacterAdded:Connect(function(character)
        -- рюкзак
        local backpack = player:WaitForChild("Backpack")
        -- что-то попало в рюкзак
        backpack.ChildAdded:Connect(function(tool) Check(player, character, tool) end)
        -- или сразу в руки (в методичке Check вызывался без игрока)
        character.ChildAdded:Connect(function(tool) Check(player, character, tool) end)
    -- конец функции
    end)
-- конец функции
end
-- новые игроки
Players.PlayerAdded:Connect(Setup)
-- кто зашёл раньше скрипта
for _, player in ipairs(Players:GetPlayers()) do Setup(player) end

DeathServer

Где: ServerScriptService · Script
Зачем: Возрождение.

-- игроки
local Players = game:GetService("Players")
-- события
local RS = game:GetService("ReplicatedStorage")
-- кнопка «В лобби»
local deathEvent = RS:WaitForChild("DeathEvent")
-- столько же, сколько отсчёт на экране смерти
local RESPAWN_TIME = 8
-- сами решаем, когда игрок возрождается
Players.CharacterAutoLoads = false

-- игрок мёртв?
local function IsDead(player)
    -- Humanoid
    local humanoid = player.Character and player.Character:FindFirstChildOfClass("Humanoid")
    -- нет персонажа или здоровье 0
    return not humanoid or humanoid.Health <= 0
-- конец функции
end
-- возродить в лобби
local function Respawn(player)
    -- только мёртвых — живой не «перезапустится» кнопкой
    if player.Parent and IsDead(player) then player:LoadCharacter() end
-- конец функции
end
-- игрок зашёл
local function Setup(player)
    -- новый персонаж
    player.CharacterAdded:Connect(function(character)
        -- погиб
        character:WaitForChild("Humanoid").Died:Connect(function()
            -- страховка: если клиент не нажал — возродим сами
            task.delay(RESPAWN_TIME + 2, Respawn, player)
        -- конец функции
        end)
    -- конец функции
    end)
    -- первый персонаж — сами (автозагрузка выключена)
    if not player.Character then player:LoadCharacter() end
-- конец функции
end
-- новые игроки
Players.PlayerAdded:Connect(Setup)
-- кто зашёл раньше скрипта
for _, player in ipairs(Players:GetPlayers()) do task.spawn(Setup, player) end
-- нажал «В лобби» или кончился отсчёт
deathEvent.OnServerEvent:Connect(Respawn)

DropServer

Где: ServerScriptService · Script
Зачем: Лут погибшего.

-- игроки
local Players = game:GetService("Players")
-- образец дропа
local SS = game:GetService("ServerStorage")
-- удаление через время
local Debris = game:GetService("Debris")
-- шар здоровья (внутри Sound)
local healthSample = SS:WaitForChild("HealthDrop")
-- сколько лечит
local HEAL = 40
-- через сколько секунд исчезнет, если не взяли
local LIFETIME = 120

-- шар здоровья на месте смерти
local function DropHealth(position)
    -- копия
    local orb = healthSample:Clone()
    -- висит
    orb.Anchored = true
    -- сквозь
    orb.CanCollide = false
    -- где погиб
    orb.Position = position
    -- в мир
    orb.Parent = workspace
    -- ещё не взяли
    local used = false
    -- коснулись
    orb.Touched:Connect(function(hit)
        -- персонаж
        local humanoid = hit.Parent:FindFirstChildOfClass("Humanoid")
        -- уже взяли или мёртв
        if used or not humanoid or humanoid.Health <= 0 then return end
        -- один раз (в методичке лечил, пока играл звук)
        used = true
        -- лечим
        humanoid.Health = math.min(humanoid.Health + HEAL, humanoid.MaxHealth)
        -- звук
        orb.Sound:Play()
        -- шар исчезает
        orb.Transparency = 1
        -- удалим, когда доиграет звук
        Debris:AddItem(orb, 2)
    -- конец функции
    end)
    -- не взяли — исчезнет сам
    Debris:AddItem(orb, LIFETIME)
-- конец функции
end

-- предметы погибшего — на землю
local function DropTools(player, character, position)
    -- список
    local tools = {}
    -- рюкзак
    for _, tool in ipairs(player.Backpack:GetChildren()) do
        -- инструменты
        if tool:IsA("Tool") then table.insert(tools, tool) end
    -- конец перебора
    end
    -- что было в руках
    local held = character:FindFirstChildOfClass("Tool")
    -- тоже
    if held then table.insert(tools, held) end
    -- каждый
    for i, tool in ipairs(tools) do
        -- за что двигать
        local handle = tool:FindFirstChild("Handle")
        -- есть
        if handle then
            -- раскладываем по кругу
            local angle = i / #tools * math.pi * 2
            -- на землю
            tool.Parent = workspace:FindFirstChild("Loot") or workspace
            -- вокруг места смерти
            handle.CFrame = CFrame.new(position + Vector3.new(math.cos(angle) * 4, 1, math.sin(angle) * 4))
        -- конец проверки
        end
    -- конец перебора
    end
-- конец функции
end

-- игрок зашёл
local function Setup(player)
    -- новый персонаж
    player.CharacterAdded:Connect(function(character)
        -- погиб
        character:WaitForChild("Humanoid").Died:Connect(function()
            -- центр
            local root = character:FindFirstChild("HumanoidRootPart")
            -- уже нет
            if not root then return end
            -- место смерти
            local position = root.Position
            -- лут погибшего
            DropTools(player, character, position)
            -- секунда
            task.wait(1)
            -- шар здоровья
            DropHealth(position)
        -- конец функции
        end)
    -- конец функции
    end)
-- конец функции
end
-- новые игроки
Players.PlayerAdded:Connect(Setup)
-- кто зашёл раньше скрипта
for _, player in ipairs(Players:GetPlayers()) do Setup(player) end

SafeZoneScript

Где: ServerScriptService · Script
Зачем: Бессмертие в лобби.

-- игроки
local Players = game:GetService("Players")
-- невидимая коробка над всем лобби
local zone = workspace:WaitForChild("Lobby"):WaitForChild("SafeZone")

-- точка в коробке?
local function Inside(position)
    -- координаты относительно коробки
    local p = zone.CFrame:PointToObjectSpace(position)
    -- внутри по всем трём осям
    return math.abs(p.X) <= zone.Size.X / 2 and math.abs(p.Y) <= zone.Size.Y / 2 and math.abs(p.Z) <= zone.Size.Z / 2
-- конец функции
end

-- постоянно
while true do
    -- 5 раз в секунду
    task.wait(0.2)
    -- каждый игрок
    for _, player in ipairs(Players:GetPlayers()) do
        -- персонаж
        local character = player.Character
        -- Humanoid
        local humanoid = character and character:FindFirstChildOfClass("Humanoid")
        -- центр
        local root = character and character:FindFirstChild("HumanoidRootPart")
        -- жив
        if humanoid and root and humanoid.Health > 0 then
            -- щит
            local shield = character:FindFirstChild("LobbyShield")
            -- в лобби
            if Inside(root.Position) then
                -- здоровье полное
                humanoid.Health = humanoid.MaxHealth
                -- щита нет
                if not shield then
                    -- ForceField: TakeDamage по нему не проходит
                    shield = Instance.new("ForceField")
                    -- имя
                    shield.Name = "LobbyShield"
                    -- без светящегося шара
                    shield.Visible = false
                    -- в персонажа
                    shield.Parent = character
                -- конец проверки
                end
            -- вышел из лобби
            elseif shield then
                -- щит снимаем
                shield:Destroy()
            -- конец проверки
            end
        -- конец проверки
        end
    -- конец перебора
    end
-- конец цикла
end

WeaponEffects

Где: ServerScriptService · ModuleScript
Зачем: Эффекты оружия (раздел «Оружие»).

-- удаление по таймеру
local Debris = game:GetService("Debris")
-- игроки — для ослепления
local Players = game:GetService("Players")
-- норма скорости и прыжка игрока
local StarterPlayer = game:GetService("StarterPlayer")
-- изменения каждой цели; исчезнувшие цели забываются сами
local mods = setmetatable({}, {__mode = "k"})
-- во сколько раз сильнее урон по цели с меткой
local MARK_BONUS = 1.5
-- таблица модуля
local Effects = {}

-- читает настройку; нет такой — значит 0 (эффект выключен)
local function Get(config, name)
    -- ищем контейнер
    local v = config:FindFirstChild(name)
    -- есть — его значение
    if v then return v.Value end
    -- нет — ноль
    return 0
-- конец функции
end

-- норма характеристики: к ней всё возвращается, когда эффекты кончились
local function Base(humanoid, stat)
    -- уже знаем норму этой цели?
    local saved = humanoid:GetAttribute("Base" .. stat)
    -- знаем — её и берём (можно задать вручную атрибутом)
    if saved then return saved end
    -- NPC: его собственное значение (у быстрого врага — своё)
    local value = humanoid[stat]
    -- это игрок?
    local player = Players:GetPlayerFromCharacter(humanoid.Parent)
    -- скорость игрока
    if player and stat == "WalkSpeed" then
        -- норма из StarterPlayer — по умолчанию 16
        value = StarterPlayer.CharacterWalkSpeed
    -- прыжок игрока
    elseif player and stat == "JumpHeight" then
        -- норма из StarterPlayer — по умолчанию 7.2
        value = StarterPlayer.CharacterJumpHeight
    -- конец выбора
    end
    -- запоминаем норму на цели
    humanoid:SetAttribute("Base" .. stat, value)
    -- норма
    return value
-- конец функции
end

-- пересчитывает характеристику: норма + все действующие изменения
local function Recalc(humanoid, stat)
    -- сумма прибавок, произведение множителей и делителей, «установить»
    local add, mul, div, set = 0, 1, 1, nil
    -- текущее время
    local now = os.clock()
    -- все изменения этой цели
    for _, m in pairs(mods[humanoid] or {}) do
        -- та же характеристика и ещё действует
        if m.stat == stat and (m.untilTime == nil or now < m.untilTime) then
            -- прибавить
            if m.op == "add" then add += m.value
            -- умножить
            elseif m.op == "mul" then mul *= m.value
            -- разделить
            elseif m.op == "div" then div *= m.value
            -- установить — важнее всего
            elseif m.op == "set" then set = m.value end
        -- конец проверки
        end
    -- конец перебора
    end
    -- формула: (норма + прибавки) × множители ÷ делители
    local value = (Base(humanoid, stat) + add) * mul / div
    -- «установить» перекрывает всё (заморозка = 0)
    if set then value = set end
    -- не меньше нуля
    value = math.max(value, 0)
    -- прыжок
    if stat == "JumpHeight" then
        -- высота прыжка
        humanoid.JumpHeight = value
        -- та же высота через силу прыжка — работает при любой настройке игры
        humanoid.JumpPower = math.sqrt(2 * workspace.Gravity * value)
    -- скорость или здоровье
    else
        -- записываем
        humanoid[stat] = value
    -- конец выбора
    end
-- конец функции
end

-- изменение: id — имя, stat — WalkSpeed / JumpHeight / MaxHealth, op — add / mul / div / set
function Effects.Modify(humanoid, id, stat, op, value, seconds)
    -- на ноль не делим
    if op == "div" and value == 0 then return end
    -- список изменений цели
    mods[humanoid] = mods[humanoid] or {}
    -- он
    local list = mods[humanoid]
    -- без seconds — действует, пока не уберут
    local untilTime = nil
    -- изменение на время
    if seconds and seconds > 0 then
        -- до какого момента
        untilTime = os.clock() + seconds
        -- такое же изменение уже есть?
        local old = list[id]
        -- и оно кончится позже
        if old and old.untilTime and old.untilTime > untilTime then
            -- не укорачиваем
            untilTime = old.untilTime
        -- конец проверки
        end
        -- время конца — для видимых эффектов
        humanoid:SetAttribute(id .. "Until", untilTime)
        -- когда время выйдет
        task.delay(seconds + 0.05, function()
            -- это изменение
            local m = list[id]
            -- не продлили — кончилось
            if m and m.untilTime and os.clock() >= m.untilTime then
                -- убираем
                list[id] = nil
            -- конец проверки
            end
            -- пересчитываем: без эффектов — снова норма
            Recalc(humanoid, stat)
        -- конец отложенной функции
        end)
    -- конец проверки
    end
    -- записываем изменение (то же имя — заменяет старое)
    list[id] = {stat = stat, op = op, value = value, untilTime = untilTime}
    -- пересчитываем сразу
    Recalc(humanoid, stat)
-- конец функции
end

-- убирает изменение по имени (например, отпустили Shift)
function Effects.Remove(humanoid, id)
    -- изменения цели
    local list = mods[humanoid]
    -- такого нет
    if not list or not list[id] then return end
    -- какую характеристику оно меняло
    local stat = list[id].stat
    -- убираем
    list[id] = nil
    -- пересчитываем
    Recalc(humanoid, stat)
-- конец функции
end

-- видимый эффект, пока действует attr; второй такой же не создаём
local function Show(humanoid, attr, name, make)
    -- уже показан — время уже продлили
    if humanoid.Parent:FindFirstChild(name, true) then return end
    -- создаём эффект и узнаём, куда его положить
    local visual, parent = make()
    -- имя — чтобы найти его в следующий раз
    visual.Name = name
    -- показываем
    visual.Parent = parent
    -- отдельный поток — следит за временем
    task.spawn(function()
        -- пока цель жива и время не вышло
        while humanoid.Parent and humanoid.Health > 0 and os.clock() < (humanoid:GetAttribute(attr) or 0) do
            -- ждём
            task.wait(0.1)
        -- конец цикла
        end
        -- убираем эффект
        visual:Destroy()
    -- конец потока
    end)
-- конец функции
end

-- урон каждую секунду: огонь, яд, кровотечение
local function Dot(humanoid, attr, name, seconds, damage, make, movingBonus)
    -- продлеваем
    humanoid:SetAttribute(attr, math.max(humanoid:GetAttribute(attr) or 0, os.clock() + seconds))
    -- уже действует — хватит продления
    if humanoid.Parent:FindFirstChild(name, true) then return end
    -- видимый эффект
    local visual, parent = make()
    -- имя
    visual.Name = name
    -- показываем
    visual.Parent = parent
    -- центр цели — по нему видно, бежит ли она
    local root = humanoid.Parent:FindFirstChild("HumanoidRootPart")
    -- отдельный поток
    task.spawn(function()
        -- пока цель жива и время не вышло
        while humanoid.Health > 0 and os.clock() < humanoid:GetAttribute(attr) do
            -- раз в секунду
            task.wait(1)
            -- цель бежит?
            local moving = root and root.AssemblyLinearVelocity.Magnitude > 2
            -- кровотечение у бегущей цели
            if movingBonus and moving then
                -- вдвое сильнее
                humanoid:TakeDamage(damage * 2)
            -- иначе
            else
                -- обычный урон
                humanoid:TakeDamage(damage)
            -- конец выбора
            end
        -- конец цикла
        end
        -- эффект закончился
        visual:Destroy()
    -- конец потока
    end)
-- конец функции
end

-- отбрасывает цель от стрелка или от центра взрыва
local function Knockback(root, attacker, origin, power)
    -- центр взрыва, если он есть
    local from = origin
    -- это не взрыв
    if not from then
        -- центр атакующего
        local attackerRoot = attacker and attacker:FindFirstChild("HumanoidRootPart")
        -- не знаем, откуда толкать
        if not attackerRoot then return end
        -- толкаем от атакующего
        from = attackerRoot.Position
    -- конец проверки
    end
    -- направление по земле
    local flat = (root.Position - from) * Vector3.new(1, 0, 1)
    -- стоят в одной точке — направления нет
    if flat.Magnitude < 0.1 then return end
    -- точка приложения
    local attachment = Instance.new("Attachment")
    -- в центр цели
    attachment.Parent = root
    -- толчок; работает и на игроках — их физику считает их компьютер
    local push = Instance.new("LinearVelocity")
    -- куда приложен
    push.Attachment0 = attachment
    -- без ограничения силы
    push.MaxForce = math.huge
    -- назад и немного вверх
    push.VectorVelocity = flat.Unit * power + Vector3.new(0, power * 0.4, 0)
    -- толкаем
    push.Parent = root
    -- 0,2 секунды
    Debris:AddItem(push, 0.2)
    -- убираем и точку
    Debris:AddItem(attachment, 0.2)
-- конец функции
end

-- молния между двумя точками
local function Beam(a, b)
    -- парт-луч
    local beam = Instance.new("Part")
    -- не падает
    beam.Anchored = true
    -- ни с чем не сталкивается
    beam.CanCollide = false
    -- не мешает поиску целей
    beam.CanQuery = false
    -- светится
    beam.Material = Enum.Material.Neon
    -- фиолетовый
    beam.Color = Color3.fromRGB(150, 120, 255)
    -- длина — от точки до точки
    beam.Size = Vector3.new(0.2, 0.2, (b - a).Magnitude)
    -- между точками
    beam.CFrame = CFrame.new(a, b) * CFrame.new(0, 0, -(b - a).Magnitude / 2)
    -- показываем
    beam.Parent = workspace
    -- вспышка 0,2 секунды
    Debris:AddItem(beam, 0.2)
-- конец функции
end

-- цепная молния: перескакивает на ближайших
local function Chain(attacker, first, config)
    -- сколько прыжков
    local count = Get(config, "ChainCount")
    -- 0 — выключена
    if count <= 0 then return end
    -- дальность прыжка
    local range = Get(config, "ChainRange")
    -- не задана — 12
    if range <= 0 then range = 12 end
    -- первую цель уже ударили
    local hit = {[first] = true}
    -- откуда прыгает
    local from = first.Parent:FindFirstChild("HumanoidRootPart")
    -- настройки поиска
    local params = OverlapParams.new()
    -- не замечать из списка
    params.FilterType = Enum.RaycastFilterType.Exclude
    -- атакующего не бьём
    params.FilterDescendantsInstances = {attacker}
    -- прыжок за прыжком
    for i = 1, count do
        -- прыгать неоткуда
        if not from then return end
        -- ближайшая новая цель
        local best, bestRoot, bestDist = nil, nil, range
        -- всё вокруг
        for _, part in pairs(workspace:GetPartBoundsInRadius(from.Position, range, params)) do
            -- модель детали
            local model = part:FindFirstAncestorOfClass("Model")
            -- живая?
            local target = model and model:FindFirstChildOfClass("Humanoid")
            -- её центр
            local targetRoot = model and model:FindFirstChild("HumanoidRootPart")
            -- живая и ещё не ударена
            if target and targetRoot and target.Health > 0 and not hit[target] then
                -- расстояние
                local dist = (targetRoot.Position - from.Position).Magnitude
                -- ближе найденной
                if dist < bestDist then
                    -- запоминаем
                    best, bestRoot, bestDist = target, targetRoot, dist
                -- конец проверки
                end
            -- конец проверки
            end
        -- конец перебора
        end
        -- рядом никого — молния гаснет
        if not best then return end
        -- запоминаем
        hit[best] = true
        -- рисуем молнию
        Beam(from.Position, bestRoot.Position)
        -- урон молнии
        best:TakeDamage(Get(config, "ChainDamage"))
        -- следующий прыжок — отсюда
        from = bestRoot
    -- следующий прыжок
    end
-- конец функции
end

-- белый экран у задетого игрока
local function Blind(humanoid, seconds)
    -- это игрок?
    local player = Players:GetPlayerFromCharacter(humanoid.Parent)
    -- NPC не ослепить — у него нет экрана
    if not player then return end
    -- продлеваем
    humanoid:SetAttribute("BlindUntil", math.max(humanoid:GetAttribute("BlindUntil") or 0, os.clock() + seconds))
    -- экран игрока
    local playerGui = player:FindFirstChild("PlayerGui")
    -- уже ослеплён — хватит продления
    if not playerGui or playerGui:FindFirstChild("BlindEffect") then return end
    -- слой поверх экрана
    local gui = Instance.new("ScreenGui")
    -- имя
    gui.Name = "BlindEffect"
    -- на весь экран, включая верхнюю панель
    gui.IgnoreGuiInset = true
    -- белая заливка
    local flash = Instance.new("Frame")
    -- во весь экран
    flash.Size = UDim2.fromScale(1, 1)
    -- белая
    flash.BackgroundColor3 = Color3.new(1, 1, 1)
    -- почти непрозрачная
    flash.BackgroundTransparency = 0.05
    -- в слой
    flash.Parent = gui
    -- на экран игрока
    gui.Parent = playerGui
    -- следим за временем
    task.spawn(function()
        -- пока не вышло
        while gui.Parent and os.clock() < humanoid:GetAttribute("BlindUntil") do
            -- ждём
            task.wait(0.1)
        -- конец цикла
        end
        -- снова видно
        gui:Destroy()
    -- конец потока
    end)
-- конец функции
end

-- попадание: урон и все эффекты из Configuration
function Effects.Hit(attacker, humanoid, config, origin)
    -- цель уже мертва
    if humanoid.Health <= 0 then return end
    -- модель цели
    local character = humanoid.Parent
    -- её центр
    local root = character:FindFirstChild("HumanoidRootPart")
    -- урон
    local damage = Get(config, "Damage")
    -- на цели метка
    if os.clock() < (humanoid:GetAttribute("MarkedUntil") or 0) then
        -- урон ×1,5
        damage = damage * MARK_BONUS
    -- конец проверки
    end
    -- наносим урон
    humanoid:TakeDamage(damage)

    -- замедление
    if Get(config, "SlowTime") > 0 then
        -- скорость × SlowFactor на SlowTime секунд
        Effects.Modify(humanoid, "Slow", "WalkSpeed", "mul", Get(config, "SlowFactor"), Get(config, "SlowTime"))
    -- конец
    end
    -- заморозка
    if Get(config, "FreezeTime") > 0 then
        -- стоит
        Effects.Modify(humanoid, "Freeze", "WalkSpeed", "set", 0, Get(config, "FreezeTime"))
        -- не прыгает
        Effects.Modify(humanoid, "FreezeJump", "JumpHeight", "set", 0, Get(config, "FreezeTime"))
        -- ледяная подсветка
        Show(humanoid, "FreezeUntil", "FreezeEffect", function()
            -- подсветка всей модели
            local ice = Instance.new("Highlight")
            -- голубой лёд
            ice.FillColor = Color3.fromRGB(150, 220, 255)
            -- плотный
            ice.FillTransparency = 0.3
            -- белый контур
            ice.OutlineColor = Color3.new(1, 1, 1)
            -- на модель цели
            return ice, character
        -- конец функции
        end)
    -- конец
    end
    -- огонь, яд или ничего
    local effect = config:FindFirstChild("Effect") and config.Effect.Value or "None"
    -- огонь или яд
    if root and (effect == "Fire" or effect == "Poison") and Get(config, "EffectTime") > 0 then
        -- урон каждую секунду
        Dot(humanoid, "EffectUntil", "WeaponEffect", Get(config, "EffectTime"), Get(config, "EffectDamage"), function()
            -- огонь
            if effect == "Fire" then
                -- пламя на цели
                return Instance.new("Fire"), root
            -- конец
            end
            -- яд — облако
            local smoke = Instance.new("Smoke")
            -- ядовито-зелёное
            smoke.Color = Color3.fromRGB(90, 220, 60)
            -- полупрозрачное
            smoke.Opacity = 0.5
            -- на цели
            return smoke, root
        -- бонуса за бег нет
        end, false)
    -- конец
    end
    -- кровотечение
    if root and Get(config, "BleedTime") > 0 then
        -- урон каждую секунду
        Dot(humanoid, "BleedUntil", "BleedEffect", Get(config, "BleedTime"), Get(config, "BleedDamage"), function()
            -- капли
            local drops = Instance.new("ParticleEmitter")
            -- тёмно-красные
            drops.Color = ColorSequence.new(Color3.fromRGB(170, 0, 0))
            -- мелкие
            drops.Size = NumberSequence.new(0.2)
            -- живут полсекунды
            drops.Lifetime = NumberRange.new(0.5)
            -- 15 капель в секунду
            drops.Rate = 15
            -- падают вниз
            drops.Acceleration = Vector3.new(0, -30, 0)
            -- из центра цели
            return drops, root
        -- у бегущей цели — вдвое сильнее
        end, true)
    -- конец
    end
    -- отбрасывание
    if root and Get(config, "Knockback") > 0 then
        -- толкаем
        Knockback(root, attacker, origin, Get(config, "Knockback"))
    -- конец
    end
    -- вампиризм: доля урона в лечение
    local steal = Get(config, "Lifesteal")
    -- Humanoid атакующего
    local attackerHumanoid = attacker and attacker:FindFirstChildOfClass("Humanoid")
    -- вампиризм включён, атакующий жив
    if steal > 0 and attackerHumanoid and attackerHumanoid.Health > 0 then
        -- лечим, но не выше максимума
        attackerHumanoid.Health = math.min(attackerHumanoid.MaxHealth, attackerHumanoid.Health + damage * steal)
    -- конец
    end
    -- цепная молния, если ChainCount > 0
    Chain(attacker, humanoid, config)
    -- ослепление
    if Get(config, "BlindTime") > 0 then
        -- белый экран у игрока
        Blind(humanoid, Get(config, "BlindTime"))
    -- конец
    end
    -- метка
    if Get(config, "MarkTime") > 0 then
        -- продлеваем
        humanoid:SetAttribute("MarkedUntil", math.max(humanoid:GetAttribute("MarkedUntil") or 0, os.clock() + Get(config, "MarkTime")))
        -- красный контур
        Show(humanoid, "MarkedUntil", "MarkEffect", function()
            -- подсветка
            local mark = Instance.new("Highlight")
            -- без заливки
            mark.FillTransparency = 1
            -- красный контур — видно сквозь стены
            mark.OutlineColor = Color3.fromRGB(255, 40, 40)
            -- на модель цели
            return mark, character
        -- конец функции
        end)
    -- конец
    end
-- конец функции
end

-- усиление: kind = "Speed" (скорость) или "Jump" (прыжок)
function Effects.Boost(humanoid, kind, factor, seconds)
    -- мёртвым не помогает
    if humanoid.Health <= 0 then return end
    -- скорость
    local stat = "WalkSpeed"
    -- или прыжок
    if kind == "Jump" then stat = "JumpHeight" end
    -- × factor на seconds секунд; повторный приём продлевает
    Effects.Modify(humanoid, "Boost" .. kind, stat, "mul", factor, seconds)
    -- искры, пока действует
    Show(humanoid, "Boost" .. kind .. "Until", "Boost" .. kind .. "Effect", function()
        -- искры
        local sparkles = Instance.new("Sparkles")
        -- прыжок
        if kind == "Jump" then
            -- голубые
            sparkles.SparkleColor = Color3.fromRGB(80, 200, 255)
        -- скорость
        else
            -- жёлтые
            sparkles.SparkleColor = Color3.fromRGB(255, 220, 60)
        -- конец выбора
        end
        -- на центр персонажа
        return sparkles, humanoid.Parent:FindFirstChild("HumanoidRootPart")
    -- конец функции
    end)
-- конец функции
end

-- лечение: сразу amount, потом regen в секунду seconds секунд
function Effects.Heal(humanoid, amount, regen, seconds)
    -- мёртвых не лечим
    if humanoid.Health <= 0 then return end
    -- сразу, не выше максимума
    humanoid.Health = math.min(humanoid.MaxHealth, humanoid.Health + amount)
    -- восстановление выключено
    if regen <= 0 or seconds <= 0 then return end
    -- сколько в секунду
    humanoid:SetAttribute("RegenRate", regen)
    -- до какого времени
    humanoid:SetAttribute("RegenUntil", math.max(humanoid:GetAttribute("RegenUntil") or 0, os.clock() + seconds))
    -- уже восстанавливается — хватит продления
    if humanoid.Parent:FindFirstChild("RegenEffect", true) then return end
    -- зелёные искры, пока лечит
    Show(humanoid, "RegenUntil", "RegenEffect", function()
        -- искры
        local sparkles = Instance.new("Sparkles")
        -- зелёные
        sparkles.SparkleColor = Color3.fromRGB(80, 255, 120)
        -- на центр персонажа
        return sparkles, humanoid.Parent:FindFirstChild("HumanoidRootPart")
    -- конец функции
    end)
    -- отдельный поток
    task.spawn(function()
        -- пока жив и время не вышло
        while humanoid.Health > 0 and os.clock() < humanoid:GetAttribute("RegenUntil") do
            -- раз в секунду
            task.wait(1)
            -- прибавляем здоровье
            humanoid.Health = math.min(humanoid.MaxHealth, humanoid.Health + humanoid:GetAttribute("RegenRate"))
        -- конец цикла
        end
    -- конец потока
    end)
-- конец функции
end

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

GateScript

Где: Workspace → Lobby → Gate · Script
Зачем: Лобби.

-- игроки
local Players = game:GetService("Players")
-- события
local RS = game:GetService("ReplicatedStorage")
-- сигнал начала раунда
local SS = game:GetService("ServerStorage")
-- модель Gate
local gate = script.Parent
-- модель Lobby
local lobby = gate.Parent
-- светящаяся рамка — войти в неё
local gatePart = gate:WaitForChild("Gate")
-- табло над воротами
local board = gate:WaitForChild("GuiPart"):WaitForChild("Board"):WaitForChild("Frame")
-- площадка готовых
local readyPart = lobby:WaitForChild("ReadyZone"):WaitForChild("TeleportPart")
-- куда вернуть, если передумал
local leavePart = lobby:WaitForChild("LeaveRoomPart")
-- кнопка «Выйти»
local leaveEvent = RS:WaitForChild("LeaveEvent")
-- затемнение экрана
local teleportingEvent = RS:WaitForChild("TeleportingEvent")
-- BindableEvent: RoundServer начинает раунд
local roundStart = SS:WaitForChild("RoundStart")
-- мест в раунде
local MAX_PLAYERS = 30
-- для проверки одному; в настоящей игре поставь 2 и больше
local MIN_PLAYERS = 1
-- секунд отсчёта
local WAIT_TIME = 30
-- игрок → true: кто вошёл в ворота
local ready = {}
-- идёт отправка — ворота закрыты
local sending = false

-- сколько готовых
local function Count()
    -- счётчик
    local n = 0
    -- каждый готовый
    for player in pairs(ready) do
        -- вышел из игры — убираем
        if player.Parent then n += 1 else ready[player] = nil end
    -- конец перебора
    end
    -- число
    return n
-- конец функции
end
-- обновить табло
local function UpdateBoard(status, seconds)
    -- готовые
    board.Players.Text = "Игроков: " .. Count() .. "/" .. MAX_PLAYERS
    -- состояние
    board.Status.Text = status
    -- отсчёт
    board.Time.Text = seconds and (seconds .. " с") or ""
-- конец функции
end

-- кто-то вошёл в ворота
gatePart.Touched:Connect(function(hit)
    -- модель персонажа
    local character = hit.Parent
    -- игрок (а не NPC или предмет)
    local player = Players:GetPlayerFromCharacter(character)
    -- Humanoid
    local humanoid = character:FindFirstChildOfClass("Humanoid")
    -- уже готов, отправка или погиб
    if not player or ready[player] or sending or not humanoid or humanoid.Health <= 0 then return end
    -- раунд уже идёт
    if RS:GetAttribute("Phase") ~= "Lobby" then return end
    -- мест нет
    if Count() >= MAX_PLAYERS then return end
    -- готов
    ready[player] = true
    -- на площадку готовых
    character:PivotTo(readyPart.CFrame + Vector3.new(math.random(-6, 6), 3, math.random(-6, 6)))
    -- показать кнопку «Выйти»
    leaveEvent:FireClient(player, true)
-- конец функции
end)
-- нажал «Выйти»
leaveEvent.OnServerEvent:Connect(function(player)
    -- не в списке или уже отправляем
    if not ready[player] or sending then return end
    -- не готов
    ready[player] = nil
    -- обратно в лобби
    if player.Character then player.Character:PivotTo(leavePart.CFrame + Vector3.new(0, 3, 0)) end
    -- спрятать кнопку
    leaveEvent:FireClient(player, false)
-- конец функции
end)
-- игрок зашёл
local function Setup(player)
    -- персонаж пропал — не готов
    player.CharacterRemoving:Connect(function() ready[player] = nil end)
-- конец функции
end
-- новые игроки
Players.PlayerAdded:Connect(Setup)
-- кто зашёл раньше скрипта
for _, player in ipairs(Players:GetPlayers()) do Setup(player) end

-- бесконечно
while true do
    -- идёт раунд
    while RS:GetAttribute("Phase") ~= "Lobby" do
        -- табло
        UpdateBoard("ИДЁТ РАУНД")
        -- ждём
        task.wait(1)
    -- конец ожидания
    end
    -- отсчёт с начала
    local left = WAIT_TIME
    -- пока не 0
    while left > 0 do
        -- мало игроков
        if Count() < MIN_PLAYERS then
            -- отсчёт заново
            left = WAIT_TIME
            -- табло
            UpdateBoard("ЖДЁМ ИГРОКОВ")
        -- хватает
        else
            -- табло с секундами
            UpdateBoard("ГОТОВЬСЯ", left)
            -- минус секунда
            left -= 1
        -- конец проверки
        end
        -- секунда
        task.wait(1)
    -- конец отсчёта
    end
    -- ворота закрыты
    sending = true
    -- кого отправляем
    local list = {}
    -- готовые
    for player in pairs(ready) do
        -- ещё в игре
        if player.Parent then
            -- в список
            table.insert(list, player)
            -- кнопка «Выйти» больше не нужна
            leaveEvent:FireClient(player, false)
            -- экран темнеет
            teleportingEvent:FireClient(player)
        -- конец проверки
        end
    -- конец перебора
    end
    -- список пуст
    table.clear(ready)
    -- табло
    UpdateBoard("ОТПРАВКА")
    -- экран успел потемнеть
    task.wait(2)
    -- RoundServer сажает их в самолёт
    roundStart:Fire(list)
    -- раунд успел начаться
    task.wait(1)
    -- ворота снова открыты
    sending = false
-- конец цикла
end

HelicopterServer

Где: Workspace → Helicopter · Script
Зачем: Вертолёт.

-- игроки
local Players = game:GetService("Players")
-- модель Helicopter
local heli = script.Parent
-- кресло пилота
local seat = heli:WaitForChild("VehicleSeat")
-- моторы лопастей
local blades = {heli:WaitForChild("MainBlades"):WaitForChild("HingeConstraint"), heli:WaitForChild("BackBlades"):WaitForChild("HingeConstraint")}
-- скорость вращения лопастей
local BLADE_SPEED = 25

-- точка, к которой приложены силы
local attachment = Instance.new("Attachment")
-- имя
attachment.Name = "FlyAttachment"
-- в кресло
attachment.Parent = seat
-- держит скорость вертолёта
local move = Instance.new("LinearVelocity")
-- имя — его ищет HelicopterLocal
move.Name = "Move"
-- за кресло
move.Attachment0 = attachment
-- сильнее гравитации и веса
move.MaxForce = 10000000
-- висит на месте
move.VectorVelocity = Vector3.zero
-- пока пилота нет — выключено
move.Enabled = false
-- в кресло
move.Parent = seat
-- держит поворот: не кувыркается
local turn = Instance.new("AlignOrientation")
-- имя — его ищет HelicopterLocal
turn.Name = "Turn"
-- к заданному направлению
turn.Mode = Enum.OrientationAlignmentMode.OneAttachment
-- за кресло
turn.Attachment0 = attachment
-- сильно
turn.MaxTorque = 10000000
-- поворачивает плавно
turn.Responsiveness = 15
-- пока пилота нет — выключено
turn.Enabled = false
-- в кресло
turn.Parent = seat

-- сел или встал
seat:GetPropertyChangedSignal("Occupant"):Connect(function()
    -- кто сидит
    local humanoid = seat.Occupant
    -- игрок
    local player = humanoid and Players:GetPlayerFromCharacter(humanoid.Parent)
    -- пилот сел
    if player then
        -- куда смотрит вертолёт
        local _, yaw = seat.CFrame:ToOrientation()
        -- держим ровно в эту сторону
        turn.CFrame = CFrame.Angles(0, yaw, 0)
        -- висим
        move.VectorVelocity = Vector3.zero
        -- силы включены
        move.Enabled = true
        -- поворот включён
        turn.Enabled = true
        -- лопасти крутятся
        for _, hinge in ipairs(blades) do hinge.AngularVelocity = BLADE_SPEED end
        -- физику вертолёта считает компьютер пилота — без задержек
        seat:SetNetworkOwner(player)
    -- пилот вышел
    else
        -- силы выключены — вертолёт падает и садится
        move.Enabled = false
        -- поворот выключен
        turn.Enabled = false
        -- лопасти останавливаются
        for _, hinge in ipairs(blades) do hinge.AngularVelocity = 0 end
        -- физику снова считает сервер
        seat:SetNetworkOwner(nil)
    -- конец проверки
    end
-- конец функции
end)

ZipScript

Где: Workspace → Zip → Start → ProximityPrompt · Script
Зачем: Зиплайн.

-- каждый кадр
local RunService = game:GetService("RunService")
-- ProximityPrompt «Спуститься»
local prompt = script.Parent
-- верхняя точка троса
local zipStart = prompt.Parent
-- нижняя точка
local zipEnd = zipStart.Parent:WaitForChild("End")
-- скорость спуска, studs/с
local SPEED = 40
-- персонаж висит под тросом
local OFFSET = Vector3.new(0, -3.5, 0)
-- игрок → true: кто едет
local riding = {}

-- нажал E
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 riding[player] or not humanoid or not root or humanoid.Health <= 0 then return end
    -- едет (другие тоже могут — кнопка не блокируется)
    riding[player] = true
    -- откуда
    local from = zipStart.Position + OFFSET
    -- куда
    local to = zipEnd.Position + OFFSET
    -- смотрит вдоль троса, а не боком
    local facing = CFrame.lookAt(from, Vector3.new(to.X, from.Y, to.Z)).Rotation
    -- время спуска — одинаковая скорость на любом тросе
    local duration = (to - from).Magnitude / SPEED
    -- едет по тросу, а не падает
    root.Anchored = true
    -- сколько едем
    local t = 0
    -- пока не доехали
    while t < duration and root.Parent and humanoid.Health > 0 do
        -- прошёл кадр
        t += RunService.Heartbeat:Wait()
        -- точка на тросе — от начала к концу
        root.CFrame = facing + from:Lerp(to, math.min(t / duration, 1))
    -- конец спуска
    end
    -- отпускаем
    if root.Parent then root.Anchored = false end
    -- приехал
    riding[player] = nil
-- конец функции
end)

MedkitScript

Где: ServerStorage → Loot → Medkit · Script
Зачем: Аптечка.

-- аптечка
local tool = script.Parent
-- звук
local healSound = tool:WaitForChild("Handle"):WaitForChild("Heal")
-- сколько лечит
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
    -- звук
    healSound:Play()
    -- лечим, не выше максимума
    humanoid.Health = math.min(humanoid.Health + HEAL, humanoid.MaxHealth)
    -- звук доиграет
    task.wait(0.5)
    -- аптечки больше нет
    tool:Destroy()
-- конец функции
end)

GrenadeLocal

Где: ServerStorage → Loot → Flashbang / SmokeGrenade · LocalScript
Зачем: Бросок.

-- игроки
local Players = game:GetService("Players")
-- граната
local tool = script.Parent
-- просим сервер бросить
local throwEvent = tool:WaitForChild("Throw")
-- бросок; ключевой кадр «Release» — момент, когда граната вылетает
local anim = tool:WaitForChild("Animation")
-- куда целимся
local mouse = Players.LocalPlayer:GetMouse()
-- бросок уже начат
local busy = false

-- граната вылетает
local function Release()
    -- точка под курсором
    throwEvent:FireServer(mouse.Hit.Position)
-- конец функции
end
-- клик
tool.Activated:Connect(function()
    -- кто держит
    local humanoid = tool.Parent:FindFirstChildOfClass("Humanoid")
    -- уже бросает или погиб
    if busy or not humanoid or humanoid.Health <= 0 then return end
    -- одна граната — один бросок
    busy = true
    -- анимации нет — бросаем сразу
    if anim.AnimationId == "" then Release() return end
    -- загружаем бросок
    local track = humanoid:WaitForChild("Animator"):LoadAnimation(anim)
    -- граната ещё в руке
    local released = false
    -- анимация дошла до ключевого кадра
    track.KeyframeReached:Connect(function(name)
        -- кадр «Release» — бросок
        if name == "Release" and not released then released = true Release() end
    -- конец функции
    end)
    -- анимация кончилась
    track.Stopped:Once(function()
        -- кадра «Release» нет — бросаем в конце
        if not released then released = true Release() end
    -- конец функции
    end)
    -- замах
    track:Play()
-- конец функции
end)

FlashServer

Где: ServerStorage → Loot → Flashbang · Script
Зачем: Вспышка.

-- игроки
local Players = game:GetService("Players")
-- световая граната
local tool = script.Parent
-- сама граната
local handle = tool:WaitForChild("Handle")
-- клиент бросил
local throwEvent = tool:WaitForChild("Throw")
-- ослепить игрока (ловит FlashEffect у каждого)
local flashEvent = game:GetService("ReplicatedStorage"):WaitForChild("FlashEvent")
-- дальше вспышка не слепит
local RADIUS = 40
-- секунд до взрыва
local FUSE = 2
-- сила броска
local POWER = 70
-- уже брошена
local thrown = false

-- клиент прислал точку
throwEvent.OnServerEvent:Connect(function(player, target)
    -- кто держит
    local character = tool.Parent
    -- уже, чужая граната или мусор
    if thrown or Players:GetPlayerFromCharacter(character) ~= player or typeof(target) ~= "Vector3" then return end
    -- центр
    local root = character:FindFirstChild("HumanoidRootPart")
    -- персонажа нет
    if not root then return end
    -- брошена
    thrown = true
    -- летящая копия
    local grenade = handle:Clone()
    -- имя
    grenade.Name = "Thrown" .. tool.Name
    -- отскакивает от стен и пола
    grenade.CanCollide = true
    -- у неё есть вес
    grenade.Massless = false
    -- перед лицом
    grenade.CFrame = root.CFrame * CFrame.new(0, 2, -2)
    -- в мир
    grenade.Parent = workspace
    -- к точке
    local direction = (target - grenade.Position).Unit
    -- бросок чуть вверх — по дуге
    grenade.AssemblyLinearVelocity = (direction + Vector3.new(0, 0.35, 0)).Unit * POWER
    -- полёт считает сервер — у всех одинаково
    grenade:SetNetworkOwner(nil)
    -- звук броска
    grenade.Throw:Play()
    -- в руке гранаты больше нет
    handle.Transparency = 1
    -- её нельзя подобрать
    handle.CanTouch = false
    -- не падает сквозь карту
    handle.Anchored = true
    -- инструмент из рук убираем; скрипт продолжает работать (если удалить — остановится)
    tool.Parent = workspace
    -- фитиль
    task.wait(FUSE)
    -- взрыв
    grenade.Boom:Play()
    -- проверка прямой видимости
    local params = RaycastParams.new()
    -- исключаем…
    params.FilterType = Enum.RaycastFilterType.Exclude
    -- …саму гранату
    params.FilterDescendantsInstances = {grenade}
    -- все игроки
    for _, other in ipairs(Players:GetPlayers()) do
        -- голова
        local head = other.Character and other.Character:FindFirstChild("Head")
        -- близко (в методичке вычитали длины векторов)
        if head and (head.Position - grenade.Position).Magnitude <= RADIUS then
            -- луч от гранаты к голове
            local result = workspace:Raycast(grenade.Position, head.Position - grenade.Position, params)
            -- стены между ними нет
            if not result or result.Instance:IsDescendantOf(other.Character) then
                -- ослепить
                flashEvent:FireClient(other, grenade.Position)
            -- конец проверки
            end
        -- конец проверки
        end
    -- конец перебора
    end
    -- звук доиграет
    task.wait(1)
    -- гранаты больше нет
    grenade:Destroy()
    -- и инструмента
    tool:Destroy()
-- конец функции
end)

SmokeServer

Где: ServerStorage → Loot → SmokeGrenade · Script
Зачем: Дым.

-- игроки
local Players = game:GetService("Players")
-- дымовая граната
local tool = script.Parent
-- сама граната
local handle = tool:WaitForChild("Handle")
-- клиент бросил
local throwEvent = tool:WaitForChild("Throw")
-- сколько секунд держится дым
local SMOKE_TIME = 20
-- секунд до взрыва
local FUSE = 2
-- сила броска
local POWER = 70
-- уже брошена
local thrown = false

-- клиент прислал точку
throwEvent.OnServerEvent:Connect(function(player, target)
    -- кто держит
    local character = tool.Parent
    -- уже, чужая граната или мусор
    if thrown or Players:GetPlayerFromCharacter(character) ~= player or typeof(target) ~= "Vector3" then return end
    -- центр
    local root = character:FindFirstChild("HumanoidRootPart")
    -- персонажа нет
    if not root then return end
    -- брошена
    thrown = true
    -- летящая копия
    local grenade = handle:Clone()
    -- имя
    grenade.Name = "Thrown" .. tool.Name
    -- отскакивает от стен и пола
    grenade.CanCollide = true
    -- у неё есть вес
    grenade.Massless = false
    -- перед лицом
    grenade.CFrame = root.CFrame * CFrame.new(0, 2, -2)
    -- в мир
    grenade.Parent = workspace
    -- к точке
    local direction = (target - grenade.Position).Unit
    -- бросок чуть вверх — по дуге
    grenade.AssemblyLinearVelocity = (direction + Vector3.new(0, 0.35, 0)).Unit * POWER
    -- полёт считает сервер — у всех одинаково
    grenade:SetNetworkOwner(nil)
    -- звук броска
    grenade.Throw:Play()
    -- в руке гранаты больше нет
    handle.Transparency = 1
    -- её нельзя подобрать
    handle.CanTouch = false
    -- не падает сквозь карту
    handle.Anchored = true
    -- инструмент из рук убираем; скрипт продолжает работать (если удалить — остановится)
    tool.Parent = workspace
    -- фитиль
    task.wait(FUSE)
    -- взрыв
    grenade.Boom:Play()
    -- граната лежит на месте
    grenade.Anchored = true
    -- эффекты дыма
    for _, smoke in ipairs(grenade:GetChildren()) do
        -- включаем
        if smoke:IsA("Smoke") then smoke.Enabled = true end
    -- конец перебора
    end
    -- дым держится
    task.wait(SMOKE_TIME)
    -- гранаты больше нет
    grenade:Destroy()
    -- и инструмента
    tool:Destroy()
-- конец функции
end)

HelicopterLocal

Где: StarterPlayer → StarterCharacterScripts · LocalScript
Зачем: Управление вертолётом.

-- привязка клавиш к действиям
local CAS = game:GetService("ContextActionService")
-- каждый кадр
local RunService = game:GetService("RunService")
-- игроки
local Players = game:GetService("Players")
-- этот игрок
local player = Players.LocalPlayer
-- Humanoid персонажа
local humanoid = script.Parent:WaitForChild("Humanoid")
-- скорость вперёд-назад
local SPEED = 45
-- скорость вверх-вниз
local UP_SPEED = 18
-- поворот, радиан в секунду
local TURN_SPEED = 1.5
-- действие → клавиша
local ACTIONS = {
    -- вверх, вниз
    HeliUp = Enum.KeyCode.E, HeliDown = Enum.KeyCode.Q,
    -- вперёд, назад
    HeliForward = Enum.KeyCode.W, HeliBack = Enum.KeyCode.S,
    -- поворот
    HeliLeft = Enum.KeyCode.A, HeliRight = Enum.KeyCode.D,
-- конец таблицы
}
-- какие действия сейчас зажаты
local held = {}
-- кресло вертолёта, если сидим
local seat = nil
-- куда повёрнут вертолёт
local yaw = 0
-- дальность камеры до посадки
local oldZoom = player.CameraMaxZoomDistance

-- нажали или отпустили клавишу действия
local function OnAction(name, state)
    -- зажата?
    held[name] = state == Enum.UserInputState.Begin
    -- клавиша больше никуда не идёт
    return Enum.ContextActionResult.Sink
-- конец функции
end
-- включить или выключить управление
local function Bind(on)
    -- каждое действие
    for name, key in pairs(ACTIONS) do
        -- привязать / отвязать (false — без кнопки на телефоне)
        if on then CAS:BindAction(name, OnAction, false, key) else CAS:UnbindAction(name) end
    -- конец перебора
    end
    -- ничего не зажато
    table.clear(held)
-- конец функции
end
-- 1, -1 или 0 по двум клавишам
local function Axis(plus, minus)
    -- обе зажаты — 0
    return (held[plus] and 1 or 0) - (held[minus] and 1 or 0)
-- конец функции
end

-- сел или встал
humanoid.Seated:Connect(function(active, seatPart)
    -- сел в вертолёт
    if active and seatPart and seatPart:IsA("VehicleSeat") and seatPart.Parent.Name == "Helicopter" then
        -- запоминаем
        seat = seatPart
        -- куда смотрит
        local _, y = seat.CFrame:ToOrientation()
        -- начальный поворот
        yaw = y
        -- запоминаем камеру
        oldZoom = player.CameraMaxZoomDistance
        -- камеру можно отдалить — видно вертолёт
        player.CameraMaxZoomDistance = 60
        -- управление
        Bind(true)
    -- вышел (пробел) или погиб
    elseif seat then
        -- не в вертолёте
        seat = nil
        -- камера как была
        player.CameraMaxZoomDistance = oldZoom
        -- клавиши свободны
        Bind(false)
    -- конец проверки
    end
-- конец функции
end)

-- каждый кадр
RunService.RenderStepped:Connect(function(dt)
    -- не летим
    if not seat then return end
    -- скорость (создаёт HelicopterServer)
    local move = seat:FindFirstChild("Move")
    -- поворот
    local turn = seat:FindFirstChild("Turn")
    -- вертолёт без скрипта
    if not move or not turn then return end
    -- D — вправо, A — влево
    yaw -= Axis("HeliRight", "HeliLeft") * TURN_SPEED * dt
    -- направление вертолёта
    local facing = CFrame.Angles(0, yaw, 0)
    -- поворачиваем
    turn.CFrame = facing
    -- вперёд-назад и вверх-вниз
    move.VectorVelocity = facing.LookVector * Axis("HeliForward", "HeliBack") * SPEED + Vector3.new(0, Axis("HeliUp", "HeliDown") * UP_SPEED, 0)
-- конец функции
end)

PlaneLocal

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

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- каждый кадр
local RunService = game:GetService("RunService")
-- прыжок
local UIS = game:GetService("UserInputService")
-- игроки
local Players = game:GetService("Players")
-- сервер: «ты в самолёте» / «ты прыгнул»
local planeEvent = RS:WaitForChild("PlaneEvent")
-- камера
local camera = workspace.CurrentCamera
-- уже в самолёте?
local onPlane = Players.LocalPlayer:GetAttribute("OnPlane") == true

-- сервер сообщил
planeEvent.OnClientEvent:Connect(function(value)
    -- запоминаем
    onPlane = value
    -- прыгнул — камера за персонажем
    if not value then camera.CameraType = Enum.CameraType.Custom end
-- конец функции
end)
-- каждый кадр
RunService.RenderStepped:Connect(function()
    -- самолёт
    local plane = workspace:FindFirstChild("Plane")
    -- не летим
    if not onPlane or not plane then return end
    -- камерой управляет скрипт
    camera.CameraType = Enum.CameraType.Scriptable
    -- где самолёт
    local cf = plane:GetPivot()
    -- сзади-сверху смотрим на самолёт
    camera.CFrame = CFrame.lookAt(cf.Position - cf.LookVector * 120 + Vector3.new(0, 50, 0), cf.Position)
-- конец функции
end)
-- пробел (или кнопка прыжка на телефоне)
UIS.JumpRequest:Connect(function()
    -- просим выпрыгнуть — сервер проверит, что над островом
    if onPlane then planeEvent:FireServer() end
-- конец функции
end)

ParachuteLocal

Где: StarterPlayer → StarterCharacterScripts · LocalScript
Зачем: Падение и парашют.

-- клавиатура
local UIS = game:GetService("UserInputService")
-- события
local RS = game:GetService("ReplicatedStorage")
-- каждый кадр
local RunService = game:GetService("RunService")
-- персонаж
local character = script.Parent
-- Humanoid
local humanoid = character:WaitForChild("Humanoid")
-- центр
local root = character:WaitForChild("HumanoidRootPart")
-- просим сервер: следы, парашют, приземлились
local event = RS:WaitForChild("ParachuteEvent")
-- поза падения (ID можно не вставлять)
local anim = script:WaitForChild("Animation")
-- загружаем, если ID есть
local track = anim.AnimationId ~= "" and humanoid:WaitForChild("Animator"):LoadAnimation(anim) or nil
-- ниже этой высоты парашют раскроется сам
local AUTO_HEIGHT = 60
-- ниже — открывать поздно
local MIN_HEIGHT = 20
-- когда начали падать
local fallStart = nil
-- парашют уже открыт
local opened = false
-- следы уже включены
local trails = false
-- настройки луча
local params = RaycastParams.new()
-- исключаем…
params.FilterType = Enum.RaycastFilterType.Exclude
-- …самого себя
params.FilterDescendantsInstances = {character}

-- высота над землёй
local function Height()
    -- луч строго вниз (в методичке летел не туда)
    local result = workspace:Raycast(root.Position, Vector3.new(0, -2000, 0), params)
    -- до земли или «очень высоко»
    return result and root.Position.Y - result.Position.Y or math.huge
-- конец функции
end
-- раскрыть парашют
local function Open()
    -- уже
    if opened then return end
    -- раскрыт
    opened = true
    -- сервер вешает парашют и замедляет падение
    event:FireServer("Open")
-- конец функции
end

-- состояние персонажа сменилось
humanoid.StateChanged:Connect(function(_, new)
    -- начал падать
    if new == Enum.HumanoidStateType.Freefall then
        -- запоминаем время
        fallStart = os.clock()
    -- падение закончилось
    elseif fallStart then
        -- не падаем
        fallStart = nil
        -- поза падения выключается
        if track then track:Stop() end
        -- убрать парашют и следы
        if opened or trails then event:FireServer("Landed") end
        -- к следующему падению всё сначала
        opened, trails = false, false
    -- конец проверки
    end
-- конец функции
end)
-- нажали клавишу
UIS.InputBegan:Connect(function(input, processed)
    -- пробел во время падения
    if processed or input.KeyCode ~= Enum.KeyCode.Space or not fallStart then return end
    -- падаем давно и ещё высоко
    if os.clock() - fallStart > 0.7 and Height() > MIN_HEIGHT then Open() end
-- конец функции
end)
-- каждый кадр
RunService.Heartbeat:Connect(function()
    -- обычный прыжок короче секунды — не трогаем
    if not fallStart or os.clock() - fallStart < 1 then return end
    -- падаем долго — значит с высоты
    if not trails then
        -- один раз
        trails = true
        -- поза падения
        if track then track:Play() end
        -- сервер добавит следы-Trail на руки и ноги
        event:FireServer("Fall")
    -- конец проверки
    end
    -- забыл раскрыть — раскроется сам
    if not opened and Height() < AUTO_HEIGHT then Open() end
-- конец функции
end)

ScrollSwitch

Где: StarterPlayer → StarterCharacterScripts · LocalScript
Зачем: Колесо мыши.

-- игроки
local Players = game:GetService("Players")
-- этот игрок
local player = Players.LocalPlayer
-- персонаж
local character = script.Parent
-- Humanoid
local humanoid = character:WaitForChild("Humanoid")
-- рюкзак
local backpack = player:WaitForChild("Backpack")
-- колёсико мыши
local mouse = player:GetMouse()
-- звук переключения (Sound внутри скрипта — по желанию)
local click = script:FindFirstChild("Click")
-- колесо теперь переключает оружие…
player.CameraMinZoomDistance = 12
-- …поэтому камеру фиксируем на одном расстоянии
player.CameraMaxZoomDistance = 12

-- инструменты по порядку — каждый раз заново (в методичке список был один при входе)
local function Tools()
    -- список
    local list = {}
    -- рюкзак
    for _, tool in ipairs(backpack:GetChildren()) do
        -- только инструменты
        if tool:IsA("Tool") then table.insert(list, tool) end
    -- конец перебора
    end
    -- что в руках
    local equipped = character:FindFirstChildOfClass("Tool")
    -- тоже в списке
    if equipped then table.insert(list, equipped) end
    -- по имени — порядок не прыгает
    table.sort(list, function(a, b) return a.Name < b.Name end)
    -- список и что в руках
    return list, equipped
-- конец функции
end
-- следующий (1) или предыдущий (-1)
local function Switch(step)
    -- погиб
    if humanoid.Health <= 0 then return end
    -- что есть
    local list, equipped = Tools()
    -- рюкзак пуст
    if #list == 0 then return end
    -- номер того, что в руках
    local index = table.find(list, equipped)
    -- что-то в руках
    if index then
        -- по кругу: после последнего — первый
        index = (index - 1 + step) % #list + 1
    -- руки пусты
    else
        -- первый или последний
        index = step > 0 and 1 or #list
    -- конец проверки
    end
    -- берём в руки
    humanoid:EquipTool(list[index])
    -- щелчок
    if click then click:Play() end
-- конец функции
end
-- колесо от себя — назад
mouse.WheelForward:Connect(function() Switch(-1) end)
-- колесо на себя — вперёд
mouse.WheelBackward:Connect(function() Switch(1) end)

FlashEffect

Где: StarterPlayer → StarterPlayerScripts · LocalScript
Зачем: Ослепление.

-- события
local RS = game:GetService("ReplicatedStorage")
-- плавность
local TS = game:GetService("TweenService")
-- эффекты экрана
local Lighting = game:GetService("Lighting")
-- сервер: тебя ослепило
local flashEvent = RS:WaitForChild("FlashEvent")

-- точка взрыва
flashEvent.OnClientEvent:Connect(function(position)
    -- камера
    local camera = workspace.CurrentCamera
    -- направление на вспышку
    local toFlash = (position - camera.CFrame.Position).Unit
    -- 1 — смотрит прямо, −1 — спиной
    local facing = camera.CFrame.LookVector:Dot(toFlash)
    -- отвернулся — слепит слабее
    local strength = facing > 0.3 and 1 or 0.35
    -- белый экран — только у этого игрока
    local flash = Instance.new("ColorCorrectionEffect")
    -- имя
    flash.Name = "Flash"
    -- яркость
    flash.Brightness = 2 * strength
    -- включаем
    flash.Parent = Lighting
    -- ослеплён
    task.wait(1.5 * strength)
    -- проходит
    local tween = TS:Create(flash, TweenInfo.new(1.5), {Brightness = 0})
    -- плавно
    tween:Play()
    -- ждём
    tween.Completed:Wait()
    -- убираем
    flash:Destroy()
-- конец функции
end)

RoundLocal

Где: StarterGui → RoundGui · LocalScript
Зачем: Таймер, живые, победитель.

-- игроки
local Players = game:GetService("Players")
-- атрибуты раунда
local RS = game:GetService("ReplicatedStorage")
-- этот игрок
local player = Players.LocalPlayer
-- RoundGui
local gui = script.Parent
-- таймер зоны
local timerFrame = gui:WaitForChild("TimerFrame")
-- что происходит
local info = timerFrame:WaitForChild("Info")
-- мм:сс
local timeLabel = timerFrame:WaitForChild("Time")
-- «Ты за зоной!»
local warnText = gui:WaitForChild("WarnText")
-- сколько живых
local aliveText = gui:WaitForChild("AliveText")
-- подсказка и победитель
local centerText = gui:WaitForChild("CenterText")

-- обновить таймер
local function UpdateTimer()
    -- текст от модуля зоны
    local text = RS:GetAttribute("ZoneText") or ""
    -- секунды
    local left = RS:GetAttribute("TimeLeft") or 0
    -- нет раунда — таймер скрыт
    timerFrame.Visible = text ~= ""
    -- текст
    info.Text = text
    -- минуты и секунды двумя цифрами
    timeLabel.Text = string.format("%02d:%02d", left // 60, left % 60)
-- конец функции
end
-- текст сменился
RS:GetAttributeChangedSignal("ZoneText"):Connect(UpdateTimer)
-- секунда прошла
RS:GetAttributeChangedSignal("TimeLeft"):Connect(UpdateTimer)
-- сразу
UpdateTimer()

-- предупреждение
local function UpdateWarn()
    -- за зоной — видно
    warnText.Visible = player:GetAttribute("OutsideZone") == true
-- конец функции
end
-- вышел или вернулся
player:GetAttributeChangedSignal("OutsideZone"):Connect(UpdateWarn)
-- сразу
UpdateWarn()

-- живые, подсказка, победитель
local function UpdateRound()
    -- имя победителя
    local winner = RS:GetAttribute("Winner") or ""
    -- счётчик — только во время боя
    aliveText.Visible = RS:GetAttribute("Phase") == "Fight"
    -- сколько
    aliveText.Text = "Живых: " .. (RS:GetAttribute("Alive") or 0)
    -- раунд закончен
    if winner ~= "" then
        -- поздравляем
        centerText.Text = "Победитель: " .. winner
    -- в самолёте
    elseif player:GetAttribute("OnPlane") then
        -- подсказка
        centerText.Text = "Пробел — прыгнуть"
    -- иначе
    else
        -- пусто
        centerText.Text = ""
    -- конец проверки
    end
-- конец функции
end
-- атрибуты раунда
for _, name in ipairs({"Phase", "Alive", "Winner"}) do
    -- изменился — обновить
    RS:GetAttributeChangedSignal(name):Connect(UpdateRound)
-- конец перебора
end
-- сел в самолёт или прыгнул
player:GetAttributeChangedSignal("OnPlane"):Connect(UpdateRound)
-- сразу
UpdateRound()

TeleportingLocal

Где: StarterGui → MainGui → Teleporting · LocalScript
Зачем: Затемнение.

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- плавность
local TS = game:GetService("TweenService")
-- Frame «Teleporting» на весь экран
local background = script.Parent
-- надпись
local text = background:WaitForChild("TextLabel")
-- сервер отправляет
local teleportingEvent = RS:WaitForChild("TeleportingEvent")
-- 2 секунды
local info = TweenInfo.new(2)

-- отправка
teleportingEvent.OnClientEvent:Connect(function()
    -- экран темнеет
    TS:Create(background, info, {BackgroundTransparency = 0}):Play()
    -- надпись проявляется
    TS:Create(text, info, {TextTransparency = 0}):Play()
    -- пока переносят
    task.wait(3.5)
    -- экран светлеет
    TS:Create(background, info, {BackgroundTransparency = 1}):Play()
    -- надпись исчезает
    TS:Create(text, info, {TextTransparency = 1}):Play()
-- конец функции
end)

LeaveLocal

Где: StarterGui → MainGui → LeaveButton · LocalScript
Зачем: Кнопка «Выйти».

-- общее хранилище
local RS = game:GetService("ReplicatedStorage")
-- кнопка «Выйти»
local button = script.Parent
-- событие
local leaveEvent = RS:WaitForChild("LeaveEvent")
-- сначала скрыта
button.Visible = false

-- сервер: показать или спрятать
leaveEvent.OnClientEvent:Connect(function(show)
    -- видна или нет
    button.Visible = show
-- конец функции
end)
-- клик
button.MouseButton1Click:Connect(function()
    -- просим вернуть в лобби
    leaveEvent:FireServer()
-- конец функции
end)

DeathLocal

Где: StarterGui → DeathGui → DeathFrame · LocalScript
Зачем: Экран смерти.

-- игроки
local Players = game:GetService("Players")
-- события
local RS = game:GetService("ReplicatedStorage")
-- плавность
local TS = game:GetService("TweenService")
-- размытие
local Lighting = game:GetService("Lighting")
-- этот игрок
local player = Players.LocalPlayer
-- DeathFrame
local frame = script.Parent
-- «В лобби»
local button = frame:WaitForChild("TeleportButton")
-- отсчёт
local timeLabel = frame:WaitForChild("TimeToTeleport")
-- попросить возрождение
local deathEvent = RS:WaitForChild("DeathEvent")
-- секунд до автоматического возвращения
local RESPAWN_TIME = 8
-- размытие мира
local blur = Lighting:FindFirstChild("DeathScreenBlur") or Instance.new("BlurEffect")
-- имя
blur.Name = "DeathScreenBlur"
-- пока не размыто
blur.Size = 0
-- включено (сила 0)
blur.Parent = Lighting
-- плавно
local info = TweenInfo.new(1, Enum.EasingStyle.Quint, Enum.EasingDirection.InOut)
-- сейчас мёртв?
local dead = false
-- экран смерти скрыт
frame.Visible = false

-- в лобби
local function Leave()
    -- уже
    if not dead then return end
    -- больше не ждём
    dead = false
    -- прячем экран
    frame.Visible = false
    -- мир снова чёткий
    TS:Create(blur, info, {Size = 0}):Play()
    -- сервер возрождает
    deathEvent:FireServer()
-- конец функции
end
-- подписка одна (в методичке — при каждой смерти)
button.MouseButton1Click:Connect(Leave)

-- новый персонаж
local function OnCharacter(character)
    -- Humanoid
    local humanoid = character:WaitForChild("Humanoid")
    -- жив
    dead = false
    -- экран скрыт
    frame.Visible = false
    -- без размытия
    blur.Size = 0
    -- погиб
    humanoid.Died:Connect(function()
        -- мёртв
        dead = true
        -- мир размывается
        TS:Create(blur, info, {Size = 20}):Play()
        -- секунда
        task.wait(1)
        -- уже нажал
        if not dead then return end
        -- экран смерти
        frame.Visible = true
        -- отсчёт
        for i = RESPAWN_TIME, 1, -1 do
            -- нажал кнопку
            if not dead then return end
            -- текст
            timeLabel.Text = "В лобби через " .. i .. " с"
            -- секунда
            task.wait(1)
        -- конец отсчёта
        end
        -- время вышло
        Leave()
    -- конец функции
    end)
-- конец функции
end
-- каждый новый персонаж
player.CharacterAdded:Connect(OnCharacter)
-- текущий
if player.Character then OnCharacter(player.Character) end

StartLocal

Где: StarterGui → StartGui → StartFrame · LocalScript
Зачем: Стартовый экран.

-- плавность
local TS = game:GetService("TweenService")
-- StartFrame на весь экран
local frame = script.Parent
-- логотип
local logoImage = frame:WaitForChild("LogoFrame"):WaitForChild("LogoImage")
-- название игры (в методичке то TextLabel, то LogoText)
local logoText = frame.LogoFrame:WaitForChild("LogoText")
-- «Игру сделал»
local presentedBy = frame:WaitForChild("PresentedBy")
-- имя разработчика
local me = frame:WaitForChild("Me")
-- плавно изменить прозрачность
local function Fade(object, property, value, seconds)
    -- анимация
    local tween = TS:Create(object, TweenInfo.new(seconds or 1), {[property] = value})
    -- запуск
    tween:Play()
    -- отдаём
    return tween
-- конец функции
end

-- экран закрыт
frame.Visible = true
-- чёрный
frame.BackgroundTransparency = 0
-- пауза
task.wait(1)
-- логотип проявляется
Fade(logoImage, "ImageTransparency", 0)
-- название
Fade(logoText, "TextTransparency", 0)
-- смотрим
task.wait(2.5)
-- исчезает
Fade(logoImage, "ImageTransparency", 1)
-- ждём
Fade(logoText, "TextTransparency", 1).Completed:Wait()
-- «Игру сделал»
Fade(presentedBy, "TextTransparency", 0)
-- смотрим
task.wait(2)
-- исчезает
Fade(presentedBy, "TextTransparency", 1).Completed:Wait()
-- имя
Fade(me, "TextTransparency", 0)
-- смотрим
task.wait(2)
-- исчезает
Fade(me, "TextTransparency", 1).Completed:Wait()
-- экран открывается
Fade(frame, "BackgroundTransparency", 1, 2).Completed:Wait()
-- больше не мешает кликам
frame.Visible = false

Проверка

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

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