roblox-opencode 1.0.0 → 1.0.2

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.
Files changed (246) hide show
  1. package/README.md +112 -122
  2. package/commands/setup-game.md +108 -108
  3. package/commands/sync-check.md +53 -53
  4. package/core/roblox-core.md +93 -93
  5. package/dist/server.js +189 -167
  6. package/package.json +35 -35
  7. package/skills/roblox-analytics/SKILL.md +277 -277
  8. package/skills/roblox-analytics/references/event-batcher.luau +75 -75
  9. package/skills/roblox-animation-vfx/SKILL.md +1325 -1325
  10. package/skills/roblox-architecture/SKILL.md +877 -863
  11. package/skills/roblox-architecture/references/combat-systems.md +1381 -1381
  12. package/skills/roblox-code-review/SKILL.md +686 -686
  13. package/skills/roblox-data/SKILL.md +889 -889
  14. package/skills/roblox-data/references/inventory-systems.md +1729 -1729
  15. package/skills/roblox-debug/SKILL.md +98 -98
  16. package/skills/roblox-gui/SKILL.md +1103 -1103
  17. package/skills/roblox-gui-fusion/SKILL.md +150 -150
  18. package/skills/roblox-gui-fusion/references/inventory.luau +427 -427
  19. package/skills/roblox-gui-fusion/references/settings-menu.luau +579 -579
  20. package/skills/roblox-gui-fusion/references/shop.luau +411 -411
  21. package/skills/roblox-luau-mastery/SKILL.md +1618 -1519
  22. package/skills/roblox-monetization/SKILL.md +1084 -1084
  23. package/skills/roblox-monetization/references/process-receipt.luau +131 -131
  24. package/skills/roblox-networking/SKILL.md +669 -669
  25. package/skills/roblox-networking/references/remote-validator.luau +193 -193
  26. package/skills/roblox-publish-checklist/SKILL.md +127 -127
  27. package/skills/roblox-runtime/SKILL.md +753 -753
  28. package/skills/roblox-sharp-edges/SKILL.md +294 -294
  29. package/skills/roblox-sync/SKILL.md +126 -126
  30. package/skills/roblox-testing/SKILL.md +943 -943
  31. package/skills/roblox-tooling/SKILL.md +149 -149
  32. package/vendor/LICENSES/ProfileStore-LICENSE +201 -201
  33. package/vendor/LICENSES/RbxUtil-LICENSE +7 -7
  34. package/vendor/LICENSES/promise-LICENSE +20 -20
  35. package/vendor/LICENSES/t-LICENSE +21 -21
  36. package/vendor/LICENSES/testez-LICENSE +200 -200
  37. package/vendor/README.md +83 -83
  38. package/vendor/fusion/Animation/ExternalTime.luau +83 -83
  39. package/vendor/fusion/Animation/Spring.luau +321 -321
  40. package/vendor/fusion/Animation/Stopwatch.luau +127 -127
  41. package/vendor/fusion/Animation/Tween.luau +187 -187
  42. package/vendor/fusion/Animation/getTweenDuration.luau +27 -27
  43. package/vendor/fusion/Animation/getTweenRatio.luau +47 -47
  44. package/vendor/fusion/Animation/lerpType.luau +163 -163
  45. package/vendor/fusion/Animation/packType.luau +99 -99
  46. package/vendor/fusion/Animation/springCoefficients.luau +80 -80
  47. package/vendor/fusion/Animation/unpackType.luau +102 -102
  48. package/vendor/fusion/Colour/Oklab.luau +70 -70
  49. package/vendor/fusion/Colour/sRGB.luau +54 -54
  50. package/vendor/fusion/External.luau +167 -167
  51. package/vendor/fusion/ExternalDebug.luau +69 -69
  52. package/vendor/fusion/Graph/Observer.luau +113 -113
  53. package/vendor/fusion/Graph/castToGraph.luau +28 -28
  54. package/vendor/fusion/Graph/change.luau +80 -80
  55. package/vendor/fusion/Graph/depend.luau +32 -32
  56. package/vendor/fusion/Graph/evaluate.luau +55 -55
  57. package/vendor/fusion/Instances/Attribute.luau +57 -57
  58. package/vendor/fusion/Instances/AttributeChange.luau +46 -46
  59. package/vendor/fusion/Instances/AttributeOut.luau +63 -63
  60. package/vendor/fusion/Instances/Child.luau +21 -21
  61. package/vendor/fusion/Instances/Children.luau +147 -147
  62. package/vendor/fusion/Instances/Hydrate.luau +32 -32
  63. package/vendor/fusion/Instances/New.luau +52 -52
  64. package/vendor/fusion/Instances/OnChange.luau +49 -49
  65. package/vendor/fusion/Instances/OnEvent.luau +53 -53
  66. package/vendor/fusion/Instances/Out.luau +69 -69
  67. package/vendor/fusion/Instances/applyInstanceProps.luau +148 -148
  68. package/vendor/fusion/Instances/defaultProps.luau +194 -194
  69. package/vendor/fusion/LICENSE +21 -21
  70. package/vendor/fusion/Logging/formatError.luau +48 -48
  71. package/vendor/fusion/Logging/messages.luau +51 -51
  72. package/vendor/fusion/Logging/parseError.luau +24 -24
  73. package/vendor/fusion/Memory/checkLifetime.luau +133 -133
  74. package/vendor/fusion/Memory/deriveScope.luau +23 -23
  75. package/vendor/fusion/Memory/deriveScopeImpl.luau +44 -44
  76. package/vendor/fusion/Memory/doCleanup.luau +78 -78
  77. package/vendor/fusion/Memory/innerScope.luau +33 -33
  78. package/vendor/fusion/Memory/legacyCleanup.luau +17 -17
  79. package/vendor/fusion/Memory/needsDestruction.luau +16 -16
  80. package/vendor/fusion/Memory/poisonScope.luau +33 -33
  81. package/vendor/fusion/Memory/scopePool.luau +54 -54
  82. package/vendor/fusion/Memory/scoped.luau +26 -26
  83. package/vendor/fusion/Memory/whichLivesLonger.luau +74 -74
  84. package/vendor/fusion/RobloxExternal.luau +97 -97
  85. package/vendor/fusion/State/Computed.luau +138 -138
  86. package/vendor/fusion/State/For/Disassembly.luau +210 -210
  87. package/vendor/fusion/State/For/ForTypes.luau +30 -30
  88. package/vendor/fusion/State/For/init.luau +109 -109
  89. package/vendor/fusion/State/ForKeys.luau +93 -93
  90. package/vendor/fusion/State/ForPairs.luau +96 -96
  91. package/vendor/fusion/State/ForValues.luau +93 -93
  92. package/vendor/fusion/State/Value.luau +87 -87
  93. package/vendor/fusion/State/castToState.luau +25 -25
  94. package/vendor/fusion/State/peek.luau +30 -30
  95. package/vendor/fusion/Types.luau +314 -314
  96. package/vendor/fusion/Utility/Contextual.luau +90 -90
  97. package/vendor/fusion/Utility/Safe.luau +22 -22
  98. package/vendor/fusion/Utility/isSimilar.luau +29 -29
  99. package/vendor/fusion/Utility/merge.luau +35 -35
  100. package/vendor/fusion/Utility/nameOf.luau +34 -34
  101. package/vendor/fusion/Utility/never.luau +13 -13
  102. package/vendor/fusion/Utility/nicknames.luau +10 -10
  103. package/vendor/fusion/Utility/xtypeof.luau +26 -26
  104. package/vendor/fusion/init.luau +82 -82
  105. package/vendor/profilestore/init.luau +2242 -2242
  106. package/vendor/promise/init.luau +1982 -1982
  107. package/vendor/rbxutil/buffer-util/Buffer.test.luau +25 -25
  108. package/vendor/rbxutil/buffer-util/BufferReader.luau +228 -228
  109. package/vendor/rbxutil/buffer-util/BufferWriter.luau +269 -269
  110. package/vendor/rbxutil/buffer-util/DataTypeBuffer.luau +223 -223
  111. package/vendor/rbxutil/buffer-util/Types.luau +60 -60
  112. package/vendor/rbxutil/buffer-util/index.d.ts +153 -153
  113. package/vendor/rbxutil/buffer-util/init.luau +41 -41
  114. package/vendor/rbxutil/buffer-util/package.json +16 -16
  115. package/vendor/rbxutil/buffer-util/wally.toml +9 -9
  116. package/vendor/rbxutil/comm/Client/ClientComm.luau +232 -232
  117. package/vendor/rbxutil/comm/Client/ClientRemoteProperty.luau +156 -156
  118. package/vendor/rbxutil/comm/Client/ClientRemoteSignal.luau +109 -109
  119. package/vendor/rbxutil/comm/Client/init.luau +135 -135
  120. package/vendor/rbxutil/comm/Server/RemoteProperty.luau +295 -295
  121. package/vendor/rbxutil/comm/Server/RemoteSignal.luau +211 -211
  122. package/vendor/rbxutil/comm/Server/ServerComm.luau +211 -211
  123. package/vendor/rbxutil/comm/Server/init.luau +140 -140
  124. package/vendor/rbxutil/comm/Types.luau +18 -18
  125. package/vendor/rbxutil/comm/Util.luau +27 -27
  126. package/vendor/rbxutil/comm/init.luau +35 -35
  127. package/vendor/rbxutil/comm/wally.toml +13 -13
  128. package/vendor/rbxutil/component/init.luau +759 -759
  129. package/vendor/rbxutil/component/init.test.luau +311 -311
  130. package/vendor/rbxutil/component/wally.toml +14 -14
  131. package/vendor/rbxutil/concur/init.luau +542 -542
  132. package/vendor/rbxutil/concur/init.test.luau +364 -364
  133. package/vendor/rbxutil/concur/wally.toml +8 -8
  134. package/vendor/rbxutil/enum-list/init.luau +101 -101
  135. package/vendor/rbxutil/enum-list/init.test.luau +91 -91
  136. package/vendor/rbxutil/enum-list/wally.toml +8 -8
  137. package/vendor/rbxutil/find/index.d.ts +20 -20
  138. package/vendor/rbxutil/find/init.luau +44 -44
  139. package/vendor/rbxutil/find/package.json +17 -17
  140. package/vendor/rbxutil/find/wally.toml +8 -8
  141. package/vendor/rbxutil/input/Gamepad.luau +559 -559
  142. package/vendor/rbxutil/input/Keyboard.luau +124 -124
  143. package/vendor/rbxutil/input/Mouse.luau +278 -278
  144. package/vendor/rbxutil/input/PreferredInput.luau +91 -91
  145. package/vendor/rbxutil/input/Touch.luau +120 -120
  146. package/vendor/rbxutil/input/init.luau +33 -33
  147. package/vendor/rbxutil/input/wally.toml +12 -12
  148. package/vendor/rbxutil/loader/index.d.ts +15 -15
  149. package/vendor/rbxutil/loader/init.luau +137 -137
  150. package/vendor/rbxutil/loader/wally.toml +8 -8
  151. package/vendor/rbxutil/log/index.d.ts +38 -38
  152. package/vendor/rbxutil/log/init.luau +746 -746
  153. package/vendor/rbxutil/log/wally.toml +8 -8
  154. package/vendor/rbxutil/net/init.luau +190 -190
  155. package/vendor/rbxutil/net/wally.toml +8 -8
  156. package/vendor/rbxutil/option/index.d.ts +44 -44
  157. package/vendor/rbxutil/option/init.luau +489 -489
  158. package/vendor/rbxutil/option/init.test.luau +342 -342
  159. package/vendor/rbxutil/option/wally.toml +8 -8
  160. package/vendor/rbxutil/pid/index.d.ts +53 -53
  161. package/vendor/rbxutil/pid/init.luau +195 -195
  162. package/vendor/rbxutil/pid/package.json +16 -16
  163. package/vendor/rbxutil/pid/wally.toml +9 -9
  164. package/vendor/rbxutil/quaternion/index.d.ts +117 -117
  165. package/vendor/rbxutil/quaternion/init.luau +570 -570
  166. package/vendor/rbxutil/quaternion/package.json +16 -16
  167. package/vendor/rbxutil/quaternion/wally.toml +9 -9
  168. package/vendor/rbxutil/query/index.d.ts +43 -43
  169. package/vendor/rbxutil/query/init.luau +117 -117
  170. package/vendor/rbxutil/query/package.json +18 -18
  171. package/vendor/rbxutil/query/wally.toml +9 -9
  172. package/vendor/rbxutil/sequent/index.d.ts +28 -28
  173. package/vendor/rbxutil/sequent/init.luau +340 -340
  174. package/vendor/rbxutil/sequent/package.json +16 -16
  175. package/vendor/rbxutil/sequent/wally.toml +9 -9
  176. package/vendor/rbxutil/ser/init.luau +175 -175
  177. package/vendor/rbxutil/ser/init.test.luau +50 -50
  178. package/vendor/rbxutil/ser/wally.toml +11 -11
  179. package/vendor/rbxutil/shake/index.d.ts +36 -36
  180. package/vendor/rbxutil/shake/init.luau +532 -532
  181. package/vendor/rbxutil/shake/init.test.luau +267 -267
  182. package/vendor/rbxutil/shake/package.json +16 -16
  183. package/vendor/rbxutil/shake/wally.toml +9 -9
  184. package/vendor/rbxutil/signal/index.d.ts +100 -100
  185. package/vendor/rbxutil/signal/init.luau +432 -432
  186. package/vendor/rbxutil/signal/init.test.luau +190 -190
  187. package/vendor/rbxutil/signal/package.json +17 -17
  188. package/vendor/rbxutil/signal/wally.toml +9 -9
  189. package/vendor/rbxutil/silo/TableWatcher.luau +65 -65
  190. package/vendor/rbxutil/silo/Util.luau +55 -55
  191. package/vendor/rbxutil/silo/init.luau +338 -338
  192. package/vendor/rbxutil/silo/init.test.luau +215 -215
  193. package/vendor/rbxutil/silo/wally.toml +8 -8
  194. package/vendor/rbxutil/spring/index.d.ts +40 -40
  195. package/vendor/rbxutil/spring/init.luau +97 -97
  196. package/vendor/rbxutil/spring/package.json +17 -17
  197. package/vendor/rbxutil/spring/wally.toml +8 -8
  198. package/vendor/rbxutil/stream/index.d.ts +88 -88
  199. package/vendor/rbxutil/stream/init.luau +597 -597
  200. package/vendor/rbxutil/stream/package.json +18 -18
  201. package/vendor/rbxutil/stream/wally.toml +9 -9
  202. package/vendor/rbxutil/streamable/Streamable.luau +202 -202
  203. package/vendor/rbxutil/streamable/StreamableUtil.luau +80 -80
  204. package/vendor/rbxutil/streamable/init.luau +8 -8
  205. package/vendor/rbxutil/streamable/wally.toml +12 -12
  206. package/vendor/rbxutil/symbol/init.luau +56 -56
  207. package/vendor/rbxutil/symbol/init.test.luau +37 -37
  208. package/vendor/rbxutil/symbol/wally.toml +8 -8
  209. package/vendor/rbxutil/table-util/init.luau +938 -938
  210. package/vendor/rbxutil/table-util/init.test.luau +439 -439
  211. package/vendor/rbxutil/task-queue/index.d.ts +27 -27
  212. package/vendor/rbxutil/task-queue/init.luau +97 -97
  213. package/vendor/rbxutil/task-queue/wally.toml +8 -8
  214. package/vendor/rbxutil/timer/index.d.ts +81 -81
  215. package/vendor/rbxutil/timer/init.luau +249 -249
  216. package/vendor/rbxutil/timer/init.test.luau +73 -73
  217. package/vendor/rbxutil/timer/wally.toml +11 -11
  218. package/vendor/rbxutil/tree/index.d.ts +15 -15
  219. package/vendor/rbxutil/tree/init.luau +137 -137
  220. package/vendor/rbxutil/tree/wally.toml +8 -8
  221. package/vendor/rbxutil/trove/index.d.ts +46 -46
  222. package/vendor/rbxutil/trove/init.luau +787 -787
  223. package/vendor/rbxutil/trove/init.test.luau +203 -203
  224. package/vendor/rbxutil/trove/wally.toml +8 -8
  225. package/vendor/rbxutil/typed-remote/init.luau +196 -196
  226. package/vendor/rbxutil/typed-remote/wally.toml +8 -8
  227. package/vendor/rbxutil/wait-for/index.d.ts +17 -17
  228. package/vendor/rbxutil/wait-for/init.luau +257 -257
  229. package/vendor/rbxutil/wait-for/init.test.luau +182 -182
  230. package/vendor/rbxutil/wait-for/wally.toml +11 -11
  231. package/vendor/t/t.lua +1350 -1350
  232. package/vendor/testez/Context.lua +26 -26
  233. package/vendor/testez/Expectation.lua +311 -311
  234. package/vendor/testez/ExpectationContext.lua +38 -38
  235. package/vendor/testez/LifecycleHooks.lua +89 -89
  236. package/vendor/testez/Reporters/TeamCityReporter.lua +101 -101
  237. package/vendor/testez/Reporters/TextReporter.lua +105 -105
  238. package/vendor/testez/Reporters/TextReporterQuiet.lua +96 -96
  239. package/vendor/testez/TestBootstrap.lua +146 -146
  240. package/vendor/testez/TestEnum.lua +27 -27
  241. package/vendor/testez/TestPlan.lua +304 -304
  242. package/vendor/testez/TestPlanner.lua +39 -39
  243. package/vendor/testez/TestResults.lua +111 -111
  244. package/vendor/testez/TestRunner.lua +188 -188
  245. package/vendor/testez/TestSession.lua +243 -243
  246. package/vendor/testez/init.lua +39 -39
@@ -1,579 +1,579 @@
1
- --[[
2
- Settings Menu — Fusion 0.3
3
- Source patterns: VirtualButFake/fusion-components (MIT), dphfox/Fusion docs
4
- Ported to Fusion 0.3 scoped syntax.
5
-
6
- A complete settings menu with:
7
- - Tabbed sections (Audio, Graphics, Controls)
8
- - Sliders (volume, render distance)
9
- - Toggle switches (fullscreen, vsync, show FPS)
10
- - Dropdown select (quality preset)
11
- - Spring open/close animation
12
- - Saves to a settings table (caller persists however they want)
13
-
14
- ADAPT THIS:
15
- - Replace settings entries with your game's actual settings
16
- - Wire OnSettingChanged to your DataStore or local storage
17
- - Add/remove tabs and controls as needed
18
- - Adjust colors to match your game's palette
19
-
20
- Requires: Fusion 0.3 (wally: elttob/fusion@0.3.0)
21
- ]]
22
-
23
- local ReplicatedStorage = game:GetService("ReplicatedStorage")
24
- local Players = game:GetService("Players")
25
-
26
- -- Adjust this path to match your project structure:
27
- -- Wally: require(ReplicatedStorage.Packages.Fusion)
28
- -- Custom: require(game.ReplicatedStorage.Fusion)
29
- -- Vendored: (default, from roblox-opencode setup)
30
- local Fusion = require(ReplicatedStorage[".opencode"].vendor.fusion)
31
- local scoped = Fusion.scoped
32
- local peek = Fusion.peek
33
- local Children = Fusion.Children
34
- local OnEvent = Fusion.OnEvent
35
-
36
- --------------------------------------------------------------------------------
37
- -- Types
38
- --------------------------------------------------------------------------------
39
-
40
- type SettingValue = number | boolean | string
41
-
42
- type SliderSetting = {
43
- Type: "slider",
44
- Label: string,
45
- Key: string,
46
- Min: number,
47
- Max: number,
48
- Step: number?,
49
- Default: number,
50
- }
51
-
52
- type ToggleSetting = {
53
- Type: "toggle",
54
- Label: string,
55
- Key: string,
56
- Default: boolean,
57
- }
58
-
59
- type DropdownSetting = {
60
- Type: "dropdown",
61
- Label: string,
62
- Key: string,
63
- Options: { string },
64
- Default: string,
65
- }
66
-
67
- type SettingEntry = SliderSetting | ToggleSetting | DropdownSetting
68
-
69
- type SettingsTab = {
70
- Name: string,
71
- Icon: string?,
72
- Settings: { SettingEntry },
73
- }
74
-
75
- type SettingsProps = {
76
- Visible: Fusion.Value<boolean>,
77
- Tabs: { SettingsTab },
78
- Values: { [string]: Fusion.Value<SettingValue> },
79
- OnSettingChanged: (key: string, value: SettingValue) -> (),
80
- OnClose: () -> (),
81
- }
82
-
83
- --------------------------------------------------------------------------------
84
- -- Theme
85
- --------------------------------------------------------------------------------
86
-
87
- local COLORS = {
88
- Background = Color3.fromRGB(18, 18, 26),
89
- Surface = Color3.fromRGB(28, 28, 40),
90
- SurfaceAlt = Color3.fromRGB(35, 35, 48),
91
- Primary = Color3.fromRGB(80, 140, 255),
92
- Text = Color3.fromRGB(235, 235, 240),
93
- TextSecondary = Color3.fromRGB(155, 155, 170),
94
- TextMuted = Color3.fromRGB(90, 90, 105),
95
- ToggleOn = Color3.fromRGB(80, 200, 120),
96
- ToggleOff = Color3.fromRGB(60, 60, 75),
97
- SliderTrack = Color3.fromRGB(45, 45, 60),
98
- SliderFill = Color3.fromRGB(80, 140, 255),
99
- TabActive = Color3.fromRGB(80, 140, 255),
100
- TabInactive = Color3.fromRGB(60, 60, 75),
101
- }
102
-
103
- local FONT = {
104
- Title = Font.new("rbxasset://fonts/families/GothamSSm.json", Enum.FontWeight.Bold),
105
- Body = Font.new("rbxasset://fonts/families/GothamSSm.json", Enum.FontWeight.Medium),
106
- Small = Font.new("rbxasset://fonts/families/GothamSSm.json", Enum.FontWeight.Regular),
107
- }
108
-
109
- --------------------------------------------------------------------------------
110
- -- Toggle Switch component
111
- --------------------------------------------------------------------------------
112
-
113
- local function ToggleSwitch(
114
- scope: Fusion.Scope,
115
- state: Fusion.Value<boolean>,
116
- onToggle: (newState: boolean) -> ()
117
- )
118
- return scope:New "TextButton" {
119
- Name = "Toggle",
120
- BackgroundColor3 = scope:Spring(scope:Computed(function(use)
121
- return if use(state) then COLORS.ToggleOn else COLORS.ToggleOff
122
- end), 25),
123
- Size = UDim2.fromOffset(40, 22),
124
- AutoButtonColor = false,
125
- Text = "",
126
-
127
- [OnEvent "Activated"] = function()
128
- local newVal = not peek(state)
129
- state:set(newVal)
130
- onToggle(newVal)
131
- end,
132
-
133
- [Children] = {
134
- scope:New "UICorner" { CornerRadius = UDim.new(1, 0) },
135
-
136
- -- Knob
137
- scope:New "Frame" {
138
- Name = "Knob",
139
- AnchorPoint = Vector2.new(0, 0.5),
140
- Position = scope:Spring(scope:Computed(function(use)
141
- return if use(state)
142
- then UDim2.new(1, -20, 0.5, 0)
143
- else UDim2.new(0, 2, 0.5, 0)
144
- end), 30),
145
- Size = UDim2.fromOffset(18, 18),
146
- BackgroundColor3 = COLORS.Text,
147
- [Children] = { scope:New "UICorner" { CornerRadius = UDim.new(1, 0) } },
148
- },
149
- },
150
- }
151
- end
152
-
153
- --------------------------------------------------------------------------------
154
- -- Slider component
155
- --------------------------------------------------------------------------------
156
-
157
- local function SettingsSlider(
158
- scope: Fusion.Scope,
159
- value: Fusion.Value<number>,
160
- min: number,
161
- max: number,
162
- step: number?,
163
- onChange: (newValue: number) -> ()
164
- )
165
- local effectiveStep = step or 1
166
- local range = max - min
167
-
168
- local percentage = scope:Computed(function(use)
169
- return (use(value) - min) / range
170
- end)
171
-
172
- return scope:New "Frame" {
173
- Name = "SliderContainer",
174
- BackgroundTransparency = 1,
175
- Size = UDim2.new(1, 0, 0, 20),
176
-
177
- [Children] = {
178
- -- Track
179
- scope:New "TextButton" {
180
- Name = "Track",
181
- BackgroundColor3 = COLORS.SliderTrack,
182
- AnchorPoint = Vector2.new(0, 0.5),
183
- Position = UDim2.new(0, 0, 0.5, 0),
184
- Size = UDim2.new(1, -40, 0, 6),
185
- AutoButtonColor = false,
186
- Text = "",
187
-
188
- [OnEvent "InputBegan"] = function(input)
189
- if input.UserInputType == Enum.UserInputType.MouseButton1
190
- or input.UserInputType == Enum.UserInputType.Touch then
191
- -- Basic click-to-set (full drag requires InputChanged listener)
192
- local track = input.Position
193
- -- Note: for full drag support, connect UserInputService.InputChanged
194
- -- and track isDragging state. Simplified here for reference clarity.
195
- end
196
- end,
197
-
198
- [Children] = {
199
- scope:New "UICorner" { CornerRadius = UDim.new(1, 0) },
200
-
201
- -- Fill
202
- scope:New "Frame" {
203
- Name = "Fill",
204
- BackgroundColor3 = COLORS.SliderFill,
205
- Size = scope:Spring(scope:Computed(function(use)
206
- return UDim2.new(use(percentage), 0, 1, 0)
207
- end), 30),
208
- [Children] = { scope:New "UICorner" { CornerRadius = UDim.new(1, 0) } },
209
- },
210
-
211
- -- Handle
212
- scope:New "Frame" {
213
- Name = "Handle",
214
- AnchorPoint = Vector2.new(0.5, 0.5),
215
- Position = scope:Spring(scope:Computed(function(use)
216
- return UDim2.new(use(percentage), 0, 0.5, 0)
217
- end), 30),
218
- Size = UDim2.fromOffset(14, 14),
219
- BackgroundColor3 = COLORS.Text,
220
- ZIndex = 2,
221
- [Children] = { scope:New "UICorner" { CornerRadius = UDim.new(1, 0) } },
222
- },
223
- },
224
- },
225
-
226
- -- Value label
227
- scope:New "TextLabel" {
228
- Name = "ValueLabel",
229
- BackgroundTransparency = 1,
230
- AnchorPoint = Vector2.new(1, 0.5),
231
- Position = UDim2.new(1, 0, 0.5, 0),
232
- Size = UDim2.fromOffset(36, 16),
233
- FontFace = FONT.Small,
234
- Text = scope:Computed(function(use)
235
- local v = use(value)
236
- return if v == math.floor(v) then tostring(math.floor(v)) else string.format("%.1f", v)
237
- end),
238
- TextColor3 = COLORS.TextSecondary,
239
- TextSize = 12,
240
- TextXAlignment = Enum.TextXAlignment.Right,
241
- },
242
- },
243
- }
244
- end
245
-
246
- --------------------------------------------------------------------------------
247
- -- Dropdown component
248
- --------------------------------------------------------------------------------
249
-
250
- local function SettingsDropdown(
251
- scope: Fusion.Scope,
252
- selected: Fusion.Value<string>,
253
- options: { string },
254
- onChange: (newValue: string) -> ()
255
- )
256
- local isOpen = scope:Value(false)
257
-
258
- return scope:New "Frame" {
259
- Name = "DropdownContainer",
260
- BackgroundTransparency = 1,
261
- Size = UDim2.new(0, 140, 0, 26),
262
- ClipsDescendants = false,
263
-
264
- [Children] = {
265
- -- Trigger button
266
- scope:New "TextButton" {
267
- Name = "Trigger",
268
- BackgroundColor3 = COLORS.SurfaceAlt,
269
- Size = UDim2.new(1, 0, 0, 26),
270
- AutoButtonColor = false,
271
- Text = "",
272
- [OnEvent "Activated"] = function()
273
- isOpen:set(not peek(isOpen))
274
- end,
275
- [Children] = {
276
- scope:New "UICorner" { CornerRadius = UDim.new(0, 4) },
277
- scope:New "UIPadding" {
278
- PaddingLeft = UDim.new(0, 8),
279
- PaddingRight = UDim.new(0, 8),
280
- },
281
- scope:New "TextLabel" {
282
- BackgroundTransparency = 1,
283
- Size = UDim2.new(1, -16, 1, 0),
284
- FontFace = FONT.Small,
285
- Text = selected,
286
- TextColor3 = COLORS.Text,
287
- TextSize = 12,
288
- TextXAlignment = Enum.TextXAlignment.Left,
289
- },
290
- scope:New "TextLabel" {
291
- BackgroundTransparency = 1,
292
- AnchorPoint = Vector2.new(1, 0.5),
293
- Position = UDim2.new(1, 0, 0.5, 0),
294
- Size = UDim2.fromOffset(12, 12),
295
- FontFace = FONT.Small,
296
- Text = "\u{25BC}",
297
- TextColor3 = COLORS.TextMuted,
298
- TextSize = 8,
299
- },
300
- },
301
- },
302
-
303
- -- Options list
304
- scope:New "Frame" {
305
- Name = "OptionsList",
306
- BackgroundColor3 = COLORS.SurfaceAlt,
307
- Position = UDim2.new(0, 0, 1, 4),
308
- Size = UDim2.new(1, 0, 0, #options * 24 + 4),
309
- Visible = isOpen,
310
- ZIndex = 50,
311
- ClipsDescendants = true,
312
- [Children] = {
313
- scope:New "UICorner" { CornerRadius = UDim.new(0, 4) },
314
- scope:New "UIPadding" { PaddingTop = UDim.new(0, 2), PaddingBottom = UDim.new(0, 2) },
315
- scope:New "UIListLayout" {
316
- FillDirection = Enum.FillDirection.Vertical,
317
- Padding = UDim.new(0, 0),
318
- SortOrder = Enum.SortOrder.LayoutOrder,
319
- },
320
-
321
- scope:ForPairs(options, function(use, optScope, i, option)
322
- return i, optScope:New "TextButton" {
323
- Name = "Option_" .. option,
324
- BackgroundColor3 = COLORS.SurfaceAlt,
325
- BackgroundTransparency = 0,
326
- Size = UDim2.new(1, 0, 0, 24),
327
- AutoButtonColor = false,
328
- FontFace = FONT.Small,
329
- Text = option,
330
- TextColor3 = optScope:Computed(function(use)
331
- return if use(selected) == option then COLORS.Primary else COLORS.Text
332
- end),
333
- TextSize = 12,
334
- LayoutOrder = i,
335
- [OnEvent "Activated"] = function()
336
- selected:set(option)
337
- onChange(option)
338
- isOpen:set(false)
339
- end,
340
- }
341
- end),
342
- },
343
- },
344
- },
345
- }
346
- end
347
-
348
- --------------------------------------------------------------------------------
349
- -- Setting Row (renders one setting entry)
350
- --------------------------------------------------------------------------------
351
-
352
- local function SettingRow(
353
- scope: Fusion.Scope,
354
- entry: SettingEntry,
355
- values: { [string]: Fusion.Value<SettingValue> },
356
- onChanged: (string, SettingValue) -> ()
357
- )
358
- local valueState = values[entry.Key]
359
-
360
- local control
361
- if entry.Type == "slider" then
362
- control = SettingsSlider(scope, valueState :: Fusion.Value<number>, entry.Min, entry.Max, entry.Step, function(v)
363
- onChanged(entry.Key, v)
364
- end)
365
- elseif entry.Type == "toggle" then
366
- control = ToggleSwitch(scope, valueState :: Fusion.Value<boolean>, function(v)
367
- onChanged(entry.Key, v)
368
- end)
369
- elseif entry.Type == "dropdown" then
370
- control = SettingsDropdown(scope, valueState :: Fusion.Value<string>, entry.Options, function(v)
371
- onChanged(entry.Key, v)
372
- end)
373
- end
374
-
375
- return scope:New "Frame" {
376
- Name = "Row_" .. entry.Key,
377
- BackgroundTransparency = 1,
378
- Size = UDim2.new(1, 0, 0, 32),
379
-
380
- [Children] = {
381
- scope:New "TextLabel" {
382
- BackgroundTransparency = 1,
383
- Size = UDim2.new(0.5, -8, 1, 0),
384
- FontFace = FONT.Body,
385
- Text = entry.Label,
386
- TextColor3 = COLORS.Text,
387
- TextSize = 13,
388
- TextXAlignment = Enum.TextXAlignment.Left,
389
- },
390
-
391
- scope:New "Frame" {
392
- BackgroundTransparency = 1,
393
- AnchorPoint = Vector2.new(1, 0.5),
394
- Position = UDim2.new(1, 0, 0.5, 0),
395
- Size = UDim2.new(0.5, -8, 0, 26),
396
- [Children] = {
397
- scope:New "UIListLayout" {
398
- FillDirection = Enum.FillDirection.Horizontal,
399
- HorizontalAlignment = Enum.HorizontalAlignment.Right,
400
- VerticalAlignment = Enum.VerticalAlignment.Center,
401
- },
402
- control,
403
- },
404
- },
405
- },
406
- }
407
- end
408
-
409
- --------------------------------------------------------------------------------
410
- -- Settings Screen (main export)
411
- --------------------------------------------------------------------------------
412
-
413
- local function SettingsScreen(props: SettingsProps)
414
- local scope = scoped(Fusion)
415
-
416
- local activeTab = scope:Value(1)
417
-
418
- local screenGui = scope:New "ScreenGui" {
419
- Name = "SettingsScreen",
420
- ResetOnSpawn = false,
421
- IgnoreGuiInset = true,
422
- Enabled = props.Visible,
423
- DisplayOrder = 20,
424
- Parent = Players.LocalPlayer:WaitForChild("PlayerGui"),
425
-
426
- [Children] = {
427
- -- Backdrop
428
- scope:New "TextButton" {
429
- Name = "Backdrop",
430
- BackgroundColor3 = Color3.fromRGB(0, 0, 0),
431
- BackgroundTransparency = scope:Spring(scope:Computed(function(use)
432
- return if use(props.Visible) then 0.5 else 1
433
- end), 20),
434
- Size = UDim2.new(1, 0, 1, 0),
435
- AutoButtonColor = false,
436
- Text = "",
437
- [OnEvent "Activated"] = props.OnClose,
438
- },
439
-
440
- -- Settings panel (centered card)
441
- scope:New "Frame" {
442
- Name = "SettingsPanel",
443
- AnchorPoint = Vector2.new(0.5, 0.5),
444
- Position = UDim2.fromScale(0.5, 0.5),
445
- Size = UDim2.new(0.85, 0, 0.7, 0),
446
- BackgroundColor3 = COLORS.Background,
447
-
448
- [Children] = {
449
- scope:New "UICorner" { CornerRadius = UDim.new(0, 12) },
450
- scope:New "UISizeConstraint" {
451
- MinSize = Vector2.new(300, 250),
452
- MaxSize = Vector2.new(500, 450),
453
- },
454
- scope:New "UIPadding" {
455
- PaddingTop = UDim.new(0, 16),
456
- PaddingBottom = UDim.new(0, 16),
457
- PaddingLeft = UDim.new(0, 16),
458
- PaddingRight = UDim.new(0, 16),
459
- },
460
- scope:New "UIListLayout" {
461
- FillDirection = Enum.FillDirection.Vertical,
462
- Padding = UDim.new(0, 12),
463
- SortOrder = Enum.SortOrder.LayoutOrder,
464
- },
465
-
466
- -- Header
467
- scope:New "Frame" {
468
- BackgroundTransparency = 1,
469
- Size = UDim2.new(1, 0, 0, 24),
470
- LayoutOrder = 1,
471
- [Children] = {
472
- scope:New "TextLabel" {
473
- BackgroundTransparency = 1,
474
- Size = UDim2.new(1, -30, 1, 0),
475
- FontFace = FONT.Title,
476
- Text = "Settings",
477
- TextColor3 = COLORS.Text,
478
- TextSize = 18,
479
- TextXAlignment = Enum.TextXAlignment.Left,
480
- },
481
- scope:New "TextButton" {
482
- AnchorPoint = Vector2.new(1, 0),
483
- Position = UDim2.fromScale(1, 0),
484
- Size = UDim2.fromOffset(24, 24),
485
- BackgroundTransparency = 1,
486
- FontFace = FONT.Title,
487
- Text = "\u{2715}",
488
- TextColor3 = COLORS.TextSecondary,
489
- TextSize = 16,
490
- AutoButtonColor = false,
491
- [OnEvent "Activated"] = props.OnClose,
492
- },
493
- },
494
- },
495
-
496
- -- Tab bar
497
- scope:New "Frame" {
498
- Name = "TabBar",
499
- BackgroundTransparency = 1,
500
- Size = UDim2.new(1, 0, 0, 28),
501
- LayoutOrder = 2,
502
- [Children] = {
503
- scope:New "UIListLayout" {
504
- FillDirection = Enum.FillDirection.Horizontal,
505
- Padding = UDim.new(0, 4),
506
- VerticalAlignment = Enum.VerticalAlignment.Center,
507
- },
508
-
509
- scope:ForPairs(props.Tabs, function(use, tabScope, i, tab)
510
- local isActive = tabScope:Computed(function(use)
511
- return use(activeTab) == i
512
- end)
513
-
514
- return i, tabScope:New "TextButton" {
515
- Name = "Tab_" .. tab.Name,
516
- BackgroundColor3 = tabScope:Spring(tabScope:Computed(function(use)
517
- return if use(isActive) then COLORS.TabActive else COLORS.TabInactive
518
- end), 25),
519
- Size = UDim2.new(0, 70, 0, 26),
520
- AutoButtonColor = false,
521
- FontFace = FONT.Body,
522
- Text = tab.Name,
523
- TextColor3 = tabScope:Computed(function(use)
524
- return if use(isActive) then COLORS.Text else COLORS.TextSecondary
525
- end),
526
- TextSize = 11,
527
- LayoutOrder = i,
528
- [OnEvent "Activated"] = function()
529
- activeTab:set(i)
530
- end,
531
- [Children] = { tabScope:New "UICorner" { CornerRadius = UDim.new(0, 4) } },
532
- }
533
- end),
534
- },
535
- },
536
-
537
- -- Settings content (scrollable)
538
- scope:New "ScrollingFrame" {
539
- Name = "SettingsContent",
540
- BackgroundTransparency = 1,
541
- Size = UDim2.new(1, 0, 1, -76),
542
- LayoutOrder = 3,
543
- AutomaticCanvasSize = Enum.AutomaticSize.Y,
544
- ScrollingDirection = Enum.ScrollingDirection.Y,
545
- ScrollBarThickness = 3,
546
- ScrollBarImageColor3 = COLORS.TextMuted,
547
- CanvasSize = UDim2.new(0, 0, 0, 0),
548
-
549
- [Children] = {
550
- scope:New "UIListLayout" {
551
- FillDirection = Enum.FillDirection.Vertical,
552
- Padding = UDim.new(0, 8),
553
- SortOrder = Enum.SortOrder.LayoutOrder,
554
- },
555
- scope:New "UIPadding" { PaddingTop = UDim.new(0, 4), PaddingBottom = UDim.new(0, 4) },
556
-
557
- -- Render settings for active tab
558
- scope:Computed(function(use)
559
- local tabIndex = use(activeTab)
560
- local tab = props.Tabs[tabIndex]
561
- if not tab then return {} end
562
-
563
- local rows = {}
564
- for i, entry in tab.Settings do
565
- rows[i] = SettingRow(scope, entry, props.Values, props.OnSettingChanged)
566
- end
567
- return rows
568
- end),
569
- },
570
- },
571
- },
572
- },
573
- },
574
- }
575
-
576
- return screenGui, scope
577
- end
578
-
579
- return SettingsScreen
1
+ --[[
2
+ Settings Menu — Fusion 0.3
3
+ Source patterns: VirtualButFake/fusion-components (MIT), dphfox/Fusion docs
4
+ Ported to Fusion 0.3 scoped syntax.
5
+
6
+ A complete settings menu with:
7
+ - Tabbed sections (Audio, Graphics, Controls)
8
+ - Sliders (volume, render distance)
9
+ - Toggle switches (fullscreen, vsync, show FPS)
10
+ - Dropdown select (quality preset)
11
+ - Spring open/close animation
12
+ - Saves to a settings table (caller persists however they want)
13
+
14
+ ADAPT THIS:
15
+ - Replace settings entries with your game's actual settings
16
+ - Wire OnSettingChanged to your DataStore or local storage
17
+ - Add/remove tabs and controls as needed
18
+ - Adjust colors to match your game's palette
19
+
20
+ Requires: Fusion 0.3 (wally: elttob/fusion@0.3.0)
21
+ ]]
22
+
23
+ local ReplicatedStorage = game:GetService("ReplicatedStorage")
24
+ local Players = game:GetService("Players")
25
+
26
+ -- Adjust this path to match your project structure:
27
+ -- Wally: require(ReplicatedStorage.Packages.Fusion)
28
+ -- Custom: require(game.ReplicatedStorage.Fusion)
29
+ -- Vendored: (default, from roblox-opencode setup)
30
+ local Fusion = require(ReplicatedStorage[".opencode"].vendor.fusion)
31
+ local scoped = Fusion.scoped
32
+ local peek = Fusion.peek
33
+ local Children = Fusion.Children
34
+ local OnEvent = Fusion.OnEvent
35
+
36
+ --------------------------------------------------------------------------------
37
+ -- Types
38
+ --------------------------------------------------------------------------------
39
+
40
+ type SettingValue = number | boolean | string
41
+
42
+ type SliderSetting = {
43
+ Type: "slider",
44
+ Label: string,
45
+ Key: string,
46
+ Min: number,
47
+ Max: number,
48
+ Step: number?,
49
+ Default: number,
50
+ }
51
+
52
+ type ToggleSetting = {
53
+ Type: "toggle",
54
+ Label: string,
55
+ Key: string,
56
+ Default: boolean,
57
+ }
58
+
59
+ type DropdownSetting = {
60
+ Type: "dropdown",
61
+ Label: string,
62
+ Key: string,
63
+ Options: { string },
64
+ Default: string,
65
+ }
66
+
67
+ type SettingEntry = SliderSetting | ToggleSetting | DropdownSetting
68
+
69
+ type SettingsTab = {
70
+ Name: string,
71
+ Icon: string?,
72
+ Settings: { SettingEntry },
73
+ }
74
+
75
+ type SettingsProps = {
76
+ Visible: Fusion.Value<boolean>,
77
+ Tabs: { SettingsTab },
78
+ Values: { [string]: Fusion.Value<SettingValue> },
79
+ OnSettingChanged: (key: string, value: SettingValue) -> (),
80
+ OnClose: () -> (),
81
+ }
82
+
83
+ --------------------------------------------------------------------------------
84
+ -- Theme
85
+ --------------------------------------------------------------------------------
86
+
87
+ local COLORS = {
88
+ Background = Color3.fromRGB(18, 18, 26),
89
+ Surface = Color3.fromRGB(28, 28, 40),
90
+ SurfaceAlt = Color3.fromRGB(35, 35, 48),
91
+ Primary = Color3.fromRGB(80, 140, 255),
92
+ Text = Color3.fromRGB(235, 235, 240),
93
+ TextSecondary = Color3.fromRGB(155, 155, 170),
94
+ TextMuted = Color3.fromRGB(90, 90, 105),
95
+ ToggleOn = Color3.fromRGB(80, 200, 120),
96
+ ToggleOff = Color3.fromRGB(60, 60, 75),
97
+ SliderTrack = Color3.fromRGB(45, 45, 60),
98
+ SliderFill = Color3.fromRGB(80, 140, 255),
99
+ TabActive = Color3.fromRGB(80, 140, 255),
100
+ TabInactive = Color3.fromRGB(60, 60, 75),
101
+ }
102
+
103
+ local FONT = {
104
+ Title = Font.new("rbxasset://fonts/families/GothamSSm.json", Enum.FontWeight.Bold),
105
+ Body = Font.new("rbxasset://fonts/families/GothamSSm.json", Enum.FontWeight.Medium),
106
+ Small = Font.new("rbxasset://fonts/families/GothamSSm.json", Enum.FontWeight.Regular),
107
+ }
108
+
109
+ --------------------------------------------------------------------------------
110
+ -- Toggle Switch component
111
+ --------------------------------------------------------------------------------
112
+
113
+ local function ToggleSwitch(
114
+ scope: Fusion.Scope,
115
+ state: Fusion.Value<boolean>,
116
+ onToggle: (newState: boolean) -> ()
117
+ )
118
+ return scope:New "TextButton" {
119
+ Name = "Toggle",
120
+ BackgroundColor3 = scope:Spring(scope:Computed(function(use)
121
+ return if use(state) then COLORS.ToggleOn else COLORS.ToggleOff
122
+ end), 25),
123
+ Size = UDim2.fromOffset(40, 22),
124
+ AutoButtonColor = false,
125
+ Text = "",
126
+
127
+ [OnEvent "Activated"] = function()
128
+ local newVal = not peek(state)
129
+ state:set(newVal)
130
+ onToggle(newVal)
131
+ end,
132
+
133
+ [Children] = {
134
+ scope:New "UICorner" { CornerRadius = UDim.new(1, 0) },
135
+
136
+ -- Knob
137
+ scope:New "Frame" {
138
+ Name = "Knob",
139
+ AnchorPoint = Vector2.new(0, 0.5),
140
+ Position = scope:Spring(scope:Computed(function(use)
141
+ return if use(state)
142
+ then UDim2.new(1, -20, 0.5, 0)
143
+ else UDim2.new(0, 2, 0.5, 0)
144
+ end), 30),
145
+ Size = UDim2.fromOffset(18, 18),
146
+ BackgroundColor3 = COLORS.Text,
147
+ [Children] = { scope:New "UICorner" { CornerRadius = UDim.new(1, 0) } },
148
+ },
149
+ },
150
+ }
151
+ end
152
+
153
+ --------------------------------------------------------------------------------
154
+ -- Slider component
155
+ --------------------------------------------------------------------------------
156
+
157
+ local function SettingsSlider(
158
+ scope: Fusion.Scope,
159
+ value: Fusion.Value<number>,
160
+ min: number,
161
+ max: number,
162
+ step: number?,
163
+ onChange: (newValue: number) -> ()
164
+ )
165
+ local effectiveStep = step or 1
166
+ local range = max - min
167
+
168
+ local percentage = scope:Computed(function(use)
169
+ return (use(value) - min) / range
170
+ end)
171
+
172
+ return scope:New "Frame" {
173
+ Name = "SliderContainer",
174
+ BackgroundTransparency = 1,
175
+ Size = UDim2.new(1, 0, 0, 20),
176
+
177
+ [Children] = {
178
+ -- Track
179
+ scope:New "TextButton" {
180
+ Name = "Track",
181
+ BackgroundColor3 = COLORS.SliderTrack,
182
+ AnchorPoint = Vector2.new(0, 0.5),
183
+ Position = UDim2.new(0, 0, 0.5, 0),
184
+ Size = UDim2.new(1, -40, 0, 6),
185
+ AutoButtonColor = false,
186
+ Text = "",
187
+
188
+ [OnEvent "InputBegan"] = function(input)
189
+ if input.UserInputType == Enum.UserInputType.MouseButton1
190
+ or input.UserInputType == Enum.UserInputType.Touch then
191
+ -- Basic click-to-set (full drag requires InputChanged listener)
192
+ local track = input.Position
193
+ -- Note: for full drag support, connect UserInputService.InputChanged
194
+ -- and track isDragging state. Simplified here for reference clarity.
195
+ end
196
+ end,
197
+
198
+ [Children] = {
199
+ scope:New "UICorner" { CornerRadius = UDim.new(1, 0) },
200
+
201
+ -- Fill
202
+ scope:New "Frame" {
203
+ Name = "Fill",
204
+ BackgroundColor3 = COLORS.SliderFill,
205
+ Size = scope:Spring(scope:Computed(function(use)
206
+ return UDim2.new(use(percentage), 0, 1, 0)
207
+ end), 30),
208
+ [Children] = { scope:New "UICorner" { CornerRadius = UDim.new(1, 0) } },
209
+ },
210
+
211
+ -- Handle
212
+ scope:New "Frame" {
213
+ Name = "Handle",
214
+ AnchorPoint = Vector2.new(0.5, 0.5),
215
+ Position = scope:Spring(scope:Computed(function(use)
216
+ return UDim2.new(use(percentage), 0, 0.5, 0)
217
+ end), 30),
218
+ Size = UDim2.fromOffset(14, 14),
219
+ BackgroundColor3 = COLORS.Text,
220
+ ZIndex = 2,
221
+ [Children] = { scope:New "UICorner" { CornerRadius = UDim.new(1, 0) } },
222
+ },
223
+ },
224
+ },
225
+
226
+ -- Value label
227
+ scope:New "TextLabel" {
228
+ Name = "ValueLabel",
229
+ BackgroundTransparency = 1,
230
+ AnchorPoint = Vector2.new(1, 0.5),
231
+ Position = UDim2.new(1, 0, 0.5, 0),
232
+ Size = UDim2.fromOffset(36, 16),
233
+ FontFace = FONT.Small,
234
+ Text = scope:Computed(function(use)
235
+ local v = use(value)
236
+ return if v == math.floor(v) then tostring(math.floor(v)) else string.format("%.1f", v)
237
+ end),
238
+ TextColor3 = COLORS.TextSecondary,
239
+ TextSize = 12,
240
+ TextXAlignment = Enum.TextXAlignment.Right,
241
+ },
242
+ },
243
+ }
244
+ end
245
+
246
+ --------------------------------------------------------------------------------
247
+ -- Dropdown component
248
+ --------------------------------------------------------------------------------
249
+
250
+ local function SettingsDropdown(
251
+ scope: Fusion.Scope,
252
+ selected: Fusion.Value<string>,
253
+ options: { string },
254
+ onChange: (newValue: string) -> ()
255
+ )
256
+ local isOpen = scope:Value(false)
257
+
258
+ return scope:New "Frame" {
259
+ Name = "DropdownContainer",
260
+ BackgroundTransparency = 1,
261
+ Size = UDim2.new(0, 140, 0, 26),
262
+ ClipsDescendants = false,
263
+
264
+ [Children] = {
265
+ -- Trigger button
266
+ scope:New "TextButton" {
267
+ Name = "Trigger",
268
+ BackgroundColor3 = COLORS.SurfaceAlt,
269
+ Size = UDim2.new(1, 0, 0, 26),
270
+ AutoButtonColor = false,
271
+ Text = "",
272
+ [OnEvent "Activated"] = function()
273
+ isOpen:set(not peek(isOpen))
274
+ end,
275
+ [Children] = {
276
+ scope:New "UICorner" { CornerRadius = UDim.new(0, 4) },
277
+ scope:New "UIPadding" {
278
+ PaddingLeft = UDim.new(0, 8),
279
+ PaddingRight = UDim.new(0, 8),
280
+ },
281
+ scope:New "TextLabel" {
282
+ BackgroundTransparency = 1,
283
+ Size = UDim2.new(1, -16, 1, 0),
284
+ FontFace = FONT.Small,
285
+ Text = selected,
286
+ TextColor3 = COLORS.Text,
287
+ TextSize = 12,
288
+ TextXAlignment = Enum.TextXAlignment.Left,
289
+ },
290
+ scope:New "TextLabel" {
291
+ BackgroundTransparency = 1,
292
+ AnchorPoint = Vector2.new(1, 0.5),
293
+ Position = UDim2.new(1, 0, 0.5, 0),
294
+ Size = UDim2.fromOffset(12, 12),
295
+ FontFace = FONT.Small,
296
+ Text = "\u{25BC}",
297
+ TextColor3 = COLORS.TextMuted,
298
+ TextSize = 8,
299
+ },
300
+ },
301
+ },
302
+
303
+ -- Options list
304
+ scope:New "Frame" {
305
+ Name = "OptionsList",
306
+ BackgroundColor3 = COLORS.SurfaceAlt,
307
+ Position = UDim2.new(0, 0, 1, 4),
308
+ Size = UDim2.new(1, 0, 0, #options * 24 + 4),
309
+ Visible = isOpen,
310
+ ZIndex = 50,
311
+ ClipsDescendants = true,
312
+ [Children] = {
313
+ scope:New "UICorner" { CornerRadius = UDim.new(0, 4) },
314
+ scope:New "UIPadding" { PaddingTop = UDim.new(0, 2), PaddingBottom = UDim.new(0, 2) },
315
+ scope:New "UIListLayout" {
316
+ FillDirection = Enum.FillDirection.Vertical,
317
+ Padding = UDim.new(0, 0),
318
+ SortOrder = Enum.SortOrder.LayoutOrder,
319
+ },
320
+
321
+ scope:ForPairs(options, function(use, optScope, i, option)
322
+ return i, optScope:New "TextButton" {
323
+ Name = "Option_" .. option,
324
+ BackgroundColor3 = COLORS.SurfaceAlt,
325
+ BackgroundTransparency = 0,
326
+ Size = UDim2.new(1, 0, 0, 24),
327
+ AutoButtonColor = false,
328
+ FontFace = FONT.Small,
329
+ Text = option,
330
+ TextColor3 = optScope:Computed(function(use)
331
+ return if use(selected) == option then COLORS.Primary else COLORS.Text
332
+ end),
333
+ TextSize = 12,
334
+ LayoutOrder = i,
335
+ [OnEvent "Activated"] = function()
336
+ selected:set(option)
337
+ onChange(option)
338
+ isOpen:set(false)
339
+ end,
340
+ }
341
+ end),
342
+ },
343
+ },
344
+ },
345
+ }
346
+ end
347
+
348
+ --------------------------------------------------------------------------------
349
+ -- Setting Row (renders one setting entry)
350
+ --------------------------------------------------------------------------------
351
+
352
+ local function SettingRow(
353
+ scope: Fusion.Scope,
354
+ entry: SettingEntry,
355
+ values: { [string]: Fusion.Value<SettingValue> },
356
+ onChanged: (string, SettingValue) -> ()
357
+ )
358
+ local valueState = values[entry.Key]
359
+
360
+ local control
361
+ if entry.Type == "slider" then
362
+ control = SettingsSlider(scope, valueState :: Fusion.Value<number>, entry.Min, entry.Max, entry.Step, function(v)
363
+ onChanged(entry.Key, v)
364
+ end)
365
+ elseif entry.Type == "toggle" then
366
+ control = ToggleSwitch(scope, valueState :: Fusion.Value<boolean>, function(v)
367
+ onChanged(entry.Key, v)
368
+ end)
369
+ elseif entry.Type == "dropdown" then
370
+ control = SettingsDropdown(scope, valueState :: Fusion.Value<string>, entry.Options, function(v)
371
+ onChanged(entry.Key, v)
372
+ end)
373
+ end
374
+
375
+ return scope:New "Frame" {
376
+ Name = "Row_" .. entry.Key,
377
+ BackgroundTransparency = 1,
378
+ Size = UDim2.new(1, 0, 0, 32),
379
+
380
+ [Children] = {
381
+ scope:New "TextLabel" {
382
+ BackgroundTransparency = 1,
383
+ Size = UDim2.new(0.5, -8, 1, 0),
384
+ FontFace = FONT.Body,
385
+ Text = entry.Label,
386
+ TextColor3 = COLORS.Text,
387
+ TextSize = 13,
388
+ TextXAlignment = Enum.TextXAlignment.Left,
389
+ },
390
+
391
+ scope:New "Frame" {
392
+ BackgroundTransparency = 1,
393
+ AnchorPoint = Vector2.new(1, 0.5),
394
+ Position = UDim2.new(1, 0, 0.5, 0),
395
+ Size = UDim2.new(0.5, -8, 0, 26),
396
+ [Children] = {
397
+ scope:New "UIListLayout" {
398
+ FillDirection = Enum.FillDirection.Horizontal,
399
+ HorizontalAlignment = Enum.HorizontalAlignment.Right,
400
+ VerticalAlignment = Enum.VerticalAlignment.Center,
401
+ },
402
+ control,
403
+ },
404
+ },
405
+ },
406
+ }
407
+ end
408
+
409
+ --------------------------------------------------------------------------------
410
+ -- Settings Screen (main export)
411
+ --------------------------------------------------------------------------------
412
+
413
+ local function SettingsScreen(props: SettingsProps)
414
+ local scope = scoped(Fusion)
415
+
416
+ local activeTab = scope:Value(1)
417
+
418
+ local screenGui = scope:New "ScreenGui" {
419
+ Name = "SettingsScreen",
420
+ ResetOnSpawn = false,
421
+ IgnoreGuiInset = true,
422
+ Enabled = props.Visible,
423
+ DisplayOrder = 20,
424
+ Parent = Players.LocalPlayer:WaitForChild("PlayerGui"),
425
+
426
+ [Children] = {
427
+ -- Backdrop
428
+ scope:New "TextButton" {
429
+ Name = "Backdrop",
430
+ BackgroundColor3 = Color3.fromRGB(0, 0, 0),
431
+ BackgroundTransparency = scope:Spring(scope:Computed(function(use)
432
+ return if use(props.Visible) then 0.5 else 1
433
+ end), 20),
434
+ Size = UDim2.new(1, 0, 1, 0),
435
+ AutoButtonColor = false,
436
+ Text = "",
437
+ [OnEvent "Activated"] = props.OnClose,
438
+ },
439
+
440
+ -- Settings panel (centered card)
441
+ scope:New "Frame" {
442
+ Name = "SettingsPanel",
443
+ AnchorPoint = Vector2.new(0.5, 0.5),
444
+ Position = UDim2.fromScale(0.5, 0.5),
445
+ Size = UDim2.new(0.85, 0, 0.7, 0),
446
+ BackgroundColor3 = COLORS.Background,
447
+
448
+ [Children] = {
449
+ scope:New "UICorner" { CornerRadius = UDim.new(0, 12) },
450
+ scope:New "UISizeConstraint" {
451
+ MinSize = Vector2.new(300, 250),
452
+ MaxSize = Vector2.new(500, 450),
453
+ },
454
+ scope:New "UIPadding" {
455
+ PaddingTop = UDim.new(0, 16),
456
+ PaddingBottom = UDim.new(0, 16),
457
+ PaddingLeft = UDim.new(0, 16),
458
+ PaddingRight = UDim.new(0, 16),
459
+ },
460
+ scope:New "UIListLayout" {
461
+ FillDirection = Enum.FillDirection.Vertical,
462
+ Padding = UDim.new(0, 12),
463
+ SortOrder = Enum.SortOrder.LayoutOrder,
464
+ },
465
+
466
+ -- Header
467
+ scope:New "Frame" {
468
+ BackgroundTransparency = 1,
469
+ Size = UDim2.new(1, 0, 0, 24),
470
+ LayoutOrder = 1,
471
+ [Children] = {
472
+ scope:New "TextLabel" {
473
+ BackgroundTransparency = 1,
474
+ Size = UDim2.new(1, -30, 1, 0),
475
+ FontFace = FONT.Title,
476
+ Text = "Settings",
477
+ TextColor3 = COLORS.Text,
478
+ TextSize = 18,
479
+ TextXAlignment = Enum.TextXAlignment.Left,
480
+ },
481
+ scope:New "TextButton" {
482
+ AnchorPoint = Vector2.new(1, 0),
483
+ Position = UDim2.fromScale(1, 0),
484
+ Size = UDim2.fromOffset(24, 24),
485
+ BackgroundTransparency = 1,
486
+ FontFace = FONT.Title,
487
+ Text = "\u{2715}",
488
+ TextColor3 = COLORS.TextSecondary,
489
+ TextSize = 16,
490
+ AutoButtonColor = false,
491
+ [OnEvent "Activated"] = props.OnClose,
492
+ },
493
+ },
494
+ },
495
+
496
+ -- Tab bar
497
+ scope:New "Frame" {
498
+ Name = "TabBar",
499
+ BackgroundTransparency = 1,
500
+ Size = UDim2.new(1, 0, 0, 28),
501
+ LayoutOrder = 2,
502
+ [Children] = {
503
+ scope:New "UIListLayout" {
504
+ FillDirection = Enum.FillDirection.Horizontal,
505
+ Padding = UDim.new(0, 4),
506
+ VerticalAlignment = Enum.VerticalAlignment.Center,
507
+ },
508
+
509
+ scope:ForPairs(props.Tabs, function(use, tabScope, i, tab)
510
+ local isActive = tabScope:Computed(function(use)
511
+ return use(activeTab) == i
512
+ end)
513
+
514
+ return i, tabScope:New "TextButton" {
515
+ Name = "Tab_" .. tab.Name,
516
+ BackgroundColor3 = tabScope:Spring(tabScope:Computed(function(use)
517
+ return if use(isActive) then COLORS.TabActive else COLORS.TabInactive
518
+ end), 25),
519
+ Size = UDim2.new(0, 70, 0, 26),
520
+ AutoButtonColor = false,
521
+ FontFace = FONT.Body,
522
+ Text = tab.Name,
523
+ TextColor3 = tabScope:Computed(function(use)
524
+ return if use(isActive) then COLORS.Text else COLORS.TextSecondary
525
+ end),
526
+ TextSize = 11,
527
+ LayoutOrder = i,
528
+ [OnEvent "Activated"] = function()
529
+ activeTab:set(i)
530
+ end,
531
+ [Children] = { tabScope:New "UICorner" { CornerRadius = UDim.new(0, 4) } },
532
+ }
533
+ end),
534
+ },
535
+ },
536
+
537
+ -- Settings content (scrollable)
538
+ scope:New "ScrollingFrame" {
539
+ Name = "SettingsContent",
540
+ BackgroundTransparency = 1,
541
+ Size = UDim2.new(1, 0, 1, -76),
542
+ LayoutOrder = 3,
543
+ AutomaticCanvasSize = Enum.AutomaticSize.Y,
544
+ ScrollingDirection = Enum.ScrollingDirection.Y,
545
+ ScrollBarThickness = 3,
546
+ ScrollBarImageColor3 = COLORS.TextMuted,
547
+ CanvasSize = UDim2.new(0, 0, 0, 0),
548
+
549
+ [Children] = {
550
+ scope:New "UIListLayout" {
551
+ FillDirection = Enum.FillDirection.Vertical,
552
+ Padding = UDim.new(0, 8),
553
+ SortOrder = Enum.SortOrder.LayoutOrder,
554
+ },
555
+ scope:New "UIPadding" { PaddingTop = UDim.new(0, 4), PaddingBottom = UDim.new(0, 4) },
556
+
557
+ -- Render settings for active tab
558
+ scope:Computed(function(use)
559
+ local tabIndex = use(activeTab)
560
+ local tab = props.Tabs[tabIndex]
561
+ if not tab then return {} end
562
+
563
+ local rows = {}
564
+ for i, entry in tab.Settings do
565
+ rows[i] = SettingRow(scope, entry, props.Values, props.OnSettingChanged)
566
+ end
567
+ return rows
568
+ end),
569
+ },
570
+ },
571
+ },
572
+ },
573
+ },
574
+ }
575
+
576
+ return screenGui, scope
577
+ end
578
+
579
+ return SettingsScreen