@adiholick/nvstudio-mcp 2.2.2 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -47
- package/agent-plugin/mcp_config.json +8 -0
- package/agent-plugin/plugin.json +6 -0
- package/agent-plugin/rules/AGENTS.md +87 -0
- package/agent-plugin/skills/nvstudio-assets-terrain-guide/SKILL.md +27 -0
- package/agent-plugin/skills/nvstudio-gui-standards/SKILL.md +419 -0
- package/agent-plugin/skills/nvstudio-mcp-guide/SKILL.md +69 -0
- package/agent-plugin/skills/nvstudio-roblox-sync-guide/SKILL.md +16 -0
- package/dist/build-plugin.js +55 -0
- package/dist/index.js +988 -146
- package/dist/luau-validator.js +99 -34
- package/dist/roblox-bridge.js +390 -72
- package/dist/roblox-docs.js +264 -0
- package/dist/task-queue.js +57 -6
- package/dist/ws-gateway.js +579 -0
- package/package.json +7 -3
- package/public/index.html +634 -109
- package/studio-plugin/Main.server.lua +484 -53
- package/studio-plugin/batch_create_instances.lua +128 -0
- package/studio-plugin/batch_set_properties.lua +69 -0
- package/studio-plugin/get_properties.lua +25 -2
- package/studio-plugin/insert_asset.lua +85 -12
- package/studio-plugin/nvstudio_mcp.rbxmx +1393 -68
- package/studio-plugin/preview_asset.lua +99 -0
- package/studio-plugin/scan_security.lua +81 -0
- package/studio-plugin/security_scanner.lua +346 -0
- package/studio-plugin/set_properties.lua +10 -0
- package/studio-plugin/ui_manager.lua +1 -1
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nvstudio-gui-standards
|
|
3
|
+
description: Panduan standar arsitektur dan taste desain StarterGui Roblox (Struktur Instance, Smart Device Detection NVSelfTitle/NVDance, Layout Hierarchy, dan NV Dark Modern Aesthetics).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# NVStudio Roblox GUI Design Standards & Taste Guide
|
|
7
|
+
|
|
8
|
+
Gunakan skill ini sebagai acuan mutlak setiap kali membuat atau memodifikasi GUI di Roblox Studio (`StarterGui`). Semua GUI dirancang mengikuti filosofi arsitektur **NV-Series** (`NVMembership`, `NVMusic`, `NVSelfTitle`, `NVServer`, `NVDance`).
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. Aturan Fundamental Arsitektur GUI
|
|
13
|
+
|
|
14
|
+
1. **Roblox Instance Penuh (Bukan Pure Script):**
|
|
15
|
+
- GUI dibuat sebagai hierarki **Instance nyata di Roblox Studio** (menggunakan MCP `create_instance` / `set_properties` atau builder helper Luau).
|
|
16
|
+
- DILARANG membuat GUI murni on-the-fly dari script kosong dengan ratusan baris `Instance.new`.
|
|
17
|
+
- Script Luau (`LocalScript` / `ModuleScript`) hanya bertindak sebagai **UI Controller** yang mengontrol state, event click/input, tweening, dan integrasi server.
|
|
18
|
+
|
|
19
|
+
2. **Hierarki ScreenGui & MainFrame:**
|
|
20
|
+
- Setiap GUI berada di bawah `ScreenGui` dengan properti:
|
|
21
|
+
- `ResetOnSpawn = false`
|
|
22
|
+
- `ZIndexBehavior = Enum.ZIndexBehavior.Sibling`
|
|
23
|
+
- `IgnoreGuiInset = false`
|
|
24
|
+
- Child utama dari `ScreenGui` WAJIB berupa Frame bernama **`MainFrame`** (atau `ContainerFrame` yang membungkus `MainFrame`).
|
|
25
|
+
- `MainFrame.AnchorPoint` default: `Vector2.new(0.5, 0.5)` dengan posisi tengah `UDim2.new(0.5, 0, 0.5, 0)`.
|
|
26
|
+
|
|
27
|
+
3. **Aturan Wajib Layout Frame:**
|
|
28
|
+
- **Setiap Frame** WAJIB memiliki:
|
|
29
|
+
- `UIListLayout` (atau `UIGridLayout` jika grid konten) dengan `SortOrder = Enum.SortOrder.LayoutOrder`.
|
|
30
|
+
- `UIPadding` (memberikan padding konsisten agar elemen tidak menempel ke tepi).
|
|
31
|
+
- **Setiap Item di dalam Frame** WAJIB memiliki properti `LayoutOrder` eksplisit (0, 1, 2, 3...) agar susunan elemen teratur secara deterministik.
|
|
32
|
+
|
|
33
|
+
4. **Aturan Wajib ScrollingFrame:**
|
|
34
|
+
- Setiap `ScrollingFrame` WAJIB memiliki Frame transparan pembatas bernama **`Spacer`** atau **`SPACER`**:
|
|
35
|
+
- `Name = "Spacer"`
|
|
36
|
+
- `BackgroundTransparency = 1`
|
|
37
|
+
- `BorderSizePixel = 0`
|
|
38
|
+
- `LayoutOrder = 99999` (atau `999999999`)
|
|
39
|
+
- Size: `UDim2.new(1, 0, 0.15, 0)` atau `UDim2.new(0, 0, 0, 20)` (menyesuaikan orientasi scroll vertical/horizontal).
|
|
40
|
+
- Ini memastikan elemen paling bawah/ujung tidak terpotong saat pemain melakukan scroll.
|
|
41
|
+
|
|
42
|
+
5. **Pembangun Deklaratif Cepat (`batch_create_instances`):**
|
|
43
|
+
- Sangat disarankan membangun seluruh pohon GUI menggunakan tool `batch_create_instances` dengan mode `tree` bersarang.
|
|
44
|
+
- Contoh payload tree:
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"target": "StarterGui.MyScreen",
|
|
48
|
+
"tree": {
|
|
49
|
+
"className": "Frame",
|
|
50
|
+
"name": "MainFrame",
|
|
51
|
+
"properties": {
|
|
52
|
+
"Size": [0.5, 0, 0.6, 0],
|
|
53
|
+
"Position": [0.5, 0, 0.5, 0],
|
|
54
|
+
"AnchorPoint": [0.5, 0.5],
|
|
55
|
+
"BackgroundColor3": [28, 28, 30]
|
|
56
|
+
},
|
|
57
|
+
"children": [
|
|
58
|
+
{ "className": "UICorner", "properties": { "CornerRadius": [0, 10] } },
|
|
59
|
+
{ "className": "UIStroke", "properties": { "Color": [63, 63, 63], "Thickness": 1 } },
|
|
60
|
+
{ "className": "UIListLayout", "properties": { "SortOrder": "LayoutOrder" } },
|
|
61
|
+
{ "className": "UIPadding", "properties": { "PaddingTop": [0, 10], "PaddingBottom": [0, 10] } }
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 2. Smart Device Detection & Responsive Scaling System
|
|
70
|
+
|
|
71
|
+
Diadopsi langsung dari **`NVSelfTitle`** dan **`NVDance`**. Sistem ini membedakan ukuran perangkat secara akurat dan memisahkan skala PC, Mobile, Tablet, dan Console secara independen.
|
|
72
|
+
|
|
73
|
+
### Modul Logika Deteksi Perangkat:
|
|
74
|
+
```lua
|
|
75
|
+
local GuiService = game:GetService("GuiService")
|
|
76
|
+
local UserInputService = game:GetService("UserInputService")
|
|
77
|
+
local Workspace = game:GetService("Workspace")
|
|
78
|
+
|
|
79
|
+
-- [KONFIGURASI SKALA INDEPENDEN PER PERANGKAT]
|
|
80
|
+
local PC_Scale = 1.0 -- Skala normal PC / Laptop
|
|
81
|
+
local PC_Portrait_Scale = 1.2 -- Jendela PC orientasi portrait
|
|
82
|
+
local Mobile_Landscape_Scale = 1.45 -- Smartphone landscape
|
|
83
|
+
local Mobile_Portrait_Scale = 1.7 -- Smartphone portrait
|
|
84
|
+
local Tablet_Landscape_Scale = 1.25 -- Tablet / iPad landscape
|
|
85
|
+
local Tablet_Portrait_Scale = 1.45 -- Tablet / iPad portrait
|
|
86
|
+
local Console_Scale = 1.15 -- PlayStation / Xbox / TV
|
|
87
|
+
local GlobalScaleMultiplier = 1.0
|
|
88
|
+
|
|
89
|
+
-- Fungsi Cerdas Deteksi Perangkat (NVSelfTitle / NVDance Standard)
|
|
90
|
+
local function GetDeviceType(vw: number, vh: number, mainFrame: GuiObject): string
|
|
91
|
+
-- 1. Opsi override untuk testing di Studio lewat Attribute "ForceDevice" pada MainFrame:
|
|
92
|
+
local forceDevice = mainFrame:GetAttribute("ForceDevice")
|
|
93
|
+
if forceDevice and (forceDevice == "Phone" or forceDevice == "Tablet" or forceDevice == "PC" or forceDevice == "Console") then
|
|
94
|
+
return forceDevice :: string
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
-- 2. Console (PlayStation / Xbox / Smart TV)
|
|
98
|
+
if GuiService:IsTenFootInterface() then
|
|
99
|
+
return "Console"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
-- 3. Touch Device (Smartphone vs Tablet)
|
|
103
|
+
-- Touchscreen aktif dan keyboard fisik tidak aktif
|
|
104
|
+
if UserInputService.TouchEnabled and not UserInputService.KeyboardEnabled then
|
|
105
|
+
local smallerDimension = math.min(vw, vh)
|
|
106
|
+
-- Dimensi logika layar smartphone di Roblox umumnya < 550 pixel, Tablet >= 550 pixel
|
|
107
|
+
if smallerDimension > 0 and smallerDimension < 550 then
|
|
108
|
+
return "Phone"
|
|
109
|
+
else
|
|
110
|
+
return "Tablet"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
-- 4. Default: Desktop / PC / Laptop
|
|
115
|
+
return "PC"
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Handler Update Skala & Event Listener:
|
|
120
|
+
```lua
|
|
121
|
+
local function UpdateMainFrameScale(mainFrame: Frame, defaultSize: UDim2, isOpen: boolean)
|
|
122
|
+
local camera = Workspace.CurrentCamera
|
|
123
|
+
if not camera then return end
|
|
124
|
+
|
|
125
|
+
local vw, vh = camera.ViewportSize.X, camera.ViewportSize.Y
|
|
126
|
+
local isPortrait = vh > vw
|
|
127
|
+
local deviceType = GetDeviceType(vw, vh, mainFrame)
|
|
128
|
+
|
|
129
|
+
local baseScale = 1.0
|
|
130
|
+
if deviceType == "Phone" then
|
|
131
|
+
baseScale = isPortrait and Mobile_Portrait_Scale or Mobile_Landscape_Scale
|
|
132
|
+
elseif deviceType == "Tablet" then
|
|
133
|
+
baseScale = isPortrait and Tablet_Portrait_Scale or Tablet_Landscape_Scale
|
|
134
|
+
elseif deviceType == "Console" then
|
|
135
|
+
baseScale = Console_Scale
|
|
136
|
+
else
|
|
137
|
+
baseScale = isPortrait and PC_Portrait_Scale or PC_Scale
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
local totalScale = GlobalScaleMultiplier * baseScale
|
|
141
|
+
|
|
142
|
+
mainFrame.Size = UDim2.new(
|
|
143
|
+
defaultSize.X.Scale * totalScale,
|
|
144
|
+
defaultSize.X.Offset * totalScale,
|
|
145
|
+
defaultSize.Y.Scale * totalScale,
|
|
146
|
+
defaultSize.Y.Offset * totalScale
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if isOpen then
|
|
150
|
+
mainFrame.Position = UDim2.new(0.5, 0, 0.5, 0)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Studio Attributes Testing Support:
|
|
156
|
+
Pasang Attribute pada `MainFrame`:
|
|
157
|
+
- `ForceDevice` (`String`: `"Phone"`, `"Tablet"`, `"PC"`, `"Console"`)
|
|
158
|
+
- `PCScale` (`Number`)
|
|
159
|
+
- `MobileScale` (`Number`)
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## 3. NV Dark Modern Taste & Design Tokens
|
|
164
|
+
|
|
165
|
+
Semua GUI NV-Series menggunakan tema gelap elegan (*business-grade dark mode*) dengan kontras tinggi dan sentuhan rounded halus.
|
|
166
|
+
|
|
167
|
+
| Komponen | Token Warna / Properti | Nilai RGB / Hex | Kegunaan |
|
|
168
|
+
| :--- | :--- | :--- | :--- |
|
|
169
|
+
| **MainFrame Background** | `BackgroundColor3` | `Color3.fromRGB(28, 28, 30)` (`#1C1C1E`) | Latar belakang frame utama jendela |
|
|
170
|
+
| **Card / Item Background** | `BackgroundColor3` | `Color3.fromRGB(25, 25, 25)` (`#191919`) | Latar belakang list item, segment container |
|
|
171
|
+
| **Sub-Frame / Input** | `BackgroundColor3` | `Color3.fromRGB(13, 13, 13)` (`#0D0D0D`) | Textbox search, area preview, inner box |
|
|
172
|
+
| **Border / UIStroke** | `Color`, `Thickness = 1` | `Color3.fromRGB(63, 63, 63)` (`#3F3F3F`) | Garis tepi halus (`ApplyStrokeMode.Border`) |
|
|
173
|
+
| **Active Accent (Cyan)** | `Color3` | `Color3.fromRGB(0, 170, 255)` / `fromRGB(0, 255, 255)` | Tab aktif, border seleksi (`SelectStroke`) |
|
|
174
|
+
| **Text Primary** | `TextColor3` | `Color3.fromRGB(255, 255, 255)` (`#FFFFFF`) | Judul, label penting, nama item |
|
|
175
|
+
| **Text Secondary** | `TextColor3` | `Color3.fromRGB(163, 162, 165)` (`#A3A2A5`) | Deskripsi, subtitle, teks tidak aktif |
|
|
176
|
+
|
|
177
|
+
### Corner Radius (`UICorner`):
|
|
178
|
+
- `MainFrame`: `UDim.new(0, 10)` atau `UDim.new(0.03, 0)`
|
|
179
|
+
- `Card / Container Frame`: `UDim.new(0, 8)` atau `UDim.new(0, 10)`
|
|
180
|
+
- `Buttons / Pills / Tags`: `UDim.new(0.2, 0)` atau `UDim.new(0.5, 0)`
|
|
181
|
+
|
|
182
|
+
### Tipografi (`Font`):
|
|
183
|
+
- **Judul & Header:** `Enum.Font.GothamBold`
|
|
184
|
+
- **Teks Tubuh & List:** `Enum.Font.Gotham` atau `Enum.Font.GothamMedium`
|
|
185
|
+
- **Aksen / Badge:** `Enum.Font.FredokaOne` atau `Enum.Font.SourceSansBold`
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 4. UI Controller Script Template (LocalScript)
|
|
190
|
+
|
|
191
|
+
Setiap ScreenGui memiliki Controller (`LocalScript`) dengan struktur standar berikut:
|
|
192
|
+
|
|
193
|
+
```lua
|
|
194
|
+
--!strict
|
|
195
|
+
local Players = game:GetService("Players")
|
|
196
|
+
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
197
|
+
local TweenService = game:GetService("TweenService")
|
|
198
|
+
local UserInputService = game:GetService("UserInputService")
|
|
199
|
+
local GuiService = game:GetService("GuiService")
|
|
200
|
+
local Workspace = game:GetService("Workspace")
|
|
201
|
+
|
|
202
|
+
local Modules = ReplicatedStorage:WaitForChild("Modules")
|
|
203
|
+
local Maid = require(Modules:WaitForChild("Maid"))
|
|
204
|
+
local Icon = require(ReplicatedStorage:WaitForChild("Icon")) -- TopbarPlus
|
|
205
|
+
|
|
206
|
+
local GUI = script.Parent
|
|
207
|
+
local MainFrame = GUI:WaitForChild("MainFrame") :: Frame
|
|
208
|
+
local ScrollingFrame = MainFrame:FindFirstChildWhichIsA("ScrollingFrame", true)
|
|
209
|
+
|
|
210
|
+
local Controller = {
|
|
211
|
+
_maid = Maid.new(),
|
|
212
|
+
IsOpen = false,
|
|
213
|
+
DefaultSize = MainFrame.Size,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
-- [1] ANIMASI TOGGLE WINDOW (Pop-In & Slide)
|
|
217
|
+
function Controller:Toggle(state: boolean)
|
|
218
|
+
self.IsOpen = state
|
|
219
|
+
local tweenInfo = TweenInfo.new(0.4, Enum.EasingStyle.Quint, Enum.EasingDirection.Out)
|
|
220
|
+
local targetPos = state and UDim2.new(0.5, 0, 0.5, 0) or UDim2.new(0.5, 0, -0.6, 0)
|
|
221
|
+
|
|
222
|
+
if state then
|
|
223
|
+
MainFrame.Visible = true
|
|
224
|
+
UpdateMainFrameScale(MainFrame, self.DefaultSize, true)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
local tween = TweenService:Create(MainFrame, tweenInfo, { Position = targetPos })
|
|
228
|
+
tween:Play()
|
|
229
|
+
|
|
230
|
+
if not state then
|
|
231
|
+
tween.Completed:Connect(function()
|
|
232
|
+
if not self.IsOpen then
|
|
233
|
+
MainFrame.Visible = false
|
|
234
|
+
end
|
|
235
|
+
end)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
-- [2] SELEKSI TOMBOL INTERAKTIF (Pola SelectStroke)
|
|
240
|
+
function Controller:HighlightButton(parentContainer: Instance, activeBtn: Instance?)
|
|
241
|
+
for _, child in ipairs(parentContainer:GetChildren()) do
|
|
242
|
+
if child:IsA("TextButton") or child:IsA("ImageButton") then
|
|
243
|
+
local stroke = child:FindFirstChild("SelectStroke") :: UIStroke?
|
|
244
|
+
if not stroke then
|
|
245
|
+
stroke = Instance.new("UIStroke")
|
|
246
|
+
stroke.Name = "SelectStroke"
|
|
247
|
+
stroke.Thickness = 1
|
|
248
|
+
stroke.ApplyStrokeMode = Enum.ApplyStrokeMode.Border
|
|
249
|
+
stroke.Parent = child
|
|
250
|
+
end
|
|
251
|
+
if child == activeBtn then
|
|
252
|
+
stroke.Color = Color3.fromRGB(0, 255, 255)
|
|
253
|
+
else
|
|
254
|
+
stroke.Color = Color3.fromRGB(60, 60, 65)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
-- [3] SETUP TOPBARPLUS ICON
|
|
261
|
+
local topbarIcon = Icon.new()
|
|
262
|
+
topbarIcon:setLabel(GUI.Name)
|
|
263
|
+
topbarIcon.toggled:Connect(function(isSelected: boolean)
|
|
264
|
+
Controller:Toggle(isSelected)
|
|
265
|
+
end)
|
|
266
|
+
|
|
267
|
+
-- [4] HOOK CAMERA & ATTRIBUTES
|
|
268
|
+
local function hookCamera()
|
|
269
|
+
local cam = Workspace.CurrentCamera
|
|
270
|
+
if cam then
|
|
271
|
+
cam:GetPropertyChangedSignal("ViewportSize"):Connect(function()
|
|
272
|
+
UpdateMainFrameScale(MainFrame, Controller.DefaultSize, Controller.IsOpen)
|
|
273
|
+
end)
|
|
274
|
+
UpdateMainFrameScale(MainFrame, Controller.DefaultSize, Controller.IsOpen)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
hookCamera()
|
|
278
|
+
Workspace:GetPropertyChangedSignal("CurrentCamera"):Connect(hookCamera)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## 5. Komponen Interaktif Standar NV-Series
|
|
284
|
+
|
|
285
|
+
### A. Vertical Slider (`VerticalSlider`)
|
|
286
|
+
Diadopsi dari `NVMusic` (Volume Fader Master, Deck A/B Volume, Speed Control):
|
|
287
|
+
- **Struktur Instance:**
|
|
288
|
+
- `SliderFrame` (Frame Track Vertikal: Width `0.12` - `0.2`, Height `0.85`, background `#101010`, `UICorner`, `UIStroke`, `UIPadding`).
|
|
289
|
+
- `Button` (Draggable Knob: AnchorPoint `0.5, 0.5`, `UICorner`).
|
|
290
|
+
- `ValueTextTarget` (TextLabel indikator nilai).
|
|
291
|
+
- **Logika Luau:**
|
|
292
|
+
```lua
|
|
293
|
+
local function ProcessVerticalInput(inputY, sliderFrame, minVal, maxVal, step)
|
|
294
|
+
local frameY = sliderFrame.AbsolutePosition.Y
|
|
295
|
+
local frameH = sliderFrame.AbsoluteSize.Y
|
|
296
|
+
if frameH <= 0 then return minVal end
|
|
297
|
+
|
|
298
|
+
local relativeY = math.clamp(inputY - frameY, 0, frameH)
|
|
299
|
+
local ratio = 1 - (relativeY / frameH) -- Dari bawah ke atas
|
|
300
|
+
local rawVal = minVal + ratio * (maxVal - minVal)
|
|
301
|
+
local steps = math.floor(rawVal / step + 0.5)
|
|
302
|
+
return math.clamp(steps * step, minVal, maxVal), ratio
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
local function UpdateVerticalVisual(button, ratio, textLabel, val)
|
|
306
|
+
button.Position = UDim2.new(
|
|
307
|
+
button.Position.X.Scale,
|
|
308
|
+
button.Position.X.Offset,
|
|
309
|
+
(1 - ratio),
|
|
310
|
+
-button.Size.Y.Offset / 2
|
|
311
|
+
)
|
|
312
|
+
if textLabel then
|
|
313
|
+
textLabel.Text = tostring(math.floor(val * 100) / 100)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
### B. Horizontal Slider (`HorizontalSlider`)
|
|
321
|
+
Diadopsi dari `NVMusic` (Volume Horizontal, Master Crossfade) dan `NVDance` (Emote Speed Slider):
|
|
322
|
+
- **Struktur Instance:**
|
|
323
|
+
- `SliderFrame` (Track Horizontal: Height `0.2` - `0.25`, Width `1.0`, background `#101010`, `UICorner`, `UIStroke`).
|
|
324
|
+
- `Fill` (Frame Bar kemajuan aksen Cyan/Putih, Size `UDim2.new(ratio, 0, 1, 0)`).
|
|
325
|
+
- `Button` / `Knob` (Frame/Button bulat, Position `UDim2.new(ratio, 0, 0.5, 0)`).
|
|
326
|
+
- `Trigger` (TextButton transparan pembungkus input sentuh `Size = UDim2.new(1, 0, 1, 0)`).
|
|
327
|
+
- **Logika Luau:**
|
|
328
|
+
```lua
|
|
329
|
+
local function ProcessHorizontalInput(inputX, barFrame, minVal, maxVal, step)
|
|
330
|
+
local frameX = barFrame.AbsolutePosition.X
|
|
331
|
+
local frameW = barFrame.AbsoluteSize.X
|
|
332
|
+
if frameW <= 0 then return minVal end
|
|
333
|
+
|
|
334
|
+
local relX = math.clamp((inputX - frameX) / frameW, 0, 1)
|
|
335
|
+
local rawVal = minVal + relX * (maxVal - minVal)
|
|
336
|
+
local steps = math.floor(rawVal / step + 0.5)
|
|
337
|
+
return math.clamp(steps * step, minVal, maxVal), relX
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
local function UpdateHorizontalVisual(fillFrame, knobFrame, ratio, valueLabel, val)
|
|
341
|
+
if fillFrame then fillFrame.Size = UDim2.new(ratio, 0, 1, 0) end
|
|
342
|
+
if knobFrame then knobFrame.Position = UDim2.new(ratio, -knobFrame.Size.X.Offset / 2, 0.5, 0) end
|
|
343
|
+
if valueLabel then valueLabel.Text = string.format("%.2f", val) end
|
|
344
|
+
end
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
### C. Switch Button / Toggle (`ToggleSwitch`)
|
|
350
|
+
Diadopsi dari `NVMusic.GuiController.ToggleSwitch` (DJ Switch Controller):
|
|
351
|
+
- **Struktur Instance:**
|
|
352
|
+
- `SwitchContainer` (Frame Pill / Kapsul: `UICorner` CornerRadius `UDim.new(1, 0)`, background `#141414`).
|
|
353
|
+
- `SwitchFrame` (Indicator bulat / knob kecil di dalam container, `UICorner` `UDim.new(1, 0)`).
|
|
354
|
+
- `SwitchButton` (TextButton transparan `Size = UDim2.new(1, 0, 1, 0)`, `BackgroundTransparency = 1`).
|
|
355
|
+
- **Logika & Animasi Tween Luau:**
|
|
356
|
+
```lua
|
|
357
|
+
local ThemeColors = {
|
|
358
|
+
ActiveItem = Color3.fromRGB(0, 170, 255), -- Cyan saat ON
|
|
359
|
+
Background = Color3.fromRGB(20, 20, 20), -- Gelap saat OFF
|
|
360
|
+
White = Color3.fromRGB(255, 255, 255),
|
|
361
|
+
SubText = Color3.fromRGB(150, 150, 150)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
local function SetToggleState(isOn: boolean, container: Frame, indicator: Frame, animate: boolean)
|
|
365
|
+
local bgColor = isOn and ThemeColors.ActiveItem or ThemeColors.Background
|
|
366
|
+
local offset = indicator and indicator.Size.X.Offset or 14
|
|
367
|
+
local indicatorPos = isOn and UDim2.new(1, -(offset + 2), 0.5, 0) or UDim2.new(0, 2, 0.5, 0)
|
|
368
|
+
local indicatorColor = isOn and ThemeColors.White or ThemeColors.SubText
|
|
369
|
+
|
|
370
|
+
if animate then
|
|
371
|
+
TweenService:Create(container, TweenInfo.new(0.2), { BackgroundColor3 = bgColor }):Play()
|
|
372
|
+
TweenService:Create(indicator, TweenInfo.new(0.2), { Position = indicatorPos, BackgroundColor3 = indicatorColor }):Play()
|
|
373
|
+
else
|
|
374
|
+
container.BackgroundColor3 = bgColor
|
|
375
|
+
indicator.Position = indicatorPos
|
|
376
|
+
indicator.BackgroundColor3 = indicatorColor
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
### D. Segment Frame (Tab Bar Segment)
|
|
384
|
+
Diadopsi dari `NVMembership`, `NVServer`, `NVMusic`, dan `NVDance`:
|
|
385
|
+
- **Struktur Instance:**
|
|
386
|
+
- `SegmentFrame` (Container Tab Horizontal: `BackgroundColor3 = Color3.fromRGB(25, 25, 25)`, `UICorner` kapsul `UDim.new(1, 0)` atau `0.5`, `UIPadding`, `UIStroke`).
|
|
387
|
+
- `UIListLayout` (FillDirection `Horizontal`, HorizontalAlignment `Center`, SortOrder `LayoutOrder`).
|
|
388
|
+
- Tab Buttons (`ServerListButton`, `PlayerListButton`, `ManageButton`, dll) dengan `UICorner` rounded.
|
|
389
|
+
- **Logika Luau:**
|
|
390
|
+
```lua
|
|
391
|
+
local function SetActiveTab(tabButtons: { [string]: TextButton }, activeTabName: string, contentFrames: { [string]: GuiObject })
|
|
392
|
+
for name, btn in pairs(tabButtons) do
|
|
393
|
+
if name == activeTabName then
|
|
394
|
+
btn.BackgroundColor3 = Color3.fromRGB(55, 55, 60) -- Active Tab
|
|
395
|
+
else
|
|
396
|
+
btn.BackgroundColor3 = Color3.fromRGB(25, 25, 25) -- Inactive Tab
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
for name, frame in pairs(contentFrames) do
|
|
401
|
+
frame.Visible = (name == activeTabName)
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
### E. Standar Tombol Aktif & Inaktif (Button State Styling)
|
|
409
|
+
1. **Gaya Tab / Segment (`ThemeColors`):**
|
|
410
|
+
- **Active:** `Color3.fromRGB(55, 55, 60)` (abu-abu terang kontras).
|
|
411
|
+
- **Inactive:** `Color3.fromRGB(25, 25, 25)` (abu-abu gelap menyatu dengan background).
|
|
412
|
+
2. **Gaya Seleksi Item (`SelectStroke`):**
|
|
413
|
+
- Menggunakan child `UIStroke` bernama `SelectStroke` pada tombol:
|
|
414
|
+
- **Active (Selected):** `Color3.fromRGB(0, 255, 255)` (Cyan menyala).
|
|
415
|
+
- **Inactive:** `Color3.fromRGB(60, 60, 65)` (Outline abu-abu gelap).
|
|
416
|
+
3. **Gaya Disable / Cooldown (`AutoButtonColor`):**
|
|
417
|
+
- Saat dalam cooldown / limit habis: `AutoButtonColor = false`, teks diubah menjadi `"WAIT (Xs)"` atau `"OUT OF QUOTA"`, dan warna teks/latar diredupkan.
|
|
418
|
+
|
|
419
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nvstudio-mcp-guide
|
|
3
|
+
description: Panduan umum untuk berinteraksi dengan Roblox Studio menggunakan MCP nvstudio-mcp.
|
|
4
|
+
---
|
|
5
|
+
# nvstudio-mcp-guide
|
|
6
|
+
|
|
7
|
+
Gunakan skill ini sebagai referensi umum saat bekerja dengan Roblox Studio via `nvstudio-mcp`.
|
|
8
|
+
|
|
9
|
+
## Dasar Pemanggilan
|
|
10
|
+
1. Jika target path tidak diketahui, gunakan perintah eksplorasi seperti `get_children` atau `search_instance` (khusus pencarian berdasarkan nama) terlebih dahulu.
|
|
11
|
+
2. Tentukan target path string yang valid (misalnya `game.Workspace.Model.Part`). LLM/Agent tidak bisa berinteraksi menggunakan reference Luau langsung.
|
|
12
|
+
3. Gunakan native semantic tools yang tersedia (`execute_luau`, `batch_create_instances`, `get_roblox_docs`, dll.) atau gunakan legacy dispatcher `call_mcp_tool` untuk kompatibilitas.
|
|
13
|
+
|
|
14
|
+
## Batch Instance Builder (`batch_create_instances` & `batch_set_properties`)
|
|
15
|
+
Saat membuat banyak instance (terutama struktur GUI seperti `MainFrame`, `UICorner`, `UIStroke`, `UIListLayout`, `UIPadding`, `ScrollingFrame`, `Spacer`), **JANGAN** membuat instance satu per satu dengan `create_instance`. Gunakan `batch_create_instances`:
|
|
16
|
+
- **Mode `tree` (Rekomendasi Utama UI):**
|
|
17
|
+
Membangun seluruh hierarki bersarang dalam 1 roundtrip:
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"target": "StarterGui.NVMenu",
|
|
21
|
+
"tree": {
|
|
22
|
+
"className": "Frame",
|
|
23
|
+
"name": "MainFrame",
|
|
24
|
+
"properties": {
|
|
25
|
+
"Size": [0.5, 0, 0.6, 0],
|
|
26
|
+
"Position": [0.5, 0, 0.5, 0],
|
|
27
|
+
"AnchorPoint": [0.5, 0.5],
|
|
28
|
+
"BackgroundColor3": [28, 28, 30]
|
|
29
|
+
},
|
|
30
|
+
"children": [
|
|
31
|
+
{ "className": "UICorner", "name": "UICorner", "properties": { "CornerRadius": [0, 10] } },
|
|
32
|
+
{ "className": "UIStroke", "name": "UIStroke", "properties": { "Color": [63, 63, 63], "Thickness": 1 } },
|
|
33
|
+
{ "className": "UIListLayout", "name": "UIListLayout", "properties": { "SortOrder": "LayoutOrder" } },
|
|
34
|
+
{ "className": "UIPadding", "name": "UIPadding", "properties": { "PaddingTop": [0, 10] } }
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
- **Mode `instances` (Flat Array):**
|
|
40
|
+
Membuat banyak objek di berbagai parent berbeda sekaligus:
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"instances": [
|
|
44
|
+
{ "className": "Part", "name": "LightA", "parent": "Workspace.Lights", "properties": { "Anchored": true } },
|
|
45
|
+
{ "className": "Part", "name": "LightB", "parent": "Workspace.Lights", "properties": { "Anchored": true } }
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
- **Pembaruan Massal (`batch_set_properties`):**
|
|
50
|
+
Untuk mengubah properti banyak instance secara atomik dalam 1 pemanggilan:
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"updates": [
|
|
54
|
+
{ "target": "Workspace.PartA", "properties": { "Transparency": 0.5, "CanCollide": false } },
|
|
55
|
+
{ "target": "Workspace.PartB", "properties": { "Color": [255, 0, 0] } }
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Verifikasi Dokumentasi Engine Resmi (`get_roblox_docs`)
|
|
61
|
+
Sebelum menulis skrip Luau atau menggunakan API/Service yang belum pasti nama properti atau method-nya, **selalu panggil `get_roblox_docs`**:
|
|
62
|
+
- **Format Panggilan:**
|
|
63
|
+
- `query`: Nama class, service, datatype, enum, atau library (contoh: `"TweenService"`, `"CanvasGroup"`, `"UDim2"`, `"EasingStyle"`, `"task"`).
|
|
64
|
+
- `member` *(opsional)*: Untuk menghemat token, tentukan nama method atau properti spesifik (contoh: `query: "CanvasGroup", member: "GroupTransparency"`).
|
|
65
|
+
- **Keunggulan:** Tool ini membaca data resmi terverifikasi dari Creator Docs dengan caching lokal berkecepatan tinggi (~2 ms) untuk mencegah halusinasi kode Luau.
|
|
66
|
+
|
|
67
|
+
## Modifikasi Skrip Cerdas (`patch_script_source`)
|
|
68
|
+
- Jika hanya ingin mengubah sebagian baris kode tanpa menimpa seluruh file skrip, gunakan `patch_script_source` dengan parameter `search` dan `replacement`.
|
|
69
|
+
- Untuk penggantian skrip utuh, gunakan `update_script_source` (otomatis dicek oleh AST Validator di Node.js untuk mencegah syntax error fatal).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nvstudio-roblox-sync-guide
|
|
3
|
+
description: Aturan anti-halusinasi untuk membaca, mengedit, dan me-rollback script Roblox.
|
|
4
|
+
---
|
|
5
|
+
# nvstudio-roblox-sync-guide
|
|
6
|
+
|
|
7
|
+
Panduan sinkronisasi dan manajemen skrip (properti `.Source`) di Roblox Studio.
|
|
8
|
+
|
|
9
|
+
## Aturan update_script_source
|
|
10
|
+
1. **PENTING:** Anda WAJIB melakukan `get_script_source` terlebih dahulu sebelum mengedit script apa pun. Ini agar Anda memahami kode apa yang ada saat ini dan mencegah hilangnya kode krusial.
|
|
11
|
+
2. Operasi ini menimpa KESELURUHAN kode di dalam skrip.
|
|
12
|
+
3. Server jembatan memiliki "Luau AST Validator" internal. Jika script Anda mengandung syntax error fatal (seperti lupa `end`), file tidak akan diupdate dan Anda akan menerima pesan error.
|
|
13
|
+
|
|
14
|
+
## Aturan rollback_script
|
|
15
|
+
1. Anda dapat membatalkan perubahan terakhir pada script dengan memanggil `rollback_script`.
|
|
16
|
+
2. Pastikan target path cocok persis dengan script yang baru saja Anda edit.
|
package/dist/build-plugin.js
CHANGED
|
@@ -117,5 +117,60 @@ function copyToRobloxPlugins() {
|
|
|
117
117
|
}
|
|
118
118
|
catch { }
|
|
119
119
|
}
|
|
120
|
+
// Auto-sync dist, package.json, and agent-plugin to ~/.nvstudio-mcp & Antigravity
|
|
121
|
+
syncRuntimeAndPlugins();
|
|
122
|
+
}
|
|
123
|
+
function syncRuntimeAndPlugins() {
|
|
124
|
+
const homeDir = os_1.default.homedir();
|
|
125
|
+
const nvRuntimeDir = path_1.default.join(homeDir, '.nvstudio-mcp');
|
|
126
|
+
const antigravityPluginDir = path_1.default.join(homeDir, '.gemini', 'config', 'plugins', 'nvstudio-mcp');
|
|
127
|
+
// Sync to ~/.nvstudio-mcp
|
|
128
|
+
if (fs_1.default.existsSync(nvRuntimeDir)) {
|
|
129
|
+
try {
|
|
130
|
+
const rootDir = process.cwd();
|
|
131
|
+
const distSrc = path_1.default.join(rootDir, 'dist');
|
|
132
|
+
const distDest = path_1.default.join(nvRuntimeDir, 'dist');
|
|
133
|
+
if (fs_1.default.existsSync(distSrc)) {
|
|
134
|
+
fs_1.default.cpSync(distSrc, distDest, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
const pkgSrc = path_1.default.join(rootDir, 'package.json');
|
|
137
|
+
const pkgDest = path_1.default.join(nvRuntimeDir, 'package.json');
|
|
138
|
+
if (fs_1.default.existsSync(pkgSrc)) {
|
|
139
|
+
fs_1.default.copyFileSync(pkgSrc, pkgDest);
|
|
140
|
+
}
|
|
141
|
+
const pluginSrc = path_1.default.join(rootDir, 'studio-plugin');
|
|
142
|
+
const pluginDest = path_1.default.join(nvRuntimeDir, 'studio-plugin');
|
|
143
|
+
if (fs_1.default.existsSync(pluginSrc)) {
|
|
144
|
+
fs_1.default.cpSync(pluginSrc, pluginDest, { recursive: true, force: true });
|
|
145
|
+
}
|
|
146
|
+
const srcSrc = path_1.default.join(rootDir, 'src');
|
|
147
|
+
const srcDest = path_1.default.join(nvRuntimeDir, 'src');
|
|
148
|
+
if (fs_1.default.existsSync(srcSrc)) {
|
|
149
|
+
fs_1.default.cpSync(srcSrc, srcDest, { recursive: true, force: true });
|
|
150
|
+
}
|
|
151
|
+
const publicSrc = path_1.default.join(rootDir, 'public');
|
|
152
|
+
const publicDest = path_1.default.join(nvRuntimeDir, 'public');
|
|
153
|
+
if (fs_1.default.existsSync(publicSrc)) {
|
|
154
|
+
fs_1.default.cpSync(publicSrc, publicDest, { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
console.log(`[Build] ✅ Berhasil menyinkronkan runtime ${nvRuntimeDir} (dist, public, studio-plugin, src)`);
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
console.warn(`[Build] Gagal menyinkronkan ke ~/.nvstudio-mcp: ${err.message}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Sync to ~/.gemini/config/plugins/nvstudio-mcp
|
|
163
|
+
if (fs_1.default.existsSync(antigravityPluginDir)) {
|
|
164
|
+
try {
|
|
165
|
+
const agentPluginSrc = path_1.default.join(process.cwd(), 'agent-plugin');
|
|
166
|
+
if (fs_1.default.existsSync(agentPluginSrc)) {
|
|
167
|
+
fs_1.default.cpSync(agentPluginSrc, antigravityPluginDir, { recursive: true, force: true });
|
|
168
|
+
console.log(`[Build] ✅ Berhasil menyinkronkan agent-plugin ke ${antigravityPluginDir}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
console.warn(`[Build] Gagal menyinkronkan ke agent-plugin Antigravity: ${err.message}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
120
175
|
}
|
|
121
176
|
buildRbxmx();
|