Roblox · Оружие

Пистолет с замедлением, огнём и ядом

Пистолет с магазином, перезарядкой по R (звук + анимация), счётчиком патронов и прицелом на ПКМ. Что делает попадание — урон, замедление, горение или отравление — задаётся в папке Configuration, без правки скрипта.

Структура

Синим — что нужно создать.

StarterPack — выдаётся каждому игроку
└─ Pistol (Tool)
   ├─ Handle (Part) — модель; Anchored и CanCollide выкл.
   │  ├─ Shoot (Sound) — выстрел, SoundId rbxassetid://132746925891911
   │  ├─ Empty (Sound) — пустой магазин, SoundId rbxassetid://1136276162
   │  └─ Reload (Sound) — перезарядка, SoundId rbxassetid://139827026287805
   ├─ Configuration (Configuration)
   │  ├─ Damage (NumberValue) — урон за попадание, 20
   │  ├─ SlowTime (NumberValue) — секунд замедления, 5 (0 — не замедляет)
   │  ├─ SlowFactor (NumberValue) — множитель скорости, 0.5
   │  ├─ Effect (StringValue) — None / Fire / Poison
   │  ├─ EffectDamage (NumberValue) — урон эффекта в секунду, 3
   │  ├─ EffectTime (NumberValue) — секунд эффекта, 4
   │  ├─ FreezeTime (NumberValue) — сейчас 0
   │  ├─ BleedTime (NumberValue) — сейчас 0
   │  ├─ BleedDamage (NumberValue) — сейчас 2
   │  ├─ Knockback (NumberValue) — сейчас 0
   │  ├─ Lifesteal (NumberValue) — сейчас 0
   │  ├─ ChainCount (NumberValue) — сейчас 0
   │  ├─ ChainDamage (NumberValue) — сейчас 10
   │  ├─ ChainRange (NumberValue) — сейчас 12
   │  ├─ BlindTime (NumberValue) — сейчас 0
   │  └─ MarkTime (NumberValue) — сейчас 0
   ├─ Ammo (NumberValue) — 12
   ├─ MaxAmmo (NumberValue) — 12
   ├─ Shot (RemoteEvent) — клиент → сервер
   ├─ GunGui (ScreenGui)
   │  └─ Frame (Frame)
   │     └─ TextLabel (TextLabel) — «12 / 12»
   ├─ GunLocal (LocalScript)
   │  └─ Animation (Animation) — AnimationId rbxassetid://129692399767480 — перезарядка
   └─ GunServer (Script)
ServerScriptService
└─ WeaponEffects (ModuleScript) — общий модуль эффектов

Настройки эффектов

Значения в папке Configuration этого оружия. Меняй число в Properties — скрипт трогать не нужно.

Шаги

  1. Модель: Workspace → «+» → Part (или собери из нескольких партов и объедини: выдели их → вкладка Model → Union). Переименуй в Handle. В Properties: Anchored — выкл., CanCollide — выкл., Massless — вкл.
  2. Workspace → «+» → Tool, переименуй в Pistol. Перетащи Handle внутрь Pistol. Если в руке модель держится криво — плагин Tool Grip Editor (Toolbox → Plugins).
  3. Звуки: в Handle «+» → Sound три раза. Переименуй и в Properties впиши SoundId:
    • Shoot — выстрел: rbxassetid://132746925891911
    • Reload — перезарядка: rbxassetid://139827026287805
    • Empty — щелчок пустого магазина: rbxassetid://1136276162
    Свой звук — Toolbox → вкладка Audio → ПКМ по звуку → Copy Asset ID.
  4. Configuration — папка для настроек (как Folder, значок шестерёнки): наведи на Pistol → «+» → Configuration. Внутри создай по списку: «+» → NumberValue (если в скобках другой тип — его) → переименуй → в Properties впиши Value:
    • Damage = 20 — урон за одно попадание
    • SlowTime = 5 — замедление: сколько секунд цель идёт медленнее; 0 — выключено
    • SlowFactor = 0.5 — замедление: во сколько раз медленнее — 0.5 значит вдвое
    • FreezeTime = 0 — заморозка: сколько секунд цель стоит и не может прыгать; 0 — выключено
    • Effect (StringValue) = None — огонь или яд: None — нет, Fire — пламя, Poison — зелёный дым (пиши точно так)
    • EffectDamage = 3 — огонь или яд: урон каждую секунду
    • EffectTime = 4 — огонь или яд: сколько секунд горит или действует яд
    • BleedDamage = 2 — кровотечение: урон в секунду (вдвое больше, если цель бежит)
    • BleedTime = 0 — кровотечение: сколько секунд идёт урон; 0 — выключено
    • Knockback = 0 — отбрасывание: сила толчка от тебя (у гранаты — от взрыва); 0 — выключено
    • Lifesteal = 0 — вампиризм: какая доля урона лечит тебя, 0.3 = 30 %; 0 — выключено
    • ChainCount = 0 — цепная молния: на сколько соседних целей перескочит; 0 — выключено
    • ChainDamage = 10 — цепная молния: урон каждой следующей цели
    • ChainRange = 12 — цепная молния: как далеко может перескочить
    • BlindTime = 0 — ослепление: сколько секунд у задетого игрока белый экран; 0 — выключено
    • MarkTime = 0 — метка: сколько секунд цель подсвечена, урон по ней ×1,5; 0 — выключено
  5. В Pistol: «+» → NumberValue «Ammo» и ещё одно «MaxAmmo», обоим Value = 12 — патроны в магазине.
  6. В Pistol: «+» → RemoteEvent, переименуй в Shot — по нему клиент сообщает серверу о выстреле.
  7. Счётчик патронов: StarterGui → «+» → ScreenGui «GunGui» → в нём «+» → Frame → во Frame «+» → TextLabel. Размеры — через Scale (первое и третье число в Size). Готовый GunGui перетащи внутрь Pistol.
  8. В Pistol: «+» → LocalScript «GunLocal», вставь текст GunLocal из «Скриптов». В GunLocal: «+» → Animation, имя Animation, в Properties → AnimationId впиши rbxassetid://129692399767480 — анимация перезарядки.
  9. Анимация играет только в играх её владельца. Если в твоей игре перезарядка без движения — сделай свою: Avatar → Rig Builder → риг; Avatar → Animation Editor → ключевые кадры; Priority — Action; Publish to Roblox → скопируй ID → вставь в AnimationId. Без анимации оружие тоже работает.
  10. В Pistol: «+» → Script «GunServer», вставь текст GunServer из «Скриптов».
  11. ServerScriptService → «+» → ModuleScript, переименуй в WeaponEffects, вставь текст WeaponEffects из «Скриптов». Модуль один на всю игру: если он уже есть — пропусти этот шаг.
  12. Перетащи Pistol в StarterPack — будет у каждого игрока с начала игры. Или оставь в Workspace: подойди — и он попадёт в инвентарь.
  13. Как работает: GunLocal ловит клик и сообщает серверу точку под курсором; GunServer создаёт пулю — её видят все — и при попадании вызывает Effects.Hit из модуля WeaponEffects: урон и все эффекты из Configuration.
  14. Effect пиши точно: None, Fire или Poison (с большой буквы).
  15. Наборы: обычный — SlowTime 0, Effect None; ледяной — SlowTime 5, SlowFactor 0.4 или FreezeTime 2; огненный — Effect Fire, EffectDamage 3, EffectTime 4; ядовитый — Effect Poison, EffectDamage 2, EffectTime 8; электрический — ChainCount 3, ChainDamage 8.

Скрипты

GunServer

Где: StarterPack → Pistol · Script
Зачем: Пуля наносит урон из Configuration, замедляет и поджигает/отравляет цель.

-- пистолет (Tool)
local gun = script.Parent
-- модель пистолета
local handle = gun:WaitForChild("Handle")
-- урон и все эффекты
local config = gun:WaitForChild("Configuration")
-- сигнал «выстрел» от клиента
local shotEvent = gun:WaitForChild("Shot")
-- звук выстрела
local shootSound = handle:WaitForChild("Shoot")
-- удаление по таймеру
local Debris = game:GetService("Debris")
-- скорость пули
local bulletSpeed = 600
-- общий модуль эффектов (страница «Эффекты оружия»)
local Effects = require(game:GetService("ServerScriptService"):WaitForChild("WeaponEffects"))

-- клиент сообщил о выстреле
shotEvent.OnServerEvent:Connect(function(player, mousePosition)
    -- стреляет только тот, у кого пистолет в руках
    if gun.Parent ~= player.Character then return end
    -- новая пуля
    local bullet = Instance.new("Part")
    -- шарик
    bullet.Shape = Enum.PartType.Ball
    -- маленький
    bullet.Size = Vector3.new(0.3, 0.3, 0.3)
    -- чёрный
    bullet.BrickColor = BrickColor.new("Black")
    -- не отскакивает от своего пистолета
    bullet.CanCollide = false
    -- у ствола, смотрит на курсор
    bullet.CFrame = CFrame.new(handle.Position, mousePosition)
    -- в мир
    bullet.Parent = workspace
    -- физику пули считает сервер
    bullet:SetNetworkOwner(nil)
    -- летит вперёд
    bullet.AssemblyLinearVelocity = bullet.CFrame.LookVector * bulletSpeed
    -- звук выстрела
    shootSound:Play()
    -- пуля ещё никого не задела
    local hit = false
    -- пуля чего-то коснулась
    bullet.Touched:Connect(function(otherPart)
        -- уже попала или задела стрелка
        if hit or otherPart:IsDescendantOf(player.Character) then return end
        -- запоминаем попадание
        hit = true
        -- модель, которой принадлежит деталь
        local model = otherPart:FindFirstAncestorOfClass("Model")
        -- есть ли у неё Humanoid
        local humanoid = model and model:FindFirstChildOfClass("Humanoid")
        -- попали в живое
        if humanoid then
            -- урон и все эффекты из Configuration
            Effects.Hit(player.Character, humanoid, config)
        -- конец проверки
        end
        -- пуля исчезает при любом попадании
        bullet:Destroy()
    -- конец обработчика касания
    end)
    -- промах — удалится через 10 секунд
    Debris:AddItem(bullet, 10)
-- конец обработчика
end)

GunLocal

Где: StarterPack → Pistol · LocalScript
Зачем: Патроны, щелчок пустого магазина, перезарядка по R с анимацией, счётчик на экране, прицел на ПКМ.

-- оружие (Tool)
local gun = script.Parent
-- сигнал «выстрел» на сервер
local shotEvent = gun:WaitForChild("Shot")
-- этот игрок
local player = game.Players.LocalPlayer
-- его мышь: mouse.Hit — точка под курсором
local mouse = player:GetMouse()
-- оружие в руках?
local equipped = false
-- нажатия клавиш
local UIS = game:GetService("UserInputService")
-- модель оружия
local handle = gun:WaitForChild("Handle")
-- щелчок пустого магазина
local emptySound = handle:WaitForChild("Empty")
-- звук перезарядки
local reloadSound = handle:WaitForChild("Reload")
-- патроны в магазине сейчас
local ammo = gun:WaitForChild("Ammo")
-- вместимость магазина
local maxAmmo = gun:WaitForChild("MaxAmmo")
-- анимация перезарядки
local reloadAnimation = script:WaitForChild("Animation")
-- загрузим, когда возьмём оружие
local animTrack = nil
-- идёт перезарядка?
local reloading = false
-- счётчик патронов
local gunGui = gun:WaitForChild("GunGui")
-- надпись «30 / 30»
local ammoText = gunGui.Frame.TextLabel
-- камера — для прицела
local camera = workspace.CurrentCamera

-- обновляет счётчик
local function UpdateAmmo()
    -- «патроны / вместимость»
    ammoText.Text = ammo.Value .. " / " .. maxAmmo.Value
-- конец функции
end
-- патроны изменились — обновляем
ammo.Changed:Connect(UpdateAmmo)

-- события каждого кадра
local RunService = game:GetService("RunService")
-- показывать оружие в виде от первого лица
local SHOW_GUN = true
-- сейчас целимся?
local aiming = false
-- режим камеры до прицеливания
local normalMode = nil
-- минимальное приближение до прицеливания
local normalMinZoom = nil
-- подписка «оружие видно»
local showConnection = nil

-- включить или выключить вид от первого лица
local function FirstPerson(on)
    -- уже так — ничего не делаем
    if aiming == on then return end
    -- запоминаем
    aiming = on
    -- начали целиться
    if on then
        -- запоминаем режим камеры
        normalMode = player.CameraMode
        -- и минимальное приближение
        normalMinZoom = player.CameraMinZoomDistance
        -- камера в голове персонажа
        player.CameraMode = Enum.CameraMode.LockFirstPerson
        -- оружие должно быть видно
        if SHOW_GUN then
            -- каждый кадр
            showConnection = RunService.RenderStepped:Connect(function()
                -- все детали оружия
                for _, part in ipairs(gun:GetDescendants()) do
                    -- Roblox прячет их от первого лица — возвращаем
                    if part:IsA("BasePart") then part.LocalTransparencyModifier = 0 end
                -- конец перебора
                end
            -- конец обработчика
            end)
        -- конец проверки
        end
    -- перестали целиться
    else
        -- оружие снова прячется как обычно
        if showConnection then showConnection:Disconnect() end
        -- подписки нет
        showConnection = nil
        -- режим камеры, который был
        player.CameraMode = normalMode or Enum.CameraMode.Classic
        -- камера обычная
        if player.CameraMode == Enum.CameraMode.Classic then
            -- отодвигаем камеру назад — иначе останется в голове
            player.CameraMinZoomDistance = 10
            -- один кадр
            task.wait()
            -- возвращаем приближение, как было
            player.CameraMinZoomDistance = normalMinZoom or 0.5
        -- конец проверки
        end
    -- конец проверки
    end
-- конец функции
end

-- взяли оружие в руки
local function OnEquipped()
    -- запоминаем
    equipped = true
    -- в руках — значит, родитель оружия персонаж
    local humanoid = gun.Parent:FindFirstChild("Humanoid")
    -- анимация задана
    if reloadAnimation.AnimationId ~= "" then
        -- готовим анимацию перезарядки
        animTrack = humanoid:LoadAnimation(reloadAnimation)
    -- конец проверки
    end
    -- показываем счётчик на экране
    gunGui.Parent = player.PlayerGui
    -- с актуальным числом
    UpdateAmmo()
    -- курсор-перекрестье
    mouse.Icon = "rbxassetid://6884107931"
-- конец функции
end
-- каждый раз, когда берут в руки
gun.Equipped:Connect(OnEquipped)
-- подобрали с земли: скрипт запустился, когда оружие уже в руках, — Equipped был раньше
if gun.Parent and gun.Parent:FindFirstChildOfClass("Humanoid") then OnEquipped() end

-- убрали оружие
gun.Unequipped:Connect(function()
    -- запоминаем
    equipped = false
    -- прячем счётчик обратно в оружие
    gunGui.Parent = gun
    -- обычный курсор
    mouse.Icon = ""
    -- выходим из прицела
    camera.FieldOfView = 70
    -- и из вида от первого лица
    FirstPerson(false)
-- конец обработчика
end)

-- клик, пока оружие в руках
gun.Activated:Connect(function()
    -- во время перезарядки не стреляем
    if reloading then return end
    -- патроны есть
    if ammo.Value > 0 then
        -- выстрел
        shotEvent:FireServer(mouse.Hit.Position)
        -- минус патрон
        ammo.Value -= 1
    -- магазин пуст
    else
        -- щелчок
        emptySound:Play()
    -- конец проверки
    end
-- конец обработчика
end)

-- нажата клавиша
UIS.InputBegan:Connect(function(input, gp)
    -- печатает в чат / оружие не в руках / уже перезаряжаемся
    if gp or not equipped or reloading then return end
    -- R, и магазин не полный
    if input.KeyCode == Enum.KeyCode.R and ammo.Value < maxAmmo.Value then
        -- началась перезарядка
        reloading = true
        -- звук
        reloadSound:Play()
        -- анимация, если задана
        if animTrack then animTrack:Play() end
        -- ждём длину звука (минимум 1 с — даже если звук не загрузился)
        task.wait(math.max(reloadSound.TimeLength, 1))
        -- полный магазин
        ammo.Value = maxAmmo.Value
        -- перезарядка закончилась
        reloading = false
    -- конец проверки
    end
-- конец обработчика
end)

-- нажата ПКМ
mouse.Button2Down:Connect(function()
    -- приближаем камеру — прицел
    if equipped then camera.FieldOfView = 30 end
    -- смотрим от первого лица
    if equipped then FirstPerson(true) end
-- конец обработчика
end)
-- ПКМ отпущена
mouse.Button2Up:Connect(function()
    -- обычный обзор
    camera.FieldOfView = 70
    -- камера снова за спиной
    FirstPerson(false)
-- конец обработчика
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

Проверка

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

Configuration одной командой
  1. View → Command Bar — строка ввода внизу окна Studio.
  2. Скопируй скрипт кнопкой «Копировать», вставь в Command Bar, нажми Enter.
  3. В Workspace появится Configuration со всеми значениями Pistol — имена и числа как на этой странице. Перетащи её внутрь Pistol и поменяй числа под себя.
-- создаёт контейнер-значение
local function Value(class, valueName, value, parent)
    -- NumberValue или StringValue
    local v = Instance.new(class)
    -- имя — точно такое ждут скрипты
    v.Name = valueName
    -- значение
    v.Value = value
    -- куда положить
    v.Parent = parent
-- конец функции
end

-- папка настроек
local config = Instance.new("Configuration")
-- урон за попадание
Value("NumberValue", "Damage", 20, config)
-- секунд замедления (0 — выкл.)
Value("NumberValue", "SlowTime", 5, config)
-- множитель скорости при замедлении
Value("NumberValue", "SlowFactor", 0.5, config)
-- секунд заморозки (0 — выкл.)
Value("NumberValue", "FreezeTime", 0, config)
-- None, Fire или Poison
Value("StringValue", "Effect", "None", config)
-- урон огня или яда в секунду
Value("NumberValue", "EffectDamage", 3, config)
-- секунд огня или яда
Value("NumberValue", "EffectTime", 4, config)
-- урон кровотечения в секунду (×2, если цель бежит)
Value("NumberValue", "BleedDamage", 2, config)
-- секунд кровотечения (0 — выкл.)
Value("NumberValue", "BleedTime", 0, config)
-- сила отбрасывания (0 — выкл.)
Value("NumberValue", "Knockback", 0, config)
-- доля урона в лечение атакующего: 0.3 = 30 %
Value("NumberValue", "Lifesteal", 0, config)
-- прыжков цепной молнии (0 — выкл.)
Value("NumberValue", "ChainCount", 0, config)
-- урон молнии за прыжок
Value("NumberValue", "ChainDamage", 10, config)
-- дальность прыжка молнии
Value("NumberValue", "ChainRange", 12, config)
-- секунд ослепления игрока (0 — выкл.)
Value("NumberValue", "BlindTime", 0, config)
-- секунд метки: урон по цели ×1,5 (0 — выкл.)
Value("NumberValue", "MarkTime", 0, config)
-- в Workspace — уже со всеми значениями
config.Parent = workspace
-- выделяем в Explorer
game:GetService("Selection"):Set({config})
-- сообщение в Output
print("Configuration готова: перетащи её внутрь Pistol")
Патроны одной командой: Ammo, MaxAmmo, Shot
  1. View → Command Bar — строка ввода внизу окна Studio.
  2. Скопируй скрипт кнопкой «Копировать», вставь в Command Bar, нажми Enter.
  3. В Workspace появятся Ammo и MaxAmmo (по 12) и RemoteEvent Shot — уже выделены. Перетащи их прямо внутрь Pistol, не в Configuration.
-- патроны в магазине сейчас
local ammo = Instance.new("NumberValue")
-- имя — его ждёт GunLocal
ammo.Name = "Ammo"
-- полный магазин
ammo.Value = 12
-- в Workspace
ammo.Parent = workspace
-- вместимость магазина
local maxAmmo = Instance.new("NumberValue")
-- имя — его ждёт GunLocal
maxAmmo.Name = "MaxAmmo"
-- сколько влезает
maxAmmo.Value = 12
-- в Workspace
maxAmmo.Parent = workspace
-- сигнал «выстрел» клиент → сервер
local shot = Instance.new("RemoteEvent")
-- имя — его ждут оба скрипта
shot.Name = "Shot"
-- в Workspace
shot.Parent = workspace
-- выделяем все три
game:GetService("Selection"):Set({ammo, maxAmmo, shot})
-- сообщение в Output
print("Ammo, MaxAmmo и Shot готовы: перетащи внутрь Pistol")
Счётчик патронов GunGui одной командой
  1. View → Command Bar — строка ввода внизу окна Studio.
  2. Скопируй скрипт кнопкой «Копировать», вставь в Command Bar, нажми Enter.
  3. В Workspace появится GunGui → Frame → TextLabel «12 / 12». Перетащи GunGui внутрь Pistol. На экране он появится, когда оружие возьмут в руки.
  4. Посмотреть, как выглядит, — временно перетащи GunGui в StarterGui, потом верни в оружие.
-- счётчик патронов
local gui = Instance.new("ScreenGui")
-- имя — его ждёт GunLocal
gui.Name = "GunGui"
-- подложка; имя Frame — его ждёт GunLocal
local frame = Instance.new("Frame")
-- размер в долях экрана — одинаково на телефоне и ПК
frame.Size = UDim2.new(0.18, 0, 0.08, 0)
-- правый нижний угол
frame.Position = UDim2.new(0.8, 0, 0.88, 0)
-- тёмная
frame.BackgroundColor3 = Color3.fromRGB(25, 25, 35)
-- полупрозрачная
frame.BackgroundTransparency = 0.3
-- в GunGui
frame.Parent = gui
-- надпись; имя TextLabel — его ждёт GunLocal
local label = Instance.new("TextLabel")
-- во всю подложку
label.Size = UDim2.new(1, 0, 1, 0)
-- без фона
label.BackgroundTransparency = 1
-- белый текст
label.TextColor3 = Color3.new(1, 1, 1)
-- по размеру
label.TextScaled = true
-- начальный текст
label.Text = "12 / 12"
-- в подложку
label.Parent = frame
-- в Workspace — в игре его на экран переносит GunLocal
gui.Parent = workspace
-- выделяем
game:GetService("Selection"):Set({gui})
-- сообщение в Output
print("GunGui готов: перетащи его внутрь Pistol")
Собрать пистолет одной командой