From a1faf237a1fcb88bb8abfa6510cca0894ad6989a Mon Sep 17 00:00:00 2001 From: min Date: Mon, 8 Jun 2026 12:45:15 +0300 Subject: [PATCH] =?UTF-8?q?feat(lua):=20=D0=AD=D1=82=D0=B0=D0=BF=D1=8B=206?= =?UTF-8?q?+7=20=E2=80=94=20Sound=20+=20=D0=BF=D0=BE=D0=BB=D0=BD=D0=B0?= =?UTF-8?q?=D1=8F=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20RUBLOX=5FLUA=5FAPI.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Этап 6 — Sound: local s = Instance.new('Sound') s.SoundId = 'coin' -- или 'jump'/'win'/'lose'/'hit'/'click'/'pickup' s.Volume = 1 s.PlaybackSpeed = 1.2 s:Play() s.Ended:Connect(function() print('ended') end) SoundService:PlayLocalSound(sound) тоже работает. Маппинг roblox-AssetID на встроенные звуки по эвристике (substring match). Этап 7 — Документация: RUBLOX_LUA_API.md — полный справочник всего реализованного. Содержание: базовые типы, DataModel, Part-setters, Instance.new, события (Touched/Heartbeat/RemoteEvent), таймеры, GUI, Sound, TweenService, Humanoid, что не работает, готовый пример игры (KillBrick + Coin + GUI-счётчик). Этим завершается план RUBLOX_LUA_SUPPORT_PLAN (все 7 этапов). Co-Authored-By: Claude Opus 4.7 --- RUBLOX_LUA_API.md | 504 ++++++++++++++++++++++++++++ src/editor/engine/lua/RobloxShim.js | 52 ++- 2 files changed, 555 insertions(+), 1 deletion(-) create mode 100644 RUBLOX_LUA_API.md diff --git a/RUBLOX_LUA_API.md b/RUBLOX_LUA_API.md new file mode 100644 index 0000000..84c95dc --- /dev/null +++ b/RUBLOX_LUA_API.md @@ -0,0 +1,504 @@ +# Lua API Рублокса (справочник для скриптеров) + +Этот документ — полный список того, что работает в Lua-скриптах Рублокса. +API максимально приближен к Roblox, чтобы можно было переносить чужие +скрипты с минимальными правками. + +> **Как переключить скрипт на Lua:** в шапке вкладки редактора кода кликни +> по переключателю **JS / Lua**. Подсветка синтаксиса и автодополнение +> автоматически переключатся. + +--- + +## Содержание + +1. [Базовые типы](#базовые-типы) +2. [DataModel: game, workspace, Players](#datamodel) +3. [Part — куб на сцене](#part) +4. [Создание и удаление](#создание-и-удаление) +5. [События: Touched, Heartbeat, RemoteEvent](#события) +6. [Таймеры: task.wait, task.delay](#таймеры) +7. [GUI: TextLabel, TextButton, Frame](#gui) +8. [Звук: Sound](#звук) +9. [Анимации: TweenService](#tweenservice) +10. [Игрок: Humanoid, LocalPlayer](#игрок) +11. [Чего пока нет](#чего-пока-нет) + +--- + +## Базовые типы + +### `Vector3` + +```lua +local v = Vector3.new(1, 2, 3) +print(v.X, v.Y, v.Z) -- 1 2 3 +print(v.Magnitude) -- 3.7416... (длина) +print(v.Unit) -- нормализованный +print(v:Dot(otherVec)) -- скалярное произведение +print(v:Cross(otherVec)) -- векторное произведение +local mid = v:Lerp(otherVec, 0.5) -- линейная интерполяция + +-- Константы: +Vector3.zero -- (0,0,0) +Vector3.one -- (1,1,1) +Vector3.xAxis -- (1,0,0) +Vector3.yAxis, Vector3.zAxis +``` + +Поддержаны операторы: `+`, `-`, `*` (на число), `/`, унарный `-`. + +### `Color3` + +```lua +local c = Color3.new(0.5, 0.2, 0.8) -- 0..1 каждый +local c2 = Color3.fromRGB(255, 128, 0) -- 0..255 +local c3 = Color3.fromHSV(0.1, 0.8, 1) +local c4 = Color3.fromHex("#FF8000") +local mid = c:Lerp(c2, 0.5) +print(c:ToHex()) -- "#7F33CC" +``` + +### `UDim2` / `UDim` / `Vector2` + +Для GUI-координат: + +```lua +local pos = UDim2.new(0.5, 0, 0.5, 0) -- центр экрана (scale/offset) +local pos2 = UDim2.fromScale(0.2, 0.1) +local pos3 = UDim2.fromOffset(100, 50) -- в пикселях +``` + +### `CFrame` + +```lua +local cf = CFrame.new(0, 10, 0) -- позиция +local cf2 = CFrame.lookAt(eye, target) -- упрощённый +print(cf.Position) -- Vector3 +``` + +### `Enum` + +```lua +Enum.KeyCode.W +Enum.KeyCode.Space +Enum.Material.Plastic, Enum.Material.Neon, Enum.Material.Wood +Enum.UserInputType.MouseButton1 +Enum.HumanoidStateType.Running +``` + +--- + +## DataModel + +Виртуальное дерево, как в Roblox: + +```lua +game -- корневой DataModel +game.Workspace -- = workspace (короче) +game.Players -- сервис игроков +game.Players.LocalPlayer -- локальный игрок +game.ReplicatedStorage -- хранилище общих ресурсов +game.StarterGui -- стартовое GUI +game.Lighting -- свет +``` + +Методы: + +```lua +local svc = game:GetService("RunService") +local part = workspace:FindFirstChild("Coin") +local part2 = workspace:FindFirstChildOfClass("Part") +local all = workspace:GetChildren() -- массив всех детей +local descendants = workspace:GetDescendants() +local sib = workspace.Coin:FindFirstAncestorOfClass("Workspace") +print(workspace:IsA("Workspace")) -- true +``` + +--- + +## Part + +`Part` — куб/сфера/цилиндр на сцене. **Это обёртка над примитивом Рублокса.** +Скрипт привязанный к кубу получает его через `script.Parent`: + +```lua +-- script.Parent — Part к которому прицеплен скрипт +print(script.Parent.Name) -- "Part_1" + +-- Чтение свойств +print(script.Parent.Position) -- Vector3 +print(script.Parent.Size) -- Vector3 +print(script.Parent.Color) -- Color3 +print(script.Parent.Anchored) -- bool +print(script.Parent.CanCollide) -- bool +print(script.Parent.Transparency) -- 0..1 + +-- Запись (двигает куб в реальном времени!) +script.Parent.Position = Vector3.new(0, 10, 0) +script.Parent.Size = Vector3.new(5, 1, 5) +script.Parent.Color = Color3.fromRGB(255, 0, 0) +script.Parent.Anchored = false -- куб начнёт падать (физика) +script.Parent.Transparency = 0.5 -- полупрозрачный +script.Parent.CFrame = CFrame.new(0, 20, 0) +``` + +--- + +## Создание и удаление + +### `Instance.new` + +```lua +-- Создать Part на сцене +local p = Instance.new("Part") +p.Position = Vector3.new(0, 5, 0) +p.Size = Vector3.new(2, 2, 2) +p.Color = Color3.fromRGB(255, 100, 0) +p.Anchored = true +p.Parent = workspace + +-- Удалить через 3 секунды +task.delay(3, function() + p:Destroy() +end) +``` + +Поддержанные классы: +- **Сцена:** `Part`, `WedgePart`, `MeshPart` +- **События:** `RemoteEvent`, `BindableEvent` +- **GUI:** `ScreenGui`, `Frame`, `TextLabel`, `TextButton`, `ImageLabel`, + `ImageButton`, `TextBox`, `ScrollingFrame` +- **Звук:** `Sound` +- **Прочее:** `Folder`, `Humanoid`, `Configuration`, любой `ClassName` + +--- + +## События + +### `script.Parent.Touched` — касание игрока + +```lua +script.Parent.Touched:Connect(function(hit) + print("Игрок коснулся!", hit.Name) + local h = game.Players.LocalPlayer.Character:FindFirstChildOfClass("Humanoid") + if h then + h:TakeDamage(100) -- KillBrick + end +end) +``` + +### `RunService.Heartbeat` — каждый кадр + +```lua +local RunService = game:GetService("RunService") +RunService.Heartbeat:Connect(function(dt) + -- dt — время с прошлого кадра (~0.016) + script.Parent.Position = script.Parent.Position + Vector3.new(0, 0.1, 0) +end) +``` + +### `BindableEvent` / `RemoteEvent` — общение между скриптами + +```lua +-- Скрипт A создаёт событие в общем месте +local event = Instance.new("BindableEvent") +event.Name = "MyEvent" +event.Parent = game.ReplicatedStorage + +-- Скрипт B подписывается +local event = game.ReplicatedStorage:WaitForChild("MyEvent") +event.Event:Connect(function(msg, num) + print("Получено:", msg, num) +end) + +-- Скрипт A триггерит +event:Fire("привет", 42) +``` + +### `Humanoid.Died` + +```lua +local h = game.Players.LocalPlayer.Character:FindFirstChildOfClass("Humanoid") +h.Died:Connect(function() + print("игрок умер") +end) +h.HealthChanged:Connect(function(newHp) + print("здоровье:", newHp) +end) +``` + +--- + +## Таймеры + +### `task.wait(сек)` — приостановить скрипт + +```lua +print("сейчас") +task.wait(1) +print("через секунду") +``` + +`task.wait` **не блокирует** другие скрипты — это yield через coroutines. +Можно использовать в `while true do ... task.wait(0.1) end` без проблем. + +### `task.delay(сек, fn)` — выполнить через + +```lua +task.delay(2, function() + print("через 2 секунды") +end) +``` + +### `task.spawn(fn)` — асинхронно + +```lua +task.spawn(function() + print("параллельно с основным потоком") +end) +``` + +--- + +## GUI + +### Базовая иерархия + +```lua +-- ScreenGui — корень всех GUI +local sg = Instance.new("ScreenGui") +sg.Parent = game.Players.LocalPlayer.PlayerGui + +-- TextLabel — статичный текст +local label = Instance.new("TextLabel") +label.Parent = sg +label.Text = "Привет!" +label.TextColor3 = Color3.fromRGB(255, 255, 0) +label.BackgroundColor3 = Color3.fromRGB(50, 30, 20) +label.Position = UDim2.new(0.4, 0, 0.1, 0) -- 40% от ширины, 10% от высоты +label.Size = UDim2.new(0.2, 0, 0.05, 0) +label.TextSize = 24 + +-- TextButton — кликабельная кнопка +local btn = Instance.new("TextButton") +btn.Parent = sg +btn.Text = "Нажми" +btn.Position = UDim2.new(0.4, 0, 0.5, 0) +btn.Size = UDim2.new(0.2, 0, 0.08, 0) +btn.MouseButton1Click:Connect(function() + print("Клик!") + label.Text = "Нажата!" +end) +``` + +### Свойства + +| Свойство | Тип | Описание | +|----------------------|-----------|-----------------------------------| +| `Text` | string | Видимый текст | +| `TextColor3` | Color3 | Цвет текста | +| `TextSize` | number | Размер шрифта | +| `BackgroundColor3` | Color3 | Цвет фона | +| `BackgroundTransparency` | 0..1 | 0=сплошной, 1=прозрачный | +| `Position` | UDim2 | Позиция (scale=%, offset=px/10) | +| `Size` | UDim2 | Размер | +| `Visible` | bool | Виден или нет | + +### События кнопок + +```lua +btn.MouseButton1Click:Connect(fn) -- ЛКМ клик +btn.MouseEnter:Connect(fn) -- наведение +btn.MouseLeave:Connect(fn) -- увод +btn.Activated:Connect(fn) -- = MouseButton1Click +``` + +--- + +## Звук + +```lua +local sound = Instance.new("Sound") +sound.SoundId = "coin" -- или "jump", "win", "lose", "hit", "click", "pickup" +sound.Volume = 1 -- 0..2 +sound.PlaybackSpeed = 1 -- pitch +sound:Play() +``` + +Также Roblox-AssetID работает с эвристикой: + +```lua +sound.SoundId = "rbxassetid://1234567890" -- автоподбор по имени переменной +``` + +Поддержанные звуки (процедурные, не из файлов): +- `jump` — прыжок +- `pickup` — подбор +- `coin` — звон монеты +- `win` — победа +- `lose` — поражение +- `click` — клик +- `hit` — удар + +Зацикливание: + +```lua +sound.Looped = true +sound:Play() -- играет до sound:Stop() +``` + +--- + +## TweenService + +Плавная анимация свойств: + +```lua +local TweenService = game:GetService("TweenService") + +local part = script.Parent +local tween = TweenService:Create( + part, + { Time = 2 }, -- длительность 2 сек + { Position = Vector3.new(0, 20, 0), + Color = Color3.fromRGB(255, 0, 0) } -- цели +) +tween:Play() + +tween.Completed:Connect(function() + print("Анимация завершилась!") +end) +``` + +Работает с `Position`, `Size`, `Color` (Vector3/Color3) и числовыми +свойствами (`Transparency`, `TextSize`, и т.д.). + +--- + +## Игрок + +### `game.Players.LocalPlayer` + +```lua +local plr = game.Players.LocalPlayer +print(plr.Name, plr.UserId, plr.DisplayName) +print(plr.Character) -- Model +``` + +### `Humanoid` + +```lua +local char = game.Players.LocalPlayer.Character +local h = char:FindFirstChildOfClass("Humanoid") + +print(h.Health, h.MaxHealth) +print(h.WalkSpeed) -- скорость ходьбы +print(h.JumpPower) -- сила прыжка + +h.Health = 0 -- мгновенная смерть → респавн +h:TakeDamage(50) -- урон с учётом invulnerability + +h.Died:Connect(function() + print("Помер") +end) +h.HealthChanged:Connect(function(newHp) + if newHp < 30 then + print("Здоровье низкое!") + end +end) +``` + +### `HumanoidRootPart` + +```lua +local hrp = char:FindFirstChild("HumanoidRootPart") +print(hrp.Position) +``` + +--- + +## Чего пока нет + +Не работает (пока): + +- **Скрипты не делятся на Server/LocalScript** — все скрипты client-side. +- **DataStoreService** — методы есть, но возвращают nil/no-op. +- **`workspace:Raycast`** / **`game.Lighting.ClockTime`** — заглушки. +- **`Players.PlayerAdded`** — никогда не фейерится (только один игрок). +- **3D-анимации (`Animation` instance + `AnimationController`)** — + `LoadAnimation` возвращает заглушку. +- **`Sound` из файлов** — только встроенные процедурные. +- **`SurfaceGui` / `BillboardGui`** — нет, только `ScreenGui`. +- **`Model:MoveTo` / `:SetPrimaryPartCFrame`** — нет. +- **Networking (`RemoteFunction:InvokeServer`)** — RemoteEvent работает + только в пределах одного клиента. + +Если что-то из этого критично — открой issue в репо. + +--- + +## Пример: KillBrick + монета + GUI-счётчик + +Положи 1 куб и 1 шарик на сцене. К каждому привяжи скрипт: + +**На кубе (KillBrick):** +```lua +script.Parent.Color = Color3.fromRGB(200, 30, 30) +script.Parent.Touched:Connect(function() + local h = game.Players.LocalPlayer.Character:FindFirstChildOfClass("Humanoid") + if h then h:TakeDamage(100) end +end) +``` + +**На шарике (Coin):** +```lua +script.Parent.Color = Color3.fromRGB(255, 215, 0) +script.Parent.Touched:Connect(function() + -- Запускаем событие на ReplicatedStorage + local re = game.ReplicatedStorage:FindFirstChild("CoinPicked") + if not re then + re = Instance.new("BindableEvent") + re.Name = "CoinPicked" + re.Parent = game.ReplicatedStorage + end + re:Fire() + script.Parent:Destroy() +end) +``` + +**Глобальный скрипт (GUI):** +```lua +local sg = Instance.new("ScreenGui") +sg.Parent = game.Players.LocalPlayer.PlayerGui + +local label = Instance.new("TextLabel") +label.Parent = sg +label.Text = "Монет: 0" +label.Position = UDim2.new(0.05, 0, 0.05, 0) +label.Size = UDim2.new(0.1, 0, 0.05, 0) +label.TextSize = 20 +label.TextColor3 = Color3.fromRGB(255, 215, 0) + +local count = 0 +task.spawn(function() + while not game.ReplicatedStorage:FindFirstChild("CoinPicked") do + task.wait(0.1) + end + game.ReplicatedStorage.CoinPicked.Event:Connect(function() + count = count + 1 + label.Text = "Монет: " .. count + local sound = Instance.new("Sound") + sound.SoundId = "coin" + sound:Play() + end) +end) +``` + +Получится: красный куб убивает, золотая монета даёт +1 к счётчику со +звуком. + +--- + +**Версия документации:** Этап 7 (готово после реализации Этапов 1-6). +Если что-то описанное здесь не работает — это баг, репортуй. diff --git a/src/editor/engine/lua/RobloxShim.js b/src/editor/engine/lua/RobloxShim.js index 00c491e..e133e7b 100644 --- a/src/editor/engine/lua/RobloxShim.js +++ b/src/editor/engine/lua/RobloxShim.js @@ -610,7 +610,10 @@ export function registerRobloxShim(lua, opts) { makeService('Lighting').Ambient = new RbxColor3(0.5, 0.5, 0.5); makeService('Chat'); - makeService('SoundService'); + const soundService = makeService('SoundService'); + soundService.PlayLocalSound = function (sound) { + if (sound && typeof sound.Play === 'function') sound.Play(); + }; makeService('PathfindingService'); makeService('CollectionService'); makeService('MarketplaceService'); @@ -829,6 +832,53 @@ export function registerRobloxShim(lua, opts) { inst.Health = 100; inst.MaxHealth = 100; inst.Died = makeSignal(); inst.HealthChanged = makeSignal(); inst.TakeDamage = function (n) { this.Health = Math.max(0, this.Health - (n || 0)); }; + } else if (className === 'Sound') { + // Sound — процедурные звуки через _playSound. + // SoundId → имя процедурного звука (rbxassetid игнорится). + inst = newInstance('Sound', 'Sound'); + inst.SoundId = ''; + inst.Volume = 1; + inst.PlaybackSpeed = 1; + inst.Pitch = 1; + inst.Looped = false; + inst.IsPlaying = false; + inst.Played = makeSignal(); + inst.Ended = makeSignal(); + // Map SoundId/имя на встроенный звук (jump/pickup/win/lose/click/hit/coin). + const _mapSoundName = (idOrName) => { + if (!idOrName) return 'click'; + const s = String(idOrName).toLowerCase(); + // Прямые ключи имеют приоритет + if (['jump','pickup','win','lose','click','hit','coin'].indexOf(s) >= 0) return s; + // Эвристика по части строки (для Roblox AssetID) + if (s.includes('jump')) return 'jump'; + if (s.includes('pickup') || s.includes('collect')) return 'pickup'; + if (s.includes('win') || s.includes('victory')) return 'win'; + if (s.includes('lose') || s.includes('death')) return 'lose'; + if (s.includes('hit') || s.includes('damage')) return 'hit'; + if (s.includes('coin') || s.includes('gem')) return 'coin'; + return 'click'; + }; + inst.Play = function () { + const name = _mapSoundName(this.SoundId || this.Name); + const pitch = +this.PlaybackSpeed || +this.Pitch || 1; + const volume = +this.Volume || 1; + send('sound.play', { name, volume, pitch }); + this.IsPlaying = true; + this.Played.Fire(); + // Простая модель: считаем что звук длится 0.5с + SCHEDULER.sleeping.push({ + wakeAt: SCHEDULER.now() + 500, + run: () => { + this.IsPlaying = false; + this.Ended.Fire(); + if (this.Looped) this.Play(); + }, + }); + }; + inst.Stop = function () { this.IsPlaying = false; }; + inst.Pause = function () { this.IsPlaying = false; }; + inst.Resume = function () { if (!this.IsPlaying) this.Play(); }; } else if (className === 'ScreenGui') { // ScreenGui — логический корень GUI. В Rublox overlay глобальный, // поэтому ScreenGui это просто контейнер-no-op (без gui.create).