feat: 50 игр на Lua + импорт Roblox для всех + поддержка Lua в плеере #39
100
RUBLOX_LUA_API_CHANGELOG.md
Normal file
100
RUBLOX_LUA_API_CHANGELOG.md
Normal file
@ -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. **Логировать сюда после каждой итерации** — что было добавлено и из какой игры.
|
||||||
@ -518,6 +518,53 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
fromHSV: (h, s, v) => RbxColor3.fromHSV(h, s, v),
|
fromHSV: (h, s, v) => RbxColor3.fromHSV(h, s, v),
|
||||||
fromHex: (hex) => RbxColor3.fromHex(hex),
|
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('UDim', { new: (s, o) => new RbxUDim(s, o) });
|
||||||
global.set('UDim2', {
|
global.set('UDim2', {
|
||||||
new: (sx, ox, sy, oy) => new RbxUDim2(sx, ox, sy, oy),
|
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);
|
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');
|
makeService('Chat');
|
||||||
const soundService = makeService('SoundService');
|
const soundService = makeService('SoundService');
|
||||||
soundService.PlayLocalSound = function (sound) {
|
soundService.PlayLocalSound = function (sound) {
|
||||||
@ -843,7 +906,28 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
if (name === 'Players') return players;
|
if (name === 'Players') return players;
|
||||||
return services[name] || makeService(name);
|
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.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);
|
||||||
global.set('Game', game);
|
global.set('Game', game);
|
||||||
@ -1090,6 +1174,80 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
|| className === 'ScrollingFrame') {
|
|| className === 'ScrollingFrame') {
|
||||||
inst = newGuiInstance(className);
|
inst = newGuiInstance(className);
|
||||||
guiByLocalRef.set(inst.__guiLocalRef, inst);
|
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 {
|
} else {
|
||||||
inst = newInstance(className, className);
|
inst = newInstance(className, className);
|
||||||
}
|
}
|
||||||
@ -1142,6 +1300,10 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
lua.doStringSync(`
|
lua.doStringSync(`
|
||||||
local function rbx_wait(sec)
|
local function rbx_wait(sec)
|
||||||
sec = sec or 0
|
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)
|
local ret = coroutine.yield(sec)
|
||||||
return ret or sec
|
return ret or sec
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user