diff --git a/RUBLOX_LUA_API_CHANGELOG.md b/RUBLOX_LUA_API_CHANGELOG.md new file mode 100644 index 0000000..c7e99f6 --- /dev/null +++ b/RUBLOX_LUA_API_CHANGELOG.md @@ -0,0 +1,100 @@ +# Lua API — журнал изменений + +Файл фиксирует **что было добавлено в Lua-runtime** при работе с реальными +Roblox-играми. Цель — потом продублировать тот же API для **JS-движка** +(на будущее, сейчас работаем только с Lua). + +Формат: дата + что и почему + куда добавлено + надо ли портировать в JS. + +--- + +## 2026-06-08 — Итерация 1: RayGun (проект 2792, 9 скриптов) + +**Контекст:** Roblox-Tool пушка-стрелялка, использует Tool-API, Lighting, +Mouse, Welds, BodyForce, BrickColor, IntValue для leaderboard. + +### Добавлено в `RobloxShim.js` + +**Глобалы:** +- `BrickColor.new("Bright red")` + ~25 цветов (White, Black, Bright red/blue/green, + Pink, Brown, Reddish brown, Cyan, Magenta и др.). Возвращает `{Color, Name, R, G, B}`. +- `Ray.new(origin, direction)` — для raycast (заглушка структуры). +- `Region3.new(min, max)` — куб (заглушка). +- `TweenInfo.new(time, easingStyle, easingDirection, repeatCount, reverses, delayTime)` +- `NumberSequence`, `ColorSequence`, `NumberRange`, `Rect` — конструкторы-стабы. + +**Enum расширения:** InfoType, SortOrder, FillDirection, Font, +TextXAlignment/TextYAlignment, ScaleType, AspectType, PartType, SurfaceType, +ContextActionResult, UserInputState, BorderMode, FormFactor. + +**`game` методы:** +- `game:service(name)` (lowercase alias на GetService) — старый Roblox API. +- `game.GetServiceFromName` = alias. +- `game.JobId/PlaceId/GameId/CreatorId/CreatorType` — stub fields. + +**Lighting:** +- `Brightness`, `ClockTime`, `TimeOfDay`, `OutdoorAmbient`, `FogStart/End/Color`. +- `GetMinutesAfterMidnight()`, `SetMinutesAfterMidnight(m)`. +- `GetMoonDirection()`, `GetSunDirection()`. + +**Players:** +- `GetPlayers()`, `GetPlayerFromCharacter(char)`, `playerFromCharacter` alias. +- `PlayerAdded`, `PlayerRemoving`, `ChildAdded` signals. + +**Instance.new новые типы:** +- `Tool` / `HopperBin` — Equipped/Unequipped/Activated/Deactivated signals, + GripForward/Right/Up/Pos, CanBeDropped, RequiresHandle, ToolTip. +- `IntValue` / `NumberValue` / `BoolValue` / `StringValue` / `ObjectValue` / + `CFrameValue` / `Vector3Value` / `Color3Value` / `BrickColorValue` / `RayValue` + — `.Value` + `.Changed` сигнал. +- `BodyForce` / `BodyVelocity` / `BodyPosition` / `BodyGyro` / `BodyAngularVelocity` + / `BodyThrust` — `.force`, `.Velocity`, `.MaxForce`, `.P/.D`. +- `Weld` / `WeldConstraint` / `Motor6D` / `Snap` / `HingeConstraint` / + `BallSocketConstraint` / `RopeConstraint` / `SpringConstraint` — Part0/Part1/C0/C1/Enabled. +- `Sparkles` / `ParticleEmitter` / `Smoke` / `Fire` / `Trail` / `Beam` / + `PointLight` / `SurfaceLight` / `SpotLight` — Enabled/Color/Rate/Lifetime/Brightness/Range. +- `Mouse` — Button1Down/Up, Button2Down/Up, Move, KeyDown/Up, WheelForward/Backward, + Icon, Hit (Position), Target, TargetSurface, X/Y, ViewSizeX/Y. + +### Исправлено + +- `rbx_wait(sec)`: минимум 0.016с (1 кадр). `while true do wait() end` без + аргумента раньше делал tight loop без yield → WASM stack overflow + ("memory access out of bounds"). + +### Надо ли портировать в JS-движок? + +✅ **Да, всё** — это базовый Roblox-совместимый API, который должен работать +независимо от языка скриптов. + +**JS-эквивалент будет такой же структурой:** +- `BrickColor.new("Bright red")` → `new BrickColor("Bright red")` +- `Tool` Equipped/Unequipped → JS-EventEmitter методы +- BodyForce/Weld/Sparkles → JS-классы с теми же полями +- Mouse — глобальный объект `game.mouse` или через `player:GetMouse()`. + +--- + +## Куда добавляется API + +| Источник | Файл | Что туда идёт | +|----------|------|---------------| +| Глобалы (Vector3, Color3, BrickColor, Enum) | `RobloxShim.js` через `global.set` | Конструкторы, Enum-таблицы | +| Instance.new типы | `RobloxShim.js` в ветке `global.set('Instance', {new: ...})` | Tool, BodyForce, Weld, Sparkles и т.д. | +| Сервисы | `RobloxShim.js` через `makeService(name)` | Lighting, Players, RunService и т.д. | +| Wait/Task | `RobloxShim.js` в Lua-prelude (`lua.doStringSync`) | rbx_wait, task.wait | +| Setter Part-свойств | `newPart()` через `Object.defineProperty` | Position, Color, Anchored шлют partSet | +| Команды от Lua к Babylon | `rbxl-lua-integration.js` `handleLuaCommand` | partSet, sceneCreate, sceneDelete | + +--- + +## Принципы расширения API + +1. **No-op > Падение.** Лучше пустой stub-метод чем `nil error`. +2. **Сигналы (`Connect`/`Fire`) всегда есть на любом объекте.** +3. **Coloncall совместимость.** Если есть `Foo.Bar`, обычно делаем и `Foo:Bar` + (lowercase) как alias. +4. **При добавлении нового Instance-типа** — давай ему **все типичные поля** + сразу, не только те что нужны прямо сейчас (Equipped + Unequipped + Activated + вместе, даже если скрипт юзает только Equipped). +5. **Логировать сюда после каждой итерации** — что было добавлено и из какой игры. diff --git a/src/editor/engine/lua/RobloxShim.js b/src/editor/engine/lua/RobloxShim.js index 533f016..63fd219 100644 --- a/src/editor/engine/lua/RobloxShim.js +++ b/src/editor/engine/lua/RobloxShim.js @@ -518,6 +518,53 @@ export function registerRobloxShim(lua, opts) { fromHSV: (h, s, v) => RbxColor3.fromHSV(h, s, v), fromHex: (hex) => RbxColor3.fromHex(hex), }); + // BrickColor — старая система цветов Roblox по имени + const BRICK_COLORS = { + 'White': [1, 1, 1], 'Black': [0.1, 0.1, 0.1], 'Grey': [0.6, 0.6, 0.6], + 'Bright red': [0.77, 0.2, 0.2], 'Bright blue': [0.05, 0.4, 0.7], + 'Bright green': [0.3, 0.8, 0.2], 'Bright yellow': [1, 0.85, 0.1], + 'Bright orange': [0.85, 0.5, 0.15], 'Bright violet': [0.45, 0.2, 0.65], + 'Dark blue': [0.05, 0.15, 0.4], 'Dark green': [0.15, 0.4, 0.2], + 'Dark red': [0.4, 0.1, 0.1], 'Lime green': [0.7, 0.95, 0.3], + 'Pink': [1, 0.55, 0.7], 'Brown': [0.4, 0.25, 0.15], + 'Reddish brown': [0.45, 0.2, 0.15], 'Sand red': [0.85, 0.6, 0.55], + 'Medium blue': [0.4, 0.65, 0.85], 'Cyan': [0, 0.8, 0.8], + 'Magenta': [0.85, 0, 0.85], 'Really red': [1, 0, 0], 'Really blue': [0, 0, 1], + 'Really black': [0, 0, 0], 'Really white': [1, 1, 1], + }; + function _brickToColor3(name) { + const rgb = BRICK_COLORS[name] || [0.5, 0.5, 0.5]; + return new RbxColor3(rgb[0], rgb[1], rgb[2]); + } + global.set('BrickColor', { + new(nameOrR, g, b) { + // BrickColor.new("Bright red") или BrickColor.new(r, g, b) + const name = typeof nameOrR === 'string' ? nameOrR : 'White'; + const c = typeof nameOrR === 'string' + ? _brickToColor3(nameOrR) + : new RbxColor3(nameOrR, g, b); + return { Color: c, Name: name, Number: 1, R: c.R, G: c.G, B: c.B, + r: c.R, g: c.G, b: c.B }; + }, + random() { return { Color: new RbxColor3(Math.random(), Math.random(), Math.random()), Name: 'Random' }; }, + White() { return this.new('White'); }, + Black() { return this.new('Black'); }, + Gray() { return this.new('Grey'); }, + Red() { return this.new('Bright red'); }, + Yellow() { return this.new('Bright yellow'); }, + Green() { return this.new('Bright green'); }, + Blue() { return this.new('Bright blue'); }, + DarkGray() { return this.new('Dark stone grey'); }, + palette(n) { return this.new('White'); }, + }); + // Ray — луч, используется в raycast + global.set('Ray', { + new(origin, direction) { return { Origin: origin, Direction: direction }; }, + }); + // Region3 — куб в пространстве + global.set('Region3', { + new(min, max) { return { Min: min, Max: max, CFrame: { Position: min }, Size: max }; }, + }); global.set('UDim', { new: (s, o) => new RbxUDim(s, o) }); global.set('UDim2', { new: (sx, ox, sy, oy) => new RbxUDim2(sx, ox, sy, oy), @@ -807,7 +854,23 @@ export function registerRobloxShim(lua, opts) { const r = Math.random() * 16 | 0; return (c === 'x' ? r : (r & 3 | 8)).toString(16); }); - makeService('Lighting').Ambient = new RbxColor3(0.5, 0.5, 0.5); + const lighting = makeService('Lighting'); + lighting.Ambient = new RbxColor3(0.5, 0.5, 0.5); + lighting.Brightness = 1; + lighting.ClockTime = 14; + lighting.TimeOfDay = "14:00:00"; + lighting.OutdoorAmbient = new RbxColor3(0.5, 0.5, 0.5); + lighting.FogEnd = 100000; + lighting.FogStart = 0; + lighting.FogColor = new RbxColor3(0.75, 0.75, 0.75); + lighting._minutes = 14 * 60; + lighting.GetMinutesAfterMidnight = function () { return lighting._minutes; }; + lighting.SetMinutesAfterMidnight = function (m) { + lighting._minutes = (Number(m) || 0) % 1440; + lighting.ClockTime = lighting._minutes / 60; + }; + lighting.GetMoonDirection = function () { return new RbxVector3(0, 1, 0); }; + lighting.GetSunDirection = function () { return new RbxVector3(0, 1, 0); }; makeService('Chat'); const soundService = makeService('SoundService'); soundService.PlayLocalSound = function (sound) { @@ -843,7 +906,28 @@ export function registerRobloxShim(lua, opts) { if (name === 'Players') return players; return services[name] || makeService(name); }; + // Старый Roblox API: game:service(name) lowercase + game.service = game.GetService; + game.GetServiceFromName = game.GetService; game.FindService = function (name) { return services[name] || null; }; + game.JobId = ''; + game.PlaceId = 0; + game.GameId = 0; + game.CreatorId = 0; + game.CreatorType = 'User'; + + // Players API extensions + players.GetPlayers = function () { return [players.LocalPlayer].filter(Boolean); }; + players.GetPlayerFromCharacter = function (character) { + if (character && players.LocalPlayer && players.LocalPlayer.Character === character) { + return players.LocalPlayer; + } + return undefined; + }; + players.playerFromCharacter = players.GetPlayerFromCharacter; + players.PlayerAdded = makeSignal(); + players.PlayerRemoving = makeSignal(); + players.ChildAdded = makeSignal(); global.set('game', game); global.set('Game', game); @@ -1090,6 +1174,80 @@ export function registerRobloxShim(lua, opts) { || className === 'ScrollingFrame') { inst = newGuiInstance(className); guiByLocalRef.set(inst.__guiLocalRef, inst); + } else if (className === 'Tool' || className === 'HopperBin') { + // Tool — оружие в Roblox. У нас нет инвентаря, поэтому это + // просто контейнер с правильными событиями + Handle (заглушка). + inst = newInstance(className, 'Tool'); + inst.Equipped = makeSignal(); + inst.Unequipped = makeSignal(); + inst.Activated = makeSignal(); + inst.Deactivated = makeSignal(); + inst.GripForward = new RbxVector3(0, -1, 0); + inst.GripRight = new RbxVector3(1, 0, 0); + inst.GripUp = new RbxVector3(0, 1, 0); + inst.GripPos = new RbxVector3(0, 0, 0); + inst.CanBeDropped = true; + inst.Enabled = true; + inst.RequiresHandle = true; + inst.TextureId = ''; + inst.ToolTip = ''; + } else if (className === 'IntValue' || className === 'NumberValue' + || className === 'BoolValue' || className === 'StringValue' + || className === 'ObjectValue' || className === 'CFrameValue' + || className === 'Vector3Value' || className === 'Color3Value' + || className === 'BrickColorValue' || className === 'RayValue') { + inst = newInstance(className, className); + inst.Value = className === 'BoolValue' ? false + : className === 'StringValue' ? '' + : (className === 'IntValue' || className === 'NumberValue') ? 0 + : undefined; + inst.Changed = makeSignal(); + } else if (className === 'BodyForce' || className === 'BodyVelocity' + || className === 'BodyPosition' || className === 'BodyGyro' + || className === 'BodyAngularVelocity' || className === 'BodyThrust') { + inst = newInstance(className, className); + inst.force = new RbxVector3(0, 0, 0); + inst.Force = inst.force; + inst.Velocity = new RbxVector3(0, 0, 0); + inst.MaxForce = new RbxVector3(0, 0, 0); + inst.P = 1000; inst.D = 100; + } else if (className === 'Weld' || className === 'WeldConstraint' + || className === 'Motor6D' || className === 'Snap' + || className === 'HingeConstraint' || className === 'BallSocketConstraint' + || className === 'RopeConstraint' || className === 'SpringConstraint') { + inst = newInstance(className, className); + inst.Part0 = undefined; inst.Part1 = undefined; + inst.C0 = { Position: new RbxVector3(0, 0, 0) }; + inst.C1 = { Position: new RbxVector3(0, 0, 0) }; + inst.Enabled = true; + } else if (className === 'Sparkles' || className === 'ParticleEmitter' + || className === 'Smoke' || className === 'Fire' || className === 'Trail' + || className === 'Beam' || className === 'PointLight' + || className === 'SurfaceLight' || className === 'SpotLight') { + inst = newInstance(className, className); + inst.Enabled = true; + inst.Color = new RbxColor3(1, 1, 1); + inst.Rate = 20; + inst.Lifetime = { Min: 1, Max: 1 }; + inst.Brightness = 1; + inst.Range = 8; + } else if (className === 'Mouse') { + inst = newInstance('Mouse', 'Mouse'); + inst.Button1Down = makeSignal(); + inst.Button1Up = makeSignal(); + inst.Button2Down = makeSignal(); + inst.Button2Up = makeSignal(); + inst.Move = makeSignal(); + inst.KeyDown = makeSignal(); + inst.KeyUp = makeSignal(); + inst.WheelForward = makeSignal(); + inst.WheelBackward = makeSignal(); + inst.Icon = ''; + inst.Hit = { Position: new RbxVector3(0, 0, 0), p: new RbxVector3(0, 0, 0) }; + inst.Target = undefined; + inst.TargetSurface = 'Top'; + inst.X = 0; inst.Y = 0; + inst.ViewSizeX = 1920; inst.ViewSizeY = 1080; } else { inst = newInstance(className, className); } @@ -1142,6 +1300,10 @@ export function registerRobloxShim(lua, opts) { lua.doStringSync(` local function rbx_wait(sec) sec = sec or 0 + -- Минимум 1 кадр (≈0.0166с). wait() и wait(0) в Roblox ждут до + -- следующего Heartbeat — без этого while true do wait() end + -- стал бы tight loop без yield и упёрся в WASM stack overflow. + if sec < 0.016 then sec = 0.016 end local ret = coroutine.yield(sec) return ret or sec end