Roblox · RPG · урок 11

Магазин и торговцы

Торговая площадь: у каждого класса свой торговец. Зелье лечения, свиток телепорта домой, нимб скорости. Покупки проверяет сервер и кладёт в рюкзак.

Структура

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

Workspace
├─ MapFolder → Market (Model)
│  └─ Торговец (Part) — невидимый; CanInteract (вкл.), CanBuy = Swordsman / Archer / Mage
└─ SafePart (Part) — куда переносит свиток
ServerStorage
└─ Shop (Folder)
   └─ Swordsman, Archer, Mage (Folder) — товары класса
      ├─ Potion (Tool) — Handle, Drink (Animation), PotionScript, Price
      ├─ Scroll (Tool) — Handle, ScrollScript, Price
      └─ Halo (Accessory) — Handle → HatAttachment, HaloScript, Price
ReplicatedStorage
└─ BuyFunction (RemoteFunction)
ServerScriptService
└─ Buy (Script)
StarterGui
└─ ShopGui (ScreenGui) — ResetOnSpawn выкл.
   └─ Swordsman, Archer, Mage (Frame) — Title, Items (кнопки = имена товаров), Message, CloseButton
StarterPlayerScripts → InteractSystem (LocalScript) — дополнили

Шаги

  1. Три лавки, за каждой торговец своего класса (модели из Toolbox — без скриптов). На лавке невидимый Part «Торговец» с BoolValue «CanInteract» и StringValue «CanBuy» = имя класса.
  2. Зелье: Tool «Potion» с Handle, Animation «Drink» (Action), Script «PotionScript» — +50 здоровья.
  3. Свиток: Tool «Scroll» с Handle и Script «ScrollScript» — переносит к невидимой детали «SafePart» в безопасном месте.
  4. Нимб: Accessory «Halo», внутри Handle с Attachment «HatAttachment» (по ней нимб встанет над головой) и Script «HaloScript» — +4 к скорости.
  5. Товары — в ServerStorage → Shop → папка класса, в каждом IntValue «Price».
  6. ShopGui — отдельный ScreenGui (не внутри InteractGui) с тремя Frame, названными как классы. В каждом: Title, Frame «Items» с кнопками, названными как товары (текст «Зелье — 20»), TextLabel «Message», CloseButton.
  7. ReplicatedStorage → RemoteFunction «BuyFunction», ServerScriptService → Script «Buy». Сервер проверяет класс, товар и деньги.
  8. В методичке кнопки подписывались при каждом открытии магазина, а купленное падало на землю — здесь подписка одна, товар в рюкзаке.

Скрипты

PotionScript

Где: ServerStorage → Shop → … → Potion · Script
Зачем: Зелье лечения.

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

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

ScrollScript

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

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

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

HaloScript

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

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

Buy

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

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

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

InteractSystem

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

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

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

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

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

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

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

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

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

Проверка

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