feat(lua): Итерация 1 RayGun — Tool/Mouse/BodyForce/Weld/IntValue/BrickColor

Поддержка скриптов проекта 2792 (Roblox RayGun Tool, 9 скриптов).

Lighting: ClockTime, GetMinutesAfterMidnight, SetMinutesAfterMidnight,
GetSunDirection, fog* поля.

game:service(name) — старый Roblox API (lowercase alias на GetService).
Players: GetPlayerFromCharacter, playerFromCharacter, PlayerAdded, ChildAdded.

Instance.new новых типов:
- Tool/HopperBin: Equipped/Unequipped/Activated/Grip*/CanBeDropped
- IntValue/NumberValue/BoolValue/StringValue/ObjectValue + Value/Changed
- BodyForce/BodyVelocity/BodyPosition/BodyGyro + force/Velocity/MaxForce
- Weld/Motor6D/HingeConstraint + Part0/Part1/C0/C1
- Sparkles/ParticleEmitter/Fire/PointLight + Enabled/Color/Rate
- Mouse: Button1Down/KeyDown signals, Icon, Hit, Target, X/Y

Глобалы: BrickColor.new('name'/r,g,b) с палитрой 25+ цветов,
Ray.new, Region3.new.

Фикс WASM crash: rbx_wait минимум 0.016с (1 кадр) — без этого
while true do wait() end делал tight-loop без yield → stack overflow.

Добавлен RUBLOX_LUA_API_CHANGELOG.md — журнал что было добавлено
для каждой игры (для будущего портирования API в JS-движок).
This commit is contained in:
min 2026-06-08 13:39:56 +03:00
parent 34062993ee
commit ca92ba1988
2 changed files with 263 additions and 1 deletions

100
RUBLOX_LUA_API_CHANGELOG.md Normal file
View 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. **Логировать сюда после каждой итерации** — что было добавлено и из какой игры.

View File

@ -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